Add process spawning to config api

This commit is contained in:
Seaotatop 2023-06-21 14:48:38 -05:00
parent 130116ec54
commit c46159c77a
9 changed files with 304 additions and 25 deletions

View file

@ -89,20 +89,22 @@ Lua = {
``` ```
into your Lua language server settings. into your Lua language server settings.
Doc website soon:tm:
## Controls ## Controls
The following controls are currently hardcoded: The following controls are currently hardcoded:
- `Esc`: Stop Pinnacle - `Esc`: Stop Pinnacle
- `Ctrl + Left Mouse`: Move a window - `Ctrl + Left Mouse`: Move a window
- `Ctrl + Right Mouse`: Resize 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): The following controls are set in the [`example_config`](api/lua/example_config.lua):
- `Ctrl + Alt + C`: Close the currently focused window - `Ctrl + Alt + C`: Close the currently focused window
- `Ctrl + Alt + Space`: Toggle "floating" for 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. "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.

View file

@ -5,7 +5,7 @@ local M = {}
function M.close_window(client_id) function M.close_window(client_id)
SendMsg({ SendMsg({
CloseWindow = { CloseWindow = {
client_id = client_id or "nil", client_id = client_id,
}, },
}) })
end end
@ -15,7 +15,7 @@ end
function M.toggle_floating(client_id) function M.toggle_floating(client_id)
SendMsg({ SendMsg({
ToggleFloating = { ToggleFloating = {
client_id = client_id or "nil", client_id = client_id,
}, },
}) })
end end

View file

@ -1,7 +1,25 @@
require("pinnacle").setup(function(pinnacle) require("pinnacle").setup(function(pinnacle)
local input = pinnacle.input local input = pinnacle.input
local client = pinnacle.client local client = pinnacle.client
local keys = pinnacle.keys
local process = pinnacle.process
input.keybind({ "Alt", "Ctrl" }, 99, client.close_window) input.keybind({ "Alt", "Ctrl" }, keys.c, client.close_window)
input.keybind({ "Ctrl", "Alt" }, 32, client.toggle_floating) 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) end)

View file

@ -43,6 +43,8 @@ end
local pinnacle = { local pinnacle = {
input = require("input"), input = require("input"),
client = require("client"), client = require("client"),
keys = require("keys"),
process = require("process"),
} }
---Configure Pinnacle. You should put mostly eveything into the config_func to avoid invalid state. ---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, path = SOCKET_PATH,
}), "Failed to connect to Pinnacle socket") }), "Failed to connect to Pinnacle socket")
---@type fun()[] ---@type fun(args: table?)[]
CallbackTable = {} CallbackTable = {}
function SendMsg(data) function SendMsg(data)
@ -82,9 +84,14 @@ function M.setup(config_func)
assert(msg_bytes) assert(msg_bytes)
local tb = msgpack.decode(msg_bytes) local tb = msgpack.decode(msg_bytes)
print(msg_bytes)
if tb.CallCallback then if tb.CallCallback and tb.CallCallback.callback_id then
CallbackTable[tb.CallCallback]() 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 end
end end

35
api/lua/process.lua Normal file
View file

@ -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

View file

@ -66,6 +66,7 @@ impl PinnacleSocketSource {
} }
} }
#[must_use]
pub fn send_to_client( pub fn send_to_client(
stream: &mut UnixStream, stream: &mut UnixStream,
msg: &OutgoingMsg, msg: &OutgoingMsg,

View file

@ -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 // 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 // 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)] #[derive(Debug, serde::Serialize, serde::Deserialize)]
pub enum Msg { pub enum Msg {
// Input
SetKeybind { SetKeybind {
key: u32, key: u32,
modifiers: Vec<Modifiers>, modifiers: Vec<Modifiers>,
callback_id: u32, callback_id: CallbackId,
}, },
SetMousebind { SetMousebind {
button: u8, button: u8,
}, },
// Window management
CloseWindow { CloseWindow {
#[serde(default)]
client_id: Option<u32>, client_id: Option<u32>,
}, },
ToggleFloating { ToggleFloating {
#[serde(default)]
client_id: Option<u32>, client_id: Option<u32>,
}, },
// Process management
/// Spawn a program with an optional callback.
Spawn {
command: Vec<String>,
#[serde(default)]
callback_id: Option<CallbackId>,
},
/// Run a command using the optionally specified shell and callback.
SpawnShell {
shell: Option<String>,
command: Vec<String>,
#[serde(default)]
callback_id: Option<CallbackId>,
},
} }
#[derive(Debug, serde::Serialize, serde::Deserialize)] #[derive(Debug, PartialEq, Eq, Copy, Clone, serde::Serialize, serde::Deserialize)]
pub enum Modifiers { pub enum Modifiers {
Shift = 0b0000_0001, Shift = 0b0000_0001,
Ctrl = 0b0000_0010, Ctrl = 0b0000_0010,
@ -28,7 +52,7 @@ pub enum Modifiers {
} }
/// A bitmask of [Modifiers] for the purpose of hashing. /// A bitmask of [Modifiers] for the purpose of hashing.
#[derive(PartialEq, Eq, Hash)] #[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)]
pub struct ModifierMask(u8); pub struct ModifierMask(u8);
impl<T: IntoIterator<Item = Modifiers>> From<T> for ModifierMask { impl<T: IntoIterator<Item = Modifiers>> From<T> for ModifierMask {
@ -42,8 +66,46 @@ impl<T: IntoIterator<Item = Modifiers>> From<T> for ModifierMask {
} }
} }
impl ModifierMask {
pub fn values(self) -> Vec<Modifiers> {
let mut res = Vec::<Modifiers>::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. /// Messages sent from the server to the client.
#[derive(Debug, serde::Serialize, serde::Deserialize)] #[derive(Debug, serde::Serialize, serde::Deserialize)]
pub enum OutgoingMsg { pub enum OutgoingMsg {
CallCallback(u32), CallCallback {
callback_id: CallbackId,
#[serde(default)]
args: Option<Args>,
},
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
pub enum Args {
Spawn {
#[serde(default)]
stdout: Option<String>,
#[serde(default)]
stderr: Option<String>,
#[serde(default)]
exit_code: Option<i32>,
#[serde(default)]
exit_msg: Option<String>,
},
} }

View file

@ -1,6 +1,6 @@
use std::collections::HashMap; use std::collections::HashMap;
use crate::api::msg::{ModifierMask, Modifiers, OutgoingMsg}; use crate::api::msg::{CallbackId, ModifierMask, Modifiers, OutgoingMsg};
use smithay::{ use smithay::{
backend::input::{ backend::input::{
AbsolutePositionEvent, Axis, AxisSource, ButtonState, Event, InputBackend, InputEvent, AbsolutePositionEvent, Axis, AxisSource, ButtonState, Event, InputBackend, InputEvent,
@ -266,9 +266,13 @@ impl<B: Backend> State<B> {
Some(400) => "foot", Some(400) => "foot",
Some(callback_id) => { Some(callback_id) => {
if let Some(stream) = self.api_state.stream.as_mut() { if let Some(stream) = self.api_state.stream.as_mut() {
if let Err(err) = if let Err(err) = crate::api::send_to_client(
crate::api::send_to_client(stream, &OutgoingMsg::CallCallback(callback_id)) &mut self.api_state.stream.as_ref().unwrap().lock().unwrap(),
{ &OutgoingMsg::CallCallback {
callback_id: CallbackId(callback_id),
args: None,
},
) {
// TODO: print error // TODO: print error
} }
} }

View file

@ -1,11 +1,17 @@
use std::{ use std::{
error::Error, error::Error,
ffi::OsString,
io::{BufRead, BufReader},
os::{fd::AsRawFd, unix::net::UnixStream}, os::{fd::AsRawFd, unix::net::UnixStream},
sync::Arc, process::Stdio,
sync::{Arc, Mutex},
}; };
use crate::{ use crate::{
api::{msg::Msg, PinnacleSocketSource}, api::{
msg::{Args, CallbackId, Msg, OutgoingMsg},
PinnacleSocketSource,
},
focus::FocusState, focus::FocusState,
}; };
use smithay::{ use smithay::{
@ -122,10 +128,11 @@ impl<B: Backend> State<B> {
data.state data.state
.input_state .input_state
.keybinds .keybinds
.insert((modifiers.into(), key), callback_id); .insert((modifiers.into(), key), callback_id.0);
} }
Msg::SetMousebind { button } => todo!(), Msg::SetMousebind { button } => todo!(),
Msg::CloseWindow { client_id } => { Msg::CloseWindow { client_id } => {
// TODO: client_id
tracing::info!("CloseWindow {:?}", client_id); tracing::info!("CloseWindow {:?}", client_id);
if let Some(window) = data.state.focus_state.current_focus() { if let Some(window) = data.state.focus_state.current_focus() {
window.toplevel().send_close(); window.toplevel().send_close();
@ -137,6 +144,140 @@ impl<B: Backend> State<B> {
crate::window::toggle_floating(&mut data.state, &window); 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!(), Event::Closed => todo!(),
@ -145,8 +286,17 @@ impl<B: Backend> State<B> {
// We want to replace the client if a new one pops up // We want to replace the client if a new one pops up
// INFO: this source try_clone()s the stream // INFO: this source try_clone()s the stream
loop_handle.insert_source(PinnacleSocketSource::new(tx_channel)?, |stream, _, data| { loop_handle.insert_source(PinnacleSocketSource::new(tx_channel)?, |stream, _, data| {
if let Some(old_stream) = data.state.api_state.stream.replace(stream) { if let Some(old_stream) = data
old_stream.shutdown(std::net::Shutdown::Both).unwrap(); .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)] #[derive(Default)]
pub struct ApiState { pub struct ApiState {
pub stream: Option<UnixStream>, pub stream: Option<Arc<Mutex<UnixStream>>>,
} }
impl ApiState { impl ApiState {