diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index eebf49ac..936b8b5c 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -122,6 +122,7 @@ pub(crate) struct ConfigClient { window_match_handlers: RefCell>, feat_mod_mask: Cell, + feat_show_workspace_on: Cell, } struct ClientMatchHandler { @@ -266,6 +267,7 @@ pub unsafe extern "C" fn init( client_match_handlers: Default::default(), window_match_handlers: Default::default(), feat_mod_mask: Cell::new(false), + feat_show_workspace_on: Cell::new(false), }); let init = unsafe { slice::from_raw_parts(init, size) }; client.handle_init_msg(init); @@ -591,6 +593,18 @@ impl ConfigClient { self.send(&ClientMessage::ShowWorkspace { seat, workspace }); } + pub fn show_workspace_on(&self, seat: Seat, workspace: Workspace, connector: Connector) { + if self.feat_show_workspace_on.get() && connector.connected() { + self.send(&ClientMessage::ShowWorkspaceOn { + seat, + workspace, + connector, + }); + } else { + self.show_workspace(seat, workspace); + } + } + pub fn set_seat_workspace(&self, seat: Seat, workspace: Workspace) { self.send(&ClientMessage::SetSeatWorkspace { seat, workspace }); } @@ -2086,6 +2100,7 @@ impl ConfigClient { match feat { ServerFeature::NONE => {} ServerFeature::MOD_MASK => self.feat_mod_mask.set(true), + ServerFeature::SHOW_WORKSPACE_ON => self.feat_show_workspace_on.set(true), _ => {} } } diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs index 87c14008..02e9fbfd 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -30,6 +30,7 @@ pub struct ServerFeature(u16); impl ServerFeature { pub const NONE: Self = Self(0); pub const MOD_MASK: Self = Self(1); + pub const SHOW_WORKSPACE_ON: Self = Self(2); } #[derive(Serialize, Deserialize, Debug)] @@ -782,6 +783,11 @@ pub enum ClientMessage<'a> { matcher: ClientMatcher, caps: ClientCapabilities, }, + ShowWorkspaceOn { + seat: Seat, + workspace: Workspace, + connector: Connector, + }, } #[derive(Serialize, Deserialize, Debug)] diff --git a/jay-config/src/input.rs b/jay-config/src/input.rs index cebf72f5..61b6c484 100644 --- a/jay-config/src/input.rs +++ b/jay-config/src/input.rs @@ -439,6 +439,15 @@ impl Seat { get!().show_workspace(self, workspace) } + /// Shows the workspace and sets the keyboard focus of the seat to that workspace. + /// + /// If the workspace doesn't currently exist and the connector is connected, the + /// workspace is created on the given connector. If the connector is not connected, + /// the workspace is created on the output that contains the seat's cursor. + pub fn show_workspace_on(self, workspace: Workspace, connector: Connector) { + get!().show_workspace_on(self, workspace, connector) + } + /// Moves the currently focused window to the workspace. pub fn set_workspace(self, workspace: Workspace) { get!().set_seat_workspace(self, workspace) diff --git a/src/config.rs b/src/config.rs index 0c28577e..85724ff8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -280,7 +280,7 @@ impl ConfigProxy { pub fn configure(&self, reload: bool) { self.send(&ServerMessage::Features { - features: vec![ServerFeature::MOD_MASK], + features: vec![ServerFeature::MOD_MASK, ServerFeature::SHOW_WORKSPACE_ON], }); self.send(&ServerMessage::Configure { reload }); } diff --git a/src/config/handler.rs b/src/config/handler.rs index 36934cae..8573532e 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -1006,10 +1006,16 @@ impl ConfigProxyHandler { Ok(()) } - fn handle_show_workspace(&self, seat: Seat, ws: Workspace) -> Result<(), CphError> { + fn handle_show_workspace( + &self, + seat: Seat, + ws: Workspace, + output: Option, + ) -> Result<(), CphError> { let seat = self.get_seat(seat)?; let name = self.get_workspace(ws)?; - self.state.show_workspace(&seat, &name); + let output = output.map(|o| self.get_output_node(o)).transpose()?; + self.state.show_workspace(&seat, &name, output); Ok(()) } @@ -2725,7 +2731,7 @@ impl ConfigProxyHandler { } ClientMessage::GetWorkspace { name } => self.handle_get_workspace(name), ClientMessage::ShowWorkspace { seat, workspace } => self - .handle_show_workspace(seat, workspace) + .handle_show_workspace(seat, workspace, None) .wrn("show_workspace")?, ClientMessage::SetSeatWorkspace { seat, workspace } => self .handle_set_seat_workspace(seat, workspace) @@ -3203,6 +3209,13 @@ impl ConfigProxyHandler { ClientMessage::SetClientMatcherBoundingCapabilities { matcher, caps } => self .handle_set_client_matcher_bounding_capabilities(matcher, caps) .wrn("set_client_matcher_bounding_capabilities")?, + ClientMessage::ShowWorkspaceOn { + seat, + workspace, + connector, + } => self + .handle_show_workspace(seat, workspace, Some(connector)) + .wrn("show_workspace_on")?, } Ok(()) } diff --git a/src/state.rs b/src/state.rs index 935f073b..5b4087a5 100644 --- a/src/state.rs +++ b/src/state.rs @@ -906,11 +906,16 @@ impl State { } } - pub fn show_workspace(&self, seat: &Rc, name: &str) { + pub fn show_workspace( + &self, + seat: &Rc, + name: &str, + output: Option>, + ) { let ws = match self.workspaces.get(name) { Some(ws) => ws, _ => { - let output = seat.get_output(); + let output = output.unwrap_or_else(|| seat.get_output()); if output.is_dummy { log::warn!("Not showing workspace because seat is on dummy output"); return; diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index adab6b6e..6d240d96 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -141,6 +141,7 @@ pub enum Action { }, ShowWorkspace { name: String, + output: Option, }, SimpleCommand { cmd: SimpleCommand, diff --git a/toml-config/src/config/parsers/action.rs b/toml-config/src/config/parsers/action.rs index 1a4423d6..10a2efcd 100644 --- a/toml-config/src/config/parsers/action.rs +++ b/toml-config/src/config/parsers/action.rs @@ -88,6 +88,8 @@ pub enum ActionParserError { JumpToMark(#[source] MarkIdParserError), #[error("Could not parse a copy-mark action")] CopyMark(#[source] MarkIdParserError), + #[error("Could not parse a show-workspace action")] + ShowWorkspace(#[source] OutputMatchParserError), } pub struct ActionParser<'a>(pub &'a Context<'a>); @@ -184,8 +186,15 @@ impl ActionParser<'_> { } fn parse_show_workspace(&mut self, ext: &mut Extractor<'_>) -> ParseResult { - let name = ext.extract(str("name"))?.value.to_string(); - Ok(Action::ShowWorkspace { name }) + let (name, output) = ext.extract((str("name"), opt(val("output"))))?; + let name = name.value.to_string(); + let output = output + .map(|o| { + o.parse_map(&mut OutputMatchParser(self.0)) + .map_spanned_err(ActionParserError::ShowWorkspace) + }) + .transpose()?; + Ok(Action::ShowWorkspace { name, output }) } fn parse_move_to_workspace(&mut self, ext: &mut Extractor<'_>) -> ParseResult { diff --git a/toml-config/src/lib.rs b/toml-config/src/lib.rs index a1eb04d7..cf5bd21b 100644 --- a/toml-config/src/lib.rs +++ b/toml-config/src/lib.rs @@ -235,9 +235,26 @@ impl Action { } Action::Exec { exec } => b.new(move || create_command(&exec).spawn()), Action::SwitchToVt { num } => b.new(move || switch_to_vt(num)), - Action::ShowWorkspace { name } => { + Action::ShowWorkspace { name, output } => { let workspace = get_workspace(&name); - b.new(move || s.show_workspace(workspace)) + let state = state.clone(); + b.new(move || { + let output = 'get_output: { + let Some(output) = &output else { + break 'get_output None; + }; + for connector in connectors() { + if connector.connected() && output.matches(connector, &state) { + break 'get_output Some(connector); + } + } + None + }; + match output { + Some(o) => s.show_workspace_on(workspace, o), + _ => s.show_workspace(workspace), + } + }) } Action::MoveToWorkspace { name } => { let workspace = get_workspace(&name); diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index d6ac7e73..6cf63163 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -125,7 +125,7 @@ ] }, { - "description": "Switches to a workspace.\n\n- Example:\n\n ```toml\n [shortcuts]\n alt-F1 = { type = \"show-workspace\", name = \"1\" }\n ```\n", + "description": "Switches to a workspace.\n\n- Example 1:\n\n ```toml\n [shortcuts]\n alt-F1 = { type = \"show-workspace\", name = \"1\" }\n ```\n\n- Example 2:\n\n ```toml\n [shortcuts]\n alt-F1 = { type = \"show-workspace\", name = \"1\", output.name = \"left\" }\n ```\n", "type": "object", "properties": { "type": { @@ -134,6 +134,10 @@ "name": { "type": "string", "description": "The name of the workspace." + }, + "output": { + "description": "The output to show a newly created workspace on. This has no effect on\nworkspaces that already exist.\n\nIf this is not set, then a new workspace is shown on the output that\ncontains the cursor.\n\nIf multiple outputs match, the workspace is shown on the first matching\noutput.\n", + "$ref": "#/$defs/OutputMatch" } }, "required": [ diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index dd15622f..bc3c3c18 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -232,12 +232,19 @@ This table is a tagged union. The variant is determined by the `type` field. It Switches to a workspace. - - Example: + - Example 1: ```toml [shortcuts] alt-F1 = { type = "show-workspace", name = "1" } ``` + + - Example 2: + + ```toml + [shortcuts] + alt-F1 = { type = "show-workspace", name = "1", output.name = "left" } + ``` The table has the following fields: @@ -247,6 +254,19 @@ This table is a tagged union. The variant is determined by the `type` field. It The value of this field should be a string. + - `output` (optional): + + The output to show a newly created workspace on. This has no effect on + workspaces that already exist. + + If this is not set, then a new workspace is shown on the output that + contains the cursor. + + If multiple outputs match, the workspace is shown on the first matching + output. + + The value of this field should be a [OutputMatch](#types-OutputMatch). + - `move-to-workspace`: Moves the currently focused window to a workspace. diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index 28608a00..d6e29a47 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -239,17 +239,36 @@ Action: description: | Switches to a workspace. - - Example: + - Example 1: ```toml [shortcuts] alt-F1 = { type = "show-workspace", name = "1" } ``` + + - Example 2: + + ```toml + [shortcuts] + alt-F1 = { type = "show-workspace", name = "1", output.name = "left" } + ``` fields: name: description: The name of the workspace. required: true kind: string + output: + description: | + The output to show a newly created workspace on. This has no effect on + workspaces that already exist. + + If this is not set, then a new workspace is shown on the output that + contains the cursor. + + If multiple outputs match, the workspace is shown on the first matching + output. + required: false + ref: OutputMatch move-to-workspace: description: | Moves the currently focused window to a workspace.