mirror of
https://github.com/pinnacle-comp/pinnacle.git
synced 2024-12-25 09:59:21 +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:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Get Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Cache stuff
|
||||
|
@ -39,6 +41,8 @@ jobs:
|
|||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Get Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Cache stuff
|
||||
|
@ -55,16 +59,18 @@ jobs:
|
|||
uses: extractions/setup-just@v1
|
||||
- name: Test
|
||||
if: ${{ runner.debug != '1' }}
|
||||
run: just install test -- --test-threads=1
|
||||
run: just install test --no-default-features -- --test-threads=1
|
||||
- name: Test (debug)
|
||||
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:
|
||||
runs-on: ubuntu-24.04
|
||||
name: Check formatting
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Get Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
|
@ -77,6 +83,8 @@ jobs:
|
|||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Get Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
|
|
2
.github/workflows/ldoc.yml
vendored
2
.github/workflows/ldoc.yml
vendored
|
@ -25,6 +25,8 @@ jobs:
|
|||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Get ldoc_gen
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
|
|
4
.github/workflows/rustdoc.yml
vendored
4
.github/workflows/rustdoc.yml
vendored
|
@ -22,10 +22,12 @@ jobs:
|
|||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Get protoc
|
||||
run: sudo apt install protobuf-compiler
|
||||
- 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
|
||||
run: echo "<meta http-equiv=\"refresh\" content=\"0; url=pinnacle_api\">" > ./target/doc/index.html
|
||||
- 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",
|
||||
"wlcs_pinnacle",
|
||||
]
|
||||
exclude = ["snowcap"]
|
||||
|
||||
[workspace.package]
|
||||
authors = ["Ottatop <ottatop1227@gmail.com>"]
|
||||
|
@ -13,7 +14,7 @@ repository = "https://github.com/pinnacle-comp/pinnacle/"
|
|||
|
||||
[workspace.dependencies]
|
||||
# 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"] }
|
||||
# gRPC
|
||||
prost = "0.12.4"
|
||||
|
@ -32,6 +33,7 @@ bitflags = "2.5.0"
|
|||
clap = { version = "4.5.4", features = ["derive"] }
|
||||
dircpy = "0.3.16"
|
||||
tempfile = "3.10.1"
|
||||
indexmap = "2.2.6"
|
||||
|
||||
[workspace.dependencies.smithay]
|
||||
git = "https://github.com/Smithay/smithay"
|
||||
|
@ -113,11 +115,13 @@ pinnacle-api-defs = { workspace = true }
|
|||
dircpy = { workspace = true }
|
||||
chrono = "0.4.38"
|
||||
bytemuck = "1.16.0"
|
||||
pinnacle-api = { path = "./api/rust" }
|
||||
pinnacle-api = { path = "./api/rust", default-features = false }
|
||||
gag = "1.0.0"
|
||||
drm-sys = "0.7.0"
|
||||
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]
|
||||
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"
|
||||
tempfile = { workspace = true }
|
||||
test-log = { version = "0.2.16", default-features = false, features = ["trace"] }
|
||||
pinnacle = { path = ".", features = ["wlcs"] }
|
||||
pinnacle-api = { path = "./api/rust" }
|
||||
pinnacle = { path = ".", features = ["wlcs"], default-features = false }
|
||||
pinnacle-api = { path = "./api/rust", default-features = false }
|
||||
|
||||
[features]
|
||||
default = ["snowcap"]
|
||||
snowcap = ["pinnacle-api/snowcap", "dep:snowcap", "dep:snowcap-api"]
|
||||
testing = [
|
||||
"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)
|
||||
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
|
||||
- Tag system
|
||||
- Customizable layouts, including most of the ones from Awesome
|
||||
|
@ -42,6 +50,7 @@ for Wayland.
|
|||
- wlr-layer-shell support
|
||||
- Configurable in Lua or Rust
|
||||
- wlr-screencopy support
|
||||
- A really *really* WIP widget system
|
||||
- Is very cool :thumbsup:
|
||||
|
||||
### Roadmap
|
||||
|
@ -51,57 +60,63 @@ for Wayland.
|
|||
You will need:
|
||||
|
||||
- [Rust](https://www.rust-lang.org/) 1.75 or newer
|
||||
- Packages for [Smithay](https://github.com/Smithay/smithay):
|
||||
`libwayland libxkbcommon libudev libinput libgdm libseat`, as well as `xwayland`
|
||||
- Arch:
|
||||
- The following external dependencies:
|
||||
- `libwayland`
|
||||
- `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
|
||||
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
|
||||
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
|
||||
`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
|
||||
> 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
|
||||
|
||||
# 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:
|
||||
```sh
|
||||
cargo build [--release]
|
||||
|
@ -126,6 +141,16 @@ After building, run the executable located in either:
|
|||
./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
|
||||
```sh
|
||||
cargo run [--release]
|
||||
|
@ -145,12 +170,16 @@ the Lua or Rust default configs standalone, run one of the following in the crat
|
|||
|
||||
```sh
|
||||
# For a Lua configuration
|
||||
cargo run -- -c "./api/lua/examples/default"
|
||||
just install run -- -c "./api/lua/examples/default"
|
||||
|
||||
# For a Rust configuration
|
||||
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
|
||||
|
@ -161,15 +190,9 @@ just install run -- -c "./api/rust/examples/default_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:
|
||||
```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.
|
||||
|
@ -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).
|
||||
| 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 right drag</kbd>| Resize window |
|
||||
| <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
|
||||
|
||||
Problems:
|
||||
|
@ -8,4 +6,3 @@ Problems:
|
|||
- 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
|
||||
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
|
||||
Input.keybind({ mod_key, "alt" }, "q", function()
|
||||
Pinnacle.quit()
|
||||
end)
|
||||
end, {
|
||||
group = "Compositor",
|
||||
description = "Quit Pinnacle",
|
||||
})
|
||||
|
||||
-- mod_key + alt + r = Reload config
|
||||
Input.keybind({ mod_key, "alt" }, "r", function()
|
||||
Pinnacle.reload_config()
|
||||
end)
|
||||
end, {
|
||||
group = "Compositor",
|
||||
description = "Reload the config",
|
||||
})
|
||||
|
||||
-- mod_key + alt + c = Close window
|
||||
Input.keybind({ mod_key, "alt" }, "c", function()
|
||||
|
@ -47,12 +53,18 @@ require("pinnacle").setup(function(Pinnacle)
|
|||
if focused then
|
||||
focused:close()
|
||||
end
|
||||
end)
|
||||
end, {
|
||||
group = "Window",
|
||||
description = "Close the focused window",
|
||||
})
|
||||
|
||||
-- mod_key + alt + Return = Spawn `terminal`
|
||||
Input.keybind({ mod_key }, key.Return, function()
|
||||
Process.spawn(terminal)
|
||||
end)
|
||||
end, {
|
||||
group = "Process",
|
||||
description = "Spawn `alacritty`",
|
||||
})
|
||||
|
||||
-- mod_key + alt + space = Toggle floating
|
||||
Input.keybind({ mod_key, "alt" }, key.space, function()
|
||||
|
@ -61,7 +73,10 @@ require("pinnacle").setup(function(Pinnacle)
|
|||
focused:toggle_floating()
|
||||
focused:raise()
|
||||
end
|
||||
end)
|
||||
end, {
|
||||
group = "Window",
|
||||
description = "Toggle floating on the focused window",
|
||||
})
|
||||
|
||||
-- mod_key + f = Toggle fullscreen
|
||||
Input.keybind({ mod_key }, "f", function()
|
||||
|
@ -70,7 +85,10 @@ require("pinnacle").setup(function(Pinnacle)
|
|||
focused:toggle_fullscreen()
|
||||
focused:raise()
|
||||
end
|
||||
end)
|
||||
end, {
|
||||
group = "Window",
|
||||
description = "Toggle fullscreen on the focused window",
|
||||
})
|
||||
|
||||
-- mod_key + m = Toggle maximized
|
||||
Input.keybind({ mod_key }, "m", function()
|
||||
|
@ -79,7 +97,10 @@ require("pinnacle").setup(function(Pinnacle)
|
|||
focused:toggle_maximized()
|
||||
focused:raise()
|
||||
end
|
||||
end)
|
||||
end, {
|
||||
group = "Window",
|
||||
description = "Toggle maximized on the focused window",
|
||||
})
|
||||
|
||||
----------------------
|
||||
-- Tags and Outputs --
|
||||
|
@ -109,12 +130,18 @@ require("pinnacle").setup(function(Pinnacle)
|
|||
-- mod_key + 1-5 = Switch to tags 1-5
|
||||
Input.keybind({ mod_key }, tag_name, function()
|
||||
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
|
||||
Input.keybind({ mod_key, "shift" }, tag_name, function()
|
||||
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
|
||||
Input.keybind({ mod_key, "alt" }, tag_name, function()
|
||||
|
@ -122,7 +149,10 @@ require("pinnacle").setup(function(Pinnacle)
|
|||
if focused then
|
||||
focused:move_to_tag(Tag.get(tag_name) --[[@as TagHandle]])
|
||||
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
|
||||
Input.keybind({ mod_key, "shift", "alt" }, tag_name, function()
|
||||
|
@ -130,7 +160,10 @@ require("pinnacle").setup(function(Pinnacle)
|
|||
if focused then
|
||||
focused:toggle_tag(Tag.get(tag_name) --[[@as TagHandle]])
|
||||
end
|
||||
end)
|
||||
end, {
|
||||
group = "Tag",
|
||||
description = "Toggle tag " .. tag_name .. " on the focused window",
|
||||
})
|
||||
end
|
||||
|
||||
--------------------
|
||||
|
@ -226,7 +259,10 @@ require("pinnacle").setup(function(Pinnacle)
|
|||
Layout.request_layout(focused_op)
|
||||
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
|
||||
Input.keybind({ mod_key, "shift" }, key.space, function()
|
||||
|
@ -257,7 +293,10 @@ require("pinnacle").setup(function(Pinnacle)
|
|||
Layout.request_layout(focused_op)
|
||||
end
|
||||
end
|
||||
end)
|
||||
end, {
|
||||
group = "Layout",
|
||||
description = "Cycle the layout backward on the first active tag",
|
||||
})
|
||||
|
||||
Input.set_libinput_settings({
|
||||
tap = true,
|
||||
|
|
|
@ -258,6 +258,8 @@ local pinnacle_input_v0alpha1_Modifier = {
|
|||
---@field modifiers pinnacle.input.v0alpha1.Modifier[]?
|
||||
---@field raw_code integer?
|
||||
---@field xkb_name string?
|
||||
---@field group string?
|
||||
---@field description string?
|
||||
|
||||
---@class pinnacle.input.v0alpha1.SetKeybindResponse
|
||||
|
||||
|
@ -274,6 +276,18 @@ local pinnacle_input_v0alpha1_SetMousebindRequest_MouseEdge = {
|
|||
|
||||
---@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
|
||||
---@field rules string?
|
||||
---@field variant string?
|
||||
|
@ -729,6 +743,13 @@ defs.pinnacle = {
|
|||
response = "pinnacle.input.v0alpha1.SetMousebindResponse",
|
||||
},
|
||||
---@type GrpcRequestArgs
|
||||
KeybindDescriptions = {
|
||||
service = "pinnacle.input.v0alpha1.InputService",
|
||||
method = "KeybindDescriptions",
|
||||
request = "pinnacle.input.v0alpha1.KeybindDescriptionsRequest",
|
||||
response = "pinnacle.input.v0alpha1.KeybindDescriptionsResponse",
|
||||
},
|
||||
---@type GrpcRequestArgs
|
||||
SetXkbConfig = {
|
||||
service = "pinnacle.input.v0alpha1.InputService",
|
||||
method = "SetXkbConfig",
|
||||
|
|
|
@ -74,6 +74,10 @@ local input = {
|
|||
}
|
||||
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.
|
||||
---
|
||||
---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 key Key | string The key used to trigger the bind
|
||||
---@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 xkb_name = nil
|
||||
|
||||
|
@ -130,6 +135,8 @@ function input.keybind(mods, key, action)
|
|||
modifiers = mod_values,
|
||||
raw_code = raw_code,
|
||||
xkb_name = xkb_name,
|
||||
group = keybind_info and keybind_info.group,
|
||||
description = keybind_info and keybind_info.description,
|
||||
}, action)
|
||||
end
|
||||
|
||||
|
@ -165,6 +172,31 @@ function input.mousebind(mods, button, edge, action)
|
|||
}, action)
|
||||
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
|
||||
---@field rules string?
|
||||
---@field model string?
|
||||
|
|
|
@ -18,9 +18,25 @@ message SetKeybindRequest {
|
|||
uint32 raw_code = 2;
|
||||
string xkb_name = 3;
|
||||
}
|
||||
optional string group = 4;
|
||||
optional string description = 5;
|
||||
}
|
||||
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 {
|
||||
repeated Modifier modifiers = 1;
|
||||
// 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 SetMousebind(SetMousebindRequest) returns (stream SetMousebindResponse);
|
||||
|
||||
rpc KeybindDescriptions(KeybindDescriptionsRequest) returns (KeybindDescriptionsResponse);
|
||||
|
||||
rpc SetXkbConfig(SetXkbConfigRequest) returns (google.protobuf.Empty);
|
||||
rpc SetRepeatRate(SetRepeatRateRequest) returns (google.protobuf.Empty);
|
||||
|
||||
|
|
|
@ -21,3 +21,9 @@ num_enum = "0.7.2"
|
|||
xkbcommon = { workspace = true }
|
||||
rand = "0.8.5"
|
||||
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"
|
||||
|
||||
[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::KeybindInfo;
|
||||
use pinnacle_api::layout::{
|
||||
CornerLayout, CornerLocation, CyclingLayoutManager, DwindleLayout, FairLayout, MasterSide,
|
||||
MasterStackLayout, SpiralLayout,
|
||||
|
@ -28,6 +29,8 @@ async fn main() {
|
|||
tag,
|
||||
layout,
|
||||
render,
|
||||
#[cfg(feature = "snowcap")]
|
||||
snowcap,
|
||||
..
|
||||
} = modules;
|
||||
|
||||
|
@ -53,51 +56,124 @@ async fn main() {
|
|||
// 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
|
||||
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();
|
||||
});
|
||||
},
|
||||
KeybindInfo {
|
||||
group: Some("Compositor".into()),
|
||||
description: Some("Quit Pinnacle".into()),
|
||||
},
|
||||
);
|
||||
|
||||
// `mod_key + alt + r` reloads the config
|
||||
input.keybind([mod_key, Mod::Alt], 'r', || {
|
||||
input.keybind(
|
||||
[mod_key, Mod::Alt],
|
||||
'r',
|
||||
|| {
|
||||
pinnacle.reload_config();
|
||||
});
|
||||
},
|
||||
KeybindInfo {
|
||||
group: Some("Compositor".into()),
|
||||
description: Some("Reload the config".into()),
|
||||
},
|
||||
);
|
||||
|
||||
// `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() {
|
||||
window.close();
|
||||
}
|
||||
});
|
||||
},
|
||||
KeybindInfo {
|
||||
group: Some("Window".into()),
|
||||
description: Some("Close the focused window".into()),
|
||||
},
|
||||
);
|
||||
|
||||
// `mod_key + Return` spawns a terminal
|
||||
input.keybind([mod_key], Keysym::Return, move || {
|
||||
input.keybind(
|
||||
[mod_key],
|
||||
Keysym::Return,
|
||||
move || {
|
||||
process.spawn([terminal]);
|
||||
});
|
||||
},
|
||||
KeybindInfo {
|
||||
group: Some("Process".into()),
|
||||
description: Some(format!("Spawn `{terminal}`")),
|
||||
},
|
||||
);
|
||||
|
||||
// `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() {
|
||||
window.toggle_floating();
|
||||
window.raise();
|
||||
}
|
||||
});
|
||||
},
|
||||
KeybindInfo {
|
||||
group: Some("Window".into()),
|
||||
description: Some("Toggle floating on the focused window".into()),
|
||||
},
|
||||
);
|
||||
|
||||
// `mod_key + f` toggles fullscreen
|
||||
input.keybind([mod_key], 'f', || {
|
||||
input.keybind(
|
||||
[mod_key],
|
||||
'f',
|
||||
|| {
|
||||
if let Some(window) = window.get_focused() {
|
||||
window.toggle_fullscreen();
|
||||
window.raise();
|
||||
}
|
||||
});
|
||||
},
|
||||
KeybindInfo {
|
||||
group: Some("Window".into()),
|
||||
description: Some("Toggle fullscreen on the focused window".into()),
|
||||
},
|
||||
);
|
||||
|
||||
// `mod_key + m` toggles maximized
|
||||
input.keybind([mod_key], 'm', || {
|
||||
input.keybind(
|
||||
[mod_key],
|
||||
'm',
|
||||
|| {
|
||||
if let Some(window) = window.get_focused() {
|
||||
window.toggle_maximized();
|
||||
window.raise();
|
||||
}
|
||||
});
|
||||
},
|
||||
KeybindInfo {
|
||||
group: Some("Window".into()),
|
||||
description: Some("Toggle maximized on the focused window".into()),
|
||||
},
|
||||
);
|
||||
|
||||
//------------------------
|
||||
// Window rules |
|
||||
|
@ -180,7 +256,10 @@ async fn main() {
|
|||
let mut layout_requester_clone = layout_requester.clone();
|
||||
|
||||
// `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(first_active_tag) = focused_op.tags().batch_find(
|
||||
|tg| Box::pin(tg.active_async()),
|
||||
|
@ -191,10 +270,18 @@ async fn main() {
|
|||
|
||||
layout_requester.cycle_layout_forward(&first_active_tag);
|
||||
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
|
||||
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(first_active_tag) = focused_op.tags().batch_find(
|
||||
|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.request_layout_on_output(&focused_op);
|
||||
});
|
||||
},
|
||||
KeybindInfo {
|
||||
group: Some("Layout".into()),
|
||||
description: Some("Cycle the layout backward on the first active tag".into()),
|
||||
},
|
||||
);
|
||||
|
||||
//------------------------
|
||||
// Tags |
|
||||
|
@ -218,36 +310,68 @@ async fn main() {
|
|||
|
||||
for tag_name in tag_names {
|
||||
// `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) {
|
||||
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"
|
||||
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) {
|
||||
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"
|
||||
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(win) = window.get_focused() {
|
||||
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
|
||||
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(win) = window.get_focused() {
|
||||
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));
|
||||
|
|
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::{
|
||||
input_service_client::InputServiceClient,
|
||||
set_libinput_setting_request::{CalibrationMatrix, Setting},
|
||||
SetKeybindRequest, SetLibinputSettingRequest, SetMousebindRequest, SetRepeatRateRequest,
|
||||
SetXkbConfigRequest,
|
||||
KeybindDescriptionsRequest, SetKeybindRequest, SetLibinputSettingRequest,
|
||||
SetMousebindRequest, SetRepeatRateRequest, SetXkbConfigRequest,
|
||||
},
|
||||
};
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
|
@ -99,6 +99,32 @@ pub struct Input {
|
|||
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 {
|
||||
pub(crate) fn new(
|
||||
channel: Channel,
|
||||
|
@ -157,25 +183,28 @@ impl Input {
|
|||
mods: impl IntoIterator<Item = Mod>,
|
||||
key: impl Key + Send + 'static,
|
||||
mut action: impl FnMut() + Send + 'static,
|
||||
keybind_info: impl Into<Option<KeybindInfo>>,
|
||||
) {
|
||||
let mut client = self.create_input_client();
|
||||
|
||||
let modifiers = mods.into_iter().map(|modif| modif as i32).collect();
|
||||
|
||||
self.fut_sender
|
||||
.send(
|
||||
async move {
|
||||
let mut stream = client
|
||||
.set_keybind(SetKeybindRequest {
|
||||
let keybind_info: Option<KeybindInfo> = keybind_info.into();
|
||||
|
||||
let mut stream = block_on_tokio(client.set_keybind(SetKeybindRequest {
|
||||
modifiers,
|
||||
key: Some(input::v0alpha1::set_keybind_request::Key::RawCode(
|
||||
key.into_keysym().raw(),
|
||||
)),
|
||||
})
|
||||
.await
|
||||
group: keybind_info.clone().and_then(|info| info.group),
|
||||
description: keybind_info.clone().and_then(|info| info.description),
|
||||
}))
|
||||
.unwrap()
|
||||
.into_inner();
|
||||
|
||||
self.fut_sender
|
||||
.send(
|
||||
async move {
|
||||
while let Some(Ok(_response)) = stream.next().await {
|
||||
action();
|
||||
tokio::task::yield_now().await;
|
||||
|
@ -218,20 +247,17 @@ impl Input {
|
|||
let mut client = self.create_input_client();
|
||||
|
||||
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
|
||||
.send(
|
||||
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 {
|
||||
action();
|
||||
tokio::task::yield_now().await;
|
||||
|
@ -242,6 +268,31 @@ impl Input {
|
|||
.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.
|
||||
///
|
||||
/// This allows you to set several xkeyboard options like `layout` and `rules`.
|
||||
|
|
|
@ -85,6 +85,8 @@ use pinnacle::Pinnacle;
|
|||
use process::Process;
|
||||
use render::Render;
|
||||
use signal::SignalState;
|
||||
#[cfg(feature = "snowcap")]
|
||||
use snowcap::Snowcap;
|
||||
use tag::Tag;
|
||||
use tokio::sync::{
|
||||
mpsc::{unbounded_channel, UnboundedReceiver},
|
||||
|
@ -102,11 +104,15 @@ pub mod pinnacle;
|
|||
pub mod process;
|
||||
pub mod render;
|
||||
pub mod signal;
|
||||
#[cfg(feature = "snowcap")]
|
||||
pub mod snowcap;
|
||||
pub mod tag;
|
||||
pub mod util;
|
||||
pub mod window;
|
||||
|
||||
pub use pinnacle_api_macros::config;
|
||||
#[cfg(feature = "snowcap")]
|
||||
pub use snowcap_api;
|
||||
pub use tokio;
|
||||
pub use xkbcommon;
|
||||
|
||||
|
@ -131,6 +137,10 @@ pub struct ApiModules {
|
|||
/// The [`Render`] struct
|
||||
pub render: &'static Render,
|
||||
signal: Arc<RwLock<SignalState>>,
|
||||
|
||||
#[cfg(feature = "snowcap")]
|
||||
/// The snowcap widget system.
|
||||
pub snowcap: &'static Snowcap,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ApiModules {
|
||||
|
@ -145,16 +155,23 @@ impl std::fmt::Debug for ApiModules {
|
|||
.field("layout", &self.layout)
|
||||
.field("render", &self.render)
|
||||
.field("signal", &"...")
|
||||
// TODO: snowcap
|
||||
.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.
|
||||
///
|
||||
/// This function is inserted at the top of your config through the [`config`] macro.
|
||||
/// You should use that macro instead of this function directly.
|
||||
pub async fn connect(
|
||||
) -> Result<(ApiModules, UnboundedReceiver<BoxFuture<'static, ()>>), Box<dyn std::error::Error>> {
|
||||
pub async fn connect() -> Result<(ApiModules, Receivers), Box<dyn std::error::Error>> {
|
||||
// port doesn't matter, we use a unix socket
|
||||
let channel = Endpoint::try_from("http://[::]:50051")?
|
||||
.connect_with_connector(service_fn(|_: Uri| {
|
||||
|
@ -163,7 +180,8 @@ pub async fn connect(
|
|||
.expect("PINNACLE_GRPC_SOCKET was not set; is Pinnacle running?"),
|
||||
)
|
||||
}))
|
||||
.await?;
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
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 layout = Box::leak(Box::new(Layout::new(channel.clone(), fut_sender.clone())));
|
||||
|
||||
#[cfg(not(feature = "snowcap"))]
|
||||
let modules = ApiModules {
|
||||
pinnacle,
|
||||
process,
|
||||
|
@ -193,13 +212,40 @@ pub async fn connect(
|
|||
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());
|
||||
output.finish_init(modules.clone());
|
||||
tag.finish_init(modules.clone());
|
||||
layout.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.
|
||||
|
@ -209,7 +255,15 @@ pub async fn connect(
|
|||
///
|
||||
/// This function is inserted at the end of your config through the [`config`] macro.
|
||||
/// 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 set = futures::stream::FuturesUnordered::new();
|
||||
|
||||
|
@ -222,6 +276,9 @@ pub async fn listen(api: ApiModules, fut_recv: UnboundedReceiver<BoxFuture<'stat
|
|||
}
|
||||
.boxed();
|
||||
|
||||
#[cfg(feature = "snowcap")]
|
||||
tokio::spawn(snowcap_api::listen(snowcap_recv));
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
fut = fut_recv.next() => {
|
||||
|
|
|
@ -74,6 +74,8 @@ macro_rules! signals {
|
|||
.send((self.current_id, callback))
|
||||
.expect("failed to send callback");
|
||||
|
||||
self.callback_count.fetch_add(1, Ordering::SeqCst);
|
||||
|
||||
let handle = SignalHandle::new(self.current_id, remove_callback_sender);
|
||||
|
||||
self.current_id.0 += 1;
|
||||
|
@ -376,6 +378,10 @@ where
|
|||
|
||||
control_sender
|
||||
.send(Req::from_control(StreamControl::Ready))
|
||||
.map_err(|err| {
|
||||
println!("{err}");
|
||||
err
|
||||
})
|
||||
.expect("send failed");
|
||||
|
||||
loop {
|
||||
|
@ -407,7 +413,8 @@ where
|
|||
callback = callback_recv_recv => {
|
||||
if let Some((id, callback)) = 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 => {
|
||||
|
|
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
|
||||
mesa
|
||||
xwayland
|
||||
libdisplay-info
|
||||
|
||||
# winit on x11
|
||||
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,
|
||||
set_libinput_setting_request::{AccelProfile, ClickMethod, ScrollMethod, TapButtonMap},
|
||||
set_mousebind_request::MouseEdge,
|
||||
KeybindDescription, KeybindDescriptionsRequest, KeybindDescriptionsResponse, Modifier,
|
||||
SetKeybindRequest, SetKeybindResponse, SetLibinputSettingRequest, SetMousebindRequest,
|
||||
SetMousebindResponse, SetRepeatRateRequest, SetXkbConfigRequest,
|
||||
},
|
||||
|
@ -55,7 +56,7 @@ use tracing::{debug, error, info, trace, warn};
|
|||
use crate::{
|
||||
backend::{udev::drm_mode_from_api_modeline, BackendData},
|
||||
config::ConnectorSavedState,
|
||||
input::ModifierMask,
|
||||
input::{KeybindData, ModifierMask},
|
||||
output::{OutputMode, OutputName},
|
||||
render::util::snapshot::capture_snapshots_on_output,
|
||||
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| {
|
||||
let keybind_data = KeybindData {
|
||||
sender,
|
||||
group,
|
||||
description,
|
||||
};
|
||||
|
||||
state
|
||||
.pinnacle
|
||||
.input_state
|
||||
.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(
|
||||
&self,
|
||||
request: Request<SetXkbConfigRequest>,
|
||||
|
|
|
@ -678,7 +678,7 @@ impl WlrLayerShellHandler for State {
|
|||
let output = output
|
||||
.as_ref()
|
||||
.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 {
|
||||
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},
|
||||
window::WindowElement,
|
||||
};
|
||||
use indexmap::IndexMap;
|
||||
use pinnacle_api_defs::pinnacle::input::v0alpha1::{
|
||||
set_libinput_setting_request::Setting, set_mousebind_request, SetKeybindResponse,
|
||||
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)]
|
||||
pub struct InputState {
|
||||
// TODO: move all of these to config
|
||||
pub reload_keybind: Option<(ModifierMask, Keysym)>,
|
||||
pub kill_keybind: Option<(ModifierMask, Keysym)>,
|
||||
|
||||
pub keybinds:
|
||||
HashMap<(ModifierMask, Keysym), UnboundedSender<Result<SetKeybindResponse, tonic::Status>>>,
|
||||
pub keybinds: IndexMap<(ModifierMask, Keysym), KeybindData>,
|
||||
pub mousebinds: HashMap<
|
||||
(ModifierMask, u32, set_mousebind_request::MouseEdge),
|
||||
UnboundedSender<Result<SetMousebindResponse, tonic::Status>>,
|
||||
|
@ -540,7 +547,7 @@ impl State {
|
|||
let raw_sym = keysym.raw_syms().iter().next();
|
||||
let mod_sym = keysym.modified_sym();
|
||||
|
||||
if let Some(sender) = state
|
||||
if let Some(keybind_data) = state
|
||||
.pinnacle
|
||||
.input_state
|
||||
.keybinds
|
||||
|
@ -557,7 +564,7 @@ impl State {
|
|||
{
|
||||
if state.pinnacle.lock_state.is_unlocked() {
|
||||
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
|
||||
#![warn(clippy::unwrap_used)]
|
||||
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::{
|
||||
io::{BufRead, BufReader},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use pinnacle::{
|
||||
|
@ -43,7 +46,7 @@ async fn main() -> anyhow::Result<()> {
|
|||
let env_filter = EnvFilter::try_from_default_env();
|
||||
|
||||
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()
|
||||
.compact()
|
||||
|
@ -51,7 +54,8 @@ async fn main() -> anyhow::Result<()> {
|
|||
.with_writer(appender)
|
||||
.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()
|
||||
.compact()
|
||||
.with_writer(std::io::stdout)
|
||||
|
@ -170,6 +174,35 @@ async fn main() -> anyhow::Result<()> {
|
|||
.pinnacle
|
||||
.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 {
|
||||
match state.pinnacle.insert_xwayland_source() {
|
||||
Ok(()) => {
|
||||
|
@ -189,7 +222,7 @@ async fn main() -> anyhow::Result<()> {
|
|||
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();
|
||||
})?;
|
||||
|
||||
|
|
28
src/state.rs
28
src/state.rs
|
@ -155,6 +155,11 @@ pub struct Pinnacle {
|
|||
pub idle_inhibiting_surfaces: HashSet<WlSurface>,
|
||||
|
||||
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 {
|
||||
|
@ -170,6 +175,19 @@ impl State {
|
|||
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
|
||||
for output in self.pinnacle.space.outputs().cloned().collect::<Vec<_>>() {
|
||||
output.with_state_mut(|state| {
|
||||
|
@ -355,6 +373,11 @@ impl Pinnacle {
|
|||
idle_inhibiting_surfaces: HashSet::new(),
|
||||
|
||||
outputs: IndexMap::new(),
|
||||
|
||||
#[cfg(feature = "snowcap")]
|
||||
snowcap_shutdown_ping: None,
|
||||
#[cfg(feature = "snowcap")]
|
||||
snowcap_join_handle: None,
|
||||
};
|
||||
|
||||
Ok(pinnacle)
|
||||
|
@ -388,6 +411,11 @@ impl Pinnacle {
|
|||
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]
|
||||
smithay = { workspace = true }
|
||||
pinnacle = { path = "..", features = [ "wlcs" ] }
|
||||
pinnacle-api = { path = "../api/rust" }
|
||||
pinnacle = { path = "..", features = ["wlcs"], default-features = false }
|
||||
pinnacle-api = { path = "../api/rust", default-features = false }
|
||||
wayland-sys = { version = "0.31.1", features = ["client", "server"] }
|
||||
wlcs = "0.1"
|
||||
|
||||
|
|
Loading…
Reference in a new issue