Fixed some bugs, updated how packages are handeled. Prepared on AUR publishing
All checks were successful
CI / test (push) Successful in 40s

This commit is contained in:
Asger Weirsøe
2026-05-19 15:59:28 +02:00
parent 5489847475
commit 0d8f843814
11 changed files with 1317 additions and 340 deletions

0
tests/__init__.py Normal file
View File

View File

@@ -0,0 +1,400 @@
from textwrap import dedent
from pi3.smart_workspace import (
Rect,
_planned_files,
expand_i3_includes,
find_output_at_cursor,
parse_workspaces_per_output,
render_bindings,
render_workspaces_for_output,
sanitize_var_suffix,
)
# ---------------------------------------------------------------------------
# parse_workspaces_per_output
# ---------------------------------------------------------------------------
def test_parses_readme_example():
"""The full README.md example config round-trips through the parser."""
config = dedent("""
# Displays
set $primary eDP
set $top HDMI-A-0
set $bottom HDMI2
# Workspaces
set $ws1 1:1
set $ws2 2:2
set $TopWs1 3:1
set $TopWs2 4:2
set $BottomWs1 5:1
workspace $ws1 output $primary
workspace $ws2 output $primary
workspace $TopWs1 output $top
workspace $TopWs2 output $top
workspace $BottomWs1 output $bottom
""")
result = parse_workspaces_per_output(config, ["eDP", "HDMI-A-0", "HDMI2"])
assert result == {
"eDP": ["1:1", "2:2"],
"HDMI-A-0": ["3:1", "4:2"],
"HDMI2": ["5:1"],
}
def test_inactive_output_is_skipped():
config = dedent("""
set $primary eDP
set $external HDMI2
set $ws1 1:1
set $extWs1 9:ext
workspace $ws1 output $primary
workspace $extWs1 output $external
""")
# HDMI2 is unplugged
result = parse_workspaces_per_output(config, ["eDP"])
assert result == {"eDP": ["1:1"]}
def test_declaration_order_is_preserved():
config = dedent("""
set $out eDP
set $a 3:c
set $b 1:a
set $c 2:b
workspace $b output $out
workspace $c output $out
workspace $a output $out
""")
result = parse_workspaces_per_output(config, ["eDP"])
assert result == {"eDP": ["1:a", "2:b", "3:c"]}
def test_unrelated_set_lines_are_ignored():
config = dedent("""
set $primary eDP
set $ws1 1:1
set $term i3-sensible-terminal
set $launcher dmenu_run
set $mod Mod4
workspace $ws1 output $primary
""")
result = parse_workspaces_per_output(config, ["eDP"])
assert result == {"eDP": ["1:1"]}
def test_empty_active_outputs_returns_empty():
config = "set $primary eDP\nset $ws1 1:1\nworkspace $ws1 output $primary\n"
assert parse_workspaces_per_output(config, []) == {}
def test_binding_with_unknown_var_is_skipped():
config = dedent("""
set $primary eDP
set $ws1 1:1
workspace $ws1 output $unknownOutput
workspace $unknownWs output $primary
""")
assert parse_workspaces_per_output(config, ["eDP"]) == {}
def test_output_name_with_special_regex_chars_is_escaped():
# DisplayPort outputs often look like DP-1, HDMI-A-0 — regex-sensitive
config = dedent("""
set $main DP-1
set $ws1 1:1
workspace $ws1 output $main
""")
assert parse_workspaces_per_output(config, ["DP-1"]) == {"DP-1": ["1:1"]}
# ---------------------------------------------------------------------------
# find_output_at_cursor
# ---------------------------------------------------------------------------
def _outputs(*specs):
return [(name, Rect(*rect)) for name, rect in specs]
def test_single_output_contains_origin():
outputs = _outputs(("eDP", (0, 0, 1920, 1080)))
assert find_output_at_cursor(outputs, (0, 0)) == "eDP"
assert find_output_at_cursor(outputs, (960, 540)) == "eDP"
assert find_output_at_cursor(outputs, (1920, 1080)) == "eDP"
def test_cursor_outside_all_outputs_returns_none():
outputs = _outputs(("eDP", (0, 0, 1920, 1080)))
assert find_output_at_cursor(outputs, (3000, 2000)) is None
def test_side_by_side_outputs_shared_vertical_edge_belongs_to_left():
# Primary at origin, secondary directly to the right of it.
outputs = _outputs(
("eDP", (0, 0, 1920, 1080)),
("HDMI", (1920, 0, 1920, 1080)),
)
# On the shared edge x=1920 → belongs to the LEFT output.
assert find_output_at_cursor(outputs, (1920, 500)) == "eDP"
# Just past it → right output.
assert find_output_at_cursor(outputs, (1921, 500)) == "HDMI"
# Deep inside right output.
assert find_output_at_cursor(outputs, (3000, 500)) == "HDMI"
def test_stacked_outputs_shared_horizontal_edge_belongs_to_top():
outputs = _outputs(
("eDP", (0, 0, 1920, 1080)),
("HDMI", (0, 1080, 1920, 1080)),
)
# On shared edge y=1080 → top output.
assert find_output_at_cursor(outputs, (500, 1080)) == "eDP"
# Just past it → bottom output.
assert find_output_at_cursor(outputs, (500, 1081)) == "HDMI"
def test_iteration_order_does_not_change_edge_attribution():
# Same as side-by-side, but with outputs declared in opposite order.
outputs = _outputs(
("HDMI", (1920, 0, 1920, 1080)),
("eDP", (0, 0, 1920, 1080)),
)
assert find_output_at_cursor(outputs, (1920, 500)) == "eDP"
# ---------------------------------------------------------------------------
# Quoted workspace values (e.g. set $ws1 "1")
# ---------------------------------------------------------------------------
def test_parses_quoted_workspace_values():
"""i3 configs commonly use 'set $ws1 "1"' — the parser must strip quotes."""
config = dedent('''
set $primary eDP
set $ws1 "1"
set $ws2 "2:web"
workspace $ws1 output $primary
workspace $ws2 output $primary
''')
assert parse_workspaces_per_output(config, ["eDP"]) == {"eDP": ["1", "2:web"]}
def test_quoted_and_unquoted_can_mix():
config = dedent('''
set $primary eDP
set $ws1 "1"
set $ws2 2:b
workspace $ws1 output $primary
workspace $ws2 output $primary
''')
assert parse_workspaces_per_output(config, ["eDP"]) == {"eDP": ["1", "2:b"]}
# ---------------------------------------------------------------------------
# init rendering
# ---------------------------------------------------------------------------
def test_sanitize_var_suffix_replaces_non_alnum():
assert sanitize_var_suffix("eDP-1") == "eDP_1"
assert sanitize_var_suffix("HDMI-A-0") == "HDMI_A_0"
assert sanitize_var_suffix("DVI-I-2-2") == "DVI_I_2_2"
assert sanitize_var_suffix("plain") == "plain"
def test_render_workspaces_for_output_uses_global_offset():
out = render_workspaces_for_output("HDMI-A-0", global_start=5, count=3)
# Global numbers continue from 5; local labels reset to 1.
assert "set $ws_HDMI_A_0_1 5:1" in out
assert "set $ws_HDMI_A_0_2 6:2" in out
assert "set $ws_HDMI_A_0_3 7:3" in out
# Output alias is present and uses the sanitized var name.
assert "set $out_HDMI_A_0 HDMI-A-0" in out
# All three workspaces are bound to the output.
assert out.count("output $out_HDMI_A_0") == 3
def test_render_workspaces_rejects_bad_args():
import pytest
with pytest.raises(ValueError):
render_workspaces_for_output("eDP-1", global_start=0, count=8)
with pytest.raises(ValueError):
render_workspaces_for_output("eDP-1", global_start=1, count=0)
def test_render_bindings_emits_three_blocks():
out = render_bindings(count=2)
assert "bindsym $mod+1 exec --no-startup-id pi3-smart-workspace -i 1" in out
assert "bindsym $mod+2 exec --no-startup-id pi3-smart-workspace -i 2" in out
assert "bindsym $mod+Shift+1 exec --no-startup-id pi3-smart-workspace -i 1 -s" in out
assert "bindsym $mod+Ctrl+2 exec --no-startup-id pi3-smart-workspace -i 2 -s -k" in out
def test_render_bindings_honors_custom_mod():
out = render_bindings(count=1, mod="$myMod")
assert "$myMod+1" in out
assert "$mod+1" not in out
def test_planned_files_assigns_contiguous_global_ranges():
from pathlib import Path
outputs = [
("eDP-1", Rect(0, 0, 1920, 1080)),
("HDMI-A-0", Rect(1920, 0, 1920, 1080)),
("DP-1", Rect(3840, 0, 1920, 1080)),
]
files = _planned_files(Path("/tmp/x"), outputs, count=3)
# First file is the shared bindings.conf, then one workspaces.conf per output.
paths = [p for p, _ in files]
assert paths == [
Path("/tmp/x/bindings.conf"),
Path("/tmp/x/eDP-1/workspaces.conf"),
Path("/tmp/x/HDMI-A-0/workspaces.conf"),
Path("/tmp/x/DP-1/workspaces.conf"),
]
edp = files[1][1]
hdmi = files[2][1]
dp = files[3][1]
assert "set $ws_eDP_1_1 1:1" in edp
assert "set $ws_HDMI_A_0_1 4:1" in hdmi # 3 (count) + 1
assert "set $ws_DP_1_1 7:1" in dp # 6 + 1
# ---------------------------------------------------------------------------
# expand_i3_includes
# ---------------------------------------------------------------------------
def test_expand_includes_inlines_absolute_path(tmp_path):
inc = tmp_path / "inc.conf"
inc.write_text("set $foo bar\n")
main = f"# top\ninclude {inc}\n# tail\n"
result = expand_i3_includes(main, tmp_path)
assert "set $foo bar" in result
assert "include " not in result # directive consumed
assert "# top" in result and "# tail" in result
def test_expand_includes_handles_glob_in_alphabetical_order(tmp_path):
(tmp_path / "20-b.conf").write_text("# B\n")
(tmp_path / "10-a.conf").write_text("# A\n")
(tmp_path / "30-c.conf").write_text("# C\n")
main = f"include {tmp_path}/*.conf\n"
result = expand_i3_includes(main, tmp_path)
# Glob results are sorted, so A appears before B which appears before C.
assert result.index("# A") < result.index("# B") < result.index("# C")
def test_expand_includes_resolves_relative_paths_against_base_dir(tmp_path):
(tmp_path / "sub.conf").write_text("set $rel ok\n")
main = "include sub.conf\n"
result = expand_i3_includes(main, tmp_path)
assert "set $rel ok" in result
def test_expand_includes_recurses_through_chained_includes(tmp_path):
(tmp_path / "a.conf").write_text(f"include {tmp_path}/b.conf\n")
(tmp_path / "b.conf").write_text("set $deep yes\n")
main = f"include {tmp_path}/a.conf\n"
result = expand_i3_includes(main, tmp_path)
assert "set $deep yes" in result
def test_expand_includes_breaks_self_referential_cycle(tmp_path):
a = tmp_path / "a.conf"
a.write_text(f"set $marker present\ninclude {a}\n")
main = f"include {a}\n"
# Without cycle protection this would recurse forever.
result = expand_i3_includes(main, tmp_path)
assert result.count("set $marker present") == 1
def test_expand_includes_tolerates_missing_files(tmp_path):
main = f"set $before yes\ninclude {tmp_path}/does-not-exist.conf\nset $after yes\n"
# Glob simply yields nothing — must not raise.
result = expand_i3_includes(main, tmp_path)
assert "set $before yes" in result and "set $after yes" in result
def test_expand_then_parse_finds_workspaces_from_included_files(tmp_path):
"""The end-to-end fix: workspaces declared in an included file must
end up in parse_workspaces_per_output's output."""
(tmp_path / "out.conf").write_text(
"set $primary eDP\n"
"set $ws1 1:1\n"
"set $ws2 2:2\n"
"workspace $ws1 output $primary\n"
"workspace $ws2 output $primary\n"
)
main = f"set $mod Mod4\ninclude {tmp_path}/out.conf\n"
expanded = expand_i3_includes(main, tmp_path)
result = parse_workspaces_per_output(expanded, ["eDP"])
assert result == {"eDP": ["1:1", "2:2"]}
def test_init_output_parses_back_through_parser():
"""Round-trip: what `init` generates is parseable by `parse_workspaces_per_output`."""
from pathlib import Path
outputs = [
("eDP-1", Rect(0, 0, 1920, 1080)),
("HDMI-A-0", Rect(1920, 0, 1920, 1080)),
]
files = _planned_files(Path("/anywhere"), outputs, count=3)
# Concatenate everything except bindings.conf (parser only cares about
# the workspace/output declarations).
combined = "\n".join(content for _, content in files[1:])
result = parse_workspaces_per_output(combined, ["eDP-1", "HDMI-A-0"])
assert result == {
"eDP-1": ["1:1", "2:2", "3:3"],
"HDMI-A-0": ["4:1", "5:2", "6:3"],
}