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

View file

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

View file

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

View file

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

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(
stream: &mut UnixStream,
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
// 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<Modifiers>,
callback_id: u32,
callback_id: CallbackId,
},
SetMousebind {
button: u8,
},
// Window management
CloseWindow {
#[serde(default)]
client_id: Option<u32>,
},
ToggleFloating {
#[serde(default)]
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 {
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<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.
#[derive(Debug, serde::Serialize, serde::Deserialize)]
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 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<B: Backend> State<B> {
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
}
}

View file

@ -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<B: Backend> State<B> {
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<B: Backend> State<B> {
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<B: Backend> State<B> {
// 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<UnixStream>,
pub stream: Option<Arc<Mutex<UnixStream>>>,
}
impl ApiState {