action.go RawHistoryFolder

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

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