clickable-path
v0.0.1
Published
Make file paths printed by CLIs ctrl/cmd-clickable, using OSC 8 hyperlinks
Downloads
7,909
Readme
clickable-path
Make the file paths your CLI prints ctrl/cmd-clickable
CLIs print paths, but making them open can be tricky.
Thankfully, OSC 8 hyperlinks exist. They point at an invisible absolute file:// URL.
clickable-path is a zero-dependency package to print paths as OSC 8 hyperlinks if your terminal supports them, falling back to a plain label if not.
Usage
npm install clickable-path[!NOTE] Requires Node 22.1 or newer, which is where
pathToFileURLgained thewindowsoption used to convert Windows-shaped paths on any platform.
import { link } from 'clickable-path'
console.log(`Building ${link('/home/me/project/nuxt.config.ts')}`)
// -> label `nuxt.config.ts`, linking file:///home/me/project/nuxt.config.ts
console.log(link('src/index.ts', { line: 12 }))
// -> label `src/index.ts:12`, linking file:///home/me/project/src/index.ts#L12API
link(path, options?)
Wraps path in an OSC 8 hyperlink pointing at it on disk, labelled with the cwd-relative form (plus :<line>:<column> if given) or with whatever options.formatter returns. Relative input is resolved against options.cwd.
createLinker(defaults?)
Returns a { link } with defaults pre-applied, so you can configure a formatter and cwd once rather than at every call site:
const { link } = createLinker({
formatter: absolute => absolute.replace(rootDir, '~'),
})
console.log(link('/project/app/tailwind.config.ts'))
// label `~/tailwind.config.ts`, linking file:///project/app/tailwind.config.tsThe formatter receives the resolved absolute path, plus line and column if they were passed, and its return value is used without modification. (The default formatter is path.relative with a :<line>:<column> suffix.) Showing the position is up to the formatter; a label that omits it still links to it, so the file opens at the right line either way. Every LinkOptions key can be defaulted this way and overridden per call.
[!TIP] Pad and align inside the formatter, not on the result. The returned string contains invisible escapes, so
link(path).padEnd(20)pads to the wrong width, whileformatter: absolute => basename(absolute).padEnd(20)lines up as expected.
supportsHyperlinks(stream?)
Whether stream (default process.stdout) will render hyperlinks.
The environment is read on every call rather than snapshotted at module load, so a .env file or a --no-color flag applied after import is still respected. It costs around a microsecond; if you are linking in a hot loop, call this once and pass the result as enabled.
LinkOptions
| Option | Default | |
| --- | --- | --- |
| formatter | cwd-relative path plus :<line>:<column> | builds the label, given the absolute path, line and column |
| cwd | process.cwd(), read at call time | base for relative paths |
| stream | process.stdout | stream whose TTY state gates output |
| line / column | | shown as :<line>:<column> and linked as #L<line>,<column> |
| id | | OSC 8 id param, so a label wrapped across lines hovers as one link |
| enabled | detection result | force on/off |
Paths are converted with pathToFileURL, so spaces, #, ? and non-ASCII characters are percent-encoded. Windows-shaped inputs (C:\..., \\server\share\...) are converted on any platform.
Non-TTY and CI
[!NOTE] No escapes are emitted when the target stream isn't a TTY, or when
CIis set, as they would otherwise end up in log files. Netlify is the exception, since it renders build logs as HTML and never allocates a TTY.
Overrides, in order of precedence: FORCE_HYPERLINK (set to 0 to disable), --no-hyperlink / --hyperlink flags, then NO_COLOR / NO_HYPERLINK / NO_HYPERLINKS.
[!WARNING]
FORCE_HYPERLINK=1bypasses every check, including the CI and non-TTY ones. Escape sequences will end up in whatever you are redirecting to.
NO_COLOR disables hyperlinks here. Colour support and hyperlink support are not the same capability, so there is no general colour-support check, but someone who has asked for no escape sequences at all should get none.
Terminal support
| Terminal | Detection |
| --- | --- |
| Windows Terminal >= 1.4 | WT_SESSION (any other terminal on win32 is treated as unsupported) |
| VS Code >= 1.72 | TERM_PROGRAM=vscode + version |
| Cursor | TERM_PROGRAM=vscode + CURSOR_TRACE_ID (own 0.x version line) |
| iTerm2 >= 3.1 | TERM_PROGRAM=iTerm.app + version |
| WezTerm >= 20200620 | TERM_PROGRAM=WezTerm, including Nix's 0-unstable-YYYY-MM-DD scheme |
| ghostty | TERM_PROGRAM=ghostty or TERM=xterm-ghostty |
| kitty | TERM=xterm-kitty |
| Alacritty >= 0.11 | TERM=alacritty |
| zed, rio, Tabby, Warp, Orca | TERM_PROGRAM |
| GNOME Terminal / VTE >= 0.50.1 | VTE_VERSION (0.50.0 is excluded: it segfaults on hyperlinks) |
| tmux >= 3.4 | TERM_PROGRAM=tmux + version |
Terminal.app is explicitly unsupported: it ignores OSC 8, though it degrades gracefully to the plain label. TeamCity is excluded. Anything unrecognised gets no escapes, since a wrong positive prints visible junk.
Passing line (and optionally column) appends :12:3 to the default label and #L12,3 to the URL. Most terminals ignore the fragment and simply open the file, so treat jump-to-line as a hint. You can decide whether to show a position in a custom formatter if you want.
[!IMPORTANT] The comma in
#L12,3is deliberate. VS Code parses the fragment with/^L?(\d+)(?:,(\d+))?/, so#L12:3matches the line and silently drops the column.
Differences from terminal-link and supports-hyperlinks
- When hyperlinks are unsupported, only the label is printed.
terminal-linkfalls back to appending the raw URL (nuxt.config.ts file:///home/me/nuxt.config.ts), which is noise in a log file and breaks any width maths. Here the output is exactly what you would have printed anyway. - tmux is detected. tmux overwrites
TERM_PROGRAMwithtmux, hiding the outer terminal, sosupports-hyperlinksreports no support inside every tmux session. tmux itself has handled OSC 8 since 3.4, so that version and up are supported directly. - Detection reads the environment per call.
supports-hyperlinkssnapshotssupportsHyperlinks.stdoutat module load, so anything that mutates the environment after import is missed, such as loading a.envfile, or normalising--no-colorintoNO_COLORwhile parsing argv. - No colour-support coupling.
supports-hyperlinksreturns false wheneversupports-colordoes, but this isn't necessarily correct. (We still honourNO_COLORif set.) - The OSC 8
idparam is exposed, whichterminal-linkdoes not surface. - Labels are derived, not passed in.
terminal-link(text, url)makes every call site build both halves; here the path is the argument and the label comes from a formatter you configure once. - Zero dependencies, versus
supports-color+has-flag+ansi-escapes.
Credits
supports-hyperlinksandterminal-linkby Sindre Sorhus and James Talmage are excellent. Detection coverage in this package is informed bysupports-hyperlinks, reimplemented so there are no dependencies and no module-load-time caching. If you want hyperlinks in general rather than paths specifically,terminal-linkwould be a good choice.- Egmont Koblinger's hyperlinks in terminal emulators is the de facto OSC 8 spec.
💻 Development
- Clone this repository
- Enable Corepack using
corepack enable - Install dependencies using
pnpm install - Run interactive tests using
pnpm dev
License
Made with ❤️
Published under MIT License.
