mirror of
https://github.com/pinnacle-comp/pinnacle.git
synced 2025-01-29 20:34:46 +01:00
Merge pull request #56 from Ottatop/config_reload
Add metaconfig and config reloading
This commit is contained in:
commit
43949e386d
14 changed files with 1174 additions and 904 deletions
|
@ -23,6 +23,8 @@ async-process = { version = "1.7.0" }
|
|||
itertools = { version = "0.11.0" }
|
||||
x11rb = { version = "0.12.0", default-features = false, features = ["composite"], optional = true }
|
||||
shellexpand = "3.1.0"
|
||||
toml = "0.7.6"
|
||||
anyhow = { version = "1.0.74", features = ["backtrace"] }
|
||||
|
||||
[features]
|
||||
default = ["egl", "winit", "udev", "xwayland"]
|
||||
|
|
132
README.md
132
README.md
|
@ -11,20 +11,27 @@
|
|||
A very, VERY WIP Smithay-based wayland compositor
|
||||
</div>
|
||||
|
||||
## Changelog
|
||||
See [`CHANGELOG.md`](CHANGELOG.md).
|
||||
## Info
|
||||
### What is Pinnacle?
|
||||
Pinnacle is a Wayland compositor built in Rust using [Smithay](https://github.com/Smithay/smithay).
|
||||
It's my attempt at creating something like [AwesomeWM](https://github.com/awesomeWM/awesome)
|
||||
for Wayland.
|
||||
|
||||
## Features
|
||||
- [x] Winit backend
|
||||
- [x] Udev backend
|
||||
- This is currently just a copy of Anvil's udev backend.
|
||||
- [x] Basic tags
|
||||
It sports high configurability through a (soon to be) extensive Lua API, with plans for a Rust API in the future.
|
||||
|
||||
Showcase/gallery soon:tm:
|
||||
|
||||
### Features
|
||||
> This is a non-exhaustive list.
|
||||
- [x] Winit backend (so you can run Pinnacle in your graphical environment)
|
||||
- [x] Udev backend (so you can run Pinnacle in a tty)
|
||||
- [x] Tag system
|
||||
- [ ] Layout system
|
||||
- [x] Left master stack, corner, dwindle, spiral layouts
|
||||
- [ ] Other three master stack directions, floating, magnifier, maximized, and fullscreen layouts
|
||||
- [ ] Resizable layouts
|
||||
- [x] XWayland support
|
||||
- This is currently somewhat buggy. If you find a problem that's not already listed in GitHub issues, feel free to submit it!
|
||||
- This is currently somewhat buggy. If you find a problem, please submit an issue!
|
||||
- [x] Layer-shell support
|
||||
- [ ] wlr-screencopy support
|
||||
- [ ] wlr-output-management support
|
||||
|
@ -34,21 +41,15 @@ See [`CHANGELOG.md`](CHANGELOG.md).
|
|||
- [ ] The other stuff Awesome has
|
||||
- [x] Is very cool :thumbsup:
|
||||
|
||||
## Info
|
||||
### Why Pinnacle?
|
||||
Well, I currently use [Awesome](https://github.com/awesomeWM/awesome). And I really like it! Unfortunately, Awesome doesn't exist for Wayland ([anymore](http://way-cooler.org/blog/2020/01/09/way-cooler-post-mortem.html)). There doesn't seem to be any Wayland compositor out there that has all of the following:
|
||||
- Tags for window management
|
||||
- Configurable in Lua (or any other programming language for that matter)
|
||||
- Has a bunch of batteries included (widget system, systray, etc)
|
||||
|
||||
So, this is my attempt at making an Awesome-esque Wayland compositor.
|
||||
|
||||
## Dependencies
|
||||
> I have not tested these. If Pinnacle doesn't work properly with these packages installed, please submit an issue.
|
||||
|
||||
You'll need the following packages, as specified by [Smithay](https://github.com/Smithay/smithay):
|
||||
`libwayland libxkbcommon libudev libinput libgdm libseat`, as well as `xwayland`.
|
||||
- Arch:
|
||||
```
|
||||
sudo pacman -S wayland wayland-protocols libxkbcommon systemd-libs libinput mesa seatda xwayland
|
||||
sudo pacman -S wayland wayland-protocols libxkbcommon systemd-libs libinput mesa seatd xwayland
|
||||
```
|
||||
- Debian:
|
||||
```
|
||||
|
@ -66,9 +67,11 @@ cargo build [--release]
|
|||
```
|
||||
|
||||
For NixOS users, there is a provided [`shell.nix`](shell.nix) file that you can use for `nix-shell`.
|
||||
It *should* work, but if it doesn't, please raise an issue. <sup>flake soon:tm:</sup>
|
||||
<sup>flake soon:tm:</sup>
|
||||
|
||||
## Running
|
||||
> :information_source: Before running, read the information in [Configuration](#configuration).
|
||||
|
||||
After building, run the executable located in either:
|
||||
```sh
|
||||
./target/debug/pinnacle # without --release
|
||||
|
@ -80,71 +83,61 @@ Or, run the project directly with
|
|||
cargo run [--release]
|
||||
```
|
||||
|
||||
There is an additional flag you can pass in: `--<backend>`. You most likely do not need to use it.
|
||||
|
||||
Pinnacle will automatically initialize the correct backend for your environment.
|
||||
|
||||
However, there is an additional flag you can pass in: `--<backend>`. You most likely do not need to use it.
|
||||
|
||||
`backend` can be one of two values:
|
||||
|
||||
- `winit`: run Pinnacle as a window in your graphical environment
|
||||
- `udev`: run Pinnacle in a tty. NOTE: I tried running udev in Awesome and some things broke so uh, don't do that
|
||||
- `udev`: run Pinnacle in a tty.
|
||||
|
||||
If you try to run either in environments where you shouldn't be, you will get a warning requiring you to
|
||||
pass in the `--force` flag to continue. *You probably shouldn't be doing that.*
|
||||
|
||||
> :information_source: When running in debug mode, the compositor will drastically slow down
|
||||
> if there are too many windows on screen. If you don't want this to happen, use release mode.
|
||||
|
||||
> #### :exclamation: IMPORTANT: Read the following before you launch the `udev` backend:
|
||||
> If you successfully enter the `udev` backend but none of the controls work, this means either Pinnacle
|
||||
failed to find your config, or the config process crashed.
|
||||
>
|
||||
> You can either switch ttys or press
|
||||
> `Ctrl + Alt + Shift + Escape`,
|
||||
> which has been hardcoded in to kill the compositor.
|
||||
> #### :information_source: Make sure `command` in your `metaconfig.toml` is set to the right file.
|
||||
> If it isn't, the compositor will open, but your config will not apply.
|
||||
In that case, kill the compositor using the keybind defined in
|
||||
`kill_keybind` (default <kbd>Ctrl</kbd><kbd>Alt</kbd><kbd>Shift</kbd> + <kbd>Esc</kbd>) and set `command` properly.
|
||||
|
||||
> #### :information_source: Pinnacle will open a socket in the `/tmp` directory.
|
||||
> If for whatever reason you need the socket to be in a different place, run Pinnacle with
|
||||
> the `SOCKET_DIR` environment variable:
|
||||
> ```sh
|
||||
> SOCKET_DIR=/path/to/new/dir/ cargo run
|
||||
> ```
|
||||
> If for whatever reason you need the socket to be in a different place, set `socket_dir` in
|
||||
> your `metaconfig.toml` file to a directory of your choosing.
|
||||
|
||||
> #### :warning: Don't run Pinnacle as root.
|
||||
> #### :warning: Do not run Pinnacle as root.
|
||||
> This will open the socket with root-only permissions, and future non-root invocations
|
||||
of Pinnacle will fail when trying to remove the socket until it is removed manually.
|
||||
|
||||
## Configuration
|
||||
Please note: this is WIP and has few options.
|
||||
Pinnacle is configured in Lua. Rust support is planned.
|
||||
|
||||
Pinnacle supports configuration through Lua (and hopefully more languages if it's not too unwieldy :crab:).
|
||||
|
||||
Run Pinnacle with the `PINNACLE_CONFIG` environment variable set to the path of your config file.
|
||||
If not specified, Pinnacle will look for the following:
|
||||
Pinnacle will search for a `metaconfig.toml` file in the following directories, from top to bottom:
|
||||
```sh
|
||||
$XDG_CONFIG_HOME/pinnacle/init.lua
|
||||
~/.config/pinnacle/init.lua # if XDG_CONFIG_HOME isn't set
|
||||
```
|
||||
The following will use the example config file in [`api/lua`](api/lua):
|
||||
```sh
|
||||
PINNACLE_CONFIG="./api/lua/example_config.lua" cargo run
|
||||
$PINNACLE_CONFIG_DIR
|
||||
$XDG_CONFIG_HOME/pinnacle/
|
||||
~/.config/pinnacle
|
||||
```
|
||||
|
||||
> #### :information_source: The config is an external process.
|
||||
> If it crashes for whatever reason, all of your keybinds will stop working.
|
||||
> Again, you can switch ttys or exit the compositor with `Ctrl + Alt + Shift + Escape`.
|
||||
>
|
||||
> Config reloading soon:tm:
|
||||
The `metaconfig.toml` file provides information on what config to run, kill and reload keybinds,
|
||||
and any environment variables you want set. For more details, see the provided
|
||||
[`metaconfig.toml`](api/lua/metaconfig.toml) file.
|
||||
|
||||
### API Documentation
|
||||
There is a preliminary [doc website](https://ottatop.github.io/pinnacle/main) generated with LDoc.
|
||||
Note that there are some missing things like the `Keys` table and `Layout` enum
|
||||
as well as any function overloads, but these should be autocompleted through the language server.
|
||||
To use the provided Lua config, run the following in the root of the Git project:
|
||||
```sh
|
||||
PINNACLE_CONFIG_DIR="./api/lua" cargo run
|
||||
```
|
||||
|
||||
Documentation for other branches can be reached at `https://ottatop.github.io/pinnacle/<branch name>`.
|
||||
To run without the above environment variable, copy [`metaconfig.toml`](api/lua/metaconfig.toml) and
|
||||
[`example_config.lua`](api/lua/example_config.lua) to `$XDG_CONFIG_HOME/pinnacle/`
|
||||
(this will probably be `~/.config/pinnacle`).
|
||||
|
||||
### :information_source: Using the Lua Language Server :information_source:
|
||||
It is *highly* recommended to use the [Lua language server](https://github.com/LuaLS/lua-language-server)
|
||||
and set it up to have the [`api/lua`](api/lua) directory as a library, as I'll be using
|
||||
its doc comments to provide documentation, autocomplete, and error checking.
|
||||
> If you rename `example_config.lua` to something like `init.lua`, you will need to change `command` in `metaconfig.toml` to reflect that.
|
||||
|
||||
### :information_source: Using the Lua Language Server
|
||||
It is ***highly*** recommended to use the [Lua language server](https://github.com/LuaLS/lua-language-server)
|
||||
and set it up to have the [`api/lua`](api/lua) directory as a library.
|
||||
This will provide documentation, autocomplete, and error checking.
|
||||
|
||||
#### For VS Code:
|
||||
Install the [Lua](https://marketplace.visualstudio.com/items?itemName=sumneko.lua) plugin, then go into
|
||||
|
@ -162,15 +155,24 @@ Lua = {
|
|||
}
|
||||
```
|
||||
|
||||
### API Documentation
|
||||
You can find online documentation for the Lua API [here](https://ottatop.github.io/pinnacle/main).
|
||||
|
||||
Note that there are some missing things like the `Keys` table and `Layout` enum
|
||||
as well as any function overloads, but these should be autocompleted through the language server.
|
||||
|
||||
Documentation for other branches can be reached at `https://ottatop.github.io/pinnacle/<branch name>`.
|
||||
|
||||
## Controls
|
||||
The following controls are currently hardcoded:
|
||||
|
||||
- `Ctrl + Left Mouse`: Move a window
|
||||
- `Ctrl + Right Mouse`: Resize a window
|
||||
- `Ctrl + Alt + Shift + Esc`: Kill Pinnacle. This is for when the compositor inevitably
|
||||
locks up because I did a dumb thing :thumbsup:
|
||||
- <kbd>Ctrl</kbd> + <kbd>Left click drag</kbd>: Move a window
|
||||
- <kbd>Ctrl</kbd> + <kbd>Right click drag</kbd>: Resize a window
|
||||
|
||||
You can find the rest of the controls in the [`example_config`](api/lua/example_config.lua).
|
||||
|
||||
## Feature Requests, Bug Reports, Contributions, and Questions
|
||||
See [`CONTRIBUTING.md`](CONTRIBUTING.md).
|
||||
|
||||
## Changelog
|
||||
See [`CHANGELOG.md`](CHANGELOG.md).
|
||||
|
|
53
api/lua/metaconfig.toml
Normal file
53
api/lua/metaconfig.toml
Normal file
|
@ -0,0 +1,53 @@
|
|||
# This metaconfig.toml file dictates what config Pinnacle will run.
|
||||
#
|
||||
# When running Pinnacle, the compositor will look in the following directories for a metaconfig.toml file,
|
||||
# in order from top to bottom:
|
||||
# $PINNACLE_CONFIG_DIR
|
||||
# $XDG_CONFIG_HOME/pinnacle/
|
||||
# ~/.config/pinnacle/
|
||||
#
|
||||
# When Pinnacle finds a metaconfig.toml file, it will execute the command provided to `command`.
|
||||
# For now, the only thing that should be here is `lua` with a path to the main config file.
|
||||
# In the future, there will be a Rust API that can be run using `cargo run`.
|
||||
#
|
||||
# Because configuration is done using an external process, if it ever crashes, you lose all of your keybinds.
|
||||
# In order prevent you from getting stuck in the compositor, you must define keybinds to reload your config
|
||||
# and kill Pinnacle.
|
||||
#
|
||||
# More details on each setting can be found below.
|
||||
|
||||
# The command Pinnacle will run on startup and when you reload your config.
|
||||
# Paths are relative to the directory the metaconfig.toml file is in.
|
||||
command = "lua example_config.lua"
|
||||
|
||||
### Keybinds ###
|
||||
# Each keybind takes in a table with two fields: `modifiers` and `key`.
|
||||
# - `modifiers` can be one of "Ctrl", "Alt", "Shift", or "Super".
|
||||
# - `key` can be a string of any lowercase letter, number,
|
||||
# "numN" where N is a number for numpad keys, or "esc"/"escape".
|
||||
# Support for any xkbcommon key is planned for a future update.
|
||||
|
||||
# The keybind that will reload your config.
|
||||
reload_keybind = { modifiers = ["Ctrl", "Alt"], key = "r" }
|
||||
# The keybind that will kill Pinnacle.
|
||||
kill_keybind = { modifiers = ["Ctrl", "Alt", "Shift"], key = "escape" }
|
||||
|
||||
### Socket directory ###
|
||||
# Pinnacle will open a Unix socket at `/tmp` by default. If you want/need to change this,
|
||||
# use the `socket_dir` setting set to the directory of your choosing.
|
||||
#
|
||||
# Changing this between reloads will not change the actual location of the socket
|
||||
# until you restart Pinnacle.
|
||||
# socket_dir = "/your/dir/here/"
|
||||
|
||||
### Environment Variables ###
|
||||
# You may need to specify to Lua where Pinnacle's Lua API library is.
|
||||
# This is currently done using the `envs` table, with keys as the name of the environment variable and
|
||||
# the value as the variable value. This supports $var expansion, and paths are relative to this metaconfig.toml file.
|
||||
#
|
||||
# Pinnacle will run your config with the additional PINNACLE_DIR environment variable.
|
||||
#
|
||||
# Here, LUA_PATH and LUA_CPATH are used to tell Lua the path to the library.
|
||||
[envs]
|
||||
LUA_PATH = "$PINNACLE_DIR/api/lua/?.lua;$PINNACLE_DIR/api/lua/?/init.lua;$PINNACLE_DIR/api/lua/lib/?.lua;$PINNACLE_DIR/api/lua/lib/?/init.lua;$LUA_PATH"
|
||||
LUA_CPATH = "$PINNACLE_DIR/api/lua/lib/?.so;$LUA_CPATH"
|
|
@ -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.
|
||||
|
|
59
src/api.rs
59
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<Msg>) -> Result<Self, io::Error> {
|
||||
let socket_path = std::env::var("SOCKET_DIR").unwrap_or(DEFAULT_SOCKET_DIR.to_string());
|
||||
let socket_path = Path::new(&socket_path);
|
||||
if !socket_path.is_dir() {
|
||||
tracing::error!("SOCKET_DIR must be a directory");
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"SOCKET_DIR must be a directory",
|
||||
));
|
||||
}
|
||||
|
||||
let Some(socket_path) = socket_path
|
||||
.join("pinnacle_socket")
|
||||
.to_str()
|
||||
.map(|st| st.to_string())
|
||||
else {
|
||||
tracing::error!("Socket path {socket_path:?} had invalid Unicode");
|
||||
return Err(io::Error::new(io::ErrorKind::Other, "socket path had invalid unicode"));
|
||||
};
|
||||
|
||||
let socket_path = shellexpand::tilde(&socket_path).to_string();
|
||||
let socket_path = Path::new(&socket_path);
|
||||
|
||||
// TODO: use anyhow
|
||||
/// Create a loop source that listens for connections to the provided socket_dir.
|
||||
/// This will also set PINNACLE_SOCKET for use in API implementations.
|
||||
pub fn new(sender: Sender<Msg>, socket_dir: &Path) -> anyhow::Result<Self> {
|
||||
let socket_path = socket_dir.join("pinnacle_socket");
|
||||
|
||||
if let Ok(exists) = socket_path.try_exists() {
|
||||
if exists {
|
||||
if let Err(err) = std::fs::remove_file(socket_path) {
|
||||
tracing::error!("Failed to remove old socket: {err}");
|
||||
return Err(err);
|
||||
}
|
||||
std::fs::remove_file(&socket_path).context("Failed to remove old socket")?;
|
||||
}
|
||||
}
|
||||
|
||||
let listener = match UnixListener::bind(socket_path) {
|
||||
Ok(listener) => {
|
||||
tracing::info!("Bound to socket at {socket_path:?}");
|
||||
listener
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("Failed to bind to socket: {err}");
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
if let Err(err) = listener.set_nonblocking(true) {
|
||||
tracing::error!("Failed to set socket to nonblocking: {err}");
|
||||
return Err(err);
|
||||
}
|
||||
let listener = UnixListener::bind(&socket_path)
|
||||
.with_context(|| format!("Failed to bind to socket at {socket_path:?}"))?;
|
||||
tracing::info!("Bound to socket at {socket_path:?}");
|
||||
|
||||
listener
|
||||
.set_nonblocking(true)
|
||||
.context("Failed to set socket to nonblocking")?;
|
||||
|
||||
let socket = Generic::new(listener, Interest::READ, Mode::Level);
|
||||
|
||||
std::env::set_var("PINNACLE_SOCKET", socket_path);
|
||||
|
||||
Ok(Self { socket, sender })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
#![allow(clippy::unwrap_used)] // I don't know what this stuff does yet
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
error::Error,
|
||||
ffi::OsString,
|
||||
os::fd::FromRawFd,
|
||||
path::Path,
|
||||
|
@ -172,7 +171,7 @@ impl Backend for UdevData {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn run_udev() -> Result<(), Box<dyn Error>> {
|
||||
pub fn run_udev() -> anyhow::Result<()> {
|
||||
let mut event_loop = EventLoop::try_new().unwrap();
|
||||
let mut display = Display::new().unwrap();
|
||||
|
||||
|
@ -281,12 +280,16 @@ pub fn run_udev() -> Result<(), Box<dyn Error>> {
|
|||
/*
|
||||
* Bind all our objects that get driven by the event loop
|
||||
*/
|
||||
event_loop
|
||||
let insert_ret = event_loop
|
||||
.handle()
|
||||
.insert_source(libinput_backend, move |event, _, data| {
|
||||
// println!("event: {:?}", event);
|
||||
data.state.process_input_event(event);
|
||||
})?;
|
||||
});
|
||||
|
||||
if let Err(err) = insert_ret {
|
||||
anyhow::bail!("Failed to insert libinput_backend into event loop: {err}");
|
||||
}
|
||||
|
||||
let handle = event_loop.handle();
|
||||
event_loop
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
use std::{error::Error, ffi::OsString, time::Duration};
|
||||
use std::{ffi::OsString, time::Duration};
|
||||
|
||||
use smithay::{
|
||||
backend::{
|
||||
|
@ -82,7 +82,7 @@ impl DmabufHandler for State<WinitData> {
|
|||
delegate_dmabuf!(State<WinitData>);
|
||||
|
||||
/// Start Pinnacle as a window in a graphical environment.
|
||||
pub fn run_winit() -> Result<(), Box<dyn Error>> {
|
||||
pub fn run_winit() -> anyhow::Result<()> {
|
||||
let mut event_loop: EventLoop<CalloopData<WinitData>> = EventLoop::try_new()?;
|
||||
|
||||
let mut display: Display<State<WinitData>> = Display::new()?;
|
||||
|
@ -90,7 +90,11 @@ pub fn run_winit() -> Result<(), Box<dyn Error>> {
|
|||
|
||||
let evt_loop_handle = event_loop.handle();
|
||||
|
||||
let (mut winit_backend, mut winit_evt_loop) = smithay::backend::winit::init::<GlesRenderer>()?;
|
||||
let (mut winit_backend, mut winit_evt_loop) =
|
||||
match smithay::backend::winit::init::<GlesRenderer>() {
|
||||
Ok(ret) => ret,
|
||||
Err(err) => anyhow::bail!("Failed to init winit backend: {err}"),
|
||||
};
|
||||
|
||||
let mode = smithay::output::Mode {
|
||||
size: winit_backend.window_size().physical_size,
|
||||
|
@ -205,160 +209,164 @@ pub fn run_winit() -> Result<(), Box<dyn Error>> {
|
|||
|
||||
let mut pointer_element = PointerElement::<GlesTexture>::new();
|
||||
|
||||
// TODO: pointer
|
||||
state
|
||||
.loop_handle
|
||||
.insert_source(Timer::immediate(), move |_instant, _metadata, data| {
|
||||
let display = &mut data.display;
|
||||
let state = &mut data.state;
|
||||
let insert_ret =
|
||||
state
|
||||
.loop_handle
|
||||
.insert_source(Timer::immediate(), move |_instant, _metadata, data| {
|
||||
let display = &mut data.display;
|
||||
let state = &mut data.state;
|
||||
|
||||
let result = winit_evt_loop.dispatch_new_events(|event| match event {
|
||||
WinitEvent::Resized {
|
||||
size,
|
||||
scale_factor: _,
|
||||
} => {
|
||||
output.change_current_state(
|
||||
Some(smithay::output::Mode {
|
||||
size,
|
||||
refresh: 144_000,
|
||||
}),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
layer_map_for_output(&output).arrange();
|
||||
state.update_windows(&output);
|
||||
// state.re_layout(&output);
|
||||
}
|
||||
WinitEvent::Focus(_) => {}
|
||||
WinitEvent::Input(input_evt) => {
|
||||
state.process_input_event(input_evt);
|
||||
}
|
||||
WinitEvent::Refresh => {}
|
||||
});
|
||||
let result = winit_evt_loop.dispatch_new_events(|event| match event {
|
||||
WinitEvent::Resized {
|
||||
size,
|
||||
scale_factor: _,
|
||||
} => {
|
||||
output.change_current_state(
|
||||
Some(smithay::output::Mode {
|
||||
size,
|
||||
refresh: 144_000,
|
||||
}),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
layer_map_for_output(&output).arrange();
|
||||
state.update_windows(&output);
|
||||
// state.re_layout(&output);
|
||||
}
|
||||
WinitEvent::Focus(_) => {}
|
||||
WinitEvent::Input(input_evt) => {
|
||||
state.process_input_event(input_evt);
|
||||
}
|
||||
WinitEvent::Refresh => {}
|
||||
});
|
||||
|
||||
match result {
|
||||
Ok(_) => {}
|
||||
Err(WinitError::WindowClosed) => {
|
||||
state.loop_signal.stop();
|
||||
}
|
||||
};
|
||||
|
||||
if let CursorImageStatus::Surface(surface) = &state.cursor_status {
|
||||
if !surface.alive() {
|
||||
state.cursor_status = CursorImageStatus::Default;
|
||||
}
|
||||
}
|
||||
|
||||
let cursor_visible = !matches!(state.cursor_status, CursorImageStatus::Surface(_));
|
||||
|
||||
pointer_element.set_status(state.cursor_status.clone());
|
||||
|
||||
let full_redraw = &mut state.backend_data.full_redraw;
|
||||
*full_redraw = full_redraw.saturating_sub(1);
|
||||
|
||||
let output_render_elements = crate::render::generate_render_elements(
|
||||
state.backend_data.backend.renderer(),
|
||||
&state.space,
|
||||
&output,
|
||||
state.seat.input_method(),
|
||||
state.pointer_location,
|
||||
&mut pointer_element,
|
||||
None,
|
||||
&mut state.cursor_status,
|
||||
state.dnd_icon.as_ref(),
|
||||
&state.focus_state.focus_stack,
|
||||
);
|
||||
|
||||
let render_res = state.backend_data.backend.bind().and_then(|_| {
|
||||
let age = if *full_redraw > 0 {
|
||||
0
|
||||
} else {
|
||||
state.backend_data.backend.buffer_age().unwrap_or(0)
|
||||
match result {
|
||||
Ok(_) => {}
|
||||
Err(WinitError::WindowClosed) => {
|
||||
state.loop_signal.stop();
|
||||
}
|
||||
};
|
||||
|
||||
let renderer = state.backend_data.backend.renderer();
|
||||
|
||||
// render_output()
|
||||
|
||||
state
|
||||
.backend_data
|
||||
.damage_tracker
|
||||
.render_output(renderer, age, &output_render_elements, [0.5, 0.5, 0.5, 1.0])
|
||||
.map_err(|err| match err {
|
||||
damage::Error::Rendering(err) => err.into(),
|
||||
damage::Error::OutputNoMode(_) => todo!(),
|
||||
})
|
||||
});
|
||||
|
||||
match render_res {
|
||||
Ok(render_output_result) => {
|
||||
let has_rendered = render_output_result.damage.is_some();
|
||||
if let Some(damage) = render_output_result.damage {
|
||||
if let Err(err) = state.backend_data.backend.submit(Some(&damage)) {
|
||||
tracing::warn!("{}", err);
|
||||
}
|
||||
if let CursorImageStatus::Surface(surface) = &state.cursor_status {
|
||||
if !surface.alive() {
|
||||
state.cursor_status = CursorImageStatus::Default;
|
||||
}
|
||||
}
|
||||
|
||||
let cursor_visible = !matches!(state.cursor_status, CursorImageStatus::Surface(_));
|
||||
|
||||
pointer_element.set_status(state.cursor_status.clone());
|
||||
|
||||
let full_redraw = &mut state.backend_data.full_redraw;
|
||||
*full_redraw = full_redraw.saturating_sub(1);
|
||||
|
||||
let output_render_elements = crate::render::generate_render_elements(
|
||||
state.backend_data.backend.renderer(),
|
||||
&state.space,
|
||||
&output,
|
||||
state.seat.input_method(),
|
||||
state.pointer_location,
|
||||
&mut pointer_element,
|
||||
None,
|
||||
&mut state.cursor_status,
|
||||
state.dnd_icon.as_ref(),
|
||||
&state.focus_state.focus_stack,
|
||||
);
|
||||
|
||||
let render_res = state.backend_data.backend.bind().and_then(|_| {
|
||||
let age = if *full_redraw > 0 {
|
||||
0
|
||||
} else {
|
||||
state.backend_data.backend.buffer_age().unwrap_or(0)
|
||||
};
|
||||
|
||||
let renderer = state.backend_data.backend.renderer();
|
||||
|
||||
// render_output()
|
||||
|
||||
state
|
||||
.backend_data
|
||||
.backend
|
||||
.window()
|
||||
.set_cursor_visible(cursor_visible);
|
||||
.damage_tracker
|
||||
.render_output(renderer, age, &output_render_elements, [0.5, 0.5, 0.5, 1.0])
|
||||
.map_err(|err| match err {
|
||||
damage::Error::Rendering(err) => err.into(),
|
||||
damage::Error::OutputNoMode(_) => todo!(),
|
||||
})
|
||||
});
|
||||
|
||||
let time = state.clock.now();
|
||||
match render_res {
|
||||
Ok(render_output_result) => {
|
||||
let has_rendered = render_output_result.damage.is_some();
|
||||
if let Some(damage) = render_output_result.damage {
|
||||
if let Err(err) = state.backend_data.backend.submit(Some(&damage)) {
|
||||
tracing::warn!("{}", err);
|
||||
}
|
||||
}
|
||||
|
||||
// Send frames to the cursor surface so it updates in xwayland
|
||||
if let CursorImageStatus::Surface(surf) = &state.cursor_status {
|
||||
if let Some(op) = state.focus_state.focused_output.as_ref() {
|
||||
send_frames_surface_tree(
|
||||
surf,
|
||||
op,
|
||||
state
|
||||
.backend_data
|
||||
.backend
|
||||
.window()
|
||||
.set_cursor_visible(cursor_visible);
|
||||
|
||||
let time = state.clock.now();
|
||||
|
||||
// Send frames to the cursor surface so it updates in xwayland
|
||||
if let CursorImageStatus::Surface(surf) = &state.cursor_status {
|
||||
if let Some(op) = state.focus_state.focused_output.as_ref() {
|
||||
send_frames_surface_tree(
|
||||
surf,
|
||||
op,
|
||||
time,
|
||||
Some(Duration::ZERO),
|
||||
|_, _| None,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
super::post_repaint(
|
||||
&output,
|
||||
&render_output_result.states,
|
||||
&state.space,
|
||||
None,
|
||||
time.into(),
|
||||
);
|
||||
|
||||
if has_rendered {
|
||||
let mut output_presentation_feedback = take_presentation_feedback(
|
||||
&output,
|
||||
&state.space,
|
||||
&render_output_result.states,
|
||||
);
|
||||
output_presentation_feedback.presented(
|
||||
time,
|
||||
Some(Duration::ZERO),
|
||||
|_, _| None,
|
||||
output
|
||||
.current_mode()
|
||||
.map(|mode| mode.refresh as u32)
|
||||
.unwrap_or_default(),
|
||||
0,
|
||||
wp_presentation_feedback::Kind::Vsync,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
super::post_repaint(
|
||||
&output,
|
||||
&render_output_result.states,
|
||||
&state.space,
|
||||
None,
|
||||
time.into(),
|
||||
);
|
||||
|
||||
if has_rendered {
|
||||
let mut output_presentation_feedback = take_presentation_feedback(
|
||||
&output,
|
||||
&state.space,
|
||||
&render_output_result.states,
|
||||
);
|
||||
output_presentation_feedback.presented(
|
||||
time,
|
||||
output
|
||||
.current_mode()
|
||||
.map(|mode| mode.refresh as u32)
|
||||
.unwrap_or_default(),
|
||||
0,
|
||||
wp_presentation_feedback::Kind::Vsync,
|
||||
);
|
||||
Err(err) => {
|
||||
tracing::warn!("{}", err);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!("{}", err);
|
||||
}
|
||||
}
|
||||
|
||||
state.space.refresh();
|
||||
state.popup_manager.cleanup();
|
||||
display
|
||||
.flush_clients()
|
||||
.expect("failed to flush client buffers");
|
||||
state.space.refresh();
|
||||
state.popup_manager.cleanup();
|
||||
display
|
||||
.flush_clients()
|
||||
.expect("failed to flush client buffers");
|
||||
|
||||
TimeoutAction::ToDuration(Duration::from_millis(1))
|
||||
})?;
|
||||
TimeoutAction::ToDuration(Duration::from_millis(1))
|
||||
});
|
||||
|
||||
if let Err(err) = insert_ret {
|
||||
anyhow::bail!("Failed to insert winit events into event loop: {err}");
|
||||
}
|
||||
|
||||
event_loop.run(
|
||||
Some(Duration::from_millis(1)),
|
||||
|
|
34
src/input.rs
34
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<B: Backend> State<B> {
|
||||
|
@ -113,6 +120,10 @@ impl<B: Backend> State<B> {
|
|||
let time = event.time_msec();
|
||||
let press_state = event.state();
|
||||
let mut move_mode = false;
|
||||
|
||||
let reload_keybind = self.input_state.reload_keybind;
|
||||
let kill_keybind = self.input_state.kill_keybind;
|
||||
|
||||
let action = self
|
||||
.seat
|
||||
.get_keyboard()
|
||||
|
@ -138,23 +149,23 @@ impl<B: Backend> State<B> {
|
|||
if modifiers.logo {
|
||||
modifier_mask.push(Modifier::Super);
|
||||
}
|
||||
let modifier_mask = ModifierMask::from(modifier_mask);
|
||||
let raw_sym = if keysym.raw_syms().len() == 1 {
|
||||
keysym.raw_syms()[0]
|
||||
} else {
|
||||
keysyms::KEY_NoSymbol
|
||||
};
|
||||
|
||||
if let Some(callback_id) = state
|
||||
.input_state
|
||||
.keybinds
|
||||
.get(&(modifier_mask.into(), raw_sym))
|
||||
.get(&(modifier_mask, raw_sym))
|
||||
{
|
||||
return FilterResult::Intercept(KeyAction::CallCallback(*callback_id));
|
||||
} else if modifiers.ctrl
|
||||
&& modifiers.shift
|
||||
&& modifiers.alt
|
||||
&& keysym.modified_sym() == keysyms::KEY_Escape
|
||||
{
|
||||
} else if (modifier_mask, raw_sym) == kill_keybind {
|
||||
return FilterResult::Intercept(KeyAction::Quit);
|
||||
} else if (modifier_mask, raw_sym) == reload_keybind {
|
||||
return FilterResult::Intercept(KeyAction::ReloadConfig);
|
||||
} else if let mut vt @ keysyms::KEY_XF86Switch_VT_1..=keysyms::KEY_XF86Switch_VT_12 =
|
||||
keysym.modified_sym() {
|
||||
vt = vt - keysyms::KEY_XF86Switch_VT_1 + 1;
|
||||
|
@ -205,7 +216,10 @@ impl<B: Backend> State<B> {
|
|||
Some(KeyAction::Quit) => {
|
||||
self.loop_signal.stop();
|
||||
}
|
||||
_ => {}
|
||||
Some(KeyAction::ReloadConfig) => {
|
||||
self.restart_config().expect("failed to restart config");
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -59,7 +59,7 @@ impl<B: Backend> State<B> {
|
|||
state.focused_tags().next().cloned().map(|tag| tag.layout())
|
||||
}) else { return };
|
||||
|
||||
let (windows_on_foc_tags, windows_not_on_foc_tags): (Vec<_>, _) =
|
||||
let (windows_on_foc_tags, mut windows_not_on_foc_tags): (Vec<_>, _) =
|
||||
output.with_state(|state| {
|
||||
let focused_tags = state.focused_tags().collect::<Vec<_>>();
|
||||
self.windows.iter().cloned().partition(|win| {
|
||||
|
@ -67,6 +67,8 @@ impl<B: Backend> State<B> {
|
|||
})
|
||||
});
|
||||
|
||||
windows_not_on_foc_tags.retain(|win| win.output(self) == Some(output.clone()));
|
||||
|
||||
let tiled_windows = windows_on_foc_tags
|
||||
.iter()
|
||||
.filter(|win| {
|
||||
|
|
|
@ -19,6 +19,7 @@ mod grab;
|
|||
mod handlers;
|
||||
mod input;
|
||||
mod layout;
|
||||
mod metaconfig;
|
||||
mod output;
|
||||
mod pointer;
|
||||
mod render;
|
||||
|
@ -26,9 +27,7 @@ mod state;
|
|||
mod tag;
|
||||
mod window;
|
||||
|
||||
use std::error::Error;
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
fn main() -> anyhow::Result<()> {
|
||||
match tracing_subscriber::EnvFilter::try_from_default_env() {
|
||||
Ok(env_filter) => {
|
||||
tracing_subscriber::fmt()
|
||||
|
|
105
src/metaconfig.rs
Normal file
105
src/metaconfig.rs
Normal file
|
@ -0,0 +1,105 @@
|
|||
use std::path::Path;
|
||||
|
||||
use anyhow::Context;
|
||||
use smithay::input::keyboard::keysyms;
|
||||
use toml::Table;
|
||||
|
||||
use crate::api::msg::Modifier;
|
||||
|
||||
#[derive(serde::Deserialize, Debug)]
|
||||
pub struct Metaconfig {
|
||||
pub command: String,
|
||||
pub envs: Option<Table>,
|
||||
pub reload_keybind: Keybind,
|
||||
pub kill_keybind: Keybind,
|
||||
pub socket_dir: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug)]
|
||||
pub struct Keybind {
|
||||
pub modifiers: Vec<Modifier>,
|
||||
pub key: Key,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug, Clone, Copy)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[repr(u32)]
|
||||
pub enum Key {
|
||||
A = keysyms::KEY_a,
|
||||
B = keysyms::KEY_b,
|
||||
C = keysyms::KEY_c,
|
||||
D = keysyms::KEY_d,
|
||||
E = keysyms::KEY_e,
|
||||
F = keysyms::KEY_f,
|
||||
G = keysyms::KEY_g,
|
||||
H = keysyms::KEY_h,
|
||||
I = keysyms::KEY_i,
|
||||
J = keysyms::KEY_j,
|
||||
K = keysyms::KEY_k,
|
||||
L = keysyms::KEY_l,
|
||||
M = keysyms::KEY_m,
|
||||
N = keysyms::KEY_n,
|
||||
O = keysyms::KEY_o,
|
||||
P = keysyms::KEY_p,
|
||||
Q = keysyms::KEY_q,
|
||||
R = keysyms::KEY_r,
|
||||
S = keysyms::KEY_s,
|
||||
T = keysyms::KEY_t,
|
||||
U = keysyms::KEY_u,
|
||||
V = keysyms::KEY_v,
|
||||
W = keysyms::KEY_w,
|
||||
X = keysyms::KEY_x,
|
||||
Y = keysyms::KEY_y,
|
||||
Z = keysyms::KEY_z,
|
||||
#[serde(alias = "0")]
|
||||
Zero = keysyms::KEY_0,
|
||||
#[serde(alias = "1")]
|
||||
One = keysyms::KEY_1,
|
||||
#[serde(alias = "2")]
|
||||
Two = keysyms::KEY_2,
|
||||
#[serde(alias = "3")]
|
||||
Three = keysyms::KEY_3,
|
||||
#[serde(alias = "4")]
|
||||
Four = keysyms::KEY_4,
|
||||
#[serde(alias = "5")]
|
||||
Five = keysyms::KEY_5,
|
||||
#[serde(alias = "6")]
|
||||
Six = keysyms::KEY_6,
|
||||
#[serde(alias = "7")]
|
||||
Seven = keysyms::KEY_7,
|
||||
#[serde(alias = "8")]
|
||||
Eight = keysyms::KEY_8,
|
||||
#[serde(alias = "9")]
|
||||
Nine = keysyms::KEY_9,
|
||||
#[serde(alias = "num0")]
|
||||
NumZero = keysyms::KEY_KP_0,
|
||||
#[serde(alias = "num1")]
|
||||
NumOne = keysyms::KEY_KP_1,
|
||||
#[serde(alias = "num2")]
|
||||
NumTwo = keysyms::KEY_KP_2,
|
||||
#[serde(alias = "num3")]
|
||||
NumThree = keysyms::KEY_KP_3,
|
||||
#[serde(alias = "num4")]
|
||||
NumFour = keysyms::KEY_KP_4,
|
||||
#[serde(alias = "num5")]
|
||||
NumFive = keysyms::KEY_KP_5,
|
||||
#[serde(alias = "num6")]
|
||||
NumSix = keysyms::KEY_KP_6,
|
||||
#[serde(alias = "num7")]
|
||||
NumSeven = keysyms::KEY_KP_7,
|
||||
#[serde(alias = "num8")]
|
||||
NumEight = keysyms::KEY_KP_8,
|
||||
#[serde(alias = "num9")]
|
||||
NumNine = keysyms::KEY_KP_9,
|
||||
#[serde(alias = "esc")]
|
||||
Escape = keysyms::KEY_Escape,
|
||||
}
|
||||
|
||||
pub fn parse(config_dir: &Path) -> anyhow::Result<Metaconfig> {
|
||||
let config_dir = config_dir.join("metaconfig.toml");
|
||||
|
||||
let metaconfig =
|
||||
std::fs::read_to_string(config_dir).context("Failed to read metaconfig.toml")?;
|
||||
|
||||
toml::from_str(&metaconfig).context("Failed to deserialize toml")
|
||||
}
|
786
src/state.rs
786
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<B: Backend> {
|
|||
pub windows: Vec<WindowElement>,
|
||||
|
||||
pub async_scheduler: Scheduler<()>,
|
||||
pub config_process: async_process::Child,
|
||||
|
||||
// TODO: move into own struct
|
||||
// | basically just clean this mess up
|
||||
|
@ -117,559 +114,6 @@ pub struct State<B: Backend> {
|
|||
pub xdisplay: Option<u32>,
|
||||
}
|
||||
|
||||
impl<B: Backend> State<B> {
|
||||
pub fn handle_msg(&mut self, msg: Msg) {
|
||||
// tracing::debug!("Got {msg:?}");
|
||||
match msg {
|
||||
Msg::SetKeybind {
|
||||
key,
|
||||
modifiers,
|
||||
callback_id,
|
||||
} => {
|
||||
tracing::info!("set keybind: {:?}, {}", modifiers, key);
|
||||
self.input_state
|
||||
.keybinds
|
||||
.insert((modifiers.into(), key), callback_id);
|
||||
}
|
||||
Msg::SetMousebind { button: _ } => todo!(),
|
||||
Msg::CloseWindow { window_id } => {
|
||||
if let Some(window) = window_id.window(self) {
|
||||
match window {
|
||||
WindowElement::Wayland(window) => window.toplevel().send_close(),
|
||||
WindowElement::X11(surface) => {
|
||||
surface.close().expect("failed to close x11 win");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Msg::Spawn {
|
||||
command,
|
||||
callback_id,
|
||||
} => {
|
||||
self.handle_spawn(command, callback_id);
|
||||
}
|
||||
|
||||
Msg::SetWindowSize {
|
||||
window_id,
|
||||
width,
|
||||
height,
|
||||
} => {
|
||||
let Some(window) = window_id.window(self) else { return };
|
||||
|
||||
// TODO: tiled vs floating
|
||||
// FIXME: this will map unmapped windows at 0,0
|
||||
let window_loc = self
|
||||
.space
|
||||
.element_location(&window)
|
||||
.unwrap_or((0, 0).into());
|
||||
let mut window_size = window.geometry().size;
|
||||
if let Some(width) = width {
|
||||
window_size.w = width;
|
||||
}
|
||||
if let Some(height) = height {
|
||||
window_size.h = height;
|
||||
}
|
||||
window.request_size_change(&mut self.space, window_loc, window_size);
|
||||
}
|
||||
Msg::MoveWindowToTag { window_id, tag_id } => {
|
||||
let Some(window) = window_id.window(self) else { return };
|
||||
let Some(tag) = tag_id.tag(self) else { return };
|
||||
window.with_state(|state| {
|
||||
state.tags = vec![tag.clone()];
|
||||
});
|
||||
let Some(output) = tag.output(self) else { return };
|
||||
self.update_windows(&output);
|
||||
// self.re_layout(&output);
|
||||
}
|
||||
Msg::ToggleTagOnWindow { window_id, tag_id } => {
|
||||
let Some(window) = window_id.window(self) else { return };
|
||||
let Some(tag) = tag_id.tag(self) else { return };
|
||||
|
||||
window.with_state(|state| {
|
||||
if state.tags.contains(&tag) {
|
||||
state.tags.retain(|tg| tg != &tag);
|
||||
} else {
|
||||
state.tags.push(tag.clone());
|
||||
}
|
||||
});
|
||||
|
||||
let Some(output) = tag.output(self) else { return };
|
||||
self.update_windows(&output);
|
||||
// self.re_layout(&output);
|
||||
}
|
||||
Msg::ToggleFloating { window_id } => {
|
||||
let Some(window) = window_id.window(self) else { return };
|
||||
window.toggle_floating();
|
||||
|
||||
let Some(output) = window.output(self) else { return };
|
||||
self.update_windows(&output);
|
||||
}
|
||||
Msg::ToggleFullscreen { window_id } => {
|
||||
let Some(window) = window_id.window(self) else { return };
|
||||
window.toggle_fullscreen();
|
||||
|
||||
let Some(output) = window.output(self) else { return };
|
||||
self.update_windows(&output);
|
||||
}
|
||||
Msg::ToggleMaximized { window_id } => {
|
||||
let Some(window) = window_id.window(self) else { return };
|
||||
window.toggle_maximized();
|
||||
|
||||
let Some(output) = window.output(self) else { return };
|
||||
self.update_windows(&output);
|
||||
}
|
||||
|
||||
// Tags ----------------------------------------
|
||||
Msg::ToggleTag { tag_id } => {
|
||||
tracing::debug!("ToggleTag");
|
||||
if let Some(tag) = tag_id.tag(self) {
|
||||
tag.set_active(!tag.active());
|
||||
if let Some(output) = tag.output(self) {
|
||||
self.update_windows(&output);
|
||||
// self.re_layout(&output);
|
||||
}
|
||||
}
|
||||
}
|
||||
Msg::SwitchToTag { tag_id } => {
|
||||
let Some(tag) = tag_id.tag(self) else { return };
|
||||
let Some(output) = tag.output(self) else { return };
|
||||
output.with_state(|state| {
|
||||
for op_tag in state.tags.iter_mut() {
|
||||
op_tag.set_active(false);
|
||||
}
|
||||
tag.set_active(true);
|
||||
});
|
||||
self.update_windows(&output);
|
||||
// self.re_layout(&output);
|
||||
}
|
||||
Msg::AddTags {
|
||||
output_name,
|
||||
tag_names,
|
||||
} => {
|
||||
if let Some(output) = self
|
||||
.space
|
||||
.outputs()
|
||||
.find(|output| output.name() == output_name)
|
||||
{
|
||||
output.with_state(|state| {
|
||||
state.tags.extend(tag_names.iter().cloned().map(Tag::new));
|
||||
tracing::debug!("tags added, are now {:?}", state.tags);
|
||||
});
|
||||
}
|
||||
}
|
||||
Msg::RemoveTags { tag_ids } => {
|
||||
let tags = tag_ids.into_iter().filter_map(|tag_id| tag_id.tag(self));
|
||||
for tag in tags {
|
||||
let Some(output) = tag.output(self) else { continue };
|
||||
output.with_state(|state| {
|
||||
state.tags.retain(|tg| tg != &tag);
|
||||
});
|
||||
}
|
||||
}
|
||||
Msg::SetLayout { tag_id, layout } => {
|
||||
let Some(tag) = tag_id.tag(self) else { return };
|
||||
tag.set_layout(layout);
|
||||
let Some(output) = tag.output(self) else { return };
|
||||
self.update_windows(&output);
|
||||
// self.re_layout(&output);
|
||||
}
|
||||
|
||||
Msg::ConnectForAllOutputs { callback_id } => {
|
||||
let stream = self
|
||||
.api_state
|
||||
.stream
|
||||
.as_ref()
|
||||
.expect("Stream doesn't exist");
|
||||
let mut stream = stream.lock().expect("Couldn't lock stream");
|
||||
for output in self.space.outputs() {
|
||||
crate::api::send_to_client(
|
||||
&mut stream,
|
||||
&OutgoingMsg::CallCallback {
|
||||
callback_id,
|
||||
args: Some(Args::ConnectForAllOutputs {
|
||||
output_name: output.name(),
|
||||
}),
|
||||
},
|
||||
)
|
||||
.expect("Send to client failed");
|
||||
}
|
||||
self.output_callback_ids.push(callback_id);
|
||||
}
|
||||
Msg::SetOutputLocation { output_name, x, y } => {
|
||||
let Some(output) = output_name.output(self) else { return };
|
||||
let mut loc = output.current_location();
|
||||
if let Some(x) = x {
|
||||
loc.x = x;
|
||||
}
|
||||
if let Some(y) = y {
|
||||
loc.y = y;
|
||||
}
|
||||
output.change_current_state(None, None, None, Some(loc));
|
||||
self.space.map_output(&output, loc);
|
||||
tracing::debug!("mapping output {} to {loc:?}", output.name());
|
||||
self.update_windows(&output);
|
||||
// self.re_layout(&output);
|
||||
}
|
||||
|
||||
Msg::Quit => {
|
||||
self.loop_signal.stop();
|
||||
}
|
||||
|
||||
Msg::Request {
|
||||
request_id,
|
||||
request,
|
||||
} => {
|
||||
self.handle_request(request_id, request);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_request(&mut self, request_id: RequestId, request: Request) {
|
||||
let stream = self
|
||||
.api_state
|
||||
.stream
|
||||
.as_ref()
|
||||
.expect("Stream doesn't exist");
|
||||
let mut stream = stream.lock().expect("Couldn't lock stream");
|
||||
match request {
|
||||
Request::GetWindows => {
|
||||
let window_ids = self
|
||||
.windows
|
||||
.iter()
|
||||
.map(|win| win.with_state(|state| state.id))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// FIXME: figure out what to do if error
|
||||
crate::api::send_to_client(
|
||||
&mut stream,
|
||||
&OutgoingMsg::RequestResponse {
|
||||
request_id,
|
||||
response: RequestResponse::Windows { window_ids },
|
||||
},
|
||||
)
|
||||
.expect("Couldn't send to client");
|
||||
}
|
||||
Request::GetWindowProps { window_id } => {
|
||||
let window = window_id.window(self);
|
||||
let size = window
|
||||
.as_ref()
|
||||
.map(|win| (win.geometry().size.w, win.geometry().size.h));
|
||||
let loc = window
|
||||
.as_ref()
|
||||
.and_then(|win| self.space.element_location(win))
|
||||
.map(|loc| (loc.x, loc.y));
|
||||
let (class, title) = window.as_ref().map_or((None, None), |win| match &win {
|
||||
WindowElement::Wayland(_) => {
|
||||
if let Some(wl_surf) = win.wl_surface() {
|
||||
compositor::with_states(&wl_surf, |states| {
|
||||
let lock = states
|
||||
.data_map
|
||||
.get::<XdgToplevelSurfaceData>()
|
||||
.expect("XdgToplevelSurfaceData wasn't in surface's data map")
|
||||
.lock()
|
||||
.expect("failed to acquire lock");
|
||||
(lock.app_id.clone(), lock.title.clone())
|
||||
})
|
||||
} else {
|
||||
(None, None)
|
||||
}
|
||||
}
|
||||
WindowElement::X11(surface) => (Some(surface.class()), Some(surface.title())),
|
||||
});
|
||||
let focused = window.as_ref().and_then(|win| {
|
||||
self.focus_state
|
||||
.current_focus() // TODO: actual focus
|
||||
.map(|foc_win| win == &foc_win)
|
||||
});
|
||||
let floating = window
|
||||
.as_ref()
|
||||
.map(|win| win.with_state(|state| state.floating_or_tiled.is_floating()));
|
||||
let fullscreen_or_maximized = window
|
||||
.as_ref()
|
||||
.map(|win| win.with_state(|state| state.fullscreen_or_maximized));
|
||||
crate::api::send_to_client(
|
||||
&mut stream,
|
||||
&OutgoingMsg::RequestResponse {
|
||||
request_id,
|
||||
response: RequestResponse::WindowProps {
|
||||
size,
|
||||
loc,
|
||||
class,
|
||||
title,
|
||||
focused,
|
||||
floating,
|
||||
fullscreen_or_maximized,
|
||||
},
|
||||
},
|
||||
)
|
||||
.expect("failed to send to client");
|
||||
}
|
||||
Request::GetOutputs => {
|
||||
let output_names = self
|
||||
.space
|
||||
.outputs()
|
||||
.map(|output| output.name())
|
||||
.collect::<Vec<_>>();
|
||||
crate::api::send_to_client(
|
||||
&mut stream,
|
||||
&OutgoingMsg::RequestResponse {
|
||||
request_id,
|
||||
response: RequestResponse::Outputs { output_names },
|
||||
},
|
||||
)
|
||||
.expect("failed to send to client");
|
||||
}
|
||||
Request::GetOutputProps { output_name } => {
|
||||
let output = self
|
||||
.space
|
||||
.outputs()
|
||||
.find(|output| output.name() == output_name);
|
||||
let res = output.as_ref().and_then(|output| {
|
||||
output.current_mode().map(|mode| (mode.size.w, mode.size.h))
|
||||
});
|
||||
let refresh_rate = output
|
||||
.as_ref()
|
||||
.and_then(|output| output.current_mode().map(|mode| mode.refresh));
|
||||
let model = output
|
||||
.as_ref()
|
||||
.map(|output| output.physical_properties().model);
|
||||
let physical_size = output.as_ref().map(|output| {
|
||||
(
|
||||
output.physical_properties().size.w,
|
||||
output.physical_properties().size.h,
|
||||
)
|
||||
});
|
||||
let make = output
|
||||
.as_ref()
|
||||
.map(|output| output.physical_properties().make);
|
||||
let loc = output
|
||||
.as_ref()
|
||||
.map(|output| (output.current_location().x, output.current_location().y));
|
||||
let focused = self
|
||||
.focus_state
|
||||
.focused_output
|
||||
.as_ref()
|
||||
.and_then(|foc_op| output.map(|op| op == foc_op));
|
||||
let tag_ids = output.as_ref().map(|output| {
|
||||
output.with_state(|state| {
|
||||
state.tags.iter().map(|tag| tag.id()).collect::<Vec<_>>()
|
||||
})
|
||||
});
|
||||
crate::api::send_to_client(
|
||||
&mut stream,
|
||||
&OutgoingMsg::RequestResponse {
|
||||
request_id,
|
||||
response: RequestResponse::OutputProps {
|
||||
make,
|
||||
model,
|
||||
loc,
|
||||
res,
|
||||
refresh_rate,
|
||||
physical_size,
|
||||
focused,
|
||||
tag_ids,
|
||||
},
|
||||
},
|
||||
)
|
||||
.expect("failed to send to client");
|
||||
}
|
||||
Request::GetTags => {
|
||||
let tag_ids = self
|
||||
.space
|
||||
.outputs()
|
||||
.flat_map(|op| op.with_state(|state| state.tags.clone()))
|
||||
.map(|tag| tag.id())
|
||||
.collect::<Vec<_>>();
|
||||
tracing::debug!("GetTags: {:?}", tag_ids);
|
||||
crate::api::send_to_client(
|
||||
&mut stream,
|
||||
&OutgoingMsg::RequestResponse {
|
||||
request_id,
|
||||
response: RequestResponse::Tags { tag_ids },
|
||||
},
|
||||
)
|
||||
.expect("failed to send to client");
|
||||
}
|
||||
Request::GetTagProps { tag_id } => {
|
||||
let tag = tag_id.tag(self);
|
||||
let output_name = tag
|
||||
.as_ref()
|
||||
.and_then(|tag| tag.output(self))
|
||||
.map(|output| output.name());
|
||||
let active = tag.as_ref().map(|tag| tag.active());
|
||||
let name = tag.as_ref().map(|tag| tag.name());
|
||||
crate::api::send_to_client(
|
||||
&mut stream,
|
||||
&OutgoingMsg::RequestResponse {
|
||||
request_id,
|
||||
response: RequestResponse::TagProps {
|
||||
active,
|
||||
name,
|
||||
output_name,
|
||||
},
|
||||
},
|
||||
)
|
||||
.expect("failed to send to client");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_spawn(&self, command: Vec<String>, callback_id: Option<CallbackId>) {
|
||||
let mut command = command.into_iter();
|
||||
let Some(program) = command.next() else {
|
||||
// TODO: notify that command was nothing
|
||||
return;
|
||||
};
|
||||
|
||||
let program = OsString::from(program);
|
||||
let Ok(mut child) = async_process::Command::new(&program)
|
||||
.envs(
|
||||
[("WAYLAND_DISPLAY", self.socket_name.clone())]
|
||||
.into_iter()
|
||||
.chain(
|
||||
self.xdisplay.map(|xdisp| ("DISPLAY", format!(":{xdisp}")))
|
||||
)
|
||||
)
|
||||
.stdin(if callback_id.is_some() {
|
||||
Stdio::piped()
|
||||
} else {
|
||||
// piping to null because foot won't open without a callback_id
|
||||
// otherwise
|
||||
Stdio::null()
|
||||
})
|
||||
.stdout(if callback_id.is_some() {
|
||||
Stdio::piped()
|
||||
} else {
|
||||
Stdio::null()
|
||||
})
|
||||
.stderr(if callback_id.is_some() {
|
||||
Stdio::piped()
|
||||
} else {
|
||||
Stdio::null()
|
||||
})
|
||||
.args(command)
|
||||
.spawn()
|
||||
else {
|
||||
// TODO: notify user that program doesn't exist
|
||||
tracing::warn!("tried to run {}, but it doesn't exist", program.to_string_lossy());
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(callback_id) = callback_id {
|
||||
let stdout = child.stdout.take();
|
||||
let stderr = child.stderr.take();
|
||||
let stream_out = self
|
||||
.api_state
|
||||
.stream
|
||||
.as_ref()
|
||||
.expect("Stream doesn't exist")
|
||||
.clone();
|
||||
let stream_err = stream_out.clone();
|
||||
let stream_exit = stream_out.clone();
|
||||
|
||||
if let Some(stdout) = stdout {
|
||||
let future = async move {
|
||||
// TODO: use BufReader::new().lines()
|
||||
let mut reader = futures_lite::io::BufReader::new(stdout);
|
||||
loop {
|
||||
let mut buf = String::new();
|
||||
match reader.read_line(&mut buf).await {
|
||||
Ok(0) => break,
|
||||
Ok(_) => {
|
||||
let mut stream = stream_out.lock().expect("Couldn't lock stream");
|
||||
crate::api::send_to_client(
|
||||
&mut stream,
|
||||
&OutgoingMsg::CallCallback {
|
||||
callback_id,
|
||||
args: Some(Args::Spawn {
|
||||
stdout: Some(buf.trim_end_matches('\n').to_string()),
|
||||
stderr: None,
|
||||
exit_code: None,
|
||||
exit_msg: None,
|
||||
}),
|
||||
},
|
||||
)
|
||||
.expect("Send to client failed"); // TODO: notify instead of crash
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!("child read err: {err}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// This is not important enough to crash on error, so just print the error instead
|
||||
if let Err(err) = self.async_scheduler.schedule(future) {
|
||||
tracing::error!("Failed to schedule future: {err}");
|
||||
}
|
||||
}
|
||||
if let Some(stderr) = stderr {
|
||||
let future = async move {
|
||||
let mut reader = futures_lite::io::BufReader::new(stderr);
|
||||
loop {
|
||||
let mut buf = String::new();
|
||||
match reader.read_line(&mut buf).await {
|
||||
Ok(0) => break,
|
||||
Ok(_) => {
|
||||
let mut stream = stream_err.lock().expect("Couldn't lock stream");
|
||||
crate::api::send_to_client(
|
||||
&mut stream,
|
||||
&OutgoingMsg::CallCallback {
|
||||
callback_id,
|
||||
args: Some(Args::Spawn {
|
||||
stdout: None,
|
||||
stderr: Some(buf.trim_end_matches('\n').to_string()),
|
||||
exit_code: None,
|
||||
exit_msg: None,
|
||||
}),
|
||||
},
|
||||
)
|
||||
.expect("Send to client failed"); // TODO: notify instead of crash
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!("child read err: {err}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
if let Err(err) = self.async_scheduler.schedule(future) {
|
||||
tracing::error!("Failed to schedule future: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
let future = async move {
|
||||
match child.status().await {
|
||||
Ok(exit_status) => {
|
||||
let mut stream = stream_exit.lock().expect("Couldn't lock stream");
|
||||
crate::api::send_to_client(
|
||||
&mut stream,
|
||||
&OutgoingMsg::CallCallback {
|
||||
callback_id,
|
||||
args: Some(Args::Spawn {
|
||||
stdout: None,
|
||||
stderr: None,
|
||||
exit_code: exit_status.code(),
|
||||
exit_msg: Some(exit_status.to_string()),
|
||||
}),
|
||||
},
|
||||
)
|
||||
.expect("Send to client failed"); // TODO: notify instead of crash
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!("child wait() err: {err}");
|
||||
}
|
||||
}
|
||||
};
|
||||
if let Err(err) = self.async_scheduler.schedule(future) {
|
||||
tracing::error!("Failed to schedule future: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Schedule something to be done when windows have finished committing and have become
|
||||
/// idle.
|
||||
pub fn schedule_on_commit<F, B: Backend>(
|
||||
|
@ -718,7 +162,7 @@ impl<B: Backend> State<B> {
|
|||
display: &mut Display<Self>,
|
||||
loop_signal: LoopSignal,
|
||||
loop_handle: LoopHandle<'static, CalloopData<B>>,
|
||||
) -> Result<Self, Box<dyn Error>> {
|
||||
) -> anyhow::Result<Self> {
|
||||
let socket = ListeningSocketSource::new_auto()?;
|
||||
let socket_name = socket.socket_name().to_os_string();
|
||||
|
||||
|
@ -766,21 +210,40 @@ impl<B: Backend> State<B> {
|
|||
|
||||
let (tx_channel, rx_channel) = calloop::channel::channel::<Msg>();
|
||||
|
||||
// We want to replace the client if a new one pops up
|
||||
// TODO: there should only ever be one client working at a time, and creating a new client
|
||||
// | when one is already running should be impossible.
|
||||
// INFO: this source try_clone()s the stream
|
||||
let config_dir = get_config_dir();
|
||||
|
||||
// TODO: probably use anyhow or something
|
||||
let socket_source = match PinnacleSocketSource::new(tx_channel) {
|
||||
Ok(source) => source,
|
||||
Err(err) => {
|
||||
tracing::error!("Failed to create the socket source: {err}");
|
||||
Err(err)?
|
||||
}
|
||||
let metaconfig = crate::metaconfig::parse(&config_dir)?;
|
||||
|
||||
let socket_dir = {
|
||||
let dir_string = shellexpand::full(
|
||||
metaconfig
|
||||
.socket_dir
|
||||
.as_deref()
|
||||
.unwrap_or(DEFAULT_SOCKET_DIR),
|
||||
)?
|
||||
.to_string();
|
||||
|
||||
// cd into the metaconfig dir and canonicalize to preserve relative paths
|
||||
// like ./dir/here
|
||||
let current_dir = std::env::current_dir()?;
|
||||
|
||||
std::env::set_current_dir(&config_dir)?;
|
||||
let pathbuf = PathBuf::from(&dir_string).canonicalize()?;
|
||||
std::env::set_current_dir(current_dir)?;
|
||||
|
||||
pathbuf
|
||||
};
|
||||
|
||||
loop_handle.insert_source(socket_source, |stream, _, data| {
|
||||
let socket_source = PinnacleSocketSource::new(tx_channel, &socket_dir)
|
||||
.context("Failed to create socket source")?;
|
||||
|
||||
let ConfigReturn {
|
||||
reload_keybind,
|
||||
kill_keybind,
|
||||
config_child_handle,
|
||||
} = start_config(metaconfig, &config_dir)?;
|
||||
|
||||
let insert_ret = loop_handle.insert_source(socket_source, |stream, _, data| {
|
||||
if let Some(old_stream) = data
|
||||
.state
|
||||
.api_state
|
||||
|
@ -793,13 +256,17 @@ impl<B: Backend> State<B> {
|
|||
.shutdown(std::net::Shutdown::Both)
|
||||
.expect("Couldn't shutdown old stream");
|
||||
}
|
||||
})?;
|
||||
});
|
||||
|
||||
if let Err(err) = insert_ret {
|
||||
anyhow::bail!("Failed to insert socket source into event loop: {err}");
|
||||
}
|
||||
|
||||
let (executor, sched) =
|
||||
calloop::futures::executor::<()>().expect("Couldn't create executor");
|
||||
loop_handle.insert_source(executor, |_, _, _| {})?;
|
||||
|
||||
start_lua_config()?;
|
||||
if let Err(err) = loop_handle.insert_source(executor, |_, _, _| {}) {
|
||||
anyhow::bail!("Failed to insert async executor into event loop: {err}");
|
||||
}
|
||||
|
||||
let display_handle = display.handle();
|
||||
let mut seat_state = SeatState::new();
|
||||
|
@ -882,7 +349,7 @@ impl<B: Backend> State<B> {
|
|||
primary_selection_state: PrimarySelectionState::new::<Self>(&display_handle),
|
||||
layer_shell_state: WlrLayerShellState::new::<Self>(&display_handle),
|
||||
|
||||
input_state: InputState::new(),
|
||||
input_state: InputState::new(reload_keybind, kill_keybind),
|
||||
api_state: ApiState::new(),
|
||||
focus_state: FocusState::new(),
|
||||
|
||||
|
@ -896,6 +363,7 @@ impl<B: Backend> State<B> {
|
|||
popup_manager: PopupManager::default(),
|
||||
|
||||
async_scheduler: sched,
|
||||
config_process: config_child_handle,
|
||||
|
||||
windows: vec![],
|
||||
output_callback_ids: vec![],
|
||||
|
@ -907,60 +375,114 @@ impl<B: Backend> State<B> {
|
|||
}
|
||||
}
|
||||
|
||||
fn start_lua_config() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// TODO: move all this into the lua api
|
||||
let config_path = std::env::var("PINNACLE_CONFIG")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|_| {
|
||||
let default_path = std::env::var("XDG_CONFIG_HOME").unwrap_or("~/.config".to_string());
|
||||
let mut default_path = PathBuf::from(default_path);
|
||||
default_path.push("pinnacle/init.lua");
|
||||
default_path
|
||||
});
|
||||
fn get_config_dir() -> PathBuf {
|
||||
let config_dir = std::env::var("PINNACLE_CONFIG_DIR").unwrap_or_else(|_| {
|
||||
let default_config_dir =
|
||||
std::env::var("XDG_CONFIG_HOME").unwrap_or("~/.config".to_string());
|
||||
|
||||
let config_path = {
|
||||
let path = shellexpand::tilde(&config_path.to_string_lossy().to_string()).to_string();
|
||||
PathBuf::from(path)
|
||||
};
|
||||
|
||||
if config_path.exists() {
|
||||
let lua_path = std::env::var("LUA_PATH").unwrap_or_else(|_| {
|
||||
tracing::info!("LUA_PATH was not set, using empty string");
|
||||
"".to_string()
|
||||
});
|
||||
let mut local_lua_path = std::env::current_dir()
|
||||
.expect("Couldn't get current dir")
|
||||
PathBuf::from(default_config_dir)
|
||||
.join("pinnacle")
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
local_lua_path.push_str("/api/lua"); // TODO: get from crate root and do dynamically
|
||||
let new_lua_path =
|
||||
format!("{local_lua_path}/?.lua;{local_lua_path}/?/init.lua;{local_lua_path}/lib/?.lua;{local_lua_path}/lib/?/init.lua;{lua_path}");
|
||||
.to_string()
|
||||
});
|
||||
PathBuf::from(shellexpand::tilde(&config_dir).to_string())
|
||||
}
|
||||
|
||||
let lua_cpath = std::env::var("LUA_CPATH").unwrap_or_else(|_| {
|
||||
tracing::info!("LUA_CPATH was not set, using empty string");
|
||||
"".to_string()
|
||||
});
|
||||
let new_lua_cpath = format!("{local_lua_path}/lib/?.so;{lua_cpath}");
|
||||
/// This should be called *after* you have created the [`PinnacleSocketSource`] to ensure
|
||||
/// PINNACLE_SOCKET is set correctly for use in API implementations.
|
||||
fn start_config(metaconfig: Metaconfig, config_dir: &Path) -> anyhow::Result<ConfigReturn> {
|
||||
let reload_keybind = metaconfig.reload_keybind;
|
||||
let kill_keybind = metaconfig.kill_keybind;
|
||||
|
||||
if let Err(err) = std::process::Command::new("lua")
|
||||
.arg(config_path)
|
||||
.env("LUA_PATH", new_lua_path)
|
||||
.env("LUA_CPATH", new_lua_cpath)
|
||||
.spawn()
|
||||
{
|
||||
tracing::error!("Failed to start Lua: {err}");
|
||||
return Err(err)?;
|
||||
let mut command = metaconfig.command.split(' ');
|
||||
|
||||
let arg1 = command.next().expect("empty command");
|
||||
|
||||
std::env::set_var("PINNACLE_DIR", std::env::current_dir()?);
|
||||
|
||||
let envs = metaconfig
|
||||
.envs
|
||||
.unwrap_or(toml::map::Map::new())
|
||||
.into_iter()
|
||||
.filter_map(|(key, val)| {
|
||||
if let toml::Value::String(string) = val {
|
||||
Some((
|
||||
key,
|
||||
shellexpand::full_with_context(
|
||||
&string,
|
||||
|| std::env::var("HOME").ok(),
|
||||
|var| Ok::<_, ()>(Some(std::env::var(var).unwrap_or("".to_string()))),
|
||||
)
|
||||
.ok()?
|
||||
.to_string(),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
tracing::debug!("Config envs are {:?}", envs);
|
||||
|
||||
let child = async_process::Command::new(arg1)
|
||||
.args(command)
|
||||
.envs(envs)
|
||||
.current_dir(config_dir)
|
||||
.spawn()
|
||||
.expect("failed to spawn config");
|
||||
|
||||
tracing::info!("Started config with {}", metaconfig.command);
|
||||
|
||||
let reload_mask = ModifierMask::from(reload_keybind.modifiers);
|
||||
let kill_mask = ModifierMask::from(kill_keybind.modifiers);
|
||||
|
||||
Ok(ConfigReturn {
|
||||
reload_keybind: (reload_mask, reload_keybind.key as u32),
|
||||
kill_keybind: (kill_mask, kill_keybind.key as u32),
|
||||
config_child_handle: child,
|
||||
})
|
||||
}
|
||||
|
||||
struct ConfigReturn {
|
||||
reload_keybind: (ModifierMask, u32),
|
||||
kill_keybind: (ModifierMask, u32),
|
||||
config_child_handle: async_process::Child,
|
||||
}
|
||||
|
||||
impl<B: Backend> State<B> {
|
||||
pub fn restart_config(&mut self) -> anyhow::Result<()> {
|
||||
tracing::info!("Restarting config");
|
||||
tracing::debug!("Clearing tags");
|
||||
for output in self.space.outputs() {
|
||||
output.with_state(|state| state.tags.clear());
|
||||
}
|
||||
TagId::reset();
|
||||
|
||||
tracing::debug!("Clearing mouse- and keybinds");
|
||||
self.input_state.keybinds.clear();
|
||||
self.input_state.mousebinds.clear();
|
||||
|
||||
tracing::debug!("Killing old config");
|
||||
if let Err(err) = self.config_process.kill() {
|
||||
tracing::warn!("Error when killing old config: {err}");
|
||||
}
|
||||
|
||||
let config_dir = get_config_dir();
|
||||
|
||||
let metaconfig =
|
||||
crate::metaconfig::parse(&config_dir).context("Failed to parse metaconfig.toml")?;
|
||||
|
||||
let ConfigReturn {
|
||||
reload_keybind,
|
||||
kill_keybind,
|
||||
config_child_handle,
|
||||
} = start_config(metaconfig, &config_dir)?;
|
||||
|
||||
self.input_state.reload_keybind = reload_keybind;
|
||||
self.input_state.kill_keybind = kill_keybind;
|
||||
self.config_process = config_child_handle;
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
tracing::error!("Could not find config {:?}", config_path);
|
||||
if std::env::var("PINNACLE_CONFIG").is_err() {
|
||||
tracing::error!("Help: Run Pinnacle with PINNACLE_CONFIG set to a valid config file, or copy the provided example_config.lua to the path mentioned above.");
|
||||
}
|
||||
Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
"No config found",
|
||||
))?
|
||||
}
|
||||
}
|
||||
|
||||
|
|
585
src/state/api_handlers.rs
Normal file
585
src/state/api_handlers.rs
Normal file
|
@ -0,0 +1,585 @@
|
|||
use std::ffi::OsString;
|
||||
|
||||
use async_process::Stdio;
|
||||
use futures_lite::AsyncBufReadExt;
|
||||
use smithay::{
|
||||
desktop::space::SpaceElement,
|
||||
wayland::{compositor, shell::xdg::XdgToplevelSurfaceData},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
api::msg::{Args, CallbackId, Msg, OutgoingMsg, Request, RequestId, RequestResponse},
|
||||
backend::Backend,
|
||||
tag::Tag,
|
||||
window::WindowElement,
|
||||
};
|
||||
|
||||
use super::{State, WithState};
|
||||
|
||||
impl<B: Backend> State<B> {
|
||||
pub fn handle_msg(&mut self, msg: Msg) {
|
||||
// tracing::debug!("Got {msg:?}");
|
||||
match msg {
|
||||
Msg::SetKeybind {
|
||||
key,
|
||||
modifiers,
|
||||
callback_id,
|
||||
} => {
|
||||
tracing::info!("set keybind: {:?}, {}", modifiers, key);
|
||||
self.input_state
|
||||
.keybinds
|
||||
.insert((modifiers.into(), key), callback_id);
|
||||
}
|
||||
Msg::SetMousebind { button: _ } => todo!(),
|
||||
Msg::CloseWindow { window_id } => {
|
||||
if let Some(window) = window_id.window(self) {
|
||||
match window {
|
||||
WindowElement::Wayland(window) => window.toplevel().send_close(),
|
||||
WindowElement::X11(surface) => {
|
||||
surface.close().expect("failed to close x11 win");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Msg::Spawn {
|
||||
command,
|
||||
callback_id,
|
||||
} => {
|
||||
self.handle_spawn(command, callback_id);
|
||||
}
|
||||
|
||||
Msg::SetWindowSize {
|
||||
window_id,
|
||||
width,
|
||||
height,
|
||||
} => {
|
||||
let Some(window) = window_id.window(self) else { return };
|
||||
|
||||
// TODO: tiled vs floating
|
||||
// FIXME: this will map unmapped windows at 0,0
|
||||
let window_loc = self
|
||||
.space
|
||||
.element_location(&window)
|
||||
.unwrap_or((0, 0).into());
|
||||
let mut window_size = window.geometry().size;
|
||||
if let Some(width) = width {
|
||||
window_size.w = width;
|
||||
}
|
||||
if let Some(height) = height {
|
||||
window_size.h = height;
|
||||
}
|
||||
window.request_size_change(&mut self.space, window_loc, window_size);
|
||||
}
|
||||
Msg::MoveWindowToTag { window_id, tag_id } => {
|
||||
let Some(window) = window_id.window(self) else { return };
|
||||
let Some(tag) = tag_id.tag(self) else { return };
|
||||
window.with_state(|state| {
|
||||
state.tags = vec![tag.clone()];
|
||||
});
|
||||
let Some(output) = tag.output(self) else { return };
|
||||
self.update_windows(&output);
|
||||
// self.re_layout(&output);
|
||||
}
|
||||
Msg::ToggleTagOnWindow { window_id, tag_id } => {
|
||||
let Some(window) = window_id.window(self) else { return };
|
||||
let Some(tag) = tag_id.tag(self) else { return };
|
||||
|
||||
window.with_state(|state| {
|
||||
if state.tags.contains(&tag) {
|
||||
state.tags.retain(|tg| tg != &tag);
|
||||
} else {
|
||||
state.tags.push(tag.clone());
|
||||
}
|
||||
});
|
||||
|
||||
let Some(output) = tag.output(self) else { return };
|
||||
self.update_windows(&output);
|
||||
// self.re_layout(&output);
|
||||
}
|
||||
Msg::ToggleFloating { window_id } => {
|
||||
let Some(window) = window_id.window(self) else { return };
|
||||
window.toggle_floating();
|
||||
|
||||
let Some(output) = window.output(self) else { return };
|
||||
self.update_windows(&output);
|
||||
}
|
||||
Msg::ToggleFullscreen { window_id } => {
|
||||
let Some(window) = window_id.window(self) else { return };
|
||||
window.toggle_fullscreen();
|
||||
|
||||
let Some(output) = window.output(self) else { return };
|
||||
self.update_windows(&output);
|
||||
}
|
||||
Msg::ToggleMaximized { window_id } => {
|
||||
let Some(window) = window_id.window(self) else { return };
|
||||
window.toggle_maximized();
|
||||
|
||||
let Some(output) = window.output(self) else { return };
|
||||
self.update_windows(&output);
|
||||
}
|
||||
|
||||
// Tags ----------------------------------------
|
||||
Msg::ToggleTag { tag_id } => {
|
||||
tracing::debug!("ToggleTag");
|
||||
if let Some(tag) = tag_id.tag(self) {
|
||||
tag.set_active(!tag.active());
|
||||
if let Some(output) = tag.output(self) {
|
||||
self.update_windows(&output);
|
||||
// self.re_layout(&output);
|
||||
}
|
||||
}
|
||||
}
|
||||
Msg::SwitchToTag { tag_id } => {
|
||||
let Some(tag) = tag_id.tag(self) else { return };
|
||||
let Some(output) = tag.output(self) else { return };
|
||||
output.with_state(|state| {
|
||||
for op_tag in state.tags.iter_mut() {
|
||||
op_tag.set_active(false);
|
||||
}
|
||||
tag.set_active(true);
|
||||
});
|
||||
self.update_windows(&output);
|
||||
// self.re_layout(&output);
|
||||
}
|
||||
Msg::AddTags {
|
||||
output_name,
|
||||
tag_names,
|
||||
} => {
|
||||
if let Some(output) = self
|
||||
.space
|
||||
.outputs()
|
||||
.find(|output| output.name() == output_name)
|
||||
{
|
||||
let new_tags = tag_names.into_iter().map(Tag::new).collect::<Vec<_>>();
|
||||
output.with_state(|state| {
|
||||
state.tags.extend(new_tags.clone());
|
||||
tracing::debug!("tags added, are now {:?}", state.tags);
|
||||
});
|
||||
|
||||
// replace tags that windows have that are the same id
|
||||
// (this should only happen on config reload)
|
||||
for tag in new_tags {
|
||||
for window in self.windows.iter() {
|
||||
window.with_state(|state| {
|
||||
for win_tag in state.tags.iter_mut() {
|
||||
if win_tag.id() == tag.id() {
|
||||
*win_tag = tag.clone();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Msg::RemoveTags { tag_ids } => {
|
||||
let tags = tag_ids.into_iter().filter_map(|tag_id| tag_id.tag(self));
|
||||
for tag in tags {
|
||||
let Some(output) = tag.output(self) else { continue };
|
||||
output.with_state(|state| {
|
||||
state.tags.retain(|tg| tg != &tag);
|
||||
});
|
||||
}
|
||||
}
|
||||
Msg::SetLayout { tag_id, layout } => {
|
||||
let Some(tag) = tag_id.tag(self) else { return };
|
||||
tag.set_layout(layout);
|
||||
let Some(output) = tag.output(self) else { return };
|
||||
self.update_windows(&output);
|
||||
// self.re_layout(&output);
|
||||
}
|
||||
|
||||
Msg::ConnectForAllOutputs { callback_id } => {
|
||||
let stream = self
|
||||
.api_state
|
||||
.stream
|
||||
.as_ref()
|
||||
.expect("Stream doesn't exist");
|
||||
let mut stream = stream.lock().expect("Couldn't lock stream");
|
||||
for output in self.space.outputs() {
|
||||
crate::api::send_to_client(
|
||||
&mut stream,
|
||||
&OutgoingMsg::CallCallback {
|
||||
callback_id,
|
||||
args: Some(Args::ConnectForAllOutputs {
|
||||
output_name: output.name(),
|
||||
}),
|
||||
},
|
||||
)
|
||||
.expect("Send to client failed");
|
||||
}
|
||||
self.output_callback_ids.push(callback_id);
|
||||
}
|
||||
Msg::SetOutputLocation { output_name, x, y } => {
|
||||
let Some(output) = output_name.output(self) else { return };
|
||||
let mut loc = output.current_location();
|
||||
if let Some(x) = x {
|
||||
loc.x = x;
|
||||
}
|
||||
if let Some(y) = y {
|
||||
loc.y = y;
|
||||
}
|
||||
output.change_current_state(None, None, None, Some(loc));
|
||||
self.space.map_output(&output, loc);
|
||||
tracing::debug!("mapping output {} to {loc:?}", output.name());
|
||||
self.update_windows(&output);
|
||||
// self.re_layout(&output);
|
||||
}
|
||||
|
||||
Msg::Quit => {
|
||||
self.loop_signal.stop();
|
||||
}
|
||||
|
||||
Msg::Request {
|
||||
request_id,
|
||||
request,
|
||||
} => {
|
||||
self.handle_request(request_id, request);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_request(&mut self, request_id: RequestId, request: Request) {
|
||||
let stream = self
|
||||
.api_state
|
||||
.stream
|
||||
.as_ref()
|
||||
.expect("Stream doesn't exist");
|
||||
let mut stream = stream.lock().expect("Couldn't lock stream");
|
||||
match request {
|
||||
Request::GetWindows => {
|
||||
let window_ids = self
|
||||
.windows
|
||||
.iter()
|
||||
.map(|win| win.with_state(|state| state.id))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// FIXME: figure out what to do if error
|
||||
crate::api::send_to_client(
|
||||
&mut stream,
|
||||
&OutgoingMsg::RequestResponse {
|
||||
request_id,
|
||||
response: RequestResponse::Windows { window_ids },
|
||||
},
|
||||
)
|
||||
.expect("Couldn't send to client");
|
||||
}
|
||||
Request::GetWindowProps { window_id } => {
|
||||
let window = window_id.window(self);
|
||||
let size = window
|
||||
.as_ref()
|
||||
.map(|win| (win.geometry().size.w, win.geometry().size.h));
|
||||
let loc = window
|
||||
.as_ref()
|
||||
.and_then(|win| self.space.element_location(win))
|
||||
.map(|loc| (loc.x, loc.y));
|
||||
let (class, title) = window.as_ref().map_or((None, None), |win| match &win {
|
||||
WindowElement::Wayland(_) => {
|
||||
if let Some(wl_surf) = win.wl_surface() {
|
||||
compositor::with_states(&wl_surf, |states| {
|
||||
let lock = states
|
||||
.data_map
|
||||
.get::<XdgToplevelSurfaceData>()
|
||||
.expect("XdgToplevelSurfaceData wasn't in surface's data map")
|
||||
.lock()
|
||||
.expect("failed to acquire lock");
|
||||
(lock.app_id.clone(), lock.title.clone())
|
||||
})
|
||||
} else {
|
||||
(None, None)
|
||||
}
|
||||
}
|
||||
WindowElement::X11(surface) => (Some(surface.class()), Some(surface.title())),
|
||||
});
|
||||
let focused = window.as_ref().and_then(|win| {
|
||||
self.focus_state
|
||||
.current_focus() // TODO: actual focus
|
||||
.map(|foc_win| win == &foc_win)
|
||||
});
|
||||
let floating = window
|
||||
.as_ref()
|
||||
.map(|win| win.with_state(|state| state.floating_or_tiled.is_floating()));
|
||||
let fullscreen_or_maximized = window
|
||||
.as_ref()
|
||||
.map(|win| win.with_state(|state| state.fullscreen_or_maximized));
|
||||
crate::api::send_to_client(
|
||||
&mut stream,
|
||||
&OutgoingMsg::RequestResponse {
|
||||
request_id,
|
||||
response: RequestResponse::WindowProps {
|
||||
size,
|
||||
loc,
|
||||
class,
|
||||
title,
|
||||
focused,
|
||||
floating,
|
||||
fullscreen_or_maximized,
|
||||
},
|
||||
},
|
||||
)
|
||||
.expect("failed to send to client");
|
||||
}
|
||||
Request::GetOutputs => {
|
||||
let output_names = self
|
||||
.space
|
||||
.outputs()
|
||||
.map(|output| output.name())
|
||||
.collect::<Vec<_>>();
|
||||
crate::api::send_to_client(
|
||||
&mut stream,
|
||||
&OutgoingMsg::RequestResponse {
|
||||
request_id,
|
||||
response: RequestResponse::Outputs { output_names },
|
||||
},
|
||||
)
|
||||
.expect("failed to send to client");
|
||||
}
|
||||
Request::GetOutputProps { output_name } => {
|
||||
let output = self
|
||||
.space
|
||||
.outputs()
|
||||
.find(|output| output.name() == output_name);
|
||||
let res = output.as_ref().and_then(|output| {
|
||||
output.current_mode().map(|mode| (mode.size.w, mode.size.h))
|
||||
});
|
||||
let refresh_rate = output
|
||||
.as_ref()
|
||||
.and_then(|output| output.current_mode().map(|mode| mode.refresh));
|
||||
let model = output
|
||||
.as_ref()
|
||||
.map(|output| output.physical_properties().model);
|
||||
let physical_size = output.as_ref().map(|output| {
|
||||
(
|
||||
output.physical_properties().size.w,
|
||||
output.physical_properties().size.h,
|
||||
)
|
||||
});
|
||||
let make = output
|
||||
.as_ref()
|
||||
.map(|output| output.physical_properties().make);
|
||||
let loc = output
|
||||
.as_ref()
|
||||
.map(|output| (output.current_location().x, output.current_location().y));
|
||||
let focused = self
|
||||
.focus_state
|
||||
.focused_output
|
||||
.as_ref()
|
||||
.and_then(|foc_op| output.map(|op| op == foc_op));
|
||||
let tag_ids = output.as_ref().map(|output| {
|
||||
output.with_state(|state| {
|
||||
state.tags.iter().map(|tag| tag.id()).collect::<Vec<_>>()
|
||||
})
|
||||
});
|
||||
crate::api::send_to_client(
|
||||
&mut stream,
|
||||
&OutgoingMsg::RequestResponse {
|
||||
request_id,
|
||||
response: RequestResponse::OutputProps {
|
||||
make,
|
||||
model,
|
||||
loc,
|
||||
res,
|
||||
refresh_rate,
|
||||
physical_size,
|
||||
focused,
|
||||
tag_ids,
|
||||
},
|
||||
},
|
||||
)
|
||||
.expect("failed to send to client");
|
||||
}
|
||||
Request::GetTags => {
|
||||
let tag_ids = self
|
||||
.space
|
||||
.outputs()
|
||||
.flat_map(|op| op.with_state(|state| state.tags.clone()))
|
||||
.map(|tag| tag.id())
|
||||
.collect::<Vec<_>>();
|
||||
tracing::debug!("GetTags: {:?}", tag_ids);
|
||||
crate::api::send_to_client(
|
||||
&mut stream,
|
||||
&OutgoingMsg::RequestResponse {
|
||||
request_id,
|
||||
response: RequestResponse::Tags { tag_ids },
|
||||
},
|
||||
)
|
||||
.expect("failed to send to client");
|
||||
}
|
||||
Request::GetTagProps { tag_id } => {
|
||||
let tag = tag_id.tag(self);
|
||||
let output_name = tag
|
||||
.as_ref()
|
||||
.and_then(|tag| tag.output(self))
|
||||
.map(|output| output.name());
|
||||
let active = tag.as_ref().map(|tag| tag.active());
|
||||
let name = tag.as_ref().map(|tag| tag.name());
|
||||
crate::api::send_to_client(
|
||||
&mut stream,
|
||||
&OutgoingMsg::RequestResponse {
|
||||
request_id,
|
||||
response: RequestResponse::TagProps {
|
||||
active,
|
||||
name,
|
||||
output_name,
|
||||
},
|
||||
},
|
||||
)
|
||||
.expect("failed to send to client");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_spawn(&self, command: Vec<String>, callback_id: Option<CallbackId>) {
|
||||
let mut command = command.into_iter();
|
||||
let Some(program) = command.next() else {
|
||||
// TODO: notify that command was nothing
|
||||
return;
|
||||
};
|
||||
|
||||
let program = OsString::from(program);
|
||||
let Ok(mut child) = async_process::Command::new(&program)
|
||||
.envs(
|
||||
[("WAYLAND_DISPLAY", self.socket_name.clone())]
|
||||
.into_iter()
|
||||
.chain(
|
||||
self.xdisplay.map(|xdisp| ("DISPLAY", format!(":{xdisp}")))
|
||||
)
|
||||
)
|
||||
.stdin(if callback_id.is_some() {
|
||||
Stdio::piped()
|
||||
} else {
|
||||
// piping to null because foot won't open without a callback_id
|
||||
// otherwise
|
||||
Stdio::null()
|
||||
})
|
||||
.stdout(if callback_id.is_some() {
|
||||
Stdio::piped()
|
||||
} else {
|
||||
Stdio::null()
|
||||
})
|
||||
.stderr(if callback_id.is_some() {
|
||||
Stdio::piped()
|
||||
} else {
|
||||
Stdio::null()
|
||||
})
|
||||
.args(command)
|
||||
.spawn()
|
||||
else {
|
||||
// TODO: notify user that program doesn't exist
|
||||
tracing::warn!("tried to run {}, but it doesn't exist", program.to_string_lossy());
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(callback_id) = callback_id {
|
||||
let stdout = child.stdout.take();
|
||||
let stderr = child.stderr.take();
|
||||
let stream_out = self
|
||||
.api_state
|
||||
.stream
|
||||
.as_ref()
|
||||
.expect("Stream doesn't exist")
|
||||
.clone();
|
||||
let stream_err = stream_out.clone();
|
||||
let stream_exit = stream_out.clone();
|
||||
|
||||
if let Some(stdout) = stdout {
|
||||
let future = async move {
|
||||
// TODO: use BufReader::new().lines()
|
||||
let mut reader = futures_lite::io::BufReader::new(stdout);
|
||||
loop {
|
||||
let mut buf = String::new();
|
||||
match reader.read_line(&mut buf).await {
|
||||
Ok(0) => break,
|
||||
Ok(_) => {
|
||||
let mut stream = stream_out.lock().expect("Couldn't lock stream");
|
||||
crate::api::send_to_client(
|
||||
&mut stream,
|
||||
&OutgoingMsg::CallCallback {
|
||||
callback_id,
|
||||
args: Some(Args::Spawn {
|
||||
stdout: Some(buf.trim_end_matches('\n').to_string()),
|
||||
stderr: None,
|
||||
exit_code: None,
|
||||
exit_msg: None,
|
||||
}),
|
||||
},
|
||||
)
|
||||
.expect("Send to client failed"); // TODO: notify instead of crash
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!("child read err: {err}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// This is not important enough to crash on error, so just print the error instead
|
||||
if let Err(err) = self.async_scheduler.schedule(future) {
|
||||
tracing::error!("Failed to schedule future: {err}");
|
||||
}
|
||||
}
|
||||
if let Some(stderr) = stderr {
|
||||
let future = async move {
|
||||
let mut reader = futures_lite::io::BufReader::new(stderr);
|
||||
loop {
|
||||
let mut buf = String::new();
|
||||
match reader.read_line(&mut buf).await {
|
||||
Ok(0) => break,
|
||||
Ok(_) => {
|
||||
let mut stream = stream_err.lock().expect("Couldn't lock stream");
|
||||
crate::api::send_to_client(
|
||||
&mut stream,
|
||||
&OutgoingMsg::CallCallback {
|
||||
callback_id,
|
||||
args: Some(Args::Spawn {
|
||||
stdout: None,
|
||||
stderr: Some(buf.trim_end_matches('\n').to_string()),
|
||||
exit_code: None,
|
||||
exit_msg: None,
|
||||
}),
|
||||
},
|
||||
)
|
||||
.expect("Send to client failed"); // TODO: notify instead of crash
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!("child read err: {err}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
if let Err(err) = self.async_scheduler.schedule(future) {
|
||||
tracing::error!("Failed to schedule future: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
let future = async move {
|
||||
match child.status().await {
|
||||
Ok(exit_status) => {
|
||||
let mut stream = stream_exit.lock().expect("Couldn't lock stream");
|
||||
crate::api::send_to_client(
|
||||
&mut stream,
|
||||
&OutgoingMsg::CallCallback {
|
||||
callback_id,
|
||||
args: Some(Args::Spawn {
|
||||
stdout: None,
|
||||
stderr: None,
|
||||
exit_code: exit_status.code(),
|
||||
exit_msg: Some(exit_status.to_string()),
|
||||
}),
|
||||
},
|
||||
)
|
||||
.expect("Send to client failed"); // TODO: notify instead of crash
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!("child wait() err: {err}");
|
||||
}
|
||||
}
|
||||
};
|
||||
if let Err(err) = self.async_scheduler.schedule(future) {
|
||||
tracing::error!("Failed to schedule future: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)]
|
||||
|
|
Loading…
Add table
Reference in a new issue