mirror of
https://github.com/pinnacle-comp/pinnacle.git
synced 2025-02-05 20:46:27 +01:00
Extract state api handling
This commit is contained in:
parent
924588005c
commit
73d1916403
2 changed files with 574 additions and 620 deletions
624
src/state.rs
624
src/state.rs
|
@ -1,33 +1,30 @@
|
|||
// 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,
|
||||
sync::{Arc, Mutex},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
api::{
|
||||
msg::{Args, CallbackId, Msg, OutgoingMsg, Request, RequestId, RequestResponse},
|
||||
msg::{CallbackId, Msg},
|
||||
PinnacleSocketSource,
|
||||
},
|
||||
cursor::Cursor,
|
||||
focus::FocusState,
|
||||
grab::resize_grab::ResizeSurfaceState,
|
||||
tag::Tag,
|
||||
window::{window_state::LocationRequestState, WindowElement},
|
||||
};
|
||||
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 +52,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,
|
||||
|
@ -117,559 +111,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>(
|
||||
|
@ -957,63 +398,6 @@ fn start_config() -> Result<(), Box<dyn std::error::Error>> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
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
|
||||
});
|
||||
|
||||
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")
|
||||
.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}");
|
||||
|
||||
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}");
|
||||
|
||||
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)?;
|
||||
}
|
||||
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",
|
||||
))?
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CalloopData<B: Backend> {
|
||||
pub display: Display<State<B>>,
|
||||
pub state: State<B>,
|
||||
|
|
570
src/state/api_handlers.rs
Normal file
570
src/state/api_handlers.rs
Normal file
|
@ -0,0 +1,570 @@
|
|||
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)
|
||||
{
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue