Merge pull request #56 from Ottatop/config_reload

Add metaconfig and config reloading
This commit is contained in:
Ottatop 2023-08-16 20:43:33 -05:00 committed by GitHub
commit 43949e386d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 1174 additions and 904 deletions

View file

@ -23,6 +23,8 @@ async-process = { version = "1.7.0" }
itertools = { version = "0.11.0" }
x11rb = { version = "0.12.0", default-features = false, features = ["composite"], optional = true }
shellexpand = "3.1.0"
toml = "0.7.6"
anyhow = { version = "1.0.74", features = ["backtrace"] }
[features]
default = ["egl", "winit", "udev", "xwayland"]

132
README.md
View file

@ -11,20 +11,27 @@
A very, VERY WIP Smithay-based wayland compositor
</div>
## Changelog
See [`CHANGELOG.md`](CHANGELOG.md).
## Info
### What is Pinnacle?
Pinnacle is a Wayland compositor built in Rust using [Smithay](https://github.com/Smithay/smithay).
It's my attempt at creating something like [AwesomeWM](https://github.com/awesomeWM/awesome)
for Wayland.
## Features
- [x] Winit backend
- [x] Udev backend
- This is currently just a copy of Anvil's udev backend.
- [x] Basic tags
It sports high configurability through a (soon to be) extensive Lua API, with plans for a Rust API in the future.
Showcase/gallery soon:tm:
### Features
> This is a non-exhaustive list.
- [x] Winit backend (so you can run Pinnacle in your graphical environment)
- [x] Udev backend (so you can run Pinnacle in a tty)
- [x] Tag system
- [ ] Layout system
- [x] Left master stack, corner, dwindle, spiral layouts
- [ ] Other three master stack directions, floating, magnifier, maximized, and fullscreen layouts
- [ ] Resizable layouts
- [x] XWayland support
- This is currently somewhat buggy. If you find a problem that's not already listed in GitHub issues, feel free to submit it!
- This is currently somewhat buggy. If you find a problem, please submit an issue!
- [x] Layer-shell support
- [ ] wlr-screencopy support
- [ ] wlr-output-management support
@ -34,21 +41,15 @@ See [`CHANGELOG.md`](CHANGELOG.md).
- [ ] The other stuff Awesome has
- [x] Is very cool :thumbsup:
## Info
### Why Pinnacle?
Well, I currently use [Awesome](https://github.com/awesomeWM/awesome). And I really like it! Unfortunately, Awesome doesn't exist for Wayland ([anymore](http://way-cooler.org/blog/2020/01/09/way-cooler-post-mortem.html)). There doesn't seem to be any Wayland compositor out there that has all of the following:
- Tags for window management
- Configurable in Lua (or any other programming language for that matter)
- Has a bunch of batteries included (widget system, systray, etc)
So, this is my attempt at making an Awesome-esque Wayland compositor.
## Dependencies
> I have not tested these. If Pinnacle doesn't work properly with these packages installed, please submit an issue.
You'll need the following packages, as specified by [Smithay](https://github.com/Smithay/smithay):
`libwayland libxkbcommon libudev libinput libgdm libseat`, as well as `xwayland`.
- Arch:
```
sudo pacman -S wayland wayland-protocols libxkbcommon systemd-libs libinput mesa seatda xwayland
sudo pacman -S wayland wayland-protocols libxkbcommon systemd-libs libinput mesa seatd xwayland
```
- Debian:
```
@ -66,9 +67,11 @@ cargo build [--release]
```
For NixOS users, there is a provided [`shell.nix`](shell.nix) file that you can use for `nix-shell`.
It *should* work, but if it doesn't, please raise an issue. <sup>flake soon:tm:</sup>
<sup>flake soon:tm:</sup>
## Running
> :information_source: Before running, read the information in [Configuration](#configuration).
After building, run the executable located in either:
```sh
./target/debug/pinnacle # without --release
@ -80,71 +83,61 @@ Or, run the project directly with
cargo run [--release]
```
There is an additional flag you can pass in: `--<backend>`. You most likely do not need to use it.
Pinnacle will automatically initialize the correct backend for your environment.
However, there is an additional flag you can pass in: `--<backend>`. You most likely do not need to use it.
`backend` can be one of two values:
- `winit`: run Pinnacle as a window in your graphical environment
- `udev`: run Pinnacle in a tty. NOTE: I tried running udev in Awesome and some things broke so uh, don't do that
- `udev`: run Pinnacle in a tty.
If you try to run either in environments where you shouldn't be, you will get a warning requiring you to
pass in the `--force` flag to continue. *You probably shouldn't be doing that.*
> :information_source: When running in debug mode, the compositor will drastically slow down
> if there are too many windows on screen. If you don't want this to happen, use release mode.
> #### :exclamation: IMPORTANT: Read the following before you launch the `udev` backend:
> If you successfully enter the `udev` backend but none of the controls work, this means either Pinnacle
failed to find your config, or the config process crashed.
>
> You can either switch ttys or press
> `Ctrl + Alt + Shift + Escape`,
> which has been hardcoded in to kill the compositor.
> #### :information_source: Make sure `command` in your `metaconfig.toml` is set to the right file.
> If it isn't, the compositor will open, but your config will not apply.
In that case, kill the compositor using the keybind defined in
`kill_keybind` (default <kbd>Ctrl</kbd><kbd>Alt</kbd><kbd>Shift</kbd> + <kbd>Esc</kbd>) and set `command` properly.
> #### :information_source: Pinnacle will open a socket in the `/tmp` directory.
> If for whatever reason you need the socket to be in a different place, run Pinnacle with
> the `SOCKET_DIR` environment variable:
> ```sh
> SOCKET_DIR=/path/to/new/dir/ cargo run
> ```
> If for whatever reason you need the socket to be in a different place, set `socket_dir` in
> your `metaconfig.toml` file to a directory of your choosing.
> #### :warning: Don't run Pinnacle as root.
> #### :warning: Do not run Pinnacle as root.
> This will open the socket with root-only permissions, and future non-root invocations
of Pinnacle will fail when trying to remove the socket until it is removed manually.
## Configuration
Please note: this is WIP and has few options.
Pinnacle is configured in Lua. Rust support is planned.
Pinnacle supports configuration through Lua (and hopefully more languages if it's not too unwieldy :crab:).
Run Pinnacle with the `PINNACLE_CONFIG` environment variable set to the path of your config file.
If not specified, Pinnacle will look for the following:
Pinnacle will search for a `metaconfig.toml` file in the following directories, from top to bottom:
```sh
$XDG_CONFIG_HOME/pinnacle/init.lua
~/.config/pinnacle/init.lua # if XDG_CONFIG_HOME isn't set
```
The following will use the example config file in [`api/lua`](api/lua):
```sh
PINNACLE_CONFIG="./api/lua/example_config.lua" cargo run
$PINNACLE_CONFIG_DIR
$XDG_CONFIG_HOME/pinnacle/
~/.config/pinnacle
```
> #### :information_source: The config is an external process.
> If it crashes for whatever reason, all of your keybinds will stop working.
> Again, you can switch ttys or exit the compositor with `Ctrl + Alt + Shift + Escape`.
>
> Config reloading soon:tm:
The `metaconfig.toml` file provides information on what config to run, kill and reload keybinds,
and any environment variables you want set. For more details, see the provided
[`metaconfig.toml`](api/lua/metaconfig.toml) file.
### API Documentation
There is a preliminary [doc website](https://ottatop.github.io/pinnacle/main) generated with LDoc.
Note that there are some missing things like the `Keys` table and `Layout` enum
as well as any function overloads, but these should be autocompleted through the language server.
To use the provided Lua config, run the following in the root of the Git project:
```sh
PINNACLE_CONFIG_DIR="./api/lua" cargo run
```
Documentation for other branches can be reached at `https://ottatop.github.io/pinnacle/<branch name>`.
To run without the above environment variable, copy [`metaconfig.toml`](api/lua/metaconfig.toml) and
[`example_config.lua`](api/lua/example_config.lua) to `$XDG_CONFIG_HOME/pinnacle/`
(this will probably be `~/.config/pinnacle`).
### :information_source: Using the Lua Language Server :information_source:
It is *highly* recommended to use the [Lua language server](https://github.com/LuaLS/lua-language-server)
and set it up to have the [`api/lua`](api/lua) directory as a library, as I'll be using
its doc comments to provide documentation, autocomplete, and error checking.
> If you rename `example_config.lua` to something like `init.lua`, you will need to change `command` in `metaconfig.toml` to reflect that.
### :information_source: Using the Lua Language Server
It is ***highly*** recommended to use the [Lua language server](https://github.com/LuaLS/lua-language-server)
and set it up to have the [`api/lua`](api/lua) directory as a library.
This will provide documentation, autocomplete, and error checking.
#### For VS Code:
Install the [Lua](https://marketplace.visualstudio.com/items?itemName=sumneko.lua) plugin, then go into
@ -162,15 +155,24 @@ Lua = {
}
```
### API Documentation
You can find online documentation for the Lua API [here](https://ottatop.github.io/pinnacle/main).
Note that there are some missing things like the `Keys` table and `Layout` enum
as well as any function overloads, but these should be autocompleted through the language server.
Documentation for other branches can be reached at `https://ottatop.github.io/pinnacle/<branch name>`.
## Controls
The following controls are currently hardcoded:
- `Ctrl + Left Mouse`: Move a window
- `Ctrl + Right Mouse`: Resize a window
- `Ctrl + Alt + Shift + Esc`: Kill Pinnacle. This is for when the compositor inevitably
locks up because I did a dumb thing :thumbsup:
- <kbd>Ctrl</kbd> + <kbd>Left click drag</kbd>: Move a window
- <kbd>Ctrl</kbd> + <kbd>Right click drag</kbd>: Resize a window
You can find the rest of the controls in the [`example_config`](api/lua/example_config.lua).
## Feature Requests, Bug Reports, Contributions, and Questions
See [`CONTRIBUTING.md`](CONTRIBUTING.md).
## Changelog
See [`CHANGELOG.md`](CHANGELOG.md).

53
api/lua/metaconfig.toml Normal file
View file

@ -0,0 +1,53 @@
# 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`.
# For now, the only thing that should be here is `lua` with a path to the main config file.
# In the future, there will be a Rust API that can be run using `cargo run`.
#
# Because configuration is done using an external process, if it ever crashes, you lose all of your keybinds.
# 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.
command = "lua example_config.lua"
### 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 `/tmp` by default. If you want/need to change this,
# use the `socket_dir` setting set to the directory of your choosing.
#
# Changing this between reloads will not change the actual location of the socket
# until you restart Pinnacle.
# socket_dir = "/your/dir/here/"
### Environment Variables ###
# You may need to specify to Lua where Pinnacle's Lua API library is.
# This is currently done using the `envs` table, with keys as the name of the environment variable and
# the value as the variable value. This supports $var expansion, and paths are relative to this metaconfig.toml file.
#
# Pinnacle will run your config with the additional PINNACLE_DIR environment variable.
#
# Here, LUA_PATH and LUA_CPATH are used to tell Lua the path to the library.
[envs]
LUA_PATH = "$PINNACLE_DIR/api/lua/?.lua;$PINNACLE_DIR/api/lua/?/init.lua;$PINNACLE_DIR/api/lua/lib/?.lua;$PINNACLE_DIR/api/lua/lib/?/init.lua;$LUA_PATH"
LUA_CPATH = "$PINNACLE_DIR/api/lua/lib/?.so;$LUA_CPATH"

View file

@ -3,13 +3,7 @@
local socket = require("posix.sys.socket")
local msgpack = require("msgpack")
local socket_dir = os.getenv("SOCKET_DIR")
if socket_dir then
if socket_dir:match("/$") then
socket_dir = socket_dir:sub(0, socket_dir:len() - 1)
end
end
local SOCKET_PATH = (socket_dir or "/tmp") .. "/pinnacle_socket"
local SOCKET_PATH = os.getenv("PINNACLE_SOCKET") or "/tmp/pinnacle_socket"
---From https://gist.github.com/stuby/5445834#file-rprint-lua
---rPrint(struct, [limit], [indent]) Recursively print arbitrary data.

View file

@ -42,13 +42,14 @@ use std::{
path::Path,
};
use anyhow::Context;
use smithay::reexports::calloop::{
self, channel::Sender, generic::Generic, EventSource, Interest, Mode, PostAction,
};
use self::msg::{Msg, OutgoingMsg};
const DEFAULT_SOCKET_DIR: &str = "/tmp";
pub const DEFAULT_SOCKET_DIR: &str = "/tmp";
fn handle_client(
mut stream: UnixStream,
@ -86,57 +87,29 @@ pub struct PinnacleSocketSource {
}
impl PinnacleSocketSource {
pub fn new(sender: Sender<Msg>) -> Result<Self, io::Error> {
let socket_path = std::env::var("SOCKET_DIR").unwrap_or(DEFAULT_SOCKET_DIR.to_string());
let socket_path = Path::new(&socket_path);
if !socket_path.is_dir() {
tracing::error!("SOCKET_DIR must be a directory");
return Err(io::Error::new(
io::ErrorKind::Other,
"SOCKET_DIR must be a directory",
));
}
let Some(socket_path) = socket_path
.join("pinnacle_socket")
.to_str()
.map(|st| st.to_string())
else {
tracing::error!("Socket path {socket_path:?} had invalid Unicode");
return Err(io::Error::new(io::ErrorKind::Other, "socket path had invalid unicode"));
};
let socket_path = shellexpand::tilde(&socket_path).to_string();
let socket_path = Path::new(&socket_path);
// TODO: use anyhow
/// Create a loop source that listens for connections to the provided socket_dir.
/// This will also set PINNACLE_SOCKET for use in API implementations.
pub fn new(sender: Sender<Msg>, socket_dir: &Path) -> anyhow::Result<Self> {
let socket_path = socket_dir.join("pinnacle_socket");
if let Ok(exists) = socket_path.try_exists() {
if exists {
if let Err(err) = std::fs::remove_file(socket_path) {
tracing::error!("Failed to remove old socket: {err}");
return Err(err);
}
std::fs::remove_file(&socket_path).context("Failed to remove old socket")?;
}
}
let listener = match UnixListener::bind(socket_path) {
Ok(listener) => {
tracing::info!("Bound to socket at {socket_path:?}");
listener
}
Err(err) => {
tracing::error!("Failed to bind to socket: {err}");
return Err(err);
}
};
if let Err(err) = listener.set_nonblocking(true) {
tracing::error!("Failed to set socket to nonblocking: {err}");
return Err(err);
}
let listener = UnixListener::bind(&socket_path)
.with_context(|| format!("Failed to bind to socket at {socket_path:?}"))?;
tracing::info!("Bound to socket at {socket_path:?}");
listener
.set_nonblocking(true)
.context("Failed to set socket to nonblocking")?;
let socket = Generic::new(listener, Interest::READ, Mode::Level);
std::env::set_var("PINNACLE_SOCKET", socket_path);
Ok(Self { socket, sender })
}
}

View file

@ -6,7 +6,6 @@
#![allow(clippy::unwrap_used)] // I don't know what this stuff does yet
use std::{
collections::{HashMap, HashSet},
error::Error,
ffi::OsString,
os::fd::FromRawFd,
path::Path,
@ -172,7 +171,7 @@ impl Backend for UdevData {
}
}
pub fn run_udev() -> Result<(), Box<dyn Error>> {
pub fn run_udev() -> anyhow::Result<()> {
let mut event_loop = EventLoop::try_new().unwrap();
let mut display = Display::new().unwrap();
@ -281,12 +280,16 @@ pub fn run_udev() -> Result<(), Box<dyn Error>> {
/*
* Bind all our objects that get driven by the event loop
*/
event_loop
let insert_ret = event_loop
.handle()
.insert_source(libinput_backend, move |event, _, data| {
// println!("event: {:?}", event);
data.state.process_input_event(event);
})?;
});
if let Err(err) = insert_ret {
anyhow::bail!("Failed to insert libinput_backend into event loop: {err}");
}
let handle = event_loop.handle();
event_loop

View file

@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-3.0-or-later
use std::{error::Error, ffi::OsString, time::Duration};
use std::{ffi::OsString, time::Duration};
use smithay::{
backend::{
@ -82,7 +82,7 @@ impl DmabufHandler for State<WinitData> {
delegate_dmabuf!(State<WinitData>);
/// Start Pinnacle as a window in a graphical environment.
pub fn run_winit() -> Result<(), Box<dyn Error>> {
pub fn run_winit() -> anyhow::Result<()> {
let mut event_loop: EventLoop<CalloopData<WinitData>> = EventLoop::try_new()?;
let mut display: Display<State<WinitData>> = Display::new()?;
@ -90,7 +90,11 @@ pub fn run_winit() -> Result<(), Box<dyn Error>> {
let evt_loop_handle = event_loop.handle();
let (mut winit_backend, mut winit_evt_loop) = smithay::backend::winit::init::<GlesRenderer>()?;
let (mut winit_backend, mut winit_evt_loop) =
match smithay::backend::winit::init::<GlesRenderer>() {
Ok(ret) => ret,
Err(err) => anyhow::bail!("Failed to init winit backend: {err}"),
};
let mode = smithay::output::Mode {
size: winit_backend.window_size().physical_size,
@ -205,160 +209,164 @@ pub fn run_winit() -> Result<(), Box<dyn Error>> {
let mut pointer_element = PointerElement::<GlesTexture>::new();
// TODO: pointer
state
.loop_handle
.insert_source(Timer::immediate(), move |_instant, _metadata, data| {
let display = &mut data.display;
let state = &mut data.state;
let insert_ret =
state
.loop_handle
.insert_source(Timer::immediate(), move |_instant, _metadata, data| {
let display = &mut data.display;
let state = &mut data.state;
let result = winit_evt_loop.dispatch_new_events(|event| match event {
WinitEvent::Resized {
size,
scale_factor: _,
} => {
output.change_current_state(
Some(smithay::output::Mode {
size,
refresh: 144_000,
}),
None,
None,
None,
);
layer_map_for_output(&output).arrange();
state.update_windows(&output);
// state.re_layout(&output);
}
WinitEvent::Focus(_) => {}
WinitEvent::Input(input_evt) => {
state.process_input_event(input_evt);
}
WinitEvent::Refresh => {}
});
let result = winit_evt_loop.dispatch_new_events(|event| match event {
WinitEvent::Resized {
size,
scale_factor: _,
} => {
output.change_current_state(
Some(smithay::output::Mode {
size,
refresh: 144_000,
}),
None,
None,
None,
);
layer_map_for_output(&output).arrange();
state.update_windows(&output);
// state.re_layout(&output);
}
WinitEvent::Focus(_) => {}
WinitEvent::Input(input_evt) => {
state.process_input_event(input_evt);
}
WinitEvent::Refresh => {}
});
match result {
Ok(_) => {}
Err(WinitError::WindowClosed) => {
state.loop_signal.stop();
}
};
if let CursorImageStatus::Surface(surface) = &state.cursor_status {
if !surface.alive() {
state.cursor_status = CursorImageStatus::Default;
}
}
let cursor_visible = !matches!(state.cursor_status, CursorImageStatus::Surface(_));
pointer_element.set_status(state.cursor_status.clone());
let full_redraw = &mut state.backend_data.full_redraw;
*full_redraw = full_redraw.saturating_sub(1);
let output_render_elements = crate::render::generate_render_elements(
state.backend_data.backend.renderer(),
&state.space,
&output,
state.seat.input_method(),
state.pointer_location,
&mut pointer_element,
None,
&mut state.cursor_status,
state.dnd_icon.as_ref(),
&state.focus_state.focus_stack,
);
let render_res = state.backend_data.backend.bind().and_then(|_| {
let age = if *full_redraw > 0 {
0
} else {
state.backend_data.backend.buffer_age().unwrap_or(0)
match result {
Ok(_) => {}
Err(WinitError::WindowClosed) => {
state.loop_signal.stop();
}
};
let renderer = state.backend_data.backend.renderer();
// render_output()
state
.backend_data
.damage_tracker
.render_output(renderer, age, &output_render_elements, [0.5, 0.5, 0.5, 1.0])
.map_err(|err| match err {
damage::Error::Rendering(err) => err.into(),
damage::Error::OutputNoMode(_) => todo!(),
})
});
match render_res {
Ok(render_output_result) => {
let has_rendered = render_output_result.damage.is_some();
if let Some(damage) = render_output_result.damage {
if let Err(err) = state.backend_data.backend.submit(Some(&damage)) {
tracing::warn!("{}", err);
}
if let CursorImageStatus::Surface(surface) = &state.cursor_status {
if !surface.alive() {
state.cursor_status = CursorImageStatus::Default;
}
}
let cursor_visible = !matches!(state.cursor_status, CursorImageStatus::Surface(_));
pointer_element.set_status(state.cursor_status.clone());
let full_redraw = &mut state.backend_data.full_redraw;
*full_redraw = full_redraw.saturating_sub(1);
let output_render_elements = crate::render::generate_render_elements(
state.backend_data.backend.renderer(),
&state.space,
&output,
state.seat.input_method(),
state.pointer_location,
&mut pointer_element,
None,
&mut state.cursor_status,
state.dnd_icon.as_ref(),
&state.focus_state.focus_stack,
);
let render_res = state.backend_data.backend.bind().and_then(|_| {
let age = if *full_redraw > 0 {
0
} else {
state.backend_data.backend.buffer_age().unwrap_or(0)
};
let renderer = state.backend_data.backend.renderer();
// render_output()
state
.backend_data
.backend
.window()
.set_cursor_visible(cursor_visible);
.damage_tracker
.render_output(renderer, age, &output_render_elements, [0.5, 0.5, 0.5, 1.0])
.map_err(|err| match err {
damage::Error::Rendering(err) => err.into(),
damage::Error::OutputNoMode(_) => todo!(),
})
});
let time = state.clock.now();
match render_res {
Ok(render_output_result) => {
let has_rendered = render_output_result.damage.is_some();
if let Some(damage) = render_output_result.damage {
if let Err(err) = state.backend_data.backend.submit(Some(&damage)) {
tracing::warn!("{}", err);
}
}
// Send frames to the cursor surface so it updates in xwayland
if let CursorImageStatus::Surface(surf) = &state.cursor_status {
if let Some(op) = state.focus_state.focused_output.as_ref() {
send_frames_surface_tree(
surf,
op,
state
.backend_data
.backend
.window()
.set_cursor_visible(cursor_visible);
let time = state.clock.now();
// Send frames to the cursor surface so it updates in xwayland
if let CursorImageStatus::Surface(surf) = &state.cursor_status {
if let Some(op) = state.focus_state.focused_output.as_ref() {
send_frames_surface_tree(
surf,
op,
time,
Some(Duration::ZERO),
|_, _| None,
);
}
}
super::post_repaint(
&output,
&render_output_result.states,
&state.space,
None,
time.into(),
);
if has_rendered {
let mut output_presentation_feedback = take_presentation_feedback(
&output,
&state.space,
&render_output_result.states,
);
output_presentation_feedback.presented(
time,
Some(Duration::ZERO),
|_, _| None,
output
.current_mode()
.map(|mode| mode.refresh as u32)
.unwrap_or_default(),
0,
wp_presentation_feedback::Kind::Vsync,
);
}
}
super::post_repaint(
&output,
&render_output_result.states,
&state.space,
None,
time.into(),
);
if has_rendered {
let mut output_presentation_feedback = take_presentation_feedback(
&output,
&state.space,
&render_output_result.states,
);
output_presentation_feedback.presented(
time,
output
.current_mode()
.map(|mode| mode.refresh as u32)
.unwrap_or_default(),
0,
wp_presentation_feedback::Kind::Vsync,
);
Err(err) => {
tracing::warn!("{}", err);
}
}
Err(err) => {
tracing::warn!("{}", err);
}
}
state.space.refresh();
state.popup_manager.cleanup();
display
.flush_clients()
.expect("failed to flush client buffers");
state.space.refresh();
state.popup_manager.cleanup();
display
.flush_clients()
.expect("failed to flush client buffers");
TimeoutAction::ToDuration(Duration::from_millis(1))
})?;
TimeoutAction::ToDuration(Duration::from_millis(1))
});
if let Err(err) = insert_ret {
anyhow::bail!("Failed to insert winit events into event loop: {err}");
}
event_loop.run(
Some(Duration::from_millis(1)),

View file

@ -31,17 +31,23 @@ use crate::{
state::State,
};
#[derive(Default)]
pub struct InputState {
/// A hashmap of modifier keys and keycodes to callback IDs
pub keybinds: HashMap<(ModifierMask, u32), CallbackId>,
/// A hashmap of modifier keys and mouse button codes to callback IDs
pub mousebinds: HashMap<(ModifierMask, u32), CallbackId>,
pub reload_keybind: (ModifierMask, u32),
pub kill_keybind: (ModifierMask, u32),
}
impl InputState {
pub fn new() -> Self {
Default::default()
pub fn new(reload_keybind: (ModifierMask, u32), kill_keybind: (ModifierMask, u32)) -> Self {
Self {
keybinds: HashMap::new(),
mousebinds: HashMap::new(),
reload_keybind,
kill_keybind,
}
}
}
@ -50,6 +56,7 @@ enum KeyAction {
CallCallback(CallbackId),
Quit,
SwitchVt(i32),
ReloadConfig,
}
impl<B: Backend> State<B> {
@ -113,6 +120,10 @@ impl<B: Backend> State<B> {
let time = event.time_msec();
let press_state = event.state();
let mut move_mode = false;
let reload_keybind = self.input_state.reload_keybind;
let kill_keybind = self.input_state.kill_keybind;
let action = self
.seat
.get_keyboard()
@ -138,23 +149,23 @@ impl<B: Backend> State<B> {
if modifiers.logo {
modifier_mask.push(Modifier::Super);
}
let modifier_mask = ModifierMask::from(modifier_mask);
let raw_sym = if keysym.raw_syms().len() == 1 {
keysym.raw_syms()[0]
} else {
keysyms::KEY_NoSymbol
};
if let Some(callback_id) = state
.input_state
.keybinds
.get(&(modifier_mask.into(), raw_sym))
.get(&(modifier_mask, raw_sym))
{
return FilterResult::Intercept(KeyAction::CallCallback(*callback_id));
} else if modifiers.ctrl
&& modifiers.shift
&& modifiers.alt
&& keysym.modified_sym() == keysyms::KEY_Escape
{
} else if (modifier_mask, raw_sym) == kill_keybind {
return FilterResult::Intercept(KeyAction::Quit);
} else if (modifier_mask, raw_sym) == reload_keybind {
return FilterResult::Intercept(KeyAction::ReloadConfig);
} else if let mut vt @ keysyms::KEY_XF86Switch_VT_1..=keysyms::KEY_XF86Switch_VT_12 =
keysym.modified_sym() {
vt = vt - keysyms::KEY_XF86Switch_VT_1 + 1;
@ -205,7 +216,10 @@ impl<B: Backend> State<B> {
Some(KeyAction::Quit) => {
self.loop_signal.stop();
}
_ => {}
Some(KeyAction::ReloadConfig) => {
self.restart_config().expect("failed to restart config");
}
None => {}
}
}

View file

@ -59,7 +59,7 @@ impl<B: Backend> State<B> {
state.focused_tags().next().cloned().map(|tag| tag.layout())
}) else { return };
let (windows_on_foc_tags, windows_not_on_foc_tags): (Vec<_>, _) =
let (windows_on_foc_tags, mut windows_not_on_foc_tags): (Vec<_>, _) =
output.with_state(|state| {
let focused_tags = state.focused_tags().collect::<Vec<_>>();
self.windows.iter().cloned().partition(|win| {
@ -67,6 +67,8 @@ impl<B: Backend> State<B> {
})
});
windows_not_on_foc_tags.retain(|win| win.output(self) == Some(output.clone()));
let tiled_windows = windows_on_foc_tags
.iter()
.filter(|win| {

View file

@ -19,6 +19,7 @@ mod grab;
mod handlers;
mod input;
mod layout;
mod metaconfig;
mod output;
mod pointer;
mod render;
@ -26,9 +27,7 @@ mod state;
mod tag;
mod window;
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
fn main() -> anyhow::Result<()> {
match tracing_subscriber::EnvFilter::try_from_default_env() {
Ok(env_filter) => {
tracing_subscriber::fmt()

105
src/metaconfig.rs Normal file
View file

@ -0,0 +1,105 @@
use std::path::Path;
use anyhow::Context;
use smithay::input::keyboard::keysyms;
use toml::Table;
use crate::api::msg::Modifier;
#[derive(serde::Deserialize, Debug)]
pub struct Metaconfig {
pub command: String,
pub envs: Option<Table>,
pub reload_keybind: Keybind,
pub kill_keybind: Keybind,
pub socket_dir: Option<String>,
}
#[derive(serde::Deserialize, Debug)]
pub struct Keybind {
pub modifiers: Vec<Modifier>,
pub key: Key,
}
#[derive(serde::Deserialize, Debug, Clone, Copy)]
#[serde(rename_all = "snake_case")]
#[repr(u32)]
pub enum Key {
A = keysyms::KEY_a,
B = keysyms::KEY_b,
C = keysyms::KEY_c,
D = keysyms::KEY_d,
E = keysyms::KEY_e,
F = keysyms::KEY_f,
G = keysyms::KEY_g,
H = keysyms::KEY_h,
I = keysyms::KEY_i,
J = keysyms::KEY_j,
K = keysyms::KEY_k,
L = keysyms::KEY_l,
M = keysyms::KEY_m,
N = keysyms::KEY_n,
O = keysyms::KEY_o,
P = keysyms::KEY_p,
Q = keysyms::KEY_q,
R = keysyms::KEY_r,
S = keysyms::KEY_s,
T = keysyms::KEY_t,
U = keysyms::KEY_u,
V = keysyms::KEY_v,
W = keysyms::KEY_w,
X = keysyms::KEY_x,
Y = keysyms::KEY_y,
Z = keysyms::KEY_z,
#[serde(alias = "0")]
Zero = keysyms::KEY_0,
#[serde(alias = "1")]
One = keysyms::KEY_1,
#[serde(alias = "2")]
Two = keysyms::KEY_2,
#[serde(alias = "3")]
Three = keysyms::KEY_3,
#[serde(alias = "4")]
Four = keysyms::KEY_4,
#[serde(alias = "5")]
Five = keysyms::KEY_5,
#[serde(alias = "6")]
Six = keysyms::KEY_6,
#[serde(alias = "7")]
Seven = keysyms::KEY_7,
#[serde(alias = "8")]
Eight = keysyms::KEY_8,
#[serde(alias = "9")]
Nine = keysyms::KEY_9,
#[serde(alias = "num0")]
NumZero = keysyms::KEY_KP_0,
#[serde(alias = "num1")]
NumOne = keysyms::KEY_KP_1,
#[serde(alias = "num2")]
NumTwo = keysyms::KEY_KP_2,
#[serde(alias = "num3")]
NumThree = keysyms::KEY_KP_3,
#[serde(alias = "num4")]
NumFour = keysyms::KEY_KP_4,
#[serde(alias = "num5")]
NumFive = keysyms::KEY_KP_5,
#[serde(alias = "num6")]
NumSix = keysyms::KEY_KP_6,
#[serde(alias = "num7")]
NumSeven = keysyms::KEY_KP_7,
#[serde(alias = "num8")]
NumEight = keysyms::KEY_KP_8,
#[serde(alias = "num9")]
NumNine = keysyms::KEY_KP_9,
#[serde(alias = "esc")]
Escape = keysyms::KEY_Escape,
}
pub fn parse(config_dir: &Path) -> anyhow::Result<Metaconfig> {
let config_dir = config_dir.join("metaconfig.toml");
let metaconfig =
std::fs::read_to_string(config_dir).context("Failed to read metaconfig.toml")?;
toml::from_str(&metaconfig).context("Failed to deserialize toml")
}

View file

@ -1,33 +1,32 @@
// SPDX-License-Identifier: GPL-3.0-or-later
mod api_handlers;
use std::{
cell::RefCell,
error::Error,
ffi::OsString,
os::{fd::AsRawFd, unix::net::UnixStream},
path::PathBuf,
process::Stdio,
path::{Path, PathBuf},
sync::{Arc, Mutex},
time::Duration,
};
use crate::{
api::{
msg::{Args, CallbackId, Msg, OutgoingMsg, Request, RequestId, RequestResponse},
PinnacleSocketSource,
msg::{CallbackId, ModifierMask, Msg},
PinnacleSocketSource, DEFAULT_SOCKET_DIR,
},
cursor::Cursor,
focus::FocusState,
grab::resize_grab::ResizeSurfaceState,
tag::Tag,
metaconfig::Metaconfig,
tag::TagId,
window::{window_state::LocationRequestState, WindowElement},
};
use anyhow::Context;
use calloop::futures::Scheduler;
use futures_lite::AsyncBufReadExt;
use smithay::{
backend::renderer::element::RenderElementStates,
desktop::{
space::SpaceElement,
utils::{
surface_presentation_feedback_flags_from_states, surface_primary_scanout_output,
OutputPresentationFeedback,
@ -55,10 +54,7 @@ use smithay::{
fractional_scale::FractionalScaleManagerState,
output::OutputManagerState,
primary_selection::PrimarySelectionState,
shell::{
wlr_layer::WlrLayerShellState,
xdg::{XdgShellState, XdgToplevelSurfaceData},
},
shell::{wlr_layer::WlrLayerShellState, xdg::XdgShellState},
shm::ShmState,
socket::ListeningSocketSource,
viewporter::ViewporterState,
@ -107,6 +103,7 @@ pub struct State<B: Backend> {
pub windows: Vec<WindowElement>,
pub async_scheduler: Scheduler<()>,
pub config_process: async_process::Child,
// TODO: move into own struct
// | basically just clean this mess up
@ -117,559 +114,6 @@ pub struct State<B: Backend> {
pub xdisplay: Option<u32>,
}
impl<B: Backend> State<B> {
pub fn handle_msg(&mut self, msg: Msg) {
// tracing::debug!("Got {msg:?}");
match msg {
Msg::SetKeybind {
key,
modifiers,
callback_id,
} => {
tracing::info!("set keybind: {:?}, {}", modifiers, key);
self.input_state
.keybinds
.insert((modifiers.into(), key), callback_id);
}
Msg::SetMousebind { button: _ } => todo!(),
Msg::CloseWindow { window_id } => {
if let Some(window) = window_id.window(self) {
match window {
WindowElement::Wayland(window) => window.toplevel().send_close(),
WindowElement::X11(surface) => {
surface.close().expect("failed to close x11 win");
}
}
}
}
Msg::Spawn {
command,
callback_id,
} => {
self.handle_spawn(command, callback_id);
}
Msg::SetWindowSize {
window_id,
width,
height,
} => {
let Some(window) = window_id.window(self) else { return };
// TODO: tiled vs floating
// FIXME: this will map unmapped windows at 0,0
let window_loc = self
.space
.element_location(&window)
.unwrap_or((0, 0).into());
let mut window_size = window.geometry().size;
if let Some(width) = width {
window_size.w = width;
}
if let Some(height) = height {
window_size.h = height;
}
window.request_size_change(&mut self.space, window_loc, window_size);
}
Msg::MoveWindowToTag { window_id, tag_id } => {
let Some(window) = window_id.window(self) else { return };
let Some(tag) = tag_id.tag(self) else { return };
window.with_state(|state| {
state.tags = vec![tag.clone()];
});
let Some(output) = tag.output(self) else { return };
self.update_windows(&output);
// self.re_layout(&output);
}
Msg::ToggleTagOnWindow { window_id, tag_id } => {
let Some(window) = window_id.window(self) else { return };
let Some(tag) = tag_id.tag(self) else { return };
window.with_state(|state| {
if state.tags.contains(&tag) {
state.tags.retain(|tg| tg != &tag);
} else {
state.tags.push(tag.clone());
}
});
let Some(output) = tag.output(self) else { return };
self.update_windows(&output);
// self.re_layout(&output);
}
Msg::ToggleFloating { window_id } => {
let Some(window) = window_id.window(self) else { return };
window.toggle_floating();
let Some(output) = window.output(self) else { return };
self.update_windows(&output);
}
Msg::ToggleFullscreen { window_id } => {
let Some(window) = window_id.window(self) else { return };
window.toggle_fullscreen();
let Some(output) = window.output(self) else { return };
self.update_windows(&output);
}
Msg::ToggleMaximized { window_id } => {
let Some(window) = window_id.window(self) else { return };
window.toggle_maximized();
let Some(output) = window.output(self) else { return };
self.update_windows(&output);
}
// Tags ----------------------------------------
Msg::ToggleTag { tag_id } => {
tracing::debug!("ToggleTag");
if let Some(tag) = tag_id.tag(self) {
tag.set_active(!tag.active());
if let Some(output) = tag.output(self) {
self.update_windows(&output);
// self.re_layout(&output);
}
}
}
Msg::SwitchToTag { tag_id } => {
let Some(tag) = tag_id.tag(self) else { return };
let Some(output) = tag.output(self) else { return };
output.with_state(|state| {
for op_tag in state.tags.iter_mut() {
op_tag.set_active(false);
}
tag.set_active(true);
});
self.update_windows(&output);
// self.re_layout(&output);
}
Msg::AddTags {
output_name,
tag_names,
} => {
if let Some(output) = self
.space
.outputs()
.find(|output| output.name() == output_name)
{
output.with_state(|state| {
state.tags.extend(tag_names.iter().cloned().map(Tag::new));
tracing::debug!("tags added, are now {:?}", state.tags);
});
}
}
Msg::RemoveTags { tag_ids } => {
let tags = tag_ids.into_iter().filter_map(|tag_id| tag_id.tag(self));
for tag in tags {
let Some(output) = tag.output(self) else { continue };
output.with_state(|state| {
state.tags.retain(|tg| tg != &tag);
});
}
}
Msg::SetLayout { tag_id, layout } => {
let Some(tag) = tag_id.tag(self) else { return };
tag.set_layout(layout);
let Some(output) = tag.output(self) else { return };
self.update_windows(&output);
// self.re_layout(&output);
}
Msg::ConnectForAllOutputs { callback_id } => {
let stream = self
.api_state
.stream
.as_ref()
.expect("Stream doesn't exist");
let mut stream = stream.lock().expect("Couldn't lock stream");
for output in self.space.outputs() {
crate::api::send_to_client(
&mut stream,
&OutgoingMsg::CallCallback {
callback_id,
args: Some(Args::ConnectForAllOutputs {
output_name: output.name(),
}),
},
)
.expect("Send to client failed");
}
self.output_callback_ids.push(callback_id);
}
Msg::SetOutputLocation { output_name, x, y } => {
let Some(output) = output_name.output(self) else { return };
let mut loc = output.current_location();
if let Some(x) = x {
loc.x = x;
}
if let Some(y) = y {
loc.y = y;
}
output.change_current_state(None, None, None, Some(loc));
self.space.map_output(&output, loc);
tracing::debug!("mapping output {} to {loc:?}", output.name());
self.update_windows(&output);
// self.re_layout(&output);
}
Msg::Quit => {
self.loop_signal.stop();
}
Msg::Request {
request_id,
request,
} => {
self.handle_request(request_id, request);
}
}
}
fn handle_request(&mut self, request_id: RequestId, request: Request) {
let stream = self
.api_state
.stream
.as_ref()
.expect("Stream doesn't exist");
let mut stream = stream.lock().expect("Couldn't lock stream");
match request {
Request::GetWindows => {
let window_ids = self
.windows
.iter()
.map(|win| win.with_state(|state| state.id))
.collect::<Vec<_>>();
// FIXME: figure out what to do if error
crate::api::send_to_client(
&mut stream,
&OutgoingMsg::RequestResponse {
request_id,
response: RequestResponse::Windows { window_ids },
},
)
.expect("Couldn't send to client");
}
Request::GetWindowProps { window_id } => {
let window = window_id.window(self);
let size = window
.as_ref()
.map(|win| (win.geometry().size.w, win.geometry().size.h));
let loc = window
.as_ref()
.and_then(|win| self.space.element_location(win))
.map(|loc| (loc.x, loc.y));
let (class, title) = window.as_ref().map_or((None, None), |win| match &win {
WindowElement::Wayland(_) => {
if let Some(wl_surf) = win.wl_surface() {
compositor::with_states(&wl_surf, |states| {
let lock = states
.data_map
.get::<XdgToplevelSurfaceData>()
.expect("XdgToplevelSurfaceData wasn't in surface's data map")
.lock()
.expect("failed to acquire lock");
(lock.app_id.clone(), lock.title.clone())
})
} else {
(None, None)
}
}
WindowElement::X11(surface) => (Some(surface.class()), Some(surface.title())),
});
let focused = window.as_ref().and_then(|win| {
self.focus_state
.current_focus() // TODO: actual focus
.map(|foc_win| win == &foc_win)
});
let floating = window
.as_ref()
.map(|win| win.with_state(|state| state.floating_or_tiled.is_floating()));
let fullscreen_or_maximized = window
.as_ref()
.map(|win| win.with_state(|state| state.fullscreen_or_maximized));
crate::api::send_to_client(
&mut stream,
&OutgoingMsg::RequestResponse {
request_id,
response: RequestResponse::WindowProps {
size,
loc,
class,
title,
focused,
floating,
fullscreen_or_maximized,
},
},
)
.expect("failed to send to client");
}
Request::GetOutputs => {
let output_names = self
.space
.outputs()
.map(|output| output.name())
.collect::<Vec<_>>();
crate::api::send_to_client(
&mut stream,
&OutgoingMsg::RequestResponse {
request_id,
response: RequestResponse::Outputs { output_names },
},
)
.expect("failed to send to client");
}
Request::GetOutputProps { output_name } => {
let output = self
.space
.outputs()
.find(|output| output.name() == output_name);
let res = output.as_ref().and_then(|output| {
output.current_mode().map(|mode| (mode.size.w, mode.size.h))
});
let refresh_rate = output
.as_ref()
.and_then(|output| output.current_mode().map(|mode| mode.refresh));
let model = output
.as_ref()
.map(|output| output.physical_properties().model);
let physical_size = output.as_ref().map(|output| {
(
output.physical_properties().size.w,
output.physical_properties().size.h,
)
});
let make = output
.as_ref()
.map(|output| output.physical_properties().make);
let loc = output
.as_ref()
.map(|output| (output.current_location().x, output.current_location().y));
let focused = self
.focus_state
.focused_output
.as_ref()
.and_then(|foc_op| output.map(|op| op == foc_op));
let tag_ids = output.as_ref().map(|output| {
output.with_state(|state| {
state.tags.iter().map(|tag| tag.id()).collect::<Vec<_>>()
})
});
crate::api::send_to_client(
&mut stream,
&OutgoingMsg::RequestResponse {
request_id,
response: RequestResponse::OutputProps {
make,
model,
loc,
res,
refresh_rate,
physical_size,
focused,
tag_ids,
},
},
)
.expect("failed to send to client");
}
Request::GetTags => {
let tag_ids = self
.space
.outputs()
.flat_map(|op| op.with_state(|state| state.tags.clone()))
.map(|tag| tag.id())
.collect::<Vec<_>>();
tracing::debug!("GetTags: {:?}", tag_ids);
crate::api::send_to_client(
&mut stream,
&OutgoingMsg::RequestResponse {
request_id,
response: RequestResponse::Tags { tag_ids },
},
)
.expect("failed to send to client");
}
Request::GetTagProps { tag_id } => {
let tag = tag_id.tag(self);
let output_name = tag
.as_ref()
.and_then(|tag| tag.output(self))
.map(|output| output.name());
let active = tag.as_ref().map(|tag| tag.active());
let name = tag.as_ref().map(|tag| tag.name());
crate::api::send_to_client(
&mut stream,
&OutgoingMsg::RequestResponse {
request_id,
response: RequestResponse::TagProps {
active,
name,
output_name,
},
},
)
.expect("failed to send to client");
}
}
}
pub fn handle_spawn(&self, command: Vec<String>, callback_id: Option<CallbackId>) {
let mut command = command.into_iter();
let Some(program) = command.next() else {
// TODO: notify that command was nothing
return;
};
let program = OsString::from(program);
let Ok(mut child) = async_process::Command::new(&program)
.envs(
[("WAYLAND_DISPLAY", self.socket_name.clone())]
.into_iter()
.chain(
self.xdisplay.map(|xdisp| ("DISPLAY", format!(":{xdisp}")))
)
)
.stdin(if callback_id.is_some() {
Stdio::piped()
} else {
// piping to null because foot won't open without a callback_id
// otherwise
Stdio::null()
})
.stdout(if callback_id.is_some() {
Stdio::piped()
} else {
Stdio::null()
})
.stderr(if callback_id.is_some() {
Stdio::piped()
} else {
Stdio::null()
})
.args(command)
.spawn()
else {
// TODO: notify user that program doesn't exist
tracing::warn!("tried to run {}, but it doesn't exist", program.to_string_lossy());
return;
};
if let Some(callback_id) = callback_id {
let stdout = child.stdout.take();
let stderr = child.stderr.take();
let stream_out = self
.api_state
.stream
.as_ref()
.expect("Stream doesn't exist")
.clone();
let stream_err = stream_out.clone();
let stream_exit = stream_out.clone();
if let Some(stdout) = stdout {
let future = async move {
// TODO: use BufReader::new().lines()
let mut reader = futures_lite::io::BufReader::new(stdout);
loop {
let mut buf = String::new();
match reader.read_line(&mut buf).await {
Ok(0) => break,
Ok(_) => {
let mut stream = stream_out.lock().expect("Couldn't lock stream");
crate::api::send_to_client(
&mut stream,
&OutgoingMsg::CallCallback {
callback_id,
args: Some(Args::Spawn {
stdout: Some(buf.trim_end_matches('\n').to_string()),
stderr: None,
exit_code: None,
exit_msg: None,
}),
},
)
.expect("Send to client failed"); // TODO: notify instead of crash
}
Err(err) => {
tracing::warn!("child read err: {err}");
break;
}
}
}
};
// This is not important enough to crash on error, so just print the error instead
if let Err(err) = self.async_scheduler.schedule(future) {
tracing::error!("Failed to schedule future: {err}");
}
}
if let Some(stderr) = stderr {
let future = async move {
let mut reader = futures_lite::io::BufReader::new(stderr);
loop {
let mut buf = String::new();
match reader.read_line(&mut buf).await {
Ok(0) => break,
Ok(_) => {
let mut stream = stream_err.lock().expect("Couldn't lock stream");
crate::api::send_to_client(
&mut stream,
&OutgoingMsg::CallCallback {
callback_id,
args: Some(Args::Spawn {
stdout: None,
stderr: Some(buf.trim_end_matches('\n').to_string()),
exit_code: None,
exit_msg: None,
}),
},
)
.expect("Send to client failed"); // TODO: notify instead of crash
}
Err(err) => {
tracing::warn!("child read err: {err}");
break;
}
}
}
};
if let Err(err) = self.async_scheduler.schedule(future) {
tracing::error!("Failed to schedule future: {err}");
}
}
let future = async move {
match child.status().await {
Ok(exit_status) => {
let mut stream = stream_exit.lock().expect("Couldn't lock stream");
crate::api::send_to_client(
&mut stream,
&OutgoingMsg::CallCallback {
callback_id,
args: Some(Args::Spawn {
stdout: None,
stderr: None,
exit_code: exit_status.code(),
exit_msg: Some(exit_status.to_string()),
}),
},
)
.expect("Send to client failed"); // TODO: notify instead of crash
}
Err(err) => {
tracing::warn!("child wait() err: {err}");
}
}
};
if let Err(err) = self.async_scheduler.schedule(future) {
tracing::error!("Failed to schedule future: {err}");
}
}
}
}
/// Schedule something to be done when windows have finished committing and have become
/// idle.
pub fn schedule_on_commit<F, B: Backend>(
@ -718,7 +162,7 @@ impl<B: Backend> State<B> {
display: &mut Display<Self>,
loop_signal: LoopSignal,
loop_handle: LoopHandle<'static, CalloopData<B>>,
) -> Result<Self, Box<dyn Error>> {
) -> anyhow::Result<Self> {
let socket = ListeningSocketSource::new_auto()?;
let socket_name = socket.socket_name().to_os_string();
@ -766,21 +210,40 @@ impl<B: Backend> State<B> {
let (tx_channel, rx_channel) = calloop::channel::channel::<Msg>();
// We want to replace the client if a new one pops up
// TODO: there should only ever be one client working at a time, and creating a new client
// | when one is already running should be impossible.
// INFO: this source try_clone()s the stream
let config_dir = get_config_dir();
// TODO: probably use anyhow or something
let socket_source = match PinnacleSocketSource::new(tx_channel) {
Ok(source) => source,
Err(err) => {
tracing::error!("Failed to create the socket source: {err}");
Err(err)?
}
let metaconfig = crate::metaconfig::parse(&config_dir)?;
let socket_dir = {
let dir_string = shellexpand::full(
metaconfig
.socket_dir
.as_deref()
.unwrap_or(DEFAULT_SOCKET_DIR),
)?
.to_string();
// cd into the metaconfig dir and canonicalize to preserve relative paths
// like ./dir/here
let current_dir = std::env::current_dir()?;
std::env::set_current_dir(&config_dir)?;
let pathbuf = PathBuf::from(&dir_string).canonicalize()?;
std::env::set_current_dir(current_dir)?;
pathbuf
};
loop_handle.insert_source(socket_source, |stream, _, data| {
let socket_source = PinnacleSocketSource::new(tx_channel, &socket_dir)
.context("Failed to create socket source")?;
let ConfigReturn {
reload_keybind,
kill_keybind,
config_child_handle,
} = start_config(metaconfig, &config_dir)?;
let insert_ret = loop_handle.insert_source(socket_source, |stream, _, data| {
if let Some(old_stream) = data
.state
.api_state
@ -793,13 +256,17 @@ impl<B: Backend> State<B> {
.shutdown(std::net::Shutdown::Both)
.expect("Couldn't shutdown old stream");
}
})?;
});
if let Err(err) = insert_ret {
anyhow::bail!("Failed to insert socket source into event loop: {err}");
}
let (executor, sched) =
calloop::futures::executor::<()>().expect("Couldn't create executor");
loop_handle.insert_source(executor, |_, _, _| {})?;
start_lua_config()?;
if let Err(err) = loop_handle.insert_source(executor, |_, _, _| {}) {
anyhow::bail!("Failed to insert async executor into event loop: {err}");
}
let display_handle = display.handle();
let mut seat_state = SeatState::new();
@ -882,7 +349,7 @@ impl<B: Backend> State<B> {
primary_selection_state: PrimarySelectionState::new::<Self>(&display_handle),
layer_shell_state: WlrLayerShellState::new::<Self>(&display_handle),
input_state: InputState::new(),
input_state: InputState::new(reload_keybind, kill_keybind),
api_state: ApiState::new(),
focus_state: FocusState::new(),
@ -896,6 +363,7 @@ impl<B: Backend> State<B> {
popup_manager: PopupManager::default(),
async_scheduler: sched,
config_process: config_child_handle,
windows: vec![],
output_callback_ids: vec![],
@ -907,60 +375,114 @@ impl<B: Backend> State<B> {
}
}
fn start_lua_config() -> Result<(), Box<dyn std::error::Error>> {
// TODO: move all this into the lua api
let config_path = std::env::var("PINNACLE_CONFIG")
.map(PathBuf::from)
.unwrap_or_else(|_| {
let default_path = std::env::var("XDG_CONFIG_HOME").unwrap_or("~/.config".to_string());
let mut default_path = PathBuf::from(default_path);
default_path.push("pinnacle/init.lua");
default_path
});
fn get_config_dir() -> PathBuf {
let config_dir = std::env::var("PINNACLE_CONFIG_DIR").unwrap_or_else(|_| {
let default_config_dir =
std::env::var("XDG_CONFIG_HOME").unwrap_or("~/.config".to_string());
let config_path = {
let path = shellexpand::tilde(&config_path.to_string_lossy().to_string()).to_string();
PathBuf::from(path)
};
if config_path.exists() {
let lua_path = std::env::var("LUA_PATH").unwrap_or_else(|_| {
tracing::info!("LUA_PATH was not set, using empty string");
"".to_string()
});
let mut local_lua_path = std::env::current_dir()
.expect("Couldn't get current dir")
PathBuf::from(default_config_dir)
.join("pinnacle")
.to_string_lossy()
.to_string();
local_lua_path.push_str("/api/lua"); // TODO: get from crate root and do dynamically
let new_lua_path =
format!("{local_lua_path}/?.lua;{local_lua_path}/?/init.lua;{local_lua_path}/lib/?.lua;{local_lua_path}/lib/?/init.lua;{lua_path}");
.to_string()
});
PathBuf::from(shellexpand::tilde(&config_dir).to_string())
}
let lua_cpath = std::env::var("LUA_CPATH").unwrap_or_else(|_| {
tracing::info!("LUA_CPATH was not set, using empty string");
"".to_string()
});
let new_lua_cpath = format!("{local_lua_path}/lib/?.so;{lua_cpath}");
/// This should be called *after* you have created the [`PinnacleSocketSource`] to ensure
/// PINNACLE_SOCKET is set correctly for use in API implementations.
fn start_config(metaconfig: Metaconfig, config_dir: &Path) -> anyhow::Result<ConfigReturn> {
let reload_keybind = metaconfig.reload_keybind;
let kill_keybind = metaconfig.kill_keybind;
if let Err(err) = std::process::Command::new("lua")
.arg(config_path)
.env("LUA_PATH", new_lua_path)
.env("LUA_CPATH", new_lua_cpath)
.spawn()
{
tracing::error!("Failed to start Lua: {err}");
return Err(err)?;
let mut command = metaconfig.command.split(' ');
let arg1 = command.next().expect("empty command");
std::env::set_var("PINNACLE_DIR", std::env::current_dir()?);
let envs = metaconfig
.envs
.unwrap_or(toml::map::Map::new())
.into_iter()
.filter_map(|(key, val)| {
if let toml::Value::String(string) = val {
Some((
key,
shellexpand::full_with_context(
&string,
|| std::env::var("HOME").ok(),
|var| Ok::<_, ()>(Some(std::env::var(var).unwrap_or("".to_string()))),
)
.ok()?
.to_string(),
))
} else {
None
}
})
.collect::<Vec<_>>();
tracing::debug!("Config envs are {:?}", envs);
let child = async_process::Command::new(arg1)
.args(command)
.envs(envs)
.current_dir(config_dir)
.spawn()
.expect("failed to spawn config");
tracing::info!("Started config with {}", metaconfig.command);
let reload_mask = ModifierMask::from(reload_keybind.modifiers);
let kill_mask = ModifierMask::from(kill_keybind.modifiers);
Ok(ConfigReturn {
reload_keybind: (reload_mask, reload_keybind.key as u32),
kill_keybind: (kill_mask, kill_keybind.key as u32),
config_child_handle: child,
})
}
struct ConfigReturn {
reload_keybind: (ModifierMask, u32),
kill_keybind: (ModifierMask, u32),
config_child_handle: async_process::Child,
}
impl<B: Backend> State<B> {
pub fn restart_config(&mut self) -> anyhow::Result<()> {
tracing::info!("Restarting config");
tracing::debug!("Clearing tags");
for output in self.space.outputs() {
output.with_state(|state| state.tags.clear());
}
TagId::reset();
tracing::debug!("Clearing mouse- and keybinds");
self.input_state.keybinds.clear();
self.input_state.mousebinds.clear();
tracing::debug!("Killing old config");
if let Err(err) = self.config_process.kill() {
tracing::warn!("Error when killing old config: {err}");
}
let config_dir = get_config_dir();
let metaconfig =
crate::metaconfig::parse(&config_dir).context("Failed to parse metaconfig.toml")?;
let ConfigReturn {
reload_keybind,
kill_keybind,
config_child_handle,
} = start_config(metaconfig, &config_dir)?;
self.input_state.reload_keybind = reload_keybind;
self.input_state.kill_keybind = kill_keybind;
self.config_process = config_child_handle;
Ok(())
} else {
tracing::error!("Could not find config {:?}", config_path);
if std::env::var("PINNACLE_CONFIG").is_err() {
tracing::error!("Help: Run Pinnacle with PINNACLE_CONFIG set to a valid config file, or copy the provided example_config.lua to the path mentioned above.");
}
Err(std::io::Error::new(
std::io::ErrorKind::Other,
"No config found",
))?
}
}

585
src/state/api_handlers.rs Normal file
View file

@ -0,0 +1,585 @@
use std::ffi::OsString;
use async_process::Stdio;
use futures_lite::AsyncBufReadExt;
use smithay::{
desktop::space::SpaceElement,
wayland::{compositor, shell::xdg::XdgToplevelSurfaceData},
};
use crate::{
api::msg::{Args, CallbackId, Msg, OutgoingMsg, Request, RequestId, RequestResponse},
backend::Backend,
tag::Tag,
window::WindowElement,
};
use super::{State, WithState};
impl<B: Backend> State<B> {
pub fn handle_msg(&mut self, msg: Msg) {
// tracing::debug!("Got {msg:?}");
match msg {
Msg::SetKeybind {
key,
modifiers,
callback_id,
} => {
tracing::info!("set keybind: {:?}, {}", modifiers, key);
self.input_state
.keybinds
.insert((modifiers.into(), key), callback_id);
}
Msg::SetMousebind { button: _ } => todo!(),
Msg::CloseWindow { window_id } => {
if let Some(window) = window_id.window(self) {
match window {
WindowElement::Wayland(window) => window.toplevel().send_close(),
WindowElement::X11(surface) => {
surface.close().expect("failed to close x11 win");
}
}
}
}
Msg::Spawn {
command,
callback_id,
} => {
self.handle_spawn(command, callback_id);
}
Msg::SetWindowSize {
window_id,
width,
height,
} => {
let Some(window) = window_id.window(self) else { return };
// TODO: tiled vs floating
// FIXME: this will map unmapped windows at 0,0
let window_loc = self
.space
.element_location(&window)
.unwrap_or((0, 0).into());
let mut window_size = window.geometry().size;
if let Some(width) = width {
window_size.w = width;
}
if let Some(height) = height {
window_size.h = height;
}
window.request_size_change(&mut self.space, window_loc, window_size);
}
Msg::MoveWindowToTag { window_id, tag_id } => {
let Some(window) = window_id.window(self) else { return };
let Some(tag) = tag_id.tag(self) else { return };
window.with_state(|state| {
state.tags = vec![tag.clone()];
});
let Some(output) = tag.output(self) else { return };
self.update_windows(&output);
// self.re_layout(&output);
}
Msg::ToggleTagOnWindow { window_id, tag_id } => {
let Some(window) = window_id.window(self) else { return };
let Some(tag) = tag_id.tag(self) else { return };
window.with_state(|state| {
if state.tags.contains(&tag) {
state.tags.retain(|tg| tg != &tag);
} else {
state.tags.push(tag.clone());
}
});
let Some(output) = tag.output(self) else { return };
self.update_windows(&output);
// self.re_layout(&output);
}
Msg::ToggleFloating { window_id } => {
let Some(window) = window_id.window(self) else { return };
window.toggle_floating();
let Some(output) = window.output(self) else { return };
self.update_windows(&output);
}
Msg::ToggleFullscreen { window_id } => {
let Some(window) = window_id.window(self) else { return };
window.toggle_fullscreen();
let Some(output) = window.output(self) else { return };
self.update_windows(&output);
}
Msg::ToggleMaximized { window_id } => {
let Some(window) = window_id.window(self) else { return };
window.toggle_maximized();
let Some(output) = window.output(self) else { return };
self.update_windows(&output);
}
// Tags ----------------------------------------
Msg::ToggleTag { tag_id } => {
tracing::debug!("ToggleTag");
if let Some(tag) = tag_id.tag(self) {
tag.set_active(!tag.active());
if let Some(output) = tag.output(self) {
self.update_windows(&output);
// self.re_layout(&output);
}
}
}
Msg::SwitchToTag { tag_id } => {
let Some(tag) = tag_id.tag(self) else { return };
let Some(output) = tag.output(self) else { return };
output.with_state(|state| {
for op_tag in state.tags.iter_mut() {
op_tag.set_active(false);
}
tag.set_active(true);
});
self.update_windows(&output);
// self.re_layout(&output);
}
Msg::AddTags {
output_name,
tag_names,
} => {
if let Some(output) = self
.space
.outputs()
.find(|output| output.name() == output_name)
{
let new_tags = tag_names.into_iter().map(Tag::new).collect::<Vec<_>>();
output.with_state(|state| {
state.tags.extend(new_tags.clone());
tracing::debug!("tags added, are now {:?}", state.tags);
});
// replace tags that windows have that are the same id
// (this should only happen on config reload)
for tag in new_tags {
for window in self.windows.iter() {
window.with_state(|state| {
for win_tag in state.tags.iter_mut() {
if win_tag.id() == tag.id() {
*win_tag = tag.clone();
}
}
});
}
}
}
}
Msg::RemoveTags { tag_ids } => {
let tags = tag_ids.into_iter().filter_map(|tag_id| tag_id.tag(self));
for tag in tags {
let Some(output) = tag.output(self) else { continue };
output.with_state(|state| {
state.tags.retain(|tg| tg != &tag);
});
}
}
Msg::SetLayout { tag_id, layout } => {
let Some(tag) = tag_id.tag(self) else { return };
tag.set_layout(layout);
let Some(output) = tag.output(self) else { return };
self.update_windows(&output);
// self.re_layout(&output);
}
Msg::ConnectForAllOutputs { callback_id } => {
let stream = self
.api_state
.stream
.as_ref()
.expect("Stream doesn't exist");
let mut stream = stream.lock().expect("Couldn't lock stream");
for output in self.space.outputs() {
crate::api::send_to_client(
&mut stream,
&OutgoingMsg::CallCallback {
callback_id,
args: Some(Args::ConnectForAllOutputs {
output_name: output.name(),
}),
},
)
.expect("Send to client failed");
}
self.output_callback_ids.push(callback_id);
}
Msg::SetOutputLocation { output_name, x, y } => {
let Some(output) = output_name.output(self) else { return };
let mut loc = output.current_location();
if let Some(x) = x {
loc.x = x;
}
if let Some(y) = y {
loc.y = y;
}
output.change_current_state(None, None, None, Some(loc));
self.space.map_output(&output, loc);
tracing::debug!("mapping output {} to {loc:?}", output.name());
self.update_windows(&output);
// self.re_layout(&output);
}
Msg::Quit => {
self.loop_signal.stop();
}
Msg::Request {
request_id,
request,
} => {
self.handle_request(request_id, request);
}
}
}
fn handle_request(&mut self, request_id: RequestId, request: Request) {
let stream = self
.api_state
.stream
.as_ref()
.expect("Stream doesn't exist");
let mut stream = stream.lock().expect("Couldn't lock stream");
match request {
Request::GetWindows => {
let window_ids = self
.windows
.iter()
.map(|win| win.with_state(|state| state.id))
.collect::<Vec<_>>();
// FIXME: figure out what to do if error
crate::api::send_to_client(
&mut stream,
&OutgoingMsg::RequestResponse {
request_id,
response: RequestResponse::Windows { window_ids },
},
)
.expect("Couldn't send to client");
}
Request::GetWindowProps { window_id } => {
let window = window_id.window(self);
let size = window
.as_ref()
.map(|win| (win.geometry().size.w, win.geometry().size.h));
let loc = window
.as_ref()
.and_then(|win| self.space.element_location(win))
.map(|loc| (loc.x, loc.y));
let (class, title) = window.as_ref().map_or((None, None), |win| match &win {
WindowElement::Wayland(_) => {
if let Some(wl_surf) = win.wl_surface() {
compositor::with_states(&wl_surf, |states| {
let lock = states
.data_map
.get::<XdgToplevelSurfaceData>()
.expect("XdgToplevelSurfaceData wasn't in surface's data map")
.lock()
.expect("failed to acquire lock");
(lock.app_id.clone(), lock.title.clone())
})
} else {
(None, None)
}
}
WindowElement::X11(surface) => (Some(surface.class()), Some(surface.title())),
});
let focused = window.as_ref().and_then(|win| {
self.focus_state
.current_focus() // TODO: actual focus
.map(|foc_win| win == &foc_win)
});
let floating = window
.as_ref()
.map(|win| win.with_state(|state| state.floating_or_tiled.is_floating()));
let fullscreen_or_maximized = window
.as_ref()
.map(|win| win.with_state(|state| state.fullscreen_or_maximized));
crate::api::send_to_client(
&mut stream,
&OutgoingMsg::RequestResponse {
request_id,
response: RequestResponse::WindowProps {
size,
loc,
class,
title,
focused,
floating,
fullscreen_or_maximized,
},
},
)
.expect("failed to send to client");
}
Request::GetOutputs => {
let output_names = self
.space
.outputs()
.map(|output| output.name())
.collect::<Vec<_>>();
crate::api::send_to_client(
&mut stream,
&OutgoingMsg::RequestResponse {
request_id,
response: RequestResponse::Outputs { output_names },
},
)
.expect("failed to send to client");
}
Request::GetOutputProps { output_name } => {
let output = self
.space
.outputs()
.find(|output| output.name() == output_name);
let res = output.as_ref().and_then(|output| {
output.current_mode().map(|mode| (mode.size.w, mode.size.h))
});
let refresh_rate = output
.as_ref()
.and_then(|output| output.current_mode().map(|mode| mode.refresh));
let model = output
.as_ref()
.map(|output| output.physical_properties().model);
let physical_size = output.as_ref().map(|output| {
(
output.physical_properties().size.w,
output.physical_properties().size.h,
)
});
let make = output
.as_ref()
.map(|output| output.physical_properties().make);
let loc = output
.as_ref()
.map(|output| (output.current_location().x, output.current_location().y));
let focused = self
.focus_state
.focused_output
.as_ref()
.and_then(|foc_op| output.map(|op| op == foc_op));
let tag_ids = output.as_ref().map(|output| {
output.with_state(|state| {
state.tags.iter().map(|tag| tag.id()).collect::<Vec<_>>()
})
});
crate::api::send_to_client(
&mut stream,
&OutgoingMsg::RequestResponse {
request_id,
response: RequestResponse::OutputProps {
make,
model,
loc,
res,
refresh_rate,
physical_size,
focused,
tag_ids,
},
},
)
.expect("failed to send to client");
}
Request::GetTags => {
let tag_ids = self
.space
.outputs()
.flat_map(|op| op.with_state(|state| state.tags.clone()))
.map(|tag| tag.id())
.collect::<Vec<_>>();
tracing::debug!("GetTags: {:?}", tag_ids);
crate::api::send_to_client(
&mut stream,
&OutgoingMsg::RequestResponse {
request_id,
response: RequestResponse::Tags { tag_ids },
},
)
.expect("failed to send to client");
}
Request::GetTagProps { tag_id } => {
let tag = tag_id.tag(self);
let output_name = tag
.as_ref()
.and_then(|tag| tag.output(self))
.map(|output| output.name());
let active = tag.as_ref().map(|tag| tag.active());
let name = tag.as_ref().map(|tag| tag.name());
crate::api::send_to_client(
&mut stream,
&OutgoingMsg::RequestResponse {
request_id,
response: RequestResponse::TagProps {
active,
name,
output_name,
},
},
)
.expect("failed to send to client");
}
}
}
pub fn handle_spawn(&self, command: Vec<String>, callback_id: Option<CallbackId>) {
let mut command = command.into_iter();
let Some(program) = command.next() else {
// TODO: notify that command was nothing
return;
};
let program = OsString::from(program);
let Ok(mut child) = async_process::Command::new(&program)
.envs(
[("WAYLAND_DISPLAY", self.socket_name.clone())]
.into_iter()
.chain(
self.xdisplay.map(|xdisp| ("DISPLAY", format!(":{xdisp}")))
)
)
.stdin(if callback_id.is_some() {
Stdio::piped()
} else {
// piping to null because foot won't open without a callback_id
// otherwise
Stdio::null()
})
.stdout(if callback_id.is_some() {
Stdio::piped()
} else {
Stdio::null()
})
.stderr(if callback_id.is_some() {
Stdio::piped()
} else {
Stdio::null()
})
.args(command)
.spawn()
else {
// TODO: notify user that program doesn't exist
tracing::warn!("tried to run {}, but it doesn't exist", program.to_string_lossy());
return;
};
if let Some(callback_id) = callback_id {
let stdout = child.stdout.take();
let stderr = child.stderr.take();
let stream_out = self
.api_state
.stream
.as_ref()
.expect("Stream doesn't exist")
.clone();
let stream_err = stream_out.clone();
let stream_exit = stream_out.clone();
if let Some(stdout) = stdout {
let future = async move {
// TODO: use BufReader::new().lines()
let mut reader = futures_lite::io::BufReader::new(stdout);
loop {
let mut buf = String::new();
match reader.read_line(&mut buf).await {
Ok(0) => break,
Ok(_) => {
let mut stream = stream_out.lock().expect("Couldn't lock stream");
crate::api::send_to_client(
&mut stream,
&OutgoingMsg::CallCallback {
callback_id,
args: Some(Args::Spawn {
stdout: Some(buf.trim_end_matches('\n').to_string()),
stderr: None,
exit_code: None,
exit_msg: None,
}),
},
)
.expect("Send to client failed"); // TODO: notify instead of crash
}
Err(err) => {
tracing::warn!("child read err: {err}");
break;
}
}
}
};
// This is not important enough to crash on error, so just print the error instead
if let Err(err) = self.async_scheduler.schedule(future) {
tracing::error!("Failed to schedule future: {err}");
}
}
if let Some(stderr) = stderr {
let future = async move {
let mut reader = futures_lite::io::BufReader::new(stderr);
loop {
let mut buf = String::new();
match reader.read_line(&mut buf).await {
Ok(0) => break,
Ok(_) => {
let mut stream = stream_err.lock().expect("Couldn't lock stream");
crate::api::send_to_client(
&mut stream,
&OutgoingMsg::CallCallback {
callback_id,
args: Some(Args::Spawn {
stdout: None,
stderr: Some(buf.trim_end_matches('\n').to_string()),
exit_code: None,
exit_msg: None,
}),
},
)
.expect("Send to client failed"); // TODO: notify instead of crash
}
Err(err) => {
tracing::warn!("child read err: {err}");
break;
}
}
}
};
if let Err(err) = self.async_scheduler.schedule(future) {
tracing::error!("Failed to schedule future: {err}");
}
}
let future = async move {
match child.status().await {
Ok(exit_status) => {
let mut stream = stream_exit.lock().expect("Couldn't lock stream");
crate::api::send_to_client(
&mut stream,
&OutgoingMsg::CallCallback {
callback_id,
args: Some(Args::Spawn {
stdout: None,
stderr: None,
exit_code: exit_status.code(),
exit_msg: Some(exit_status.to_string()),
}),
},
)
.expect("Send to client failed"); // TODO: notify instead of crash
}
Err(err) => {
tracing::warn!("child wait() err: {err}");
}
}
};
if let Err(err) = self.async_scheduler.schedule(future) {
tracing::error!("Failed to schedule future: {err}");
}
}
}
}

View file

@ -32,6 +32,14 @@ impl TagId {
.flat_map(|op| op.with_state(|state| state.tags.clone()))
.find(|tag| &tag.id() == self)
}
/// Reset the global TagId counter.
///
/// This is used, for example, when a config is reloaded and you want to keep
/// windows on the same tags.
pub fn reset() {
TAG_ID_COUNTER.store(0, Ordering::SeqCst);
}
}
#[derive(Debug)]