summaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
authorSean Whitton <spwhitton@spwhitton.name>2024-01-13 17:45:44 +0000
committerSean Whitton <spwhitton@spwhitton.name>2024-01-13 20:58:38 +0000
commit2d8f89e3473355c13fb28e04861218cf7fde2555 (patch)
treeb8774e4aa83327a4cfde6cac19bef129952081b5 /scripts
parentccc837803226e9c1b9cffc6335b0b83268b0ecbe (diff)
downloaddotfiles-2d8f89e3473355c13fb28e04861218cf7fde2555.tar.gz
start work on PaperWM-inspired extensions to Sway setup
Diffstat (limited to 'scripts')
-rwxr-xr-xscripts/desktop/i3status-wrapper309
-rwxr-xr-xscripts/desktop/i3status-wrapper-msg10
2 files changed, 276 insertions, 43 deletions
diff --git a/scripts/desktop/i3status-wrapper b/scripts/desktop/i3status-wrapper
index 665630b3..3c5502cd 100755
--- a/scripts/desktop/i3status-wrapper
+++ b/scripts/desktop/i3status-wrapper
@@ -2,7 +2,7 @@
# i3status-wrapper -- wrapper for i3status(1), plus other monitoring
#
-# Copyright (C) 2019, 2021-2023 Sean Whitton
+# Copyright (C) 2019, 2021-2024 Sean Whitton
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -27,6 +27,10 @@ use Local::Desktop;
use IO::Pipe;
use IPC::Shareable;
use Sys::Hostname;
+use POSIX "floor", "mkfifo";
+use File::Basename "basename", "dirname";
+use File::Spec::Functions "catfile";
+use List::Util "first", "max";
$| = 1;
@@ -42,72 +46,238 @@ unless ($i3status) {
tie my %info, "IPC::Shareable", undef, { destroy => 1 };
-unless (fork // warn "couldn't fork monitoring loop") {
- my $caffeinated_id;
+sub with_ignored_events (&) {
+ system $wmipc, "-q", "-t", "send_tick", "i3status-wrapper-ign";
+ $_[0]->();
+ system $wmipc, "-q", "-t", "send_tick", "i3status-wrapper-unign";
+}
+unless (fork // warn "couldn't fork monitoring loop") {
open my $events, "-|",
- $wmipc, "-t", "subscribe", "-m", '[ "window", "workspace" ]';
+ $wmipc, "-t", "subscribe", "-m", '[ "tick", "window", "workspace" ]';
- sub register_caffeinated {
- $caffeinated_id = $_[0]->{id};
- $info{caffeinated_name} = $_[0]->{name};
- kill USR1 => $i3status;
+ # Determine the initial state -- the WM might just have been reloaded.
+ # Move any previously-hidden containers to a fresh workspace for perusal.
+
+ my @old_ids;
+ for (@{decode_json `$wmipc -t get_workspaces`}) {
+ $info{focused_ws} = $_->{name} if $_->{focused};
+ push @old_ids, $1 if $_->{name} =~ /\A\*(\d+)\*\z/;
+ }
+ if (@old_ids) {
+ fresh_workspace(go => 1);
+ wmipc map("[con_id=$_] move container workspace current", @old_ids),
+ "focus child";
}
+ update_paper_ws_cols();
+
+ # Now loop forever reading events, assuming no exceptions.
- sub clear_caffeinated {
- undef $caffeinated_id;
- undef $info{caffeinated_name};
- kill USR1 => $i3status;
+ sub new_container {
+ # New window on one of our monitored workspaces.
+ # Push a window off the workspace to accommodate new one.
+ # Leave it to a later loop iteration to update the focus.
+ my $ws = $info{paper_ws}{$info{focused_ws}};
+ my $cols = $ws->{cols};
+ my $i = first { $cols->[$_] == $ws->{focused_col} } 0..$#$cols;
+ splice $cols->@*, $i+1, 0, shift->{container}{id};
+ if ($ws->{monocle} || $cols->@* > $ws->{ncols}) {
+ with_ignored_events {
+ my $pushed = shift $cols->@*;
+ wmipc hide_con($pushed);
+ push $ws->{off_left}->@*, $pushed;
+ };
+ }
}
+ eval {
+ while (my $e = decode_json <$events>) {
+ state $last_e;
- # Determine the initial state -- the WM might just have been reloaded.
+ # New containers
+ if ($last_e && $last_e->{change} && $last_e->{change} eq "new") {
+ new_container($last_e)
+ unless $e->{change} && $e->{change} eq "floating";
+ undef $last_e;
+ } elsif ($e->{change} && $e->{change} eq "new"
+ && exists $info{paper_ws}{$info{focused_ws}}) {
+ # We have to go round the loop once more to find out if it's
+ # just a floating dialog that we'll ignore.
+ $last_e = $e;
+ } elsif ($e->{change} && $e->{change} eq "floating"
+ && $e->{container}{type} ne "floating_con") {
+ # A container stopped floating -- it's as though it's new.
+ new_container($e);
+ $info{paper_ws}{$info{focused_ws}}{focused_col}
+ = $e->{container}{id};
+ }
- my $workspaces = @{ decode_json `$wmipc -t get_workspaces` };
- wsbuttons($workspaces > 1 ? "yes" : "no");
+ # Closing containers
+ elsif (($e->{change} && $e->{change} eq "close"
+ || $e->{change} && $e->{change} eq "floating"
+ && $e->{container}{type} eq "floating_con")
+ && exists $info{paper_ws}{$info{focused_ws}}) {
+ # We just drop the column here. When the focus change event
+ # comes in, that part of the loop is responsible for pulling
+ # in another container as a replacement, if necessary.
+ my $cols = $info{paper_ws}{$info{focused_ws}}{cols};
+ my $i = first { $cols->[$_] == $e->{container}{id} }
+ 0..$#$cols;
+ splice $cols->@*, $i, 1;
+ }
- my @trees = decode_json `$wmipc -t get_tree`;
- while (@trees) {
- for ((shift @trees)->{nodes}->@*) {
- if (grep $_ eq "caffeinated", $_->{marks}->@*) {
- register_caffeinated($_);
- last;
- } else {
- unshift @trees, $_;
+ # Other container changes
+ elsif ($e->{change} && $e->{change} eq "focus"
+ && $e->{container} && $e->{container}{type} eq "con"
+ && exists $info{paper_ws}{$info{focused_ws}}
+ && !$info{paper_ws}{$info{focused_ws}}{monocle}) {
+ # Change of window focus on one of our monitored workspaces.
+ # Update which column we think is focused.
+ my $ws = $info{paper_ws}{$info{focused_ws}};
+ $ws->{focused_col} = $e->{container}{id};
+
+ # If the change of focus was triggered by a container closing,
+ # we might need to pull in a container.
+ if ($ws->{ncols} > $ws->{cols}->@*
+ && ($ws->{off_left}->@* || $ws->{off_right}->@*)) {
+ my $cols = $ws->{cols};
+ my $i = first { $cols->[$_] eq $e->{container}{id} }
+ 0..$#$cols;
+ my $mid_i = floor @$cols/2;
+ if ($ws->{off_left}->@*
+ && ($i < $mid_i || !$ws->{off_right}->@*)) {
+ with_ignored_events {
+ my $pulled = pop $ws->{off_left}->@*;
+ wmipc +("focus left")x$i,
+ show_con($pulled), "move left",
+ ("focus right")x$i;
+ unshift @$cols, $pulled;
+ $ws->{focused_col} = $cols->[$i];
+ };
+ } elsif ($ws->{off_right}->@*
+ && ($i >= $mid_i || !$ws->{off_left}->@*)) {
+ with_ignored_events {
+ my $j = $#$cols - $i;
+ my $pulled = pop $ws->{off_right}->@*;
+ wmipc +("focus right")x$j,
+ show_con($pulled), ("focus left")x$j;
+ push @$cols, $pulled;
+ $ws->{focused_col} = $cols->[$i+1];
+ };
+ }
+ }
+ } elsif ($e->{change} && $e->{change} eq "move") {
+ update_paper_ws_cols();
}
- }
- }
- # Now loop forever reading events, assuming no exceptions.
+ # Ticks
+ elsif ($e->{payload} && $e->{payload} eq "i3status-wrapper-ign") {
+ # Ignore everything until tick telling us to unignore.
+ # Thread that sent the ignore is responsible for updating data
+ # structures in the meantime.
+ while (my $next = decode_json <$events>) {
+ last if $next->{payload}
+ && $next->{payload} eq "i3status-wrapper-unign";
+ }
+ }
- eval {
- while (my $event = decode_json <$events>) {
- if ($event->{change} eq "mark") {
- if (grep $_ eq "caffeinated", $event->{container}{marks}->@*)
- {
- register_caffeinated($event->{container});
- } elsif ($caffeinated_id
- and $caffeinated_id == $event->{container}{id}) {
+ # Workspace changes
+ elsif ($e->{change} && $e->{change} eq "focus" && $e->{current}) {
+ $info{focused_ws} = $e->{current}{name};
+ } elsif ($e->{change} && $e->{change} eq "init"
+ && $e->{current}) {
+ $info{paper_ws}{$e->{current}{name}}
+ = { off_left => [], off_right => [],
+ ncols => 2, cols => [] };
+ } elsif ($e->{change} && $e->{change} eq "empty"
+ && $e->{current}) {
+ delete $info{paper_ws}{$e->{current}{name}};
+ }
+
+ # Mark changes
+ elsif ($e->{change} && $e->{change} eq "mark") {
+ if (grep $_ eq "caffeinated", $e->{container}{marks}->@*) {
+ register_caffeinated($e->{container});
+ } elsif ($info{caffeinated_id}
+ and $info{caffeinated_id} == $e->{container}{id}) {
clear_caffeinated();
}
- } elsif ($event->{change} eq "init") {
- ++$workspaces == 2 and wsbuttons("yes");
- } elsif ($event->{change} eq "empty") {
- --$workspaces == 1 and wsbuttons("no");
- # For simplicity, only fresh-workspace script calls
- # compact_workspaces atm. For greater consistency we could
- # call it here, too, without supplying a leave_gap argument.
}
}
};
# Give up if there's a decoding error. We can't ignore the problem
- # because we don't want our ideas regarding how many workspaces there are,
- # and whether anything is caffeinated, to get out of sync.
+ # because we don't want our ideas regarding what workspaces there are, and
+ # whether anything is caffeinated, to get out of sync.
#
# The user can use the WM's "reload" command to restart this loop.
$@ and wsbuttons("yes"), clear_caffeinated();
}
+my $wm_ipc_socket = $ENV{SWAYSOCK} || $ENV{I3SOCK};
+(basename $wm_ipc_socket) =~ /\d[\d.]*\d/;
+my $cmdpipe = catfile dirname($wm_ipc_socket), "i3status-wrapper.$&.pipe";
+-e and unlink for $cmdpipe;
+
+unless (fork // warn "couldn't fork command pipe reader") {
+ mkfifo $cmdpipe, 0700 or die "mkfifo $cmdpipe failed: $!";
+ open my $cmdpipe_r, "<", $cmdpipe;
+
+ # Hold the pipe open with a writer that won't write anything.
+ open my $cmdpipe_w, ">", $cmdpipe;
+
+ while (my $cmd = <$cmdpipe_r>) {
+ my $ws = $info{paper_ws}{$info{focused_ws}};
+
+ if ($cmd =~ /^(focus|move) (left|right)$/) {
+ my @cols = $ws->{cols}->@*;
+ my ($i) = grep $cols[$_] == $ws->{focused_col}, 0..$#cols;
+ my $move = $1 eq "move";
+ $2 eq "right" ? $i++ : $i--;
+ if ($ws->{cols}->@* > $i >= 0) {
+ wmipc $cmd;
+ } elsif ($i == @cols && $ws->{off_right}->@*) {
+ with_ignored_events {
+ my $pushed = shift $ws->{cols}->@*;
+ my $pulled = pop $ws->{off_right}->@*;
+ my @cmds = show_con($pulled);
+ push $ws->{off_left}->@*, $pushed;
+ if ($move) {
+ push @cmds, "focus left", "move right";
+ my $tem = pop $ws->{cols}->@*;
+ push $ws->{cols}->@*, $pulled, $tem;
+ } else {
+ $ws->{focused_col} = $pulled;
+ push $ws->{cols}->@*, $pulled;
+ }
+
+ wmipc @cmds, hide_con($pushed);
+ };
+ kill USR1 => $i3status;
+ } elsif ($i == -1 && $ws->{off_left}->@*) {
+ with_ignored_events {
+ my $pushed = pop $ws->{cols}->@*;
+ my $pulled = pop $ws->{off_left}->@*;
+ my @cmds = show_con($pulled);
+ push $ws->{off_right}->@*, $pushed;
+
+ if ($move) {
+ push @cmds, "focus left";
+ my $tem = shift $ws->{cols}->@*;
+ unshift $ws->{cols}->@*, $tem, $pulled;
+ } else {
+ push @cmds, "move left";
+ $ws->{focused_col} = $pulled;
+ unshift $ws->{cols}->@*, $pulled;
+ }
+
+ wmipc @cmds, hide_con($pushed);
+ };
+ kill USR1 => $i3status;
+ }
+ }
+ }
+}
+
$pipe->reader;
open STDIN, "<&=", $pipe->fileno or die "couldn't reopen STDIN!";
@@ -148,3 +318,56 @@ sub wsbuttons {
return unless $ENV{XDG_CURRENT_DESKTOP} eq "sway";
wmipc "bar bar-0 workspace_buttons $_[0]";
}
+
+sub register_caffeinated {
+ $info{caffeinated_id} = $_[0]->{id};
+ $info{caffeinated_name} = $_[0]->{name};
+ kill USR1 => $i3status;
+}
+
+sub clear_caffeinated {
+ undef $info{caffeinated_id};
+ undef $info{caffeinated_name};
+ kill USR1 => $i3status;
+}
+
+sub update_paper_ws_cols {
+ my @trees = decode_json `$wmipc -t get_tree`;
+ while (@trees) {
+ foreach my $node ((shift @trees)->{nodes}->@*) {
+ if ($node->{type} eq "workspace"
+ && grep $_ eq $node->{name}, @all_workspaces) {
+ my $entry = $info{paper_ws}{$node->{name}}
+ //= { off_left => [], off_right => [] };
+
+ # Here we assume that the containers for the columns are
+ # directly below the type=workspace node. That won't be true
+ # if workspace_layout is not configured to 'default'.
+ foreach my $child_id ($node->{focus}->@*) {
+ my $child_node
+ = first { $_->{id} == $child_id } $node->{nodes}->@*;
+ $entry->{focused_col} = $child_id, last
+ if $child_node->{type} eq "con";
+ }
+
+ $entry->{cols} = [];
+ foreach my $child_node ($node->{nodes}->@*) {
+ push $entry->{cols}->@*, $child_node->{id}
+ if $child_node->{type} eq "con";
+ }
+ $entry->{ncols} = max 2, scalar $entry->{cols}->@*;
+ } elsif (grep $_ eq "caffeinated", $node->{marks}->@*) {
+ register_caffeinated($node);
+ }
+ unshift @trees, $node;
+ }
+ }
+}
+
+sub hide_con {
+ sprintf "[con_id=%s] move container to workspace %s", $_[0], "*$_[0]*"
+}
+
+sub show_con{
+ sprintf "[con_id=%s] move container to workspace current, focus", $_[0]
+}
diff --git a/scripts/desktop/i3status-wrapper-msg b/scripts/desktop/i3status-wrapper-msg
new file mode 100755
index 00000000..602014e3
--- /dev/null
+++ b/scripts/desktop/i3status-wrapper-msg
@@ -0,0 +1,10 @@
+#!/bin/sh
+
+[ -n "$1" ] || exit 127
+socket=${SWAYSOCK:-$I3SOCK}
+pipe="$(printf "%s/i3status-wrapper.%s.pipe" \
+ "$(dirname "$socket")" \
+ "$(basename "$socket" \
+ | sed -n 's/^[^0-9]*\([0-9][0-9.]*[0-9]\).*/\1/p')")"
+[ -w "$pipe" ] || exit 127
+echo "$@" >"$pipe"