main 5041353

mode_toaster_layout.go RawHistoryFolder

1package ui
2
3import (
4	"github.com/bnema/dumber/internal/domain/entity"
5	"github.com/bnema/dumber/internal/ui/input"
6)
7
8// Legend band geometry. The legend is a wide band anchored on the mode toaster,
9// not a full-width bar: its width follows the window up to a cap, and its
10// column count follows its width.
11const (
12	// legendMaxWidth caps the band width.
13	legendMaxWidth = 1100
14	// legendMinColumnWidth is the narrowest a column may get before the band
15	// drops a column and grows taller instead.
16	legendMinColumnWidth = 240
17	// legendComfortColumnWidth is the column width the band shrinks to when the
18	// mode has fewer groups than the width could hold columns.
19	legendComfortColumnWidth = 360
20	legendMaxColumns         = 4
21	// legendColumnGap is the horizontal space between columns.
22	legendColumnGap = 12
23	// legendPanelPadding is the horizontal chrome (border and padding) around
24	// the columns.
25	legendPanelPadding = 28
26	// legendMaxHeightPercent bounds the legend's scrolling body, as a share of
27	// the content height.
28	legendMaxHeightPercent = 45
29	// legendChromeHeight is the vertical chrome (border, padding, hint line)
30	// around the scrolling body, subtracted from the height budget.
31	legendChromeHeight  = 40
32	legendMinBodyHeight = 60
33	// modeToasterMargin separates the toaster from the window edges, and the
34	// legend band from the right edge.
35	modeToasterMargin = 8
36)
37
38// legendColumnCount returns how many columns of at least legendMinColumnWidth
39// fit a band of panelWidth, from 1 to legendMaxColumns.
40func legendColumnCount(panelWidth int) int {
41	columns := (panelWidth - legendPanelPadding + legendColumnGap) / (legendMinColumnWidth + legendColumnGap)
42	return min(max(columns, 1), legendMaxColumns)
43}
44
45// legendBandWidth returns the band width and column count for a legend of
46// groups groups when availableWidth pixels lie between the toaster's left edge
47// and the right margin. A band for few groups does not stretch over columns it
48// cannot fill.
49func legendBandWidth(availableWidth, groups int) (width, columns int) {
50	width = min(availableWidth, legendMaxWidth)
51	if width <= 0 || groups <= 0 {
52		return 0, 0
53	}
54	columns = legendColumnCount(width)
55	if groups < columns {
56		columns = groups
57		width = min(width, legendPanelPadding+columns*legendComfortColumnWidth+(columns-1)*legendColumnGap)
58	}
59	return width, columns
60}
61
62// legendBodyMaxHeight returns the height past which the legend body scrolls.
63func legendBodyMaxHeight(contentHeight int) int {
64	return max(contentHeight*legendMaxHeightPercent/100-legendChromeHeight, legendMinBodyHeight)
65}
66
67// distributeLegendGroups splits groups, given by their heights in display
68// order, into at most columns columns. Each column holds consecutive groups, so
69// the order reads down the first column, then the next, and no group is split.
70// Among all splits it picks the one with the shortest tallest column, then the
71// most even one; remaining ties go to the earliest split. The result lists
72// group indices per column and has no empty column.
73func distributeLegendGroups(heights []int, columns int) [][]int {
74	if len(heights) == 0 || columns < 1 {
75		return nil
76	}
77	columns = min(columns, len(heights))
78	best := legendSplit{tallest: -1}
79	cuts := make([]int, 0, columns-1)
80	var search func(start int)
81	search = func(start int) {
82		if len(cuts) == columns-1 {
83			candidate := scoreLegendSplit(heights, cuts)
84			if best.tallest < 0 || candidate.betterThan(best) {
85				best = candidate
86			}
87			return
88		}
89		// Leave one group for each column still to come.
90		remaining := columns - 1 - len(cuts)
91		for cut := start; cut <= len(heights)-remaining; cut++ {
92			cuts = append(cuts, cut)
93			search(cut + 1)
94			cuts = cuts[:len(cuts)-1]
95		}
96	}
97	search(1)
98
99	out := make([][]int, 0, columns)
100	first := 0
101	for _, cut := range append(best.cuts, len(heights)) {
102		column := make([]int, 0, cut-first)
103		for i := first; i < cut; i++ {
104			column = append(column, i)
105		}
106		out = append(out, column)
107		first = cut
108	}
109	return out
110}
111
112type legendSplit struct {
113	cuts       []int
114	tallest    int
115	sumSquares int
116}
117
118func (s legendSplit) betterThan(other legendSplit) bool {
119	if s.tallest != other.tallest {
120		return s.tallest < other.tallest
121	}
122	return s.sumSquares < other.sumSquares
123}
124
125func scoreLegendSplit(heights, cuts []int) legendSplit {
126	split := legendSplit{cuts: append([]int(nil), cuts...)}
127	first, sum := 0, 0
128	for _, cut := range append(append([]int(nil), cuts...), len(heights)) {
129		sum = 0
130		for _, h := range heights[first:cut] {
131			sum += h
132		}
133		split.tallest = max(split.tallest, sum)
134		split.sumSquares += sum * sum
135		first = cut
136	}
137	return split
138}
139
140// modeToasterVisible reports whether the toaster is shown for mode. The
141// indicator preference alone does not hide it while a legend can expand from
142// it: the toaster is the legend's header.
143func modeToasterVisible(mode input.Mode, cfg entity.WorkspaceStylingConfig) bool {
144	return mode != input.ModeNormal && (cfg.ModeIndicatorToasterEnabled || modeLegendEnabled(cfg))
145}
146
147// modeLegendEnabled reports whether a legend can expand from the toaster.
148// With the legend off the toaster is a plain indicator: no chevron, no toggle.
149func modeLegendEnabled(cfg entity.WorkspaceStylingConfig) bool {
150	return cfg.ModeLegend != "off"
151}
152
153// legendBodyShown reports whether the expanded legend is on screen. ready is
154// set once the legend's show delay has elapsed. A suspended legend (an in-page
155// interaction owns the keys) and a collapsed one stay hidden.
156func legendBodyShown(ready, collapsed, suspended bool) bool {
157	return ready && !collapsed && !suspended
158}
159
160// nextModeLegendCollapsed is the collapsed state after a click on the toaster.
161func nextModeLegendCollapsed(collapsed bool) bool {
162	return !collapsed
163}

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 8

Tags 42

↑↓ moveenter openesc close