diff --git a/.github/workflows/toml-spec.yml b/.github/workflows/toml-spec.yml index 3a82660d..a00fa052 100644 --- a/.github/workflows/toml-spec.yml +++ b/.github/workflows/toml-spec.yml @@ -11,7 +11,7 @@ env: CARGO_TERM_COLOR: always jobs: - rustfmt: + toml-spec: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index 22fd8561..6915f056 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -4,7 +4,7 @@ use { crate::{ _private::{ bincode_ops, - ipc::{ClientMessage, InitMessage, Response, ServerMessage}, + ipc::{ClientMessage, InitMessage, Response, ServerMessage, WorkspaceSource}, logging, Config, ConfigEntry, ConfigEntryGen, PollableId, WireMode, VERSION, }, exec::Command, @@ -421,6 +421,13 @@ impl Client { self.send(&ClientMessage::DisablePointerConstraint { seat }); } + pub fn move_to_output(&self, workspace: WorkspaceSource, connector: Connector) { + self.send(&ClientMessage::MoveToOutput { + workspace, + connector, + }); + } + pub fn set_fullscreen(&self, seat: Seat, fullscreen: bool) { self.send(&ClientMessage::SetFullscreen { seat, fullscreen }); } diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs index 377bd3bc..07c1f7ed 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -424,6 +424,16 @@ pub enum ClientMessage<'a> { SetIdle { timeout: Duration, }, + MoveToOutput { + workspace: WorkspaceSource, + connector: Connector, + }, +} + +#[derive(Serialize, Deserialize, Debug)] +pub enum WorkspaceSource { + Seat(Seat), + Explicit(Workspace), } #[derive(Serialize, Deserialize, Debug)] diff --git a/jay-config/src/input.rs b/jay-config/src/input.rs index 58666aee..b0db1cdd 100644 --- a/jay-config/src/input.rs +++ b/jay-config/src/input.rs @@ -8,7 +8,8 @@ use { input::{acceleration::AccelProfile, capability::Capability}, keyboard::Keymap, Axis, Direction, ModifiedKeySym, Workspace, - _private::DEFAULT_SEAT_NAME, + _private::{ipc::WorkspaceSource, DEFAULT_SEAT_NAME}, + video::Connector, }, serde::{Deserialize, Serialize}, std::time::Duration, @@ -319,6 +320,11 @@ impl Seat { pub fn disable_pointer_constraint(self) { get!().disable_pointer_constraint(self) } + + /// Moves the currently focused workspace to another output. + pub fn move_to_output(self, connector: Connector) { + get!().move_to_output(WorkspaceSource::Seat(self), connector); + } } /// Returns all seats. diff --git a/jay-config/src/lib.rs b/jay-config/src/lib.rs index 5881821c..83543401 100644 --- a/jay-config/src/lib.rs +++ b/jay-config/src/lib.rs @@ -40,7 +40,7 @@ )] use { - crate::keyboard::ModifiedKeySym, + crate::{_private::ipc::WorkspaceSource, keyboard::ModifiedKeySym, video::Connector}, serde::{Deserialize, Serialize}, std::{ fmt::{Debug, Display, Formatter}, @@ -159,6 +159,13 @@ impl Workspace { let get = get!(); get.set_workspace_capture(self, !get.get_workspace_capture(self)); } + + /// Moves this workspace to another output. + /// + /// This has no effect if the workspace is not currently being shown. + pub fn move_to_output(self, output: Connector) { + get!().move_to_output(WorkspaceSource::Explicit(self), output); + } } /// Returns the workspace with the given name. diff --git a/src/compositor.rs b/src/compositor.rs index 8f48948c..8e4e5fd6 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -440,9 +440,8 @@ fn create_dummy_output(state: &Rc) { title_texture: Cell::new(None), attention_requests: Default::default(), }); - dummy_workspace.output_link.set(Some( - dummy_output.workspaces.add_last(dummy_workspace.clone()), - )); + *dummy_workspace.output_link.borrow_mut() = + Some(dummy_output.workspaces.add_last(dummy_workspace.clone())); dummy_output.show_workspace(&dummy_workspace); state.dummy_output.set(Some(dummy_output)); } diff --git a/src/config/handler.rs b/src/config/handler.rs index fe76c274..117938c2 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -12,7 +12,10 @@ use { scale::Scale, state::{ConnectorData, DeviceHandlerData, DrmDevData, OutputData, State}, theme::{Color, ThemeSized, DEFAULT_FONT}, - tree::{ContainerNode, ContainerSplit, FloatNode, Node, NodeVisitorBase, OutputNode}, + tree::{ + move_ws_to_output, ContainerNode, ContainerSplit, FloatNode, Node, NodeVisitorBase, + OutputNode, WsMoveConfig, + }, utils::{ asyncevent::AsyncEvent, copyhashmap::CopyHashMap, @@ -29,7 +32,7 @@ use { jay_config::{ _private::{ bincode_ops, - ipc::{ClientMessage, Response, ServerMessage}, + ipc::{ClientMessage, Response, ServerMessage, WorkspaceSource}, PollableId, WireMode, }, input::{ @@ -753,6 +756,45 @@ impl ConfigProxyHandler { Ok(()) } + fn handle_move_to_output( + &self, + workspace: WorkspaceSource, + connector: Connector, + ) -> Result<(), CphError> { + let output = self.get_output(connector)?; + let ws = match workspace { + WorkspaceSource::Explicit(ws) => { + let name = self.get_workspace(ws)?; + match self.state.workspaces.get(name.as_str()) { + Some(ws) => ws, + _ => return Ok(()), + } + } + WorkspaceSource::Seat(s) => match self.get_seat(s)?.get_output().workspace.get() { + Some(ws) => ws, + _ => return Ok(()), + }, + }; + if ws.is_dummy || output.node.is_dummy { + return Ok(()); + } + if ws.output.get().id == output.node.id { + return Ok(()); + } + let link = match &*ws.output_link.borrow() { + None => return Ok(()), + Some(l) => l.to_ref(), + }; + let config = WsMoveConfig { + make_visible_if_empty: true, + source_is_destroyed: false, + }; + move_ws_to_output(&link, &output.node, config); + self.state.tree_changed(); + self.state.damage(); + Ok(()) + } + fn handle_set_idle(&self, timeout: Duration) { self.state.idle.set_timeout(timeout); } @@ -1676,6 +1718,12 @@ impl ConfigProxyHandler { .handle_get_input_device_devnode(device) .wrn("get_input_device_devnode")?, ClientMessage::SetIdle { timeout } => self.handle_set_idle(timeout), + ClientMessage::MoveToOutput { + workspace, + connector, + } => self + .handle_move_to_output(workspace, connector) + .wrn("move_to_output")?, } Ok(()) } diff --git a/src/ifs/wl_surface.rs b/src/ifs/wl_surface.rs index ca4079cf..47d9a209 100644 --- a/src/ifs/wl_surface.rs +++ b/src/ifs/wl_surface.rs @@ -1015,6 +1015,9 @@ impl WlSurface { } self.send_seat_release_events(); self.seat_state.destroy_node(self); + if self.visible.get() { + self.client.state.damage(); + } } pub fn set_content_type(&self, content_type: Option) { diff --git a/src/state.rs b/src/state.rs index 66942fe2..014387d4 100644 --- a/src/state.rs +++ b/src/state.rs @@ -591,6 +591,7 @@ impl State { ws.flush_jay_workspaces(); output.schedule_update_render_data(); self.tree_changed(); + self.damage(); // let seats = self.globals.seats.lock(); // for seat in seats.values() { // seat.workspace_changed(&output); diff --git a/src/tasks/connector.rs b/src/tasks/connector.rs index 6d78b454..a88060c6 100644 --- a/src/tasks/connector.rs +++ b/src/tasks/connector.rs @@ -3,11 +3,12 @@ use { backend::{Connector, ConnectorEvent, ConnectorId, MonitorInfo}, ifs::wl_output::{OutputId, PersistentOutputState, WlOutputGlobal}, state::{ConnectorData, OutputData, State}, - tree::{OutputNode, OutputRenderData}, + tree::{move_ws_to_output, OutputNode, OutputRenderData, WsMoveConfig}, utils::{asyncevent::AsyncEvent, clonecell::CloneCell}, }, std::{ cell::{Cell, RefCell}, + collections::VecDeque, rc::Rc, }, }; @@ -156,6 +157,8 @@ impl ConnectorHandler { node: on.clone(), }); self.state.outputs.set(self.id, output_data); + global.node.set(Some(on.clone())); + let mut ws_to_move = VecDeque::new(); if self.state.outputs.len() == 1 { let seats = self.state.globals.seats.lock(); let pos = global.pos.get(); @@ -164,59 +167,45 @@ impl ConnectorHandler { for seat in seats.values() { seat.set_position(x, y); } - } - global.node.set(Some(on.clone())); - if let Some(config) = self.state.config.get() { - config.connector_connected(self.id); - } - { - for source in self.state.outputs.lock().values() { - if source.node.id == on.id { + let dummy = self.state.dummy_output.get().unwrap(); + for ws in dummy.workspaces.iter() { + if ws.is_dummy { continue; } - let mut ws_to_move = vec![]; - for ws in source.node.workspaces.iter() { - if ws.is_dummy { - continue; - } - if ws.desired_output.get() == global.output_id { - ws_to_move.push(ws.clone()); - } - } - for ws in ws_to_move { - ws.set_output(&on); - on.workspaces.add_last_existing(&ws); - if ws.visible_on_desired_output.get() && on.workspace.is_none() { - on.show_workspace(&ws); - } else { - ws.set_visible(false); - } - ws.flush_jay_workspaces(); - if let Some(visible) = source.node.workspace.get() { - if visible.id == ws.id { - source.node.workspace.take(); - } - } - } - if source.node.workspace.is_none() { - if let Some(ws) = source.node.workspaces.first() { - source.node.show_workspace(&ws); - ws.flush_jay_workspaces(); - } - } - source.node.schedule_update_render_data(); + ws_to_move.push_back(ws); + } + } + for source in self.state.outputs.lock().values() { + if source.node.id == on.id { + continue; } - if on.workspace.is_none() { - if let Some(ws) = on.workspaces.first() { - on.show_workspace(&ws); - ws.flush_jay_workspaces(); + for ws in source.node.workspaces.iter() { + if ws.is_dummy { + continue; + } + if ws.desired_output.get() == global.output_id { + ws_to_move.push_back(ws.clone()); } } } - on.schedule_update_render_data(); + while let Some(ws) = ws_to_move.pop_front() { + let make_visible = (ws.visible_on_desired_output.get() + && ws.desired_output.get() == output_id) + || ws_to_move.is_empty(); + let config = WsMoveConfig { + make_visible_if_empty: make_visible, + source_is_destroyed: false, + }; + move_ws_to_output(&ws, &on, config); + } + if let Some(config) = self.state.config.get() { + config.connector_connected(self.id); + } self.state.root.outputs.set(self.id, on.clone()); self.state.root.update_extents(); self.state.add_global(&global); + self.state.tree_changed(); + self.state.damage(); 'outer: loop { while let Some(event) = self.data.connector.event() { match event { @@ -261,31 +250,19 @@ impl ConnectorHandler { surface.send_closed(); } } - let mut target_is_dummy = false; let target = match self.state.outputs.lock().values().next() { Some(o) => o.node.clone(), - _ => { - target_is_dummy = true; - self.state.dummy_output.get().unwrap() - } + _ => self.state.dummy_output.get().unwrap(), }; - if !on.workspaces.is_empty() { - for ws in on.workspaces.iter() { - let is_visible = - !target_is_dummy && target.workspaces.is_empty() && ws.visible.get(); + for ws in on.workspaces.iter() { + if ws.desired_output.get() == output_id { ws.visible_on_desired_output.set(ws.visible.get()); - ws.set_output(&target); - target.workspaces.add_last_existing(&ws); - if is_visible { - target.show_workspace(&ws); - } else if ws.visible.get() { - ws.set_visible(false); - } - ws.flush_jay_workspaces(); } - target.schedule_update_render_data(); - self.state.tree_changed(); - self.state.damage(); + let config = WsMoveConfig { + make_visible_if_empty: ws.visible.get(), + source_is_destroyed: true, + }; + move_ws_to_output(&ws, &target, config); } let seats = self.state.globals.seats.lock(); for seat in seats.values() { @@ -300,5 +277,7 @@ impl ConnectorHandler { self.state .remove_output_scale(on.global.persistent.scale.get()); let _ = self.state.remove_global(&*global); + self.state.tree_changed(); + self.state.damage(); } } diff --git a/src/tree/output.rs b/src/tree/output.rs index b1f0c7d8..b0e274fe 100644 --- a/src/tree/output.rs +++ b/src/tree/output.rs @@ -337,7 +337,7 @@ impl OutputNode { stacked: Default::default(), seat_state: Default::default(), name: name.to_string(), - output_link: Cell::new(None), + output_link: Default::default(), visible: Cell::new(false), fullscreen: Default::default(), visible_on_desired_output: Cell::new(false), @@ -347,8 +347,7 @@ impl OutputNode { title_texture: Default::default(), attention_requests: Default::default(), }); - ws.output_link - .set(Some(self.workspaces.add_last(ws.clone()))); + *ws.output_link.borrow_mut() = Some(self.workspaces.add_last(ws.clone())); self.state.workspaces.set(name.to_string(), ws.clone()); if self.workspace.is_none() { self.show_workspace(&ws); diff --git a/src/tree/workspace.rs b/src/tree/workspace.rs index 7604df68..9471576a 100644 --- a/src/tree/workspace.rs +++ b/src/tree/workspace.rs @@ -19,12 +19,17 @@ use { utils::{ clonecell::CloneCell, copyhashmap::CopyHashMap, - linkedlist::{LinkedList, LinkedNode}, + linkedlist::{LinkedList, LinkedNode, NodeRef}, threshold_counter::ThresholdCounter, }, wire::JayWorkspaceId, }, - std::{cell::Cell, fmt::Debug, ops::Deref, rc::Rc}, + std::{ + cell::{Cell, RefCell}, + fmt::Debug, + ops::Deref, + rc::Rc, + }, }; tree_id!(WorkspaceNodeId); @@ -38,7 +43,7 @@ pub struct WorkspaceNode { pub stacked: LinkedList>, pub seat_state: NodeSeatState, pub name: String, - pub output_link: Cell>>>, + pub output_link: RefCell>>>, pub visible: Cell, pub fullscreen: CloneCell>>, pub visible_on_desired_output: Cell, @@ -52,7 +57,7 @@ pub struct WorkspaceNode { impl WorkspaceNode { pub fn clear(&self) { self.container.set(None); - self.output_link.set(None); + *self.output_link.borrow_mut() = None; self.fullscreen.set(None); self.jay_workspaces.clear(); } @@ -304,3 +309,43 @@ impl ContainingNode for WorkspaceNode { self } } + +pub struct WsMoveConfig { + pub make_visible_if_empty: bool, + pub source_is_destroyed: bool, +} + +pub fn move_ws_to_output( + ws: &NodeRef>, + target: &Rc, + config: WsMoveConfig, +) { + let source = ws.output.get(); + ws.set_output(&target); + target.workspaces.add_last_existing(&ws); + if config.make_visible_if_empty && target.workspace.is_none() && !target.is_dummy { + target.show_workspace(&ws); + } else { + ws.set_visible(false); + } + ws.flush_jay_workspaces(); + if let Some(visible) = source.workspace.get() { + if visible.id == ws.id { + source.workspace.take(); + } + } + if !config.source_is_destroyed && !source.is_dummy { + if source.workspace.is_none() { + if let Some(ws) = source.workspaces.first() { + source.show_workspace(&ws); + ws.flush_jay_workspaces(); + } + } + } + if !target.is_dummy { + target.schedule_update_render_data(); + } + if !source.is_dummy { + source.schedule_update_render_data(); + } +} diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index 8fb5023b..efdcceef 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -22,7 +22,7 @@ use { status::MessageFormat, theme::Color, video::{GfxApi, Transform}, - Axis, Direction, + Axis, Direction, Workspace, }, std::{ error::Error, @@ -53,25 +53,70 @@ pub enum SimpleCommand { #[derive(Debug, Clone)] pub enum Action { - ConfigureConnector { con: ConfigConnector }, - ConfigureDirectScanout { enabled: bool }, - ConfigureDrmDevice { dev: ConfigDrmDevice }, - ConfigureIdle { idle: Duration }, - ConfigureInput { input: Input }, - ConfigureOutput { out: Output }, - Exec { exec: Exec }, - Multi { actions: Vec }, - SetEnv { env: Vec<(String, String)> }, - SetGfxApi { api: GfxApi }, - SetKeymap { map: ConfigKeymap }, - SetLogLevel { level: LogLevel }, - SetRenderDevice { dev: DrmDeviceMatch }, - SetStatus { status: Option }, - SetTheme { theme: Box }, - ShowWorkspace { name: String }, - SimpleCommand { cmd: SimpleCommand }, - SwitchToVt { num: u32 }, - UnsetEnv { env: Vec }, + ConfigureConnector { + con: ConfigConnector, + }, + ConfigureDirectScanout { + enabled: bool, + }, + ConfigureDrmDevice { + dev: ConfigDrmDevice, + }, + ConfigureIdle { + idle: Duration, + }, + ConfigureInput { + input: Input, + }, + ConfigureOutput { + out: Output, + }, + Exec { + exec: Exec, + }, + MoveToWorkspace { + name: String, + }, + Multi { + actions: Vec, + }, + SetEnv { + env: Vec<(String, String)>, + }, + SetGfxApi { + api: GfxApi, + }, + SetKeymap { + map: ConfigKeymap, + }, + SetLogLevel { + level: LogLevel, + }, + SetRenderDevice { + dev: DrmDeviceMatch, + }, + SetStatus { + status: Option, + }, + SetTheme { + theme: Box, + }, + ShowWorkspace { + name: String, + }, + SimpleCommand { + cmd: SimpleCommand, + }, + SwitchToVt { + num: u32, + }, + UnsetEnv { + env: Vec, + }, + MoveToOutput { + workspace: Option, + output: OutputMatch, + }, } #[derive(Debug, Clone, Default)] diff --git a/toml-config/src/config/parsers/action.rs b/toml-config/src/config/parsers/action.rs index b697f82e..16ca5083 100644 --- a/toml-config/src/config/parsers/action.rs +++ b/toml-config/src/config/parsers/action.rs @@ -16,19 +16,24 @@ use { keymap::{KeymapParser, KeymapParserError}, log_level::{LogLevelParser, LogLevelParserError}, output::{OutputParser, OutputParserError}, + output_match::{OutputMatchParser, OutputMatchParserError}, status::{StatusParser, StatusParserError}, theme::{ThemeParser, ThemeParserError}, StringParser, StringParserError, }, + spanned::SpannedErrorExt, Action, }, toml::{ - toml_span::{Span, Spanned, SpannedExt}, + toml_span::{DespanExt, Span, Spanned, SpannedExt}, toml_value::Value, }, }, indexmap::IndexMap, - jay_config::Axis::{Horizontal, Vertical}, + jay_config::{ + get_workspace, + Axis::{Horizontal, Vertical}, + }, thiserror::Error, }; @@ -45,31 +50,33 @@ pub enum ActionParserError { #[error(transparent)] Extract(#[from] ExtractorError), #[error("Could not parse the exec action")] - Exec(#[from] ExecParserError), + Exec(#[source] ExecParserError), #[error("Could not parse the configure-connector action")] - ConfigureConnector(#[from] ConnectorParserError), + ConfigureConnector(#[source] ConnectorParserError), #[error("Could not parse the configure-input action")] - ConfigureInput(#[from] InputParserError), + ConfigureInput(#[source] InputParserError), #[error("Could not parse the configure-output action")] - ConfigureOutput(#[from] OutputParserError), + ConfigureOutput(#[source] OutputParserError), #[error("Could not parse the environment variables")] - Env(#[from] EnvParserError), + Env(#[source] EnvParserError), #[error("Could not parse a set-keymap action")] - SetKeymap(#[from] KeymapParserError), + SetKeymap(#[source] KeymapParserError), #[error("Could not parse a set-status action")] - Status(#[from] StatusParserError), + Status(#[source] StatusParserError), #[error("Could not parse a set-theme action")] - Theme(#[from] ThemeParserError), + Theme(#[source] ThemeParserError), #[error("Could not parse a set-log-level action")] - SetLogLevel(#[from] LogLevelParserError), + SetLogLevel(#[source] LogLevelParserError), #[error("Could not parse a set-gfx-api action")] - GfxApi(#[from] GfxApiParserError), + GfxApi(#[source] GfxApiParserError), #[error("Could not parse a configure-drm-device action")] - DrmDevice(#[from] DrmDeviceParserError), + DrmDevice(#[source] DrmDeviceParserError), #[error("Could not parse a set-render-device action")] - SetRenderDevice(#[from] DrmDeviceMatchParserError), + SetRenderDevice(#[source] DrmDeviceMatchParserError), #[error("Could not parse a configure-idle action")] - ConfigureIdle(#[from] IdleParserError), + ConfigureIdle(#[source] IdleParserError), + #[error("Could not parse a move-to-output action")] + MoveToOutput(#[source] OutputMatchParserError), } pub struct ActionParser<'a>(pub &'a Context<'a>); @@ -117,7 +124,8 @@ impl ActionParser<'_> { fn parse_exec(&mut self, ext: &mut Extractor<'_>) -> ParseResult { let exec = ext .extract(val("exec"))? - .parse_map(&mut ExecParser(self.0))?; + .parse_map(&mut ExecParser(self.0)) + .map_spanned_err(ActionParserError::Exec)?; Ok(Action::Exec { exec }) } @@ -133,41 +141,52 @@ impl ActionParser<'_> { fn parse_move_to_workspace(&mut self, ext: &mut Extractor<'_>) -> ParseResult { let name = ext.extract(str("name"))?.value.to_string(); - Ok(Action::ShowWorkspace { name }) + Ok(Action::MoveToWorkspace { name }) } fn parse_configure_connector(&mut self, ext: &mut Extractor<'_>) -> ParseResult { let con = ext .extract(val("connector"))? - .parse_map(&mut ConnectorParser(self.0))?; + .parse_map(&mut ConnectorParser(self.0)) + .map_spanned_err(ActionParserError::ConfigureConnector)?; Ok(Action::ConfigureConnector { con }) } fn parse_configure_input(&mut self, ext: &mut Extractor<'_>) -> ParseResult { - let input = ext.extract(val("input"))?.parse_map(&mut InputParser { - cx: self.0, - tag_ok: false, - })?; + let input = ext + .extract(val("input"))? + .parse_map(&mut InputParser { + cx: self.0, + tag_ok: false, + }) + .map_spanned_err(ActionParserError::ConfigureInput)?; Ok(Action::ConfigureInput { input }) } fn parse_configure_idle(&mut self, ext: &mut Extractor<'_>) -> ParseResult { let idle = ext .extract(val("idle"))? - .parse_map(&mut IdleParser(self.0))?; + .parse_map(&mut IdleParser(self.0)) + .map_spanned_err(ActionParserError::ConfigureIdle)?; Ok(Action::ConfigureIdle { idle }) } fn parse_configure_output(&mut self, ext: &mut Extractor<'_>) -> ParseResult { - let out = ext.extract(val("output"))?.parse_map(&mut OutputParser { - cx: self.0, - name_ok: false, - })?; + let out = ext + .extract(val("output"))? + .parse_map(&mut OutputParser { + cx: self.0, + name_ok: false, + }) + .map_spanned_err(ActionParserError::ConfigureOutput)?; Ok(Action::ConfigureOutput { out }) } fn parse_set_env(&mut self, ext: &mut Extractor<'_>) -> ParseResult { - let env = ext.extract(val("env"))?.parse_map(&mut EnvParser)?; + let env = ext + .extract(val("env"))? + .parse_map(&mut EnvParser) + .map_spanned_err(ActionParserError::Env)?; Ok(Action::SetEnv { env }) } @@ -195,17 +214,23 @@ impl ActionParser<'_> { } fn parse_set_keymap(&mut self, ext: &mut Extractor<'_>) -> ParseResult { - let map = ext.extract(val("map"))?.parse_map(&mut KeymapParser { - cx: self.0, - definition: false, - })?; + let map = ext + .extract(val("map"))? + .parse_map(&mut KeymapParser { + cx: self.0, + definition: false, + }) + .map_spanned_err(ActionParserError::SetKeymap)?; Ok(Action::SetKeymap { map }) } fn parse_set_status(&mut self, ext: &mut Extractor<'_>) -> ParseResult { let status = match ext.extract(opt(val("status")))? { None => None, - Some(v) => Some(v.parse_map(&mut StatusParser(self.0))?), + Some(v) => Some( + v.parse_map(&mut StatusParser(self.0)) + .map_spanned_err(ActionParserError::Status)?, + ), }; Ok(Action::SetStatus { status }) } @@ -213,26 +238,34 @@ impl ActionParser<'_> { fn parse_set_theme(&mut self, ext: &mut Extractor<'_>) -> ParseResult { let theme = ext .extract(val("theme"))? - .parse_map(&mut ThemeParser(self.0))?; + .parse_map(&mut ThemeParser(self.0)) + .map_spanned_err(ActionParserError::Theme)?; Ok(Action::SetTheme { theme: Box::new(theme), }) } fn parse_set_log_level(&mut self, ext: &mut Extractor<'_>) -> ParseResult { - let level = ext.extract(val("level"))?.parse_map(&mut LogLevelParser)?; + let level = ext + .extract(val("level"))? + .parse_map(&mut LogLevelParser) + .map_spanned_err(ActionParserError::SetLogLevel)?; Ok(Action::SetLogLevel { level }) } fn parse_set_gfx_api(&mut self, ext: &mut Extractor<'_>) -> ParseResult { - let api = ext.extract(val("api"))?.parse_map(&mut GfxApiParser)?; + let api = ext + .extract(val("api"))? + .parse_map(&mut GfxApiParser) + .map_spanned_err(ActionParserError::GfxApi)?; Ok(Action::SetGfxApi { api }) } fn parse_set_render_device(&mut self, ext: &mut Extractor<'_>) -> ParseResult { let dev = ext .extract(val("dev"))? - .parse_map(&mut DrmDeviceMatchParser(self.0))?; + .parse_map(&mut DrmDeviceMatchParser(self.0)) + .map_spanned_err(ActionParserError::SetRenderDevice)?; Ok(Action::SetRenderDevice { dev }) } @@ -242,12 +275,26 @@ impl ActionParser<'_> { } fn parse_configure_drm_device(&mut self, ext: &mut Extractor<'_>) -> ParseResult { - let dev = ext.extract(val("dev"))?.parse_map(&mut DrmDeviceParser { - cx: self.0, - name_ok: false, - })?; + let dev = ext + .extract(val("dev"))? + .parse_map(&mut DrmDeviceParser { + cx: self.0, + name_ok: false, + }) + .map_spanned_err(ActionParserError::DrmDevice)?; Ok(Action::ConfigureDrmDevice { dev }) } + + fn parse_move_to_output(&mut self, ext: &mut Extractor<'_>) -> ParseResult { + let (ws, output) = ext.extract((opt(str("workspace")), val("output")))?; + let output = output + .parse_map(&mut OutputMatchParser(self.0)) + .map_spanned_err(ActionParserError::MoveToOutput)?; + Ok(Action::MoveToOutput { + workspace: ws.despan().map(get_workspace), + output, + }) + } } impl<'a> Parser for ActionParser<'a> { @@ -297,6 +344,7 @@ impl<'a> Parser for ActionParser<'a> { "configure-drm-device" => self.parse_configure_drm_device(&mut ext), "set-render-device" => self.parse_set_render_device(&mut ext), "configure-idle" => self.parse_configure_idle(&mut ext), + "move-to-output" => self.parse_move_to_output(&mut ext), v => { ext.ignore_unused(); return Err(ActionParserError::UnknownType(v.to_string()).spanned(ty.span)); diff --git a/toml-config/src/lib.rs b/toml-config/src/lib.rs index a3ba4adb..edfd840d 100644 --- a/toml-config/src/lib.rs +++ b/toml-config/src/lib.rs @@ -75,6 +75,10 @@ impl Action { let workspace = get_workspace(&name); Box::new(move || s.show_workspace(workspace)) } + Action::MoveToWorkspace { name } => { + let workspace = get_workspace(&name); + Box::new(move || s.set_workspace(workspace)) + } Action::ConfigureConnector { con } => Box::new(move || { for c in connectors() { if con.match_.matches(c) { @@ -150,6 +154,23 @@ impl Action { }) } Action::ConfigureIdle { idle } => Box::new(move || set_idle(Some(idle))), + Action::MoveToOutput { output, workspace } => { + let state = state.clone(); + Box::new(move || { + let output = 'get_output: { + for connector in connectors() { + if connector.connected() && output.matches(connector, &state) { + break 'get_output connector; + } + } + return; + }; + match workspace { + Some(ws) => ws.move_to_output(output), + None => s.move_to_output(output), + } + }) + } } } } diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index 9301b6bd..cdf97b93 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -136,6 +136,27 @@ "name" ] }, + { + "description": "Moves a workspace to a different output.\n\n- Example 1:\n\n ```toml\n [shortcuts]\n alt-F1 = { type = \"move-to-output\", workspace = \"1\", output.name = \"right\" }\n ```\n\n- Example 2:\n\n ```toml\n [shortcuts]\n alt-F1 = { type = \"move-to-output\", output.name = \"right\" }\n ```\n", + "type": "object", + "properties": { + "type": { + "const": "move-to-output" + }, + "workspace": { + "type": "string", + "description": "The name of the workspace.\n\nIf this is omitted, the currently active workspace is moved.\n" + }, + "output": { + "description": "The output to move to.\n\nIf multiple outputs match, the workspace is moved to the first matching\noutput.\n", + "$ref": "#/$defs/OutputMatch" + } + }, + "required": [ + "type", + "output" + ] + }, { "description": "Applies a configuration to connectors.\n\n- Example:\n\n ```toml\n [shortcuts]\n alt-j = { type = \"configure-connector\", connector = { match.name = \"eDP-1\", enabled = false } }\n alt-k = { type = \"configure-connector\", connector = { match.name = \"eDP-1\", enabled = true } }\n ```\n", "type": "object", diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index 28bc1d07..45c6201e 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -214,6 +214,43 @@ This table is a tagged union. The variant is determined by the `type` field. It The value of this field should be a string. +- `move-to-output`: + + Moves a workspace to a different output. + + - Example 1: + + ```toml + [shortcuts] + alt-F1 = { type = "move-to-output", workspace = "1", output.name = "right" } + ``` + + - Example 2: + + ```toml + [shortcuts] + alt-F1 = { type = "move-to-output", output.name = "right" } + ``` + + The table has the following fields: + + - `workspace` (optional): + + The name of the workspace. + + If this is omitted, the currently active workspace is moved. + + The value of this field should be a string. + + - `output` (required): + + The output to move to. + + If multiple outputs match, the workspace is moved to the first matching + output. + + The value of this field should be a [OutputMatch](#types-OutputMatch). + - `configure-connector`: Applies a configuration to connectors. diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index 75822054..458a05d6 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -219,6 +219,39 @@ Action: description: The name of the workspace. required: true kind: string + move-to-output: + description: | + Moves a workspace to a different output. + + - Example 1: + + ```toml + [shortcuts] + alt-F1 = { type = "move-to-output", workspace = "1", output.name = "right" } + ``` + + - Example 2: + + ```toml + [shortcuts] + alt-F1 = { type = "move-to-output", output.name = "right" } + ``` + fields: + workspace: + description: | + The name of the workspace. + + If this is omitted, the currently active workspace is moved. + required: false + kind: string + output: + description: | + The output to move to. + + If multiple outputs match, the workspace is moved to the first matching + output. + required: true + ref: OutputMatch configure-connector: description: | Applies a configuration to connectors.