@@ -765,6 +765,174 @@ const writePtyInput = (pty: PtyBridge | null, data: string): void => {
765765
766766const shellQuote = ( value : string ) : string => `'${ value . replace ( / ' / gu, "'\\''" ) } '`
767767
768+ // CHANGE: Predicate for when tmux should forward right-click pane events.
769+ // WHY: Mouse-aware apps and copy/view mode still need pane mouse events, while tmux menus must stay disabled.
770+ // QUOTE(TZ): issue #340 right-click must not open the default tmux menu in browser terminals.
771+ // REF: PR #342 tmux right-click handling.
772+ // SOURCE: n/a
773+ // FORMAT THEOREM: mouse-aware-or-copy-mode => predicate evaluates truthy in tmux.
774+ // PURITY: CORE
775+ // EFFECT: none
776+ // INVARIANT: The predicate contains only tmux format language and no shell interpolation.
777+ // COMPLEXITY: O(1) time/O(1) space.
778+ /**
779+ * Tmux format predicate used by right-click pane bindings.
780+ *
781+ * @returns A tmux format expression, not a shell command.
782+ * @pure true
783+ * @effect none
784+ * @invariant Expression is constant and contains no user-controlled input.
785+ * @precondition tmux understands mouse_any_flag and pane mode format variables.
786+ * @postcondition The value is safe to embed after shellQuote.
787+ * @complexity O(1) time/O(1) space.
788+ * @throws Never
789+ */
790+ const tmuxRightClickForwardPredicate =
791+ "#{||:#{mouse_any_flag},#{&&:#{pane_in_mode},#{?#{m/r:(copy|view)-mode,#{pane_mode}},0,1}}}"
792+ // CHANGE: Pane right-click bindings that are overridden at tmux startup.
793+ // WHY: These cover down/drag/up/end and Meta-modified events that previously reached display-menu.
794+ // QUOTE(TZ): issue #340 right-click must not open the default tmux menu in browser terminals.
795+ // REF: PR #342 tmux right-click handling.
796+ // SOURCE: n/a
797+ // FORMAT THEOREM: every binding in the array is mapped to renderTmuxPaneRightClickBinding.
798+ // PURITY: CORE
799+ // EFFECT: none
800+ // INVARIANT: Each entry is a static tmux root-table mouse binding name.
801+ // COMPLEXITY: O(1) time/O(1) space.
802+ /**
803+ * Tmux pane right-click binding names that should conditionally forward mouse events.
804+ *
805+ * @pure true
806+ * @effect none
807+ * @invariant The array contains only static tmux binding identifiers.
808+ * @precondition tmux root key table supports these binding names.
809+ * @postcondition Consumers can map each entry to a shell-safe bind-key command.
810+ * @complexity O(1) time/O(1) space.
811+ * @throws Never
812+ */
813+ const tmuxRightClickPaneBindings : ReadonlyArray < string > = [
814+ "MouseDown3Pane" ,
815+ "MouseDrag3Pane" ,
816+ "MouseDragEnd3Pane" ,
817+ "MouseUp3Pane" ,
818+ "M-MouseDown3Pane" ,
819+ "M-MouseDrag3Pane" ,
820+ "M-MouseDragEnd3Pane" ,
821+ "M-MouseUp3Pane"
822+ ]
823+ // CHANGE: Non-pane right-click bindings that are suppressed at tmux startup.
824+ // WHY: Status and border right-clicks are the tmux menu entry points that cannot be forwarded to pane apps.
825+ // QUOTE(TZ): issue #340 right-click must not open the default tmux menu in browser terminals.
826+ // REF: PR #342 tmux right-click handling.
827+ // SOURCE: n/a
828+ // FORMAT THEOREM: every binding in the array is mapped to renderTmuxRightClickSuppressBinding.
829+ // PURITY: CORE
830+ // EFFECT: none
831+ // INVARIANT: Each entry is a static tmux root-table mouse binding name.
832+ // COMPLEXITY: O(1) time/O(1) space.
833+ /**
834+ * Tmux status/border right-click binding names that should be unbound.
835+ *
836+ * @pure true
837+ * @effect none
838+ * @invariant The array contains only static tmux binding identifiers.
839+ * @precondition tmux root key table supports these binding names.
840+ * @postcondition Consumers can map each entry to a shell-safe unbind-key command.
841+ * @complexity O(1) time/O(1) space.
842+ * @throws Never
843+ */
844+ const tmuxRightClickSuppressBindings : ReadonlyArray < string > = [
845+ "MouseDown3Status" ,
846+ "MouseDown3StatusLeft" ,
847+ "MouseDown3StatusRight" ,
848+ "MouseDown3Border" ,
849+ "M-MouseDown3Status" ,
850+ "M-MouseDown3StatusLeft" ,
851+ "M-MouseDown3StatusRight" ,
852+ "M-MouseDown3Border"
853+ ]
854+
855+ // CHANGE: Render one tmux bind-key command for a right-click pane event.
856+ // WHY: Pane events must reach mouse-aware programs without allowing tmux display-menu.
857+ // QUOTE(TZ): issue #340 right-click must not open the default tmux menu in browser terminals.
858+ // REF: PR #342 tmux right-click handling.
859+ // SOURCE: n/a
860+ // FORMAT THEOREM: static binding => shellQuote(protected fragments) in result.
861+ // PURITY: CORE
862+ // EFFECT: none
863+ // INVARIANT: Dynamic shell fragments are emitted through shellQuote.
864+ // COMPLEXITY: O(1) time/O(1) space.
865+ /**
866+ * Builds a tmux root-table command for a pane right-click binding.
867+ *
868+ * @param binding - Static tmux mouse binding name.
869+ * @returns Shell command that binds the event to conditional pane forwarding.
870+ * @pure true
871+ * @effect none
872+ * @invariant Shell-interpreted tmux format/action fragments are quoted.
873+ * @precondition binding is one of tmuxRightClickPaneBindings.
874+ * @postcondition The command exits successfully even when tmux rejects a binding.
875+ * @complexity O(1) time/O(1) space.
876+ * @throws Never
877+ */
878+ const renderTmuxPaneRightClickBinding = ( binding : string ) : string =>
879+ `tmux bind-key -T root ${ binding } if-shell -F -t = ${ shellQuote ( tmuxRightClickForwardPredicate ) } ${
880+ shellQuote ( "select-pane -t = ; send-keys -M" )
881+ } >/dev/null 2>&1 || true`
882+
883+ // CHANGE: Render one tmux unbind-key command for a suppressed right-click event.
884+ // WHY: Non-pane right-click targets are tmux UI affordances and should not open display-menu.
885+ // QUOTE(TZ): issue #340 right-click must not open the default tmux menu in browser terminals.
886+ // REF: PR #342 tmux right-click handling.
887+ // SOURCE: n/a
888+ // FORMAT THEOREM: static binding => deterministic unbind command.
889+ // PURITY: CORE
890+ // EFFECT: none
891+ // INVARIANT: Result contains no user-controlled input.
892+ // COMPLEXITY: O(1) time/O(1) space.
893+ /**
894+ * Builds a tmux root-table command that suppresses a non-pane right-click binding.
895+ *
896+ * @param binding - Static tmux mouse binding name.
897+ * @returns Shell command that unbinds the event and tolerates unsupported bindings.
898+ * @pure true
899+ * @effect none
900+ * @invariant The returned command contains only static text plus binding.
901+ * @precondition binding is one of tmuxRightClickSuppressBindings.
902+ * @postcondition The command exits successfully even when the binding is absent.
903+ * @complexity O(1) time/O(1) space.
904+ * @throws Never
905+ */
906+ const renderTmuxRightClickSuppressBinding = ( binding : string ) : string =>
907+ `tmux unbind-key -T root ${ binding } >/dev/null 2>&1 || true`
908+
909+ // CHANGE: Aggregate all tmux right-click startup commands.
910+ // WHY: Terminal session startup needs one ordered command list for pane forwarding and UI suppression.
911+ // QUOTE(TZ): PR #342 preserves right-click copy while tmux mouse tracking is active.
912+ // REF: PR #342 tmux right-click handling.
913+ // SOURCE: n/a
914+ // FORMAT THEOREM: result length = paneBindings length + suppressBindings length.
915+ // PURITY: CORE
916+ // EFFECT: none
917+ // INVARIANT: Pane commands precede suppress commands.
918+ // COMPLEXITY: O(n) time/O(n) space where n is the total binding count.
919+ /**
920+ * Renders the complete tmux right-click binding setup command list.
921+ *
922+ * @returns Readonly array of shell commands for tmux startup.
923+ * @pure true
924+ * @effect none
925+ * @invariant Pane forwarding commands are emitted before suppressing status/border commands.
926+ * @precondition Binding arrays contain static tmux binding identifiers.
927+ * @postcondition The result contains one command per configured binding.
928+ * @complexity O(n) time/O(n) space where n is total binding count.
929+ * @throws Never
930+ */
931+ const renderTmuxRightClickBindingCommands = ( ) : ReadonlyArray < string > => [
932+ ...tmuxRightClickPaneBindings . map ( renderTmuxPaneRightClickBinding ) ,
933+ ...tmuxRightClickSuppressBindings . map ( renderTmuxRightClickSuppressBinding )
934+ ]
935+
768936const writeBufferToProjectContainer = (
769937 containerName : string ,
770938 containerPath : string ,
@@ -982,6 +1150,7 @@ export const renderTmuxAttachCommand = (
9821150 `tmux set-option -t ${ shellQuote ( args . tmuxName ) } status off >/dev/null 2>&1 || true` ,
9831151 `tmux set-option -t ${ shellQuote ( args . tmuxName ) } history-limit 50000 >/dev/null 2>&1 || true` ,
9841152 `tmux set-option -t ${ shellQuote ( args . tmuxName ) } mouse on >/dev/null 2>&1 || true` ,
1153+ ...renderTmuxRightClickBindingCommands ( ) ,
9851154 `exec tmux attach-session -t ${ shellQuote ( args . tmuxName ) } `
9861155 ] . join ( "; " )
9871156 return `bash --noprofile --norc -lc ${ shellQuote ( script ) } `
0 commit comments