diff --git a/src/state.rs b/src/state.rs index 0fb13de..c393b0e 100644 --- a/src/state.rs +++ b/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 { pub xdisplay: Option, } -impl State { - 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::>(); - - // 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::() - .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::>(); - 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::>() - }) - }); - 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::>(); - 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, callback_id: Option) { - 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( @@ -957,63 +398,6 @@ fn start_config() -> Result<(), Box> { Ok(()) } -fn start_lua_config() -> Result<(), Box> { - // 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 { pub display: Display>, pub state: State, diff --git a/src/state/api_handlers.rs b/src/state/api_handlers.rs new file mode 100644 index 0000000..4ade93b --- /dev/null +++ b/src/state/api_handlers.rs @@ -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 State { + 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::>(); + + // 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::() + .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::>(); + 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::>() + }) + }); + 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::>(); + 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, callback_id: Option) { + 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}"); + } + } + } +}