1
0
Fork 0
forked from wry/wry

Merge pull request #132 from mahkoh/jorth/move-workspace

tree: fix restoration of workspaces to their desired outputs
This commit is contained in:
mahkoh 2024-03-17 13:45:52 +01:00 committed by GitHub
commit 1401697fdc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 453 additions and 144 deletions

View file

@ -11,7 +11,7 @@ env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always
jobs: jobs:
rustfmt: toml-spec:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2

View file

@ -4,7 +4,7 @@ use {
crate::{ crate::{
_private::{ _private::{
bincode_ops, bincode_ops,
ipc::{ClientMessage, InitMessage, Response, ServerMessage}, ipc::{ClientMessage, InitMessage, Response, ServerMessage, WorkspaceSource},
logging, Config, ConfigEntry, ConfigEntryGen, PollableId, WireMode, VERSION, logging, Config, ConfigEntry, ConfigEntryGen, PollableId, WireMode, VERSION,
}, },
exec::Command, exec::Command,
@ -421,6 +421,13 @@ impl Client {
self.send(&ClientMessage::DisablePointerConstraint { seat }); 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) { pub fn set_fullscreen(&self, seat: Seat, fullscreen: bool) {
self.send(&ClientMessage::SetFullscreen { seat, fullscreen }); self.send(&ClientMessage::SetFullscreen { seat, fullscreen });
} }

View file

@ -424,6 +424,16 @@ pub enum ClientMessage<'a> {
SetIdle { SetIdle {
timeout: Duration, timeout: Duration,
}, },
MoveToOutput {
workspace: WorkspaceSource,
connector: Connector,
},
}
#[derive(Serialize, Deserialize, Debug)]
pub enum WorkspaceSource {
Seat(Seat),
Explicit(Workspace),
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]

View file

@ -8,7 +8,8 @@ use {
input::{acceleration::AccelProfile, capability::Capability}, input::{acceleration::AccelProfile, capability::Capability},
keyboard::Keymap, keyboard::Keymap,
Axis, Direction, ModifiedKeySym, Workspace, Axis, Direction, ModifiedKeySym, Workspace,
_private::DEFAULT_SEAT_NAME, _private::{ipc::WorkspaceSource, DEFAULT_SEAT_NAME},
video::Connector,
}, },
serde::{Deserialize, Serialize}, serde::{Deserialize, Serialize},
std::time::Duration, std::time::Duration,
@ -319,6 +320,11 @@ impl Seat {
pub fn disable_pointer_constraint(self) { pub fn disable_pointer_constraint(self) {
get!().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. /// Returns all seats.

View file

@ -40,7 +40,7 @@
)] )]
use { use {
crate::keyboard::ModifiedKeySym, crate::{_private::ipc::WorkspaceSource, keyboard::ModifiedKeySym, video::Connector},
serde::{Deserialize, Serialize}, serde::{Deserialize, Serialize},
std::{ std::{
fmt::{Debug, Display, Formatter}, fmt::{Debug, Display, Formatter},
@ -159,6 +159,13 @@ impl Workspace {
let get = get!(); let get = get!();
get.set_workspace_capture(self, !get.get_workspace_capture(self)); 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. /// Returns the workspace with the given name.

View file

@ -440,9 +440,8 @@ fn create_dummy_output(state: &Rc<State>) {
title_texture: Cell::new(None), title_texture: Cell::new(None),
attention_requests: Default::default(), attention_requests: Default::default(),
}); });
dummy_workspace.output_link.set(Some( *dummy_workspace.output_link.borrow_mut() =
dummy_output.workspaces.add_last(dummy_workspace.clone()), Some(dummy_output.workspaces.add_last(dummy_workspace.clone()));
));
dummy_output.show_workspace(&dummy_workspace); dummy_output.show_workspace(&dummy_workspace);
state.dummy_output.set(Some(dummy_output)); state.dummy_output.set(Some(dummy_output));
} }

View file

@ -12,7 +12,10 @@ use {
scale::Scale, scale::Scale,
state::{ConnectorData, DeviceHandlerData, DrmDevData, OutputData, State}, state::{ConnectorData, DeviceHandlerData, DrmDevData, OutputData, State},
theme::{Color, ThemeSized, DEFAULT_FONT}, 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::{ utils::{
asyncevent::AsyncEvent, asyncevent::AsyncEvent,
copyhashmap::CopyHashMap, copyhashmap::CopyHashMap,
@ -29,7 +32,7 @@ use {
jay_config::{ jay_config::{
_private::{ _private::{
bincode_ops, bincode_ops,
ipc::{ClientMessage, Response, ServerMessage}, ipc::{ClientMessage, Response, ServerMessage, WorkspaceSource},
PollableId, WireMode, PollableId, WireMode,
}, },
input::{ input::{
@ -753,6 +756,45 @@ impl ConfigProxyHandler {
Ok(()) 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) { fn handle_set_idle(&self, timeout: Duration) {
self.state.idle.set_timeout(timeout); self.state.idle.set_timeout(timeout);
} }
@ -1676,6 +1718,12 @@ impl ConfigProxyHandler {
.handle_get_input_device_devnode(device) .handle_get_input_device_devnode(device)
.wrn("get_input_device_devnode")?, .wrn("get_input_device_devnode")?,
ClientMessage::SetIdle { timeout } => self.handle_set_idle(timeout), ClientMessage::SetIdle { timeout } => self.handle_set_idle(timeout),
ClientMessage::MoveToOutput {
workspace,
connector,
} => self
.handle_move_to_output(workspace, connector)
.wrn("move_to_output")?,
} }
Ok(()) Ok(())
} }

View file

@ -1015,6 +1015,9 @@ impl WlSurface {
} }
self.send_seat_release_events(); self.send_seat_release_events();
self.seat_state.destroy_node(self); self.seat_state.destroy_node(self);
if self.visible.get() {
self.client.state.damage();
}
} }
pub fn set_content_type(&self, content_type: Option<ContentType>) { pub fn set_content_type(&self, content_type: Option<ContentType>) {

View file

@ -591,6 +591,7 @@ impl State {
ws.flush_jay_workspaces(); ws.flush_jay_workspaces();
output.schedule_update_render_data(); output.schedule_update_render_data();
self.tree_changed(); self.tree_changed();
self.damage();
// let seats = self.globals.seats.lock(); // let seats = self.globals.seats.lock();
// for seat in seats.values() { // for seat in seats.values() {
// seat.workspace_changed(&output); // seat.workspace_changed(&output);

View file

@ -3,11 +3,12 @@ use {
backend::{Connector, ConnectorEvent, ConnectorId, MonitorInfo}, backend::{Connector, ConnectorEvent, ConnectorId, MonitorInfo},
ifs::wl_output::{OutputId, PersistentOutputState, WlOutputGlobal}, ifs::wl_output::{OutputId, PersistentOutputState, WlOutputGlobal},
state::{ConnectorData, OutputData, State}, state::{ConnectorData, OutputData, State},
tree::{OutputNode, OutputRenderData}, tree::{move_ws_to_output, OutputNode, OutputRenderData, WsMoveConfig},
utils::{asyncevent::AsyncEvent, clonecell::CloneCell}, utils::{asyncevent::AsyncEvent, clonecell::CloneCell},
}, },
std::{ std::{
cell::{Cell, RefCell}, cell::{Cell, RefCell},
collections::VecDeque,
rc::Rc, rc::Rc,
}, },
}; };
@ -156,6 +157,8 @@ impl ConnectorHandler {
node: on.clone(), node: on.clone(),
}); });
self.state.outputs.set(self.id, output_data); 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 { if self.state.outputs.len() == 1 {
let seats = self.state.globals.seats.lock(); let seats = self.state.globals.seats.lock();
let pos = global.pos.get(); let pos = global.pos.get();
@ -164,59 +167,45 @@ impl ConnectorHandler {
for seat in seats.values() { for seat in seats.values() {
seat.set_position(x, y); seat.set_position(x, y);
} }
let dummy = self.state.dummy_output.get().unwrap();
for ws in dummy.workspaces.iter() {
if ws.is_dummy {
continue;
}
ws_to_move.push_back(ws);
}
}
for source in self.state.outputs.lock().values() {
if source.node.id == on.id {
continue;
}
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());
}
}
}
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);
} }
global.node.set(Some(on.clone()));
if let Some(config) = self.state.config.get() { if let Some(config) = self.state.config.get() {
config.connector_connected(self.id); config.connector_connected(self.id);
} }
{
for source in self.state.outputs.lock().values() {
if source.node.id == on.id {
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();
}
if on.workspace.is_none() {
if let Some(ws) = on.workspaces.first() {
on.show_workspace(&ws);
ws.flush_jay_workspaces();
}
}
}
on.schedule_update_render_data();
self.state.root.outputs.set(self.id, on.clone()); self.state.root.outputs.set(self.id, on.clone());
self.state.root.update_extents(); self.state.root.update_extents();
self.state.add_global(&global); self.state.add_global(&global);
self.state.tree_changed();
self.state.damage();
'outer: loop { 'outer: loop {
while let Some(event) = self.data.connector.event() { while let Some(event) = self.data.connector.event() {
match event { match event {
@ -261,31 +250,19 @@ impl ConnectorHandler {
surface.send_closed(); surface.send_closed();
} }
} }
let mut target_is_dummy = false;
let target = match self.state.outputs.lock().values().next() { let target = match self.state.outputs.lock().values().next() {
Some(o) => o.node.clone(), Some(o) => o.node.clone(),
_ => { _ => self.state.dummy_output.get().unwrap(),
target_is_dummy = true;
self.state.dummy_output.get().unwrap()
}
}; };
if !on.workspaces.is_empty() { for ws in on.workspaces.iter() {
for ws in on.workspaces.iter() { if ws.desired_output.get() == output_id {
let is_visible =
!target_is_dummy && target.workspaces.is_empty() && ws.visible.get();
ws.visible_on_desired_output.set(ws.visible.get()); 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(); let config = WsMoveConfig {
self.state.tree_changed(); make_visible_if_empty: ws.visible.get(),
self.state.damage(); source_is_destroyed: true,
};
move_ws_to_output(&ws, &target, config);
} }
let seats = self.state.globals.seats.lock(); let seats = self.state.globals.seats.lock();
for seat in seats.values() { for seat in seats.values() {
@ -300,5 +277,7 @@ impl ConnectorHandler {
self.state self.state
.remove_output_scale(on.global.persistent.scale.get()); .remove_output_scale(on.global.persistent.scale.get());
let _ = self.state.remove_global(&*global); let _ = self.state.remove_global(&*global);
self.state.tree_changed();
self.state.damage();
} }
} }

View file

@ -337,7 +337,7 @@ impl OutputNode {
stacked: Default::default(), stacked: Default::default(),
seat_state: Default::default(), seat_state: Default::default(),
name: name.to_string(), name: name.to_string(),
output_link: Cell::new(None), output_link: Default::default(),
visible: Cell::new(false), visible: Cell::new(false),
fullscreen: Default::default(), fullscreen: Default::default(),
visible_on_desired_output: Cell::new(false), visible_on_desired_output: Cell::new(false),
@ -347,8 +347,7 @@ impl OutputNode {
title_texture: Default::default(), title_texture: Default::default(),
attention_requests: Default::default(), attention_requests: Default::default(),
}); });
ws.output_link *ws.output_link.borrow_mut() = Some(self.workspaces.add_last(ws.clone()));
.set(Some(self.workspaces.add_last(ws.clone())));
self.state.workspaces.set(name.to_string(), ws.clone()); self.state.workspaces.set(name.to_string(), ws.clone());
if self.workspace.is_none() { if self.workspace.is_none() {
self.show_workspace(&ws); self.show_workspace(&ws);

View file

@ -19,12 +19,17 @@ use {
utils::{ utils::{
clonecell::CloneCell, clonecell::CloneCell,
copyhashmap::CopyHashMap, copyhashmap::CopyHashMap,
linkedlist::{LinkedList, LinkedNode}, linkedlist::{LinkedList, LinkedNode, NodeRef},
threshold_counter::ThresholdCounter, threshold_counter::ThresholdCounter,
}, },
wire::JayWorkspaceId, wire::JayWorkspaceId,
}, },
std::{cell::Cell, fmt::Debug, ops::Deref, rc::Rc}, std::{
cell::{Cell, RefCell},
fmt::Debug,
ops::Deref,
rc::Rc,
},
}; };
tree_id!(WorkspaceNodeId); tree_id!(WorkspaceNodeId);
@ -38,7 +43,7 @@ pub struct WorkspaceNode {
pub stacked: LinkedList<Rc<dyn StackedNode>>, pub stacked: LinkedList<Rc<dyn StackedNode>>,
pub seat_state: NodeSeatState, pub seat_state: NodeSeatState,
pub name: String, pub name: String,
pub output_link: Cell<Option<LinkedNode<Rc<WorkspaceNode>>>>, pub output_link: RefCell<Option<LinkedNode<Rc<WorkspaceNode>>>>,
pub visible: Cell<bool>, pub visible: Cell<bool>,
pub fullscreen: CloneCell<Option<Rc<dyn ToplevelNode>>>, pub fullscreen: CloneCell<Option<Rc<dyn ToplevelNode>>>,
pub visible_on_desired_output: Cell<bool>, pub visible_on_desired_output: Cell<bool>,
@ -52,7 +57,7 @@ pub struct WorkspaceNode {
impl WorkspaceNode { impl WorkspaceNode {
pub fn clear(&self) { pub fn clear(&self) {
self.container.set(None); self.container.set(None);
self.output_link.set(None); *self.output_link.borrow_mut() = None;
self.fullscreen.set(None); self.fullscreen.set(None);
self.jay_workspaces.clear(); self.jay_workspaces.clear();
} }
@ -304,3 +309,43 @@ impl ContainingNode for WorkspaceNode {
self self
} }
} }
pub struct WsMoveConfig {
pub make_visible_if_empty: bool,
pub source_is_destroyed: bool,
}
pub fn move_ws_to_output(
ws: &NodeRef<Rc<WorkspaceNode>>,
target: &Rc<OutputNode>,
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();
}
}

View file

@ -22,7 +22,7 @@ use {
status::MessageFormat, status::MessageFormat,
theme::Color, theme::Color,
video::{GfxApi, Transform}, video::{GfxApi, Transform},
Axis, Direction, Axis, Direction, Workspace,
}, },
std::{ std::{
error::Error, error::Error,
@ -53,25 +53,70 @@ pub enum SimpleCommand {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Action { pub enum Action {
ConfigureConnector { con: ConfigConnector }, ConfigureConnector {
ConfigureDirectScanout { enabled: bool }, con: ConfigConnector,
ConfigureDrmDevice { dev: ConfigDrmDevice }, },
ConfigureIdle { idle: Duration }, ConfigureDirectScanout {
ConfigureInput { input: Input }, enabled: bool,
ConfigureOutput { out: Output }, },
Exec { exec: Exec }, ConfigureDrmDevice {
Multi { actions: Vec<Action> }, dev: ConfigDrmDevice,
SetEnv { env: Vec<(String, String)> }, },
SetGfxApi { api: GfxApi }, ConfigureIdle {
SetKeymap { map: ConfigKeymap }, idle: Duration,
SetLogLevel { level: LogLevel }, },
SetRenderDevice { dev: DrmDeviceMatch }, ConfigureInput {
SetStatus { status: Option<Status> }, input: Input,
SetTheme { theme: Box<Theme> }, },
ShowWorkspace { name: String }, ConfigureOutput {
SimpleCommand { cmd: SimpleCommand }, out: Output,
SwitchToVt { num: u32 }, },
UnsetEnv { env: Vec<String> }, Exec {
exec: Exec,
},
MoveToWorkspace {
name: String,
},
Multi {
actions: Vec<Action>,
},
SetEnv {
env: Vec<(String, String)>,
},
SetGfxApi {
api: GfxApi,
},
SetKeymap {
map: ConfigKeymap,
},
SetLogLevel {
level: LogLevel,
},
SetRenderDevice {
dev: DrmDeviceMatch,
},
SetStatus {
status: Option<Status>,
},
SetTheme {
theme: Box<Theme>,
},
ShowWorkspace {
name: String,
},
SimpleCommand {
cmd: SimpleCommand,
},
SwitchToVt {
num: u32,
},
UnsetEnv {
env: Vec<String>,
},
MoveToOutput {
workspace: Option<Workspace>,
output: OutputMatch,
},
} }
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]

View file

@ -16,19 +16,24 @@ use {
keymap::{KeymapParser, KeymapParserError}, keymap::{KeymapParser, KeymapParserError},
log_level::{LogLevelParser, LogLevelParserError}, log_level::{LogLevelParser, LogLevelParserError},
output::{OutputParser, OutputParserError}, output::{OutputParser, OutputParserError},
output_match::{OutputMatchParser, OutputMatchParserError},
status::{StatusParser, StatusParserError}, status::{StatusParser, StatusParserError},
theme::{ThemeParser, ThemeParserError}, theme::{ThemeParser, ThemeParserError},
StringParser, StringParserError, StringParser, StringParserError,
}, },
spanned::SpannedErrorExt,
Action, Action,
}, },
toml::{ toml::{
toml_span::{Span, Spanned, SpannedExt}, toml_span::{DespanExt, Span, Spanned, SpannedExt},
toml_value::Value, toml_value::Value,
}, },
}, },
indexmap::IndexMap, indexmap::IndexMap,
jay_config::Axis::{Horizontal, Vertical}, jay_config::{
get_workspace,
Axis::{Horizontal, Vertical},
},
thiserror::Error, thiserror::Error,
}; };
@ -45,31 +50,33 @@ pub enum ActionParserError {
#[error(transparent)] #[error(transparent)]
Extract(#[from] ExtractorError), Extract(#[from] ExtractorError),
#[error("Could not parse the exec action")] #[error("Could not parse the exec action")]
Exec(#[from] ExecParserError), Exec(#[source] ExecParserError),
#[error("Could not parse the configure-connector action")] #[error("Could not parse the configure-connector action")]
ConfigureConnector(#[from] ConnectorParserError), ConfigureConnector(#[source] ConnectorParserError),
#[error("Could not parse the configure-input action")] #[error("Could not parse the configure-input action")]
ConfigureInput(#[from] InputParserError), ConfigureInput(#[source] InputParserError),
#[error("Could not parse the configure-output action")] #[error("Could not parse the configure-output action")]
ConfigureOutput(#[from] OutputParserError), ConfigureOutput(#[source] OutputParserError),
#[error("Could not parse the environment variables")] #[error("Could not parse the environment variables")]
Env(#[from] EnvParserError), Env(#[source] EnvParserError),
#[error("Could not parse a set-keymap action")] #[error("Could not parse a set-keymap action")]
SetKeymap(#[from] KeymapParserError), SetKeymap(#[source] KeymapParserError),
#[error("Could not parse a set-status action")] #[error("Could not parse a set-status action")]
Status(#[from] StatusParserError), Status(#[source] StatusParserError),
#[error("Could not parse a set-theme action")] #[error("Could not parse a set-theme action")]
Theme(#[from] ThemeParserError), Theme(#[source] ThemeParserError),
#[error("Could not parse a set-log-level action")] #[error("Could not parse a set-log-level action")]
SetLogLevel(#[from] LogLevelParserError), SetLogLevel(#[source] LogLevelParserError),
#[error("Could not parse a set-gfx-api action")] #[error("Could not parse a set-gfx-api action")]
GfxApi(#[from] GfxApiParserError), GfxApi(#[source] GfxApiParserError),
#[error("Could not parse a configure-drm-device action")] #[error("Could not parse a configure-drm-device action")]
DrmDevice(#[from] DrmDeviceParserError), DrmDevice(#[source] DrmDeviceParserError),
#[error("Could not parse a set-render-device action")] #[error("Could not parse a set-render-device action")]
SetRenderDevice(#[from] DrmDeviceMatchParserError), SetRenderDevice(#[source] DrmDeviceMatchParserError),
#[error("Could not parse a configure-idle action")] #[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>); pub struct ActionParser<'a>(pub &'a Context<'a>);
@ -117,7 +124,8 @@ impl ActionParser<'_> {
fn parse_exec(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> { fn parse_exec(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> {
let exec = ext let exec = ext
.extract(val("exec"))? .extract(val("exec"))?
.parse_map(&mut ExecParser(self.0))?; .parse_map(&mut ExecParser(self.0))
.map_spanned_err(ActionParserError::Exec)?;
Ok(Action::Exec { exec }) Ok(Action::Exec { exec })
} }
@ -133,41 +141,52 @@ impl ActionParser<'_> {
fn parse_move_to_workspace(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> { fn parse_move_to_workspace(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> {
let name = ext.extract(str("name"))?.value.to_string(); 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<Self> { fn parse_configure_connector(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> {
let con = ext let con = ext
.extract(val("connector"))? .extract(val("connector"))?
.parse_map(&mut ConnectorParser(self.0))?; .parse_map(&mut ConnectorParser(self.0))
.map_spanned_err(ActionParserError::ConfigureConnector)?;
Ok(Action::ConfigureConnector { con }) Ok(Action::ConfigureConnector { con })
} }
fn parse_configure_input(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> { fn parse_configure_input(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> {
let input = ext.extract(val("input"))?.parse_map(&mut InputParser { let input = ext
cx: self.0, .extract(val("input"))?
tag_ok: false, .parse_map(&mut InputParser {
})?; cx: self.0,
tag_ok: false,
})
.map_spanned_err(ActionParserError::ConfigureInput)?;
Ok(Action::ConfigureInput { input }) Ok(Action::ConfigureInput { input })
} }
fn parse_configure_idle(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> { fn parse_configure_idle(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> {
let idle = ext let idle = ext
.extract(val("idle"))? .extract(val("idle"))?
.parse_map(&mut IdleParser(self.0))?; .parse_map(&mut IdleParser(self.0))
.map_spanned_err(ActionParserError::ConfigureIdle)?;
Ok(Action::ConfigureIdle { idle }) Ok(Action::ConfigureIdle { idle })
} }
fn parse_configure_output(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> { fn parse_configure_output(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> {
let out = ext.extract(val("output"))?.parse_map(&mut OutputParser { let out = ext
cx: self.0, .extract(val("output"))?
name_ok: false, .parse_map(&mut OutputParser {
})?; cx: self.0,
name_ok: false,
})
.map_spanned_err(ActionParserError::ConfigureOutput)?;
Ok(Action::ConfigureOutput { out }) Ok(Action::ConfigureOutput { out })
} }
fn parse_set_env(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> { fn parse_set_env(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> {
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 }) Ok(Action::SetEnv { env })
} }
@ -195,17 +214,23 @@ impl ActionParser<'_> {
} }
fn parse_set_keymap(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> { fn parse_set_keymap(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> {
let map = ext.extract(val("map"))?.parse_map(&mut KeymapParser { let map = ext
cx: self.0, .extract(val("map"))?
definition: false, .parse_map(&mut KeymapParser {
})?; cx: self.0,
definition: false,
})
.map_spanned_err(ActionParserError::SetKeymap)?;
Ok(Action::SetKeymap { map }) Ok(Action::SetKeymap { map })
} }
fn parse_set_status(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> { fn parse_set_status(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> {
let status = match ext.extract(opt(val("status")))? { let status = match ext.extract(opt(val("status")))? {
None => None, 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 }) Ok(Action::SetStatus { status })
} }
@ -213,26 +238,34 @@ impl ActionParser<'_> {
fn parse_set_theme(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> { fn parse_set_theme(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> {
let theme = ext let theme = ext
.extract(val("theme"))? .extract(val("theme"))?
.parse_map(&mut ThemeParser(self.0))?; .parse_map(&mut ThemeParser(self.0))
.map_spanned_err(ActionParserError::Theme)?;
Ok(Action::SetTheme { Ok(Action::SetTheme {
theme: Box::new(theme), theme: Box::new(theme),
}) })
} }
fn parse_set_log_level(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> { fn parse_set_log_level(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> {
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 }) Ok(Action::SetLogLevel { level })
} }
fn parse_set_gfx_api(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> { fn parse_set_gfx_api(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> {
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 }) Ok(Action::SetGfxApi { api })
} }
fn parse_set_render_device(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> { fn parse_set_render_device(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> {
let dev = ext let dev = ext
.extract(val("dev"))? .extract(val("dev"))?
.parse_map(&mut DrmDeviceMatchParser(self.0))?; .parse_map(&mut DrmDeviceMatchParser(self.0))
.map_spanned_err(ActionParserError::SetRenderDevice)?;
Ok(Action::SetRenderDevice { dev }) Ok(Action::SetRenderDevice { dev })
} }
@ -242,12 +275,26 @@ impl ActionParser<'_> {
} }
fn parse_configure_drm_device(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> { fn parse_configure_drm_device(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> {
let dev = ext.extract(val("dev"))?.parse_map(&mut DrmDeviceParser { let dev = ext
cx: self.0, .extract(val("dev"))?
name_ok: false, .parse_map(&mut DrmDeviceParser {
})?; cx: self.0,
name_ok: false,
})
.map_spanned_err(ActionParserError::DrmDevice)?;
Ok(Action::ConfigureDrmDevice { dev }) Ok(Action::ConfigureDrmDevice { dev })
} }
fn parse_move_to_output(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> {
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> { 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), "configure-drm-device" => self.parse_configure_drm_device(&mut ext),
"set-render-device" => self.parse_set_render_device(&mut ext), "set-render-device" => self.parse_set_render_device(&mut ext),
"configure-idle" => self.parse_configure_idle(&mut ext), "configure-idle" => self.parse_configure_idle(&mut ext),
"move-to-output" => self.parse_move_to_output(&mut ext),
v => { v => {
ext.ignore_unused(); ext.ignore_unused();
return Err(ActionParserError::UnknownType(v.to_string()).spanned(ty.span)); return Err(ActionParserError::UnknownType(v.to_string()).spanned(ty.span));

View file

@ -75,6 +75,10 @@ impl Action {
let workspace = get_workspace(&name); let workspace = get_workspace(&name);
Box::new(move || s.show_workspace(workspace)) 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 || { Action::ConfigureConnector { con } => Box::new(move || {
for c in connectors() { for c in connectors() {
if con.match_.matches(c) { if con.match_.matches(c) {
@ -150,6 +154,23 @@ impl Action {
}) })
} }
Action::ConfigureIdle { idle } => Box::new(move || set_idle(Some(idle))), 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),
}
})
}
} }
} }
} }

View file

@ -136,6 +136,27 @@
"name" "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", "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", "type": "object",

View file

@ -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. 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`: - `configure-connector`:
Applies a configuration to connectors. Applies a configuration to connectors.

View file

@ -219,6 +219,39 @@ Action:
description: The name of the workspace. description: The name of the workspace.
required: true required: true
kind: string 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: configure-connector:
description: | description: |
Applies a configuration to connectors. Applies a configuration to connectors.