summaryrefslogtreecommitdiff
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
parentccc837803226e9c1b9cffc6335b0b83268b0ecbe (diff)
downloaddotfiles-2d8f89e3473355c13fb28e04861218cf7fde2555.tar.gz
start work on PaperWM-inspired extensions to Sway setup
-rw-r--r--.config/sway/config61
-rw-r--r--perl5/Local/Desktop.pm5
-rwxr-xr-xscripts/desktop/i3status-wrapper309
-rwxr-xr-xscripts/desktop/i3status-wrapper-msg10
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"