Skip to content

refactor: remove internal ID for URL sequences#2416

Open
joelim-work wants to merge 1 commit intogokcehan:masterfrom
joelim-work:remove-url-id
Open

refactor: remove internal ID for URL sequences#2416
joelim-work wants to merge 1 commit intogokcehan:masterfrom
joelim-work:remove-url-id

Conversation

@joelim-work
Copy link
Collaborator

Now that Tcell has been upgraded to v3 in #2286, I think we no longer need the autogenerated ID workaround since Tcell now handles spaces correctly.

Copy link
Collaborator

@CatsDeservePets CatsDeservePets left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not only about links including spaces, it is also about links without an ID.

Using lowdown to preview CHANGELOG.md no longer links the two parts correctly together in Ghostty:

@joelim-work
Copy link
Collaborator Author

It is not only about links including spaces, it is also about links without an ID.

Using lowdown to preview CHANGELOG.md no longer links the two parts correctly together in Ghostty:

Shouldn't it be the responsibility of lowdown (or any other tool that creates OSC 8 sequences) to ensure that related sections of text are joined correctly by the id parameter? I found that running lowdown outside of lf does not populate id and that the sections are similarly not linked together:

lowdown -t term CHANGELOG.md | less -R

Otherwise, keeping this internal ID generation is essentially compensating for missing functionality in external tools. Also, I think it means it's impossible to deliberately create two hyperlinks that happen to have the same URL but are unlinked, at least not without having to specify a different id for both.

@CatsDeservePets
Copy link
Collaborator

CatsDeservePets commented Mar 5, 2026

Yeah, lowdown does not provide an ID, that is true.

However, according to the reference, lf should assign one in that case:

Complex apps that display data that might itself contain OSC 8 hyperlinks (such as terminal multiplexers, less -R) should do the following: If the encountered OSC 8 hyperlink already has an id, they should prefix it with some static string, or if multiple windows/panes are supported by the app, a prefix that's unique to that window/pane to prevent conflict with other windows/panes. If the encountered OSC 8 hyperlink does not have an id, they should automatically create one so that they can still have multiple windows/panes and can still crazily partially update the screen and keep it as a semantically single hyperlink towards the host emulator.

@joelim-work
Copy link
Collaborator Author

That advice appears to be referring to the case of partially redrawing a hyperlink, where a custom ID is used to indicate that the new text is part of the original hyperlink and not a new one. But lf uses Tcell, which contains an internal buffer and handles the low-level responsibility of writing necessary changes to the terminal. As an application developer, I would be somewhat surprised if every Style.Url object should explicitly specify a custom ID just so that it is not blank.

I admit that I'm not an expert on this matter, so I will just ask this. Do you know of an actual example where assigning a custom ID has an improvement over leaving the ID blank? The CHANGELOG.md example doesn't count because each part (Keep a Changelog and https://keepachangelog.com/en/1.1.0/) is wrapped in its own separate terminal sequence (that just happens to point to the same URL), so I would expect them to not be linked - this is the behavior that I see when I view the output directly in less.

In fact, the current implementation in lf generates an ID based on the URL, so it ends up linking everything that points to the same URL even if they are separated. For the following Markdown file, every link is underlined when hovering the mouse over it in the lf preview because they all share the same generated ID.

# Example Markdown

A link:

[Example](https://example.com)

Another link:

[Example](https://example.com)

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident,
sunt in culpa qui officia deserunt mollit anim id est laborum.

Yet another link:

[Example](https://example.com)

@CatsDeservePets
Copy link
Collaborator

I am no expert either. The ref only really talks about 'adjacent' links with the same URI. These are also included in the linked example file.

Maybe @veltza can help us here.

@veltza
Copy link
Contributor

veltza commented Mar 5, 2026

Based on my tests, terminal emulators handle hyperlinks with the same URL but no ID in three different ways:

  • Underline adjacent cells containing the same URL (ghostty, wezterm).
  • Underline all cells on the screen containing the same URL (kitty).
  • Underline only cells belonging to the same OSC-8 sequence (alacritty, contour, vte terminals).

I debugged this PR with my st fork and found that Tcell behaves almost the same as Ghostty and WezTerm, because it merges adjacent hyperlinks into one link if they have the same URL and no ID. So if you do something like this:

printf '\e]8;;http://example.com\e\\foo\e]8;;\e\\\e]8;;http://example.com\e\\bar\e]8;;\e\\\n' > foobar.txt

And you open foobar.txt in lf, Tcell sends the hyperlinks to your terminal as a single link like this:

printf '\e]8;;http://example.com\e\\foobar\e]8;;\e\\\n'

For this reason, hyperlinks may behave slightly differently in lf and less (default i keybinding). However, since there’s no single correct way to handle hyperlinks, I think this new behavior in lf is fine.

@joelim-work
Copy link
Collaborator Author

joelim-work commented Mar 6, 2026

Tcell maintains an internal buffer where each cell has its own style information. When drawing the UI, Tcell will only send the OSC 8 sequence when the URL style changes between cells, in other words it will merge adjacent cells a single hyperlink.

One interesting thing to note is that Tcell skips any cells if they have not changed (for the purposes of writing updates to the terminal), so in theory if a portion of the text is changed, a new OSC 8 sequence will be sent for only that portion, and that could be treated as a different hyperlink from the original one. This is the reason behind the advice for always assigning an ID so that the old and new text are forcibly linked together (CMIIW). However I think it would make more sense if Tcell could detect and resend the entire hyperlink if any portion of it changes, so that manually assigning IDs like this is not necessary. My understanding for IDs was that it is mainly used to mark separate sections of text as linked such as below:

╔═ file1 ════╗
║          ╔═ file2 ═══╗
║http://exa║Lorem ipsum║
║le.com    ║ dolor sit ║
║          ║amet, conse║
╚══════════║ctetur adip║
           ╚═══════════╝

Anyway I think there are three solutions on how to deal with blank IDs:

  • Simple passthrough (this PR), and leave it to the terminal to deal with linking
  • Assign ID based on URL (current behavior)
  • Assign ID based on incrementing counter (and possibly URL too), which prevents multiple links pointing to the same URL from being associated with each other (see my Markdown example in refactor: remove internal ID for URL sequences #2416 (comment))

I still lean towards the first one, provided it doesn't cause any issues, but I am probably ultimately fine with whichever solution depending on feedback from users (this is a somewhat niche feature so I'm not sure if there will be much feedback anyway).

EDIT: For the initial lowdown example in #2416 (review), there are two pieces of text each wrapped in a separate OSC 8 sequence, and are furthermore separated by a regular space, so I would expect that they represent two separate hyperlinks regardless.

@CatsDeservePets
Copy link
Collaborator

Anyway I think there are three solutions on how to deal with blank IDs:

  • Simple passthrough (this PR), and leave it to the terminal to deal with linking
  • Assign ID based on URL (current behavior)
  • Assign ID based on incrementing counter (and possibly URL too), which prevents multiple links pointing to the same URL from being associated with each other (see my Markdown example in refactor: remove internal ID for URL sequences #2416 (comment))

I still lean towards the first one, provided it doesn't cause any issues, but I am probably ultimately fine with whichever solution depending on feedback from users (this is a somewhat niche feature so I'm not sure if there will be much feedback anyway).

Thinking about it, I'd also say let's stick to the passthrough method. It is probably not worth it to over-engineer such a niche feature. I also doubt that there will be any user feedback regarding this. If there is, we can discuss this again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants