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)]