detached d4ce71d

fix(ui): improve tree view layout and icon ordering

- Swap icon order: HTTP status first, container icon next to domain name
- Use ├─ / └─ connectors on image subtitle lines
- Use ├─ when node has children below, └─ when it's the last item
- Remove blank lines between top-level items for compact output
commit
d4ce71df827595160dbad9196eae88d6f807a19b
author
brice · · 2026-03-24 06:23 UTC
parent
6ea4218
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +15 -10
M internal/adapters/in/cli/routes.go +2 -2
@@ -340,10 +340,10 @@ func routeTitle(route remote.RouteInfo, health map[string]*remote.RouteHealth) s
340340 }
341341 }
342342
343- containerIcon := components.StatusIcon(styles.IconContainerStatus, components.ParseStatus(containerStatus))
344343 httpIcon := components.StatusIcon(styles.IconHTTPStatus, httpHealthToStatus(health[route.Domain]))
344+ containerIcon := components.StatusIcon(styles.IconContainerStatus, components.ParseStatus(containerStatus))
345345
346- return containerIcon + " " + httpIcon + " " + route.Domain
346+ return httpIcon + " " + containerIcon + " " + route.Domain
347347 }
348348
349349 // addAttachmentChildren adds attachment nodes as children of a route node.
@@ -340,10 +340,10 @@ func routeTitle(route remote.RouteInfo, health map[string]*remote.RouteHealth) s
340 }340 }
341 }341 }
342 342
343- containerIcon := components.StatusIcon(styles.IconContainerStatus, components.ParseStatus(containerStatus))
344 httpIcon := components.StatusIcon(styles.IconHTTPStatus, httpHealthToStatus(health[route.Domain]))343 httpIcon := components.StatusIcon(styles.IconHTTPStatus, httpHealthToStatus(health[route.Domain]))
344+ containerIcon := components.StatusIcon(styles.IconContainerStatus, components.ParseStatus(containerStatus))
345 345
346- return containerIcon + " " + httpIcon + " " + route.Domain346+ return httpIcon + " " + containerIcon + " " + route.Domain
347 }347 }
348 348
349 // addAttachmentChildren adds attachment nodes as children of a route node.349 // addAttachmentChildren adds attachment nodes as children of a route node.
M internal/adapters/in/cli/ui/components/tree.go +13 -8
@@ -70,10 +70,7 @@ func (t *Tree) Render() string {
7070 }
7171
7272 var b strings.Builder
73- for i, item := range t.items {
74- if i > 0 {
75- b.WriteString("\n")
76- }
73+ for _, item := range t.items {
7774 if item.group != nil {
7875 t.renderGroup(&b, item.group)
7976 } else {
@@ -105,10 +102,17 @@ func (t *Tree) renderNode(b *strings.Builder, node *Node, prefix string) {
105102 // Title line
106103 b.WriteString(prefix + node.Title + "\n")
107104
108- // Subtitle line (muted, aligned with title text after icons)
105+ // Subtitle line (muted, with tree connector aligned under container icon)
109106 if node.Subtitle != "" {
107+ var connectorGlyph string
108+ if len(node.Children) > 0 {
109+ connectorGlyph = styles.IconTreeBranch + styles.IconTreeLine // ├─ more items below
110+ } else {
111+ connectorGlyph = styles.IconTreeLast + styles.IconTreeLine // └─ last item
112+ }
113+ connector := styles.Theme.Muted.Render(connectorGlyph)
110114 subtitle := styles.Theme.Muted.Render(node.Subtitle)
111- b.WriteString(prefix + " " + subtitle + "\n")
115+ b.WriteString(prefix + " " + connector + " " + subtitle + "\n")
112116 }
113117
114118 // Children
@@ -128,10 +132,11 @@ func (t *Tree) renderNode(b *strings.Builder, node *Node, prefix string) {
128132 // Child title
129133 b.WriteString(prefix + " " + branch + " " + child.Title + "\n")
130134
131- // Child subtitle (indented to align under child name)
135+ // Child subtitle (with tree connector under child name)
132136 if child.Subtitle != "" {
137+ connector := styles.Theme.Muted.Render(styles.IconTreeLast + styles.IconTreeLine)
133138 subtitle := styles.Theme.Muted.Render(child.Subtitle)
134- b.WriteString(prefix + " " + childPrefix + " " + subtitle + "\n")
139+ b.WriteString(prefix + " " + childPrefix + " " + connector + " " + subtitle + "\n")
135140 }
136141 }
137142 }
@@ -70,10 +70,7 @@ func (t *Tree) Render() string {
70 }70 }
71 71
72 var b strings.Builder72 var b strings.Builder
73- for i, item := range t.items {73+ for _, item := range t.items {
74- if i > 0 {
75- b.WriteString("\n")
76- }
77 if item.group != nil {74 if item.group != nil {
78 t.renderGroup(&b, item.group)75 t.renderGroup(&b, item.group)
79 } else {76 } else {
@@ -105,10 +102,17 @@ func (t *Tree) renderNode(b *strings.Builder, node *Node, prefix string) {
105 // Title line102 // Title line
106 b.WriteString(prefix + node.Title + "\n")103 b.WriteString(prefix + node.Title + "\n")
107 104
108- // Subtitle line (muted, aligned with title text after icons)105+ // Subtitle line (muted, with tree connector aligned under container icon)
109 if node.Subtitle != "" {106 if node.Subtitle != "" {
107+ var connectorGlyph string
108+ if len(node.Children) > 0 {
109+ connectorGlyph = styles.IconTreeBranch + styles.IconTreeLine // ├─ more items below
110+ } else {
111+ connectorGlyph = styles.IconTreeLast + styles.IconTreeLine // └─ last item
112+ }
113+ connector := styles.Theme.Muted.Render(connectorGlyph)
110 subtitle := styles.Theme.Muted.Render(node.Subtitle)114 subtitle := styles.Theme.Muted.Render(node.Subtitle)
111- b.WriteString(prefix + " " + subtitle + "\n")115+ b.WriteString(prefix + " " + connector + " " + subtitle + "\n")
112 }116 }
113 117
114 // Children118 // Children
@@ -128,10 +132,11 @@ func (t *Tree) renderNode(b *strings.Builder, node *Node, prefix string) {
128 // Child title132 // Child title
129 b.WriteString(prefix + " " + branch + " " + child.Title + "\n")133 b.WriteString(prefix + " " + branch + " " + child.Title + "\n")
130 134
131- // Child subtitle (indented to align under child name)135+ // Child subtitle (with tree connector under child name)
132 if child.Subtitle != "" {136 if child.Subtitle != "" {
137+ connector := styles.Theme.Muted.Render(styles.IconTreeLast + styles.IconTreeLine)
133 subtitle := styles.Theme.Muted.Render(child.Subtitle)138 subtitle := styles.Theme.Muted.Render(child.Subtitle)
134- b.WriteString(prefix + " " + childPrefix + " " + subtitle + "\n")139+ b.WriteString(prefix + " " + childPrefix + " " + connector + " " + subtitle + "\n")
135 }140 }
136 }141 }
137 }142 }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close