Skip to content

Commit ea0f9a8

Browse files
committed
feat(setup): make a fresh install land in Flock, not plain Zellij
Everything that distinguishes Flock — the sidebar dock, the project selector, agent status — was opt-in through hand-authored KDL, so `flock` with no config was byte-for-byte upstream `zellij`. Phase 0 of the out-of-the-box plan: - default_layout ships as "flock" instead of being unset, so the sidebar is docked on startup. - Super s opens the project selector. Super is swallowed by some terminals before Flock sees it; documented, with the rebind. - flock-selector and flock-sidebar are registered as plugin aliases, and the bundled layouts now reference those aliases instead of restating args. Folder args are therefore stated once in config.kdl and reach the layouts, the keybinding, and each project session's sidebar alike. - Sessions created from the selector default to the `flock` layout. They previously got Zellij's `default`, leaving a project opened *through* Flock with no sidebar and no way back to the selector. The alias indirection removes an existing bug class by construction rather than by discipline: a keybinding whose arg set disagreed with the layout's used to miss the running selector and launch a second one (see the subset-match test in plugins/plugin_map.rs). With one source of truth the sets cannot disagree. Because the alias is resolved late and silently — an unresolved alias just loads no plugin and renders an empty dock — both bundled layouts get a test asserting they resolve against the shipped default config. BREAKING: a custom ~/.config/flock/layouts/default.kdl is no longer loaded on startup, since resolution now looks for the name "flock". Set default_layout "default" to restore it, or rename the file to flock.kdl. Pinned by tests in both directions.
1 parent 7b3772f commit ea0f9a8

22 files changed

Lines changed: 2010 additions & 50 deletions

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,28 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
66

77
## [Unreleased]
8+
* feat(setup): make a fresh install land in Flock rather than in plain Zellij.
9+
Everything that distinguishes Flock — the sidebar dock, the project selector,
10+
agent status — was previously opt-in through hand-authored KDL, so `flock` with
11+
no config behaved exactly like upstream `zellij`. Now:
12+
- `default_layout` ships as `"flock"` (the default chrome plus the sidebar as a
13+
left-edge dock) instead of being unset.
14+
- `Super s` opens the project selector.
15+
- `flock-selector` and `flock-sidebar` are registered as plugin aliases, so
16+
their folder args are stated **once** in `config.kdl` and reach the bundled
17+
layouts, the keybinding, and each project session's sidebar alike. The
18+
bundled layouts now reference those aliases instead of restating args, which
19+
removes by construction the arg-set disagreement that made a keybinding
20+
launch a second selector.
21+
- Sessions created from the selector default to the `flock` layout, so a
22+
project opened through Flock has the sidebar. Previously they got Zellij's
23+
`default` layout with no way back to the selector.
24+
25+
**Breaking:** a custom `~/.config/flock/layouts/default.kdl` is no longer
26+
loaded on startup, because layout resolution now looks for the name `flock`.
27+
Set `default_layout "default"` in your `config.kdl` to get it back, or rename
28+
the file to `flock.kdl`. Setting `default_layout` to any explicit value has
29+
always taken precedence and still does.
830

931
## [26.10.1] - 2026-07-28
1032
* fix(remote): make the sidebar's upgrade confirmation mouse-reachable, report

README.md

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -50,27 +50,64 @@ cargo xtask run
5050
cargo xtask test
5151
```
5252

53-
## Enable Flock
53+
## Configure Flock
5454

55-
The bundled `flock-selector` and `flock-sidebar` layouts work out of the box.
56-
Remote providers are opt-in:
55+
Flock works with no configuration: it starts with the sidebar docked to the left
56+
edge, and `Super s` opens the project selector. The one thing it cannot guess is
57+
where your projects live.
58+
59+
Point it at them in `~/.config/flock/config.kdl` by giving the bundled
60+
`flock-selector` alias its folder args:
5761

5862
```kdl
59-
plugin location="zellij:flock-selector" {
60-
root_dirs "~/src"
61-
codespaces_enabled "true"
62-
devcontainers_enabled "true"
63-
coder_enabled "true"
63+
plugins {
64+
flock-selector location="zellij:flock-selector" {
65+
cwd "/"
66+
root_dirs "~/src;~/work" // each scanned one level deep
67+
individual_dirs "~/dotfiles" // each is itself one project
68+
}
69+
flock-sidebar location="zellij:flock-sidebar" {
70+
cwd "/"
71+
root_dirs "~/src;~/work"
72+
individual_dirs "~/dotfiles"
73+
}
6474
}
75+
```
76+
77+
These are *aliases*, so this is the only place the args are stated — the bundled
78+
layouts, the `Super s` keybinding, and the sidebar in each project session all
79+
resolve through them. Run `flock setup --dump-config` to see the shipped defaults
80+
with every option documented inline.
6581

66-
plugin location="zellij:flock-sidebar" {
67-
root_dirs "~/src"
68-
codespaces_enabled "true"
69-
devcontainers_enabled "true"
70-
coder_enabled "true"
82+
If `Super s` does nothing, your terminal is intercepting `Super` (Cmd on macOS)
83+
before Flock sees it. Bind a key that does reach the app — this adds to the
84+
shipped binding rather than replacing it, which is harmless:
85+
86+
```kdl
87+
keybinds {
88+
shared_except "locked" {
89+
bind "Alt s" {
90+
LaunchOrFocusPlugin "flock-selector" {
91+
floating true
92+
move_to_focused_tab true
93+
};
94+
}
95+
}
7196
}
7297
```
7398

99+
Remote providers are opt-in, because each needs an authenticated CLI on `PATH`.
100+
Add them to the aliases above:
101+
102+
```kdl
103+
codespaces_enabled "true"
104+
devcontainers_enabled "true"
105+
coder_enabled "true"
106+
ssh_enabled "true"
107+
```
108+
109+
To get the plain upstream Zellij chrome instead, set `default_layout "default"`.
110+
74111
Provider requirements:
75112

76113
- Codespaces: authenticated `gh` CLI.

default-plugins/flock-selector/src/config.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,13 @@ use std::collections::BTreeMap;
1919
use std::path::{Path, PathBuf};
2020

2121
/// Layout new sessions open with when none is configured.
22-
pub const DEFAULT_SESSION_LAYOUT: &str = "default";
22+
///
23+
/// The bundled `flock` layout, not Zellij's `default`: a session opened *from
24+
/// the selector* that lacked the sidebar dock had no way back to the selector
25+
/// and no agent status, which is the entire reason to pick a project through
26+
/// Flock. Remote-bound sessions already default to a dock-bearing layout (see
27+
/// `codespaces::FLOCK_LAYOUT_TEMPLATE`); this makes local ones agree.
28+
pub const DEFAULT_SESSION_LAYOUT: &str = "flock";
2329
pub const DEFAULT_CODER_DOTFILES_PARAMETER: &str = "dotfiles_uri";
2430
pub const DEFAULT_CODER_DOTFILES_BRANCH_PARAMETER: &str = "dotfiles_branch";
2531

zellij-utils/assets/config/default.kdl

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,21 @@ keybinds {
204204
// conjure a second dock pane, which is exactly what a plugin-URL pipe does
205205
// when the keybind's configuration doesn't match the layout's.
206206
bind "Alt b" { MessagePlugin { name "flock-toggle-dock"; }; }
207+
// Open the project selector. Deliberately bound through the
208+
// `flock-selector` *alias* (declared in the `plugins` block below) and
209+
// with no configuration of its own: the alias is the single place the
210+
// selector's args are stated, so this binding can never disagree with
211+
// the layout's copy and launch a second selector.
212+
//
213+
// Note that `Super` (Cmd on macOS) is swallowed by some terminal
214+
// emulators before Flock ever sees it. Rebind this to eg. "Alt s" if
215+
// the key does nothing in your terminal.
216+
bind "Super s" {
217+
LaunchOrFocusPlugin "flock-selector" {
218+
floating true
219+
move_to_focused_tab true
220+
};
221+
}
207222
}
208223
shared_except "normal" "locked" {
209224
bind "Enter" "Esc" { SwitchToMode "Normal"; }
@@ -248,6 +263,51 @@ plugins {
248263
configuration location="zellij:configuration"
249264
plugin-manager location="zellij:plugin-manager"
250265
about location="zellij:about"
266+
267+
// The two Flock plugins. Aliasing them here is what makes their
268+
// configuration statable *once*: a layout, a keybinding and a generated
269+
// remote layout can all say `location="flock-selector"` with no args, and
270+
// every one of them resolves to the same configuration. Adding args at a
271+
// call site still overrides the alias, so a single layout can opt out.
272+
//
273+
// Point the selector at your projects by giving the alias its folder args:
274+
//
275+
// flock-selector location="zellij:flock-selector" {
276+
// root_dirs "~/src;~/work" // each scanned one level deep
277+
// individual_dirs "~/dotfiles" // each is itself one project
278+
// }
279+
//
280+
// The sidebar takes the same folder args and uses them to decide which
281+
// sessions belong to which project, so keep the two in sync.
282+
//
283+
// Remote providers are opt-in and off by default, because each needs an
284+
// authenticated CLI on PATH to work at all:
285+
//
286+
// codespaces_enabled "true" // needs an authenticated `gh`
287+
// devcontainers_enabled "true" // needs the `devcontainer` CLI + Docker
288+
// coder_enabled "true" // needs an authenticated `coder`
289+
// ssh_enabled "true" // saved-hosts tab; Ctrl-o adds a host.
290+
// // Key or agent auth is required — the
291+
// // transport runs ssh with BatchMode=yes.
292+
//
293+
// Selector-only (the sidebar never creates workspaces), and applied only
294+
// when the chosen Coder template exposes the named parameter:
295+
//
296+
// coder_dotfiles_uri "https://github.com/example/dotfiles.git"
297+
// coder_dotfiles_branch "main"
298+
// coder_dotfiles_parameter "dotfiles_uri"
299+
// coder_dotfiles_branch_parameter "dotfiles_branch"
300+
//
301+
// `cwd` is a reserved plugin property: it sets the plugin's initial working
302+
// directory (its `/host` mount) and is deliberately *not* forwarded as a
303+
// configuration key. Both plugins need `/` because the project folders they
304+
// scan are arbitrary host paths outside any single project.
305+
flock-selector location="zellij:flock-selector" {
306+
cwd "/"
307+
}
308+
flock-sidebar location="zellij:flock-sidebar" {
309+
cwd "/"
310+
}
251311
}
252312

253313
// Plugins to load in the background when a new session starts
@@ -344,11 +404,15 @@ load_plugins {
344404
//
345405
// theme "default"
346406

347-
// The name of the default layout to load on startup
348-
// Default: "default"
407+
// The name of the default layout to load on startup.
349408
// (Requires restart)
350409
//
351-
// default_layout "compact"
410+
// Flock ships "flock" rather than upstream Zellij's "default": it is the
411+
// "default" layout plus the flock-sidebar as a left-edge dock, which is what
412+
// makes agent status and session switching visible without any setup. Set this
413+
// to "default" for the plain upstream Zellij chrome, or to "compact" /
414+
// "classic" / "strider" for the other bundled layouts.
415+
default_layout "flock"
352416

353417
// Choose the mode that zellij uses when starting up.
354418
// Default: normal

zellij-utils/assets/layouts/flock-selector.kdl

Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
// The `flock-selector` layout floats the project picker over a fresh session —
2-
// the cold-shell entry point for a `zf = "zellij --layout flock-selector"`
3-
// alias, mirroring the user's `zs = "zellij --layout sessionizer"`.
2+
// the cold-shell entry point, reached with `flock --layout flock-selector`.
43
//
5-
// Folder sources are passed to the plugin as KDL args, matching
6-
// zellij-sessionizer's arg shape (individual_dirs / root_dirs / session_layout /
7-
// cwd). The values below are placeholders — a real config (e.g. the nix module)
8-
// overrides them by shipping its own copy of this layout, or the user edits it.
4+
// This layout deliberately carries no folder args. The picker resolves them
5+
// from the `flock-selector` alias in config.kdl, which is the one place they
6+
// are stated: adding `root_dirs` there reaches this layout, the `Super s`
7+
// keybinding, and the sidebar in every project session alike. Restating them
8+
// here would not just be redundant — a set that disagreed with the keybinding's
9+
// would make `Super s` miss the running picker and launch a second one.
910
layout {
1011
// Keep the cold-selector session alive until the floating picker is ready.
1112
// An interactive default shell can exit during startup (eg. from shell
@@ -16,25 +17,11 @@ layout {
1617
}
1718
floating_panes {
1819
pane {
19-
plugin location="zellij:flock-selector" {
20-
cwd "/"
20+
plugin location="flock-selector" {
2121
// Rename this cold-shell entry session to a fixed name so it is
2222
// always the same session (and the sidebar hides it) instead of
2323
// a random throwaway name accumulating in the workspace list.
2424
session_name "flock-selector"
25-
// individual_dirs "/path/to/project;/another/project"
26-
// root_dirs "/path/to/projects;/path/to/work"
27-
// session_layout "default"
28-
// coder_enabled "true"
29-
// Saved-SSH-hosts tab (Ctrl-o adds a host; key/agent auth is
30-
// required — the transport runs ssh with BatchMode=yes).
31-
// ssh_enabled "true"
32-
// Optional, applied only when the selected Coder template
33-
// exposes this conventional parameter.
34-
// coder_dotfiles_uri "https://github.com/example/dotfiles.git"
35-
// coder_dotfiles_branch "main"
36-
// coder_dotfiles_parameter "dotfiles_uri"
37-
// coder_dotfiles_branch_parameter "dotfiles_branch"
3825
}
3926
width "70%"
4027
height "70%"

zellij-utils/assets/layouts/flock.kdl

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,12 @@ layout {
99
// owns which one is in effect (Alt b toggles it) and clamps both so the
1010
// content area always stays usable. `closed_size` must stay under the
1111
// sidebar's 16-column label threshold for the rail to render.
12+
//
13+
// Referenced through the `flock-sidebar` alias (not `zellij:flock-sidebar`)
14+
// so the folder args a user adds to that alias in config.kdl reach the
15+
// sidebar here without editing this layout.
1216
dock size=40 closed_size=5 {
13-
plugin location="zellij:flock-sidebar"
17+
plugin location="flock-sidebar"
1418
}
1519
pane size=1 borderless=true {
1620
plugin location="tab-bar"

zellij-utils/src/input/unit/layout_test.rs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2581,6 +2581,79 @@ fn flock_builtin_layout_loads_with_swap_layout() {
25812581
.unwrap();
25822582
}
25832583

2584+
#[test]
2585+
fn flock_bundled_layout_dock_resolves_through_the_shipped_alias() {
2586+
// The bundled flock layout names the sidebar by *alias*
2587+
// (`location="flock-sidebar"`), not by `zellij:flock-sidebar`, so that folder
2588+
// args a user adds to that alias in config.kdl reach the sidebar without
2589+
// editing the layout. That indirection is silent when it breaks: an
2590+
// unresolved alias simply loads no plugin and the dock renders empty. Assert
2591+
// the shipped default config actually resolves it.
2592+
let kdl_layout = include_str!("../../../assets/layouts/flock.kdl");
2593+
let mut layout = Layout::from_kdl(kdl_layout, Some("flock.kdl".into()), None, None).unwrap();
2594+
2595+
let dock_run = &dock_of(&layout).run;
2596+
assert!(
2597+
matches!(dock_run, RunPluginOrAlias::Alias(alias) if alias.name == "flock-sidebar"),
2598+
"the dock should name the sidebar by alias, got {:?}",
2599+
dock_run
2600+
);
2601+
assert!(
2602+
dock_run.get_run_plugin().is_none(),
2603+
"an alias should be unresolved before populating"
2604+
);
2605+
2606+
let default_config = crate::input::config::Config::from_default_assets().unwrap();
2607+
layout.populate_plugin_aliases_in_layout(&default_config.plugins);
2608+
2609+
let resolved = dock_of(&layout)
2610+
.run
2611+
.get_run_plugin()
2612+
.expect("the shipped default config must define a `flock-sidebar` alias");
2613+
assert_eq!(
2614+
resolved.location,
2615+
RunPluginLocation::parse("zellij:flock-sidebar", None).unwrap(),
2616+
"the alias must resolve to the built-in flock-sidebar plugin"
2617+
);
2618+
}
2619+
2620+
#[test]
2621+
fn flock_selector_bundled_layout_resolves_through_the_shipped_alias() {
2622+
// Same contract for the cold-shell picker layout. It keeps only
2623+
// `session_name` as a call-site arg; everything else must come from the
2624+
// alias, so that this layout and the `Super s` keybinding agree on
2625+
// configuration and the keybinding focuses the running picker instead of
2626+
// launching a second one.
2627+
let (layout_path, kdl_layout, _) =
2628+
Layout::stringified_from_default_assets(std::path::Path::new("flock-selector")).unwrap();
2629+
let mut layout = Layout::from_kdl(&kdl_layout, Some(layout_path), None, None).unwrap();
2630+
2631+
let default_config = crate::input::config::Config::from_default_assets().unwrap();
2632+
layout.populate_plugin_aliases_in_layout(&default_config.plugins);
2633+
2634+
let (_, floating_panes) = layout.template.clone().expect("layout has a template");
2635+
let selector = floating_panes
2636+
.iter()
2637+
.find_map(|pane| match pane.run.as_ref() {
2638+
Some(Run::Plugin(run)) => run.get_run_plugin(),
2639+
_ => None,
2640+
})
2641+
.expect("the selector layout floats a resolved plugin pane");
2642+
assert_eq!(
2643+
selector.location,
2644+
RunPluginLocation::parse("zellij:flock-selector", None).unwrap(),
2645+
);
2646+
assert_eq!(
2647+
selector
2648+
.configuration
2649+
.inner()
2650+
.get("session_name")
2651+
.map(|s| s.as_str()),
2652+
Some("flock-selector"),
2653+
"the call-site session_name must survive alias merging"
2654+
);
2655+
}
2656+
25842657
fn dock_of(layout: &Layout) -> &DockLayout {
25852658
layout.dock.as_ref().expect("layout should have a dock")
25862659
}

zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string.snap

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,12 @@ keybinds clear-defaults=true {
172172
bind "Alt p" { TogglePaneInGroup; }
173173
bind "Alt Shift p" { ToggleGroupMarking; }
174174
bind "Ctrl q" { Quit; }
175+
bind "Super s" {
176+
LaunchOrFocusPlugin "flock-selector" {
177+
floating true
178+
move_to_focused_tab true
179+
}
180+
}
175181
}
176182
shared_except "locked" "move" {
177183
bind "Ctrl h" { SwitchToMode "move"; }
@@ -267,6 +273,12 @@ plugins {
267273
filepicker location="zellij:strider" {
268274
cwd "/"
269275
}
276+
flock-selector location="zellij:flock-selector" {
277+
cwd "/"
278+
}
279+
flock-sidebar location="zellij:flock-sidebar" {
280+
cwd "/"
281+
}
270282
plugin-manager location="zellij:plugin-manager"
271283
session-manager location="zellij:session-manager"
272284
status-bar location="zellij:status-bar"
@@ -282,4 +294,5 @@ load_plugins {
282294
web_client {
283295
font "monospace"
284296
}
297+
default_layout "flock"
285298

0 commit comments

Comments
 (0)