feat: clickable links, hover cursor, and mouse capture toggle#17
Merged
Conversation
EnableMouseCapture grabs *all* mouse events, which means the terminal can't do its own text selection or handle OSC 8 hyperlink clicks. The links were already being emitted with proper OSC 8 sequences, but nobody could actually click them. Not great. Three things here: Add a link_at_position() helper that maps terminal (row, col) coordinates back to the underlying span and checks for a link_url. Left-clicking a link now opens it via open::that(), same as the link picker but without the extra ceremony. Mouse hover over links sends OSC 22 to switch the pointer to a hand cursor, and resets it when you move off. Not every terminal supports OSC 22, but those that don't will silently ignore it, so this is harmless. Press 'm' to toggle mouse capture on/off entirely. When off, the terminal regains control and you can select text like a normal human being. When on, scroll wheel works as before. Status message confirms the state.
The clickable links feature had three problems that a code review
turned up, ranging from "silently wrong" to "actively dangerous."
First, open::that() was being called on *any* URL the user clicked,
with zero scheme validation. The existing link picker correctly
restricts to http://, https://, and mailto: — but the new click
handler just blindly opened whatever was in the href. A malicious
markdown file with file:/// or other scheme URLs would happily get
passed to the OS handler. Not great.
Second, anchor link clicks used a naive to_lowercase().replace(' ',
"-") slug instead of the proper heading_to_slug() function that
strips punctuation, collapses consecutive hyphens, and handles
Unicode. So clicking an anchor to "What's new?" would try to match
"what's new?" instead of "whats-new", silently fail, and give the
user zero feedback. While at it, added a "Heading not found" status
message for the miss case.
Third, link_at_position() used chars().count() for column tracking,
but terminal coordinates are in display-width columns. CJK and emoji
characters occupy two columns but count as one char, so the hit-test
was wrong for any line containing wide characters. Switched to
UnicodeWidthStr::width().
Also reset the OSC 22 pointer cursor when toggling mouse capture
off, so you don't get a stuck hand cursor.
- Reset cursor shape (OSC 22) in TerminalGuard::drop to prevent leaked pointer cursor on exit - Fix renderer column tracking to use UnicodeWidthStr::width() instead of chars().count(), aligning with link_at_position hit-testing for wide characters (CJK, emoji) - Extract left gutter width into GUTTER_COLS constant - Add 7 unit tests for link_at_position covering hit/miss, gutter, title bar, past-end-of-line, past-last-line, and multiple links
The URL dispatch logic — scheme validation, open::that, anchor navigation — was copy-pasted between the mouse click handler and the link picker. Two copies of the same logic, each with slightly different ordering of the checks. This is not great. It turns out open::that errors were also being silently swallowed with `let _ = ...`, while the status bar cheerfully reported "Opened: ..." regardless. So if your browser wasn't configured or the open failed, you'd never know. Let's fix that. Extract a shared dispatch_link() that both code paths now call. The function actually checks the Result from open::that and shows the error in the status bar on failure. While at it, add reset_cursor_shape() calls at every mode transition away from Normal (search, TOC, link picker, fuzzy heading, help). Previously, if you hovered a link and then pressed '/' to search, the cursor stayed as a hand pointer until you happened to move the mouse back in Normal mode.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #13 — mouse capture was grabbing all events, preventing text selection and link clicks.
open::that()as the link picker)m): Pressmto disable mouse capture entirely so the terminal can handle text selection natively. Press again to re-enable scroll wheel support. Status message confirms the stateTest plan
cargo clippy— no warningscargo test— 102 passed, 0 failedmto toggle mouse capture off, verify text selection worksmagain, verify scroll wheel worksF1help screen shows the newmkeybinding