[READ-ONLY] Mirror of https://github.com/danielroe/clickable-path. Zero-dependency package to print paths as OSC 8 hyperlinks.
cli osc8
0

Configure Feed

Select the types of activity you want to include in your feed.

TypeScript 92.5%
JavaScript 3.1%
Other 4.4%
8 3 1

Clone this repository

https://git.vm.fail/danielroe.dev/clickable-path https://git.vm.fail/did:plc:vglgfdr3j2sqjdwgb5uvsyff
ssh://git@knot1.tangled.sh:2222/danielroe.dev/clickable-path ssh://git@knot1.tangled.sh:2222/did:plc:vglgfdr3j2sqjdwgb5uvsyff

For self-hosted knots, clone URLs may differ based on your setup.


README.md

clickable-path#

npm version npm downloads Github Actions Codecov

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 pathToFileURL gained the windows option 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#L12

API#

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.ts

The 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, while formatter: 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 CI is 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=1 bypasses 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,3 is deliberate. VS Code parses the fragment with /^L?(\d+)(?:,(\d+))?/, so #L12:3 matches the line and silently drops the column.

  • When hyperlinks are unsupported, only the label is printed. terminal-link falls 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_PROGRAM with tmux, hiding the outer terminal, so supports-hyperlinks reports 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-hyperlinks snapshots supportsHyperlinks.stdout at module load, so anything that mutates the environment after import is missed, such as loading a .env file, or normalising --no-color into NO_COLOR while parsing argv.
  • No colour-support coupling. supports-hyperlinks returns false whenever supports-color does, but this isn't necessarily correct. (We still honour NO_COLOR if set.)
  • The OSC 8 id param is exposed, which terminal-link does 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#

💻 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.