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"
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]]
name = "anstream"
version = "0.6.11"
@ -295,6 +310,17 @@ dependencies = [
"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]]
name = "bumpalo"
version = "3.14.0"
@ -397,6 +423,20 @@ dependencies = [
"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]]
name = "clap"
version = "4.5.1"
@ -437,6 +477,19 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "color_quant"
version = "1.1.0"
@ -468,6 +521,19 @@ dependencies = [
"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]]
name = "const_format"
version = "0.2.32"
@ -528,6 +594,19 @@ dependencies = [
"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]]
name = "crossbeam-channel"
version = "0.5.11"
@ -556,6 +635,15 @@ dependencies = [
"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]]
name = "crossbeam-utils"
version = "0.8.19"
@ -577,6 +665,17 @@ dependencies = [
"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]]
name = "dirs"
version = "5.0.1"
@ -670,6 +769,12 @@ version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
[[package]]
name = "encode_unicode"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
[[package]]
name = "encoding_rs"
version = "0.8.33"
@ -1013,6 +1118,29 @@ dependencies = [
"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]]
name = "icrate"
version = "0.0.4"
@ -1056,6 +1184,19 @@ dependencies = [
"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]]
name = "input"
version = "0.9.0"
@ -1075,6 +1216,15 @@ version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "io-lifetimes"
version = "1.0.11"
@ -1147,6 +1297,16 @@ dependencies = [
"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]]
name = "khronos_api"
version = "3.1.0"
@ -1451,6 +1611,12 @@ dependencies = [
"syn",
]
[[package]]
name = "number_prefix"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
[[package]]
name = "objc-sys"
version = "0.3.2"
@ -1503,6 +1669,15 @@ dependencies = [
"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]]
name = "overload"
version = "0.1.1"
@ -1586,7 +1761,10 @@ version = "0.0.1"
dependencies = [
"anyhow",
"bitflags 2.4.2",
"chrono",
"clap",
"cliclack",
"dircpy",
"image",
"nix",
"pinnacle-api-defs",
@ -1667,6 +1845,12 @@ dependencies = [
"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]]
name = "powerfmt"
version = "0.2.0"
@ -2023,7 +2207,9 @@ version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da03fa3b94cc19e3ebfc88c4229c49d8f08cdbd1228870a45f0ffdf84988e14b"
dependencies = [
"bstr",
"dirs",
"os_str_bytes",
]
[[package]]
@ -2050,6 +2236,12 @@ version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7"
[[package]]
name = "smawk"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c"
[[package]]
name = "smithay"
version = "0.3.0"
@ -2212,6 +2404,17 @@ dependencies = [
"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]]
name = "thiserror"
version = "1.0.57"
@ -2565,12 +2768,24 @@ version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "unicode-linebreak"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
[[package]]
name = "unicode-segmentation"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
[[package]]
name = "unicode-width"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
[[package]]
name = "unicode-xid"
version = "0.2.4"
@ -3271,3 +3486,23 @@ dependencies = [
"quote",
"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/"
[workspace.dependencies]
# Tokio
tokio = { version = "1.36.0", features = ["macros", "rt-multi-thread"]}
tokio-stream = { version = "0.1.14", features = ["net"] }
# gRPC
prost = "0.12.3"
tonic = "0.11.0"
tonic-reflection = "0.11.0"
tonic-build = "0.11.0"
# API definitions
pinnacle-api-defs = { path = "./pinnacle-api-defs" }
# Misc.
xkbcommon = "0.7.0"
xdg = "2.5.2"
#################################################################################
########################################################################yo😎###########
[package]
name = "pinnacle"
@ -34,38 +35,42 @@ repository.workspace = true
keywords = ["wayland", "compositor", "smithay", "lua"]
[dependencies]
# Smithay
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" }
# Tracing
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "registry"] }
tracing-appender = "0.2.3"
# Errors
anyhow = { version = "1.0.79", features = ["backtrace"] }
thiserror = "1.0.57"
# xcursor stuff
xcursor = { version = "0.3.5" }
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"] }
toml = "0.8.10"
shellexpand = "3.1.0"
clap = { version = "4.5.1", features = ["derive"] }
shellexpand = { version = "3.1.0", features = ["path"] }
x11rb = { version = "0.13.0", default-features = false, features = ["composite"] }
xkbcommon = { workspace = true }
xdg = { workspace = true }
sysinfo = "0.30.5"
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 }
dircpy = "0.3.16"
chrono = "0.4.34"
[build-dependencies]
xdg = { workspace = true }

107
README.md
View file

@ -13,10 +13,11 @@ https://github.com/Ottatop/pinnacle/assets/120758733/c175ba80-9796-4759-92c3-1d7
- [Configuration](#configuration)
- [Out-of-the-box configurations](#out-of-the-box-configurations)
- [Custom configuration](#custom-configuration)
- [Lua](#lua)
- [Generating a config](#generating-a-config)
- [More on configuration and the `metaconfig.toml` file](#more-on-configuration-and-the-metaconfig.toml-file)
- [The `metaconfig.toml` file](#the-metaconfig.toml-file)
- [Lua Language Server completion](#lua-language-server-completion)
- [Rust](#rust)
- [API References](#api-references)
- [API references](#api-references)
- [Controls](#controls)
- [Feature Requests, Bug Reports, Contributions, and Questions](#feature-requests-bug-reports-contributions-and-questions)
- [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)
for Wayland.
It can be configured through either Lua or Rust.
> ### More video examples below!
> <details>
>
@ -96,7 +95,9 @@ cargo build [--release]
> [!NOTE]
> On build, [`build.rs`](build.rs) will:
> - 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`
# Running
@ -122,14 +123,13 @@ Pinnacle is configured in your choice of Lua or Rust.
## Out-of-the-box configurations
If you just want to test Pinnacle out without copying stuff to your config directory,
run one of the following in the crate root:
```sh
# For a Lua configuration
PINNACLE_CONFIG_DIR="./api/lua/examples/default" cargo run
# 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
cargo run -- -c "./api/lua/examples/default"
# For a Rust configuration
PINNACLE_CONFIG_DIR="./api/rust/examples/default_config" cargo run
cargo run -- -c "./api/rust/examples/default_config"
```
## Custom configuration
@ -137,58 +137,65 @@ PINNACLE_CONFIG_DIR="./api/rust/examples/default_config" cargo run
> [!IMPORTANT]
> 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 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
$PINNACLE_CONFIG_DIR
$XDG_CONFIG_HOME/pinnacle
~/.config/pinnacle # Only if $XDG_CONFIG_HOME is not defined
cargo run -- config gen
```
The `metaconfig.toml` file provides information on what config to run, kill and reload keybinds,
and any environment variables you want set. For more details, see the provided
[`metaconfig.toml`](api/lua/examples/default/metaconfig.toml) file.
This will prompt you to choose a language (Lua or Rust) and directory to put the config in.
It will then generate a config at that directory. If Lua is chosen and there are conflicting
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
For custom configuration in Lua, copy the contents of `~/.local/share/pinnacle/default_config` to
`~/.config/pinnacle` (or `$XDG_CONFIG_HOME/pinnacle`):
```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.
## More on configuration and the `metaconfig.toml` file
Pinnacle is configured purely through IPC using [gRPC](https://grpc.io/). This is done through
configuration clients that use the [Lua](api/lua) and [Rust](api/rust) interface libraries.
> If you rename `default_config.lua`, make sure `command` in your `metaconfig.toml` is updated to reflect that.
> If it isn't, the compositor will load the default config instead.
As the compositor has no direct integration with these clients, it must know what it needs to run
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
and will set the correct workspace library files for use with the
[Lua language server](https://github.com/LuaLS/lua-language-server).
### Rust
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
## API references
<b>Lua: https://pinnacle-comp.github.io/lua-reference/main.<br>
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/protocol");
@ -6,51 +8,60 @@ fn main() {
let proto_dir = xdg.place_data_file("protobuf").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 copy_protos = format!("cp -r ./api/protocol {proto_dir:?}");
let remove_default_config = format!("rm -r {default_config_dir:?}");
let copy_default_config = format!("cp -r ./api/lua/examples/default {default_config_dir:?}");
let remove_default_config_dir = format!("rm -r {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(&remove_protos)
.spawn()
.unwrap()
.wait()
.unwrap();
.spawn()?
.wait()?;
std::process::Command::new("/bin/sh")
Command::new("/bin/sh")
.arg("-c")
.arg(&copy_protos)
.spawn()
.unwrap()
.wait()
.unwrap();
.spawn()?
.wait()?;
std::process::Command::new("/bin/sh")
Command::new("/bin/sh")
.arg("-c")
.arg(&remove_default_config)
.spawn()
.unwrap()
.wait()
.unwrap();
.arg(&remove_default_config_dir)
.spawn()?
.wait()?;
std::process::Command::new("/bin/sh")
std::fs::create_dir_all(&default_config_dir)?;
Command::new("/bin/sh")
.arg("-c")
.arg(&copy_default_config)
.spawn()
.unwrap()
.wait()
.unwrap();
.arg(&copy_default_lua_config)
.spawn()?
.wait()?;
Command::new("/bin/sh")
.arg("-c")
.arg(&copy_default_rust_config)
.spawn()?
.wait()?;
std::env::set_current_dir("api/lua").unwrap();
std::process::Command::new("luarocks")
Command::new("luarocks")
.arg("make")
.arg("--local")
.spawn()
.expect("Luarocks is not installed")
.wait()
.unwrap();
.wait()?;
Ok(())
}

View file

@ -3,7 +3,7 @@
use std::{
collections::{HashMap, HashSet},
ffi::OsString,
path::Path,
path::{Path, PathBuf},
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 display = Display::new()?;
@ -283,6 +283,8 @@ pub fn run_udev() -> anyhow::Result<()> {
display,
event_loop.get_signal(),
event_loop.handle(),
no_config,
config_dir,
)?;
// Initialize the udev backend

View file

@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-3.0-or-later
use std::{ffi::OsString, time::Duration};
use std::{ffi::OsString, path::PathBuf, time::Duration};
use smithay::{
backend::{
@ -62,7 +62,7 @@ impl Backend {
}
/// 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 display: Display<State> = Display::new()?;
@ -153,16 +153,20 @@ pub fn run_winit() -> anyhow::Result<()> {
tracing::info!("EGL hardware-acceleration enabled");
}
let mut state = State::init(
Backend::Winit(Winit {
let backend = Backend::Winit(Winit {
backend: winit_backend,
damage_tracker: OutputDamageTracker::from_output(&output),
dmabuf_state,
full_redraw: 0,
}),
});
let mut state = State::init(
backend,
display,
event_loop.get_signal(),
evt_loop_handle,
no_config,
config_dir,
)?;
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 toml::Table;
use tracing::{debug, error, info};
use xdg::BaseDirectories;
use xkbcommon::xkb::Keysym;
@ -171,10 +172,27 @@ pub struct Config {
config_join_handle: Option<JoinHandle<()>>,
config_reload_on_crash_token: Option<RegistrationToken>,
pub no_config: bool,
config_dir: Option<PathBuf>,
}
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.connector_saved_states.clear();
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,
/// 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")
.ok()
.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.
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.get_data_file("default_config");
let default_lua_config_dir = self
.xdg_base_dirs
.get_data_file("default_config")
.join("lua");
let load_default_config = |state: &mut State, reason: &str| {
tracing::error!(
error!(
"Unable to load config at {}: {reason}",
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)
};
// 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) {
Ok(metaconfig) => metaconfig,
Err(err) => {
// Stops infinite recursion if somehow the default_config dir is screwed up
if config_dir == default_lua_config_dir {
tracing::error!("The metaconfig at the default Lua config directory is either malformed or missing.");
tracing::error!(
error!("The metaconfig at the default Lua config directory is either malformed or missing.");
error!(
"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()
);
@ -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() {
output.with_state(|state| state.tags.clear());
}
TagId::reset();
tracing::debug!("Clearing input state");
debug!("Clearing input state");
self.input_state.clear();
@ -293,9 +338,6 @@ impl State {
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 arg0 = match command.next() {
@ -305,32 +347,26 @@ impl State {
let command = command.collect::<Vec<_>>();
tracing::debug!(arg0, ?command);
debug!(arg0, ?command);
let envs = metaconfig
.envs
.unwrap_or(toml::map::Map::new())
.into_iter()
.filter_map(|(key, val)| {
.map(|(key, val)| -> anyhow::Result<Option<(String, String)>> {
if let toml::Value::String(string) = val {
Some((
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(),
))
Ok(Some((key, shellexpand::full(&string)?.to_string())))
} 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)
.args(command)
@ -345,23 +381,14 @@ impl State {
Err(err) => return load_default_config(self, &err.to_string()),
};
tracing::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);
info!("Started config with {:?}", metaconfig.command);
let (pinger, ping_source) = calloop::ping::make_ping()?;
let token = self
.loop_handle
.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
.start_config(&default_lua_config_dir)
.expect("failed to start default lua config");
@ -422,7 +449,7 @@ impl State {
.starts_with("pinnacle-grpc")
})
{
tracing::debug!("Removing socket at {:?}", file.path());
debug!("Removing socket at {:?}", file.path());
std::fs::remove_file(file.path())
.context(format!("Failed to remove old socket at {:?}", file.path()))?;
}
@ -439,7 +466,7 @@ impl State {
self.loop_handle
.insert_source(grpc_receiver, |msg, _, state| match msg {
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");
@ -474,7 +501,7 @@ impl State {
Some(_) => {
self.grpc_server_join_handle = Some(tokio::spawn(async move {
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| {
state.grpc_server_join_handle = Some(tokio::spawn(async move {
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;
#[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";
temp_env::with_var("PINNACLE_CONFIG_DIR", Some(relative_path), || {
@ -519,7 +546,7 @@ mod tests {
}
#[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/"), || {
let xdg_base_dirs = BaseDirectories::with_prefix("pinnacle")?;
let expected = PathBuf::from(var("HOME")?).join("some/dir/somewhere");
@ -531,7 +558,7 @@ mod tests {
}
#[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";
temp_env::with_var("PINNACLE_CONFIG_DIR", Some(absolute_path), || {
@ -545,7 +572,7 @@ mod tests {
}
#[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";
temp_env::with_vars(
@ -565,7 +592,7 @@ mod tests {
}
#[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(
[
("PINNACLE_CONFIG_DIR", None::<&str>),
@ -685,4 +712,26 @@ mod tests {
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();
}
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");
}
None => (),

View file

@ -12,7 +12,7 @@
#![warn(clippy::unwrap_used)]
use anyhow::Context;
use clap::Parser;
use cli::Cli;
use nix::unistd::Uid;
use tracing::{info, level_filters::LevelFilter, warn};
use tracing_appender::rolling::Rotation;
@ -21,6 +21,7 @@ use xdg::BaseDirectories;
mod api;
mod backend;
mod cli;
mod config;
mod cursor;
mod focus;
@ -34,30 +35,6 @@ mod state;
mod tag;
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]
async fn main() -> anyhow::Result<()> {
let xdg_state_dir = BaseDirectories::with_prefix("pinnacle")?.get_state_home();
@ -96,13 +73,15 @@ async fn main() -> anyhow::Result<()> {
.with(stdout_layer)
.init();
let args = Args::parse();
let Some(cli) = Cli::parse_and_prompt() else {
return Ok(());
};
if Uid::effective().is_root() {
if !args.allow_root {
if !cli.allow_root {
warn!("You are trying to run Pinnacle as root.");
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.");
return Ok(());
} else {
@ -118,48 +97,47 @@ async fn main() -> anyhow::Result<()> {
warn!("You may see LOTS of file descriptors open under Pinnacle.");
}
match (args.backend.winit, args.backend.udev, args.force) {
(false, false, _) => {
match (cli.backend, cli.force) {
(None, _) => {
if in_graphical_env {
info!("Starting winit backend");
crate::backend::winit::run_winit()?;
crate::backend::winit::run_winit(cli.no_config, cli.config_dir)?;
} else {
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 force {
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 {
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 really want to, additionally pass in the --force flag.");
warn!("If you really want to, additionally pass in the `--force` flag.");
}
} else {
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 force {
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 {
warn!("WAYLAND_DISPLAY and/or DISPLAY are set.");
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!("If you really want to, additionally pass in the --force flag.");
warn!("If you really want to, additionally pass in the `--force` flag.");
}
} else {
info!("Starting udev backend");
crate::backend::udev::run_udev()?;
crate::backend::udev::run_udev(cli.no_config, cli.config_dir)?;
}
}
_ => unreachable!(),
}
Ok(())

View file

@ -34,8 +34,9 @@ use smithay::{
},
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 tracing::{error, info};
use xdg::BaseDirectories;
use crate::input::InputState;
@ -108,11 +109,13 @@ impl State {
display: Display<Self>,
loop_signal: LoopSignal,
loop_handle: LoopHandle<'static, Self>,
no_config: bool,
config_dir: Option<PathBuf>,
) -> anyhow::Result<Self> {
let socket = ListeningSocketSource::new_auto()?;
let socket_name = socket.socket_name().to_os_string();
tracing::info!(
info!(
"Setting WAYLAND_DISPLAY to {}",
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
// 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(
nix::sys::resource::Resource::RLIMIT_NOFILE,
65536,
65536 * 2,
) {
tracing::error!("Could not raise fd limit: errno {err}");
error!("Could not raise fd limit: errno {err}");
} else {
tracing::info!("Fd raise success!");
info!("Fd raise success!");
}
loop_handle.insert_source(socket, |stream, _metadata, data| {
@ -158,9 +161,7 @@ impl State {
)?;
loop_handle.insert_idle(|state| {
if let Err(err) =
state.start_config(crate::config::get_config_dir(&state.xdg_base_dirs))
{
if let Err(err) = state.start_config(state.config.dir(&state.xdg_base_dirs)) {
panic!("failed to start config: {err}");
}
});
@ -212,7 +213,7 @@ impl State {
}
});
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
};
@ -254,7 +255,7 @@ impl State {
output_focus_stack: FocusStack::default(),
z_index_stack: FocusStack::default(),
config: Config::default(),
config: Config::new(no_config, config_dir),
seat,
@ -302,7 +303,7 @@ impl State {
}
pub fn shutdown(&self) {
tracing::info!("Shutting down Pinnacle");
info!("Shutting down Pinnacle");
self.loop_signal.stop();
}
}