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}