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

Preliminary snowcap integration (Rust)
This commit is contained in:
Ottatop 2024-06-16 19:12:19 -05:00 committed by GitHub
commit c1ebfd9eed
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 2913 additions and 380 deletions

View file

@ -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:

View file

@ -25,6 +25,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive
- name: Get ldoc_gen
uses: actions/checkout@v4
with:

View file

@ -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
View file

@ -0,0 +1,3 @@
[submodule "snowcap"]
path = snowcap
url = https://github.com/pinnacle-comp/snowcap

1980
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -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,11 +130,13 @@ 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",
]
wlcs = [ "testing" ]
wlcs = ["testing"]

128
README.md
View file

@ -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:
```sh
sudo pacman -S wayland wayland-protocols libxkbcommon systemd-libs libinput mesa seatd xorg-xwayland
```
- Debian/Ubuntu:
```sh
sudo apt install libwayland-dev libxkbcommon-dev libudev-dev libinput-dev libgdm-dev libseat-dev xwayland
```
- NixOS: There is flake [`flake.nix`](flake.nix) with a devShell. It also
- 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 libxkbcommon libinput mesa seatd systemd-libs libdisplay-info xorg-xwayland protobuf
# And optionally
sudo pacman -S just lua luarocks
```
- Debian and derivatives:
```sh
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
```
- 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 |

View file

@ -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

View file

@ -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,

View file

@ -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",

View file

@ -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?

View file

@ -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);

View file

@ -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"]

View file

@ -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"]

View file

@ -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', || {
pinnacle.quit();
});
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', || {
pinnacle.reload_config();
});
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', || {
if let Some(window) = window.get_focused() {
window.close();
}
});
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 || {
process.spawn([terminal]);
});
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, || {
if let Some(window) = window.get_focused() {
window.toggle_floating();
window.raise();
}
});
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', || {
if let Some(window) = window.get_focused() {
window.toggle_fullscreen();
window.raise();
}
});
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', || {
if let Some(window) = window.get_focused() {
window.toggle_maximized();
window.raise();
}
});
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,32 +256,48 @@ 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 || {
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()),
|active| active == &Some(true),
) else {
return;
};
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()),
|active| active == &Some(true),
) else {
return;
};
layout_requester.cycle_layout_forward(&first_active_tag);
layout_requester.request_layout_on_output(&focused_op);
});
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 || {
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()),
|active| active == &Some(true),
) else {
return;
};
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()),
|active| active == &Some(true),
) else {
return;
};
layout_requester_clone.cycle_layout_backward(&first_active_tag);
layout_requester_clone.request_layout_on_output(&focused_op);
});
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 || {
if let Some(tg) = tag.get(tag_name) {
tg.switch_to();
}
});
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 || {
if let Some(tg) = tag.get(tag_name) {
tg.toggle_active();
}
});
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 || {
if let Some(tg) = tag.get(tag_name) {
if let Some(win) = window.get_focused() {
win.move_to_tag(&tg);
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 || {
if let Some(tg) = tag.get(tag_name) {
if let Some(win) = window.get_focused() {
win.toggle_tag(&tg);
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));

View file

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

View file

@ -0,0 +1,46 @@
# This metaconfig.toml file dictates what config Pinnacle will run.
#
# When running Pinnacle, the compositor will look in the following directories for a metaconfig.toml file,
# in order from top to bottom:
# $PINNACLE_CONFIG_DIR
# $XDG_CONFIG_HOME/pinnacle/
# ~/.config/pinnacle/
#
# When Pinnacle finds a metaconfig.toml file, it will execute the command provided to `command`.
# To use a Rust config, this should be changed to something like ["cargo", "run"].
#
# Because configuration is done using an external process, if it ever crashes, you lose all of your keybinds.
# The compositor will load the default config if that happens, but in the event that you don't have
# the necessary dependencies for it to run, you may get softlocked.
# In order prevent you from getting stuck in the compositor, you must define keybinds to reload your config
# and kill Pinnacle.
#
# More details on each setting can be found below.
# The command Pinnacle will run on startup and when you reload your config.
# Paths are relative to the directory the metaconfig.toml file is in.
# This must be an array.
command = ["cargo", "run", "--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"

View file

@ -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();
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(),
)),
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 {
let mut stream = client
.set_keybind(SetKeybindRequest {
modifiers,
key: Some(input::v0alpha1::set_keybind_request::Key::RawCode(
key.into_keysym().raw(),
)),
})
.await
.unwrap()
.into_inner();
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`.

View file

@ -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() => {

View file

@ -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
View 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);
}
}

View 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();
});
}
}

View file

@ -50,6 +50,7 @@
libinput
mesa
xwayland
libdisplay-info
# winit on x11
xorg.libXcursor

1
snowcap Submodule

@ -0,0 +1 @@
Subproject commit 3e79a16e1065f07001b08fabdc763bb274cac394

View file

@ -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>,

View file

@ -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");

View file

@ -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(),
));
}
}

View file

@ -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();
})?;

View file

@ -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();
}
}
}

View file

@ -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"