aboutsummaryrefslogtreecommitdiff
path: root/tests/waybar-airplane
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-21 17:48:47 -0400
committerCraig Jennings <c@cjennings.net>2026-05-21 17:48:47 -0400
commit09f4d205fe463faf676f95e798d08e8bf498be96 (patch)
treeac60b2aa4d8350d5ab3c0e6f76361daf70d1d702 /tests/waybar-airplane
parenteee30be993c6ff79a5e7fa5f37d6ba368dc0c3d9 (diff)
downloadarchsetup-09f4d205fe463faf676f95e798d08e8bf498be96.tar.gz
archsetup-09f4d205fe463faf676f95e798d08e8bf498be96.zip
feat(hyprland): add airplane-mode waybar toggle
I added a laptop-only waybar button that drops the machine into a low-power state and restores it on a second click. Engaging turns wifi off, sets the CPU energy-performance preference to power, dims the backlight to 35%, and stops network-only services (tailscale, proton-vpn, avahi, cups, wsdd, geoclue, sshd, fail2ban, syncthing). Bluetooth is left alone so earbuds keep working. Disengaging replays the state recorded when airplane mode was engaged rather than writing hardcoded defaults. A lever already in its low-power position is left untouched: wifi that was already off stays off, and a service that was already stopped isn't restarted. The indicator hides itself on machines with no battery, so desktops never show the button. State lives in $XDG_RUNTIME_DIR/airplane-state, and the bar refreshes the moment the toggle fires via a realtime signal.
Diffstat (limited to 'tests/waybar-airplane')
-rw-r--r--tests/waybar-airplane/test_waybar_airplane.py156
1 files changed, 156 insertions, 0 deletions
diff --git a/tests/waybar-airplane/test_waybar_airplane.py b/tests/waybar-airplane/test_waybar_airplane.py
new file mode 100644
index 0000000..b3b9c06
--- /dev/null
+++ b/tests/waybar-airplane/test_waybar_airplane.py
@@ -0,0 +1,156 @@
+"""Tests for dotfiles/hyprland/.local/bin/waybar-airplane.
+
+The script emits one JSON line for waybar's custom/airplane module, derived
+from the airplane-mode state file that the airplane-mode toggle maintains at
+$XDG_RUNTIME_DIR/airplane-state. The file holds key=value lines; the only
+key the indicator reads is `mode` (on/off). Tests point XDG_RUNTIME_DIR at a
+temp dir and assert on the emitted JSON. No mocking: the real script runs
+against a real state file.
+
+Run from repo root:
+ python3 -m unittest tests.waybar-airplane.test_waybar_airplane
+"""
+
+import json
+import os
+import shutil
+import subprocess
+import tempfile
+import unittest
+
+
+REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
+SCRIPT = os.path.join(REPO_ROOT, "dotfiles/hyprland/.local/bin/waybar-airplane")
+
+
+class WaybarAirplaneHarness(unittest.TestCase):
+
+ def setUp(self):
+ self.tmp = tempfile.mkdtemp(prefix="waybar-airplane-test-")
+ self.state_file = os.path.join(self.tmp, "airplane-state")
+ # Fake power-supply dir with a battery → the script treats the host as
+ # a laptop. Desktop tests point this at a battery-less dir.
+ self.ps_dir = os.path.join(self.tmp, "power_supply")
+ os.makedirs(os.path.join(self.ps_dir, "BAT0"))
+
+ def tearDown(self):
+ shutil.rmtree(self.tmp, ignore_errors=True)
+
+ def set_state(self, contents):
+ with open(self.state_file, "w") as f:
+ f.write(contents)
+
+ def run_script(self):
+ env = os.environ.copy()
+ env["XDG_RUNTIME_DIR"] = self.tmp
+ env["AIRPLANE_POWER_SUPPLY_DIR"] = self.ps_dir
+ return subprocess.run(
+ [SCRIPT], env=env, capture_output=True, text=True, timeout=10,
+ )
+
+ def emitted_json(self):
+ result = self.run_script()
+ self.assertEqual(result.returncode, 0, msg=result.stderr)
+ return json.loads(result.stdout)
+
+
+# -----------------------------------------------------------------------------
+# Normal cases
+# -----------------------------------------------------------------------------
+
+class TestWaybarAirplaneNormal(WaybarAirplaneHarness):
+
+ def test_mode_on_emits_active_class_and_plane_icon(self):
+ # State is carried by class/color, not a slashed glyph: the same clear
+ # plane shows in both states (gold when active, gray when inactive).
+ self.set_state("mode=on\nwifi=enabled\n")
+ data = self.emitted_json()
+ self.assertEqual(data["class"], "active")
+ self.assertIn("", data["text"]) # plane
+ self.assertIn("on", data["tooltip"].lower())
+
+ def test_mode_off_emits_inactive_class_and_plane_icon(self):
+ self.set_state("mode=off\n")
+ data = self.emitted_json()
+ self.assertEqual(data["class"], "inactive")
+ self.assertIn("", data["text"]) # plane
+ self.assertIn("off", data["tooltip"].lower())
+
+
+# -----------------------------------------------------------------------------
+# Boundary cases
+# -----------------------------------------------------------------------------
+
+class TestWaybarAirplaneBoundary(WaybarAirplaneHarness):
+
+ def test_missing_state_file_defaults_to_inactive(self):
+ # No state file → airplane mode is not engaged (radios on = safe default).
+ data = self.emitted_json()
+ self.assertEqual(data["class"], "inactive")
+
+ def test_mode_on_with_other_keys_present(self):
+ # The toggle writes several keys; the indicator only cares about mode.
+ self.set_state(
+ "mode=on\nwifi=enabled\nepp=balance_performance\n"
+ "brightness=96000\nstopped_system=sshd.service tailscaled.service\n"
+ )
+ data = self.emitted_json()
+ self.assertEqual(data["class"], "active")
+
+ def test_unknown_mode_value_treated_as_inactive(self):
+ # Anything that isn't "on" reads as off (fail-safe: radios shown as on).
+ self.set_state("mode=garbage\n")
+ data = self.emitted_json()
+ self.assertEqual(data["class"], "inactive")
+
+ def test_empty_state_file_defaults_to_inactive(self):
+ self.set_state("")
+ data = self.emitted_json()
+ self.assertEqual(data["class"], "inactive")
+
+ def test_output_is_a_single_json_object(self):
+ self.set_state("mode=on\n")
+ result = self.run_script()
+ lines = [ln for ln in result.stdout.splitlines() if ln.strip()]
+ self.assertEqual(len(lines), 1, msg=f"expected one line, got {lines!r}")
+
+
+# -----------------------------------------------------------------------------
+# Laptop gating — the module only shows on machines with a battery
+# -----------------------------------------------------------------------------
+
+class TestWaybarAirplaneLaptopGating(WaybarAirplaneHarness):
+
+ def run_desktop(self):
+ # Point the power-supply dir at a battery-less location (a desktop).
+ env = os.environ.copy()
+ env["XDG_RUNTIME_DIR"] = self.tmp
+ empty = os.path.join(self.tmp, "power_supply_desktop")
+ os.makedirs(empty, exist_ok=True) # AC adapter only, no BAT*
+ os.makedirs(os.path.join(empty, "ADP1"), exist_ok=True)
+ env["AIRPLANE_POWER_SUPPLY_DIR"] = empty
+ return subprocess.run(
+ [SCRIPT], env=env, capture_output=True, text=True, timeout=10,
+ )
+
+ def test_desktop_emits_nothing(self):
+ # No battery → module hidden → script prints no output.
+ self.set_state("mode=off\n")
+ result = self.run_desktop()
+ self.assertEqual(result.returncode, 0, msg=result.stderr)
+ self.assertEqual(result.stdout.strip(), "")
+
+ def test_desktop_emits_nothing_even_when_engaged(self):
+ self.set_state("mode=on\n")
+ result = self.run_desktop()
+ self.assertEqual(result.stdout.strip(), "")
+
+ def test_laptop_with_battery_emits_module(self):
+ # Sanity: the battery-present harness (default) does emit JSON.
+ self.set_state("mode=off\n")
+ data = self.emitted_json()
+ self.assertEqual(data["class"], "inactive")
+
+
+if __name__ == "__main__":
+ unittest.main()