From c46159c77ab87a74c221a8e8d2c46306f0a97450 Mon Sep 17 00:00:00 2001 From: Seaotatop Date: Wed, 21 Jun 2023 14:48:38 -0500 Subject: [PATCH] Add process spawning to config api --- README.md | 10 ++- api/lua/client.lua | 4 +- api/lua/example_config.lua | 22 ++++- api/lua/pinnacle.lua | 13 ++- api/lua/process.lua | 35 ++++++++ src/api.rs | 1 + src/api/msg.rs | 70 +++++++++++++++- src/input.rs | 12 ++- src/state.rs | 162 +++++++++++++++++++++++++++++++++++-- 9 files changed, 304 insertions(+), 25 deletions(-) create mode 100644 api/lua/process.lua diff --git a/README.md b/README.md index caf33e0..c38f2a8 100644 --- a/README.md +++ b/README.md @@ -89,20 +89,22 @@ Lua = { ``` into your Lua language server settings. +Doc website soon:tm: + ## Controls The following controls are currently hardcoded: - `Esc`: Stop Pinnacle - `Ctrl + Left Mouse`: Move a window - `Ctrl + Right Mouse`: Resize a window - - `Shift + L`: Open Alacritty - - `Shift + K`: Open Nautilus - - `Shift + J`: Open Kitty - - `Shift + H`: Open Foot The following controls are set in the [`example_config`](api/lua/example_config.lua): - `Ctrl + Alt + C`: Close the currently focused window - `Ctrl + Alt + Space`: Toggle "floating" for the currently focused window + - `Ctrl + Return`: Open Alacritty + - `Ctrl + 1`: Open Kitty + - `Ctrl + 2`: Open Foot + - `Ctrl + 3`: Open Nautilus "Floating" is in quotes because while windows do currently tile themselves, tiled ones can still be moved just like a floating window. Toggling to and from floating will retile all tiled windows. diff --git a/api/lua/client.lua b/api/lua/client.lua index 024ec4f..6af9d8d 100644 --- a/api/lua/client.lua +++ b/api/lua/client.lua @@ -5,7 +5,7 @@ local M = {} function M.close_window(client_id) SendMsg({ CloseWindow = { - client_id = client_id or "nil", + client_id = client_id, }, }) end @@ -15,7 +15,7 @@ end function M.toggle_floating(client_id) SendMsg({ ToggleFloating = { - client_id = client_id or "nil", + client_id = client_id, }, }) end diff --git a/api/lua/example_config.lua b/api/lua/example_config.lua index 1820a0e..7331547 100644 --- a/api/lua/example_config.lua +++ b/api/lua/example_config.lua @@ -1,7 +1,25 @@ require("pinnacle").setup(function(pinnacle) local input = pinnacle.input local client = pinnacle.client + local keys = pinnacle.keys + local process = pinnacle.process - input.keybind({ "Alt", "Ctrl" }, 99, client.close_window) - input.keybind({ "Ctrl", "Alt" }, 32, client.toggle_floating) + input.keybind({ "Alt", "Ctrl" }, keys.c, client.close_window) + input.keybind({ "Ctrl", "Alt" }, keys.space, client.toggle_floating) + + input.keybind({ "Ctrl" }, keys.Return, function() + process.spawn("alacritty", function(stdout, stderr, exit_code, exit_msg) + -- do something with the output here + end) + end) + + input.keybind({ "Ctrl" }, keys.KEY_1, function() + process.spawn("kitty") + end) + input.keybind({ "Ctrl" }, keys.KEY_2, function() + process.spawn("foot") + end) + input.keybind({ "Ctrl" }, keys.KEY_3, function() + process.spawn("nautilus") + end) end) diff --git a/api/lua/pinnacle.lua b/api/lua/pinnacle.lua index 7748462..9937ef5 100644 --- a/api/lua/pinnacle.lua +++ b/api/lua/pinnacle.lua @@ -43,6 +43,8 @@ end local pinnacle = { input = require("input"), client = require("client"), + keys = require("keys"), + process = require("process"), } ---Configure Pinnacle. You should put mostly eveything into the config_func to avoid invalid state. @@ -58,7 +60,7 @@ function M.setup(config_func) path = SOCKET_PATH, }), "Failed to connect to Pinnacle socket") - ---@type fun()[] + ---@type fun(args: table?)[] CallbackTable = {} function SendMsg(data) @@ -82,9 +84,14 @@ function M.setup(config_func) assert(msg_bytes) local tb = msgpack.decode(msg_bytes) + print(msg_bytes) - if tb.CallCallback then - CallbackTable[tb.CallCallback]() + if tb.CallCallback and tb.CallCallback.callback_id then + if tb.CallCallback.args then -- TODO: can just inline + CallbackTable[tb.CallCallback.callback_id](tb.CallCallback.args) + else + CallbackTable[tb.CallCallback.callback_id](nil) + end end end end diff --git a/api/lua/process.lua b/api/lua/process.lua new file mode 100644 index 0000000..1080f07 --- /dev/null +++ b/api/lua/process.lua @@ -0,0 +1,35 @@ +local process = {} + +---Spawn a process with an optional callback for its stdout and stderr. +---@param command string|string[] The command as one whole string or a table of each of its arguments +---@param callback fun(stdout: string?, stderr: string?, exit_code: integer?, exit_msg: string?)? A callback to do something whenever the process's stdout or stderr print a line. Only one will be non-nil at a time. +function process.spawn(command, callback) + ---@type integer|nil + local callback_id = nil + + if callback ~= nil then + table.insert(CallbackTable, function(args) + local args = args or {} + callback(args.stdout, args.stderr, args.exit_code, args.exit_msg) + end) + callback_id = #CallbackTable + end + + local command_str = command + local command = command + if type(command_str) == "string" then + command = {} + for i in string.gmatch(command_str, "%S+") do + table.insert(command, i) + end + end + + SendMsg({ + Spawn = { + command = command, + callback_id = callback_id, + }, + }) +end + +return process diff --git a/src/api.rs b/src/api.rs index 3a0f2eb..1bbeead 100644 --- a/src/api.rs +++ b/src/api.rs @@ -66,6 +66,7 @@ impl PinnacleSocketSource { } } +#[must_use] pub fn send_to_client( stream: &mut UnixStream, msg: &OutgoingMsg, diff --git a/src/api/msg.rs b/src/api/msg.rs index ba3966b..d120542 100644 --- a/src/api/msg.rs +++ b/src/api/msg.rs @@ -1,25 +1,49 @@ // The MessagePack format for these is a one-element map where the element's key is the enum name and its // value is a map of the enum's values +#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, Copy)] +pub struct CallbackId(pub u32); + #[derive(Debug, serde::Serialize, serde::Deserialize)] pub enum Msg { + // Input SetKeybind { key: u32, modifiers: Vec, - callback_id: u32, + callback_id: CallbackId, }, SetMousebind { button: u8, }, + + // Window management CloseWindow { + #[serde(default)] client_id: Option, }, ToggleFloating { + #[serde(default)] client_id: Option, }, + + // Process management + /// Spawn a program with an optional callback. + Spawn { + command: Vec, + #[serde(default)] + callback_id: Option, + }, + + /// Run a command using the optionally specified shell and callback. + SpawnShell { + shell: Option, + command: Vec, + #[serde(default)] + callback_id: Option, + }, } -#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[derive(Debug, PartialEq, Eq, Copy, Clone, serde::Serialize, serde::Deserialize)] pub enum Modifiers { Shift = 0b0000_0001, Ctrl = 0b0000_0010, @@ -28,7 +52,7 @@ pub enum Modifiers { } /// A bitmask of [Modifiers] for the purpose of hashing. -#[derive(PartialEq, Eq, Hash)] +#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)] pub struct ModifierMask(u8); impl> From for ModifierMask { @@ -42,8 +66,46 @@ impl> From for ModifierMask { } } +impl ModifierMask { + pub fn values(self) -> Vec { + let mut res = Vec::::new(); + if self.0 & Modifiers::Shift as u8 == Modifiers::Shift as u8 { + res.push(Modifiers::Shift); + } + if self.0 & Modifiers::Ctrl as u8 == Modifiers::Ctrl as u8 { + res.push(Modifiers::Ctrl); + } + if self.0 & Modifiers::Alt as u8 == Modifiers::Alt as u8 { + res.push(Modifiers::Alt); + } + if self.0 & Modifiers::Super as u8 == Modifiers::Super as u8 { + res.push(Modifiers::Super); + } + res + } +} + /// Messages sent from the server to the client. #[derive(Debug, serde::Serialize, serde::Deserialize)] pub enum OutgoingMsg { - CallCallback(u32), + CallCallback { + callback_id: CallbackId, + #[serde(default)] + args: Option, + }, +} + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[serde(untagged)] +pub enum Args { + Spawn { + #[serde(default)] + stdout: Option, + #[serde(default)] + stderr: Option, + #[serde(default)] + exit_code: Option, + #[serde(default)] + exit_msg: Option, + }, } diff --git a/src/input.rs b/src/input.rs index f9706a4..5eb0713 100644 --- a/src/input.rs +++ b/src/input.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use crate::api::msg::{ModifierMask, Modifiers, OutgoingMsg}; +use crate::api::msg::{CallbackId, ModifierMask, Modifiers, OutgoingMsg}; use smithay::{ backend::input::{ AbsolutePositionEvent, Axis, AxisSource, ButtonState, Event, InputBackend, InputEvent, @@ -266,9 +266,13 @@ impl State { Some(400) => "foot", Some(callback_id) => { if let Some(stream) = self.api_state.stream.as_mut() { - if let Err(err) = - crate::api::send_to_client(stream, &OutgoingMsg::CallCallback(callback_id)) - { + if let Err(err) = crate::api::send_to_client( + &mut self.api_state.stream.as_ref().unwrap().lock().unwrap(), + &OutgoingMsg::CallCallback { + callback_id: CallbackId(callback_id), + args: None, + }, + ) { // TODO: print error } } diff --git a/src/state.rs b/src/state.rs index 12849f4..66bf1e0 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,11 +1,17 @@ use std::{ error::Error, + ffi::OsString, + io::{BufRead, BufReader}, os::{fd::AsRawFd, unix::net::UnixStream}, - sync::Arc, + process::Stdio, + sync::{Arc, Mutex}, }; use crate::{ - api::{msg::Msg, PinnacleSocketSource}, + api::{ + msg::{Args, CallbackId, Msg, OutgoingMsg}, + PinnacleSocketSource, + }, focus::FocusState, }; use smithay::{ @@ -122,10 +128,11 @@ impl State { data.state .input_state .keybinds - .insert((modifiers.into(), key), callback_id); + .insert((modifiers.into(), key), callback_id.0); } Msg::SetMousebind { button } => todo!(), Msg::CloseWindow { client_id } => { + // TODO: client_id tracing::info!("CloseWindow {:?}", client_id); if let Some(window) = data.state.focus_state.current_focus() { window.toplevel().send_close(); @@ -137,6 +144,140 @@ impl State { crate::window::toggle_floating(&mut data.state, &window); } } + + Msg::Spawn { + command, + callback_id, + } => { + let mut command = command.into_iter().peekable(); + if command.peek().is_none() { + // TODO: notify that command was nothing + return; + } + + // TODO: may need to set env for WAYLAND_DISPLAY + let mut child = + std::process::Command::new(OsString::from(command.next().unwrap())) + .env("WAYLAND_DISPLAY", data.state.socket_name.clone()) + .stdin(if callback_id.is_some() { + Stdio::piped() + } else { + 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() + .unwrap(); // TODO: handle unwrap + + // TODO: find a way to make this hellish code look better, deal with unwraps + if let Some(callback_id) = callback_id { + let stdout = child.stdout.take().unwrap(); + let stderr = child.stderr.take().unwrap(); + let stream = data.state.api_state.stream.as_ref().unwrap().clone(); + // data.state + // .api_state + // .stream + // .replace(stream.try_clone().unwrap()); + let stream2 = stream.clone(); + let stream3 = stream.clone(); + std::thread::spawn(move || { + // TODO: maybe make this not a thread? + let mut reader = BufReader::new(stdout); + loop { + let mut buf = String::new(); + match reader.read_line(&mut buf) { + Ok(0) => break, // EOF + Ok(_) => { + let mut stream = stream.lock().unwrap(); + 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, + }), + }, + ) + .unwrap(); + } + Err(err) => { + tracing::error!("child read err: {err}"); + break; + } + } + } + }); + std::thread::spawn(move || { + let mut reader = BufReader::new(stderr); + loop { + let mut buf = String::new(); + match reader.read_line(&mut buf) { + Ok(0) => break, // EOF + Ok(_) => { + let mut stream = stream2.lock().unwrap(); + 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, + }), + }, + ) + .unwrap(); + } + Err(err) => { + tracing::error!("child read err: {err}"); + break; + } + } + } + }); + std::thread::spawn(move || match child.wait() { + Ok(exit_status) => { + let mut stream = stream3.lock().unwrap(); + 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()), + }), + }, + ) + .unwrap() + } + Err(err) => { + tracing::warn!("child wait() err: {err}"); + } + }); + } + } + + // TODO: add the rest + _ => (), }; } Event::Closed => todo!(), @@ -145,8 +286,17 @@ impl State { // We want to replace the client if a new one pops up // INFO: this source try_clone()s the stream loop_handle.insert_source(PinnacleSocketSource::new(tx_channel)?, |stream, _, data| { - if let Some(old_stream) = data.state.api_state.stream.replace(stream) { - old_stream.shutdown(std::net::Shutdown::Both).unwrap(); + if let Some(old_stream) = data + .state + .api_state + .stream + .replace(Arc::new(Mutex::new(stream))) + { + old_stream + .lock() + .unwrap() + .shutdown(std::net::Shutdown::Both) + .unwrap(); } })?; @@ -280,7 +430,7 @@ pub fn take_presentation_feedback( #[derive(Default)] pub struct ApiState { - pub stream: Option, + pub stream: Option>>, } impl ApiState {