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}