sessionview 0.2.0__tar.gz → 0.3.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.
- {sessionview-0.2.0 → sessionview-0.3.0}/PKG-INFO +10 -5
- {sessionview-0.2.0 → sessionview-0.3.0}/README.md +9 -4
- {sessionview-0.2.0 → sessionview-0.3.0}/bin/sessionview +32 -4
- {sessionview-0.2.0 → sessionview-0.3.0}/bin/sessionview-frame +17 -4
- {sessionview-0.2.0 → sessionview-0.3.0}/bin/sessionview-session +12 -6
- {sessionview-0.2.0 → sessionview-0.3.0}/bin/sessionview-sidebar-toggle +20 -7
- {sessionview-0.2.0 → sessionview-0.3.0}/bin/tmux-sidebar +103 -21
- {sessionview-0.2.0 → sessionview-0.3.0}/config/README.md +4 -0
- {sessionview-0.2.0 → sessionview-0.3.0}/config/config.json +2 -0
- sessionview-0.3.0/lib/client.sh +57 -0
- {sessionview-0.2.0 → sessionview-0.3.0}/pyproject.toml +1 -1
- {sessionview-0.2.0 → sessionview-0.3.0}/src/sessionview/__init__.py +1 -1
- {sessionview-0.2.0 → sessionview-0.3.0}/.gitignore +0 -0
- {sessionview-0.2.0 → sessionview-0.3.0}/LICENSE +0 -0
- {sessionview-0.2.0 → sessionview-0.3.0}/bin/sessionview-app-install +0 -0
- {sessionview-0.2.0 → sessionview-0.3.0}/bin/sessionview-autostart +0 -0
- {sessionview-0.2.0 → sessionview-0.3.0}/bin/sessionview-config +0 -0
- {sessionview-0.2.0 → sessionview-0.3.0}/bin/sessionview-from +0 -0
- {sessionview-0.2.0 → sessionview-0.3.0}/bin/sessionview-install +0 -0
- {sessionview-0.2.0 → sessionview-0.3.0}/bin/sessionview-messages +0 -0
- {sessionview-0.2.0 → sessionview-0.3.0}/bin/sessionview-newtab +0 -0
- {sessionview-0.2.0 → sessionview-0.3.0}/bin/sessionview-send +0 -0
- {sessionview-0.2.0 → sessionview-0.3.0}/bin/sessionview-terminal +0 -0
- {sessionview-0.2.0 → sessionview-0.3.0}/bin/sessionview-usermail +0 -0
- {sessionview-0.2.0 → sessionview-0.3.0}/bin/tmux-send-agent +0 -0
- {sessionview-0.2.0 → sessionview-0.3.0}/config/keys.md +0 -0
- {sessionview-0.2.0 → sessionview-0.3.0}/config/neomuttrc +0 -0
- {sessionview-0.2.0 → sessionview-0.3.0}/lib/python.sh +0 -0
- {sessionview-0.2.0 → sessionview-0.3.0}/lib/sessionview-config-screen +0 -0
- {sessionview-0.2.0 → sessionview-0.3.0}/lib/state.sh +0 -0
- {sessionview-0.2.0 → sessionview-0.3.0}/macos/SessionView.applescript +0 -0
- {sessionview-0.2.0 → sessionview-0.3.0}/macos/dreamcatcher.icns +0 -0
- {sessionview-0.2.0 → sessionview-0.3.0}/plugins/session/plugin.toml +0 -0
- {sessionview-0.2.0 → sessionview-0.3.0}/plugins/session/start-agent +0 -0
- {sessionview-0.2.0 → sessionview-0.3.0}/plugins/taskmaster/plugin.toml +0 -0
- {sessionview-0.2.0 → sessionview-0.3.0}/plugins/taskmaster/tasks-tab +0 -0
- {sessionview-0.2.0 → sessionview-0.3.0}/plugins/voicemode/plugin.toml +0 -0
- {sessionview-0.2.0 → sessionview-0.3.0}/plugins/voicemode/speak +0 -0
- {sessionview-0.2.0 → sessionview-0.3.0}/skills/tmux/SKILL.md +0 -0
- {sessionview-0.2.0 → sessionview-0.3.0}/skills/tmux/scripts/tmux-peek +0 -0
- {sessionview-0.2.0 → sessionview-0.3.0}/skills/tmux/scripts/tmux-show +0 -0
- {sessionview-0.2.0 → sessionview-0.3.0}/skills/tmux/scripts/tmux-type +0 -0
- {sessionview-0.2.0 → sessionview-0.3.0}/skills/tmux/scripts/tmux-where +0 -0
- {sessionview-0.2.0 → sessionview-0.3.0}/tmux/agent-menu.conf +0 -0
- {sessionview-0.2.0 → sessionview-0.3.0}/tmux/frame.conf +0 -0
- {sessionview-0.2.0 → sessionview-0.3.0}/tmux/tabs-top.conf +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sessionview
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: A sidebar for tmux: your projects (sessions) and your agents, here and on remote hosts.
|
|
5
5
|
Project-URL: Homepage, https://github.com/sessionkit/sessionview
|
|
6
6
|
Project-URL: Repository, https://github.com/sessionkit/sessionview
|
|
@@ -58,7 +58,7 @@ neomutt and notmuch's index on your machine.
|
|
|
58
58
|
sidebar's mail, the agent pane menu, and the session, taskmaster and
|
|
59
59
|
voicemode plugins (each lists `message` in its `requires`). Mail is
|
|
60
60
|
sent as you: `$USER`, unless `sessionview-config set mail_from NAME`
|
|
61
|
-
(or `$SESSIONVIEW_FROM`) names your mail name; `sessionview
|
|
61
|
+
(or `$SESSIONVIEW_FROM`) names your mail name; `sessionview from`
|
|
62
62
|
prints the name in effect.
|
|
63
63
|
- **neomutt** for the `messages` tab (`bin/sessionview-messages` runs
|
|
64
64
|
`neomutt -R`; the tab exits 2 without it): `brew install neomutt`, built
|
|
@@ -287,9 +287,14 @@ and every key in
|
|
|
287
287
|
error with its traceback). Follow it with
|
|
288
288
|
`tail -f ~/.local/state/sessionview/sessionview.log` when a click
|
|
289
289
|
seems to do nothing.
|
|
290
|
-
-
|
|
291
|
-
|
|
292
|
-
|
|
290
|
+
- **Levels:** `log_level` in your settings (or `$SESSIONVIEW_LOG_LEVEL`
|
|
291
|
+
for one run). `error`, the default, logs only what went wrong.
|
|
292
|
+
`timing` adds a line per click or key with its milliseconds, and each
|
|
293
|
+
command that took 50 ms or more, so a slow click names its slow part.
|
|
294
|
+
`debug` adds every command and every mouse event (what the old
|
|
295
|
+
`sidebar-mouse.log` held).
|
|
296
|
+
- It sits under `$SESSIONVIEW_STATE` when that is set. It is not rotated:
|
|
297
|
+
turn `timing` or `debug` on while you look, then back to `error`.
|
|
293
298
|
- **Health:** `sessionview install --check`: exit 0 and `ready` when
|
|
294
299
|
every requirement is present, exit 1 with each MISSING one named
|
|
295
300
|
otherwise. It checks the machine, not a running sidebar.
|
|
@@ -41,7 +41,7 @@ neomutt and notmuch's index on your machine.
|
|
|
41
41
|
sidebar's mail, the agent pane menu, and the session, taskmaster and
|
|
42
42
|
voicemode plugins (each lists `message` in its `requires`). Mail is
|
|
43
43
|
sent as you: `$USER`, unless `sessionview-config set mail_from NAME`
|
|
44
|
-
(or `$SESSIONVIEW_FROM`) names your mail name; `sessionview
|
|
44
|
+
(or `$SESSIONVIEW_FROM`) names your mail name; `sessionview from`
|
|
45
45
|
prints the name in effect.
|
|
46
46
|
- **neomutt** for the `messages` tab (`bin/sessionview-messages` runs
|
|
47
47
|
`neomutt -R`; the tab exits 2 without it): `brew install neomutt`, built
|
|
@@ -270,9 +270,14 @@ and every key in
|
|
|
270
270
|
error with its traceback). Follow it with
|
|
271
271
|
`tail -f ~/.local/state/sessionview/sessionview.log` when a click
|
|
272
272
|
seems to do nothing.
|
|
273
|
-
-
|
|
274
|
-
|
|
275
|
-
|
|
273
|
+
- **Levels:** `log_level` in your settings (or `$SESSIONVIEW_LOG_LEVEL`
|
|
274
|
+
for one run). `error`, the default, logs only what went wrong.
|
|
275
|
+
`timing` adds a line per click or key with its milliseconds, and each
|
|
276
|
+
command that took 50 ms or more, so a slow click names its slow part.
|
|
277
|
+
`debug` adds every command and every mouse event (what the old
|
|
278
|
+
`sidebar-mouse.log` held).
|
|
279
|
+
- It sits under `$SESSIONVIEW_STATE` when that is set. It is not rotated:
|
|
280
|
+
turn `timing` or `debug` on while you look, then back to `error`.
|
|
276
281
|
- **Health:** `sessionview install --check`: exit 0 and `ready` when
|
|
277
282
|
every requirement is present, exit 1 with each MISSING one named
|
|
278
283
|
otherwise. It checks the machine, not a running sidebar.
|
|
@@ -5,13 +5,15 @@
|
|
|
5
5
|
# sessionview COMMAND [ARGS]
|
|
6
6
|
#
|
|
7
7
|
# sessionview [SESSION] the frame: sidebar on the left, SESSION attached on
|
|
8
|
-
# the right (default main
|
|
9
|
-
# --width, columns.
|
|
8
|
+
# the right (default main; a named SESSION must exist).
|
|
9
|
+
# --sock names its tmux server; --width, columns.
|
|
10
10
|
# sessionview config settings: no verb opens the screen (get/set/list ...)
|
|
11
11
|
# sessionview session NAME open a project session with its standard tabs
|
|
12
12
|
# sessionview messages PROJECT a project's mail, read-only, in NeoMutt
|
|
13
13
|
# sessionview install check requirements (--app: the macOS Dock app)
|
|
14
14
|
# sessionview autostart --status which agent starts when sessionview opens
|
|
15
|
+
# sessionview from the name your mail is sent as (mail_from, else $USER)
|
|
16
|
+
# sessionview sidebar-toggle hide the sidebar, or show it again
|
|
15
17
|
# sessionview sidebar the sidebar under its supervisor (the frame's left
|
|
16
18
|
# pane runs this). In it: q reloads, Q quits.
|
|
17
19
|
#
|
|
@@ -25,9 +27,11 @@
|
|
|
25
27
|
# without the comment marks, so the help reads cleanly.
|
|
26
28
|
SESSIONVIEW_ROOT="${SESSIONVIEW_ROOT:-$(cd "$(dirname "$(readlink -f "$0" 2>/dev/null || echo "$0")")/.." && pwd)}"
|
|
27
29
|
SESSIONVIEW_STATE="${SESSIONVIEW_STATE:-$HOME/.local/state/sessionview}"; . "$SESSIONVIEW_ROOT/lib/state.sh"; . "$SESSIONVIEW_ROOT/lib/python.sh"; export SESSIONVIEW_ROOT SESSIONVIEW_STATE
|
|
30
|
+
# The header above is the usage: -h prints it, and so does an unknown word.
|
|
31
|
+
usage() { sed -n '2,22p' "$0" | sed 's/^# \{0,1\}//'; }
|
|
28
32
|
case "${1:-}" in
|
|
29
|
-
-h|--help|help)
|
|
30
|
-
config|session|messages|install|autostart|terminal|usermail)
|
|
33
|
+
-h|--help|help) usage; exit 0;;
|
|
34
|
+
config|session|messages|install|autostart|terminal|usermail|from|sidebar-toggle)
|
|
31
35
|
sub="$1"; shift; exec "$SESSIONVIEW_ROOT/bin/sessionview-$sub" "$@";;
|
|
32
36
|
sidebar)
|
|
33
37
|
# The supervisor: q = exit 0 = reload (picks up an edited tmux-sidebar or
|
|
@@ -44,4 +48,28 @@ case "${1:-}" in
|
|
|
44
48
|
done
|
|
45
49
|
exit 0;;
|
|
46
50
|
esac
|
|
51
|
+
# Anything else is the frame. A word that names no subcommand opens a frame
|
|
52
|
+
# ONLY on a session that already exists on the server the frame will use
|
|
53
|
+
# (--sock, else this tmux's own server when run inside one, else default):
|
|
54
|
+
# a typo or a probe word must not mean "open a frame on a new session named
|
|
55
|
+
# after it". Read-only check; on a miss, usage and exit 2, nothing touched.
|
|
56
|
+
# A new named session is `sessionview session NAME`. No word = main, as before.
|
|
57
|
+
word=""; sock=""; prev=""
|
|
58
|
+
for a in "$@"; do
|
|
59
|
+
case "$prev" in --sock) sock="$a"; prev=""; continue;; --width) prev=""; continue;; esac
|
|
60
|
+
case "$a" in --sock|--width) prev="$a";; -*) ;; *) word="$a";; esac
|
|
61
|
+
done
|
|
62
|
+
if [ -n "$word" ]; then
|
|
63
|
+
extra=""
|
|
64
|
+
if [ -z "$sock" ]; then
|
|
65
|
+
if [ -n "${TMUX:-}" ]; then sock="$(printf %s "${TMUX%%,*}")"; sock="${sock##*/}"; extra=1; else sock=default; fi
|
|
66
|
+
fi
|
|
67
|
+
if ! tmux -L "$sock" has-session -t "=$word" 2>/dev/null; then
|
|
68
|
+
echo "sessionview: no command or session named '$word' (on tmux server $sock); a new session is: sessionview session $word" >&2
|
|
69
|
+
usage >&2
|
|
70
|
+
exit 2
|
|
71
|
+
fi
|
|
72
|
+
# the frame looks where this checked: inside tmux, the caller's own server
|
|
73
|
+
[ -n "$extra" ] && set -- "$@" --sock "$sock"
|
|
74
|
+
fi
|
|
47
75
|
exec "$SESSIONVIEW_ROOT/bin/sessionview-frame" "$@"
|
|
@@ -31,6 +31,13 @@ while [ $# -gt 0 ]; do case "$1" in
|
|
|
31
31
|
-*) echo "sessionview-frame: unknown flag $1" >&2; exit 2;;
|
|
32
32
|
*) SESSION="$1"; GIVEN=1;;
|
|
33
33
|
esac; shift; done
|
|
34
|
+
# The frame takes over the terminal it runs in, so it needs one: a script or
|
|
35
|
+
# an agent's shell (stdin/stdout not a tty) is refused and nothing is touched.
|
|
36
|
+
# Every refusal is one line on stderr, exit 2.
|
|
37
|
+
refuse() { echo "sessionview: refused: $1" >&2; exit 2; }
|
|
38
|
+
if [ ! -t 0 ] || [ ! -t 1 ]; then
|
|
39
|
+
refuse "not a terminal: run it from a terminal (stdin and stdout must be a tty)"
|
|
40
|
+
fi
|
|
34
41
|
# Already inside a tmux client? Attaching a frame here nests a client in a
|
|
35
42
|
# pane: a terminal that auto-attaches tmux at login would run sessionview
|
|
36
43
|
# inside itself, a loop of frames in frames. Better to deal with it, for
|
|
@@ -42,15 +49,21 @@ esac; shift; done
|
|
|
42
49
|
# "run shell-command to replace the client"). The session the user was
|
|
43
50
|
# in stays alive and becomes the frame's right pane unless SESSION was
|
|
44
51
|
# given; its server becomes --sock. (-E was measured to do this.)
|
|
52
|
+
# "This client" is the CALLER's own, and only that: lib/client.sh proves it
|
|
53
|
+
# (this tty is $TMUX_PANE's, and exactly one client shows its session) or
|
|
54
|
+
# refuses. An untargeted `display -p '#{client_tty}'` once answered with
|
|
55
|
+
# another terminal's client on the same server, and the detach replaced it.
|
|
45
56
|
if [ -n "${TMUX:-}" ]; then
|
|
46
|
-
|
|
57
|
+
. "$SESSIONVIEW_ROOT/lib/client.sh"
|
|
58
|
+
sv_own_client || refuse "$SV_REFUSAL"
|
|
59
|
+
ctty="$SV_CLIENT"
|
|
47
60
|
if tmux -L "$FRAME_SOCK" list-panes -a -F '#{pane_tty}' 2>/dev/null | grep -qx "$ctty"; then
|
|
48
61
|
echo "sessionview: already inside a sessionview frame (client $ctty) - the sidebar is on the left; a frame in a frame is the loop" >&2
|
|
49
62
|
exit 1
|
|
50
63
|
fi
|
|
51
|
-
|
|
64
|
+
here_path="${TMUX%%,*}"; here_sock="${here_path##*/}"
|
|
52
65
|
if [ -z "$GIVEN" ]; then
|
|
53
|
-
SESSION="$
|
|
66
|
+
SESSION="$SV_SESSION"; SOCK="$here_sock"
|
|
54
67
|
fi
|
|
55
68
|
q() { printf "'%s'" "$(printf %s "$1" | sed "s/'/'\\\\''/g")"; }
|
|
56
69
|
# -E runs the command with the CLIENT's environment, not this shell's
|
|
@@ -60,7 +73,7 @@ if [ -n "${TMUX:-}" ]; then
|
|
|
60
73
|
cmd="$cmd SESSIONVIEW_FRAME_SOCK=$(q "$FRAME_SOCK") TMUX="
|
|
61
74
|
cmd="$cmd exec $(q "$0") $(q "$SESSION") --sock $(q "$SOCK")"
|
|
62
75
|
[ -n "$WIDTH" ] && cmd="$cmd --width $(q "$WIDTH")"
|
|
63
|
-
exec tmux detach-client -E "$cmd"
|
|
76
|
+
exec tmux -S "$here_path" detach-client -t "$ctty" -E "$cmd"
|
|
64
77
|
fi
|
|
65
78
|
if [ -z "$WIDTH" ]; then
|
|
66
79
|
WIDTH="$("$SESSIONVIEW_ROOT/bin/sessionview-config" get width 2>/dev/null || true)"
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
# shell the root - the fallback when nothing else applies; pin it
|
|
39
39
|
# (auto: true) and every session gets one
|
|
40
40
|
# {editor} is VISUAL, else EDITOR, else nvim; when it is nvim it LISTENS on the
|
|
41
|
-
# socket show-me looks for (<socket dir>/nvim-show
|
|
41
|
+
# socket show-me looks for (<socket dir>/nvim-show-<server key>-pane-<pane number>).
|
|
42
42
|
# Every window is a SHELL with the tool typed into it, so quitting nvim or
|
|
43
43
|
# lazygit leaves a prompt in the right directory. An entry's `when` is a NAME
|
|
44
44
|
# this script tests (always | has_repo | has_projdir | has_mail | fallback) and
|
|
@@ -207,6 +207,10 @@ show_socket_dir() { # show-me's get_socket_dir(), verbatim in spirit
|
|
|
207
207
|
elif [ -n "${XDG_RUNTIME_DIR:-}" ] && [ -d "$XDG_RUNTIME_DIR" ]; then echo "${XDG_RUNTIME_DIR%/}"
|
|
208
208
|
else mkdir -p "$HOME/.local/run" && chmod 700 "$HOME/.local/run"; echo "$HOME/.local/run"; fi
|
|
209
209
|
}
|
|
210
|
+
show_server_key() { # show-me's show_server_key(): the cksum of a tmux server's socket path.
|
|
211
|
+
# Pane ids count per SERVER, so a socket named by pane id alone collides across servers.
|
|
212
|
+
printf '%s' "$1" | cksum | awk '{print $1}'
|
|
213
|
+
}
|
|
210
214
|
say "session=$NAME socket=$SOCK store=$STORE root=$ROOT repo=${REPO:-NONE} projdir=${PROJDIR:-NONE} store_id=${STOREID:-$([ "$STOREMISS" = 1 ] && echo UNREGISTERED || echo NONE)} profile=$([ "$PLAIN" = 1 ] && echo off || echo on)$([ -n "$STOREVIEW" ] && echo " store_view=$STOREVIEW store_project=${STOREPROJ:-NONE} fresh=$FRESH")"
|
|
211
215
|
|
|
212
216
|
# -- cascading config: built-in > host > store > project, all JSON (JSON
|
|
@@ -288,19 +292,21 @@ dir_of() { # dir_of CWD -> the path, or "" when that dir is unknown for this pr
|
|
|
288
292
|
esac
|
|
289
293
|
}
|
|
290
294
|
editor_for() { # editor_for WINDOW -> what {editor} becomes there. nvim LISTENS on the socket
|
|
291
|
-
# show-me looks for (<socket dir>/nvim-show
|
|
292
|
-
# with neovim has the socket running, so show-me can reach it;
|
|
295
|
+
# show-me looks for (<socket dir>/nvim-show-<server key>-pane-<pane number>): a code
|
|
296
|
+
# window opened with neovim has the socket running, so show-me can reach it;
|
|
293
297
|
# any other editor is typed as it is. No window yet (--list-windows): the bare name.
|
|
294
|
-
local pid
|
|
298
|
+
local pid key pane
|
|
295
299
|
case "$(basename "${ED%% *}")" in
|
|
296
300
|
nvim) [ -n "$1" ] || { printf '%s' "$ED"; return; }
|
|
297
|
-
if [ "$PRINT" = 1 ]; then pid='<pane>';
|
|
301
|
+
if [ "$PRINT" = 1 ]; then pid='<pane>'; key='<server>'
|
|
302
|
+
else pane="$(tmux display-message -p -t "=$NAME:$1" '#{pane_id} #{socket_path}')"
|
|
303
|
+
pid="${pane%% *}"; key="$(show_server_key "${pane#* }")"; fi
|
|
298
304
|
# and ROOTS at the window's cwd: LazyVim's <leader>e opens neo-tree at LazyVim.root(),
|
|
299
305
|
# which walks up to .git - in the project window that is the STORE, not the project
|
|
300
306
|
# (the project directory should be the root). -c, not --cmd: LazyVim's own
|
|
301
307
|
# options.lua overwrites vim.g.root_spec at load.
|
|
302
308
|
# Only nvims this script opens get it; the user's config is untouched.
|
|
303
|
-
printf '%s --listen %s -c %s' "$ED" "$(printf %q "$(show_socket_dir)/nvim-show-pane-${pid#%}")" "$(printf %q 'lua vim.g.root_spec={"cwd"}')";;
|
|
309
|
+
printf '%s --listen %s -c %s' "$ED" "$(printf %q "$(show_socket_dir)/nvim-show-$key-pane-${pid#%}")" "$(printf %q 'lua vim.g.root_spec={"cwd"}')";;
|
|
304
310
|
*) printf '%s' "$ED";;
|
|
305
311
|
esac
|
|
306
312
|
}
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
2
|
# sessionview-sidebar-toggle - hide the sidebar, or show it again.
|
|
3
3
|
#
|
|
4
|
-
# sessionview
|
|
5
|
-
# tab-bar glyph, a key, a shell):
|
|
4
|
+
# sessionview sidebar-toggle from a shell INSIDE the inner tmux:
|
|
6
5
|
# finds the frame by the client's tty
|
|
7
|
-
# sessionview
|
|
8
|
-
# sessionview
|
|
9
|
-
# sessionview
|
|
6
|
+
# sessionview sidebar-toggle --hide | --show one direction only (default: toggle)
|
|
7
|
+
# sessionview sidebar-toggle --frame SOCK:SESSION name the frame (tests, the sidebar)
|
|
8
|
+
# sessionview sidebar-toggle --tty /dev/ttysNNN the clicking client (the tab-bar glyph)
|
|
9
|
+
#
|
|
10
|
+
# The tab-bar glyph and the tmux bindings run this file by path
|
|
11
|
+
# ($SESSIONVIEW_ROOT/bin/sessionview-sidebar-toggle); from a shell, the
|
|
12
|
+
# `sessionview` front door runs it, so it works from a package install too.
|
|
13
|
+
# -h prints the usage above.
|
|
10
14
|
#
|
|
11
15
|
# On a small laptop screen the sidebar needs a way to hide and show again, a
|
|
12
16
|
# little glyph. That is table stakes, not icing: a sidebar that cannot get out
|
|
@@ -32,14 +36,23 @@ while [ $# -gt 0 ]; do
|
|
|
32
36
|
--tty) TTY="$2"; shift 2;; # the clicking client's tty, from the tab-bar binding
|
|
33
37
|
--hide) MODE="hide"; shift;;
|
|
34
38
|
--show) MODE="show"; shift;;
|
|
39
|
+
-h|--help) sed -n '2,8p' "$0" | sed 's/^# \{0,1\}//'; exit 0;;
|
|
35
40
|
*) echo "sessionview-sidebar-toggle: unknown argument '$1'" >&2; exit 2;;
|
|
36
41
|
esac
|
|
37
42
|
done
|
|
38
43
|
if [ -n "$FRAME" ]; then
|
|
39
44
|
FSOCK="${FRAME%%:*}"; F="${FRAME#*:}"
|
|
40
45
|
else
|
|
41
|
-
tty="$
|
|
42
|
-
[ -
|
|
46
|
+
tty="$TTY"
|
|
47
|
+
if [ -z "$tty" ]; then
|
|
48
|
+
# no --tty: the CALLER's own client, proven (lib/client.sh) or refused. An
|
|
49
|
+
# untargeted `display -p '#{client_tty}'` answers with another terminal's
|
|
50
|
+
# client when this session has none, or has two - and toggled THAT frame.
|
|
51
|
+
SESSIONVIEW_ROOT="${SESSIONVIEW_ROOT:-$(cd "$(dirname "$(readlink -f "$0" 2>/dev/null || echo "$0")")/.." && pwd)}"
|
|
52
|
+
. "$SESSIONVIEW_ROOT/lib/client.sh"
|
|
53
|
+
sv_own_client || { echo "sessionview-sidebar-toggle: refused: $SV_REFUSAL" >&2; exit 2; }
|
|
54
|
+
tty="$SV_CLIENT"
|
|
55
|
+
fi
|
|
43
56
|
F="$(tmux -L "$FSOCK" list-panes -a -F '#{pane_tty} #{session_name}' 2>/dev/null | awk -v t="$tty" '$1 == t {print $2; exit}')"
|
|
44
57
|
[ -n "$F" ] || { echo "sessionview-sidebar-toggle: no frame shows this client ($tty) on server $FSOCK" >&2; exit 1; }
|
|
45
58
|
fi
|
|
@@ -162,16 +162,34 @@ _migrate_state()
|
|
|
162
162
|
os.makedirs(SESSIONVIEW_STATE, exist_ok=True)
|
|
163
163
|
def state(name): return os.path.join(SESSIONVIEW_STATE, name)
|
|
164
164
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
165
|
+
LOG_LEVELS = ("error", "timing", "debug")
|
|
166
|
+
LOG_LEVEL = "error" # set from `log_level` once the config is read (below CFG)
|
|
167
|
+
|
|
168
|
+
def log_at(level):
|
|
169
|
+
"""True when LEVEL's lines are written. Each level holds the ones before
|
|
170
|
+
it: errors always, timing at timing or debug, debug at debug only."""
|
|
171
|
+
return LOG_LEVELS.index(level) <= LOG_LEVELS.index(LOG_LEVEL)
|
|
172
|
+
|
|
173
|
+
def log(level, where, text):
|
|
174
|
+
"""ONE log for everything: ~/.local/state/sessionview/sessionview.log.
|
|
175
|
+
A line a person can read: time, socket, where, what. An error line is
|
|
176
|
+
exactly as it always was; a timing or debug line names its level after
|
|
177
|
+
the socket (`sock=X timing run: ...`), so the errors stay easy to grep.
|
|
178
|
+
Written only when `log_level` lets LEVEL through. Never raises."""
|
|
179
|
+
if not log_at(level):
|
|
180
|
+
return
|
|
181
|
+
tag = "" if level == "error" else level + " "
|
|
182
|
+
sock = globals().get("SOCK", "-") # a line written before SOCK is known still lands
|
|
169
183
|
try:
|
|
170
184
|
with open(state("sessionview.log"), "a") as lg:
|
|
171
|
-
lg.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')} sock={
|
|
185
|
+
lg.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')} sock={sock} {tag}{where}: {text}\n")
|
|
172
186
|
except Exception:
|
|
173
187
|
pass
|
|
174
188
|
|
|
189
|
+
def log_err(where, text):
|
|
190
|
+
"""An error line in the one log: written at every level."""
|
|
191
|
+
log("error", where, text)
|
|
192
|
+
|
|
175
193
|
def run_checked(cmd, where):
|
|
176
194
|
"""subprocess.run that LOGS a non-zero exit with its stderr instead of
|
|
177
195
|
swallowing it; returns the CompletedProcess. The + popup's new-window
|
|
@@ -249,6 +267,40 @@ def load_config():
|
|
|
249
267
|
cfg["refresh"] = float(os.environ["SIDEBAR_REFRESH"])
|
|
250
268
|
return cfg
|
|
251
269
|
CFG = load_config()
|
|
270
|
+
|
|
271
|
+
def log_level():
|
|
272
|
+
"""`log_level` from the config, $SESSIONVIEW_LOG_LEVEL over it (for one
|
|
273
|
+
run). error (the default) logs only what went wrong; timing adds a line
|
|
274
|
+
per click or key with its milliseconds, and each command that took
|
|
275
|
+
TIMING_MIN_MS or more; debug adds every command and every mouse event.
|
|
276
|
+
An unknown word is error, and the log says so once."""
|
|
277
|
+
raw = os.environ.get("SESSIONVIEW_LOG_LEVEL") or CFG.get("log_level") or "error"
|
|
278
|
+
v = str(raw).strip().lower()
|
|
279
|
+
if v in LOG_LEVELS:
|
|
280
|
+
return v
|
|
281
|
+
log_err("log_level", f"unknown level {raw!r}: using error (one of {', '.join(LOG_LEVELS)})")
|
|
282
|
+
return "error"
|
|
283
|
+
LOG_LEVEL = log_level()
|
|
284
|
+
|
|
285
|
+
TIMING_MIN_MS = 50 # timing: a command this slow or slower gets its line
|
|
286
|
+
_RUN = subprocess.run
|
|
287
|
+
def _timed_run(*args, **kw):
|
|
288
|
+
"""subprocess.run, timed: the command and its milliseconds go to the log
|
|
289
|
+
(timing when it took TIMING_MIN_MS or more, else debug). Installed only
|
|
290
|
+
when the level is timing or debug, so at error it costs nothing. Every
|
|
291
|
+
tm, tmux, git and sessionview-session call goes through here, the
|
|
292
|
+
worker threads' too: the slow one in a slow click is named."""
|
|
293
|
+
t0 = time.monotonic()
|
|
294
|
+
try:
|
|
295
|
+
return _RUN(*args, **kw)
|
|
296
|
+
finally:
|
|
297
|
+
ms = (time.monotonic() - t0) * 1000
|
|
298
|
+
cmd = args[0] if args else kw.get("args", "")
|
|
299
|
+
if isinstance(cmd, (list, tuple)):
|
|
300
|
+
cmd = " ".join(str(c) for c in cmd)
|
|
301
|
+
log("timing" if ms >= TIMING_MIN_MS else "debug", "run", f"{ms:.0f}ms {cmd}"[:300])
|
|
302
|
+
if LOG_LEVEL != "error":
|
|
303
|
+
subprocess.run = _timed_run
|
|
252
304
|
def wants_context(fmt):
|
|
253
305
|
"""A format that shows context, or the model (the scan is where the model
|
|
254
306
|
id comes from), pays the transcript read; nothing else does."""
|
|
@@ -2204,8 +2256,8 @@ def client_where():
|
|
|
2204
2256
|
only, while the pin sat on a store's server attached to demo
|
|
2205
2257
|
(measured), so the sessionview client was never found and a click on any
|
|
2206
2258
|
other store's session was refused as view-only, when a click on a
|
|
2207
|
-
tmux session should simply work.
|
|
2208
|
-
|
|
2259
|
+
tmux session should simply work. No pin found = no client (never the
|
|
2260
|
+
most recently active one: another terminal). Cached 5s; a move clears it."""
|
|
2209
2261
|
now = time.time()
|
|
2210
2262
|
if _CLIENT.get("t") and now - _CLIENT["t"] < 5:
|
|
2211
2263
|
return _CLIENT["v"]
|
|
@@ -2217,8 +2269,12 @@ def client_where():
|
|
|
2217
2269
|
# one of the user's other terminals: a click on
|
|
2218
2270
|
# demo switched a different window, focusing it in
|
|
2219
2271
|
# another terminal.
|
|
2220
|
-
|
|
2221
|
-
|
|
2272
|
+
# And even INSIDE the popup an untargeted display is not its client:
|
|
2273
|
+
# measured, it answered with the MORE RECENTLY ACTIVE client, not
|
|
2274
|
+
# the one showing the popup. A popup has no pane ($TMUX_PANE is
|
|
2275
|
+
# empty), so its client is the ONE client on $TMUX's session; none
|
|
2276
|
+
# or two = no client known, and a click moves nothing.
|
|
2277
|
+
v = (attached_socket(), popup_client())
|
|
2222
2278
|
else:
|
|
2223
2279
|
pin = os.environ.get("SESSIONVIEW_PIN") or state("frame.tty") # sessionview-frame: its own pin
|
|
2224
2280
|
tty_ = open(pin).read().strip() if os.path.exists(pin) else ""
|
|
@@ -2231,10 +2287,10 @@ def client_where():
|
|
|
2231
2287
|
break
|
|
2232
2288
|
if found:
|
|
2233
2289
|
v = (found, tty_)
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2290
|
+
# No pin found: NO client, never "the most recently active client on
|
|
2291
|
+
# the plain socket" - that is whichever terminal was used last,
|
|
2292
|
+
# someone else's, and a click would switch it. With no tty known
|
|
2293
|
+
# move_client moves nothing (a frame recreates its own right pane).
|
|
2238
2294
|
_CLIENT.update(t=now, v=v)
|
|
2239
2295
|
return v
|
|
2240
2296
|
|
|
@@ -2306,6 +2362,18 @@ def frame_attach(frame, sock, session):
|
|
|
2306
2362
|
f.write(tty_)
|
|
2307
2363
|
_CLIENT.clear()
|
|
2308
2364
|
|
|
2365
|
+
def popup_client():
|
|
2366
|
+
"""The tty of the client showing this popup: the ONE client attached to
|
|
2367
|
+
$TMUX's session (socket,pid,session-number), or "" when there are none
|
|
2368
|
+
or several - several cannot be told apart from here, and a guess moves
|
|
2369
|
+
another terminal."""
|
|
2370
|
+
parts = os.environ.get("TMUX", "").split(",")
|
|
2371
|
+
if len(parts) < 3 or not parts[0] or not parts[2].isdigit():
|
|
2372
|
+
return ""
|
|
2373
|
+
out = sh(["tmux", "-S", parts[0], "list-clients", "-t", "$" + parts[2], "-F", "#{client_tty}"])
|
|
2374
|
+
ttys = out.split()
|
|
2375
|
+
return ttys[0] if len(ttys) == 1 else ""
|
|
2376
|
+
|
|
2309
2377
|
def move_client(sock, session):
|
|
2310
2378
|
"""Show SESSION (on server SOCK) in the sessionview client. Same server:
|
|
2311
2379
|
switch-client. Another server: `detach-client -E` re-execs the client
|
|
@@ -3106,6 +3174,12 @@ def fit():
|
|
|
3106
3174
|
next open is the width chosen. The sidebar once
|
|
3107
3175
|
undid every drag on the next redraw.
|
|
3108
3176
|
|
|
3177
|
+
Only while a client is attached to the frame. Before the first attach the
|
|
3178
|
+
pane changes inside the same window too: the frame's own split and its
|
|
3179
|
+
resize to the configured width. Nobody can drag a border in a session
|
|
3180
|
+
with no client, so those are put back, never remembered (a new user's
|
|
3181
|
+
first open once wrote width 100, half the window the frame starts in).
|
|
3182
|
+
|
|
3109
3183
|
A drag below MIN_DRAG is a slip and is put back, not remembered. With
|
|
3110
3184
|
`fit: auto` the wish itself moves (see wish()); the pane follows it."""
|
|
3111
3185
|
global WIDTH
|
|
@@ -3119,10 +3193,13 @@ def fit():
|
|
|
3119
3193
|
WIDTH = max(8, min(wish(), cols))
|
|
3120
3194
|
return
|
|
3121
3195
|
fsock = frame.split(":", 1)[0]
|
|
3122
|
-
|
|
3123
|
-
|
|
3196
|
+
# one call: the window's width, and whether a client is attached to the frame
|
|
3197
|
+
w = sh(["tmux", "-L", fsock, "display-message", "-p", "-t", pane,
|
|
3198
|
+
"#{window_width} #{session_attached}"]).split()
|
|
3199
|
+
win = w[0] if w else ""
|
|
3200
|
+
attached = len(w) > 1 and w[1] != "0"
|
|
3124
3201
|
known = _FIT["pane"]
|
|
3125
|
-
dragged = (win and win == _FIT["win"] and known is not None and cols != known)
|
|
3202
|
+
dragged = (attached and win and win == _FIT["win"] and known is not None and cols != known)
|
|
3126
3203
|
if dragged and cols >= MIN_DRAG:
|
|
3127
3204
|
CFG["width"] = cols # the drag is the wish
|
|
3128
3205
|
if str(CFG.get("fit", "fixed")) in ("auto", "narrow", "focus"):
|
|
@@ -3154,6 +3231,7 @@ def draw(lines):
|
|
|
3154
3231
|
sys.stdout.write("\033[H\033[2J" + "\r\n".join(lines) + "\r")
|
|
3155
3232
|
sys.stdout.flush()
|
|
3156
3233
|
|
|
3234
|
+
INPUT = [""] # what the input being handled was, for its timing line
|
|
3157
3235
|
def main():
|
|
3158
3236
|
global GROUPED, MENU, PROMPT, WRITE_TABS, FILTER, ANCHOR
|
|
3159
3237
|
if "--pick" in sys.argv: # the picker alone, in a popup
|
|
@@ -3211,6 +3289,9 @@ def main():
|
|
|
3211
3289
|
if not r:
|
|
3212
3290
|
lines, targets = render(); draw(lines); continue
|
|
3213
3291
|
buf += os.read(fd, 64).decode(errors="ignore")
|
|
3292
|
+
t_in = time.monotonic()
|
|
3293
|
+
keys_in = MOUSE_RE.sub("", buf).replace("\x1b[O", "").replace("\x1b[I", "")
|
|
3294
|
+
INPUT[0] = f"key {keys_in!r}" if keys_in else ""
|
|
3214
3295
|
if "\x1b[O" in buf: # focus LEFT the sidebar: a click in the main section
|
|
3215
3296
|
buf = buf.replace("\x1b[O", "")
|
|
3216
3297
|
FOCUS["in"] = False
|
|
@@ -3249,12 +3330,11 @@ def main():
|
|
|
3249
3330
|
int(m.group(3)), m.group(4))
|
|
3250
3331
|
if kind != "M":
|
|
3251
3332
|
continue
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
lg.write(f"{time.strftime('%H:%M:%S')} btn={btn} x={x} y={y} target={targets.get(y - 1)}\n"[:200])
|
|
3255
|
-
except OSError:
|
|
3256
|
-
pass
|
|
3333
|
+
# evidence for "my click opened the menu": what button arrived
|
|
3334
|
+
log("debug", "mouse", f"btn={btn} x={x} y={y} target={targets.get(y - 1)}"[:200])
|
|
3257
3335
|
t = targets.get(y - 1)
|
|
3336
|
+
if t:
|
|
3337
|
+
INPUT[0] = (f"click{'' if btn == 0 else f' btn={btn}'} {t[0]} {t[1]}")[:120]
|
|
3258
3338
|
ANCHOR = y - 1 # a menu this click opens sits beside this row
|
|
3259
3339
|
if MENU is not None and not (t and t[0] in ("menu-item", "menu-close")):
|
|
3260
3340
|
# A click OUTSIDE an open menu closes it and is consumed -
|
|
@@ -3338,6 +3418,8 @@ def main():
|
|
|
3338
3418
|
project_menu(current_session())
|
|
3339
3419
|
ANCHOR = None # a keyboard open has no row: centred
|
|
3340
3420
|
lines, targets = render(); draw(lines)
|
|
3421
|
+
if INPUT[0]: # a click or a key: how long until you saw the answer
|
|
3422
|
+
log("timing", "input", f"{(time.monotonic() - t_in) * 1000:.0f}ms {INPUT[0]}")
|
|
3341
3423
|
buf = buf[buf.rfind("\x1b"):] if buf.count("\x1b") and not MOUSE_RE.search(buf[buf.rfind("\x1b"):]) else ""
|
|
3342
3424
|
finally:
|
|
3343
3425
|
tab_clear() # no tab-bar glyph outlives us
|
|
@@ -140,6 +140,10 @@ labels.
|
|
|
140
140
|
tmux call (a new tab that never appeared), a `--window` that refused,
|
|
141
141
|
an uncaught exception, each a line with time, socket, where, what.
|
|
142
142
|
`tail -f` it when something silently does nothing.
|
|
143
|
+
- `log_level` - what that ONE file records: `error` (the default), `timing`
|
|
144
|
+
(a line per click or key with its ms, and each command of 50 ms or more)
|
|
145
|
+
or `debug` (every command, every mouse event). `$SESSIONVIEW_LOG_LEVEL`
|
|
146
|
+
overrides it for one run.
|
|
143
147
|
- `window_pins` - your pins over that list, `{"name": true|false}`,
|
|
144
148
|
written by ^p / right-click in the new-tab picker. A delta, so the list
|
|
145
149
|
keeps flowing to you.
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
"width_min": 20,
|
|
10
10
|
"width_max": 60,
|
|
11
11
|
"refresh": 2,
|
|
12
|
+
"_log_level": "What goes in the ONE log, ~/.local/state/sessionview/sessionview.log. error: only what went wrong. timing: also a line per click or key with its milliseconds (from the input arriving to the redraw), and each command that took 50 ms or more. debug: also every command and every mouse event (the old sidebar-mouse.log). $SESSIONVIEW_LOG_LEVEL overrides it for one run. The log is not rotated: leave timing or debug on only while you look.",
|
|
13
|
+
"log_level": "error",
|
|
12
14
|
"project_two_line": false,
|
|
13
15
|
"project_expand_selected": false,
|
|
14
16
|
"project_gap": 0,
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# lib/client.sh - sourced by an entry script that acts on "the tmux client
|
|
2
|
+
# this was run from": sessionview-frame's hand-over (detach-client -E) and
|
|
3
|
+
# sessionview-sidebar-toggle without --tty. One rule, in one place.
|
|
4
|
+
#
|
|
5
|
+
# sv_own_client sets SV_CLIENT (that client's tty) and SV_SESSION (the
|
|
6
|
+
# session that client shows: the caller's) and returns 0, or sets SV_REFUSAL (one neutral
|
|
7
|
+
# line saying why) and returns 1. It reads tmux and changes nothing.
|
|
8
|
+
#
|
|
9
|
+
# An untargeted `tmux display -p '#{client_tty}'` is NOT the caller's client.
|
|
10
|
+
# tmux answers with the most recently active client of the caller's session,
|
|
11
|
+
# and when that session has no client attached, of the WHOLE server: some
|
|
12
|
+
# other terminal. Even `display -t "$TMUX_PANE"` falls back that way. So:
|
|
13
|
+
# 1. stdin and stdout are terminals, and `tty` names one. A script or an
|
|
14
|
+
# agent's shell that inherited TMUX and TMUX_PANE has neither.
|
|
15
|
+
# 2. that tty IS the pane's: `#{pane_tty}` of $TMUX_PANE. A process in a
|
|
16
|
+
# pane has the pane's pty as its terminal (never the client's), so this
|
|
17
|
+
# proves the caller is the process in that pane.
|
|
18
|
+
# 3. exactly one client is on a session that holds the pane's window. Not
|
|
19
|
+
# only the session `display -t "$TMUX_PANE"` names: a window can be in
|
|
20
|
+
# several sessions (a session group, `link-window`), and tmux names the
|
|
21
|
+
# most recently active of them, so a count over that one session alone
|
|
22
|
+
# handed over another terminal's client. None: no terminal shows the
|
|
23
|
+
# pane. More than one: no way to tell which terminal is the caller's,
|
|
24
|
+
# so refuse rather than guess.
|
|
25
|
+
sv_own_client() {
|
|
26
|
+
SV_CLIENT=""; SV_SESSION=""; SV_REFUSAL=""
|
|
27
|
+
if [ ! -t 0 ] || [ ! -t 1 ]; then
|
|
28
|
+
SV_REFUSAL="not a terminal: run it from a terminal (stdin and stdout must be a tty)"; return 1
|
|
29
|
+
fi
|
|
30
|
+
_sv_own="$(tty 2>/dev/null)" || { SV_REFUSAL="not a terminal: run it from a terminal (no controlling tty)"; return 1; }
|
|
31
|
+
if [ -z "${TMUX:-}" ] || [ -z "${TMUX_PANE:-}" ]; then
|
|
32
|
+
SV_REFUSAL="not inside a tmux pane (TMUX or TMUX_PANE is empty)"; return 1
|
|
33
|
+
fi
|
|
34
|
+
_sv_sock="${TMUX%%,*}"
|
|
35
|
+
_sv_tab="$(printf '\t')"
|
|
36
|
+
_sv_info="$(tmux -S "$_sv_sock" display-message -p -t "$TMUX_PANE" "#{pane_tty}$_sv_tab#{window_id}$_sv_tab#{session_name}" 2>/dev/null)" || _sv_info=""
|
|
37
|
+
_sv_ptty="${_sv_info%%"$_sv_tab"*}"; _sv_rest="${_sv_info#*"$_sv_tab"}"
|
|
38
|
+
_sv_wid="${_sv_rest%%"$_sv_tab"*}"; _sv_name="${_sv_rest#*"$_sv_tab"}"
|
|
39
|
+
if [ -z "$_sv_info" ] || [ "$_sv_ptty" != "$_sv_own" ]; then
|
|
40
|
+
SV_REFUSAL="not this pane's terminal: $_sv_own is not the tty of pane $TMUX_PANE"; return 1
|
|
41
|
+
fi
|
|
42
|
+
# every session that holds this pane's window, then the clients on those
|
|
43
|
+
_sv_sids="$(tmux -S "$_sv_sock" list-windows -a -F "#{window_id}$_sv_tab#{session_id}" 2>/dev/null \
|
|
44
|
+
| awk -F "$_sv_tab" -v w="$_sv_wid" '$1 == w {printf "%s ", $2}')" || _sv_sids=""
|
|
45
|
+
_sv_clients="$(tmux -S "$_sv_sock" list-clients -F "#{session_id}$_sv_tab#{client_tty}$_sv_tab#{session_name}" 2>/dev/null \
|
|
46
|
+
| awk -F "$_sv_tab" -v s="$_sv_sids" 'BEGIN {n = split(s, a, " "); for (i = 1; i <= n; i++) ok[a[i]] = 1}
|
|
47
|
+
($1 in ok) {print $2 "\t" $3}')" || _sv_clients=""
|
|
48
|
+
_sv_n="$(printf '%s\n' "$_sv_clients" | grep -c . || true)"
|
|
49
|
+
if [ "$_sv_n" -eq 0 ]; then
|
|
50
|
+
SV_REFUSAL="no terminal is attached to this session ($_sv_name)"; return 1
|
|
51
|
+
fi
|
|
52
|
+
if [ "$_sv_n" -gt 1 ]; then
|
|
53
|
+
SV_REFUSAL="this session ($_sv_name) is open in more than one terminal (detach the others, or run it outside tmux)"; return 1
|
|
54
|
+
fi
|
|
55
|
+
SV_CLIENT="${_sv_clients%%"$_sv_tab"*}"; SV_SESSION="${_sv_clients#*"$_sv_tab"}"
|
|
56
|
+
return 0
|
|
57
|
+
}
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sessionview"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "A sidebar for tmux: your projects (sessions) and your agents, here and on remote hosts."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11,<3.15"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|