v0.2.0 13c6a90

action.go RawHistoryFolder

1package core
2
3import (
4	"strconv"
5	"strings"
6)
7
8type Action string
9
10const (
11	ActionSpawnTerminal      Action = "spawn-terminal"
12	ActionFocusColumnLeft    Action = "focus-column-left"
13	ActionFocusColumnRight   Action = "focus-column-right"
14	ActionFocusWindowUp      Action = "focus-window-up"
15	ActionFocusWindowDown    Action = "focus-window-down"
16	ActionMoveColumnLeft     Action = "move-column-left"
17	ActionMoveColumnRight    Action = "move-column-right"
18	ActionCycleColumnWidth   Action = "cycle-column-width"
19	ActionMaximizeColumn     Action = "maximize-column"
20	ActionToggleFullscreen   Action = "toggle-fullscreen"
21	ActionToggleWindowStash  Action = "toggle-window-stash"
22	ActionToggleStashVisible Action = "toggle-stash-visible"
23	ActionToggleOverview     Action = "toggle-overview"
24	ActionCloseWindow        Action = "close-window"
25	ActionQuit               Action = "quit"
26	// Workspaces stack vertically; up/down stop at the ends.
27	ActionFocusWorkspaceUp   Action = "focus-workspace-up"
28	ActionFocusWorkspaceDown Action = "focus-workspace-down"
29	// Move the focused column, or only the focused window, one workspace
30	// up or down.
31	ActionMoveColumnToWorkspaceUp   Action = "move-column-to-workspace-up"
32	ActionMoveColumnToWorkspaceDown Action = "move-column-to-workspace-down"
33	ActionMoveWindowToWorkspaceUp   Action = "move-window-to-workspace-up"
34	ActionMoveWindowToWorkspaceDown Action = "move-window-to-workspace-down"
35	// Monitors are ordered left to right (ADR 011).
36	ActionFocusMonitorLeft   Action = "focus-monitor-left"
37	ActionFocusMonitorRight  Action = "focus-monitor-right"
38	ActionMoveWorkspaceLeft  Action = "move-workspace-to-monitor-left"
39	ActionMoveWorkspaceRight Action = "move-workspace-to-monitor-right"
40	// Scale steps through the clean scales of the output (see CleanScales).
41	ActionScaleUp   Action = "scale-up"
42	ActionScaleDown Action = "scale-down"
43	// Consume or expel the focused window (ADR 016).
44	ActionConsumeOrExpelLeft  Action = "consume-or-expel-window-left"
45	ActionConsumeOrExpelRight Action = "consume-or-expel-window-right"
46	// Move the focused window up or down inside its column.
47	ActionMoveWindowUp   Action = "move-window-up"
48	ActionMoveWindowDown Action = "move-window-down"
49	// Swap the active numbered workspace with its neighbor.
50	ActionMoveWorkspaceUp   Action = "move-workspace-up"
51	ActionMoveWorkspaceDown Action = "move-workspace-down"
52	// Turn the focused tile into a free floating window, or back.
53	ActionToggleFloating Action = "toggle-floating"
54)
55
56// Resize axes of ResizeArg.
57const (
58	ResizeWidth  = 0
59	ResizeHeight = 1
60)
61
62var resizePrefixes = [...]string{
63	ResizeWidth:  "set-column-width ",
64	ResizeHeight: "set-window-height ",
65}
66
67// ResizeArg parses "set-column-width +N%" (axis ResizeWidth) and
68// "set-window-height -N%" (axis ResizeHeight); N is 1 to 100 and the sign
69// is required.
70func ResizeArg(a Action) (axis, pct int, ok bool) {
71	for i, prefix := range resizePrefixes {
72		rest, found := strings.CutPrefix(string(a), prefix)
73		if !found {
74			continue
75		}
76		if p, ok := parseResizeStep(strings.TrimSpace(rest)); ok {
77			return i, p, true
78		}
79		return 0, 0, false
80	}
81	return 0, 0, false
82}
83
84// parseResizeStep parses "+N%" or "-N%" with N from 1 to 100.
85func parseResizeStep(s string) (int, bool) {
86	if len(s) < 3 || (s[0] != '+' && s[0] != '-') || s[len(s)-1] != '%' {
87		return 0, false
88	}
89	n, err := strconv.Atoi(s[1 : len(s)-1])
90	if err != nil || n < 1 || n > 100 || s[1] == '+' || s[1] == '-' {
91		return 0, false
92	}
93	if s[0] == '-' {
94		n = -n
95	}
96	return n, true
97}
98
99// WorkspaceOp is what a numbered workspace action does.
100type WorkspaceOp int
101
102const (
103	FocusWorkspace WorkspaceOp = iota
104	MoveColumnToWorkspace
105	MoveWindowToWorkspace
106)
107
108var workspacePrefixes = [...]string{
109	FocusWorkspace:        "focus-workspace ",
110	MoveColumnToWorkspace: "move-column-to-workspace ",
111	MoveWindowToWorkspace: "move-window-to-workspace ",
112}
113
114// WorkspaceArg parses "focus-workspace N", "move-column-to-workspace N" and
115// "move-window-to-workspace N" (N from 1).
116func WorkspaceArg(a Action) (n int, op WorkspaceOp, ok bool) {
117	for i, prefix := range workspacePrefixes {
118		rest, found := strings.CutPrefix(string(a), prefix)
119		if !found {
120			continue
121		}
122		n, err := strconv.Atoi(strings.TrimSpace(rest))
123		if err != nil || n < 1 || n > 99 {
124			return 0, 0, false
125		}
126		return n, WorkspaceOp(i), true
127	}
128	return 0, 0, false
129}
130
131// namedPrefix toggles a named workspace: "workspace dev".
132const namedPrefix = "workspace "
133
134// NamedArg returns the name of a "workspace <name>" action.
135func NamedArg(a Action) (string, bool) {
136	rest, ok := strings.CutPrefix(string(a), namedPrefix)
137	name := strings.TrimSpace(rest)
138	return name, ok && name != "" && !strings.ContainsAny(name, " \t")
139}
140
141// spawnPrefix starts a bind action that runs a command: "spawn fuzzel --flag".
142// Arguments are split on whitespace; there is no shell (use "spawn sh -c ...").
143const spawnPrefix = "spawn "
144
145// SpawnArgv returns the command of a "spawn <cmd>" action.
146func SpawnArgv(a Action) ([]string, bool) {
147	rest, ok := strings.CutPrefix(string(a), spawnPrefix)
148	argv := strings.Fields(rest)
149	return argv, ok && len(argv) > 0
150}
151
152type Effect struct {
153	Spawn bool
154	Argv  []string // command to run; nil means the configured terminal
155	Close WindowID
156	Quit  bool
157}
158
159// applyAction runs a bind action. Monitor actions change the focused
160// screen; column focus past the edge column moves to the neighbor screen;
161// everything else applies to the focused screen's monitor.
162func (c *Core) applyAction(a Action) Effect {
163	if e, ok := c.cur().mon.overviewAction(a); ok {
164		return e
165	}
166	// A bind acts on the window on screen: under a covering fullscreen
167	// window, that is the one Focused reports.
168	c.cur().mon.Current().focusCover()
169	// Before the edge moves below: a free float moves on its screen.
170	if c.cur().mon.Current().freeAction(a) {
171		return Effect{}
172	}
173	if name, ok := NamedArg(a); ok && c.bringNamed(name) {
174		return Effect{}
175	}
176	dir := 0
177	switch a {
178	case ActionFocusMonitorLeft, ActionMoveWorkspaceLeft:
179		dir = -1
180	case ActionFocusMonitorRight, ActionMoveWorkspaceRight:
181		dir = 1
182	}
183	switch a {
184	case ActionFocusMonitorLeft, ActionFocusMonitorRight:
185		if i := c.neighbor(dir); i >= 0 {
186			c.focusScreen = i
187		}
188		return Effect{}
189	case ActionMoveWorkspaceLeft, ActionMoveWorkspaceRight:
190		c.moveWorkspace(dir)
191		return Effect{}
192	case ActionFocusColumnLeft, ActionFocusColumnRight:
193		w := c.cur().mon.Current()
194		if a == ActionFocusColumnLeft {
195			dir = -1
196		} else {
197			dir = 1
198		}
199		// A covering fullscreen window is left for the window on that side;
200		// with none (the workspace or stash edge), the move goes to the
201		// neighbor monitor and fullscreen stays.
202		if w.pinned() {
203			if !w.FocusColumn(dir) {
204				if i := c.neighbor(dir); i >= 0 {
205					c.focusScreen = i
206				}
207			}
208			return Effect{}
209		}
210		// The first move from a float stays here unless nothing is under it:
211		// a native float leaves for the stash or the columns, and the stash
212		// keeps the focus at its ends.
213		edge := w.columnToward(dir) < 0 && (!w.onFloat() || w.floatFocus && !w.canLeaveFloat())
214		if i := c.neighbor(dir); edge && i >= 0 {
215			c.focusScreen = i
216			return Effect{}
217		}
218	case ActionConsumeOrExpelLeft, ActionConsumeOrExpelRight:
219		dir = 1
220		if a == ActionConsumeOrExpelLeft {
221			dir = -1
222		}
223		from := c.cur()
224		if from.mon.Current().ConsumeOrExpel(dir) {
225			from.mon.normalize()
226			return Effect{}
227		}
228		// Full fixed workspace, stacked edge column: one hop to the
229		// neighbor monitor, if any; focus follows the window.
230		i := c.neighbor(dir)
231		if i < 0 {
232			return Effect{}
233		}
234		id, ok := from.mon.Current().takeWindow()
235		if !ok {
236			return Effect{}
237		}
238		c.focusScreen = i
239		c.cur().mon.Current().expelTo(id, dir)
240		from.mon.normalize()
241		c.cur().mon.normalize()
242		return Effect{}
243	case ActionMoveColumnLeft, ActionMoveColumnRight:
244		// Past the edge the column moves to the neighbor screen, on the
245		// side facing this one, and focus follows it.
246		w := c.cur().mon.Current()
247		dir = 1
248		if a == ActionMoveColumnLeft {
249			dir = -1
250		}
251		edge := (dir < 0 && w.Focus == 0) || (dir > 0 && w.Focus == len(w.Columns)-1)
252		if i := c.neighbor(dir); edge && i >= 0 {
253			col, ok := w.takeColumn()
254			if !ok {
255				return Effect{}
256			}
257			from := c.cur()
258			c.focusScreen = i
259			to := c.cur().mon.Current()
260			at := 0
261			if dir < 0 {
262				at = len(to.Columns)
263			}
264			to.receive(col, at)
265			from.mon.normalize()
266			c.cur().mon.normalize()
267			return Effect{}
268		}
269	}
270	return c.cur().mon.Apply(a)
271}
272
273// Apply runs a bind action on the monitor.
274func (m *Monitor) Apply(a Action) Effect {
275	if e, ok := m.overviewAction(a); ok {
276		return e
277	}
278	if movesToWorkspace(a) {
279		if i, column, ok := m.moveDest(a); ok {
280			m.MoveToWorkspace(i, column)
281		}
282		return Effect{}
283	}
284	if n, _, ok := WorkspaceArg(a); ok {
285		before := m.Current()
286		m.FocusNumber(n)
287		if m.ov.open && m.Current() != before {
288			m.selectRow()
289		}
290		return Effect{}
291	}
292	if name, ok := NamedArg(a); ok {
293		before := m.Current()
294		m.ToggleNamed(name)
295		if m.ov.open && m.Current() != before {
296			m.selectRow()
297		}
298		return Effect{}
299	}
300	switch a {
301	case ActionFocusWindowUp, ActionFocusWindowDown:
302		// Past the column and any on-screen neighbor or demoted float,
303		// move to the next workspace.
304		dir := 1
305		if a == ActionFocusWindowUp {
306			dir = -1
307		}
308		// A hidden workspace is outside the vertical list.
309		if !m.Current().FocusWindow(dir) && m.shown == nil {
310			m.Focus(m.Active + dir)
311		}
312		return Effect{}
313	case ActionToggleFullscreen:
314		m.ToggleFullscreen()
315		return Effect{}
316	case ActionToggleWindowStash:
317		m.Current().ToggleWindowStash()
318		return Effect{}
319	case ActionToggleStashVisible:
320		m.Current().ToggleStashVisible()
321		return Effect{}
322	case ActionToggleOverview:
323		m.ToggleOverview()
324		return Effect{}
325	case ActionMaximizeColumn:
326		// A client that made itself fullscreen (Wine at monitor size) goes
327		// back to its column width; the next press maximizes the column.
328		if w := m.Current(); w.fullscreen != 0 {
329			if id, ok := w.Focused(); ok && id == w.fullscreen {
330				m.ToggleFullscreen()
331				return Effect{}
332			}
333		}
334	case ActionFocusWorkspaceUp, ActionFocusWorkspaceDown:
335		if m.shown == nil && a == ActionFocusWorkspaceUp {
336			m.Focus(m.Active - 1)
337		} else if m.shown == nil {
338			m.Focus(m.Active + 1)
339		}
340		return Effect{}
341	case ActionMoveWorkspaceUp:
342		m.MoveWorkspace(-1)
343		return Effect{}
344	case ActionMoveWorkspaceDown:
345		m.MoveWorkspace(1)
346		return Effect{}
347	}
348	return m.Current().Apply(a)
349}
350
351// movesToWorkspace reports a bind that moves a window to another workspace.
352func movesToWorkspace(a Action) bool {
353	if _, op, ok := WorkspaceArg(a); ok {
354		return op != FocusWorkspace
355	}
356	switch a {
357	case ActionMoveColumnToWorkspaceUp, ActionMoveColumnToWorkspaceDown,
358		ActionMoveWindowToWorkspaceUp, ActionMoveWindowToWorkspaceDown:
359		return true
360	}
361	return false
362}
363
364// moveDest resolves a move-to-workspace bind to the workspace index it
365// moves to and whether the whole column goes; ok is false when it has
366// nowhere to go. Up and down do nothing on a named workspace.
367func (m *Monitor) moveDest(a Action) (i int, column, ok bool) {
368	if n, op, isArg := WorkspaceArg(a); isArg {
369		return min(max(n-1, 0), len(m.Workspaces)-1), op == MoveColumnToWorkspace, true
370	}
371	if m.shown != nil {
372		return 0, false, false
373	}
374	switch a {
375	case ActionMoveColumnToWorkspaceUp, ActionMoveWindowToWorkspaceUp:
376		return m.Active - 1, a == ActionMoveColumnToWorkspaceUp, m.Active > 0
377	case ActionMoveColumnToWorkspaceDown, ActionMoveWindowToWorkspaceDown:
378		return min(m.Active+1, len(m.Workspaces)-1), a == ActionMoveColumnToWorkspaceDown, true
379	}
380	return 0, false, false
381}
382
383// Apply runs a bind action that only touches this workspace.
384func (w *Workspace) Apply(a Action) Effect {
385	if argv, ok := SpawnArgv(a); ok {
386		return Effect{Spawn: true, Argv: argv}
387	}
388	if w.freeAction(a) {
389		return Effect{}
390	}
391	if axis, pct, ok := ResizeArg(a); ok {
392		if axis == ResizeWidth {
393			w.ResizeColumn(pct)
394		} else {
395			w.ResizeRow(pct)
396		}
397		return Effect{}
398	}
399	switch a {
400	case ActionSpawnTerminal:
401		return Effect{Spawn: true}
402	case ActionFocusColumnLeft:
403		w.FocusColumn(-1)
404	case ActionFocusColumnRight:
405		w.FocusColumn(1)
406	case ActionFocusWindowUp:
407		w.FocusWindow(-1)
408	case ActionFocusWindowDown:
409		w.FocusWindow(1)
410	case ActionMoveColumnLeft:
411		w.MoveColumn(-1)
412	case ActionMoveColumnRight:
413		w.MoveColumn(1)
414	case ActionMoveWindowUp:
415		w.MoveWindow(-1)
416	case ActionMoveWindowDown:
417		w.MoveWindow(1)
418	case ActionCycleColumnWidth:
419		w.CycleWidth()
420	case ActionMaximizeColumn:
421		w.ToggleFullWidth()
422	case ActionToggleFullscreen:
423		w.ToggleFullscreen()
424	case ActionToggleWindowStash:
425		w.ToggleWindowStash()
426	case ActionToggleStashVisible:
427		w.ToggleStashVisible()
428	case ActionToggleFloating:
429		w.ToggleFloating()
430	case ActionCloseWindow:
431		id, _ := w.Focused()
432		return Effect{Close: id}
433	case ActionQuit:
434		return Effect{Quit: true}
435	}
436	return Effect{}
437}

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 25

Tags 10

↑↓ moveenter openesc close