Extension Details

- Tmux Session
- by gingerbeardman
- 9 Recent Installs | 9 Total Installs
- Share a tmux session between Nova's terminal and an external terminal
- Repository
- Bug Reports
-
Read Files
-
Launch Subprocesses
-
Read & Write Clipboard
-
This extension is allowed to:
Readme
Tmux Session
Use tmux as Nova's terminal shell, and open any of its sessions in an external terminal: Ghostty, iTerm, kitty or Terminal.
Set Nova's terminal shell to the script below. Each tab rejoins the detached tmux session for that project, or starts one when every session there is already open. Quit tmux and the tab closes. Close the tab instead and the session keeps running, so you can join it again from Nova or your external terminal.
tmux also provides split panes, windows and searchable scrollback inside Nova's terminal.
Setup
- Install tmux:
brew install tmux - Save the shell script below as
~/tmux.shand runchmod +x ~/tmux.sh. - In Nova's Settings > Terminal, set the shell to the script's absolute path. Nova does not expand
~here, so~/tmux.shdoes not work. Use the full path, such as/Users/matt/tmux.sh. - Open a new terminal tab in Nova. You'll see tmux's status bar along the bottom.
Nova starts that program itself whenever it opens or restores a terminal tab. The extension is not involved in that launch, so it cannot attach a tab to a session. The script is what rejoins.
If you used the ~/.zshrc snippet from version 1.x, remove it.
Shell script
The script finds tmux with which after adding Homebrew to PATH. Nova's own PATH is only /usr/bin:/bin:/usr/sbin:/sbin, so which would otherwise miss /opt/homebrew/bin/tmux. A new tab rejoins the detached session last used in that folder. A new session is created only when every session for the folder is already showing. ~/tmux.sh cleanup ends the sessions nothing is attached to. End Unattached Sessions… does that from Nova.
#!/bin/sh
# Nova's terminal shell. Rejoin the detached tmux session for this directory,
# or create one when every session here is already attached.
#
# Nova launches its shell with PATH=/usr/bin:/bin:/usr/sbin:/sbin, so the
# directories where tmux actually lives have to be added before `which`.
export PATH="/opt/homebrew/bin:/usr/local/bin:/opt/local/bin:$PATH"
tmux=$(which tmux 2>/dev/null)
if [ -z "$tmux" ] || [ ! -x "$tmux" ]; then
printf '%s\n' "tmux was not found." >&2
if [ $# -eq 0 ]; then
exec /bin/zsh -l
fi
exit 1
fi
lock=nova-shell-claim
# Without a UTF-8 locale tmux prints each tab in list-sessions output as "_",
# which breaks the tab-separated parsing below. -u makes tmux use UTF-8 anyway.
release() {
"$tmux" wait-for -U "$lock" 2>/dev/null || true
}
# End every session nothing is attached to. A session claimed in the last few
# seconds is left alone: a Nova tab has picked it and is about to join it.
cleanup_unattached() {
now=$(date +%s)
"$tmux" wait-for -L "$lock"
trap release EXIT
sessions=$(
"$tmux" -u list-sessions -F '#{session_attached} #{session_name} #{session_path} #{@nova_claim}' 2>/dev/null || true
)
if [ -z "$sessions" ]; then
release
trap - EXIT
printf '%s\n' "No tmux sessions are running."
exit 0
fi
# A temp file, not a $(...) capture: a case pattern's ")" would close
# the capture early.
report=$(mktemp "${TMPDIR:-/tmp}/tmux-cleanup.XXXXXX")
printf '%s\n' "$sessions" | while IFS=$(printf '\t') read -r attached name path claim; do
[ -n "$name" ] || continue
if [ "$attached" != 0 ]; then
continue
fi
if [ -n "$claim" ]; then
case $claim in
*[!0-9]*) ;;
*)
if [ $((now - claim)) -lt 15 ]; then
printf 'Left %s (%s), a tab is about to join it.\n' "$name" "$path"
continue
fi
;;
esac
fi
if "$tmux" kill-session -t "=$name:"; then
printf 'Ended %s (%s)\n' "$name" "$path"
fi
done > "$report"
release
trap - EXIT
if [ -s "$report" ]; then
cat "$report"
else
printf '%s\n' "No unattached sessions."
fi
rm -f "$report"
exit 0
}
case ${1:-} in
"")
;;
cleanup | cleanup-unattached)
cleanup_unattached
;;
*)
printf '%s\n' \
"Usage: tmux.sh [cleanup]" \
" (no arguments) rejoin or create a session for this directory" \
" cleanup end every tmux session that nothing is attached to" >&2
exit 1
;;
esac
# pwd -P resolves /tmp to /private/tmp. tmux keeps whichever path the tab was
# started with, so treat those two as the same directory.
dir=$(pwd -P)
now=$(date +%s)
trap release EXIT
"$tmux" wait-for -L "$lock"
# Skip a session claimed in the last few seconds: another tab has picked it
# and is about to attach. An older claim belongs to a tab that already quit.
target=$(
"$tmux" -u list-sessions -F '#{session_last_attached} #{session_attached} #{session_name} #{session_path} #{@nova_claim}' 2>/dev/null |
awk -F '\t' -v p="$dir" -v now="$now" '
function norm(path) {
sub("^/private/tmp", "/tmp", path)
sub("^/private/var", "/var", path)
return path
}
$2 == 0 && norm($4) == norm(p) {
if ($5 != "" && now - ($5 + 0) < 15) next
print $1 "\t" $3
}
' | sort -nr | head -1 | cut -f 2
)
if [ -n "$target" ]; then
"$tmux" set-option -t "=$target:" @nova_claim "$now"
else
target=$("$tmux" new-session -d -P -F '#{session_name}' -c "$dir")
target=$(printf '%s' "$target" | tr -d '[:space:]')
if [ -z "$target" ]; then
release
trap - EXIT
exec "$tmux"
fi
"$tmux" set-option -t "=$target:" @nova_claim "$now"
fi
release
trap - EXIT
# The trailing colon makes the target a session. set-option does not treat a
# leading "=" as an exact name unless that colon is there.
exec "$tmux" attach-session -t "=$target" ";" set-option -u -t "=$target:" @nova_claim
Keep sessions after quitting Nova
If the tmux server is started by Nova's first terminal tab, it belongs to Nova and its sessions can end when Nova quits or crashes. To keep them around to reattach to later, start the server outside Nova with a launchd agent. Then if Nova crashes, none of your sessions are lost: everything running in them carries on, and you can rejoin them when Nova is back.
Run Copy launchd Agent to copy the agent with your tmux path and locale filled in, or copy it from here. Save it as ~/Library/LaunchAgents/local.tmux.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>local.tmux</string>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/bin/tmux</string>
<string>-D</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
<key>LANG</key>
<string>en_US.UTF-8</string>
</dict>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
</dict>
</plist>
Then load it with launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/local.tmux.plist. The server starts now and at every login, and restarts if it stops. Set LANG to your own locale.
Recommended tmux settings
These make tmux behave more like Nova's own terminal: scroll and select with the mouse, copy to the macOS clipboard, and page back through output. Run Copy Recommended tmux Settings to copy them, or copy them from here. Add them to ~/.tmux.conf, then run tmux source-file ~/.tmux.conf or open a new tab:
# More scrollback
set -g history-limit 50000
# Mouse control: drag to select, copied to the macOS clipboard on release
set -g mouse on
bind -T copy-mode MouseDragEnd1Pane send -X copy-pipe-and-cancel
bind -T copy-mode-vi MouseDragEnd1Pane send -X copy-pipe-and-cancel
# A plain click in copy mode clears the selection rather than starting one
bind -T copy-mode MouseDown1Pane select-pane \; send -X clear-selection
bind -T copy-mode-vi MouseDown1Pane select-pane \; send -X clear-selection
# Clicking away from Nova mid-drag can leave tmux extending a selection with
# every later mouse move. Clear it whenever the terminal loses or gains focus.
set -g focus-events on
set-hook -g client-focus-out 'if -F "#{pane_in_mode}" "send -X clear-selection"'
set-hook -g client-focus-in 'if -F "#{pane_in_mode}" "send -X clear-selection"'
# Send tmux copies to the macOS clipboard (tmux 3.2+)
set -s copy-command 'pbcopy'
set -g set-clipboard on
set -as terminal-features ',*:clipboard'
# PageUp from a prompt scrolls back; scrolling to the bottom returns to the prompt
bind -n PageUp copy-mode -eu
Commands
These commands are available under Extensions > Tmux Session and in the Command Palette.
| Command | What it does |
|---|---|
| Open in External Terminal | Opens one of this project's sessions in your terminal app |
| Copy Join Command | Copies a command you can paste into another terminal to join one of this project's sessions |
| Show All Sessions… | Lists every running session, so you can open, copy, rename or end one |
| Rename Session… | Gives one of this project's sessions a new name |
| End Session… | Ends one of this project's sessions and everything running in it |
| End Unattached Sessions… | Ends every session nothing is attached to, after asking |
| Copy Recommended tmux Settings | Copies the settings from Recommended tmux settings |
| Copy launchd Agent | Copies the agent from Keep sessions after quitting Nova, with your tmux path and locale |
Settings
In Extensions > Extension Library > Tmux Session > Settings:
- External Terminal: the app used by Open in External Terminal. Ghostty by default.
- tmux Path: only needed if tmux isn't found automatically.
A project's sessions are the ones started in its folder. When there are several, the commands ask which one to use. When there are none, Open in External Terminal and Copy Join Command start a new session named after the project folder.
Tips
- tmux's default shortcut prefix is Ctrl+B. Nova reserves Cmd for its own shortcuts.
- When Nova and another terminal show the same window, tmux sizes it to whichever one you typed in last.
Troubleshooting
Open in External Terminal does nothing. Check that your terminal app is enabled under System Settings > Privacy & Security > Automation > Nova. For error details, open Extensions > Show Extension Console.
Ghostty shows an AppleScript error. Update Ghostty to version 1.3 or later, or choose another terminal in Settings.
"tmux was not found." Set tmux Path in Settings.
New Nova tabs don't start tmux. Check the shell in Nova's Settings > Terminal. It must be the absolute path of the script, such as /Users/matt/tmux.sh, then open a new tab. It won't affect tabs that are already open.
Old sessions pile up. Closing a Nova tab leaves its session running. Use End Unattached Sessions… to stop the ones nothing is showing, Show All Sessions… to rejoin or end one, or quit tmux in the tab instead of closing it.
Part of a Nova tab is filled with dots. Another terminal is showing the same window at a different size. Type in the Nova tab and it resizes to fit.
A Nova tab fills with random text. Image output from apps running in Ghostty or kitty can appear as garbled text in Nova, which doesn't support terminal images. Run tmux refresh-client to redraw the tab.
Requirements
The first time you open a session in Ghostty, iTerm or Terminal, macOS asks whether Nova may control that app. kitty doesn't need this permission.
Release Notes
2.0.x
add:
- Copy Recommended tmux Settings and Copy launchd Agent commands
- End Unattached Sessions command
- README shell script that rejoins a project's detached session when Nova opens a tab
- README section on starting the tmux server with launchd so sessions survive quitting Nova
- README section with recommended tmux settings for Nova (mouse selection, clipboard, scrollback)
change:
- designed for tmux as Nova's terminal shell, so the ~/.zshrc snippet is no longer needed
- a project's sessions are found by the folder they started in, and commands ask which one to use when there are several
- session pickers list free sessions first and mark the ones already open with “IN USE”
fix:
- Rename Session, End Session and other commands could say a project had no sessions when it did
1.0.x
- Initial release