Skip to content

Commit cbba60f

Browse files
committed
chore: proof of concept wip
1 parent 136bbb0 commit cbba60f

File tree

17 files changed

+2443
-123
lines changed

17 files changed

+2443
-123
lines changed

internal/tui/msg/msg.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,36 @@ type ChangePage struct {
1111
PageFactory func(s Store) tea.Model
1212
}
1313

14+
// SelectNode is sent when a sidebar node is selected
15+
type SelectNode struct {
16+
NodeType string // "pod", "project", "token", "settings", "section"
17+
ID string // Entity ID (e.g., pod ID, project ID)
18+
}
19+
20+
// OpenModal opens a modal overlay
21+
type OpenModal struct {
22+
Modal tea.Model
23+
}
24+
25+
// CloseModal closes the current modal
26+
type CloseModal struct{}
27+
28+
// CloseForm signals to close the current form panel and return to layout
29+
type CloseForm struct{}
30+
31+
// OpenDomainForm opens the domain form for creating/editing a domain
32+
type OpenDomainForm struct {
33+
Pod *model.Pod
34+
Proj *model.Project
35+
Domain *model.PodDomain // nil for new, non-nil for edit
36+
}
37+
38+
// OpenDomainDelete opens the domain delete confirmation
39+
type OpenDomainDelete struct {
40+
Pod *model.Pod
41+
Proj *model.Project
42+
Domain model.PodDomain
43+
}
1444

1545
// Store interface for page factories
1646
type Store interface {

internal/tui/ui/components/tree.go

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
package components
2+
3+
import (
4+
"strings"
5+
6+
tea "charm.land/bubbletea/v2"
7+
lipgloss "charm.land/lipgloss/v2"
8+
"github.com/deeploy-sh/deeploy/internal/tui/ui/styles"
9+
)
10+
11+
// NodeType identifies the type of tree node
12+
type NodeType int
13+
14+
const (
15+
NodeSection NodeType = iota // Collapsible section header (Projects, Git Tokens, etc.)
16+
NodeProject // Project node (collapsible, contains pods)
17+
NodePod // Pod node (leaf)
18+
NodeItem // Generic leaf item (token, setting, etc.)
19+
)
20+
21+
// TreeNode represents a single node in the tree
22+
type TreeNode struct {
23+
ID string
24+
Label string
25+
Type NodeType
26+
Expanded bool // Only relevant for Section/Project
27+
Children []string // IDs of child nodes (for rebuilding)
28+
Indent int // Indentation level (0, 1, 2, ...)
29+
Parent string // Parent node ID (empty for root)
30+
}
31+
32+
// Tree is a simple tree navigation component
33+
type Tree struct {
34+
nodes []TreeNode // Flat list of visible nodes
35+
cursor int
36+
viewStart int
37+
width int
38+
height int
39+
}
40+
41+
// NewTree creates an empty tree
42+
func NewTree(width, height int) Tree {
43+
return Tree{
44+
width: width,
45+
height: height,
46+
}
47+
}
48+
49+
// SetNodes replaces all nodes (already flattened and filtered for visibility)
50+
func (t *Tree) SetNodes(nodes []TreeNode) {
51+
t.nodes = nodes
52+
// Keep cursor in bounds
53+
if t.cursor >= len(nodes) {
54+
t.cursor = max(0, len(nodes)-1)
55+
}
56+
// Adjust view
57+
if t.viewStart > t.cursor {
58+
t.viewStart = t.cursor
59+
}
60+
}
61+
62+
// SetSize updates the tree dimensions
63+
func (t *Tree) SetSize(width, height int) {
64+
t.width = width
65+
t.height = height
66+
}
67+
68+
// CursorUp moves the cursor up
69+
func (t *Tree) CursorUp() {
70+
if len(t.nodes) == 0 {
71+
return
72+
}
73+
t.cursor--
74+
if t.cursor < 0 {
75+
t.cursor = len(t.nodes) - 1
76+
t.viewStart = max(0, len(t.nodes)-t.height)
77+
} else if t.cursor < t.viewStart {
78+
t.viewStart = t.cursor
79+
}
80+
}
81+
82+
// CursorDown moves the cursor down
83+
func (t *Tree) CursorDown() {
84+
if len(t.nodes) == 0 {
85+
return
86+
}
87+
t.cursor++
88+
if t.cursor >= len(t.nodes) {
89+
t.cursor = 0
90+
t.viewStart = 0
91+
} else if t.cursor >= t.viewStart+t.height {
92+
t.viewStart = t.cursor - t.height + 1
93+
}
94+
}
95+
96+
// Selected returns the currently selected node, or nil
97+
func (t *Tree) Selected() *TreeNode {
98+
if t.cursor >= 0 && t.cursor < len(t.nodes) {
99+
return &t.nodes[t.cursor]
100+
}
101+
return nil
102+
}
103+
104+
// SelectByID selects a node by its ID
105+
func (t *Tree) SelectByID(id string) {
106+
for i, n := range t.nodes {
107+
if n.ID == id {
108+
t.cursor = i
109+
// Adjust view
110+
if t.cursor < t.viewStart {
111+
t.viewStart = t.cursor
112+
} else if t.cursor >= t.viewStart+t.height {
113+
t.viewStart = t.cursor - t.height + 1
114+
}
115+
return
116+
}
117+
}
118+
}
119+
120+
// Init implements tea.Model
121+
func (t Tree) Init() tea.Cmd {
122+
return nil
123+
}
124+
125+
// Update implements tea.Model
126+
func (t Tree) Update(msg tea.Msg) (Tree, tea.Cmd) {
127+
switch msg := msg.(type) {
128+
case tea.KeyPressMsg:
129+
switch msg.String() {
130+
case "up", "k":
131+
t.CursorUp()
132+
case "down", "j":
133+
t.CursorDown()
134+
}
135+
}
136+
return t, nil
137+
}
138+
139+
// View implements tea.Model
140+
func (t Tree) View() string {
141+
if len(t.nodes) == 0 {
142+
return styles.MutedStyle().Render("No items")
143+
}
144+
145+
var b strings.Builder
146+
end := min(t.viewStart+t.height, len(t.nodes))
147+
148+
for i := t.viewStart; i < end; i++ {
149+
node := t.nodes[i]
150+
line := t.renderNode(node, i == t.cursor)
151+
b.WriteString(line)
152+
if i < end-1 {
153+
b.WriteString("\n")
154+
}
155+
}
156+
157+
return b.String()
158+
}
159+
160+
func (t Tree) renderNode(node TreeNode, selected bool) string {
161+
// Build indentation
162+
indent := strings.Repeat(" ", node.Indent)
163+
164+
// Build prefix based on type
165+
var prefix string
166+
switch node.Type {
167+
case NodeSection:
168+
if node.Expanded {
169+
prefix = "▾ "
170+
} else {
171+
prefix = "▸ "
172+
}
173+
case NodeProject:
174+
if node.Expanded {
175+
prefix = "▾ "
176+
} else {
177+
prefix = "▸ "
178+
}
179+
case NodePod, NodeItem:
180+
prefix = " " // Leaf nodes have no expand indicator
181+
}
182+
183+
// Build the line content
184+
content := indent + prefix + node.Label
185+
186+
// Truncate if too long
187+
if len(content) > t.width-2 {
188+
content = content[:t.width-5] + "..."
189+
}
190+
191+
// Pad to width
192+
content = content + strings.Repeat(" ", max(0, t.width-len(content)))
193+
194+
// Apply style
195+
if selected {
196+
return lipgloss.NewStyle().
197+
Background(styles.ColorBackgroundElement()).
198+
Foreground(styles.ColorPrimary()).
199+
Bold(true).
200+
Render(content)
201+
}
202+
203+
// Style based on node type
204+
switch node.Type {
205+
case NodeSection:
206+
return styles.MutedStyle().Bold(true).Render(content)
207+
case NodeProject:
208+
return styles.ForegroundStyle().Render(content)
209+
case NodePod:
210+
return styles.ForegroundStyle().Render(content)
211+
case NodeItem:
212+
return styles.ForegroundStyle().Render(content)
213+
}
214+
215+
return content
216+
}

0 commit comments

Comments
 (0)