1package core
2
3import "slices"
4
5// Monitor owns an ordered list of workspaces for one output (ADR 011). Numbers
6// are positions: Cmd+N targets Workspaces[N-1]. An empty workspace always sits
7// below the last one, and an empty unnamed workspace is removed once left,
8// except the first. Output-wide settings apply to every workspace.
9//
10// Named workspaces come from config and are never removed while configured.
11// A hidden one is not numbered and not reachable by up/down: only its
12// `workspace <name>` bind shows it.
13type Monitor struct {
14 // Name is the connector; Key identifies the physical monitor (make,
15 // model and serial, else the connector). Workspaces remember the Key of
16 // their home monitor.
17 Name, Key string
18 Workspaces []*Workspace
19 Active int
20 hidden []*Workspace
21 shown *Workspace // hidden workspace on screen, nil for Workspaces[Active]
22 back *Workspace // where a named workspace toggle returns to
23 template Workspace
24 named []NamedWorkspace
25 // followMove shows the target workspace after a move to it.
26 followMove bool
27}
28
29// NamedWorkspace configures a named workspace. Zero MaxColumns and an empty
30// Overflow follow the monitor defaults.
31type NamedWorkspace struct {
32 Name string
33 // Monitor is the home monitor (connector or key); "" for the first one.
34 Monitor string
35 Hidden bool
36 MaxColumns int
37 Overflow Overflow
38}
39
40func NewMonitor() *Monitor { return newMonitor("", "") }
41
42func newMonitor(name, key string) *Monitor {
43 m := &Monitor{Name: name, Key: key}
44 m.normalize()
45 return m
46}
47
48// matches reports whether home designates this monitor, by key or connector.
49func (m *Monitor) matches(home string) bool {
50 return home != "" && (home == m.Key || home == m.Name)
51}
52
53// all lists every workspace, numbered then hidden.
54func (m *Monitor) all() []*Workspace {
55 return append(append([]*Workspace(nil), m.Workspaces...), m.hidden...)
56}
57
58// isHidden reports whether w is in the hidden list.
59func (m *Monitor) isHidden(w *Workspace) bool { return indexOf(m.hidden, w) >= 0 }
60
61// take removes w from the monitor. The monitor keeps a workspace on screen:
62// the one at the same position, or the numbered active one.
63func (m *Monitor) take(w *Workspace) {
64 if m.back == w {
65 m.back = nil
66 }
67 if i := indexOf(m.hidden, w); i >= 0 {
68 m.hidden = append(m.hidden[:i], m.hidden[i+1:]...)
69 if m.shown == w {
70 m.shown = nil
71 }
72 m.normalize()
73 return
74 }
75 if i := indexOf(m.Workspaces, w); i >= 0 {
76 m.removeNumbered(i)
77 m.normalize()
78 }
79}
80
81// adopt adds a workspace from another monitor: hidden, or numbered at pos
82// (clamped above the trailing empty workspace). It takes this monitor's
83// output-wide settings and does not change what is on screen.
84func (m *Monitor) adopt(w *Workspace, hidden bool, pos int) {
85 w.presets = append([]Width(nil), m.template.presets...)
86 w.Gaps = m.template.Gaps
87 w.border = m.template.border
88 w.SetOutput(m.template.Output.W, m.template.Output.H)
89 w.SetUsable(m.template.Usable)
90 if hidden {
91 m.hidden = append(m.hidden, w)
92 } else {
93 // normalize keeps a trailing empty workspace: stay above it.
94 at := min(max(pos, 0), len(m.Workspaces)-1)
95 m.Workspaces = slices.Insert(m.Workspaces, at, w)
96 if at <= m.Active {
97 m.Active++
98 }
99 }
100 m.applyNamed()
101 m.normalize()
102}
103
104// useSpecs sets the overrides of every named workspace, including those
105// that live on other monitors and may move here.
106func (m *Monitor) useSpecs(all []NamedWorkspace) {
107 m.named = append([]NamedWorkspace(nil), all...)
108 m.applyNamed()
109}
110
111// Current is the workspace on screen.
112func (m *Monitor) Current() *Workspace {
113 if m.shown != nil {
114 return m.shown
115 }
116 return m.Workspaces[m.Active]
117}
118
119func (m *Monitor) newWorkspace() *Workspace {
120 w := m.template
121 w.presets = append([]Width(nil), m.template.presets...)
122 w.Columns, w.Floats, w.floatFocus, w.home, w.origin, w.back = nil, nil, false, "", nil, origPlace{}
123 return &w
124}
125
126// normalize keeps one trailing empty unnamed workspace and drops other empty
127// unnamed ones that are neither the first nor active. The active workspace
128// never changes.
129func (m *Monitor) normalize() {
130 m.fullscreenHome()
131 spare := func(i int) bool { return m.Workspaces[i].empty() && m.Workspaces[i].Name == "" }
132 for i := len(m.Workspaces) - 2; i >= 1; i-- {
133 if spare(i) && i != m.Active {
134 m.Workspaces = append(m.Workspaces[:i], m.Workspaces[i+1:]...)
135 if i < m.Active {
136 m.Active--
137 }
138 }
139 }
140 for n := len(m.Workspaces); n >= 2 && spare(n-1) && spare(n-2) && m.Active != n-1; n-- {
141 m.Workspaces = m.Workspaces[:n-1]
142 }
143 if n := len(m.Workspaces); n == 0 || !spare(n-1) {
144 m.Workspaces = append(m.Workspaces, m.newWorkspace())
145 }
146}
147
148// Focus switches to the numbered workspace at index i, clamped to the list.
149func (m *Monitor) Focus(i int) {
150 i = min(max(i, 0), len(m.Workspaces)-1)
151 m.shown = nil
152 m.Active = i
153 m.normalize()
154}
155
156// FocusNumber switches to workspace n (1-based); past the end means the last.
157func (m *Monitor) FocusNumber(n int) { m.Focus(n - 1) }
158
159// show puts w on screen, numbered or hidden.
160func (m *Monitor) show(w *Workspace) {
161 if i := indexOf(m.Workspaces, w); i >= 0 {
162 m.Focus(i)
163 return
164 }
165 m.shown = w
166 m.normalize()
167}
168
169// ToggleNamed shows the named workspace, or returns to the previous one when
170// it is already on screen. Unknown names do nothing.
171func (m *Monitor) ToggleNamed(name string) {
172 target := m.byName(name)
173 switch cur := m.Current(); {
174 case target == nil:
175 case target != cur:
176 m.back = cur
177 m.show(target)
178 case m.back != nil && m.has(m.back):
179 back := m.back
180 m.back = nil
181 m.show(back)
182 case m.shown != nil:
183 m.back = nil
184 m.shown = nil
185 m.normalize()
186 }
187}
188
189// byName returns the configured workspace with that name, or nil.
190func (m *Monitor) byName(name string) *Workspace {
191 for _, w := range append(append([]*Workspace(nil), m.Workspaces...), m.hidden...) {
192 if w.Name == name {
193 return w
194 }
195 }
196 return nil
197}
198
199func indexOf(list []*Workspace, w *Workspace) int {
200 for i, v := range list {
201 if v == w {
202 return i
203 }
204 }
205 return -1
206}
207
208// has reports whether w still belongs to the monitor.
209func (m *Monitor) has(w *Workspace) bool {
210 return indexOf(m.Workspaces, w) >= 0 || indexOf(m.hidden, w) >= 0
211}
212
213// find returns the workspace holding the window; i is -1 for hidden ones.
214func (m *Monitor) find(id WindowID) (*Workspace, int) {
215 for i, w := range m.Workspaces {
216 if w.has(id) {
217 return w, i
218 }
219 }
220 for _, w := range m.hidden {
221 if w.has(id) {
222 return w, -1
223 }
224 }
225 return nil, -1
226}
227
228// AddWindow places a new window on the workspace on screen. On a
229// fullscreen workspace it floats, above the fullscreen window: a tiled one
230// would be hidden behind it (fixed overflow) while it has the focus.
231func (m *Monitor) AddWindow(id WindowID) {
232 if w, _ := m.find(id); w != nil {
233 return
234 }
235 if cur := m.Current(); cur.origin != nil {
236 cur.joinFullscreen(id)
237 m.normalize()
238 return
239 }
240 m.Current().AddWindow(id)
241 m.normalize()
242}
243
244// AddFloating adds a floating window to the active workspace.
245func (m *Monitor) AddFloating(id WindowID, width, height int) {
246 if w, _ := m.find(id); w != nil {
247 return
248 }
249 m.Current().AddFloating(id, width, height)
250 m.normalize()
251}
252
253// RemoveWindow drops the window wherever it is; focus stays on the active
254// workspace, except that closing a fullscreen workspace goes back to its origin.
255func (m *Monitor) RemoveWindow(id WindowID) {
256 if w, _ := m.find(id); w != nil {
257 if w.origin != nil && id == w.back.id && m.has(w.origin) {
258 // Its dialogs go home with the view.
259 origin, back := w.origin, w.back
260 w.RemoveWindow(id)
261 w.origin, w.back = nil, origPlace{}
262 m.foldInto(w, origin, back.col, false, back.tiled)
263 } else {
264 w.RemoveWindow(id)
265 }
266 m.normalize()
267 }
268}
269
270// SetFullscreen applies a client request. Under fixed overflow the window
271// gets its own workspace, as with the bind. A client request never moves
272// the user (ADR 011): the view follows only a window that is focused and
273// on screen, and a window coming back does not take the focus.
274func (m *Monitor) SetFullscreen(id WindowID, on bool) {
275 w, _ := m.find(id)
276 switch {
277 case w == nil:
278 case w.origin != nil && id != w.back.id:
279 // A dialog of a fullscreen workspace already floats above it; its
280 // own fullscreen would take the workspace from the window it holds.
281 case on && m.ownWorkspace(w, id):
282 focused, _ := w.Focused()
283 m.enterFullscreen(w, id, w == m.Current() && focused == id)
284 case !on && w.origin != nil && w.fullscreen == id:
285 // The user was on that window: they stay on it, back home.
286 m.leaveFullscreen(w, m.Current() == w)
287 default:
288 w.SetFullscreen(id, on)
289 }
290 m.normalize()
291}
292
293// ToggleFullscreen is the bind. Under fixed overflow the focused window
294// moves to a new workspace below the current one, so workspace up/down
295// still reach the others; toggling again sends it back where it was.
296func (m *Monitor) ToggleFullscreen() {
297 w := m.Current()
298 id, ok := w.Focused()
299 switch {
300 case !ok:
301 case w.origin != nil && w.fullscreen == w.back.id:
302 // From its window or one of its dialogs.
303 m.leaveFullscreen(w, true)
304 case m.ownWorkspace(w, id):
305 m.enterFullscreen(w, id, true)
306 default:
307 w.ToggleFullscreen()
308 }
309 m.normalize()
310}
311
312// ownWorkspace reports whether going fullscreen moves the window to its own
313// workspace: fixed overflow hides every other window behind a fullscreen
314// one and never scrolls, so it would leave the user stuck. Not needed when
315// the window is alone, or already has its own workspace.
316func (m *Monitor) ownWorkspace(w *Workspace, id WindowID) bool {
317 return w.Overflow == OverflowFixed && w.origin == nil && w.fullscreen != id && len(w.windows()) > 1 && m.has(w)
318}
319
320// enterFullscreen moves window id from w to a new workspace just below the
321// numbered one on screen, fullscreen, and shows it when show is set. It
322// remembers the window's place in w to return there.
323func (m *Monitor) enterFullscreen(w *Workspace, id WindowID, show bool) {
324 fs := m.newWorkspace()
325 fs.Overflow, fs.MaxColumns = w.Overflow, w.MaxColumns
326 fs.origin = w
327 fs.back.id = id
328 if f := w.floatIndex(id); f >= 0 {
329 fl := w.Floats[f]
330 fs.back.float = &fl
331 w.RemoveWindow(id)
332 fs.AddFloating(id, fl.W, fl.H)
333 } else {
334 col := slices.IndexFunc(w.Columns, func(c Column) bool { return slices.Contains(c.Windows, id) })
335 c := w.Columns[col]
336 fs.back = origPlace{id: id, col: col, row: slices.Index(c.Windows, id), slot: c.Slot, width: c.Width}
337 for _, v := range c.Windows {
338 if v != id {
339 fs.back.stacked = append(fs.back.stacked, v)
340 }
341 }
342 w.RemoveWindow(id)
343 fs.AddWindow(id)
344 fs.Columns[0].Width = c.Width
345 }
346 fs.SetFullscreen(id, true)
347 at := m.Active + 1
348 if i := indexOf(m.Workspaces, w); i >= 0 {
349 at = i + 1
350 }
351 m.Workspaces = slices.Insert(m.Workspaces, at, fs)
352 if at <= m.Active {
353 m.Active++
354 }
355 if show {
356 m.show(fs)
357 }
358}
359
360// leaveFullscreen sends the window of fs back to its place in the origin,
361// with the windows opened there meanwhile (its dialogs) just after it, and
362// drops fs; the view goes along when fs was on screen. focus is set when
363// the user was on the window: it gets the focus back there.
364func (m *Monitor) leaveFullscreen(fs *Workspace, focus bool) {
365 if m.fullscreenHome(); fs.origin == nil {
366 fs.SetFullscreen(fs.fullscreen, false)
367 return
368 }
369 origin, back, id := fs.origin, fs.back, fs.back.id
370 fs.origin, fs.back = nil, origPlace{}
371 fs.RemoveWindow(id)
372 // A closure: back.col is where the window lands, known after the switch.
373 defer func() { m.foldInto(fs, origin, back.col+1, focus, back.tiled) }()
374 switch prev := origin.Focus; {
375 case back.float != nil && focus:
376 origin.AddFloating(id, back.float.W, back.float.H)
377 case back.float != nil:
378 // At the bottom: the top float, maybe focused, stays on top.
379 origin.Floats = slices.Insert(origin.Floats, 0, Float{ID: id, W: back.float.W, H: back.float.H})
380 case slices.ContainsFunc(origin.Columns, back.holdsStack):
381 back.col = slices.IndexFunc(origin.Columns, back.holdsStack)
382 c := &origin.Columns[back.col]
383 // Below the windows that were above it and are still there.
384 row := 0
385 for _, v := range back.stacked[:back.row] {
386 if slices.Contains(c.Windows, v) {
387 row++
388 }
389 }
390 row = min(row, len(c.Windows))
391 c.Windows = slices.Insert(c.Windows, row, id)
392 if focus {
393 origin.Focus, c.Focus, origin.floatFocus = back.col, row, false
394 } else if row <= c.Focus && len(c.Windows) > 1 {
395 c.Focus++
396 }
397 default:
398 at := min(back.col, len(origin.Columns))
399 origin.insertColumn(at, Column{Windows: []WindowID{id}, Width: back.width, Slot: back.slot})
400 if focus {
401 origin.floatFocus = false
402 } else if len(origin.Columns) > 1 {
403 origin.Focus = prev
404 if at <= prev {
405 origin.Focus++
406 }
407 }
408 }
409}
410
411// foldInto moves what is left in fs (windows opened while fullscreen: its
412// dialogs) to origin, tiled ones from column at, floating ones on top, and
413// drops fs. The view goes along when fs was on screen.
414func (m *Monitor) foldInto(fs, origin *Workspace, at int, focus bool, tiled []WindowID) {
415 shown := m.Current() == fs
416 at = min(max(at, 0), len(origin.Columns))
417 for _, c := range slices.Backward(fs.Columns) {
418 origin.Columns = slices.Insert(origin.Columns, at, Column{Windows: c.Windows, Width: c.Width, Focus: c.Focus})
419 if at <= origin.Focus && len(origin.Columns) > 1 {
420 origin.Focus++
421 }
422 }
423 var floats []Float
424 for _, f := range fs.Floats {
425 if !slices.Contains(tiled, f.ID) {
426 floats = append(floats, f)
427 continue
428 }
429 origin.Columns = slices.Insert(origin.Columns, at, Column{Windows: []WindowID{f.ID}})
430 if at <= origin.Focus && len(origin.Columns) > 1 {
431 origin.Focus++
432 }
433 at++
434 }
435 if focus || shown {
436 origin.Floats = append(origin.Floats, floats...)
437 origin.floatFocus = origin.floatFocus || len(floats) > 0 && fs.floatFocus
438 } else {
439 // The user is elsewhere: below, the focused float keeps the focus.
440 origin.Floats = append(floats, origin.Floats...)
441 }
442 fs.Columns, fs.Floats = nil, nil
443 origin.scroll()
444 m.take(fs)
445 if shown {
446 m.show(origin)
447 }
448}
449
450// awayInSlot reports whether id is away from slot n of w, fullscreen in a
451// workspace that returns it there.
452func (m *Monitor) awayInSlot(w *Workspace, id WindowID, n int) bool {
453 for _, fs := range m.all() {
454 if fs.origin == w && fs.back.id == id && fs.back.slot == n {
455 return true
456 }
457 }
458 return false
459}
460
461// holdsStack reports whether c holds a window of the stack left behind.
462func (p origPlace) holdsStack(c Column) bool {
463 return slices.ContainsFunc(c.Windows, func(v WindowID) bool { return slices.Contains(p.stacked, v) })
464}
465
466// fullscreenHome keeps each fullscreen workspace linked to its origin only
467// while its window is still there and fullscreen, and the origin is still
468// on this monitor. Otherwise (the window moved out or activated away from
469// fullscreen, the origin gone to another monitor) it is a normal workspace.
470// Other windows opened there (a dialog) stay when the window returns.
471func (m *Monitor) fullscreenHome() {
472 for _, w := range m.all() {
473 if w.origin == nil {
474 continue
475 }
476 if !m.has(w.origin) || w.fullscreen != w.back.id || !w.has(w.back.id) {
477 w.origin, w.back = nil, origPlace{}
478 }
479 }
480}
481
482// MoveToWorkspace moves the focused column (or only the focused window) to
483// numbered workspace index i. A floating window always moves alone. Focus
484// stays here unless followMove is set (ADR 017).
485func (m *Monitor) MoveToWorkspace(i int, column bool) {
486 i = min(max(i, 0), len(m.Workspaces)-1)
487 cur, to := m.Current(), m.Workspaces[i]
488 id, ok := cur.Focused()
489 if !ok || to == cur {
490 return
491 }
492 if f := cur.floatIndex(id); f >= 0 {
493 fl := cur.Floats[f]
494 cur.RemoveWindow(id)
495 to.AddFloating(id, fl.W, fl.H)
496 to.FocusID(id)
497 } else {
498 col := Column{Windows: []WindowID{id}}
499 ok := true
500 if column {
501 col, ok = cur.takeColumn()
502 } else {
503 _, ok = cur.takeWindow()
504 }
505 if !ok {
506 return
507 }
508 to.receive(col, -1)
509 }
510 if m.followMove {
511 m.Active = i
512 m.shown = nil
513 }
514 m.normalize()
515}
516
517// receive adds a column moved in from another workspace and focuses it: at
518// index at, or where a new window goes when at < 0. It stays visible: on a
519// fixed-overflow fullscreen workspace its windows float above, and an
520// in-place fullscreen is left.
521func (w *Workspace) receive(col Column, at int) {
522 id := col.Windows[col.Focus]
523 switch {
524 case w.origin != nil:
525 for _, v := range col.Windows {
526 w.joinFullscreen(v)
527 }
528 case at < 0:
529 w.addColumn(col)
530 default:
531 w.insertColumn(at, col)
532 }
533 w.Activate(id)
534}
535
536// joinFullscreen adds a tiled window to a fullscreen workspace fs: it floats
537// above the fullscreen window, which would hide it (fixed overflow), and
538// tiles again once home.
539func (fs *Workspace) joinFullscreen(id WindowID) {
540 fs.AddFloating(id, 0, 0)
541 fs.back.tiled = append(fs.back.tiled, id)
542}
543
544func (m *Monitor) Focused() (WindowID, bool) { return m.Current().Focused() }
545
546// Layout places every window: the workspace on screen laid out, others hidden.
547func (m *Monitor) Layout() []Placement {
548 var result []Placement
549 cur := m.Current()
550 for _, w := range append(append([]*Workspace(nil), m.Workspaces...), m.hidden...) {
551 if w == cur {
552 result = append(result, w.Layout()...)
553 continue
554 }
555 for _, id := range w.windows() {
556 result = append(result, Placement{ID: id, Hidden: true})
557 }
558 }
559 return result
560}
561
562func (m *Monitor) Output() Rect { return m.template.Output }
563
564func (m *Monitor) each(f func(*Workspace)) {
565 f(&m.template)
566 for _, w := range m.Workspaces {
567 f(w)
568 }
569 for _, w := range m.hidden {
570 f(w)
571 }
572}
573func (m *Monitor) SetOutput(width, height int) {
574 m.each(func(w *Workspace) { w.SetOutput(width, height) })
575}
576func (m *Monitor) SetUsable(r Rect) { m.each(func(w *Workspace) { w.SetUsable(r) }) }
577func (m *Monitor) SetGaps(g int) { m.each(func(w *Workspace) { w.SetGaps(g) }) }
578func (m *Monitor) SetBorder(b int) { m.each(func(w *Workspace) { w.border = max(b, 0) }) }
579func (m *Monitor) SetPresets(v []Width) { m.each(func(w *Workspace) { w.SetPresets(v) }) }
580
581// SetFollowMove makes moves to another workspace show it (focus.follow-move).
582func (m *Monitor) SetFollowMove(on bool) { m.followMove = on }
583
584// SetMaxColumns sets the default; named workspaces may override it.
585func (m *Monitor) SetMaxColumns(n int) {
586 m.template.MaxColumns = n
587 m.applyNamed()
588}
589
590// SetOverflow sets the default; named workspaces may override it.
591func (m *Monitor) SetOverflow(o Overflow) {
592 m.template.Overflow = o
593 m.applyNamed()
594}
595
596// applyNamed gives every workspace the defaults or its named overrides.
597func (m *Monitor) applyNamed() {
598 specs := map[string]NamedWorkspace{}
599 for _, s := range m.named {
600 specs[s.Name] = s
601 }
602 m.each(func(w *Workspace) {
603 s := specs[w.Name]
604 w.Overflow = m.template.Overflow
605 if s.Overflow != "" {
606 w.Overflow = s.Overflow
607 }
608 w.SetMaxColumns(m.template.MaxColumns)
609 if s.MaxColumns > 0 {
610 w.SetMaxColumns(s.MaxColumns)
611 }
612 })
613}
614
615// SetNamed applies the named workspaces from config. New numbered ones are
616// inserted above the trailing empty workspace (never before the first one).
617// A workspace dropped from config becomes a normal one; a dropped hidden one
618// hands its windows to the active numbered workspace. The workspace on
619// screen stays on screen.
620func (m *Monitor) SetNamed(specs []NamedWorkspace) {
621 cur := m.Current()
622 want := map[string]NamedWorkspace{}
623 for _, s := range specs {
624 want[s.Name] = s
625 }
626 byName := map[string]*Workspace{}
627 for _, w := range m.Workspaces {
628 if _, ok := want[w.Name]; w.Name != "" && !ok {
629 w.Name = ""
630 }
631 if w.Name != "" {
632 byName[w.Name] = w
633 }
634 }
635 var hidden []*Workspace
636 for _, w := range m.hidden {
637 if _, ok := want[w.Name]; ok {
638 byName[w.Name] = w
639 hidden = append(hidden, w)
640 continue
641 }
642 if w == cur {
643 cur = nil
644 }
645 for _, c := range w.Columns {
646 for _, id := range c.Windows {
647 m.Workspaces[m.Active].AddWindow(id)
648 }
649 }
650 for _, f := range w.Floats {
651 m.Workspaces[m.Active].AddFloating(f.ID, f.W, f.H)
652 }
653 }
654 m.hidden = hidden
655 m.named = append([]NamedWorkspace(nil), specs...)
656 var nextHidden []*Workspace
657 for _, s := range specs {
658 w := byName[s.Name]
659 if w == nil {
660 w = m.newWorkspace()
661 w.Name = s.Name
662 }
663 i := indexOf(m.Workspaces, w)
664 switch {
665 case s.Hidden:
666 if i >= 0 {
667 m.removeNumbered(i)
668 }
669 nextHidden = append(nextHidden, w)
670 case i < 0:
671 at := max(len(m.Workspaces)-1, 1)
672 m.Workspaces = append(m.Workspaces[:at], append([]*Workspace{w}, m.Workspaces[at:]...)...)
673 if at <= m.Active {
674 m.Active++
675 }
676 }
677 }
678 m.hidden = nextHidden
679 if m.back != nil && !m.has(m.back) {
680 m.back = nil
681 }
682 m.shown = nil
683 switch {
684 case cur == nil:
685 case indexOf(m.hidden, cur) >= 0:
686 m.shown = cur
687 case indexOf(m.Workspaces, cur) >= 0:
688 m.Active = indexOf(m.Workspaces, cur)
689 }
690 m.applyNamed()
691 m.normalize()
692}
693
694// removeNumbered drops Workspaces[i] and keeps Active on the same workspace
695// when possible.
696func (m *Monitor) removeNumbered(i int) {
697 m.Workspaces = append(m.Workspaces[:i], m.Workspaces[i+1:]...)
698 if i < m.Active || m.Active >= len(m.Workspaces) {
699 m.Active = max(m.Active-1, 0)
700 }
701 if len(m.Workspaces) == 0 {
702 m.Workspaces = append(m.Workspaces, m.newWorkspace())
703 }
704}