v0.1.0 d9218d6

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	ActionToggleFullscreen Action = "toggle-fullscreen"
20	ActionCloseWindow      Action = "close-window"
21	ActionQuit             Action = "quit"
22	// Workspaces stack vertically; up/down stop at the ends.
23	ActionFocusWorkspaceUp   Action = "focus-workspace-up"
24	ActionFocusWorkspaceDown Action = "focus-workspace-down"
25	// Move the focused column, or only the focused window, one workspace
26	// up or down.
27	ActionMoveColumnToWorkspaceUp   Action = "move-column-to-workspace-up"
28	ActionMoveColumnToWorkspaceDown Action = "move-column-to-workspace-down"
29	ActionMoveWindowToWorkspaceUp   Action = "move-window-to-workspace-up"
30	ActionMoveWindowToWorkspaceDown Action = "move-window-to-workspace-down"
31	// Monitors are ordered left to right (ADR 011).
32	ActionFocusMonitorLeft   Action = "focus-monitor-left"
33	ActionFocusMonitorRight  Action = "focus-monitor-right"
34	ActionMoveWorkspaceLeft  Action = "move-workspace-to-monitor-left"
35	ActionMoveWorkspaceRight Action = "move-workspace-to-monitor-right"
36	// Scale steps through the clean scales of the output (see CleanScales).
37	ActionScaleUp   Action = "scale-up"
38	ActionScaleDown Action = "scale-down"
39	// Consume or expel the focused window (ADR 016).
40	ActionConsumeOrExpelLeft  Action = "consume-or-expel-window-left"
41	ActionConsumeOrExpelRight Action = "consume-or-expel-window-right"
42)
43
44// WorkspaceOp is what a numbered workspace action does.
45type WorkspaceOp int
46
47const (
48	FocusWorkspace WorkspaceOp = iota
49	MoveColumnToWorkspace
50	MoveWindowToWorkspace
51)
52
53var workspacePrefixes = [...]string{
54	FocusWorkspace:        "focus-workspace ",
55	MoveColumnToWorkspace: "move-column-to-workspace ",
56	MoveWindowToWorkspace: "move-window-to-workspace ",
57}
58
59// WorkspaceArg parses "focus-workspace N", "move-column-to-workspace N" and
60// "move-window-to-workspace N" (N from 1).
61func WorkspaceArg(a Action) (n int, op WorkspaceOp, ok bool) {
62	for i, prefix := range workspacePrefixes {
63		rest, found := strings.CutPrefix(string(a), prefix)
64		if !found {
65			continue
66		}
67		n, err := strconv.Atoi(strings.TrimSpace(rest))
68		if err != nil || n < 1 || n > 99 {
69			return 0, 0, false
70		}
71		return n, WorkspaceOp(i), true
72	}
73	return 0, 0, false
74}
75
76// namedPrefix toggles a named workspace: "workspace dev".
77const namedPrefix = "workspace "
78
79// NamedArg returns the name of a "workspace <name>" action.
80func NamedArg(a Action) (string, bool) {
81	rest, ok := strings.CutPrefix(string(a), namedPrefix)
82	name := strings.TrimSpace(rest)
83	return name, ok && name != "" && !strings.ContainsAny(name, " \t")
84}
85
86// spawnPrefix starts a bind action that runs a command: "spawn fuzzel --flag".
87// Arguments are split on whitespace; there is no shell (use "spawn sh -c ...").
88const spawnPrefix = "spawn "
89
90// SpawnArgv returns the command of a "spawn <cmd>" action.
91func SpawnArgv(a Action) ([]string, bool) {
92	rest, ok := strings.CutPrefix(string(a), spawnPrefix)
93	argv := strings.Fields(rest)
94	return argv, ok && len(argv) > 0
95}
96
97type Effect struct {
98	Spawn bool
99	Argv  []string // command to run; nil means the configured terminal
100	Close WindowID
101	Quit  bool
102}
103
104// applyAction runs a bind action. Monitor actions change the focused
105// screen; column focus past the edge column moves to the neighbor screen;
106// everything else applies to the focused screen's monitor.
107func (c *Core) applyAction(a Action) Effect {
108	dir := 0
109	switch a {
110	case ActionFocusMonitorLeft, ActionMoveWorkspaceLeft:
111		dir = -1
112	case ActionFocusMonitorRight, ActionMoveWorkspaceRight:
113		dir = 1
114	}
115	switch a {
116	case ActionFocusMonitorLeft, ActionFocusMonitorRight:
117		if i := c.neighbor(dir); i >= 0 {
118			c.focusScreen = i
119		}
120		return Effect{}
121	case ActionMoveWorkspaceLeft, ActionMoveWorkspaceRight:
122		c.moveWorkspace(dir)
123		return Effect{}
124	case ActionFocusColumnLeft, ActionFocusColumnRight:
125		w := c.cur().mon.Current()
126		edge := len(w.Columns) == 0 || (a == ActionFocusColumnLeft && w.Focus == 0) || (a == ActionFocusColumnRight && w.Focus == len(w.Columns)-1)
127		if a == ActionFocusColumnLeft {
128			dir = -1
129		} else {
130			dir = 1
131		}
132		if i := c.neighbor(dir); edge && i >= 0 {
133			c.focusScreen = i
134			return Effect{}
135		}
136	case ActionConsumeOrExpelLeft, ActionConsumeOrExpelRight:
137		dir = 1
138		if a == ActionConsumeOrExpelLeft {
139			dir = -1
140		}
141		from := c.cur()
142		if from.mon.Current().ConsumeOrExpel(dir) {
143			from.mon.normalize()
144			return Effect{}
145		}
146		// Full fixed workspace, stacked edge column: one hop to the
147		// neighbor monitor, if any; focus follows the window.
148		i := c.neighbor(dir)
149		if i < 0 {
150			return Effect{}
151		}
152		id, ok := from.mon.Current().takeWindow()
153		if !ok {
154			return Effect{}
155		}
156		c.focusScreen = i
157		c.cur().mon.Current().expelTo(id, dir)
158		from.mon.normalize()
159		c.cur().mon.normalize()
160		return Effect{}
161	case ActionMoveColumnLeft, ActionMoveColumnRight:
162		// Past the edge the column moves to the neighbor screen, on the
163		// side facing this one, and focus follows it.
164		w := c.cur().mon.Current()
165		dir = 1
166		if a == ActionMoveColumnLeft {
167			dir = -1
168		}
169		edge := (dir < 0 && w.Focus == 0) || (dir > 0 && w.Focus == len(w.Columns)-1)
170		if i := c.neighbor(dir); edge && i >= 0 {
171			// The last column stays: a workspace on screen is never
172			// empty, and moving it would only spawn a new terminal.
173			if len(w.Columns) < 2 && c.keepsTerminal() {
174				return Effect{}
175			}
176			col, ok := w.takeColumn()
177			if !ok {
178				return Effect{}
179			}
180			from := c.cur()
181			c.focusScreen = i
182			to := c.cur().mon.Current()
183			at := 0
184			if dir < 0 {
185				at = len(to.Columns)
186			}
187			to.receive(col, at)
188			from.mon.normalize()
189			c.cur().mon.normalize()
190			return Effect{}
191		}
192	}
193	return c.cur().mon.Apply(a)
194}
195
196// Apply runs a bind action on the monitor.
197func (m *Monitor) Apply(a Action) Effect {
198	if n, op, ok := WorkspaceArg(a); ok {
199		if op == FocusWorkspace {
200			m.FocusNumber(n)
201		} else {
202			m.MoveToWorkspace(n-1, op == MoveColumnToWorkspace)
203		}
204		return Effect{}
205	}
206	if name, ok := NamedArg(a); ok {
207		m.ToggleNamed(name)
208		return Effect{}
209	}
210	switch a {
211	case ActionFocusWindowUp, ActionFocusWindowDown:
212		// Past the top or bottom window of the column, move to the next workspace.
213		dir := 1
214		if a == ActionFocusWindowUp {
215			dir = -1
216		}
217		// A hidden workspace is outside the vertical list.
218		if !m.Current().FocusWindow(dir) && m.shown == nil {
219			m.Focus(m.Active + dir)
220		}
221		return Effect{}
222	case ActionToggleFullscreen:
223		m.ToggleFullscreen()
224		return Effect{}
225	case ActionFocusWorkspaceUp, ActionFocusWorkspaceDown:
226		if m.shown == nil && a == ActionFocusWorkspaceUp {
227			m.Focus(m.Active - 1)
228		} else if m.shown == nil {
229			m.Focus(m.Active + 1)
230		}
231		return Effect{}
232	case ActionMoveColumnToWorkspaceUp, ActionMoveWindowToWorkspaceUp:
233		if m.shown == nil && m.Active > 0 {
234			m.MoveToWorkspace(m.Active-1, a == ActionMoveColumnToWorkspaceUp)
235		}
236		return Effect{}
237	case ActionMoveColumnToWorkspaceDown, ActionMoveWindowToWorkspaceDown:
238		if m.shown == nil {
239			m.MoveToWorkspace(m.Active+1, a == ActionMoveColumnToWorkspaceDown)
240		}
241		return Effect{}
242	}
243	return m.Current().Apply(a)
244}
245
246// Apply runs a bind action that only touches this workspace.
247func (w *Workspace) Apply(a Action) Effect {
248	if argv, ok := SpawnArgv(a); ok {
249		return Effect{Spawn: true, Argv: argv}
250	}
251	switch a {
252	case ActionSpawnTerminal:
253		return Effect{Spawn: true}
254	case ActionFocusColumnLeft:
255		w.FocusColumn(-1)
256	case ActionFocusColumnRight:
257		w.FocusColumn(1)
258	case ActionFocusWindowUp:
259		w.FocusWindow(-1)
260	case ActionFocusWindowDown:
261		w.FocusWindow(1)
262	case ActionMoveColumnLeft:
263		w.MoveColumn(-1)
264	case ActionMoveColumnRight:
265		w.MoveColumn(1)
266	case ActionCycleColumnWidth:
267		w.CycleWidth()
268	case ActionToggleFullscreen:
269		// In place only: binds go through Monitor.Apply, which gives a
270		// fixed-overflow fullscreen its own workspace.
271		w.ToggleFullscreen()
272	case ActionCloseWindow:
273		id, _ := w.Focused()
274		return Effect{Close: id}
275	case ActionQuit:
276		return Effect{Quit: true}
277	}
278	return Effect{}
279}

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 23

Tags 10

↑↓ moveenter openesc close