aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-19 12:31:17 -0500
committerCraig Jennings <c@cjennings.net>2026-05-19 12:31:17 -0500
commit924fec1f00e7ef5e497575488701e8c9eb2606f0 (patch)
tree9c0df313352ab01bd608fd408871ef7de418a39c /tests
parent5d8d9df7d1b95c1a6a7bf25ddf57652c86f110b3 (diff)
downloadarchsetup-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')
-rwxr-xr-xtests/tmux-util/fake-tmux65
-rw-r--r--tests/tmux-util/test_tmux_util.py142
2 files changed, 187 insertions, 20 deletions
diff --git a/tests/tmux-util/fake-tmux b/tests/tmux-util/fake-tmux
index f6217bb..ba2d5cc 100755
--- a/tests/tmux-util/fake-tmux
+++ b/tests/tmux-util/fake-tmux
@@ -1,9 +1,11 @@
-#!/bin/sh
+#!/bin/bash
# Fake tmux for testing tmux-util.
#
# State file: $FAKE_TMUX_DIR/sessions.txt
-# One line per session, space-separated: <name> <attached> <pids_csv> [last_activity_epoch]
+# One line per session, space-separated:
+# <name> <attached> <pids_csv> [<activity_epoch> [<windows> [<cwd>]]]
# pids_csv is a comma-separated list of pane PIDs (or '-' for none)
+# cwd cannot contain spaces in test data.
#
# Log file: $FAKE_TMUX_DIR/calls.log
# Each invocation appended as a single line: tmux <args>
@@ -19,19 +21,36 @@ printf 'tmux %s\n' "$*" >> "$LOG"
cmd="$1"
shift
-# Helper: read state file, ignoring blank lines
read_state() {
[ -f "$STATE" ] || return 0
grep -v '^[[:space:]]*$' "$STATE" || true
}
+# Render a tmux format string against one session's fields.
+# Args: format, name, attached, activity, windows, cwd
+render_format() {
+ local out="$1" name="$2" attached="$3" activity="$4" windows="$5" cwd="$6"
+ out="${out//\#\{session_name\}/$name}"
+ out="${out//\#\{session_attached\}/$attached}"
+ out="${out//\#\{session_activity\}/$activity}"
+ out="${out//\#\{session_windows\}/$windows}"
+ out="${out//\#\{pane_current_path\}/$cwd}"
+ echo "$out"
+}
+
case "$cmd" in
list-sessions)
- # Format string is ignored; we always emit "<name> <attached>" because
- # that's the only format tmux-util uses against list-sessions.
- read_state | while IFS=' ' read -r name attached pids _rest; do
+ fmt=""
+ while [ "$#" -gt 0 ]; do
+ case "$1" in
+ -F) shift; fmt="${1:-}"; shift ;;
+ *) shift ;;
+ esac
+ done
+ [ -n "$fmt" ] || fmt='#{session_name} #{session_attached}'
+ read_state | while IFS=' ' read -r name attached pids activity windows cwd; do
[ -n "$name" ] || continue
- echo "$name $attached"
+ render_format "$fmt" "$name" "$attached" "${activity:-0}" "${windows:-1}" "${cwd:-/tmp}"
done
;;
list-panes)
@@ -51,6 +70,25 @@ case "$cmd" in
fi
done
;;
+ display)
+ fmt=""
+ target=""
+ while [ "$#" -gt 0 ]; do
+ case "$1" in
+ -p) shift ;;
+ -t) shift; target="$1"; shift ;;
+ *) fmt="$1"; shift ;;
+ esac
+ done
+ while IFS=' ' read -r name attached pids activity windows cwd; do
+ [ -n "$name" ] || continue
+ if [ "$name" = "$target" ]; then
+ render_format "$fmt" "$name" "$attached" "${activity:-0}" "${windows:-1}" "${cwd:-/tmp}"
+ exit 0
+ fi
+ done < "$STATE"
+ exit 1
+ ;;
has-session)
session=""
while [ "$#" -gt 0 ]; do
@@ -76,14 +114,11 @@ case "$cmd" in
done
tmp="$STATE.tmp"
: > "$tmp"
- while IFS=' ' read -r name attached pids rest; do
- [ -n "$name" ] || continue
- if [ "$name" != "$session" ]; then
- if [ -n "$rest" ]; then
- printf '%s %s %s %s\n' "$name" "$attached" "$pids" "$rest" >> "$tmp"
- else
- printf '%s %s %s\n' "$name" "$attached" "$pids" >> "$tmp"
- fi
+ while IFS= read -r line; do
+ [ -n "$line" ] || continue
+ first="${line%% *}"
+ if [ "$first" != "$session" ]; then
+ printf '%s\n' "$line" >> "$tmp"
fi
done < "$STATE"
mv "$tmp" "$STATE"
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()