Replace user configuration with KDL and improve Rust source spacing
This commit is contained in:
1 parent
dbdfc5688e
commit
9e90de4e74
41 files changed
+1668
-559
No files matched your search
Generated
+72
-72
@@ -837,7 +837,7 @@ checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"termcolor",
|
"termcolor",
|
||||||
"unicode-width",
|
"unicode-width 0.2.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -848,9 +848,9 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "combine"
|
name = "combine"
|
||||||
version = "4.6.8"
|
version = "4.6.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e"
|
checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"memchr",
|
"memchr",
|
||||||
@@ -1305,7 +1305,7 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
"option-ext",
|
"option-ext",
|
||||||
"redox_users",
|
"redox_users",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1573,6 +1573,7 @@ dependencies = [
|
|||||||
"ab_glyph",
|
"ab_glyph",
|
||||||
"calloop 0.14.4",
|
"calloop 0.14.4",
|
||||||
"ferese-animation",
|
"ferese-animation",
|
||||||
|
"ferese-config",
|
||||||
"ferese-core",
|
"ferese-core",
|
||||||
"ferese-ipc",
|
"ferese-ipc",
|
||||||
"ferese-layout",
|
"ferese-layout",
|
||||||
@@ -1584,7 +1585,6 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"smithay",
|
"smithay",
|
||||||
"toml 0.9.12+spec-1.1.0",
|
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"xcursor",
|
"xcursor",
|
||||||
@@ -1597,6 +1597,15 @@ dependencies = [
|
|||||||
"ferese-layout",
|
"ferese-layout",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ferese-config"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"kdl",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ferese-core"
|
name = "ferese-core"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -1633,6 +1642,7 @@ dependencies = [
|
|||||||
name = "ferese-settings"
|
name = "ferese-settings"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"ferese-config",
|
||||||
"ferese-ipc",
|
"ferese-ipc",
|
||||||
"image",
|
"image",
|
||||||
"libcosmic",
|
"libcosmic",
|
||||||
@@ -1641,13 +1651,13 @@ dependencies = [
|
|||||||
"shlex 1.3.0",
|
"shlex 1.3.0",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"tokio",
|
"tokio",
|
||||||
"toml_edit",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ferese-shell"
|
name = "ferese-shell"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"ferese-config",
|
||||||
"ferese-core",
|
"ferese-core",
|
||||||
"ferese-protocols",
|
"ferese-protocols",
|
||||||
"iced_accessibility",
|
"iced_accessibility",
|
||||||
@@ -1656,8 +1666,6 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"tokio",
|
"tokio",
|
||||||
"toml 0.9.12+spec-1.1.0",
|
|
||||||
"toml_edit",
|
|
||||||
"wayland-client",
|
"wayland-client",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1675,7 +1683,7 @@ version = "0.6.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "59a98bbaacea1c0eb6a0876280051b892eb73594fd90cf3b20e9c817029c57d2"
|
checksum = "59a98bbaacea1c0eb6a0876280051b892eb73594fd90cf3b20e9c817029c57d2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"toml 0.5.11",
|
"toml",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2901,9 +2909,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.105"
|
version = "0.3.104"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e"
|
checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
@@ -2919,6 +2927,18 @@ dependencies = [
|
|||||||
"mutate_once",
|
"mutate_once",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "kdl"
|
||||||
|
version = "6.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "082ddf81b2acd76fe04412655d17befedfff1837db772f8e74c38050d25ed670"
|
||||||
|
dependencies = [
|
||||||
|
"miette",
|
||||||
|
"num-traits",
|
||||||
|
"serde",
|
||||||
|
"winnow 0.7.15",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "keyboard-types"
|
name = "keyboard-types"
|
||||||
version = "0.8.3"
|
version = "0.8.3"
|
||||||
@@ -2952,7 +2972,7 @@ version = "1.4.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7a1886916523694cd6ea3d175f03a1e5010699a2a4cc13696d83d7bea1d80638"
|
checksum = "7a1886916523694cd6ea3d175f03a1e5010699a2a4cc13696d83d7bea1d80638"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3076,9 +3096,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libredox"
|
name = "libredox"
|
||||||
version = "0.1.25"
|
version = "0.1.20"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d"
|
checksum = "28d0a00925a9f930d679b6789b721e3a7f9ed110f41b86d2497caa780c3a070a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.13.2",
|
"bitflags 2.13.2",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -3306,6 +3326,16 @@ dependencies = [
|
|||||||
"paste",
|
"paste",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "miette"
|
||||||
|
version = "7.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"unicode-width 0.1.14",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mime"
|
name = "mime"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -4132,9 +4162,9 @@ checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "portable-atomic-util"
|
name = "portable-atomic-util"
|
||||||
version = "0.2.8"
|
version = "0.2.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715"
|
checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"portable-atomic",
|
"portable-atomic",
|
||||||
]
|
]
|
||||||
@@ -4415,10 +4445,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_users"
|
name = "redox_users"
|
||||||
version = "0.5.3"
|
version = "0.5.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "60dc65c0ff1a7ae1294b0c67b9f14baf70b644404010370171787bfac1038fc0"
|
checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"getrandom 0.2.17",
|
||||||
"libredox",
|
"libredox",
|
||||||
"thiserror 2.0.21",
|
"thiserror 2.0.21",
|
||||||
]
|
]
|
||||||
@@ -4723,15 +4754,6 @@ dependencies = [
|
|||||||
"syn 3.0.6",
|
"syn 3.0.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde_spanned"
|
|
||||||
version = "1.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
|
|
||||||
dependencies = [
|
|
||||||
"serde_core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha2"
|
name = "sha2"
|
||||||
version = "0.10.9"
|
version = "0.10.9"
|
||||||
@@ -5351,30 +5373,6 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "toml"
|
|
||||||
version = "0.9.12+spec-1.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863"
|
|
||||||
dependencies = [
|
|
||||||
"indexmap",
|
|
||||||
"serde_core",
|
|
||||||
"serde_spanned",
|
|
||||||
"toml_datetime 0.7.5+spec-1.1.0",
|
|
||||||
"toml_parser",
|
|
||||||
"toml_writer",
|
|
||||||
"winnow 0.7.15",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "toml_datetime"
|
|
||||||
version = "0.7.5+spec-1.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347"
|
|
||||||
dependencies = [
|
|
||||||
"serde_core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_datetime"
|
name = "toml_datetime"
|
||||||
version = "1.1.1+spec-1.1.0"
|
version = "1.1.1+spec-1.1.0"
|
||||||
@@ -5391,9 +5389,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614"
|
checksum = "1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"toml_datetime 1.1.1+spec-1.1.0",
|
"toml_datetime",
|
||||||
"toml_parser",
|
"toml_parser",
|
||||||
"toml_writer",
|
|
||||||
"winnow 1.0.4",
|
"winnow 1.0.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -5406,12 +5403,6 @@ dependencies = [
|
|||||||
"winnow 1.0.4",
|
"winnow 1.0.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "toml_writer"
|
|
||||||
version = "1.1.2+spec-1.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing"
|
name = "tracing"
|
||||||
version = "0.1.44"
|
version = "0.1.44"
|
||||||
@@ -5615,6 +5606,12 @@ version = "0.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94"
|
checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-width"
|
||||||
|
version = "0.1.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-width"
|
name = "unicode-width"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
@@ -5723,9 +5720,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen"
|
name = "wasm-bindgen"
|
||||||
version = "0.2.128"
|
version = "0.2.127"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf"
|
checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
@@ -5736,9 +5733,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-futures"
|
name = "wasm-bindgen-futures"
|
||||||
version = "0.4.78"
|
version = "0.4.77"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928"
|
checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
@@ -5746,9 +5743,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro"
|
name = "wasm-bindgen-macro"
|
||||||
version = "0.2.128"
|
version = "0.2.127"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed"
|
checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"wasm-bindgen-macro-support",
|
"wasm-bindgen-macro-support",
|
||||||
@@ -5756,22 +5753,22 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro-support"
|
name = "wasm-bindgen-macro-support"
|
||||||
version = "0.2.128"
|
version = "0.2.127"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a"
|
checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bumpalo",
|
"bumpalo",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 3.0.6",
|
"syn 2.0.119",
|
||||||
"wasm-bindgen-shared",
|
"wasm-bindgen-shared",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-shared"
|
name = "wasm-bindgen-shared"
|
||||||
version = "0.2.128"
|
version = "0.2.127"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e"
|
checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
@@ -5953,9 +5950,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "web-sys"
|
name = "web-sys"
|
||||||
version = "0.3.105"
|
version = "0.3.104"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8"
|
checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
@@ -6874,6 +6871,9 @@ name = "winnow"
|
|||||||
version = "0.7.15"
|
version = "0.7.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
|
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winnow"
|
name = "winnow"
|
||||||
|
|||||||
+1
-1
@@ -4,6 +4,7 @@ members = [
|
|||||||
"feresectl",
|
"feresectl",
|
||||||
"crates/ferese-animation",
|
"crates/ferese-animation",
|
||||||
"crates/ferese-core",
|
"crates/ferese-core",
|
||||||
|
"crates/ferese-config",
|
||||||
"crates/ferese-ipc",
|
"crates/ferese-ipc",
|
||||||
"crates/ferese-layout",
|
"crates/ferese-layout",
|
||||||
"crates/ferese-protocols",
|
"crates/ferese-protocols",
|
||||||
@@ -40,7 +41,6 @@ tracing-subscriber = { version = "0.3.20", features = ["env-filter"] }
|
|||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
serde_json = "1.0.145"
|
serde_json = "1.0.145"
|
||||||
tempfile = "3.23.0"
|
tempfile = "3.23.0"
|
||||||
toml = "0.9.7"
|
|
||||||
xcursor = "0.3.11"
|
xcursor = "0.3.11"
|
||||||
wayland-backend = "0.3.17"
|
wayland-backend = "0.3.17"
|
||||||
wayland-client = "0.31.15"
|
wayland-client = "0.31.15"
|
||||||
|
|||||||
@@ -101,10 +101,12 @@ raising the window; clicking raises it within its layer.
|
|||||||
## Make it yours
|
## Make it yours
|
||||||
|
|
||||||
Open **Control Center → Settings**, or run `ferese-settings`.
|
Open **Control Center → Settings**, or run `ferese-settings`.
|
||||||
Advanced options live in `~/.config/ferese/config.toml` (or under
|
Advanced options live in `~/.config/ferese/config.kdl` (or under
|
||||||
`$XDG_CONFIG_HOME`). Changes reload automatically. The complete
|
`$XDG_CONFIG_HOME`). Changes reload automatically. The complete
|
||||||
[configuration reference](docs/configuration.md) covers every supported option;
|
[configuration reference](docs/configuration.md) covers every supported option;
|
||||||
the [example config](packaging/config.toml) is a starting point.
|
the [example config](packaging/config.kdl) is a starting point. KDL uses compact nested sections
|
||||||
|
and bindings such as `binding "Swipe3Up" "toggle-overview"`. See the
|
||||||
|
[configuration guide](docs/configuration.md#kdl-syntax).
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ license.workspace = true
|
|||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
ferese-config = { path = "../crates/ferese-config" }
|
||||||
ab_glyph = "0.2.32"
|
ab_glyph = "0.2.32"
|
||||||
fontdb = "0.23.0"
|
fontdb = "0.23.0"
|
||||||
calloop.workspace = true
|
calloop.workspace = true
|
||||||
@@ -20,7 +21,6 @@ image = { version = "0.25.10", default-features = false, features = ["png", "jpe
|
|||||||
smithay.workspace = true
|
smithay.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
toml.workspace = true
|
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
tracing-subscriber.workspace = true
|
tracing-subscriber.workspace = true
|
||||||
xcursor.workspace = true
|
xcursor.workspace = true
|
||||||
+136
-113
@@ -1,5 +1,4 @@
|
|||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::env;
|
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
@@ -265,7 +264,7 @@ struct ThemeConfig {
|
|||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
pub(crate) fn parse_source(source: &str) -> Result<Self, ConfigError> {
|
pub(crate) fn parse_source(source: &str) -> Result<Self, ConfigError> {
|
||||||
toml::from_str(source).map_err(|source| ConfigError::Parse {
|
ferese_config::from_str(source).map_err(|source| ConfigError::Parse {
|
||||||
path: config_path().unwrap_or_default(),
|
path: config_path().unwrap_or_default(),
|
||||||
source,
|
source,
|
||||||
})
|
})
|
||||||
@@ -308,6 +307,7 @@ impl Config {
|
|||||||
overview_font_family: self.overview_font_family(),
|
overview_font_family: self.overview_font_family(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn overview_font_family(&self) -> String {
|
pub(crate) fn overview_font_family(&self) -> String {
|
||||||
self.theme
|
self.theme
|
||||||
.typography
|
.typography
|
||||||
@@ -315,6 +315,7 @@ impl Config {
|
|||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| "sans-serif".into())
|
.unwrap_or_else(|| "sans-serif".into())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn wallpaper_settings(&self) -> crate::wallpaper::WallpaperConfig {
|
pub(crate) fn wallpaper_settings(&self) -> crate::wallpaper::WallpaperConfig {
|
||||||
self.theme.background.clone()
|
self.theme.background.clone()
|
||||||
}
|
}
|
||||||
@@ -738,7 +739,7 @@ pub enum ConfigError {
|
|||||||
},
|
},
|
||||||
Parse {
|
Parse {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
source: toml::de::Error,
|
source: ferese_config::Error,
|
||||||
},
|
},
|
||||||
InvalidColumnWidth {
|
InvalidColumnWidth {
|
||||||
field: &'static str,
|
field: &'static str,
|
||||||
@@ -827,7 +828,7 @@ impl Config {
|
|||||||
Err(source) => return Err(ConfigError::Read { path, source }),
|
Err(source) => return Err(ConfigError::Read { path, source }),
|
||||||
};
|
};
|
||||||
|
|
||||||
toml::from_str(&source).map_err(|source| ConfigError::Parse { path, source })
|
ferese_config::from_str(&source).map_err(|source| ConfigError::Parse { path, source })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn layout_mode(&self) -> LayoutMode {
|
pub fn layout_mode(&self) -> LayoutMode {
|
||||||
@@ -1813,42 +1814,59 @@ fn parse_column_width(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn config_path() -> Option<PathBuf> {
|
pub(crate) fn config_path() -> Option<PathBuf> {
|
||||||
if let Some(directory) = env::var_os("XDG_CONFIG_HOME").filter(|value| !value.is_empty()) {
|
ferese_config::config_path()
|
||||||
return Some(PathBuf::from(directory).join("ferese/config.toml"));
|
|
||||||
}
|
|
||||||
|
|
||||||
env::var_os("HOME")
|
|
||||||
.filter(|value| !value.is_empty())
|
|
||||||
.map(PathBuf::from)
|
|
||||||
.map(|home| home.join(".config/ferese/config.toml"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn packaged_and_custom_kdl_pass_runtime_validation() {
|
||||||
|
Config::parse_source(include_str!("../../packaging/config.kdl"))
|
||||||
|
.unwrap()
|
||||||
|
.runtime_config()
|
||||||
|
.unwrap();
|
||||||
|
let source = "input {\n touchpad {\n swipe-threshold 96\n }\n}\nbinding keys=\"Swipe3Up\" action=\"toggle-overview\"\noutput-profile name=\"desk\" {\n output match=\"DP-1\" scale=1.5 {\n position 0 0\n }\n}\ndesktop-widgets {\n clock {\n enabled #true\n outputs \"DP-1\"\n }\n}\nautostart {\n command \"program\" \"argument with space\"\n}\n";
|
||||||
|
let config = Config::parse_source(source).unwrap();
|
||||||
|
config.runtime_config().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
config.input_settings().unwrap().touchpad.swipe_threshold,
|
||||||
|
96
|
||||||
|
);
|
||||||
|
assert_eq!(config.desktop_widgets.clock.outputs, ["DP-1"]);
|
||||||
|
assert_eq!(
|
||||||
|
config.output_profiles().unwrap()[0].outputs[0].position,
|
||||||
|
Some([0, 0])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn desktop_clock_settings_are_validated_before_live_publication() {
|
fn desktop_clock_settings_are_validated_before_live_publication() {
|
||||||
assert!(
|
assert!(
|
||||||
parse(
|
parse(
|
||||||
"[desktop_widgets.clock]\nenabled = true\nanchor = 'center'\ntime_format = '%H:%M'"
|
"desktop-widgets {\n clock {\n enabled #true\n anchor \"center\"\n time-format \"%H:%M\"\n }\n}\n"
|
||||||
)
|
)
|
||||||
.runtime_config()
|
.runtime_config()
|
||||||
.is_ok()
|
.is_ok()
|
||||||
);
|
);
|
||||||
for source in [
|
for source in [
|
||||||
"[desktop_widgets.clock]\nwidth = 8192",
|
"desktop-widgets {\n clock {\n width 8192\n }\n}\n",
|
||||||
"[desktop_widgets.clock]\nopacity = nan",
|
"desktop-widgets {\n clock {\n opacity #nan\n }\n}\n",
|
||||||
"[desktop_widgets.clock]\ntime_format = '%'",
|
"desktop-widgets {\n clock {\n time-format \"%\"\n }\n}\n",
|
||||||
"[desktop_widgets.clock]\ntime_zone = 'invalid/zone'",
|
"desktop-widgets {\n clock {\n time-zone \"invalid/zone\"\n }\n}\n",
|
||||||
] {
|
] {
|
||||||
assert!(parse(source).runtime_config().is_err());
|
assert!(
|
||||||
|
Config::parse_source(source)
|
||||||
|
.and_then(|config| config.runtime_config())
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
assert!(Config::parse_source("[desktop_widgets.clock]\nanchor = 'wrong'").is_err());
|
assert!(Config::parse_source("desktop-widgets { clock { anchor \"wrong\"; }; }").is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse(source: &str) -> Config {
|
fn parse(source: &str) -> Config {
|
||||||
toml::from_str(source).unwrap()
|
ferese_config::from_str(source).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1865,8 +1883,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn accepts_full_and_numeric_column_widths() {
|
fn accepts_full_and_numeric_column_widths() {
|
||||||
let full = parse("[scrolling]\ndefault_column_width = \"full\"");
|
let full = parse("scrolling {\n default-column-width \"full\"\n}\n");
|
||||||
let numeric = parse("[scrolling]\ndefault_column_width = 1.0");
|
let numeric = parse("scrolling {\n default-column-width 1.0\n}\n");
|
||||||
|
|
||||||
assert_eq!(full.default_column_width().unwrap(), ColumnWidth::Full);
|
assert_eq!(full.default_column_width().unwrap(), ColumnWidth::Full);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -1878,7 +1896,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn parses_width_presets_and_supplies_defaults() {
|
fn parses_width_presets_and_supplies_defaults() {
|
||||||
let defaults = parse("");
|
let defaults = parse("");
|
||||||
let configured = parse("[scrolling]\nwidth_presets = [0.5, 1.0, \"full\"]");
|
let configured = parse("scrolling {\n width-presets 0.5 1.0 \"full\"\n}\n");
|
||||||
|
|
||||||
assert_eq!(defaults.width_presets().unwrap().len(), 4);
|
assert_eq!(defaults.width_presets().unwrap().len(), 4);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -1894,8 +1912,8 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn parses_scrolling_focus_strategy() {
|
fn parses_scrolling_focus_strategy() {
|
||||||
let minimal = parse("");
|
let minimal = parse("");
|
||||||
let centered = parse("[scrolling]\nfocus_strategy = \"center_on_focus\"");
|
let centered = parse("scrolling {\n focus-strategy \"center_on_focus\"\n}\n");
|
||||||
let paged = parse("[scrolling]\nfocus_strategy = \"paged\"");
|
let paged = parse("scrolling {\n focus-strategy \"paged\"\n}\n");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
paged.scrolling_focus_strategy(),
|
paged.scrolling_focus_strategy(),
|
||||||
ViewportFocusStrategy::Paged
|
ViewportFocusStrategy::Paged
|
||||||
@@ -1913,8 +1931,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rejects_non_positive_or_unknown_column_widths() {
|
fn rejects_non_positive_or_unknown_column_widths() {
|
||||||
let zero = parse("[scrolling]\ndefault_column_width = 0.0");
|
let zero = parse("scrolling {\n default-column-width 0.0\n}\n");
|
||||||
let unknown = parse("[scrolling]\ndefault_column_width = \"wide\"");
|
let unknown = parse("scrolling {\n default-column-width \"wide\"\n}\n");
|
||||||
|
|
||||||
assert!(zero.default_column_width().is_err());
|
assert!(zero.default_column_width().is_err());
|
||||||
assert!(unknown.default_column_width().is_err());
|
assert!(unknown.default_column_width().is_err());
|
||||||
@@ -1923,7 +1941,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn parses_animation_policy_and_reduced_motion() {
|
fn parses_animation_policy_and_reduced_motion() {
|
||||||
let config = parse(
|
let config = parse(
|
||||||
"[animations]\nspeed = 1.5\nreduced_motion = true\n\n[animations.spring]\nmass = 2.0\nstiffness = 500.0\ndamping = 40.0",
|
"animations {\n speed 1.5\n reduced-motion #true\n spring {\n mass 2.0\n stiffness 500.0\n damping 40.0\n }\n}\n",
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(!config.animations_enabled());
|
assert!(!config.animations_enabled());
|
||||||
@@ -1941,8 +1959,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rejects_invalid_animation_numbers() {
|
fn rejects_invalid_animation_numbers() {
|
||||||
let speed = parse("[animations]\nspeed = 0.0");
|
let speed = parse("animations {\n speed 0.0\n}\n");
|
||||||
let damping = parse("[animations.spring]\ndamping = -1.0");
|
let damping = parse("animations {\n spring {\n damping -1.0\n }\n}\n");
|
||||||
|
|
||||||
assert!(speed.animation_speed().is_err());
|
assert!(speed.animation_speed().is_err());
|
||||||
assert!(damping.spring_config().is_err());
|
assert!(damping.spring_config().is_err());
|
||||||
@@ -1950,8 +1968,9 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parses_and_validates_layout_gaps() {
|
fn parses_and_validates_layout_gaps() {
|
||||||
let configured = parse("[layout]\ninner_gap = 6.0\nouter_gap = 14.0\nsmart_gaps = true");
|
let configured =
|
||||||
let invalid = parse("[layout]\nouter_gap = -1.0");
|
parse("layout {\n inner-gap 6.0\n outer-gap 14.0\n smart-gaps #true\n}\n");
|
||||||
|
let invalid = parse("layout {\n outer-gap -1.0\n}\n");
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
configured.gap_config().unwrap(),
|
configured.gap_config().unwrap(),
|
||||||
@@ -1967,11 +1986,14 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn parses_and_validates_theme_window_tokens() {
|
fn parses_and_validates_theme_window_tokens() {
|
||||||
let configured = parse(
|
let configured = parse(
|
||||||
"[theme.colors]\nborder = \"#11223344\"\naccent = \"#AABBCC\"\nshadow = \"#01020380\"\n\n[theme.geometry]\nborder_width = 1.5\nfocus_ring_width = 3.0\nwindow_radius = 12.0\n\n[theme.shadow.soft]\noffset_y = -2.0\nblur = 24.0\nopacity = 0.4\n\n[theme.material]\nstyle = \"translucent\"",
|
"theme {\n colors {\n border \"#11223344\"\n accent \"#AABBCC\"\n shadow \"#01020380\"\n }\n geometry {\n border-width 1.5\n focus-ring-width 3.0\n window-radius 12.0\n }\n shadow {\n soft {\n offset-y -2.0\n blur 24.0\n opacity 0.4\n }\n }\n material {\n style \"translucent\"\n }\n}\n",
|
||||||
|
);
|
||||||
|
let invalid_color = parse("theme {\n colors {\n accent \"blue\"\n }\n}\n");
|
||||||
|
let invalid_radius =
|
||||||
|
parse("theme {\n geometry {\n window-radius -1.0\n }\n}\n");
|
||||||
|
let invalid_opacity = parse(
|
||||||
|
"theme {\n shadow {\n soft {\n opacity 1.1\n }\n }\n}\n",
|
||||||
);
|
);
|
||||||
let invalid_color = parse("[theme.colors]\naccent = \"blue\"");
|
|
||||||
let invalid_radius = parse("[theme.geometry]\nwindow_radius = -1.0");
|
|
||||||
let invalid_opacity = parse("[theme.shadow.soft]\nopacity = 1.1");
|
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
configured.theme_settings().unwrap(),
|
configured.theme_settings().unwrap(),
|
||||||
@@ -2003,7 +2025,12 @@ mod tests {
|
|||||||
assert!(invalid_color.theme_settings().is_err());
|
assert!(invalid_color.theme_settings().is_err());
|
||||||
assert!(invalid_radius.theme_settings().is_err());
|
assert!(invalid_radius.theme_settings().is_err());
|
||||||
assert!(invalid_opacity.theme_settings().is_err());
|
assert!(invalid_opacity.theme_settings().is_err());
|
||||||
assert!(toml::from_str::<Config>("[theme.material]\nstyle = \"mist\"").is_err());
|
assert!(
|
||||||
|
ferese_config::from_str::<Config>(
|
||||||
|
"theme {\n material {\n style \"mist\"\n }\n}\n"
|
||||||
|
)
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -2012,7 +2039,12 @@ mod tests {
|
|||||||
parse("").theme_settings().unwrap().material_style,
|
parse("").theme_settings().unwrap().material_style,
|
||||||
MaterialStyle::Solid
|
MaterialStyle::Solid
|
||||||
);
|
);
|
||||||
assert!(toml::from_str::<Config>("[theme.material]\nstyle = \"glass\"").is_err());
|
assert!(
|
||||||
|
ferese_config::from_str::<Config>(
|
||||||
|
"theme {\n material {\n style \"glass\"\n }\n}\n"
|
||||||
|
)
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -2021,14 +2053,14 @@ mod tests {
|
|||||||
assert_eq!(defaults.border_gradient, None);
|
assert_eq!(defaults.border_gradient, None);
|
||||||
assert_eq!(defaults.focus_ring_gradient, None);
|
assert_eq!(defaults.focus_ring_gradient, None);
|
||||||
let configured =
|
let configured =
|
||||||
parse("[theme.focus_ring.gradient]\nfrom = '#e5c890'\nto = '#b98d5880'\nangle = -45")
|
parse("theme {\n focus-ring {\n gradient {\n from \"#e5c890\"\n to \"#b98d5880\"\n angle -45\n }\n }\n}\n")
|
||||||
.theme_settings()
|
.theme_settings()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let gradient = configured.focus_ring_gradient.unwrap();
|
let gradient = configured.focus_ring_gradient.unwrap();
|
||||||
assert_eq!(configured.border_gradient, None);
|
assert_eq!(configured.border_gradient, None);
|
||||||
assert_eq!(gradient.angle, 315.0);
|
assert_eq!(gradient.angle, 315.0);
|
||||||
assert_eq!(gradient.to.0[3], 128.0 / 255.0);
|
assert_eq!(gradient.to.0[3], 128.0 / 255.0);
|
||||||
let border = parse("[theme.border.gradient]\nfrom = '#112233'\nto = '#445566'")
|
let border = parse("theme {\n border {\n gradient {\n from \"#112233\"\n to \"#445566\"\n }\n }\n}\n")
|
||||||
.theme_settings()
|
.theme_settings()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(border.border_gradient.unwrap().angle, 0.0);
|
assert_eq!(border.border_gradient.unwrap().angle, 0.0);
|
||||||
@@ -2040,23 +2072,25 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn border_gradients_reject_invalid_colors_and_nonfinite_angles() {
|
fn border_gradients_reject_invalid_colors_and_nonfinite_angles() {
|
||||||
for settings in [
|
for settings in [
|
||||||
"from = 'invalid'\nto = '#445566'",
|
"from \"invalid\"\nto \"#445566\"\n",
|
||||||
"from = '#112233'\nto = 'invalid'",
|
"from \"#112233\"\nto \"invalid\"\n",
|
||||||
"from = '#112233'\nto = '#445566'\nangle = nan",
|
"from \"#112233\"\nto \"#445566\"\nangle #nan\n",
|
||||||
"from = '#112233'\nto = '#445566'\nangle = inf",
|
"from \"#112233\"\nto \"#445566\"\nangle #inf\n",
|
||||||
] {
|
] {
|
||||||
assert!(
|
assert!(
|
||||||
parse(&format!("[theme.focus_ring.gradient]\n{settings}"))
|
Config::parse_source(&format!(
|
||||||
.theme_settings()
|
"theme {{ focus-ring {{ gradient {{\n{settings}\n}} }} }}"
|
||||||
.is_err()
|
))
|
||||||
|
.and_then(|config| config.theme_settings())
|
||||||
|
.is_err()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
assert!(toml::from_str::<Config>("[theme.border.gradient]\nfrom = '#112233'").is_err());
|
assert!(ferese_config::from_str::<Config>("theme {\n border {\n gradient {\n from \"#112233\"\n }\n }\n}\n").is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn material_color_comes_from_surface_base() {
|
fn material_color_comes_from_surface_base() {
|
||||||
let theme = parse("[theme.colors]\nsurface_base = \"#000000\"\n[theme.surface.bar]\nbackground = \"#FFFFFF\"")
|
let theme = parse("theme {\n colors {\n surface-base \"#000000\"\n }\n surface {\n bar {\n background \"#FFFFFF\"\n }\n }\n}\n")
|
||||||
.theme_settings()
|
.theme_settings()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(theme.surface_base_color, RgbaColor([0.0, 0.0, 0.0, 1.0]));
|
assert_eq!(theme.surface_base_color, RgbaColor([0.0, 0.0, 0.0, 1.0]));
|
||||||
@@ -2067,18 +2101,22 @@ mod tests {
|
|||||||
assert_eq!(parse("").theme_settings().unwrap().panel_opacity, 0.78);
|
assert_eq!(parse("").theme_settings().unwrap().panel_opacity, 0.78);
|
||||||
for opacity in [0.0, 0.65, 1.0] {
|
for opacity in [0.0, 0.65, 1.0] {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse(&format!("[theme.surface.bar]\nopacity = {opacity}"))
|
parse(&format!(
|
||||||
.theme_settings()
|
"theme {{ surface {{ bar {{ opacity {opacity}; }} }} }}"
|
||||||
.unwrap()
|
))
|
||||||
.panel_opacity,
|
.theme_settings()
|
||||||
|
.unwrap()
|
||||||
|
.panel_opacity,
|
||||||
opacity
|
opacity
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
for opacity in ["-0.1", "1.1", "nan", "inf"] {
|
for opacity in ["-0.1", "1.1", "#nan", "#inf"] {
|
||||||
assert!(
|
assert!(
|
||||||
parse(&format!("[theme.surface.bar]\nopacity = {opacity}"))
|
Config::parse_source(&format!(
|
||||||
.theme_settings()
|
"theme {{ surface {{ bar {{ opacity {opacity}; }} }} }}"
|
||||||
.is_err()
|
))
|
||||||
|
.and_then(|config| config.theme_settings())
|
||||||
|
.is_err()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2087,23 +2125,23 @@ mod tests {
|
|||||||
fn backdrop_blur_is_configurable_without_glass_settings() {
|
fn backdrop_blur_is_configurable_without_glass_settings() {
|
||||||
assert_eq!(parse("").theme_settings().unwrap().backdrop_blur, 12.0);
|
assert_eq!(parse("").theme_settings().unwrap().backdrop_blur, 12.0);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse("[theme.material]\nstyle = \"translucent\"\nblur_radius = 100")
|
parse("theme {\n material {\n style \"translucent\"\n blur-radius 100\n }\n}\n")
|
||||||
.theme_settings()
|
.theme_settings()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.backdrop_blur,
|
.backdrop_blur,
|
||||||
32.0
|
32.0
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse("[theme.material]\nblur_radius = 0")
|
parse("theme {\n material {\n blur-radius 0\n }\n}\n")
|
||||||
.theme_settings()
|
.theme_settings()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.backdrop_blur,
|
.backdrop_blur,
|
||||||
0.0
|
0.0
|
||||||
);
|
);
|
||||||
for value in ["-1", "nan", "inf"] {
|
for value in ["-1", "#nan", "#inf"] {
|
||||||
assert!(
|
assert!(
|
||||||
parse(&format!("[theme.material]\nblur_radius = {value}"))
|
Config::parse_source(&format!("theme {{ material {{ blur-radius {value}; }} }}"))
|
||||||
.theme_settings()
|
.and_then(|config| config.theme_settings())
|
||||||
.is_err()
|
.is_err()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -2116,7 +2154,7 @@ mod tests {
|
|||||||
assert_eq!(defaults.amount, 0.15);
|
assert_eq!(defaults.amount, 0.15);
|
||||||
assert_eq!(defaults.duration_ms, 150.0);
|
assert_eq!(defaults.duration_ms, 150.0);
|
||||||
let settings =
|
let settings =
|
||||||
parse("[appearance.inactive_dim]\nenabled = true\namount = 0.25\nduration_ms = 100")
|
parse("appearance {\n inactive-dim {\n enabled #true\n amount 0.25\n duration-ms 100\n }\n}\n")
|
||||||
.theme_settings()
|
.theme_settings()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.inactive_dim;
|
.inactive_dim;
|
||||||
@@ -2126,14 +2164,16 @@ mod tests {
|
|||||||
for (key, value) in [
|
for (key, value) in [
|
||||||
("amount", "-0.1"),
|
("amount", "-0.1"),
|
||||||
("amount", "1.1"),
|
("amount", "1.1"),
|
||||||
("amount", "nan"),
|
("amount", "#nan"),
|
||||||
("duration_ms", "-1"),
|
("duration_ms", "-1"),
|
||||||
("duration_ms", "inf"),
|
("duration_ms", "#inf"),
|
||||||
] {
|
] {
|
||||||
assert!(
|
assert!(
|
||||||
parse(&format!("[appearance.inactive_dim]\n{key} = {value}"))
|
Config::parse_source(&format!(
|
||||||
.theme_settings()
|
"appearance {{ inactive-dim {{ {key} {value}; }} }}"
|
||||||
.is_err()
|
))
|
||||||
|
.and_then(|config| config.theme_settings())
|
||||||
|
.is_err()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2141,7 +2181,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn parses_input_and_touchpad_settings() {
|
fn parses_input_and_touchpad_settings() {
|
||||||
let config = parse(
|
let config = parse(
|
||||||
"[input]\nfocus_follows_mouse = true\nxkb_layout = \"us,de\"\nxkb_variant = \",nodeadkeys\"\nxkb_options = [\"grp:alt_shift_toggle\"]\nrepeat_rate = 30\nrepeat_delay_ms = 450\n\n[input.touchpad]\ntap = false\nnatural_scroll = false\ndisable_while_typing = true",
|
"input {\n focus-follows-mouse #true\n xkb-layout \"us,de\"\n xkb-variant \",nodeadkeys\"\n xkb-options \"grp:alt_shift_toggle\"\n repeat-rate 30\n repeat-delay-ms 450\n touchpad {\n tap #false\n natural-scroll #false\n disable-while-typing #true\n }\n}\n",
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -2165,9 +2205,9 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rejects_invalid_input_settings() {
|
fn rejects_invalid_input_settings() {
|
||||||
let layout = parse("[input]\nxkb_layout = \"\"");
|
let layout = parse("input {\n xkb-layout \"\"\n}\n");
|
||||||
let rate = parse("[input]\nrepeat_rate = 0");
|
let rate = parse("input {\n repeat-rate 0\n}\n");
|
||||||
let delay = parse("[input]\nrepeat_delay_ms = -1");
|
let delay = parse("input {\n repeat-delay-ms -1\n}\n");
|
||||||
|
|
||||||
assert!(layout.input_settings().is_err());
|
assert!(layout.input_settings().is_err());
|
||||||
assert!(rate.input_settings().is_err());
|
assert!(rate.input_settings().is_err());
|
||||||
@@ -2178,7 +2218,7 @@ mod tests {
|
|||||||
fn swipe_bindings_can_override_actions_disable_defaults_and_set_distance() {
|
fn swipe_bindings_can_override_actions_disable_defaults_and_set_distance() {
|
||||||
use crate::gestures::SwipeDirection;
|
use crate::gestures::SwipeDirection;
|
||||||
let config = parse(
|
let config = parse(
|
||||||
"[input.touchpad]\nswipe_threshold = 120\n[[bindings]]\nkeys = 'Swipe3Up'\naction = 'toggle-overview'\n[[bindings]]\nkeys = 'Swipe3Left'\naction = 'move'\nargument = 'left'\n[[bindings]]\nkeys = 'Swipe3Down'\ndisabled = true",
|
"input {\n touchpad {\n swipe-threshold 120\n }\n}\nbinding keys=\"Swipe3Up\" action=\"toggle-overview\"\nbinding keys=\"Swipe3Left\" action=\"move\" argument=\"left\"\nbinding keys=\"Swipe3Down\" disabled=#true\n",
|
||||||
);
|
);
|
||||||
let input = config.input_settings().unwrap();
|
let input = config.input_settings().unwrap();
|
||||||
assert_eq!(input.touchpad.swipe_threshold, 120);
|
assert_eq!(input.touchpad.swipe_threshold, 120);
|
||||||
@@ -2202,9 +2242,11 @@ mod tests {
|
|||||||
);
|
);
|
||||||
for threshold in [0, 15, 1001] {
|
for threshold in [0, 15, 1001] {
|
||||||
assert!(
|
assert!(
|
||||||
parse(&format!("[input.touchpad]\nswipe_threshold = {threshold}"))
|
parse(&format!(
|
||||||
.input_settings()
|
"input {{ touchpad {{ swipe-threshold {threshold}; }} }}"
|
||||||
.is_err()
|
))
|
||||||
|
.input_settings()
|
||||||
|
.is_err()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2212,12 +2254,10 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn gesture_bindings_validate_fingers_directions_and_actions() {
|
fn gesture_bindings_validate_fingers_directions_and_actions() {
|
||||||
for keys in ["Swipe2Up", "Swipe3Diagonal", "Super+Swipe3Up"] {
|
for keys in ["Swipe2Up", "Swipe3Diagonal", "Super+Swipe3Up"] {
|
||||||
let config = parse(&format!(
|
let config = parse(&format!("binding \"{keys}\" \"toggle-overview\""));
|
||||||
"[[bindings]]\nkeys = '{keys}'\naction = 'toggle-overview'"
|
|
||||||
));
|
|
||||||
assert!(config.bindings(&config.input_settings().unwrap()).is_err());
|
assert!(config.bindings(&config.input_settings().unwrap()).is_err());
|
||||||
}
|
}
|
||||||
let config = parse("[[bindings]]\nkeys = 'Swipe4Up'\naction = 'toggle-overview'");
|
let config = parse("binding keys=\"Swipe4Up\" action=\"toggle-overview\"\n");
|
||||||
let bindings = config.bindings(&config.input_settings().unwrap()).unwrap();
|
let bindings = config.bindings(&config.input_settings().unwrap()).unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
bindings
|
bindings
|
||||||
@@ -2277,9 +2317,9 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn replaces_and_unbinds_default_bindings() {
|
fn replaces_and_unbinds_default_bindings() {
|
||||||
let replaced = parse(
|
let replaced = parse(
|
||||||
"[commands]\nterm = [\"foot\", \"--app-id\", \"work\"]\n\n[[bindings]]\nkeys = \"Super+Enter\"\naction = \"spawn\"\nargument = \"term\"",
|
"commands {\n term \"foot\" \"--app-id\" \"work\"\n}\nbinding keys=\"Super+Enter\" action=\"spawn\" argument=\"term\"\n",
|
||||||
);
|
);
|
||||||
let unbound = parse("[[bindings]]\nkeys = \"Super+Q\"\ndisabled = true");
|
let unbound = parse("binding keys=\"Super+Q\" disabled=#true\n");
|
||||||
|
|
||||||
let input = replaced.input_settings().unwrap();
|
let input = replaced.input_settings().unwrap();
|
||||||
let bindings = replaced.bindings(&input).unwrap();
|
let bindings = replaced.bindings(&input).unwrap();
|
||||||
@@ -2306,7 +2346,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn accepts_physical_xkb_key_names() {
|
fn accepts_physical_xkb_key_names() {
|
||||||
let config = parse(
|
let config = parse(
|
||||||
"[[bindings]]\nkeys = \"Super+AD06\"\nmatch = \"physical\"\naction = \"focus\"\nargument = \"left\"",
|
"binding keys=\"Super+AD06\" match=\"physical\" action=\"focus\" argument=\"left\"\n",
|
||||||
);
|
);
|
||||||
let input = config.input_settings().unwrap();
|
let input = config.input_settings().unwrap();
|
||||||
let bindings = config.bindings(&input).unwrap();
|
let bindings = config.bindings(&input).unwrap();
|
||||||
@@ -2320,13 +2360,12 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn rejects_duplicate_or_invalid_user_bindings() {
|
fn rejects_duplicate_or_invalid_user_bindings() {
|
||||||
let duplicate = parse(
|
let duplicate = parse(
|
||||||
"[[bindings]]\nkeys = \"Super+Q\"\naction = \"close\"\n\n[[bindings]]\nkeys = \"logo+q\"\naction = \"close\"",
|
"binding keys=\"Super+Q\" action=\"close\"\nbinding keys=\"logo+q\" action=\"close\"\n",
|
||||||
);
|
|
||||||
let missing_command = parse(
|
|
||||||
"[[bindings]]\nkeys = \"Super+Enter\"\naction = \"spawn\"\nargument = \"missing\"",
|
|
||||||
);
|
);
|
||||||
|
let missing_command =
|
||||||
|
parse("binding keys=\"Super+Enter\" action=\"spawn\" argument=\"missing\"\n");
|
||||||
let invalid_argument =
|
let invalid_argument =
|
||||||
parse("[[bindings]]\nkeys = \"Super+Q\"\naction = \"close\"\nargument = \"left\"");
|
parse("binding keys=\"Super+Q\" action=\"close\" argument=\"left\"\n");
|
||||||
|
|
||||||
for config in [duplicate, missing_command, invalid_argument] {
|
for config in [duplicate, missing_command, invalid_argument] {
|
||||||
let input = config.input_settings().unwrap();
|
let input = config.input_settings().unwrap();
|
||||||
@@ -2337,7 +2376,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn parses_and_validates_window_rules() {
|
fn parses_and_validates_window_rules() {
|
||||||
let config = parse(
|
let config = parse(
|
||||||
"[[window_rules]]\napp_id = \"org.example.Editor\"\nworkspace = 3\nfloating = true\nwidth = 900.0\nheight = 600.0\nfullscreen = false",
|
"window-rule app-id=\"org.example.Editor\" workspace=3 floating=#true width=900.0 height=600.0 fullscreen=#false\n",
|
||||||
);
|
);
|
||||||
let rules = config.window_rules().unwrap();
|
let rules = config.window_rules().unwrap();
|
||||||
let result = window_rules::resolve(
|
let result = window_rules::resolve(
|
||||||
@@ -2356,8 +2395,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rejects_invalid_window_rule_configuration() {
|
fn rejects_invalid_window_rule_configuration() {
|
||||||
let catch_all = parse("[[window_rules]]\nfloating = true");
|
let catch_all = parse("window-rule floating=#true\n");
|
||||||
let zero_workspace = parse("[[window_rules]]\napp_id = \"editor\"\nworkspace = 0");
|
let zero_workspace = parse("window-rule app-id=\"editor\" workspace=0\n");
|
||||||
|
|
||||||
assert!(catch_all.window_rules().is_err());
|
assert!(catch_all.window_rules().is_err());
|
||||||
assert!(zero_workspace.window_rules().is_err());
|
assert!(zero_workspace.window_rules().is_err());
|
||||||
@@ -2376,21 +2415,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn parses_output_profiles() {
|
fn parses_output_profiles() {
|
||||||
let config = parse(
|
let config = parse(
|
||||||
r#"
|
"output-profile name=\"docked\" {\n output match=\"HDMI-A-1\" mode=\"[email protected]\" scale=1.6 {\n position 0 0\n }\n output match=\"eDP-1\" enabled=#false transform=\"rotate_90\"\n}\n",
|
||||||
[[output_profiles]]
|
|
||||||
name = "docked"
|
|
||||||
|
|
||||||
[[output_profiles.outputs]]
|
|
||||||
match = "HDMI-A-1"
|
|
||||||
mode = "[email protected]"
|
|
||||||
scale = 1.6
|
|
||||||
position = [0, 0]
|
|
||||||
|
|
||||||
[[output_profiles.outputs]]
|
|
||||||
match = "eDP-1"
|
|
||||||
enabled = false
|
|
||||||
transform = "rotate_90"
|
|
||||||
"#,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -2425,14 +2450,12 @@ transform = "rotate_90"
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rejects_invalid_output_profiles() {
|
fn rejects_invalid_output_profiles() {
|
||||||
let invalid_scale = parse(
|
let invalid_scale =
|
||||||
"[[output_profiles]]\nname = \"bad\"\n[[output_profiles.outputs]]\nmatch = \"eDP-1\"\nscale = 0.0",
|
parse("output-profile name=\"bad\" {\n output match=\"eDP-1\" scale=0.0\n}\n");
|
||||||
);
|
let invalid_mode =
|
||||||
let invalid_mode = parse(
|
parse("output-profile name=\"bad\" {\n output match=\"eDP-1\" mode=\"native\"\n}\n");
|
||||||
"[[output_profiles]]\nname = \"bad\"\n[[output_profiles.outputs]]\nmatch = \"eDP-1\"\nmode = \"native\"",
|
|
||||||
);
|
|
||||||
let duplicate = parse(
|
let duplicate = parse(
|
||||||
"[[output_profiles]]\nname = \"bad\"\n[[output_profiles.outputs]]\nmatch = \"eDP-1\"\n[[output_profiles.outputs]]\nmatch = \"eDP-1\"",
|
"output-profile name=\"bad\" {\n output match=\"eDP-1\"\n output match=\"eDP-1\"\n}\n",
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(invalid_scale.output_profiles().is_err());
|
assert!(invalid_scale.output_profiles().is_err());
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ impl Runner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(configs: &[DaemonConfig], nested: bool) -> Self {
|
pub fn new(configs: &[DaemonConfig], nested: bool) -> Self {
|
||||||
Self(
|
Self(
|
||||||
configs
|
configs
|
||||||
@@ -155,6 +156,7 @@ mod tests {
|
|||||||
std::thread::sleep(Duration::from_millis(10));
|
std::thread::sleep(Duration::from_millis(10));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn previews_do_not_duplicate_session_daemons() {
|
fn previews_do_not_duplicate_session_daemons() {
|
||||||
let configs = vec![DaemonConfig {
|
let configs = vec![DaemonConfig {
|
||||||
|
|||||||
@@ -368,6 +368,7 @@ pub(crate) fn begin_surface_dismiss(surface: &WlSurface) -> bool {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn fade_dismissed_surface(surface: &WlSurface, opacity: f64) {
|
pub(crate) fn fade_dismissed_surface(surface: &WlSurface, opacity: f64) {
|
||||||
with_states(surface, |states| {
|
with_states(surface, |states| {
|
||||||
if let Some(effects) = states.data_map.get::<SurfaceEffectsState>() {
|
if let Some(effects) = states.data_map.get::<SurfaceEffectsState>() {
|
||||||
|
|||||||
@@ -194,6 +194,7 @@ impl PointerGrab<Ferese> for ResizeSurfaceGrab {
|
|||||||
) {
|
) {
|
||||||
handle.gesture_swipe_begin(data, event);
|
handle.gesture_swipe_begin(data, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn gesture_swipe_update(
|
fn gesture_swipe_update(
|
||||||
&mut self,
|
&mut self,
|
||||||
data: &mut Ferese,
|
data: &mut Ferese,
|
||||||
@@ -202,6 +203,7 @@ impl PointerGrab<Ferese> for ResizeSurfaceGrab {
|
|||||||
) {
|
) {
|
||||||
handle.gesture_swipe_update(data, event);
|
handle.gesture_swipe_update(data, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn gesture_swipe_end(
|
fn gesture_swipe_end(
|
||||||
&mut self,
|
&mut self,
|
||||||
data: &mut Ferese,
|
data: &mut Ferese,
|
||||||
@@ -210,6 +212,7 @@ impl PointerGrab<Ferese> for ResizeSurfaceGrab {
|
|||||||
) {
|
) {
|
||||||
handle.gesture_swipe_end(data, event);
|
handle.gesture_swipe_end(data, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn gesture_pinch_begin(
|
fn gesture_pinch_begin(
|
||||||
&mut self,
|
&mut self,
|
||||||
data: &mut Ferese,
|
data: &mut Ferese,
|
||||||
@@ -218,6 +221,7 @@ impl PointerGrab<Ferese> for ResizeSurfaceGrab {
|
|||||||
) {
|
) {
|
||||||
handle.gesture_pinch_begin(data, event);
|
handle.gesture_pinch_begin(data, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn gesture_pinch_update(
|
fn gesture_pinch_update(
|
||||||
&mut self,
|
&mut self,
|
||||||
data: &mut Ferese,
|
data: &mut Ferese,
|
||||||
@@ -226,6 +230,7 @@ impl PointerGrab<Ferese> for ResizeSurfaceGrab {
|
|||||||
) {
|
) {
|
||||||
handle.gesture_pinch_update(data, event);
|
handle.gesture_pinch_update(data, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn gesture_pinch_end(
|
fn gesture_pinch_end(
|
||||||
&mut self,
|
&mut self,
|
||||||
data: &mut Ferese,
|
data: &mut Ferese,
|
||||||
@@ -234,6 +239,7 @@ impl PointerGrab<Ferese> for ResizeSurfaceGrab {
|
|||||||
) {
|
) {
|
||||||
handle.gesture_pinch_end(data, event);
|
handle.gesture_pinch_end(data, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn gesture_hold_begin(
|
fn gesture_hold_begin(
|
||||||
&mut self,
|
&mut self,
|
||||||
data: &mut Ferese,
|
data: &mut Ferese,
|
||||||
@@ -242,6 +248,7 @@ impl PointerGrab<Ferese> for ResizeSurfaceGrab {
|
|||||||
) {
|
) {
|
||||||
handle.gesture_hold_begin(data, event);
|
handle.gesture_hold_begin(data, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn gesture_hold_end(
|
fn gesture_hold_end(
|
||||||
&mut self,
|
&mut self,
|
||||||
data: &mut Ferese,
|
data: &mut Ferese,
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ impl OverviewState {
|
|||||||
self.labels.clear();
|
self.labels.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn with_font_family(font_family: String) -> Self {
|
pub(crate) fn with_font_family(font_family: String) -> Self {
|
||||||
Self {
|
Self {
|
||||||
font_family,
|
font_family,
|
||||||
@@ -199,6 +200,7 @@ impl OverviewState {
|
|||||||
self.labels.insert(key, label.clone());
|
self.labels.insert(key, label.clone());
|
||||||
Some(label)
|
Some(label)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn is_active(&self) -> bool {
|
pub(crate) fn is_active(&self) -> bool {
|
||||||
self.active
|
self.active
|
||||||
}
|
}
|
||||||
@@ -367,6 +369,7 @@ impl Ferese {
|
|||||||
let name = self.workspaces.workspace(workspace)?.name.clone();
|
let name = self.workspaces.workspace(workspace)?.name.clone();
|
||||||
self.overview.label(&name, scale)
|
self.overview.label(&name, scale)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn set_overview_active(&mut self, active: bool) {
|
pub(crate) fn set_overview_active(&mut self, active: bool) {
|
||||||
if self.overview.is_active() == active {
|
if self.overview.is_active() == active {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -93,15 +93,19 @@ impl Element for PhysicalShaderElement {
|
|||||||
fn id(&self) -> &Id {
|
fn id(&self) -> &Id {
|
||||||
self.inner.id()
|
self.inner.id()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_commit(&self) -> CommitCounter {
|
fn current_commit(&self) -> CommitCounter {
|
||||||
self.inner.current_commit()
|
self.inner.current_commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn src(&self) -> Rectangle<f64, Buffer> {
|
fn src(&self) -> Rectangle<f64, Buffer> {
|
||||||
self.inner.src()
|
self.inner.src()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn geometry(&self, _: Scale<f64>) -> Rectangle<i32, Physical> {
|
fn geometry(&self, _: Scale<f64>) -> Rectangle<i32, Physical> {
|
||||||
self.geometry
|
self.geometry
|
||||||
}
|
}
|
||||||
|
|
||||||
fn damage_since(
|
fn damage_since(
|
||||||
&self,
|
&self,
|
||||||
_: Scale<f64>,
|
_: Scale<f64>,
|
||||||
@@ -114,6 +118,7 @@ impl Element for PhysicalShaderElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderElement<GlesRenderer> for PhysicalShaderElement {
|
impl RenderElement<GlesRenderer> for PhysicalShaderElement {
|
||||||
fn draw(
|
fn draw(
|
||||||
&self,
|
&self,
|
||||||
@@ -140,25 +145,32 @@ pub(crate) struct NativeTextureElement {
|
|||||||
pub program: Option<GlesTexProgram>,
|
pub program: Option<GlesTexProgram>,
|
||||||
pub uniforms: Vec<Uniform<'static>>,
|
pub uniforms: Vec<Uniform<'static>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Element for NativeTextureElement {
|
impl Element for NativeTextureElement {
|
||||||
fn id(&self) -> &Id {
|
fn id(&self) -> &Id {
|
||||||
&self.id
|
&self.id
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_commit(&self) -> CommitCounter {
|
fn current_commit(&self) -> CommitCounter {
|
||||||
self.commit
|
self.commit
|
||||||
}
|
}
|
||||||
|
|
||||||
fn src(&self) -> Rectangle<f64, Buffer> {
|
fn src(&self) -> Rectangle<f64, Buffer> {
|
||||||
self.source
|
self.source
|
||||||
}
|
}
|
||||||
|
|
||||||
fn geometry(&self, _: Scale<f64>) -> Rectangle<i32, Physical> {
|
fn geometry(&self, _: Scale<f64>) -> Rectangle<i32, Physical> {
|
||||||
self.geometry
|
self.geometry
|
||||||
}
|
}
|
||||||
|
|
||||||
fn alpha(&self) -> f32 {
|
fn alpha(&self) -> f32 {
|
||||||
self.alpha
|
self.alpha
|
||||||
}
|
}
|
||||||
|
|
||||||
fn kind(&self) -> Kind {
|
fn kind(&self) -> Kind {
|
||||||
Kind::Unspecified
|
Kind::Unspecified
|
||||||
}
|
}
|
||||||
|
|
||||||
fn damage_since(
|
fn damage_since(
|
||||||
&self,
|
&self,
|
||||||
_: Scale<f64>,
|
_: Scale<f64>,
|
||||||
@@ -171,6 +183,7 @@ impl Element for NativeTextureElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderElement<GlesRenderer> for NativeTextureElement {
|
impl RenderElement<GlesRenderer> for NativeTextureElement {
|
||||||
fn draw(
|
fn draw(
|
||||||
&self,
|
&self,
|
||||||
@@ -231,6 +244,7 @@ mod tests {
|
|||||||
let growing_target = ferese_layout::Rect::new(0.0, 0.0, 1000.0, 600.0);
|
let growing_target = ferese_layout::Rect::new(0.0, 0.0, 1000.0, 600.0);
|
||||||
assert!(!resize_needs_old_frame(visual, growing_target, 1000, 600));
|
assert!(!resize_needs_old_frame(visual, growing_target, 1000, 600));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn fresh_snapshot_and_retarget_wait_do_not_skip_the_handoff() {
|
fn fresh_snapshot_and_retarget_wait_do_not_skip_the_handoff() {
|
||||||
let mut elapsed = Duration::ZERO;
|
let mut elapsed = Duration::ZERO;
|
||||||
@@ -262,6 +276,7 @@ mod tests {
|
|||||||
1.0
|
1.0
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn mixed_refresh_outputs_do_not_double_the_animation_clock() {
|
fn mixed_refresh_outputs_do_not_double_the_animation_clock() {
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
@@ -274,6 +289,7 @@ mod tests {
|
|||||||
assert_eq!(frame_delta(&mut last, start), Duration::ZERO);
|
assert_eq!(frame_delta(&mut last, start), Duration::ZERO);
|
||||||
assert_eq!(last, start + Duration::from_millis(32));
|
assert_eq!(last, start + Duration::from_millis(32));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn fractional_motion_does_not_quantize_to_logical_pixels() {
|
fn fractional_motion_does_not_quantize_to_logical_pixels() {
|
||||||
let rect = ferese_layout::Rect::new(0.3, 0.3, 100.2, 80.2);
|
let rect = ferese_layout::Rect::new(0.3, 0.3, 100.2, 80.2);
|
||||||
@@ -283,6 +299,7 @@ mod tests {
|
|||||||
(180, 144).into()
|
(180, 144).into()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn adjacent_frames_share_edges_at_all_output_scales() {
|
fn adjacent_frames_share_edges_at_all_output_scales() {
|
||||||
for scale in [1.0, 1.25, 1.5, 1.8, 2.0] {
|
for scale in [1.0, 1.25, 1.5, 1.8, 2.0] {
|
||||||
@@ -299,6 +316,7 @@ mod tests {
|
|||||||
assert_eq!(left.loc.x + left.size.w, right.loc.x);
|
assert_eq!(left.loc.x + left.size.w, right.loc.x);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn handoff_has_bounded_lifetime_and_no_alpha_jump() {
|
fn handoff_has_bounded_lifetime_and_no_alpha_jump() {
|
||||||
assert_eq!(handoff_alpha(Duration::ZERO), 1.0);
|
assert_eq!(handoff_alpha(Duration::ZERO), 1.0);
|
||||||
|
|||||||
+27
-12
@@ -26,6 +26,7 @@ pub(crate) struct ConfigMonitor {
|
|||||||
dirty: Option<Instant>,
|
dirty: Option<Instant>,
|
||||||
observed: Option<String>,
|
observed: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConfigMonitor {
|
impl ConfigMonitor {
|
||||||
pub(crate) fn new(path: PathBuf) -> notify::Result<Self> {
|
pub(crate) fn new(path: PathBuf) -> notify::Result<Self> {
|
||||||
let (sender, events) = mpsc::sync_channel(1);
|
let (sender, events) = mpsc::sync_channel(1);
|
||||||
@@ -64,6 +65,7 @@ impl ConfigMonitor {
|
|||||||
observed: None,
|
observed: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn poll(&mut self, now: Instant) -> Option<Result<String, String>> {
|
pub(crate) fn poll(&mut self, now: Instant) -> Option<Result<String, String>> {
|
||||||
if self.events.try_iter().next().is_some() {
|
if self.events.try_iter().next().is_some() {
|
||||||
self.dirty = Some(now);
|
self.dirty = Some(now);
|
||||||
@@ -118,6 +120,7 @@ impl crate::Ferese {
|
|||||||
tracing::info!("configuration reloaded live");
|
tracing::info!("configuration reloaded live");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn reload_config(&mut self) -> Result<(), String> {
|
pub(crate) fn reload_config(&mut self) -> Result<(), String> {
|
||||||
let path = crate::config::config_path().ok_or("config directory is unavailable")?;
|
let path = crate::config::config_path().ok_or("config directory is unavailable")?;
|
||||||
let source = read_source(&path)?;
|
let source = read_source(&path)?;
|
||||||
@@ -141,18 +144,28 @@ mod tests {
|
|||||||
Self(path)
|
Self(path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for Directory {
|
impl Drop for Directory {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
let _ = std::fs::remove_dir_all(&self.0);
|
let _ = std::fs::remove_dir_all(&self.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn runtime_validation_rejects_bad_edits_before_application() {
|
fn runtime_validation_rejects_bad_edits_before_application() {
|
||||||
for source in [
|
for source in [
|
||||||
"[animations]\nspeed = 0",
|
"animations {\n speed 0\n}\n",
|
||||||
"[theme.focus_ring.gradient]\nfrom = 'bad'\nto = '#ffffff'",
|
r##"theme {
|
||||||
"[status]\nlow_battery_threshold = 'bad'",
|
focus-ring {
|
||||||
"[layout]\ninner_gap = -1",
|
gradient {
|
||||||
|
from "bad"
|
||||||
|
to "#ffffff"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"##,
|
||||||
|
"status {\n low-battery-threshold \"bad\"\n}\n",
|
||||||
|
"layout {\n inner-gap -1\n}\n",
|
||||||
] {
|
] {
|
||||||
assert!(
|
assert!(
|
||||||
crate::config::Config::parse_source(source)
|
crate::config::Config::parse_source(source)
|
||||||
@@ -161,17 +174,18 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
assert!(
|
assert!(
|
||||||
crate::config::Config::parse_source("[animations]\nspeed = 0.75")
|
crate::config::Config::parse_source("animations {\n speed 0.75\n}\n")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.runtime_config()
|
.runtime_config()
|
||||||
.is_ok()
|
.is_ok()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn directory_watch_detects_atomic_save_and_does_not_read_during_idle() {
|
fn directory_watch_detects_atomic_save_and_does_not_read_during_idle() {
|
||||||
let directory = Directory::new();
|
let directory = Directory::new();
|
||||||
let path = directory.0.join("config.toml");
|
let path = directory.0.join("config.kdl");
|
||||||
std::fs::write(&path, "[animations]\nspeed = 1").unwrap();
|
std::fs::write(&path, "animations {\n speed 1\n}\n").unwrap();
|
||||||
let mut monitor = ConfigMonitor::new(path.clone()).unwrap();
|
let mut monitor = ConfigMonitor::new(path.clone()).unwrap();
|
||||||
monitor.dirty = Some(Instant::now() - Duration::from_secs(1));
|
monitor.dirty = Some(Instant::now() - Duration::from_secs(1));
|
||||||
assert!(monitor.poll(Instant::now()).unwrap().is_ok());
|
assert!(monitor.poll(Instant::now()).unwrap().is_ok());
|
||||||
@@ -180,13 +194,13 @@ mod tests {
|
|||||||
monitor.path = directory.0.join("missing");
|
monitor.path = directory.0.join("missing");
|
||||||
assert!(monitor.poll(Instant::now()).is_none());
|
assert!(monitor.poll(Instant::now()).is_none());
|
||||||
monitor.path = original_path;
|
monitor.path = original_path;
|
||||||
let temporary = directory.0.join("config.toml.new");
|
let temporary = directory.0.join("config.kdl.new");
|
||||||
std::fs::write(&temporary, "[animations]\nspeed = 0.75").unwrap();
|
std::fs::write(&temporary, "animations {\n speed 0.75\n}\n").unwrap();
|
||||||
std::fs::rename(&temporary, &path).unwrap();
|
std::fs::rename(&temporary, &path).unwrap();
|
||||||
let deadline = Instant::now() + Duration::from_secs(3);
|
let deadline = Instant::now() + Duration::from_secs(3);
|
||||||
loop {
|
loop {
|
||||||
if let Some(result) = monitor.poll(Instant::now()) {
|
if let Some(result) = monitor.poll(Instant::now()) {
|
||||||
assert_eq!(result.unwrap(), "[animations]\nspeed = 0.75");
|
assert_eq!(result.unwrap(), "animations {\n speed 0.75\n}\n");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
assert!(
|
assert!(
|
||||||
@@ -196,13 +210,14 @@ mod tests {
|
|||||||
std::thread::sleep(Duration::from_millis(10));
|
std::thread::sleep(Duration::from_millis(10));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn source_size_limit_is_bounded_and_debounce_coalesces_edits() {
|
fn source_size_limit_is_bounded_and_debounce_coalesces_edits() {
|
||||||
let directory = Directory::new();
|
let directory = Directory::new();
|
||||||
let path = directory.0.join("config.toml");
|
let path = directory.0.join("config.kdl");
|
||||||
std::fs::write(&path, "x".repeat(60 * 1024 + 1)).unwrap();
|
std::fs::write(&path, "x".repeat(60 * 1024 + 1)).unwrap();
|
||||||
assert!(read_source(&path).is_err());
|
assert!(read_source(&path).is_err());
|
||||||
std::fs::write(&path, "[animations]\nspeed = 1").unwrap();
|
std::fs::write(&path, "animations {\n speed 1\n}\n").unwrap();
|
||||||
let mut monitor = ConfigMonitor::new(path).unwrap();
|
let mut monitor = ConfigMonitor::new(path).unwrap();
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
monitor.dirty = Some(now);
|
monitor.dirty = Some(now);
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ impl ResizeTransaction {
|
|||||||
self.source_geometry = Some(geometry);
|
self.source_geometry = Some(geometry);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn source_geometry(self) -> Option<Rectangle<i32, Logical>> {
|
pub(crate) fn source_geometry(self) -> Option<Rectangle<i32, Logical>> {
|
||||||
self.source_geometry
|
self.source_geometry
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ impl SessionLockHandler for Ferese {
|
|||||||
fn lock_state(&mut self) -> &mut SessionLockManagerState {
|
fn lock_state(&mut self) -> &mut SessionLockManagerState {
|
||||||
&mut self.session_lock_state
|
&mut self.session_lock_state
|
||||||
}
|
}
|
||||||
|
|
||||||
fn lock(&mut self, confirmation: SessionLocker) {
|
fn lock(&mut self, confirmation: SessionLocker) {
|
||||||
if self.session_lock.active {
|
if self.session_lock.active {
|
||||||
return;
|
return;
|
||||||
@@ -153,12 +154,14 @@ impl SessionLockHandler for Ferese {
|
|||||||
self.intercepted_keys.clear();
|
self.intercepted_keys.clear();
|
||||||
crate::backends::direct::render_all(self);
|
crate::backends::direct::render_all(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unlock(&mut self) {
|
fn unlock(&mut self) {
|
||||||
self.session_lock = Lock::default();
|
self.session_lock = Lock::default();
|
||||||
self.restore_keyboard_focus();
|
self.restore_keyboard_focus();
|
||||||
crate::backends::direct::render_all(self);
|
crate::backends::direct::render_all(self);
|
||||||
tracing::info!("session unlocked by lock owner");
|
tracing::info!("session unlocked by lock owner");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_surface(&mut self, surface: LockSurface, output: WlOutput) {
|
fn new_surface(&mut self, surface: LockSurface, output: WlOutput) {
|
||||||
if let Some(output) = Output::from_resource(&output) {
|
if let Some(output) = Output::from_resource(&output) {
|
||||||
if let Some(geometry) = self.space.output_geometry(&output) {
|
if let Some(geometry) = self.space.output_geometry(&output) {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ pub(crate) fn config_chunks(source: &str) -> Vec<&str> {
|
|||||||
}
|
}
|
||||||
chunks
|
chunks
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn send_shell_config(shell: &FereseShellV1, source: &str) {
|
pub(crate) fn send_shell_config(shell: &FereseShellV1, source: &str) {
|
||||||
shell.config_begin();
|
shell.config_begin();
|
||||||
for chunk in config_chunks(source) {
|
for chunk in config_chunks(source) {
|
||||||
|
|||||||
@@ -322,6 +322,7 @@ impl Ferese {
|
|||||||
crate::backends::direct::render_all(self);
|
crate::backends::direct::render_all(self);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn register_output(&mut self, output: &Output, identity: String) {
|
pub fn register_output(&mut self, output: &Output, identity: String) {
|
||||||
let Some(geometry) = self.space.output_geometry(output) else {
|
let Some(geometry) = self.space.output_geometry(output) else {
|
||||||
tracing::error!(output = %output.name(), "cannot register an unmapped output");
|
tracing::error!(output = %output.name(), "cannot register an unmapped output");
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ pub(crate) struct WallpaperConfig {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub mode: WallpaperMode,
|
pub mode: WallpaperMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq)]
|
#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub(crate) enum WallpaperMode {
|
pub(crate) enum WallpaperMode {
|
||||||
@@ -78,10 +79,12 @@ impl WallpaperState {
|
|||||||
textures: HashMap::new(),
|
textures: HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub fn owns_background(&self) -> bool {
|
pub fn owns_background(&self) -> bool {
|
||||||
self.owned
|
self.owned
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reload(&mut self, config: WallpaperConfig) {
|
pub fn reload(&mut self, config: WallpaperConfig) {
|
||||||
if self.receiver.is_some() {
|
if self.receiver.is_some() {
|
||||||
self.pending = (self.config != config).then_some(config);
|
self.pending = (self.config != config).then_some(config);
|
||||||
@@ -110,9 +113,11 @@ impl WallpaperState {
|
|||||||
self.textures.clear();
|
self.textures.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn forget_context(&mut self, context: &ErasedContextId) {
|
pub fn forget_context(&mut self, context: &ErasedContextId) {
|
||||||
self.textures.remove(context);
|
self.textures.remove(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn poll(&mut self) -> bool {
|
pub fn poll(&mut self) -> bool {
|
||||||
let Some(receiver) = &self.receiver else {
|
let Some(receiver) = &self.receiver else {
|
||||||
return false;
|
return false;
|
||||||
@@ -153,6 +158,7 @@ impl WallpaperState {
|
|||||||
Err(mpsc::TryRecvError::Empty) => false,
|
Err(mpsc::TryRecvError::Empty) => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn element(
|
pub fn element(
|
||||||
&mut self,
|
&mut self,
|
||||||
renderer: &mut GlesRenderer,
|
renderer: &mut GlesRenderer,
|
||||||
@@ -270,6 +276,7 @@ mod tests {
|
|||||||
state.reload(state.config.clone());
|
state.reload(state.config.clone());
|
||||||
assert_eq!(state.commit, before);
|
assert_eq!(state.commit, before);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn reverting_a_pending_reload_keeps_latest_request_and_failed_decode_keeps_pixels() {
|
fn reverting_a_pending_reload_keeps_latest_request_and_failed_decode_keeps_pixels() {
|
||||||
let mut state = WallpaperState::new(WallpaperConfig::default());
|
let mut state = WallpaperState::new(WallpaperConfig::default());
|
||||||
@@ -287,6 +294,7 @@ mod tests {
|
|||||||
assert!(state.pixels.is_some());
|
assert!(state.pixels.is_some());
|
||||||
assert_eq!(state.mode, WallpaperMode::Fill);
|
assert_eq!(state.mode, WallpaperMode::Fill);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn fill_crops_and_fit_letterboxes_without_reallocating_image() {
|
fn fill_crops_and_fit_letterboxes_without_reallocating_image() {
|
||||||
let (geometry, source) = image_geometry(
|
let (geometry, source) = image_geometry(
|
||||||
@@ -303,6 +311,7 @@ mod tests {
|
|||||||
assert_eq!(geometry.loc, (0, 50).into());
|
assert_eq!(geometry.loc, (0, 50).into());
|
||||||
assert_eq!(source.size, (3840.0, 2160.0).into());
|
assert_eq!(source.size, (3840.0, 2160.0).into());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn missing_wallpaper_preserves_external_shell_fallback() {
|
fn missing_wallpaper_preserves_external_shell_fallback() {
|
||||||
let state = WallpaperState::new(WallpaperConfig::default());
|
let state = WallpaperState::new(WallpaperConfig::default());
|
||||||
|
|||||||
@@ -2530,6 +2530,7 @@ fn blur_uniforms(p: &MaterialParameters) -> Vec<Uniform<'static>> {
|
|||||||
fn framebuffer_capture_rect(rect: [f32; 4]) -> [i32; 4] {
|
fn framebuffer_capture_rect(rect: [f32; 4]) -> [i32; 4] {
|
||||||
rect.map(|v| v.round() as i32)
|
rect.map(|v| v.round() as i32)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn expanded_blur_region(
|
fn expanded_blur_region(
|
||||||
visible: Rectangle<i32, Logical>,
|
visible: Rectangle<i32, Logical>,
|
||||||
radius: i32,
|
radius: i32,
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[package]
|
||||||
|
name = "ferese-config"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
rust-version = "1.95"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
kdl = "6.7.1"
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
@@ -0,0 +1,791 @@
|
|||||||
|
use kdl::{KdlDocument, KdlEntry, KdlNode, KdlValue};
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
use serde_json::{Map, Value};
|
||||||
|
use std::{fmt, path::PathBuf};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Error(String);
|
||||||
|
|
||||||
|
impl fmt::Display for Error {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.write_str(&self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for Error {}
|
||||||
|
|
||||||
|
impl From<String> for Error {
|
||||||
|
fn from(value: String) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn config_path() -> Option<PathBuf> {
|
||||||
|
std::env::var_os("XDG_CONFIG_HOME")
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.or_else(|| std::env::var_os("HOME").map(|p| PathBuf::from(p).join(".config")))
|
||||||
|
.map(|p| p.join("ferese/config.kdl"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn field(name: &str, parent: &str) -> String {
|
||||||
|
if parent == "commands" {
|
||||||
|
return name.to_owned();
|
||||||
|
}
|
||||||
|
match name {
|
||||||
|
"binding" => "bindings".into(),
|
||||||
|
"window-rule" => "window_rules".into(),
|
||||||
|
"output-profile" => "output_profiles".into(),
|
||||||
|
"output" => "outputs".into(),
|
||||||
|
"note" => "notes".into(),
|
||||||
|
_ => name.replace('-', "_"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_records(key: &str, parent: &str) -> bool {
|
||||||
|
match (parent, key) {
|
||||||
|
("", "bindings" | "window_rules" | "output_profiles" | "autostart") => true,
|
||||||
|
("output_profiles", "outputs") | ("desktop_widgets", "notes") => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_array(key: &str, parent: &str) -> bool {
|
||||||
|
parent == "commands"
|
||||||
|
|| matches!(
|
||||||
|
key,
|
||||||
|
"command"
|
||||||
|
| "position"
|
||||||
|
| "width_presets"
|
||||||
|
| "xkb_options"
|
||||||
|
| "settings_command"
|
||||||
|
| "outputs"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scalar(value: &KdlValue) -> Result<Value, Error> {
|
||||||
|
Ok(match value {
|
||||||
|
KdlValue::String(s) => Value::String(s.clone()),
|
||||||
|
KdlValue::Bool(v) => Value::Bool(*v),
|
||||||
|
KdlValue::Null => {
|
||||||
|
return Err(Error(
|
||||||
|
"Omit optional settings instead of using #null".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
KdlValue::Integer(n) => {
|
||||||
|
if let Ok(n) = i64::try_from(*n) {
|
||||||
|
n.into()
|
||||||
|
} else if let Ok(n) = u64::try_from(*n) {
|
||||||
|
n.into()
|
||||||
|
} else {
|
||||||
|
return Err(Error("Integer is outside the supported range".into()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KdlValue::Float(n) => serde_json::Number::from_f64(*n)
|
||||||
|
.map(Value::Number)
|
||||||
|
.ok_or_else(|| Error("Configuration numbers must be finite".into()))?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert(map: &mut Map<String, Value>, key: String, value: Value) -> Result<(), Error> {
|
||||||
|
if map.insert(key.clone(), value).is_some() {
|
||||||
|
return Err(Error(format!("Duplicate setting: {key}")));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn object(doc: &KdlDocument, parent: &str) -> Result<Map<String, Value>, Error> {
|
||||||
|
let mut result = Map::new();
|
||||||
|
|
||||||
|
for node in doc.nodes() {
|
||||||
|
let key = field(node.name().value(), parent);
|
||||||
|
let value = node_value(node, &key, parent)?;
|
||||||
|
if is_records(&key, parent) {
|
||||||
|
let records = result
|
||||||
|
.entry(key)
|
||||||
|
.or_insert_with(|| Value::Array(Vec::new()));
|
||||||
|
records.as_array_mut().unwrap().push(value);
|
||||||
|
} else {
|
||||||
|
insert(&mut result, key, value)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn node_value(node: &KdlNode, key: &str, parent: &str) -> Result<Value, Error> {
|
||||||
|
if node.ty().is_some() || node.entries().iter().any(|e| e.ty().is_some()) {
|
||||||
|
return Err(Error(
|
||||||
|
"Type annotations are not supported in configuration".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let args = node
|
||||||
|
.entries()
|
||||||
|
.iter()
|
||||||
|
.filter(|e| e.name().is_none())
|
||||||
|
.map(|e| scalar(e.value()))
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
let props = node
|
||||||
|
.entries()
|
||||||
|
.iter()
|
||||||
|
.filter_map(|e| e.name().map(|n| (field(n.value(), key), e)))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
if is_records(key, parent) {
|
||||||
|
let mut map = node
|
||||||
|
.children()
|
||||||
|
.map(|d| object(d, key))
|
||||||
|
.transpose()?
|
||||||
|
.unwrap_or_default();
|
||||||
|
for (name, entry) in props {
|
||||||
|
insert(&mut map, name, scalar(entry.value())?)?;
|
||||||
|
}
|
||||||
|
let positional: &[&str] = match key {
|
||||||
|
"bindings" => &["keys", "action", "argument"],
|
||||||
|
"output_profiles" => &["name"],
|
||||||
|
"outputs" => &["match"],
|
||||||
|
"notes" => &["id"],
|
||||||
|
"autostart" => &[],
|
||||||
|
"window_rules" => &[],
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if key == "autostart" && !args.is_empty() {
|
||||||
|
insert(&mut map, "command".into(), args.into())?;
|
||||||
|
} else {
|
||||||
|
if args.len() > positional.len() {
|
||||||
|
return Err(Error(format!("Too many arguments for {}", node.name())));
|
||||||
|
}
|
||||||
|
for (name, value) in positional.iter().zip(args) {
|
||||||
|
insert(&mut map, (*name).into(), value)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(Value::Object(map));
|
||||||
|
}
|
||||||
|
|
||||||
|
if node.children().is_some() || !props.is_empty() {
|
||||||
|
if !args.is_empty() {
|
||||||
|
return Err(Error(format!(
|
||||||
|
"{} cannot combine scalar arguments with a section",
|
||||||
|
node.name()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let mut map = node
|
||||||
|
.children()
|
||||||
|
.map(|d| object(d, key))
|
||||||
|
.transpose()?
|
||||||
|
.unwrap_or_default();
|
||||||
|
for (name, entry) in props {
|
||||||
|
insert(&mut map, name, scalar(entry.value())?)?;
|
||||||
|
}
|
||||||
|
return Ok(Value::Object(map));
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_array(key, parent) {
|
||||||
|
return Ok(Value::Array(args));
|
||||||
|
}
|
||||||
|
if args.len() != 1 {
|
||||||
|
return Err(Error(format!("{} requires one value", node.name())));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(args.into_iter().next().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Document {
|
||||||
|
doc: KdlDocument,
|
||||||
|
value: Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Document {
|
||||||
|
pub fn parse(source: &str) -> Result<Self, Error> {
|
||||||
|
let doc = source
|
||||||
|
.parse::<KdlDocument>()
|
||||||
|
.map_err(|e| Error(format!("{e:?}")))?;
|
||||||
|
let value = Value::Object(object(&doc, "")?);
|
||||||
|
|
||||||
|
Ok(Self { doc, value })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn value(&self) -> &Value {
|
||||||
|
&self.value
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self, path: &str) -> Option<&Value> {
|
||||||
|
let mut value = &self.value;
|
||||||
|
|
||||||
|
for part in path.split('.') {
|
||||||
|
value = if let Ok(index) = part.parse::<usize>() {
|
||||||
|
value.get(index)?
|
||||||
|
} else {
|
||||||
|
value.get(part)?
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Some(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn refresh(&mut self) -> Result<(), Error> {
|
||||||
|
self.value = Value::Object(object(&self.doc, "")?);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set(&mut self, path: &str, value: Value) -> Result<(), Error> {
|
||||||
|
let mut candidate = self.clone();
|
||||||
|
let parts = path.split('.').collect::<Vec<_>>();
|
||||||
|
set_in(&mut candidate.doc, &parts, "", value)?;
|
||||||
|
format_document(&mut candidate.doc);
|
||||||
|
candidate.refresh()?;
|
||||||
|
*self = candidate;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add(&mut self, path: &str, fields: Vec<(String, Value)>) -> Result<(), Error> {
|
||||||
|
let mut candidate = self.clone();
|
||||||
|
let (last, parents) = path.rsplit_once('.').map_or((path, ""), |(p, l)| (l, p));
|
||||||
|
let parent = parents
|
||||||
|
.rsplit('.')
|
||||||
|
.find(|s| s.parse::<usize>().is_err())
|
||||||
|
.unwrap_or("");
|
||||||
|
|
||||||
|
if !is_records(last, parent) {
|
||||||
|
return Err(Error("Expected a list of records".into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let doc = section_mut(
|
||||||
|
&mut candidate.doc,
|
||||||
|
&parents
|
||||||
|
.split('.')
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
"",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
doc.nodes_mut().push(value_node(
|
||||||
|
last,
|
||||||
|
&Value::Object(fields.into_iter().collect()),
|
||||||
|
parent,
|
||||||
|
)?);
|
||||||
|
format_document(&mut candidate.doc);
|
||||||
|
candidate.refresh()?;
|
||||||
|
*self = candidate;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove(&mut self, path: &str, index: usize) -> Result<(), Error> {
|
||||||
|
let mut candidate = self.clone();
|
||||||
|
let (last, parents) = path.rsplit_once('.').map_or((path, ""), |(p, l)| (l, p));
|
||||||
|
let doc = section_mut(
|
||||||
|
&mut candidate.doc,
|
||||||
|
&parents
|
||||||
|
.split('.')
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
"",
|
||||||
|
)?;
|
||||||
|
let i = doc
|
||||||
|
.nodes()
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_, n)| {
|
||||||
|
field(n.name().value(), parents.rsplit('.').next().unwrap_or("")) == last
|
||||||
|
})
|
||||||
|
.nth(index)
|
||||||
|
.map(|(i, _)| i)
|
||||||
|
.ok_or_else(|| Error("This item no longer exists. Reload settings.".into()))?;
|
||||||
|
|
||||||
|
doc.nodes_mut().remove(i);
|
||||||
|
format_document(&mut candidate.doc);
|
||||||
|
candidate.refresh()?;
|
||||||
|
*self = candidate;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Document {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
self.doc.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_entry_value(entry: &mut KdlEntry, value: KdlValue) {
|
||||||
|
let representation = value.to_string();
|
||||||
|
entry.set_value(value);
|
||||||
|
if let Some(format) = entry.format_mut() {
|
||||||
|
format.value_repr = representation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type NodeFormat = (Vec<Option<kdl::KdlEntryFormat>>, Option<String>);
|
||||||
|
|
||||||
|
fn collect_node_formats(doc: &KdlDocument, formats: &mut Vec<NodeFormat>) {
|
||||||
|
for node in doc.nodes() {
|
||||||
|
let entries = node
|
||||||
|
.entries()
|
||||||
|
.iter()
|
||||||
|
.map(|entry| entry.format().cloned())
|
||||||
|
.collect();
|
||||||
|
let terminator = node
|
||||||
|
.format()
|
||||||
|
.map(|format| format.terminator.clone())
|
||||||
|
.filter(|text| text.contains("//") || text.contains("/*"));
|
||||||
|
formats.push((entries, terminator));
|
||||||
|
if let Some(children) = node.children() {
|
||||||
|
collect_node_formats(children, formats);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn restore_node_formats(doc: &mut KdlDocument, formats: &mut impl Iterator<Item = NodeFormat>) {
|
||||||
|
for node in doc.nodes_mut() {
|
||||||
|
if let Some((entries, terminator)) = formats.next() {
|
||||||
|
for (entry, format) in node.entries_mut().iter_mut().zip(entries) {
|
||||||
|
if let Some(format) = format {
|
||||||
|
entry.set_format(format);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(terminator) = terminator {
|
||||||
|
if let Some(format) = node.format_mut() {
|
||||||
|
format.before_terminator = " ".into();
|
||||||
|
format.terminator = terminator;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(children) = node.children_mut() {
|
||||||
|
restore_node_formats(children, formats);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_document(doc: &mut KdlDocument) {
|
||||||
|
let mut formats = Vec::new();
|
||||||
|
collect_node_formats(doc, &mut formats);
|
||||||
|
doc.autoformat();
|
||||||
|
restore_node_formats(doc, &mut formats.into_iter());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn node_index(doc: &mut KdlDocument, key: &str, parent: &str) -> usize {
|
||||||
|
if let Some(i) = doc
|
||||||
|
.nodes()
|
||||||
|
.iter()
|
||||||
|
.position(|n| field(n.name().value(), parent) == key)
|
||||||
|
{
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
doc.nodes_mut().push(KdlNode::new(node_name(key, parent)));
|
||||||
|
doc.nodes().len() - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn section_mut<'a>(
|
||||||
|
doc: &'a mut KdlDocument,
|
||||||
|
parts: &[&str],
|
||||||
|
parent: &str,
|
||||||
|
) -> Result<&'a mut KdlDocument, Error> {
|
||||||
|
if parts.is_empty() {
|
||||||
|
return Ok(doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = parts[0];
|
||||||
|
let i = node_index(doc, key, parent);
|
||||||
|
let mut used = 1;
|
||||||
|
let i = if is_records(key, parent) {
|
||||||
|
let index = parts
|
||||||
|
.get(1)
|
||||||
|
.and_then(|s| s.parse::<usize>().ok())
|
||||||
|
.ok_or_else(|| Error("Record index missing".into()))?;
|
||||||
|
used = 2;
|
||||||
|
doc.nodes()
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_, n)| field(n.name().value(), parent) == key)
|
||||||
|
.nth(index)
|
||||||
|
.map(|(i, _)| i)
|
||||||
|
.ok_or_else(|| Error("This item no longer exists. Reload settings.".into()))?
|
||||||
|
} else {
|
||||||
|
i
|
||||||
|
};
|
||||||
|
let node = &mut doc.nodes_mut()[i];
|
||||||
|
let child = node.children_mut().get_or_insert_with(KdlDocument::new);
|
||||||
|
|
||||||
|
section_mut(child, &parts[used..], key)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_in(doc: &mut KdlDocument, parts: &[&str], parent: &str, value: Value) -> Result<(), Error> {
|
||||||
|
if parts.is_empty() {
|
||||||
|
return Err(Error("Missing setting path".into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = parts[0];
|
||||||
|
let i = node_index(doc, key, parent);
|
||||||
|
|
||||||
|
if parts.len() == 1 {
|
||||||
|
let new = value_node(key, &value, parent)?;
|
||||||
|
let node = &mut doc.nodes_mut()[i];
|
||||||
|
if new.entries().len() == 1 && node.entries().len() == 1 && node.children().is_none() {
|
||||||
|
set_entry_value(&mut node.entries_mut()[0], new.entries()[0].value().clone());
|
||||||
|
node.entries_mut()[0].set_name(None::<String>);
|
||||||
|
} else {
|
||||||
|
*node.entries_mut() = new.entries().to_vec();
|
||||||
|
*node.children_mut() = new.children().cloned();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_records(key, parent) {
|
||||||
|
let index = parts[1]
|
||||||
|
.parse::<usize>()
|
||||||
|
.map_err(|_| Error("Record index missing".into()))?;
|
||||||
|
let i = doc
|
||||||
|
.nodes()
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_, n)| field(n.name().value(), parent) == key)
|
||||||
|
.nth(index)
|
||||||
|
.map(|(i, _)| i)
|
||||||
|
.ok_or_else(|| Error("This item no longer exists. Reload settings.".into()))?;
|
||||||
|
let node = &mut doc.nodes_mut()[i];
|
||||||
|
let rest = &parts[2..];
|
||||||
|
|
||||||
|
if rest.len() == 1 {
|
||||||
|
let positions: &[&str] = match key {
|
||||||
|
"bindings" => &["keys", "action", "argument"],
|
||||||
|
"outputs" => &["match"],
|
||||||
|
"output_profiles" => &["name"],
|
||||||
|
"notes" => &["id"],
|
||||||
|
_ => &[],
|
||||||
|
};
|
||||||
|
let position = positions.iter().position(|p| *p == rest[0]);
|
||||||
|
let mut arg_index = 0;
|
||||||
|
|
||||||
|
for entry in node.entries_mut() {
|
||||||
|
let matches = if let Some(name) = entry.name() {
|
||||||
|
field(name.value(), key) == rest[0]
|
||||||
|
} else {
|
||||||
|
let matches = Some(arg_index) == position;
|
||||||
|
arg_index += 1;
|
||||||
|
matches
|
||||||
|
};
|
||||||
|
|
||||||
|
if matches {
|
||||||
|
set_entry_value(entry, kdl_value(&value)?);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if key == "autostart"
|
||||||
|
&& rest[0] == "command"
|
||||||
|
&& node.children().is_none_or(|d| d.get("command").is_none())
|
||||||
|
{
|
||||||
|
node.entries_mut().retain(|e| e.name().is_some());
|
||||||
|
for value in value
|
||||||
|
.as_array()
|
||||||
|
.ok_or_else(|| Error("Command must be an array".into()))?
|
||||||
|
{
|
||||||
|
node.entries_mut().push(KdlEntry::new(kdl_value(value)?));
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return set_in(
|
||||||
|
node.children_mut().get_or_insert_with(KdlDocument::new),
|
||||||
|
rest,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let node = &mut doc.nodes_mut()[i];
|
||||||
|
|
||||||
|
if parts.len() == 2 {
|
||||||
|
if let Some(entry) = node
|
||||||
|
.entries_mut()
|
||||||
|
.iter_mut()
|
||||||
|
.find(|e| e.name().is_some_and(|n| field(n.value(), key) == parts[1]))
|
||||||
|
{
|
||||||
|
set_entry_value(entry, kdl_value(&value)?);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set_in(
|
||||||
|
node.children_mut().get_or_insert_with(KdlDocument::new),
|
||||||
|
&parts[1..],
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kdl_value(value: &Value) -> Result<KdlValue, Error> {
|
||||||
|
Ok(match value {
|
||||||
|
Value::String(s) => KdlValue::String(s.clone()),
|
||||||
|
Value::Bool(b) => KdlValue::Bool(*b),
|
||||||
|
Value::Number(n) => {
|
||||||
|
if let Some(i) = n.as_i64() {
|
||||||
|
KdlValue::Integer(i.into())
|
||||||
|
} else if let Some(i) = n.as_u64() {
|
||||||
|
KdlValue::Integer(i.into())
|
||||||
|
} else {
|
||||||
|
KdlValue::Float(n.as_f64().unwrap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => return Err(Error("Expected a scalar value".into())),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn node_name(key: &str, parent: &str) -> String {
|
||||||
|
if parent == "commands" {
|
||||||
|
return key.into();
|
||||||
|
}
|
||||||
|
|
||||||
|
if !is_records(key, parent) {
|
||||||
|
return key.replace('_', "-");
|
||||||
|
}
|
||||||
|
|
||||||
|
match key {
|
||||||
|
"bindings" => "binding".into(),
|
||||||
|
"window_rules" => "window-rule".into(),
|
||||||
|
"output_profiles" => "output-profile".into(),
|
||||||
|
"outputs" => "output".into(),
|
||||||
|
"notes" => "note".into(),
|
||||||
|
_ => key.replace('_', "-"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn value_node(key: &str, value: &Value, parent: &str) -> Result<KdlNode, Error> {
|
||||||
|
let mut node = KdlNode::new(node_name(key, parent));
|
||||||
|
|
||||||
|
match value {
|
||||||
|
Value::Object(map) => {
|
||||||
|
let positions: &[&str] = match key {
|
||||||
|
"bindings" => &["keys", "action", "argument"],
|
||||||
|
"output_profiles" => &["name"],
|
||||||
|
"outputs" => &["match"],
|
||||||
|
"notes" => &["id"],
|
||||||
|
_ => &[],
|
||||||
|
};
|
||||||
|
let mut child = KdlDocument::new();
|
||||||
|
let mut consumed = Vec::new();
|
||||||
|
|
||||||
|
for name in positions {
|
||||||
|
if let Some(value) = map.get(*name) {
|
||||||
|
node.entries_mut().push(KdlEntry::new(kdl_value(value)?));
|
||||||
|
consumed.push(*name);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (name, value) in map {
|
||||||
|
if consumed.contains(&name.as_str()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if key == "autostart" && name == "command" {
|
||||||
|
for value in value
|
||||||
|
.as_array()
|
||||||
|
.ok_or_else(|| Error("Command must be an array".into()))?
|
||||||
|
{
|
||||||
|
node.entries_mut().push(KdlEntry::new(kdl_value(value)?));
|
||||||
|
}
|
||||||
|
} else if is_records(key, parent)
|
||||||
|
&& map.len() <= 4
|
||||||
|
&& !value.is_array()
|
||||||
|
&& !value.is_object()
|
||||||
|
{
|
||||||
|
node.entries_mut().push(KdlEntry::new_prop(
|
||||||
|
name.replace('_', "-"),
|
||||||
|
kdl_value(value)?,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
append(&mut child, name, value, key)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !child.nodes().is_empty() || node.entries().is_empty() {
|
||||||
|
node.set_children(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Value::Array(array) => {
|
||||||
|
for value in array {
|
||||||
|
node.entries_mut().push(KdlEntry::new(kdl_value(value)?));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => node.entries_mut().push(KdlEntry::new(kdl_value(value)?)),
|
||||||
|
}
|
||||||
|
Ok(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn append(doc: &mut KdlDocument, key: &str, value: &Value, parent: &str) -> Result<(), Error> {
|
||||||
|
if is_records(key, parent) {
|
||||||
|
for record in value
|
||||||
|
.as_array()
|
||||||
|
.ok_or_else(|| Error("Expected records".into()))?
|
||||||
|
{
|
||||||
|
doc.nodes_mut().push(value_node(key, record, parent)?);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
doc.nodes_mut().push(value_node(key, value, parent)?);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_str<T: DeserializeOwned>(source: &str) -> Result<T, Error> {
|
||||||
|
serde_json::from_value(Document::parse(source)?.value).map_err(|e| Error(e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
#[test]
|
||||||
|
fn widget_output_arrays_and_command_names_survive_edits() {
|
||||||
|
let source = r#"commands {
|
||||||
|
my_command "program"
|
||||||
|
}
|
||||||
|
desktop-widgets {
|
||||||
|
clock enabled=#true {
|
||||||
|
outputs "DP-1"
|
||||||
|
}
|
||||||
|
note "n" text="x" {
|
||||||
|
outputs "DP-2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
let mut doc = Document::parse(source).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
doc.get("commands.my_command").unwrap(),
|
||||||
|
&serde_json::json!(["program"])
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
doc.get("desktop_widgets.clock.outputs").unwrap(),
|
||||||
|
&serde_json::json!(["DP-1"])
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
doc.get("desktop_widgets.notes.0.outputs").unwrap(),
|
||||||
|
&serde_json::json!(["DP-2"])
|
||||||
|
);
|
||||||
|
doc.set("commands.new_command", serde_json::json!(["two", "words"]))
|
||||||
|
.unwrap();
|
||||||
|
doc.set("desktop_widgets.clock.outputs", serde_json::json!([]))
|
||||||
|
.unwrap();
|
||||||
|
let reparsed = Document::parse(&doc.to_string()).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
reparsed.get("commands.new_command").unwrap(),
|
||||||
|
&serde_json::json!(["two", "words"])
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
reparsed.get("desktop_widgets.clock.outputs").unwrap(),
|
||||||
|
&serde_json::json!([])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn inline_comments_and_failed_edits_leave_document_intact() {
|
||||||
|
let mut doc =
|
||||||
|
Document::parse("// header\nanimations {\n speed 0.75 // keep inline\n}\n").unwrap();
|
||||||
|
doc.set("animations.speed", 0.8.into()).unwrap();
|
||||||
|
assert!(doc.to_string().contains("// keep inline"));
|
||||||
|
assert_eq!(
|
||||||
|
Document::parse(&doc.to_string())
|
||||||
|
.unwrap()
|
||||||
|
.get("animations.speed")
|
||||||
|
.unwrap(),
|
||||||
|
0.8
|
||||||
|
);
|
||||||
|
let previous = doc.to_string();
|
||||||
|
assert!(doc.set("bindings.4.keys", "X".into()).is_err());
|
||||||
|
assert_eq!(doc.to_string(), previous);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn editing_autostart_commands_handles_both_representations() {
|
||||||
|
for source in [
|
||||||
|
"autostart \"old\" enabled=#true\n",
|
||||||
|
"autostart { command \"old\"; enabled #true; }\n",
|
||||||
|
] {
|
||||||
|
let mut doc = Document::parse(source).unwrap();
|
||||||
|
doc.set("autostart.0.command", serde_json::json!(["new", "arg"]))
|
||||||
|
.unwrap();
|
||||||
|
let reparsed = Document::parse(&doc.to_string()).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
reparsed.get("autostart.0.command").unwrap(),
|
||||||
|
&serde_json::json!(["new", "arg"])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn compact_syntax_and_nested_arrays_roundtrip() {
|
||||||
|
let source = r#"input repeat-rate=30 { xkb-options "compose:ralt"; }
|
||||||
|
binding "Swipe3Up" "toggle-overview"
|
||||||
|
output-profile "desk" { output "HDMI-A-1" scale=1.5 { position 0 0; }; }
|
||||||
|
autostart "program" "two words" enabled=#false
|
||||||
|
"#;
|
||||||
|
let mut doc = Document::parse(source).unwrap();
|
||||||
|
assert_eq!(doc.get("bindings.0.action").unwrap(), "toggle-overview");
|
||||||
|
doc.set("bindings.0.action", "none".into()).unwrap();
|
||||||
|
doc.set("input.repeat_rate", 25.into()).unwrap();
|
||||||
|
doc.set("output_profiles.0.outputs.0.scale", 2.0.into())
|
||||||
|
.unwrap();
|
||||||
|
let reparsed = Document::parse(&doc.to_string()).unwrap();
|
||||||
|
assert_eq!(reparsed.get("bindings.0.action").unwrap(), "none");
|
||||||
|
assert_eq!(reparsed.get("input.repeat_rate").unwrap(), 25);
|
||||||
|
assert_eq!(
|
||||||
|
reparsed.get("output_profiles.0.outputs.0.scale").unwrap(),
|
||||||
|
2.0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_duplicate_fields_ambiguous_syntax_and_nonfinite_values() {
|
||||||
|
for source in [
|
||||||
|
"input { repeat-rate 25; repeat_rate 30; }",
|
||||||
|
"binding \"X\" keys=\"Y\"",
|
||||||
|
"input 1 { repeat-rate 25; }",
|
||||||
|
"layout { inner-gap #inf; }",
|
||||||
|
"layout { inner-gap #null; }",
|
||||||
|
] {
|
||||||
|
assert!(Document::parse(source).is_err(), "{source}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn edits_preserve_comments_commands_and_note_contents() {
|
||||||
|
let source = r#"// personal comment
|
||||||
|
commands {
|
||||||
|
screenshot-full "ferese-screenshot" "--full"
|
||||||
|
}
|
||||||
|
binding "Swipe3Left" "move" "left"
|
||||||
|
desktop-widgets {
|
||||||
|
note "a" text="line one"
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
let mut doc = Document::parse(source).unwrap();
|
||||||
|
doc.set("desktop_widgets.notes.0.text", "one\ntwo".into())
|
||||||
|
.unwrap();
|
||||||
|
assert!(doc.to_string().contains("// personal comment"));
|
||||||
|
let reparsed = Document::parse(&doc.to_string()).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
reparsed.get("commands.screenshot-full").unwrap(),
|
||||||
|
&serde_json::json!(["ferese-screenshot", "--full"])
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
reparsed.get("desktop_widgets.notes.0.text").unwrap(),
|
||||||
|
"one\ntwo"
|
||||||
|
);
|
||||||
|
doc.add("desktop_widgets.notes", vec![("id".into(), "b".into())])
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
doc.get("desktop_widgets.notes")
|
||||||
|
.unwrap()
|
||||||
|
.as_array()
|
||||||
|
.unwrap()
|
||||||
|
.len(),
|
||||||
|
2
|
||||||
|
);
|
||||||
|
doc.remove("desktop_widgets.notes", 0).unwrap();
|
||||||
|
assert_eq!(doc.get("desktop_widgets.notes.0.id").unwrap(), "b");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -97,6 +97,7 @@ impl StickyNote {
|
|||||||
&& self.width == other.width
|
&& self.width == other.width
|
||||||
&& self.height == other.height
|
&& self.height == other.height
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn validate(&self) -> Result<(), String> {
|
pub fn validate(&self) -> Result<(), String> {
|
||||||
let invalid = |field: &str| {
|
let invalid = |field: &str| {
|
||||||
Err(format!(
|
Err(format!(
|
||||||
@@ -347,12 +348,14 @@ mod tests {
|
|||||||
..note.clone()
|
..note.clone()
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn defaults_are_safe_and_opt_in() {
|
fn defaults_are_safe_and_opt_in() {
|
||||||
let clock = Clock::default();
|
let clock = Clock::default();
|
||||||
clock.validate().unwrap();
|
clock.validate().unwrap();
|
||||||
assert!(!clock.on_output(Some("HDMI-A-1")));
|
assert!(!clock.on_output(Some("HDMI-A-1")));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn output_filters_do_not_guess_monitor_names() {
|
fn output_filters_do_not_guess_monitor_names() {
|
||||||
let clock = Clock {
|
let clock = Clock {
|
||||||
@@ -364,6 +367,7 @@ mod tests {
|
|||||||
assert!(!clock.on_output(Some("eDP-1")));
|
assert!(!clock.on_output(Some("eDP-1")));
|
||||||
assert!(!clock.on_output(None));
|
assert!(!clock.on_output(None));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rejects_bad_sizes_colors_formats_and_zones() {
|
fn rejects_bad_sizes_colors_formats_and_zones() {
|
||||||
for clock in [
|
for clock in [
|
||||||
@@ -391,6 +395,7 @@ mod tests {
|
|||||||
assert!(clock.validate().is_err());
|
assert!(clock.validate().is_err());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn formats_timezone_and_lowercase() {
|
fn formats_timezone_and_lowercase() {
|
||||||
let now: jiff::Zoned = "2026-09-26T21:05:00+00:00[UTC]".parse().unwrap();
|
let now: jiff::Zoned = "2026-09-26T21:05:00+00:00[UTC]".parse().unwrap();
|
||||||
|
|||||||
@@ -449,6 +449,7 @@ impl WorkspaceSet {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(
|
pub fn new(
|
||||||
default_layout_mode: LayoutMode,
|
default_layout_mode: LayoutMode,
|
||||||
default_column_width: ColumnWidth,
|
default_column_width: ColumnWidth,
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ impl ScrollingLayout {
|
|||||||
pub fn set_default_width(&mut self, width: ColumnWidth) {
|
pub fn set_default_width(&mut self, width: ColumnWidth) {
|
||||||
self.default_width = normalized_width(width);
|
self.default_width = normalized_width(width);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_default_width(default_width: ColumnWidth) -> Self {
|
pub fn with_default_width(default_width: ColumnWidth) -> Self {
|
||||||
Self {
|
Self {
|
||||||
default_width: normalized_width(default_width),
|
default_width: normalized_width(default_width),
|
||||||
|
|||||||
+96
-64
@@ -1,16 +1,58 @@
|
|||||||
# Configuration
|
# Configuration
|
||||||
|
|
||||||
Edit `~/.config/ferese/config.toml` (or `$XDG_CONFIG_HOME/ferese/config.toml`),
|
Edit `~/.config/ferese/config.kdl` (or `$XDG_CONFIG_HOME/ferese/config.kdl`),
|
||||||
or open **Control Center → Settings**. Start with the [example](../packaging/config.toml).
|
or open **Control Center → Settings**. Start with the [example](../packaging/config.kdl).
|
||||||
All supported configuration sections are listed below; [desktop widgets](desktop-widgets.md)
|
All supported configuration sections are listed below; [desktop widgets](desktop-widgets.md)
|
||||||
covers the complete clock and sticky-note fields separately.
|
covers the complete clock and sticky-note fields separately.
|
||||||
|
|
||||||
|
## KDL syntax
|
||||||
|
|
||||||
|
Sections use nested braces, values follow their names, and booleans are `#true`
|
||||||
|
or `#false`. Multiple values represent arrays. Hyphenated keys are preferred;
|
||||||
|
underscore spellings remain accepted. Enum values retain their documented spelling.
|
||||||
|
A newline or semicolon ends each node. Comments use `//` or `/* ... */`.
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```kdl
|
||||||
|
input {
|
||||||
|
repeat-rate 30
|
||||||
|
touchpad {
|
||||||
|
swipe-threshold 80
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
commands {
|
||||||
|
terminal "foot"
|
||||||
|
}
|
||||||
|
|
||||||
|
binding "Super+Enter" "spawn" "terminal"
|
||||||
|
binding "Swipe3Up" "toggle-overview"
|
||||||
|
binding "Swipe3Down" disabled=#true
|
||||||
|
output-profile "docked" {
|
||||||
|
output "HDMI-A-1" {
|
||||||
|
scale 1.5
|
||||||
|
position 0 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Repeated `binding`, `window-rule`, `output-profile`, `output`, `note` and
|
||||||
|
`autostart` nodes describe lists of settings. Bindings accept positional
|
||||||
|
keys/action/argument; profiles, outputs and notes accept a positional name,
|
||||||
|
match or ID. Other fields can be properties or child nodes. Autostart accepts
|
||||||
|
its command as positional arguments. Duplicate fields are rejected.
|
||||||
|
The dotted paths in the reference tables below describe nested sections.
|
||||||
|
|
||||||
|
Settings preserves comments and custom fields when editing, while normalizing
|
||||||
|
indentation. Runtime configuration uses KDL only. KDL parsing requires Rust 1.95
|
||||||
|
or newer to build.
|
||||||
|
|
||||||
## Saving and validation
|
## Saving and validation
|
||||||
|
|
||||||
Settings saves automatically: text on Enter/focus loss, sliders when released.
|
Settings saves automatically: text on Enter/focus loss, sliders when released.
|
||||||
Undo restores the previous save; Reload picks up external edits without merging
|
Undo restores the previous save; Reload picks up external edits without merging
|
||||||
unfinished drafts. Wallpaper browsing needs `zenity`; a path can also be entered.
|
unfinished drafts. Wallpaper browsing needs `zenity`; a path can also be entered.
|
||||||
Settings keeps `config.toml.settings-backup` before saving.
|
Settings keeps `config.kdl.settings-backup` before saving.
|
||||||
|
|
||||||
File edits reload automatically, including atomic editor saves. Invalid changes
|
File edits reload automatically, including atomic editor saves. Invalid changes
|
||||||
keep the last working configuration. Appearance, wallpaper, motion, input,
|
keep the last working configuration. Appearance, wallpaper, motion, input,
|
||||||
@@ -29,14 +71,14 @@ Live config is limited to 60 KiB.
|
|||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
|
|
||||||
| `[layout]` key | Type / values | Default | Meaning |
|
| `layout` key | Type / values | Default | Meaning |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `mode` | `"scrolling"`, `"tree"` | `"scrolling"` | Workspace layout |
|
| `mode` | `"scrolling"`, `"tree"` | `"scrolling"` | Workspace layout |
|
||||||
| `inner_gap` | number ≥ 0 | `10` | Between windows |
|
| `inner_gap` | number ≥ 0 | `10` | Between windows |
|
||||||
| `outer_gap` | number ≥ 0 | `4` | Around workspace edges |
|
| `outer_gap` | number ≥ 0 | `4` | Around workspace edges |
|
||||||
| `smart_gaps` | boolean | `false` | Remove outer gaps for one tiled window |
|
| `smart_gaps` | boolean | `false` | Remove outer gaps for one tiled window |
|
||||||
|
|
||||||
| `[scrolling]` key | Type / values | Default | Meaning |
|
| `scrolling` key | Type / values | Default | Meaning |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `default_column_width` | proportion > 0; or `"full"` | `0.5` | Width of new columns |
|
| `default_column_width` | proportion > 0; or `"full"` | `0.5` | Width of new columns |
|
||||||
| `focus_strategy` | `"minimal"`, `"center_on_focus"`, `"paged"` | `"minimal"` | Viewport movement on focus |
|
| `focus_strategy` | `"minimal"`, `"center_on_focus"`, `"paged"` | `"minimal"` | Viewport movement on focus |
|
||||||
@@ -75,7 +117,7 @@ Changing default width preserves manually resized columns.
|
|||||||
| `theme.material.style` | `"solid"`, `"translucent"` | `"solid"` | Shell background material |
|
| `theme.material.style` | `"solid"`, `"translucent"` | `"solid"` | Shell background material |
|
||||||
| `theme.material.blur_radius` | number ≥ 0 | `12` | Translucent backdrop blur, capped at 32; 0 disables |
|
| `theme.material.blur_radius` | number ≥ 0 | `12` | Translucent backdrop blur, capped at 32; 0 disables |
|
||||||
|
|
||||||
| `[theme.colors]` key | Default | Meaning |
|
| `theme.colors` key | Default | Meaning |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `surface_base` | `"#111821"` | Material/card base color |
|
| `surface_base` | `"#111821"` | Material/card base color |
|
||||||
| `text_primary` | `"#F4F7FB"` | Main text |
|
| `text_primary` | `"#F4F7FB"` | Main text |
|
||||||
@@ -84,7 +126,7 @@ Changing default width preserves manually resized columns.
|
|||||||
| `border` | `"#FFFFFF18"` | Unfocused window border |
|
| `border` | `"#FFFFFF18"` | Unfocused window border |
|
||||||
| `shadow` | `"#00000055"` | Window shadow color |
|
| `shadow` | `"#00000055"` | Window shadow color |
|
||||||
|
|
||||||
| `[theme.geometry]` key | Type | Default | Meaning |
|
| `theme.geometry` key | Type | Default | Meaning |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `border_width` | number ≥ 0 | `1` | Window border thickness |
|
| `border_width` | number ≥ 0 | `1` | Window border thickness |
|
||||||
| `focus_ring_width` | number ≥ 0 | `2` | Focused border thickness |
|
| `focus_ring_width` | number ≥ 0 | `2` | Focused border thickness |
|
||||||
@@ -111,21 +153,26 @@ Changing default width preserves manually resized columns.
|
|||||||
Solid materials omit blur; translucent materials blur behind the surface color.
|
Solid materials omit blur; translucent materials blur behind the surface color.
|
||||||
True fullscreen removes decorations. Focus transitions never scale the content.
|
True fullscreen removes decorations. Focus transitions never scale the content.
|
||||||
|
|
||||||
Optional `[theme.focus_ring.gradient]` and `[theme.border.gradient]` use the same
|
Optional `theme.focus_ring.gradient` and `theme.border.gradient` use the same
|
||||||
keys: required `from` and `to` colors, and `angle` (number, default `0`). Angles
|
keys: required `from` and `to` colors, and `angle` (number, default `0`). Angles
|
||||||
are clockwise: 0 is left-to-right, 90 top-to-bottom. Omit the gradient section
|
are clockwise: 0 is left-to-right, 90 top-to-bottom. Omit the gradient section
|
||||||
for solid `accent` or `border` colors.
|
for solid `accent` or `border` colors.
|
||||||
|
|
||||||
```toml
|
```kdl
|
||||||
[theme.focus_ring.gradient]
|
theme {
|
||||||
from = "#e5c890"
|
focus-ring {
|
||||||
to = "#b98d58"
|
gradient {
|
||||||
angle = 135.0
|
from "#e5c890"
|
||||||
|
to "#b98d58"
|
||||||
|
angle 135.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Input
|
## Input
|
||||||
|
|
||||||
| `[input]` key | Type | Default | Meaning |
|
| `input` key | Type | Default | Meaning |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `focus_follows_mouse` | boolean | `false` | Focus windows under the pointer without raising them; click to raise |
|
| `focus_follows_mouse` | boolean | `false` | Focus windows under the pointer without raising them; click to raise |
|
||||||
| `xkb_layout` | nonempty string | `"us"` | XKB layout |
|
| `xkb_layout` | nonempty string | `"us"` | XKB layout |
|
||||||
@@ -150,37 +197,28 @@ keyboard bindings. Choose a "Customize swipe" button to add an override. The
|
|||||||
gesture keys are `Swipe3Up`, `Swipe3Down`, `Swipe3Left`, and `Swipe3Right`; 4 or 5
|
gesture keys are `Swipe3Up`, `Swipe3Down`, `Swipe3Left`, and `Swipe3Right`; 4 or 5
|
||||||
fingers are also supported. Changes reload live:
|
fingers are also supported. Changes reload live:
|
||||||
|
|
||||||
```toml
|
```kdl
|
||||||
[[bindings]]
|
binding keys="Swipe3Up" action="toggle-overview"
|
||||||
keys = "Swipe3Up"
|
binding keys="Swipe3Left" action="move" argument="left"
|
||||||
action = "toggle-overview"
|
binding keys="Swipe3Down" disabled=#true
|
||||||
|
|
||||||
[[bindings]]
|
|
||||||
keys = "Swipe3Left"
|
|
||||||
action = "move"
|
|
||||||
argument = "left"
|
|
||||||
|
|
||||||
[[bindings]]
|
|
||||||
keys = "Swipe3Down"
|
|
||||||
disabled = true
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`spawn` uses a named entry from `[commands]`, just like keyboard shortcuts. To
|
`spawn` uses a named entry from `commands`, just like keyboard shortcuts. To
|
||||||
disable a direction in Settings, set its action to `none` and leave Argument
|
disable a direction in Settings, set its action to `none` and leave Argument
|
||||||
empty. In TOML, `disabled = true` removes the default binding. Swipes do not use
|
empty. In KDL, `disabled=#true` removes the default binding. Swipes do not use
|
||||||
keyboard modifiers or physical key matching. The trigger is automatically
|
keyboard modifiers or physical key matching. The trigger is automatically
|
||||||
recognized from its name. Gesture navigation uses native touchpad events in a
|
recognized from its name. Gesture navigation uses native touchpad events in a
|
||||||
hardware session.
|
hardware session.
|
||||||
|
|
||||||
Adjust recognition distance in **Settings → Keyboard & mouse**, or with
|
Adjust recognition distance in **Settings → Keyboard & mouse**, or with
|
||||||
`swipe_threshold = 80` under `[input.touchpad]` (integer 16–1000 logical pixels).
|
`swipe-threshold 80` under `input.touchpad` (integer 16–1000 logical pixels).
|
||||||
|
|
||||||
## Commands and bindings
|
## Commands and bindings
|
||||||
|
|
||||||
`[commands]` maps arbitrary names to nonempty argument arrays. The built-in
|
`commands` maps arbitrary names to nonempty argument arrays. The built-in
|
||||||
`terminal = ["foot"]` can be overridden. Commands run directly, without a shell.
|
`terminal "foot"` can be overridden. Commands run directly, without a shell.
|
||||||
|
|
||||||
| `[[bindings]]` key | Type / values | Default |
|
| `binding` key | Type / values | Default |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `keys` | chord or gesture, e.g. `"Super+Enter"`, `"Swipe3Up"` | required |
|
| `keys` | chord or gesture, e.g. `"Super+Enter"`, `"Swipe3Up"` | required |
|
||||||
| `match` | `"keysym"`, `"physical"` | `"keysym"` |
|
| `match` | `"keysym"`, `"physical"` | `"keysym"` |
|
||||||
@@ -195,21 +233,18 @@ invalid. A disabled binding must omit `action` and `argument`.
|
|||||||
|
|
||||||
| Actions | Argument |
|
| Actions | Argument |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `spawn` | Name in `[commands]` |
|
| `spawn` | Name in `commands` |
|
||||||
| `focus`, `move`, `resize` | `"left"`, `"right"`, `"up"`, `"down"` |
|
| `focus`, `move`, `resize` | `"left"`, `"right"`, `"up"`, `"down"` |
|
||||||
| `workspace`, `move-to-workspace` | Workspace number string, 1–255 |
|
| `workspace`, `move-to-workspace` | Workspace number string, 1–255 |
|
||||||
| `workspace-next`, `workspace-previous` | None; next/previous workspace on this monitor |
|
| `workspace-next`, `workspace-previous` | None; next/previous workspace on this monitor |
|
||||||
| `none` | None; ignore this trigger |
|
| `none` | None; ignore this trigger |
|
||||||
| `close`, `exit`, `toggle-maximized`, `toggle-fullscreen`, `toggle-layout`, `cycle-column-width`, `center-column`, `consume`, `expel`, `toggle-floating`, `toggle-overview` | None |
|
| `close`, `exit`, `toggle-maximized`, `toggle-fullscreen`, `toggle-layout`, `cycle-column-width`, `center-column`, `consume`, `expel`, `toggle-floating`, `toggle-overview` | None |
|
||||||
|
|
||||||
```toml
|
```kdl
|
||||||
[commands]
|
commands {
|
||||||
terminal = ["foot"]
|
terminal "foot"
|
||||||
|
}
|
||||||
[[bindings]]
|
binding keys="Super+Enter" action="spawn" argument="terminal"
|
||||||
keys = "Super+Enter"
|
|
||||||
action = "spawn"
|
|
||||||
argument = "terminal"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Defaults: Super+Enter terminal; Super+Q close; Super+H/J/K/L focus;
|
Defaults: Super+Enter terminal; Super+Q close; Super+H/J/K/L focus;
|
||||||
@@ -233,7 +268,7 @@ this command. Both modes open Satty with the same save and copy workflow.
|
|||||||
|
|
||||||
## Window rules
|
## Window rules
|
||||||
|
|
||||||
| `[[window_rules]]` key | Type | Default / meaning |
|
| `window-rule` key | Type | Default / meaning |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `app_id` | nonempty string | Optional exact match; case-insensitive, `.desktop` suffix ignored |
|
| `app_id` | nonempty string | Optional exact match; case-insensitive, `.desktop` suffix ignored |
|
||||||
| `title` | nonempty string | Optional exact, case-sensitive match |
|
| `title` | nonempty string | Optional exact, case-sensitive match |
|
||||||
@@ -249,20 +284,18 @@ when `floating` is omitted. Floating apps without dimensions choose their own
|
|||||||
size and open centered on the output. Title changes do not trigger new rules;
|
size and open centered on the output. Title changes do not trigger new rules;
|
||||||
config rule changes apply to existing windows.
|
config rule changes apply to existing windows.
|
||||||
|
|
||||||
```toml
|
```kdl
|
||||||
[[window_rules]]
|
window-rule app-id="dev.ferese.Settings" floating=#true
|
||||||
app_id = "dev.ferese.Settings"
|
|
||||||
floating = true
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Displays
|
## Displays
|
||||||
|
|
||||||
`[[output_profiles]]` has a required unique nonempty `name` and one or more
|
`output-profile` has a required unique nonempty `name` and one or more
|
||||||
`[[output_profiles.outputs]]` entries. The first profile whose listed monitors
|
`output-profile → output` entries. The first profile whose listed monitors
|
||||||
are connected wins. Find connector names and persistent identities using
|
are connected wins. Find connector names and persistent identities using
|
||||||
`feresectl get-outputs`.
|
`feresectl get-outputs`.
|
||||||
|
|
||||||
| `[[output_profiles.outputs]]` key | Type | Default |
|
| `output-profile → output` key | Type | Default |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `match` | nonempty connector or persistent identity string | required; unique within profile |
|
| `match` | nonempty connector or persistent identity string | required; unique within profile |
|
||||||
| `enabled` | boolean | `true` |
|
| `enabled` | boolean | `true` |
|
||||||
@@ -275,14 +308,12 @@ Transforms: `normal`, `rotate_90`, `rotate_180`, `rotate_270`, `flipped`,
|
|||||||
`flipped_90`, `flipped_180`, `flipped_270`. Unspecified connected displays stay
|
`flipped_90`, `flipped_180`, `flipped_270`. Unspecified connected displays stay
|
||||||
enabled with defaults. Disabling every usable output is rejected.
|
enabled with defaults. Disabling every usable output is rejected.
|
||||||
|
|
||||||
```toml
|
```kdl
|
||||||
[[output_profiles]]
|
output-profile name="docked" {
|
||||||
name = "docked"
|
output match="HDMI-A-1" scale=1.5 {
|
||||||
|
position 0 0
|
||||||
[[output_profiles.outputs]]
|
}
|
||||||
match = "HDMI-A-1"
|
}
|
||||||
scale = 1.5
|
|
||||||
position = [0, 0]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
With a usable external display, closing the lid disables internal panels.
|
With a usable external display, closing the lid disables internal panels.
|
||||||
@@ -291,7 +322,7 @@ returns. Ferese does not change the system's suspend policy.
|
|||||||
|
|
||||||
## Status controls
|
## Status controls
|
||||||
|
|
||||||
| `[status]` key | Type | Default | Meaning |
|
| `status` key | Type | Default | Meaning |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `battery_percentage` | boolean | `true` | Show percentage beside icon |
|
| `battery_percentage` | boolean | `true` | Show percentage beside icon |
|
||||||
| `low_battery_threshold` | integer 0–100 | `20` | Warning-color threshold |
|
| `low_battery_threshold` | integer 0–100 | `20` | Warning-color threshold |
|
||||||
@@ -299,7 +330,7 @@ returns. Ferese does not change the system's suspend policy.
|
|||||||
|
|
||||||
## Login items and locking
|
## Login items and locking
|
||||||
|
|
||||||
| `[[autostart]]` key | Type | Default | Meaning |
|
| `autostart` key | Type | Default | Meaning |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `command` | nonempty argument array | required | Foreground process, not a self-daemonizing command |
|
| `command` | nonempty argument array | required | Foreground process, not a self-daemonizing command |
|
||||||
| `enabled` | boolean | `true` | Start service |
|
| `enabled` | boolean | `true` | Start service |
|
||||||
@@ -309,9 +340,10 @@ returns. Ferese does not change the system's suspend policy.
|
|||||||
Unchanged services keep running on reload; removed/disabled items stop. To lock
|
Unchanged services keep running on reload; removed/disabled items stop. To lock
|
||||||
after five minutes and before sleep:
|
after five minutes and before sleep:
|
||||||
|
|
||||||
```toml
|
```kdl
|
||||||
[[autostart]]
|
autostart {
|
||||||
command = ["swayidle", "-w", "timeout", "300", "ferese-lock", "before-sleep", "ferese-lock", "lock", "ferese-lock"]
|
command "swayidle" "-w" "timeout" "300" "ferese-lock" "before-sleep" "ferese-lock" "lock" "ferese-lock"
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`ferese-lock` uses swaylock/swaylock-effects and optional
|
`ferese-lock` uses swaylock/swaylock-effects and optional
|
||||||
@@ -321,7 +353,7 @@ ending the session from another TTY.
|
|||||||
|
|
||||||
## Optional session protocols
|
## Optional session protocols
|
||||||
|
|
||||||
These are launch-time environment switches, not TOML keys. Enable only for
|
These are launch-time environment switches, not KDL keys. Enable only for
|
||||||
trusted clients: `FERESE_ENABLE_INPUT_METHOD=1`,
|
trusted clients: `FERESE_ENABLE_INPUT_METHOD=1`,
|
||||||
`FERESE_ENABLE_SHORTCUT_INHIBIT=1`, `FERESE_ENABLE_SCREENCOPY=1`.
|
`FERESE_ENABLE_SHORTCUT_INHIBIT=1`, `FERESE_ENABLE_SCREENCOPY=1`.
|
||||||
The installed session launcher enables screencopy for screenshots unless
|
The installed session launcher enables screencopy for screenshots unless
|
||||||
|
|||||||
+32
-40
@@ -11,32 +11,31 @@ Lock-screen rendering remains separate and does not expose the desktop clock.
|
|||||||
|
|
||||||
Enable it in Settings → Desktop widgets or in your Ferese config:
|
Enable it in Settings → Desktop widgets or in your Ferese config:
|
||||||
|
|
||||||
```toml
|
```kdl
|
||||||
[desktop_widgets.clock]
|
desktop-widgets {
|
||||||
enabled = true
|
clock enabled=#true anchor="top_left" margin-x=64 margin-y=80 {
|
||||||
outputs = [] # every output; or ["DP-2"] using your own connector
|
outputs // every output; or outputs "DP-2"
|
||||||
anchor = "top_left"
|
width 440
|
||||||
margin_x = 64 # distance from anchored left/right edge
|
height 160
|
||||||
margin_y = 80 # distance from anchored top/bottom edge
|
font-family "" // inherit the desktop font
|
||||||
width = 440
|
bold #false
|
||||||
height = 160
|
time-size 72.0
|
||||||
font_family = "" # empty/omitted follows your desktop font
|
date-size 18.0
|
||||||
bold = false
|
time-format "%-I:%M %p" // use "%H:%M" for 24-hour time
|
||||||
time_size = 72.0
|
date-format "%A, %-d %B"
|
||||||
date_size = 18.0
|
time-zone "" // system zone; or "Africa/Lagos"
|
||||||
time_format = "%-I:%M %p" # 12-hour; "%H:%M" for 24-hour, "%H:%M:%S" for seconds
|
show-date #true
|
||||||
date_format = "%A, %-d %B"
|
lowercase #true
|
||||||
time_zone = "" # system zone; or e.g. "Africa/Lagos", "UTC"
|
color "" // theme primary text
|
||||||
show_date = true
|
date-color "" // theme muted text
|
||||||
lowercase = true
|
opacity 0.9
|
||||||
color = "" # theme primary text; or "#ffffff", "#ffffffcc"
|
alignment "center" // left, center, right
|
||||||
date_color = "" # theme muted text
|
gap 4.0
|
||||||
opacity = 0.9 # multiplies text and optional background alpha
|
padding 12.0
|
||||||
alignment = "center" # left, center, right
|
background "" // transparent; or "#10101080"
|
||||||
gap = 4.0
|
radius 16.0
|
||||||
padding = 12.0
|
}
|
||||||
background = "" # transparent; or e.g. "#10101080" for a solid tint
|
}
|
||||||
radius = 16.0 # optional background corner radius
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Positions: `top_left`, `top_center`, `top_right`, `center_left`, `center`,
|
Positions: `top_left`, `top_center`, `top_right`, `center_left`, `center`,
|
||||||
@@ -55,7 +54,7 @@ configuration. Theme changes apply to colors/fonts that have no explicit overrid
|
|||||||
Formatting uses [Jiff's strftime-compatible formats](https://docs.rs/jiff/latest/jiff/fmt/strtime/index.html).
|
Formatting uses [Jiff's strftime-compatible formats](https://docs.rs/jiff/latest/jiff/fmt/strtime/index.html).
|
||||||
Seconds update on the shell's existing timer. Without seconds, unchanged labels
|
Seconds update on the shell's existing timer. Without seconds, unchanged labels
|
||||||
produce no additional clock buffer damage; there is no widget animation loop.
|
produce no additional clock buffer damage; there is no widget animation loop.
|
||||||
Widget placement can be changed by dragging or through Settings and TOML.
|
Widget placement can be changed by dragging or through Settings and KDL.
|
||||||
Sticky notes also support editing directly on the desktop.
|
Sticky notes also support editing directly on the desktop.
|
||||||
|
|
||||||
## Clock limits
|
## Clock limits
|
||||||
@@ -87,21 +86,14 @@ on release; dragging changes the anchor to `top_left` and saves pixel margins.
|
|||||||
Title, size and style are also editable in Settings. Notes persist in the config.
|
Title, size and style are also editable in Settings. Notes persist in the config.
|
||||||
These are plain-text notes, not Markdown or HTML.
|
These are plain-text notes, not Markdown or HTML.
|
||||||
|
|
||||||
```toml
|
```kdl
|
||||||
[[desktop_widgets.notes]]
|
desktop-widgets {
|
||||||
id = "today"
|
note id="today" title="Today" text="Review the pull request\nTake a break\n" anchor="top_right" margin-x=48 margin-y=80
|
||||||
title = "Today"
|
}
|
||||||
text = """
|
|
||||||
Review the pull request
|
|
||||||
Take a break
|
|
||||||
"""
|
|
||||||
anchor = "top_right"
|
|
||||||
margin_x = 48
|
|
||||||
margin_y = 80
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Repeat the table for additional notes; IDs must be unique. Cards stay behind windows
|
Repeat the `note` node for additional notes; IDs must be unique. Cards stay behind windows
|
||||||
and take keyboard focus only when clicked. Set `interactive = false` for a fully
|
and take keyboard focus only when clicked. Set `interactive #false` for a fully
|
||||||
click-through card. Long text wraps; increase height if needed. Notes have no idle
|
click-through card. Long text wraps; increase height if needed. Notes have no idle
|
||||||
animation loop. Clock and notes share a compact drag preview moved by the
|
animation loop. Clock and notes share a compact drag preview moved by the
|
||||||
compositor, not an output-sized canvas. It is released when dragging ends.
|
compositor, not an output-sized canvas. It is released when dragging ends.
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ It preserves your configuration and other desktop sessions. Log out and select
|
|||||||
```
|
```
|
||||||
|
|
||||||
Use `--help` for all options. New configs can be copied from
|
Use `--help` for all options. New configs can be copied from
|
||||||
`/usr/local/lib/ferese/current/config.example.toml` to
|
`/usr/local/lib/ferese/current/config.example.kdl` to
|
||||||
`~/.config/ferese/config.toml`. See [Configuration](configuration.md).
|
`~/.config/ferese/config.kdl`. See [Configuration](configuration.md).
|
||||||
|
|
||||||
## Preview and logs
|
## Preview and logs
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
commands {
|
||||||
|
terminal "foot"
|
||||||
|
screenshot "ferese-screenshot"
|
||||||
|
screenshot-full "ferese-screenshot" "--full"
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
xkb-layout "us"
|
||||||
|
focus-follows-mouse #false
|
||||||
|
|
||||||
|
touchpad {
|
||||||
|
swipe-threshold 80
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
layout {
|
||||||
|
smart-gaps #false
|
||||||
|
}
|
||||||
|
|
||||||
|
theme {
|
||||||
|
surface {
|
||||||
|
bar {
|
||||||
|
background "#1C202E"
|
||||||
|
opacity 0.95
|
||||||
|
text-primary "#F0F3FA"
|
||||||
|
text-muted "#AAB4C7"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window-rule app-id="dev.ferese.Settings" floating=#true
|
||||||
|
window-rule title="Choose a wallpaper" floating=#true
|
||||||
|
|
||||||
|
desktop-widgets {
|
||||||
|
clock {
|
||||||
|
enabled #false
|
||||||
|
anchor "top_left"
|
||||||
|
margin-x 64
|
||||||
|
margin-y 80
|
||||||
|
time-size 72.0
|
||||||
|
date-size 18.0
|
||||||
|
opacity 0.9
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
# User configuration: ~/.config/ferese/config.toml
|
|
||||||
# Unspecified monitors use automatic placement and preferred modes.
|
|
||||||
[commands]
|
|
||||||
terminal = ["foot"]
|
|
||||||
screenshot = ["ferese-screenshot"]
|
|
||||||
screenshot-full = ["ferese-screenshot", "--full"]
|
|
||||||
|
|
||||||
[input]
|
|
||||||
xkb_layout = "us"
|
|
||||||
focus_follows_mouse = false
|
|
||||||
|
|
||||||
[input.touchpad]
|
|
||||||
swipe_threshold = 80
|
|
||||||
|
|
||||||
[layout]
|
|
||||||
smart_gaps = false
|
|
||||||
|
|
||||||
# Edge-to-edge menu bar; its foreground palette is independent of popup surfaces.
|
|
||||||
[theme.surface.bar]
|
|
||||||
background = "#1C202E"
|
|
||||||
opacity = 0.95
|
|
||||||
text_primary = "#F0F3FA"
|
|
||||||
text_muted = "#AAB4C7"
|
|
||||||
|
|
||||||
# Super+Enter opens Foot. Super+Shift+S selects a screenshot. Super+Shift+E logs out.
|
|
||||||
# See README.md for monitor profiles and additional bindings.
|
|
||||||
|
|
||||||
[[window_rules]]
|
|
||||||
app_id = "dev.ferese.Settings"
|
|
||||||
floating = true
|
|
||||||
|
|
||||||
[[window_rules]]
|
|
||||||
title = "Choose a wallpaper"
|
|
||||||
floating = true
|
|
||||||
|
|
||||||
# Optional desktop clock; all dimensions are logical pixels.
|
|
||||||
[desktop_widgets.clock]
|
|
||||||
enabled = false
|
|
||||||
anchor = "top_left"
|
|
||||||
margin_x = 64
|
|
||||||
margin_y = 80
|
|
||||||
time_size = 72.0
|
|
||||||
date_size = 18.0
|
|
||||||
opacity = 0.9
|
|
||||||
|
|
||||||
# Swipe bindings use the same actions and arguments as keyboard bindings.
|
|
||||||
# Defaults: up/down switch workspaces; left/right focus windows.
|
|
||||||
# Example override:
|
|
||||||
# [[bindings]]
|
|
||||||
# keys = "Swipe3Up"
|
|
||||||
# action = "toggle-overview"
|
|
||||||
@@ -55,7 +55,7 @@ done
|
|||||||
install -m 0755 -- "$repo_dir/packaging/ferese-session" "$release_dir/ferese-session"
|
install -m 0755 -- "$repo_dir/packaging/ferese-session" "$release_dir/ferese-session"
|
||||||
install -m 0755 -- "$repo_dir/packaging/ferese-lock" "$release_dir/ferese-lock"
|
install -m 0755 -- "$repo_dir/packaging/ferese-lock" "$release_dir/ferese-lock"
|
||||||
install -m 0755 -- "$repo_dir/packaging/ferese-screenshot" "$release_dir/ferese-screenshot"
|
install -m 0755 -- "$repo_dir/packaging/ferese-screenshot" "$release_dir/ferese-screenshot"
|
||||||
install -m 0644 -- "$repo_dir/packaging/config.toml" "$release_dir/config.example.toml"
|
install -m 0644 -- "$repo_dir/packaging/config.kdl" "$release_dir/config.example.kdl"
|
||||||
|
|
||||||
if [[ -L $install_root/current ]]; then
|
if [[ -L $install_root/current ]]; then
|
||||||
ln -sfn -- "$(readlink -- "$install_root/current")" "$install_root/previous"
|
ln -sfn -- "$(readlink -- "$install_root/current")" "$install_root/previous"
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ license.workspace = true
|
|||||||
rust-version = "1.93"
|
rust-version = "1.93"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
ferese-config = { path = "../../crates/ferese-config" }
|
||||||
libcosmic = { workspace = true, features = ["wgpu", "a11y", "xdg-portal"] }
|
libcosmic = { workspace = true, features = ["wgpu", "a11y", "xdg-portal"] }
|
||||||
ferese-ipc = { path = "../../crates/ferese-ipc" }
|
ferese-ipc = { path = "../../crates/ferese-ipc" }
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
toml_edit = "0.25.15"
|
|
||||||
tempfile.workspace = true
|
tempfile.workspace = true
|
||||||
shlex = "1.3.0"
|
shlex = "1.3.0"
|
||||||
notify = "8.2.0"
|
notify = "8.2.0"
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ fn main() -> cosmic::iced::Result {
|
|||||||
std::env::set_var("ICED_BACKEND", "tiny-skia,wgpu");
|
std::env::set_var("ICED_BACKEND", "tiny-skia,wgpu");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut args = std::env::args_os().skip(1);
|
let mut args = std::env::args_os().skip(1);
|
||||||
let path = match args.next() {
|
let path = match args.next() {
|
||||||
Some(arg) if arg == "--config" => {
|
Some(arg) if arg == "--config" => {
|
||||||
@@ -35,6 +36,7 @@ fn main() -> cosmic::iced::Result {
|
|||||||
let path = path.canonicalize().unwrap_or(path);
|
let path = path.canonicalize().unwrap_or(path);
|
||||||
let initial = Snapshot::read(&path);
|
let initial = Snapshot::read(&path);
|
||||||
let theme = visuals::native_theme(initial.as_ref().ok());
|
let theme = visuals::native_theme(initial.as_ref().ok());
|
||||||
|
|
||||||
cosmic::app::run::<App>(
|
cosmic::app::run::<App>(
|
||||||
Settings::default()
|
Settings::default()
|
||||||
.theme(theme)
|
.theme(theme)
|
||||||
@@ -107,14 +109,18 @@ impl cosmic::Application for App {
|
|||||||
type Flags = (PathBuf, Result<Snapshot, String>);
|
type Flags = (PathBuf, Result<Snapshot, String>);
|
||||||
type Message = Message;
|
type Message = Message;
|
||||||
const APP_ID: &'static str = "dev.ferese.Settings";
|
const APP_ID: &'static str = "dev.ferese.Settings";
|
||||||
|
|
||||||
fn core(&self) -> &Core {
|
fn core(&self) -> &Core {
|
||||||
&self.core
|
&self.core
|
||||||
}
|
}
|
||||||
|
|
||||||
fn core_mut(&mut self) -> &mut Core {
|
fn core_mut(&mut self) -> &mut Core {
|
||||||
&mut self.core
|
&mut self.core
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init(mut core: Core, (path, initial): Self::Flags) -> (Self, Task<Message>) {
|
fn init(mut core: Core, (path, initial): Self::Flags) -> (Self, Task<Message>) {
|
||||||
core.window.show_headerbar = false;
|
core.window.show_headerbar = false;
|
||||||
|
|
||||||
let status = if path == store::config_path() {
|
let status = if path == store::config_path() {
|
||||||
"Changes save automatically"
|
"Changes save automatically"
|
||||||
} else {
|
} else {
|
||||||
@@ -152,15 +158,18 @@ impl cosmic::Application for App {
|
|||||||
.main_window_id()
|
.main_window_id()
|
||||||
.map(|id| app.set_window_title("Ferese Settings".into(), id))
|
.map(|id| app.set_window_title("Ferese Settings".into(), id))
|
||||||
.unwrap_or_else(Task::none);
|
.unwrap_or_else(Task::none);
|
||||||
|
|
||||||
app.sync_notes();
|
app.sync_notes();
|
||||||
(app, task)
|
(app, task)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(&mut self, message: Message) -> Task<Message> {
|
fn update(&mut self, message: Message) -> Task<Message> {
|
||||||
match message {
|
match message {
|
||||||
Message::NoteAction(id, action) => {
|
Message::NoteAction(id, action) => {
|
||||||
if let Some(editor) = self.note_editors.get_mut(&id) {
|
if let Some(editor) = self.note_editors.get_mut(&id) {
|
||||||
let edited = action.is_edit();
|
let edited = action.is_edit();
|
||||||
editor.content.perform(action);
|
editor.content.perform(action);
|
||||||
|
|
||||||
if edited {
|
if edited {
|
||||||
editor.dirty = true;
|
editor.dirty = true;
|
||||||
editor.revision = editor.revision.wrapping_add(1);
|
editor.revision = editor.revision.wrapping_add(1);
|
||||||
@@ -176,6 +185,7 @@ impl cosmic::Application for App {
|
|||||||
let Some(index) = self.note_index(&id) else {
|
let Some(index) = self.note_index(&id) else {
|
||||||
return Task::none();
|
return Task::none();
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(editor) = self.note_editors.get_mut(&id)
|
if let Some(editor) = self.note_editors.get_mut(&id)
|
||||||
&& editor.dirty
|
&& editor.dirty
|
||||||
&& editor.revision == revision
|
&& editor.revision == revision
|
||||||
@@ -203,6 +213,7 @@ impl cosmic::Application for App {
|
|||||||
("text".into(), "".into()),
|
("text".into(), "".into()),
|
||||||
],
|
],
|
||||||
));
|
));
|
||||||
|
|
||||||
self.sync_notes();
|
self.sync_notes();
|
||||||
return task;
|
return task;
|
||||||
}
|
}
|
||||||
@@ -229,6 +240,7 @@ impl cosmic::Application for App {
|
|||||||
self.undo = None;
|
self.undo = None;
|
||||||
self.error = None;
|
self.error = None;
|
||||||
self.status = "Updated from your config".into();
|
self.status = "Updated from your config".into();
|
||||||
|
|
||||||
return Task::batch([
|
return Task::batch([
|
||||||
cosmic::command::set_theme(visuals::native_theme(Some(&self.draft))),
|
cosmic::command::set_theme(visuals::native_theme(Some(&self.draft))),
|
||||||
self.load_thumbnail(),
|
self.load_thumbnail(),
|
||||||
@@ -266,9 +278,12 @@ impl cosmic::Application for App {
|
|||||||
if let Some(value) = self.inputs.remove(&field.path) {
|
if let Some(value) = self.inputs.remove(&field.path) {
|
||||||
let edit = if matches!(field.kind, Kind::Text { argv: true, .. }) {
|
let edit = if matches!(field.kind, Kind::Text { argv: true, .. }) {
|
||||||
match shlex::split(&value).filter(|v| !v.is_empty()) {
|
match shlex::split(&value).filter(|v| !v.is_empty()) {
|
||||||
Some(args) => {
|
Some(args) => set(
|
||||||
set(&field.path, args.into_iter().collect::<toml_edit::Array>())
|
&field.path,
|
||||||
}
|
args.into_iter()
|
||||||
|
.map(serde_json::Value::from)
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
),
|
||||||
None => {
|
None => {
|
||||||
self.error = Some("Enter a program and arguments with balanced quotes. Commands are not run through a shell.".into());
|
self.error = Some("Enter a program and arguments with balanced quotes. Commands are not run through a shell.".into());
|
||||||
return Task::none();
|
return Task::none();
|
||||||
@@ -289,11 +304,13 @@ impl cosmic::Application for App {
|
|||||||
self.current = snapshot;
|
self.current = snapshot;
|
||||||
self.font = visuals::configured_font(&self.current);
|
self.font = visuals::configured_font(&self.current);
|
||||||
self.draft = self.current.clone();
|
self.draft = self.current.clone();
|
||||||
|
|
||||||
for edit in &self.pending {
|
for edit in &self.pending {
|
||||||
if let Err(error) = self.draft.edit(edit) {
|
if let Err(error) = self.draft.edit(edit) {
|
||||||
self.error = Some(error);
|
self.error = Some(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.sync_notes();
|
self.sync_notes();
|
||||||
self.status = if live {
|
self.status = if live {
|
||||||
"Saved · desktop updated"
|
"Saved · desktop updated"
|
||||||
@@ -303,7 +320,9 @@ impl cosmic::Application for App {
|
|||||||
"Saved · could not confirm desktop reload"
|
"Saved · could not confirm desktop reload"
|
||||||
}
|
}
|
||||||
.into();
|
.into();
|
||||||
|
|
||||||
let theme = visuals::native_theme(Some(&self.draft));
|
let theme = visuals::native_theme(Some(&self.draft));
|
||||||
|
|
||||||
return Task::batch([
|
return Task::batch([
|
||||||
cosmic::command::set_theme(theme),
|
cosmic::command::set_theme(theme),
|
||||||
self.flush(),
|
self.flush(),
|
||||||
@@ -331,6 +350,7 @@ impl cosmic::Application for App {
|
|||||||
self.error = None;
|
self.error = None;
|
||||||
self.undo = None;
|
self.undo = None;
|
||||||
self.status = "Reloaded from your config".into();
|
self.status = "Reloaded from your config".into();
|
||||||
|
|
||||||
return Task::batch([
|
return Task::batch([
|
||||||
cosmic::command::set_theme(visuals::native_theme(Some(&self.draft))),
|
cosmic::command::set_theme(visuals::native_theme(Some(&self.draft))),
|
||||||
self.load_thumbnail(),
|
self.load_thumbnail(),
|
||||||
@@ -349,6 +369,7 @@ impl cosmic::Application for App {
|
|||||||
self.error = Some(error);
|
self.error = Some(error);
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
|
|
||||||
self.pending.push(edit);
|
self.pending.push(edit);
|
||||||
}
|
}
|
||||||
return self.flush();
|
return self.flush();
|
||||||
@@ -420,7 +441,10 @@ impl cosmic::Application for App {
|
|||||||
vec![
|
vec![
|
||||||
(
|
(
|
||||||
"command".into(),
|
"command".into(),
|
||||||
args.into_iter().collect::<toml_edit::Array>().into(),
|
args.into_iter()
|
||||||
|
.map(serde_json::Value::from)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.into(),
|
||||||
),
|
),
|
||||||
("enabled".into(), true.into()),
|
("enabled".into(), true.into()),
|
||||||
("restart".into(), true.into()),
|
("restart".into(), true.into()),
|
||||||
@@ -444,9 +468,11 @@ impl cosmic::Application for App {
|
|||||||
("keys".into(), keys.into()),
|
("keys".into(), keys.into()),
|
||||||
("action".into(), action.into()),
|
("action".into(), action.into()),
|
||||||
];
|
];
|
||||||
|
|
||||||
if let Some(argument) = argument {
|
if let Some(argument) = argument {
|
||||||
fields.push(("argument".into(), argument.into()));
|
fields.push(("argument".into(), argument.into()));
|
||||||
}
|
}
|
||||||
|
|
||||||
return self.change(Edit::Add("bindings".into(), fields));
|
return self.change(Edit::Add("bindings".into(), fields));
|
||||||
}
|
}
|
||||||
Message::Remove(table, index) if !self.saving => {
|
Message::Remove(table, index) if !self.saving => {
|
||||||
@@ -459,9 +485,11 @@ impl cosmic::Application for App {
|
|||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn subscription(&self) -> cosmic::iced::Subscription<Message> {
|
fn subscription(&self) -> cosmic::iced::Subscription<Message> {
|
||||||
cosmic::iced::Subscription::run_with(self.path.clone(), watch::changes)
|
cosmic::iced::Subscription::run_with(self.path.clone(), watch::changes)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view(&self) -> Element<'_, Message> {
|
fn view(&self) -> Element<'_, Message> {
|
||||||
let palette = visuals::Palette::from(&self.draft);
|
let palette = visuals::Palette::from(&self.draft);
|
||||||
let mut sidebar = column([])
|
let mut sidebar = column([])
|
||||||
@@ -538,6 +566,7 @@ impl cosmic::Application for App {
|
|||||||
.width(Length::Fill),
|
.width(Length::Fill),
|
||||||
);
|
);
|
||||||
let mut body = column([]).spacing(12);
|
let mut body = column([]).spacing(12);
|
||||||
|
|
||||||
if !self.search.is_empty() {
|
if !self.search.is_empty() {
|
||||||
if !Page::ALL.into_iter().any(|p| p.matches(&self.search)) {
|
if !Page::ALL.into_iter().any(|p| p.matches(&self.search)) {
|
||||||
body = body
|
body = body
|
||||||
@@ -586,6 +615,7 @@ impl cosmic::Application for App {
|
|||||||
}
|
}
|
||||||
body = body.push(presets);
|
body = body.push(presets);
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.page == Page::Wallpaper {
|
if self.page == Page::Wallpaper {
|
||||||
if let Some(handle) = &self.thumbnail {
|
if let Some(handle) = &self.thumbnail {
|
||||||
body = body.push(
|
body = body.push(
|
||||||
@@ -607,6 +637,7 @@ impl cosmic::Application for App {
|
|||||||
body.push(button::standard("Choose image…").on_press(Message::PickWallpaper));
|
body.push(button::standard("Choose image…").on_press(Message::PickWallpaper));
|
||||||
}
|
}
|
||||||
let fields = schema::fields(self.page);
|
let fields = schema::fields(self.page);
|
||||||
|
|
||||||
if !fields.is_empty() {
|
if !fields.is_empty() {
|
||||||
let mut group = column([]).spacing(1);
|
let mut group = column([]).spacing(1);
|
||||||
for field in fields {
|
for field in fields {
|
||||||
@@ -618,6 +649,7 @@ impl cosmic::Application for App {
|
|||||||
.class(visuals::surface(palette.card, 14.)),
|
.class(visuals::surface(palette.card, 14.)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
match self.page {
|
match self.page {
|
||||||
Page::Desktop => {
|
Page::Desktop => {
|
||||||
let can_change_list =
|
let can_change_list =
|
||||||
@@ -629,9 +661,11 @@ impl cosmic::Application for App {
|
|||||||
.draft
|
.draft
|
||||||
.string(&format!("desktop_widgets.notes.{index}.id"), "note");
|
.string(&format!("desktop_widgets.notes.{index}.id"), "note");
|
||||||
let mut group = column([]).spacing(8);
|
let mut group = column([]).spacing(8);
|
||||||
|
|
||||||
for field in schema::note_fields(index) {
|
for field in schema::note_fields(index) {
|
||||||
group = group.push(self.field(field));
|
group = group.push(self.field(field));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(editor) = self.note_editors.get(&id) {
|
if let Some(editor) = self.note_editors.get(&id) {
|
||||||
let id = id.clone();
|
let id = id.clone();
|
||||||
group = group.push(
|
group = group.push(
|
||||||
@@ -666,6 +700,7 @@ impl cosmic::Application for App {
|
|||||||
}
|
}
|
||||||
Page::Startup => {
|
Page::Startup => {
|
||||||
body = body.push(self.note("Login items update live. Disabling or removing an item stops the session-owned process; enabling one starts it."));
|
body = body.push(self.note("Login items update live. Disabling or removing an item stops the session-owned process; enabling one starts it."));
|
||||||
|
|
||||||
for index in 0..self.draft.records("autostart") {
|
for index in 0..self.draft.records("autostart") {
|
||||||
let prefix = format!("autostart.{index}");
|
let prefix = format!("autostart.{index}");
|
||||||
let group = column([])
|
let group = column([])
|
||||||
@@ -735,6 +770,7 @@ impl cosmic::Application for App {
|
|||||||
.string(&format!("bindings.{index}.keys"), "")
|
.string(&format!("bindings.{index}.keys"), "")
|
||||||
.eq_ignore_ascii_case(keys)
|
.eq_ignore_ascii_case(keys)
|
||||||
});
|
});
|
||||||
|
|
||||||
if !exists {
|
if !exists {
|
||||||
gestures =
|
gestures =
|
||||||
gestures.push(button::standard(label).on_press_maybe(
|
gestures.push(button::standard(label).on_press_maybe(
|
||||||
@@ -742,7 +778,9 @@ impl cosmic::Application for App {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body = body.push(gestures);
|
body = body.push(gestures);
|
||||||
|
|
||||||
for index in 0..self.draft.records("bindings") {
|
for index in 0..self.draft.records("bindings") {
|
||||||
let prefix = format!("bindings.{index}");
|
let prefix = format!("bindings.{index}");
|
||||||
let group = column([])
|
let group = column([])
|
||||||
@@ -772,7 +810,7 @@ impl cosmic::Application for App {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if self.draft.records("bindings") == 0 {
|
if self.draft.records("bindings") == 0 {
|
||||||
body = body.push(self.note("You are using the built-in shortcuts. Add custom bindings in config.toml; they will appear here after Reload."));
|
body = body.push(self.note("You are using the built-in shortcuts. Add custom bindings in config.kdl; they will appear here after Reload."));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Page::Displays => {
|
Page::Displays => {
|
||||||
@@ -786,6 +824,7 @@ impl cosmic::Application for App {
|
|||||||
17.,
|
17.,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
for output in 0..self.draft.records(&format!("{prefix}.outputs")) {
|
for output in 0..self.draft.records(&format!("{prefix}.outputs")) {
|
||||||
let prefix = format!("{prefix}.outputs.{output}");
|
let prefix = format!("{prefix}.outputs.{output}");
|
||||||
let group = column([]).spacing(1)
|
let group = column([]).spacing(1)
|
||||||
@@ -806,7 +845,9 @@ impl cosmic::Application for App {
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut content = column([]).spacing(16).push(heading);
|
let mut content = column([]).spacing(16).push(heading);
|
||||||
|
|
||||||
if let Some(error) = &self.error {
|
if let Some(error) = &self.error {
|
||||||
content = content.push(
|
content = content.push(
|
||||||
container(self.label(error.clone(), 12.))
|
container(self.label(error.clone(), 12.))
|
||||||
@@ -815,6 +856,7 @@ impl cosmic::Application for App {
|
|||||||
.class(visuals::surface(palette.error, 10.)),
|
.class(visuals::surface(palette.error, 10.)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
content = content.push(
|
content = content.push(
|
||||||
scrollable(container(body).padding([0, 2]).width(Length::Fill)).height(Length::Fill),
|
scrollable(container(body).padding([0, 2]).width(Length::Fill)).height(Length::Fill),
|
||||||
);
|
);
|
||||||
@@ -902,8 +944,10 @@ impl App {
|
|||||||
== id
|
== id
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sync_notes(&mut self) {
|
fn sync_notes(&mut self) {
|
||||||
let mut ids = Vec::new();
|
let mut ids = Vec::new();
|
||||||
|
|
||||||
for index in 0..self.draft.records("desktop_widgets.notes") {
|
for index in 0..self.draft.records("desktop_widgets.notes") {
|
||||||
let prefix = format!("desktop_widgets.notes.{index}");
|
let prefix = format!("desktop_widgets.notes.{index}");
|
||||||
let id = self.draft.string(&format!("{prefix}.id"), "note");
|
let id = self.draft.string(&format!("{prefix}.id"), "note");
|
||||||
@@ -921,8 +965,10 @@ impl App {
|
|||||||
}
|
}
|
||||||
ids.push(id);
|
ids.push(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.note_editors.retain(|id, _| ids.contains(id));
|
self.note_editors.retain(|id, _| ids.contains(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_thumbnail(&mut self) -> Task<Message> {
|
fn load_thumbnail(&mut self) -> Task<Message> {
|
||||||
let path = self.current.string("theme.background.path", "");
|
let path = self.current.string("theme.background.path", "");
|
||||||
if self.page != Page::Wallpaper || self.thumbnail_loading || path == self.thumbnail_path {
|
if self.page != Page::Wallpaper || self.thumbnail_loading || path == self.thumbnail_path {
|
||||||
@@ -934,6 +980,7 @@ impl App {
|
|||||||
self.thumbnail_error = None;
|
self.thumbnail_error = None;
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
|
|
||||||
self.thumbnail_loading = true;
|
self.thumbnail_loading = true;
|
||||||
cosmic::task::future(async move {
|
cosmic::task::future(async move {
|
||||||
let (send, receive) = cosmic::iced::futures::channel::oneshot::channel();
|
let (send, receive) = cosmic::iced::futures::channel::oneshot::channel();
|
||||||
@@ -970,6 +1017,7 @@ impl App {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn label<'a>(
|
fn label<'a>(
|
||||||
&self,
|
&self,
|
||||||
text: impl Into<std::borrow::Cow<'a, str>> + 'a,
|
text: impl Into<std::borrow::Cow<'a, str>> + 'a,
|
||||||
@@ -977,12 +1025,14 @@ impl App {
|
|||||||
) -> widget::Text<'a, cosmic::Theme, cosmic::Renderer> {
|
) -> widget::Text<'a, cosmic::Theme, cosmic::Renderer> {
|
||||||
widget::text(text).size(size).font(self.font)
|
widget::text(text).size(size).font(self.font)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn note(&self, text: &str) -> Element<'static, Message> {
|
fn note(&self, text: &str) -> Element<'static, Message> {
|
||||||
let palette = visuals::Palette::from(&self.draft);
|
let palette = visuals::Palette::from(&self.draft);
|
||||||
self.label(text.to_owned(), 12.)
|
self.label(text.to_owned(), 12.)
|
||||||
.class(cosmic::theme::Text::Color(palette.muted))
|
.class(cosmic::theme::Text::Color(palette.muted))
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn field(&self, field: Field) -> Element<'static, Message> {
|
fn field(&self, field: Field) -> Element<'static, Message> {
|
||||||
let palette = visuals::Palette::from(&self.draft);
|
let palette = visuals::Palette::from(&self.draft);
|
||||||
let mut labels = column([])
|
let mut labels = column([])
|
||||||
@@ -1096,6 +1146,7 @@ impl App {
|
|||||||
.class(visuals::surface(palette.card, 0.))
|
.class(visuals::surface(palette.card, 0.))
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn change(&mut self, edit: Edit) -> Task<Message> {
|
fn change(&mut self, edit: Edit) -> Task<Message> {
|
||||||
match self.draft.edit(&edit) {
|
match self.draft.edit(&edit) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
@@ -1122,6 +1173,7 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn flush(&mut self) -> Task<Message> {
|
fn flush(&mut self) -> Task<Message> {
|
||||||
if self.saving || self.pending.is_empty() {
|
if self.saving || self.pending.is_empty() {
|
||||||
return Task::none();
|
return Task::none();
|
||||||
@@ -1132,6 +1184,7 @@ impl App {
|
|||||||
}
|
}
|
||||||
self.start_save(self.draft.source.clone())
|
self.start_save(self.draft.source.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start_save(&mut self, desired: String) -> Task<Message> {
|
fn start_save(&mut self, desired: String) -> Task<Message> {
|
||||||
self.saving = true;
|
self.saving = true;
|
||||||
self.saving_previous = self.current.source.clone();
|
self.saving_previous = self.current.source.clone();
|
||||||
@@ -1164,12 +1217,13 @@ mod tests {
|
|||||||
App::init(
|
App::init(
|
||||||
Core::default(),
|
Core::default(),
|
||||||
(
|
(
|
||||||
PathBuf::from("/unused/settings-test.toml"),
|
PathBuf::from("/unused/settings-test.kdl"),
|
||||||
Snapshot::parse(String::new()),
|
Snapshot::parse(String::new()),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.0
|
.0
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn notes_save_only_the_latest_revision_and_preserve_multiline_text() {
|
fn notes_save_only_the_latest_revision_and_preserve_multiline_text() {
|
||||||
let mut app = app();
|
let mut app = app();
|
||||||
@@ -1210,11 +1264,12 @@ mod tests {
|
|||||||
app.sync_notes();
|
app.sync_notes();
|
||||||
app.note_editors.get_mut("test").unwrap().dirty = true;
|
app.note_editors.get_mut("test").unwrap().dirty = true;
|
||||||
let _ = app.update(Message::ExternalConfig(Snapshot::parse(
|
let _ = app.update(Message::ExternalConfig(Snapshot::parse(
|
||||||
"[animations]\nspeed = 0.5".into(),
|
"animations {\n speed 0.5\n}\n".into(),
|
||||||
)));
|
)));
|
||||||
assert!(app.note_editors.contains_key("test"));
|
assert!(app.note_editors.contains_key("test"));
|
||||||
assert!(app.status.contains("externally"));
|
assert!(app.status.contains("externally"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rapid_edits_are_serialized_and_not_lost_when_a_save_finishes() {
|
fn rapid_edits_are_serialized_and_not_lost_when_a_save_finishes() {
|
||||||
let mut app = app();
|
let mut app = app();
|
||||||
@@ -1230,24 +1285,26 @@ mod tests {
|
|||||||
assert_eq!(app.draft.number("layout.inner_gap", 0.), 6.);
|
assert_eq!(app.draft.number("layout.inner_gap", 0.), 6.);
|
||||||
assert_eq!(app.draft.number("animations.speed", 0.), 0.75);
|
assert_eq!(app.draft.number("animations.speed", 0.), 0.75);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn external_config_refreshes_idle_settings_but_preserves_active_edits() {
|
fn external_config_refreshes_idle_settings_but_preserves_active_edits() {
|
||||||
let mut app = app();
|
let mut app = app();
|
||||||
let external = Snapshot::parse("[animations]\nspeed = 0.5".into()).unwrap();
|
let external = Snapshot::parse("animations {\n speed 0.5\n}\n".into()).unwrap();
|
||||||
let _ = app.update(Message::ExternalConfig(Ok(external.clone())));
|
let _ = app.update(Message::ExternalConfig(Ok(external.clone())));
|
||||||
assert_eq!(app.current.source, external.source);
|
assert_eq!(app.current.source, external.source);
|
||||||
assert_eq!(app.draft.number("animations.speed", 0.), 0.5);
|
assert_eq!(app.draft.number("animations.speed", 0.), 0.5);
|
||||||
app.inputs
|
app.inputs
|
||||||
.insert("theme.colors.accent".into(), "#ffffff".into());
|
.insert("theme.colors.accent".into(), "#ffffff".into());
|
||||||
let newer = Snapshot::parse("[animations]\nspeed = 0.8".into()).unwrap();
|
let newer = Snapshot::parse("animations {\n speed 0.8\n}\n".into()).unwrap();
|
||||||
let _ = app.update(Message::ExternalConfig(Ok(newer)));
|
let _ = app.update(Message::ExternalConfig(Ok(newer)));
|
||||||
assert_eq!(app.current.source, external.source);
|
assert_eq!(app.current.source, external.source);
|
||||||
assert_eq!(app.inputs["theme.colors.accent"], "#ffffff");
|
assert_eq!(app.inputs["theme.colors.accent"], "#ffffff");
|
||||||
assert!(app.status.contains("externally"));
|
assert!(app.status.contains("externally"));
|
||||||
let _ = app.update(Message::ExternalConfig(Err("invalid TOML".into())));
|
let _ = app.update(Message::ExternalConfig(Err("invalid KDL".into())));
|
||||||
assert_eq!(app.current.source, external.source);
|
assert_eq!(app.current.source, external.source);
|
||||||
assert!(app.error.unwrap().contains("invalid TOML"));
|
assert!(app.error.unwrap().contains("invalid KDL"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn settings_has_no_native_headerbar() {
|
fn settings_has_no_native_headerbar() {
|
||||||
assert!(!app().core.window.show_headerbar);
|
assert!(!app().core.window.show_headerbar);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ pub enum Page {
|
|||||||
Startup,
|
Startup,
|
||||||
Displays,
|
Displays,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Page {
|
impl Page {
|
||||||
pub const ALL: [Self; 10] = [
|
pub const ALL: [Self; 10] = [
|
||||||
Self::Appearance,
|
Self::Appearance,
|
||||||
@@ -24,6 +25,7 @@ impl Page {
|
|||||||
Self::Startup,
|
Self::Startup,
|
||||||
Self::Displays,
|
Self::Displays,
|
||||||
];
|
];
|
||||||
|
|
||||||
pub fn title(self) -> &'static str {
|
pub fn title(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::Appearance => "Appearance",
|
Self::Appearance => "Appearance",
|
||||||
@@ -38,6 +40,7 @@ impl Page {
|
|||||||
Self::Displays => "Displays",
|
Self::Displays => "Displays",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn subtitle(self) -> &'static str {
|
pub fn subtitle(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::Appearance => "A desktop that feels like yours.",
|
Self::Appearance => "A desktop that feels like yours.",
|
||||||
@@ -52,6 +55,7 @@ impl Page {
|
|||||||
Self::Displays => "A place for every screen.",
|
Self::Displays => "A place for every screen.",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn icon(self) -> &'static str {
|
pub fn icon(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::Appearance => {
|
Self::Appearance => {
|
||||||
@@ -72,6 +76,7 @@ impl Page {
|
|||||||
Self::Displays => "M3 4h18v13H3z M12 17v4 M8 21h8",
|
Self::Displays => "M3 4h18v13H3z M12 17v4 M8 21h8",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn matches(self, query: &str) -> bool {
|
pub fn matches(self, query: &str) -> bool {
|
||||||
let query = query.to_lowercase();
|
let query = query.to_lowercase();
|
||||||
self.title().to_lowercase().contains(&query)
|
self.title().to_lowercase().contains(&query)
|
||||||
@@ -103,6 +108,7 @@ pub enum Kind {
|
|||||||
argv: bool,
|
argv: bool,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Field {
|
pub struct Field {
|
||||||
pub path: String,
|
pub path: String,
|
||||||
@@ -110,6 +116,7 @@ pub struct Field {
|
|||||||
pub description: String,
|
pub description: String,
|
||||||
pub kind: Kind,
|
pub kind: Kind,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Field {
|
impl Field {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
path: impl Into<String>,
|
path: impl Into<String>,
|
||||||
@@ -125,9 +132,11 @@ impl Field {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn toggle(path: &str, label: &str, description: &str, default: bool) -> Field {
|
fn toggle(path: &str, label: &str, description: &str, default: bool) -> Field {
|
||||||
Field::new(path, label, description, Kind::Toggle(default))
|
Field::new(path, label, description, Kind::Toggle(default))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn range(
|
pub fn range(
|
||||||
path: impl Into<String>,
|
path: impl Into<String>,
|
||||||
label: impl Into<String>,
|
label: impl Into<String>,
|
||||||
@@ -153,6 +162,7 @@ pub fn range(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn text(
|
pub fn text(
|
||||||
path: impl Into<String>,
|
path: impl Into<String>,
|
||||||
label: impl Into<String>,
|
label: impl Into<String>,
|
||||||
@@ -169,6 +179,7 @@ pub fn text(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn choice(
|
fn choice(
|
||||||
path: &str,
|
path: &str,
|
||||||
label: &str,
|
label: &str,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use ferese_config::Document;
|
||||||
|
use serde_json::Value;
|
||||||
use std::{
|
use std::{
|
||||||
fs,
|
fs,
|
||||||
io::{Read, Write},
|
io::{Read, Write},
|
||||||
@@ -5,23 +7,24 @@ use std::{
|
|||||||
process::{Command, Stdio},
|
process::{Command, Stdio},
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
use toml_edit::{DocumentMut, Item, Value};
|
|
||||||
|
|
||||||
pub const LIMIT: usize = 60 * 1024;
|
pub const LIMIT: usize = 60 * 1024;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Snapshot {
|
pub struct Snapshot {
|
||||||
pub source: String,
|
pub source: String,
|
||||||
pub doc: DocumentMut,
|
pub doc: Document,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Snapshot {
|
impl Snapshot {
|
||||||
pub fn parse(source: String) -> Result<Self, String> {
|
pub fn parse(source: String) -> Result<Self, String> {
|
||||||
if source.len() > LIMIT {
|
if source.len() > LIMIT {
|
||||||
return Err("Configuration exceeds the 60 KiB limit.".into());
|
return Err("Configuration exceeds the 60 KiB limit.".into());
|
||||||
}
|
}
|
||||||
let doc = source.parse::<DocumentMut>().map_err(|e| e.to_string())?;
|
let doc = Document::parse(&source).map_err(|e| e.to_string())?;
|
||||||
Ok(Self { source, doc })
|
Ok(Self { source, doc })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read(path: &Path) -> Result<Self, String> {
|
pub fn read(path: &Path) -> Result<Self, String> {
|
||||||
let source = match fs::File::open(path) {
|
let source = match fs::File::open(path) {
|
||||||
Ok(file) => {
|
Ok(file) => {
|
||||||
@@ -36,39 +39,35 @@ impl Snapshot {
|
|||||||
};
|
};
|
||||||
Self::parse(source)
|
Self::parse(source)
|
||||||
}
|
}
|
||||||
pub fn item(&self, path: &str) -> Option<&Item> {
|
|
||||||
let mut item = self.doc.as_item();
|
pub fn item(&self, path: &str) -> Option<&Value> {
|
||||||
for key in path.split('.') {
|
self.doc.get(path)
|
||||||
item = if let Ok(index) = key.parse::<usize>() {
|
|
||||||
item.get(index)?
|
|
||||||
} else {
|
|
||||||
item.get(key)?
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Some(item)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn string(&self, path: &str, fallback: &str) -> String {
|
pub fn string(&self, path: &str, fallback: &str) -> String {
|
||||||
self.item(path)
|
self.item(path)
|
||||||
.and_then(Item::as_str)
|
.and_then(Value::as_str)
|
||||||
.unwrap_or(fallback)
|
.unwrap_or(fallback)
|
||||||
.to_owned()
|
.to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn number(&self, path: &str, fallback: f64) -> f64 {
|
pub fn number(&self, path: &str, fallback: f64) -> f64 {
|
||||||
self.item(path)
|
self.item(path).and_then(|v| v.as_f64()).unwrap_or(fallback)
|
||||||
.and_then(|v| v.as_float().or_else(|| v.as_integer().map(|v| v as f64)))
|
|
||||||
.unwrap_or(fallback)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn boolean(&self, path: &str, fallback: bool) -> bool {
|
pub fn boolean(&self, path: &str, fallback: bool) -> bool {
|
||||||
self.item(path).and_then(Item::as_bool).unwrap_or(fallback)
|
self.item(path).and_then(Value::as_bool).unwrap_or(fallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn records(&self, path: &str) -> usize {
|
pub fn records(&self, path: &str) -> usize {
|
||||||
self.item(path)
|
self.item(path)
|
||||||
.and_then(Item::as_array_of_tables)
|
.and_then(Value::as_array)
|
||||||
.map_or(0, |a| a.len())
|
.map_or(0, |a| a.len())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn argv(&self, path: &str) -> String {
|
pub fn argv(&self, path: &str) -> String {
|
||||||
self.item(path)
|
self.item(path)
|
||||||
.and_then(Item::as_array)
|
.and_then(Value::as_array)
|
||||||
.map(|a| {
|
.map(|a| {
|
||||||
a.iter()
|
a.iter()
|
||||||
.filter_map(Value::as_str)
|
.filter_map(Value::as_str)
|
||||||
@@ -82,79 +81,26 @@ impl Snapshot {
|
|||||||
})
|
})
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn edit(&mut self, edit: &Edit) -> Result<(), String> {
|
pub fn edit(&mut self, edit: &Edit) -> Result<(), String> {
|
||||||
match edit {
|
match edit {
|
||||||
Edit::Set(path, value) => {
|
Edit::Set(path, value) => self.doc.set(path, value.clone()),
|
||||||
let mut item = self.doc.as_item_mut();
|
Edit::Add(path, fields) => self.doc.add(path, fields.clone()),
|
||||||
for key in path.split('.') {
|
Edit::Remove(path, index) => self.doc.remove(path, *index),
|
||||||
if let Ok(index) = key.parse::<usize>() {
|
|
||||||
item = item
|
|
||||||
.get_mut(index)
|
|
||||||
.ok_or("This item no longer exists. Reload settings.")?;
|
|
||||||
} else {
|
|
||||||
if item.is_none() {
|
|
||||||
*item = Item::Table(toml_edit::Table::new());
|
|
||||||
}
|
|
||||||
let table = item
|
|
||||||
.as_table_like_mut()
|
|
||||||
.ok_or("This setting is not a table.")?;
|
|
||||||
item = table.entry(key).or_insert(Item::None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let mut value = value.clone();
|
|
||||||
if let Some(previous) = item.as_value() {
|
|
||||||
*value.decor_mut() = previous.decor().clone();
|
|
||||||
}
|
|
||||||
*item = Item::Value(value);
|
|
||||||
}
|
|
||||||
Edit::Add(table, fields) => {
|
|
||||||
let item = nested_item_mut(self.doc.as_item_mut(), table)?;
|
|
||||||
if item.is_none() {
|
|
||||||
*item = Item::ArrayOfTables(Default::default());
|
|
||||||
}
|
|
||||||
let array = item
|
|
||||||
.as_array_of_tables_mut()
|
|
||||||
.ok_or("Expected a list of settings.")?;
|
|
||||||
let mut record = toml_edit::Table::new();
|
|
||||||
for (key, value) in fields {
|
|
||||||
record[key] = Item::Value(value.clone());
|
|
||||||
}
|
|
||||||
array.push(record);
|
|
||||||
}
|
|
||||||
Edit::Remove(table, index) => {
|
|
||||||
let array = nested_item_mut(self.doc.as_item_mut(), table)?
|
|
||||||
.as_array_of_tables_mut()
|
|
||||||
.ok_or("This list no longer exists.")?;
|
|
||||||
if *index >= array.len() {
|
|
||||||
return Err("This item no longer exists. Reload settings.".into());
|
|
||||||
}
|
|
||||||
array.remove(*index);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
self.source = self.doc.to_string();
|
self.source = self.doc.to_string();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn nested_item_mut<'a>(mut item: &'a mut Item, path: &str) -> Result<&'a mut Item, String> {
|
|
||||||
for key in path.split('.') {
|
|
||||||
if item.is_none() {
|
|
||||||
*item = Item::Table(toml_edit::Table::new());
|
|
||||||
}
|
|
||||||
let table = item
|
|
||||||
.as_table_like_mut()
|
|
||||||
.ok_or("Expected a settings table.")?;
|
|
||||||
item = table.entry(key).or_insert(Item::None);
|
|
||||||
}
|
|
||||||
Ok(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum Edit {
|
pub enum Edit {
|
||||||
Set(String, Value),
|
Set(String, Value),
|
||||||
Add(String, Vec<(String, Value)>),
|
Add(String, Vec<(String, Value)>),
|
||||||
Remove(String, usize),
|
Remove(String, usize),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set(path: &str, value: impl Into<Value>) -> Edit {
|
pub fn set(path: &str, value: impl Into<Value>) -> Edit {
|
||||||
Edit::Set(path.to_owned(), value.into())
|
Edit::Set(path.to_owned(), value.into())
|
||||||
}
|
}
|
||||||
@@ -164,7 +110,7 @@ pub fn config_path() -> PathBuf {
|
|||||||
.map(PathBuf::from)
|
.map(PathBuf::from)
|
||||||
.or_else(|| std::env::var_os("HOME").map(|home| PathBuf::from(home).join(".config")))
|
.or_else(|| std::env::var_os("HOME").map(|home| PathBuf::from(home).join(".config")))
|
||||||
.unwrap_or_else(|| PathBuf::from("."));
|
.unwrap_or_else(|| PathBuf::from("."));
|
||||||
let path = directory.join("ferese/config.toml");
|
let path = directory.join("ferese/config.kdl");
|
||||||
path.canonicalize().unwrap_or(path)
|
path.canonicalize().unwrap_or(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,6 +133,7 @@ fn validate(path: &Path) -> Result<(), String> {
|
|||||||
.spawn()
|
.spawn()
|
||||||
.map_err(|e| format!("Cannot validate configuration: {e}"))?;
|
.map_err(|e| format!("Cannot validate configuration: {e}"))?;
|
||||||
let deadline = Instant::now() + Duration::from_secs(5);
|
let deadline = Instant::now() + Duration::from_secs(5);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if let Some(status) = child.try_wait().map_err(|e| e.to_string())? {
|
if let Some(status) = child.try_wait().map_err(|e| e.to_string())? {
|
||||||
return if status.success() {
|
return if status.success() {
|
||||||
@@ -199,11 +146,13 @@ fn validate(path: &Path) -> Result<(), String> {
|
|||||||
.collect())
|
.collect())
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if Instant::now() >= deadline {
|
if Instant::now() >= deadline {
|
||||||
let _ = child.kill();
|
let _ = child.kill();
|
||||||
let _ = child.wait();
|
let _ = child.wait();
|
||||||
return Err("Configuration validation timed out.".into());
|
return Err("Configuration validation timed out.".into());
|
||||||
}
|
}
|
||||||
|
|
||||||
std::thread::sleep(Duration::from_millis(20));
|
std::thread::sleep(Duration::from_millis(20));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -213,6 +162,7 @@ fn validate(path: &Path) -> Result<(), String> {
|
|||||||
pub fn save(path: &Path, expected: &str, desired: &str) -> Result<Snapshot, String> {
|
pub fn save(path: &Path, expected: &str, desired: &str) -> Result<Snapshot, String> {
|
||||||
save_with(path, expected, desired, validate)
|
save_with(path, expected, desired, validate)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn save_with(
|
fn save_with(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
expected: &str,
|
expected: &str,
|
||||||
@@ -251,7 +201,7 @@ fn save_with(
|
|||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
backup.as_file().sync_all().map_err(|e| e.to_string())?;
|
backup.as_file().sync_all().map_err(|e| e.to_string())?;
|
||||||
backup
|
backup
|
||||||
.persist(path.with_extension("toml.settings-backup"))
|
.persist(path.with_extension("kdl.settings-backup"))
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
staged.persist(path).map_err(|e| e.to_string())?;
|
staged.persist(path).map_err(|e| e.to_string())?;
|
||||||
Ok(snapshot)
|
Ok(snapshot)
|
||||||
@@ -285,53 +235,68 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
#[test]
|
#[test]
|
||||||
fn edits_preserve_comments_custom_keys_and_other_rules() {
|
fn edits_preserve_comments_custom_keys_and_other_rules() {
|
||||||
let source = "# personal theme\n[animations]\nspeed = 0.75 # keep this\n[custom]\nfuture = true\n[[window_rules]]\napp_id = 'mine'\nfloating = true\n";
|
let source = r#"// personal theme
|
||||||
|
// keep this
|
||||||
|
animations {
|
||||||
|
speed 0.75
|
||||||
|
}
|
||||||
|
custom {
|
||||||
|
future #true
|
||||||
|
}
|
||||||
|
window-rule app-id="mine" floating=#true
|
||||||
|
"#;
|
||||||
let mut snapshot = Snapshot::parse(source.into()).unwrap();
|
let mut snapshot = Snapshot::parse(source.into()).unwrap();
|
||||||
snapshot.edit(&set("animations.speed", 0.8)).unwrap();
|
snapshot.edit(&set("animations.speed", 0.8)).unwrap();
|
||||||
assert!(snapshot.source.contains("# keep this"));
|
assert!(snapshot.source.contains("// keep this"));
|
||||||
assert!(snapshot.source.contains("# personal theme"));
|
assert!(snapshot.source.contains("// personal theme"));
|
||||||
assert_eq!(snapshot.string("window_rules.0.app_id", ""), "mine");
|
assert_eq!(snapshot.string("window_rules.0.app_id", ""), "mine");
|
||||||
assert!(snapshot.boolean("custom.future", false));
|
assert!(snapshot.boolean("custom.future", false));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rejects_conflicts_and_failed_validation_without_overwriting() {
|
fn rejects_conflicts_and_failed_validation_without_overwriting() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
let path = dir.path().join("config.toml");
|
let path = dir.path().join("config.kdl");
|
||||||
fs::write(&path, "# external edit\n").unwrap();
|
fs::write(&path, "// external edit\n").unwrap();
|
||||||
assert!(save_with(&path, "", "[animations]\nspeed=1.0", |_| Ok(())).is_err());
|
assert!(save_with(&path, "", "animations {\n speed 1.0\n}\n", |_| Ok(())).is_err());
|
||||||
assert!(
|
assert!(
|
||||||
save_with(
|
save_with(
|
||||||
&path,
|
&path,
|
||||||
"# external edit\n",
|
"// external edit\n",
|
||||||
"[animations]\nspeed=1.0",
|
"animations {\n speed 1.0\n}\n",
|
||||||
|_| Err("invalid".into())
|
|_| Err("invalid".into())
|
||||||
)
|
)
|
||||||
.is_err()
|
.is_err()
|
||||||
);
|
);
|
||||||
assert_eq!(fs::read_to_string(&path).unwrap(), "# external edit\n");
|
assert_eq!(fs::read_to_string(&path).unwrap(), "// external edit\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn catches_edits_during_validation_and_keeps_a_backup_on_success() {
|
fn catches_edits_during_validation_and_keeps_a_backup_on_success() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
let path = dir.path().join("config.toml");
|
let path = dir.path().join("config.kdl");
|
||||||
fs::write(&path, "# initial\n").unwrap();
|
fs::write(&path, "// initial\n").unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
save_with(&path, "# initial\n", "# desired\n", |_| {
|
save_with(&path, "// initial\n", "// desired\n", |_| {
|
||||||
fs::write(&path, "# another editor\n").unwrap();
|
fs::write(&path, "// another editor\n").unwrap();
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.is_err()
|
.is_err()
|
||||||
);
|
);
|
||||||
save_with(&path, "# another editor\n", "# desired\n", |_| Ok(())).unwrap();
|
save_with(&path, "// another editor\n", "// desired\n", |_| Ok(())).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
fs::read_to_string(path.with_extension("toml.settings-backup")).unwrap(),
|
fs::read_to_string(path.with_extension("kdl.settings-backup")).unwrap(),
|
||||||
"# another editor\n"
|
"// another editor\n"
|
||||||
);
|
);
|
||||||
assert_eq!(fs::read_to_string(path).unwrap(), "# desired\n");
|
assert_eq!(fs::read_to_string(path).unwrap(), "// desired\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn nested_array_edits_and_login_items_roundtrip() {
|
fn nested_array_edits_and_login_items_roundtrip() {
|
||||||
let mut snapshot = Snapshot::parse("[[output_profiles]]\nname='desk'\n[[output_profiles.outputs]]\nmatch='HDMI-A-1'\nscale=1.5\n".into()).unwrap();
|
let mut snapshot = Snapshot::parse(
|
||||||
|
"output-profile name=\"desk\" {\n output match=\"HDMI-A-1\" scale=1.5\n}\n".into(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
snapshot
|
snapshot
|
||||||
.edit(&set("output_profiles.0.outputs.0.scale", 1.75))
|
.edit(&set("output_profiles.0.outputs.0.scale", 1.75))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -347,7 +312,8 @@ mod tests {
|
|||||||
"command".into(),
|
"command".into(),
|
||||||
["program", "argument with space"]
|
["program", "argument with space"]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.collect::<toml_edit::Array>()
|
.map(Value::from)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
.into(),
|
.into(),
|
||||||
),
|
),
|
||||||
("enabled".into(), false.into()),
|
("enabled".into(), false.into()),
|
||||||
@@ -362,10 +328,13 @@ mod tests {
|
|||||||
snapshot.edit(&Edit::Remove("autostart".into(), 0)).unwrap();
|
snapshot.edit(&Edit::Remove("autostart".into(), 0)).unwrap();
|
||||||
assert_eq!(snapshot.records("autostart"), 0);
|
assert_eq!(snapshot.records("autostart"), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn nested_note_lists_keep_existing_clock_settings() {
|
fn nested_note_lists_keep_existing_clock_settings() {
|
||||||
let mut snapshot =
|
let mut snapshot = Snapshot::parse(
|
||||||
Snapshot::parse("[desktop_widgets.clock]\nenabled = true\n".into()).unwrap();
|
"desktop-widgets {\n clock {\n enabled #true\n }\n}\n".into(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
snapshot
|
snapshot
|
||||||
.edit(&Edit::Add(
|
.edit(&Edit::Add(
|
||||||
"desktop_widgets.notes".into(),
|
"desktop_widgets.notes".into(),
|
||||||
@@ -392,7 +361,7 @@ mod tests {
|
|||||||
fn all_gui_controls_and_presets_pass_compositor_validation() {
|
fn all_gui_controls_and_presets_pass_compositor_validation() {
|
||||||
let binary = std::env::var_os("FERESE_TEST_BINARY").expect("Set FERESE_TEST_BINARY");
|
let binary = std::env::var_os("FERESE_TEST_BINARY").expect("Set FERESE_TEST_BINARY");
|
||||||
let directory = tempfile::tempdir().unwrap();
|
let directory = tempfile::tempdir().unwrap();
|
||||||
let path = directory.path().join("config.toml");
|
let path = directory.path().join("config.kdl");
|
||||||
let validate = |snapshot: &Snapshot| {
|
let validate = |snapshot: &Snapshot| {
|
||||||
fs::write(&path, &snapshot.source).unwrap();
|
fs::write(&path, &snapshot.source).unwrap();
|
||||||
let result = Command::new(&binary)
|
let result = Command::new(&binary)
|
||||||
|
|||||||
@@ -20,17 +20,21 @@ pub struct Palette {
|
|||||||
pub muted: Color,
|
pub muted: Color,
|
||||||
pub error: Color,
|
pub error: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn color(value: &str, fallback: Color) -> Color {
|
pub fn color(value: &str, fallback: Color) -> Color {
|
||||||
let Some(value) = value.strip_prefix('#') else {
|
let Some(value) = value.strip_prefix('#') else {
|
||||||
return fallback;
|
return fallback;
|
||||||
};
|
};
|
||||||
|
|
||||||
if value.len() != 6 {
|
if value.len() != 6 {
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
u32::from_str_radix(value, 16)
|
u32::from_str_radix(value, 16)
|
||||||
.map(|rgb| Color::from_rgb8((rgb >> 16) as u8, (rgb >> 8) as u8, rgb as u8))
|
.map(|rgb| Color::from_rgb8((rgb >> 16) as u8, (rgb >> 8) as u8, rgb as u8))
|
||||||
.unwrap_or(fallback)
|
.unwrap_or(fallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Palette {
|
impl Palette {
|
||||||
pub fn from(snapshot: &Snapshot) -> Self {
|
pub fn from(snapshot: &Snapshot) -> Self {
|
||||||
let base = color(
|
let base = color(
|
||||||
@@ -57,6 +61,7 @@ impl Palette {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mix(a: Color, b: Color, t: f32) -> Color {
|
fn mix(a: Color, b: Color, t: f32) -> Color {
|
||||||
Color::from_rgb(
|
Color::from_rgb(
|
||||||
a.r + (b.r - a.r) * t,
|
a.r + (b.r - a.r) * t,
|
||||||
@@ -64,6 +69,7 @@ fn mix(a: Color, b: Color, t: f32) -> Color {
|
|||||||
a.b + (b.b - a.b) * t,
|
a.b + (b.b - a.b) * t,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hex(c: Color) -> String {
|
fn hex(c: Color) -> String {
|
||||||
format!(
|
format!(
|
||||||
"#{:02x}{:02x}{:02x}",
|
"#{:02x}{:02x}{:02x}",
|
||||||
@@ -72,6 +78,7 @@ fn hex(c: Color) -> String {
|
|||||||
(c.b * 255.) as u8
|
(c.b * 255.) as u8
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn surface(background: Color, radius: f32) -> theme::Container<'static> {
|
pub fn surface(background: Color, radius: f32) -> theme::Container<'static> {
|
||||||
theme::Container::custom(move |_| container::Style {
|
theme::Container::custom(move |_| container::Style {
|
||||||
background: Some(Background::Color(background)),
|
background: Some(Background::Color(background)),
|
||||||
@@ -82,12 +89,15 @@ pub fn surface(background: Color, radius: f32) -> theme::Container<'static> {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn button_style(p: Palette, selected: bool) -> theme::Button {
|
pub fn button_style(p: Palette, selected: bool) -> theme::Button {
|
||||||
styled_button(p, selected, false)
|
styled_button(p, selected, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn navigation_style(p: Palette, selected: bool) -> theme::Button {
|
pub fn navigation_style(p: Palette, selected: bool) -> theme::Button {
|
||||||
styled_button(p, selected, true)
|
styled_button(p, selected, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn input_style(p: Palette) -> theme::TextInput {
|
pub fn input_style(p: Palette) -> theme::TextInput {
|
||||||
let appearance = move |focused: bool, hovered: bool| cosmic::widget::text_input::Appearance {
|
let appearance = move |focused: bool, hovered: bool| cosmic::widget::text_input::Appearance {
|
||||||
background: mix(p.sidebar, p.card, if hovered { 0.65 } else { 0.4 }).into(),
|
background: mix(p.sidebar, p.card, if hovered { 0.65 } else { 0.4 }).into(),
|
||||||
@@ -110,6 +120,7 @@ pub fn input_style(p: Palette) -> theme::TextInput {
|
|||||||
disabled: Box::new(move |_| appearance(false, false)),
|
disabled: Box::new(move |_| appearance(false, false)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn styled_button(p: Palette, selected: bool, navigation: bool) -> theme::Button {
|
fn styled_button(p: Palette, selected: bool, navigation: bool) -> theme::Button {
|
||||||
let style = move |hover: bool| button::Style {
|
let style = move |hover: bool| button::Style {
|
||||||
background: Some(Background::Color(if selected {
|
background: Some(Background::Color(if selected {
|
||||||
@@ -131,6 +142,7 @@ fn styled_button(p: Palette, selected: bool, navigation: bool) -> theme::Button
|
|||||||
overlay: None,
|
overlay: None,
|
||||||
shadow_offset: Vector::ZERO,
|
shadow_offset: Vector::ZERO,
|
||||||
};
|
};
|
||||||
|
|
||||||
theme::Button::Custom {
|
theme::Button::Custom {
|
||||||
active: Box::new(move |_, _| style(false)),
|
active: Box::new(move |_, _| style(false)),
|
||||||
hovered: Box::new(move |_, _| style(true)),
|
hovered: Box::new(move |_, _| style(true)),
|
||||||
@@ -138,6 +150,7 @@ fn styled_button(p: Palette, selected: bool, navigation: bool) -> theme::Button
|
|||||||
disabled: Box::new(move |_| style(false)),
|
disabled: Box::new(move |_| style(false)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn native_theme(snapshot: Option<&Snapshot>) -> cosmic::Theme {
|
pub fn native_theme(snapshot: Option<&Snapshot>) -> cosmic::Theme {
|
||||||
let mut theme = cosmic::theme::COSMIC_DARK.clone();
|
let mut theme = cosmic::theme::COSMIC_DARK.clone();
|
||||||
if let Some(snapshot) = snapshot {
|
if let Some(snapshot) = snapshot {
|
||||||
@@ -149,6 +162,7 @@ pub fn native_theme(snapshot: Option<&Snapshot>) -> cosmic::Theme {
|
|||||||
}
|
}
|
||||||
cosmic::Theme::custom(std::sync::Arc::new(theme))
|
cosmic::Theme::custom(std::sync::Arc::new(theme))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn configured_font(snapshot: &Snapshot) -> cosmic::font::Font {
|
pub fn configured_font(snapshot: &Snapshot) -> cosmic::font::Font {
|
||||||
static FONTS: std::sync::OnceLock<
|
static FONTS: std::sync::OnceLock<
|
||||||
std::sync::Mutex<std::collections::HashMap<String, cosmic::font::Font>>,
|
std::sync::Mutex<std::collections::HashMap<String, cosmic::font::Font>>,
|
||||||
@@ -165,15 +179,18 @@ pub fn configured_font(snapshot: &Snapshot) -> cosmic::font::Font {
|
|||||||
fonts.insert(family, font);
|
fonts.insert(family, font);
|
||||||
font
|
font
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn icon(page: Page, tint: Color) -> svg_icon::Icon {
|
pub fn icon(page: Page, tint: Color) -> svg_icon::Icon {
|
||||||
svg_icon::from_svg_bytes(format!(r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="{}" fill="none" stroke="{}" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>"##, page.icon(),hex(tint)).into_bytes()).symbolic(false).icon().size(19)
|
svg_icon::from_svg_bytes(format!(r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="{}" fill="none" stroke="{}" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>"##, page.icon(),hex(tint)).into_bytes()).symbolic(false).icon().size(19)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn brand_icon() -> svg_icon::Icon {
|
pub fn brand_icon() -> svg_icon::Icon {
|
||||||
svg_icon::from_svg_bytes(include_bytes!("../../../packaging/icons/ferese.svg").as_slice())
|
svg_icon::from_svg_bytes(include_bytes!("../../../packaging/icons/ferese.svg").as_slice())
|
||||||
.symbolic(false)
|
.symbolic(false)
|
||||||
.icon()
|
.icon()
|
||||||
.size(32)
|
.size(32)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn action_icon(path: &str, tint: Color) -> svg_icon::Icon {
|
pub fn action_icon(path: &str, tint: Color) -> svg_icon::Icon {
|
||||||
svg_icon::from_svg_bytes(format!(r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="{path}" fill="none" stroke="{}" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>"##, hex(tint)).into_bytes())
|
svg_icon::from_svg_bytes(format!(r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="{path}" fill="none" stroke="{}" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>"##, hex(tint)).into_bytes())
|
||||||
.symbolic(false)
|
.symbolic(false)
|
||||||
@@ -199,6 +216,7 @@ pub fn preset(index: usize) -> Vec<Edit> {
|
|||||||
set("theme.focus_ring.gradient.to", end),
|
set("theme.focus_ring.gradient.to", end),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn preset_selected(snapshot: &Snapshot, index: usize) -> bool {
|
pub fn preset_selected(snapshot: &Snapshot, index: usize) -> bool {
|
||||||
let (accent, base, text, muted, _) = PRESETS[index];
|
let (accent, base, text, muted, _) = PRESETS[index];
|
||||||
[
|
[
|
||||||
@@ -214,6 +232,7 @@ pub fn preset_selected(snapshot: &Snapshot, index: usize) -> bool {
|
|||||||
.eq_ignore_ascii_case(value)
|
.eq_ignore_ascii_case(value)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn swatches(index: usize) -> Element<'static, Message> {
|
pub fn swatches(index: usize) -> Element<'static, Message> {
|
||||||
let (accent, base, _, _, end) = PRESETS[index];
|
let (accent, base, _, _, end) = PRESETS[index];
|
||||||
let mut row = widget::row([]).spacing(3);
|
let mut row = widget::row([]).spacing(3);
|
||||||
|
|||||||
@@ -65,22 +65,22 @@ mod tests {
|
|||||||
use cosmic::iced::futures::{StreamExt, pin_mut};
|
use cosmic::iced::futures::{StreamExt, pin_mut};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn watches_atomic_replacement_and_retains_subscription_after_bad_toml() {
|
fn watches_atomic_replacement_and_retains_subscription_after_bad_kdl() {
|
||||||
tokio::runtime::Builder::new_current_thread()
|
tokio::runtime::Builder::new_current_thread()
|
||||||
.enable_all()
|
.enable_all()
|
||||||
.build()
|
.build()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.block_on(async {
|
.block_on(async {
|
||||||
let directory = tempfile::tempdir().unwrap();
|
let directory = tempfile::tempdir().unwrap();
|
||||||
let path = directory.path().join("config.toml");
|
let path = directory.path().join("config.kdl");
|
||||||
std::fs::write(&path, "[animations]\nspeed = 1").unwrap();
|
std::fs::write(&path, "animations {\n speed 1\n}\n").unwrap();
|
||||||
let stream = changes(&path);
|
let stream = changes(&path);
|
||||||
pin_mut!(stream);
|
pin_mut!(stream);
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
stream.next().await,
|
stream.next().await,
|
||||||
Some(Message::ExternalConfig(Ok(_)))
|
Some(Message::ExternalConfig(Ok(_)))
|
||||||
));
|
));
|
||||||
for source in ["bad [", "[animations]\nspeed = 0.75"] {
|
for source in ["bad [", "animations {\n speed 0.75\n}\n"] {
|
||||||
let temporary = directory.path().join("replacement");
|
let temporary = directory.path().join("replacement");
|
||||||
std::fs::write(&temporary, source).unwrap();
|
std::fs::write(&temporary, source).unwrap();
|
||||||
std::fs::rename(temporary, &path).unwrap();
|
std::fs::rename(temporary, &path).unwrap();
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ license.workspace = true
|
|||||||
rust-version = "1.93"
|
rust-version = "1.93"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
ferese-config = { path = "../../crates/ferese-config" }
|
||||||
ferese-core = { path = "../../crates/ferese-core" }
|
ferese-core = { path = "../../crates/ferese-core" }
|
||||||
ferese-protocols = { path = "../../crates/ferese-protocols", default-features = false, features = ["client"] }
|
ferese-protocols = { path = "../../crates/ferese-protocols", default-features = false, features = ["client"] }
|
||||||
jiff.workspace = true
|
jiff.workspace = true
|
||||||
@@ -14,8 +15,6 @@ jiff.workspace = true
|
|||||||
libcosmic = { workspace = true, features = ["wgpu", "a11y"] }
|
libcosmic = { workspace = true, features = ["wgpu", "a11y"] }
|
||||||
iced_accessibility = { git = "https://github.com/pop-os/libcosmic", rev = "03d7dcb832bc6d15bc1bb07327a1aa426b2d5bc0" }
|
iced_accessibility = { git = "https://github.com/pop-os/libcosmic", rev = "03d7dcb832bc6d15bc1bb07327a1aa426b2d5bc0" }
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
toml.workspace = true
|
|
||||||
toml_edit = "0.25.15"
|
|
||||||
tempfile.workspace = true
|
tempfile.workspace = true
|
||||||
tokio = { version = "1", features = ["time", "rt"] }
|
tokio = { version = "1", features = ["time", "rt"] }
|
||||||
wayland-client.workspace = true
|
wayland-client.workspace = true
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
use std::env;
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
@@ -174,11 +173,13 @@ impl Default for BarConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct PopoverConfig {
|
struct PopoverConfig {
|
||||||
#[serde(default = "default_popover_opacity")]
|
#[serde(default = "default_popover_opacity")]
|
||||||
opacity: f32,
|
opacity: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for PopoverConfig {
|
impl Default for PopoverConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -186,6 +187,7 @@ impl Default for PopoverConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fn default_popover_opacity() -> f32 {
|
const fn default_popover_opacity() -> f32 {
|
||||||
0.96
|
0.96
|
||||||
}
|
}
|
||||||
@@ -299,13 +301,13 @@ pub(crate) fn load() -> ShellConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn parse_source(source: &str) -> Result<ShellConfig, toml::de::Error> {
|
pub(crate) fn parse_source(source: &str) -> Result<ShellConfig, ferese_config::Error> {
|
||||||
match toml::from_str::<FereseConfig>(source) {
|
match ferese_config::from_str::<FereseConfig>(source) {
|
||||||
Ok(config) => {
|
Ok(config) => {
|
||||||
config
|
config
|
||||||
.desktop_widgets
|
.desktop_widgets
|
||||||
.validate()
|
.validate()
|
||||||
.map_err(<toml::de::Error as serde::de::Error>::custom)?;
|
.map_err(ferese_config::Error::from)?;
|
||||||
let mut theme = shell_theme(&config.theme);
|
let mut theme = shell_theme(&config.theme);
|
||||||
theme.material_radius =
|
theme.material_radius =
|
||||||
nonnegative_or(config.appearance.corner_radius.unwrap_or(14.0), 14.0);
|
nonnegative_or(config.appearance.corner_radius.unwrap_or(14.0), 14.0);
|
||||||
@@ -415,48 +417,63 @@ fn finite_or(value: f32, fallback: f32) -> f32 {
|
|||||||
fn default_surface_base() -> String {
|
fn default_surface_base() -> String {
|
||||||
"#111821".to_owned()
|
"#111821".to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_text_primary() -> String {
|
fn default_text_primary() -> String {
|
||||||
"#F4F7FB".to_owned()
|
"#F4F7FB".to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_text_muted() -> String {
|
fn default_text_muted() -> String {
|
||||||
"#7F8A98".to_owned()
|
"#7F8A98".to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_accent() -> String {
|
fn default_accent() -> String {
|
||||||
"#5B8CFF".to_owned()
|
"#5B8CFF".to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_border() -> String {
|
fn default_border() -> String {
|
||||||
"#FFFFFF18".to_owned()
|
"#FFFFFF18".to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_shadow() -> String {
|
fn default_shadow() -> String {
|
||||||
"#00000055".to_owned()
|
"#00000055".to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
const fn default_bar_height() -> f32 {
|
const fn default_bar_height() -> f32 {
|
||||||
28.0
|
28.0
|
||||||
}
|
}
|
||||||
|
|
||||||
const fn default_bar_window_gap() -> i32 {
|
const fn default_bar_window_gap() -> i32 {
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
|
||||||
const fn default_bar_margin_top() -> i32 {
|
const fn default_bar_margin_top() -> i32 {
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
|
||||||
const fn default_bar_margin_horizontal() -> i32 {
|
const fn default_bar_margin_horizontal() -> i32 {
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
|
||||||
const fn default_bar_radius() -> f32 {
|
const fn default_bar_radius() -> f32 {
|
||||||
0.0
|
0.0
|
||||||
}
|
}
|
||||||
|
|
||||||
const fn default_panel_padding() -> f32 {
|
const fn default_panel_padding() -> f32 {
|
||||||
12.0
|
12.0
|
||||||
}
|
}
|
||||||
|
|
||||||
const fn default_control_gap() -> f32 {
|
const fn default_control_gap() -> f32 {
|
||||||
12.0
|
12.0
|
||||||
}
|
}
|
||||||
|
|
||||||
const fn default_shadow_offset_y() -> f32 {
|
const fn default_shadow_offset_y() -> f32 {
|
||||||
4.0
|
4.0
|
||||||
}
|
}
|
||||||
|
|
||||||
const fn default_shadow_blur() -> f32 {
|
const fn default_shadow_blur() -> f32 {
|
||||||
18.0
|
18.0
|
||||||
}
|
}
|
||||||
|
|
||||||
const fn default_shadow_opacity() -> f32 {
|
const fn default_shadow_opacity() -> f32 {
|
||||||
0.20
|
0.20
|
||||||
}
|
}
|
||||||
@@ -466,42 +483,59 @@ pub(crate) const fn default_background() -> [u8; 3] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn config_path() -> Option<PathBuf> {
|
pub(crate) fn config_path() -> Option<PathBuf> {
|
||||||
if let Some(directory) = env::var_os("XDG_CONFIG_HOME").filter(|value| !value.is_empty()) {
|
ferese_config::config_path()
|
||||||
return Some(PathBuf::from(directory).join("ferese/config.toml"));
|
|
||||||
}
|
|
||||||
|
|
||||||
env::var_os("HOME")
|
|
||||||
.filter(|value| !value.is_empty())
|
|
||||||
.map(PathBuf::from)
|
|
||||||
.map(|home| home.join(".config/ferese/config.toml"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn desktop_clock_parses_and_rejects_invalid_reload_values() {
|
fn desktop_clock_parses_and_rejects_invalid_reload_values() {
|
||||||
let clock = super::parse_source("[desktop_widgets.clock]\nenabled = true\nanchor = 'bottom_right'\nfont_family = ''\ncolor = ''\ntime_zone = ''").unwrap().desktop_widgets.clock;
|
let clock = super::parse_source(
|
||||||
|
r#"desktop-widgets {
|
||||||
|
clock {
|
||||||
|
enabled #true
|
||||||
|
anchor "bottom_right"
|
||||||
|
font-family ""
|
||||||
|
color ""
|
||||||
|
time-zone ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.desktop_widgets
|
||||||
|
.clock;
|
||||||
assert!(clock.enabled);
|
assert!(clock.enabled);
|
||||||
assert_eq!(clock.anchor, ferese_core::desktop::Anchor::BottomRight);
|
assert_eq!(clock.anchor, ferese_core::desktop::Anchor::BottomRight);
|
||||||
assert!(super::parse_source("[desktop_widgets.clock]\nopacity = 1.1").is_err());
|
assert!(
|
||||||
assert!(super::parse_source("[desktop_widgets.clock]\ntime_format = '%'").is_err());
|
super::parse_source("desktop-widgets {\n clock {\n opacity 1.1\n }\n}\n")
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
super::parse_source(
|
||||||
|
"desktop-widgets {\n clock {\n time-format \"%\"\n }\n}\n"
|
||||||
|
)
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parses_theme_without_rejecting_compositor_sections() {
|
fn parses_theme_without_rejecting_compositor_sections() {
|
||||||
let config: FereseConfig = toml::from_str(
|
let config: FereseConfig = ferese_config::from_str(
|
||||||
r#"
|
r#"layout {
|
||||||
[layout]
|
mode "scrolling"
|
||||||
mode = "scrolling"
|
}
|
||||||
|
theme {
|
||||||
[theme.typography]
|
typography {
|
||||||
font_family = "JetBrainsMono Nerd Font"
|
font-family "JetBrainsMono Nerd Font"
|
||||||
|
}
|
||||||
[theme.background]
|
background {
|
||||||
path = "/tmp/wallpaper.png"
|
path "/tmp/wallpaper.png"
|
||||||
mode = "fit"
|
mode "fit"
|
||||||
"#,
|
}
|
||||||
|
}
|
||||||
|
"#,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -518,7 +552,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn defaults_to_the_ferese_visual_profile() {
|
fn defaults_to_the_ferese_visual_profile() {
|
||||||
let config: FereseConfig = toml::from_str("").unwrap();
|
let config: FereseConfig = ferese_config::from_str("").unwrap();
|
||||||
|
|
||||||
assert_eq!(config.theme.typography.font_family, None);
|
assert_eq!(config.theme.typography.font_family, None);
|
||||||
assert_eq!(config.theme.background.path, None);
|
assert_eq!(config.theme.background.path, None);
|
||||||
@@ -535,13 +569,17 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn bar_palette_can_be_changed_without_changing_popovers() {
|
fn bar_palette_can_be_changed_without_changing_popovers() {
|
||||||
let config: FereseConfig = toml::from_str(
|
let config: FereseConfig = ferese_config::from_str(
|
||||||
r##"
|
r##"theme {
|
||||||
[theme.surface.bar]
|
surface {
|
||||||
background = "#EAECEEDD"
|
bar {
|
||||||
text_primary = "#222222"
|
background "#EAECEEDD"
|
||||||
text_muted = "#666666"
|
text-primary "#222222"
|
||||||
"##,
|
text-muted "#666666"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"##,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let theme = shell_theme(&config.theme);
|
let theme = shell_theme(&config.theme);
|
||||||
@@ -553,13 +591,20 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn bar_transparency_and_shadow_opacity_are_independent() {
|
fn bar_transparency_and_shadow_opacity_are_independent() {
|
||||||
let config: FereseConfig = toml::from_str(
|
let config: FereseConfig = ferese_config::from_str(
|
||||||
r#"
|
r#"theme {
|
||||||
[theme.surface.bar]
|
surface {
|
||||||
opacity = 0.6
|
bar {
|
||||||
[theme.shadow.soft]
|
opacity 0.6
|
||||||
opacity = 0.07
|
}
|
||||||
"#,
|
}
|
||||||
|
shadow {
|
||||||
|
soft {
|
||||||
|
opacity 0.07
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"#,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let theme = shell_theme(&config.theme);
|
let theme = shell_theme(&config.theme);
|
||||||
|
|||||||
@@ -689,6 +689,7 @@ impl FereseShell {
|
|||||||
}
|
}
|
||||||
Task::batch(tasks)
|
Task::batch(tasks)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn output_event(
|
fn output_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
event: wayland::OutputEvent,
|
event: wayland::OutputEvent,
|
||||||
@@ -2264,6 +2265,7 @@ impl EffectsBinding {
|
|||||||
self.connection.flush()?;
|
self.connection.flush()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_opacity(&self, opacity: f32) -> Result<(), Box<dyn std::error::Error>> {
|
fn set_opacity(&self, opacity: f32) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let opacity = (opacity.clamp(0.0, 1.0) * 1000.0).round() as u32;
|
let opacity = (opacity.clamp(0.0, 1.0) * 1000.0).round() as u32;
|
||||||
if self.surface.version() >= 3 && self.opacity.get() != Some(opacity) {
|
if self.surface.version() >= 3 && self.opacity.get() != Some(opacity) {
|
||||||
@@ -2349,6 +2351,7 @@ mod tests {
|
|||||||
Point::new(200., 100.)
|
Point::new(200., 100.)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn note_drag_origin_and_bounds_use_logical_output_coordinates() {
|
fn note_drag_origin_and_bounds_use_logical_output_coordinates() {
|
||||||
use cosmic::iced::Point;
|
use cosmic::iced::Point;
|
||||||
@@ -2371,6 +2374,7 @@ mod tests {
|
|||||||
Point::ORIGIN
|
Point::ORIGIN
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn desktop_clock_anchor_margins_match_only_anchored_edges() {
|
fn desktop_clock_anchor_margins_match_only_anchored_edges() {
|
||||||
use ferese_core::desktop::{Anchor as Position, Clock};
|
use ferese_core::desktop::{Anchor as Position, Clock};
|
||||||
@@ -2396,6 +2400,7 @@ mod tests {
|
|||||||
(0, 0, 0, 0)
|
(0, 0, 0, 0)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn clock_uses_lowercase_date_and_twelve_hour_time() {
|
fn clock_uses_lowercase_date_and_twelve_hour_time() {
|
||||||
for (stamp, expected) in [
|
for (stamp, expected) in [
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ pub(crate) struct Settings {
|
|||||||
reduced_motion: bool,
|
reduced_motion: bool,
|
||||||
speed: f64,
|
speed: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Settings {
|
impl Default for Settings {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -22,10 +23,12 @@ impl Default for Settings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Settings {
|
impl Settings {
|
||||||
fn hover_duration(self) -> std::time::Duration {
|
fn hover_duration(self) -> std::time::Duration {
|
||||||
self.duration(120.0)
|
self.duration(120.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn duration(self, milliseconds: f64) -> std::time::Duration {
|
pub(crate) fn duration(self, milliseconds: f64) -> std::time::Duration {
|
||||||
if !self.enabled || self.reduced_motion {
|
if !self.enabled || self.reduced_motion {
|
||||||
return std::time::Duration::ZERO;
|
return std::time::Duration::ZERO;
|
||||||
@@ -52,6 +55,7 @@ pub(crate) struct PopupMotion {
|
|||||||
started: Option<std::time::Instant>,
|
started: Option<std::time::Instant>,
|
||||||
settings: Settings,
|
settings: Settings,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PopupMotion {
|
impl PopupMotion {
|
||||||
pub(crate) fn update_settings(&mut self, settings: Settings) {
|
pub(crate) fn update_settings(&mut self, settings: Settings) {
|
||||||
let now = std::time::Instant::now();
|
let now = std::time::Instant::now();
|
||||||
@@ -59,6 +63,7 @@ impl PopupMotion {
|
|||||||
self.settings = settings;
|
self.settings = settings;
|
||||||
self.started = Some(now);
|
self.started = Some(now);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn new(settings: Settings) -> Self {
|
pub(crate) fn new(settings: Settings) -> Self {
|
||||||
Self {
|
Self {
|
||||||
start: 0.0,
|
start: 0.0,
|
||||||
@@ -67,11 +72,13 @@ impl PopupMotion {
|
|||||||
settings,
|
settings,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn begin(&mut self, now: std::time::Instant) {
|
pub(crate) fn begin(&mut self, now: std::time::Instant) {
|
||||||
if self.started.is_none() {
|
if self.started.is_none() {
|
||||||
self.started = Some(now);
|
self.started = Some(now);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn progress_at(&self, now: std::time::Instant) -> f32 {
|
pub(crate) fn progress_at(&self, now: std::time::Instant) -> f32 {
|
||||||
let duration = self
|
let duration = self
|
||||||
.settings
|
.settings
|
||||||
@@ -89,15 +96,19 @@ impl PopupMotion {
|
|||||||
}
|
}
|
||||||
self.start + (self.target - self.start) * t * t * (3.0 - 2.0 * t)
|
self.start + (self.target - self.start) * t * t * (3.0 - 2.0 * t)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn progress(&self) -> f32 {
|
pub(crate) fn progress(&self) -> f32 {
|
||||||
self.progress_at(std::time::Instant::now())
|
self.progress_at(std::time::Instant::now())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn closing(&self) -> bool {
|
pub(crate) fn closing(&self) -> bool {
|
||||||
self.target == 0.0
|
self.target == 0.0
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn animating(&self) -> bool {
|
pub(crate) fn animating(&self) -> bool {
|
||||||
self.progress() != self.target
|
self.progress() != self.target
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn retarget(&mut self, target: f32, now: std::time::Instant) {
|
pub(crate) fn retarget(&mut self, target: f32, now: std::time::Instant) {
|
||||||
self.start = self.progress_at(now);
|
self.start = self.progress_at(now);
|
||||||
self.target = target;
|
self.target = target;
|
||||||
@@ -157,6 +168,7 @@ struct HoverState {
|
|||||||
target: f32,
|
target: f32,
|
||||||
started: Option<std::time::Instant>,
|
started: Option<std::time::Instant>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HoverState {
|
impl HoverState {
|
||||||
fn advance(
|
fn advance(
|
||||||
&mut self,
|
&mut self,
|
||||||
@@ -185,11 +197,13 @@ impl HoverState {
|
|||||||
self.current != target
|
self.current != target
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Hover<'a, M> {
|
struct Hover<'a, M> {
|
||||||
content: Element<'a, M>,
|
content: Element<'a, M>,
|
||||||
progress: std::rc::Rc<std::cell::Cell<f32>>,
|
progress: std::rc::Rc<std::cell::Cell<f32>>,
|
||||||
duration: std::time::Duration,
|
duration: std::time::Duration,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<M> Widget<M, Theme, cosmic::Renderer> for Hover<'_, M> {
|
impl<M> Widget<M, Theme, cosmic::Renderer> for Hover<'_, M> {
|
||||||
fn a11y_nodes(
|
fn a11y_nodes(
|
||||||
&self,
|
&self,
|
||||||
@@ -201,21 +215,27 @@ impl<M> Widget<M, Theme, cosmic::Renderer> for Hover<'_, M> {
|
|||||||
.as_widget()
|
.as_widget()
|
||||||
.a11y_nodes(layout, &tree.children[0], cursor)
|
.a11y_nodes(layout, &tree.children[0], cursor)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tag(&self) -> widget::tree::Tag {
|
fn tag(&self) -> widget::tree::Tag {
|
||||||
widget::tree::Tag::of::<HoverState>()
|
widget::tree::Tag::of::<HoverState>()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn state(&self) -> widget::tree::State {
|
fn state(&self) -> widget::tree::State {
|
||||||
widget::tree::State::new(HoverState::default())
|
widget::tree::State::new(HoverState::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn children(&self) -> Vec<widget::Tree> {
|
fn children(&self) -> Vec<widget::Tree> {
|
||||||
vec![widget::Tree::new(&self.content)]
|
vec![widget::Tree::new(&self.content)]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn diff(&mut self, tree: &mut widget::Tree) {
|
fn diff(&mut self, tree: &mut widget::Tree) {
|
||||||
tree.diff_children(std::slice::from_mut(&mut self.content));
|
tree.diff_children(std::slice::from_mut(&mut self.content));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn size(&self) -> Size<Length> {
|
fn size(&self) -> Size<Length> {
|
||||||
self.content.as_widget().size()
|
self.content.as_widget().size()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn layout(
|
fn layout(
|
||||||
&mut self,
|
&mut self,
|
||||||
tree: &mut widget::Tree,
|
tree: &mut widget::Tree,
|
||||||
@@ -226,6 +246,7 @@ impl<M> Widget<M, Theme, cosmic::Renderer> for Hover<'_, M> {
|
|||||||
.as_widget_mut()
|
.as_widget_mut()
|
||||||
.layout(&mut tree.children[0], renderer, limits)
|
.layout(&mut tree.children[0], renderer, limits)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(
|
fn update(
|
||||||
&mut self,
|
&mut self,
|
||||||
tree: &mut widget::Tree,
|
tree: &mut widget::Tree,
|
||||||
@@ -255,6 +276,7 @@ impl<M> Widget<M, Theme, cosmic::Renderer> for Hover<'_, M> {
|
|||||||
viewport,
|
viewport,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw(
|
fn draw(
|
||||||
&self,
|
&self,
|
||||||
tree: &widget::Tree,
|
tree: &widget::Tree,
|
||||||
@@ -277,6 +299,7 @@ impl<M> Widget<M, Theme, cosmic::Renderer> for Hover<'_, M> {
|
|||||||
viewport,
|
viewport,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mouse_interaction(
|
fn mouse_interaction(
|
||||||
&self,
|
&self,
|
||||||
tree: &widget::Tree,
|
tree: &widget::Tree,
|
||||||
@@ -293,6 +316,7 @@ impl<M> Widget<M, Theme, cosmic::Renderer> for Hover<'_, M> {
|
|||||||
renderer,
|
renderer,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn operate(
|
fn operate(
|
||||||
&mut self,
|
&mut self,
|
||||||
tree: &mut widget::Tree,
|
tree: &mut widget::Tree,
|
||||||
@@ -304,6 +328,7 @@ impl<M> Widget<M, Theme, cosmic::Renderer> for Hover<'_, M> {
|
|||||||
.as_widget_mut()
|
.as_widget_mut()
|
||||||
.operate(&mut tree.children[0], layout, renderer, operation);
|
.operate(&mut tree.children[0], layout, renderer, operation);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn overlay<'a>(
|
fn overlay<'a>(
|
||||||
&'a mut self,
|
&'a mut self,
|
||||||
tree: &'a mut widget::Tree,
|
tree: &'a mut widget::Tree,
|
||||||
@@ -333,37 +358,46 @@ pub fn animated<'a, M: 'a>(
|
|||||||
regions,
|
regions,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Motion<'a, M> {
|
struct Motion<'a, M> {
|
||||||
content: Element<'a, M>,
|
content: Element<'a, M>,
|
||||||
progress: f32,
|
progress: f32,
|
||||||
regions: Regions,
|
regions: Regions,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<M> Motion<'_, M> {
|
impl<M> Motion<'_, M> {
|
||||||
fn translation(&self) -> Vector {
|
fn translation(&self) -> Vector {
|
||||||
Vector::new(0.0, -4.0 * (1.0 - self.progress))
|
Vector::new(0.0, -4.0 * (1.0 - self.progress))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cursor(&self, cursor: mouse::Cursor) -> mouse::Cursor {
|
fn cursor(&self, cursor: mouse::Cursor) -> mouse::Cursor {
|
||||||
cursor.position().map_or(mouse::Cursor::Unavailable, |p| {
|
cursor.position().map_or(mouse::Cursor::Unavailable, |p| {
|
||||||
mouse::Cursor::Available(p - self.translation())
|
mouse::Cursor::Available(p - self.translation())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<M> Widget<M, Theme, cosmic::Renderer> for Motion<'_, M> {
|
impl<M> Widget<M, Theme, cosmic::Renderer> for Motion<'_, M> {
|
||||||
fn tag(&self) -> widget::tree::Tag {
|
fn tag(&self) -> widget::tree::Tag {
|
||||||
self.content.as_widget().tag()
|
self.content.as_widget().tag()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn state(&self) -> widget::tree::State {
|
fn state(&self) -> widget::tree::State {
|
||||||
self.content.as_widget().state()
|
self.content.as_widget().state()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn children(&self) -> Vec<widget::Tree> {
|
fn children(&self) -> Vec<widget::Tree> {
|
||||||
self.content.as_widget().children()
|
self.content.as_widget().children()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn diff(&mut self, tree: &mut widget::Tree) {
|
fn diff(&mut self, tree: &mut widget::Tree) {
|
||||||
self.content.as_widget_mut().diff(tree);
|
self.content.as_widget_mut().diff(tree);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn size(&self) -> Size<Length> {
|
fn size(&self) -> Size<Length> {
|
||||||
self.content.as_widget().size()
|
self.content.as_widget().size()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn layout(
|
fn layout(
|
||||||
&mut self,
|
&mut self,
|
||||||
tree: &mut widget::Tree,
|
tree: &mut widget::Tree,
|
||||||
@@ -372,6 +406,7 @@ impl<M> Widget<M, Theme, cosmic::Renderer> for Motion<'_, M> {
|
|||||||
) -> layout::Node {
|
) -> layout::Node {
|
||||||
self.content.as_widget_mut().layout(tree, renderer, limits)
|
self.content.as_widget_mut().layout(tree, renderer, limits)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(
|
fn update(
|
||||||
&mut self,
|
&mut self,
|
||||||
tree: &mut widget::Tree,
|
tree: &mut widget::Tree,
|
||||||
@@ -417,6 +452,7 @@ impl<M> Widget<M, Theme, cosmic::Renderer> for Motion<'_, M> {
|
|||||||
tree, event, layout, cursor, renderer, clipboard, shell, viewport,
|
tree, event, layout, cursor, renderer, clipboard, shell, viewport,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw(
|
fn draw(
|
||||||
&self,
|
&self,
|
||||||
tree: &widget::Tree,
|
tree: &widget::Tree,
|
||||||
@@ -441,6 +477,7 @@ impl<M> Widget<M, Theme, cosmic::Renderer> for Motion<'_, M> {
|
|||||||
)
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mouse_interaction(
|
fn mouse_interaction(
|
||||||
&self,
|
&self,
|
||||||
tree: &widget::Tree,
|
tree: &widget::Tree,
|
||||||
@@ -457,6 +494,7 @@ impl<M> Widget<M, Theme, cosmic::Renderer> for Motion<'_, M> {
|
|||||||
renderer,
|
renderer,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn operate(
|
fn operate(
|
||||||
&mut self,
|
&mut self,
|
||||||
tree: &mut widget::Tree,
|
tree: &mut widget::Tree,
|
||||||
@@ -475,6 +513,7 @@ impl widget::Operation for CollectRegions {
|
|||||||
fn traverse(&mut self, children: &mut dyn FnMut(&mut dyn widget::Operation)) {
|
fn traverse(&mut self, children: &mut dyn FnMut(&mut dyn widget::Operation)) {
|
||||||
children(self);
|
children(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn container(&mut self, id: Option<&widget::Id>, bounds: Rectangle) {
|
fn container(&mut self, id: Option<&widget::Id>, bounds: Rectangle) {
|
||||||
if id == Some(&widget::Id::new("ferese-blur-card")) {
|
if id == Some(&widget::Id::new("ferese-blur-card")) {
|
||||||
let contains = |outer: &Rectangle, inner: &Rectangle| {
|
let contains = |outer: &Rectangle, inner: &Rectangle| {
|
||||||
@@ -524,6 +563,7 @@ mod tests {
|
|||||||
0.0
|
0.0
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn popup_reversal_and_motion_policy_are_consistent() {
|
fn popup_reversal_and_motion_policy_are_consistent() {
|
||||||
let now = std::time::Instant::now();
|
let now = std::time::Instant::now();
|
||||||
@@ -560,6 +600,7 @@ mod tests {
|
|||||||
assert_eq!(motion.progress_at(now), 0.0);
|
assert_eq!(motion.progress_at(now), 0.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn hover_fades_settle_and_idle_does_not_schedule_frames() {
|
fn hover_fades_settle_and_idle_does_not_schedule_frames() {
|
||||||
let now = std::time::Instant::now();
|
let now = std::time::Instant::now();
|
||||||
@@ -578,6 +619,7 @@ mod tests {
|
|||||||
assert!(!hover.advance(0.0, start + duration * 12, duration));
|
assert!(!hover.advance(0.0, start + duration * 12, duration));
|
||||||
assert_eq!(hover.current, 0.0);
|
assert_eq!(hover.current, 0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn hover_reversals_preserve_current_paint() {
|
fn hover_reversals_preserve_current_paint() {
|
||||||
let now = std::time::Instant::now();
|
let now = std::time::Instant::now();
|
||||||
@@ -591,6 +633,7 @@ mod tests {
|
|||||||
hover.advance(0.0, now + duration, duration);
|
hover.advance(0.0, now + duration, duration);
|
||||||
assert!(hover.current > 0.0 && hover.current < before);
|
assert!(hover.current > 0.0 && hover.current < before);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn hover_obeys_global_motion_policy() {
|
fn hover_obeys_global_motion_policy() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -617,6 +660,7 @@ mod tests {
|
|||||||
assert_eq!(hover.current, 1.0);
|
assert_eq!(hover.current, 1.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn material_regions_include_cards_but_not_the_parent_or_gaps() {
|
fn material_regions_include_cards_but_not_the_parent_or_gaps() {
|
||||||
let mut collector = CollectRegions(Vec::new());
|
let mut collector = CollectRegions(Vec::new());
|
||||||
|
|||||||
@@ -12,48 +12,45 @@ pub fn save(path: &Path, edits: &[Edit]) -> Result<(), String> {
|
|||||||
let canonical = path.canonicalize().map_err(|e| e.to_string())?;
|
let canonical = path.canonicalize().map_err(|e| e.to_string())?;
|
||||||
let path = canonical.as_path();
|
let path = canonical.as_path();
|
||||||
let source = fs::read_to_string(path).map_err(|e| e.to_string())?;
|
let source = fs::read_to_string(path).map_err(|e| e.to_string())?;
|
||||||
let mut doc: toml_edit::DocumentMut = source
|
let mut doc = ferese_config::Document::parse(&source).map_err(|e| e.to_string())?;
|
||||||
.parse()
|
|
||||||
.map_err(|e: toml_edit::TomlError| e.to_string())?;
|
|
||||||
for edit in edits {
|
for edit in edits {
|
||||||
if let Edit::ClockPosition(x, y) = edit {
|
if let Edit::ClockPosition(x, y) = edit {
|
||||||
let clock = doc
|
if doc.get("desktop_widgets.clock").is_none() {
|
||||||
.get_mut("desktop_widgets")
|
return Err("Clock was removed from the config.".into());
|
||||||
.and_then(|item| item.get_mut("clock"))
|
}
|
||||||
.ok_or("Clock was removed from the config.")?;
|
for (field, value) in [
|
||||||
clock["anchor"] = toml_edit::value("top_left");
|
("anchor", "top_left".into()),
|
||||||
clock["margin_x"] = toml_edit::value(i64::from(*x));
|
("margin_x", i64::from(*x).into()),
|
||||||
clock["margin_y"] = toml_edit::value(i64::from(*y));
|
("margin_y", i64::from(*y).into()),
|
||||||
|
] {
|
||||||
|
doc.set(&format!("desktop_widgets.clock.{field}"), value)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let notes = doc
|
|
||||||
.get_mut("desktop_widgets")
|
|
||||||
.and_then(|item| item.get_mut("notes"))
|
|
||||||
.and_then(toml_edit::Item::as_array_of_tables_mut)
|
|
||||||
.ok_or("Notes were removed from the config.")?;
|
|
||||||
let id = match edit {
|
let id = match edit {
|
||||||
Edit::Text(id, _) | Edit::Position(id, _, _) => id,
|
Edit::Text(id, _) | Edit::Position(id, _, _) => id,
|
||||||
Edit::ClockPosition(..) => unreachable!(),
|
Edit::ClockPosition(..) => unreachable!(),
|
||||||
};
|
};
|
||||||
let note = notes
|
let index = doc
|
||||||
.iter_mut()
|
.get("desktop_widgets.notes")
|
||||||
.find(|note| {
|
.and_then(|v| v.as_array())
|
||||||
note.get("id")
|
.ok_or("Notes were removed from the config.")?
|
||||||
.and_then(toml_edit::Item::as_str)
|
.iter()
|
||||||
.unwrap_or("note")
|
.position(|note| note.get("id").and_then(|v| v.as_str()).unwrap_or("note") == id)
|
||||||
== id
|
|
||||||
})
|
|
||||||
.ok_or("This note no longer exists in the config.")?;
|
.ok_or("This note no longer exists in the config.")?;
|
||||||
match edit {
|
let fields = match edit {
|
||||||
Edit::Text(_, value) => {
|
Edit::Text(_, value) => vec![("text", value.clone().into())],
|
||||||
note["text"] = toml_edit::value(value);
|
Edit::Position(_, x, y) => vec![
|
||||||
}
|
("anchor", "top_left".into()),
|
||||||
Edit::Position(_, x, y) => {
|
("margin_x", i64::from(*x).into()),
|
||||||
note["anchor"] = toml_edit::value("top_left");
|
("margin_y", i64::from(*y).into()),
|
||||||
note["margin_x"] = toml_edit::value(i64::from(*x));
|
],
|
||||||
note["margin_y"] = toml_edit::value(i64::from(*y));
|
|
||||||
}
|
|
||||||
Edit::ClockPosition(..) => unreachable!(),
|
Edit::ClockPosition(..) => unreachable!(),
|
||||||
|
};
|
||||||
|
for (field, value) in fields {
|
||||||
|
doc.set(&format!("desktop_widgets.notes.{index}.{field}"), value)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let updated = doc.to_string();
|
let updated = doc.to_string();
|
||||||
@@ -87,10 +84,10 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn clock_position_saves_without_a_notes_table() {
|
fn clock_position_saves_without_a_notes_table() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
let path = dir.path().join("config.toml");
|
let path = dir.path().join("config.kdl");
|
||||||
fs::write(
|
fs::write(
|
||||||
&path,
|
&path,
|
||||||
"[desktop_widgets.clock]\nenabled = true\ntime_size = 80\n",
|
"desktop-widgets {\n clock {\n enabled #true\n time-size 80\n }\n}\n",
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
save(&path, &[Edit::ClockPosition(120, 240)]).unwrap();
|
save(&path, &[Edit::ClockPosition(120, 240)]).unwrap();
|
||||||
@@ -104,11 +101,25 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(config.desktop_widgets.clock.time_size, 80.);
|
assert_eq!(config.desktop_widgets.clock.time_size, 80.);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn note_edits_preserve_unrelated_config_and_multiline_content() {
|
fn note_edits_preserve_unrelated_config_and_multiline_content() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
let path = dir.path().join("config.toml");
|
let path = dir.path().join("config.kdl");
|
||||||
fs::write(&path, "# keep this\n[theme.colors]\naccent = '#123456'\n[[desktop_widgets.notes]]\nid = 'a'\ntext = 'old'\n").unwrap();
|
fs::write(
|
||||||
|
&path,
|
||||||
|
r##"// keep this
|
||||||
|
theme {
|
||||||
|
colors {
|
||||||
|
accent "#123456"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
desktop-widgets {
|
||||||
|
note id="a" text="old"
|
||||||
|
}
|
||||||
|
"##,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
save(
|
save(
|
||||||
&path,
|
&path,
|
||||||
&[
|
&[
|
||||||
@@ -118,7 +129,7 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let source = fs::read_to_string(&path).unwrap();
|
let source = fs::read_to_string(&path).unwrap();
|
||||||
assert!(source.contains("# keep this"));
|
assert!(source.contains("// keep this"));
|
||||||
assert!(source.contains("#123456"));
|
assert!(source.contains("#123456"));
|
||||||
let notes = crate::config::parse_source(&source)
|
let notes = crate::config::parse_source(&source)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
|||||||
@@ -23,28 +23,33 @@ pub struct Snapshot {
|
|||||||
pub reboot: bool,
|
pub reboot: bool,
|
||||||
pub suspend: bool,
|
pub suspend: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Network {
|
pub struct Network {
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
pub connection: Option<String>,
|
pub connection: Option<String>,
|
||||||
pub signal: u8,
|
pub signal: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Bluetooth {
|
pub struct Bluetooth {
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
pub devices: Vec<String>,
|
pub devices: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Audio {
|
pub struct Audio {
|
||||||
pub volume: u8,
|
pub volume: u8,
|
||||||
pub muted: bool,
|
pub muted: bool,
|
||||||
pub output: String,
|
pub output: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Battery {
|
pub struct Battery {
|
||||||
pub percent: u8,
|
pub percent: u8,
|
||||||
pub status: String,
|
pub status: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Notifications {
|
pub struct Notifications {
|
||||||
pub count: u32,
|
pub count: u32,
|
||||||
@@ -65,22 +70,26 @@ pub enum Action {
|
|||||||
Reboot,
|
Reboot,
|
||||||
Suspend,
|
Suspend,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Action {
|
impl Action {
|
||||||
fn key(&self) -> std::mem::Discriminant<Self> {
|
fn key(&self) -> std::mem::Discriminant<Self> {
|
||||||
std::mem::discriminant(self)
|
std::mem::discriminant(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Update {
|
pub struct Update {
|
||||||
pub snapshot: Snapshot,
|
pub snapshot: Snapshot,
|
||||||
pub generation: u64,
|
pub generation: u64,
|
||||||
pub error: Option<String>,
|
pub error: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Service {
|
pub struct Service {
|
||||||
settings: Arc<Mutex<Option<Vec<String>>>>,
|
settings: Arc<Mutex<Option<Vec<String>>>>,
|
||||||
tx: SyncSender<(u64, Action)>,
|
tx: SyncSender<(u64, Action)>,
|
||||||
rx: Receiver<Update>,
|
rx: Receiver<Update>,
|
||||||
pub generation: u64,
|
pub generation: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Service {
|
impl Service {
|
||||||
pub fn start(settings: Option<Vec<String>>) -> Self {
|
pub fn start(settings: Option<Vec<String>>) -> Self {
|
||||||
let live_settings = Arc::new(Mutex::new(settings));
|
let live_settings = Arc::new(Mutex::new(settings));
|
||||||
@@ -153,6 +162,7 @@ impl Service {
|
|||||||
generation: 0,
|
generation: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send(&mut self, action: Action) -> Result<(), String> {
|
pub fn send(&mut self, action: Action) -> Result<(), String> {
|
||||||
let generation = self.generation + 1;
|
let generation = self.generation + 1;
|
||||||
self.tx
|
self.tx
|
||||||
@@ -161,9 +171,11 @@ impl Service {
|
|||||||
self.generation = generation;
|
self.generation = generation;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_settings(&self, settings: Option<Vec<String>>) {
|
pub fn update_settings(&self, settings: Option<Vec<String>>) {
|
||||||
*self.settings.lock().unwrap() = settings;
|
*self.settings.lock().unwrap() = settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn poll(&self) -> Option<Update> {
|
pub fn poll(&self) -> Option<Update> {
|
||||||
self.rx.try_iter().last()
|
self.rx.try_iter().last()
|
||||||
}
|
}
|
||||||
@@ -222,6 +234,7 @@ fn poll() -> Snapshot {
|
|||||||
suspend: can_power("CanSuspend"),
|
suspend: can_power("CanSuspend"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn network() -> Option<Network> {
|
fn network() -> Option<Network> {
|
||||||
// No Wi-Fi device means no Wi-Fi control (wired connectivity isn't called Wi-Fi).
|
// No Wi-Fi device means no Wi-Fi control (wired connectivity isn't called Wi-Fi).
|
||||||
let devices = run("nmcli", &["-t", "-f", "TYPE", "device", "status"]).ok()?;
|
let devices = run("nmcli", &["-t", "-f", "TYPE", "device", "status"]).ok()?;
|
||||||
@@ -261,6 +274,7 @@ fn network() -> Option<Network> {
|
|||||||
signal,
|
signal,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bluetooth() -> Option<Bluetooth> {
|
fn bluetooth() -> Option<Bluetooth> {
|
||||||
let info = run("bluetoothctl", &["show"]).ok()?;
|
let info = run("bluetoothctl", &["show"]).ok()?;
|
||||||
let enabled = info
|
let enabled = info
|
||||||
@@ -282,6 +296,7 @@ fn bluetooth() -> Option<Bluetooth> {
|
|||||||
};
|
};
|
||||||
Some(Bluetooth { enabled, devices })
|
Some(Bluetooth { enabled, devices })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_audio(value: &str) -> Option<(u8, bool)> {
|
pub fn parse_audio(value: &str) -> Option<(u8, bool)> {
|
||||||
let volume = value
|
let volume = value
|
||||||
.strip_prefix("Volume: ")?
|
.strip_prefix("Volume: ")?
|
||||||
@@ -297,6 +312,7 @@ pub fn parse_audio(value: &str) -> Option<(u8, bool)> {
|
|||||||
value.contains("[MUTED]"),
|
value.contains("[MUTED]"),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn audio() -> Option<Audio> {
|
fn audio() -> Option<Audio> {
|
||||||
let (volume, muted) =
|
let (volume, muted) =
|
||||||
parse_audio(&run("wpctl", &["get-volume", "@DEFAULT_AUDIO_SINK@"]).ok()?)?;
|
parse_audio(&run("wpctl", &["get-volume", "@DEFAULT_AUDIO_SINK@"]).ok()?)?;
|
||||||
@@ -312,11 +328,13 @@ fn audio() -> Option<Audio> {
|
|||||||
output,
|
output,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read(path: &Path, name: &str) -> Option<String> {
|
fn read(path: &Path, name: &str) -> Option<String> {
|
||||||
fs::read_to_string(path.join(name))
|
fs::read_to_string(path.join(name))
|
||||||
.ok()
|
.ok()
|
||||||
.map(|s| s.trim().to_owned())
|
.map(|s| s.trim().to_owned())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn battery() -> Option<Battery> {
|
fn battery() -> Option<Battery> {
|
||||||
let entries = fs::read_dir("/sys/class/power_supply").ok()?;
|
let entries = fs::read_dir("/sys/class/power_supply").ok()?;
|
||||||
// Prefer a system battery, not a mouse/headset battery.
|
// Prefer a system battery, not a mouse/headset battery.
|
||||||
@@ -336,6 +354,7 @@ fn battery() -> Option<Battery> {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn brightness() -> Option<u8> {
|
fn brightness() -> Option<u8> {
|
||||||
let max = run("brightnessctl", &["--class=backlight", "max"])
|
let max = run("brightnessctl", &["--class=backlight", "max"])
|
||||||
.ok()?
|
.ok()?
|
||||||
@@ -348,6 +367,7 @@ fn brightness() -> Option<u8> {
|
|||||||
(max > 0.0 && current.is_finite())
|
(max > 0.0 && current.is_finite())
|
||||||
.then(|| (100.0 * current / max).round().clamp(0.0, 100.0) as u8)
|
.then(|| (100.0 * current / max).round().clamp(0.0, 100.0) as u8)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn notifications() -> Option<Notifications> {
|
fn notifications() -> Option<Notifications> {
|
||||||
// Check service ownership first: swaync-client otherwise waits indefinitely.
|
// Check service ownership first: swaync-client otherwise waits indefinitely.
|
||||||
let owner = run(
|
let owner = run(
|
||||||
@@ -375,6 +395,7 @@ fn notifications() -> Option<Notifications> {
|
|||||||
};
|
};
|
||||||
Some(Notifications { count, dnd })
|
Some(Notifications { count, dnd })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn can_power(method: &str) -> bool {
|
fn can_power(method: &str) -> bool {
|
||||||
run(
|
run(
|
||||||
"busctl",
|
"busctl",
|
||||||
@@ -389,6 +410,7 @@ fn can_power(method: &str) -> bool {
|
|||||||
)
|
)
|
||||||
.is_ok_and(|s| s == "s \"yes\"" || s == "s \"challenge\"")
|
.is_ok_and(|s| s == "s \"yes\"" || s == "s \"challenge\"")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn execute(action: &Action, settings: Option<&[String]>) -> Result<(), String> {
|
fn execute(action: &Action, settings: Option<&[String]>) -> Result<(), String> {
|
||||||
match action {
|
match action {
|
||||||
Action::Wifi(on) => run("nmcli", &["radio", "wifi", if *on { "on" } else { "off" }]),
|
Action::Wifi(on) => run("nmcli", &["radio", "wifi", if *on { "on" } else { "off" }]),
|
||||||
@@ -462,6 +484,7 @@ mod tests {
|
|||||||
assert_eq!(parse_audio(value), None);
|
assert_eq!(parse_audio(value), None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn default_snapshot_does_not_invent_services() {
|
fn default_snapshot_does_not_invent_services() {
|
||||||
let s = Snapshot::default();
|
let s = Snapshot::default();
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ pub enum Menu {
|
|||||||
Notifications,
|
Notifications,
|
||||||
System,
|
System,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Menu {
|
impl Menu {
|
||||||
fn width(self) -> f32 {
|
fn width(self) -> f32 {
|
||||||
match self {
|
match self {
|
||||||
@@ -28,10 +29,12 @@ impl Menu {
|
|||||||
Self::Audio | Self::Notifications => 290.0,
|
Self::Audio | Self::Notifications => 290.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn material_role(self) -> Option<ferese_surface_effects_v1::Role> {
|
pub(super) fn material_role(self) -> Option<ferese_surface_effects_v1::Role> {
|
||||||
// Popovers contain independent cards, with no shared material backing.
|
// Popovers contain independent cards, with no shared material backing.
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn available(self, status: &Snapshot) -> bool {
|
fn available(self, status: &Snapshot) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Self::Network => status.network.is_some(),
|
Self::Network => status.network.is_some(),
|
||||||
@@ -52,10 +55,12 @@ pub struct OpenMenu {
|
|||||||
pub effects: Option<EffectsBinding>,
|
pub effects: Option<EffectsBinding>,
|
||||||
pub regions: super::motion::Regions,
|
pub regions: super::motion::Regions,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OpenMenu {
|
impl OpenMenu {
|
||||||
pub fn progress(&self) -> f32 {
|
pub fn progress(&self) -> f32 {
|
||||||
self.motion.progress()
|
self.motion.progress()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn animating(&self) -> bool {
|
pub fn animating(&self) -> bool {
|
||||||
self.motion.animating()
|
self.motion.animating()
|
||||||
}
|
}
|
||||||
@@ -115,6 +120,7 @@ impl FereseShell {
|
|||||||
);
|
);
|
||||||
destroy.chain(cosmic::task::message(cosmic::Action::Surface(action)))
|
destroy.chain(cosmic::task::message(cosmic::Action::Surface(action)))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn destroy_menu(&mut self) -> Task<Message> {
|
pub fn destroy_menu(&mut self) -> Task<Message> {
|
||||||
super::EFFECT_FRAME_PENDING.store(false, std::sync::atomic::Ordering::Relaxed);
|
super::EFFECT_FRAME_PENDING.store(false, std::sync::atomic::Ordering::Relaxed);
|
||||||
self.menu.take().map_or_else(Task::none, |menu| {
|
self.menu.take().map_or_else(Task::none, |menu| {
|
||||||
@@ -123,6 +129,7 @@ impl FereseShell {
|
|||||||
))
|
))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn close_menu(&mut self) -> Task<Message> {
|
pub fn close_menu(&mut self) -> Task<Message> {
|
||||||
let Some(menu) = &mut self.menu else {
|
let Some(menu) = &mut self.menu else {
|
||||||
return Task::none();
|
return Task::none();
|
||||||
@@ -142,6 +149,7 @@ impl FereseShell {
|
|||||||
}
|
}
|
||||||
self.destroy_menu()
|
self.destroy_menu()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn animate_menu(&mut self) -> Task<Message> {
|
pub fn animate_menu(&mut self) -> Task<Message> {
|
||||||
if let Some(menu) = &self.menu {
|
if let Some(menu) = &self.menu {
|
||||||
if let Some(effects) = &menu.effects {
|
if let Some(effects) = &menu.effects {
|
||||||
@@ -153,6 +161,7 @@ impl FereseShell {
|
|||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn optimistic_status(&mut self, action: &Action) {
|
pub fn optimistic_status(&mut self, action: &Action) {
|
||||||
match action {
|
match action {
|
||||||
Action::Volume(v) => {
|
Action::Volume(v) => {
|
||||||
@@ -184,6 +193,7 @@ impl FereseShell {
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn view_status_bar(&self) -> Element<'_, cosmic::Action<Message>> {
|
pub fn view_status_bar(&self) -> Element<'_, cosmic::Action<Message>> {
|
||||||
let theme = self.config.theme.for_bar();
|
let theme = self.config.theme.for_bar();
|
||||||
let metrics = BarMetrics::from(theme);
|
let metrics = BarMetrics::from(theme);
|
||||||
@@ -861,6 +871,7 @@ fn menu_button<'a>(
|
|||||||
opacity,
|
opacity,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn toggle_row<'a>(
|
fn toggle_row<'a>(
|
||||||
label: &'a str,
|
label: &'a str,
|
||||||
on: bool,
|
on: bool,
|
||||||
@@ -987,6 +998,7 @@ fn audio_icon(volume: u8, muted: bool) -> &'static [u8] {
|
|||||||
include_bytes!("../assets/icons/status/volume-high.svg")
|
include_bytes!("../assets/icons/status/volume-high.svg")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn status_icon(kind: Menu, s: &Snapshot) -> (&'static [u8], bool) {
|
fn status_icon(kind: Menu, s: &Snapshot) -> (&'static [u8], bool) {
|
||||||
match kind {
|
match kind {
|
||||||
Menu::Network => {
|
Menu::Network => {
|
||||||
@@ -1111,6 +1123,7 @@ mod tests {
|
|||||||
assert!(!Menu::Notifications.available(&s));
|
assert!(!Menu::Notifications.available(&s));
|
||||||
assert!(Menu::System.available(&s));
|
assert!(Menu::System.available(&s));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn icons_follow_real_states() {
|
fn icons_follow_real_states() {
|
||||||
let mut s = Snapshot::default();
|
let mut s = Snapshot::default();
|
||||||
|
|||||||
Reference in new issue
Block a user