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" } itertools = { version = "0.11.0" }
x11rb = { version = "0.12.0", default-features = false, features = ["composite"], optional = true } x11rb = { version = "0.12.0", default-features = false, features = ["composite"], optional = true }
shellexpand = "3.1.0" shellexpand = "3.1.0"
toml = "0.7.6"
anyhow = { version = "1.0.74", features = ["backtrace"] }
[features] [features]
default = ["egl", "winit", "udev", "xwayland"] default = ["egl", "winit", "udev", "xwayland"]

132
README.md
View file

@ -11,20 +11,27 @@
A very, VERY WIP Smithay-based wayland compositor A very, VERY WIP Smithay-based wayland compositor
</div> </div>
## Changelog ## Info
See [`CHANGELOG.md`](CHANGELOG.md). ### 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 It sports high configurability through a (soon to be) extensive Lua API, with plans for a Rust API in the future.
- [x] Winit backend
- [x] Udev backend Showcase/gallery soon:tm:
- This is currently just a copy of Anvil's udev backend.
- [x] Basic tags ### 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 - [ ] Layout system
- [x] Left master stack, corner, dwindle, spiral layouts - [x] Left master stack, corner, dwindle, spiral layouts
- [ ] Other three master stack directions, floating, magnifier, maximized, and fullscreen layouts - [ ] Other three master stack directions, floating, magnifier, maximized, and fullscreen layouts
- [ ] Resizable layouts - [ ] Resizable layouts
- [x] XWayland support - [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 - [x] Layer-shell support
- [ ] wlr-screencopy support - [ ] wlr-screencopy support
- [ ] wlr-output-management support - [ ] wlr-output-management support
@ -34,21 +41,15 @@ See [`CHANGELOG.md`](CHANGELOG.md).
- [ ] The other stuff Awesome has - [ ] The other stuff Awesome has
- [x] Is very cool :thumbsup: - [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 ## 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): 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`. `libwayland libxkbcommon libudev libinput libgdm libseat`, as well as `xwayland`.
- Arch: - 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: - 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`. 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 ## Running
> :information_source: Before running, read the information in [Configuration](#configuration).
After building, run the executable located in either: After building, run the executable located in either:
```sh ```sh
./target/debug/pinnacle # without --release ./target/debug/pinnacle # without --release
@ -80,71 +83,61 @@ Or, run the project directly with
cargo run [--release] 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: `backend` can be one of two values:
- `winit`: run Pinnacle as a window in your graphical environment - `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 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.* 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 > #### :information_source: Make sure `command` in your `metaconfig.toml` is set to the right file.
> if there are too many windows on screen. If you don't want this to happen, use release mode. > 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
> #### :exclamation: IMPORTANT: Read the following before you launch the `udev` backend: `kill_keybind` (default <kbd>Ctrl</kbd><kbd>Alt</kbd><kbd>Shift</kbd> + <kbd>Esc</kbd>) and set `command` properly.
> 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: Pinnacle will open a socket in the `/tmp` directory. > #### :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 > If for whatever reason you need the socket to be in a different place, set `socket_dir` in
> the `SOCKET_DIR` environment variable: > your `metaconfig.toml` file to a directory of your choosing.
> ```sh
> SOCKET_DIR=/path/to/new/dir/ cargo run
> ```
> #### :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 > 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. of Pinnacle will fail when trying to remove the socket until it is removed manually.
## Configuration ## 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:). Pinnacle will search for a `metaconfig.toml` file in the following directories, from top to bottom:
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:
```sh ```sh
$XDG_CONFIG_HOME/pinnacle/init.lua $PINNACLE_CONFIG_DIR
~/.config/pinnacle/init.lua # if XDG_CONFIG_HOME isn't set $XDG_CONFIG_HOME/pinnacle/
``` ~/.config/pinnacle
The following will use the example config file in [`api/lua`](api/lua):
```sh
PINNACLE_CONFIG="./api/lua/example_config.lua" cargo run
``` ```
> #### :information_source: The config is an external process. The `metaconfig.toml` file provides information on what config to run, kill and reload keybinds,
> If it crashes for whatever reason, all of your keybinds will stop working. and any environment variables you want set. For more details, see the provided
> Again, you can switch ttys or exit the compositor with `Ctrl + Alt + Shift + Escape`. [`metaconfig.toml`](api/lua/metaconfig.toml) file.
>
> Config reloading soon:tm:
### API Documentation To use the provided Lua config, run the following in the root of the Git project:
There is a preliminary [doc website](https://ottatop.github.io/pinnacle/main) generated with LDoc. ```sh
Note that there are some missing things like the `Keys` table and `Layout` enum PINNACLE_CONFIG_DIR="./api/lua" cargo run
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>`. 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: > If you rename `example_config.lua` to something like `init.lua`, you will need to change `command` in `metaconfig.toml` to reflect that.
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 ### :information_source: Using the Lua Language Server
its doc comments to provide documentation, autocomplete, and error checking. 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: #### For VS Code:
Install the [Lua](https://marketplace.visualstudio.com/items?itemName=sumneko.lua) plugin, then go into 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 ## Controls
The following controls are currently hardcoded: The following controls are currently hardcoded:
- `Ctrl + Left Mouse`: Move a window - <kbd>Ctrl</kbd> + <kbd>Left click drag</kbd>: Move a window
- `Ctrl + Right Mouse`: Resize a window - <kbd>Ctrl</kbd> + <kbd>Right click drag</kbd>: 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:
You can find the rest of the controls in the [`example_config`](api/lua/example_config.lua). You can find the rest of the controls in the [`example_config`](api/lua/example_config.lua).
## Feature Requests, Bug Reports, Contributions, and Questions ## Feature Requests, Bug Reports, Contributions, and Questions
See [`CONTRIBUTING.md`](CONTRIBUTING.md). 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 socket = require("posix.sys.socket")
local msgpack = require("msgpack") local msgpack = require("msgpack")
local socket_dir = os.getenv("SOCKET_DIR") local SOCKET_PATH = os.getenv("PINNACLE_SOCKET") or "/tmp/pinnacle_socket"
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"
---From https://gist.github.com/stuby/5445834#file-rprint-lua ---From https://gist.github.com/stuby/5445834#file-rprint-lua
---rPrint(struct, [limit], [indent]) Recursively print arbitrary data. ---rPrint(struct, [limit], [indent]) Recursively print arbitrary data.

View file

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

View file

@ -6,7 +6,6 @@
#![allow(clippy::unwrap_used)] // I don't know what this stuff does yet #![allow(clippy::unwrap_used)] // I don't know what this stuff does yet
use std::{ use std::{
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
error::Error,
ffi::OsString, ffi::OsString,
os::fd::FromRawFd, os::fd::FromRawFd,
path::Path, 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 event_loop = EventLoop::try_new().unwrap();
let mut display = Display::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 * Bind all our objects that get driven by the event loop
*/ */
event_loop let insert_ret = event_loop
.handle() .handle()
.insert_source(libinput_backend, move |event, _, data| { .insert_source(libinput_backend, move |event, _, data| {
// println!("event: {:?}", event); // println!("event: {:?}", event);
data.state.process_input_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(); let handle = event_loop.handle();
event_loop event_loop

View file

@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
use std::{error::Error, ffi::OsString, time::Duration}; use std::{ffi::OsString, time::Duration};
use smithay::{ use smithay::{
backend::{ backend::{
@ -82,7 +82,7 @@ impl DmabufHandler for State<WinitData> {
delegate_dmabuf!(State<WinitData>); delegate_dmabuf!(State<WinitData>);
/// Start Pinnacle as a window in a graphical environment. /// 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 event_loop: EventLoop<CalloopData<WinitData>> = EventLoop::try_new()?;
let mut display: Display<State<WinitData>> = Display::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 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 { let mode = smithay::output::Mode {
size: winit_backend.window_size().physical_size, 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(); let mut pointer_element = PointerElement::<GlesTexture>::new();
// TODO: pointer let insert_ret =
state state
.loop_handle .loop_handle
.insert_source(Timer::immediate(), move |_instant, _metadata, data| { .insert_source(Timer::immediate(), move |_instant, _metadata, data| {
let display = &mut data.display; let display = &mut data.display;
let state = &mut data.state; let state = &mut data.state;
let result = winit_evt_loop.dispatch_new_events(|event| match event { let result = winit_evt_loop.dispatch_new_events(|event| match event {
WinitEvent::Resized { WinitEvent::Resized {
size, size,
scale_factor: _, scale_factor: _,
} => { } => {
output.change_current_state( output.change_current_state(
Some(smithay::output::Mode { Some(smithay::output::Mode {
size, size,
refresh: 144_000, refresh: 144_000,
}), }),
None, None,
None, None,
None, None,
); );
layer_map_for_output(&output).arrange(); layer_map_for_output(&output).arrange();
state.update_windows(&output); state.update_windows(&output);
// state.re_layout(&output); // state.re_layout(&output);
} }
WinitEvent::Focus(_) => {} WinitEvent::Focus(_) => {}
WinitEvent::Input(input_evt) => { WinitEvent::Input(input_evt) => {
state.process_input_event(input_evt); state.process_input_event(input_evt);
} }
WinitEvent::Refresh => {} WinitEvent::Refresh => {}
}); });
match result { match result {
Ok(_) => {} Ok(_) => {}
Err(WinitError::WindowClosed) => { Err(WinitError::WindowClosed) => {
state.loop_signal.stop(); 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)
}; };
let renderer = state.backend_data.backend.renderer(); if let CursorImageStatus::Surface(surface) = &state.cursor_status {
if !surface.alive() {
// render_output() state.cursor_status = CursorImageStatus::Default;
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);
}
} }
}
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 state
.backend_data .backend_data
.backend .damage_tracker
.window() .render_output(renderer, age, &output_render_elements, [0.5, 0.5, 0.5, 1.0])
.set_cursor_visible(cursor_visible); .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 state
if let CursorImageStatus::Surface(surf) = &state.cursor_status { .backend_data
if let Some(op) = state.focus_state.focused_output.as_ref() { .backend
send_frames_surface_tree( .window()
surf, .set_cursor_visible(cursor_visible);
op,
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, time,
Some(Duration::ZERO), output
|_, _| None, .current_mode()
.map(|mode| mode.refresh as u32)
.unwrap_or_default(),
0,
wp_presentation_feedback::Kind::Vsync,
); );
} }
} }
Err(err) => {
super::post_repaint( tracing::warn!("{}", err);
&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);
}
}
state.space.refresh(); state.space.refresh();
state.popup_manager.cleanup(); state.popup_manager.cleanup();
display display
.flush_clients() .flush_clients()
.expect("failed to flush client buffers"); .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( event_loop.run(
Some(Duration::from_millis(1)), Some(Duration::from_millis(1)),

View file

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

View file

@ -19,6 +19,7 @@ mod grab;
mod handlers; mod handlers;
mod input; mod input;
mod layout; mod layout;
mod metaconfig;
mod output; mod output;
mod pointer; mod pointer;
mod render; mod render;
@ -26,9 +27,7 @@ mod state;
mod tag; mod tag;
mod window; mod window;
use std::error::Error; fn main() -> anyhow::Result<()> {
fn main() -> Result<(), Box<dyn Error>> {
match tracing_subscriber::EnvFilter::try_from_default_env() { match tracing_subscriber::EnvFilter::try_from_default_env() {
Ok(env_filter) => { Ok(env_filter) => {
tracing_subscriber::fmt() 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 // SPDX-License-Identifier: GPL-3.0-or-later
mod api_handlers;
use std::{ use std::{
cell::RefCell, cell::RefCell,
error::Error,
ffi::OsString,
os::{fd::AsRawFd, unix::net::UnixStream}, os::{fd::AsRawFd, unix::net::UnixStream},
path::PathBuf, path::{Path, PathBuf},
process::Stdio,
sync::{Arc, Mutex}, sync::{Arc, Mutex},
time::Duration, time::Duration,
}; };
use crate::{ use crate::{
api::{ api::{
msg::{Args, CallbackId, Msg, OutgoingMsg, Request, RequestId, RequestResponse}, msg::{CallbackId, ModifierMask, Msg},
PinnacleSocketSource, PinnacleSocketSource, DEFAULT_SOCKET_DIR,
}, },
cursor::Cursor, cursor::Cursor,
focus::FocusState, focus::FocusState,
grab::resize_grab::ResizeSurfaceState, grab::resize_grab::ResizeSurfaceState,
tag::Tag, metaconfig::Metaconfig,
tag::TagId,
window::{window_state::LocationRequestState, WindowElement}, window::{window_state::LocationRequestState, WindowElement},
}; };
use anyhow::Context;
use calloop::futures::Scheduler; use calloop::futures::Scheduler;
use futures_lite::AsyncBufReadExt;
use smithay::{ use smithay::{
backend::renderer::element::RenderElementStates, backend::renderer::element::RenderElementStates,
desktop::{ desktop::{
space::SpaceElement,
utils::{ utils::{
surface_presentation_feedback_flags_from_states, surface_primary_scanout_output, surface_presentation_feedback_flags_from_states, surface_primary_scanout_output,
OutputPresentationFeedback, OutputPresentationFeedback,
@ -55,10 +54,7 @@ use smithay::{
fractional_scale::FractionalScaleManagerState, fractional_scale::FractionalScaleManagerState,
output::OutputManagerState, output::OutputManagerState,
primary_selection::PrimarySelectionState, primary_selection::PrimarySelectionState,
shell::{ shell::{wlr_layer::WlrLayerShellState, xdg::XdgShellState},
wlr_layer::WlrLayerShellState,
xdg::{XdgShellState, XdgToplevelSurfaceData},
},
shm::ShmState, shm::ShmState,
socket::ListeningSocketSource, socket::ListeningSocketSource,
viewporter::ViewporterState, viewporter::ViewporterState,
@ -107,6 +103,7 @@ pub struct State<B: Backend> {
pub windows: Vec<WindowElement>, pub windows: Vec<WindowElement>,
pub async_scheduler: Scheduler<()>, pub async_scheduler: Scheduler<()>,
pub config_process: async_process::Child,
// TODO: move into own struct // TODO: move into own struct
// | basically just clean this mess up // | basically just clean this mess up
@ -117,559 +114,6 @@ pub struct State<B: Backend> {
pub xdisplay: Option<u32>, 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 /// Schedule something to be done when windows have finished committing and have become
/// idle. /// idle.
pub fn schedule_on_commit<F, B: Backend>( pub fn schedule_on_commit<F, B: Backend>(
@ -718,7 +162,7 @@ impl<B: Backend> State<B> {
display: &mut Display<Self>, display: &mut Display<Self>,
loop_signal: LoopSignal, loop_signal: LoopSignal,
loop_handle: LoopHandle<'static, CalloopData<B>>, loop_handle: LoopHandle<'static, CalloopData<B>>,
) -> Result<Self, Box<dyn Error>> { ) -> anyhow::Result<Self> {
let socket = ListeningSocketSource::new_auto()?; let socket = ListeningSocketSource::new_auto()?;
let socket_name = socket.socket_name().to_os_string(); 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>(); let (tx_channel, rx_channel) = calloop::channel::channel::<Msg>();
// We want to replace the client if a new one pops up let config_dir = get_config_dir();
// 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
// TODO: probably use anyhow or something let metaconfig = crate::metaconfig::parse(&config_dir)?;
let socket_source = match PinnacleSocketSource::new(tx_channel) {
Ok(source) => source, let socket_dir = {
Err(err) => { let dir_string = shellexpand::full(
tracing::error!("Failed to create the socket source: {err}"); metaconfig
Err(err)? .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 if let Some(old_stream) = data
.state .state
.api_state .api_state
@ -793,13 +256,17 @@ impl<B: Backend> State<B> {
.shutdown(std::net::Shutdown::Both) .shutdown(std::net::Shutdown::Both)
.expect("Couldn't shutdown old stream"); .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) = let (executor, sched) =
calloop::futures::executor::<()>().expect("Couldn't create executor"); calloop::futures::executor::<()>().expect("Couldn't create executor");
loop_handle.insert_source(executor, |_, _, _| {})?; if let Err(err) = loop_handle.insert_source(executor, |_, _, _| {}) {
anyhow::bail!("Failed to insert async executor into event loop: {err}");
start_lua_config()?; }
let display_handle = display.handle(); let display_handle = display.handle();
let mut seat_state = SeatState::new(); let mut seat_state = SeatState::new();
@ -882,7 +349,7 @@ impl<B: Backend> State<B> {
primary_selection_state: PrimarySelectionState::new::<Self>(&display_handle), primary_selection_state: PrimarySelectionState::new::<Self>(&display_handle),
layer_shell_state: WlrLayerShellState::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(), api_state: ApiState::new(),
focus_state: FocusState::new(), focus_state: FocusState::new(),
@ -896,6 +363,7 @@ impl<B: Backend> State<B> {
popup_manager: PopupManager::default(), popup_manager: PopupManager::default(),
async_scheduler: sched, async_scheduler: sched,
config_process: config_child_handle,
windows: vec![], windows: vec![],
output_callback_ids: vec![], output_callback_ids: vec![],
@ -907,60 +375,114 @@ impl<B: Backend> State<B> {
} }
} }
fn start_lua_config() -> Result<(), Box<dyn std::error::Error>> { fn get_config_dir() -> PathBuf {
// TODO: move all this into the lua api let config_dir = std::env::var("PINNACLE_CONFIG_DIR").unwrap_or_else(|_| {
let config_path = std::env::var("PINNACLE_CONFIG") let default_config_dir =
.map(PathBuf::from) std::env::var("XDG_CONFIG_HOME").unwrap_or("~/.config".to_string());
.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
});
let config_path = { PathBuf::from(default_config_dir)
let path = shellexpand::tilde(&config_path.to_string_lossy().to_string()).to_string(); .join("pinnacle")
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")
.to_string_lossy() .to_string_lossy()
.to_string(); .to_string()
local_lua_path.push_str("/api/lua"); // TODO: get from crate root and do dynamically });
let new_lua_path = PathBuf::from(shellexpand::tilde(&config_dir).to_string())
format!("{local_lua_path}/?.lua;{local_lua_path}/?/init.lua;{local_lua_path}/lib/?.lua;{local_lua_path}/lib/?/init.lua;{lua_path}"); }
let lua_cpath = std::env::var("LUA_CPATH").unwrap_or_else(|_| { /// This should be called *after* you have created the [`PinnacleSocketSource`] to ensure
tracing::info!("LUA_CPATH was not set, using empty string"); /// PINNACLE_SOCKET is set correctly for use in API implementations.
"".to_string() fn start_config(metaconfig: Metaconfig, config_dir: &Path) -> anyhow::Result<ConfigReturn> {
}); let reload_keybind = metaconfig.reload_keybind;
let new_lua_cpath = format!("{local_lua_path}/lib/?.so;{lua_cpath}"); let kill_keybind = metaconfig.kill_keybind;
if let Err(err) = std::process::Command::new("lua") let mut command = metaconfig.command.split(' ');
.arg(config_path)
.env("LUA_PATH", new_lua_path) let arg1 = command.next().expect("empty command");
.env("LUA_CPATH", new_lua_cpath)
.spawn() std::env::set_var("PINNACLE_DIR", std::env::current_dir()?);
{
tracing::error!("Failed to start Lua: {err}"); let envs = metaconfig
return Err(err)?; .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(()) 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())) .flat_map(|op| op.with_state(|state| state.tags.clone()))
.find(|tag| &tag.id() == self) .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)] #[derive(Debug)]