Merge pull request #172 from pinnacle-comp/cli

Make a better CLI
This commit is contained in:
Ottatop 2024-03-04 15:25:02 -06:00 committed by GitHub
commit 96d5c9a70f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 1125 additions and 212 deletions

235
Cargo.lock generated
View file

@ -66,6 +66,21 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04"
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "anstream" name = "anstream"
version = "0.6.11" version = "0.6.11"
@ -295,6 +310,17 @@ dependencies = [
"objc2", "objc2",
] ]
[[package]]
name = "bstr"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c48f0051a4b4c5e0b6d365cd04af53aeaa209e3cc15ec2cdb69e73cc87fbd0dc"
dependencies = [
"memchr",
"regex-automata 0.4.5",
"serde",
]
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.14.0" version = "3.14.0"
@ -397,6 +423,20 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[package]]
name = "chrono"
version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-targets 0.52.0",
]
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.1" version = "4.5.1"
@ -437,6 +477,19 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
[[package]]
name = "cliclack"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be29210ca32b96e4f67fe9a520d2eeacc078d94ff4027100dc6b7262fdfec5c4"
dependencies = [
"console",
"indicatif",
"once_cell",
"textwrap",
"zeroize",
]
[[package]] [[package]]
name = "color_quant" name = "color_quant"
version = "1.1.0" version = "1.1.0"
@ -468,6 +521,19 @@ dependencies = [
"crossbeam-utils", "crossbeam-utils",
] ]
[[package]]
name = "console"
version = "0.15.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb"
dependencies = [
"encode_unicode",
"lazy_static",
"libc",
"unicode-width",
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "const_format" name = "const_format"
version = "0.2.32" version = "0.2.32"
@ -528,6 +594,19 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "crossbeam"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8"
dependencies = [
"crossbeam-channel",
"crossbeam-deque",
"crossbeam-epoch",
"crossbeam-queue",
"crossbeam-utils",
]
[[package]] [[package]]
name = "crossbeam-channel" name = "crossbeam-channel"
version = "0.5.11" version = "0.5.11"
@ -556,6 +635,15 @@ dependencies = [
"crossbeam-utils", "crossbeam-utils",
] ]
[[package]]
name = "crossbeam-queue"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35"
dependencies = [
"crossbeam-utils",
]
[[package]] [[package]]
name = "crossbeam-utils" name = "crossbeam-utils"
version = "0.8.19" version = "0.8.19"
@ -577,6 +665,17 @@ dependencies = [
"powerfmt", "powerfmt",
] ]
[[package]]
name = "dircpy"
version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29259db751c34980bfc44100875890c507f585323453b91936960ab1104272ca"
dependencies = [
"jwalk",
"log",
"walkdir",
]
[[package]] [[package]]
name = "dirs" name = "dirs"
version = "5.0.1" version = "5.0.1"
@ -670,6 +769,12 @@ version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
[[package]]
name = "encode_unicode"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
[[package]] [[package]]
name = "encoding_rs" name = "encoding_rs"
version = "0.8.33" version = "0.8.33"
@ -1013,6 +1118,29 @@ dependencies = [
"tokio-io-timeout", "tokio-io-timeout",
] ]
[[package]]
name = "iana-time-zone"
version = "0.1.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]] [[package]]
name = "icrate" name = "icrate"
version = "0.0.4" version = "0.0.4"
@ -1056,6 +1184,19 @@ dependencies = [
"hashbrown 0.14.3", "hashbrown 0.14.3",
] ]
[[package]]
name = "indicatif"
version = "0.17.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3"
dependencies = [
"console",
"instant",
"number_prefix",
"portable-atomic",
"unicode-width",
]
[[package]] [[package]]
name = "input" name = "input"
version = "0.9.0" version = "0.9.0"
@ -1075,6 +1216,15 @@ version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd4f5b4d1c00331c5245163aacfe5f20be75b564c7112d45893d4ae038119eb0" checksum = "bd4f5b4d1c00331c5245163aacfe5f20be75b564c7112d45893d4ae038119eb0"
[[package]]
name = "instant"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
dependencies = [
"cfg-if",
]
[[package]] [[package]]
name = "io-lifetimes" name = "io-lifetimes"
version = "1.0.11" version = "1.0.11"
@ -1147,6 +1297,16 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "jwalk"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2735847566356cd2179a2a38264839308f7079fa96e6bd5a42d740460e003c56"
dependencies = [
"crossbeam",
"rayon",
]
[[package]] [[package]]
name = "khronos_api" name = "khronos_api"
version = "3.1.0" version = "3.1.0"
@ -1451,6 +1611,12 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "number_prefix"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
[[package]] [[package]]
name = "objc-sys" name = "objc-sys"
version = "0.3.2" version = "0.3.2"
@ -1503,6 +1669,15 @@ dependencies = [
"libredox 0.0.2", "libredox 0.0.2",
] ]
[[package]]
name = "os_str_bytes"
version = "6.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "overload" name = "overload"
version = "0.1.1" version = "0.1.1"
@ -1586,7 +1761,10 @@ version = "0.0.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bitflags 2.4.2", "bitflags 2.4.2",
"chrono",
"clap", "clap",
"cliclack",
"dircpy",
"image", "image",
"nix", "nix",
"pinnacle-api-defs", "pinnacle-api-defs",
@ -1667,6 +1845,12 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "portable-atomic"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0"
[[package]] [[package]]
name = "powerfmt" name = "powerfmt"
version = "0.2.0" version = "0.2.0"
@ -2023,7 +2207,9 @@ version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da03fa3b94cc19e3ebfc88c4229c49d8f08cdbd1228870a45f0ffdf84988e14b" checksum = "da03fa3b94cc19e3ebfc88c4229c49d8f08cdbd1228870a45f0ffdf84988e14b"
dependencies = [ dependencies = [
"bstr",
"dirs", "dirs",
"os_str_bytes",
] ]
[[package]] [[package]]
@ -2050,6 +2236,12 @@ version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7"
[[package]]
name = "smawk"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c"
[[package]] [[package]]
name = "smithay" name = "smithay"
version = "0.3.0" version = "0.3.0"
@ -2212,6 +2404,17 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "textwrap"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9"
dependencies = [
"smawk",
"unicode-linebreak",
"unicode-width",
]
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.57" version = "1.0.57"
@ -2565,12 +2768,24 @@ version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "unicode-linebreak"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
[[package]] [[package]]
name = "unicode-segmentation" name = "unicode-segmentation"
version = "1.11.0" version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
[[package]]
name = "unicode-width"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
[[package]] [[package]]
name = "unicode-xid" name = "unicode-xid"
version = "0.2.4" version = "0.2.4"
@ -3271,3 +3486,23 @@ dependencies = [
"quote", "quote",
"syn", "syn",
] ]
[[package]]
name = "zeroize"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d"
dependencies = [
"zeroize_derive",
]
[[package]]
name = "zeroize_derive"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [
"proc-macro2",
"quote",
"syn",
]

View file

@ -7,20 +7,21 @@ edition = "2021"
repository = "https://github.com/pinnacle-comp/pinnacle/" repository = "https://github.com/pinnacle-comp/pinnacle/"
[workspace.dependencies] [workspace.dependencies]
# Tokio
tokio = { version = "1.36.0", features = ["macros", "rt-multi-thread"]} tokio = { version = "1.36.0", features = ["macros", "rt-multi-thread"]}
tokio-stream = { version = "0.1.14", features = ["net"] } tokio-stream = { version = "0.1.14", features = ["net"] }
# gRPC
prost = "0.12.3" prost = "0.12.3"
tonic = "0.11.0" tonic = "0.11.0"
tonic-reflection = "0.11.0" tonic-reflection = "0.11.0"
tonic-build = "0.11.0" tonic-build = "0.11.0"
# API definitions
pinnacle-api-defs = { path = "./pinnacle-api-defs" } pinnacle-api-defs = { path = "./pinnacle-api-defs" }
# Misc.
xkbcommon = "0.7.0" xkbcommon = "0.7.0"
xdg = "2.5.2" xdg = "2.5.2"
################################################################################# ########################################################################yo😎###########
[package] [package]
name = "pinnacle" name = "pinnacle"
@ -34,38 +35,42 @@ repository.workspace = true
keywords = ["wayland", "compositor", "smithay", "lua"] keywords = ["wayland", "compositor", "smithay", "lua"]
[dependencies] [dependencies]
# Smithay
smithay = { git = "https://github.com/Smithay/smithay", rev = "418190e", default-features = false, features = ["desktop", "wayland_frontend"] } smithay = { git = "https://github.com/Smithay/smithay", rev = "418190e", default-features = false, features = ["desktop", "wayland_frontend"] }
smithay-drm-extras = { git = "https://github.com/Smithay/smithay", rev = "418190e" } smithay-drm-extras = { git = "https://github.com/Smithay/smithay", rev = "418190e" }
# Tracing
tracing = "0.1.40" tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "registry"] } tracing-subscriber = { version = "0.3.18", features = ["env-filter", "registry"] }
tracing-appender = "0.2.3" tracing-appender = "0.2.3"
# Errors
anyhow = { version = "1.0.79", features = ["backtrace"] } anyhow = { version = "1.0.79", features = ["backtrace"] }
thiserror = "1.0.57" thiserror = "1.0.57"
# xcursor stuff
xcursor = { version = "0.3.5" } xcursor = { version = "0.3.5" }
image = { version = "0.24.8", default-features = false } image = { version = "0.24.8", default-features = false }
# gRPC
prost = { workspace = true }
tonic = { workspace = true }
tonic-reflection = { workspace = true }
# Tokio
tokio = { workspace = true, features = ["process", "io-util", "signal"] }
tokio-stream = { workspace = true }
# CLI
clap = { version = "4.5.1", features = ["derive"] }
cliclack = "0.1.13"
# Misc.
bitflags = "2.4.2"
serde = { version = "1.0.196", features = ["derive"] } serde = { version = "1.0.196", features = ["derive"] }
toml = "0.8.10" toml = "0.8.10"
shellexpand = "3.1.0" shellexpand = { version = "3.1.0", features = ["path"] }
clap = { version = "4.5.1", features = ["derive"] }
x11rb = { version = "0.13.0", default-features = false, features = ["composite"] } x11rb = { version = "0.13.0", default-features = false, features = ["composite"] }
xkbcommon = { workspace = true } xkbcommon = { workspace = true }
xdg = { workspace = true } xdg = { workspace = true }
sysinfo = "0.30.5" sysinfo = "0.30.5"
nix = { version = "0.27.1", features = ["user", "resource"] } nix = { version = "0.27.1", features = ["user", "resource"] }
prost = { workspace = true }
tonic = { workspace = true }
tonic-reflection = { workspace = true }
tokio = { workspace = true, features = ["process", "io-util", "signal"] }
tokio-stream = { workspace = true }
bitflags = "2.4.2"
pinnacle-api-defs = { workspace = true } pinnacle-api-defs = { workspace = true }
dircpy = "0.3.16"
chrono = "0.4.34"
[build-dependencies] [build-dependencies]
xdg = { workspace = true } xdg = { workspace = true }

109
README.md
View file

@ -13,10 +13,11 @@ https://github.com/Ottatop/pinnacle/assets/120758733/c175ba80-9796-4759-92c3-1d7
- [Configuration](#configuration) - [Configuration](#configuration)
- [Out-of-the-box configurations](#out-of-the-box-configurations) - [Out-of-the-box configurations](#out-of-the-box-configurations)
- [Custom configuration](#custom-configuration) - [Custom configuration](#custom-configuration)
- [Lua](#lua) - [Generating a config](#generating-a-config)
- [Lua Language Server completion](#lua-language-server-completion) - [More on configuration and the `metaconfig.toml` file](#more-on-configuration-and-the-metaconfig.toml-file)
- [Rust](#rust) - [The `metaconfig.toml` file](#the-metaconfig.toml-file)
- [API References](#api-references) - [Lua Language Server completion](#lua-language-server-completion)
- [API references](#api-references)
- [Controls](#controls) - [Controls](#controls)
- [Feature Requests, Bug Reports, Contributions, and Questions](#feature-requests-bug-reports-contributions-and-questions) - [Feature Requests, Bug Reports, Contributions, and Questions](#feature-requests-bug-reports-contributions-and-questions)
- [Changelog](#changelog) - [Changelog](#changelog)
@ -27,8 +28,6 @@ Pinnacle is a Wayland compositor built in Rust using [Smithay](https://github.co
It's my attempt at creating something like [AwesomeWM](https://github.com/awesomeWM/awesome) It's my attempt at creating something like [AwesomeWM](https://github.com/awesomeWM/awesome)
for Wayland. for Wayland.
It can be configured through either Lua or Rust.
> ### More video examples below! > ### More video examples below!
> <details> > <details>
> >
@ -96,7 +95,9 @@ cargo build [--release]
> [!NOTE] > [!NOTE]
> On build, [`build.rs`](build.rs) will: > On build, [`build.rs`](build.rs) will:
> - Copy Protobuf definition files to `$XDG_DATA_HOME/pinnacle/protobuf` > - Copy Protobuf definition files to `$XDG_DATA_HOME/pinnacle/protobuf`
> - Copy the [default config](api/lua/examples/default) to `$XDG_DATA_HOME/pinnacle/default_config` > - Copy the [Lua default config](api/lua/examples/default) and
> [Rust default config](api/rust/examples/default_config/for_copying) to
> `$XDG_DATA_HOME/pinnacle/default_config/{lua,rust}`
> - `cd` into [`api/lua`](api/lua) and run `luarocks make` to install the Lua library to `~/.luarocks/share/lua/5.4` > - `cd` into [`api/lua`](api/lua) and run `luarocks make` to install the Lua library to `~/.luarocks/share/lua/5.4`
# Running # Running
@ -122,14 +123,13 @@ Pinnacle is configured in your choice of Lua or Rust.
## Out-of-the-box configurations ## Out-of-the-box configurations
If you just want to test Pinnacle out without copying stuff to your config directory, If you just want to test Pinnacle out without copying stuff to your config directory,
run one of the following in the crate root: run one of the following in the crate root:
```sh ```sh
# For a Lua configuration # For a Lua configuration
PINNACLE_CONFIG_DIR="./api/lua/examples/default" cargo run cargo run -- -c "./api/lua/examples/default"
# This should also have been copied to the directory below, so below will do the same
PINNACLE_CONFIG_DIR="~/.local/share/pinnacle/default_config" cargo run
# For a Rust configuration # For a Rust configuration
PINNACLE_CONFIG_DIR="./api/rust/examples/default_config" cargo run cargo run -- -c "./api/rust/examples/default_config"
``` ```
## Custom configuration ## Custom configuration
@ -137,58 +137,65 @@ PINNACLE_CONFIG_DIR="./api/rust/examples/default_config" cargo run
> [!IMPORTANT] > [!IMPORTANT]
> Pinnacle is under development, and there *will* be major breaking changes to these APIs > Pinnacle is under development, and there *will* be major breaking changes to these APIs
> until I release version 0.1, at which point there will be an API stability spec in place. > until I release version 0.1, at which point there will be an API stability spec in place.
>
> Until then, I recommend you either use the out-of-the-box configs above or prepare for
> your config to break every now and then.
Pinnacle will search for a `metaconfig.toml` file in the following directories, from top to bottom: ### Generating a config
Run the following command to open up the interactive config generator:
```sh ```sh
$PINNACLE_CONFIG_DIR cargo run -- config gen
$XDG_CONFIG_HOME/pinnacle
~/.config/pinnacle # Only if $XDG_CONFIG_HOME is not defined
``` ```
The `metaconfig.toml` file provides information on what config to run, kill and reload keybinds, This will prompt you to choose a language (Lua or Rust) and directory to put the config in.
and any environment variables you want set. For more details, see the provided It will then generate a config at that directory. If Lua is chosen and there are conflicting
[`metaconfig.toml`](api/lua/examples/default/metaconfig.toml) file. files in the directory, the generator will prompt to rename them to a backup before continuing.
If Rust is chosen, the directory must be manually emptied to continue.
If no `metaconfig.toml` file is found, the default Lua config will be loaded. Run `cargo run -- config gen --help` for information on the command.
### Lua ## More on configuration and the `metaconfig.toml` file
For custom configuration in Lua, copy the contents of `~/.local/share/pinnacle/default_config` to Pinnacle is configured purely through IPC using [gRPC](https://grpc.io/). This is done through
`~/.config/pinnacle` (or `$XDG_CONFIG_HOME/pinnacle`): configuration clients that use the [Lua](api/lua) and [Rust](api/rust) interface libraries.
```sh
mkdir ~/.config/pinnacle
cp -r ~/.local/share/pinnacle/default_config/. ~/.config/pinnacle
```
Note: there is a `.luarc.json` file that may not get copied if you do `cp <path>/* ~/.config/pinnacle`.
The above command takes that into account.
> If you rename `default_config.lua`, make sure `command` in your `metaconfig.toml` is updated to reflect that. As the compositor has no direct integration with these clients, it must know what it needs to run
> If it isn't, the compositor will load the default config instead. through a separate file, aptly called the `metaconfig.toml` file.
#### Lua Language Server completion To start a config, Pinnacle will search for a `metaconfig.toml` file in the first directory
that exists from the following:
1. The directory passed in through `--config-dir`/`-c`
2. `$PINNACLE_CONFIG_DIR`
3. `$XDG_CONFIG_HOME/pinnacle`
4. `~/.config/pinnacle` if $XDG_CONFIG_HOME is not defined
If there is no `metaconfig.toml` file in that directory, Pinnacle will start the default Lua config
at `$XDG_DATA_HOME/pinnacle/default_config/lua` (typically `~/.local/share/pinnacle/default_config/lua`).
Additionally, if your config crashes, Pinnacle will also start the default Lua config.
> [!NOTE]
> If you have not run `eval $(luarocks path --lua-version 5.4)`, Pinnacle will go into an endless loop of
> starting the default Lua config only for it to crash because it can't find the Lua library.
### The `metaconfig.toml` file
A `metaconfig.toml` file must contain the following entries:
- `command`: An array denoting the program and arguments Pinnacle will run to start a config.
- `reload_keybind`: A table denoting a keybind that Pinnacle will hardcode to restart your config.
- `kill_keybind`: A table denoting a keybind that Pinnacle will hardcode to quit the compositor.
- The two keybinds above prevent you from getting locked in the compositor if the default config fails to start.
It also has the following optional entries:
- `socket_dir`: A directory that Pinnacle will place its IPC socket in (this defaults to `$XDG_RUNTIME_DIR`,
falling back to `/tmp` if that doesn't exist).
- `[envs]`: A table of environment variables that Pinnacle will start the config with.
For the specifics, see the default [`metaconfig.toml`](api/lua/examples/default/metaconfig.toml) file.
## Lua Language Server completion
A [`.luarc.json`](api/lua/examples/default/.luarc.json) file is included with the default Lua config A [`.luarc.json`](api/lua/examples/default/.luarc.json) file is included with the default Lua config
and will set the correct workspace library files for use with the and will set the correct workspace library files for use with the
[Lua language server](https://github.com/LuaLS/lua-language-server). [Lua language server](https://github.com/LuaLS/lua-language-server).
### Rust ## API references
If you want to use Rust to configure Pinnacle, follow these steps:
1. In `~/.config/pinnacle`, run `cargo init`.
2. In the `Cargo.toml` file, add the following under `[dependencies]`:
```toml
pinnacle-api = { git = "http://github.com/pinnacle-comp/pinnacle" }
```
3. Create the file `metaconfig.toml` at the root. Add the following to the file:
```toml
command = ["cargo", "run"]
reload_keybind = { modifiers = ["Ctrl", "Alt"], key = "r" }
kill_keybind = { modifiers = ["Ctrl", "Alt", "Shift"], key = "escape" }
```
4. Copy the [default config](api/rust/examples/default_config/main.rs) to `src/main.rs`.
5. Run Pinnacle! (You may want to run `cargo build` beforehand so you don't have to wait for your config to compile.)
### API References
<b>Lua: https://pinnacle-comp.github.io/lua-reference/main.<br> <b>Lua: https://pinnacle-comp.github.io/lua-reference/main.<br>
Rust: https://pinnacle-comp.github.io/rust-reference/main.</b> Rust: https://pinnacle-comp.github.io/rust-reference/main.</b>

View file

@ -0,0 +1,7 @@
[package]
name = "pinnacle-config"
version = "0.1.0"
edition = "2021"
[dependencies]
pinnacle-api = { git = "http://github.com/pinnacle-comp/pinnacle" }

View file

@ -0,0 +1,46 @@
# This metaconfig.toml file dictates what config Pinnacle will run.
#
# When running Pinnacle, the compositor will look in the following directories for a metaconfig.toml file,
# in order from top to bottom:
# $PINNACLE_CONFIG_DIR
# $XDG_CONFIG_HOME/pinnacle/
# ~/.config/pinnacle/
#
# When Pinnacle finds a metaconfig.toml file, it will execute the command provided to `command`.
# To use a Rust config, this should be changed to something like ["cargo", "run"].
#
# Because configuration is done using an external process, if it ever crashes, you lose all of your keybinds.
# The compositor will load the default config if that happens, but in the event that you don't have
# the necessary dependencies for it to run, you may get softlocked.
# In order prevent you from getting stuck in the compositor, you must define keybinds to reload your config
# and kill Pinnacle.
#
# More details on each setting can be found below.
# The command Pinnacle will run on startup and when you reload your config.
# Paths are relative to the directory the metaconfig.toml file is in.
# This must be an array.
command = ["cargo", "run"]
### Keybinds ###
# Each keybind takes in a table with two fields: `modifiers` and `key`.
# - `modifiers` can be one of "Ctrl", "Alt", "Shift", or "Super".
# - `key` can be a string of any lowercase letter, number,
# "numN" where N is a number for numpad keys, or "esc"/"escape".
# Support for any xkbcommon key is planned for a future update.
# The keybind that will reload your config.
reload_keybind = { modifiers = ["Ctrl", "Alt"], key = "r" }
# The keybind that will kill Pinnacle.
kill_keybind = { modifiers = ["Ctrl", "Alt", "Shift"], key = "escape" }
### Socket directory ###
# Pinnacle will open a Unix socket at `$XDG_RUNTIME_DIR` by default, falling back to `/tmp` if it doesn't exist.
# If you want/need to change this, use the `socket_dir` setting set to the directory of your choosing.
#
# socket_dir = "/your/dir/here/"
### Environment Variables ###
# If you need to spawn your config with any environment variables, list them here.
[envs]
# key = "value"

View file

@ -0,0 +1 @@
../../main.rs

View file

@ -1,4 +1,6 @@
fn main() { use std::process::Command;
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("cargo:rerun-if-changed=api/lua"); println!("cargo:rerun-if-changed=api/lua");
println!("cargo:rerun-if-changed=api/protocol"); println!("cargo:rerun-if-changed=api/protocol");
@ -6,51 +8,60 @@ fn main() {
let proto_dir = xdg.place_data_file("protobuf").unwrap(); let proto_dir = xdg.place_data_file("protobuf").unwrap();
let default_config_dir = xdg.place_data_file("default_config").unwrap(); let default_config_dir = xdg.place_data_file("default_config").unwrap();
let default_lua_config_dir = default_config_dir.join("lua");
let default_rust_config_dir = default_config_dir.join("rust");
let remove_protos = format!("rm -r {proto_dir:?}"); let remove_protos = format!("rm -r {proto_dir:?}");
let copy_protos = format!("cp -r ./api/protocol {proto_dir:?}"); let copy_protos = format!("cp -r ./api/protocol {proto_dir:?}");
let remove_default_config = format!("rm -r {default_config_dir:?}"); let remove_default_config_dir = format!("rm -r {default_config_dir:?}");
let copy_default_config = format!("cp -r ./api/lua/examples/default {default_config_dir:?}");
std::process::Command::new("/bin/sh") let copy_default_lua_config =
format!("cp -r ./api/lua/examples/default {default_lua_config_dir:?}");
let copy_default_rust_config = format!(
"cp -LR ./api/rust/examples/default_config/for_copying {default_rust_config_dir:?}"
);
Command::new("/bin/sh")
.arg("-c") .arg("-c")
.arg(&remove_protos) .arg(&remove_protos)
.spawn() .spawn()?
.unwrap() .wait()?;
.wait()
.unwrap();
std::process::Command::new("/bin/sh") Command::new("/bin/sh")
.arg("-c") .arg("-c")
.arg(&copy_protos) .arg(&copy_protos)
.spawn() .spawn()?
.unwrap() .wait()?;
.wait()
.unwrap();
std::process::Command::new("/bin/sh") Command::new("/bin/sh")
.arg("-c") .arg("-c")
.arg(&remove_default_config) .arg(&remove_default_config_dir)
.spawn() .spawn()?
.unwrap() .wait()?;
.wait()
.unwrap();
std::process::Command::new("/bin/sh") std::fs::create_dir_all(&default_config_dir)?;
Command::new("/bin/sh")
.arg("-c") .arg("-c")
.arg(&copy_default_config) .arg(&copy_default_lua_config)
.spawn() .spawn()?
.unwrap() .wait()?;
.wait()
.unwrap(); Command::new("/bin/sh")
.arg("-c")
.arg(&copy_default_rust_config)
.spawn()?
.wait()?;
std::env::set_current_dir("api/lua").unwrap(); std::env::set_current_dir("api/lua").unwrap();
std::process::Command::new("luarocks") Command::new("luarocks")
.arg("make") .arg("make")
.arg("--local") .arg("--local")
.spawn() .spawn()
.expect("Luarocks is not installed") .expect("Luarocks is not installed")
.wait() .wait()?;
.unwrap();
Ok(())
} }

View file

@ -3,7 +3,7 @@
use std::{ use std::{
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
ffi::OsString, ffi::OsString,
path::Path, path::{Path, PathBuf},
time::Duration, time::Duration,
}; };
@ -236,7 +236,7 @@ impl BackendData for Udev {
} }
} }
pub fn run_udev() -> anyhow::Result<()> { pub fn run_udev(no_config: bool, config_dir: Option<PathBuf>) -> anyhow::Result<()> {
let mut event_loop = EventLoop::try_new()?; let mut event_loop = EventLoop::try_new()?;
let display = Display::new()?; let display = Display::new()?;
@ -283,6 +283,8 @@ pub fn run_udev() -> anyhow::Result<()> {
display, display,
event_loop.get_signal(), event_loop.get_signal(),
event_loop.handle(), event_loop.handle(),
no_config,
config_dir,
)?; )?;
// Initialize the udev backend // Initialize the udev backend

View file

@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
use std::{ffi::OsString, time::Duration}; use std::{ffi::OsString, path::PathBuf, time::Duration};
use smithay::{ use smithay::{
backend::{ backend::{
@ -62,7 +62,7 @@ impl Backend {
} }
/// Start Pinnacle as a window in a graphical environment. /// Start Pinnacle as a window in a graphical environment.
pub fn run_winit() -> anyhow::Result<()> { pub fn run_winit(no_config: bool, config_dir: Option<PathBuf>) -> anyhow::Result<()> {
let mut event_loop: EventLoop<State> = EventLoop::try_new()?; let mut event_loop: EventLoop<State> = EventLoop::try_new()?;
let display: Display<State> = Display::new()?; let display: Display<State> = Display::new()?;
@ -153,16 +153,20 @@ pub fn run_winit() -> anyhow::Result<()> {
tracing::info!("EGL hardware-acceleration enabled"); tracing::info!("EGL hardware-acceleration enabled");
} }
let backend = Backend::Winit(Winit {
backend: winit_backend,
damage_tracker: OutputDamageTracker::from_output(&output),
dmabuf_state,
full_redraw: 0,
});
let mut state = State::init( let mut state = State::init(
Backend::Winit(Winit { backend,
backend: winit_backend,
damage_tracker: OutputDamageTracker::from_output(&output),
dmabuf_state,
full_redraw: 0,
}),
display, display,
event_loop.get_signal(), event_loop.get_signal(),
evt_loop_handle, evt_loop_handle,
no_config,
config_dir,
)?; )?;
state.output_focus_stack.set_focus(output.clone()); state.output_focus_stack.set_focus(output.clone());

567
src/cli.rs Normal file
View file

@ -0,0 +1,567 @@
use std::{io::IsTerminal, path::PathBuf};
use clap::{Parser, ValueHint};
use tracing::{error, warn};
/// Valid backends that Pinnacle can run.
#[derive(clap::ValueEnum, Debug, Clone, Copy)]
pub enum Backend {
/// Run Pinnacle in a window in your graphical environment
Winit,
/// Run Pinnacle from a tty
Udev,
}
/// The main CLI struct.
#[derive(clap::Parser, Debug)]
#[command(author, version, about, long_about = None)]
pub struct Cli {
/// Start Pinnacle with the config at this directory
#[arg(short, long, value_name("DIR"), value_hint(ValueHint::DirPath))]
pub config_dir: Option<PathBuf>,
/// Run Pinnacle with the specified backend
///
/// This is usually not necessary, but if your environment variables are mucked up
/// then this can be used to choose a backend.
#[arg(short, long)]
pub backend: Option<Backend>,
/// Force Pinnacle to run with the provided backend
#[arg(long, requires = "backend")]
pub force: bool,
/// Allow running Pinnacle as root (this is NOT recommended)
#[arg(long)]
pub allow_root: bool,
/// Start Pinnacle without a config
///
/// This is meant to be used for debugging.
/// Additionally, Pinnacle will not load the
/// default config if a manually spawned one
/// crashes or exits.
#[arg(long)]
pub no_config: bool,
/// Cli subcommands
#[command(subcommand)]
subcommand: Option<CliSubcommand>,
}
impl Cli {
pub fn parse_and_prompt() -> Option<Self> {
let mut cli = Cli::parse();
// oh my god rustfmt is starting to piss me off
cli.config_dir = cli.config_dir.and_then(|dir| {
let new_dir = shellexpand::path::full(&dir);
match new_dir {
Ok(new_dir) => Some(new_dir.to_path_buf()),
Err(err) => {
warn!("Could not shellexpand `--config-dir`'s argument: {err}; unsetting `--config-dir`");
None
}
}
});
if let Some(subcommand) = &cli.subcommand {
match subcommand {
CliSubcommand::Config(ConfigSubcommand::Gen(config_gen)) => {
if let Err(err) = generate_config(config_gen.clone()) {
error!("Error generating config: {err}");
}
}
}
return None;
}
Some(cli)
}
}
/// Cli subcommands.
#[derive(clap::Subcommand, Debug)]
enum CliSubcommand {
/// Commands dealing with configuration
#[command(subcommand)]
Config(ConfigSubcommand),
}
/// Config subcommands
#[derive(clap::Subcommand, Debug)]
enum ConfigSubcommand {
/// Generate a config
///
/// If not all flags are provided, this will launch an
/// interactive prompt unless `--non-interactive` is passed
/// or this is run in a non-interactive shell.
Gen(ConfigGen),
}
/// Config arguments.
#[derive(clap::Args, Debug, Clone, PartialEq)]
struct ConfigGen {
/// Generate a config in a specific language
#[arg(short, long)]
pub lang: Option<Lang>,
/// Generate a config at this directory
#[arg(short, long, value_hint(ValueHint::DirPath))]
pub dir: Option<PathBuf>,
/// Do not show interactive prompts if both `--lang` and `--dir` are set
///
/// This does nothing inside of non-interactive shells.
#[arg(
short,
long,
requires("lang"),
requires("dir"),
default_value_t = !std::io::stdout().is_terminal()
)]
pub non_interactive: bool,
}
/// Possible languages for configuration.
#[derive(clap::ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
enum Lang {
/// Generate a Lua config
Lua,
/// Generate a Rust config
Rust,
}
impl std::fmt::Display for Lang {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{self:?}")
}
}
//////////////////////////////////////////////////////////////////////
/// Generate a new config.
///
/// If `--non-interactive` is passed or the shell is non-interactive, this will not
/// output interactive prompts.
fn generate_config(args: ConfigGen) -> anyhow::Result<()> {
let interactive = !args.non_interactive;
if !interactive && (args.lang.is_none() || args.dir.is_none()) {
eprintln!("Error: both `--lang` and `--dir` must be set in a non-interactive shell.");
return Ok(());
}
if interactive {
cliclack::intro("Welcome to the interactive config generator!")?;
tokio::spawn(async {
tokio::signal::ctrl_c()
.await
.expect("failed to listen for ctrl-c");
});
}
enum Level {
Info,
Success,
}
let message = |msg: &str, level: Level| -> anyhow::Result<()> {
if interactive {
Ok(match level {
Level::Info => cliclack::log::info(msg),
Level::Success => cliclack::log::success(msg),
}?)
} else {
println!("{msg}");
Ok(())
}
};
let exit_message = |msg: &str| {
if interactive {
cliclack::outro_cancel(msg).expect("failed to display outro_cancel");
} else {
eprintln!("{msg}, exiting");
}
};
let lang = match args.lang {
Some(lang) => {
let msg = format!("Select a language:\n{lang} (from -l/--lang)");
message(&msg, Level::Success)?;
lang
}
None => {
assert!(interactive);
cliclack::select("Select a language:")
.items(&[(Lang::Lua, "Lua", ""), (Lang::Rust, "Rust", "")])
.interact()?
}
};
let default_dir = xdg::BaseDirectories::with_prefix("pinnacle")?.get_config_home();
let default_dir_clone = default_dir.clone();
let dir_validator = move |s: &String| {
let mut target_dir = if s.is_empty() {
default_dir_clone.clone()
} else {
PathBuf::from(
shellexpand::full(s)
.map_err(|err| format!("Directory expansion failed: {err}"))?
.to_string(),
)
};
if target_dir.is_relative() {
let mut new_dir = std::env::current_dir().map_err(|err| {
format!("Failed to get the current dir to resolve relative path: {err}")
})?;
new_dir.push(target_dir);
target_dir = new_dir;
}
match target_dir.try_exists() {
Ok(exists) => {
if exists {
if !target_dir.is_dir() {
Err(format!(
"`{}` exists but is not a directory",
target_dir.display()
))
} else if lang == Lang::Rust
&& std::fs::read_dir(&target_dir)
.map_err(|err| {
format!(
"Failed to check if `{}` is empty: {err}",
target_dir.display()
)
})?
.next()
.is_some()
{
Err(format!(
"`{}` exists but is not empty. Empty it to generate a Rust config in it.",
target_dir.display()
))
} else {
Ok(())
}
} else {
Ok(())
}
}
Err(err) => Err(format!(
"Failed to check if `{}` exists: {err}",
target_dir.display()
)),
}
};
let target_dir = match args.dir {
Some(dir) => {
let msg = format!(
"Choose a directory to place the config in:\n{} (from -d/--dir)",
dir.display()
);
message(&msg, Level::Success)?;
if lang == Lang::Rust && matches!(dir.try_exists(), Ok(true)) {
exit_message("Directory must be empty to create a Rust config in it");
anyhow::bail!("{msg}");
}
dir
}
None => {
assert!(interactive);
let dir: PathBuf = cliclack::input("Choose a directory to place the config in:")
.default_input(default_dir.to_string_lossy().as_ref())
.validate_interactively(dir_validator)
.interact()?;
let mut dir = shellexpand::path::full(&dir)?.to_path_buf();
if dir.is_relative() {
let mut new_dir = std::env::current_dir()?;
new_dir.push(dir);
dir = new_dir;
}
dir
}
};
if let Ok(false) = target_dir.try_exists() {
let msg = format!(
"`{}` doesn't exist and will be created.",
target_dir.display()
);
message(&msg, Level::Info)?;
}
if interactive {
let confirm_creation = cliclack::confirm(format!(
"Create a {} config inside `{}`?",
lang,
target_dir.display()
))
.initial_value(false)
.interact()?;
if !confirm_creation {
exit_message("Config generation cancelled.");
return Ok(());
}
}
std::fs::create_dir_all(&target_dir)?;
// Generate the config
let xdg_base_dirs = xdg::BaseDirectories::with_prefix("pinnacle")?;
let mut default_config_dir = xdg_base_dirs.get_data_file("default_config");
// %F = %Y-%m-%d or year-month-day in ISO 8601
// %T = %H:%M:%S
let now = format!("{}", chrono::Local::now().format("%F.%T"));
match lang {
Lang::Lua => {
default_config_dir.push("lua");
let mut files_to_backup: Vec<(String, String)> = Vec::new();
for file in std::fs::read_dir(&default_config_dir)? {
let file = file?;
let name = file.file_name();
let target_file = target_dir.join(&name);
if let Ok(true) = target_file.try_exists() {
let backup_name = format!("{}.{now}.bak", name.to_string_lossy());
files_to_backup.push((name.to_string_lossy().to_string(), backup_name));
}
}
if !files_to_backup.is_empty() {
let msg = files_to_backup
.iter()
.map(|(src, dst)| format!("{src} -> {dst}"))
.collect::<Vec<_>>()
.join("\n");
if interactive {
cliclack::note("The following files will be renamed:", msg)?;
let r#continue = cliclack::confirm("Continue?").interact()?;
if !r#continue {
exit_message("Config generation cancelled.");
return Ok(());
}
} else {
println!("The following files will be renamed:");
println!("{msg}");
}
for (src, dst) in files_to_backup.iter() {
std::fs::rename(target_dir.join(src), target_dir.join(dst))?;
}
message("Renamed old files", Level::Info)?;
}
dircpy::copy_dir(&default_config_dir, &target_dir)?;
}
Lang::Rust => {
default_config_dir.push("rust");
assert!(
std::fs::read_dir(&target_dir)?.next().is_none(),
"target directory was not empty"
);
dircpy::copy_dir(&default_config_dir, &target_dir)?;
}
}
message("Copied new config over", Level::Info)?;
let mut outro_msg = format!("{lang} config created in {}!", target_dir.display());
if lang == Lang::Rust {
outro_msg = format!(
"{outro_msg}\nYou may want to run `cargo build` in the \
config directory beforehand to avoid waiting when starting up Pinnacle."
);
}
if interactive {
cliclack::outro(outro_msg)?;
} else {
println!("{outro_msg}");
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
// TODO: find a way to test the interactive bits programmatically
#[test]
fn cli_config_gen_parses_correctly() -> anyhow::Result<()> {
let temp_dir = tempfile::tempdir()?;
let temp_dir = temp_dir.path().join("cli_config_gen_parses_correctly");
let cli = Cli::parse_from([
"pinnacle",
"config",
"gen",
"--lang",
"rust",
"--dir",
temp_dir
.to_str()
.ok_or(anyhow::anyhow!("not valid unicode"))?,
"--non-interactive",
]);
let expected_config_gen = ConfigGen {
lang: Some(Lang::Rust),
dir: Some(temp_dir.to_path_buf()),
non_interactive: true,
};
let Some(CliSubcommand::Config(ConfigSubcommand::Gen(config_gen))) = cli.subcommand else {
anyhow::bail!("cli.subcommand config_gen doesn't exist");
};
assert_eq!(config_gen, expected_config_gen);
let cli = Cli::parse_from(["pinnacle", "config", "gen", "--lang", "lua"]);
let expected_config_gen = ConfigGen {
lang: Some(Lang::Lua),
dir: None,
non_interactive: !std::io::stdout().is_terminal(),
};
let Some(CliSubcommand::Config(ConfigSubcommand::Gen(config_gen))) = cli.subcommand else {
anyhow::bail!("cli.subcommand config_gen doesn't exist");
};
assert_eq!(config_gen, expected_config_gen);
Ok(())
}
#[test]
fn non_interactive_config_gen_lua_works() -> anyhow::Result<()> {
let temp_dir = tempfile::tempdir()?;
let temp_dir = temp_dir.path().join("non_interactive_config_gen_lua_works");
let config_gen = ConfigGen {
lang: Some(Lang::Lua),
dir: Some(temp_dir.clone()),
non_interactive: true,
};
generate_config(config_gen)?;
assert!(matches!(
temp_dir.join("default_config.lua").try_exists(),
Ok(true)
));
assert!(matches!(
temp_dir.join("metaconfig.toml").try_exists(),
Ok(true)
));
assert!(matches!(
temp_dir.join(".luarc.json").try_exists(),
Ok(true)
));
Ok(())
}
#[test]
fn non_interactive_config_gen_rust_works() -> anyhow::Result<()> {
let temp_dir = tempfile::tempdir()?;
let temp_dir = temp_dir
.path()
.join("non_interactive_config_gen_rust_works");
let config_gen = ConfigGen {
lang: Some(Lang::Rust),
dir: Some(temp_dir.clone()),
non_interactive: true,
};
generate_config(config_gen)?;
assert!(matches!(
temp_dir.join("src/main.rs").try_exists(),
Ok(true)
));
assert!(matches!(
temp_dir.join("metaconfig.toml").try_exists(),
Ok(true)
));
assert!(matches!(temp_dir.join("Cargo.toml").try_exists(), Ok(true)));
Ok(())
}
#[test]
fn non_interactive_config_gen_lua_backup_works() -> anyhow::Result<()> {
let temp_dir = tempfile::tempdir()?;
let temp_dir = temp_dir
.path()
.join("non_interactive_config_gen_lua_backup_works");
let config_gen = ConfigGen {
lang: Some(Lang::Lua),
dir: Some(temp_dir.clone()),
non_interactive: true,
};
generate_config(config_gen.clone())?;
generate_config(config_gen)?;
let generated_file_count = std::fs::read_dir(&temp_dir)?
.collect::<Result<Vec<_>, _>>()?
.len();
// 3 for original, 3 for backups
assert_eq!(generated_file_count, 6);
Ok(())
}
#[test]
fn non_interactive_config_gen_rust_nonempty_dir_does_not_work() -> anyhow::Result<()> {
let temp_dir = tempfile::tempdir()?;
let temp_dir = temp_dir
.path()
.join("non_interactive_config_gen_rust_nonempty_dir_does_not_work");
let config_gen = ConfigGen {
lang: Some(Lang::Rust),
dir: Some(temp_dir),
non_interactive: true,
};
generate_config(config_gen.clone())?;
assert!(generate_config(config_gen.clone()).is_err());
Ok(())
}
}

View file

@ -33,6 +33,7 @@ use sysinfo::ProcessRefreshKind;
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
use toml::Table; use toml::Table;
use tracing::{debug, error, info};
use xdg::BaseDirectories; use xdg::BaseDirectories;
use xkbcommon::xkb::Keysym; use xkbcommon::xkb::Keysym;
@ -171,10 +172,27 @@ pub struct Config {
config_join_handle: Option<JoinHandle<()>>, config_join_handle: Option<JoinHandle<()>>,
config_reload_on_crash_token: Option<RegistrationToken>, config_reload_on_crash_token: Option<RegistrationToken>,
pub no_config: bool,
config_dir: Option<PathBuf>,
} }
impl Config { impl Config {
pub fn clear(&mut self, loop_handle: &LoopHandle<State>) { pub fn new(no_config: bool, config_dir: Option<PathBuf>) -> Self {
Config {
no_config,
config_dir,
..Default::default()
}
}
pub fn dir(&self, xdg_base_dirs: &BaseDirectories) -> PathBuf {
self.config_dir
.clone()
.unwrap_or_else(|| get_config_dir(xdg_base_dirs))
}
fn clear(&mut self, loop_handle: &LoopHandle<State>) {
self.window_rules.clear(); self.window_rules.clear();
self.connector_saved_states.clear(); self.connector_saved_states.clear();
if let Some(join_handle) = self.config_join_handle.take() { if let Some(join_handle) = self.config_join_handle.take() {
@ -208,7 +226,7 @@ fn parse_metaconfig(config_dir: &Path) -> anyhow::Result<Metaconfig> {
/// Get the config dir. This is $PINNACLE_CONFIG_DIR, then $XDG_CONFIG_HOME/pinnacle, /// Get the config dir. This is $PINNACLE_CONFIG_DIR, then $XDG_CONFIG_HOME/pinnacle,
/// then ~/.config/pinnacle. /// then ~/.config/pinnacle.
pub fn get_config_dir(xdg_base_dirs: &BaseDirectories) -> PathBuf { fn get_config_dir(xdg_base_dirs: &BaseDirectories) -> PathBuf {
let config_dir = std::env::var("PINNACLE_CONFIG_DIR") let config_dir = std::env::var("PINNACLE_CONFIG_DIR")
.ok() .ok()
.and_then(|s| Some(PathBuf::from(shellexpand::full(&s).ok()?.to_string()))); .and_then(|s| Some(PathBuf::from(shellexpand::full(&s).ok()?.to_string())));
@ -221,28 +239,34 @@ impl State {
/// ///
/// If this method is called while a config is already running, it will be replaced. /// If this method is called while a config is already running, it will be replaced.
pub fn start_config(&mut self, config_dir: impl AsRef<Path>) -> anyhow::Result<()> { pub fn start_config(&mut self, config_dir: impl AsRef<Path>) -> anyhow::Result<()> {
let config_dir = config_dir.as_ref(); let mut config_dir = config_dir.as_ref();
tracing::info!("Starting config at {}", config_dir.display()); let default_lua_config_dir = self
.xdg_base_dirs
let default_lua_config_dir = self.xdg_base_dirs.get_data_file("default_config"); .get_data_file("default_config")
.join("lua");
let load_default_config = |state: &mut State, reason: &str| { let load_default_config = |state: &mut State, reason: &str| {
tracing::error!( error!(
"Unable to load config at {}: {reason}", "Unable to load config at {}: {reason}",
config_dir.display() config_dir.display()
); );
tracing::info!("Falling back to default Lua config"); info!("Falling back to default Lua config");
state.start_config(&default_lua_config_dir) state.start_config(&default_lua_config_dir)
}; };
// If `--no-config` was set, still load the keybinds from the default metaconfig
if self.config.no_config {
config_dir = &default_lua_config_dir
}
let metaconfig = match parse_metaconfig(config_dir) { let metaconfig = match parse_metaconfig(config_dir) {
Ok(metaconfig) => metaconfig, Ok(metaconfig) => metaconfig,
Err(err) => { Err(err) => {
// Stops infinite recursion if somehow the default_config dir is screwed up // Stops infinite recursion if somehow the default_config dir is screwed up
if config_dir == default_lua_config_dir { if config_dir == default_lua_config_dir {
tracing::error!("The metaconfig at the default Lua config directory is either malformed or missing."); error!("The metaconfig at the default Lua config directory is either malformed or missing.");
tracing::error!( error!(
"If you have not touched {}, this is a bug and you should file an issue (pretty please with a cherry on top?).", "If you have not touched {}, this is a bug and you should file an issue (pretty please with a cherry on top?).",
default_lua_config_dir.display() default_lua_config_dir.display()
); );
@ -252,14 +276,35 @@ impl State {
} }
}; };
tracing::debug!("Clearing tags"); let reload_keybind = metaconfig.reload_keybind;
let kill_keybind = metaconfig.kill_keybind;
let reload_mask = ModifierMask::from(reload_keybind.modifiers);
let kill_mask = ModifierMask::from(kill_keybind.modifiers);
let reload_keybind = (reload_mask, Keysym::from(reload_keybind.key as u32));
let kill_keybind = (kill_mask, Keysym::from(kill_keybind.key as u32));
self.input_state.reload_keybind = Some(reload_keybind);
self.input_state.kill_keybind = Some(kill_keybind);
if self.config.no_config {
info!("`--no-config` was set, not spawning config");
return Ok(());
}
assert!(!self.config.no_config);
// Clear state
debug!("Clearing tags");
for output in self.space.outputs() { for output in self.space.outputs() {
output.with_state(|state| state.tags.clear()); output.with_state(|state| state.tags.clear());
} }
TagId::reset(); TagId::reset();
tracing::debug!("Clearing input state"); debug!("Clearing input state");
self.input_state.clear(); self.input_state.clear();
@ -293,9 +338,6 @@ impl State {
self.start_grpc_server(socket_dir.as_path())?; self.start_grpc_server(socket_dir.as_path())?;
} }
let reload_keybind = metaconfig.reload_keybind;
let kill_keybind = metaconfig.kill_keybind;
let mut command = metaconfig.command.iter(); let mut command = metaconfig.command.iter();
let arg0 = match command.next() { let arg0 = match command.next() {
@ -305,32 +347,26 @@ impl State {
let command = command.collect::<Vec<_>>(); let command = command.collect::<Vec<_>>();
tracing::debug!(arg0, ?command); debug!(arg0, ?command);
let envs = metaconfig let envs = metaconfig
.envs .envs
.unwrap_or(toml::map::Map::new()) .unwrap_or(toml::map::Map::new())
.into_iter() .into_iter()
.filter_map(|(key, val)| { .map(|(key, val)| -> anyhow::Result<Option<(String, String)>> {
if let toml::Value::String(string) = val { if let toml::Value::String(string) = val {
Some(( Ok(Some((key, shellexpand::full(&string)?.to_string())))
key,
shellexpand::full_with_context(
&string,
|| std::env::var("HOME").ok(),
// Expand nonexistent vars to an empty string instead of crashing
|var| Ok::<_, ()>(Some(std::env::var(var).unwrap_or("".to_string()))),
)
.ok()?
.to_string(),
))
} else { } else {
None Ok(None)
} }
}) })
.collect::<Vec<_>>(); .collect::<Result<Vec<_>, _>>()?
.into_iter()
.flatten();
tracing::debug!("Config envs are {envs:?}"); debug!("Config envs are {envs:?}");
info!("Starting config at {}", config_dir.display());
let mut child = match tokio::process::Command::new(arg0) let mut child = match tokio::process::Command::new(arg0)
.args(command) .args(command)
@ -345,23 +381,14 @@ impl State {
Err(err) => return load_default_config(self, &err.to_string()), Err(err) => return load_default_config(self, &err.to_string()),
}; };
tracing::info!("Started config with {:?}", metaconfig.command); info!("Started config with {:?}", metaconfig.command);
let reload_mask = ModifierMask::from(reload_keybind.modifiers);
let kill_mask = ModifierMask::from(kill_keybind.modifiers);
let reload_keybind = (reload_mask, Keysym::from(reload_keybind.key as u32));
let kill_keybind = (kill_mask, Keysym::from(kill_keybind.key as u32));
self.input_state.reload_keybind = Some(reload_keybind);
self.input_state.kill_keybind = Some(kill_keybind);
let (pinger, ping_source) = calloop::ping::make_ping()?; let (pinger, ping_source) = calloop::ping::make_ping()?;
let token = self let token = self
.loop_handle .loop_handle
.insert_source(ping_source, move |_, _, state| { .insert_source(ping_source, move |_, _, state| {
tracing::error!("Config crashed! Falling back to default Lua config"); error!("Config crashed! Falling back to default Lua config");
state state
.start_config(&default_lua_config_dir) .start_config(&default_lua_config_dir)
.expect("failed to start default lua config"); .expect("failed to start default lua config");
@ -422,7 +449,7 @@ impl State {
.starts_with("pinnacle-grpc") .starts_with("pinnacle-grpc")
}) })
{ {
tracing::debug!("Removing socket at {:?}", file.path()); debug!("Removing socket at {:?}", file.path());
std::fs::remove_file(file.path()) std::fs::remove_file(file.path())
.context(format!("Failed to remove old socket at {:?}", file.path()))?; .context(format!("Failed to remove old socket at {:?}", file.path()))?;
} }
@ -439,7 +466,7 @@ impl State {
self.loop_handle self.loop_handle
.insert_source(grpc_receiver, |msg, _, state| match msg { .insert_source(grpc_receiver, |msg, _, state| match msg {
Event::Msg(f) => f(state), Event::Msg(f) => f(state),
Event::Closed => tracing::error!("grpc receiver was closed"), Event::Closed => error!("grpc receiver was closed"),
}) })
.expect("failed to insert grpc_receiver into loop"); .expect("failed to insert grpc_receiver into loop");
@ -474,7 +501,7 @@ impl State {
Some(_) => { Some(_) => {
self.grpc_server_join_handle = Some(tokio::spawn(async move { self.grpc_server_join_handle = Some(tokio::spawn(async move {
if let Err(err) = grpc_server.serve_with_incoming(uds_stream).await { if let Err(err) = grpc_server.serve_with_incoming(uds_stream).await {
tracing::error!("gRPC server error: {err}"); error!("gRPC server error: {err}");
} }
})); }));
} }
@ -486,7 +513,7 @@ impl State {
move |state| { move |state| {
state.grpc_server_join_handle = Some(tokio::spawn(async move { state.grpc_server_join_handle = Some(tokio::spawn(async move {
if let Err(err) = grpc_server.serve_with_incoming(uds_stream).await { if let Err(err) = grpc_server.serve_with_incoming(uds_stream).await {
tracing::error!("gRPC server error: {err}"); error!("gRPC server error: {err}");
} }
})); }));
}, },
@ -503,7 +530,7 @@ mod tests {
use std::env::var; use std::env::var;
#[test] #[test]
fn config_dir_with_relative_env_works() -> anyhow::Result<()> { fn get_config_dir_with_relative_env_works() -> anyhow::Result<()> {
let relative_path = "api/rust/examples/default_config"; let relative_path = "api/rust/examples/default_config";
temp_env::with_var("PINNACLE_CONFIG_DIR", Some(relative_path), || { temp_env::with_var("PINNACLE_CONFIG_DIR", Some(relative_path), || {
@ -519,7 +546,7 @@ mod tests {
} }
#[test] #[test]
fn config_dir_with_tilde_env_works() -> anyhow::Result<()> { fn get_config_dir_with_tilde_env_works() -> anyhow::Result<()> {
temp_env::with_var("PINNACLE_CONFIG_DIR", Some("~/some/dir/somewhere/"), || { temp_env::with_var("PINNACLE_CONFIG_DIR", Some("~/some/dir/somewhere/"), || {
let xdg_base_dirs = BaseDirectories::with_prefix("pinnacle")?; let xdg_base_dirs = BaseDirectories::with_prefix("pinnacle")?;
let expected = PathBuf::from(var("HOME")?).join("some/dir/somewhere"); let expected = PathBuf::from(var("HOME")?).join("some/dir/somewhere");
@ -531,7 +558,7 @@ mod tests {
} }
#[test] #[test]
fn config_dir_with_absolute_env_works() -> anyhow::Result<()> { fn get_config_dir_with_absolute_env_works() -> anyhow::Result<()> {
let absolute_path = "/its/morbin/time"; let absolute_path = "/its/morbin/time";
temp_env::with_var("PINNACLE_CONFIG_DIR", Some(absolute_path), || { temp_env::with_var("PINNACLE_CONFIG_DIR", Some(absolute_path), || {
@ -545,7 +572,7 @@ mod tests {
} }
#[test] #[test]
fn config_dir_without_env_and_with_xdg_works() -> anyhow::Result<()> { fn get_config_dir_without_env_and_with_xdg_works() -> anyhow::Result<()> {
let xdg_config_home = "/some/different/xdg/config/path"; let xdg_config_home = "/some/different/xdg/config/path";
temp_env::with_vars( temp_env::with_vars(
@ -565,7 +592,7 @@ mod tests {
} }
#[test] #[test]
fn config_dir_without_env_and_without_xdg_works() -> anyhow::Result<()> { fn get_config_dir_without_env_and_without_xdg_works() -> anyhow::Result<()> {
temp_env::with_vars( temp_env::with_vars(
[ [
("PINNACLE_CONFIG_DIR", None::<&str>), ("PINNACLE_CONFIG_DIR", None::<&str>),
@ -685,4 +712,26 @@ mod tests {
Ok(()) Ok(())
} }
// Ayo can we get Kotlin style test function naming so I can do something like
// fn `config.dir with --config-dir returns correct dir`() {}
#[test]
fn config_dot_dir_with_dash_dash_config_dir_returns_correct_dir() -> anyhow::Result<()> {
let dir = PathBuf::from("/some/dir/here");
let config = Config::new(false, Some(dir.clone()));
assert_eq!(config.dir(&BaseDirectories::with_prefix("pinnacle")?), dir);
Ok(())
}
#[test]
fn config_dot_dir_without_dash_dash_config_dir_returns_correct_dir() -> anyhow::Result<()> {
let config = Config::new(false, None);
let xdg_base_dirs = BaseDirectories::with_prefix("pinnacle")?;
assert_eq!(config.dir(&xdg_base_dirs), get_config_dir(&xdg_base_dirs));
Ok(())
}
} }

View file

@ -329,7 +329,7 @@ impl State {
self.shutdown(); self.shutdown();
} }
Some(KeyAction::ReloadConfig) => { Some(KeyAction::ReloadConfig) => {
self.start_config(crate::config::get_config_dir(&self.xdg_base_dirs)) self.start_config(self.config.dir(&self.xdg_base_dirs))
.expect("failed to restart config"); .expect("failed to restart config");
} }
None => (), None => (),

View file

@ -12,7 +12,7 @@
#![warn(clippy::unwrap_used)] #![warn(clippy::unwrap_used)]
use anyhow::Context; use anyhow::Context;
use clap::Parser; use cli::Cli;
use nix::unistd::Uid; use nix::unistd::Uid;
use tracing::{info, level_filters::LevelFilter, warn}; use tracing::{info, level_filters::LevelFilter, warn};
use tracing_appender::rolling::Rotation; use tracing_appender::rolling::Rotation;
@ -21,6 +21,7 @@ use xdg::BaseDirectories;
mod api; mod api;
mod backend; mod backend;
mod cli;
mod config; mod config;
mod cursor; mod cursor;
mod focus; mod focus;
@ -34,30 +35,6 @@ mod state;
mod tag; mod tag;
mod window; mod window;
#[derive(clap::Args, Debug)]
#[group(id = "backend", required = false, multiple = false)]
struct Backends {
#[arg(long, group = "backend")]
/// Run Pinnacle in a window in your graphical environment
winit: bool,
#[arg(long, group = "backend")]
/// Run Pinnacle from a tty
udev: bool,
}
#[derive(clap::Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
#[command(flatten)]
backend: Backends,
#[arg(long)]
/// Allow running Pinnacle as root (this is NOT recommended)
allow_root: bool,
#[arg(long, requires = "backend")]
/// Force Pinnacle to run with the provided backend
force: bool,
}
#[tokio::main] #[tokio::main]
async fn main() -> anyhow::Result<()> { async fn main() -> anyhow::Result<()> {
let xdg_state_dir = BaseDirectories::with_prefix("pinnacle")?.get_state_home(); let xdg_state_dir = BaseDirectories::with_prefix("pinnacle")?.get_state_home();
@ -96,13 +73,15 @@ async fn main() -> anyhow::Result<()> {
.with(stdout_layer) .with(stdout_layer)
.init(); .init();
let args = Args::parse(); let Some(cli) = Cli::parse_and_prompt() else {
return Ok(());
};
if Uid::effective().is_root() { if Uid::effective().is_root() {
if !args.allow_root { if !cli.allow_root {
warn!("You are trying to run Pinnacle as root."); warn!("You are trying to run Pinnacle as root.");
warn!("This is NOT recommended."); warn!("This is NOT recommended.");
warn!("To run Pinnacle as root, pass in the --allow-root flag."); warn!("To run Pinnacle as root, pass in the `--allow-root` flag.");
warn!("Again, this is NOT recommended."); warn!("Again, this is NOT recommended.");
return Ok(()); return Ok(());
} else { } else {
@ -118,48 +97,47 @@ async fn main() -> anyhow::Result<()> {
warn!("You may see LOTS of file descriptors open under Pinnacle."); warn!("You may see LOTS of file descriptors open under Pinnacle.");
} }
match (args.backend.winit, args.backend.udev, args.force) { match (cli.backend, cli.force) {
(false, false, _) => { (None, _) => {
if in_graphical_env { if in_graphical_env {
info!("Starting winit backend"); info!("Starting winit backend");
crate::backend::winit::run_winit()?; crate::backend::winit::run_winit(cli.no_config, cli.config_dir)?;
} else { } else {
info!("Starting udev backend"); info!("Starting udev backend");
crate::backend::udev::run_udev()?; crate::backend::udev::run_udev(cli.no_config, cli.config_dir)?;
} }
} }
(true, false, force) => { (Some(cli::Backend::Winit), force) => {
if !in_graphical_env { if !in_graphical_env {
if force { if force {
warn!("Starting winit backend with no detected graphical environment"); warn!("Starting winit backend with no detected graphical environment");
crate::backend::winit::run_winit()?; crate::backend::winit::run_winit(cli.no_config, cli.config_dir)?;
} else { } else {
warn!("Both WAYLAND_DISPLAY and DISPLAY are not set."); warn!("Both WAYLAND_DISPLAY and DISPLAY are not set.");
warn!("If you are trying to run the winit backend in a tty, it won't work."); warn!("If you are trying to run the winit backend in a tty, it won't work.");
warn!("If you really want to, additionally pass in the --force flag."); warn!("If you really want to, additionally pass in the `--force` flag.");
} }
} else { } else {
info!("Starting winit backend"); info!("Starting winit backend");
crate::backend::winit::run_winit()?; crate::backend::winit::run_winit(cli.no_config, cli.config_dir)?;
} }
} }
(false, true, force) => { (Some(cli::Backend::Udev), force) => {
if in_graphical_env { if in_graphical_env {
if force { if force {
warn!("Starting udev backend with a detected graphical environment"); warn!("Starting udev backend with a detected graphical environment");
crate::backend::udev::run_udev()?; crate::backend::udev::run_udev(cli.no_config, cli.config_dir)?;
} else { } else {
warn!("WAYLAND_DISPLAY and/or DISPLAY are set."); warn!("WAYLAND_DISPLAY and/or DISPLAY are set.");
warn!("If you are trying to run the udev backend in a graphical environment,"); warn!("If you are trying to run the udev backend in a graphical environment,");
warn!("it won't work and may mess some things up."); warn!("it won't work and may mess some things up.");
warn!("If you really want to, additionally pass in the --force flag."); warn!("If you really want to, additionally pass in the `--force` flag.");
} }
} else { } else {
info!("Starting udev backend"); info!("Starting udev backend");
crate::backend::udev::run_udev()?; crate::backend::udev::run_udev(cli.no_config, cli.config_dir)?;
} }
} }
_ => unreachable!(),
} }
Ok(()) Ok(())

View file

@ -34,8 +34,9 @@ use smithay::{
}, },
xwayland::{X11Wm, XWayland, XWaylandEvent}, xwayland::{X11Wm, XWayland, XWaylandEvent},
}; };
use std::{cell::RefCell, sync::Arc, time::Duration}; use std::{cell::RefCell, path::PathBuf, sync::Arc, time::Duration};
use sysinfo::{ProcessRefreshKind, RefreshKind}; use sysinfo::{ProcessRefreshKind, RefreshKind};
use tracing::{error, info};
use xdg::BaseDirectories; use xdg::BaseDirectories;
use crate::input::InputState; use crate::input::InputState;
@ -108,11 +109,13 @@ impl State {
display: Display<Self>, display: Display<Self>,
loop_signal: LoopSignal, loop_signal: LoopSignal,
loop_handle: LoopHandle<'static, Self>, loop_handle: LoopHandle<'static, Self>,
no_config: bool,
config_dir: Option<PathBuf>,
) -> anyhow::Result<Self> { ) -> anyhow::Result<Self> {
let socket = ListeningSocketSource::new_auto()?; let socket = ListeningSocketSource::new_auto()?;
let socket_name = socket.socket_name().to_os_string(); let socket_name = socket.socket_name().to_os_string();
tracing::info!( info!(
"Setting WAYLAND_DISPLAY to {}", "Setting WAYLAND_DISPLAY to {}",
socket_name.to_string_lossy() socket_name.to_string_lossy()
); );
@ -124,15 +127,15 @@ impl State {
// //
// To fix this, I just set the limit to be higher. As Pinnacle is the whole graphical // To fix this, I just set the limit to be higher. As Pinnacle is the whole graphical
// environment, I *think* this is ok. // environment, I *think* this is ok.
tracing::info!("Trying to raise file descriptor limit..."); info!("Trying to raise file descriptor limit...");
if let Err(err) = nix::sys::resource::setrlimit( if let Err(err) = nix::sys::resource::setrlimit(
nix::sys::resource::Resource::RLIMIT_NOFILE, nix::sys::resource::Resource::RLIMIT_NOFILE,
65536, 65536,
65536 * 2, 65536 * 2,
) { ) {
tracing::error!("Could not raise fd limit: errno {err}"); error!("Could not raise fd limit: errno {err}");
} else { } else {
tracing::info!("Fd raise success!"); info!("Fd raise success!");
} }
loop_handle.insert_source(socket, |stream, _metadata, data| { loop_handle.insert_source(socket, |stream, _metadata, data| {
@ -158,9 +161,7 @@ impl State {
)?; )?;
loop_handle.insert_idle(|state| { loop_handle.insert_idle(|state| {
if let Err(err) = if let Err(err) = state.start_config(state.config.dir(&state.xdg_base_dirs)) {
state.start_config(crate::config::get_config_dir(&state.xdg_base_dirs))
{
panic!("failed to start config: {err}"); panic!("failed to start config: {err}");
} }
}); });
@ -212,7 +213,7 @@ impl State {
} }
}); });
if let Err(err) = res { if let Err(err) = res {
tracing::error!("Failed to insert XWayland source into loop: {err}"); error!("Failed to insert XWayland source into loop: {err}");
} }
xwayland xwayland
}; };
@ -254,7 +255,7 @@ impl State {
output_focus_stack: FocusStack::default(), output_focus_stack: FocusStack::default(),
z_index_stack: FocusStack::default(), z_index_stack: FocusStack::default(),
config: Config::default(), config: Config::new(no_config, config_dir),
seat, seat,
@ -302,7 +303,7 @@ impl State {
} }
pub fn shutdown(&self) { pub fn shutdown(&self) {
tracing::info!("Shutting down Pinnacle"); info!("Shutting down Pinnacle");
self.loop_signal.stop(); self.loop_signal.stop();
} }
} }