v0.4.0 b6be3a8

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

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