@@ -564,6 +564,16 @@ export class Terminal implements ITerminalCore {
564564 /**
565565 * Get selection position as buffer range
566566 */
567+ /**
568+ * Get the current viewport Y position.
569+ *
570+ * This is the number of lines scrolled back from the bottom of the
571+ * scrollback buffer. It may be fractional during smooth scrolling.
572+ */
573+ public getViewportY ( ) : number {
574+ return this . viewportY ;
575+ }
576+
567577 public getSelectionPosition ( ) : IBufferRange | undefined {
568578 return this . selectionManager ?. getSelectionPosition ( ) ;
569579 }
@@ -1041,16 +1051,22 @@ export class Terminal implements ITerminalCore {
10411051 let hyperlinkId = 0 ;
10421052
10431053 // When scrolled, fetch from scrollback or screen based on position
1054+ // NOTE: viewportY may be fractional during smooth scrolling. The renderer
1055+ // uses Math.floor(viewportY) when mapping viewport rows to scrollback vs
1056+ // screen; we mirror that logic here so link hit-testing matches what the
1057+ // user sees on screen.
10441058 let line : GhosttyCell [ ] | null = null ;
1045- if ( this . viewportY > 0 ) {
1059+ const rawViewportY = this . getViewportY ( ) ;
1060+ const viewportY = Math . max ( 0 , Math . floor ( rawViewportY ) ) ;
1061+ if ( viewportY > 0 ) {
10461062 const scrollbackLength = this . wasmTerm . getScrollbackLength ( ) ;
1047- if ( viewportRow < this . viewportY ) {
1063+ if ( viewportRow < viewportY ) {
10481064 // Mouse is over scrollback content
1049- const scrollbackOffset = scrollbackLength - this . viewportY + viewportRow ;
1065+ const scrollbackOffset = scrollbackLength - viewportY + viewportRow ;
10501066 line = this . wasmTerm . getScrollbackLine ( scrollbackOffset ) ;
10511067 } else {
10521068 // Mouse is over screen content (bottom part of viewport)
1053- const screenRow = viewportRow - this . viewportY ;
1069+ const screenRow = viewportRow - viewportY ;
10541070 line = this . wasmTerm . getLine ( screenRow ) ;
10551071 }
10561072 } else {
@@ -1077,14 +1093,18 @@ export class Terminal implements ITerminalCore {
10771093 const scrollbackLength = this . wasmTerm . getScrollbackLength ( ) ;
10781094 let bufferRow : number ;
10791095
1080- if ( this . viewportY > 0 ) {
1096+ // Use floored viewportY for buffer mapping (must match renderer & selection)
1097+ const rawViewportYForBuffer = this . getViewportY ( ) ;
1098+ const viewportYForBuffer = Math . max ( 0 , Math . floor ( rawViewportYForBuffer ) ) ;
1099+
1100+ if ( viewportYForBuffer > 0 ) {
10811101 // When scrolled, the buffer row depends on where in the viewport we are
1082- if ( viewportRow < this . viewportY ) {
1102+ if ( viewportRow < viewportYForBuffer ) {
10831103 // Mouse is over scrollback content
1084- bufferRow = scrollbackLength - this . viewportY + viewportRow ;
1104+ bufferRow = scrollbackLength - viewportYForBuffer + viewportRow ;
10851105 } else {
10861106 // Mouse is over screen content (bottom part of viewport)
1087- const screenRow = viewportRow - this . viewportY ;
1107+ const screenRow = viewportRow - viewportYForBuffer ;
10881108 bufferRow = scrollbackLength + screenRow ;
10891109 }
10901110 } else {
@@ -1119,8 +1139,11 @@ export class Terminal implements ITerminalCore {
11191139 const scrollbackLength = this . wasmTerm ?. getScrollbackLength ( ) || 0 ;
11201140
11211141 // Calculate viewport Y for start and end positions
1122- const startViewportY = link . range . start . y - scrollbackLength + this . viewportY ;
1123- const endViewportY = link . range . end . y - scrollbackLength + this . viewportY ;
1142+ // Use floored viewportY so overlay rows match renderer & selection
1143+ const rawViewportYForLinks = this . getViewportY ( ) ;
1144+ const viewportYForLinks = Math . max ( 0 , Math . floor ( rawViewportYForLinks ) ) ;
1145+ const startViewportY = link . range . start . y - scrollbackLength + viewportYForLinks ;
1146+ const endViewportY = link . range . end . y - scrollbackLength + viewportYForLinks ;
11241147
11251148 // Only show underline if link is visible in viewport
11261149 if ( startViewportY < this . rows && endViewportY >= 0 ) {
@@ -1192,11 +1215,15 @@ export class Terminal implements ITerminalCore {
11921215 const scrollbackLength = this . wasmTerm . getScrollbackLength ( ) ;
11931216 let bufferRow : number ;
11941217
1195- if ( this . viewportY > 0 ) {
1196- if ( viewportRow < this . viewportY ) {
1197- bufferRow = scrollbackLength - this . viewportY + viewportRow ;
1218+ // Use floored viewportY for buffer mapping (must match renderer & selection)
1219+ const rawViewportYForClick = this . getViewportY ( ) ;
1220+ const viewportYForClick = Math . max ( 0 , Math . floor ( rawViewportYForClick ) ) ;
1221+
1222+ if ( viewportYForClick > 0 ) {
1223+ if ( viewportRow < viewportYForClick ) {
1224+ bufferRow = scrollbackLength - viewportYForClick + viewportRow ;
11981225 } else {
1199- const screenRow = viewportRow - this . viewportY ;
1226+ const screenRow = viewportRow - viewportYForClick ;
12001227 bufferRow = scrollbackLength + screenRow ;
12011228 }
12021229 } else {
0 commit comments