TL;DR
I built a dynamic workspace loader for i3wm that lets me switch between multiple context-aware workspace groups (like "Client", "Company", and "Personal") without losing state. It uses Perl, AnyEvent::I3, YAML config, and tick events to load layouts and launch apps on demand.
Workspaces on Demand
I have been using i3 as a Window Manager for years now. And while I love working with i3, there is one thing I found annoying. And that is that it lacks context awareness. What do I mean by this? I use it in several contexts: "Client", "Company", and "Personal". Meaning, I'm in either client mode, company mode or personal mode. And 'client mode' can mean working with one or multiple clients at once. I pitched my initial idea on i3's discussion pages and someone else asked about something similar as KDE's "Activities".
Not enough workspaces == mental overload
With i3 you can create a ton of workspaces, but I found myself often using workspaces 1-3, and 8-0. These are only 6 workspaces, that are directly accessible. In most contexts I want to have three workspaces: "debug", "cli", and "misc". If we have three contexts we use 1-3, 4-6, and 7-9 plus workspace 0 as my safe haven. And I use 0 for things I want to super-quick access to regardless of context.
But what if we add a new client, or we want to have different workspaces for "personal" contexts? I didn't want to remember in which context I was, I just wanted to remember, 1-3 are where I code, 0 is where my safe haven is and everything else is just hidden from sight.
Proof of concept: zsh and jq
I needed something that could deal with these contexts while preserving state of other contexts already in use. I started out with a simple zsh script but I quickly ran into some issues where the shell wasn't really the right tool for the job. I have a love/hate relationship with jq
. I love it for quick things, but I hate it for more complex things. I always forget the syntax and with Perl (or any other programming language) I can just work with the data structure.
I also looked into IPC events from i3 — you use them with i3-msg already. Now I wanted to know how I could hook into events from i3 to determine state changes. And I could.
I3 also ships a Perl module AnyEvent::I3. Big hat tip to the i3-developers.
AnyEvent::I3X::Workspace::OnDemand
First I started out with a simple subscribe
method that checked for the init
event to ensure I was able to see the creation of a workspace. It's important to note that while you define a workspace in i3's config, it doesn't actually create them. It is created run time when you request it.
On init
things are rather simple, we append the layout using append_layout
and we start up all the applications we want to use. To support this I created a couple of things:
- A constructor param that has a layout path defined. So all your layouts are stored there.
- A constructor param that has workspaces defined.
- A constructor param that defines which applications are started.
- And a constructor param that defines which groups are used.
How does this work? You configure your workspaces, groups and applications like so:
use AnyEvent::I3X::Workspace::OnDemand;
my $i3 = AnyEvent::I3X::Workspace::OnDemand->new(
debug => 0,
layout_path => "$ENV{HOME}/.config/i3",
workspaces => {
foo => {
layout => 'foo.json',
},
bar => {
layout => 'bar.json',
groups => {
foo => undef,
# Override the layout for group bar
bar => { layout => 'foo.json' },
}
},
baz => {
layout => 'baz.json',
groups => {
all => undef,
}
}
},
groups => [
qw(foo bar baz)
],
swallows => [
{
cmd => 'kitty',
match => {
class => '^kitty$',
}
},
{
# Start firefox on group bar
cmd => 'firefox',
on => {
group => 'bar',
}
match => {
window_role => '^browser$',
}
},
{
cmd => 'google-chrome',
on => {
group => 'foo',
}
match => {
window_role => '^browser$',
}
}
],
);
Bulletproofing edge cases
Now, because of timing issues you can end up with a workspace that has a layout defined but nothing has started yet. So we also look for the focus
event and here we also try to start the applications defined on the workspace.
When the user switches to a workspace, we walk the i3 tree to find any nodes that match our layout, and we spawn the apps that belong there — only if they aren’t already running.
Sending events ourselves to ourselves
So now that we have this logic defined we can go into changing groups. Group changes are sent via ticks
:
i3-msg -t send_tick group:personal
This means our workspace logic needs to listen to tick events too. This meant that I needed to support multiple event types.
Supported event types
The module supports the following event types: workspace, barconfig_update (caveat emptor), tick, shutdown, output, mode, window, and binding. The most important ones for us are: workspace, tick, and shutdown. These have simple helper functions on_workspace
, on_tick
and on_shutdown
:
$self->on_workspace($name, $type, $sub)
Subscribe to a workspace event for workspace $name of $type with $sub.
$type can be any of the following events from i3 plus any or *
$i3->on_workspace(
'www', 'init',
sub {
my $self = shift;
my $i3 = shift;
my $event = shift;
$self->append_layout($event->{current}{name}, '/path/to/layout.json');
}
);
$self->on_tick($payload, $sub)
Subscribe to a tick event with $payload and perform the action. Your sub needs to support the following prototype:
sub foo($self, $i3, $event) {
print "Yay processed foo tick";
}
$self->on_tick('foo', \&foo)
On shutdown isn't really documented on CPAN just yet, but its rather simple:
$i3->on_shutdown(exit => sub { exit 0 });
$i3->on_shutdown(restart => sub { exit 0 });
Configuring i3
So now the only thing you need to do in your i3 config is configure them. I personally use the binding modes from i3 to organize similar commands into one grouping:
# Dynamic workspaces
bindsym $mod+w mode "Activities"
mode "Activities" {
bindsym 0 exec i3-msg -t send_tick group:foo; mode default
bindsym 9 exec i3-msg -t send_tick group:bar; mode default
bindsym 8 exec i3-msg -t send_tick group:baz; mode default
bindsym Return mode "default"
bindsym Escape mode "default"
}
Now I can switch between almost unlimited contexts. I only use three, but you can go wild.
Layout saving and restoring
And for those who want to know how to create a layout:
i3-save-tree --workspace foo > ~/.config/i3/foo.json
You'll need to edit the JSON, eg, trim the swallow
block, but I'll leave that to you and the documentation found on the i3 site.
Planned for release
I personally use a YAML configuration file to configure my workspaces and I have a special i3-wod script that will always be executed:
exec_always --no-startup-id "i3-wod &"
I'm planning on releasing i3-wod in the main module. I had it separate because I wanted to play a bit with it without committing to a public interface. But we've come a long way since.
You can install the module by running:
cpanm AnyEvent::I3X::Workspace::OnDemand
And for now, i3-wod, grab it here: GitLab repo – i3-wod script.
Top comments (0)