fix(workspaces): fullscreen, presets and insertion order in fixed overflow; guard hidden moves
Fixed overflow: fullscreen covers the output and hides the other windows, presets are ignored, new windows always go last, spiral windows keep borders. move-to-workspace-up/down do nothing on a hidden workspace. Config warns about binds to undeclared workspaces and hidden workspaces without a bind; the monitor key is rejected until multi-monitor support.
- commit
- 96f4e50e9fdcfaf36ce50ba51801546c46fd38bc
- author
- brice · · 2026-09-24 16:53 UTC
- parent
- c703b25
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
8 files · +197 -37
M internal/adapters/config/config.go +33 -5
| @@ -251,6 +251,7 @@ func parse(r io.Reader) (ports.Config, map[string]string, []Warning, error) { | |||
| 251 | 251 | if err := scanner.Err(); err != nil { | |
| 252 | 252 | return c, raw, warnings, err | |
| 253 | 253 | } | |
| 254 | + | warnings = append(warnings, checkWorkspaceBinds(c, seen)...) | |
| 254 | 255 | // A user bind on a digit (cmd+1, even "none") replaces the default bound | |
| 255 | 256 | // to the same physical key (cmd+code:2), so older configs keep working. | |
| 256 | 257 | for combo := range c.Binds { | |
| @@ -432,6 +433,36 @@ func set(c *ports.Config, key, v string) error { | |||
| 432 | 433 | return nil | |
| 433 | 434 | } | |
| 434 | 435 | ||
| 436 | + | // checkWorkspaceBinds warns about a `workspace <name>` bind to an undeclared | |
| 437 | + | // workspace (it does nothing) and a hidden workspace no bind reaches (its | |
| 438 | + | // windows could only come back by editing the config). | |
| 439 | + | func checkWorkspaceBinds(c ports.Config, seen map[string]int) []Warning { | |
| 440 | + | declared := map[string]bool{} | |
| 441 | + | for _, w := range c.Workspaces { | |
| 442 | + | declared[w.Name] = true | |
| 443 | + | } | |
| 444 | + | bound := map[string]bool{} | |
| 445 | + | var warnings []Warning | |
| 446 | + | for combo, a := range c.Binds { | |
| 447 | + | name, ok := strings.CutPrefix(a, "workspace ") | |
| 448 | + | if !ok { | |
| 449 | + | continue | |
| 450 | + | } | |
| 451 | + | name = strings.TrimSpace(name) | |
| 452 | + | bound[name] = true | |
| 453 | + | if !declared[name] { | |
| 454 | + | warnings = append(warnings, Warning{Line: seen["bind."+combo], Msg: fmt.Sprintf("bind.%s: no workspace.%s.* declared", combo, name)}) | |
| 455 | + | } | |
| 456 | + | } | |
| 457 | + | for _, w := range c.Workspaces { | |
| 458 | + | if w.Hidden && !bound[w.Name] { | |
| 459 | + | warnings = append(warnings, Warning{Line: seen["workspace."+w.Name+".hidden"], Msg: fmt.Sprintf("workspace.%s.hidden: no bind shows it (add bind.<keys> = workspace %s)", w.Name, w.Name)}) | |
| 460 | + | } | |
| 461 | + | } | |
| 462 | + | sort.Slice(warnings, func(i, j int) bool { return warnings[i].Line < warnings[j].Line }) | |
| 463 | + | return warnings | |
| 464 | + | } | |
| 465 | + | ||
| 435 | 466 | var workspaceName = regexp.MustCompile(`^[A-Za-z0-9_-]+$`) | |
| 436 | 467 | ||
| 437 | 468 | // setWorkspace applies one workspace.<name>.<field> key. | |
| @@ -442,10 +473,7 @@ func setWorkspace(w *ports.WorkspaceConfig, field, v string) error { | |||
| 442 | 473 | w.Hidden = b | |
| 443 | 474 | return err | |
| 444 | 475 | case "monitor": | |
| 445 | - | if v == "" { | |
| 446 | - | return fmt.Errorf("must not be empty") | |
| 447 | - | } | |
| 448 | - | w.Monitor = v | |
| 476 | + | return fmt.Errorf("not supported yet: one monitor only") | |
| 449 | 477 | case "max-columns": | |
| 450 | 478 | return positive(&w.MaxColumns, v, 16) | |
| 451 | 479 | case "overflow": | |
| @@ -454,7 +482,7 @@ func setWorkspace(w *ports.WorkspaceConfig, field, v string) error { | |||
| 454 | 482 | } | |
| 455 | 483 | w.Overflow = v | |
| 456 | 484 | default: | |
| 457 | - | return fmt.Errorf("unknown key (hidden, monitor, max-columns, overflow)") | |
| 485 | + | return fmt.Errorf("unknown key (hidden, max-columns, overflow)") | |
| 458 | 486 | } | |
| 459 | 487 | return nil | |
| 460 | 488 | } | |
| @@ -251,6 +251,7 @@ func parse(r io.Reader) (ports.Config, map[string]string, []Warning, error) { | |||||
| 251 | if err := scanner.Err(); err != nil { | 251 | if err := scanner.Err(); err != nil { | ||
| 252 | return c, raw, warnings, err | 252 | return c, raw, warnings, err | ||
| 253 | } | 253 | } | ||
| 254 | + | warnings = append(warnings, checkWorkspaceBinds(c, seen)...) | |||
| 254 | // A user bind on a digit (cmd+1, even "none") replaces the default bound | 255 | // A user bind on a digit (cmd+1, even "none") replaces the default bound | ||
| 255 | // to the same physical key (cmd+code:2), so older configs keep working. | 256 | // to the same physical key (cmd+code:2), so older configs keep working. | ||
| 256 | for combo := range c.Binds { | 257 | for combo := range c.Binds { | ||
| @@ -432,6 +433,36 @@ func set(c *ports.Config, key, v string) error { | |||||
| 432 | return nil | 433 | return nil | ||
| 433 | } | 434 | } | ||
| 434 | 435 | ||||
| 436 | + | // checkWorkspaceBinds warns about a `workspace <name>` bind to an undeclared | |||
| 437 | + | // workspace (it does nothing) and a hidden workspace no bind reaches (its | |||
| 438 | + | // windows could only come back by editing the config). | |||
| 439 | + | func checkWorkspaceBinds(c ports.Config, seen map[string]int) []Warning { | |||
| 440 | + | declared := map[string]bool{} | |||
| 441 | + | for _, w := range c.Workspaces { | |||
| 442 | + | declared[w.Name] = true | |||
| 443 | + | } | |||
| 444 | + | bound := map[string]bool{} | |||
| 445 | + | var warnings []Warning | |||
| 446 | + | for combo, a := range c.Binds { | |||
| 447 | + | name, ok := strings.CutPrefix(a, "workspace ") | |||
| 448 | + | if !ok { | |||
| 449 | + | continue | |||
| 450 | + | } | |||
| 451 | + | name = strings.TrimSpace(name) | |||
| 452 | + | bound[name] = true | |||
| 453 | + | if !declared[name] { | |||
| 454 | + | warnings = append(warnings, Warning{Line: seen["bind."+combo], Msg: fmt.Sprintf("bind.%s: no workspace.%s.* declared", combo, name)}) | |||
| 455 | + | } | |||
| 456 | + | } | |||
| 457 | + | for _, w := range c.Workspaces { | |||
| 458 | + | if w.Hidden && !bound[w.Name] { | |||
| 459 | + | warnings = append(warnings, Warning{Line: seen["workspace."+w.Name+".hidden"], Msg: fmt.Sprintf("workspace.%s.hidden: no bind shows it (add bind.<keys> = workspace %s)", w.Name, w.Name)}) | |||
| 460 | + | } | |||
| 461 | + | } | |||
| 462 | + | sort.Slice(warnings, func(i, j int) bool { return warnings[i].Line < warnings[j].Line }) | |||
| 463 | + | return warnings | |||
| 464 | + | } | |||
| 465 | + | ||||
| 435 | var workspaceName = regexp.MustCompile(`^[A-Za-z0-9_-]+$`) | 466 | var workspaceName = regexp.MustCompile(`^[A-Za-z0-9_-]+$`) | ||
| 436 | 467 | ||||
| 437 | // setWorkspace applies one workspace.<name>.<field> key. | 468 | // setWorkspace applies one workspace.<name>.<field> key. | ||
| @@ -442,10 +473,7 @@ func setWorkspace(w *ports.WorkspaceConfig, field, v string) error { | |||||
| 442 | w.Hidden = b | 473 | w.Hidden = b | ||
| 443 | return err | 474 | return err | ||
| 444 | case "monitor": | 475 | case "monitor": | ||
| 445 | - | if v == "" { | 476 | + | return fmt.Errorf("not supported yet: one monitor only") |
| 446 | - | return fmt.Errorf("must not be empty") | |||
| 447 | - | } | |||
| 448 | - | w.Monitor = v | |||
| 449 | case "max-columns": | 477 | case "max-columns": | ||
| 450 | return positive(&w.MaxColumns, v, 16) | 478 | return positive(&w.MaxColumns, v, 16) | ||
| 451 | case "overflow": | 479 | case "overflow": | ||
| @@ -454,7 +482,7 @@ func setWorkspace(w *ports.WorkspaceConfig, field, v string) error { | |||||
| 454 | } | 482 | } | ||
| 455 | w.Overflow = v | 483 | w.Overflow = v | ||
| 456 | default: | 484 | default: | ||
| 457 | - | return fmt.Errorf("unknown key (hidden, monitor, max-columns, overflow)") | 485 | + | return fmt.Errorf("unknown key (hidden, max-columns, overflow)") |
| 458 | } | 486 | } | ||
| 459 | return nil | 487 | return nil | ||
| 460 | } | 488 | } | ||
M internal/adapters/config/config_test.go +5 -2
| @@ -245,10 +245,13 @@ workspace.dev.color = red | |||
| 245 | 245 | workspace.web.overflow = spiral | |
| 246 | 246 | bind.cmd+d = workspace dev | |
| 247 | 247 | bind.cmd+w = workspace | |
| 248 | + | bind.cmd+x = workspace typo | |
| 249 | + | workspace.lost.hidden = on | |
| 248 | 250 | `) | |
| 249 | 251 | want := []ports.WorkspaceConfig{ | |
| 250 | - | {Name: "dev", Hidden: true, MaxColumns: 3, Monitor: "DP-2"}, | |
| 252 | + | {Name: "dev", Hidden: true, MaxColumns: 3}, | |
| 251 | 253 | {Name: "web", Overflow: "scroll"}, | |
| 254 | + | {Name: "lost", Hidden: true}, | |
| 252 | 255 | } | |
| 253 | 256 | if c.Layout.Overflow != "fixed" || !reflect.DeepEqual(c.Workspaces, want) { | |
| 254 | 257 | t.Fatalf("%q %+v", c.Layout.Overflow, c.Workspaces) | |
| @@ -260,7 +263,7 @@ bind.cmd+w = workspace | |||
| 260 | 263 | for _, x := range w { | |
| 261 | 264 | lines = append(lines, x.Line) | |
| 262 | 265 | } | |
| 263 | - | if !reflect.DeepEqual(lines, []int{6, 7, 8, 10}) { | |
| 266 | + | if !reflect.DeepEqual(lines, []int{5, 6, 7, 8, 10, 11, 12}) { | |
| 264 | 267 | t.Fatal(w) | |
| 265 | 268 | } | |
| 266 | 269 | } | |
| @@ -245,10 +245,13 @@ workspace.dev.color = red | |||||
| 245 | workspace.web.overflow = spiral | 245 | workspace.web.overflow = spiral | ||
| 246 | bind.cmd+d = workspace dev | 246 | bind.cmd+d = workspace dev | ||
| 247 | bind.cmd+w = workspace | 247 | bind.cmd+w = workspace | ||
| 248 | + | bind.cmd+x = workspace typo | |||
| 249 | + | workspace.lost.hidden = on | |||
| 248 | `) | 250 | `) | ||
| 249 | want := []ports.WorkspaceConfig{ | 251 | want := []ports.WorkspaceConfig{ | ||
| 250 | - | {Name: "dev", Hidden: true, MaxColumns: 3, Monitor: "DP-2"}, | 252 | + | {Name: "dev", Hidden: true, MaxColumns: 3}, |
| 251 | {Name: "web", Overflow: "scroll"}, | 253 | {Name: "web", Overflow: "scroll"}, | ||
| 254 | + | {Name: "lost", Hidden: true}, | |||
| 252 | } | 255 | } | ||
| 253 | if c.Layout.Overflow != "fixed" || !reflect.DeepEqual(c.Workspaces, want) { | 256 | if c.Layout.Overflow != "fixed" || !reflect.DeepEqual(c.Workspaces, want) { | ||
| 254 | t.Fatalf("%q %+v", c.Layout.Overflow, c.Workspaces) | 257 | t.Fatalf("%q %+v", c.Layout.Overflow, c.Workspaces) | ||
| @@ -260,7 +263,7 @@ bind.cmd+w = workspace | |||||
| 260 | for _, x := range w { | 263 | for _, x := range w { | ||
| 261 | lines = append(lines, x.Line) | 264 | lines = append(lines, x.Line) | ||
| 262 | } | 265 | } | ||
| 263 | - | if !reflect.DeepEqual(lines, []int{6, 7, 8, 10}) { | 266 | + | if !reflect.DeepEqual(lines, []int{5, 6, 7, 8, 10, 11, 12}) { |
| 264 | t.Fatal(w) | 267 | t.Fatal(w) | ||
| 265 | } | 268 | } | ||
| 266 | } | 269 | } | ||
M internal/core/action.go +4 -2
| @@ -111,12 +111,14 @@ func (m *Monitor) Apply(a Action) Effect { | |||
| 111 | 111 | } | |
| 112 | 112 | return Effect{} | |
| 113 | 113 | case ActionMoveToWorkspaceUp: | |
| 114 | - | if m.Active > 0 { | |
| 114 | + | if m.shown == nil && m.Active > 0 { | |
| 115 | 115 | m.MoveToWorkspace(m.Active - 1) | |
| 116 | 116 | } | |
| 117 | 117 | return Effect{} | |
| 118 | 118 | case ActionMoveToWorkspaceDown: | |
| 119 | - | m.MoveToWorkspace(m.Active + 1) | |
| 119 | + | if m.shown == nil { | |
| 120 | + | m.MoveToWorkspace(m.Active + 1) | |
| 121 | + | } | |
| 120 | 122 | return Effect{} | |
| 121 | 123 | } | |
| 122 | 124 | return m.Current().Apply(a) | |
| @@ -111,12 +111,14 @@ func (m *Monitor) Apply(a Action) Effect { | |||||
| 111 | } | 111 | } | ||
| 112 | return Effect{} | 112 | return Effect{} | ||
| 113 | case ActionMoveToWorkspaceUp: | 113 | case ActionMoveToWorkspaceUp: | ||
| 114 | - | if m.Active > 0 { | 114 | + | if m.shown == nil && m.Active > 0 { |
| 115 | m.MoveToWorkspace(m.Active - 1) | 115 | m.MoveToWorkspace(m.Active - 1) | ||
| 116 | } | 116 | } | ||
| 117 | return Effect{} | 117 | return Effect{} | ||
| 118 | case ActionMoveToWorkspaceDown: | 118 | case ActionMoveToWorkspaceDown: | ||
| 119 | - | m.MoveToWorkspace(m.Active + 1) | 119 | + | if m.shown == nil { |
| 120 | + | m.MoveToWorkspace(m.Active + 1) | |||
| 121 | + | } | |||
| 120 | return Effect{} | 122 | return Effect{} | ||
| 121 | } | 123 | } | ||
| 122 | return m.Current().Apply(a) | 124 | return m.Current().Apply(a) | ||
M internal/core/layout.go +14 -4
| @@ -142,7 +142,10 @@ func (w *Workspace) AddWindow(id WindowID) { | |||
| 142 | 142 | return | |
| 143 | 143 | } | |
| 144 | 144 | at := 0 | |
| 145 | - | if len(w.Columns) > 0 { | |
| 145 | + | if w.Overflow == OverflowFixed { | |
| 146 | + | // The spiral depends only on window order: new windows go last. | |
| 147 | + | at = len(w.Columns) | |
| 148 | + | } else if len(w.Columns) > 0 { | |
| 146 | 149 | at = w.Focus + 1 | |
| 147 | 150 | } | |
| 148 | 151 | w.Columns = append(w.Columns, Column{}) | |
| @@ -327,7 +330,8 @@ func (w *Workspace) columnWidth(i int) int { | |||
| 327 | 330 | return w.Output.W | |
| 328 | 331 | } | |
| 329 | 332 | g := w.gap() | |
| 330 | - | if w.Columns[i].Width == (Width{}) { | |
| 333 | + | // Fixed overflow never scrolls, so presets would push columns off screen. | |
| 334 | + | if w.Columns[i].Width == (Width{}) || w.Overflow == OverflowFixed { | |
| 331 | 335 | if len(w.Columns) == 1 { | |
| 332 | 336 | return max(w.Usable.W-2*g, 0) | |
| 333 | 337 | } | |
| @@ -425,14 +429,20 @@ func (w *Workspace) Layout() []Placement { | |||
| 425 | 429 | h = min(h, bottom-y) | |
| 426 | 430 | r := Rect{X: col.X, Y: y, W: col.W, H: h} | |
| 427 | 431 | full := w.fullscreen == id && id != 0 | |
| 428 | - | hidden := fullColumn && !full | |
| 432 | + | // Fixed overflow keeps every column on screen: hide them all | |
| 433 | + | // behind a fullscreen window. | |
| 434 | + | hidden := (fullColumn || w.Overflow == OverflowFixed && w.fullscreen != 0) && !full | |
| 429 | 435 | if full { | |
| 436 | + | // Scroll mode aligns the view on the column; fixed never scrolls. | |
| 430 | 437 | r = Rect{X: col.X, Y: 0, W: w.Output.W, H: w.Output.H} | |
| 438 | + | if w.Overflow == OverflowFixed { | |
| 439 | + | r.X = 0 | |
| 440 | + | } | |
| 431 | 441 | } | |
| 432 | 442 | if hidden { | |
| 433 | 443 | r = Rect{} | |
| 434 | 444 | } | |
| 435 | - | result = append(result, Placement{ID: id, Rect: r, Fullscreen: full, Focused: i == w.Focus && j == c.Focus, Hidden: hidden, Borderless: col.W >= w.Usable.W-2*gap}) | |
| 445 | + | result = append(result, Placement{ID: id, Rect: r, Fullscreen: full, Focused: i == w.Focus && j == c.Focus, Hidden: hidden, Borderless: col.W >= w.Usable.W-2*gap && (w.Overflow != OverflowFixed || len(w.Columns) == 1)}) | |
| 436 | 446 | y += h + gap | |
| 437 | 447 | } | |
| 438 | 448 | } | |
| @@ -142,7 +142,10 @@ func (w *Workspace) AddWindow(id WindowID) { | |||||
| 142 | return | 142 | return | ||
| 143 | } | 143 | } | ||
| 144 | at := 0 | 144 | at := 0 | ||
| 145 | - | if len(w.Columns) > 0 { | 145 | + | if w.Overflow == OverflowFixed { |
| 146 | + | // The spiral depends only on window order: new windows go last. | |||
| 147 | + | at = len(w.Columns) | |||
| 148 | + | } else if len(w.Columns) > 0 { | |||
| 146 | at = w.Focus + 1 | 149 | at = w.Focus + 1 | ||
| 147 | } | 150 | } | ||
| 148 | w.Columns = append(w.Columns, Column{}) | 151 | w.Columns = append(w.Columns, Column{}) | ||
| @@ -327,7 +330,8 @@ func (w *Workspace) columnWidth(i int) int { | |||||
| 327 | return w.Output.W | 330 | return w.Output.W | ||
| 328 | } | 331 | } | ||
| 329 | g := w.gap() | 332 | g := w.gap() | ||
| 330 | - | if w.Columns[i].Width == (Width{}) { | 333 | + | // Fixed overflow never scrolls, so presets would push columns off screen. |
| 334 | + | if w.Columns[i].Width == (Width{}) || w.Overflow == OverflowFixed { | |||
| 331 | if len(w.Columns) == 1 { | 335 | if len(w.Columns) == 1 { | ||
| 332 | return max(w.Usable.W-2*g, 0) | 336 | return max(w.Usable.W-2*g, 0) | ||
| 333 | } | 337 | } | ||
| @@ -425,14 +429,20 @@ func (w *Workspace) Layout() []Placement { | |||||
| 425 | h = min(h, bottom-y) | 429 | h = min(h, bottom-y) | ||
| 426 | r := Rect{X: col.X, Y: y, W: col.W, H: h} | 430 | r := Rect{X: col.X, Y: y, W: col.W, H: h} | ||
| 427 | full := w.fullscreen == id && id != 0 | 431 | full := w.fullscreen == id && id != 0 | ||
| 428 | - | hidden := fullColumn && !full | 432 | + | // Fixed overflow keeps every column on screen: hide them all |
| 433 | + | // behind a fullscreen window. | |||
| 434 | + | hidden := (fullColumn || w.Overflow == OverflowFixed && w.fullscreen != 0) && !full | |||
| 429 | if full { | 435 | if full { | ||
| 436 | + | // Scroll mode aligns the view on the column; fixed never scrolls. | |||
| 430 | r = Rect{X: col.X, Y: 0, W: w.Output.W, H: w.Output.H} | 437 | r = Rect{X: col.X, Y: 0, W: w.Output.W, H: w.Output.H} | ||
| 438 | + | if w.Overflow == OverflowFixed { | |||
| 439 | + | r.X = 0 | |||
| 440 | + | } | |||
| 431 | } | 441 | } | ||
| 432 | if hidden { | 442 | if hidden { | ||
| 433 | r = Rect{} | 443 | r = Rect{} | ||
| 434 | } | 444 | } | ||
| 435 | - | result = append(result, Placement{ID: id, Rect: r, Fullscreen: full, Focused: i == w.Focus && j == c.Focus, Hidden: hidden, Borderless: col.W >= w.Usable.W-2*gap}) | 445 | + | result = append(result, Placement{ID: id, Rect: r, Fullscreen: full, Focused: i == w.Focus && j == c.Focus, Hidden: hidden, Borderless: col.W >= w.Usable.W-2*gap && (w.Overflow != OverflowFixed || len(w.Columns) == 1)}) |
| 436 | y += h + gap | 446 | y += h + gap | ||
| 437 | } | 447 | } | ||
| 438 | } | 448 | } | ||
M internal/core/layout_test.go +64 -2
| @@ -358,8 +358,6 @@ func TestFixedOverflowSpiral(t *testing.T) { | |||
| 358 | 358 | w.SetOutput(100, 80) | |
| 359 | 359 | for id := WindowID(1); id <= 5; id++ { | |
| 360 | 360 | w.AddWindow(id) | |
| 361 | - | // New columns open after the focused one; keep order by focusing the last. | |
| 362 | - | w.Focus = len(w.Columns) - 1 | |
| 363 | 361 | } | |
| 364 | 362 | got := map[WindowID]Rect{} | |
| 365 | 363 | for _, p := range w.Layout() { | |
| @@ -387,3 +385,67 @@ func TestFixedOverflowSpiral(t *testing.T) { | |||
| 387 | 385 | t.Fatal(p) | |
| 388 | 386 | } | |
| 389 | 387 | } | |
| 388 | + | ||
| 389 | + | func fixedWorkspace(maxCols, gaps, n int) *Workspace { | |
| 390 | + | w := &Workspace{MaxColumns: maxCols, Overflow: OverflowFixed, Gaps: gaps} | |
| 391 | + | w.SetOutput(100, 80) | |
| 392 | + | for id := 1; id <= n; id++ { | |
| 393 | + | w.AddWindow(WindowID(id)) | |
| 394 | + | } | |
| 395 | + | return w | |
| 396 | + | } | |
| 397 | + | ||
| 398 | + | func placements(w *Workspace) map[WindowID]Placement { | |
| 399 | + | got := map[WindowID]Placement{} | |
| 400 | + | for _, p := range w.Layout() { | |
| 401 | + | got[p.ID] = p | |
| 402 | + | } | |
| 403 | + | return got | |
| 404 | + | } | |
| 405 | + | ||
| 406 | + | func TestFixedOverflowEdgeCases(t *testing.T) { | |
| 407 | + | t.Run("new windows go last whatever the focus", func(t *testing.T) { | |
| 408 | + | w := fixedWorkspace(2, 0, 3) | |
| 409 | + | w.FocusID(1) | |
| 410 | + | w.AddWindow(4) | |
| 411 | + | if got := w.Columns[len(w.Columns)-1].Windows[0]; got != 4 { | |
| 412 | + | t.Fatal(got) | |
| 413 | + | } | |
| 414 | + | }) | |
| 415 | + | for _, col := range []WindowID{1, 4} { // a plain column and a spiral slot | |
| 416 | + | t.Run("fullscreen covers the output", func(t *testing.T) { | |
| 417 | + | w := fixedWorkspace(2, 4, 4) | |
| 418 | + | w.FocusID(col) | |
| 419 | + | w.ToggleFullscreen() | |
| 420 | + | for id, p := range placements(w) { | |
| 421 | + | if id == col && (p.Rect != Rect{W: 100, H: 80} || !p.Fullscreen) { | |
| 422 | + | t.Fatal(p) | |
| 423 | + | } | |
| 424 | + | if id != col && !p.Hidden { | |
| 425 | + | t.Fatalf("window %d visible over fullscreen: %+v", id, p) | |
| 426 | + | } | |
| 427 | + | } | |
| 428 | + | }) | |
| 429 | + | } | |
| 430 | + | t.Run("presets ignored", func(t *testing.T) { | |
| 431 | + | w := fixedWorkspace(2, 0, 2) | |
| 432 | + | w.SetPresets([]Width{{Num: 2, Den: 3}}) | |
| 433 | + | w.CycleWidth() | |
| 434 | + | w.FocusColumn(-1) | |
| 435 | + | w.CycleWidth() | |
| 436 | + | for id, p := range placements(w) { | |
| 437 | + | if p.Rect.X+p.Rect.W > 100 || p.Rect.W != 50 { | |
| 438 | + | t.Fatal(id, p) | |
| 439 | + | } | |
| 440 | + | } | |
| 441 | + | }) | |
| 442 | + | t.Run("gaps between spiral halves", func(t *testing.T) { | |
| 443 | + | got := placements(fixedWorkspace(1, 4, 2)) | |
| 444 | + | if got[1].Rect != (Rect{X: 4, Y: 4, W: 92, H: 34}) || got[2].Rect != (Rect{X: 4, Y: 42, W: 92, H: 34}) { | |
| 445 | + | t.Fatal(got[1].Rect, got[2].Rect) | |
| 446 | + | } | |
| 447 | + | if got[1].Borderless || got[2].Borderless { | |
| 448 | + | t.Fatal("spiral windows must keep borders to show focus") | |
| 449 | + | } | |
| 450 | + | }) | |
| 451 | + | } | |
| @@ -358,8 +358,6 @@ func TestFixedOverflowSpiral(t *testing.T) { | |||||
| 358 | w.SetOutput(100, 80) | 358 | w.SetOutput(100, 80) | ||
| 359 | for id := WindowID(1); id <= 5; id++ { | 359 | for id := WindowID(1); id <= 5; id++ { | ||
| 360 | w.AddWindow(id) | 360 | w.AddWindow(id) | ||
| 361 | - | // New columns open after the focused one; keep order by focusing the last. | |||
| 362 | - | w.Focus = len(w.Columns) - 1 | |||
| 363 | } | 361 | } | ||
| 364 | got := map[WindowID]Rect{} | 362 | got := map[WindowID]Rect{} | ||
| 365 | for _, p := range w.Layout() { | 363 | for _, p := range w.Layout() { | ||
| @@ -387,3 +385,67 @@ func TestFixedOverflowSpiral(t *testing.T) { | |||||
| 387 | t.Fatal(p) | 385 | t.Fatal(p) | ||
| 388 | } | 386 | } | ||
| 389 | } | 387 | } | ||
| 388 | + | ||||
| 389 | + | func fixedWorkspace(maxCols, gaps, n int) *Workspace { | |||
| 390 | + | w := &Workspace{MaxColumns: maxCols, Overflow: OverflowFixed, Gaps: gaps} | |||
| 391 | + | w.SetOutput(100, 80) | |||
| 392 | + | for id := 1; id <= n; id++ { | |||
| 393 | + | w.AddWindow(WindowID(id)) | |||
| 394 | + | } | |||
| 395 | + | return w | |||
| 396 | + | } | |||
| 397 | + | ||||
| 398 | + | func placements(w *Workspace) map[WindowID]Placement { | |||
| 399 | + | got := map[WindowID]Placement{} | |||
| 400 | + | for _, p := range w.Layout() { | |||
| 401 | + | got[p.ID] = p | |||
| 402 | + | } | |||
| 403 | + | return got | |||
| 404 | + | } | |||
| 405 | + | ||||
| 406 | + | func TestFixedOverflowEdgeCases(t *testing.T) { | |||
| 407 | + | t.Run("new windows go last whatever the focus", func(t *testing.T) { | |||
| 408 | + | w := fixedWorkspace(2, 0, 3) | |||
| 409 | + | w.FocusID(1) | |||
| 410 | + | w.AddWindow(4) | |||
| 411 | + | if got := w.Columns[len(w.Columns)-1].Windows[0]; got != 4 { | |||
| 412 | + | t.Fatal(got) | |||
| 413 | + | } | |||
| 414 | + | }) | |||
| 415 | + | for _, col := range []WindowID{1, 4} { // a plain column and a spiral slot | |||
| 416 | + | t.Run("fullscreen covers the output", func(t *testing.T) { | |||
| 417 | + | w := fixedWorkspace(2, 4, 4) | |||
| 418 | + | w.FocusID(col) | |||
| 419 | + | w.ToggleFullscreen() | |||
| 420 | + | for id, p := range placements(w) { | |||
| 421 | + | if id == col && (p.Rect != Rect{W: 100, H: 80} || !p.Fullscreen) { | |||
| 422 | + | t.Fatal(p) | |||
| 423 | + | } | |||
| 424 | + | if id != col && !p.Hidden { | |||
| 425 | + | t.Fatalf("window %d visible over fullscreen: %+v", id, p) | |||
| 426 | + | } | |||
| 427 | + | } | |||
| 428 | + | }) | |||
| 429 | + | } | |||
| 430 | + | t.Run("presets ignored", func(t *testing.T) { | |||
| 431 | + | w := fixedWorkspace(2, 0, 2) | |||
| 432 | + | w.SetPresets([]Width{{Num: 2, Den: 3}}) | |||
| 433 | + | w.CycleWidth() | |||
| 434 | + | w.FocusColumn(-1) | |||
| 435 | + | w.CycleWidth() | |||
| 436 | + | for id, p := range placements(w) { | |||
| 437 | + | if p.Rect.X+p.Rect.W > 100 || p.Rect.W != 50 { | |||
| 438 | + | t.Fatal(id, p) | |||
| 439 | + | } | |||
| 440 | + | } | |||
| 441 | + | }) | |||
| 442 | + | t.Run("gaps between spiral halves", func(t *testing.T) { | |||
| 443 | + | got := placements(fixedWorkspace(1, 4, 2)) | |||
| 444 | + | if got[1].Rect != (Rect{X: 4, Y: 4, W: 92, H: 34}) || got[2].Rect != (Rect{X: 4, Y: 42, W: 92, H: 34}) { | |||
| 445 | + | t.Fatal(got[1].Rect, got[2].Rect) | |||
| 446 | + | } | |||
| 447 | + | if got[1].Borderless || got[2].Borderless { | |||
| 448 | + | t.Fatal("spiral windows must keep borders to show focus") | |||
| 449 | + | } | |||
| 450 | + | }) | |||
| 451 | + | } | |||
M internal/core/monitor.go +0 -14
| @@ -160,17 +160,6 @@ func (m *Monitor) RemoveWindow(id WindowID) { | |||
| 160 | 160 | } | |
| 161 | 161 | } | |
| 162 | 162 | ||
| 163 | - | // FocusID shows the window's workspace and focuses it. | |
| 164 | - | func (m *Monitor) FocusID(id WindowID) bool { | |
| 165 | - | w, _ := m.find(id) | |
| 166 | - | if w == nil { | |
| 167 | - | return false | |
| 168 | - | } | |
| 169 | - | m.show(w) | |
| 170 | - | w.FocusID(id) | |
| 171 | - | return true | |
| 172 | - | } | |
| 173 | - | ||
| 174 | 163 | // SetFullscreen applies a client request on the window's own workspace. It does | |
| 175 | 164 | // not switch workspaces: client requests never move the user. | |
| 176 | 165 | func (m *Monitor) SetFullscreen(id WindowID, on bool) { | |
| @@ -274,7 +263,6 @@ func (m *Monitor) SetNamed(specs []NamedWorkspace) { | |||
| 274 | 263 | want[s.Name] = s | |
| 275 | 264 | } | |
| 276 | 265 | byName := map[string]*Workspace{} | |
| 277 | - | var keep []*Workspace | |
| 278 | 266 | for _, w := range m.Workspaces { | |
| 279 | 267 | if _, ok := want[w.Name]; w.Name != "" && !ok { | |
| 280 | 268 | w.Name = "" | |
| @@ -282,9 +270,7 @@ func (m *Monitor) SetNamed(specs []NamedWorkspace) { | |||
| 282 | 270 | if w.Name != "" { | |
| 283 | 271 | byName[w.Name] = w | |
| 284 | 272 | } | |
| 285 | - | keep = append(keep, w) | |
| 286 | 273 | } | |
| 287 | - | m.Workspaces = keep | |
| 288 | 274 | var hidden []*Workspace | |
| 289 | 275 | for _, w := range m.hidden { | |
| 290 | 276 | if _, ok := want[w.Name]; ok { | |
| @@ -160,17 +160,6 @@ func (m *Monitor) RemoveWindow(id WindowID) { | |||||
| 160 | } | 160 | } | ||
| 161 | } | 161 | } | ||
| 162 | 162 | ||||
| 163 | - | // FocusID shows the window's workspace and focuses it. | |||
| 164 | - | func (m *Monitor) FocusID(id WindowID) bool { | |||
| 165 | - | w, _ := m.find(id) | |||
| 166 | - | if w == nil { | |||
| 167 | - | return false | |||
| 168 | - | } | |||
| 169 | - | m.show(w) | |||
| 170 | - | w.FocusID(id) | |||
| 171 | - | return true | |||
| 172 | - | } | |||
| 173 | - | ||||
| 174 | // SetFullscreen applies a client request on the window's own workspace. It does | 163 | // SetFullscreen applies a client request on the window's own workspace. It does | ||
| 175 | // not switch workspaces: client requests never move the user. | 164 | // not switch workspaces: client requests never move the user. | ||
| 176 | func (m *Monitor) SetFullscreen(id WindowID, on bool) { | 165 | func (m *Monitor) SetFullscreen(id WindowID, on bool) { | ||
| @@ -274,7 +263,6 @@ func (m *Monitor) SetNamed(specs []NamedWorkspace) { | |||||
| 274 | want[s.Name] = s | 263 | want[s.Name] = s | ||
| 275 | } | 264 | } | ||
| 276 | byName := map[string]*Workspace{} | 265 | byName := map[string]*Workspace{} | ||
| 277 | - | var keep []*Workspace | |||
| 278 | for _, w := range m.Workspaces { | 266 | for _, w := range m.Workspaces { | ||
| 279 | if _, ok := want[w.Name]; w.Name != "" && !ok { | 267 | if _, ok := want[w.Name]; w.Name != "" && !ok { | ||
| 280 | w.Name = "" | 268 | w.Name = "" | ||
| @@ -282,9 +270,7 @@ func (m *Monitor) SetNamed(specs []NamedWorkspace) { | |||||
| 282 | if w.Name != "" { | 270 | if w.Name != "" { | ||
| 283 | byName[w.Name] = w | 271 | byName[w.Name] = w | ||
| 284 | } | 272 | } | ||
| 285 | - | keep = append(keep, w) | |||
| 286 | } | 273 | } | ||
| 287 | - | m.Workspaces = keep | |||
| 288 | var hidden []*Workspace | 274 | var hidden []*Workspace | ||
| 289 | for _, w := range m.hidden { | 275 | for _, w := range m.hidden { | ||
| 290 | if _, ok := want[w.Name]; ok { | 276 | if _, ok := want[w.Name]; ok { | ||
M internal/core/monitor_test.go +77 -6
| @@ -118,9 +118,6 @@ func TestMonitorLayoutHidesOtherWorkspaces(t *testing.T) { | |||
| 118 | 118 | t.Fatal(m.Layout()) | |
| 119 | 119 | } | |
| 120 | 120 | } | |
| 121 | - | if !m.FocusID(1) || m.Active != 0 { | |
| 122 | - | t.Fatal(m.Active) | |
| 123 | - | } | |
| 124 | 121 | } | |
| 125 | 122 | ||
| 126 | 123 | func TestMonitorFullscreenRequestStaysPut(t *testing.T) { | |
| @@ -278,9 +275,12 @@ func TestHiddenWorkspaceToggle(t *testing.T) { | |||
| 278 | 275 | if id, _ := m.Focused(); id != 1 { | |
| 279 | 276 | t.Fatal(id) | |
| 280 | 277 | } | |
| 281 | - | // Clicking a window of the hidden workspace shows it (FocusID). | |
| 282 | - | if !m.FocusID(2) || m.Current().Name != "dev" { | |
| 283 | - | t.Fatal("FocusID did not show the hidden workspace") | |
| 278 | + | // Moving a window up/down from a hidden workspace does nothing. | |
| 279 | + | m.Apply("workspace dev") | |
| 280 | + | m.Apply(ActionMoveToWorkspaceDown) | |
| 281 | + | m.Apply(ActionMoveToWorkspaceUp) | |
| 282 | + | if id, _ := m.Focused(); id != 2 || m.Current().Name != "dev" { | |
| 283 | + | t.Fatal(id, windows(m)) | |
| 284 | 284 | } | |
| 285 | 285 | } | |
| 286 | 286 | ||
| @@ -346,3 +346,74 @@ func TestNamedWorkspaceSettings(t *testing.T) { | |||
| 346 | 346 | t.Fatal(m.Current().MaxColumns, m.Current().Overflow) | |
| 347 | 347 | } | |
| 348 | 348 | } | |
| 349 | + | ||
| 350 | + | // checkInvariants fails when the monitor is in a state no action should reach. | |
| 351 | + | func checkInvariants(t *testing.T, m *Monitor, want []WindowID) { | |
| 352 | + | t.Helper() | |
| 353 | + | if m.Active < 0 || m.Active >= len(m.Workspaces) { | |
| 354 | + | t.Fatalf("active %d of %d", m.Active, len(m.Workspaces)) | |
| 355 | + | } | |
| 356 | + | if last := m.Workspaces[len(m.Workspaces)-1]; !last.empty() || last.Name != "" { | |
| 357 | + | t.Fatal("no trailing empty unnamed workspace") | |
| 358 | + | } | |
| 359 | + | if m.shown != nil && indexOf(m.hidden, m.shown) < 0 { | |
| 360 | + | t.Fatal("shown is not a hidden workspace") | |
| 361 | + | } | |
| 362 | + | if m.back != nil && !m.has(m.back) { | |
| 363 | + | t.Fatal("back dangles") | |
| 364 | + | } | |
| 365 | + | seen := map[WindowID]int{} | |
| 366 | + | for _, w := range append(append([]*Workspace(nil), m.Workspaces...), m.hidden...) { | |
| 367 | + | for _, c := range w.Columns { | |
| 368 | + | for _, id := range c.Windows { | |
| 369 | + | seen[id]++ | |
| 370 | + | } | |
| 371 | + | } | |
| 372 | + | } | |
| 373 | + | for _, id := range want { | |
| 374 | + | if seen[id] != 1 { | |
| 375 | + | t.Fatalf("window %d present %d times", id, seen[id]) | |
| 376 | + | } | |
| 377 | + | } | |
| 378 | + | if len(seen) != len(want) { | |
| 379 | + | t.Fatalf("windows %v, want %v", seen, want) | |
| 380 | + | } | |
| 381 | + | } | |
| 382 | + | ||
| 383 | + | func TestSetNamedTransitions(t *testing.T) { | |
| 384 | + | dev := NamedWorkspace{Name: "dev"} | |
| 385 | + | devHidden := NamedWorkspace{Name: "dev", Hidden: true} | |
| 386 | + | web := NamedWorkspace{Name: "web"} | |
| 387 | + | for _, tc := range []struct { | |
| 388 | + | name string | |
| 389 | + | before []NamedWorkspace | |
| 390 | + | after []NamedWorkspace | |
| 391 | + | onDev bool // show dev before the reload | |
| 392 | + | want string // workspace name on screen after | |
| 393 | + | }{ | |
| 394 | + | {"numbered active becomes hidden", []NamedWorkspace{dev}, []NamedWorkspace{devHidden}, true, "dev"}, | |
| 395 | + | {"hidden shown becomes numbered", []NamedWorkspace{devHidden}, []NamedWorkspace{dev}, true, "dev"}, | |
| 396 | + | {"rename drops the old name", []NamedWorkspace{dev}, []NamedWorkspace{web}, true, ""}, | |
| 397 | + | {"hidden shown is removed", []NamedWorkspace{devHidden}, nil, true, ""}, | |
| 398 | + | {"hidden not shown is removed", []NamedWorkspace{devHidden, web}, []NamedWorkspace{web}, false, ""}, | |
| 399 | + | {"add while on numbered", nil, []NamedWorkspace{devHidden, web}, false, ""}, | |
| 400 | + | } { | |
| 401 | + | t.Run(tc.name, func(t *testing.T) { | |
| 402 | + | m := named(monitor(), tc.before...) | |
| 403 | + | m.AddWindow(1) | |
| 404 | + | if tc.onDev { | |
| 405 | + | m.Apply("workspace dev") | |
| 406 | + | } | |
| 407 | + | m.AddWindow(2) | |
| 408 | + | checkInvariants(t, m, []WindowID{1, 2}) | |
| 409 | + | m.SetNamed(tc.after) | |
| 410 | + | checkInvariants(t, m, []WindowID{1, 2}) | |
| 411 | + | if got := m.Current().Name; got != tc.want { | |
| 412 | + | t.Fatalf("on %q, want %q", got, tc.want) | |
| 413 | + | } | |
| 414 | + | if id, _ := m.Focused(); id != 2 { | |
| 415 | + | t.Fatalf("focus moved to %d", id) | |
| 416 | + | } | |
| 417 | + | }) | |
| 418 | + | } | |
| 419 | + | } | |
| @@ -118,9 +118,6 @@ func TestMonitorLayoutHidesOtherWorkspaces(t *testing.T) { | |||||
| 118 | t.Fatal(m.Layout()) | 118 | t.Fatal(m.Layout()) | ||
| 119 | } | 119 | } | ||
| 120 | } | 120 | } | ||
| 121 | - | if !m.FocusID(1) || m.Active != 0 { | |||
| 122 | - | t.Fatal(m.Active) | |||
| 123 | - | } | |||
| 124 | } | 121 | } | ||
| 125 | 122 | ||||
| 126 | func TestMonitorFullscreenRequestStaysPut(t *testing.T) { | 123 | func TestMonitorFullscreenRequestStaysPut(t *testing.T) { | ||
| @@ -278,9 +275,12 @@ func TestHiddenWorkspaceToggle(t *testing.T) { | |||||
| 278 | if id, _ := m.Focused(); id != 1 { | 275 | if id, _ := m.Focused(); id != 1 { | ||
| 279 | t.Fatal(id) | 276 | t.Fatal(id) | ||
| 280 | } | 277 | } | ||
| 281 | - | // Clicking a window of the hidden workspace shows it (FocusID). | 278 | + | // Moving a window up/down from a hidden workspace does nothing. |
| 282 | - | if !m.FocusID(2) || m.Current().Name != "dev" { | 279 | + | m.Apply("workspace dev") |
| 283 | - | t.Fatal("FocusID did not show the hidden workspace") | 280 | + | m.Apply(ActionMoveToWorkspaceDown) |
| 281 | + | m.Apply(ActionMoveToWorkspaceUp) | |||
| 282 | + | if id, _ := m.Focused(); id != 2 || m.Current().Name != "dev" { | |||
| 283 | + | t.Fatal(id, windows(m)) | |||
| 284 | } | 284 | } | ||
| 285 | } | 285 | } | ||
| 286 | 286 | ||||
| @@ -346,3 +346,74 @@ func TestNamedWorkspaceSettings(t *testing.T) { | |||||
| 346 | t.Fatal(m.Current().MaxColumns, m.Current().Overflow) | 346 | t.Fatal(m.Current().MaxColumns, m.Current().Overflow) | ||
| 347 | } | 347 | } | ||
| 348 | } | 348 | } | ||
| 349 | + | ||||
| 350 | + | // checkInvariants fails when the monitor is in a state no action should reach. | |||
| 351 | + | func checkInvariants(t *testing.T, m *Monitor, want []WindowID) { | |||
| 352 | + | t.Helper() | |||
| 353 | + | if m.Active < 0 || m.Active >= len(m.Workspaces) { | |||
| 354 | + | t.Fatalf("active %d of %d", m.Active, len(m.Workspaces)) | |||
| 355 | + | } | |||
| 356 | + | if last := m.Workspaces[len(m.Workspaces)-1]; !last.empty() || last.Name != "" { | |||
| 357 | + | t.Fatal("no trailing empty unnamed workspace") | |||
| 358 | + | } | |||
| 359 | + | if m.shown != nil && indexOf(m.hidden, m.shown) < 0 { | |||
| 360 | + | t.Fatal("shown is not a hidden workspace") | |||
| 361 | + | } | |||
| 362 | + | if m.back != nil && !m.has(m.back) { | |||
| 363 | + | t.Fatal("back dangles") | |||
| 364 | + | } | |||
| 365 | + | seen := map[WindowID]int{} | |||
| 366 | + | for _, w := range append(append([]*Workspace(nil), m.Workspaces...), m.hidden...) { | |||
| 367 | + | for _, c := range w.Columns { | |||
| 368 | + | for _, id := range c.Windows { | |||
| 369 | + | seen[id]++ | |||
| 370 | + | } | |||
| 371 | + | } | |||
| 372 | + | } | |||
| 373 | + | for _, id := range want { | |||
| 374 | + | if seen[id] != 1 { | |||
| 375 | + | t.Fatalf("window %d present %d times", id, seen[id]) | |||
| 376 | + | } | |||
| 377 | + | } | |||
| 378 | + | if len(seen) != len(want) { | |||
| 379 | + | t.Fatalf("windows %v, want %v", seen, want) | |||
| 380 | + | } | |||
| 381 | + | } | |||
| 382 | + | ||||
| 383 | + | func TestSetNamedTransitions(t *testing.T) { | |||
| 384 | + | dev := NamedWorkspace{Name: "dev"} | |||
| 385 | + | devHidden := NamedWorkspace{Name: "dev", Hidden: true} | |||
| 386 | + | web := NamedWorkspace{Name: "web"} | |||
| 387 | + | for _, tc := range []struct { | |||
| 388 | + | name string | |||
| 389 | + | before []NamedWorkspace | |||
| 390 | + | after []NamedWorkspace | |||
| 391 | + | onDev bool // show dev before the reload | |||
| 392 | + | want string // workspace name on screen after | |||
| 393 | + | }{ | |||
| 394 | + | {"numbered active becomes hidden", []NamedWorkspace{dev}, []NamedWorkspace{devHidden}, true, "dev"}, | |||
| 395 | + | {"hidden shown becomes numbered", []NamedWorkspace{devHidden}, []NamedWorkspace{dev}, true, "dev"}, | |||
| 396 | + | {"rename drops the old name", []NamedWorkspace{dev}, []NamedWorkspace{web}, true, ""}, | |||
| 397 | + | {"hidden shown is removed", []NamedWorkspace{devHidden}, nil, true, ""}, | |||
| 398 | + | {"hidden not shown is removed", []NamedWorkspace{devHidden, web}, []NamedWorkspace{web}, false, ""}, | |||
| 399 | + | {"add while on numbered", nil, []NamedWorkspace{devHidden, web}, false, ""}, | |||
| 400 | + | } { | |||
| 401 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 402 | + | m := named(monitor(), tc.before...) | |||
| 403 | + | m.AddWindow(1) | |||
| 404 | + | if tc.onDev { | |||
| 405 | + | m.Apply("workspace dev") | |||
| 406 | + | } | |||
| 407 | + | m.AddWindow(2) | |||
| 408 | + | checkInvariants(t, m, []WindowID{1, 2}) | |||
| 409 | + | m.SetNamed(tc.after) | |||
| 410 | + | checkInvariants(t, m, []WindowID{1, 2}) | |||
| 411 | + | if got := m.Current().Name; got != tc.want { | |||
| 412 | + | t.Fatalf("on %q, want %q", got, tc.want) | |||
| 413 | + | } | |||
| 414 | + | if id, _ := m.Focused(); id != 2 { | |||
| 415 | + | t.Fatalf("focus moved to %d", id) | |||
| 416 | + | } | |||
| 417 | + | }) | |||
| 418 | + | } | |||
| 419 | + | } | |||
M internal/ports/config.go +0 -2
| @@ -58,10 +58,8 @@ type Config struct { | |||
| 58 | 58 | // WorkspaceConfig declares a named workspace. Hidden ones are not numbered and | |
| 59 | 59 | // only reachable through a `workspace <name>` bind, which toggles them. | |
| 60 | 60 | // Zero MaxColumns and an empty Overflow use the layout.* defaults. | |
| 61 | - | // Monitor names the connector it belongs to (one monitor is supported so far). | |
| 62 | 61 | type WorkspaceConfig struct { | |
| 63 | 62 | Name string | |
| 64 | - | Monitor string | |
| 65 | 63 | Hidden bool | |
| 66 | 64 | MaxColumns int | |
| 67 | 65 | Overflow string | |
| @@ -58,10 +58,8 @@ type Config struct { | |||||
| 58 | // WorkspaceConfig declares a named workspace. Hidden ones are not numbered and | 58 | // WorkspaceConfig declares a named workspace. Hidden ones are not numbered and | ||
| 59 | // only reachable through a `workspace <name>` bind, which toggles them. | 59 | // only reachable through a `workspace <name>` bind, which toggles them. | ||
| 60 | // Zero MaxColumns and an empty Overflow use the layout.* defaults. | 60 | // Zero MaxColumns and an empty Overflow use the layout.* defaults. | ||
| 61 | - | // Monitor names the connector it belongs to (one monitor is supported so far). | |||
| 62 | type WorkspaceConfig struct { | 61 | type WorkspaceConfig struct { | ||
| 63 | Name string | 62 | Name string | ||
| 64 | - | Monitor string | |||
| 65 | Hidden bool | 63 | Hidden bool | ||
| 66 | MaxColumns int | 64 | MaxColumns int | ||
| 67 | Overflow string | 65 | Overflow string | ||