@@ -158,6 +158,20 @@ const KEY_MAP: Record<string, Key> = {
158158 * Attaches keyboard event listeners to a container and converts
159159 * keyboard events to terminal input data
160160 */
161+ /**
162+ * Mouse tracking configuration
163+ */
164+ export interface MouseTrackingConfig {
165+ /** Check if any mouse tracking mode is enabled */
166+ hasMouseTracking : ( ) => boolean ;
167+ /** Check if SGR extended mouse mode is enabled (mode 1006) */
168+ hasSgrMouseMode : ( ) => boolean ;
169+ /** Get cell dimensions for pixel to cell conversion */
170+ getCellDimensions : ( ) => { width : number ; height : number } ;
171+ /** Get canvas/container offset for accurate position calculation */
172+ getCanvasOffset : ( ) => { left : number ; top : number } ;
173+ }
174+
161175export class InputHandler {
162176 private encoder : KeyEncoder ;
163177 private container : HTMLElement ;
@@ -168,15 +182,21 @@ export class InputHandler {
168182 private customKeyEventHandler ?: ( event : KeyboardEvent ) => boolean ;
169183 private getModeCallback ?: ( mode : number ) => boolean ;
170184 private onCopyCallback ?: ( ) => boolean ;
185+ private mouseConfig ?: MouseTrackingConfig ;
171186 private keydownListener : ( ( e : KeyboardEvent ) => void ) | null = null ;
172187 private keypressListener : ( ( e : KeyboardEvent ) => void ) | null = null ;
173188 private pasteListener : ( ( e : ClipboardEvent ) => void ) | null = null ;
174189 private beforeInputListener : ( ( e : InputEvent ) => void ) | null = null ;
175190 private compositionStartListener : ( ( e : CompositionEvent ) => void ) | null = null ;
176191 private compositionUpdateListener : ( ( e : CompositionEvent ) => void ) | null = null ;
177192 private compositionEndListener : ( ( e : CompositionEvent ) => void ) | null = null ;
193+ private mousedownListener : ( ( e : MouseEvent ) => void ) | null = null ;
194+ private mouseupListener : ( ( e : MouseEvent ) => void ) | null = null ;
195+ private mousemoveListener : ( ( e : MouseEvent ) => void ) | null = null ;
196+ private wheelListener : ( ( e : WheelEvent ) => void ) | null = null ;
178197 private isComposing = false ;
179198 private isDisposed = false ;
199+ private mouseButtonsPressed = 0 ; // Track which buttons are pressed for motion reporting
180200 private lastKeyDownData : string | null = null ;
181201 private lastKeyDownTime = 0 ;
182202 private lastPasteData : string | null = null ;
@@ -198,6 +218,8 @@ export class InputHandler {
198218 * @param customKeyEventHandler - Optional custom key event handler
199219 * @param getMode - Optional callback to query terminal mode state (for application cursor mode)
200220 * @param onCopy - Optional callback to handle copy (Cmd+C/Ctrl+C with selection)
221+ * @param inputElement - Optional input element for beforeinput events
222+ * @param mouseConfig - Optional mouse tracking configuration
201223 */
202224 constructor (
203225 ghostty : Ghostty ,
@@ -208,7 +230,8 @@ export class InputHandler {
208230 customKeyEventHandler ?: ( event : KeyboardEvent ) => boolean ,
209231 getMode ?: ( mode : number ) => boolean ,
210232 onCopy ?: ( ) => boolean ,
211- inputElement ?: HTMLElement
233+ inputElement ?: HTMLElement ,
234+ mouseConfig ?: MouseTrackingConfig
212235 ) {
213236 this . encoder = ghostty . createKeyEncoder ( ) ;
214237 this . container = container ;
@@ -219,6 +242,7 @@ export class InputHandler {
219242 this . customKeyEventHandler = customKeyEventHandler ;
220243 this . getModeCallback = getMode ;
221244 this . onCopyCallback = onCopy ;
245+ this . mouseConfig = mouseConfig ;
222246
223247 // Attach event listeners
224248 this . attach ( ) ;
@@ -272,6 +296,19 @@ export class InputHandler {
272296
273297 this . compositionEndListener = this . handleCompositionEnd . bind ( this ) ;
274298 this . container . addEventListener ( 'compositionend' , this . compositionEndListener ) ;
299+
300+ // Mouse event listeners (for terminal mouse tracking)
301+ this . mousedownListener = this . handleMouseDown . bind ( this ) ;
302+ this . container . addEventListener ( 'mousedown' , this . mousedownListener ) ;
303+
304+ this . mouseupListener = this . handleMouseUp . bind ( this ) ;
305+ this . container . addEventListener ( 'mouseup' , this . mouseupListener ) ;
306+
307+ this . mousemoveListener = this . handleMouseMove . bind ( this ) ;
308+ this . container . addEventListener ( 'mousemove' , this . mousemoveListener ) ;
309+
310+ this . wheelListener = this . handleWheel . bind ( this ) ;
311+ this . container . addEventListener ( 'wheel' , this . wheelListener , { passive : false } ) ;
275312 }
276313
277314 /**
@@ -679,6 +716,196 @@ export class InputHandler {
679716 }
680717 }
681718
719+ // ==========================================================================
720+ // Mouse Event Handling (for terminal mouse tracking)
721+ // ==========================================================================
722+
723+ /**
724+ * Convert pixel coordinates to terminal cell coordinates
725+ */
726+ private pixelToCell ( event : MouseEvent ) : { col : number ; row : number } | null {
727+ if ( ! this . mouseConfig ) return null ;
728+
729+ const dims = this . mouseConfig . getCellDimensions ( ) ;
730+ const offset = this . mouseConfig . getCanvasOffset ( ) ;
731+
732+ if ( dims . width <= 0 || dims . height <= 0 ) return null ;
733+
734+ const x = event . clientX - offset . left ;
735+ const y = event . clientY - offset . top ;
736+
737+ // Convert to 1-based cell coordinates (terminal uses 1-based)
738+ const col = Math . floor ( x / dims . width ) + 1 ;
739+ const row = Math . floor ( y / dims . height ) + 1 ;
740+
741+ // Clamp to valid range (at least 1)
742+ return {
743+ col : Math . max ( 1 , col ) ,
744+ row : Math . max ( 1 , row ) ,
745+ } ;
746+ }
747+
748+ /**
749+ * Get modifier flags for mouse event
750+ */
751+ private getMouseModifiers ( event : MouseEvent ) : number {
752+ let mods = 0 ;
753+ if ( event . shiftKey ) mods |= 4 ;
754+ if ( event . metaKey ) mods |= 8 ; // Meta (Cmd on Mac)
755+ if ( event . ctrlKey ) mods |= 16 ;
756+ return mods ;
757+ }
758+
759+ /**
760+ * Encode mouse event as SGR sequence
761+ * SGR format: \x1b[<Btn;Col;RowM (press/motion) or \x1b[<Btn;Col;Rowm (release)
762+ */
763+ private encodeMouseSGR (
764+ button : number ,
765+ col : number ,
766+ row : number ,
767+ isRelease : boolean ,
768+ modifiers : number
769+ ) : string {
770+ const btn = button + modifiers ;
771+ const suffix = isRelease ? 'm' : 'M' ;
772+ return `\x1b[<${ btn } ;${ col } ;${ row } ${ suffix } ` ;
773+ }
774+
775+ /**
776+ * Encode mouse event as X10/normal sequence (legacy format)
777+ * Format: \x1b[M<Btn+32><Col+32><Row+32>
778+ */
779+ private encodeMouseX10 ( button : number , col : number , row : number , modifiers : number ) : string {
780+ // X10 format adds 32 to all values and encodes as characters
781+ // Button encoding: 0=left, 1=middle, 2=right, 3=release
782+ const btn = button + modifiers + 32 ;
783+ const colChar = String . fromCharCode ( Math . min ( col + 32 , 255 ) ) ;
784+ const rowChar = String . fromCharCode ( Math . min ( row + 32 , 255 ) ) ;
785+ return `\x1b[M${ String . fromCharCode ( btn ) } ${ colChar } ${ rowChar } ` ;
786+ }
787+
788+ /**
789+ * Send mouse event to terminal
790+ */
791+ private sendMouseEvent (
792+ button : number ,
793+ col : number ,
794+ row : number ,
795+ isRelease : boolean ,
796+ event : MouseEvent
797+ ) : void {
798+ const modifiers = this . getMouseModifiers ( event ) ;
799+
800+ // Check if SGR extended mode is enabled (mode 1006)
801+ const useSGR = this . mouseConfig ?. hasSgrMouseMode ?.( ) ?? true ;
802+
803+ let sequence : string ;
804+ if ( useSGR ) {
805+ sequence = this . encodeMouseSGR ( button , col , row , isRelease , modifiers ) ;
806+ } else {
807+ // X10/normal mode doesn't support release events directly
808+ // Button 3 means release in X10 mode
809+ const x10Button = isRelease ? 3 : button ;
810+ sequence = this . encodeMouseX10 ( x10Button , col , row , modifiers ) ;
811+ }
812+
813+ this . onDataCallback ( sequence ) ;
814+ }
815+
816+ /**
817+ * Handle mousedown event
818+ */
819+ private handleMouseDown ( event : MouseEvent ) : void {
820+ if ( this . isDisposed ) return ;
821+ if ( ! this . mouseConfig ?. hasMouseTracking ( ) ) return ;
822+
823+ const cell = this . pixelToCell ( event ) ;
824+ if ( ! cell ) return ;
825+
826+ // Map browser button to terminal button
827+ // event.button: 0=left, 1=middle, 2=right
828+ // Terminal: 0=left, 1=middle, 2=right
829+ const button = event . button ;
830+
831+ // Track pressed buttons for motion events
832+ this . mouseButtonsPressed |= 1 << button ;
833+
834+ this . sendMouseEvent ( button , cell . col , cell . row , false , event ) ;
835+
836+ // Don't prevent default - let SelectionManager handle selection
837+ // Only prevent if we actually handled the event
838+ // event.preventDefault();
839+ }
840+
841+ /**
842+ * Handle mouseup event
843+ */
844+ private handleMouseUp ( event : MouseEvent ) : void {
845+ if ( this . isDisposed ) return ;
846+ if ( ! this . mouseConfig ?. hasMouseTracking ( ) ) return ;
847+
848+ const cell = this . pixelToCell ( event ) ;
849+ if ( ! cell ) return ;
850+
851+ const button = event . button ;
852+
853+ // Clear pressed button
854+ this . mouseButtonsPressed &= ~ ( 1 << button ) ;
855+
856+ this . sendMouseEvent ( button , cell . col , cell . row , true , event ) ;
857+ }
858+
859+ /**
860+ * Handle mousemove event
861+ */
862+ private handleMouseMove ( event : MouseEvent ) : void {
863+ if ( this . isDisposed ) return ;
864+ if ( ! this . mouseConfig ?. hasMouseTracking ( ) ) return ;
865+
866+ // Check if button motion mode or any-event tracking is enabled
867+ // Mode 1002 = button motion, Mode 1003 = any motion
868+ const hasButtonMotion = this . getModeCallback ?.( 1002 ) ?? false ;
869+ const hasAnyMotion = this . getModeCallback ?.( 1003 ) ?? false ;
870+
871+ if ( ! hasButtonMotion && ! hasAnyMotion ) return ;
872+
873+ // In button motion mode, only report if a button is pressed
874+ if ( hasButtonMotion && ! hasAnyMotion && this . mouseButtonsPressed === 0 ) return ;
875+
876+ const cell = this . pixelToCell ( event ) ;
877+ if ( ! cell ) return ;
878+
879+ // Determine which button to report (or 32 for motion with no button)
880+ let button = 32 ; // Motion flag
881+ if ( this . mouseButtonsPressed & 1 )
882+ button += 0 ; // Left
883+ else if ( this . mouseButtonsPressed & 2 )
884+ button += 1 ; // Middle
885+ else if ( this . mouseButtonsPressed & 4 ) button += 2 ; // Right
886+
887+ this . sendMouseEvent ( button , cell . col , cell . row , false , event ) ;
888+ }
889+
890+ /**
891+ * Handle wheel event (scroll)
892+ */
893+ private handleWheel ( event : WheelEvent ) : void {
894+ if ( this . isDisposed ) return ;
895+ if ( ! this . mouseConfig ?. hasMouseTracking ( ) ) return ;
896+
897+ const cell = this . pixelToCell ( event ) ;
898+ if ( ! cell ) return ;
899+
900+ // Wheel events: button 64 = scroll up, button 65 = scroll down
901+ const button = event . deltaY < 0 ? 64 : 65 ;
902+
903+ this . sendMouseEvent ( button , cell . col , cell . row , false , event ) ;
904+
905+ // Prevent default scrolling when mouse tracking is active
906+ event . preventDefault ( ) ;
907+ }
908+
682909 /**
683910 * Emit paste data with bracketed paste support
684911 */
@@ -847,6 +1074,26 @@ export class InputHandler {
8471074 this . compositionEndListener = null ;
8481075 }
8491076
1077+ if ( this . mousedownListener ) {
1078+ this . container . removeEventListener ( 'mousedown' , this . mousedownListener ) ;
1079+ this . mousedownListener = null ;
1080+ }
1081+
1082+ if ( this . mouseupListener ) {
1083+ this . container . removeEventListener ( 'mouseup' , this . mouseupListener ) ;
1084+ this . mouseupListener = null ;
1085+ }
1086+
1087+ if ( this . mousemoveListener ) {
1088+ this . container . removeEventListener ( 'mousemove' , this . mousemoveListener ) ;
1089+ this . mousemoveListener = null ;
1090+ }
1091+
1092+ if ( this . wheelListener ) {
1093+ this . container . removeEventListener ( 'wheel' , this . wheelListener ) ;
1094+ this . wheelListener = null ;
1095+ }
1096+
8501097 this . isDisposed = true ;
8511098 }
8521099
0 commit comments