cosmic-conf: translate Hyprland bind lines into COSMIC shortcuts

`bind = SUPER, D, exec, rofi -show drun` is the most recognisable line in a
hyprland.conf, and the hyprcosmic profile makes it necessary rather than just
idiomatic: with cosmic-launcher and cosmic-app-library not running, COSMIC's
stock Super, Super+/ and Super+A bindings point at nothing.

Binds are the one repeatable key in the language -- many lines fold into a
single map instead of the last one winning -- so they bypass the schema, which
is built around one conf key naming one value. They land in the Shortcuts
`custom` key, which cosmic-comp merges over `defaults`, so the system file is
untouched and reverting means deleting the lines and re-applying.

Actions are rendered as RON text rather than modelled as an enum: COSMIC's
Action has forty-odd variants, this crate deliberately does not link the cosmic
crates, and the mapping table only ever needs a handful. Dispatchers without a
genuine equivalent are refused rather than approximated, since a keybinding
that silently does the wrong thing is worse than one that fails to compile.

Verified the emitted file deserializes into cosmic-settings-config's own
`Shortcuts` type: five bindings, keysyms XK_a/XK_slash/XK_Return, Spawn actions.
This commit is contained in:
2026-08-10 08:51:16 +07:00
parent 214a77ea18
commit 19927bc00b
6 changed files with 594 additions and 2 deletions
+2 -2
View File
@@ -13,8 +13,8 @@
waybar -c /usr/share/hyprcosmic/waybar/config.jsonc -s /usr/share/hyprcosmic/waybar/style.css
# rofi is not a daemon. It is launched on demand by a keybinding, which COSMIC
# stores in com.system76.CosmicSettings.Shortcuts rather than here. Until that
# binding is set, run `rofi -show drun` from a terminal.
# stores in com.system76.CosmicSettings.Shortcuts rather than here. Set those
# with `bind` lines in cosmic.conf and `cosmic-conf apply`; see config/cosmic.conf.
# HyDE drives wallpaper through swww, which is not packaged for Fedora. Once
# it is available, uncomment:
+28
View File
@@ -0,0 +1,28 @@
# HyprCosmic configuration, in Hyprland's idiom.
#
# Compiled into COSMIC's config tree by `cosmic-conf apply`. The file wins:
# every key here overwrites whatever COSMIC's own settings UI last stored, so
# edit this rather than the GUI for anything it covers.
$mainMod = SUPER
# --- Launcher -----------------------------------------------------------
#
# The hyprcosmic profile does not start cosmic-launcher or cosmic-app-library,
# which leaves COSMIC's stock Super, Super+/ and Super+A bindings pointing at
# nothing. These take them over with rofi. Written to the Shortcuts `custom`
# key, which cosmic-comp merges over `defaults`, so the system file is not
# touched and reverting is a matter of deleting these lines and re-applying.
# Bare Super, exactly where stock COSMIC puts its launcher.
bind = $mainMod, , exec, rofi -show drun
bind = $mainMod, slash, exec, rofi -show drun
bind = $mainMod, A, exec, rofi -show drun
# Was System(WorkspaceOverview); cosmic-workspaces is not running either.
# rofi's window mode is the nearest thing that still shows every open window.
bind = $mainMod, W, exec, rofi -show window
# Terminal, in the Hyprland idiom. Super+T also still works — cosmic-comp
# handles System(Terminal) itself, so that binding never went dead.
bind = $mainMod, Return, exec, cosmic-term
+435
View File
@@ -0,0 +1,435 @@
//! Hyprland `bind` lines -> COSMIC shortcut bindings.
//!
//! `bind = SUPER, D, exec, rofi -show drun` is the single most recognisable
//! line in a hyprland.conf, so it is the one piece of the idiom that has to
//! feel native rather than translated.
//!
//! The target is the `custom` key of `com.system76.CosmicSettings.Shortcuts`,
//! which the compositor merges over `defaults`, letting a bind here override a
//! stock COSMIC shortcut without touching the system file
//! (cosmic-settings-daemon `config/src/shortcuts/mod.rs`: `shortcuts()` reads
//! `defaults`, then extends with `custom`).
//!
//! Actions are rendered as RON text rather than modelled as an enum. COSMIC's
//! `Action` has forty-odd variants and this crate deliberately does not link
//! the cosmic crates; mirroring the enum would mean re-copying it every time
//! upstream adds a variant, and the mapping table below only ever needs a few.
use std::fmt::Write as _;
use crate::parser::Span;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Bind {
/// COSMIC modifier names, deduplicated and in COSMIC's own order.
pub mods: Vec<&'static str>,
/// xkb keysym name. `None` is a modifier-only binding, which COSMIC
/// supports and its defaults use for the launcher on bare Super.
pub key: Option<String>,
/// Pre-rendered RON, e.g. `Spawn("rofi -show drun")` or `Focus(Left)`.
pub action: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BindError {
pub message: String,
pub help: Option<String>,
pub span: Span,
}
fn err(span: Span, message: impl Into<String>, help: Option<&str>) -> BindError {
BindError {
message: message.into(),
help: help.map(str::to_string),
span,
}
}
/// Modifier spellings Hyprland accepts, longest first so that the greedy scan
/// below consumes `SUPERSHIFT` correctly rather than stopping at a prefix.
const MODIFIERS: &[(&str, &str)] = &[
("SUPERKEY", "Super"),
("CONTROL", "Ctrl"),
("SHIFT", "Shift"),
("SUPER", "Super"),
("LOGO", "Super"),
("MOD4", "Super"),
("MOD1", "Alt"),
("CTRL", "Ctrl"),
("META", "Super"),
("ALT", "Alt"),
("WIN", "Super"),
];
/// COSMIC writes modifiers in this order in its own defaults; matching it keeps
/// generated files diffable against hand-written ones.
const MODIFIER_ORDER: &[&str] = &["Super", "Ctrl", "Alt", "Shift"];
/// Hyprland allows `SUPER SHIFT`, `SUPER+SHIFT` and bare `SUPERSHIFT`, so
/// separators are stripped and the remainder is consumed greedily.
fn parse_modifiers(raw: &str, span: Span) -> Result<Vec<&'static str>, BindError> {
let mut rest: String = raw
.chars()
.filter(|c| !c.is_whitespace() && *c != '+' && *c != '_')
.collect::<String>()
.to_ascii_uppercase();
let mut found: Vec<&'static str> = Vec::new();
'outer: while !rest.is_empty() {
for (spelling, cosmic) in MODIFIERS {
if let Some(tail) = rest.strip_prefix(spelling) {
if !found.contains(cosmic) {
found.push(cosmic);
}
rest = tail.to_string();
continue 'outer;
}
}
return Err(err(
span,
format!("unknown modifier `{rest}`"),
Some("known modifiers: SUPER, CTRL, ALT, SHIFT"),
));
}
found.sort_by_key(|m| MODIFIER_ORDER.iter().position(|o| o == m).unwrap_or(usize::MAX));
Ok(found)
}
/// Named keys whose xkb spelling differs from what a Hyprland user types.
///
/// Anything absent falls through unchanged, so exact keysyms such as
/// `XF86AudioRaiseVolume` keep working without needing an entry here.
const KEY_NAMES: &[(&str, &str)] = &[
("return", "Return"),
("enter", "Return"),
("escape", "Escape"),
("esc", "Escape"),
("tab", "Tab"),
("backspace", "BackSpace"),
("delete", "Delete"),
("insert", "Insert"),
("home", "Home"),
("end", "End"),
("pageup", "Prior"),
("pagedown", "Next"),
("left", "Left"),
("right", "Right"),
("up", "Up"),
("down", "Down"),
("print", "Print"),
];
/// COSMIC's defaults spell letters lowercase (`key: "q"`) and punctuation by
/// keysym name (`key: "slash"`), so normalise toward that.
fn normalize_key(raw: &str) -> Option<String> {
let trimmed = raw.trim();
if trimmed.is_empty() {
return None;
}
let lower = trimmed.to_ascii_lowercase();
if let Some((_, name)) = KEY_NAMES.iter().find(|(k, _)| *k == lower) {
return Some((*name).to_string());
}
// Function keys are uppercase-F in xkb.
if let Some(n) = lower.strip_prefix('f') {
if !n.is_empty() && n.chars().all(|c| c.is_ascii_digit()) {
return Some(format!("F{n}"));
}
}
if trimmed.len() == 1 && trimmed.chars().all(|c| c.is_ascii_alphabetic()) {
return Some(lower);
}
Some(trimmed.to_string())
}
fn ron_string(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
_ => out.push(c),
}
}
out.push('"');
out
}
fn direction(arg: &str, span: Span, dispatcher: &str) -> Result<&'static str, BindError> {
Ok(match arg.trim().to_ascii_lowercase().as_str() {
"l" | "left" => "Left",
"r" | "right" => "Right",
"u" | "up" => "Up",
"d" | "down" => "Down",
other => {
return Err(err(
span,
format!("`{dispatcher}` needs a direction, got `{other}`"),
Some("use l, r, u or d"),
))
}
})
}
fn workspace_index(arg: &str, span: Span, dispatcher: &str) -> Result<u8, BindError> {
arg.trim().parse::<u8>().map_err(|_| {
err(
span,
format!("`{dispatcher}` needs a workspace number, got `{}`", arg.trim()),
Some("COSMIC addresses workspaces 1-255 by index"),
)
})
}
/// Translate a Hyprland dispatcher and its argument into RON for COSMIC's
/// `Action`.
///
/// Only dispatchers with a genuine COSMIC equivalent are mapped. A dispatcher
/// that merely looks similar is rejected instead of approximated, because a
/// keybinding that silently does the wrong thing is worse than one that fails
/// to compile.
fn action(dispatcher: &str, arg: &str, span: Span) -> Result<String, BindError> {
let d = dispatcher.trim().to_ascii_lowercase();
Ok(match d.as_str() {
"exec" => {
let cmd = arg.trim();
if cmd.is_empty() {
return Err(err(span, "`exec` needs a command", None));
}
// cosmic-comp runs this through `/bin/sh -c`
// (`src/input/actions.rs`: `spawn_command`), so a full command line
// with arguments and quoting behaves as written.
format!("Spawn({})", ron_string(cmd))
}
"killactive" => "Close".into(),
"fullscreen" => "Fullscreen".into(),
"togglefloating" => "ToggleWindowFloating".into(),
"togglesplit" => "ToggleOrientation".into(),
"togglegroup" => "ToggleStacking".into(),
"pin" => "ToggleSticky".into(),
"exit" => "System(LogOut)".into(),
"movefocus" => format!("Focus({})", direction(arg, span, &d)?),
"movewindow" => format!("Move({})", direction(arg, span, &d)?),
"workspace" => format!("Workspace({})", workspace_index(arg, span, &d)?),
"movetoworkspace" => format!("MoveToWorkspace({})", workspace_index(arg, span, &d)?),
"movetoworkspacesilent" => format!("SendToWorkspace({})", workspace_index(arg, span, &d)?),
"focusmonitor" => format!("SwitchOutput({})", direction(arg, span, &d)?),
"movewindowtomonitor" => format!("MoveToOutput({})", direction(arg, span, &d)?),
// Present in Hyprland, absent from COSMIC. Named explicitly so the
// error says why rather than "unknown".
"pseudo" | "forcerendererreload" | "submap" | "toggleopaque" | "centerwindow"
| "splitratio" | "cyclenext" | "swapnext" => {
return Err(err(
span,
format!("`{d}` has no COSMIC equivalent"),
Some("remove the bind, or use `exec` to run a program instead"),
))
}
other => {
return Err(err(
span,
format!("unknown dispatcher `{other}`"),
Some("supported: exec, killactive, fullscreen, togglefloating, togglesplit, movefocus, movewindow, workspace, movetoworkspace, exit"),
))
}
})
}
/// Parse the value of one `bind = ...` line.
///
/// Shape is `MODS, KEY, dispatcher, args`, with args keeping any further
/// commas, since `exec` commands routinely contain them.
pub fn parse_bind(value: &str, span: Span) -> Result<Bind, BindError> {
let parts: Vec<&str> = value.splitn(4, ',').collect();
if parts.len() < 3 {
return Err(err(
span,
"a bind needs at least MODS, KEY and a dispatcher",
Some("for example: bind = SUPER, D, exec, rofi -show drun"),
));
}
let mods = parse_modifiers(parts[0], span)?;
let key = normalize_key(parts[1]);
if mods.is_empty() && key.is_none() {
return Err(err(span, "a bind needs a modifier or a key", None));
}
let arg = parts.get(3).copied().unwrap_or("");
let action = action(parts[2], arg, span)?;
Ok(Bind { mods, key, action })
}
/// Render the collected binds as the RON map COSMIC stores in `custom`.
pub fn render(binds: &[Bind]) -> String {
let mut out = String::from("{\n");
for b in binds {
let mods = b
.mods
.iter()
.map(|m| m.to_string())
.collect::<Vec<_>>()
.join(", ");
match &b.key {
// `key` is `skip_serializing_if = "Option::is_none"` on COSMIC's
// `Binding`, and its own defaults omit it for `(modifiers: [Super])`.
Some(k) => {
let _ = writeln!(
out,
" (modifiers: [{mods}], key: {}): {},",
ron_string(k),
b.action
);
}
None => {
let _ = writeln!(out, " (modifiers: [{mods}]): {},", b.action);
}
}
}
out.push_str("}\n");
out
}
#[cfg(test)]
mod tests {
use super::*;
const S: Span = Span {
line: 1,
col: 1,
len: 1,
};
fn bind(v: &str) -> Bind {
parse_bind(v, S).expect("should parse")
}
#[test]
fn the_canonical_hyprland_launcher_bind() {
let b = bind("SUPER, D, exec, rofi -show drun");
assert_eq!(b.mods, vec!["Super"]);
assert_eq!(b.key.as_deref(), Some("d"));
assert_eq!(b.action, r#"Spawn("rofi -show drun")"#);
}
#[test]
fn modifiers_accept_every_separator_hyprland_does() {
for spelling in ["SUPER SHIFT", "SUPER+SHIFT", "SUPERSHIFT", "super shift"] {
assert_eq!(
bind(&format!("{spelling}, Q, killactive")).mods,
vec!["Super", "Shift"],
"failed for `{spelling}`"
);
}
}
#[test]
fn modifiers_are_ordered_like_cosmics_own_defaults() {
assert_eq!(
bind("SHIFT ALT CTRL SUPER, Q, killactive").mods,
vec!["Super", "Ctrl", "Alt", "Shift"]
);
}
#[test]
fn a_bind_with_no_key_is_modifier_only() {
// COSMIC's defaults bind bare Super to the launcher this way.
let b = bind("SUPER, , exec, rofi -show drun");
assert_eq!(b.key, None);
assert_eq!(render(&[b]), "{\n (modifiers: [Super]): Spawn(\"rofi -show drun\"),\n}\n");
}
#[test]
fn keys_normalise_to_xkb_spelling() {
assert_eq!(bind("SUPER, Q, killactive").key.as_deref(), Some("q"));
assert_eq!(bind("SUPER, Return, killactive").key.as_deref(), Some("Return"));
assert_eq!(bind("SUPER, enter, killactive").key.as_deref(), Some("Return"));
assert_eq!(bind("SUPER, f5, killactive").key.as_deref(), Some("F5"));
assert_eq!(bind("SUPER, slash, killactive").key.as_deref(), Some("slash"));
// Unknown names pass through so exact keysyms stay usable.
assert_eq!(
bind("SUPER, XF86AudioRaiseVolume, killactive").key.as_deref(),
Some("XF86AudioRaiseVolume")
);
}
#[test]
fn exec_keeps_commas_in_the_command() {
assert_eq!(
bind("SUPER, E, exec, sh -c 'echo a, b'").action,
r#"Spawn("sh -c 'echo a, b'")"#
);
}
#[test]
fn quotes_in_a_command_are_escaped_not_emitted_raw() {
// Otherwise the generated RON would not parse.
assert_eq!(
bind(r#"SUPER, E, exec, echo "hi""#).action,
r#"Spawn("echo \"hi\"")"#
);
}
#[test]
fn dispatchers_map_to_cosmic_actions() {
assert_eq!(bind("SUPER, Q, killactive").action, "Close");
assert_eq!(bind("SUPER, F, fullscreen").action, "Fullscreen");
assert_eq!(bind("SUPER, left, movefocus, l").action, "Focus(Left)");
assert_eq!(bind("SUPER SHIFT, left, movewindow, l").action, "Move(Left)");
assert_eq!(bind("SUPER, 1, workspace, 1").action, "Workspace(1)");
assert_eq!(
bind("SUPER SHIFT, 1, movetoworkspace, 1").action,
"MoveToWorkspace(1)"
);
}
#[test]
fn a_dispatcher_without_an_equivalent_is_refused_not_approximated() {
let e = parse_bind("SUPER, P, pseudo", S).unwrap_err();
assert!(e.message.contains("no COSMIC equivalent"), "{}", e.message);
assert!(e.help.is_some());
}
#[test]
fn unknown_dispatchers_and_modifiers_are_reported() {
assert!(parse_bind("SUPER, X, frobnicate", S)
.unwrap_err()
.message
.contains("unknown dispatcher"));
assert!(parse_bind("HYPER, X, killactive", S)
.unwrap_err()
.message
.contains("unknown modifier"));
}
#[test]
fn a_truncated_bind_says_what_shape_is_expected() {
let e = parse_bind("SUPER, D", S).unwrap_err();
assert!(e.help.unwrap().contains("rofi -show drun"));
}
#[test]
fn rendering_matches_the_shape_cosmic_writes_in_its_defaults() {
let out = render(&[
bind("SUPER, , exec, rofi -show drun"),
bind("SUPER, slash, exec, rofi -show drun"),
bind("SUPER SHIFT, Q, killactive"),
]);
assert_eq!(
out,
concat!(
"{\n",
" (modifiers: [Super]): Spawn(\"rofi -show drun\"),\n",
" (modifiers: [Super], key: \"slash\"): Spawn(\"rofi -show drun\"),\n",
" (modifiers: [Super, Shift], key: \"q\"): Close,\n",
"}\n"
)
);
}
}
+4
View File
@@ -186,6 +186,10 @@ impl Emitter {
WriteKind::Projected(fields) => {
composite(&write.target, fields, previous.as_deref(), &path)?
}
// No merge with `previous`: cosmic.conf owns this value outright,
// which is the whole point of the one-way model. Anything set in
// COSMIC's own settings UI is replaced, not accumulated.
WriteKind::Verbatim(s) => s.clone(),
};
Ok(Planned {
+2
View File
@@ -9,6 +9,7 @@
//! running. Only `emit` binds to cosmic-config, behind the `emit` feature.
pub mod assets;
pub mod bind;
pub mod emit;
pub mod import;
pub mod parser;
@@ -16,6 +17,7 @@ pub mod resolve;
pub mod schema;
pub mod watch;
pub use bind::{parse_bind, Bind};
pub use emit::{EmitError, Emitter, Planned};
pub use import::{import_hypr_theme, render_report, Import};
pub use parser::{parse, Ast, ParseError, Span};
+123
View File
@@ -12,6 +12,7 @@
use std::collections::BTreeMap;
use crate::bind;
use crate::parser::{Ast, Item, Span, Spanned};
use crate::schema::{self, Entry, Range, Target, Ty};
@@ -49,6 +50,13 @@ pub enum WriteKind {
Whole(Value),
/// Field path -> value, folded from every conf key touching this target.
Projected(BTreeMap<Vec<String>, Value>),
/// Pre-rendered RON owning the whole value.
///
/// Used where the target's shape is a collection rather than a scalar, so
/// there is no `Value` to coerce into: keybindings fold many `bind` lines
/// into one map. Rendering happens at the point that understands the shape
/// (`bind::render`) instead of being reconstructed in `emit`.
Verbatim(String),
}
#[derive(Debug, Clone, PartialEq)]
@@ -234,7 +242,41 @@ pub fn resolve(ast: &Ast) -> Result<Resolved, Vec<Diagnostic>> {
let mut projected: BTreeMap<TargetKey, BTreeMap<Vec<String>, Value>> = BTreeMap::new();
let mut whole: BTreeMap<TargetKey, (Value, Span)> = BTreeMap::new();
// `bind` is the one repeatable key in the language: many lines fold into a
// single map rather than the last one winning, so it cannot go through the
// schema, which is built around one conf key naming one value.
let mut binds: Vec<(bind::Bind, Span)> = Vec::new();
for (conf, raw_value, key_span) in &flat {
if conf == "bind" {
let expanded = expand_vars(&raw_value.value, &vars);
match bind::parse_bind(&expanded, raw_value.span) {
Ok(b) => {
if let Some((prev, prev_span)) = binds
.iter()
.find(|(o, _)| o.mods == b.mods && o.key == b.key)
{
diags.push(Diagnostic {
message: format!(
"this key combination is already bound to `{}`",
prev.action
),
span: raw_value.span,
help: Some(format!("the earlier bind is on line {}", prev_span.line)),
});
continue;
}
binds.push((b, raw_value.span));
}
Err(e) => diags.push(Diagnostic {
message: e.message,
span: e.span,
help: e.help,
}),
}
continue;
}
let Some(entry) = schema::lookup(conf) else {
diags.push(Diagnostic {
message: format!("unknown key `{conf}`"),
@@ -277,6 +319,21 @@ pub fn resolve(ast: &Ast) -> Result<Resolved, Vec<Diagnostic>> {
kind: WriteKind::Projected(fields),
}));
if !binds.is_empty() {
let rendered = bind::render(&binds.iter().map(|(b, _)| b.clone()).collect::<Vec<_>>());
writes.push(Write {
// cosmic-comp merges `custom` over `defaults`
// (cosmic-settings-daemon `config/src/shortcuts/mod.rs`), so writing
// here overrides a stock shortcut without touching the system file.
target: TargetKey {
component: "com.system76.CosmicSettings.Shortcuts".into(),
version: 1,
key: "custom".into(),
},
kind: WriteKind::Verbatim(rendered),
});
}
writes.sort_by(|a, b| a.target.cmp(&b.target));
Ok(Resolved { writes })
}
@@ -346,6 +403,72 @@ mod tests {
resolve(&ast).unwrap_err()
}
#[test]
fn binds_fold_into_one_write_against_the_shortcuts_custom_key() {
let r = resolved(
"bind = SUPER, D, exec, rofi -show drun\nbind = SUPER, Q, killactive\n",
);
let w: Vec<_> = r
.writes
.iter()
.filter(|w| w.target.component == "com.system76.CosmicSettings.Shortcuts")
.collect();
assert_eq!(w.len(), 1, "every bind belongs to one map");
assert_eq!(w[0].target.key, "custom");
assert_eq!(w[0].target.version, 1);
let WriteKind::Verbatim(ron) = &w[0].kind else {
panic!("expected verbatim RON, got {:?}", w[0].kind);
};
assert!(ron.contains(r#"(modifiers: [Super], key: "d"): Spawn("rofi -show drun")"#), "{ron}");
assert!(ron.contains(r#"(modifiers: [Super], key: "q"): Close"#), "{ron}");
}
#[test]
fn a_bind_expands_variables_like_the_mainmod_idiom_everyone_uses() {
// Practically every hyprland.conf opens with `$mainMod = SUPER`.
let r = resolved("$mainMod = SUPER\nbind = $mainMod, D, exec, rofi -show drun\n");
let WriteKind::Verbatim(ron) = &r.writes.last().unwrap().kind else {
panic!("expected verbatim RON");
};
assert!(ron.contains("modifiers: [Super]"), "{ron}");
}
#[test]
fn arithmetic_is_not_applied_to_a_command() {
// `eval_arith` would happily rewrite the `-` in a command line.
let r = resolved("bind = SUPER, V, exec, pactl set-sink-volume @DEFAULT_SINK@ -5%\n");
let WriteKind::Verbatim(ron) = &r.writes.last().unwrap().kind else {
panic!("expected verbatim RON");
};
assert!(ron.contains("@DEFAULT_SINK@ -5%"), "{ron}");
}
#[test]
fn binding_the_same_combination_twice_is_an_error_not_a_silent_overwrite() {
let d = errors("bind = SUPER, D, exec, rofi -show drun\nbind = SUPER, D, killactive\n");
assert_eq!(d.len(), 1);
assert!(d[0].message.contains("already bound"), "{}", d[0].message);
assert!(d[0].help.as_ref().unwrap().contains("line 1"), "{:?}", d[0].help);
}
#[test]
fn no_binds_means_the_shortcuts_file_is_left_alone() {
// Writing an empty map would wipe shortcuts set through COSMIC's UI for
// anyone whose cosmic.conf simply does not mention keybindings.
let r = resolved("general {\n gaps_in = 4\n}\n");
assert!(r
.writes
.iter()
.all(|w| w.target.component != "com.system76.CosmicSettings.Shortcuts"));
}
#[test]
fn a_bad_bind_is_reported_with_the_rest_of_the_file() {
let d = errors("bind = SUPER, X, frobnicate\ngeneral {\n gaps_inn = 8\n}\n");
assert_eq!(d.len(), 2, "resolve reports everything in one pass: {d:?}");
}
fn find<'a>(r: &'a Resolved, component: &str, key: &str) -> &'a WriteKind {
&r.writes
.iter()