tmux-deck 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ __pycache__/
2
+ *.pyc
3
+ dist/
4
+ *.egg-info/
5
+ .venv/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 bdepyzy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.4
2
+ Name: tmux-deck
3
+ Version: 0.1.0
4
+ Summary: A sleek interactive grid of your tmux sessions — project status at a glance, jump in with a keystroke.
5
+ Project-URL: Homepage, https://github.com/bdepyzy/tmux-deck
6
+ Project-URL: Issues, https://github.com/bdepyzy/tmux-deck/issues
7
+ Author-email: bdepyzy <obo.dan87@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: curses,dashboard,sessions,terminal,tmux,tui
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console :: Curses
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: POSIX
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Terminals
17
+ Classifier: Topic :: Utilities
18
+ Requires-Python: >=3.9
19
+ Description-Content-Type: text/markdown
20
+
21
+ # tmux-deck
22
+
23
+ A sleek interactive deck of your tmux sessions. Each session is a card that
24
+ tells you what the project is actually up to — no LLMs, just your environment:
25
+
26
+ ```
27
+ tmux deck · 4 sessions
28
+
29
+ ╭╴api-server ●╶─────────────────────╮ ╭╴blog╶─────────────────────────────╮
30
+ │ 5 win · 3m ago │ │ 2 win · 2h ago │
31
+ │ ⎇ feat/rate-limiting ±3 ↑1 │ │ ⎇ main │
32
+ │ Add sliding-window li… 20h ago │ │ publish: zero-downtime d… 3d ago│
33
+ │ ▸ server · vim │ │ ▸ blog · zsh │
34
+ ╰───────────────────────────────╴1╶─╯ ╰───────────────────────────────╴2╶─╯
35
+
36
+ ╭╴dotfiles╶─────────────────────────╮ ╭╴scratch╶──────────────────────────╮
37
+ │ 3 win · 1d ago │ │ 1 win · 40s ago │
38
+ │ ⎇ main ±12 │ │ ~/tmp │
39
+ │ waybar: battery module… 2w ago │ │ $ python bench.py --runs 50 │
40
+ │ ▸ hypr · zsh │ │ ▸ scratch · python │
41
+ ╰───────────────────────────────╴3╶─╯ ╰───────────────────────────────╴4╶─╯
42
+
43
+ ←↑↓→ move ↵ attach 1-9 jump x kill r refresh q quit
44
+ ```
45
+
46
+ Each card shows:
47
+
48
+ - **session name**, with a green `●` when attached
49
+ - **windows · last activity**, and the session group if any
50
+ - **git status** of the active pane's directory — branch, `±N` dirty files,
51
+ `↑N`/`↓N` unpushed/unpulled commits (yellow when dirty, green when clean)
52
+ - **last commit subject + age** — the project's latest update, straight from
53
+ `git log` (falls back to the last line of pane output outside a repo)
54
+ - **what's running** — active window name and current command
55
+
56
+ ## Install
57
+
58
+ ```sh
59
+ pipx install tmux-deck # or: uv tool install tmux-deck
60
+ ```
61
+
62
+ Requires Python ≥ 3.9 and tmux. No Python dependencies — it's a single
63
+ stdlib-curses file.
64
+
65
+ ## Use
66
+
67
+ ```sh
68
+ tmux-deck
69
+ ```
70
+
71
+ | key | action |
72
+ | --- | --- |
73
+ | `←↑↓→` / mouse | move between cards |
74
+ | `Enter` / click selected / double-click | attach (uses `switch-client` inside tmux) |
75
+ | `1`–`9` | jump straight into that session |
76
+ | `x` | kill selected session (confirms) |
77
+ | `r` | refresh |
78
+ | `q` / `Esc` | quit |
79
+
80
+ Handy tmux binding — the deck as a centered popup on `prefix + S`:
81
+
82
+ ```tmux
83
+ bind S display-popup -E -w 90% -h 80% tmux-deck
84
+ ```
85
+
86
+ ## License
87
+
88
+ MIT
@@ -0,0 +1,68 @@
1
+ # tmux-deck
2
+
3
+ A sleek interactive deck of your tmux sessions. Each session is a card that
4
+ tells you what the project is actually up to — no LLMs, just your environment:
5
+
6
+ ```
7
+ tmux deck · 4 sessions
8
+
9
+ ╭╴api-server ●╶─────────────────────╮ ╭╴blog╶─────────────────────────────╮
10
+ │ 5 win · 3m ago │ │ 2 win · 2h ago │
11
+ │ ⎇ feat/rate-limiting ±3 ↑1 │ │ ⎇ main │
12
+ │ Add sliding-window li… 20h ago │ │ publish: zero-downtime d… 3d ago│
13
+ │ ▸ server · vim │ │ ▸ blog · zsh │
14
+ ╰───────────────────────────────╴1╶─╯ ╰───────────────────────────────╴2╶─╯
15
+
16
+ ╭╴dotfiles╶─────────────────────────╮ ╭╴scratch╶──────────────────────────╮
17
+ │ 3 win · 1d ago │ │ 1 win · 40s ago │
18
+ │ ⎇ main ±12 │ │ ~/tmp │
19
+ │ waybar: battery module… 2w ago │ │ $ python bench.py --runs 50 │
20
+ │ ▸ hypr · zsh │ │ ▸ scratch · python │
21
+ ╰───────────────────────────────╴3╶─╯ ╰───────────────────────────────╴4╶─╯
22
+
23
+ ←↑↓→ move ↵ attach 1-9 jump x kill r refresh q quit
24
+ ```
25
+
26
+ Each card shows:
27
+
28
+ - **session name**, with a green `●` when attached
29
+ - **windows · last activity**, and the session group if any
30
+ - **git status** of the active pane's directory — branch, `±N` dirty files,
31
+ `↑N`/`↓N` unpushed/unpulled commits (yellow when dirty, green when clean)
32
+ - **last commit subject + age** — the project's latest update, straight from
33
+ `git log` (falls back to the last line of pane output outside a repo)
34
+ - **what's running** — active window name and current command
35
+
36
+ ## Install
37
+
38
+ ```sh
39
+ pipx install tmux-deck # or: uv tool install tmux-deck
40
+ ```
41
+
42
+ Requires Python ≥ 3.9 and tmux. No Python dependencies — it's a single
43
+ stdlib-curses file.
44
+
45
+ ## Use
46
+
47
+ ```sh
48
+ tmux-deck
49
+ ```
50
+
51
+ | key | action |
52
+ | --- | --- |
53
+ | `←↑↓→` / mouse | move between cards |
54
+ | `Enter` / click selected / double-click | attach (uses `switch-client` inside tmux) |
55
+ | `1`–`9` | jump straight into that session |
56
+ | `x` | kill selected session (confirms) |
57
+ | `r` | refresh |
58
+ | `q` / `Esc` | quit |
59
+
60
+ Handy tmux binding — the deck as a centered popup on `prefix + S`:
61
+
62
+ ```tmux
63
+ bind S display-popup -E -w 90% -h 80% tmux-deck
64
+ ```
65
+
66
+ ## License
67
+
68
+ MIT
@@ -0,0 +1,32 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "tmux-deck"
7
+ version = "0.1.0"
8
+ description = "A sleek interactive grid of your tmux sessions — project status at a glance, jump in with a keystroke."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.9"
12
+ authors = [{ name = "bdepyzy", email = "obo.dan87@gmail.com" }]
13
+ keywords = ["tmux", "tui", "curses", "sessions", "terminal", "dashboard"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Environment :: Console :: Curses",
17
+ "Intended Audience :: Developers",
18
+ "Operating System :: POSIX",
19
+ "Programming Language :: Python :: 3",
20
+ "Topic :: Terminals",
21
+ "Topic :: Utilities",
22
+ ]
23
+
24
+ [project.urls]
25
+ Homepage = "https://github.com/bdepyzy/tmux-deck"
26
+ Issues = "https://github.com/bdepyzy/tmux-deck/issues"
27
+
28
+ [project.scripts]
29
+ tmux-deck = "tmux_deck.app:main"
30
+
31
+ [tool.hatch.build.targets.wheel]
32
+ packages = ["src/tmux_deck"]
@@ -0,0 +1,3 @@
1
+ from tmux_deck.app import __version__, main
2
+
3
+ __all__ = ["__version__", "main"]
@@ -0,0 +1,3 @@
1
+ from tmux_deck.app import main
2
+
3
+ main()
@@ -0,0 +1,401 @@
1
+ #!/usr/bin/env python3
2
+ """tmux-deck — a sleek interactive grid of your tmux sessions.
3
+
4
+ Each session is a card showing what the project is up to: git branch,
5
+ dirty/unpushed state, last commit, and what's running right now.
6
+ Navigate with arrows or mouse, Enter to attach, 1-9 to jump.
7
+ """
8
+
9
+ __version__ = "0.1.0"
10
+
11
+ import math
12
+ import os
13
+ import subprocess
14
+ import sys
15
+ import time
16
+
17
+ os.environ.setdefault("ESCDELAY", "25") # snappy Esc
18
+ import curses
19
+
20
+ SEP = "\x1f"
21
+ CARD_H = 6 # title border + 4 content lines + bottom border
22
+ ROW_GAP = 1
23
+ COL_GAP = 2
24
+ MIN_CARD_W = 36
25
+ MAX_CARD_W = 50
26
+
27
+ HOME = os.path.expanduser("~")
28
+
29
+ A = {} # attribute palette, filled by init_colors()
30
+
31
+
32
+ # ── data ────────────────────────────────────────────────────────────────────
33
+
34
+ def sh(args):
35
+ return subprocess.run(args, capture_output=True, text=True)
36
+
37
+
38
+ def out(args):
39
+ return sh(args).stdout
40
+
41
+
42
+ def short_when(s):
43
+ """Compress git's '%cr' style dates: '2 hours ago' -> '2h ago'."""
44
+ for long, short in (
45
+ ("seconds", "s"), ("second", "s"),
46
+ ("minutes", "m"), ("minute", "m"),
47
+ ("hours", "h"), ("hour", "h"),
48
+ ("days", "d"), ("day", "d"),
49
+ ("weeks", "w"), ("week", "w"),
50
+ ("months", "mo"), ("month", "mo"),
51
+ ("years", "y"), ("year", "y"),
52
+ ):
53
+ s = s.replace(" " + long, short).replace(long, short)
54
+ return s
55
+
56
+
57
+ def git_info(path):
58
+ if not path or not os.path.isdir(path):
59
+ return None
60
+
61
+ def g(*a):
62
+ return sh(["git", "-C", path, *a])
63
+
64
+ r = g("rev-parse", "--abbrev-ref", "HEAD")
65
+ if r.returncode != 0:
66
+ return None
67
+ branch = r.stdout.strip()
68
+
69
+ dirty = len([l for l in g("status", "--porcelain").stdout.splitlines() if l.strip()])
70
+
71
+ log = g("log", "-1", "--format=%s" + SEP + "%cr").stdout.strip()
72
+ subject, when = (log.split(SEP) + ["", ""])[:2]
73
+
74
+ ab = ""
75
+ r2 = g("rev-list", "--left-right", "--count", "@{u}...HEAD")
76
+ if r2.returncode == 0 and r2.stdout.split():
77
+ behind, ahead = r2.stdout.split()
78
+ bits = []
79
+ if int(ahead):
80
+ bits.append("↑" + ahead)
81
+ if int(behind):
82
+ bits.append("↓" + behind)
83
+ ab = " ".join(bits)
84
+
85
+ return {
86
+ "branch": branch,
87
+ "dirty": dirty,
88
+ "subject": subject,
89
+ "when": short_when(when),
90
+ "ab": ab,
91
+ }
92
+
93
+
94
+ def tilde(path):
95
+ return "~" + path[len(HOME):] if path.startswith(HOME) else path
96
+
97
+
98
+ def ell(text, n):
99
+ if n <= 0:
100
+ return ""
101
+ return text if len(text) <= n else text[: max(0, n - 1)] + "…"
102
+
103
+
104
+ def get_sessions():
105
+ fmt = SEP.join(
106
+ [
107
+ "#{session_name}",
108
+ "#{session_windows}",
109
+ "#{session_attached}",
110
+ "#{session_activity}",
111
+ "#{session_group}",
112
+ ]
113
+ )
114
+ sessions = []
115
+ for line in out(["tmux", "list-sessions", "-F", fmt]).splitlines():
116
+ name, windows, attached, activity, group = line.split(SEP)
117
+ target = f"={name}:"
118
+ winfo = out(
119
+ [
120
+ "tmux",
121
+ "display-message",
122
+ "-p",
123
+ "-t",
124
+ target,
125
+ SEP.join(
126
+ ["#{window_name}", "#{pane_current_command}", "#{pane_current_path}"]
127
+ ),
128
+ ]
129
+ ).strip()
130
+ wname, cmd, path = (winfo.split(SEP) + ["", "", ""])[:3]
131
+
132
+ tail = ""
133
+ for l in reversed(out(["tmux", "capture-pane", "-p", "-t", target]).splitlines()):
134
+ if l.strip():
135
+ tail = " ".join(l.split())
136
+ break
137
+
138
+ sessions.append(
139
+ {
140
+ "name": name,
141
+ "windows": int(windows or 0),
142
+ "attached": int(attached or 0) > 0,
143
+ "activity": int(activity or 0),
144
+ "group": group,
145
+ "wname": wname,
146
+ "cmd": cmd,
147
+ "path": path,
148
+ "git": git_info(path),
149
+ "tail": tail,
150
+ }
151
+ )
152
+ return sessions
153
+
154
+
155
+ def rel_time(ts):
156
+ d = max(0, int(time.time()) - ts)
157
+ if d < 60:
158
+ return f"{d}s ago"
159
+ if d < 3600:
160
+ return f"{d // 60}m ago"
161
+ if d < 86400:
162
+ return f"{d // 3600}h ago"
163
+ return f"{d // 86400}d ago"
164
+
165
+
166
+ # ── drawing ─────────────────────────────────────────────────────────────────
167
+
168
+ def init_colors():
169
+ curses.use_default_colors()
170
+ ext = curses.has_colors() and curses.COLORS >= 256
171
+
172
+ def mk(pid, ext_fg, base_fg, extra=0):
173
+ curses.init_pair(pid, ext_fg if ext else base_fg, -1)
174
+ return curses.color_pair(pid) | extra
175
+
176
+ A["accent"] = mk(1, 45, curses.COLOR_CYAN) # selection
177
+ A["green"] = mk(2, 114, curses.COLOR_GREEN) # clean / attached
178
+ A["yellow"] = mk(3, 179, curses.COLOR_YELLOW) # dirty
179
+ A["text"] = mk(4, 252, -1) # primary text
180
+ A["grey"] = mk(5, 246, -1, 0 if ext else curses.A_DIM) # secondary
181
+ A["dim"] = mk(6, 241, -1, 0 if ext else curses.A_DIM) # tertiary
182
+ A["border"] = mk(7, 238, -1, 0 if ext else curses.A_DIM) # card frame
183
+
184
+
185
+ def draw_card(scr, s, idx, y, x, w, selected, scr_h):
186
+ border = (A["accent"] | curses.A_BOLD) if selected else A["border"]
187
+
188
+ def put(dy, col, text, attr=0):
189
+ yy = y + dy
190
+ if 0 <= yy < scr_h and 0 <= col < w:
191
+ try:
192
+ scr.addnstr(yy, x + col, text, w - col, attr)
193
+ except curses.error:
194
+ pass
195
+
196
+ # top border carries the title: ╭╴name ●╶──────╮
197
+ put(0, 0, "╭╴", border)
198
+ title = ell(s["name"], w - 12)
199
+ name_attr = curses.A_BOLD | (A["accent"] if selected else A["text"])
200
+ put(0, 2, title, name_attr)
201
+ col = 2 + len(title)
202
+ if s["attached"]:
203
+ put(0, col, " ●", A["green"])
204
+ col += 2
205
+ put(0, col, "╶" + "─" * max(0, w - 2 - col) + "╮", border)
206
+
207
+ # side walls
208
+ for dy in range(1, CARD_H - 1):
209
+ put(dy, 0, "│", border)
210
+ put(dy, w - 1, "│", border)
211
+
212
+ # bottom border carries the quick-jump digit: ╰────╴3╶─╯
213
+ if idx < 9:
214
+ put(CARD_H - 1, 0, "╰" + "─" * (w - 6) + "╴", border)
215
+ put(CARD_H - 1, w - 4, str(idx + 1), A["dim"])
216
+ put(CARD_H - 1, w - 3, "╶─╯", border)
217
+ else:
218
+ put(CARD_H - 1, 0, "╰" + "─" * (w - 2) + "╯", border)
219
+
220
+ left = 3
221
+ iw = w - left - 2
222
+
223
+ def puti(dy, text, attr=0, col=0):
224
+ if col < iw:
225
+ put(dy, left + col, ell(text, iw - col), attr)
226
+
227
+ # 1 — windows · activity · group
228
+ meta = f"{s['windows']} win · {rel_time(s['activity'])}"
229
+ if s["group"]:
230
+ meta += f" · ⧉ {s['group']}"
231
+ puti(1, meta, A["grey"])
232
+
233
+ # 2 — git branch state, or the working directory
234
+ g = s["git"]
235
+ if g:
236
+ suffix = ""
237
+ if g["dirty"]:
238
+ suffix += f" ±{g['dirty']}"
239
+ if g["ab"]:
240
+ suffix += f" {g['ab']}"
241
+ state = ell(f"⎇ {g['branch']}", iw - len(suffix)) + suffix
242
+ puti(2, state, A["yellow"] if g["dirty"] else A["green"])
243
+ else:
244
+ puti(2, tilde(s["path"]), A["grey"])
245
+
246
+ # 3 — last commit as the project's "latest update", age right-aligned
247
+ if g and g["subject"]:
248
+ when = g["when"]
249
+ wcol = max(0, iw - len(when))
250
+ puti(3, ell(g["subject"], wcol - 1), A["text"])
251
+ puti(3, when, A["dim"], wcol)
252
+ else:
253
+ puti(3, s["tail"], A["dim"])
254
+
255
+ # 4 — what's running right now
256
+ running = f"▸ {s['wname']}"
257
+ if s["cmd"] and s["cmd"] != s["wname"]:
258
+ running += f" · {s['cmd']}"
259
+ puti(4, running, A["grey"])
260
+
261
+
262
+ def draw_chrome(scr, n, w, h):
263
+ try:
264
+ scr.addnstr(0, 2, "tmux deck", min(w - 3, 10), curses.A_BOLD | A["text"])
265
+ count = f"· {n} session{'s' if n != 1 else ''}"
266
+ scr.addnstr(0, 12, count, max(0, w - 14), A["grey"])
267
+ except curses.error:
268
+ pass
269
+ hints = "←↑↓→ move ↵ attach 1-9 jump x kill r refresh q quit"
270
+ try:
271
+ scr.addnstr(h - 1, max(0, (w - len(hints)) // 2), hints, w - 1, A["dim"])
272
+ except curses.error:
273
+ pass
274
+
275
+
276
+ def confirm(scr, msg):
277
+ h, w = scr.getmaxyx()
278
+ try:
279
+ scr.addnstr(h - 1, 0, msg.center(w - 1), w - 1, A["yellow"] | curses.A_REVERSE)
280
+ except curses.error:
281
+ pass
282
+ scr.refresh()
283
+ return scr.getch() in (ord("y"), ord("Y"))
284
+
285
+
286
+ # ── main loop ───────────────────────────────────────────────────────────────
287
+
288
+ def run(scr):
289
+ curses.curs_set(0)
290
+ init_colors()
291
+ curses.mousemask(curses.ALL_MOUSE_EVENTS)
292
+ curses.mouseinterval(200)
293
+ scr.keypad(True)
294
+
295
+ sessions = get_sessions()
296
+ sel, off = 0, 0
297
+ stride_y = CARD_H + ROW_GAP
298
+
299
+ while True:
300
+ if not sessions:
301
+ return None
302
+ sel = max(0, min(sel, len(sessions) - 1))
303
+
304
+ h, w = scr.getmaxyx()
305
+ n = len(sessions)
306
+ ncols = max(1, min((w - 2 + COL_GAP) // (MIN_CARD_W + COL_GAP), n))
307
+ card_w = min(MAX_CARD_W, (w - 2 - (ncols - 1) * COL_GAP) // ncols)
308
+ grid_w = ncols * card_w + (ncols - 1) * COL_GAP
309
+ x0 = max(1, (w - grid_w) // 2)
310
+ y0 = 2
311
+ nrows = math.ceil(n / ncols)
312
+ view_h = h - 1 - y0
313
+
314
+ sel_top = (sel // ncols) * stride_y
315
+ if sel_top < off:
316
+ off = sel_top
317
+ if sel_top + CARD_H > off + view_h:
318
+ off = sel_top + CARD_H - view_h
319
+ off = max(0, min(off, max(0, nrows * stride_y - ROW_GAP - view_h)))
320
+
321
+ scr.erase()
322
+ draw_chrome(scr, n, w, h)
323
+
324
+ boxes = []
325
+ for i, s in enumerate(sessions):
326
+ r, c = divmod(i, ncols)
327
+ y = y0 + r * stride_y - off
328
+ x = x0 + c * (card_w + COL_GAP)
329
+ boxes.append((y, x, CARD_H, card_w))
330
+ if y + CARD_H > y0 - 1 and y < h - 1:
331
+ draw_card(scr, s, i, y, x, card_w, i == sel, h - 1)
332
+ scr.refresh()
333
+
334
+ ch = scr.getch()
335
+ if ch in (ord("q"), 27):
336
+ return None
337
+ elif ch in (curses.KEY_ENTER, 10, 13):
338
+ return sessions[sel]["name"]
339
+ elif ord("1") <= ch <= ord("9"):
340
+ i = ch - ord("1")
341
+ if i < n:
342
+ return sessions[i]["name"]
343
+ elif ch == curses.KEY_LEFT:
344
+ sel = max(0, sel - 1)
345
+ elif ch == curses.KEY_RIGHT:
346
+ sel = min(n - 1, sel + 1)
347
+ elif ch == curses.KEY_UP:
348
+ if sel - ncols >= 0:
349
+ sel -= ncols
350
+ elif ch == curses.KEY_DOWN:
351
+ if sel + ncols < n:
352
+ sel += ncols
353
+ elif ch == ord("r"):
354
+ sessions = get_sessions()
355
+ elif ch == ord("x"):
356
+ name = sessions[sel]["name"]
357
+ if confirm(scr, f"kill session '{name}'? y/N"):
358
+ sh(["tmux", "kill-session", "-t", f"={name}"])
359
+ sessions = get_sessions()
360
+ elif ch == curses.KEY_MOUSE:
361
+ try:
362
+ _, mx, my, _, bstate = curses.getmouse()
363
+ except curses.error:
364
+ continue
365
+ hit = None
366
+ for i, (by, bx, bh, bw) in enumerate(boxes):
367
+ if by <= my < by + bh and bx <= mx < bx + bw:
368
+ hit = i
369
+ break
370
+ if hit is None:
371
+ continue
372
+ if bstate & curses.BUTTON1_DOUBLE_CLICKED:
373
+ return sessions[hit]["name"]
374
+ if bstate & (curses.BUTTON1_CLICKED | curses.BUTTON1_PRESSED):
375
+ if hit == sel:
376
+ return sessions[hit]["name"]
377
+ sel = hit
378
+
379
+
380
+ def main():
381
+ if len(sys.argv) > 1 and sys.argv[1] in ("--version", "-V"):
382
+ print(f"tmux-deck {__version__}")
383
+ return
384
+ if len(sys.argv) > 1 and sys.argv[1] in ("--help", "-h"):
385
+ print(__doc__.strip())
386
+ print("\nusage: tmux-deck [--version]")
387
+ return
388
+ if not out(["tmux", "list-sessions", "-F", "#{session_name}"]).strip():
389
+ print("No tmux sessions are running.", file=sys.stderr)
390
+ sys.exit(1)
391
+ name = curses.wrapper(run)
392
+ if not name:
393
+ sys.exit(0)
394
+ if os.environ.get("TMUX"):
395
+ os.execvp("tmux", ["tmux", "switch-client", "-t", f"={name}"])
396
+ else:
397
+ os.execvp("tmux", ["tmux", "attach-session", "-t", f"={name}"])
398
+
399
+
400
+ if __name__ == "__main__":
401
+ main()