diff --git a/README.md b/README.md index 235b167..4689293 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ niri-tag = { ```` it is assumed you use niri-flake, or else will use the `stable` package output; this is important for the niri IPC definitions. -next,\ +next, - add `inputs.niri-tag.nixosModules.niri-tag` to your module imports - add `services.niri-tag.enable = true;` somewhere in your config - if you wish to use a stable niri instead of unstable from niri-flake (default), set `services.niri-tag.package = inputs.niri-tag.packages.${pkgs.system}.stable;` @@ -87,7 +87,7 @@ set up: alternatively,\ you may bind as you see fit the tagctl commands `add` `remove` `toggle` for window tagging, -and `enable-tag` `disable-tag` `toggle-tag` for managing tags. +and `enable-tag` `disable-tag` `toggle-tag` `exclusive-tag` for managing tags. all commands (except `remove`) take a tag number from 1-255 after their command. diff --git a/cli/main.rs b/cli/main.rs index d346a2d..865fd77 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -21,6 +21,7 @@ enum Commands { EnableTag { tag: u8 }, DisableTag { tag: u8 }, ToggleTag { tag: u8 }, + ExclusiveTag { tag: u8 }, } impl From for niri_tag::TagCmd { @@ -32,6 +33,7 @@ impl From for niri_tag::TagCmd { Commands::EnableTag { tag } => TagCmd::EnableTag(tag), Commands::DisableTag { tag } => TagCmd::DisableTag(tag), Commands::ToggleTag { tag } => TagCmd::ToggleTag(tag), + Commands::ExclusiveTag { tag } => TagCmd::ExclusiveTag(tag), } } } @@ -47,6 +49,7 @@ fn main() -> Result<()> { EnableTag { tag } if tag > 0 => (), DisableTag { tag } if tag > 0 => (), ToggleTag { tag } if tag > 0 => (), + ExclusiveTag { .. } => (), _ => return Err(anyhow!("Can't change tag 0!")), }; diff --git a/daemon/manager.rs b/daemon/manager.rs index 1043bf1..04aea4a 100644 --- a/daemon/manager.rs +++ b/daemon/manager.rs @@ -24,8 +24,9 @@ pub struct NiriTag { } enum TagAction { - ChangeWindow(u64), - ChangeTag(u8), + Window(u64), + Tag(u8), + TagExclusive(u8), } impl NiriTag { @@ -41,39 +42,79 @@ impl NiriTag { }) } + fn same_output(&self, wsid: u64, candidates: &HashMap) -> Result { + candidates + .values() + .filter_map(|ws| { + let output = ws.output.clone()?; + let win_output = self + .state + .workspaces + .workspaces + .get(&wsid)? + .output + .clone()?; + (win_output == output).then_some(ws) + }) + .last() + .context(anyhow!( + "No inactive workspaces on output of workspace {} found", + wsid + )) + .cloned() + } + + async fn move_windows( + &mut self, + candidates: &HashMap, + affected_windows: Vec, + ) { + for wid in affected_windows { + tracing::debug!("Changing affected window {}", wid); + if let Some(win) = self.state.windows.windows.get(&wid) { + let wsid = win.workspace_id.unwrap(); + match self.same_output(wsid, candidates) { + Ok(status_same_output) => { + if let Err(e) = tell( + &mut self.socket, + Request::Action(Action::MoveWindowToWorkspace { + window_id: Some(wid), + reference: WorkspaceReferenceArg::Id(status_same_output.id), + focus: false, + }), + ) + .await + { + tracing::error!( + "Failed to move window {} to workspace {}: {}", + wid, + status_same_output.id, + e + ); + } + } + Err(e) => { + tracing::error!("Failed to get workspace on same output as {}: {}", wsid, e) + } + } + } else { + tracing::warn!("Failed to get wid {} from niri state", wid); + continue; + } + } + } + async fn do_action(&mut self, action: TagAction) -> Result<()> { use TagAction::*; - let same_output = - |wsid: u64, candidates: &HashMap<&u64, &Workspace>| -> Result { - candidates - .values() - .filter_map(|ws| { - let output = ws.output.clone()?; - let win_output = self - .state - .workspaces - .workspaces - .get(&wsid)? - .output - .clone()?; - (win_output == output).then_some(ws) - }) - .last() - .context(anyhow!( - "No inactive workspaces on output of workspace {} found", - wsid - )) - .copied() - .cloned() - }; let (active, inactive): (HashMap<_, _>, HashMap<_, _>) = self .state .workspaces .workspaces - .iter() + .clone() + .into_iter() .partition(|(_, ws)| ws.is_active); match action { - ChangeWindow(wid) => { + Window(wid) => { let current_tag = *self.windows.entry(wid).or_insert(0); let tag_visible = *self.tags.entry(current_tag).or_insert(true); let win = self @@ -88,7 +129,7 @@ impl NiriTag { let win_visible = active.contains_key(&wsid); match (win_visible, tag_visible) { (true, false) => { - let inactive_same_output = same_output(wsid, &inactive)?; + let inactive_same_output = self.same_output(wsid, &inactive)?; tell( &mut self.socket, Request::Action(Action::MoveWindowToWorkspace { @@ -100,7 +141,7 @@ impl NiriTag { .await } (false, true) => { - let active_same_output = same_output(wsid, &active)?; + let active_same_output = self.same_output(wsid, &active)?; tell( &mut self.socket, Request::Action(Action::MoveWindowToWorkspace { @@ -114,7 +155,7 @@ impl NiriTag { _ => Ok(()), } } - ChangeTag(tag) => { + Tag(tag) => { tracing::debug!("Changing tag {}", tag); let tag_visible = *self.tags.entry(tag).or_insert(true); tracing::debug!("Windows: {:?}", self.windows); @@ -131,41 +172,11 @@ impl NiriTag { affected_windows ); let focus = affected_windows.last().cloned(); - for wid in affected_windows { - tracing::debug!("Changing affected window {}", wid); - if let Some(win) = self.state.windows.windows.get(&wid) { - let wsid = win.workspace_id.unwrap(); - match same_output(wsid, if tag_visible { &active } else { &inactive }) { - Ok(status_same_output) => { - if let Err(e) = tell( - &mut self.socket, - Request::Action(Action::MoveWindowToWorkspace { - window_id: Some(wid), - reference: WorkspaceReferenceArg::Id(status_same_output.id), - focus: false, - }), - ) - .await - { - tracing::error!( - "Failed to move window {} to workspace {}: {}", - wid, - status_same_output.id, - e - ); - } - } - Err(e) => tracing::error!( - "Failed to get workspace on same output as {}: {}", - wsid, - e - ), - } - } else { - tracing::warn!("Failed to get wid {} from niri state", wid); - continue; - } - } + self.move_windows( + if tag_visible { &active } else { &inactive }, + affected_windows, + ) + .await; if let Some(focus) = focus { if tag_visible { tell( @@ -177,6 +188,24 @@ impl NiriTag { } Ok(()) } + TagExclusive(t) => { + tracing::debug!("Changing all tags"); + let (active_wid, inactive_wid): (HashMap, HashMap) = + self.windows.iter().partition(|(_, it)| **it == t); + let focus = active_wid.keys().last(); + self.move_windows(&inactive, inactive_wid.keys().cloned().collect()) + .await; + self.move_windows(&active, active_wid.keys().cloned().collect()) + .await; + if let Some(f) = focus { + tell( + &mut self.socket, + Request::Action(Action::FocusWindow { id: *f }), + ) + .await?; + } + Ok(()) + } } } @@ -266,7 +295,7 @@ impl NiriTag { tracing::debug!("adding tag {} to {}", t, wid); let tx = self.ev_tx.clone(); add_tag(tx, &self.windows, t).await; - ChangeWindow(wid) + Window(wid) } TagCmd::RemoveTagFromWin(_) => { let win = self.get_focused_window().await?; @@ -275,7 +304,7 @@ impl NiriTag { tracing::debug!("resetting tag on {}", wid); let tx = self.ev_tx.clone(); rm_tag(tx, &self.windows, wid, old_tag).await; - ChangeWindow(wid) + Window(wid) } TagCmd::ToggleTagOnWin(t) => { let win = self.get_focused_window().await?; @@ -291,18 +320,18 @@ impl NiriTag { } tracing::debug!("toggling {} to tag {}", wid, toggle); self.windows.insert(wid, toggle); - ChangeWindow(wid) + Window(wid) } TagCmd::EnableTag(t) => { self.tags.insert(t, true); send_event(self.ev_tx.clone(), TagEvent::TagEnabled(t)).await; - ChangeTag(t) + Tag(t) } TagCmd::DisableTag(t) => { self.tags.insert(t, false); send_event(self.ev_tx.clone(), TagEvent::TagDisabled(t)).await; - ChangeTag(t) + Tag(t) } TagCmd::ToggleTag(t) => { let new_state = !*self.tags.entry(t).or_insert(false); @@ -313,7 +342,13 @@ impl NiriTag { } tracing::debug!("toggling tag {} to {}", t, new_state); self.tags.insert(t, new_state); - ChangeTag(t) + Tag(t) + } + TagCmd::ExclusiveTag(t) => { + self.tags.entry(t).insert_entry(true); + self.tags.iter_mut().for_each(|(it, en)| *en = *it == t); + send_event(self.ev_tx.clone(), TagEvent::TagExclusive(t)).await; + TagExclusive(t) } }, }; @@ -375,7 +410,7 @@ impl NiriTag { WindowsChanged { windows } => { for w in windows { self.windows.entry(w.id).or_insert(0); - let action = self.do_action(TagAction::ChangeWindow(w.id)).await; + let action = self.do_action(TagAction::Window(w.id)).await; if let Err(e) = action { tracing::warn!("Failed to ChangeWindow on {}: {}", w.id, e); } diff --git a/lib/main.rs b/lib/main.rs index 3e5b44a..cc8a6b7 100644 --- a/lib/main.rs +++ b/lib/main.rs @@ -10,8 +10,7 @@ pub enum TagCmd { DisableTag(u8), ToggleTagOnWin(u8), ToggleTag(u8), - // TODO - // ExclusiveTag(u8), + ExclusiveTag(u8), } #[derive(Serialize, Deserialize, Debug)] @@ -21,6 +20,7 @@ pub enum TagEvent { TagUrgent(u8), TagEnabled(u8), TagDisabled(u8), + TagExclusive(u8), TagFullState(HashMap), }