Skip to content

Commit 17d1b02

Browse files
authored
Merge pull request #123 from darkroomengineering/port/fish-shell-integration
feat: add fish shell integration
2 parents 75e16c2 + 306fe04 commit 17d1b02

3 files changed

Lines changed: 702 additions & 1 deletion

File tree

Lines changed: 396 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,396 @@
1+
# programa shell integration for fish
2+
# Injected automatically — do not source manually
3+
4+
set -l _cmux_integration_enabled 1
5+
if set -q PROGRAMA_SHELL_INTEGRATION; and test "$PROGRAMA_SHELL_INTEGRATION" = 0
6+
set _cmux_integration_enabled 0
7+
end
8+
9+
function _cmux_restore_scrollback_once
10+
set -l path "$PROGRAMA_RESTORE_SCROLLBACK_FILE"
11+
test -n "$path"; or return 0
12+
set -e PROGRAMA_RESTORE_SCROLLBACK_FILE
13+
14+
if test -r "$path"
15+
/bin/cat -- "$path" 2>/dev/null
16+
/bin/rm -f -- "$path" >/dev/null 2>&1
17+
end
18+
end
19+
_cmux_restore_scrollback_once
20+
21+
if test "$_cmux_integration_enabled" != 0
22+
set -g _CMUX_SEND_TOOL ""
23+
if command -sq ncat
24+
set -g _CMUX_SEND_TOOL ncat
25+
else if command -sq socat
26+
set -g _CMUX_SEND_TOOL socat
27+
else if command -sq nc
28+
set -g _CMUX_SEND_TOOL nc
29+
end
30+
31+
set -g _PROGRAMA_SHELL_ACTIVITY_LAST ""
32+
set -g _PROGRAMA_PORTS_LAST_RUN 0
33+
set -g _PROGRAMA_TTY_NAME ""
34+
set -g _PROGRAMA_TTY_REPORTED 0
35+
set -g _PROGRAMA_TMUX_PUSH_SIGNATURE ""
36+
set -g _PROGRAMA_TMUX_PULL_SIGNATURE ""
37+
set -g _PROGRAMA_TMUX_SYNC_KEYS \
38+
PROGRAMA_BUNDLED_CLI_PATH \
39+
PROGRAMA_BUNDLE_ID \
40+
CMUXD_UNIX_PATH \
41+
CMUXTERM_REPO_ROOT \
42+
PROGRAMA_DEBUG_LOG \
43+
PROGRAMA_PORT \
44+
PROGRAMA_PORT_END \
45+
PROGRAMA_PORT_RANGE \
46+
PROGRAMA_REMOTE_DAEMON_ALLOW_LOCAL_BUILD \
47+
PROGRAMA_SHELL_INTEGRATION \
48+
PROGRAMA_SHELL_INTEGRATION_DIR \
49+
PROGRAMA_SOCKET_ENABLE \
50+
PROGRAMA_SOCKET_MODE \
51+
PROGRAMA_SOCKET_PATH \
52+
PROGRAMA_TAB_ID \
53+
PROGRAMA_TAG \
54+
PROGRAMA_WORKSPACE_ID
55+
set -g _PROGRAMA_TMUX_SURFACE_SCOPED_KEYS PROGRAMA_PANEL_ID PROGRAMA_SURFACE_ID
56+
57+
function _cmux_now
58+
if set -q EPOCHSECONDS
59+
printf '%s\n' "$EPOCHSECONDS"
60+
else
61+
date +%s
62+
end
63+
end
64+
65+
function _cmux_socket_is_unix
66+
test -n "$PROGRAMA_SOCKET_PATH"; and test -S "$PROGRAMA_SOCKET_PATH"
67+
end
68+
69+
function _cmux_relay_cli_path
70+
if test -n "$PROGRAMA_BUNDLED_CLI_PATH"; and test -x "$PROGRAMA_BUNDLED_CLI_PATH"
71+
printf '%s\n' "$PROGRAMA_BUNDLED_CLI_PATH"
72+
return 0
73+
end
74+
# Rebranded CLI binary ships as "programa"; fall back to the pre-rebrand
75+
# "cmux" name for older installs that only symlinked that binary.
76+
command -v programa 2>/dev/null; or command -v cmux 2>/dev/null
77+
end
78+
79+
function _cmux_socket_uses_remote_relay
80+
test -n "$PROGRAMA_SOCKET_PATH"; or return 1
81+
string match -q '/*' -- "$PROGRAMA_SOCKET_PATH"; and return 1
82+
string match -q '*:*' -- "$PROGRAMA_SOCKET_PATH"; or return 1
83+
set -l relay_cli (_cmux_relay_cli_path)
84+
test -n "$relay_cli"
85+
end
86+
87+
function _cmux_has_port_scan_transport
88+
_cmux_socket_is_unix; and return 0
89+
_cmux_socket_uses_remote_relay
90+
end
91+
92+
function _cmux_send --argument-names payload
93+
test -n "$payload"; or return 0
94+
test -n "$PROGRAMA_SOCKET_PATH"; or return 0
95+
switch "$_CMUX_SEND_TOOL"
96+
case ncat
97+
printf '%s\n' "$payload" | ncat -w 1 -U "$PROGRAMA_SOCKET_PATH" --send-only >/dev/null 2>&1
98+
case socat
99+
printf '%s\n' "$payload" | socat -T 1 - "UNIX-CONNECT:$PROGRAMA_SOCKET_PATH" >/dev/null 2>&1
100+
case nc
101+
printf '%s\n' "$payload" | nc -N -U "$PROGRAMA_SOCKET_PATH" >/dev/null 2>&1; or printf '%s\n' "$payload" | nc -w 1 -U "$PROGRAMA_SOCKET_PATH" >/dev/null 2>&1
102+
end
103+
end
104+
105+
function _cmux_send_bg --argument-names payload
106+
_cmux_send "$payload" >/dev/null 2>&1 &
107+
end
108+
109+
function _cmux_json_escape --argument-names value
110+
set -l backslash "\\"
111+
set -l escaped_backslash "\\\\"
112+
set -l quote '"'
113+
set -l escaped_quote '\"'
114+
string replace -a "$backslash" "$escaped_backslash" -- "$value" \
115+
| string replace -a "$quote" "$escaped_quote" \
116+
| string replace -a (printf '\n') "\\n" \
117+
| string replace -a (printf '\r') "\\r" \
118+
| string replace -a (printf '\t') "\\t"
119+
end
120+
121+
# Build a single-line v2 JSON-RPC request frame for the direct-socket
122+
# (fire-and-forget) path. `params_json` must already be a well-formed JSON
123+
# object string (call sites use _cmux_json_escape on any user-controlled
124+
# values before interpolating them).
125+
function _cmux_json_rpc_frame --argument-names method params_json
126+
printf '%s\n' "{\"id\":1,\"method\":\"$method\",\"params\":$params_json}"
127+
end
128+
129+
function _cmux_relay_workspace_id
130+
if test -n "$PROGRAMA_WORKSPACE_ID"
131+
printf '%s\n' "$PROGRAMA_WORKSPACE_ID"
132+
return 0
133+
end
134+
test -n "$PROGRAMA_TAB_ID"; or return 1
135+
printf '%s\n' "$PROGRAMA_TAB_ID"
136+
end
137+
138+
function _cmux_relay_rpc_bg --argument-names method params
139+
_cmux_socket_uses_remote_relay; or return 1
140+
set -l relay_cli (_cmux_relay_cli_path)
141+
test -n "$relay_cli"; or return 1
142+
"$relay_cli" rpc "$method" "$params" >/dev/null 2>&1 &
143+
end
144+
145+
function _cmux_relay_rpc --argument-names method params
146+
_cmux_socket_uses_remote_relay; or return 1
147+
set -l relay_cli (_cmux_relay_cli_path)
148+
test -n "$relay_cli"; or return 1
149+
# Relay `programa rpc` exits nonzero on server error. The real remote CLI
150+
# prints only the JSON result payload on success, while some test stubs
151+
# return the full `{"ok":...}` envelope. Retry only on explicit `ok:false`.
152+
set -l response ("$relay_cli" rpc "$method" "$params" 2>/dev/null | string collect)
153+
test -n "$response"; or return 0
154+
string match -q '*"ok":false*' -- "$response"; and return 1
155+
string match -q '*"ok": false*' -- "$response"; and return 1
156+
return 0
157+
end
158+
159+
function _cmux_report_tty_via_relay
160+
_cmux_socket_uses_remote_relay; or return 1
161+
set -l workspace_id (_cmux_relay_workspace_id); or return 1
162+
test -n "$_PROGRAMA_TTY_NAME"; or return 1
163+
set -l tty_name_json (_cmux_json_escape "$_PROGRAMA_TTY_NAME")
164+
set -l params "{\"workspace_id\":\"$workspace_id\",\"tty_name\":\"$tty_name_json\""
165+
if test -n "$PROGRAMA_PANEL_ID"
166+
set params "$params,\"surface_id\":\"$PROGRAMA_PANEL_ID\""
167+
end
168+
set params "$params}"
169+
_cmux_relay_rpc "surface.report_tty" "$params"
170+
end
171+
172+
function _cmux_report_tty_payload
173+
test -n "$PROGRAMA_TAB_ID"; or return 1
174+
test -n "$_PROGRAMA_TTY_NAME"; or return 1
175+
set -l workspace_id (_cmux_relay_workspace_id)
176+
test -n "$workspace_id"; or set workspace_id "$PROGRAMA_TAB_ID"
177+
set -l tty_name_json (_cmux_json_escape "$_PROGRAMA_TTY_NAME")
178+
set -l params "{\"workspace_id\":\"$workspace_id\",\"tty_name\":\"$tty_name_json\""
179+
if test -z "$TMUX"
180+
test -n "$PROGRAMA_PANEL_ID"; or return 1
181+
set params "$params,\"surface_id\":\"$PROGRAMA_PANEL_ID\""
182+
end
183+
set params "$params}"
184+
_cmux_json_rpc_frame "surface.report_tty" "$params"
185+
end
186+
187+
function _cmux_report_tty_once
188+
# Send the TTY name to the app once per session so the batched port
189+
# scanner knows which TTY belongs to this panel.
190+
test "$_PROGRAMA_TTY_REPORTED" = 1; and return 0
191+
_cmux_has_port_scan_transport; or return 0
192+
193+
if _cmux_socket_is_unix
194+
set -l payload (_cmux_report_tty_payload)
195+
test -n "$payload"; or return 0
196+
set -g _PROGRAMA_TTY_REPORTED 1
197+
_cmux_send_bg "$payload"
198+
else
199+
test -n "$_PROGRAMA_TTY_NAME"; or return 0
200+
# Keep the first relay TTY report synchronous so the server can
201+
# resolve the target surface before command-start kicks begin.
202+
_cmux_report_tty_via_relay; or return 0
203+
set -g _PROGRAMA_TTY_REPORTED 1
204+
end
205+
end
206+
207+
function _cmux_report_shell_activity_state --argument-names state
208+
test -n "$state"; or return 0
209+
_cmux_socket_is_unix; or return 0
210+
test -n "$PROGRAMA_TAB_ID"; or return 0
211+
test -n "$PROGRAMA_PANEL_ID"; or return 0
212+
test "$_PROGRAMA_SHELL_ACTIVITY_LAST" = "$state"; and return 0
213+
set -g _PROGRAMA_SHELL_ACTIVITY_LAST "$state"
214+
set -l workspace_id (_cmux_relay_workspace_id)
215+
test -n "$workspace_id"; or set workspace_id "$PROGRAMA_TAB_ID"
216+
set -l state_json (_cmux_json_escape "$state")
217+
set -l params "{\"workspace_id\":\"$workspace_id\",\"surface_id\":\"$PROGRAMA_PANEL_ID\",\"state\":\"$state_json\"}"
218+
_cmux_send_bg (_cmux_json_rpc_frame "surface.report_shell_state" "$params")
219+
end
220+
221+
function _cmux_ports_kick_via_relay --argument-names reason
222+
_cmux_socket_uses_remote_relay; or return 1
223+
set -l workspace_id (_cmux_relay_workspace_id); or return 1
224+
test -n "$reason"; or set reason command
225+
set -l params "{\"workspace_id\":\"$workspace_id\",\"reason\":\"$reason\""
226+
if test -n "$PROGRAMA_PANEL_ID"
227+
set params "$params,\"surface_id\":\"$PROGRAMA_PANEL_ID\""
228+
end
229+
set params "$params}"
230+
_cmux_relay_rpc_bg "surface.ports_kick" "$params"
231+
end
232+
233+
function _cmux_ports_kick --argument-names reason
234+
test -n "$reason"; or set reason command
235+
# Lightweight: just tell the app to run a batched scan for this panel.
236+
# The app coalesces kicks across all panels and runs a single ps+lsof.
237+
_cmux_has_port_scan_transport; or return 0
238+
test -n "$PROGRAMA_TAB_ID"; or return 0
239+
if _cmux_socket_is_unix
240+
test -n "$PROGRAMA_PANEL_ID"; or return 0
241+
end
242+
set -g _PROGRAMA_PORTS_LAST_RUN (_cmux_now)
243+
if _cmux_socket_is_unix
244+
set -l workspace_id (_cmux_relay_workspace_id)
245+
test -n "$workspace_id"; or set workspace_id "$PROGRAMA_TAB_ID"
246+
set -l reason_json (_cmux_json_escape "$reason")
247+
set -l params "{\"workspace_id\":\"$workspace_id\",\"surface_id\":\"$PROGRAMA_PANEL_ID\",\"reason\":\"$reason_json\"}"
248+
_cmux_send_bg (_cmux_json_rpc_frame "surface.ports_kick" "$params")
249+
else
250+
_cmux_ports_kick_via_relay "$reason"
251+
end
252+
end
253+
254+
function _cmux_reset_terminal_keyboard_protocols
255+
isatty stdout; or test -n "$PROGRAMA_TEST_FORCE_KEYBOARD_RESET$PROGRAMA_TEST_FORCE_KITTY_RESET"; or return 0
256+
printf '\033[>m\033[<8u'
257+
end
258+
259+
function _cmux_tmux_sync_key_is_managed --argument-names candidate
260+
contains -- "$candidate" $_PROGRAMA_TMUX_SYNC_KEYS
261+
end
262+
263+
function _cmux_tmux_shell_env_signature
264+
set -l parts
265+
for key in $_PROGRAMA_TMUX_SYNC_KEYS
266+
set -l value $$key
267+
test -n "$value"; or continue
268+
set -a parts "$key=$value"
269+
end
270+
string join \x1f -- $parts
271+
end
272+
273+
function _cmux_tmux_publish_cmux_environment
274+
test -z "$TMUX"; or return 0
275+
command -sq tmux; or return 0
276+
277+
set -l signature (_cmux_tmux_shell_env_signature)
278+
test -n "$signature"; or return 0
279+
test "$signature" != "$_PROGRAMA_TMUX_PUSH_SIGNATURE"; or return 0
280+
281+
for key in $_PROGRAMA_TMUX_SYNC_KEYS
282+
set -l value $$key
283+
test -n "$value"; or continue
284+
tmux set-environment -g "$key" "$value" >/dev/null 2>&1; or return 0
285+
end
286+
for key in $_PROGRAMA_TMUX_SURFACE_SCOPED_KEYS
287+
tmux set-environment -gu "$key" >/dev/null 2>&1; or return 0
288+
end
289+
290+
set -g _PROGRAMA_TMUX_PUSH_SIGNATURE "$signature"
291+
end
292+
293+
function _cmux_tmux_refresh_cmux_environment
294+
test -n "$TMUX"; or return 0
295+
command -sq tmux; or return 0
296+
297+
set -l output (tmux show-environment -g 2>/dev/null)
298+
test -n "$output"; or return 0
299+
300+
set -l filtered
301+
for line in $output
302+
string match -q 'PROGRAMA_*' -- "$line"; or continue
303+
set -l key (string split -m 1 = -- "$line")[1]
304+
_cmux_tmux_sync_key_is_managed "$key"; or continue
305+
set -a filtered "$line"
306+
end
307+
test -n "$filtered"; or return 0
308+
set -l joined (string join \n -- $filtered)
309+
test "$joined" != "$_PROGRAMA_TMUX_PULL_SIGNATURE"; or return 0
310+
311+
set -l did_change 0
312+
for line in $filtered
313+
set -l parts (string split -m 1 = -- "$line")
314+
set -l key $parts[1]
315+
_cmux_tmux_sync_key_is_managed "$key"; or continue
316+
set -l value $parts[2]
317+
if test "$$key" != "$value"
318+
set -gx $key "$value"
319+
set did_change 1
320+
end
321+
end
322+
323+
set -g _PROGRAMA_TMUX_PULL_SIGNATURE "$joined"
324+
if test "$did_change" = 1
325+
set -g _PROGRAMA_TTY_REPORTED 0
326+
set -g _PROGRAMA_SHELL_ACTIVITY_LAST ""
327+
end
328+
end
329+
330+
function _cmux_tmux_sync_cmux_environment
331+
if test -n "$TMUX"
332+
_cmux_tmux_refresh_cmux_environment
333+
else
334+
_cmux_tmux_publish_cmux_environment
335+
end
336+
end
337+
338+
function _cmux_preexec --on-event fish_preexec
339+
_cmux_tmux_sync_cmux_environment
340+
_cmux_report_tty_once
341+
_cmux_report_shell_activity_state running
342+
_cmux_ports_kick command
343+
end
344+
345+
function _cmux_prompt --on-event fish_prompt
346+
_cmux_reset_terminal_keyboard_protocols
347+
_cmux_tmux_sync_cmux_environment
348+
_cmux_report_tty_once
349+
_cmux_report_shell_activity_state prompt
350+
set -l now (_cmux_now)
351+
if test (math "$now - $_PROGRAMA_PORTS_LAST_RUN") -ge 5
352+
_cmux_ports_kick refresh
353+
end
354+
end
355+
end
356+
357+
# --- User config chain-load -------------------------------------------------
358+
# Our bootstrap points fish at this file via `--init-command 'source ...'` on a
359+
# plain `fish -il` invocation, so fish's own normal login-shell startup already
360+
# ran the user's real ~/.config/fish/config.fish, functions/, completions/, and
361+
# conf.d/*.fish *before* this file was sourced (Swift sets
362+
# PROGRAMA_FISH_USER_CONFIG_ALREADY_LOADED=1 for that path — see
363+
# GhosttyTerminalView.swift). This block only fires when that flag is absent,
364+
# e.g. a future remote-relay bootstrap that overrides HOME/XDG_CONFIG_HOME
365+
# before fish's normal startup can find the real user config.
366+
set -l _cmux_user_config_home ""
367+
if set -q PROGRAMA_FISH_CONFIG_HOME
368+
set _cmux_user_config_home "$PROGRAMA_FISH_CONFIG_HOME"
369+
else if set -q HOME
370+
set _cmux_user_config_home "$HOME/.config"
371+
end
372+
373+
set -l _cmux_user_config "$_cmux_user_config_home/fish/config.fish"
374+
if not set -q PROGRAMA_FISH_USER_CONFIG_ALREADY_LOADED; and test -n "$_cmux_user_config_home"; and test "$_cmux_user_config_home" != "$XDG_CONFIG_HOME"
375+
set -gx XDG_CONFIG_HOME "$_cmux_user_config_home"
376+
377+
set -l _cmux_user_functions "$_cmux_user_config_home/fish/functions"
378+
if test -d "$_cmux_user_functions"; and not contains -- "$_cmux_user_functions" $fish_function_path
379+
set -g fish_function_path "$_cmux_user_functions" $fish_function_path
380+
end
381+
382+
set -l _cmux_user_completions "$_cmux_user_config_home/fish/completions"
383+
if test -d "$_cmux_user_completions"; and not contains -- "$_cmux_user_completions" $fish_complete_path
384+
set -g fish_complete_path "$_cmux_user_completions" $fish_complete_path
385+
end
386+
387+
for _cmux_user_conf in "$_cmux_user_config_home"/fish/conf.d/*.fish
388+
if test -r "$_cmux_user_conf"
389+
source "$_cmux_user_conf"
390+
end
391+
end
392+
393+
if test -r "$_cmux_user_config"
394+
source "$_cmux_user_config"
395+
end
396+
end

0 commit comments

Comments
 (0)