Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 35 additions & 19 deletions internal/ui/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ type Model struct {
table table.Model
tableXOffset int

err error
err error
errorViewport viewport.Model

itemCount int
status string
Expand All @@ -58,10 +59,10 @@ type Model struct {
filter *resourceFilter
scope resourceScope

detailMode bool
detail viewport.Model
detailID string
detailContent string
detailMode bool
detailViewport viewport.Model
detailID string
detailContent string

commandMode bool
command textinput.Model
Expand Down Expand Up @@ -106,8 +107,10 @@ func New(registry *resource.Registry, openstackContext *openstack.Context, plugi
command.Cursor.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("#87CEFA"))
command.PlaceholderStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#666666"))

detail := viewport.New(1, 1)
detail.SetHorizontalStep(4)
detailViewport := viewport.New(1, 1)
detailViewport.SetHorizontalStep(4)

errorViewport := viewport.New(1, 1)

s := spinner.New()
s.Spinner = spinner.Points
Expand All @@ -123,7 +126,8 @@ func New(registry *resource.Registry, openstackContext *openstack.Context, plugi
context: openstackContext,
pluginHost: pluginHost,

detail: detail,
detailViewport: detailViewport,
errorViewport: errorViewport,

loading: true,
showLoading: true,
Expand All @@ -148,22 +152,34 @@ func (m Model) Init() tea.Cmd {

func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if m.err != nil {
if keyMsg, ok := msg.(tea.KeyMsg); ok {
switch keyMsg.String() {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "esc", "enter":
m.err = nil
m.setError(nil)

if len(m.navigation) > 0 {
return m, m.navigateBack()
}

return m, nil

case "ctrl+x":
return m, tea.Quit
}

case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
m.Resize()
m.resizeErrorViewport()
return m, nil
}

return m, nil
var cmd tea.Cmd
m.errorViewport, cmd = m.errorViewport.Update(msg)

return m, cmd
}

switch msg := msg.(type) {
Expand All @@ -177,7 +193,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.showLoading = false
m.loadingLabel = ""

m.err = msg.err
m.setError(msg.err)

slog.Error(msg.err.Error(), "cloud", m.context.Cloud)
return m, nil
Expand All @@ -188,7 +204,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.showLoading = false
m.loadingLabel = ""

m.err = msg.Err
m.setError(msg.Err)

slog.Error(msg.Err.Error(), "cloud", m.context.Cloud)
return m, nil
Expand Down Expand Up @@ -246,7 +262,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}

var cmd tea.Cmd
m.detail, cmd = m.detail.Update(msg)
m.detailViewport, cmd = m.detailViewport.Update(msg)

return m, cmd
}
Expand Down Expand Up @@ -552,8 +568,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.detailMode = true
m.detailID = msg.ID
m.detailContent = string(data)
m.detail.SetContent(colorizeJSON(data))
m.detail.GotoTop()
m.detailViewport.SetContent(colorizeJSON(data))
m.detailViewport.GotoTop()
m.Resize()

return m, nil
Expand Down Expand Up @@ -724,8 +740,8 @@ func (m *Model) Resize() {

m.table.SetHeight(contentHeight)

m.detail.Width = max(m.width-2, 1)
m.detail.Height = contentHeight
m.detailViewport.Width = max(m.width-2, 1)
m.detailViewport.Height = contentHeight

if m.resource == nil {
return
Expand Down
12 changes: 12 additions & 0 deletions internal/ui/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,15 @@ func (m *Model) suggestions() []string {

return suggestions
}

func (m *Model) setError(err error) {
m.err = err

if err == nil {
m.errorViewport.SetContent("")
m.errorViewport.GotoTop()
return
}

m.resizeErrorViewport()
}
146 changes: 142 additions & 4 deletions internal/ui/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -319,20 +319,33 @@ func (m Model) renderDetails() string {
"┘",
)

body := m.detail.View()
body := m.detailViewport.View()
bodyLines := strings.Split(body, "\n")

scrollbar := m.renderDetailScrollbar()
var scrollbarLines []string

if scrollbar != "" {
scrollbarLines = strings.Split(scrollbar, "\n")
}

for i, line := range bodyLines {
lineWidth := lipgloss.Width(line)

if lineWidth < innerWidth {
line += strings.Repeat(" ", innerWidth-lineWidth)
}

rightBorder := borderStyle.Render("│")

if i < len(scrollbarLines) {
rightBorder = scrollbarLines[i]
}

bodyLines[i] =
borderStyle.Render("│") +
line +
borderStyle.Render("│")
rightBorder
}

return top +
Expand All @@ -342,6 +355,35 @@ func (m Model) renderDetails() string {
bottom
}

func (m Model) renderDetailScrollbar() string {
height := m.detailViewport.Height

if height <= 0 || m.detailViewport.TotalLineCount() <= height {
return ""
}

totalLines := m.detailViewport.TotalLineCount()
maxOffset := max(totalLines-height, 1)

thumbHeight := max(height*height/totalLines, 1)
thumbHeight = min(thumbHeight, height)

thumbTravel := height - thumbHeight
thumbStart := m.detailViewport.YOffset * thumbTravel / maxOffset

lines := make([]string, height)

for i := range lines {
if i >= thumbStart && i < thumbStart+thumbHeight {
lines[i] = detailScrollbarThumbStyle.Render("█")
} else {
lines[i] = detailScrollbarTrackStyle.Render("│")
}
}

return strings.Join(lines, "\n")
}

func overlayCenter(background, foreground string) string {
bg := strings.Split(background, "\n")
fg := strings.Split(foreground, "\n")
Expand Down Expand Up @@ -391,9 +433,105 @@ func (m Model) renderErrorModal() string {

content := errorTitleStyle.Render("Error") +
"\n\n" +
m.err.Error() +
m.errorViewport.View() +
"\n\n" +
hint

return errorModalStyle.Render(content)
modal := errorModalStyle.Render(content)

scrollbar := m.renderErrorScrollbar()
if scrollbar == "" {
return modal
}

lines := strings.Split(modal, "\n")
scrollbarLines := strings.Split(scrollbar, "\n")

// One border row, one padding row, title row,
// one blank row, then the viewport.
const viewportStartRow = 4

for i, scrollbarLine := range scrollbarLines {
row := viewportStartRow + i

if row >= len(lines)-1 {
break
}

line := lines[row]
width := ansi.StringWidth(line)

// Preserve everything except the final border cell.
lines[row] = ansi.Cut(line, 0, width-1) + scrollbarLine
}

return strings.Join(lines, "\n")
}

func (m Model) renderErrorScrollbar() string {
height := m.errorViewport.Height

if height <= 0 || m.errorViewport.TotalLineCount() <= height {
return ""
}

totalLines := m.errorViewport.TotalLineCount()
maxOffset := max(totalLines-height, 1)

thumbHeight := max(height*height/totalLines, 1)
thumbHeight = min(thumbHeight, height)

thumbTravel := height - thumbHeight
thumbStart := m.errorViewport.YOffset * thumbTravel / maxOffset

lines := make([]string, height)

for i := range lines {
if i >= thumbStart && i < thumbStart+thumbHeight {
lines[i] = errorScrollbarThumbStyle.Render("█")
} else {
lines[i] = errorScrollbarTrackStyle.Render("│")
}
}

return strings.Join(lines, "\n")
}

func (m *Model) resizeErrorViewport() {
const (
modalWidth = 60
horizontalTrim = 4
scrollbarWidth = 2
verticalTrim = 8
maxVisibleLines = 12
)

contentWidth := modalWidth - horizontalTrim - scrollbarWidth
m.errorViewport.Width = contentWidth

if m.err == nil {
m.errorViewport.Height = 1
return
}

wrapped := lipgloss.NewStyle().
Width(contentWidth).
Render(m.err.Error())

m.errorViewport.SetContent(wrapped)

contentHeight := lipgloss.Height(wrapped)
availableHeight := max(m.height-verticalTrim, 1)

m.errorViewport.Height = max(
min(contentHeight, availableHeight, maxVisibleLines),
1,
)

m.errorViewport.SetYOffset(
min(
m.errorViewport.YOffset,
max(contentHeight-m.errorViewport.Height, 0),
),
)
}
17 changes: 16 additions & 1 deletion internal/ui/styles.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,13 @@ var (

jsonNullStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#7F848E"))

detailScrollbarTrackStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#00BFFF"))

detailScrollbarThumbStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#00BFFF")).
Bold(true)
)

//ErrorView
Expand All @@ -99,7 +106,7 @@ var (
Border(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color("#ED1944")).
Foreground(lipgloss.Color("#FFFFFF")).
Padding(1, 2).
Padding(1, 1).
Width(60)

errorTitleStyle = lipgloss.NewStyle().
Expand All @@ -124,3 +131,11 @@ func tableStyles() table.Styles {

return styles
}

var (
errorScrollbarTrackStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#ED1944"))

errorScrollbarThumbStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#ED1944"))
)
Loading