mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-11-15 13:22:19 +01:00
On NixOS the key bindings belong in the configuration, with the rest of
komorebi. services.komorebi.keybindings takes the format (skhd, whkd or sxhkd),
the bindings as text or as a file (exactly one of the two, asserted), the mode
(auto, engine or static; engine by default, as PlanMode::default() is),
chordTimeout (3000 ms), layout (us, the only one), strict and appAliases. skhd's
.load is refused by an assertion: the file it reads would not be in the store.
At build time the module runs komorebic hotkeys compile on the file in the
package komorebi-keybindings, whose share/kglobalaccel goes into the system
profile, where KWin finds the static shortcuts when the session starts. Its
diagnostics are in the build log and in diagnostics.txt, and strict passes
--strict, which fails the build on a warning or an error. execStart gains
--keybindings with the same store file, its format and mode, --chord-timeout and
an --app-alias per alias, so the daemon plans the file as the build did and runs
the bindings the build left to the engine. The unit already releases the keys
after the daemon stops.
The check keybindings-fixtures runs every file of the fixture corpus through
komorebic hotkeys compile --mode auto --dry-run, the command the module runs,
and diffs what it prints against nix/tests/keybindings.expected; it also checks
that docs/{skhdrc,whkdrc,sxhkdrc}.sample are the corpus's sample case.
nixos-module-eval gains the key binding scenarios: text, file, both, no format,
.load and a layout that is not us, and one with the typed config together with
key bindings, whose execStart has the checked file and then the keys. The corpus
gains the case vm, in the three formats: a binding a shortcut file can hold, and
one on the comma key, which X-KDE-Shortcuts cannot, so that the engine runs it
in auto mode. The VM suites vm-keys-skhd, vm-keys-whkd and vm-keys-sxhkd
configure the module with that case in their format and press both keys; they
share one script through the generator's script attribute.
For strict, nixos-module-eval has two scenarios with the same skhd text, which
has a line komorebic cannot read: strict = true and the default. Evaluation
never reads the key bindings, so the checks read the script of the build: with
strict it runs hotkeys compile --strict and exits 1 when that fails, without it
there is no --strict and the diagnostics are only printed to the build log. A
third check says the daemon gets the same command line either way. ARCHITECTURE
names them in the row for strict.
komorebic quickstart writes the sample skhdrc next to komorebi.json and
applications.json, from docs/skhdrc.sample, where the daemon and hotkeys compile
read it; the flake's source filter keeps that file for the build.
Docs: the user guide docs/usage/keybindings.md, the three sample files,
docs/example-configurations.md for KDE and NixOS, the options in
docs/installation.md, and the module's section of ARCHITECTURE. Whether the
engine or static shortcuts should be the default stays open in DESIGN: a static
shortcut took 233 ms to run its command at the median on KWin 6.8.1, the engine
3 ms, but only the static shortcut survives a dead daemon.
1309 lines
41 KiB
Rust
1309 lines
41 KiB
Rust
//! `komorebic` as a person runs it, against a socket the test serves in place of the daemon: what
|
|
//! each subcommand writes on the socket, and what it prints.
|
|
|
|
use std::io::Read;
|
|
use std::io::Write;
|
|
use std::os::unix::net::UnixListener;
|
|
use std::path::Path;
|
|
use std::path::PathBuf;
|
|
use std::process::Command;
|
|
use std::process::Output;
|
|
use std::sync::atomic::AtomicU32;
|
|
use std::sync::atomic::Ordering;
|
|
use std::thread::JoinHandle;
|
|
|
|
/// A runtime directory for one test, short enough for a unix socket path on macOS, with the
|
|
/// `komorebi` directory the daemon would make in it
|
|
struct Runtime(PathBuf);
|
|
|
|
impl Runtime {
|
|
fn new() -> Self {
|
|
static NEXT: AtomicU32 = AtomicU32::new(0);
|
|
let dir = PathBuf::from(format!(
|
|
"/tmp/komorebic-test-{}-{}",
|
|
std::process::id(),
|
|
NEXT.fetch_add(1, Ordering::SeqCst)
|
|
));
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
std::fs::create_dir_all(dir.join("komorebi")).unwrap();
|
|
Self(dir)
|
|
}
|
|
|
|
/// Listens on the daemon's socket, takes one connection, and answers what it reads on it with
|
|
/// `answer`. The thread returns what it read.
|
|
fn serve(&self, answer: &'static str) -> JoinHandle<String> {
|
|
let listener = UnixListener::bind(self.0.join("komorebi").join("komorebi.sock")).unwrap();
|
|
|
|
std::thread::spawn(move || {
|
|
let (mut stream, _) = listener.accept().unwrap();
|
|
let mut message = String::new();
|
|
stream.read_to_string(&mut message).unwrap();
|
|
stream.write_all(answer.as_bytes()).unwrap();
|
|
message
|
|
})
|
|
}
|
|
|
|
fn komorebic(&self, args: &[&str]) -> Output {
|
|
Command::new(env!("CARGO_BIN_EXE_komorebic"))
|
|
.args(args)
|
|
.env("XDG_RUNTIME_DIR", &self.0)
|
|
.output()
|
|
.unwrap()
|
|
}
|
|
|
|
/// Runs `komorebic` with `KOMOREBI_CONFIG_HOME` set to `home`
|
|
fn komorebic_with_config_home(&self, home: &Path, args: &[&str]) -> Output {
|
|
Command::new(env!("CARGO_BIN_EXE_komorebic"))
|
|
.args(args)
|
|
.env("XDG_RUNTIME_DIR", &self.0)
|
|
.env("KOMOREBI_CONFIG_HOME", home)
|
|
.output()
|
|
.unwrap()
|
|
}
|
|
}
|
|
|
|
impl Drop for Runtime {
|
|
fn drop(&mut self) {
|
|
let _ = std::fs::remove_dir_all(&self.0);
|
|
}
|
|
}
|
|
|
|
fn stdout(output: &Output) -> String {
|
|
String::from_utf8_lossy(&output.stdout).into_owned()
|
|
}
|
|
|
|
#[test]
|
|
fn state_prints_the_answer_of_the_daemon() {
|
|
let runtime = Runtime::new();
|
|
let daemon = runtime.serve(r#"{"monitors":{"elements":[],"focused":0}}"#);
|
|
|
|
let output = runtime.komorebic(&["state"]);
|
|
|
|
assert!(output.status.success(), "{output:?}");
|
|
assert_eq!(daemon.join().unwrap(), r#"{"type":"State"}"#);
|
|
assert_eq!(
|
|
stdout(&output),
|
|
"{\"monitors\":{\"elements\":[],\"focused\":0}}\n"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn query_asks_the_question_it_is_given() {
|
|
let runtime = Runtime::new();
|
|
let daemon = runtime.serve("1");
|
|
|
|
let output = runtime.komorebic(&["query", "focused-workspace-index"]);
|
|
|
|
assert!(output.status.success(), "{output:?}");
|
|
assert_eq!(
|
|
daemon.join().unwrap(),
|
|
r#"{"type":"Query","content":"FocusedWorkspaceIndex"}"#
|
|
);
|
|
assert_eq!(stdout(&output), "1\n");
|
|
}
|
|
|
|
#[test]
|
|
fn monitor_information_prints_the_answer_of_the_daemon() {
|
|
let runtime = Runtime::new();
|
|
let daemon = runtime.serve("[]");
|
|
|
|
let output = runtime.komorebic(&["monitor-info"]);
|
|
|
|
assert!(output.status.success(), "{output:?}");
|
|
assert_eq!(daemon.join().unwrap(), r#"{"type":"MonitorInformation"}"#);
|
|
assert_eq!(stdout(&output), "[]\n");
|
|
}
|
|
|
|
#[test]
|
|
fn stop_and_retile_send_their_message_and_print_nothing() {
|
|
for (subcommand, message) in [
|
|
("stop", r#"{"type":"Stop"}"#),
|
|
("retile", r#"{"type":"Retile"}"#),
|
|
] {
|
|
let runtime = Runtime::new();
|
|
let daemon = runtime.serve("");
|
|
|
|
let output = runtime.komorebic(&[subcommand]);
|
|
|
|
assert!(output.status.success(), "{subcommand}: {output:?}");
|
|
assert_eq!(daemon.join().unwrap(), message, "{subcommand}");
|
|
assert_eq!(stdout(&output), "", "{subcommand}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn stop_with_ignore_restore_sends_the_message_that_leaves_the_hidden_windows_hidden() {
|
|
let runtime = Runtime::new();
|
|
let daemon = runtime.serve("");
|
|
|
|
let output = runtime.komorebic(&["stop", "--ignore-restore"]);
|
|
|
|
assert!(output.status.success(), "{output:?}");
|
|
assert_eq!(daemon.join().unwrap(), r#"{"type":"StopIgnoreRestore"}"#);
|
|
assert_eq!(stdout(&output), "");
|
|
}
|
|
|
|
#[test]
|
|
fn subscribe_socket_and_unsubscribe_socket_send_the_name_they_are_given() {
|
|
for (subcommand, message) in [
|
|
(
|
|
"subscribe-socket",
|
|
r#"{"type":"AddSubscriberSocket","content":"bar.sock"}"#,
|
|
),
|
|
(
|
|
"unsubscribe-socket",
|
|
r#"{"type":"RemoveSubscriberSocket","content":"bar.sock"}"#,
|
|
),
|
|
] {
|
|
let runtime = Runtime::new();
|
|
let daemon = runtime.serve("");
|
|
|
|
let output = runtime.komorebic(&[subcommand, "bar.sock"]);
|
|
|
|
assert!(output.status.success(), "{subcommand}: {output:?}");
|
|
assert_eq!(daemon.join().unwrap(), message, "{subcommand}");
|
|
assert_eq!(stdout(&output), "", "{subcommand}");
|
|
}
|
|
}
|
|
|
|
/// Runs `komorebic` with `args` against a daemon that answers nothing, and checks that it wrote
|
|
/// `message` on the socket, printed nothing and exited with status 0
|
|
fn sends(args: &[&str], message: &str) {
|
|
let runtime = Runtime::new();
|
|
let daemon = runtime.serve("");
|
|
|
|
let output = runtime.komorebic(args);
|
|
|
|
assert!(output.status.success(), "{args:?}: {output:?}");
|
|
assert_eq!(daemon.join().unwrap(), message, "{args:?}");
|
|
assert_eq!(stdout(&output), "", "{args:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn focus_and_cycle_focus_send_the_direction_they_are_given() {
|
|
sends(
|
|
&["focus", "left"],
|
|
r#"{"type":"FocusWindow","content":"Left"}"#,
|
|
);
|
|
sends(
|
|
&["focus", "down"],
|
|
r#"{"type":"FocusWindow","content":"Down"}"#,
|
|
);
|
|
sends(
|
|
&["cycle-focus", "next"],
|
|
r#"{"type":"CycleFocusWindow","content":"Next"}"#,
|
|
);
|
|
sends(
|
|
&["cycle-focus", "previous"],
|
|
r#"{"type":"CycleFocusWindow","content":"Previous"}"#,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn move_and_cycle_move_send_the_direction_they_are_given() {
|
|
sends(
|
|
&["move", "right"],
|
|
r#"{"type":"MoveWindow","content":"Right"}"#,
|
|
);
|
|
sends(&["move", "up"], r#"{"type":"MoveWindow","content":"Up"}"#);
|
|
sends(
|
|
&["cycle-move", "previous"],
|
|
r#"{"type":"CycleMoveWindow","content":"Previous"}"#,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn the_layout_subcommands_send_the_layout_axis_or_direction_they_are_given() {
|
|
sends(
|
|
&["change-layout", "vertical-stack"],
|
|
r#"{"type":"ChangeLayout","content":"VerticalStack"}"#,
|
|
);
|
|
sends(
|
|
&["change-layout", "bsp"],
|
|
r#"{"type":"ChangeLayout","content":"BSP"}"#,
|
|
);
|
|
sends(
|
|
&["cycle-layout", "next"],
|
|
r#"{"type":"CycleLayout","content":"Next"}"#,
|
|
);
|
|
sends(
|
|
&["flip-layout", "horizontal-and-vertical"],
|
|
r#"{"type":"FlipLayout","content":"HorizontalAndVertical"}"#,
|
|
);
|
|
sends(&["toggle-tiling"], r#"{"type":"ToggleTiling"}"#);
|
|
sends(&["toggle-float"], r#"{"type":"ToggleFloat"}"#);
|
|
sends(
|
|
&["toggle-workspace-layer"],
|
|
r#"{"type":"ToggleWorkspaceLayer"}"#,
|
|
);
|
|
sends(&["toggle-lock"], r#"{"type":"ToggleLock"}"#);
|
|
sends(
|
|
&["preselect-direction", "up"],
|
|
r#"{"type":"PreselectDirection","content":"Up"}"#,
|
|
);
|
|
sends(&["cancel-preselect"], r#"{"type":"CancelPreselect"}"#);
|
|
}
|
|
|
|
#[test]
|
|
fn the_stack_subcommands_send_the_direction_or_the_index_they_are_given() {
|
|
sends(
|
|
&["stack", "left"],
|
|
r#"{"type":"StackWindow","content":"Left"}"#,
|
|
);
|
|
sends(
|
|
&["stack", "down"],
|
|
r#"{"type":"StackWindow","content":"Down"}"#,
|
|
);
|
|
sends(&["unstack"], r#"{"type":"UnstackWindow"}"#);
|
|
sends(&["stack-all"], r#"{"type":"StackAll"}"#);
|
|
sends(&["unstack-all"], r#"{"type":"UnstackAll"}"#);
|
|
sends(
|
|
&["cycle-stack", "next"],
|
|
r#"{"type":"CycleStack","content":"Next"}"#,
|
|
);
|
|
sends(
|
|
&["cycle-stack", "previous"],
|
|
r#"{"type":"CycleStack","content":"Previous"}"#,
|
|
);
|
|
sends(
|
|
&["cycle-stack-index", "next"],
|
|
r#"{"type":"CycleStackIndex","content":"Next"}"#,
|
|
);
|
|
sends(
|
|
&["focus-stack-window", "2"],
|
|
r#"{"type":"FocusStackWindow","content":2}"#,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn the_subcommands_for_new_windows_send_their_message() {
|
|
sends(
|
|
&["toggle-window-container-behaviour"],
|
|
r#"{"type":"ToggleWindowContainerBehaviour"}"#,
|
|
);
|
|
sends(
|
|
&["toggle-workspace-window-container-behaviour"],
|
|
r#"{"type":"ToggleWorkspaceWindowContainerBehaviour"}"#,
|
|
);
|
|
sends(
|
|
&["toggle-float-override"],
|
|
r#"{"type":"ToggleFloatOverride"}"#,
|
|
);
|
|
sends(
|
|
&["toggle-workspace-float-override"],
|
|
r#"{"type":"ToggleWorkspaceFloatOverride"}"#,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn scrolling_layout_columns_and_layout_ratios_send_what_they_are_given() {
|
|
sends(
|
|
&["scrolling-layout-columns", "3"],
|
|
r#"{"type":"ScrollingLayoutColumns","content":3}"#,
|
|
);
|
|
sends(
|
|
&["layout-ratios", "--columns", "0.6", "0.25", "--rows", "0.5"],
|
|
r#"{"type":"LayoutRatios","content":[[0.6,0.25],[0.5]]}"#,
|
|
);
|
|
sends(
|
|
&["layout-ratios", "--rows", "0.3"],
|
|
r#"{"type":"LayoutRatios","content":[null,[0.3]]}"#,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn layout_ratios_without_ratios_sends_nothing_and_says_so() {
|
|
let runtime = Runtime::new();
|
|
|
|
let output = runtime.komorebic(&["layout-ratios"]);
|
|
|
|
assert!(output.status.success(), "{output:?}");
|
|
assert!(
|
|
stdout(&output).starts_with("No ratios provided"),
|
|
"{output:?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn scrolling_layout_columns_refuses_zero() {
|
|
let runtime = Runtime::new();
|
|
|
|
let output = runtime.komorebic(&["scrolling-layout-columns", "0"]);
|
|
|
|
assert!(!output.status.success(), "{output:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn the_resize_subcommands_send_what_they_are_given() {
|
|
sends(
|
|
&["resize-edge", "left", "increase"],
|
|
r#"{"type":"ResizeWindowEdge","content":["Left","Increase"]}"#,
|
|
);
|
|
sends(
|
|
&["resize", "down", "decrease"],
|
|
r#"{"type":"ResizeWindowEdge","content":["Down","Decrease"]}"#,
|
|
);
|
|
sends(
|
|
&["resize-axis", "horizontal-and-vertical", "increase"],
|
|
r#"{"type":"ResizeWindowAxis","content":["HorizontalAndVertical","Increase"]}"#,
|
|
);
|
|
sends(
|
|
&["resize-delta", "75"],
|
|
r#"{"type":"ResizeDelta","content":75}"#,
|
|
);
|
|
sends(&["quick-save-resize"], r#"{"type":"QuickSave"}"#);
|
|
sends(&["quick-load"], r#"{"type":"QuickLoad"}"#);
|
|
sends(
|
|
&["save-resize", "/tmp/sizes.json"],
|
|
r#"{"type":"Save","content":"/tmp/sizes.json"}"#,
|
|
);
|
|
sends(
|
|
&["load", "/tmp/sizes.json"],
|
|
r#"{"type":"Load","content":"/tmp/sizes.json"}"#,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn the_promote_subcommands_send_their_message() {
|
|
sends(&["promote"], r#"{"type":"Promote"}"#);
|
|
sends(&["promote-swap"], r#"{"type":"PromoteSwap"}"#);
|
|
sends(&["promote-focus"], r#"{"type":"PromoteFocus"}"#);
|
|
sends(
|
|
&["promote-window", "right"],
|
|
r#"{"type":"PromoteWindow","content":"Right"}"#,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn the_padding_subcommands_send_the_workspace_and_the_pixels_they_are_given() {
|
|
sends(
|
|
&["focused-workspace-padding", "20"],
|
|
r#"{"type":"FocusedWorkspacePadding","content":20}"#,
|
|
);
|
|
sends(
|
|
&["focused-workspace-container-padding", "10"],
|
|
r#"{"type":"FocusedWorkspaceContainerPadding","content":10}"#,
|
|
);
|
|
sends(
|
|
&["workspace-padding", "0", "1", "12"],
|
|
r#"{"type":"WorkspacePadding","content":[0,1,12]}"#,
|
|
);
|
|
sends(
|
|
&["container-padding", "1", "0", "8"],
|
|
r#"{"type":"ContainerPadding","content":[1,0,8]}"#,
|
|
);
|
|
sends(
|
|
&["named-workspace-padding", "mail", "12"],
|
|
r#"{"type":"NamedWorkspacePadding","content":["mail",12]}"#,
|
|
);
|
|
sends(
|
|
&["named-workspace-container-padding", "mail", "8"],
|
|
r#"{"type":"NamedWorkspaceContainerPadding","content":["mail",8]}"#,
|
|
);
|
|
sends(
|
|
&["adjust-workspace-padding", "increase", "5"],
|
|
r#"{"type":"AdjustWorkspacePadding","content":["Increase",5]}"#,
|
|
);
|
|
sends(
|
|
&["adjust-container-padding", "decrease", "3"],
|
|
r#"{"type":"AdjustContainerPadding","content":["Decrease",3]}"#,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn the_offset_subcommands_send_the_offset_they_are_given() {
|
|
sends(
|
|
&["global-work-area-offset", "10", "20", "30", "40"],
|
|
r#"{"type":"WorkAreaOffset","content":{"left":10,"top":20,"right":30,"bottom":40}}"#,
|
|
);
|
|
sends(
|
|
&["monitor-work-area-offset", "1", "10", "20", "30", "40"],
|
|
r#"{"type":"MonitorWorkAreaOffset","content":[1,{"left":10,"top":20,"right":30,"bottom":40}]}"#,
|
|
);
|
|
sends(
|
|
&[
|
|
"workspace-work-area-offset",
|
|
"1",
|
|
"2",
|
|
"10",
|
|
"20",
|
|
"30",
|
|
"40",
|
|
],
|
|
r#"{"type":"WorkspaceWorkAreaOffset","content":[1,2,{"left":10,"top":20,"right":30,"bottom":40}]}"#,
|
|
);
|
|
sends(
|
|
&["toggle-window-based-work-area-offset"],
|
|
r#"{"type":"ToggleWindowBasedWorkAreaOffset"}"#,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn the_rule_subcommands_send_the_application_they_are_given() {
|
|
sends(
|
|
&["ignore-rule", "exe", "steam"],
|
|
r#"{"type":"IgnoreRule","content":["Exe","steam"]}"#,
|
|
);
|
|
sends(
|
|
&["float-rule", "class", "org.kde.kcalc"],
|
|
r#"{"type":"IgnoreRule","content":["Class","org.kde.kcalc"]}"#,
|
|
);
|
|
sends(
|
|
&["manage-rule", "title", "Picture-in-Picture"],
|
|
r#"{"type":"ManageRule","content":["Title","Picture-in-Picture"]}"#,
|
|
);
|
|
sends(&["session-float-rule"], r#"{"type":"SessionFloatRule"}"#);
|
|
sends(
|
|
&["clear-session-float-rules"],
|
|
r#"{"type":"ClearSessionFloatRules"}"#,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn global_state_and_session_float_rules_print_the_answer_of_the_daemon() {
|
|
for (args, message) in [
|
|
("global-state", r#"{"type":"GlobalState"}"#),
|
|
("session-float-rules", r#"{"type":"SessionFloatRules"}"#),
|
|
] {
|
|
let runtime = Runtime::new();
|
|
let daemon = runtime.serve("[]");
|
|
|
|
let output = runtime.komorebic(&[args]);
|
|
|
|
assert!(output.status.success(), "{output:?}");
|
|
assert_eq!(daemon.join().unwrap(), message);
|
|
assert_eq!(stdout(&output), "[]\n");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn the_border_subcommands_send_what_they_are_given() {
|
|
sends(&["border", "enable"], r#"{"type":"Border","content":true}"#);
|
|
sends(
|
|
&["border", "disable"],
|
|
r#"{"type":"Border","content":false}"#,
|
|
);
|
|
sends(
|
|
&["active-window-border", "enable"],
|
|
r#"{"type":"Border","content":true}"#,
|
|
);
|
|
sends(
|
|
&["border-colour", "10", "20", "30"],
|
|
r#"{"type":"BorderColour","content":["Single",10,20,30]}"#,
|
|
);
|
|
sends(
|
|
&[
|
|
"border-colour",
|
|
"--window-kind",
|
|
"unfocused-locked",
|
|
"1",
|
|
"2",
|
|
"3",
|
|
],
|
|
r#"{"type":"BorderColour","content":["UnfocusedLocked",1,2,3]}"#,
|
|
);
|
|
sends(
|
|
&["border-width", "8"],
|
|
r#"{"type":"BorderWidth","content":8}"#,
|
|
);
|
|
sends(
|
|
&["border-offset", "2"],
|
|
r#"{"type":"BorderOffset","content":2}"#,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn the_focus_follows_mouse_subcommands_send_what_they_are_given() {
|
|
sends(
|
|
&["focus-follows-mouse", "enable"],
|
|
r#"{"type":"FocusFollowsMouse","content":["Windows",true]}"#,
|
|
);
|
|
sends(
|
|
&[
|
|
"focus-follows-mouse",
|
|
"--implementation",
|
|
"komorebi",
|
|
"disable",
|
|
],
|
|
r#"{"type":"FocusFollowsMouse","content":["Komorebi",false]}"#,
|
|
);
|
|
sends(
|
|
&["toggle-focus-follows-mouse"],
|
|
r#"{"type":"ToggleFocusFollowsMouse","content":"Windows"}"#,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn the_transparency_subcommands_send_what_they_are_given() {
|
|
sends(
|
|
&["transparency", "enable"],
|
|
r#"{"type":"Transparency","content":true}"#,
|
|
);
|
|
sends(
|
|
&["transparency", "disable"],
|
|
r#"{"type":"Transparency","content":false}"#,
|
|
);
|
|
sends(
|
|
&["transparency-alpha", "150"],
|
|
r#"{"type":"TransparencyAlpha","content":150}"#,
|
|
);
|
|
sends(&["toggle-transparency"], r#"{"type":"ToggleTransparency"}"#);
|
|
}
|
|
|
|
#[test]
|
|
fn the_animation_subcommands_send_what_they_are_given() {
|
|
sends(
|
|
&["animation", "enable"],
|
|
r#"{"type":"Animation","content":[true,null]}"#,
|
|
);
|
|
sends(
|
|
&["animation", "disable", "--animation-type", "movement"],
|
|
r#"{"type":"Animation","content":[false,"movement"]}"#,
|
|
);
|
|
sends(
|
|
&["animation-duration", "120"],
|
|
r#"{"type":"AnimationDuration","content":[120,null]}"#,
|
|
);
|
|
sends(
|
|
&["animation-duration", "500", "-a", "movement"],
|
|
r#"{"type":"AnimationDuration","content":[500,"movement"]}"#,
|
|
);
|
|
sends(
|
|
&["animation-style", "--style", "ease-in-out-sine"],
|
|
r#"{"type":"AnimationStyle","content":["EaseInOutSine",null]}"#,
|
|
);
|
|
sends(
|
|
&["animation-style", "-a", "movement"],
|
|
r#"{"type":"AnimationStyle","content":["Linear","movement"]}"#,
|
|
);
|
|
sends(
|
|
&["animation-fps", "30"],
|
|
r#"{"type":"AnimationFps","content":30}"#,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn visible_windows_prints_the_answer_of_the_daemon() {
|
|
let runtime = Runtime::new();
|
|
let daemon = runtime.serve("{}");
|
|
|
|
let output = runtime.komorebic(&["visible-windows"]);
|
|
|
|
assert!(output.status.success(), "{output:?}");
|
|
assert_eq!(daemon.join().unwrap(), r#"{"type":"VisibleWindows"}"#);
|
|
assert_eq!(stdout(&output), "{}\n");
|
|
}
|
|
|
|
#[test]
|
|
fn the_subcommands_about_the_focused_window_and_pausing_send_their_message() {
|
|
sends(&["minimize"], r#"{"type":"Minimize"}"#);
|
|
sends(&["close"], r#"{"type":"Close"}"#);
|
|
sends(&["manage"], r#"{"type":"ManageFocusedWindow"}"#);
|
|
sends(&["unmanage"], r#"{"type":"UnmanageFocusedWindow"}"#);
|
|
sends(&["toggle-pause"], r#"{"type":"TogglePause"}"#);
|
|
sends(
|
|
&["unmanaged-window-operation-behaviour", "no-op"],
|
|
r#"{"type":"UnmanagedWindowOperationBehaviour","content":"NoOp"}"#,
|
|
);
|
|
sends(
|
|
&["unmanaged-window-operation-behaviour", "op"],
|
|
r#"{"type":"UnmanagedWindowOperationBehaviour","content":"Op"}"#,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn the_monocle_subcommands_send_their_message() {
|
|
sends(&["toggle-monocle"], r#"{"type":"ToggleMonocle"}"#);
|
|
sends(
|
|
&["monocle-focus-behaviour", "cycle"],
|
|
r#"{"type":"MonocleFocusBehaviour","content":"Cycle"}"#,
|
|
);
|
|
sends(
|
|
&["monocle-focus-behaviour", "no-op"],
|
|
r#"{"type":"MonocleFocusBehaviour","content":"NoOp"}"#,
|
|
);
|
|
sends(
|
|
&["toggle-monocle-focus-behaviour"],
|
|
r#"{"type":"ToggleMonocleFocusBehaviour"}"#,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn toggle_maximize_sends_its_message() {
|
|
sends(&["toggle-maximize"], r#"{"type":"ToggleMaximize"}"#);
|
|
}
|
|
|
|
#[test]
|
|
fn mouse_follows_focus_is_set_with_enable_and_disable_or_toggled() {
|
|
sends(
|
|
&["mouse-follows-focus", "enable"],
|
|
r#"{"type":"MouseFollowsFocus","content":true}"#,
|
|
);
|
|
sends(
|
|
&["mouse-follows-focus", "disable"],
|
|
r#"{"type":"MouseFollowsFocus","content":false}"#,
|
|
);
|
|
sends(
|
|
&["toggle-mouse-follows-focus"],
|
|
r#"{"type":"ToggleMouseFollowsFocus"}"#,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn the_workspace_subcommands_send_the_index_they_are_given() {
|
|
sends(
|
|
&["focus-workspace", "1"],
|
|
r#"{"type":"FocusWorkspaceNumber","content":1}"#,
|
|
);
|
|
sends(
|
|
&["move-to-workspace", "2"],
|
|
r#"{"type":"MoveContainerToWorkspaceNumber","content":2}"#,
|
|
);
|
|
sends(
|
|
&["send-to-workspace", "0"],
|
|
r#"{"type":"SendContainerToWorkspaceNumber","content":0}"#,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn the_other_workspace_subcommands_send_what_they_are_given() {
|
|
sends(
|
|
&["cycle-workspace", "next"],
|
|
r#"{"type":"CycleFocusWorkspace","content":"Next"}"#,
|
|
);
|
|
sends(
|
|
&["cycle-empty-workspace", "previous"],
|
|
r#"{"type":"CycleFocusEmptyWorkspace","content":"Previous"}"#,
|
|
);
|
|
sends(
|
|
&["cycle-move-to-workspace", "next"],
|
|
r#"{"type":"CycleMoveContainerToWorkspace","content":"Next"}"#,
|
|
);
|
|
sends(
|
|
&["cycle-send-to-workspace", "previous"],
|
|
r#"{"type":"CycleSendContainerToWorkspace","content":"Previous"}"#,
|
|
);
|
|
sends(
|
|
&["focus-last-workspace"],
|
|
r#"{"type":"FocusLastWorkspace"}"#,
|
|
);
|
|
sends(
|
|
&["move-to-last-workspace"],
|
|
r#"{"type":"MoveContainerToLastWorkspace"}"#,
|
|
);
|
|
sends(
|
|
&["send-to-last-workspace"],
|
|
r#"{"type":"SendContainerToLastWorkspace"}"#,
|
|
);
|
|
sends(
|
|
&["focus-workspaces", "2"],
|
|
r#"{"type":"FocusWorkspaceNumbers","content":2}"#,
|
|
);
|
|
sends(&["new-workspace"], r#"{"type":"NewWorkspace"}"#);
|
|
sends(&["close-workspace"], r#"{"type":"CloseWorkspace"}"#);
|
|
}
|
|
|
|
#[test]
|
|
fn the_named_workspace_subcommands_send_the_names_they_are_given() {
|
|
sends(
|
|
&["workspace-name", "0", "1", "mail"],
|
|
r#"{"type":"WorkspaceName","content":[0,1,"mail"]}"#,
|
|
);
|
|
sends(
|
|
&["focus-named-workspace", "mail"],
|
|
r#"{"type":"FocusNamedWorkspace","content":"mail"}"#,
|
|
);
|
|
sends(
|
|
&["ensure-workspaces", "0", "3"],
|
|
r#"{"type":"EnsureWorkspaces","content":[0,3]}"#,
|
|
);
|
|
sends(
|
|
&["ensure-named-workspaces", "0", "web", "code"],
|
|
r#"{"type":"EnsureNamedWorkspaces","content":[0,["web","code"]]}"#,
|
|
);
|
|
sends(
|
|
&["workspace-layout", "0", "1", "columns"],
|
|
r#"{"type":"WorkspaceLayout","content":[0,1,"Columns"]}"#,
|
|
);
|
|
sends(
|
|
&["named-workspace-layout", "mail", "rows"],
|
|
r#"{"type":"NamedWorkspaceLayout","content":["mail","Rows"]}"#,
|
|
);
|
|
sends(
|
|
&["workspace-tiling", "0", "1", "disable"],
|
|
r#"{"type":"WorkspaceTiling","content":[0,1,false]}"#,
|
|
);
|
|
sends(
|
|
&["named-workspace-tiling", "mail", "enable"],
|
|
r#"{"type":"NamedWorkspaceTiling","content":["mail",true]}"#,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn the_layout_rule_subcommands_send_what_they_are_given() {
|
|
sends(
|
|
&["workspace-layout-rule", "0", "1", "3", "columns"],
|
|
r#"{"type":"WorkspaceLayoutRule","content":[0,1,3,"Columns"]}"#,
|
|
);
|
|
sends(
|
|
&["named-workspace-layout-rule", "mail", "2", "rows"],
|
|
r#"{"type":"NamedWorkspaceLayoutRule","content":["mail",2,"Rows"]}"#,
|
|
);
|
|
sends(
|
|
&["clear-workspace-layout-rules", "0", "1"],
|
|
r#"{"type":"ClearWorkspaceLayoutRules","content":[0,1]}"#,
|
|
);
|
|
sends(
|
|
&["clear-named-workspace-layout-rules", "mail"],
|
|
r#"{"type":"ClearNamedWorkspaceLayoutRules","content":"mail"}"#,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn generate_static_config_prints_the_answer_of_the_daemon() {
|
|
let runtime = Runtime::new();
|
|
let daemon = runtime.serve(r#"{"resize_delta":50}"#);
|
|
|
|
let output = runtime.komorebic(&["generate-static-config"]);
|
|
|
|
assert!(output.status.success(), "{output:?}");
|
|
assert_eq!(daemon.join().unwrap(), r#"{"type":"GenerateStaticConfig"}"#);
|
|
assert_eq!(stdout(&output), "{\"resize_delta\":50}\n");
|
|
}
|
|
|
|
#[test]
|
|
fn configuration_shows_the_path_of_komorebi_json_when_there_is_one() {
|
|
let runtime = Runtime::new();
|
|
let home = runtime.0.join("config");
|
|
std::fs::create_dir_all(&home).unwrap();
|
|
|
|
// `config` is another name for it
|
|
for name in ["configuration", "config"] {
|
|
let output = runtime.komorebic_with_config_home(&home, &[name]);
|
|
assert!(output.status.success(), "{output:?}");
|
|
assert_eq!(stdout(&output), "", "no file, nothing to show");
|
|
}
|
|
|
|
std::fs::write(home.join("komorebi.json"), "{}").unwrap();
|
|
|
|
for name in ["configuration", "config"] {
|
|
let output = runtime.komorebic_with_config_home(&home, &[name]);
|
|
assert!(output.status.success(), "{output:?}");
|
|
assert_eq!(
|
|
stdout(&output),
|
|
format!("{}\n", home.join("komorebi.json").display())
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn replace_configuration_sends_the_path_it_is_given_with_the_variables_replaced() {
|
|
let runtime = Runtime::new();
|
|
let daemon = runtime.serve("");
|
|
|
|
let output = runtime.komorebic_with_config_home(
|
|
&runtime.0,
|
|
&["replace-configuration", "$KOMOREBI_CONFIG_HOME/other.json"],
|
|
);
|
|
|
|
assert!(output.status.success(), "{output:?}");
|
|
assert_eq!(
|
|
daemon.join().unwrap(),
|
|
format!(
|
|
r#"{{"type":"ReplaceConfiguration","content":"{}/other.json"}}"#,
|
|
runtime.0.display()
|
|
)
|
|
);
|
|
assert_eq!(stdout(&output), "");
|
|
}
|
|
|
|
/// Runs `komorebic quickstart` in the config home `home`, which answers `answers` to its questions
|
|
fn quickstart(runtime: &Runtime, home: &Path, answers: &str) -> Output {
|
|
let mut child = Command::new(env!("CARGO_BIN_EXE_komorebic"))
|
|
.arg("quickstart")
|
|
.env("XDG_RUNTIME_DIR", &runtime.0)
|
|
.env("KOMOREBI_CONFIG_HOME", home)
|
|
.stdin(std::process::Stdio::piped())
|
|
.stdout(std::process::Stdio::piped())
|
|
.stderr(std::process::Stdio::piped())
|
|
.spawn()
|
|
.unwrap();
|
|
child
|
|
.stdin
|
|
.take()
|
|
.unwrap()
|
|
.write_all(answers.as_bytes())
|
|
.unwrap();
|
|
child.wait_with_output().unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn quickstart_writes_the_example_files_and_asks_before_it_overwrites_one() {
|
|
let runtime = Runtime::new();
|
|
let home = runtime.0.join("config");
|
|
std::fs::create_dir_all(&home).unwrap();
|
|
|
|
let output = quickstart(&runtime, &home, "");
|
|
assert!(output.status.success(), "{output:?}");
|
|
let example = include_str!("../../docs/komorebi.example.json");
|
|
assert_eq!(
|
|
std::fs::read_to_string(home.join("komorebi.json")).unwrap(),
|
|
example
|
|
);
|
|
assert_eq!(
|
|
std::fs::read_to_string(home.join("applications.json")).unwrap(),
|
|
include_str!("../applications.json")
|
|
);
|
|
assert_eq!(
|
|
std::fs::read_to_string(home.join("skhdrc")).unwrap(),
|
|
include_str!("../../docs/skhdrc.sample")
|
|
);
|
|
assert!(stdout(&output).contains("The following example files were written"));
|
|
assert!(stdout(&output).contains("komorebic start"));
|
|
|
|
// a file that is there is kept unless the answer is yes
|
|
std::fs::write(home.join("komorebi.json"), "{}").unwrap();
|
|
std::fs::write(home.join("applications.json"), "{}").unwrap();
|
|
let output = quickstart(&runtime, &home, "n\nyes\n");
|
|
assert!(output.status.success(), "{output:?}");
|
|
assert_eq!(
|
|
std::fs::read_to_string(home.join("komorebi.json")).unwrap(),
|
|
"{}"
|
|
);
|
|
assert_eq!(
|
|
std::fs::read_to_string(home.join("applications.json")).unwrap(),
|
|
include_str!("../applications.json")
|
|
);
|
|
// the third question has no answer, which is not a yes
|
|
assert_eq!(
|
|
std::fs::read_to_string(home.join("skhdrc")).unwrap(),
|
|
include_str!("../../docs/skhdrc.sample")
|
|
);
|
|
assert!(stdout(&output).contains("Skipping"));
|
|
}
|
|
|
|
#[test]
|
|
fn autostart_writes_an_xdg_entry_that_starts_komorebi_and_takes_it_away_again() {
|
|
let runtime = Runtime::new();
|
|
let config_dir = runtime.0.join("xdg");
|
|
let entry = config_dir.join("autostart").join("komorebi.desktop");
|
|
let run = |args: &[&str]| {
|
|
Command::new(env!("CARGO_BIN_EXE_komorebic"))
|
|
.args(args)
|
|
.env("XDG_RUNTIME_DIR", &runtime.0)
|
|
.env("XDG_CONFIG_HOME", &config_dir)
|
|
.output()
|
|
.unwrap()
|
|
};
|
|
|
|
let output = run(&["disable-autostart"]);
|
|
assert!(output.status.success(), "{output:?}");
|
|
assert!(stdout(&output).contains("There is no autostart entry"));
|
|
|
|
let output = run(&["enable-autostart"]);
|
|
assert!(output.status.success(), "{output:?}");
|
|
let text = std::fs::read_to_string(&entry).unwrap();
|
|
assert!(text.starts_with("[Desktop Entry]\n"), "{text}");
|
|
assert!(text.contains("\nExec=komorebi\n"), "{text}");
|
|
assert!(
|
|
text.contains("\nX-KDE-autostart-after=plasma-workspace\n"),
|
|
"{text}"
|
|
);
|
|
|
|
// `--config` is what the daemon is started with
|
|
let output = run(&[
|
|
"enable-autostart",
|
|
"--config",
|
|
"/etc/komorebi/komorebi.json",
|
|
]);
|
|
assert!(output.status.success(), "{output:?}");
|
|
assert!(
|
|
std::fs::read_to_string(&entry)
|
|
.unwrap()
|
|
.contains("\nExec=komorebi --config \"/etc/komorebi/komorebi.json\"\n")
|
|
);
|
|
|
|
let output = run(&["disable-autostart"]);
|
|
assert!(output.status.success(), "{output:?}");
|
|
assert!(stdout(&output).starts_with("Deleted "), "{output:?}");
|
|
assert!(!entry.exists());
|
|
}
|
|
|
|
#[test]
|
|
fn a_config_home_that_is_not_a_directory_is_refused() {
|
|
let runtime = Runtime::new();
|
|
let home = runtime.0.join("not-there");
|
|
|
|
let output = runtime.komorebic_with_config_home(&home, &["configuration"]);
|
|
|
|
assert!(!output.status.success(), "{output:?}");
|
|
assert!(
|
|
String::from_utf8_lossy(&output.stderr).contains("which is not a valid directory"),
|
|
"{output:?}"
|
|
);
|
|
}
|
|
|
|
/// The JSON `komorebic` prints for a subcommand that needs no daemon
|
|
fn printed(args: &[&str]) -> serde_json::Value {
|
|
let runtime = Runtime::new();
|
|
|
|
let output = runtime.komorebic(args);
|
|
|
|
assert!(output.status.success(), "{args:?}: {output:?}");
|
|
serde_json::from_str(&stdout(&output)).unwrap_or_else(|error| panic!("{args:?}: {error}"))
|
|
}
|
|
|
|
#[test]
|
|
fn the_schema_subcommands_print_a_json_schema_without_a_daemon() {
|
|
let configuration = printed(&["static-config-schema"]);
|
|
assert!(configuration["properties"]["resize_delta"].is_object());
|
|
assert!(configuration["$defs"]["MonitorConfig"].is_object());
|
|
|
|
// `asc-schema` is another name for it
|
|
let applications = printed(&["application-specific-configuration-schema"]);
|
|
assert_eq!(applications, printed(&["asc-schema"]));
|
|
assert!(applications["$defs"]["AscApplicationRules"]["properties"]["floating"].is_object());
|
|
|
|
assert!(printed(&["notification-schema"])["$defs"]["WindowManagerEvent"].is_object());
|
|
assert!(printed(&["socket-schema"])["oneOf"].is_array());
|
|
}
|
|
|
|
#[test]
|
|
fn the_schema_files_of_the_repository_are_what_the_subcommands_print() {
|
|
let root = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap();
|
|
|
|
for (file, subcommand) in [
|
|
("schema.json", "static-config-schema"),
|
|
(
|
|
"schema.asc.json",
|
|
"application-specific-configuration-schema",
|
|
),
|
|
] {
|
|
let runtime = Runtime::new();
|
|
let printed = stdout(&runtime.komorebic(&[subcommand]));
|
|
let committed = std::fs::read_to_string(root.join(file)).unwrap();
|
|
|
|
assert_eq!(printed, committed, "{file} is stale: run `just jsonschema`");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn the_monitor_subcommands_send_what_they_are_given() {
|
|
sends(
|
|
&["focus-monitor", "1"],
|
|
r#"{"type":"FocusMonitorNumber","content":1}"#,
|
|
);
|
|
sends(
|
|
&["cycle-monitor", "next"],
|
|
r#"{"type":"CycleFocusMonitor","content":"Next"}"#,
|
|
);
|
|
sends(
|
|
&["focus-monitor-at-cursor"],
|
|
r#"{"type":"FocusMonitorAtCursor"}"#,
|
|
);
|
|
sends(
|
|
&["focus-monitor-workspace", "1", "2"],
|
|
r#"{"type":"FocusMonitorWorkspaceNumber","content":[1,2]}"#,
|
|
);
|
|
sends(
|
|
&["move-to-monitor", "1"],
|
|
r#"{"type":"MoveContainerToMonitorNumber","content":1}"#,
|
|
);
|
|
sends(
|
|
&["send-to-monitor", "0"],
|
|
r#"{"type":"SendContainerToMonitorNumber","content":0}"#,
|
|
);
|
|
sends(
|
|
&["cycle-move-to-monitor", "previous"],
|
|
r#"{"type":"CycleMoveContainerToMonitor","content":"Previous"}"#,
|
|
);
|
|
sends(
|
|
&["cycle-send-to-monitor", "next"],
|
|
r#"{"type":"CycleSendContainerToMonitor","content":"Next"}"#,
|
|
);
|
|
sends(
|
|
&["move-to-monitor-workspace", "1", "0"],
|
|
r#"{"type":"MoveContainerToMonitorWorkspaceNumber","content":[1,0]}"#,
|
|
);
|
|
sends(
|
|
&["send-to-monitor-workspace", "0", "1"],
|
|
r#"{"type":"SendContainerToMonitorWorkspaceNumber","content":[0,1]}"#,
|
|
);
|
|
sends(
|
|
&["move-to-named-workspace", "mail"],
|
|
r#"{"type":"MoveContainerToNamedWorkspace","content":"mail"}"#,
|
|
);
|
|
sends(
|
|
&["send-to-named-workspace", "mail"],
|
|
r#"{"type":"SendContainerToNamedWorkspace","content":"mail"}"#,
|
|
);
|
|
sends(
|
|
&["move-workspace-to-monitor", "1"],
|
|
r#"{"type":"MoveWorkspaceToMonitorNumber","content":1}"#,
|
|
);
|
|
sends(
|
|
&["cycle-move-workspace-to-monitor", "next"],
|
|
r#"{"type":"CycleMoveWorkspaceToMonitor","content":"Next"}"#,
|
|
);
|
|
sends(
|
|
&["swap-workspaces-with-monitor", "1"],
|
|
r#"{"type":"SwapWorkspacesToMonitorNumber","content":1}"#,
|
|
);
|
|
sends(
|
|
&["cross-monitor-move-behaviour", "insert"],
|
|
r#"{"type":"CrossMonitorMoveBehaviour","content":"Insert"}"#,
|
|
);
|
|
sends(
|
|
&["toggle-cross-monitor-move-behaviour"],
|
|
r#"{"type":"ToggleCrossMonitorMoveBehaviour"}"#,
|
|
);
|
|
sends(
|
|
&["display-index-preference", "1", "DP-2"],
|
|
r#"{"type":"DisplayIndexPreference","content":[1,"DP-2"]}"#,
|
|
);
|
|
sends(
|
|
&["eager-focus", "firefox"],
|
|
r#"{"type":"EagerFocus","content":"firefox"}"#,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn without_a_daemon_a_question_fails() {
|
|
let runtime = Runtime::new();
|
|
|
|
let output = runtime.komorebic(&["state"]);
|
|
|
|
assert!(!output.status.success(), "{output:?}");
|
|
assert_eq!(stdout(&output), "");
|
|
}
|
|
|
|
#[test]
|
|
fn data_directory_shows_where_the_socket_is() {
|
|
let runtime = Runtime::new();
|
|
|
|
let output = runtime.komorebic(&["data-directory"]);
|
|
|
|
assert!(output.status.success(), "{output:?}");
|
|
assert_eq!(
|
|
stdout(&output),
|
|
format!("{}\n", runtime.0.join("komorebi").display())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn docgen_writes_the_help_of_every_subcommand_in_a_text_fence() {
|
|
let runtime = Runtime::new();
|
|
let docs = runtime.0.join("cli");
|
|
|
|
let output = runtime.komorebic(&["docgen", "--output", docs.to_str().unwrap()]);
|
|
|
|
assert!(output.status.success(), "{output:?}");
|
|
let mut pages = std::fs::read_dir(&docs)
|
|
.unwrap()
|
|
.map(|entry| entry.unwrap().file_name().into_string().unwrap())
|
|
.collect::<Vec<_>>();
|
|
pages.sort();
|
|
assert_eq!(
|
|
pages,
|
|
[
|
|
"adjust-container-padding.md",
|
|
"adjust-workspace-padding.md",
|
|
"animation-duration.md",
|
|
"animation-fps.md",
|
|
"animation-style.md",
|
|
"animation.md",
|
|
"application-specific-configuration-schema.md",
|
|
"border-colour.md",
|
|
"border-offset.md",
|
|
"border-width.md",
|
|
"border.md",
|
|
"cancel-preselect.md",
|
|
"change-layout.md",
|
|
"check.md",
|
|
"clear-all-workspace-rules.md",
|
|
"clear-named-workspace-layout-rules.md",
|
|
"clear-named-workspace-rules.md",
|
|
"clear-session-float-rules.md",
|
|
"clear-workspace-layout-rules.md",
|
|
"clear-workspace-rules.md",
|
|
"close-workspace.md",
|
|
"close.md",
|
|
"completions.md",
|
|
"configuration.md",
|
|
"container-padding.md",
|
|
"cross-monitor-move-behaviour.md",
|
|
"cycle-empty-workspace.md",
|
|
"cycle-focus.md",
|
|
"cycle-layout.md",
|
|
"cycle-monitor.md",
|
|
"cycle-move-to-monitor.md",
|
|
"cycle-move-to-workspace.md",
|
|
"cycle-move-workspace-to-monitor.md",
|
|
"cycle-move.md",
|
|
"cycle-send-to-monitor.md",
|
|
"cycle-send-to-workspace.md",
|
|
"cycle-stack-index.md",
|
|
"cycle-stack.md",
|
|
"cycle-workspace.md",
|
|
"data-directory.md",
|
|
"disable-autostart.md",
|
|
"display-index-preference.md",
|
|
"eager-focus.md",
|
|
"enable-autostart.md",
|
|
"enforce-workspace-rules.md",
|
|
"ensure-named-workspaces.md",
|
|
"ensure-workspaces.md",
|
|
"export-kde-shortcuts.md",
|
|
"flip-layout.md",
|
|
"focus-last-workspace.md",
|
|
"focus-monitor-at-cursor.md",
|
|
"focus-monitor-workspace.md",
|
|
"focus-monitor.md",
|
|
"focus-named-workspace.md",
|
|
"focus-stack-window.md",
|
|
"focus-workspace.md",
|
|
"focus-workspaces.md",
|
|
"focus.md",
|
|
"focused-workspace-container-padding.md",
|
|
"focused-workspace-padding.md",
|
|
"generate-static-config.md",
|
|
"global-state.md",
|
|
"global-work-area-offset.md",
|
|
"hotkeys-compile.md",
|
|
"hotkeys-release.md",
|
|
"hotkeys-reload.md",
|
|
"hotkeys-state.md",
|
|
"hotkeys.md",
|
|
"ignore-rule.md",
|
|
"initial-named-workspace-rule.md",
|
|
"initial-workspace-rule.md",
|
|
"layout-ratios.md",
|
|
"load-resize.md",
|
|
"log.md",
|
|
"manage-rule.md",
|
|
"manage.md",
|
|
"minimize.md",
|
|
"monitor-information.md",
|
|
"monitor-work-area-offset.md",
|
|
"monocle-focus-behaviour.md",
|
|
"mouse-follows-focus.md",
|
|
"move-to-last-workspace.md",
|
|
"move-to-monitor-workspace.md",
|
|
"move-to-monitor.md",
|
|
"move-to-named-workspace.md",
|
|
"move-to-workspace.md",
|
|
"move-workspace-to-monitor.md",
|
|
"move.md",
|
|
"named-workspace-container-padding.md",
|
|
"named-workspace-layout-rule.md",
|
|
"named-workspace-layout.md",
|
|
"named-workspace-padding.md",
|
|
"named-workspace-rule.md",
|
|
"named-workspace-tiling.md",
|
|
"new-workspace.md",
|
|
"notification-schema.md",
|
|
"preselect-direction.md",
|
|
"promote-focus.md",
|
|
"promote-swap.md",
|
|
"promote-window.md",
|
|
"promote.md",
|
|
"query.md",
|
|
"quick-load-resize.md",
|
|
"quick-save-resize.md",
|
|
"quickstart.md",
|
|
"replace-configuration.md",
|
|
"resize-axis.md",
|
|
"resize-delta.md",
|
|
"resize-edge.md",
|
|
"retile.md",
|
|
"save-resize.md",
|
|
"scrolling-layout-columns.md",
|
|
"send-to-last-workspace.md",
|
|
"send-to-monitor-workspace.md",
|
|
"send-to-monitor.md",
|
|
"send-to-named-workspace.md",
|
|
"send-to-workspace.md",
|
|
"session-float-rule.md",
|
|
"session-float-rules.md",
|
|
"socket-schema.md",
|
|
"stack-all.md",
|
|
"stack.md",
|
|
"start.md",
|
|
"state.md",
|
|
"static-config-schema.md",
|
|
"stop.md",
|
|
"subscribe-socket.md",
|
|
"swap-workspaces-with-monitor.md",
|
|
"toggle-cross-monitor-move-behaviour.md",
|
|
"toggle-float-override.md",
|
|
"toggle-float.md",
|
|
"toggle-lock.md",
|
|
"toggle-maximize.md",
|
|
"toggle-monocle-focus-behaviour.md",
|
|
"toggle-monocle.md",
|
|
"toggle-mouse-follows-focus.md",
|
|
"toggle-pause.md",
|
|
"toggle-tiling.md",
|
|
"toggle-transparency.md",
|
|
"toggle-window-based-work-area-offset.md",
|
|
"toggle-window-container-behaviour.md",
|
|
"toggle-workspace-float-override.md",
|
|
"toggle-workspace-layer.md",
|
|
"toggle-workspace-window-container-behaviour.md",
|
|
"transparency-alpha.md",
|
|
"transparency.md",
|
|
"unmanage.md",
|
|
"unmanaged-window-operation-behaviour.md",
|
|
"unstack-all.md",
|
|
"unstack.md",
|
|
"unsubscribe-socket.md",
|
|
"visible-windows.md",
|
|
"workspace-layout-rule.md",
|
|
"workspace-layout.md",
|
|
"workspace-name.md",
|
|
"workspace-padding.md",
|
|
"workspace-rule.md",
|
|
"workspace-tiling.md",
|
|
"workspace-work-area-offset.md",
|
|
]
|
|
);
|
|
|
|
let query = std::fs::read_to_string(Path::new(&docs).join("query.md")).unwrap();
|
|
assert!(query.starts_with("```text\n"), "{query}");
|
|
assert!(query.ends_with("\n```\n"), "{query}");
|
|
assert!(query.contains("focused-workspace-index"), "{query}");
|
|
}
|
|
|
|
#[test]
|
|
fn docgen_writes_the_same_pages_whatever_the_width_of_the_terminal() {
|
|
let runtime = Runtime::new();
|
|
// without a terminal, clap wraps the help at the width in COLUMNS
|
|
let query_page = |columns: &str| {
|
|
let docs = runtime.0.join(format!("cli-{columns}"));
|
|
let output = Command::new(env!("CARGO_BIN_EXE_komorebic"))
|
|
.args(["docgen", "--output", docs.to_str().unwrap()])
|
|
.env("COLUMNS", columns)
|
|
.output()
|
|
.unwrap();
|
|
assert!(output.status.success(), "{output:?}");
|
|
std::fs::read_to_string(docs.join("query.md")).unwrap()
|
|
};
|
|
|
|
let narrow = query_page("40");
|
|
|
|
assert_eq!(narrow, query_page("250"));
|
|
// the possible values of a query are wrapped at 100 columns
|
|
assert!(narrow.lines().any(|line| line.len() > 40), "{narrow}");
|
|
assert!(narrow.lines().all(|line| line.len() <= 100), "{narrow}");
|
|
}
|
|
|
|
#[test]
|
|
fn completions_are_a_script_that_knows_the_subcommands() {
|
|
let runtime = Runtime::new();
|
|
|
|
let output = runtime.komorebic(&["completions", "bash"]);
|
|
|
|
assert!(output.status.success(), "{output:?}");
|
|
assert!(stdout(&output).contains("monitor-information"));
|
|
}
|