So I am using i3 an Linux window manager to manage my windows. In addition this is run on a laptop that is frequently mounted to several output-displays. This is handled by the following lines in my i3 config file
set $firstMonitor DP-2-1
set $secondMonitor DP-1-2
set $laptop eDP-1
workspace 1 output $firstMonitor $laptop
workspace 2 output $firstMonitor $laptop
workspace 3 output $firstMonitor $laptop
workspace 4 output $firstMonitor $laptop
workspace 5 output $secondMonitor $laptop
workspace 6 output $secondMonitor $laptop
workspace 7 output $secondMonitor $laptop
workspace 8 output $secondMonitor $laptop
workspace 9 output $secondMonitor $laptop
workspace 10 output $laptop $laptop
For convience we can define variables in the config file using set and prefix them with $. The lines above tells my window manager I would like workspace 1
to 4
on $firstMonitor
if it exists. Otherwise fall back to $laptop
.
I am using this for various scripts and therefore need to extract this information from my config file, and store it somewhere. For instance it could look like this
[
{
'DP-2-1': ['1', '2', '3', '4'],
'DP-1-2': ['5', '6', '7', '8', '9'],
'eDP-1': ['10']
},
{
'eDP-1': ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'
}
]
parsed. To do so I wrote the following Python code to extract which output each workspace is assigned to. Note that the i3 file is picky about whitespaces, meaning the line set$firstMonitor DP-2-1
will not compile.
from pathlib import Path
import collections
CONFIG = Path.home() / ".config" / "i3" / "config"
def read_config(config=CONFIG):
with open(config, "r") as f:
return f.readlines()
def read_workspace_outputs(lines=read_config()):
"""Reads an i3 config, returns which output each workspaces is assigned to
Example:
set $firstMonitor DP-2-1
set $secondMonitor DP-1-2
set $laptop eDP-1
set $browser 1
workspace $browser output $firstMonitor $laptop
workspace 2 output $firstMonitor $laptop
workspace 3 output $firstMonitor $laptop
workspace 4 output $firstMonitor $laptop
workspace 5 output $secondMonitor $laptop
workspace 6 output $secondMonitor $laptop
workspace 7 output $secondMonitor $laptop
workspace 8 output $secondMonitor $laptop
workspace 9 output $secondMonitor $laptop
workspace 10 output $laptop $laptop
Will return
[
{
'DP-2-1': ['1', '2', '3', '4'],
'DP-1-2': ['5', '6', '7', '8', '9'],
'eDP-1': ['10']
},
{
'eDP-1': ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'
}
]
>>> read_workspace_outputs(lines=['workspace 1 output eDP-1'])
[defaultdict(<class 'list'>, {'eDP-1': ['1']})]
>>> read_workspace_outputs(lines=['set $laptop eDP-1','workspace 1 output eDP-1'])
[defaultdict(<class 'list'>, {'eDP-1': ['1']})]
>>> read_workspace_outputs(lines=['set $browser 1','workspace $browser output eDP-1'])
[defaultdict(<class 'list'>, {'eDP-1': ['1']})]
>>> read_workspace_outputs(lines=[
... "set $firstMonitor DP-2-1",
... "set $secondMonitor DP-1-2",
... "set $laptop eDP-1",
... "",
... "workspace 1 output $firstMonitor $laptop",
... "workspace 3 output $secondMonitor $laptop",
... "workspace 5 output $laptop $laptop",
... ])
[defaultdict(<class 'list'>, {'DP-2-1': ['1'], 'DP-1-2': ['3'], 'eDP-1': ['5']}), defaultdict(<class 'list'>, {'eDP-1': ['1', '3', '5']})]
"""
# Extract workspaces and variables [set $name command] from file
def get_workspaces_and_variables(lines):
workspaces = []
variables_2_commands = dict()
for line in lines:
if line.startswith("workspace"):
workspaces.append(line.strip())
elif line.startswith("set"):
_, variable, *command = line.split()
variables_2_commands[variable] = " ".join(command)
return workspaces, variables_2_commands
# Convert back $name to command for outputs and workspaces
def workspaces_without_variables(workspaces, variables):
workspace_outputs = []
for workspace in workspaces:
workspace_str, output_str = workspace.split("output")
workspace, variable = workspace_str.split()
workspace_number = (
variables[variable] if variable.startswith("$") else variable
)
outputs = [
variables[output] if output.startswith("$") else output
for output in output_str.split()
]
workspace_outputs.append([workspace_number, outputs])
return workspace_outputs
# Currently things are stored as workspaces = [[output1, output 2], ...]
# This flips the order and stores it as a dict with outputs as keys and values workspaces
def workspaces_2_outputs(workspaces):
output_workspaces = [
collections.defaultdict(list)
for _ in range(len(max((x[1] for x in workspaces), key=len)))
]
for (workspace_number, outputs) in workspaces:
for j, output in enumerate(outputs):
output_workspaces[j][output].append(workspace_number)
return output_workspaces
workspaces_w_variables, variables = get_workspaces_and_variables(lines)
variable_free_workspaces = workspaces_without_variables(
workspaces_w_variables, variables
)
return workspaces_2_outputs(variable_free_workspaces)
if __name__ == "__main__":
import doctest
import yaml
from yaml.representer import Representer
doctest.testmod()
OUTPUT_WORKSPACE_CONFIG = (
Path.home() / ".config" / "i3" / "oisov-scripts" / "i3-output-workspace-2.yaml"
)
yaml.add_representer(collections.defaultdict, Representer.represent_dict)
with open(OUTPUT_WORKSPACE_CONFIG, "w") as file:
yaml.dump(read_workspace_outputs(), file)
The output.yaml
file looks like this
- DP-1-2:
- '5'
- '6'
- '7'
- '8'
- '9'
DP-2-1:
- '1'
- '2'
- '3'
- '4'
eDP-1:
- '10'
- eDP-1:
- '1'
- '2'
- '3'
- '4'
- '5'
- '6'
- '7'
- '8'
- '9'
- '10'
I will add some typing hints in the future, but for now I was wondering if this is the best approach to extract this information. The code feels a bit clunky, even if it does that I want it to.
For testing one can use the first code block in this example. A longer i3 example file can for instance be https://github.com/sainathadapa/i3-wm-config/blob/master/i3-default-config-backup, due note that this does not set the workspaces to specific outputs.