detached 008e36d

feat(ui): add Node.TextInt and Node.Masked

commit
008e36d0ffd4f5ab386943b9c2192b9bdd736624
author
brice · · 2026-10-01 16:30 UTC
parent
e1768ca
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +32 -0
M internal/ui/controls.go +32 -0
@@ -3,6 +3,7 @@ package ui
33 import (
44 "fmt"
55 "github.com/bnema/nefergui/internal/edit"
6+ "strconv"
67 "strings"
78 )
89
@@ -231,3 +232,34 @@ func (n Node) Heading(text string, options ...HeadingOption) {
231232 }
232233 }
233234 }
235+
236+// TextInt shows prefix followed by v in decimal without fmt boxing. It builds
237+// the text in a stack buffer, so the only allocation is the final string.
238+func (n Node) TextInt(prefix string, v int64, options ...ContainerOption) {
239+ child := n.child("text", "", options)
240+ if !child.valid() {
241+ return
242+ }
243+ var stack [64]byte
244+ buf := append(stack[:0], prefix...)
245+ buf = strconv.AppendInt(buf, v, 10)
246+ child.element.text = string(buf)
247+}
248+
249+// maskBullets backs Masked for common counts without allocating.
250+var maskBullets = strings.Repeat("•", 256)
251+
252+// Masked shows count bullet glyphs and stores no value, for a secret held
253+// outside the view (a password field fed from an external buffer). A negative
254+// count shows nothing.
255+func (n Node) Masked(count int, options ...ContainerOption) {
256+ child := n.child("text", "", options)
257+ if !child.valid() || count <= 0 {
258+ return
259+ }
260+ if count <= 256 {
261+ child.element.text = maskBullets[:count*len("•")]
262+ return
263+ }
264+ child.element.text = strings.Repeat("•", count)
265+}
@@ -3,6 +3,7 @@ package ui
3 import (3 import (
4 "fmt"4 "fmt"
5 "github.com/bnema/nefergui/internal/edit"5 "github.com/bnema/nefergui/internal/edit"
6+ "strconv"
6 "strings"7 "strings"
7 )8 )
8 9
@@ -231,3 +232,34 @@ func (n Node) Heading(text string, options ...HeadingOption) {
231 }232 }
232 }233 }
233 }234 }
235+
236+// TextInt shows prefix followed by v in decimal without fmt boxing. It builds
237+// the text in a stack buffer, so the only allocation is the final string.
238+func (n Node) TextInt(prefix string, v int64, options ...ContainerOption) {
239+ child := n.child("text", "", options)
240+ if !child.valid() {
241+ return
242+ }
243+ var stack [64]byte
244+ buf := append(stack[:0], prefix...)
245+ buf = strconv.AppendInt(buf, v, 10)
246+ child.element.text = string(buf)
247+}
248+
249+// maskBullets backs Masked for common counts without allocating.
250+var maskBullets = strings.Repeat("•", 256)
251+
252+// Masked shows count bullet glyphs and stores no value, for a secret held
253+// outside the view (a password field fed from an external buffer). A negative
254+// count shows nothing.
255+func (n Node) Masked(count int, options ...ContainerOption) {
256+ child := n.child("text", "", options)
257+ if !child.valid() || count <= 0 {
258+ return
259+ }
260+ if count <= 256 {
261+ child.element.text = maskBullets[:count*len("•")]
262+ return
263+ }
264+ child.element.text = strings.Repeat("•", count)
265+}

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 1

Tags 12

↑↓ moveenter openesc close