mirror of
https://github.com/pinnacle-comp/pinnacle.git
synced 2024-12-26 21:58:10 +01:00
Merge pull request #253 from pinnacle-comp/snowcap
Some checks are pending
CI (Pinnacle) / Build (push) Waiting to run
CI (Pinnacle) / Run tests (push) Waiting to run
CI (Pinnacle) / Check formatting (push) Waiting to run
CI (Pinnacle) / Clippy check (push) Waiting to run
Build Lua Docs / Build Lua docs (push) Waiting to run
Build Rust Docs / Build docs (push) Waiting to run
Some checks are pending
CI (Pinnacle) / Build (push) Waiting to run
CI (Pinnacle) / Run tests (push) Waiting to run
CI (Pinnacle) / Check formatting (push) Waiting to run
CI (Pinnacle) / Clippy check (push) Waiting to run
Build Lua Docs / Build Lua docs (push) Waiting to run
Build Rust Docs / Build docs (push) Waiting to run
Preliminary snowcap integration (Rust)
This commit is contained in:
commit
c1ebfd9eed
30 changed files with 2913 additions and 380 deletions
12
.github/workflows/ci.pinnacle.yml
vendored
12
.github/workflows/ci.pinnacle.yml
vendored
|
@ -17,6 +17,8 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
- name: Get Rust toolchain
|
- name: Get Rust toolchain
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
- name: Cache stuff
|
- name: Cache stuff
|
||||||
|
@ -39,6 +41,8 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
- name: Get Rust toolchain
|
- name: Get Rust toolchain
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
- name: Cache stuff
|
- name: Cache stuff
|
||||||
|
@ -55,16 +59,18 @@ jobs:
|
||||||
uses: extractions/setup-just@v1
|
uses: extractions/setup-just@v1
|
||||||
- name: Test
|
- name: Test
|
||||||
if: ${{ runner.debug != '1' }}
|
if: ${{ runner.debug != '1' }}
|
||||||
run: just install test -- --test-threads=1
|
run: just install test --no-default-features -- --test-threads=1
|
||||||
- name: Test (debug)
|
- name: Test (debug)
|
||||||
if: ${{ runner.debug == '1' }}
|
if: ${{ runner.debug == '1' }}
|
||||||
run: RUST_LOG=debug RUST_BACKTRACE=1 just install test -- --nocapture --test-threads=1
|
run: RUST_LOG=debug RUST_BACKTRACE=1 just install test --no-default-features -- --nocapture --test-threads=1
|
||||||
check-format:
|
check-format:
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
name: Check formatting
|
name: Check formatting
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
- name: Get Rust toolchain
|
- name: Get Rust toolchain
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
|
@ -77,6 +83,8 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
- name: Get Rust toolchain
|
- name: Get Rust toolchain
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
|
|
2
.github/workflows/ldoc.yml
vendored
2
.github/workflows/ldoc.yml
vendored
|
@ -25,6 +25,8 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
- name: Get ldoc_gen
|
- name: Get ldoc_gen
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
|
|
4
.github/workflows/rustdoc.yml
vendored
4
.github/workflows/rustdoc.yml
vendored
|
@ -22,10 +22,12 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
- name: Get protoc
|
- name: Get protoc
|
||||||
run: sudo apt install protobuf-compiler
|
run: sudo apt install protobuf-compiler
|
||||||
- name: Build docs
|
- name: Build docs
|
||||||
run: cd ./api/rust && cargo doc --no-deps -p pinnacle-api -p tokio -p xkbcommon
|
run: cd ./api/rust && cargo doc --no-deps -p pinnacle-api -p tokio -p xkbcommon -p snowcap-api
|
||||||
- name: Create index.html
|
- name: Create index.html
|
||||||
run: echo "<meta http-equiv=\"refresh\" content=\"0; url=pinnacle_api\">" > ./target/doc/index.html
|
run: echo "<meta http-equiv=\"refresh\" content=\"0; url=pinnacle_api\">" > ./target/doc/index.html
|
||||||
- name: Deploy
|
- name: Deploy
|
||||||
|
|
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
[submodule "snowcap"]
|
||||||
|
path = snowcap
|
||||||
|
url = https://github.com/pinnacle-comp/snowcap
|
1980
Cargo.lock
generated
1980
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
16
Cargo.toml
16
Cargo.toml
|
@ -5,6 +5,7 @@ members = [
|
||||||
"api/rust/pinnacle-api-macros",
|
"api/rust/pinnacle-api-macros",
|
||||||
"wlcs_pinnacle",
|
"wlcs_pinnacle",
|
||||||
]
|
]
|
||||||
|
exclude = ["snowcap"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
authors = ["Ottatop <ottatop1227@gmail.com>"]
|
authors = ["Ottatop <ottatop1227@gmail.com>"]
|
||||||
|
@ -13,7 +14,7 @@ repository = "https://github.com/pinnacle-comp/pinnacle/"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
# Tokio
|
# Tokio
|
||||||
tokio = { version = "1.37.0", features = ["macros", "rt-multi-thread"]}
|
tokio = { version = "1.38.0", features = ["macros", "rt-multi-thread"]}
|
||||||
tokio-stream = { version = "0.1.15", features = ["net"] }
|
tokio-stream = { version = "0.1.15", features = ["net"] }
|
||||||
# gRPC
|
# gRPC
|
||||||
prost = "0.12.4"
|
prost = "0.12.4"
|
||||||
|
@ -32,6 +33,7 @@ bitflags = "2.5.0"
|
||||||
clap = { version = "4.5.4", features = ["derive"] }
|
clap = { version = "4.5.4", features = ["derive"] }
|
||||||
dircpy = "0.3.16"
|
dircpy = "0.3.16"
|
||||||
tempfile = "3.10.1"
|
tempfile = "3.10.1"
|
||||||
|
indexmap = "2.2.6"
|
||||||
|
|
||||||
[workspace.dependencies.smithay]
|
[workspace.dependencies.smithay]
|
||||||
git = "https://github.com/Smithay/smithay"
|
git = "https://github.com/Smithay/smithay"
|
||||||
|
@ -113,11 +115,13 @@ pinnacle-api-defs = { workspace = true }
|
||||||
dircpy = { workspace = true }
|
dircpy = { workspace = true }
|
||||||
chrono = "0.4.38"
|
chrono = "0.4.38"
|
||||||
bytemuck = "1.16.0"
|
bytemuck = "1.16.0"
|
||||||
pinnacle-api = { path = "./api/rust" }
|
pinnacle-api = { path = "./api/rust", default-features = false }
|
||||||
gag = "1.0.0"
|
gag = "1.0.0"
|
||||||
drm-sys = "0.7.0"
|
drm-sys = "0.7.0"
|
||||||
libdisplay-info-sys = { git = "https://github.com/Smithay/libdisplay-info-rs", rev = "a482d0d" }
|
libdisplay-info-sys = { git = "https://github.com/Smithay/libdisplay-info-rs", rev = "a482d0d" }
|
||||||
indexmap = "2.2.6"
|
indexmap = { workspace = true }
|
||||||
|
snowcap = { path = "./snowcap", optional = true }
|
||||||
|
snowcap-api = { path = "./snowcap/api/rust", optional = true }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
vergen = { version = "8.3.1", features = ["git", "gitcl", "rustc", "cargo", "si"] }
|
vergen = { version = "8.3.1", features = ["git", "gitcl", "rustc", "cargo", "si"] }
|
||||||
|
@ -126,10 +130,12 @@ vergen = { version = "8.3.1", features = ["git", "gitcl", "rustc", "cargo", "si"
|
||||||
temp-env = "0.3.6"
|
temp-env = "0.3.6"
|
||||||
tempfile = { workspace = true }
|
tempfile = { workspace = true }
|
||||||
test-log = { version = "0.2.16", default-features = false, features = ["trace"] }
|
test-log = { version = "0.2.16", default-features = false, features = ["trace"] }
|
||||||
pinnacle = { path = ".", features = ["wlcs"] }
|
pinnacle = { path = ".", features = ["wlcs"], default-features = false }
|
||||||
pinnacle-api = { path = "./api/rust" }
|
pinnacle-api = { path = "./api/rust", default-features = false }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
default = ["snowcap"]
|
||||||
|
snowcap = ["pinnacle-api/snowcap", "dep:snowcap", "dep:snowcap-api"]
|
||||||
testing = [
|
testing = [
|
||||||
"smithay/renderer_test",
|
"smithay/renderer_test",
|
||||||
]
|
]
|
||||||
|
|
120
README.md
120
README.md
|
@ -35,6 +35,14 @@ 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.
|
||||||
|
|
||||||
|
### What is Snowcap?
|
||||||
|
You will see references to Snowcap throughout this README. [Snowcap](https://github.com/pinnacle-comp/snowcap) is the
|
||||||
|
very, *very* WIP widget system for Pinnacle. Currently it's only being used for the builtin quit prompt and keybind overlay.
|
||||||
|
In the future, Snowcap will be used for everything Awesome uses its widget system for: a taskbar, system tray, etc.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Only the Rust API has implemented Snowcap integration currently. Lua support soon™️
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
- Tag system
|
- Tag system
|
||||||
- Customizable layouts, including most of the ones from Awesome
|
- Customizable layouts, including most of the ones from Awesome
|
||||||
|
@ -42,6 +50,7 @@ for Wayland.
|
||||||
- wlr-layer-shell support
|
- wlr-layer-shell support
|
||||||
- Configurable in Lua or Rust
|
- Configurable in Lua or Rust
|
||||||
- wlr-screencopy support
|
- wlr-screencopy support
|
||||||
|
- A really *really* WIP widget system
|
||||||
- Is very cool :thumbsup:
|
- Is very cool :thumbsup:
|
||||||
|
|
||||||
### Roadmap
|
### Roadmap
|
||||||
|
@ -51,57 +60,63 @@ for Wayland.
|
||||||
You will need:
|
You will need:
|
||||||
|
|
||||||
- [Rust](https://www.rust-lang.org/) 1.75 or newer
|
- [Rust](https://www.rust-lang.org/) 1.75 or newer
|
||||||
- Packages for [Smithay](https://github.com/Smithay/smithay):
|
- The following external dependencies:
|
||||||
`libwayland libxkbcommon libudev libinput libgdm libseat`, as well as `xwayland`
|
- `libwayland`
|
||||||
- Arch:
|
- `libxkbcommon`
|
||||||
|
- `libudev`
|
||||||
|
- `libinput`
|
||||||
|
- `libgbm`
|
||||||
|
- `libseat`
|
||||||
|
- `libEGL`
|
||||||
|
- `libsystemd`
|
||||||
|
- `libdisplay-info` for monitor display information
|
||||||
|
- `xwayland` for Xwayland support
|
||||||
|
- [`protoc`](https://grpc.io/docs/protoc-installation/) for the API
|
||||||
|
|
||||||
|
The following are optional dependencies:
|
||||||
|
|
||||||
|
- [`just`](https://github.com/casey/just) to automate installation of libraries and files
|
||||||
|
- The following are required to use the Lua API:
|
||||||
|
- `just` as mentioned above
|
||||||
|
- [`lua`](https://www.lua.org/) 5.2 or newer
|
||||||
|
- [`luarocks`](https://luarocks.org/) for API installation
|
||||||
|
- You must run `eval $(luarocks path --lua-version <your-lua-version>)` so that your config can find the API
|
||||||
|
library files. It is recommended to place this command in your shell's startup script.
|
||||||
|
|
||||||
|
- Arch and derivatives:
|
||||||
```sh
|
```sh
|
||||||
sudo pacman -S wayland wayland-protocols libxkbcommon systemd-libs libinput mesa seatd xorg-xwayland
|
sudo pacman -S wayland libxkbcommon libinput mesa seatd systemd-libs libdisplay-info xorg-xwayland protobuf
|
||||||
|
# And optionally
|
||||||
|
sudo pacman -S just lua luarocks
|
||||||
```
|
```
|
||||||
- Debian/Ubuntu:
|
- Debian and derivatives:
|
||||||
```sh
|
```sh
|
||||||
sudo apt install libwayland-dev libxkbcommon-dev libudev-dev libinput-dev libgdm-dev libseat-dev xwayland
|
sudo apt install libwayland-dev libxkbcommon-dev libudev-dev libinput-dev libgbm-dev libseat-dev libsystemd-dev protobuf-compiler xwayland libegl-dev libdisplay-info-dev
|
||||||
|
# And optionally
|
||||||
|
sudo apt install just lua5.4 luarocks
|
||||||
```
|
```
|
||||||
- NixOS: There is flake [`flake.nix`](flake.nix) with a devShell. It also
|
- Note: `just` is only available in apt from Debian 13.
|
||||||
|
- Nix and NixOS:
|
||||||
|
- Use the provided [`flake.nix`](flake.nix) with a devShell. It also
|
||||||
includes the other tools needed for the build and sets up the
|
includes the other tools needed for the build and sets up the
|
||||||
`LD_LIBRARY_PATH` so the dynamically loaded libraries are found.
|
`LD_LIBRARY_PATH` so the dynamically loaded libraries are found.
|
||||||
> Luarocks currently doesn't install the Lua library and its dependencies due to openssh directory
|
> Luarocks currently doesn't install the Lua library and its dependencies due to openssh directory
|
||||||
> shenanigans. Fix soon, hopefully. In the meantime you can use the Rust API.
|
> shenanigans. Fix soon, hopefully. In the meantime you can use the Rust API.
|
||||||
- `libdisplay-info`, for monitor display information
|
|
||||||
- [protoc](https://grpc.io/docs/protoc-installation/), the Protocol Buffer Compiler, for configuration
|
|
||||||
- Arch:
|
|
||||||
```sh
|
|
||||||
sudo pacman -S protobuf
|
|
||||||
```
|
|
||||||
- Debian/Ubuntu:
|
|
||||||
```sh
|
|
||||||
sudo apt install protobuf-compiler
|
|
||||||
```
|
|
||||||
- [just](https://github.com/casey/just), to automate installation of libraries and files
|
|
||||||
- You don't *need* this but without installation you will not be able to run `cargo run -- config gen` or
|
|
||||||
use the Lua API (it requires the protobuf definitions at runtime)
|
|
||||||
- Arch:
|
|
||||||
```sh
|
|
||||||
sudo pacman -S just
|
|
||||||
```
|
|
||||||
|
|
||||||
If you would like to use the Lua API, you will additionally need:
|
|
||||||
|
|
||||||
- [Lua](https://www.lua.org/) 5.2 or newer
|
|
||||||
- [LuaRocks](https://luarocks.org/), the Lua package manager
|
|
||||||
- Arch:
|
|
||||||
```sh
|
|
||||||
sudo pacman -S luarocks
|
|
||||||
```
|
|
||||||
- Debian/Ubuntu:
|
|
||||||
```sh
|
|
||||||
sudo apt install luarocks
|
|
||||||
```
|
|
||||||
- You must run `eval $(luarocks path --lua-version <your-lua-version>)` so that your config can find the API
|
|
||||||
library files. It is recommended to place this command in your shell's startup script.
|
|
||||||
|
|
||||||
TODO: other distros
|
TODO: other distros
|
||||||
|
|
||||||
# Building
|
# Building
|
||||||
|
|
||||||
|
Clone this repository and, if building with Snowcap integration, update the `snowcap` submodule:
|
||||||
|
```sh
|
||||||
|
git clone https://github.com/pinnacle-comp/pinnacle
|
||||||
|
git submodule update --init
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> For all following `cargo`/`just` commands, if you would like to build without Snowcap integration,
|
||||||
|
> run with `--no-default-features`.
|
||||||
|
|
||||||
Build the project with:
|
Build the project with:
|
||||||
```sh
|
```sh
|
||||||
cargo build [--release]
|
cargo build [--release]
|
||||||
|
@ -126,6 +141,16 @@ After building, run the executable located in either:
|
||||||
./target/release/pinnacle # with --release
|
./target/release/pinnacle # with --release
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> When compiling with Snowcap integration, if you do not have Vulkan set up properly,
|
||||||
|
> Pinnacle will crash on startup.
|
||||||
|
>
|
||||||
|
> For those using Nix outside of NixOS, you will need to run the built binary
|
||||||
|
> with [nixGL](https://github.com/nix-community/nixGL) using *both* GL and Vulkan wrappers, nested inside one another:
|
||||||
|
> ```
|
||||||
|
> nix run --impure github:nix-community/nixGL -- nix run --impure github:nix-community/nixGL#nixVulkanIntel -- ./target/debug/pinnacle
|
||||||
|
> ```
|
||||||
|
|
||||||
Or, run the project directly with
|
Or, run the project directly with
|
||||||
```sh
|
```sh
|
||||||
cargo run [--release]
|
cargo run [--release]
|
||||||
|
@ -145,12 +170,16 @@ the Lua or Rust default configs standalone, run one of the following in the crat
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# For a Lua configuration
|
# For a Lua configuration
|
||||||
cargo run -- -c "./api/lua/examples/default"
|
|
||||||
just install run -- -c "./api/lua/examples/default"
|
just install run -- -c "./api/lua/examples/default"
|
||||||
|
|
||||||
# For a Rust configuration
|
# For a Rust configuration
|
||||||
cargo run -- -c "./api/rust/examples/default_config"
|
cargo run -- -c "./api/rust/examples/default_config"
|
||||||
just install run -- -c "./api/rust/examples/default_config"
|
```
|
||||||
|
|
||||||
|
When running the default Rust config standalone without compiled Snowcap integration,
|
||||||
|
run the one in the following directory:
|
||||||
|
```sh
|
||||||
|
cargo run -- -c "./api/rust/examples/default_config_no_snowcap"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Custom configuration
|
## Custom configuration
|
||||||
|
@ -161,15 +190,9 @@ just install run -- -c "./api/rust/examples/default_config"
|
||||||
|
|
||||||
### Generating a config
|
### Generating a config
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> The default configs must be installed for them to be copied:
|
|
||||||
> ```sh
|
|
||||||
> just install-configs # Or alternatively, `just install` which installs everything
|
|
||||||
> ```
|
|
||||||
|
|
||||||
Run the following command to open up the interactive config generator:
|
Run the following command to open up the interactive config generator:
|
||||||
```sh
|
```sh
|
||||||
cargo run -- config gen
|
just install-configs run -- config gen
|
||||||
```
|
```
|
||||||
|
|
||||||
This will prompt you to choose a language (Lua or Rust) and directory to put the config in.
|
This will prompt you to choose a language (Lua or Rust) and directory to put the config in.
|
||||||
|
@ -232,6 +255,7 @@ Rust: https://pinnacle-comp.github.io/rust-reference/main.</b>
|
||||||
The following are the default controls in the [`default_config`](api/rust/examples/default_config/main.rs).
|
The following are the default controls in the [`default_config`](api/rust/examples/default_config/main.rs).
|
||||||
| Binding | Action |
|
| Binding | Action |
|
||||||
|----------------------------------------------|------------------------------------|
|
|----------------------------------------------|------------------------------------|
|
||||||
|
| <kbd>Ctrl</kbd> + <kbd>s</kbd> | Show the keybind overlay |
|
||||||
| <kbd>Ctrl</kbd> + <kbd>Mouse left drag</kbd> | Move window |
|
| <kbd>Ctrl</kbd> + <kbd>Mouse left drag</kbd> | Move window |
|
||||||
| <kbd>Ctrl</kbd> + <kbd>Mouse right drag</kbd>| Resize window |
|
| <kbd>Ctrl</kbd> + <kbd>Mouse right drag</kbd>| Resize window |
|
||||||
| <kbd>Ctrl</kbd><kbd>Alt</kbd> + <kbd>q</kbd> | Quit Pinnacle |
|
| <kbd>Ctrl</kbd><kbd>Alt</kbd> + <kbd>q</kbd> | Quit Pinnacle |
|
||||||
|
|
3
TODO.md
3
TODO.md
|
@ -1,5 +1,3 @@
|
||||||
- Re-add raising file descriptor limit
|
|
||||||
- Like an idiot I managed to remove that sometime and not add it back
|
|
||||||
- Provide scale and transform on new window/layer
|
- Provide scale and transform on new window/layer
|
||||||
|
|
||||||
Problems:
|
Problems:
|
||||||
|
@ -8,4 +6,3 @@ Problems:
|
||||||
- Xwayland popups are screwed when the output is not at (0, 0)
|
- Xwayland popups are screwed when the output is not at (0, 0)
|
||||||
- Dragging an xwayland window to another output and closing a nested right click menu closes the whole
|
- Dragging an xwayland window to another output and closing a nested right click menu closes the whole
|
||||||
right click menu because the keyboard focus is getting updated on the original output.
|
right click menu because the keyboard focus is getting updated on the original output.
|
||||||
- Transactions don't render floating windows
|
|
||||||
|
|
|
@ -34,12 +34,18 @@ require("pinnacle").setup(function(Pinnacle)
|
||||||
-- mod_key + alt + q = Quit Pinnacle
|
-- mod_key + alt + q = Quit Pinnacle
|
||||||
Input.keybind({ mod_key, "alt" }, "q", function()
|
Input.keybind({ mod_key, "alt" }, "q", function()
|
||||||
Pinnacle.quit()
|
Pinnacle.quit()
|
||||||
end)
|
end, {
|
||||||
|
group = "Compositor",
|
||||||
|
description = "Quit Pinnacle",
|
||||||
|
})
|
||||||
|
|
||||||
-- mod_key + alt + r = Reload config
|
-- mod_key + alt + r = Reload config
|
||||||
Input.keybind({ mod_key, "alt" }, "r", function()
|
Input.keybind({ mod_key, "alt" }, "r", function()
|
||||||
Pinnacle.reload_config()
|
Pinnacle.reload_config()
|
||||||
end)
|
end, {
|
||||||
|
group = "Compositor",
|
||||||
|
description = "Reload the config",
|
||||||
|
})
|
||||||
|
|
||||||
-- mod_key + alt + c = Close window
|
-- mod_key + alt + c = Close window
|
||||||
Input.keybind({ mod_key, "alt" }, "c", function()
|
Input.keybind({ mod_key, "alt" }, "c", function()
|
||||||
|
@ -47,12 +53,18 @@ require("pinnacle").setup(function(Pinnacle)
|
||||||
if focused then
|
if focused then
|
||||||
focused:close()
|
focused:close()
|
||||||
end
|
end
|
||||||
end)
|
end, {
|
||||||
|
group = "Window",
|
||||||
|
description = "Close the focused window",
|
||||||
|
})
|
||||||
|
|
||||||
-- mod_key + alt + Return = Spawn `terminal`
|
-- mod_key + alt + Return = Spawn `terminal`
|
||||||
Input.keybind({ mod_key }, key.Return, function()
|
Input.keybind({ mod_key }, key.Return, function()
|
||||||
Process.spawn(terminal)
|
Process.spawn(terminal)
|
||||||
end)
|
end, {
|
||||||
|
group = "Process",
|
||||||
|
description = "Spawn `alacritty`",
|
||||||
|
})
|
||||||
|
|
||||||
-- mod_key + alt + space = Toggle floating
|
-- mod_key + alt + space = Toggle floating
|
||||||
Input.keybind({ mod_key, "alt" }, key.space, function()
|
Input.keybind({ mod_key, "alt" }, key.space, function()
|
||||||
|
@ -61,7 +73,10 @@ require("pinnacle").setup(function(Pinnacle)
|
||||||
focused:toggle_floating()
|
focused:toggle_floating()
|
||||||
focused:raise()
|
focused:raise()
|
||||||
end
|
end
|
||||||
end)
|
end, {
|
||||||
|
group = "Window",
|
||||||
|
description = "Toggle floating on the focused window",
|
||||||
|
})
|
||||||
|
|
||||||
-- mod_key + f = Toggle fullscreen
|
-- mod_key + f = Toggle fullscreen
|
||||||
Input.keybind({ mod_key }, "f", function()
|
Input.keybind({ mod_key }, "f", function()
|
||||||
|
@ -70,7 +85,10 @@ require("pinnacle").setup(function(Pinnacle)
|
||||||
focused:toggle_fullscreen()
|
focused:toggle_fullscreen()
|
||||||
focused:raise()
|
focused:raise()
|
||||||
end
|
end
|
||||||
end)
|
end, {
|
||||||
|
group = "Window",
|
||||||
|
description = "Toggle fullscreen on the focused window",
|
||||||
|
})
|
||||||
|
|
||||||
-- mod_key + m = Toggle maximized
|
-- mod_key + m = Toggle maximized
|
||||||
Input.keybind({ mod_key }, "m", function()
|
Input.keybind({ mod_key }, "m", function()
|
||||||
|
@ -79,7 +97,10 @@ require("pinnacle").setup(function(Pinnacle)
|
||||||
focused:toggle_maximized()
|
focused:toggle_maximized()
|
||||||
focused:raise()
|
focused:raise()
|
||||||
end
|
end
|
||||||
end)
|
end, {
|
||||||
|
group = "Window",
|
||||||
|
description = "Toggle maximized on the focused window",
|
||||||
|
})
|
||||||
|
|
||||||
----------------------
|
----------------------
|
||||||
-- Tags and Outputs --
|
-- Tags and Outputs --
|
||||||
|
@ -109,12 +130,18 @@ require("pinnacle").setup(function(Pinnacle)
|
||||||
-- mod_key + 1-5 = Switch to tags 1-5
|
-- mod_key + 1-5 = Switch to tags 1-5
|
||||||
Input.keybind({ mod_key }, tag_name, function()
|
Input.keybind({ mod_key }, tag_name, function()
|
||||||
Tag.get(tag_name):switch_to()
|
Tag.get(tag_name):switch_to()
|
||||||
end)
|
end, {
|
||||||
|
group = "Tag",
|
||||||
|
description = "Switch to tag " .. tag_name,
|
||||||
|
})
|
||||||
|
|
||||||
-- mod_key + shift + 1-5 = Toggle tags 1-5
|
-- mod_key + shift + 1-5 = Toggle tags 1-5
|
||||||
Input.keybind({ mod_key, "shift" }, tag_name, function()
|
Input.keybind({ mod_key, "shift" }, tag_name, function()
|
||||||
Tag.get(tag_name):toggle_active()
|
Tag.get(tag_name):toggle_active()
|
||||||
end)
|
end, {
|
||||||
|
group = "Tag",
|
||||||
|
description = "Toggle tag " .. tag_name,
|
||||||
|
})
|
||||||
|
|
||||||
-- mod_key + alt + 1-5 = Move window to tags 1-5
|
-- mod_key + alt + 1-5 = Move window to tags 1-5
|
||||||
Input.keybind({ mod_key, "alt" }, tag_name, function()
|
Input.keybind({ mod_key, "alt" }, tag_name, function()
|
||||||
|
@ -122,7 +149,10 @@ require("pinnacle").setup(function(Pinnacle)
|
||||||
if focused then
|
if focused then
|
||||||
focused:move_to_tag(Tag.get(tag_name) --[[@as TagHandle]])
|
focused:move_to_tag(Tag.get(tag_name) --[[@as TagHandle]])
|
||||||
end
|
end
|
||||||
end)
|
end, {
|
||||||
|
group = "Tag",
|
||||||
|
description = "Move the focused window to tag " .. tag_name,
|
||||||
|
})
|
||||||
|
|
||||||
-- mod_key + shift + alt + 1-5 = Toggle tags 1-5 on window
|
-- mod_key + shift + alt + 1-5 = Toggle tags 1-5 on window
|
||||||
Input.keybind({ mod_key, "shift", "alt" }, tag_name, function()
|
Input.keybind({ mod_key, "shift", "alt" }, tag_name, function()
|
||||||
|
@ -130,7 +160,10 @@ require("pinnacle").setup(function(Pinnacle)
|
||||||
if focused then
|
if focused then
|
||||||
focused:toggle_tag(Tag.get(tag_name) --[[@as TagHandle]])
|
focused:toggle_tag(Tag.get(tag_name) --[[@as TagHandle]])
|
||||||
end
|
end
|
||||||
end)
|
end, {
|
||||||
|
group = "Tag",
|
||||||
|
description = "Toggle tag " .. tag_name .. " on the focused window",
|
||||||
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
--------------------
|
--------------------
|
||||||
|
@ -226,7 +259,10 @@ require("pinnacle").setup(function(Pinnacle)
|
||||||
Layout.request_layout(focused_op)
|
Layout.request_layout(focused_op)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end)
|
end, {
|
||||||
|
group = "Layout",
|
||||||
|
description = "Cycle the layout forward on the first active tag",
|
||||||
|
})
|
||||||
|
|
||||||
-- mod_key + shift + space = Cycle backward one layout on the focused output
|
-- mod_key + shift + space = Cycle backward one layout on the focused output
|
||||||
Input.keybind({ mod_key, "shift" }, key.space, function()
|
Input.keybind({ mod_key, "shift" }, key.space, function()
|
||||||
|
@ -257,7 +293,10 @@ require("pinnacle").setup(function(Pinnacle)
|
||||||
Layout.request_layout(focused_op)
|
Layout.request_layout(focused_op)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end)
|
end, {
|
||||||
|
group = "Layout",
|
||||||
|
description = "Cycle the layout backward on the first active tag",
|
||||||
|
})
|
||||||
|
|
||||||
Input.set_libinput_settings({
|
Input.set_libinput_settings({
|
||||||
tap = true,
|
tap = true,
|
||||||
|
|
|
@ -258,6 +258,8 @@ local pinnacle_input_v0alpha1_Modifier = {
|
||||||
---@field modifiers pinnacle.input.v0alpha1.Modifier[]?
|
---@field modifiers pinnacle.input.v0alpha1.Modifier[]?
|
||||||
---@field raw_code integer?
|
---@field raw_code integer?
|
||||||
---@field xkb_name string?
|
---@field xkb_name string?
|
||||||
|
---@field group string?
|
||||||
|
---@field description string?
|
||||||
|
|
||||||
---@class pinnacle.input.v0alpha1.SetKeybindResponse
|
---@class pinnacle.input.v0alpha1.SetKeybindResponse
|
||||||
|
|
||||||
|
@ -274,6 +276,18 @@ local pinnacle_input_v0alpha1_SetMousebindRequest_MouseEdge = {
|
||||||
|
|
||||||
---@class pinnacle.input.v0alpha1.SetMousebindResponse
|
---@class pinnacle.input.v0alpha1.SetMousebindResponse
|
||||||
|
|
||||||
|
---@class pinnacle.input.v0alpha1.KeybindDescriptionsRequest
|
||||||
|
|
||||||
|
---@class pinnacle.input.v0alpha1.KeybindDescriptionsResponse
|
||||||
|
---@field descriptions pinnacle.input.v0alpha1.KeybindDescription[]?
|
||||||
|
|
||||||
|
---@class pinnacle.input.v0alpha1.KeybindDescription
|
||||||
|
---@field modifiers pinnacle.input.v0alpha1.Modifier[]?
|
||||||
|
---@field raw_code integer?
|
||||||
|
---@field xkb_name string?
|
||||||
|
---@field group string?
|
||||||
|
---@field description string?
|
||||||
|
|
||||||
---@class SetXkbConfigRequest
|
---@class SetXkbConfigRequest
|
||||||
---@field rules string?
|
---@field rules string?
|
||||||
---@field variant string?
|
---@field variant string?
|
||||||
|
@ -729,6 +743,13 @@ defs.pinnacle = {
|
||||||
response = "pinnacle.input.v0alpha1.SetMousebindResponse",
|
response = "pinnacle.input.v0alpha1.SetMousebindResponse",
|
||||||
},
|
},
|
||||||
---@type GrpcRequestArgs
|
---@type GrpcRequestArgs
|
||||||
|
KeybindDescriptions = {
|
||||||
|
service = "pinnacle.input.v0alpha1.InputService",
|
||||||
|
method = "KeybindDescriptions",
|
||||||
|
request = "pinnacle.input.v0alpha1.KeybindDescriptionsRequest",
|
||||||
|
response = "pinnacle.input.v0alpha1.KeybindDescriptionsResponse",
|
||||||
|
},
|
||||||
|
---@type GrpcRequestArgs
|
||||||
SetXkbConfig = {
|
SetXkbConfig = {
|
||||||
service = "pinnacle.input.v0alpha1.InputService",
|
service = "pinnacle.input.v0alpha1.InputService",
|
||||||
method = "SetXkbConfig",
|
method = "SetXkbConfig",
|
||||||
|
|
|
@ -74,6 +74,10 @@ local input = {
|
||||||
}
|
}
|
||||||
input.mouse_button_values = mouse_button_values
|
input.mouse_button_values = mouse_button_values
|
||||||
|
|
||||||
|
---@class KeybindInfo
|
||||||
|
---@field group string? The group to place this keybind in. Used for the keybind list.
|
||||||
|
---@field description string? The description of this keybind. Used for the keybind list.
|
||||||
|
|
||||||
---Set a keybind. If called with an already existing keybind, it gets replaced.
|
---Set a keybind. If called with an already existing keybind, it gets replaced.
|
||||||
---
|
---
|
||||||
---You must provide three arguments:
|
---You must provide three arguments:
|
||||||
|
@ -111,7 +115,8 @@ input.mouse_button_values = mouse_button_values
|
||||||
---@param mods Modifier[] The modifiers that need to be held down for the bind to trigger
|
---@param mods Modifier[] The modifiers that need to be held down for the bind to trigger
|
||||||
---@param key Key | string The key used to trigger the bind
|
---@param key Key | string The key used to trigger the bind
|
||||||
---@param action fun() The function to run when the bind is triggered
|
---@param action fun() The function to run when the bind is triggered
|
||||||
function input.keybind(mods, key, action)
|
---@param keybind_info KeybindInfo?
|
||||||
|
function input.keybind(mods, key, action, keybind_info)
|
||||||
local raw_code = nil
|
local raw_code = nil
|
||||||
local xkb_name = nil
|
local xkb_name = nil
|
||||||
|
|
||||||
|
@ -130,6 +135,8 @@ function input.keybind(mods, key, action)
|
||||||
modifiers = mod_values,
|
modifiers = mod_values,
|
||||||
raw_code = raw_code,
|
raw_code = raw_code,
|
||||||
xkb_name = xkb_name,
|
xkb_name = xkb_name,
|
||||||
|
group = keybind_info and keybind_info.group,
|
||||||
|
description = keybind_info and keybind_info.description,
|
||||||
}, action)
|
}, action)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -165,6 +172,31 @@ function input.mousebind(mods, button, edge, action)
|
||||||
}, action)
|
}, action)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@class KeybindDescription
|
||||||
|
---@field modifiers Modifier[]
|
||||||
|
---@field raw_code integer
|
||||||
|
---@field xkb_name string
|
||||||
|
---@field group string?
|
||||||
|
---@field description string?
|
||||||
|
|
||||||
|
---Get all keybinds along with their descriptions
|
||||||
|
---
|
||||||
|
---@return KeybindDescription[]
|
||||||
|
function input.keybind_descriptions()
|
||||||
|
---@type pinnacle.input.v0alpha1.KeybindDescriptionsResponse
|
||||||
|
local descs = client.unary_request(input_service.KeybindDescriptions, {})
|
||||||
|
local descs = descs.descriptions or {}
|
||||||
|
|
||||||
|
local ret = {}
|
||||||
|
|
||||||
|
for _, desc in ipairs(descs) do
|
||||||
|
desc.modifiers = desc.modifiers or {}
|
||||||
|
table.insert(ret, desc)
|
||||||
|
end
|
||||||
|
|
||||||
|
return ret
|
||||||
|
end
|
||||||
|
|
||||||
---@class XkbConfig
|
---@class XkbConfig
|
||||||
---@field rules string?
|
---@field rules string?
|
||||||
---@field model string?
|
---@field model string?
|
||||||
|
|
|
@ -18,9 +18,25 @@ message SetKeybindRequest {
|
||||||
uint32 raw_code = 2;
|
uint32 raw_code = 2;
|
||||||
string xkb_name = 3;
|
string xkb_name = 3;
|
||||||
}
|
}
|
||||||
|
optional string group = 4;
|
||||||
|
optional string description = 5;
|
||||||
}
|
}
|
||||||
message SetKeybindResponse {}
|
message SetKeybindResponse {}
|
||||||
|
|
||||||
|
message KeybindDescriptionsRequest {}
|
||||||
|
|
||||||
|
message KeybindDescriptionsResponse {
|
||||||
|
repeated KeybindDescription descriptions = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message KeybindDescription {
|
||||||
|
repeated Modifier modifiers = 1;
|
||||||
|
optional uint32 raw_code = 2;
|
||||||
|
optional string xkb_name = 3;
|
||||||
|
optional string group = 4;
|
||||||
|
optional string description = 5;
|
||||||
|
}
|
||||||
|
|
||||||
message SetMousebindRequest {
|
message SetMousebindRequest {
|
||||||
repeated Modifier modifiers = 1;
|
repeated Modifier modifiers = 1;
|
||||||
// A button code corresponding to one of the `BTN_` prefixed definitions in input-event-codes.h
|
// A button code corresponding to one of the `BTN_` prefixed definitions in input-event-codes.h
|
||||||
|
@ -131,6 +147,8 @@ service InputService {
|
||||||
rpc SetKeybind(SetKeybindRequest) returns (stream SetKeybindResponse);
|
rpc SetKeybind(SetKeybindRequest) returns (stream SetKeybindResponse);
|
||||||
rpc SetMousebind(SetMousebindRequest) returns (stream SetMousebindResponse);
|
rpc SetMousebind(SetMousebindRequest) returns (stream SetMousebindResponse);
|
||||||
|
|
||||||
|
rpc KeybindDescriptions(KeybindDescriptionsRequest) returns (KeybindDescriptionsResponse);
|
||||||
|
|
||||||
rpc SetXkbConfig(SetXkbConfigRequest) returns (google.protobuf.Empty);
|
rpc SetXkbConfig(SetXkbConfigRequest) returns (google.protobuf.Empty);
|
||||||
rpc SetRepeatRate(SetRepeatRateRequest) returns (google.protobuf.Empty);
|
rpc SetRepeatRate(SetRepeatRateRequest) returns (google.protobuf.Empty);
|
||||||
|
|
||||||
|
|
|
@ -21,3 +21,9 @@ num_enum = "0.7.2"
|
||||||
xkbcommon = { workspace = true }
|
xkbcommon = { workspace = true }
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
bitflags = { workspace = true }
|
bitflags = { workspace = true }
|
||||||
|
snowcap-api = { path = "../../snowcap/api/rust", optional = true }
|
||||||
|
indexmap = { workspace = true }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["snowcap"]
|
||||||
|
snowcap = ["dep:snowcap-api"]
|
||||||
|
|
|
@ -4,4 +4,8 @@ version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
pinnacle-api = { git = "http://github.com/pinnacle-comp/pinnacle" }
|
pinnacle-api = { git = "http://github.com/pinnacle-comp/pinnacle", default-features = false }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["snowcap"]
|
||||||
|
snowcap = ["pinnacle-api/snowcap"]
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use pinnacle_api::input::libinput::LibinputSetting;
|
use pinnacle_api::input::libinput::LibinputSetting;
|
||||||
|
use pinnacle_api::input::KeybindInfo;
|
||||||
use pinnacle_api::layout::{
|
use pinnacle_api::layout::{
|
||||||
CornerLayout, CornerLocation, CyclingLayoutManager, DwindleLayout, FairLayout, MasterSide,
|
CornerLayout, CornerLocation, CyclingLayoutManager, DwindleLayout, FairLayout, MasterSide,
|
||||||
MasterStackLayout, SpiralLayout,
|
MasterStackLayout, SpiralLayout,
|
||||||
|
@ -28,6 +29,8 @@ async fn main() {
|
||||||
tag,
|
tag,
|
||||||
layout,
|
layout,
|
||||||
render,
|
render,
|
||||||
|
#[cfg(feature = "snowcap")]
|
||||||
|
snowcap,
|
||||||
..
|
..
|
||||||
} = modules;
|
} = modules;
|
||||||
|
|
||||||
|
@ -53,51 +56,124 @@ async fn main() {
|
||||||
// Keybinds |
|
// Keybinds |
|
||||||
//------------------------
|
//------------------------
|
||||||
|
|
||||||
|
// `mod_key + s` shows the keybind overlay
|
||||||
|
#[cfg(feature = "snowcap")]
|
||||||
|
input.keybind(
|
||||||
|
[mod_key],
|
||||||
|
's',
|
||||||
|
|| {
|
||||||
|
snowcap.integration.keybind_overlay().show();
|
||||||
|
},
|
||||||
|
KeybindInfo {
|
||||||
|
group: Some("Compositor".into()),
|
||||||
|
description: Some("Show the keybind overlay".into()),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// `mod_key + alt + q` quits Pinnacle
|
// `mod_key + alt + q` quits Pinnacle
|
||||||
input.keybind([mod_key, Mod::Alt], 'q', || {
|
input.keybind(
|
||||||
|
[mod_key, Mod::Alt],
|
||||||
|
'q',
|
||||||
|
|| {
|
||||||
|
#[cfg(feature = "snowcap")]
|
||||||
|
snowcap.integration.quit_prompt().show();
|
||||||
|
#[cfg(not(feature = "snowcap"))]
|
||||||
pinnacle.quit();
|
pinnacle.quit();
|
||||||
});
|
},
|
||||||
|
KeybindInfo {
|
||||||
|
group: Some("Compositor".into()),
|
||||||
|
description: Some("Quit Pinnacle".into()),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// `mod_key + alt + r` reloads the config
|
// `mod_key + alt + r` reloads the config
|
||||||
input.keybind([mod_key, Mod::Alt], 'r', || {
|
input.keybind(
|
||||||
|
[mod_key, Mod::Alt],
|
||||||
|
'r',
|
||||||
|
|| {
|
||||||
pinnacle.reload_config();
|
pinnacle.reload_config();
|
||||||
});
|
},
|
||||||
|
KeybindInfo {
|
||||||
|
group: Some("Compositor".into()),
|
||||||
|
description: Some("Reload the config".into()),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// `mod_key + alt + c` closes the focused window
|
// `mod_key + alt + c` closes the focused window
|
||||||
input.keybind([mod_key, Mod::Alt], 'c', || {
|
input.keybind(
|
||||||
|
[mod_key, Mod::Alt],
|
||||||
|
'c',
|
||||||
|
|| {
|
||||||
if let Some(window) = window.get_focused() {
|
if let Some(window) = window.get_focused() {
|
||||||
window.close();
|
window.close();
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
KeybindInfo {
|
||||||
|
group: Some("Window".into()),
|
||||||
|
description: Some("Close the focused window".into()),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// `mod_key + Return` spawns a terminal
|
// `mod_key + Return` spawns a terminal
|
||||||
input.keybind([mod_key], Keysym::Return, move || {
|
input.keybind(
|
||||||
|
[mod_key],
|
||||||
|
Keysym::Return,
|
||||||
|
move || {
|
||||||
process.spawn([terminal]);
|
process.spawn([terminal]);
|
||||||
});
|
},
|
||||||
|
KeybindInfo {
|
||||||
|
group: Some("Process".into()),
|
||||||
|
description: Some(format!("Spawn `{terminal}`")),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// `mod_key + alt + space` toggles floating
|
// `mod_key + alt + space` toggles floating
|
||||||
input.keybind([mod_key, Mod::Alt], Keysym::space, || {
|
input.keybind(
|
||||||
|
[mod_key, Mod::Alt],
|
||||||
|
Keysym::space,
|
||||||
|
|| {
|
||||||
if let Some(window) = window.get_focused() {
|
if let Some(window) = window.get_focused() {
|
||||||
window.toggle_floating();
|
window.toggle_floating();
|
||||||
window.raise();
|
window.raise();
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
KeybindInfo {
|
||||||
|
group: Some("Window".into()),
|
||||||
|
description: Some("Toggle floating on the focused window".into()),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// `mod_key + f` toggles fullscreen
|
// `mod_key + f` toggles fullscreen
|
||||||
input.keybind([mod_key], 'f', || {
|
input.keybind(
|
||||||
|
[mod_key],
|
||||||
|
'f',
|
||||||
|
|| {
|
||||||
if let Some(window) = window.get_focused() {
|
if let Some(window) = window.get_focused() {
|
||||||
window.toggle_fullscreen();
|
window.toggle_fullscreen();
|
||||||
window.raise();
|
window.raise();
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
KeybindInfo {
|
||||||
|
group: Some("Window".into()),
|
||||||
|
description: Some("Toggle fullscreen on the focused window".into()),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// `mod_key + m` toggles maximized
|
// `mod_key + m` toggles maximized
|
||||||
input.keybind([mod_key], 'm', || {
|
input.keybind(
|
||||||
|
[mod_key],
|
||||||
|
'm',
|
||||||
|
|| {
|
||||||
if let Some(window) = window.get_focused() {
|
if let Some(window) = window.get_focused() {
|
||||||
window.toggle_maximized();
|
window.toggle_maximized();
|
||||||
window.raise();
|
window.raise();
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
KeybindInfo {
|
||||||
|
group: Some("Window".into()),
|
||||||
|
description: Some("Toggle maximized on the focused window".into()),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
//------------------------
|
//------------------------
|
||||||
// Window rules |
|
// Window rules |
|
||||||
|
@ -180,7 +256,10 @@ async fn main() {
|
||||||
let mut layout_requester_clone = layout_requester.clone();
|
let mut layout_requester_clone = layout_requester.clone();
|
||||||
|
|
||||||
// `mod_key + space` cycles to the next layout
|
// `mod_key + space` cycles to the next layout
|
||||||
input.keybind([mod_key], Keysym::space, move || {
|
input.keybind(
|
||||||
|
[mod_key],
|
||||||
|
Keysym::space,
|
||||||
|
move || {
|
||||||
let Some(focused_op) = output.get_focused() else { return };
|
let Some(focused_op) = output.get_focused() else { return };
|
||||||
let Some(first_active_tag) = focused_op.tags().batch_find(
|
let Some(first_active_tag) = focused_op.tags().batch_find(
|
||||||
|tg| Box::pin(tg.active_async()),
|
|tg| Box::pin(tg.active_async()),
|
||||||
|
@ -191,10 +270,18 @@ async fn main() {
|
||||||
|
|
||||||
layout_requester.cycle_layout_forward(&first_active_tag);
|
layout_requester.cycle_layout_forward(&first_active_tag);
|
||||||
layout_requester.request_layout_on_output(&focused_op);
|
layout_requester.request_layout_on_output(&focused_op);
|
||||||
});
|
},
|
||||||
|
KeybindInfo {
|
||||||
|
group: Some("Layout".into()),
|
||||||
|
description: Some("Cycle the layout forward on the first active tag".into()),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// `mod_key + shift + space` cycles to the previous layout
|
// `mod_key + shift + space` cycles to the previous layout
|
||||||
input.keybind([mod_key, Mod::Shift], Keysym::space, move || {
|
input.keybind(
|
||||||
|
[mod_key, Mod::Shift],
|
||||||
|
Keysym::space,
|
||||||
|
move || {
|
||||||
let Some(focused_op) = output.get_focused() else { return };
|
let Some(focused_op) = output.get_focused() else { return };
|
||||||
let Some(first_active_tag) = focused_op.tags().batch_find(
|
let Some(first_active_tag) = focused_op.tags().batch_find(
|
||||||
|tg| Box::pin(tg.active_async()),
|
|tg| Box::pin(tg.active_async()),
|
||||||
|
@ -205,7 +292,12 @@ async fn main() {
|
||||||
|
|
||||||
layout_requester_clone.cycle_layout_backward(&first_active_tag);
|
layout_requester_clone.cycle_layout_backward(&first_active_tag);
|
||||||
layout_requester_clone.request_layout_on_output(&focused_op);
|
layout_requester_clone.request_layout_on_output(&focused_op);
|
||||||
});
|
},
|
||||||
|
KeybindInfo {
|
||||||
|
group: Some("Layout".into()),
|
||||||
|
description: Some("Cycle the layout backward on the first active tag".into()),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
//------------------------
|
//------------------------
|
||||||
// Tags |
|
// Tags |
|
||||||
|
@ -218,36 +310,68 @@ async fn main() {
|
||||||
|
|
||||||
for tag_name in tag_names {
|
for tag_name in tag_names {
|
||||||
// `mod_key + 1-5` switches to tag "1" to "5"
|
// `mod_key + 1-5` switches to tag "1" to "5"
|
||||||
input.keybind([mod_key], tag_name, move || {
|
input.keybind(
|
||||||
|
[mod_key],
|
||||||
|
tag_name,
|
||||||
|
move || {
|
||||||
if let Some(tg) = tag.get(tag_name) {
|
if let Some(tg) = tag.get(tag_name) {
|
||||||
tg.switch_to();
|
tg.switch_to();
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
KeybindInfo {
|
||||||
|
group: Some("Tag".into()),
|
||||||
|
description: Some(format!("Switch to tag {tag_name}")),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// `mod_key + shift + 1-5` toggles tag "1" to "5"
|
// `mod_key + shift + 1-5` toggles tag "1" to "5"
|
||||||
input.keybind([mod_key, Mod::Shift], tag_name, move || {
|
input.keybind(
|
||||||
|
[mod_key, Mod::Shift],
|
||||||
|
tag_name,
|
||||||
|
move || {
|
||||||
if let Some(tg) = tag.get(tag_name) {
|
if let Some(tg) = tag.get(tag_name) {
|
||||||
tg.toggle_active();
|
tg.toggle_active();
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
KeybindInfo {
|
||||||
|
group: Some("Tag".into()),
|
||||||
|
description: Some(format!("Toggle tag {tag_name}")),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// `mod_key + alt + 1-5` moves the focused window to tag "1" to "5"
|
// `mod_key + alt + 1-5` moves the focused window to tag "1" to "5"
|
||||||
input.keybind([mod_key, Mod::Alt], tag_name, move || {
|
input.keybind(
|
||||||
|
[mod_key, Mod::Alt],
|
||||||
|
tag_name,
|
||||||
|
move || {
|
||||||
if let Some(tg) = tag.get(tag_name) {
|
if let Some(tg) = tag.get(tag_name) {
|
||||||
if let Some(win) = window.get_focused() {
|
if let Some(win) = window.get_focused() {
|
||||||
win.move_to_tag(&tg);
|
win.move_to_tag(&tg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
KeybindInfo {
|
||||||
|
group: Some("Tag".into()),
|
||||||
|
description: Some(format!("Move the focused window to tag {tag_name}")),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// `mod_key + shift + alt + 1-5` toggles tag "1" to "5" on the focused window
|
// `mod_key + shift + alt + 1-5` toggles tag "1" to "5" on the focused window
|
||||||
input.keybind([mod_key, Mod::Shift, Mod::Alt], tag_name, move || {
|
input.keybind(
|
||||||
|
[mod_key, Mod::Shift, Mod::Alt],
|
||||||
|
tag_name,
|
||||||
|
move || {
|
||||||
if let Some(tg) = tag.get(tag_name) {
|
if let Some(tg) = tag.get(tag_name) {
|
||||||
if let Some(win) = window.get_focused() {
|
if let Some(win) = window.get_focused() {
|
||||||
win.toggle_tag(&tg);
|
win.toggle_tag(&tg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
KeybindInfo {
|
||||||
|
group: Some("Tag".into()),
|
||||||
|
description: Some(format!("Toggle tag {tag_name} on the focused window")),
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
input.set_libinput_setting(LibinputSetting::Tap(true));
|
input.set_libinput_setting(LibinputSetting::Tap(true));
|
||||||
|
|
1
api/rust/examples/default_config_no_snowcap/main.rs
Symbolic link
1
api/rust/examples/default_config_no_snowcap/main.rs
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../default_config/main.rs
|
46
api/rust/examples/default_config_no_snowcap/metaconfig.toml
Normal file
46
api/rust/examples/default_config_no_snowcap/metaconfig.toml
Normal 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", "--example", "default_config", "--no-default-features"]
|
||||||
|
|
||||||
|
### 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"
|
|
@ -15,8 +15,8 @@ use pinnacle_api_defs::pinnacle::input::{
|
||||||
v0alpha1::{
|
v0alpha1::{
|
||||||
input_service_client::InputServiceClient,
|
input_service_client::InputServiceClient,
|
||||||
set_libinput_setting_request::{CalibrationMatrix, Setting},
|
set_libinput_setting_request::{CalibrationMatrix, Setting},
|
||||||
SetKeybindRequest, SetLibinputSettingRequest, SetMousebindRequest, SetRepeatRateRequest,
|
KeybindDescriptionsRequest, SetKeybindRequest, SetLibinputSettingRequest,
|
||||||
SetXkbConfigRequest,
|
SetMousebindRequest, SetRepeatRateRequest, SetXkbConfigRequest,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use tokio::sync::mpsc::UnboundedSender;
|
use tokio::sync::mpsc::UnboundedSender;
|
||||||
|
@ -99,6 +99,32 @@ pub struct Input {
|
||||||
fut_sender: UnboundedSender<BoxFuture<'static, ()>>,
|
fut_sender: UnboundedSender<BoxFuture<'static, ()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Keybind information.
|
||||||
|
///
|
||||||
|
/// Mainly used for the keybind list.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
|
||||||
|
pub struct KeybindInfo {
|
||||||
|
/// The group to place this keybind in.
|
||||||
|
pub group: Option<String>,
|
||||||
|
/// The description of this keybind.
|
||||||
|
pub description: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The description of a keybind.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub struct KeybindDescription {
|
||||||
|
/// The keybind's modifiers.
|
||||||
|
pub modifiers: Vec<Mod>,
|
||||||
|
/// The keysym code.
|
||||||
|
pub key_code: u32,
|
||||||
|
/// The name of the key.
|
||||||
|
pub xkb_name: String,
|
||||||
|
/// The group.
|
||||||
|
pub group: Option<String>,
|
||||||
|
/// The description of the keybind.
|
||||||
|
pub description: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
impl Input {
|
impl Input {
|
||||||
pub(crate) fn new(
|
pub(crate) fn new(
|
||||||
channel: Channel,
|
channel: Channel,
|
||||||
|
@ -157,25 +183,28 @@ impl Input {
|
||||||
mods: impl IntoIterator<Item = Mod>,
|
mods: impl IntoIterator<Item = Mod>,
|
||||||
key: impl Key + Send + 'static,
|
key: impl Key + Send + 'static,
|
||||||
mut action: impl FnMut() + Send + 'static,
|
mut action: impl FnMut() + Send + 'static,
|
||||||
|
keybind_info: impl Into<Option<KeybindInfo>>,
|
||||||
) {
|
) {
|
||||||
let mut client = self.create_input_client();
|
let mut client = self.create_input_client();
|
||||||
|
|
||||||
let modifiers = mods.into_iter().map(|modif| modif as i32).collect();
|
let modifiers = mods.into_iter().map(|modif| modif as i32).collect();
|
||||||
|
|
||||||
self.fut_sender
|
let keybind_info: Option<KeybindInfo> = keybind_info.into();
|
||||||
.send(
|
|
||||||
async move {
|
let mut stream = block_on_tokio(client.set_keybind(SetKeybindRequest {
|
||||||
let mut stream = client
|
|
||||||
.set_keybind(SetKeybindRequest {
|
|
||||||
modifiers,
|
modifiers,
|
||||||
key: Some(input::v0alpha1::set_keybind_request::Key::RawCode(
|
key: Some(input::v0alpha1::set_keybind_request::Key::RawCode(
|
||||||
key.into_keysym().raw(),
|
key.into_keysym().raw(),
|
||||||
)),
|
)),
|
||||||
})
|
group: keybind_info.clone().and_then(|info| info.group),
|
||||||
.await
|
description: keybind_info.clone().and_then(|info| info.description),
|
||||||
|
}))
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.into_inner();
|
.into_inner();
|
||||||
|
|
||||||
|
self.fut_sender
|
||||||
|
.send(
|
||||||
|
async move {
|
||||||
while let Some(Ok(_response)) = stream.next().await {
|
while let Some(Ok(_response)) = stream.next().await {
|
||||||
action();
|
action();
|
||||||
tokio::task::yield_now().await;
|
tokio::task::yield_now().await;
|
||||||
|
@ -218,20 +247,17 @@ impl Input {
|
||||||
let mut client = self.create_input_client();
|
let mut client = self.create_input_client();
|
||||||
|
|
||||||
let modifiers = mods.into_iter().map(|modif| modif as i32).collect();
|
let modifiers = mods.into_iter().map(|modif| modif as i32).collect();
|
||||||
|
let mut stream = block_on_tokio(client.set_mousebind(SetMousebindRequest {
|
||||||
|
modifiers,
|
||||||
|
button: Some(button as u32),
|
||||||
|
edge: Some(edge as i32),
|
||||||
|
}))
|
||||||
|
.unwrap()
|
||||||
|
.into_inner();
|
||||||
|
|
||||||
self.fut_sender
|
self.fut_sender
|
||||||
.send(
|
.send(
|
||||||
async move {
|
async move {
|
||||||
let mut stream = client
|
|
||||||
.set_mousebind(SetMousebindRequest {
|
|
||||||
modifiers,
|
|
||||||
button: Some(button as u32),
|
|
||||||
edge: Some(edge as i32),
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.into_inner();
|
|
||||||
|
|
||||||
while let Some(Ok(_response)) = stream.next().await {
|
while let Some(Ok(_response)) = stream.next().await {
|
||||||
action();
|
action();
|
||||||
tokio::task::yield_now().await;
|
tokio::task::yield_now().await;
|
||||||
|
@ -242,6 +268,31 @@ impl Input {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get all keybinds and their information.
|
||||||
|
pub fn keybind_descriptions(&self) -> impl Iterator<Item = KeybindDescription> {
|
||||||
|
let mut client = self.create_input_client();
|
||||||
|
let descriptions =
|
||||||
|
block_on_tokio(client.keybind_descriptions(KeybindDescriptionsRequest {})).unwrap();
|
||||||
|
let descriptions = descriptions.into_inner();
|
||||||
|
|
||||||
|
descriptions.descriptions.into_iter().map(|desc| {
|
||||||
|
let mods = desc.modifiers().flat_map(|m| match m {
|
||||||
|
input::v0alpha1::Modifier::Unspecified => None,
|
||||||
|
input::v0alpha1::Modifier::Shift => Some(Mod::Shift),
|
||||||
|
input::v0alpha1::Modifier::Ctrl => Some(Mod::Ctrl),
|
||||||
|
input::v0alpha1::Modifier::Alt => Some(Mod::Alt),
|
||||||
|
input::v0alpha1::Modifier::Super => Some(Mod::Super),
|
||||||
|
});
|
||||||
|
KeybindDescription {
|
||||||
|
modifiers: mods.collect(),
|
||||||
|
key_code: desc.raw_code(),
|
||||||
|
xkb_name: desc.xkb_name().to_string(),
|
||||||
|
group: desc.group,
|
||||||
|
description: desc.description,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Set the xkeyboard config.
|
/// Set the xkeyboard config.
|
||||||
///
|
///
|
||||||
/// This allows you to set several xkeyboard options like `layout` and `rules`.
|
/// This allows you to set several xkeyboard options like `layout` and `rules`.
|
||||||
|
|
|
@ -85,6 +85,8 @@ use pinnacle::Pinnacle;
|
||||||
use process::Process;
|
use process::Process;
|
||||||
use render::Render;
|
use render::Render;
|
||||||
use signal::SignalState;
|
use signal::SignalState;
|
||||||
|
#[cfg(feature = "snowcap")]
|
||||||
|
use snowcap::Snowcap;
|
||||||
use tag::Tag;
|
use tag::Tag;
|
||||||
use tokio::sync::{
|
use tokio::sync::{
|
||||||
mpsc::{unbounded_channel, UnboundedReceiver},
|
mpsc::{unbounded_channel, UnboundedReceiver},
|
||||||
|
@ -102,11 +104,15 @@ pub mod pinnacle;
|
||||||
pub mod process;
|
pub mod process;
|
||||||
pub mod render;
|
pub mod render;
|
||||||
pub mod signal;
|
pub mod signal;
|
||||||
|
#[cfg(feature = "snowcap")]
|
||||||
|
pub mod snowcap;
|
||||||
pub mod tag;
|
pub mod tag;
|
||||||
pub mod util;
|
pub mod util;
|
||||||
pub mod window;
|
pub mod window;
|
||||||
|
|
||||||
pub use pinnacle_api_macros::config;
|
pub use pinnacle_api_macros::config;
|
||||||
|
#[cfg(feature = "snowcap")]
|
||||||
|
pub use snowcap_api;
|
||||||
pub use tokio;
|
pub use tokio;
|
||||||
pub use xkbcommon;
|
pub use xkbcommon;
|
||||||
|
|
||||||
|
@ -131,6 +137,10 @@ pub struct ApiModules {
|
||||||
/// The [`Render`] struct
|
/// The [`Render`] struct
|
||||||
pub render: &'static Render,
|
pub render: &'static Render,
|
||||||
signal: Arc<RwLock<SignalState>>,
|
signal: Arc<RwLock<SignalState>>,
|
||||||
|
|
||||||
|
#[cfg(feature = "snowcap")]
|
||||||
|
/// The snowcap widget system.
|
||||||
|
pub snowcap: &'static Snowcap,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for ApiModules {
|
impl std::fmt::Debug for ApiModules {
|
||||||
|
@ -145,16 +155,23 @@ impl std::fmt::Debug for ApiModules {
|
||||||
.field("layout", &self.layout)
|
.field("layout", &self.layout)
|
||||||
.field("render", &self.render)
|
.field("render", &self.render)
|
||||||
.field("signal", &"...")
|
.field("signal", &"...")
|
||||||
|
// TODO: snowcap
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Api receivers.
|
||||||
|
pub struct Receivers {
|
||||||
|
pinnacle: UnboundedReceiver<BoxFuture<'static, ()>>,
|
||||||
|
#[cfg(feature = "snowcap")]
|
||||||
|
snowcap: UnboundedReceiver<tokio::task::JoinHandle<()>>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Connects to Pinnacle and builds the configuration structs.
|
/// Connects to Pinnacle and builds the configuration structs.
|
||||||
///
|
///
|
||||||
/// This function is inserted at the top of your config through the [`config`] macro.
|
/// This function is inserted at the top of your config through the [`config`] macro.
|
||||||
/// You should use that macro instead of this function directly.
|
/// You should use that macro instead of this function directly.
|
||||||
pub async fn connect(
|
pub async fn connect() -> Result<(ApiModules, Receivers), Box<dyn std::error::Error>> {
|
||||||
) -> Result<(ApiModules, UnboundedReceiver<BoxFuture<'static, ()>>), Box<dyn std::error::Error>> {
|
|
||||||
// port doesn't matter, we use a unix socket
|
// port doesn't matter, we use a unix socket
|
||||||
let channel = Endpoint::try_from("http://[::]:50051")?
|
let channel = Endpoint::try_from("http://[::]:50051")?
|
||||||
.connect_with_connector(service_fn(|_: Uri| {
|
.connect_with_connector(service_fn(|_: Uri| {
|
||||||
|
@ -163,7 +180,8 @@ pub async fn connect(
|
||||||
.expect("PINNACLE_GRPC_SOCKET was not set; is Pinnacle running?"),
|
.expect("PINNACLE_GRPC_SOCKET was not set; is Pinnacle running?"),
|
||||||
)
|
)
|
||||||
}))
|
}))
|
||||||
.await?;
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let (fut_sender, fut_recv) = unbounded_channel::<BoxFuture<'static, ()>>();
|
let (fut_sender, fut_recv) = unbounded_channel::<BoxFuture<'static, ()>>();
|
||||||
|
|
||||||
|
@ -181,6 +199,7 @@ pub async fn connect(
|
||||||
let render = Box::leak(Box::new(Render::new(channel.clone())));
|
let render = Box::leak(Box::new(Render::new(channel.clone())));
|
||||||
let layout = Box::leak(Box::new(Layout::new(channel.clone(), fut_sender.clone())));
|
let layout = Box::leak(Box::new(Layout::new(channel.clone(), fut_sender.clone())));
|
||||||
|
|
||||||
|
#[cfg(not(feature = "snowcap"))]
|
||||||
let modules = ApiModules {
|
let modules = ApiModules {
|
||||||
pinnacle,
|
pinnacle,
|
||||||
process,
|
process,
|
||||||
|
@ -193,13 +212,40 @@ pub async fn connect(
|
||||||
signal: signal.clone(),
|
signal: signal.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "snowcap")]
|
||||||
|
let (modules, snowcap_recv) = {
|
||||||
|
let (snowcap, snowcap_recv) = snowcap_api::connect().await.unwrap();
|
||||||
|
let api_modules = ApiModules {
|
||||||
|
pinnacle,
|
||||||
|
process,
|
||||||
|
window,
|
||||||
|
input,
|
||||||
|
output,
|
||||||
|
tag,
|
||||||
|
layout,
|
||||||
|
render,
|
||||||
|
signal: signal.clone(),
|
||||||
|
snowcap: Box::leak(Box::new(Snowcap::new(snowcap))),
|
||||||
|
};
|
||||||
|
(api_modules, snowcap_recv)
|
||||||
|
};
|
||||||
|
|
||||||
window.finish_init(modules.clone());
|
window.finish_init(modules.clone());
|
||||||
output.finish_init(modules.clone());
|
output.finish_init(modules.clone());
|
||||||
tag.finish_init(modules.clone());
|
tag.finish_init(modules.clone());
|
||||||
layout.finish_init(modules.clone());
|
layout.finish_init(modules.clone());
|
||||||
signal.read().await.finish_init(modules.clone());
|
signal.read().await.finish_init(modules.clone());
|
||||||
|
|
||||||
Ok((modules, fut_recv))
|
#[cfg(feature = "snowcap")]
|
||||||
|
modules.snowcap.finish_init(modules.clone());
|
||||||
|
|
||||||
|
let receivers = Receivers {
|
||||||
|
pinnacle: fut_recv,
|
||||||
|
#[cfg(feature = "snowcap")]
|
||||||
|
snowcap: snowcap_recv,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok((modules, receivers))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Listen to Pinnacle for incoming messages.
|
/// Listen to Pinnacle for incoming messages.
|
||||||
|
@ -209,7 +255,15 @@ pub async fn connect(
|
||||||
///
|
///
|
||||||
/// This function is inserted at the end of your config through the [`config`] macro.
|
/// This function is inserted at the end of your config through the [`config`] macro.
|
||||||
/// You should use the macro instead of this function directly.
|
/// You should use the macro instead of this function directly.
|
||||||
pub async fn listen(api: ApiModules, fut_recv: UnboundedReceiver<BoxFuture<'static, ()>>) {
|
pub async fn listen(api: ApiModules, receivers: Receivers) {
|
||||||
|
#[cfg(feature = "snowcap")]
|
||||||
|
let Receivers {
|
||||||
|
pinnacle: fut_recv,
|
||||||
|
snowcap: snowcap_recv,
|
||||||
|
} = receivers;
|
||||||
|
#[cfg(not(feature = "snowcap"))]
|
||||||
|
let Receivers { pinnacle: fut_recv } = receivers;
|
||||||
|
|
||||||
let mut fut_recv = UnboundedReceiverStream::new(fut_recv);
|
let mut fut_recv = UnboundedReceiverStream::new(fut_recv);
|
||||||
let mut set = futures::stream::FuturesUnordered::new();
|
let mut set = futures::stream::FuturesUnordered::new();
|
||||||
|
|
||||||
|
@ -222,6 +276,9 @@ pub async fn listen(api: ApiModules, fut_recv: UnboundedReceiver<BoxFuture<'stat
|
||||||
}
|
}
|
||||||
.boxed();
|
.boxed();
|
||||||
|
|
||||||
|
#[cfg(feature = "snowcap")]
|
||||||
|
tokio::spawn(snowcap_api::listen(snowcap_recv));
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
fut = fut_recv.next() => {
|
fut = fut_recv.next() => {
|
||||||
|
|
|
@ -74,6 +74,8 @@ macro_rules! signals {
|
||||||
.send((self.current_id, callback))
|
.send((self.current_id, callback))
|
||||||
.expect("failed to send callback");
|
.expect("failed to send callback");
|
||||||
|
|
||||||
|
self.callback_count.fetch_add(1, Ordering::SeqCst);
|
||||||
|
|
||||||
let handle = SignalHandle::new(self.current_id, remove_callback_sender);
|
let handle = SignalHandle::new(self.current_id, remove_callback_sender);
|
||||||
|
|
||||||
self.current_id.0 += 1;
|
self.current_id.0 += 1;
|
||||||
|
@ -376,6 +378,10 @@ where
|
||||||
|
|
||||||
control_sender
|
control_sender
|
||||||
.send(Req::from_control(StreamControl::Ready))
|
.send(Req::from_control(StreamControl::Ready))
|
||||||
|
.map_err(|err| {
|
||||||
|
println!("{err}");
|
||||||
|
err
|
||||||
|
})
|
||||||
.expect("send failed");
|
.expect("send failed");
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
@ -407,7 +413,8 @@ where
|
||||||
callback = callback_recv_recv => {
|
callback = callback_recv_recv => {
|
||||||
if let Some((id, callback)) = callback {
|
if let Some((id, callback)) = callback {
|
||||||
callbacks.insert(id, callback);
|
callbacks.insert(id, callback);
|
||||||
callback_count.fetch_add(1, Ordering::SeqCst);
|
// Added in `add_callback` in the macro above
|
||||||
|
// callback_count.fetch_add(1, Ordering::SeqCst);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
remove = remove_callback_recv_recv => {
|
remove = remove_callback_recv_recv => {
|
||||||
|
|
30
api/rust/src/snowcap.rs
Normal file
30
api/rust/src/snowcap.rs
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
//! The Snowcap widget system.
|
||||||
|
//! // TODO: these docs
|
||||||
|
|
||||||
|
use integration::Integration;
|
||||||
|
use snowcap_api::layer::Layer;
|
||||||
|
|
||||||
|
use crate::ApiModules;
|
||||||
|
|
||||||
|
pub mod integration;
|
||||||
|
|
||||||
|
/// Snowcap modules and Pinnacle integration.
|
||||||
|
pub struct Snowcap {
|
||||||
|
/// Create layer surface widgets.
|
||||||
|
pub layer: &'static Layer,
|
||||||
|
/// Pinnacle integrations.
|
||||||
|
pub integration: &'static Integration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Snowcap {
|
||||||
|
pub(crate) fn new(layer: Layer) -> Self {
|
||||||
|
Self {
|
||||||
|
layer: Box::leak(Box::new(layer)),
|
||||||
|
integration: Box::leak(Box::new(Integration::new())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn finish_init(&self, api: ApiModules) {
|
||||||
|
self.integration.finish_init(api);
|
||||||
|
}
|
||||||
|
}
|
318
api/rust/src/snowcap/integration.rs
Normal file
318
api/rust/src/snowcap/integration.rs
Normal file
|
@ -0,0 +1,318 @@
|
||||||
|
//! Pinnacle-specific integrations with Snowcap.
|
||||||
|
//!
|
||||||
|
//! This module includes builtin widgets like the exit prompt and keybind list.
|
||||||
|
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
|
use indexmap::IndexMap;
|
||||||
|
use snowcap_api::{
|
||||||
|
layer::{ExclusiveZone, KeyboardInteractivity, ZLayer},
|
||||||
|
widget::{
|
||||||
|
font::{Family, Font, Weight},
|
||||||
|
Alignment, Color, Column, Container, Length, Padding, Row, Scrollable, Text, WidgetDef,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use xkbcommon::xkb::Keysym;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
input::{KeybindDescription, Mod},
|
||||||
|
ApiModules,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Builtin widgets for Pinnacle.
|
||||||
|
pub struct Integration {
|
||||||
|
api: OnceLock<ApiModules>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Integration {
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
api: OnceLock::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn finish_init(&self, api: ApiModules) {
|
||||||
|
self.api.set(api).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create the default quit prompt.
|
||||||
|
///
|
||||||
|
/// Some of its characteristics can be changed by setting its fields.
|
||||||
|
pub fn quit_prompt(&self) -> QuitPrompt {
|
||||||
|
QuitPrompt {
|
||||||
|
api: self.api.get().cloned().unwrap(),
|
||||||
|
border_radius: 12.0,
|
||||||
|
border_thickness: 6.0,
|
||||||
|
background_color: [0.15, 0.03, 0.1, 0.65].into(),
|
||||||
|
border_color: [0.8, 0.2, 0.4].into(),
|
||||||
|
font: Font::new_with_family(Family::Name("Ubuntu".into())),
|
||||||
|
width: 220,
|
||||||
|
height: 120,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create the default keybind overlay.
|
||||||
|
///
|
||||||
|
/// Some of its characteristics can be changed by setting its fields.
|
||||||
|
pub fn keybind_overlay(&self) -> KeybindOverlay {
|
||||||
|
KeybindOverlay {
|
||||||
|
api: self.api.get().cloned().unwrap(),
|
||||||
|
border_radius: 12.0,
|
||||||
|
border_thickness: 6.0,
|
||||||
|
background_color: [0.15, 0.15, 0.225, 0.8].into(),
|
||||||
|
border_color: [0.4, 0.4, 0.7].into(),
|
||||||
|
font: Font::new_with_family(Family::Name("Ubuntu".into())),
|
||||||
|
width: 700,
|
||||||
|
height: 500,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A quit prompt.
|
||||||
|
///
|
||||||
|
/// When opened, pressing ENTER will quit the compositor.
|
||||||
|
pub struct QuitPrompt {
|
||||||
|
api: ApiModules,
|
||||||
|
/// The radius of the prompt's corners.
|
||||||
|
pub border_radius: f32,
|
||||||
|
/// The thickness of the prompt border.
|
||||||
|
pub border_thickness: f32,
|
||||||
|
/// The color of the prompt background.
|
||||||
|
pub background_color: Color,
|
||||||
|
/// The color of the prompt border.
|
||||||
|
pub border_color: Color,
|
||||||
|
/// The font of the prompt.
|
||||||
|
pub font: Font,
|
||||||
|
/// The height of the prompt.
|
||||||
|
pub width: u32,
|
||||||
|
/// The width of the prompt.
|
||||||
|
pub height: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl QuitPrompt {
|
||||||
|
/// Show this quit prompt.
|
||||||
|
pub fn show(&self) {
|
||||||
|
let widget = Container::new(Column::new_with_children([
|
||||||
|
Text::new("Quit Pinnacle?")
|
||||||
|
.font(self.font.clone().weight(Weight::Bold))
|
||||||
|
.size(20.0)
|
||||||
|
.into(),
|
||||||
|
Text::new("").size(8.0).into(), // Spacing because I haven't impl'd that yet
|
||||||
|
Text::new("Press ENTER to confirm, or\nany other key to close this")
|
||||||
|
.font(self.font.clone())
|
||||||
|
.size(14.0)
|
||||||
|
.into(),
|
||||||
|
]))
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill)
|
||||||
|
.vertical_alignment(Alignment::Center)
|
||||||
|
.horizontal_alignment(Alignment::Center)
|
||||||
|
.border_radius(self.border_radius)
|
||||||
|
.border_thickness(self.border_thickness)
|
||||||
|
.border_color(self.border_color)
|
||||||
|
.background_color(self.background_color);
|
||||||
|
|
||||||
|
self.api
|
||||||
|
.snowcap
|
||||||
|
.layer
|
||||||
|
.new_widget(
|
||||||
|
widget,
|
||||||
|
self.width,
|
||||||
|
self.height,
|
||||||
|
None,
|
||||||
|
KeyboardInteractivity::Exclusive,
|
||||||
|
ExclusiveZone::Respect,
|
||||||
|
ZLayer::Overlay,
|
||||||
|
)
|
||||||
|
.on_key_press(|handle, key, _mods| {
|
||||||
|
if key == Keysym::Return {
|
||||||
|
self.api.pinnacle.quit();
|
||||||
|
} else {
|
||||||
|
handle.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A keybind overlay.
|
||||||
|
pub struct KeybindOverlay {
|
||||||
|
api: ApiModules,
|
||||||
|
/// The radius of the prompt's corners.
|
||||||
|
pub border_radius: f32,
|
||||||
|
/// The thickness of the prompt border.
|
||||||
|
pub border_thickness: f32,
|
||||||
|
/// The color of the prompt background.
|
||||||
|
pub background_color: Color,
|
||||||
|
/// The color of the prompt border.
|
||||||
|
pub border_color: Color,
|
||||||
|
/// The font of the prompt.
|
||||||
|
pub font: Font,
|
||||||
|
/// The height of the prompt.
|
||||||
|
pub width: u32,
|
||||||
|
/// The width of the prompt.
|
||||||
|
pub height: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeybindOverlay {
|
||||||
|
/// Show this keybind overlay.
|
||||||
|
pub fn show(&self) {
|
||||||
|
let descriptions = self.api.input.keybind_descriptions();
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq, Hash)]
|
||||||
|
struct KeybindRepr {
|
||||||
|
mods: Vec<Mod>,
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for KeybindRepr {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let bind = self
|
||||||
|
.mods
|
||||||
|
.iter()
|
||||||
|
.map(|m| {
|
||||||
|
// TODO: strum name or something
|
||||||
|
match m {
|
||||||
|
Mod::Shift => "Shift",
|
||||||
|
Mod::Ctrl => "Ctrl",
|
||||||
|
Mod::Alt => "Alt",
|
||||||
|
Mod::Super => "Super",
|
||||||
|
}
|
||||||
|
.to_string()
|
||||||
|
})
|
||||||
|
.chain([self.name.clone()])
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" + ");
|
||||||
|
write!(f, "{bind}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct GroupData {
|
||||||
|
binds: IndexMap<KeybindRepr, Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut groups = IndexMap::<Option<String>, GroupData>::new();
|
||||||
|
|
||||||
|
for desc in descriptions {
|
||||||
|
let KeybindDescription {
|
||||||
|
modifiers,
|
||||||
|
key_code: _,
|
||||||
|
xkb_name,
|
||||||
|
group,
|
||||||
|
description,
|
||||||
|
} = desc;
|
||||||
|
|
||||||
|
let repr = KeybindRepr {
|
||||||
|
mods: modifiers,
|
||||||
|
name: xkb_name,
|
||||||
|
};
|
||||||
|
|
||||||
|
let group = groups.entry(group).or_default();
|
||||||
|
|
||||||
|
let descs = group.binds.entry(repr).or_default();
|
||||||
|
|
||||||
|
if let Some(desc) = description {
|
||||||
|
descs.push(desc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// List keybinds with no group last
|
||||||
|
if let Some(data) = groups.shift_remove(&None) {
|
||||||
|
groups.insert(None, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
let sections = groups.into_iter().flat_map(|(group, data)| {
|
||||||
|
let group_title = Text::new(group.unwrap_or("Other".into()))
|
||||||
|
.font(self.font.clone().weight(Weight::Bold))
|
||||||
|
.size(19.0);
|
||||||
|
|
||||||
|
let binds = data.binds.into_iter().map(|(key, descs)| {
|
||||||
|
if descs.is_empty() {
|
||||||
|
WidgetDef::from(Text::new(key.to_string()).font(self.font.clone()))
|
||||||
|
} else if descs.len() == 1 {
|
||||||
|
Row::new_with_children([
|
||||||
|
Text::new(key.to_string())
|
||||||
|
.width(Length::FillPortion(1))
|
||||||
|
.font(self.font.clone())
|
||||||
|
.into(),
|
||||||
|
Text::new(descs[0].clone())
|
||||||
|
.width(Length::FillPortion(2))
|
||||||
|
.font(self.font.clone())
|
||||||
|
.into(),
|
||||||
|
])
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
let mut children = Vec::<WidgetDef>::new();
|
||||||
|
children.push(
|
||||||
|
Text::new(key.to_string() + ":")
|
||||||
|
.font(self.font.clone())
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
|
||||||
|
for desc in descs {
|
||||||
|
children.push(
|
||||||
|
Text::new(format!("\t{}", desc))
|
||||||
|
.font(self.font.clone())
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Column::new_with_children(children).into()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut children = Vec::<WidgetDef>::new();
|
||||||
|
children.push(group_title.into());
|
||||||
|
for widget in binds {
|
||||||
|
children.push(widget);
|
||||||
|
}
|
||||||
|
children.push(Text::new("").size(8.0).into()); // Spacing because I haven't impl'd that yet
|
||||||
|
|
||||||
|
children
|
||||||
|
});
|
||||||
|
|
||||||
|
let scrollable = Scrollable::new(Column::new_with_children(sections))
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill);
|
||||||
|
|
||||||
|
let widget = Container::new(Column::new_with_children([
|
||||||
|
Text::new("Keybinds")
|
||||||
|
.font(self.font.clone().weight(Weight::Bold))
|
||||||
|
.size(24.0)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.into(),
|
||||||
|
Text::new("").size(8.0).into(), // Spacing because I haven't impl'd that yet
|
||||||
|
scrollable.into(),
|
||||||
|
]))
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill)
|
||||||
|
.padding(Padding {
|
||||||
|
top: 16.0,
|
||||||
|
right: 16.0,
|
||||||
|
bottom: 16.0,
|
||||||
|
left: 16.0,
|
||||||
|
})
|
||||||
|
.vertical_alignment(Alignment::Center)
|
||||||
|
.horizontal_alignment(Alignment::Center)
|
||||||
|
.border_radius(self.border_radius)
|
||||||
|
.border_thickness(self.border_thickness)
|
||||||
|
.border_color(self.border_color)
|
||||||
|
.background_color(self.background_color);
|
||||||
|
|
||||||
|
self.api
|
||||||
|
.snowcap
|
||||||
|
.layer
|
||||||
|
.new_widget(
|
||||||
|
widget,
|
||||||
|
self.width,
|
||||||
|
self.height,
|
||||||
|
None,
|
||||||
|
KeyboardInteractivity::Exclusive,
|
||||||
|
ExclusiveZone::Respect,
|
||||||
|
ZLayer::Top,
|
||||||
|
)
|
||||||
|
.on_key_press(|handle, _key, _mods| {
|
||||||
|
handle.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -50,6 +50,7 @@
|
||||||
libinput
|
libinput
|
||||||
mesa
|
mesa
|
||||||
xwayland
|
xwayland
|
||||||
|
libdisplay-info
|
||||||
|
|
||||||
# winit on x11
|
# winit on x11
|
||||||
xorg.libXcursor
|
xorg.libXcursor
|
||||||
|
|
1
snowcap
Submodule
1
snowcap
Submodule
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 3e79a16e1065f07001b08fabdc763bb274cac394
|
55
src/api.rs
55
src/api.rs
|
@ -9,6 +9,7 @@ use pinnacle_api_defs::pinnacle::{
|
||||||
input_service_server,
|
input_service_server,
|
||||||
set_libinput_setting_request::{AccelProfile, ClickMethod, ScrollMethod, TapButtonMap},
|
set_libinput_setting_request::{AccelProfile, ClickMethod, ScrollMethod, TapButtonMap},
|
||||||
set_mousebind_request::MouseEdge,
|
set_mousebind_request::MouseEdge,
|
||||||
|
KeybindDescription, KeybindDescriptionsRequest, KeybindDescriptionsResponse, Modifier,
|
||||||
SetKeybindRequest, SetKeybindResponse, SetLibinputSettingRequest, SetMousebindRequest,
|
SetKeybindRequest, SetKeybindResponse, SetLibinputSettingRequest, SetMousebindRequest,
|
||||||
SetMousebindResponse, SetRepeatRateRequest, SetXkbConfigRequest,
|
SetMousebindResponse, SetRepeatRateRequest, SetXkbConfigRequest,
|
||||||
},
|
},
|
||||||
|
@ -55,7 +56,7 @@ use tracing::{debug, error, info, trace, warn};
|
||||||
use crate::{
|
use crate::{
|
||||||
backend::{udev::drm_mode_from_api_modeline, BackendData},
|
backend::{udev::drm_mode_from_api_modeline, BackendData},
|
||||||
config::ConnectorSavedState,
|
config::ConnectorSavedState,
|
||||||
input::ModifierMask,
|
input::{KeybindData, ModifierMask},
|
||||||
output::{OutputMode, OutputName},
|
output::{OutputMode, OutputName},
|
||||||
render::util::snapshot::capture_snapshots_on_output,
|
render::util::snapshot::capture_snapshots_on_output,
|
||||||
state::{State, WithState},
|
state::{State, WithState},
|
||||||
|
@ -293,12 +294,21 @@ impl input_service_server::InputService for InputService {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let group = request.group;
|
||||||
|
let description = request.description;
|
||||||
|
|
||||||
run_server_streaming(&self.sender, move |state, sender| {
|
run_server_streaming(&self.sender, move |state, sender| {
|
||||||
|
let keybind_data = KeybindData {
|
||||||
|
sender,
|
||||||
|
group,
|
||||||
|
description,
|
||||||
|
};
|
||||||
|
|
||||||
state
|
state
|
||||||
.pinnacle
|
.pinnacle
|
||||||
.input_state
|
.input_state
|
||||||
.keybinds
|
.keybinds
|
||||||
.insert((modifiers, keysym), sender);
|
.insert((modifiers, keysym), keybind_data);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -346,6 +356,47 @@ impl input_service_server::InputService for InputService {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn keybind_descriptions(
|
||||||
|
&self,
|
||||||
|
_request: Request<KeybindDescriptionsRequest>,
|
||||||
|
) -> Result<Response<KeybindDescriptionsResponse>, Status> {
|
||||||
|
run_unary(&self.sender, |state| {
|
||||||
|
let descriptions =
|
||||||
|
state
|
||||||
|
.pinnacle
|
||||||
|
.input_state
|
||||||
|
.keybinds
|
||||||
|
.iter()
|
||||||
|
.map(|((mods, key), data)| {
|
||||||
|
let mut modifiers = Vec::<i32>::new();
|
||||||
|
if mods.contains(ModifierMask::CTRL) {
|
||||||
|
modifiers.push(Modifier::Ctrl as i32);
|
||||||
|
}
|
||||||
|
if mods.contains(ModifierMask::ALT) {
|
||||||
|
modifiers.push(Modifier::Alt as i32);
|
||||||
|
}
|
||||||
|
if mods.contains(ModifierMask::SUPER) {
|
||||||
|
modifiers.push(Modifier::Super as i32);
|
||||||
|
}
|
||||||
|
if mods.contains(ModifierMask::SHIFT) {
|
||||||
|
modifiers.push(Modifier::Shift as i32);
|
||||||
|
}
|
||||||
|
KeybindDescription {
|
||||||
|
modifiers,
|
||||||
|
raw_code: Some(key.raw()),
|
||||||
|
xkb_name: Some(xkbcommon::xkb::keysym_get_name(*key)),
|
||||||
|
group: data.group.clone(),
|
||||||
|
description: data.description.clone(),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
KeybindDescriptionsResponse {
|
||||||
|
descriptions: descriptions.collect(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
async fn set_xkb_config(
|
async fn set_xkb_config(
|
||||||
&self,
|
&self,
|
||||||
request: Request<SetXkbConfigRequest>,
|
request: Request<SetXkbConfigRequest>,
|
||||||
|
|
|
@ -678,7 +678,7 @@ impl WlrLayerShellHandler for State {
|
||||||
let output = output
|
let output = output
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(Output::from_resource)
|
.and_then(Output::from_resource)
|
||||||
.or_else(|| self.pinnacle.space.outputs().next().cloned());
|
.or_else(|| self.pinnacle.focused_output().cloned());
|
||||||
|
|
||||||
let Some(output) = output else {
|
let Some(output) = output else {
|
||||||
error!("New layer surface, but there was no output to map it on");
|
error!("New layer surface, but there was no output to map it on");
|
||||||
|
|
15
src/input.rs
15
src/input.rs
|
@ -13,6 +13,7 @@ use crate::{
|
||||||
state::{Pinnacle, WithState},
|
state::{Pinnacle, WithState},
|
||||||
window::WindowElement,
|
window::WindowElement,
|
||||||
};
|
};
|
||||||
|
use indexmap::IndexMap;
|
||||||
use pinnacle_api_defs::pinnacle::input::v0alpha1::{
|
use pinnacle_api_defs::pinnacle::input::v0alpha1::{
|
||||||
set_libinput_setting_request::Setting, set_mousebind_request, SetKeybindResponse,
|
set_libinput_setting_request::Setting, set_mousebind_request, SetKeybindResponse,
|
||||||
SetMousebindResponse,
|
SetMousebindResponse,
|
||||||
|
@ -93,14 +94,20 @@ impl From<&ModifiersState> for ModifierMask {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct KeybindData {
|
||||||
|
pub sender: UnboundedSender<Result<SetKeybindResponse, tonic::Status>>,
|
||||||
|
pub group: Option<String>,
|
||||||
|
pub description: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct InputState {
|
pub struct InputState {
|
||||||
// TODO: move all of these to config
|
// TODO: move all of these to config
|
||||||
pub reload_keybind: Option<(ModifierMask, Keysym)>,
|
pub reload_keybind: Option<(ModifierMask, Keysym)>,
|
||||||
pub kill_keybind: Option<(ModifierMask, Keysym)>,
|
pub kill_keybind: Option<(ModifierMask, Keysym)>,
|
||||||
|
|
||||||
pub keybinds:
|
pub keybinds: IndexMap<(ModifierMask, Keysym), KeybindData>,
|
||||||
HashMap<(ModifierMask, Keysym), UnboundedSender<Result<SetKeybindResponse, tonic::Status>>>,
|
|
||||||
pub mousebinds: HashMap<
|
pub mousebinds: HashMap<
|
||||||
(ModifierMask, u32, set_mousebind_request::MouseEdge),
|
(ModifierMask, u32, set_mousebind_request::MouseEdge),
|
||||||
UnboundedSender<Result<SetMousebindResponse, tonic::Status>>,
|
UnboundedSender<Result<SetMousebindResponse, tonic::Status>>,
|
||||||
|
@ -540,7 +547,7 @@ impl State {
|
||||||
let raw_sym = keysym.raw_syms().iter().next();
|
let raw_sym = keysym.raw_syms().iter().next();
|
||||||
let mod_sym = keysym.modified_sym();
|
let mod_sym = keysym.modified_sym();
|
||||||
|
|
||||||
if let Some(sender) = state
|
if let Some(keybind_data) = state
|
||||||
.pinnacle
|
.pinnacle
|
||||||
.input_state
|
.input_state
|
||||||
.keybinds
|
.keybinds
|
||||||
|
@ -557,7 +564,7 @@ impl State {
|
||||||
{
|
{
|
||||||
if state.pinnacle.lock_state.is_unlocked() {
|
if state.pinnacle.lock_state.is_unlocked() {
|
||||||
return FilterResult::Intercept(KeyAction::CallCallback(
|
return FilterResult::Intercept(KeyAction::CallCallback(
|
||||||
sender.clone(),
|
keybind_data.sender.clone(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
41
src/main.rs
41
src/main.rs
|
@ -11,7 +11,10 @@
|
||||||
// #![deny(unused_imports)] // this has remained commented out for months lol
|
// #![deny(unused_imports)] // this has remained commented out for months lol
|
||||||
#![warn(clippy::unwrap_used)]
|
#![warn(clippy::unwrap_used)]
|
||||||
|
|
||||||
use std::io::{BufRead, BufReader};
|
use std::{
|
||||||
|
io::{BufRead, BufReader},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use pinnacle::{
|
use pinnacle::{
|
||||||
|
@ -43,7 +46,7 @@ async fn main() -> anyhow::Result<()> {
|
||||||
let env_filter = EnvFilter::try_from_default_env();
|
let env_filter = EnvFilter::try_from_default_env();
|
||||||
|
|
||||||
let file_log_env_filter =
|
let file_log_env_filter =
|
||||||
EnvFilter::new("debug,h2=warn,hyper=warn,smithay::xwayland::xwm=warn");
|
EnvFilter::new("debug,h2=warn,hyper=warn,smithay::xwayland::xwm=warn,wgpu_hal=warn,naga=warn,wgpu_core=warn,cosmic_text=warn,iced_wgpu=warn,sctk=warn");
|
||||||
|
|
||||||
let file_log_layer = tracing_subscriber::fmt::layer()
|
let file_log_layer = tracing_subscriber::fmt::layer()
|
||||||
.compact()
|
.compact()
|
||||||
|
@ -51,7 +54,8 @@ async fn main() -> anyhow::Result<()> {
|
||||||
.with_writer(appender)
|
.with_writer(appender)
|
||||||
.with_filter(file_log_env_filter);
|
.with_filter(file_log_env_filter);
|
||||||
|
|
||||||
let stdout_env_filter = env_filter.unwrap_or_else(|_| EnvFilter::new("warn,pinnacle=info"));
|
let stdout_env_filter =
|
||||||
|
env_filter.unwrap_or_else(|_| EnvFilter::new("warn,pinnacle=info,snowcap=info"));
|
||||||
let stdout_layer = tracing_subscriber::fmt::layer()
|
let stdout_layer = tracing_subscriber::fmt::layer()
|
||||||
.compact()
|
.compact()
|
||||||
.with_writer(std::io::stdout)
|
.with_writer(std::io::stdout)
|
||||||
|
@ -170,6 +174,35 @@ async fn main() -> anyhow::Result<()> {
|
||||||
.pinnacle
|
.pinnacle
|
||||||
.start_grpc_server(&metaconfig.socket_dir.clone())?;
|
.start_grpc_server(&metaconfig.socket_dir.clone())?;
|
||||||
|
|
||||||
|
#[cfg(feature = "snowcap")]
|
||||||
|
{
|
||||||
|
use smithay::reexports::calloop;
|
||||||
|
use std::sync::{
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
|
Arc,
|
||||||
|
};
|
||||||
|
|
||||||
|
info!("Starting Snowcap");
|
||||||
|
let (ping, source) = calloop::ping::make_ping()?;
|
||||||
|
let ready_flag = Arc::new(AtomicBool::new(false));
|
||||||
|
let ready_clone = ready_flag.clone();
|
||||||
|
let join_handle = tokio::task::spawn_blocking(move || {
|
||||||
|
let _span = tracing::error_span!("snowcap");
|
||||||
|
let _span = _span.enter();
|
||||||
|
snowcap::start(Some(source), ready_clone);
|
||||||
|
});
|
||||||
|
|
||||||
|
while !ready_flag.load(Ordering::SeqCst) {
|
||||||
|
if join_handle.is_finished() {
|
||||||
|
panic!("snowcap failed to start");
|
||||||
|
}
|
||||||
|
event_loop.dispatch(Duration::from_secs(1), &mut state)?;
|
||||||
|
state.on_event_loop_cycle_completion();
|
||||||
|
}
|
||||||
|
state.pinnacle.snowcap_shutdown_ping = Some(ping);
|
||||||
|
state.pinnacle.snowcap_join_handle = Some(join_handle);
|
||||||
|
}
|
||||||
|
|
||||||
if !metaconfig.no_xwayland {
|
if !metaconfig.no_xwayland {
|
||||||
match state.pinnacle.insert_xwayland_source() {
|
match state.pinnacle.insert_xwayland_source() {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
|
@ -189,7 +222,7 @@ async fn main() -> anyhow::Result<()> {
|
||||||
info!("`no-config` option was set, not spawning config");
|
info!("`no-config` option was set, not spawning config");
|
||||||
}
|
}
|
||||||
|
|
||||||
event_loop.run(None, &mut state, |state| {
|
event_loop.run(Duration::from_secs(1), &mut state, |state| {
|
||||||
state.on_event_loop_cycle_completion();
|
state.on_event_loop_cycle_completion();
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
|
28
src/state.rs
28
src/state.rs
|
@ -155,6 +155,11 @@ pub struct Pinnacle {
|
||||||
pub idle_inhibiting_surfaces: HashSet<WlSurface>,
|
pub idle_inhibiting_surfaces: HashSet<WlSurface>,
|
||||||
|
|
||||||
pub outputs: IndexMap<Output, Option<GlobalId>>,
|
pub outputs: IndexMap<Output, Option<GlobalId>>,
|
||||||
|
|
||||||
|
#[cfg(feature = "snowcap")]
|
||||||
|
pub snowcap_shutdown_ping: Option<smithay::reexports::calloop::ping::Ping>,
|
||||||
|
#[cfg(feature = "snowcap")]
|
||||||
|
pub snowcap_join_handle: Option<tokio::task::JoinHandle<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
|
@ -170,6 +175,19 @@ impl State {
|
||||||
winit.render_if_scheduled(&mut self.pinnacle);
|
winit.render_if_scheduled(&mut self.pinnacle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "snowcap")]
|
||||||
|
if self
|
||||||
|
.pinnacle
|
||||||
|
.snowcap_join_handle
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|handle| handle.is_finished())
|
||||||
|
{
|
||||||
|
// If Snowcap is dead, the config has most likely crashed or will crash if it's used.
|
||||||
|
// The embedded config will also fail to start.
|
||||||
|
// We'll panic here just so people aren't stuck in the compositor.
|
||||||
|
panic!("snowcap has exited");
|
||||||
|
}
|
||||||
|
|
||||||
// FIXME: Don't poll this every cycle
|
// FIXME: Don't poll this every cycle
|
||||||
for output in self.pinnacle.space.outputs().cloned().collect::<Vec<_>>() {
|
for output in self.pinnacle.space.outputs().cloned().collect::<Vec<_>>() {
|
||||||
output.with_state_mut(|state| {
|
output.with_state_mut(|state| {
|
||||||
|
@ -355,6 +373,11 @@ impl Pinnacle {
|
||||||
idle_inhibiting_surfaces: HashSet::new(),
|
idle_inhibiting_surfaces: HashSet::new(),
|
||||||
|
|
||||||
outputs: IndexMap::new(),
|
outputs: IndexMap::new(),
|
||||||
|
|
||||||
|
#[cfg(feature = "snowcap")]
|
||||||
|
snowcap_shutdown_ping: None,
|
||||||
|
#[cfg(feature = "snowcap")]
|
||||||
|
snowcap_join_handle: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(pinnacle)
|
Ok(pinnacle)
|
||||||
|
@ -388,6 +411,11 @@ impl Pinnacle {
|
||||||
warn!("Failed to send shutdown signal to config: {err}");
|
warn!("Failed to send shutdown signal to config: {err}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "snowcap")]
|
||||||
|
if let Some(snowcap_shutdown_ping) = self.snowcap_shutdown_ping.take() {
|
||||||
|
snowcap_shutdown_ping.ping();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,8 +11,8 @@ crate-type = ["cdylib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
smithay = { workspace = true }
|
smithay = { workspace = true }
|
||||||
pinnacle = { path = "..", features = [ "wlcs" ] }
|
pinnacle = { path = "..", features = ["wlcs"], default-features = false }
|
||||||
pinnacle-api = { path = "../api/rust" }
|
pinnacle-api = { path = "../api/rust", default-features = false }
|
||||||
wayland-sys = { version = "0.31.1", features = ["client", "server"] }
|
wayland-sys = { version = "0.31.1", features = ["client", "server"] }
|
||||||
wlcs = "0.1"
|
wlcs = "0.1"
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue