libtmux-easy 0.2.0__py3-none-any.whl
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.
- libtmux_easy/__init__.py +36 -0
- libtmux_easy/_tmux.py +38 -0
- libtmux_easy/close.py +302 -0
- libtmux_easy/dash/__init__.py +46 -0
- libtmux_easy/dash/__main__.py +5 -0
- libtmux_easy/dash/app.py +519 -0
- libtmux_easy/dash/cli.py +114 -0
- libtmux_easy/dash/control.py +86 -0
- libtmux_easy/dash/homelog.py +39 -0
- libtmux_easy/dash/keys.py +153 -0
- libtmux_easy/dash/options.py +53 -0
- libtmux_easy/dash/viewport.py +145 -0
- libtmux_easy/dash/window.py +110 -0
- libtmux_easy/dash/winhook.py +62 -0
- libtmux_easy/launch.py +351 -0
- libtmux_easy/output.py +708 -0
- libtmux_easy/proc.py +135 -0
- libtmux_easy/starter.py +79 -0
- libtmux_easy/status.py +79 -0
- libtmux_easy/testing.py +442 -0
- libtmux_easy/tmux_lib.py +2514 -0
- libtmux_easy/watcher.py +277 -0
- libtmux_easy/wrap.py +513 -0
- libtmux_easy-0.2.0.dist-info/METADATA +270 -0
- libtmux_easy-0.2.0.dist-info/RECORD +30 -0
- libtmux_easy-0.2.0.dist-info/WHEEL +5 -0
- libtmux_easy-0.2.0.dist-info/entry_points.txt +7 -0
- libtmux_easy-0.2.0.dist-info/licenses/LICENSE +13 -0
- libtmux_easy-0.2.0.dist-info/licenses/NOTICE +10 -0
- libtmux_easy-0.2.0.dist-info/top_level.txt +1 -0
libtmux_easy/__init__.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# The names below are loaded on first use, not at import. The lte-run console
|
|
2
|
+
# script imports libtmux_easy.wrap, which runs this file first; importing
|
|
3
|
+
# tmux_lib here would drag libtmux and psutil into every wrapped pane (about
|
|
4
|
+
# 4x the startup time and +7 MB each) for a script that uses neither.
|
|
5
|
+
_EXPORTS = (
|
|
6
|
+
"TmuxSession",
|
|
7
|
+
"session_exists",
|
|
8
|
+
"set_server",
|
|
9
|
+
"tmux_command",
|
|
10
|
+
"DeprecatedError",
|
|
11
|
+
"libtmux_version",
|
|
12
|
+
"ExitStatus",
|
|
13
|
+
"PaneStatus",
|
|
14
|
+
"PANE_MODE_SHELL",
|
|
15
|
+
"PANE_MODE_EXEC",
|
|
16
|
+
"PANE_MODE_WRAP",
|
|
17
|
+
"Capture",
|
|
18
|
+
"OutputExpect",
|
|
19
|
+
"OutputMatch",
|
|
20
|
+
"OutputWatch",
|
|
21
|
+
"PaneExited",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
__all__ = list(_EXPORTS)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def __getattr__(name):
|
|
28
|
+
if name in _EXPORTS:
|
|
29
|
+
from . import tmux_lib
|
|
30
|
+
|
|
31
|
+
return getattr(tmux_lib, name)
|
|
32
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def __dir__():
|
|
36
|
+
return sorted(set(globals()) | set(_EXPORTS))
|
libtmux_easy/_tmux.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Running the tmux binary, for the stdlib-only modules (dash, keys, launch).
|
|
2
|
+
It talks to the server of the calling process ($TMUX), or the default one."""
|
|
3
|
+
|
|
4
|
+
import subprocess
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def tmux(*args, check=False):
|
|
8
|
+
"""Run 'tmux ARGS'; return its stdout lines. 'check' raises on failure."""
|
|
9
|
+
proc = subprocess.run(["tmux", *args], capture_output=True, text=True)
|
|
10
|
+
if check and proc.returncode:
|
|
11
|
+
raise RuntimeError(f"tmux {args[0]}: {proc.stderr.strip()}")
|
|
12
|
+
return proc.stdout.splitlines()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def option(target, name, scope="-q", run=tmux):
|
|
16
|
+
"""An option's value, or ''. 'scope': '-q' session, '-wq' window, '-gq' global."""
|
|
17
|
+
out = run("show-options", f"{scope}v", "-t", target, name) if target else \
|
|
18
|
+
run("show-options", f"{scope}v", name)
|
|
19
|
+
return out[0].strip() if out else ""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# Set by tmux for each pane, or describing the caller's own terminal or
|
|
23
|
+
# session: never copied into a pane's environment. LTE_DIR would point
|
|
24
|
+
# lte-run at another session's socket.
|
|
25
|
+
SKIP_ENV = frozenset(("TMUX", "TMUX_PANE", "TERM", "TERM_PROGRAM", "TERM_PROGRAM_VERSION",
|
|
26
|
+
"COLUMNS", "LINES", "LTE_DIR"))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def pane_env(env):
|
|
30
|
+
"""'env' (a mapping, e.g. os.environ) as it may go into a pane: SKIP_ENV
|
|
31
|
+
left out, values as strings."""
|
|
32
|
+
return {k: str(v) for k, v in (env or {}).items() if k not in SKIP_ENV}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def env_args(env):
|
|
36
|
+
"""'env' as tmux '-e NAME=VALUE' arguments (new-window, split-window,
|
|
37
|
+
respawn-pane)."""
|
|
38
|
+
return [arg for k, v in pane_env(env).items() for arg in ("-e", f"{k}={v}")]
|
libtmux_easy/close.py
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"""lte-close: close a tmux session gracefully, running its close actions.
|
|
2
|
+
|
|
3
|
+
lte-close [-t SESSION] [--timeouts INT,TERM,KILL]
|
|
4
|
+
lte-close add [-t SESSION] [--before] -- COMMAND [ARG...]
|
|
5
|
+
lte-close list [-t SESSION]
|
|
6
|
+
lte-close clear [-t SESSION]
|
|
7
|
+
|
|
8
|
+
Opens a 'close' window in the session and, there, with its progress shown:
|
|
9
|
+
|
|
10
|
+
1. runs the 'before' actions;
|
|
11
|
+
2. stops what runs in the panes: the session's script, if it is still
|
|
12
|
+
running, stops its own (and runs its Python on_close callbacks); every
|
|
13
|
+
other pane gets Ctrl-C, then SIGTERM, then SIGKILL;
|
|
14
|
+
3. runs the 'after' actions (the default for 'add');
|
|
15
|
+
4. kills the session.
|
|
16
|
+
|
|
17
|
+
Ctrl-C in the close window skips to killing the session. Works on any
|
|
18
|
+
session, with or without libtmux_easy. Sessions made by the library borrow
|
|
19
|
+
prefix Q for it (see KEY); for every session, bind it in ~/.tmux.conf:
|
|
20
|
+
|
|
21
|
+
bind-key Q run-shell -b "lte-close -t '#{session_id}'"
|
|
22
|
+
|
|
23
|
+
Stdlib only. See docs/session-close.md.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import argparse
|
|
27
|
+
import json
|
|
28
|
+
import os
|
|
29
|
+
import shlex
|
|
30
|
+
import signal
|
|
31
|
+
import subprocess
|
|
32
|
+
import sys
|
|
33
|
+
import time
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
|
|
36
|
+
from . import wrap
|
|
37
|
+
from .proc import alive, comm, pane_pids
|
|
38
|
+
from ._tmux import option, tmux as _tmux
|
|
39
|
+
from .dash.control import Control, describe, read_status
|
|
40
|
+
from .dash.options import (HOME_KIND_OPTION, HOME_OPTION, SIDEBAR_OPTION, STATUS_OPTION,
|
|
41
|
+
WINDOW_OPTION)
|
|
42
|
+
|
|
43
|
+
ACTIONS_OPTION = "@lte_on_close" # session option: JSON list of {"cmd", "cwd", "when"}
|
|
44
|
+
STATE_OPTION = "@lte_closing" # set by the script: "stopping", then "done"
|
|
45
|
+
WINDOW_OPTION_CLOSE = "@lte_close_window"
|
|
46
|
+
KEY_OPTION = "@lte_close_key" # session option: set where a borrowed close key applies
|
|
47
|
+
KEY = "Q" # borrowed by create() and lte-session, if unbound
|
|
48
|
+
KEY_ROLE = "close" # ~/.tmux.conf: set -g @lte_key_close KEY ("" for none)
|
|
49
|
+
WINDOW_NAME = "close"
|
|
50
|
+
WHEN = ("before", "after")
|
|
51
|
+
|
|
52
|
+
TIMEOUTS = (10.0, 5.0, 2.0) # SIGINT, SIGTERM, SIGKILL: as stop_pane()
|
|
53
|
+
ACK_TIMEOUT = 2.0 # for the script to take the 'close' command
|
|
54
|
+
SCRIPT_TIMEOUT = 120.0 # for it to finish
|
|
55
|
+
PAUSE = 1.5 # the summary stays up this long before the kill
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ---------- actions (session option) ----------
|
|
59
|
+
|
|
60
|
+
def actions(session, run=_tmux):
|
|
61
|
+
try:
|
|
62
|
+
return json.loads(option(session, ACTIONS_OPTION, run=run) or "[]")
|
|
63
|
+
except ValueError:
|
|
64
|
+
return []
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def add_action(session, command, when="after", cwd=None, run=_tmux):
|
|
68
|
+
"""Store shell 'command' to run on close, in 'cwd' (default: here)."""
|
|
69
|
+
if when not in WHEN:
|
|
70
|
+
raise ValueError(f"when must be one of {WHEN}, not {when!r}")
|
|
71
|
+
entries = actions(session, run) + [{"cmd": command, "cwd": cwd or os.getcwd(), "when": when}]
|
|
72
|
+
run("set-option", "-q", "-t", session, ACTIONS_OPTION, json.dumps(entries))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def clear_actions(session, run=_tmux):
|
|
76
|
+
run("set-option", "-uq", "-t", session, ACTIONS_OPTION)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def run_actions(entries, when, out=print):
|
|
80
|
+
"""Run the shell actions for one phase, in order; output goes to ours."""
|
|
81
|
+
for entry in (e for e in entries if e.get("when", "after") == when):
|
|
82
|
+
out(f"$ {entry['cmd']}")
|
|
83
|
+
try:
|
|
84
|
+
code = subprocess.run(entry["cmd"], shell=True, cwd=entry.get("cwd") or None).returncode
|
|
85
|
+
except OSError as exc:
|
|
86
|
+
out(f" failed: {exc}")
|
|
87
|
+
continue
|
|
88
|
+
if code:
|
|
89
|
+
out(f" exited {code}")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# ---------- the close sequence ----------
|
|
93
|
+
|
|
94
|
+
class Closer:
|
|
95
|
+
def __init__(self, session, timeouts=TIMEOUTS, out=print, run=_tmux):
|
|
96
|
+
self.session = session
|
|
97
|
+
self.timeouts = timeouts
|
|
98
|
+
self.out = out
|
|
99
|
+
self.run = run
|
|
100
|
+
self.name = (run("display-message", "-p", "-t", session, "#{session_name}") or [session])[0]
|
|
101
|
+
|
|
102
|
+
def opt(self, name, target=None):
|
|
103
|
+
return option(target or self.session, name, run=self.run)
|
|
104
|
+
|
|
105
|
+
def __call__(self):
|
|
106
|
+
out = self.out
|
|
107
|
+
out(f"Closing session {self.name}. Ctrl-C: skip to killing it now.\n")
|
|
108
|
+
entries = actions(self.session, self.run)
|
|
109
|
+
try:
|
|
110
|
+
run_actions(entries, "before", out)
|
|
111
|
+
self.stop_script()
|
|
112
|
+
self.stop_panes()
|
|
113
|
+
run_actions(entries, "after", out)
|
|
114
|
+
except KeyboardInterrupt:
|
|
115
|
+
out("\nSkipped the rest.")
|
|
116
|
+
out("\nKilling the session.")
|
|
117
|
+
time.sleep(PAUSE)
|
|
118
|
+
self.run("kill-session", "-t", self.session)
|
|
119
|
+
|
|
120
|
+
# ----- the script -----
|
|
121
|
+
|
|
122
|
+
def stop_script(self):
|
|
123
|
+
"""Hand the script its own panes: it knows how they end (wrap mode)
|
|
124
|
+
and runs its Python on_close callbacks."""
|
|
125
|
+
ctl_dir = self.opt(wrap.DIR_OPTION)
|
|
126
|
+
control = Control(os.path.join(ctl_dir, wrap.SOCKET_NAME) if ctl_dir else None)
|
|
127
|
+
if not control.online():
|
|
128
|
+
return
|
|
129
|
+
self.run("set-option", "-uq", "-t", self.session, STATE_OPTION)
|
|
130
|
+
control.send("close", None)
|
|
131
|
+
deadline = time.monotonic() + ACK_TIMEOUT
|
|
132
|
+
while not self.opt(STATE_OPTION) and time.monotonic() < deadline:
|
|
133
|
+
time.sleep(0.05)
|
|
134
|
+
if not self.opt(STATE_OPTION):
|
|
135
|
+
self.out("The session's script did not answer: stopping its panes directly.")
|
|
136
|
+
return
|
|
137
|
+
self.out("The session's script is stopping its panes:")
|
|
138
|
+
seen = {}
|
|
139
|
+
deadline = time.monotonic() + SCRIPT_TIMEOUT
|
|
140
|
+
while time.monotonic() < deadline:
|
|
141
|
+
done = self.opt(STATE_OPTION) == "done"
|
|
142
|
+
path = self.opt(STATUS_OPTION)
|
|
143
|
+
for entry in (read_status(path) if path else None) or ():
|
|
144
|
+
label = describe(entry)[0]
|
|
145
|
+
if seen.get(entry["pane_id"]) != label:
|
|
146
|
+
if entry["pane_id"] in seen or label != "running":
|
|
147
|
+
self.out(f" {entry['name']}: {label}")
|
|
148
|
+
seen[entry["pane_id"]] = label
|
|
149
|
+
if done:
|
|
150
|
+
return
|
|
151
|
+
time.sleep(0.2)
|
|
152
|
+
self.out(" ...gave up waiting for it.")
|
|
153
|
+
|
|
154
|
+
# ----- every other pane -----
|
|
155
|
+
|
|
156
|
+
def _panes(self):
|
|
157
|
+
"""pane id -> label, for the panes to stop: not this one, nor the
|
|
158
|
+
dashboard's sidebar or (unless it runs a command) its home pane."""
|
|
159
|
+
skip = {os.environ.get("TMUX_PANE")}
|
|
160
|
+
dash_window = self.opt(WINDOW_OPTION)
|
|
161
|
+
if dash_window:
|
|
162
|
+
skip.add(self.opt(SIDEBAR_OPTION))
|
|
163
|
+
if option(dash_window, HOME_KIND_OPTION, "-wq", run=self.run) != "command":
|
|
164
|
+
skip.add(option(dash_window, HOME_OPTION, "-wq", run=self.run))
|
|
165
|
+
panes = {}
|
|
166
|
+
for line in self.run("list-panes", "-s", "-t", self.session, "-F",
|
|
167
|
+
"#{pane_id} #{pane_pid} #{pane_dead} #{window_name}.#{pane_index}"):
|
|
168
|
+
fields = line.split(None, 3)
|
|
169
|
+
if len(fields) == 4 and fields[0] not in skip and fields[2] != "1":
|
|
170
|
+
panes[fields[0]] = (int(fields[1]), fields[3])
|
|
171
|
+
return panes
|
|
172
|
+
|
|
173
|
+
def stop_panes(self):
|
|
174
|
+
running = {}
|
|
175
|
+
for pane_id, (pid, label) in self._panes().items():
|
|
176
|
+
pids = pane_pids(pid)
|
|
177
|
+
if pids:
|
|
178
|
+
running[pane_id] = (f"{label} ({comm(pids[0]) or pids[0]})", pids)
|
|
179
|
+
if not running:
|
|
180
|
+
self.out("Nothing left running in the panes.")
|
|
181
|
+
return
|
|
182
|
+
self.out(f"Stopping {len(running)} pane(s):")
|
|
183
|
+
for step, timeout in zip(("SIGINT", "SIGTERM", "SIGKILL"), self.timeouts):
|
|
184
|
+
if not running:
|
|
185
|
+
break
|
|
186
|
+
for pane_id, (label, pids) in running.items():
|
|
187
|
+
if step == "SIGINT":
|
|
188
|
+
# A typed Ctrl-C: the foreground job, as the user would.
|
|
189
|
+
self.run("send-keys", "-t", pane_id, "C-c")
|
|
190
|
+
else:
|
|
191
|
+
for pid in pids:
|
|
192
|
+
try:
|
|
193
|
+
os.kill(pid, getattr(signal, step))
|
|
194
|
+
except (ProcessLookupError, PermissionError):
|
|
195
|
+
pass
|
|
196
|
+
self.out(f" {step} to {', '.join(l for l, _ in running.values())}")
|
|
197
|
+
deadline = time.monotonic() + timeout
|
|
198
|
+
while running and time.monotonic() < deadline:
|
|
199
|
+
for pane_id, (label, pids) in list(running.items()):
|
|
200
|
+
if not any(alive(p) for p in pids):
|
|
201
|
+
self.out(f" {label}: stopped by {step}")
|
|
202
|
+
del running[pane_id]
|
|
203
|
+
time.sleep(0.1)
|
|
204
|
+
for label, _ in running.values():
|
|
205
|
+
self.out(f" {label}: still running")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# ---------- the window ----------
|
|
209
|
+
|
|
210
|
+
def self_command():
|
|
211
|
+
"""Shell text that runs lte-close from this install, PATH or not."""
|
|
212
|
+
src = Path(__file__).resolve().parents[1]
|
|
213
|
+
return f"env PYTHONPATH={shlex.quote(str(src))} {shlex.quote(sys.executable)} -m libtmux_easy.close"
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def key_command():
|
|
217
|
+
"""tmux command text for a key binding: close the session it's pressed in."""
|
|
218
|
+
return f"run-shell -b \"{self_command()} -t '#{{session_id}}'\""
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _session_of_caller():
|
|
222
|
+
pane = os.environ.get("TMUX_PANE")
|
|
223
|
+
out = _tmux("display-message", "-p", "-t", pane, "#{session_id}") if pane else []
|
|
224
|
+
return out[0].strip() if out else None
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def open_close_window(session, timeouts=TIMEOUTS):
|
|
228
|
+
"""Run the close sequence in a new window of 'session' (or the one it
|
|
229
|
+
already has), and show it."""
|
|
230
|
+
window = option(session, WINDOW_OPTION_CLOSE)
|
|
231
|
+
if window and _tmux("list-panes", "-t", window, "-F", "#{pane_id}"):
|
|
232
|
+
_tmux("select-window", "-t", window)
|
|
233
|
+
return window
|
|
234
|
+
command = (f"{self_command()} --here -t {shlex.quote(session)} "
|
|
235
|
+
f"--timeouts {','.join(str(t) for t in timeouts)}")
|
|
236
|
+
out = _tmux("new-window", "-t", f"{session}:", "-n", WINDOW_NAME,
|
|
237
|
+
"-P", "-F", "#{window_id}", command)
|
|
238
|
+
if not out:
|
|
239
|
+
return None
|
|
240
|
+
window = out[0].strip()
|
|
241
|
+
_tmux("set-option", "-q", "-t", session, WINDOW_OPTION_CLOSE, window)
|
|
242
|
+
_tmux("select-window", "-t", window)
|
|
243
|
+
return window
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _timeouts(text):
|
|
247
|
+
values = tuple(float(v) for v in text.split(","))
|
|
248
|
+
if len(values) != 3:
|
|
249
|
+
raise argparse.ArgumentTypeError("three values: SIGINT,SIGTERM,SIGKILL seconds")
|
|
250
|
+
return values
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def main(argv=None):
|
|
254
|
+
parser = argparse.ArgumentParser(
|
|
255
|
+
prog="lte-close", description=__doc__.split("\n\n")[0],
|
|
256
|
+
formatter_class=argparse.RawDescriptionHelpFormatter, epilog=__doc__.split("\n\n", 2)[2])
|
|
257
|
+
parser.add_argument("-t", "--target", help="session (default: the one this runs in)")
|
|
258
|
+
parser.add_argument("--timeouts", type=_timeouts, default=TIMEOUTS, metavar="INT,TERM,KILL",
|
|
259
|
+
help="seconds to wait after each signal (default 10,5,2)")
|
|
260
|
+
parser.add_argument("--here", action="store_true", help=argparse.SUPPRESS)
|
|
261
|
+
parser.add_argument("action", nargs="?", default="close",
|
|
262
|
+
choices=("close", "add", "list", "clear"))
|
|
263
|
+
parser.add_argument("--before", action="store_true",
|
|
264
|
+
help="add: run before the panes are stopped (default: after)")
|
|
265
|
+
parser.add_argument("command", nargs="*", help="add: the command ('--' before one with options)")
|
|
266
|
+
argv = sys.argv[1:] if argv is None else list(argv)
|
|
267
|
+
# A command after '--' is split off by hand: argparse mishandles '--'
|
|
268
|
+
# next to an optional positional.
|
|
269
|
+
after = argv[argv.index("--") + 1:] if "--" in argv else None
|
|
270
|
+
args = parser.parse_args(argv[:argv.index("--")] if after is not None else argv)
|
|
271
|
+
if after is not None:
|
|
272
|
+
args.command = args.command + after
|
|
273
|
+
|
|
274
|
+
session = args.target or _session_of_caller()
|
|
275
|
+
if not session or not _tmux("display-message", "-p", "-t", session, "#{session_id}"):
|
|
276
|
+
sys.stderr.write("lte-close: no such session (run inside tmux, or pass -t)\n")
|
|
277
|
+
return 1
|
|
278
|
+
session = _tmux("display-message", "-p", "-t", session, "#{session_id}")[0].strip()
|
|
279
|
+
|
|
280
|
+
if args.action == "add":
|
|
281
|
+
command = args.command
|
|
282
|
+
if not command:
|
|
283
|
+
parser.error("add needs a command")
|
|
284
|
+
text = command[0] if len(command) == 1 else " ".join(shlex.quote(c) for c in command)
|
|
285
|
+
add_action(session, text, "before" if args.before else "after")
|
|
286
|
+
return 0
|
|
287
|
+
if args.action == "list":
|
|
288
|
+
for entry in actions(session):
|
|
289
|
+
print(f"{entry.get('when', 'after'):6} {entry['cmd']} (in {entry.get('cwd')})")
|
|
290
|
+
return 0
|
|
291
|
+
if args.action == "clear":
|
|
292
|
+
clear_actions(session)
|
|
293
|
+
return 0
|
|
294
|
+
if args.here:
|
|
295
|
+
signal.signal(signal.SIGHUP, signal.SIG_DFL)
|
|
296
|
+
Closer(session, args.timeouts)()
|
|
297
|
+
return 0
|
|
298
|
+
return 0 if open_close_window(session, args.timeouts) else 1
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
if __name__ == "__main__":
|
|
302
|
+
sys.exit(main())
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""lte-dash: a status sidebar for a libtmux_easy session.
|
|
2
|
+
|
|
3
|
+
lte-dash --status FILE [--ctl SOCKET]
|
|
4
|
+
|
|
5
|
+
Runs as the left pane of the session's dashboard window (see
|
|
6
|
+
TmuxSession.dashboard()). The right pane is a viewport: selecting a pane in
|
|
7
|
+
the sidebar swaps the real pane into it with 'tmux swap-pane', so it is fully
|
|
8
|
+
interactive. The "home" pane (a 'tail -F' of the script's output) is the
|
|
9
|
+
placeholder that fills the selected pane's slot in its own window while it is
|
|
10
|
+
on show, so every window keeps its layout.
|
|
11
|
+
|
|
12
|
+
Stdlib only, like wrap.py, except app.py (Textual): only cli.main() imports
|
|
13
|
+
it, so the library can use Viewport without Textual.
|
|
14
|
+
|
|
15
|
+
options.py tmux option names holding the dashboard's state
|
|
16
|
+
window.py open_window(), launcher()
|
|
17
|
+
viewport.py Viewport: swap panes in and out
|
|
18
|
+
control.py Control (actions to the script), read_status()
|
|
19
|
+
cli.py lte-dash
|
|
20
|
+
app.py the sidebar UI (Textual)
|
|
21
|
+
keys.py lte-key: borrow a prefix key
|
|
22
|
+
winhook.py a tmux hook that tells the sidebar its window changed
|
|
23
|
+
homelog.py the tee behind TmuxSession.tee_home()
|
|
24
|
+
|
|
25
|
+
A program that is not a TmuxSession can open a dashboard too: 'lte-dash
|
|
26
|
+
--open SESSION ...', or open_window(). It then writes the status file
|
|
27
|
+
itself and answers the control socket's commands.
|
|
28
|
+
|
|
29
|
+
lte-dash --open SESSION --status FILE --ctl SOCKET --home-log FILE
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from .control import Control, read_status
|
|
33
|
+
from .options import (AUTO, FROM_OPTION, HOME_CMD_OPTION, HOME_KIND_OPTION, HOME_LOG,
|
|
34
|
+
HOME_OPTION, HOME_STATUS_OPTION, INITIAL_OPTION, INSTALL_HINT, KEY,
|
|
35
|
+
KEY_COMMAND, MIN_WIDTH, RUN_DIR_OPTION, SIDEBAR_OPTION, STATUS_OPTION,
|
|
36
|
+
VIEW_OPTION, WIDTH_OPTION, WINDOW_OPTION)
|
|
37
|
+
from .viewport import Viewport
|
|
38
|
+
from .window import launcher, open_window
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"Control", "Viewport", "launcher", "open_window", "read_status",
|
|
42
|
+
"AUTO", "FROM_OPTION", "HOME_CMD_OPTION", "HOME_KIND_OPTION", "HOME_LOG", "HOME_OPTION",
|
|
43
|
+
"HOME_STATUS_OPTION", "INITIAL_OPTION", "INSTALL_HINT", "KEY", "KEY_COMMAND", "MIN_WIDTH",
|
|
44
|
+
"RUN_DIR_OPTION", "SIDEBAR_OPTION", "STATUS_OPTION", "VIEW_OPTION", "WIDTH_OPTION",
|
|
45
|
+
"WINDOW_OPTION",
|
|
46
|
+
]
|