sessionview 0.3.2__tar.gz → 0.3.3__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.
Files changed (55) hide show
  1. {sessionview-0.3.2 → sessionview-0.3.3}/PKG-INFO +16 -10
  2. {sessionview-0.3.2 → sessionview-0.3.3}/README.md +15 -9
  3. {sessionview-0.3.2 → sessionview-0.3.3}/bin/sessionview-frame +17 -6
  4. {sessionview-0.3.2 → sessionview-0.3.3}/bin/sessionview-install +9 -5
  5. {sessionview-0.3.2 → sessionview-0.3.3}/bin/sessionview-messages +17 -7
  6. {sessionview-0.3.2 → sessionview-0.3.3}/bin/tmux-send-agent +4 -3
  7. {sessionview-0.3.2 → sessionview-0.3.3}/bin/tmux-sidebar +147 -30
  8. {sessionview-0.3.2 → sessionview-0.3.3}/config/README.md +4 -3
  9. {sessionview-0.3.2 → sessionview-0.3.3}/config/config.json +2 -2
  10. {sessionview-0.3.2 → sessionview-0.3.3}/docs/configuration.md +10 -4
  11. {sessionview-0.3.2 → sessionview-0.3.3}/docs/settings.md +1 -1
  12. sessionview-0.3.3/lib/pane-menu.sh +56 -0
  13. {sessionview-0.3.2 → sessionview-0.3.3}/macos/SessionView.applescript +27 -1
  14. {sessionview-0.3.2 → sessionview-0.3.3}/pyproject.toml +1 -1
  15. {sessionview-0.3.2 → sessionview-0.3.3}/src/sessionview/__init__.py +1 -1
  16. sessionview-0.3.3/tmux/agent-menu.conf +95 -0
  17. sessionview-0.3.2/tmux/agent-menu.conf +0 -33
  18. {sessionview-0.3.2 → sessionview-0.3.3}/.gitignore +0 -0
  19. {sessionview-0.3.2 → sessionview-0.3.3}/LICENSE +0 -0
  20. {sessionview-0.3.2 → sessionview-0.3.3}/bin/sessionview +0 -0
  21. {sessionview-0.3.2 → sessionview-0.3.3}/bin/sessionview-app-install +0 -0
  22. {sessionview-0.3.2 → sessionview-0.3.3}/bin/sessionview-autostart +0 -0
  23. {sessionview-0.3.2 → sessionview-0.3.3}/bin/sessionview-config +0 -0
  24. {sessionview-0.3.2 → sessionview-0.3.3}/bin/sessionview-from +0 -0
  25. {sessionview-0.3.2 → sessionview-0.3.3}/bin/sessionview-newtab +0 -0
  26. {sessionview-0.3.2 → sessionview-0.3.3}/bin/sessionview-send +0 -0
  27. {sessionview-0.3.2 → sessionview-0.3.3}/bin/sessionview-session +0 -0
  28. {sessionview-0.3.2 → sessionview-0.3.3}/bin/sessionview-sidebar-toggle +0 -0
  29. {sessionview-0.3.2 → sessionview-0.3.3}/bin/sessionview-terminal +0 -0
  30. {sessionview-0.3.2 → sessionview-0.3.3}/bin/sessionview-usermail +0 -0
  31. {sessionview-0.3.2 → sessionview-0.3.3}/changelog.d/README.md +0 -0
  32. {sessionview-0.3.2 → sessionview-0.3.3}/config/keys.md +0 -0
  33. {sessionview-0.3.2 → sessionview-0.3.3}/config/neomuttrc +0 -0
  34. {sessionview-0.3.2 → sessionview-0.3.3}/docs/decisions/state-and-log-home.md +0 -0
  35. {sessionview-0.3.2 → sessionview-0.3.3}/docs/dependencies.md +0 -0
  36. {sessionview-0.3.2 → sessionview-0.3.3}/docs/sessionview.md +0 -0
  37. {sessionview-0.3.2 → sessionview-0.3.3}/lib/client.sh +0 -0
  38. {sessionview-0.3.2 → sessionview-0.3.3}/lib/landing.sh +0 -0
  39. {sessionview-0.3.2 → sessionview-0.3.3}/lib/python.sh +0 -0
  40. {sessionview-0.3.2 → sessionview-0.3.3}/lib/sessionview-config-screen +0 -0
  41. {sessionview-0.3.2 → sessionview-0.3.3}/lib/state.sh +0 -0
  42. {sessionview-0.3.2 → sessionview-0.3.3}/macos/dreamcatcher.icns +0 -0
  43. {sessionview-0.3.2 → sessionview-0.3.3}/plugins/session/plugin.toml +0 -0
  44. {sessionview-0.3.2 → sessionview-0.3.3}/plugins/session/start-agent +0 -0
  45. {sessionview-0.3.2 → sessionview-0.3.3}/plugins/taskmaster/plugin.toml +0 -0
  46. {sessionview-0.3.2 → sessionview-0.3.3}/plugins/taskmaster/tasks-tab +0 -0
  47. {sessionview-0.3.2 → sessionview-0.3.3}/plugins/voicemode/plugin.toml +0 -0
  48. {sessionview-0.3.2 → sessionview-0.3.3}/plugins/voicemode/speak +0 -0
  49. {sessionview-0.3.2 → sessionview-0.3.3}/skills/tmux/SKILL.md +0 -0
  50. {sessionview-0.3.2 → sessionview-0.3.3}/skills/tmux/scripts/tmux-peek +0 -0
  51. {sessionview-0.3.2 → sessionview-0.3.3}/skills/tmux/scripts/tmux-show +0 -0
  52. {sessionview-0.3.2 → sessionview-0.3.3}/skills/tmux/scripts/tmux-type +0 -0
  53. {sessionview-0.3.2 → sessionview-0.3.3}/skills/tmux/scripts/tmux-where +0 -0
  54. {sessionview-0.3.2 → sessionview-0.3.3}/tmux/frame.conf +0 -0
  55. {sessionview-0.3.2 → sessionview-0.3.3}/tmux/tabs-top.conf +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sessionview
3
- Version: 0.3.2
3
+ Version: 0.3.3
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
@@ -285,7 +285,9 @@ beside it. Each line says what puts it there and what takes it away.
285
285
  - **Services (launchd agents):** none. sessionview runs no daemon and
286
286
  writes nothing in `~/Library/LaunchAgents/`. While a frame is open it
287
287
  runs a tmux server of its own, `sessionview-frame`, beside yours; the
288
- server exits when the last frame closes.
288
+ server exits when the last frame closes. With `pane_menu` on, an
289
+ Option + right-click on that server's pane that holds your tmux goes
290
+ through to yours.
289
291
  - **Plugin and skills, optional:** `claude plugin marketplace add` and
290
292
  `claude plugin install` ([Install](#install)). Claude Code names the
291
293
  marketplace and the plugin in `~/.claude/settings.json`
@@ -310,9 +312,10 @@ beside it. Each line says what puts it there and what takes it away.
310
312
  mouse bindings on the status line (`MouseDown1Status`,
311
313
  `MouseDown1StatusRight`, `MouseDrag1Status`). It gives each session it
312
314
  opens the sessionview look, with options set on that session only.
313
- With `pane_menu` on, it also binds a right-click in a pane
314
- (`MouseDown3Pane`), in place of yours, and sets two global
315
- `@sessionview_agent_menu_` options. Nothing is written to
315
+ With `pane_menu` on, it also binds both right-clicks in a pane
316
+ (`MouseDown3Pane`, and `M-MouseDown3Pane` for Option + right-click),
317
+ in place of yours, and sets two global `@sessionview_agent_menu_`
318
+ options. Nothing is written to
316
319
  `~/.tmux.conf`: these last until that tmux server exits.
317
320
  - **Outside these folders, only on a verb:** with `fullscreen` on, the
318
321
  Dock app on Terminal.app needs the Accessibility permission you grant
@@ -372,17 +375,20 @@ keep theirs beside it in `~/.sessionkit/`.
372
375
  picker (type to filter, ↑↓, enter); `+` at the bottom → start an
373
376
  agent; the store name → that store's session and store view, its `⋯`
374
377
  (or `s`) → Switch store, right-click → Kick; `all · grouped` on the agents
375
- heading are toggles; drag the border to resize; `q` reloads, `Q`
378
+ heading are toggles (the first cycles all / store / project); drag the border to resize; `q` reloads, `Q`
376
379
  quits, esc backs out of any view
377
380
  - tab bar: `◧` at the far left hides/shows the sidebar; click selects,
378
381
  drag reorders, `+` new tab (the picker); a close `×`, on by default;
379
382
  `tmux set -g @sessionview_tab_close 0` turns it off
380
383
  - menu: ↑↓ / j k move, the letter or enter picks, esc or a click
381
384
  anywhere else closes
382
- - agent panes (opt-in): right-click an agent's pane for Speak, Continue,
383
- Read Clipboard, Park or Succeed; each mails that agent. Turn it on
384
- with `sessionview-config set pane_menu true`. It replaces your own tmux right-click menu, which is
385
- why it is off by default. Using tmux without session view? Add
385
+ - agent panes (opt-in): right-click or Option + right-click an agent's
386
+ pane for Speak, Continue, Read Clipboard, Park or Succeed; each mails
387
+ that agent. It opens even on an agent whose program uses the mouse, as
388
+ Claude Code does. An agent's pane is one whose title is
389
+ `<glyph> <address>`. Turn it on with
390
+ `sessionview-config set pane_menu true`. It replaces your own tmux
391
+ right-click menus, which is why it is off by default. Using tmux without session view? Add
386
392
  `source-file -q /path/to/sessionview/tmux/agent-menu.conf` to
387
393
  `~/.tmux.conf` and reload.
388
394
 
@@ -268,7 +268,9 @@ beside it. Each line says what puts it there and what takes it away.
268
268
  - **Services (launchd agents):** none. sessionview runs no daemon and
269
269
  writes nothing in `~/Library/LaunchAgents/`. While a frame is open it
270
270
  runs a tmux server of its own, `sessionview-frame`, beside yours; the
271
- server exits when the last frame closes.
271
+ server exits when the last frame closes. With `pane_menu` on, an
272
+ Option + right-click on that server's pane that holds your tmux goes
273
+ through to yours.
272
274
  - **Plugin and skills, optional:** `claude plugin marketplace add` and
273
275
  `claude plugin install` ([Install](#install)). Claude Code names the
274
276
  marketplace and the plugin in `~/.claude/settings.json`
@@ -293,9 +295,10 @@ beside it. Each line says what puts it there and what takes it away.
293
295
  mouse bindings on the status line (`MouseDown1Status`,
294
296
  `MouseDown1StatusRight`, `MouseDrag1Status`). It gives each session it
295
297
  opens the sessionview look, with options set on that session only.
296
- With `pane_menu` on, it also binds a right-click in a pane
297
- (`MouseDown3Pane`), in place of yours, and sets two global
298
- `@sessionview_agent_menu_` options. Nothing is written to
298
+ With `pane_menu` on, it also binds both right-clicks in a pane
299
+ (`MouseDown3Pane`, and `M-MouseDown3Pane` for Option + right-click),
300
+ in place of yours, and sets two global `@sessionview_agent_menu_`
301
+ options. Nothing is written to
299
302
  `~/.tmux.conf`: these last until that tmux server exits.
300
303
  - **Outside these folders, only on a verb:** with `fullscreen` on, the
301
304
  Dock app on Terminal.app needs the Accessibility permission you grant
@@ -355,17 +358,20 @@ keep theirs beside it in `~/.sessionkit/`.
355
358
  picker (type to filter, ↑↓, enter); `+` at the bottom → start an
356
359
  agent; the store name → that store's session and store view, its `⋯`
357
360
  (or `s`) → Switch store, right-click → Kick; `all · grouped` on the agents
358
- heading are toggles; drag the border to resize; `q` reloads, `Q`
361
+ heading are toggles (the first cycles all / store / project); drag the border to resize; `q` reloads, `Q`
359
362
  quits, esc backs out of any view
360
363
  - tab bar: `◧` at the far left hides/shows the sidebar; click selects,
361
364
  drag reorders, `+` new tab (the picker); a close `×`, on by default;
362
365
  `tmux set -g @sessionview_tab_close 0` turns it off
363
366
  - menu: ↑↓ / j k move, the letter or enter picks, esc or a click
364
367
  anywhere else closes
365
- - agent panes (opt-in): right-click an agent's pane for Speak, Continue,
366
- Read Clipboard, Park or Succeed; each mails that agent. Turn it on
367
- with `sessionview-config set pane_menu true`. It replaces your own tmux right-click menu, which is
368
- why it is off by default. Using tmux without session view? Add
368
+ - agent panes (opt-in): right-click or Option + right-click an agent's
369
+ pane for Speak, Continue, Read Clipboard, Park or Succeed; each mails
370
+ that agent. It opens even on an agent whose program uses the mouse, as
371
+ Claude Code does. An agent's pane is one whose title is
372
+ `<glyph> <address>`. Turn it on with
373
+ `sessionview-config set pane_menu true`. It replaces your own tmux
374
+ right-click menus, which is why it is off by default. Using tmux without session view? Add
369
375
  `source-file -q /path/to/sessionview/tmux/agent-menu.conf` to
370
376
  `~/.tmux.conf` and reload.
371
377
 
@@ -119,12 +119,16 @@ FRESH_WIN=""
119
119
  # global config is untouched (a normal tmux session keeps its bar at the
120
120
  # bottom).
121
121
  sh -c 'S="$1"; L="$3"; tmux() { command tmux -L "$L" "$@"; }; . "$2"' sh "$SESSION" "$SESSIONVIEW_ROOT/tmux/frame.conf" "$SOCK"
122
- # the agent pane menu (right-click an agent's pane: Speak / Continue / ...), IF
123
- # the user turned it on. Opt-in: one bind-key is global to the server and
124
- # replaces their own right-click menu. Sourced into the running server, so it
125
- # works whether or not tmux was already up. Fails open, like the rest of the
126
- # frame's setup. See tmux/agent-menu.conf.
127
- if [ "$("$SESSIONVIEW_ROOT/bin/sessionview-config" get pane_menu 2>/dev/null)" = true ]; then
122
+ # the agent pane menu (right-click or Option + right-click an agent's pane:
123
+ # Speak / Continue / ...), IF the user turned it on. Opt-in: its two bind-keys
124
+ # are global to the server and replace both of the user's own right-click
125
+ # bindings (MouseDown3Pane, M-MouseDown3Pane). Sourced into the running
126
+ # server, so it works whether or not tmux was already up. Fails open, like the
127
+ # rest of the frame's setup. See tmux/agent-menu.conf. Read once: the frame's
128
+ # own server acts on the same answer below (the Option + right-click
129
+ # pass-through).
130
+ PANE_MENU="$("$SESSIONVIEW_ROOT/bin/sessionview-config" get pane_menu 2>/dev/null || true)"
131
+ if [ "$PANE_MENU" = true ]; then
128
132
  "$TMUX_BIN" -L "$SOCK" source-file "$SESSIONVIEW_ROOT/tmux/agent-menu.conf" 2>/dev/null || true
129
133
  fi
130
134
  # the chosen agent, if the user has one - asked once, remembered, started in
@@ -194,6 +198,13 @@ o set -w -t "=$F:0" pane-active-border-style "fg=#3a3f4d,bg=#21252e"
194
198
  o set -w -t "=$F:0" pane-border-lines single
195
199
  o bind-key -T root MouseDown1Border "resize-pane -M"
196
200
  o bind-key -T root MouseDrag1Border "resize-pane -M"
201
+ # with pane_menu on, an Option + right-click on the client pane goes through to
202
+ # your tmux and its agent menu; the frame's other panes keep the binding this
203
+ # server had. Here, after new-session: before it this server may not exist.
204
+ # Off, nothing is touched. Fails open. See lib/pane-menu.sh.
205
+ if [ "$PANE_MENU" = true ]; then
206
+ { . "$SESSIONVIEW_ROOT/lib/pane-menu.sh" && sv_frame_option_forward "$FRAME_SOCK"; } >/dev/null 2>&1 || true
207
+ fi
197
208
  MAIN="$(o split-window -h -P -F '#{pane_id}' -t "$SIDE" -e "TMUX=" \
198
209
  "exec \"$TMUX_BIN\" -L \"$SOCK\" attach -t \"=$SESSION\"")"
199
210
  # the frame publishes its own panes, so the sidebar process (which cannot see
@@ -168,13 +168,17 @@ else
168
168
  say WARN "neomutt not on PATH - the messages tab, which sessionview makes only when sessionmail is installed, cannot open (brew install neomutt)"
169
169
  fi
170
170
  # notmuch - the messages tab's sidebar queries Type: and Project:; unindexed, the Asks
171
- # rows are every mail to NAME+ask, with no error
171
+ # rows are every mail to NAME+ask, with no error. Asked of sessionmail's notmuch profile
172
+ # when `message path --notmuch-config` names one (sessionmail 0.3.3), as the tab does;
173
+ # no message, an older one or no profile: notmuch's own default config.
172
174
  if command -v notmuch >/dev/null 2>&1; then
173
- if [ "$(notmuch config get index.header.Type 2>/dev/null)" = Type ] &&
174
- [ "$(notmuch config get index.header.Project 2>/dev/null)" = Project ]; then
175
- say PASS "notmuch indexes Type and Project"
175
+ NMCFG="$(message path --notmuch-config 2>/dev/null)" || NMCFG=""
176
+ NM="notmuch${NMCFG:+ --config=$NMCFG}"
177
+ if [ "$(notmuch ${NMCFG:+"--config=$NMCFG"} config get index.header.Type 2>/dev/null)" = Type ] &&
178
+ [ "$(notmuch ${NMCFG:+"--config=$NMCFG"} config get index.header.Project 2>/dev/null)" = Project ]; then
179
+ say PASS "notmuch indexes Type and Project${NMCFG:+ ($NMCFG)}"
176
180
  else
177
- say WARN "notmuch does not index Type and Project - the Asks rows show every mail to NAME+ask: notmuch config set index.header.Type Type; notmuch config set index.header.Project Project; notmuch reindex '*'"
181
+ say WARN "notmuch${NMCFG:+ ($NMCFG)} does not index Type and Project - the Asks rows show every mail to NAME+ask: $NM config set index.header.Type Type; $NM config set index.header.Project Project; $NM reindex '*'"
178
182
  fi
179
183
  else say WARN "notmuch not on PATH - the messages tab has no sidebar (brew install notmuch)"; fi
180
184
  # optional
@@ -8,7 +8,7 @@
8
8
  # named `messages` in both store and project sessions. Three facts, each
9
9
  # measured:
10
10
  # the folder `message path [SLUG/]PROJECT` (sessionmail 2454f6f) prints
11
- # the project's maildir, e.g. ~/.mail/agents/acme/myproject;
11
+ # the project's maildir, wherever sessionmail keeps it;
12
12
  # exit 2 when there is none.
13
13
  # the viewer `neomutt -R -F RC -f FOLDER`. -R (read-only) is not optional:
14
14
  # without it, reading a message moves it new/ -> cur/ and the
@@ -41,6 +41,9 @@
41
41
  # the rows are every mail to PROJECT+ask, replies too, and the rc says so;
42
42
  # without notmuch, or a folder notmuch does not hold, the tab is the one
43
43
  # folder as before and the sidebar is hidden. --print names the reason.
44
+ # notmuch here, and neomutt's (nm_config_file), read sessionmail's notmuch
45
+ # profile when `message path --notmuch-config` names one (sessionmail 0.3.3);
46
+ # an older sessionmail, or no profile (exit 1 or 2), is notmuch's own default.
44
47
  # Ctrl-o (sidebar-open) is the macOS tty's `discard` char and never reaches
45
48
  # neomutt; `stty discard undef` runs below, in the shell that owns the tty
46
49
  # (measured by the first user of this layout: a backtick inside
@@ -92,19 +95,26 @@ case "$FOLDER" in *[\"\`\\\$]*|*[[:cntrl:]]*)
92
95
  [ -d "$FOLDER" ] || { echo "sessionview-messages: $BOX: $FOLDER is not a directory" >&2; exit 2; }
93
96
  # The sidebar's rows: per project, so written here, not in config/neomuttrc.
94
97
  SIDEBAR=""; SIDEBAR_WHY=""
95
- if command -v notmuch >/dev/null 2>&1; then
96
- DB="$(notmuch config get database.path 2>/dev/null)"
98
+ NMCFG="$(message path --notmuch-config 2>/dev/null)" || NMCFG=""
99
+ nm() { notmuch ${NMCFG:+"--config=$NMCFG"} "$@"; }
100
+ NM="notmuch${NMCFG:+ --config=$NMCFG}" # the words in a fix line
101
+ case "$NMCFG" in *[\"\`\\\$]*|*[[:cntrl:]]*)
102
+ SIDEBAR_WHY="sessionmail's notmuch config refused: its path holds a \" \` \\ \$ or control character, which the rc would run or break on";; esac
103
+ if [ -n "$SIDEBAR_WHY" ]; then :
104
+ elif command -v notmuch >/dev/null 2>&1; then
105
+ DB="$(nm config get database.path 2>/dev/null)"
97
106
  if [ -z "$DB" ]; then SIDEBAR_WHY="notmuch has no database.path"
98
107
  else case "$FOLDER/" in
99
108
  "$DB"/*)
100
- if [ "$(notmuch config get index.header.Type 2>/dev/null)" = Type ] &&
101
- [ "$(notmuch config get index.header.Project 2>/dev/null)" = Project ]; then
109
+ if [ "$(nm config get index.header.Type 2>/dev/null)" = Type ] &&
110
+ [ "$(nm config get index.header.Project 2>/dev/null)" = Project ]; then
102
111
  Q="Type:ask and (to:\\\"$NAME+ask\\\" or Project:\\\"$NAME\\\")"
103
112
  else
104
113
  Q="to:\\\"$NAME+ask\\\""
105
- SIDEBAR_WHY="notmuch does not index Type and Project, so Asks is every mail to $NAME+ask, replies too - notmuch config set index.header.Type Type; notmuch config set index.header.Project Project; notmuch reindex '*'"
114
+ SIDEBAR_WHY="notmuch does not index Type and Project, so Asks is every mail to $NAME+ask, replies too - $NM config set index.header.Type Type; $NM config set index.header.Project Project; $NM reindex '*'"
106
115
  fi
107
- SIDEBAR="$(printf 'set nm_default_url="notmuch://%s"\nset nm_query_type=messages\nset virtual_spoolfile=no\nnamed-mailboxes "%s" "%s"\nvirtual-mailboxes "Asks" "notmuch://?query=%s"\nvirtual-mailboxes "Asks · threads" "notmuch://?type=threads&query=%s"\n' "$DB" "$NAME" "$FOLDER" "$Q" "$Q")";;
116
+ SIDEBAR="$(printf 'set nm_default_url="notmuch://%s"\nset nm_query_type=messages\nset virtual_spoolfile=no\nnamed-mailboxes "%s" "%s"\nvirtual-mailboxes "Asks" "notmuch://?query=%s"\nvirtual-mailboxes "Asks · threads" "notmuch://?type=threads&query=%s"\n' "$DB" "$NAME" "$FOLDER" "$Q" "$Q")"
117
+ [ -z "$NMCFG" ] || SIDEBAR="set nm_config_file=\"$NMCFG\""$'\n'"$SIDEBAR";;
108
118
  *) SIDEBAR_WHY="$FOLDER is not under notmuch database.path $DB";;
109
119
  esac; fi
110
120
  else SIDEBAR_WHY="no notmuch on PATH (brew install notmuch)"; fi
@@ -51,9 +51,10 @@ to=""
51
51
  # the session box: the last +field, :field or .field that is 8 hex
52
52
  for cand in "${title##*+}" "${title##*:}" "${title##*.}"; do
53
53
  if printf '%s' "$cand" | grep -qE '^[0-9a-f]{8}$'; then
54
- for d in "$HOME/.mail/agents/$cand"*/; do
55
- [ -d "$d" ] && { to="$(basename "$d")"; break; }
56
- done
54
+ # sessionmail names the box: the first `session ID` row of `message box
55
+ # list`. Where its mail root is, is sessionmail's to know, never ours to
56
+ # spell. A refusal or no row leaves `to` empty: the seat address below.
57
+ to="$("$message_bin" box list "$cand*" --all 2>/dev/null | awk '$1=="session"{print $2; exit}' || true)"
57
58
  break
58
59
  fi
59
60
  done
@@ -477,16 +477,50 @@ def paint(text, selected):
477
477
  WIDTH = int(CFG["width"])
478
478
  REFRESH = float(CFG["refresh"])
479
479
  GLYPH = {"busy": fg("#f9e2af") + "◐" + NOFG, "idle": fg("#a6e3a1") + "●" + NOFG,
480
- "shell": C_DIM + "○" + NOFG, "blocked": fg("#f38ba8") + "■" + NOFG,
481
- "waiting": fg("#f38ba8") + "■" + NOFG}
482
- RANK = {"blocked": 0, "waiting": 0, "busy": 1, "idle": 2, "shell": 3}
480
+ "blocked": fg("#f38ba8") + "■" + NOFG, "waiting": fg("#f38ba8") + "■" + NOFG}
481
+ RANK = {"blocked": 0, "waiting": 0, "busy": 1, "idle": 2}
482
+ # A seat's condition, in the sidebar's words: blocked, waiting, busy or idle.
483
+ # Every glyph, sort and rollup asks seat_word(); nothing else reads `status`
484
+ # for that. `state` is the session tools' word (busy | idle with children |
485
+ # idle) and decides busy against idle, but it cannot say "a prompt blocks the
486
+ # turn" (`waiting` reads idle there), so the attention words come from the
487
+ # raw `status` first. A feed with no `state` (an older session tools) has its
488
+ # `status` mapped instead. Claude Code writes `shell`, not `idle`, for a seat
489
+ # at its prompt whenever a background shell lives (a listener left running is
490
+ # one), so most idle seats say `shell`: that is idle.
491
+ ATTENTION = ("blocked", "waiting")
492
+ STATE_WORD = {"busy": "busy", "idle": "idle", "idle with children": "idle"}
493
+ STATUS_WORD = {"busy": "busy", "idle": "idle", "shell": "idle"}
494
+
495
+ def seat_word(a):
496
+ status = a.get("status")
497
+ if status in ATTENTION:
498
+ return status
499
+ state = a.get("state")
500
+ if state:
501
+ return STATE_WORD.get(state, state)
502
+ return STATUS_WORD.get(status, status)
503
+
483
504
  MOUSE_RE = re.compile(r"\x1b\[<(\d+);(\d+);(\d+)([mM])")
484
505
  ANSI_RE = re.compile(r"\x1b\[[0-9;]*m")
485
506
 
486
507
  POPUP = "--popup" in sys.argv # inside `tmux display-popup -E`: a jump closes us
487
508
  PICKER = None # the ONE type-and-filter box (new session / new tab / start an agent); see open_picker
488
509
  GROUPED = CFG.get("group_by", "none") != "none"
489
- FILTER = "all" # all | current (the agents of the selected project)
510
+ # The agents list's scopes, in the order a click on the heading word cycles
511
+ # them: widest to narrowest. scope_filter() is what each one keeps.
512
+ SCOPES = ("all", "store", "project")
513
+ # The scope the sidebar starts in: "all" | "store" | "project". An OPEN
514
+ # choice, not a decision: "all" is today's behaviour, kept until the
515
+ # owner chooses. A one-word edit; every option is already tested.
516
+ FILTER_DEFAULT = "all"
517
+ # What the `store` scope shows when the selected project is in no store
518
+ # (no store owns the viewed server, or the selected session is not one of
519
+ # its projects): "project" | "pinned" | "nostore" | "all". An OPEN choice
520
+ # too, and PROVISIONAL: "all" is the one option that cannot hide a seat.
521
+ # A one-word edit; scope_filter() implements and tests all four.
522
+ STORE_SCOPE_NO_STORE = "all"
523
+ FILTER = FILTER_DEFAULT # all | store | project; see scope_filter
490
524
  MENU = None # the project menu, drawn IN the sidebar (see project_menu)
491
525
  PROMPT = None # an inline text/confirm line, drawn IN the sidebar
492
526
  # The row a menu opens beside, so a menu appears close to the row that
@@ -996,6 +1030,30 @@ def row_store():
996
1030
  on = [s for s in stores() if (s.get("socket") or "default") == SOCK]
997
1031
  return next((s for s in on if s.get("active")), None) or (on[0] if on else {})
998
1032
 
1033
+ _STORE_PROJ = {}
1034
+ def store_projects(rec):
1035
+ """The set of tm project names in store REC, by `tm project list
1036
+ --store PATH`, cached 60s per store path (projects_recent's lifetime).
1037
+ The empty set when REC has no path, tm is absent, or its answer is not
1038
+ JSON. Only the `store` scope asks: `all` and `project` cost nothing.
1039
+ Not projects_recent(): that mixes in checkouts on disk
1040
+ (repo_projects), and a checkout is not a store project."""
1041
+ root = (rec or {}).get("path") or ""
1042
+ if not root:
1043
+ return set()
1044
+ root = os.path.realpath(root)
1045
+ now = time.time()
1046
+ hit = _STORE_PROJ.get(root)
1047
+ if hit and now - hit[0] < 60:
1048
+ return set(hit[1])
1049
+ try:
1050
+ rows = json.loads(sh(["tm", "project", "list", "-f", "json", "--store", root]) or "[]")
1051
+ names = {p.get("name", "") for p in rows if isinstance(p, dict)} - {""}
1052
+ except (json.JSONDecodeError, TypeError, AttributeError):
1053
+ names = set()
1054
+ _STORE_PROJ[root] = (now, frozenset(names))
1055
+ return set(names)
1056
+
999
1057
  def open_store(name):
1000
1058
  """The store row's name, and a pick in the Switch store modal: open
1001
1059
  NAME's store session and look at it. A down server is booted by
@@ -1768,7 +1826,7 @@ def agent_fields(a, window="", dups=(), model_w=0):
1768
1826
  numbered seat (`alice-e9`): two rows with the same name fall back to
1769
1827
  showing the extra number."""
1770
1828
  f = _Safe(a)
1771
- f["glyph"] = GLYPH.get(a.get("status"), "?")
1829
+ f["glyph"] = GLYPH.get(seat_word(a), "?")
1772
1830
  # Right-hand columns line up row to row: the model is right-justified to
1773
1831
  # the widest one on screen, the context to 4 (`100%` is real). The dash
1774
1832
  # is left padded too, so the right-hand side lines up row to row.
@@ -1810,9 +1868,9 @@ def agent_fields(a, window="", dups=(), model_w=0):
1810
1868
  # sweeps leftovers at start, so a glyph cannot outlive the sidebar by much.
1811
1869
  _TABS = {}
1812
1870
  WRITE_TABS = True
1813
- TAB_CH = {"busy": "\u25d0", "idle": "\u25cf", "shell": "\u25cb",
1871
+ TAB_CH = {"busy": "\u25d0", "idle": "\u25cf",
1814
1872
  "blocked": "\u25a0", "waiting": "\u25a0"}
1815
- TAB_FG = {"busy": "#f9e2af", "idle": "#a6e3a1", "shell": "#7f859b",
1873
+ TAB_FG = {"busy": "#f9e2af", "idle": "#a6e3a1",
1816
1874
  "blocked": "#f38ba8", "waiting": "#f38ba8"}
1817
1875
 
1818
1876
  def tab_glyphs(by_win, pl):
@@ -1828,7 +1886,7 @@ def tab_glyphs(by_win, pl):
1828
1886
  want = {}
1829
1887
  for w, states in by_win.items():
1830
1888
  st = rollup(states)
1831
- if st:
1889
+ if st in TAB_CH: # a word with no glyph draws none
1832
1890
  want[w] = st
1833
1891
  wins = {f"{p[1]}:{p[6]}" for p in pl}
1834
1892
  for w in [k for k in set(_TABS) | set(want) if k != "_swept"]:
@@ -1854,12 +1912,13 @@ def tab_clear():
1854
1912
  _TABS.clear()
1855
1913
 
1856
1914
  def sort_key(a, keys=None):
1857
- """A row's place by the `sort` config (KEYS, default CFG["sort"]): status
1858
- rank, then idle, as configured. Pins are not in here; see order_agents."""
1915
+ """A row's place by the `sort` config (KEYS, default CFG["sort"]): the
1916
+ seat's rank (seat_word), then idle, as configured. Pins are not in
1917
+ here; see order_agents."""
1859
1918
  key = []
1860
1919
  for k in (CFG["sort"] if keys is None else keys):
1861
1920
  if k == "rank":
1862
- key.append(RANK.get(a.get("status"), 9))
1921
+ key.append(RANK.get(seat_word(a), 9))
1863
1922
  else:
1864
1923
  v = a.get(k)
1865
1924
  key.append(v if v is not None else (0 if k.endswith("_s") else ""))
@@ -1941,8 +2000,9 @@ def order_agents(ag, pinned=(), keys=None, grp_of=None, group_order=None, within
1941
2000
  are stable, so the order within a group (and within the pinned block)
1942
2001
  is the first sort's. WITHIN given too: a non-pinned row's key
1943
2002
  inside its group is WITHIN(row), ties keeping the first sort's order;
1944
- the pinned block keeps its pin order. Filtering (hide, current) is the
1945
- caller's, BEFORE this: a pin orders a row, it never unhides one."""
2003
+ the pinned block keeps its pin order. Filtering is the caller's, BEFORE
2004
+ this: `hide` drops rows, pinned or not; the scope filter (scope_filter)
2005
+ keeps every pinned row. A pin orders a row; this function never adds one."""
1946
2006
  pins = pin_rank(pinned)
1947
2007
  def pin(a):
1948
2008
  return pins.get(a.get("agent"), len(pins))
@@ -1958,6 +2018,57 @@ def order_agents(ag, pinned=(), keys=None, grp_of=None, group_order=None, within
1958
2018
  out.sort(key=gkey)
1959
2019
  return out
1960
2020
 
2021
+ def scope_filter(ag, scope, p2s, cur_sess, pinned=(), store_sessions=None, no_store=None):
2022
+ """The rows of AG that SCOPE shows, in AG's order, pure: AG and its
2023
+ rows are not changed. It runs after `hide` and BEFORE order_agents.
2024
+
2025
+ A pinned row (its `agent` is in PINNED) is kept under every scope. A
2026
+ pinned row whose status is in `hide` stays hidden: `hide` runs first and
2027
+ is the user's own standing rule, not a scope.
2028
+
2029
+ P2S maps a pane to its session on the VIEWED server; CUR_SESS is the
2030
+ selected session. STORE_SESSIONS is the current store's session set
2031
+ (its tm projects plus its own session), or None when no store owns the
2032
+ viewed server. A remote row (`_host`) is never local: its pane id is the
2033
+ remote server's, and pane ids collide across servers.
2034
+
2035
+ - all: every row.
2036
+ - project: the local rows whose session is CUR_SESS.
2037
+ - store, CUR_SESS in STORE_SESSIONS: the local rows whose session is in
2038
+ STORE_SESSIONS.
2039
+ - store, anywhere else (no store, or the selected session is not one of
2040
+ its projects): NO_STORE says, default STORE_SCOPE_NO_STORE -
2041
+ "project" the project rule; "pinned" the pins only; "nostore" the
2042
+ local rows on the viewed server whose session is not in the store
2043
+ (with no store, every local row on the viewed server); "all" every row.
2044
+ It keeps to the viewed server, like the other two scopes: a seat with
2045
+ no pane here has no session here to compare.
2046
+ An unknown SCOPE or NO_STORE shows every row: a typo never hides a seat."""
2047
+ pins = pin_rank(pinned)
2048
+ if no_store is None:
2049
+ no_store = STORE_SCOPE_NO_STORE
2050
+
2051
+ def sess(a): # the row's session on the viewed server, or ""
2052
+ return "" if a.get("_host") else (p2s.get(a.get("tmux_pane") or "") or "")
2053
+
2054
+ def in_project(a):
2055
+ return bool(cur_sess) and sess(a) == cur_sess
2056
+
2057
+ store = (set(store_sessions) - {""}) if store_sessions is not None else None
2058
+ if scope == "project":
2059
+ keep = in_project
2060
+ elif scope == "store" and store is not None and cur_sess in store:
2061
+ keep = lambda a: sess(a) in store
2062
+ elif scope == "store" and no_store == "project":
2063
+ keep = in_project
2064
+ elif scope == "store" and no_store == "pinned":
2065
+ keep = lambda a: False
2066
+ elif scope == "store" and no_store == "nostore":
2067
+ keep = lambda a: bool(sess(a)) and (store is None or sess(a) not in store)
2068
+ else: # all, the "all" no-store option, or an unknown word
2069
+ keep = lambda a: True
2070
+ return [a for a in ag if a.get("agent") in pins or keep(a)]
2071
+
1961
2072
  def rollup(states):
1962
2073
  return min(states, key=lambda s: RANK.get(s, 9)) if states else None
1963
2074
 
@@ -1970,11 +2081,6 @@ IDLE_W = 4 # `999m`: the cell is always this wide, so rows
1970
2081
  C_IDLE_ORANGE = fg("#fab387") # the palette's peach, beside the busy yellow
1971
2082
  C_IDLE_RED = fg("#f38ba8") # the blocked glyph's red
1972
2083
  IDLE_DEFAULTS = {"agent_idle_orange": 45, "agent_idle_red": 60, "agent_idle_blink": 0}
1973
- # The statuses that mean "at its prompt, its turn over": Claude Code writes
1974
- # `shell`, not `idle`, whenever a background shell lives (a listener left
1975
- # running is one), so many idle seats say `shell`; the session tools read
1976
- # both as the state `idle`. busy, waiting and blocked are not idle.
1977
- IDLE_STATUSES = ("idle", "shell")
1978
2084
 
1979
2085
  def idle_minutes_setting(key):
1980
2086
  """A threshold in minutes from the config, or None for off: 0, a
@@ -1994,14 +2100,14 @@ def idle_minutes_setting(key):
1994
2100
 
1995
2101
  def fmt_idle_cell(a):
1996
2102
  """The {idle_m} cell, IDLE_W columns wide whatever it holds. Blank unless
1997
- the seat is IDLE (its status is in IDLE_STATUSES): idle_s is the time in
2103
+ the seat is IDLE (seat_word says idle): idle_s is the time in
1998
2104
  the current state, so a seat busy for an hour has a large idle_s too,
1999
2105
  and must show nothing.
2000
2106
  An idle seat shows its whole minutes (`34m`; hours past 999m): dim grey,
2001
2107
  then orange and bold from agent_idle_orange, red and bold from
2002
2108
  agent_idle_red, blinking from agent_idle_blink when that is set (it is
2003
2109
  off by default)."""
2004
- if a.get("status") not in IDLE_STATUSES:
2110
+ if seat_word(a) != "idle":
2005
2111
  return " " * IDLE_W
2006
2112
  try:
2007
2113
  secs = max(0, int(a.get("idle_s") or 0))
@@ -2112,8 +2218,8 @@ def render_base(height=None):
2112
2218
  pane = a.get("tmux_pane") or ""
2113
2219
  s = p2s.get(pane)
2114
2220
  if s:
2115
- by_sess.setdefault(s, []).append(a.get("status", ""))
2116
- by_win.setdefault(f"{s}:{p2i[pane]}", []).append(a.get("status", ""))
2221
+ by_sess.setdefault(s, []).append(seat_word(a) or "")
2222
+ by_win.setdefault(f"{s}:{p2i[pane]}", []).append(seat_word(a) or "")
2117
2223
  tab_glyphs(by_win, pl) # the tab-bar glyphs
2118
2224
 
2119
2225
  lines, targets = [], {}
@@ -2247,7 +2353,7 @@ def render_base(height=None):
2247
2353
  for a in R["agents"]:
2248
2354
  sname = R["p2s"].get(a.get("tmux_pane") or "")
2249
2355
  if sname:
2250
- rby.setdefault(sname, []).append(a.get("status", ""))
2356
+ rby.setdefault(sname, []).append(seat_word(a) or "")
2251
2357
  for name, nwin, att in R["sessions"]:
2252
2358
  st = rollup(rby.get(name, []))
2253
2359
  g = GLYPH.get(st, " ") if st else " "
@@ -2267,8 +2373,14 @@ def render_base(height=None):
2267
2373
  pane = a.get("tmux_pane") or ""
2268
2374
  ag.append(dict(a, _host=host, _window=R.get("p2w", {}).get(pane, ""),
2269
2375
  _session=R.get("p2s", {}).get(pane, "")))
2270
- if FILTER == "current":
2271
- ag = [a for a in ag if p2s.get(a.get("tmux_pane") or "") == cur_sess]
2376
+ # the scope, then the sort: a pinned row shows under every scope. The
2377
+ # store's session set (its tm projects, its own session, a live legacy
2378
+ # `store`) is asked for only under `store`: all and project cost nothing.
2379
+ in_sess = None
2380
+ if FILTER == "store" and srec:
2381
+ in_sess = (store_projects(srec) | {ssess}
2382
+ | ({LEGACY_STORE_SESSION} & {n for n, _, _ in live_sessions}))
2383
+ ag = scope_filter(ag, FILTER, p2s, cur_sess, CFG.get("pinned_agents"), in_sess)
2272
2384
  gb = CFG.get("group_by", "none") if not GROUPED else (
2273
2385
  CFG.get("group_by", "none") if CFG.get("group_by", "none") != "none" else "session")
2274
2386
  if not GROUPED:
@@ -2304,11 +2416,15 @@ def render_base(height=None):
2304
2416
  targets[len(lines)] = ("new-or-menu", (vislen(left), WIDTH - vislen(right) if right else WIDTH + 1))
2305
2417
  lines.append(row(C_DIM + lay(spec, WIDTH, chrome=True) + NOFG))
2306
2418
  put("")
2307
- # heading: "<left from config> all · grouped" -- each word a toggle,
2308
- # lit when on: show all, or show current
2419
+ # heading: "<left from config> all · grouped" -- each word a toggle.
2420
+ # The first names the scope, lit whenever a scope narrows the list; under
2421
+ # the "pinned" no-store option, outside a store, it says so: `no store`.
2309
2422
  hl = (CFG["agents_heading"].split("\t", 1) + [""])[0]
2310
- filt = "current" if FILTER == "current" else "all"
2311
- r_filt = (C_SEL if FILTER == "current" else C_DIM) + filt + C_DIM
2423
+ filt = FILTER
2424
+ if FILTER == "store" and STORE_SCOPE_NO_STORE == "pinned" \
2425
+ and not (cur_sess and in_sess is not None and cur_sess in in_sess):
2426
+ filt = "no store"
2427
+ r_filt = (C_SEL if FILTER != "all" else C_DIM) + filt + C_DIM
2312
2428
  r_grp = (C_SEL if GROUPED else C_DIM) + "grouped" + C_DIM
2313
2429
  rtxt = f"{filt} \u00b7 grouped "
2314
2430
  x_right = WIDTH - len(rtxt)
@@ -3686,7 +3802,8 @@ def main():
3686
3802
  if x >= x_grp + 1:
3687
3803
  GROUPED = not GROUPED
3688
3804
  elif x >= x_filt:
3689
- FILTER = "all" if FILTER == "current" else "current"
3805
+ FILTER = SCOPES[(SCOPES.index(FILTER) + 1) % len(SCOPES)
3806
+ if FILTER in SCOPES else 0]
3690
3807
  elif what == "plus-or-collapse": # the bottom control strip
3691
3808
  lw, rx = val
3692
3809
  if x <= lw + 1:
@@ -161,9 +161,10 @@ labels.
161
161
  `session start --list-agents` after. The live
162
162
  agents list sorts a running agent whose name is pinned first, in pin
163
163
  order, whatever its project, status or idle; the rest keep the `sort`
164
- order. Grouped, the pinned rows sit above every group. `hide` and the
165
- current filter still apply first. Empty by default; `["alice", "bob"]`
166
- keeps alice and bob on top.
164
+ order. Grouped, the pinned rows sit above every group. `hide` applies
165
+ first, and a pinned agent shows under every agents scope (all, store,
166
+ project). Empty by default; `["alice", "bob"]` keeps alice and bob on
167
+ top.
167
168
  - `code_nvim` - sessionview-session starts nvim in the code window, listening on
168
169
  show-me's socket; off = a shell.
169
170
  - `agent_command` - what "Start an agent" runs: an argv list, `{project}`
@@ -141,7 +141,7 @@
141
141
  ],
142
142
  "_window_pins": "Your pins, laid over the `windows` list's `auto`: {\"editor\": true} opens the editor with every session, {\"git\": false} stops git opening. Written by ^p / right-click in the new-tab picker; a delta, so the list itself (template, store or project) keeps flowing to you. EMPTY by default.",
143
143
  "window_pins": {},
144
- "_pinned_agents": "Agent names held at the TOP, in this order - ONE list for two places: pinned means pinned everywhere. (1) The start-an-agent picker lists them first; the rest of `session start --list-agents` follows. (2) The live agents list sorts a running agent whose name is pinned first, in pin order, whatever its project, status or idle; the rest keep the `sort` order below them. Grouped, the pinned rows sit ABOVE every group, not inside their project's. `hide` and the current filter still apply first: a pin orders a row, it never unhides one. [\"alice\", \"bob\"] keeps alice and bob at the top. EMPTY by default: a fresh install pins nobody. A pinned name the launcher cannot start is not offered - a list that lies is worse than no list. ^p or a right-click on a row in the start-an-agent picker toggles it.",
144
+ "_pinned_agents": "Agent names held at the TOP, in this order - ONE list for two places: pinned means pinned everywhere. (1) The start-an-agent picker lists them first; the rest of `session start --list-agents` follows. (2) The live agents list sorts a running agent whose name is pinned first, in pin order, whatever its project, status or idle; the rest keep the `sort` order below them. Grouped, the pinned rows sit ABOVE every group, not inside their project's. `hide` still applies first, so a hidden status stays hidden; a pinned agent then shows under every agents scope (all, store, project). [\"alice\", \"bob\"] keeps alice and bob at the top. EMPTY by default: a fresh install pins nobody. A pinned name the launcher cannot start is not offered - a list that lies is worse than no list. ^p or a right-click on a row in the start-an-agent picker toggles it.",
145
145
  "pinned_agents": [],
146
146
  "_mid_format": "The row between projects and agents. Left word opens a new SESSION. A right word (after a TAB) opens the project menu - there is none by default: the selected project row carries its own menu as three dots at the right. The click target is the row, not the text.",
147
147
  "mid_format": " +",
@@ -175,7 +175,7 @@
175
175
  "terminal": "",
176
176
  "_fullscreen": "FALSE by default: whether the dock app opens sessionview FULL SCREEN (macOS native full screen, the green button - not zoomed). On Terminal.app this needs Accessibility permission for SessionView.app (System Settings > Privacy & Security > Accessibility); the app says so in a dialog rather than failing quietly. iTerm and Ghostty need no permission.",
177
177
  "fullscreen": false,
178
- "_pane_menu": "FALSE by default: the agent pane menu - right-click an agent's pane for Speak / Continue / Read Clipboard / Park / Succeed, each a mail to that agent (tmux/agent-menu.conf, bin/tmux-send-agent). OPT-IN because a tmux key binding is global to the server: turning this on REPLACES your own right-click menu. When true, sessionview-frame loads it into the server at every launch, so it works whether or not tmux was already running. Without session view: `source-file -q <sessionview>/tmux/agent-menu.conf` in ~/.tmux.conf (a link or a conf.d glob works too). Turning it OFF takes effect when the tmux server restarts: nothing unbinds a running server's menu, since that would also drop the user's own.",
178
+ "_pane_menu": "FALSE by default: the agent pane menu - right-click or Option + right-click an agent's pane for Speak / Continue / Read Clipboard / Park / Succeed, each a mail to that agent (tmux/agent-menu.conf, bin/tmux-send-agent). An agent's pane is one whose title is `<glyph> <address>`. The plain right-click opens it even when the program there uses the mouse, as Claude Code does. OPT-IN because a tmux key binding is global to the server: turning this on REPLACES both your own right-click bindings (MouseDown3Pane and M-MouseDown3Pane). When true, sessionview-frame loads it into the server at every launch, so it works whether or not tmux was already running, and its own server passes an Option + right-click on your tmux through to it. Without session view: `source-file -q <sessionview>/tmux/agent-menu.conf` in ~/.tmux.conf (a link or a conf.d glob works too). Turning it OFF takes effect when the tmux server restarts: nothing unbinds a running server's menu, since that would also drop the user's own.",
179
179
  "pane_menu": false,
180
180
  "_pane_menu_from": "EMPTY by default: the OLDER name for mail_from, still honoured when mail_from is empty. Set mail_from instead.",
181
181
  "pane_menu_from": "",
@@ -81,7 +81,10 @@ The `messages` tab shows a project's mail in neomutt, read-only.
81
81
  - The tab's sidebar needs notmuch to index two headers:
82
82
  `notmuch config set index.header.Type Type` and
83
83
  `notmuch config set index.header.Project Project`, then
84
- `notmuch reindex '*'`.
84
+ `notmuch reindex '*'`. When sessionmail names its own notmuch profile
85
+ (`message path --notmuch-config`, sessionmail 0.3.3), the tab and
86
+ `sessionview-install --check` use that file, and the fix line they
87
+ print carries its `--config=`.
85
88
 
86
89
  Your own mail client, the sidebar's mail row, is set by the
87
90
  `user_mail_*` keys on the [settings page][settings].
@@ -119,9 +122,12 @@ repository.
119
122
  frame to its own sessions only.
120
123
  - `<checkout>/tmux/tabs-top.conf`: the same look for every tmux session.
121
124
  Optional: `source-file` it from `~/.tmux.conf`.
122
- - `<checkout>/tmux/agent-menu.conf`: the agent pane menu. The frame loads
123
- it when the `pane_menu` key is `true`; without the frame,
124
- `source-file -q` it from `~/.tmux.conf`.
125
+ - `<checkout>/tmux/agent-menu.conf`: the agent pane menu, on both
126
+ right-clicks (`MouseDown3Pane` and Option's `M-MouseDown3Pane`). The
127
+ frame loads it when the `pane_menu` key is `true`, and an Option +
128
+ right-click on your tmux in the frame reaches it. Without the frame,
129
+ `source-file -q` it from `~/.tmux.conf`. It needs tmux's `mouse`
130
+ option on, the default since tmux 3.8.
125
131
  - `tmux set -g @sessionview_tab_close 0`: no close `×` on the tabs.
126
132
 
127
133
  ## Paths
@@ -124,7 +124,7 @@ tabs; [config/README.md][config-readme] describes each field.
124
124
  | --- | --- | --- | --- |
125
125
  | `terminal` | string | `""` | The terminal the dock app opens: `iterm`, `ghostty` or `terminal`. `""` works it out. |
126
126
  | `fullscreen` | boolean | `false` | The dock app opens sessionview full screen. |
127
- | `pane_menu` | boolean | `false` | Right-click on an agent's pane opens the agent pane menu. It replaces your own tmux right-click menu. |
127
+ | `pane_menu` | boolean | `false` | Right-click or Option + right-click on an agent's pane opens the agent pane menu, even when the program there uses the mouse. It replaces both your tmux right-click bindings. |
128
128
  | `mail_from` | string | `""` | The name sessionview's mail is sent as. `""`: the next in line, on the [configuration page][mail-sender]. |
129
129
  | `pane_menu_from` | string | `""` | The older name for `mail_from`, read when `mail_from` is empty. |
130
130
 
@@ -0,0 +1,56 @@
1
+ # lib/pane-menu.sh - sourced by sessionview-frame, only when pane_menu is on.
2
+ #
3
+ # sv_frame_option_forward SOCK, on the frame's own tmux server SOCK: an Option
4
+ # + right-click (M-MouseDown3Pane) on a pane running a tmux client goes through
5
+ # to that client's server, where tmux/agent-menu.conf has the agent menu. Every
6
+ # other pane (the sidebar) keeps the binding SOCK already had.
7
+ #
8
+ # Without it the frame's server takes the Option-click itself: it shows its own
9
+ # stock menu for the pane that holds your tmux client, and your tmux never sees
10
+ # the click. The plain right-click needs nothing here: the frame's stock binding
11
+ # already forwards it, because a tmux client's pane has mouse mode on whenever
12
+ # your tmux has `mouse on`.
13
+ #
14
+ # How. The key's current binding is read back with list-keys and becomes the
15
+ # else-branch of one if-shell, so no copy of tmux's menu is kept here (a copy
16
+ # names flags a given tmux may not know, and one unknown flag rejects the whole
17
+ # bind). That binding is tmux's stock menu unless the user's ~/.tmux.conf
18
+ # changed it, and then theirs is kept. A key with no binding sends the click to
19
+ # the pane under the mouse, so its else-branch is `send-keys -M`, which does
20
+ # the same. A second call finds its own wrapper and changes nothing (a second
21
+ # frame on the same server).
22
+ #
23
+ # The separator step. list-keys prints a command SEQUENCE with ` \; ` between
24
+ # its commands. Inside braces that is a literal `;` argument, and the bind
25
+ # fails. So each ` \; ` becomes ` ; ` (and ` \;\; ` becomes ` ;; `). A `\;`
26
+ # inside a quoted string is printed as `\\;` and is left alone.
27
+ #
28
+ # Known limit: an argument that is exactly `;` (`send-keys ';'`) is printed as
29
+ # `\;` too, the same text as a separator. Nothing in the printed line tells the
30
+ # two apart: tmux's own round trip of that line, sourced back as a config line,
31
+ # loses the argument the same way. Then the bind fails (unknown command), the
32
+ # key keeps the user's binding, and the pass-through is not installed. A lone
33
+ # `;` as the binding's LAST word is fine.
34
+ #
35
+ # It runs "${TMUX_BIN:-tmux}" (the frame sets TMUX_BIN) and returns tmux's
36
+ # status; the frame fails open on it.
37
+
38
+ #: the pass-through, as bound: a pane running tmux gets the click
39
+ SV_FRAME_FORWARD="if-shell -F -t = '#{==:#{pane_current_command},tmux}' { select-pane -t = ; send-keys -M }"
40
+ #: how list-keys prints the start of the wrapper (double quotes)
41
+ SV_FRAME_FORWARD_SHOWN='if-shell -F -t = "#{==:#{pane_current_command},tmux}" { select-pane -t = ; send-keys -M }'
42
+
43
+ sv_frame_option_forward() {
44
+ _sv_pm_cur="$("${TMUX_BIN:-tmux}" -L "$1" list-keys -T root M-MouseDown3Pane 2>/dev/null)" || _sv_pm_cur=""
45
+ # bind-key [-r] -T root M-MouseDown3Pane COMMAND: keep -r, and COMMAND
46
+ _sv_pm_r="$(printf '%s\n' "$_sv_pm_cur" | sed -n 's/^bind-key *\(-r\) *-T root *M-MouseDown3Pane .*/\1 /p')"
47
+ _sv_pm_body="$(printf '%s\n' "$_sv_pm_cur" \
48
+ | sed -n 's/^bind-key *\(-r *\)\{0,1\}-T root *M-MouseDown3Pane *//p' \
49
+ | sed 's/ [\\];[\\]; / ;; /g; s/ [\\]; / ; /g')"
50
+ case "$_sv_pm_body" in
51
+ "$SV_FRAME_FORWARD_SHOWN"*) return 0;;
52
+ '') _sv_pm_body="send-keys -M";;
53
+ esac
54
+ # $_sv_pm_r is "-r " or empty, unquoted on purpose
55
+ "${TMUX_BIN:-tmux}" -L "$1" bind-key $_sv_pm_r-T root M-MouseDown3Pane "$SV_FRAME_FORWARD { $_sv_pm_body }"
56
+ }
@@ -56,11 +56,19 @@ if term is "iterm" then
56
56
  end tell
57
57
  end run"
58
58
  try
59
+ -- Compiling that string LOOKS iTERM UP BY NAME, and on a Mac without
60
+ -- iTerm the lookup does not fail: it puts up the "Where is iTerm?"
61
+ -- chooser and waits on it with no time limit, so the alert below was
62
+ -- never reached and the app hung. So ask first, with a check that
63
+ -- cannot prompt (itermFound: shell only, no application reference),
64
+ -- and when iTerm is not there, go straight to the alert.
65
+ if not itermFound() then error "iTerm was not found on this Mac." number -10814
59
66
  run script itermScript with parameters {cmd, wantFull}
60
67
  on error errMsg number errNum
61
68
  if errNum is -128 then error number -128
62
69
  -- e.g. `terminal: iterm` in the config on a Mac where iTerm is not
63
- -- installed: the string does not compile here. Say so; never fail mute.
70
+ -- installed (itermFound said no), or the string failing to compile or
71
+ -- run. Say so; never fail mute.
64
72
  display alert "SessionView cannot open iTerm" message "iTerm is the terminal chosen for sessionview, but it could not be run:
65
73
  " & errMsg & " (" & errNum & ")
66
74
 
@@ -242,3 +250,21 @@ on isInstalled(appName)
242
250
  end try
243
251
  end isInstalled
244
252
 
253
+ -- Is iTerm on this Mac at all? Asked before the iTerm string is compiled,
254
+ -- because compiling it without iTerm prompts instead of failing. Nothing here
255
+ -- is an AppleScript reference to the app, so nothing here can put up a
256
+ -- chooser: is it running, is it in either Applications folder, and does
257
+ -- LaunchServices know its bundle id (a copy kept anywhere else). That last is
258
+ -- asked through JavaScript for Automation in a child process, a plain lookup
259
+ -- that returns nothing when the app is missing. Spotlight (mdfind) is not
260
+ -- used: with indexing off it finds no app at all.
261
+ on itermFound()
262
+ set js to "ObjC.import('AppKit'); $.NSWorkspace.sharedWorkspace.URLForApplicationWithBundleIdentifier('com.googlecode.iterm2').isNil() ? 'no' : 'yes'"
263
+ try
264
+ do shell script "/usr/bin/pgrep -x iTerm2 >/dev/null || test -d /Applications/iTerm.app || test -d \"$HOME/Applications/iTerm.app\" || test \"$(/usr/bin/osascript -l JavaScript -e " & quoted form of js & " 2>/dev/null)\" = yes"
265
+ return true
266
+ on error
267
+ return false
268
+ end try
269
+ end itermFound
270
+
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sessionview"
7
- version = "0.3.2"
7
+ version = "0.3.3"
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"
@@ -10,7 +10,7 @@ import os
10
10
  import sys
11
11
  from pathlib import Path
12
12
 
13
- __version__ = "0.3.2"
13
+ __version__ = "0.3.3"
14
14
 
15
15
  DATA = Path(__file__).resolve().parent / "_data"
16
16
  PUBLIC = ("sessionview", "sessionview-config", "sessionview-session", "sessionview-messages")
@@ -0,0 +1,95 @@
1
+ # agent-menu.conf - the agent pane menu. Right-click an agent's pane, or
2
+ # Option + right-click it, for
3
+ # Speak / Continue / Read Clipboard / Park / Succeed -> <agent>
4
+ # above tmux's own pane menu. Each item mails that agent (bin/tmux-send-agent,
5
+ # sessionmail's `message send`). An agent pane is one whose title carries an
6
+ # agent address: `<glyph> <address>`, as `role.scope` or `name:id`. The agent
7
+ # items appear on agent panes only; other panes get the rest of the menu,
8
+ # sessionview's copy of tmux's own pane menu.
9
+ #
10
+ # Both right-clicks open the same menu:
11
+ # - plain right-click (MouseDown3Pane): on an agent pane it opens the menu
12
+ # even when the program there has mouse mode on (Claude Code does). On any
13
+ # other pane a program with mouse mode on still gets the click, as in
14
+ # tmux's default. A mode other than copy/view (choose-tree, clock-mode)
15
+ # still gets the plain click too.
16
+ # - Option + right-click (M-MouseDown3Pane): always the menu, on every pane,
17
+ # as in tmux's default.
18
+ # With session view, the frame passes an Option + right-click on the pane
19
+ # that holds your tmux through to it, so this menu opens there too.
20
+ #
21
+ # The menu is written ONCE, as SESSIONVIEW_AGENT_MENU below, and both
22
+ # bindings are built from it when this file is read: to change the menu,
23
+ # edit SESSIONVIEW_AGENT_MENU. SESSIONVIEW_AGENT_TITLE is the agent-pane
24
+ # test, shared by the plain right-click and every agent item. Both are
25
+ # removed from the server's environment at the end of the file.
26
+ #
27
+ # OPT-IN, because a key binding is global to the tmux server: loading this
28
+ # REPLACES BOTH your own right-click bindings (MouseDown3Pane and
29
+ # M-MouseDown3Pane). It needs tmux's `mouse` option on, or no right-click
30
+ # reaches tmux at all (on by default since tmux 3.8; older tmux needs
31
+ # `set -g mouse on` in ~/.tmux.conf).
32
+ # - with session view: sessionview-config set pane_menu true
33
+ # (sessionview-frame sources this at every launch)
34
+ # - tmux alone: add to ~/.tmux.conf, then reload (source-file):
35
+ # source-file -q /path/to/sessionview/tmux/agent-menu.conf
36
+ # The sender is you: config mail_from, else $USER (see bin/sessionview-from).
37
+ #
38
+ # Moved here from a personal ~/.tmux.conf, so that it ships with sessionview.
39
+ # The default items are still tmux 3.7b's `list-keys -T root MouseDown3Pane`;
40
+ # re-copy them if tmux's default changes.
41
+
42
+ # The helper sits beside this file's directory, wherever it is installed.
43
+ # First the path as sourced; then, since a conf.d LINK to this file would
44
+ # point that at confd/../bin (found in review), the
45
+ # link is resolved in the shell and the option overwritten with the real one.
46
+ set -gF @sessionview_agent_menu_conf "#{current_file}"
47
+ set -gF @sessionview_agent_menu_bin "#{d:current_file}/../bin/tmux-send-agent"
48
+ run-shell 'f="$(realpath "#{@sessionview_agent_menu_conf}" 2>/dev/null)" && [ -n "$f" ] && tmux set -g @sessionview_agent_menu_bin "$(dirname "$f")/../bin/tmux-send-agent"; true'
49
+
50
+ # An agent pane: its title carries an agent address. One definition, used by
51
+ # the plain right-click's test below and by every agent item in the menu.
52
+ %hidden SESSIONVIEW_AGENT_TITLE='^[^ ]+ ([a-z_][a-z0-9_+-]*[.][^ ]+|[a-z][a-z0-9_-]*:[^ ]+)$'
53
+
54
+ # The menu, once. Both right-click bindings below are built from it.
55
+ %hidden SESSIONVIEW_AGENT_MENU="display-menu -T '#[align=centre]#{pane_index} (#{pane_id})' -t = -x M -y M \
56
+ '#{?#{m/r:$SESSIONVIEW_AGENT_TITLE,#{pane_title}},Speak → #{s/[^A-Za-z0-9_.+-].*//:#{s/^[^ ]+ //:pane_title}},}' S { run-shell -b \"'#{@sessionview_agent_menu_bin}' #{pane_id}\" } \
57
+ '#{?#{m/r:$SESSIONVIEW_AGENT_TITLE,#{pane_title}},Continue → #{s/[^A-Za-z0-9_.+-].*//:#{s/^[^ ]+ //:pane_title}},}' C { run-shell -b \"'#{@sessionview_agent_menu_bin}' #{pane_id} continue\" } \
58
+ '#{?#{m/r:$SESSIONVIEW_AGENT_TITLE,#{pane_title}},Read Clipboard → #{s/[^A-Za-z0-9_.+-].*//:#{s/^[^ ]+ //:pane_title}},}' B { run-shell -b \"'#{@sessionview_agent_menu_bin}' #{pane_id} -- 'please read the clipboard (pbpaste) and act on it'\" } \
59
+ '#{?#{m/r:$SESSIONVIEW_AGENT_TITLE,#{pane_title}},Park → #{s/[^A-Za-z0-9_.+-].*//:#{s/^[^ ]+ //:pane_title}},}' P { run-shell -b \"'#{@sessionview_agent_menu_bin}' #{pane_id} 'please settle your context, then park'\" } \
60
+ '#{?#{m/r:$SESSIONVIEW_AGENT_TITLE,#{pane_title}},Succeed → #{s/[^A-Za-z0-9_.+-].*//:#{s/^[^ ]+ //:pane_title}},}' U { run-shell -b \"'#{@sessionview_agent_menu_bin}' #{pane_id} -- 'please settle your context, then succeed: start your successor'\" } \
61
+ '#{?#{m/r:(copy|view)-mode,#{pane_mode}},Go To Top,}' < { send-keys -X history-top } \
62
+ '#{?#{m/r:(copy|view)-mode,#{pane_mode}},Go To Bottom,}' > { send-keys -X history-bottom } \
63
+ '' \
64
+ '#{?#{&&:#{buffer_size},#{!:#{pane_in_mode}}},Paste #[underscore]#{=/9/...:buffer_sample},}' p { paste-buffer } \
65
+ '' \
66
+ '#{?mouse_word,Search For #[underscore]#{=/9/...:mouse_word},}' C-r { if-shell -F '#{?#{m/r:(copy|view)-mode,#{pane_mode}},0,1}' 'copy-mode -t=' ; send-keys -X -t = search-backward -- '#{q:mouse_word}' } \
67
+ '#{?mouse_word,Type #[underscore]#{=/9/...:mouse_word},}' C-y { copy-mode -q ; send-keys -l '#{q:mouse_word}' } \
68
+ '#{?mouse_word,Copy #[underscore]#{=/9/...:mouse_word},}' c { copy-mode -q ; set-buffer '#{q:mouse_word}' } \
69
+ '#{?mouse_line,Copy Line,}' l { copy-mode -q ; set-buffer '#{q:mouse_line}' } \
70
+ '' \
71
+ '#{?mouse_hyperlink,Type #[underscore]#{=/9/...:mouse_hyperlink},}' C-h { copy-mode -q ; send-keys -l '#{q:mouse_hyperlink}' } \
72
+ '#{?mouse_hyperlink,Copy #[underscore]#{=/9/...:mouse_hyperlink},}' h { copy-mode -q ; set-buffer '#{q:mouse_hyperlink}' } \
73
+ '' \
74
+ '#{?#{!:#{pane_floating_flag}},Horizontal Split,}' h { split-window -h } \
75
+ '#{?#{!:#{pane_floating_flag}},Vertical Split,}' v { split-window -v } \
76
+ '' \
77
+ '#{?#{&&:#{!:#{pane_floating_flag}},#{>:#{window_panes},1}},Swap Up,}' u { swap-pane -U } \
78
+ '#{?#{&&:#{!:#{pane_floating_flag}},#{>:#{window_panes},1}},Swap Down,}' d { swap-pane -D } \
79
+ '#{?pane_marked_set,,-}Swap Marked' s { swap-pane } \
80
+ '' \
81
+ Kill X { kill-pane } \
82
+ Respawn R { respawn-pane -k } \
83
+ '#{?pane_marked,Unmark,Mark}' m { select-pane -m } \
84
+ '#{?#{>:#{window_panes},1},,-}#{?window_zoomed_flag,Unzoom,Zoom}' z { resize-pane -Z }"
85
+
86
+ # Plain right-click: the program gets it when it has mouse mode on and the
87
+ # pane is NOT an agent's, or when a mode other than copy/view owns the pane;
88
+ # otherwise the menu. Each binding is ONE string, so tmux parses it when it
89
+ # binds: a broken menu fails this file loudly and leaves both keys as they were.
90
+ # Option + right-click: always the menu, as in tmux's default.
91
+ bind-key -T root MouseDown3Pane "if-shell -F -t = '#{||:#{&&:#{!:#{m/r:$SESSIONVIEW_AGENT_TITLE,#{pane_title}}},#{mouse_any_flag}},#{&&:#{pane_in_mode},#{?#{m/r:(copy|view)-mode,#{pane_mode}},0,1}}}' { select-pane -t = ; send-keys -M } { $SESSIONVIEW_AGENT_MENU }"
92
+ bind-key -T root M-MouseDown3Pane "$SESSIONVIEW_AGENT_MENU"
93
+ # Leave nothing behind in the server's environment.
94
+ set-environment -gu SESSIONVIEW_AGENT_TITLE
95
+ set-environment -gu SESSIONVIEW_AGENT_MENU
@@ -1,33 +0,0 @@
1
- # agent-menu.conf - the agent pane menu. Right-click an agent's pane for
2
- # Speak / Continue / Read Clipboard / Park / Succeed -> <agent>
3
- # above tmux's own pane menu. Each item mails that agent (bin/tmux-send-agent,
4
- # sessionmail's `message send`). Items appear only when the pane title
5
- # carries an agent address; other panes get tmux's plain menu.
6
- #
7
- # OPT-IN, because a key binding is global to the tmux server: loading this
8
- # REPLACES your own right-click (MouseDown3Pane) menu.
9
- # - with session view: sessionview-config set pane_menu true
10
- # (sessionview-frame sources this at every launch)
11
- # - tmux alone: add to ~/.tmux.conf, then reload (source-file):
12
- # source-file -q /path/to/sessionview/tmux/agent-menu.conf
13
- # The sender is you: config mail_from, else $USER (see bin/sessionview-from).
14
- #
15
- # Moved here from a personal ~/.tmux.conf, so that it ships with sessionview.
16
- # The default items are tmux 3.7b's `list-keys -T root MouseDown3Pane`; re-copy
17
- # them if tmux's default changes.
18
-
19
- # The helper sits beside this file's directory, wherever it is installed.
20
- # First the path as sourced; then, since a conf.d LINK to this file would
21
- # point that at confd/../bin (found in review), the
22
- # link is resolved in the shell and the option overwritten with the real one.
23
- set -gF @sessionview_agent_menu_conf "#{current_file}"
24
- set -gF @sessionview_agent_menu_bin "#{d:current_file}/../bin/tmux-send-agent"
25
- run-shell 'f="$(realpath "#{@sessionview_agent_menu_conf}" 2>/dev/null)" && [ -n "$f" ] && tmux set -g @sessionview_agent_menu_bin "$(dirname "$f")/../bin/tmux-send-agent"; true'
26
-
27
- bind-key -T root MouseDown3Pane if-shell -F -t = "#{||:#{mouse_any_flag},#{&&:#{pane_in_mode},#{?#{m/r:(copy|view)-mode,#{pane_mode}},0,1}}}" { select-pane -t = ; send-keys -M } { display-menu -T "#[align=centre]#{pane_index} (#{pane_id})" -t = -x M -y M \
28
- "#{?#{m/r:^[^ ]+ ([a-z_][a-z0-9_+-]*[.][^ ]+|[a-z][a-z0-9_-]*:[^ ]+)$,#{pane_title}},Speak → #{s/[^A-Za-z0-9_.+-].*//:#{s/^[^ ]+ //:pane_title}},}" S { run-shell -b "'#{@sessionview_agent_menu_bin}' #{pane_id}" } \
29
- "#{?#{m/r:^[^ ]+ ([a-z_][a-z0-9_+-]*[.][^ ]+|[a-z][a-z0-9_-]*:[^ ]+)$,#{pane_title}},Continue → #{s/[^A-Za-z0-9_.+-].*//:#{s/^[^ ]+ //:pane_title}},}" C { run-shell -b "'#{@sessionview_agent_menu_bin}' #{pane_id} continue" } \
30
- "#{?#{m/r:^[^ ]+ ([a-z_][a-z0-9_+-]*[.][^ ]+|[a-z][a-z0-9_-]*:[^ ]+)$,#{pane_title}},Read Clipboard → #{s/[^A-Za-z0-9_.+-].*//:#{s/^[^ ]+ //:pane_title}},}" B { run-shell -b "'#{@sessionview_agent_menu_bin}' #{pane_id} -- 'please read the clipboard (pbpaste) and act on it'" } \
31
- "#{?#{m/r:^[^ ]+ ([a-z_][a-z0-9_+-]*[.][^ ]+|[a-z][a-z0-9_-]*:[^ ]+)$,#{pane_title}},Park → #{s/[^A-Za-z0-9_.+-].*//:#{s/^[^ ]+ //:pane_title}},}" P { run-shell -b "'#{@sessionview_agent_menu_bin}' #{pane_id} 'please settle your context, then park'" } \
32
- "#{?#{m/r:^[^ ]+ ([a-z_][a-z0-9_+-]*[.][^ ]+|[a-z][a-z0-9_-]*:[^ ]+)$,#{pane_title}},Succeed → #{s/[^A-Za-z0-9_.+-].*//:#{s/^[^ ]+ //:pane_title}},}" U { run-shell -b "'#{@sessionview_agent_menu_bin}' #{pane_id} -- 'please settle your context, then succeed: start your successor'" } \
33
- "#{?#{m/r:(copy|view)-mode,#{pane_mode}},Go To Top,}" < { send-keys -X history-top } "#{?#{m/r:(copy|view)-mode,#{pane_mode}},Go To Bottom,}" > { send-keys -X history-bottom } '' "#{?#{&&:#{buffer_size},#{!:#{pane_in_mode}}},Paste #[underscore]#{=/9/...:buffer_sample},}" p { paste-buffer } '' "#{?mouse_word,Search For #[underscore]#{=/9/...:mouse_word},}" C-r { if-shell -F "#{?#{m/r:(copy|view)-mode,#{pane_mode}},0,1}" "copy-mode -t=" ; send-keys -X -t = search-backward -- "#{q:mouse_word}" } "#{?mouse_word,Type #[underscore]#{=/9/...:mouse_word},}" C-y { copy-mode -q ; send-keys -l "#{q:mouse_word}" } "#{?mouse_word,Copy #[underscore]#{=/9/...:mouse_word},}" c { copy-mode -q ; set-buffer "#{q:mouse_word}" } "#{?mouse_line,Copy Line,}" l { copy-mode -q ; set-buffer "#{q:mouse_line}" } '' "#{?mouse_hyperlink,Type #[underscore]#{=/9/...:mouse_hyperlink},}" C-h { copy-mode -q ; send-keys -l "#{q:mouse_hyperlink}" } "#{?mouse_hyperlink,Copy #[underscore]#{=/9/...:mouse_hyperlink},}" h { copy-mode -q ; set-buffer "#{q:mouse_hyperlink}" } '' "#{?#{!:#{pane_floating_flag}},Horizontal Split,}" h { split-window -h } "#{?#{!:#{pane_floating_flag}},Vertical Split,}" v { split-window -v } '' "#{?#{&&:#{!:#{pane_floating_flag}},#{>:#{window_panes},1}},Swap Up,}" u { swap-pane -U } "#{?#{&&:#{!:#{pane_floating_flag}},#{>:#{window_panes},1}},Swap Down,}" d { swap-pane -D } "#{?pane_marked_set,,-}Swap Marked" s { swap-pane } '' Kill X { kill-pane } Respawn R { respawn-pane -k } "#{?pane_marked,Unmark,Mark}" m { select-pane -m } "#{?#{>:#{window_panes},1},,-}#{?window_zoomed_flag,Unzoom,Zoom}" z { resize-pane -Z } }
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