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 | |
parent | ccc837803226e9c1b9cffc6335b0b83268b0ecbe (diff) | |
download | dotfiles-2d8f89e3473355c13fb28e04861218cf7fde2555.tar.gz |
start work on PaperWM-inspired extensions to Sway setup
-rw-r--r-- | .config/sway/config | 61 | ||||
-rw-r--r-- | perl5/Local/Desktop.pm | 5 | ||||
-rwxr-xr-x | scripts/desktop/i3status-wrapper | 309 | ||||
-rwxr-xr-x | scripts/desktop/i3status-wrapper-msg | 10 |
4 files changed, 310 insertions, 75 deletions
diff --git a/.config/sway/config b/.config/sway/config index 594e7a52..70f22651 100644 --- a/.config/sway/config +++ b/.config/sway/config @@ -14,9 +14,6 @@ client.background #FFFFF6 # Use Mouse+Mod4 to drag floating windows to their wanted position floating_modifier Mod4 -# default workspace layout -workspace_layout tabbed - mouse_warping output # Key binding strategy: @@ -113,11 +110,11 @@ mode "C-i-" { # focus the child container bindsym d focus child, mode "default" - # when screen is divided into two containers where at least one might - # have several tabs, as I usually have it, this works well to go back - # and forth - bindsym o focus_wrapping workspace, focus parent, focus right, \ - focus_wrapping yes, mode "default" + # # when screen is divided into two containers where at least one might + # # have several tabs, as I usually have it, this works well to go back + # # and forth + # bindsym o focus_wrapping workspace, focus parent, focus right, \ + # focus_wrapping yes, mode "default" # switch to workspace bindsym 1 workspace 1, mode "default" @@ -230,19 +227,19 @@ mode "C-i-" { bindsym Ctrl+c [con_mark=caffeinated] inhibit_idle none; \ [con_mark=caffeinated] mark --toggle caffeinated; mode "default" - # The two % values are for melete. Cf. `spw/maybe-scale-basic-faces'. - # - # This says: shrink such that an Emacs frame taking up the rest of the - # width can fit two 80-column windows, with Debian's fonts-hack at - # height 105. So should be <33. Sometimes we'll be able to fit an - # 80-column frame in the smaller part of the split, but usually we - # won't: the point is to ensure we can use Emacs with two 80-column - # windows, as is usual, while also displaying something narrower. - bindsym minus resize set width 31 ppt, mode "default" - # And this says: ensure that the current window can be an Emacs frame - # with at least 80 columns. Based on Debian's fonts-hack scaled up to - # height 120 by `spw/maybe-scale-basic-faces'. - bindsym equal resize set width 43 ppt, mode "default" + # # The two % values are for melete. Cf. `spw/maybe-scale-basic-faces'. + # # + # # This says: shrink such that an Emacs frame taking up the rest of the + # # width can fit two 80-column windows, with Debian's fonts-hack at + # # height 105. So should be <33. Sometimes we'll be able to fit an + # # 80-column frame in the smaller part of the split, but usually we + # # won't: the point is to ensure we can use Emacs with two 80-column + # # windows, as is usual, while also displaying something narrower. + # bindsym minus resize set width 31 ppt, mode "default" + # # And this says: ensure that the current window can be an Emacs frame + # # with at least 80 columns. Based on Debian's fonts-hack scaled up to + # # height 120 by `spw/maybe-scale-basic-faces'. + # bindsym equal resize set width 43 ppt, mode "default" bindsym Ctrl+y exec ~/src/dotfiles/scripts/desktop/sway-ftp-master-cut-note , mode "default" @@ -256,22 +253,28 @@ for_window [con_mark=caffeinated] inhibit_idle open for_window [title="ftp-master GNU mc session"] mark mc for_window [title="ftp-master dak command session"] mark dak +focus_wrapping no + # change focus -bindsym Ctrl+7 focus left -bindsym Ctrl+0 focus right +bindsym Ctrl+7 exec \ + ~/src/dotfiles/scripts/desktop/i3status-wrapper-msg focus left \ + || swaymsg focus left +bindsym Ctrl+0 exec \ + ~/src/dotfiles/scripts/desktop/i3status-wrapper-msg focus right \ + || swaymsg focus right bindsym Ctrl+8 focus down bindsym Ctrl+9 focus up # move focused window -bindsym Mod1+Ctrl+7 move left -bindsym Mod1+Ctrl+0 move right +bindsym Mod1+Ctrl+7 exec ~/src/dotfiles/scripts/desktop/i3status-wrapper-msg move left || swaymsg move left +bindsym Mod1+Ctrl+0 exec ~/src/dotfiles/scripts/desktop/i3status-wrapper-msg move right || swaymsg move right bindsym Mod1+Ctrl+8 move down bindsym Mod1+Ctrl+9 move up bindsym Ctrl+5 kill bindsym Ctrl+6 fullscreen toggle -bindsym Ctrl+apostrophe layout toggle splith splitv tabbed +# bindsym Ctrl+apostrophe layout toggle splith splitv tabbed # Cycle through all workspaces on monitor. # Warp the cursor to a point on the screen which is hopefully a titlebar. @@ -320,10 +323,8 @@ bar { binding_mode #000000 #EECD82 #000000 } - # We don't set workspace_buttons to "no" even though we start off with - # a single workspace (see i3status-wrapper) because under Sway that - # can trigger a bug which means the button display doesn't get - # properly updated. + # Leave i3status-wrapper to disable the workspace buttons once it has + # started handling workspaces. position bottom strip_workspace_numbers yes diff --git a/perl5/Local/Desktop.pm b/perl5/Local/Desktop.pm index 347495fe..5f30e507 100644 --- a/perl5/Local/Desktop.pm +++ b/perl5/Local/Desktop.pm @@ -32,6 +32,7 @@ use List::Util "first"; our @EXPORT = qw( $wmipc wmipc + @all_workspaces fresh_workspace compact_workspaces select_wallpaper_files @@ -41,11 +42,11 @@ our @EXPORT = qw( `sh -c "command -v i3-msg"`; our $wmipc = $? == 0 ? "i3-msg" : "swaymsg"; -sub wmipc { system "$wmipc -q " . join ", ", @_ } +sub wmipc { system $wmipc, "-q", join "; ", @_ } my $output_re = qr/ ([0-9]+)x([0-9]+)\+([0-9]+)\+([0-9]+) /; -my @all_workspaces = ( +our @all_workspaces = ( "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11:F1", "12:F2", "13:F3", "14:F4", "15:F5", "16:F6", "17:F7", "18:F8", 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" |