feat: add exclusive tag + minor refactor

This commit is contained in:
atagen 2025-06-23 00:34:43 +10:00
parent 66d1dfacda
commit 0997b124a2
4 changed files with 114 additions and 76 deletions

View File

@ -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.

View File

@ -21,6 +21,7 @@ enum Commands {
EnableTag { tag: u8 },
DisableTag { tag: u8 },
ToggleTag { tag: u8 },
ExclusiveTag { tag: u8 },
}
impl From<Commands> for niri_tag::TagCmd {
@ -32,6 +33,7 @@ impl From<Commands> 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!")),
};

View File

@ -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<u64, Workspace>) -> Result<Workspace> {
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<u64, Workspace>,
affected_windows: Vec<u64>,
) {
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<Workspace> {
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<u64, u8>, HashMap<u64, u8>) =
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);
}

View File

@ -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<u8, TagState>),
}