Skip to content

Commit 585ac9d

Browse files
engalarako
authored andcommitted
feat(tui): preview panel — DESCRIBE output viewport
1 parent 1051799 commit 585ac9d

2 files changed

Lines changed: 135 additions & 9 deletions

File tree

tui/model.go

Lines changed: 69 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package tui
22

33
import (
4-
"github.com/charmbracelet/lipgloss"
4+
"fmt"
55

66
tea "github.com/charmbracelet/bubbletea"
77
"github.com/mendixlabs/mxcli/tui/panels"
@@ -25,6 +25,7 @@ type Model struct {
2525
focus Focus
2626
modulesPanel panels.ModulesPanel
2727
elementsPanel panels.ElementsPanel
28+
previewPanel panels.PreviewPanel
2829
}
2930

3031
func New(mxcliPath, projectPath string) Model {
@@ -34,6 +35,7 @@ func New(mxcliPath, projectPath string) Model {
3435
focus: FocusModules,
3536
modulesPanel: panels.NewModulesPanel(30, 20),
3637
elementsPanel: panels.NewElementsPanel(40, 20),
38+
previewPanel: panels.NewPreviewPanel(50, 20),
3739
}
3840
}
3941

@@ -48,6 +50,15 @@ func (m Model) Init() tea.Cmd {
4850
}
4951
}
5052

53+
// describeNode returns a tea.Cmd that runs DESCRIBE for a given node.
54+
func (m Model) describeNode(node *panels.TreeNode) tea.Cmd {
55+
return func() tea.Msg {
56+
out, err := runMxcli(m.mxcliPath, "-p", m.projectPath, "-c",
57+
fmt.Sprintf("DESCRIBE %s %s", node.Type, node.QualifiedName))
58+
return panels.DescribeResultMsg{Content: out, Err: err}
59+
}
60+
}
61+
5162
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
5263
switch msg := msg.(type) {
5364
case tea.KeyMsg:
@@ -59,7 +70,6 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
5970
case FocusModules:
6071
switch msg.String() {
6172
case "l", "right", "enter":
62-
// Open selected module → show children in elements panel
6373
if node := m.modulesPanel.SelectedNode(); node != nil && len(node.Children) > 0 {
6474
m.elementsPanel.SetNodes(node.Children)
6575
m.focus = FocusElements
@@ -77,29 +87,66 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
7787
m.focus = FocusModules
7888
return m, nil
7989
case "l", "right", "enter":
80-
// Drill down into node with children
81-
if node := m.elementsPanel.SelectedNode(); node != nil && len(node.Children) > 0 {
82-
m.elementsPanel.SetNodes(node.Children)
83-
return m, nil
90+
if node := m.elementsPanel.SelectedNode(); node != nil {
91+
if len(node.Children) > 0 {
92+
// Drill down into node with children
93+
m.elementsPanel.SetNodes(node.Children)
94+
return m, nil
95+
}
96+
// Leaf node → show DESCRIBE in preview
97+
if node.QualifiedName != "" {
98+
m.focus = FocusPreview
99+
m.previewPanel.SetLoading()
100+
return m, m.describeNode(node)
101+
}
84102
}
103+
case "j", "down", "k", "up":
104+
// Forward navigation, then auto-preview
105+
var cmd tea.Cmd
106+
m.elementsPanel, cmd = m.elementsPanel.Update(msg)
107+
if node := m.elementsPanel.SelectedNode(); node != nil && node.QualifiedName != "" {
108+
m.previewPanel.SetLoading()
109+
return m, tea.Batch(cmd, m.describeNode(node))
110+
}
111+
return m, cmd
85112
default:
86113
var cmd tea.Cmd
87114
m.elementsPanel, cmd = m.elementsPanel.Update(msg)
88115
return m, cmd
89116
}
117+
118+
case FocusPreview:
119+
switch msg.String() {
120+
case "h", "left", "esc":
121+
m.focus = FocusElements
122+
return m, nil
123+
default:
124+
var cmd tea.Cmd
125+
m.previewPanel, cmd = m.previewPanel.Update(msg)
126+
return m, cmd
127+
}
90128
}
91129

92130
case tea.WindowSizeMsg:
93131
m.width = msg.Width
94132
m.height = msg.Height
133+
mW, eW, pW := columnWidths(m.width)
95134
contentH := m.height - 2
96-
m.modulesPanel.SetSize(m.width/3, contentH)
97-
m.elementsPanel.SetSize(m.width/3, contentH)
135+
m.modulesPanel.SetSize(mW, contentH)
136+
m.elementsPanel.SetSize(eW, contentH)
137+
m.previewPanel.SetSize(pW, contentH)
98138

99139
case panels.LoadTreeMsg:
100140
if msg.Err == nil && msg.Nodes != nil {
101141
m.modulesPanel.SetNodes(msg.Nodes)
102142
}
143+
144+
case panels.DescribeResultMsg:
145+
if msg.Err != nil {
146+
m.previewPanel.SetContent("-- Error:\n" + msg.Content)
147+
} else {
148+
m.previewPanel.SetContent(msg.Content)
149+
}
103150
}
104151
return m, nil
105152
}
@@ -110,9 +157,22 @@ func (m Model) View() string {
110157
}
111158
m.modulesPanel.SetFocused(m.focus == FocusModules)
112159
m.elementsPanel.SetFocused(m.focus == FocusElements)
160+
m.previewPanel.SetFocused(m.focus == FocusPreview)
161+
162+
mW, eW, pW := columnWidths(m.width)
163+
contentH := m.height - 2
164+
m.modulesPanel.SetSize(mW, contentH)
165+
m.elementsPanel.SetSize(eW, contentH)
166+
m.previewPanel.SetSize(pW, contentH)
167+
168+
statusLine := fmt.Sprintf(" mxcli tui %s [Tab: cycle focus | :: command | q: quit]",
169+
m.projectPath)
113170

114-
return lipgloss.JoinHorizontal(lipgloss.Top,
171+
return renderLayout(
172+
m.width,
115173
m.modulesPanel.View(),
116174
m.elementsPanel.View(),
175+
m.previewPanel.View(),
176+
statusLine,
117177
)
118178
}

tui/panels/preview.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package panels
2+
3+
import (
4+
"github.com/charmbracelet/bubbles/viewport"
5+
tea "github.com/charmbracelet/bubbletea"
6+
"github.com/charmbracelet/lipgloss"
7+
)
8+
9+
// PreviewPanel is the right column: DESCRIBE output in a scrollable viewport.
10+
type PreviewPanel struct {
11+
viewport viewport.Model
12+
focused bool
13+
loading bool
14+
width int
15+
height int
16+
}
17+
18+
// DescribeResultMsg carries DESCRIBE output.
19+
type DescribeResultMsg struct {
20+
Content string
21+
Err error
22+
}
23+
24+
func NewPreviewPanel(width, height int) PreviewPanel {
25+
vp := viewport.New(width, height)
26+
vp.SetContent("Select an element to preview its DESCRIBE output.")
27+
return PreviewPanel{viewport: vp, width: width, height: height}
28+
}
29+
30+
func (p *PreviewPanel) SetContent(content string) {
31+
p.loading = false
32+
p.viewport.SetContent(content)
33+
p.viewport.GotoTop()
34+
}
35+
36+
func (p *PreviewPanel) SetLoading() {
37+
p.loading = true
38+
p.viewport.SetContent("Loading...")
39+
}
40+
41+
func (p *PreviewPanel) SetSize(w, h int) {
42+
p.width = w
43+
p.height = h
44+
p.viewport.Width = w
45+
p.viewport.Height = h
46+
}
47+
48+
func (p *PreviewPanel) SetFocused(f bool) { p.focused = f }
49+
50+
func (p PreviewPanel) Update(msg tea.Msg) (PreviewPanel, tea.Cmd) {
51+
var cmd tea.Cmd
52+
p.viewport, cmd = p.viewport.Update(msg)
53+
return p, cmd
54+
}
55+
56+
func (p PreviewPanel) View() string {
57+
title := "Preview"
58+
if p.loading {
59+
title = "Preview (loading...)"
60+
}
61+
header := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("39")).Render(title)
62+
border := lipgloss.NewStyle().
63+
Border(lipgloss.RoundedBorder()).
64+
BorderForeground(borderColor(p.focused))
65+
return border.Render(header + "\n" + p.viewport.View())
66+
}

0 commit comments

Comments
 (0)