Terminal does not refresh the hovered link when the content changes
Windows Terminal version
1.24.10921.0
Windows build number
10.0.26220.0
Other Software
Application: Terminal.Gui v2 (https://github.com/gui-cs/Terminal.Gui)
Sample app: UICatalog -> TextView -> Editor
Steps to reproduce
When the mouse is hovering a hyperlink, Windows Terminal caches the hovered hyperlink state in viewport coordinates and only refreshes it on mouse-move / mouse-leave paths.
If the buffer contents change underneath a stationary pointer, or if scrolling changes which logical cell lives under the same viewport coordinates, the cached hover state is not invalidated. The strongest user-visible result is that the hover underline stays active after mouse-wheel scrolling even though the hovered logical cell has changed.
In a real app (Terminal.Gui TextView -> Editor), I also see a related stale-hover symptom after Ctrl+A -> Delete: locations that used to contain a link can still surface stale hover feedback. Based on the source, that appears to be the same cache invalidation bug, but the most reliable repro is the scroll case below.
With Terminal.Gui (most reliable)
git clone https://github.com/gui-cs/Terminal.Gui.git
cd Terminal.Gui
dotnet run --project Examples/UICatalog/UICatalog.csproj
Then:
- Open
TextView -> Editor.
- Hover a visible URL until Windows Terminal shows the hyperlink hover underline.
- Keep the pointer over that same screen cell.
- Use the mouse wheel to scroll the editor content.
- Observe that the hover underline is not invalidated when the viewport mapping changes under the pointer.
Plain Powershell (a bit harder to notice)
The script only stages the content; the scroll must be done by the user with the mouse wheel, because the source code path that exhibits the bug (ControlInteractivity::MouseWheel -> Terminal::UserScrollViewport -> TriggerScroll) is only reached on user-initiated viewport scroll. Application-emitted line writes that cause the viewport to advance go through a different path and do not reliably reproduce.
$ErrorActionPreference = 'Stop'
$ESC = [char]27
$BEL = [char]7
[Console]::Write("$ESC[2J$ESC[H")
[Console]::WriteLine("Windows Terminal OSC 8 stale-hover repro -- mouse wheel scroll case")
[Console]::WriteLine("")
[Console]::WriteLine("STEPS (read carefully -- the script only stages content):")
[Console]::WriteLine(" 1. Wait for 'Ready' to appear at the bottom of the screen.")
[Console]::WriteLine(" 2. Find the line marked '----> HOVER_ME_LINK <----' below.")
[Console]::WriteLine(" 3. Move the mouse onto HOVER_ME_LINK -- the hover underline should appear.")
[Console]::WriteLine(" 4. KEEP THE MOUSE PERFECTLY STILL on that same screen cell.")
[Console]::WriteLine(" 5. Spin the mouse wheel one notch up, then one notch down.")
[Console]::WriteLine("")
[Console]::WriteLine("EXPECTED: after the wheel scroll, the cell under the mouse is different")
[Console]::WriteLine("content (a filler line), so the hover underline should be gone from that cell.")
[Console]::WriteLine("ACTUAL: the underline tracks the link's new screen position instead of being")
[Console]::WriteLine("invalidated -- it 'follows' the link as it scrolls.")
[Console]::WriteLine("")
Start-Sleep -Seconds 1
# Fill plenty of scrollback above the link so wheel-up has somewhere to go.
for ($i = 1; $i -le 60; $i++) {
[Console]::WriteLine("above-link filler line {0,2} (plain text, no hyperlink)" -f $i)
}
# Emit the link with obvious visual markers and padding so it is easy to hit.
[Console]::Write(" ----> ")
[Console]::Write("${ESC}]8;;https://example.com/demo${BEL}HOVER_ME_LINK${ESC}]8;;${BEL}")
[Console]::WriteLine(" <---- (hover the underlined text)")
# A few lines below the link, so the wheel-down direction also has content.
for ($i = 1; $i -le 6; $i++) {
[Console]::WriteLine("below-link filler line {0,2} (plain text, no hyperlink)" -f $i)
}
[Console]::WriteLine("")
[Console]::WriteLine("Ready. Hover HOVER_ME_LINK above, then mouse-wheel one notch each way.")
[Console]::WriteLine("Press Enter when done observing.")
$null = Read-Host
Expected Behavior
When scrolling changes which logical cell is under the pointer, Windows Terminal should re-evaluate the hovered hyperlink at that viewport position. If the new cell is not part of a hyperlink, the hover underline should disappear immediately.
Actual Behavior
The hover underline remains driven by stale cached hover state until the mouse enters a different cell. In practice this looks like the underline continuing to appear on the scrolled hyperlink content instead of being invalidated when the hovered cell identity changes.
Secondary manifestation seen in the same app
In the same TextView -> Editor repro app:
- Hover a URL until the hover underline appears.
- Keep the pointer parked on that screen location.
- Press
Ctrl+A, then Delete.
- Move the pointer back over the location that used to contain the link.
I can still get stale hover feedback there in the real app.
I am not using this as the primary repro because the minimized overwrite-only script I tried to build was not reliable enough. The scroll repro above is the clearest and is fully explained by the source.
Why I believe this is a Windows Terminal bug
1. Hover state is cached and skipped if the mouse has not moved
src/cascadia/TerminalControl/ControlCore.cpp in v1.24.10921.0:
void ControlCore::_updateHoveredCell(const std::optional<til::point> terminalPosition)
{
if (terminalPosition == _lastHoveredCell)
{
return;
}
_lastHoveredCell = terminalPosition;
uint16_t newId{ 0u };
decltype(_terminal->GetHyperlinkIntervalFromViewportPosition({})) newInterval{ std::nullopt };
if (terminalPosition.has_value())
{
const auto lock = _terminal->LockForReading();
newId = _terminal->GetHyperlinkIdAtViewportPosition(*terminalPosition);
newInterval = _terminal->GetHyperlinkIntervalFromViewportPosition(*terminalPosition);
}
if (newId != _lastHoveredId || (newInterval != _lastHoveredInterval))
{
const auto lock = _terminal->LockForWriting();
_lastHoveredId = newId;
_lastHoveredInterval = newInterval;
_renderer->UpdateHyperlinkHoveredId(newId);
_renderer->UpdateLastHoveredInterval(newInterval);
_renderer->TriggerRedrawAll();
}
}
Key point: if the mouse is still on the same viewport cell, Windows Terminal does not re-query the hyperlink id or interval for that cell.
2. That hover-refresh path is mouse-driven, not buffer-driven
SetHoveredCell calls _updateHoveredCell, and ControlInteractivity::PointerMoved calls SetHoveredCell.
The mouse-wheel scroll path goes through:
ControlInteractivity::MouseWheel
-> _mouseScrollHandler
-> Terminal::UserScrollViewport
-> TriggerScroll
That path does not call _updateHoveredCell.
3. The hovered lookup is viewport-based, so scroll changes the logical cell identity
Terminal::GetHyperlinkIdAtViewportPosition does:
return _activeBuffer().GetCellDataAt(_ConvertToBufferCell(viewportPos, false))->TextAttr().GetHyperlinkId();
So the same viewport coordinates can refer to a different logical buffer cell after scroll. If Windows Terminal does not re-query after the scroll, it keeps using stale hover state.
Terminal does not refresh the hovered link when the content changes
Windows Terminal version
1.24.10921.0
Windows build number
10.0.26220.0
Other Software
Application: Terminal.Gui v2 (https://github.com/gui-cs/Terminal.Gui)
Sample app:
UICatalog -> TextView -> EditorSteps to reproduce
When the mouse is hovering a hyperlink, Windows Terminal caches the hovered hyperlink state in viewport coordinates and only refreshes it on mouse-move / mouse-leave paths.
If the buffer contents change underneath a stationary pointer, or if scrolling changes which logical cell lives under the same viewport coordinates, the cached hover state is not invalidated. The strongest user-visible result is that the hover underline stays active after mouse-wheel scrolling even though the hovered logical cell has changed.
In a real app (
Terminal.GuiTextView -> Editor), I also see a related stale-hover symptom afterCtrl+A->Delete: locations that used to contain a link can still surface stale hover feedback. Based on the source, that appears to be the same cache invalidation bug, but the most reliable repro is the scroll case below.With Terminal.Gui (most reliable)
git clone https://github.com/gui-cs/Terminal.Gui.git cd Terminal.Gui dotnet run --project Examples/UICatalog/UICatalog.csprojThen:
TextView -> Editor.Plain Powershell (a bit harder to notice)
The script only stages the content; the scroll must be done by the user with the mouse wheel, because the source code path that exhibits the bug (
ControlInteractivity::MouseWheel -> Terminal::UserScrollViewport -> TriggerScroll) is only reached on user-initiated viewport scroll. Application-emitted line writes that cause the viewport to advance go through a different path and do not reliably reproduce.Expected Behavior
When scrolling changes which logical cell is under the pointer, Windows Terminal should re-evaluate the hovered hyperlink at that viewport position. If the new cell is not part of a hyperlink, the hover underline should disappear immediately.
Actual Behavior
The hover underline remains driven by stale cached hover state until the mouse enters a different cell. In practice this looks like the underline continuing to appear on the scrolled hyperlink content instead of being invalidated when the hovered cell identity changes.
Secondary manifestation seen in the same app
In the same
TextView -> Editorrepro app:Ctrl+A, thenDelete.I can still get stale hover feedback there in the real app.
I am not using this as the primary repro because the minimized overwrite-only script I tried to build was not reliable enough. The scroll repro above is the clearest and is fully explained by the source.
Why I believe this is a Windows Terminal bug
1. Hover state is cached and skipped if the mouse has not moved
src/cascadia/TerminalControl/ControlCore.cppinv1.24.10921.0:Key point: if the mouse is still on the same viewport cell, Windows Terminal does not re-query the hyperlink id or interval for that cell.
2. That hover-refresh path is mouse-driven, not buffer-driven
SetHoveredCellcalls_updateHoveredCell, andControlInteractivity::PointerMovedcallsSetHoveredCell.The mouse-wheel scroll path goes through:
ControlInteractivity::MouseWheel->
_mouseScrollHandler->
Terminal::UserScrollViewport->
TriggerScrollThat path does not call
_updateHoveredCell.3. The hovered lookup is viewport-based, so scroll changes the logical cell identity
Terminal::GetHyperlinkIdAtViewportPositiondoes:So the same viewport coordinates can refer to a different logical buffer cell after scroll. If Windows Terminal does not re-query after the scroll, it keeps using stale hover state.