Make ids, titles, and URLs clickable in terminal output (OSC 8 hyperlinks)
メンテナーはふだん 1 日以内に返信
まだ誰も着手していません。
評価
- 難易度
- 4/5
- 見積もり時間
- 3〜5日
- 初心者へのやさしさ
- 58/100
- issue の種類
- 機能追加
- 明瞭さ
- おおむね明確
- 活発さ
- 活発
- 技術スタック
- go
- 領域
- cli, testing-qa
調査の方向性
ui.Match、root.go の NO_COLOR 処理、search.renderSpans から始め、続いて children.tree と find.table の padding パス、および一覧にある読み取り専用 renderer を追跡します。最初のパスでは 4 つのページ URL コマンドを対象とし、ダウンロードリンクに関する確認に実環境での検証が必要な場合は attachment-list を分けます。完了の条件は、プレーン JSON とパイプされた出力が変更されず、ui.Link にカバレッジがあり、各 renderer に fake-linker のテストがあることです。
索引モデルが issue の本文から書いたものです。
説明
Ids, titles, and URLs printed by the read-only commands are all things a reader wants to open, and right now opening one means selecting the URL and pasting it — or, in the columns that print only an id, constructing the URL by hand. Terminals have supported clickable text for years via OSC 8 hyperlinks, and markfluence already has the data to emit them: every command below already resolves a page URL for its --json output and then throws it away on the human path.
Mechanism
OSC 8 wraps visible text in an escape-delimited URL (ESC]8;;URL ESC\ text ESC]8;;ESC\). termenv.Hyperlink(url, text) emits exactly that, and github.com/muesli/termenv is already in go.mod as an indirect dependency via lipgloss — this promotes it to a direct one and adds no new module.
Support is good but not universal: iTerm2, WezTerm, kitty, Ghostty, VTE (GNOME Terminal), Konsole, foot, Windows Terminal, Alacritty, and tmux 3.4+ all handle it. macOS Terminal.app does not — it ignores the unknown OSC sequence and shows the plain text, which is the benign failure mode. There is no reliable capability query, so the plan is to emit unconditionally on a terminal and accept that an unsupported one degrades to exactly today's output.
What gets linked
| command | linked | destination | source of the URL |
|---|---|---|---|
children |
ID column |
the page/folder | pagetree.Node.URL (already populated) |
info |
id and url rows |
the page | report.url (already resolved) |
find |
ID and URL columns |
the page/folder | client.TitleMatch.URL |
search |
title line and URL line | the hit | client.SearchMatch.URL |
create |
the id and URL in Created page <id>: <url> |
the new page | createResult.url (already resolved via client.PageURL) |
update |
the URL in Published v<n>: <url> |
the updated page | updateResult.url |
attachment-list |
see below — needs a decision first | ||
attachment-upload |
see below — nothing to link today |
info's url row is the case where this pays off most, because its fallback form is the unreadable …/wiki/pages/viewpage.action?pageId=123, and the search title line is the case where the link text is most natural — a title reads like a link where a bare id does not.
Three things that need care
1. Column padding must be measured on the bare text. children.tree, find.table, and attachmentlist.table all pad with widths[j] - len([]rune(cell)). If the escape sequence is in the cell when that runs, every row's alignment breaks by ~20 invisible characters. The wrap has to be the last operation on a cell, after its pad is computed from the unwrapped value.
2. It needs a gate that termenv does not provide. Unlike ui.Match — which is safe to call anywhere because lipgloss renders it unstyled when color is off — termenv.Hyperlink is a plain sprintf and will happily write escapes into a pipe. So this wants a ui.Link(url, text string) string alongside Match, returning text unchanged when jsonMode is set, when IsPiped(), or when the lipgloss profile is Ascii (which is how --no-color reaches it, since root.go implements that flag by setting NO_COLOR). Following search.renderSpans's precedent, the renderers should take the linker as a parameter rather than calling ui.Link directly: tests run with stdout not a terminal, where the real helper returns plain text and a test wired to it would pass against unlinked output.
3. A cell that has no URL must not become a link. find prints - via orDash for a row whose space or URL could not be derived, attachment-list prints - for an unrecorded source, and search omits the URL line entirely when it is empty. A linked - pointing at the site root is worse than a plain one.
The write commands
create and update need nothing new: both already resolve the page URL through client.PageURL and interpolate it into a single success line ([file] Created page 123: https://…, [file] Published v4: https://…). The link wraps the id/URL substring inside the existing fmt.Sprintf, before it reaches ui.Success — so ui.Success and the rest of the ui printers are untouched, and the [file] prefix, warnings, and attachment lines all stay exactly as they are. A create --dry-run has no id or URL by design and takes its own branch already, so there is nothing there to link and nothing to guard.
attachment-upload is the one command in the list with nothing to link at all. It prints fmt.Sprintf("%-8s %s", a.Action, a.Filename), and client.SyncAction carries only Filename and Action — no attachment id, no URL, and no page URL, since the command never prints the page it uploaded to. Making anything there clickable means either plumbing an id/URL through SyncAction (which attachment-list's open question below has to settle first, because it is the same download-URL decision) or a file:// link to the local source file (the same local-link question, and its local path is not in SyncAction either). So it is listed for completeness but should probably ride along with whatever attachment-list decides rather than being designed on its own.
attachment-list is a separate decision
It is the one command in the list with no page-style URL to link, and that is deliberate. From cmd/attachmentlist/json.go:
There is deliberately no download_url: built on the site URL it fails under a scoped token, and built on the request base it would leak the gateway host into reader-facing output. attachment-download is how bytes are fetched.
Two candidates, neither obviously right:
SOURCE→ afile://link to the local source path. A different kind of link from every other one here (local, not Confluence), and it opens the file in whatever the desktop has registered — which for a.pngis probably right and for a.mdis probably not. The recorded path is relative to the documentation root, so it would need resolving to an absolute path, and the file may not exist on this machine at all: linking a path that 404s locally is worse than printing it plain.NAME→ the attachment's download URL. The--jsonrationale above does not transfer cleanly, because a clicked link is followed by the browser's own session rather than by the API token — so the scoped-token objection may not apply to a human-facing link the way it applies to a machine-readable field. That is an argument, not a measurement, and the gateway-host objection still stands. Worth verifying against the live instance before committing either way.
Reasonable outcome: do the four page-URL commands, and split attachment-list out to its own issue if the download-link question needs live verification.
Out of scope / unaffected
--jsonoutput does not change. Hyperlinks exist only on the human path, so the schema is untouched and no conformance test moves.- No guarantee changes. This is presentation; R1/R2 are unaffected.
- Exact-output tests stay green for free, since they run with stdout piped and the gate returns plain text there. New coverage goes on
ui.Linkitself plus one renderer test per command that passes a fake linker.
Open questions
- Should
--no-color/NO_COLORsuppress hyperlinks? They are not color. Honoring it means one switch for "give me plain output", which seems worth more than the literalism; a separate--no-hyperlinkscan wait until someone asks for it. childrenandfind: link theTITLEcolumn as well asID? The title is better link text, butchildrenindents titles by depth, so the click target would start at a ragged left edge.info: theparentrow prints a bare id, and bothparentTypeandspaceare known, so a link is constructible — but the folder URL form should be verified live rather than assumed. Same for linkingcreated/updatedauthors to their profiles, wherecreatorID/editorIDare already in hand but the profile URL form is unverified.- Does an archived page's URL (reachable via
find) open usefully, or land on an archive interstitial? create's two "already exists" failures embed a page URL in the message text (pageIDFailurecarriespage_id/urlfor exactly that reason), and those are the lines a reader most wants to click. Linking inside a composed error string is a different shape from linking a column, andpageref.NotFoundMessage-style wording is shared acrosscreate/update/fix— worth doing, but probably as a follow-up rather than in the first pass.- Should the
[file]prefix oncreate/updateoutput link to the local markdown file? Same local-link question asattachment-list'sSOURCE, and it would put afile://link on every single output line, which is likely more noise than help.
Related: #98 (a friendlier way to browse pages) wants the same thing this does — output you can navigate rather than transcribe.
- 主要言語
- Go
- スター
- 4
- フォーク
- 1
- 平均マージ
- 2時間 31分
- マージ済み PR(30日)
- 49
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートあり
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
mozilla/markfluence のほかの issue
-
read: a bold or italic span starting or ending with punctuation reads back as literal asterisksオープン
難易度 4/5 3〜5日 初心者へのやさしさ 48/100
mozilla/markfluence#216 ·
メンテナーはふだん 1 日以内に返信
-
難易度 3/5 1〜2日 初心者へのやさしさ 76/100
mozilla/markfluence#214 ·
メンテナーはふだん 1 日以内に返信
-
難易度 3/5 1〜2日 初心者へのやさしさ 68/100
mozilla/markfluence#213 ·
メンテナーはふだん 1 日以内に返信
-
enhancement
難易度 4/5 3〜5日 初心者へのやさしさ 48/100
mozilla/markfluence#210 ·
メンテナーはふだん 1 日以内に返信
-
documentation
難易度 5/5 1週間以上 初心者へのやさしさ 25/100
mozilla/markfluence#209 ·
メンテナーはふだん 1 日以内に返信
mozilla/markfluence の issue をすべて見る
似ている issue
-
unbounded value for n in `/v1/logs` can panic/OOM対応中かも このイシューにリンクされたプルリクエストがオープン中、またはマージ済みです。 オープンsmall item
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
canonical/pebble#1127 · コメント 2 件 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 89/100
open-policy-agent/opa#9366 · リアクション 1 件 ·
メンテナーはふだん 1 日以内に返信
-
defect
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
nats-io/nats-server#8752 ·
メンテナーはふだん 1 日以内に返信
-
enhancement needs-acceptance wg/router-models-inference-runtime
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
vllm-project/semantic-router#5015 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
fileserver: precompressed sidecar's Content-Encoding and Content-Length end up on error responses対応中かも @NitinKumar004 が今日担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 73/100
caddyserver/caddy#8204 ·
メンテナーはふだん 1 日以内に返信