diff --git a/Cargo.toml b/Cargo.toml index ee21577..d75bb6b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] diff --git a/README.md b/README.md index 0e1086e..b014b44 100644 --- a/README.md +++ b/README.md @@ -11,20 +11,27 @@ A very, VERY WIP Smithay-based wayland compositor -## 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. flake soon:tm: +flake soon:tm: ## 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: `--`. 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: `--`. 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 CtrlAltShift + Esc) 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/`. +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/`. + ## 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: +- Ctrl + Left click drag: Move a window +- Ctrl + Right click drag: 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). diff --git a/api/lua/metaconfig.toml b/api/lua/metaconfig.toml new file mode 100644 index 0000000..e46f3dc --- /dev/null +++ b/api/lua/metaconfig.toml @@ -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" diff --git a/api/lua/pinnacle.lua b/api/lua/pinnacle.lua index e6095b7..441114c 100644 --- a/api/lua/pinnacle.lua +++ b/api/lua/pinnacle.lua @@ -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. diff --git a/src/api.rs b/src/api.rs index b13e4fb..e1d8dd6 100644 --- a/src/api.rs +++ b/src/api.rs @@ -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) -> Result { - 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, socket_dir: &Path) -> anyhow::Result { + 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 }) } } diff --git a/src/backend/udev.rs b/src/backend/udev.rs index b263e0c..7a9c583 100644 --- a/src/backend/udev.rs +++ b/src/backend/udev.rs @@ -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> { +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> { /* * 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 diff --git a/src/backend/winit.rs b/src/backend/winit.rs index b9ba6f2..32d74da 100644 --- a/src/backend/winit.rs +++ b/src/backend/winit.rs @@ -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 { delegate_dmabuf!(State); /// Start Pinnacle as a window in a graphical environment. -pub fn run_winit() -> Result<(), Box> { +pub fn run_winit() -> anyhow::Result<()> { let mut event_loop: EventLoop> = EventLoop::try_new()?; let mut display: Display> = Display::new()?; @@ -90,7 +90,11 @@ pub fn run_winit() -> Result<(), Box> { let evt_loop_handle = event_loop.handle(); - let (mut winit_backend, mut winit_evt_loop) = smithay::backend::winit::init::()?; + let (mut winit_backend, mut winit_evt_loop) = + match smithay::backend::winit::init::() { + 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> { let mut pointer_element = PointerElement::::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)), diff --git a/src/input.rs b/src/input.rs index 5290464..0737adb 100644 --- a/src/input.rs +++ b/src/input.rs @@ -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 State { @@ -113,6 +120,10 @@ impl State { 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 State { 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 State { Some(KeyAction::Quit) => { self.loop_signal.stop(); } - _ => {} + Some(KeyAction::ReloadConfig) => { + self.restart_config().expect("failed to restart config"); + } + None => {} } } diff --git a/src/layout.rs b/src/layout.rs index 9cfa9f6..88758ff 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -59,7 +59,7 @@ impl State { 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::>(); self.windows.iter().cloned().partition(|win| { @@ -67,6 +67,8 @@ impl State { }) }); + windows_not_on_foc_tags.retain(|win| win.output(self) == Some(output.clone())); + let tiled_windows = windows_on_foc_tags .iter() .filter(|win| { diff --git a/src/main.rs b/src/main.rs index 257cdc4..c537c44 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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> { +fn main() -> anyhow::Result<()> { match tracing_subscriber::EnvFilter::try_from_default_env() { Ok(env_filter) => { tracing_subscriber::fmt() diff --git a/src/metaconfig.rs b/src/metaconfig.rs new file mode 100644 index 0000000..0e698a4 --- /dev/null +++ b/src/metaconfig.rs @@ -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, + pub reload_keybind: Keybind, + pub kill_keybind: Keybind, + pub socket_dir: Option, +} + +#[derive(serde::Deserialize, Debug)] +pub struct Keybind { + pub modifiers: Vec, + 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 { + 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") +} diff --git a/src/state.rs b/src/state.rs index 1dc9f4c..f141237 100644 --- a/src/state.rs +++ b/src/state.rs @@ -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 { pub windows: Vec, 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 { pub xdisplay: Option, } -impl State { - 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::>(); - - // 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::() - .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::>(); - 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::>() - }) - }); - 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::>(); - 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, callback_id: Option) { - 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( @@ -718,7 +162,7 @@ impl State { display: &mut Display, loop_signal: LoopSignal, loop_handle: LoopHandle<'static, CalloopData>, - ) -> Result> { + ) -> anyhow::Result { let socket = ListeningSocketSource::new_auto()?; let socket_name = socket.socket_name().to_os_string(); @@ -766,21 +210,40 @@ impl State { let (tx_channel, rx_channel) = calloop::channel::channel::(); - // 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 State { .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 State { primary_selection_state: PrimarySelectionState::new::(&display_handle), layer_shell_state: WlrLayerShellState::new::(&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 State { popup_manager: PopupManager::default(), async_scheduler: sched, + config_process: config_child_handle, windows: vec![], output_callback_ids: vec![], @@ -907,60 +375,114 @@ impl State { } } -fn start_lua_config() -> Result<(), Box> { - // 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 { + 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::>(); + + 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 State { + 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", - ))? } } diff --git a/src/state/api_handlers.rs b/src/state/api_handlers.rs new file mode 100644 index 0000000..8f16eee --- /dev/null +++ b/src/state/api_handlers.rs @@ -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 State { + 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::>(); + 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::>(); + + // 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::() + .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::>(); + 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::>() + }) + }); + 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::>(); + 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, callback_id: Option) { + 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}"); + } + } + } +} diff --git a/src/tag.rs b/src/tag.rs index 08893d0..f592eb7 100644 --- a/src/tag.rs +++ b/src/tag.rs @@ -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)]