diff options
author | Sean Whitton <spwhitton@spwhitton.name> | 2024-01-13 17:45:44 +0000 |
---|---|---|
committer | Sean Whitton <spwhitton@spwhitton.name> | 2024-01-13 20:58:38 +0000 |
commit | 2d8f89e3473355c13fb28e04861218cf7fde2555 (patch) | |
tree | b8774e4aa83327a4cfde6cac19bef129952081b5 /scripts | |
parent | ccc837803226e9c1b9cffc6335b0b83268b0ecbe (diff) | |
download | dotfiles-2d8f89e3473355c13fb28e04861218cf7fde2555.tar.gz |
start work on PaperWM-inspired extensions to Sway setup
Diffstat (limited to 'scripts')
-rwxr-xr-x | scripts/desktop/i3status-wrapper | 309 | ||||
-rwxr-xr-x | scripts/desktop/i3status-wrapper-msg | 10 |
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" |