mirror of
https://github.com/pinnacle-comp/pinnacle.git
synced 2025-01-13 08:01:05 +01:00
Add process spawning to config api
This commit is contained in:
parent
130116ec54
commit
c46159c77a
9 changed files with 304 additions and 25 deletions
10
README.md
10
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.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
35
api/lua/process.lua
Normal 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
|
|
@ -66,6 +66,7 @@ impl PinnacleSocketSource {
|
|||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn send_to_client(
|
||||
stream: &mut UnixStream,
|
||||
msg: &OutgoingMsg,
|
||||
|
|
|
@ -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>,
|
||||
},
|
||||
}
|
||||
|
|
12
src/input.rs
12
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<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
|
||||
}
|
||||
}
|
||||
|
|
162
src/state.rs
162
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<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 {
|
||||
|
|
Loading…
Reference in a new issue