detached 917da8c

Add optional theme background support

commit
917da8c414e183118e68034e0e8c6f6b746f0132
author
patriceckhart · · 2026-05-30 17:01 UTC
parent
16b95cb
signature
unsigned
refs
tag: v0.2.6
7 files · +89 -19
M docs/themes.md +3 -1
@@ -115,6 +115,7 @@ All fields are optional.
115115 "fg": 253,
116116 "muted": 244,
117117 "accent": 111,
118+ "background": "#0b1020",
118119 "user": 180,
119120 "user_bubble_bg": "#42454b",
120121 "user_bubble_fg": 248,
@@ -180,6 +181,7 @@ Most color fields are xterm-256 indexes (`0`–`255`).
180181 - `fg` — default foreground text.
181182 - `muted` — secondary text, dividers, gutters, inactive hints.
182183 - `accent` — prompt bar, bullets, links, headings, active markers.
184+- `background` — optional full-row TUI background. If missing, zot uses the terminal's existing background. Experimental: terminal background colors can vary by emulator and scrollback behavior; for the most reliable result, change your terminal background color in your terminal settings instead.
183185 - `user` — user role label color; mostly compatibility.
184186 - `user_bubble_bg` — background behind user message rows.
185187 - `user_bubble_fg` — foreground inside user message rows.
@@ -192,7 +194,7 @@ Most color fields are xterm-256 indexes (`0`–`255`).
192194 - `selection_bg` — highlighted row background.
193195 - `selection_fg` — highlighted row foreground.
194196
195-`user_bubble_bg` supports richer terminal color forms:
197+`background` and `user_bubble_bg` support richer terminal color forms:
196198
197199 ```json
198200 254
@@ -115,6 +115,7 @@ All fields are optional.
115 "fg": 253,115 "fg": 253,
116 "muted": 244,116 "muted": 244,
117 "accent": 111,117 "accent": 111,
118+ "background": "#0b1020",
118 "user": 180,119 "user": 180,
119 "user_bubble_bg": "#42454b",120 "user_bubble_bg": "#42454b",
120 "user_bubble_fg": 248,121 "user_bubble_fg": 248,
@@ -180,6 +181,7 @@ Most color fields are xterm-256 indexes (`0`–`255`).
180 - `fg` — default foreground text.181 - `fg` — default foreground text.
181 - `muted` — secondary text, dividers, gutters, inactive hints.182 - `muted` — secondary text, dividers, gutters, inactive hints.
182 - `accent` — prompt bar, bullets, links, headings, active markers.183 - `accent` — prompt bar, bullets, links, headings, active markers.
184+- `background` — optional full-row TUI background. If missing, zot uses the terminal's existing background. Experimental: terminal background colors can vary by emulator and scrollback behavior; for the most reliable result, change your terminal background color in your terminal settings instead.
183 - `user` — user role label color; mostly compatibility.185 - `user` — user role label color; mostly compatibility.
184 - `user_bubble_bg` — background behind user message rows.186 - `user_bubble_bg` — background behind user message rows.
185 - `user_bubble_fg` — foreground inside user message rows.187 - `user_bubble_fg` — foreground inside user message rows.
@@ -192,7 +194,7 @@ Most color fields are xterm-256 indexes (`0`–`255`).
192 - `selection_bg` — highlighted row background.194 - `selection_bg` — highlighted row background.
193 - `selection_fg` — highlighted row foreground.195 - `selection_fg` — highlighted row foreground.
194 196
195-`user_bubble_bg` supports richer terminal color forms:197+`background` and `user_bubble_bg` support richer terminal color forms:
196 198
197 ```json199 ```json
198 254200 254
R examples/theme/custom-spinner.json → examples/themes/custom-spinner.json +0 -0 renamed, 100% similar

renamed, 100% similar

R examples/theme/vercel.json → examples/themes/vercel.json +0 -0 renamed, 100% similar

renamed, 100% similar

M packages/agent/modes/interactive.go +4 -1
@@ -415,6 +415,8 @@ const resumeTailExpandStep = 80
415415
416416 // NewInteractive constructs an Interactive from cfg.
417417 func NewInteractive(cfg InteractiveConfig) *Interactive {
418+ renderer := tui.NewRenderer(cfg.Terminal)
419+ renderer.SetTheme(cfg.Theme)
418420 i := &Interactive{
419421 cfg: cfg,
420422 view: &tui.View{
@@ -425,7 +427,7 @@ func NewInteractive(cfg InteractiveConfig) *Interactive {
425427 // speaker labels too, so the input gutter matches the rest
426428 // of the UI.
427429 ed: tui.NewEditor(cfg.Theme.AccentBar(cfg.Theme.Accent)),
428- rend: tui.NewRenderer(cfg.Terminal),
430+ rend: renderer,
429431 toolCalls: map[string]*tui.ToolCallView{},
430432 dirty: make(chan struct{}, 8),
431433 dialog: newLoginDialog(),
@@ -2709,6 +2711,7 @@ func (i *Interactive) applyThemeNow(name string) {
27092711 i.ed.Prompt = th.AccentBar(th.Accent)
27102712 i.spin.Configure(th)
27112713 if i.rend != nil {
2714+ i.rend.SetTheme(th)
27122715 i.rend.Clear()
27132716 }
27142717 i.invalidate()
@@ -415,6 +415,8 @@ const resumeTailExpandStep = 80
415 415
416 // NewInteractive constructs an Interactive from cfg.416 // NewInteractive constructs an Interactive from cfg.
417 func NewInteractive(cfg InteractiveConfig) *Interactive {417 func NewInteractive(cfg InteractiveConfig) *Interactive {
418+ renderer := tui.NewRenderer(cfg.Terminal)
419+ renderer.SetTheme(cfg.Theme)
418 i := &Interactive{420 i := &Interactive{
419 cfg: cfg,421 cfg: cfg,
420 view: &tui.View{422 view: &tui.View{
@@ -425,7 +427,7 @@ func NewInteractive(cfg InteractiveConfig) *Interactive {
425 // speaker labels too, so the input gutter matches the rest427 // speaker labels too, so the input gutter matches the rest
426 // of the UI.428 // of the UI.
427 ed: tui.NewEditor(cfg.Theme.AccentBar(cfg.Theme.Accent)),429 ed: tui.NewEditor(cfg.Theme.AccentBar(cfg.Theme.Accent)),
428- rend: tui.NewRenderer(cfg.Terminal),430+ rend: renderer,
429 toolCalls: map[string]*tui.ToolCallView{},431 toolCalls: map[string]*tui.ToolCallView{},
430 dirty: make(chan struct{}, 8),432 dirty: make(chan struct{}, 8),
431 dialog: newLoginDialog(),433 dialog: newLoginDialog(),
@@ -2709,6 +2711,7 @@ func (i *Interactive) applyThemeNow(name string) {
2709 i.ed.Prompt = th.AccentBar(th.Accent)2711 i.ed.Prompt = th.AccentBar(th.Accent)
2710 i.spin.Configure(th)2712 i.spin.Configure(th)
2711 if i.rend != nil {2713 if i.rend != nil {
2714+ i.rend.SetTheme(th)
2712 i.rend.Clear()2715 i.rend.Clear()
2713 }2716 }
2714 i.invalidate()2717 i.invalidate()
M packages/tui/render.go +62 -13
@@ -70,6 +70,12 @@ type Renderer struct {
7070 // limitation specific to VS Code's terminal that we have no
7171 // way to work around without breaking other terminals.
7272 keepScrollback bool
73+
74+ // theme is optional renderer-level styling applied at the final
75+ // row-writing boundary. In particular, Theme.Background is painted
76+ // as a full-width row background without making every View renderer
77+ // know about terminal padding and reset semantics.
78+ theme Theme
7379 }
7480
7581 // NewRenderer returns a renderer that writes to out.
@@ -86,6 +92,13 @@ func NewRenderer(out io.Writer) *Renderer {
8692 }
8793 }
8894
95+// SetTheme updates renderer-level terminal styling. Changing the
96+// background affects every row, so cached frame state is invalidated.
97+func (r *Renderer) SetTheme(th Theme) {
98+ r.theme = th
99+ r.Invalidate()
100+}
101+
89102 // Resize tells the renderer the current terminal size.
90103 //
91104 // On a real size change we also issue a clear-screen so the next Draw
@@ -168,6 +181,23 @@ func containsImageEscape(s string) bool {
168181 return strings.Contains(s, "\x1b]1337;File=") || strings.Contains(s, "\x1b_G")
169182 }
170183
184+// paintBackgroundRow applies the optional theme background to a single
185+// already-truncated terminal row. It pads with spaces to cols so the
186+// background reaches the right edge, and re-applies the background
187+// after full SGR resets inside the row so local styling does not punch
188+// transparent holes through the global tint.
189+func paintBackgroundRow(line string, cols int, th Theme) string {
190+ bg := th.BackgroundStyle()
191+ if bg == "" || cols <= 0 || containsImageEscape(line) {
192+ return line
193+ }
194+ line = strings.ReplaceAll(line, reset, reset+bg)
195+ if w := visibleWidth(line); w < cols {
196+ line += strings.Repeat(" ", cols-w)
197+ }
198+ return bg + line + reset
199+}
200+
171201 // truncateToWidth clips s so its on-screen width doesn't exceed cols
172202 // cells, preserving ANSI CSI escape sequences (which don't consume
173203 // cells). Lines carrying an inline-image escape are returned as-is
@@ -254,7 +284,12 @@ func (r *Renderer) Draw(lines []string, cursorRow, cursorCol int) {
254284 frame[i] = ""
255285 }
256286 for i, line := range visible {
257- frame[top+i] = truncateToWidth(line, r.cols)
287+ frame[top+i] = paintBackgroundRow(truncateToWidth(line, r.cols), r.cols, r.theme)
288+ }
289+ if r.theme.Background != nil {
290+ for i := 0; i < top; i++ {
291+ frame[i] = paintBackgroundRow("", r.cols, r.theme)
292+ }
258293 }
259294
260295 var w strings.Builder
@@ -297,19 +332,22 @@ func (r *Renderer) Draw(lines []string, cursorRow, cursorCol int) {
297332 // terminal doesn't reliably clear background colors on row
298333 // overwrites, leaving ghost highlights behind.
299334 hasSelection := false
300- for _, l := range frame {
301- if strings.Contains(l, "\x1b[48;5;") {
302- hasSelection = true
303- break
304- }
305- }
306- if !hasSelection && r.prev != nil {
307- for _, l := range r.prev {
308- if strings.Contains(l, "\x1b[48;5;") {
335+ if r.theme.Background == nil {
336+ selectionBG := sgrBG(r.theme.SelectionBG)
337+ for _, l := range frame {
338+ if selectionBG != "" && strings.Contains(l, selectionBG) {
309339 hasSelection = true
310340 break
311341 }
312342 }
343+ if !hasSelection && r.prev != nil {
344+ for _, l := range r.prev {
345+ if selectionBG != "" && strings.Contains(l, selectionBG) {
346+ hasSelection = true
347+ break
348+ }
349+ }
350+ }
313351 }
314352
315353 full := r.prev == nil || len(r.prev) != r.rows
@@ -355,11 +393,11 @@ func (r *Renderer) DrawLog(chat, bottom []string, cursorBottomRow, cursorCol int
355393 }
356394 chatFrame := make([]string, len(chat))
357395 for i, line := range chat {
358- chatFrame[i] = truncateToWidth(line, r.cols)
396+ chatFrame[i] = paintBackgroundRow(truncateToWidth(line, r.cols), r.cols, r.theme)
359397 }
360398 bottomFrame := make([]string, len(bottom))
361399 for i, line := range bottom {
362- bottomFrame[i] = truncateToWidth(line, r.cols)
400+ bottomFrame[i] = paintBackgroundRow(truncateToWidth(line, r.cols), r.cols, r.theme)
363401 }
364402
365403 // Always reserve one real row below the editor/status band. This is
@@ -371,7 +409,18 @@ func (r *Renderer) DrawLog(chat, bottom []string, cursorBottomRow, cursorCol int
371409 lines = append(lines, chatFrame...)
372410 lines = append(lines, bottomFrame...)
373411 for range bottomMarginRows {
374- lines = append(lines, "")
412+ lines = append(lines, paintBackgroundRow("", r.cols, r.theme))
413+ }
414+ // In main-screen flow mode zot normally emits only its logical
415+ // content rows and leaves the rest of the terminal viewport alone.
416+ // When a theme background is configured, fill that otherwise-idle
417+ // space with painted blank rows so the full window is tinted while
418+ // keeping the scrollback-oriented renderer model unchanged for the
419+ // default transparent case.
420+ if r.theme.Background != nil {
421+ for len(lines) < r.rows {
422+ lines = append(lines, paintBackgroundRow("", r.cols, r.theme))
423+ }
375424 }
376425 if len(lines) == 0 {
377426 lines = []string{""}
@@ -70,6 +70,12 @@ type Renderer struct {
70 // limitation specific to VS Code's terminal that we have no70 // limitation specific to VS Code's terminal that we have no
71 // way to work around without breaking other terminals.71 // way to work around without breaking other terminals.
72 keepScrollback bool72 keepScrollback bool
73+
74+ // theme is optional renderer-level styling applied at the final
75+ // row-writing boundary. In particular, Theme.Background is painted
76+ // as a full-width row background without making every View renderer
77+ // know about terminal padding and reset semantics.
78+ theme Theme
73 }79 }
74 80
75 // NewRenderer returns a renderer that writes to out.81 // NewRenderer returns a renderer that writes to out.
@@ -86,6 +92,13 @@ func NewRenderer(out io.Writer) *Renderer {
86 }92 }
87 }93 }
88 94
95+// SetTheme updates renderer-level terminal styling. Changing the
96+// background affects every row, so cached frame state is invalidated.
97+func (r *Renderer) SetTheme(th Theme) {
98+ r.theme = th
99+ r.Invalidate()
100+}
101+
89 // Resize tells the renderer the current terminal size.102 // Resize tells the renderer the current terminal size.
90 //103 //
91 // On a real size change we also issue a clear-screen so the next Draw104 // On a real size change we also issue a clear-screen so the next Draw
@@ -168,6 +181,23 @@ func containsImageEscape(s string) bool {
168 return strings.Contains(s, "\x1b]1337;File=") || strings.Contains(s, "\x1b_G")181 return strings.Contains(s, "\x1b]1337;File=") || strings.Contains(s, "\x1b_G")
169 }182 }
170 183
184+// paintBackgroundRow applies the optional theme background to a single
185+// already-truncated terminal row. It pads with spaces to cols so the
186+// background reaches the right edge, and re-applies the background
187+// after full SGR resets inside the row so local styling does not punch
188+// transparent holes through the global tint.
189+func paintBackgroundRow(line string, cols int, th Theme) string {
190+ bg := th.BackgroundStyle()
191+ if bg == "" || cols <= 0 || containsImageEscape(line) {
192+ return line
193+ }
194+ line = strings.ReplaceAll(line, reset, reset+bg)
195+ if w := visibleWidth(line); w < cols {
196+ line += strings.Repeat(" ", cols-w)
197+ }
198+ return bg + line + reset
199+}
200+
171 // truncateToWidth clips s so its on-screen width doesn't exceed cols201 // truncateToWidth clips s so its on-screen width doesn't exceed cols
172 // cells, preserving ANSI CSI escape sequences (which don't consume202 // cells, preserving ANSI CSI escape sequences (which don't consume
173 // cells). Lines carrying an inline-image escape are returned as-is203 // cells). Lines carrying an inline-image escape are returned as-is
@@ -254,7 +284,12 @@ func (r *Renderer) Draw(lines []string, cursorRow, cursorCol int) {
254 frame[i] = ""284 frame[i] = ""
255 }285 }
256 for i, line := range visible {286 for i, line := range visible {
257- frame[top+i] = truncateToWidth(line, r.cols)287+ frame[top+i] = paintBackgroundRow(truncateToWidth(line, r.cols), r.cols, r.theme)
288+ }
289+ if r.theme.Background != nil {
290+ for i := 0; i < top; i++ {
291+ frame[i] = paintBackgroundRow("", r.cols, r.theme)
292+ }
258 }293 }
259 294
260 var w strings.Builder295 var w strings.Builder
@@ -297,19 +332,22 @@ func (r *Renderer) Draw(lines []string, cursorRow, cursorCol int) {
297 // terminal doesn't reliably clear background colors on row332 // terminal doesn't reliably clear background colors on row
298 // overwrites, leaving ghost highlights behind.333 // overwrites, leaving ghost highlights behind.
299 hasSelection := false334 hasSelection := false
300- for _, l := range frame {335+ if r.theme.Background == nil {
301- if strings.Contains(l, "\x1b[48;5;") {336+ selectionBG := sgrBG(r.theme.SelectionBG)
302- hasSelection = true337+ for _, l := range frame {
303- break338+ if selectionBG != "" && strings.Contains(l, selectionBG) {
304- }
305- }
306- if !hasSelection && r.prev != nil {
307- for _, l := range r.prev {
308- if strings.Contains(l, "\x1b[48;5;") {
309 hasSelection = true339 hasSelection = true
310 break340 break
311 }341 }
312 }342 }
343+ if !hasSelection && r.prev != nil {
344+ for _, l := range r.prev {
345+ if selectionBG != "" && strings.Contains(l, selectionBG) {
346+ hasSelection = true
347+ break
348+ }
349+ }
350+ }
313 }351 }
314 352
315 full := r.prev == nil || len(r.prev) != r.rows353 full := r.prev == nil || len(r.prev) != r.rows
@@ -355,11 +393,11 @@ func (r *Renderer) DrawLog(chat, bottom []string, cursorBottomRow, cursorCol int
355 }393 }
356 chatFrame := make([]string, len(chat))394 chatFrame := make([]string, len(chat))
357 for i, line := range chat {395 for i, line := range chat {
358- chatFrame[i] = truncateToWidth(line, r.cols)396+ chatFrame[i] = paintBackgroundRow(truncateToWidth(line, r.cols), r.cols, r.theme)
359 }397 }
360 bottomFrame := make([]string, len(bottom))398 bottomFrame := make([]string, len(bottom))
361 for i, line := range bottom {399 for i, line := range bottom {
362- bottomFrame[i] = truncateToWidth(line, r.cols)400+ bottomFrame[i] = paintBackgroundRow(truncateToWidth(line, r.cols), r.cols, r.theme)
363 }401 }
364 402
365 // Always reserve one real row below the editor/status band. This is403 // Always reserve one real row below the editor/status band. This is
@@ -371,7 +409,18 @@ func (r *Renderer) DrawLog(chat, bottom []string, cursorBottomRow, cursorCol int
371 lines = append(lines, chatFrame...)409 lines = append(lines, chatFrame...)
372 lines = append(lines, bottomFrame...)410 lines = append(lines, bottomFrame...)
373 for range bottomMarginRows {411 for range bottomMarginRows {
374- lines = append(lines, "")412+ lines = append(lines, paintBackgroundRow("", r.cols, r.theme))
413+ }
414+ // In main-screen flow mode zot normally emits only its logical
415+ // content rows and leaves the rest of the terminal viewport alone.
416+ // When a theme background is configured, fill that otherwise-idle
417+ // space with painted blank rows so the full window is tinted while
418+ // keeping the scrollback-oriented renderer model unchanged for the
419+ // default transparent case.
420+ if r.theme.Background != nil {
421+ for len(lines) < r.rows {
422+ lines = append(lines, paintBackgroundRow("", r.cols, r.theme))
423+ }
375 }424 }
376 if len(lines) == 0 {425 if len(lines) == 0 {
377 lines = []string{""}426 lines = []string{""}
M packages/tui/theme.go +15 -4
@@ -39,10 +39,11 @@ type Theme struct {
3939 FG int
4040 Muted int
4141 Accent int
42- User int // label color for the user role
43- UserBubbleBG TerminalColor // background tint behind user message rows
44- UserBubbleFG int // foreground colour for user message rows
45- Assistant int // label color for the zot role
42+ Background *TerminalColor // optional full-row TUI background; nil keeps terminal default
43+ User int // label color for the user role
44+ UserBubbleBG TerminalColor // background tint behind user message rows
45+ UserBubbleFG int // foreground colour for user message rows
46+ Assistant int // label color for the zot role
4647 Tool int
4748 ToolOut int
4849 Error int
@@ -229,6 +230,16 @@ func (t Theme) SelectionStyle() string {
229230 return sgrFG(t.SelectionFG) + sgrBG(t.SelectionBG)
230231 }
231232
233+// BackgroundStyle returns the SGR prefix for the optional full-row
234+// TUI background. Empty means zot should leave the terminal's
235+// configured background untouched.
236+func (t Theme) BackgroundStyle() string {
237+ if t.Background == nil {
238+ return ""
239+ }
240+ return sgrBGColor(*t.Background)
241+}
242+
232243 // SelectionStyleFG returns the SGR prefix for selected-row text with
233244 // a custom foreground. Useful for preserving semantic marks inside a
234245 // highlighted row without hardcoding escape sequences outside theme.go.
@@ -39,10 +39,11 @@ type Theme struct {
39 FG int39 FG int
40 Muted int40 Muted int
41 Accent int41 Accent int
42- User int // label color for the user role42+ Background *TerminalColor // optional full-row TUI background; nil keeps terminal default
43- UserBubbleBG TerminalColor // background tint behind user message rows43+ User int // label color for the user role
44- UserBubbleFG int // foreground colour for user message rows44+ UserBubbleBG TerminalColor // background tint behind user message rows
45- Assistant int // label color for the zot role45+ UserBubbleFG int // foreground colour for user message rows
46+ Assistant int // label color for the zot role
46 Tool int47 Tool int
47 ToolOut int48 ToolOut int
48 Error int49 Error int
@@ -229,6 +230,16 @@ func (t Theme) SelectionStyle() string {
229 return sgrFG(t.SelectionFG) + sgrBG(t.SelectionBG)230 return sgrFG(t.SelectionFG) + sgrBG(t.SelectionBG)
230 }231 }
231 232
233+// BackgroundStyle returns the SGR prefix for the optional full-row
234+// TUI background. Empty means zot should leave the terminal's
235+// configured background untouched.
236+func (t Theme) BackgroundStyle() string {
237+ if t.Background == nil {
238+ return ""
239+ }
240+ return sgrBGColor(*t.Background)
241+}
242+
232 // SelectionStyleFG returns the SGR prefix for selected-row text with243 // SelectionStyleFG returns the SGR prefix for selected-row text with
233 // a custom foreground. Useful for preserving semantic marks inside a244 // a custom foreground. Useful for preserving semantic marks inside a
234 // highlighted row without hardcoding escape sequences outside theme.go.245 // highlighted row without hardcoding escape sequences outside theme.go.
M packages/tui/theme_loader.go +5 -0
@@ -82,6 +82,7 @@ type ThemeOverrides struct {
8282 FG *int `json:"fg,omitempty"`
8383 Muted *int `json:"muted,omitempty"`
8484 Accent *int `json:"accent,omitempty"`
85+ Background *TerminalColorValue `json:"background,omitempty"`
8586 User *int `json:"user,omitempty"`
8687 UserBubbleBG *TerminalColorValue `json:"user_bubble_bg,omitempty"`
8788 UserBubbleFG *int `json:"user_bubble_fg,omitempty"`
@@ -351,6 +352,10 @@ func applyThemeOverrides(th Theme, o ThemeOverrides) Theme {
351352 if o.Accent != nil {
352353 th.Accent = *o.Accent
353354 }
355+ if o.Background != nil {
356+ bg := o.Background.TerminalColor
357+ th.Background = &bg
358+ }
354359 if o.User != nil {
355360 th.User = *o.User
356361 }
@@ -82,6 +82,7 @@ type ThemeOverrides struct {
82 FG *int `json:"fg,omitempty"`82 FG *int `json:"fg,omitempty"`
83 Muted *int `json:"muted,omitempty"`83 Muted *int `json:"muted,omitempty"`
84 Accent *int `json:"accent,omitempty"`84 Accent *int `json:"accent,omitempty"`
85+ Background *TerminalColorValue `json:"background,omitempty"`
85 User *int `json:"user,omitempty"`86 User *int `json:"user,omitempty"`
86 UserBubbleBG *TerminalColorValue `json:"user_bubble_bg,omitempty"`87 UserBubbleBG *TerminalColorValue `json:"user_bubble_bg,omitempty"`
87 UserBubbleFG *int `json:"user_bubble_fg,omitempty"`88 UserBubbleFG *int `json:"user_bubble_fg,omitempty"`
@@ -351,6 +352,10 @@ func applyThemeOverrides(th Theme, o ThemeOverrides) Theme {
351 if o.Accent != nil {352 if o.Accent != nil {
352 th.Accent = *o.Accent353 th.Accent = *o.Accent
353 }354 }
355+ if o.Background != nil {
356+ bg := o.Background.TerminalColor
357+ th.Background = &bg
358+ }
354 if o.User != nil {359 if o.User != nil {
355 th.User = *o.User360 th.User = *o.User
356 }361 }

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 476

↑↓ moveenter openesc close