diff options
| author | Craig Jennings <c@cjennings.net> | 2026-05-19 12:31:17 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-05-19 12:31:17 -0500 |
| commit | 924fec1f00e7ef5e497575488701e8c9eb2606f0 (patch) | |
| tree | 9c0df313352ab01bd608fd408871ef7de418a39c /tests/tmux-util/test_tmux_util.py | |
| parent | 5d8d9df7d1b95c1a6a7bf25ddf57652c86f110b3 (diff) | |
| download | archsetup-924fec1f00e7ef5e497575488701e8c9eb2606f0.tar.gz archsetup-924fec1f00e7ef5e497575488701e8c9eb2606f0.zip | |
feat(tmux-util): add ls subcommand
tmux-util ls is an opinionated replacement for `tmux ls` with columns for state (attached/detached), name, idle time (humanized), window count, and the current pane's cwd (tilde-fied if it sits under $HOME).
The cwd query goes through `tmux display -p -t <session> '#{pane_current_path}'`, which returns the cwd of the active pane of the active window. That's close enough to "what the session is about" for a one-line summary.
Idle calculation reads `date +%s` by default and accepts an override via the TMUX_UTIL_NOW env var so tests can pin "now" to a known epoch.
12 new tests cover Normal cases (attached / detached, multiple sessions) and Boundary cases (no sessions, idle exactly at minute / hour / day boundaries, $HOME tilde). One existing dispatch test got reworked because the original stub target (`ls`) is no longer unimplemented. Total suite is 26 tests, all green.
The fake-tmux harness picked up two things along the way: real format-string parsing for `list-sessions -F` and a new handler for `display -p`. The state file format extended to include activity epoch, window count, and cwd, with sensible defaults for older 3-tuple test inputs so the reap tests keep passing untouched.
Diffstat (limited to 'tests/tmux-util/test_tmux_util.py')
| -rw-r--r-- | tests/tmux-util/test_tmux_util.py | 142 |
1 files changed, 137 insertions, 5 deletions
diff --git a/tests/tmux-util/test_tmux_util.py b/tests/tmux-util/test_tmux_util.py index 8060ccc..283c3c7 100644 --- a/tests/tmux-util/test_tmux_util.py +++ b/tests/tmux-util/test_tmux_util.py @@ -50,11 +50,21 @@ class TmuxUtilHarness(unittest.TestCase): shutil.rmtree(self.tmp, ignore_errors=True) def set_sessions(self, sessions): - """sessions: list of (name, attached, [pane_pids]).""" + """sessions: list of tuples (name, attached, pids[, activity, windows, cwd]). + + Defaults for trailing fields: activity=0, windows=1, cwd='/tmp'. + cwd must not contain spaces (the fake's state file is space-delimited). + """ with open(self.state_file, "w") as f: - for name, attached, pids in sessions: + for s in sessions: + name = s[0] + attached = s[1] + pids = s[2] + activity = s[3] if len(s) > 3 else 0 + windows = s[4] if len(s) > 4 else 1 + cwd = s[5] if len(s) > 5 else "/tmp" pids_csv = ",".join(str(p) for p in pids) if pids else "-" - f.write(f"{name} {attached} {pids_csv}\n") + f.write(f"{name} {attached} {pids_csv} {activity} {windows} {cwd}\n") def run_script(self, *args, env_extra=None): env = os.environ.copy() @@ -118,8 +128,8 @@ class TestDispatch(TmuxUtilHarness): self.assertIn("Usage: tmux-util", result.stderr) def test_unimplemented_subcommand_exits_nonzero(self): - # ls/go/pick/find/rename will land later; for now they stub out. - result = self.run_script("ls") + # go/pick/find/rename stub out for now; pick one to confirm the stub path. + result = self.run_script("pick") self.assertNotEqual(result.returncode, 0) self.assertIn("not implemented yet", result.stderr) @@ -242,5 +252,127 @@ class TestReapBoundary(TmuxUtilHarness): self.assertEqual(self.kill_calls(), []) +# ----------------------------------------------------------------------------- +# ls — Normal cases +# ----------------------------------------------------------------------------- + +class TestLsNormal(TmuxUtilHarness): + + def test_ls_prints_header_and_columns_for_attached_session(self): + # activity=950, now=1000 → idle 50s. cwd=/tmp/work → unchanged (not under HOME). + self.set_sessions([ + ("work", 1, [101], 950, 3, "/tmp/work"), + ]) + result = self.run_script("ls", env_extra={"TMUX_UTIL_NOW": "1000"}) + self.assertEqual(result.returncode, 0, msg=result.stderr) + out = result.stdout + self.assertIn("STATE", out) + self.assertIn("NAME", out) + self.assertIn("IDLE", out) + self.assertIn("WINDOWS", out) + self.assertIn("CWD", out) + self.assertIn("attached", out) + self.assertIn("work", out) + self.assertIn("50s", out) + self.assertIn("3", out) + self.assertIn("/tmp/work", out) + + def test_ls_shows_detached_state_for_unattached_session(self): + self.set_sessions([ + ("idle", 0, [201], 900, 1, "/tmp/idle"), + ]) + result = self.run_script("ls", env_extra={"TMUX_UTIL_NOW": "1000"}) + self.assertEqual(result.returncode, 0) + self.assertIn("detached", result.stdout) + self.assertIn("idle", result.stdout) + + def test_ls_lists_multiple_sessions(self): + self.set_sessions([ + ("foo", 1, [101], 990, 2, "/tmp/foo"), + ("bar", 0, [201], 900, 1, "/tmp/bar"), + ("baz", 0, [301], 800, 4, "/tmp/baz"), + ]) + result = self.run_script("ls", env_extra={"TMUX_UTIL_NOW": "1000"}) + self.assertEqual(result.returncode, 0) + for name in ("foo", "bar", "baz"): + self.assertIn(name, result.stdout) + + +# ----------------------------------------------------------------------------- +# ls — Boundary cases +# ----------------------------------------------------------------------------- + +class TestLsBoundary(TmuxUtilHarness): + + def test_ls_no_sessions_prints_message(self): + self.set_sessions([]) + result = self.run_script("ls") + self.assertEqual(result.returncode, 0) + self.assertIn("No tmux sessions", result.stdout) + + def test_ls_idle_zero_seconds(self): + self.set_sessions([ + ("now", 1, [101], 1000, 1, "/tmp/now"), + ]) + result = self.run_script("ls", env_extra={"TMUX_UTIL_NOW": "1000"}) + self.assertEqual(result.returncode, 0) + self.assertIn("0s", result.stdout) + + def test_ls_idle_under_minute_shows_seconds(self): + self.set_sessions([ + ("s59", 0, [201], 941, 1, "/tmp/s"), # 59s ago + ]) + result = self.run_script("ls", env_extra={"TMUX_UTIL_NOW": "1000"}) + self.assertIn("59s", result.stdout) + + def test_ls_idle_exactly_one_minute_shows_minutes(self): + self.set_sessions([ + ("m1", 0, [201], 940, 1, "/tmp/m"), # 60s ago + ]) + result = self.run_script("ls", env_extra={"TMUX_UTIL_NOW": "1000"}) + self.assertIn("1m", result.stdout) + + def test_ls_idle_under_hour_shows_minutes(self): + self.set_sessions([ + ("m45", 0, [201], 0, 1, "/tmp/m"), # 2700s = 45m + ]) + result = self.run_script("ls", env_extra={"TMUX_UTIL_NOW": "2700"}) + self.assertIn("45m", result.stdout) + + def test_ls_idle_exactly_one_hour_shows_hours(self): + self.set_sessions([ + ("h1", 0, [201], 0, 1, "/tmp/h"), # 3600s = 1h + ]) + result = self.run_script("ls", env_extra={"TMUX_UTIL_NOW": "3600"}) + self.assertIn("1h", result.stdout) + + def test_ls_idle_under_day_shows_hours(self): + self.set_sessions([ + ("h23", 0, [201], 0, 1, "/tmp/h"), # 23 * 3600 = 82800s + ]) + result = self.run_script("ls", env_extra={"TMUX_UTIL_NOW": "82800"}) + self.assertIn("23h", result.stdout) + + def test_ls_idle_exactly_one_day_shows_days(self): + self.set_sessions([ + ("d1", 0, [201], 0, 1, "/tmp/d"), # 86400s = 1d + ]) + result = self.run_script("ls", env_extra={"TMUX_UTIL_NOW": "86400"}) + self.assertIn("1d", result.stdout) + + def test_ls_tilde_fies_home_paths(self): + # cwd inside $HOME should render as ~/... + home = os.environ.get("HOME", "/home/cjennings") + cwd_under_home = home + "/code/foo" + self.set_sessions([ + ("h", 1, [101], 990, 1, cwd_under_home), + ]) + result = self.run_script("ls", env_extra={"TMUX_UTIL_NOW": "1000"}) + self.assertEqual(result.returncode, 0) + self.assertIn("~/code/foo", result.stdout) + # The absolute path under HOME should NOT appear in raw form + self.assertNotIn(cwd_under_home, result.stdout) + + if __name__ == "__main__": unittest.main() |
