From 4761c48d96e24c65ba165fb4c64ab825122b2b87 Mon Sep 17 00:00:00 2001 From: rootiest Date: Sat, 4 Jul 2026 02:02:28 -0400 Subject: [PATCH] feat(functions): add open-url and repo-open browser helpers Extract the browser-detection and launch logic out of config-help into a reusable open-url function, then build repo-open on top of it to open the current repo's origin remote in a browser. - open-url: resolve the best graphical browser (fish_help_browser -> BROWSER -> xdg-mime https handler -> known binaries -> xdg-open) and launch it backgrounded. Silent by default; -v/--verbose reports the browser, -s/--silent is accepted explicitly. Browser console chatter is discarded. - repo-open: normalize the origin URL from HTTPS and SSH/scp forms, deep-link to the current branch (falling back to the remote default) and sub-directory. Provider layout resolved via git config browse.provider, hostname heuristic (github/gitlab/gitea/bitbucket, codeberg->gitea), then github default. -p/--print emits the URL, -r/--root ignores the sub-directory. - config-help: replace the inlined browser block with an open-url call. - abbr: add open-repo and url-open abbreviations that expand to the canonical command names on space/enter. - docs: document both functions in the SSoT (fish-config.md + index) and add a repo-open row to the README doc-browsing table. --- README.md | 1 + conf.d/abbr.fish | 6 ++ docs/fish-config.index | 4 + docs/fish-config.md | 63 ++++++++++++++ functions/config-help.fish | 67 +------------- functions/open-url.fish | 126 +++++++++++++++++++++++++++ functions/repo-open.fish | 174 +++++++++++++++++++++++++++++++++++++ 7 files changed, 376 insertions(+), 65 deletions(-) create mode 100644 functions/open-url.fish create mode 100644 functions/repo-open.fish diff --git a/README.md b/README.md index a4e9ddc..a73e2e2 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,7 @@ To browse the docs from the terminal: | `help config --html` | Open HTML docs at the matching section anchor | | `help config --man` | Open the compiled man page via `man -l` | | `help config --man` | Open the man page jumping to the nearest match | +| `repo-open` | Open this repo's web page in the browser (deep-links to the current branch and sub-directory) | The pager falls back through: **ov** → **bat** → **man -l** → **less** → **cat**. diff --git a/conf.d/abbr.fish b/conf.d/abbr.fish index efc43cf..c0e118f 100644 --- a/conf.d/abbr.fish +++ b/conf.d/abbr.fish @@ -313,6 +313,12 @@ abbr -a ssct 'sudo systemctl status' abbr -a sscs 'sudo systemctl start' abbr -a sscr 'sudo systemctl restart' +### Alternate command names ### +# Expand to the canonical function name so muscle-memory typos still work, +# while surfacing the real command instead of silently forwarding to it. +abbr -a open-repo repo-open +abbr -a url-open open-url + ### History Expansions and Substitutions ### # Bash-style history expansion is opinionated (C3 overrides), gated atomically # with conf.d/tricks.fish, conf.d/puffer.fish, and functions/expand_*.fish. diff --git a/docs/fish-config.index b/docs/fish-config.index index faa37dd..9fc1e8e 100644 --- a/docs/fish-config.index +++ b/docs/fish-config.index @@ -157,6 +157,10 @@ spark=### spark yt-dlp=### yt-dlp miscfns=## 5.14 Miscellaneous config-help=### config-help +open-url=### open-url +url-open=### open-url +repo-open=### repo-open +open-repo=### repo-open config-update=### config-update config-settings=### config-settings config-toggle=### config-settings diff --git a/docs/fish-config.md b/docs/fish-config.md index c58b1ba..16619aa 100644 --- a/docs/fish-config.md +++ b/docs/fish-config.md @@ -1339,6 +1339,69 @@ Add -i (interactive confirmation) to destructive commands: Also available as: help config [SECTION] [FLAGS] +### open-url + + Synopsis: open-url [-s|--silent] [-v|--verbose] + open-url -h | --help + + Opens a URL or file:// URI in the best available graphical web browser, + backgrounded so it never blocks the terminal. Resolves a real browser + binary rather than deferring to xdg-open, whose MIME dispatch can hand + local text/html files to non-browser apps (e.g. ebook readers). + + Silent by default: prints nothing on success (errors always go to + stderr). Pass --verbose / -v to report which browser is launched; + --silent / -s is accepted for explicitness. + + Resolution order: + 1. $fish_help_browser (explicit override) + 2. $BROWSER (validated; errors if not a command) + 3. xdg-mime default handler for x-scheme-handler/https + 4. First known browser binary found in a built-in list + 5. xdg-open (last resort) + + open-url https://git.rootiest.dev/rootiest/fish-config + open-url "file://$HOME/.config/fish/docs/html/index.html" + + Used internally by config-help --html. + + Typo abbreviation: url-open (expands to open-url on space/enter). + +### repo-open + + Synopsis: repo-open [-p|--print] [-r|--root] + repo-open -h | --help + + Opens the web page for the current repository's `origin` remote in a + browser (via open-url). Deep-links to the current branch when it exists + on the remote — falling back to the remote's default branch (main/master) + otherwise — and to the current sub-directory when run below the repo root. + + The remote URL is normalized from HTTPS and SSH/scp forms + (git@host:owner/repo.git, ssh://…, https://…). The web path layout is + provider-specific; the provider is resolved in order: + + 1. git config browse.provider (per-repo or --global override) + 2. Hostname heuristic (github / gitlab / gitea / bitbucket; + codeberg → gitea) + 3. Default: github-style layout + + Self-hosted hosts the heuristic can't classify (a Gitea/GitLab instance + on a custom domain) need a one-time override: + + git config browse.provider gitea + + Flags: + --print / -p Print the resolved URL instead of opening it. + --root / -r Ignore the current sub-directory; link to the repo root. + --help / -h Show usage. + + repo-open + repo-open --print + repo-open --root + + Typo abbreviation: open-repo (expands to repo-open on space/enter). + ### config-update Synopsis: config-update [-h] [-n] [-f] diff --git a/functions/config-help.fish b/functions/config-help.fish index 428868a..a96f49e 100644 --- a/functions/config-help.fish +++ b/functions/config-help.fish @@ -138,71 +138,8 @@ function config-help --description 'Open the offline fish shell configuration ma set -l page_url "file://$html_dir/$html_path" - # Browser detection — mirrors fish's help.fish priority order but - # resolves actual browser binaries before falling back to xdg-open. - # xdg-open dispatches on the file's MIME type (text/html), which can - # be associated with non-browser apps (e.g. ebook readers). Using a - # real browser binary directly with a file:// URI avoids that lookup. - set -l graphical_browsers \ - firefox firefox-esr chromium chromium-browser google-chrome \ - brave-browser vivaldi vivaldi-stable epiphany falkon qutebrowser \ - opera x-www-browser htmlview - - set -l browser $fish_help_browser - - if not set -q browser[1] - if set -q BROWSER - echo $BROWSER | read -at browser - if not type -q $browser[1] - set_color red - echo "error: \$BROWSER '$browser[1]' is not a valid command" >&2 - set_color normal - return 1 - end - else - # Resolve the https scheme handler from xdg-mime and use its - # binary directly — most reliable on modern Linux desktops. - if type -q xdg-mime - set -l desk (xdg-mime query default x-scheme-handler/https 2>/dev/null) - if test -n "$desk" - set -l candidate (string replace -r '\.desktop$' '' -- $desk) - if type -q $candidate - set browser $candidate - end - end - end - - # Fall back to trying known browser binaries in order. - if not set -q browser[1] - for b in $graphical_browsers - if type -q -f $b - set browser $b - break - end - end - end - - # Last resort: xdg-open (may hit wrong app for local files). - if not set -q browser[1]; and type -q xdg-open - set browser xdg-open - end - end - end - - if not set -q browser[1] - set_color red - echo "error: could not find a web browser — set \$fish_help_browser or \$BROWSER" >&2 - set_color normal - return 1 - end - - set_color green - echo "Opening HTML docs in $browser[1]…" - set_color normal - - # Background the browser so it doesn't block the terminal. - sh -c '("$@") &' -- $browser $page_url - return 0 + open-url $page_url + return $status end # ── --man / -m ─────────────────────────────────────────────── diff --git a/functions/open-url.fish b/functions/open-url.fish new file mode 100644 index 0000000..865bb83 --- /dev/null +++ b/functions/open-url.fish @@ -0,0 +1,126 @@ +# Copyright (C) 2026 Rootiest +# SPDX-License-Identifier: AGPL-3.0-or-later + +# SYNOPSIS +# open-url [-s|--silent] [-v|--verbose] +# open-url --help +# +# DESCRIPTION +# Opens a URL (or file:// URI) in the best available graphical web browser, +# backgrounded so it never blocks the terminal. Resolves a real browser +# binary rather than deferring to xdg-open, whose MIME dispatch can hand +# local text/html files to non-browser apps (e.g. ebook readers). +# +# Silent by default: prints nothing on success (errors always go to stderr). +# +# Resolution order: +# 1. $fish_help_browser (explicit override) +# 2. $BROWSER (validated; errors if not a command) +# 3. xdg-mime default handler for x-scheme-handler/https +# 4. First known browser binary found in a built-in list +# 5. xdg-open (last resort) +# +# ARGUMENTS +# url The URL or file:// URI to open (required) +# -s, --silent Suppress success output (the default) +# -v, --verbose Print which browser is being launched +# -h, --help Print usage and exit +# +# RETURNS +# 0 Browser launched +# 1 No URL given, invalid $BROWSER, or no browser found +# +# EXAMPLE +# open-url https://git.rootiest.dev/rootiest/fish-config +# open-url -v "file://$HOME/.config/fish/docs/html/index.html" +function open-url --description 'Open a URL in the best available web browser' + argparse h/help s/silent v/verbose -- $argv + or return 1 + + if set -q _flag_help + echo "Usage: open-url [-s|--silent] [-v|--verbose] " + echo "Open a URL or file:// URI in the best available web browser." + echo + echo " -s, --silent Suppress success output (the default)" + echo " -v, --verbose Print which browser is being launched" + echo " -h, --help Show this help" + return 0 + end + + set -l url $argv[1] + if test -z "$url" + set_color red + echo "error: open-url requires a URL argument" >&2 + set_color normal + return 1 + end + + # Browser detection — mirrors fish's help.fish priority order but + # resolves actual browser binaries before falling back to xdg-open. + # xdg-open dispatches on the file's MIME type (text/html), which can + # be associated with non-browser apps (e.g. ebook readers). Using a + # real browser binary directly with a file:// URI avoids that lookup. + set -l graphical_browsers \ + firefox firefox-esr chromium chromium-browser google-chrome \ + brave-browser vivaldi vivaldi-stable epiphany falkon qutebrowser \ + opera x-www-browser htmlview + + set -l browser $fish_help_browser + + if not set -q browser[1] + if set -q BROWSER + echo $BROWSER | read -at browser + if not type -q $browser[1] + set_color red + echo "error: \$BROWSER '$browser[1]' is not a valid command" >&2 + set_color normal + return 1 + end + else + # Resolve the https scheme handler from xdg-mime and use its + # binary directly — most reliable on modern Linux desktops. + if type -q xdg-mime + set -l desk (xdg-mime query default x-scheme-handler/https 2>/dev/null) + if test -n "$desk" + set -l candidate (string replace -r '\.desktop$' '' -- $desk) + if type -q $candidate + set browser $candidate + end + end + end + + # Fall back to trying known browser binaries in order. + if not set -q browser[1] + for b in $graphical_browsers + if type -q -f $b + set browser $b + break + end + end + end + + # Last resort: xdg-open (may hit wrong app for local files). + if not set -q browser[1]; and type -q xdg-open + set browser xdg-open + end + end + end + + if not set -q browser[1] + set_color red + echo "error: could not find a web browser — set \$fish_help_browser or \$BROWSER" >&2 + set_color normal + return 1 + end + + if set -q _flag_verbose + set_color green + echo "Opening $browser[1]…" + set_color normal + end + + # Background the browser so it doesn't block the terminal, and discard its + # own console chatter (e.g. "Opening in existing browser session."). + sh -c '("$@") >/dev/null 2>&1 &' -- $browser $url + return 0 +end diff --git a/functions/repo-open.fish b/functions/repo-open.fish new file mode 100644 index 0000000..1713000 --- /dev/null +++ b/functions/repo-open.fish @@ -0,0 +1,174 @@ +# Copyright (C) 2026 Rootiest +# SPDX-License-Identifier: AGPL-3.0-or-later + +# SYNOPSIS +# repo-open [-p|--print] [-r|--root] +# repo-open --help +# +# DESCRIPTION +# Opens the web page for the current repository's `origin` remote in a +# browser (via open-url). Deep-links to the current branch when it exists +# on the remote, falling back to the remote's default branch (main/master) +# otherwise, and to the current sub-directory when invoked below the repo +# root. +# +# The remote URL is normalized from both HTTPS and SSH/scp forms +# (git@host:owner/repo.git, ssh://…, https://…). The web path layout is +# provider-specific; the provider is resolved in this order: +# +# 1. git config browse.provider (per-repo or --global override) +# 2. Hostname heuristic (github / gitlab / gitea / bitbucket; +# codeberg → gitea) +# 3. Default: github-style layout +# +# For a self-hosted host the heuristic can't classify (e.g. a Gitea or +# GitLab instance on a custom domain), set the provider once: +# +# git config browse.provider gitea +# +# ARGUMENTS +# -p, --print Print the resolved URL instead of opening it +# -r, --root Ignore the current sub-directory; link to the repo root +# -h, --help Print usage and exit +# +# RETURNS +# 0 URL opened (or printed) +# 1 Not a git repo, no origin remote, or browser launch failed +# +# EXAMPLE +# repo-open # open current branch (+ subdir) in browser +# repo-open --print # just print the URL +# repo-open --root # repo home page for the current branch +function repo-open --description 'Open the origin remote of the current repo in a browser' + argparse -X 0 h/help p/print r/root -- $argv + or return 1 + + if set -q _flag_help + echo "Usage: repo-open [-p|--print] [-r|--root]" + echo "Open the origin remote's web page for the current repo," + echo "deep-linking to the current branch and sub-directory." + echo + echo " -p, --print Print the URL instead of opening it" + echo " -r, --root Link to the repo root, ignoring the current sub-directory" + echo " -h, --help Show this help" + return 0 + end + + if not git rev-parse --is-inside-work-tree >/dev/null 2>&1 + set_color red + echo "error: not inside a git repository" >&2 + set_color normal + return 1 + end + + set -l remote (git remote get-url origin 2>/dev/null) + if test -z "$remote" + set_color red + echo "error: no 'origin' remote configured" >&2 + set_color normal + return 1 + end + + # ── Normalize remote → host + owner/repo path ──────────────── + set -l had_scheme 0 + if string match -qr '://' -- $remote + set had_scheme 1 + end + set -l u (string replace -r '^[a-z0-9]+://' '' -- $remote) # strip scheme + set u (string replace -r '^[^@/]+@' '' -- $u) # strip user@ + set u (string replace -r '\.git$' '' -- $u) # strip .git + if test $had_scheme -eq 0 + set u (string replace ':' '/' -- $u) # scp: first colon → path sep + else + set u (string replace -r ':[0-9]+/' '/' -- $u) # url: drop :port + end + + set -l parts (string split -m1 '/' -- $u) + set -l host $parts[1] + set -l repo_path $parts[2] + if test -z "$host"; or test -z "$repo_path" + set_color red + echo "error: could not parse origin remote: $remote" >&2 + set_color normal + return 1 + end + set -l base "https://$host/$repo_path" + + # ── Resolve provider (config → hostname → default) ─────────── + set -l provider (git config --get browse.provider 2>/dev/null) + if test -z "$provider" + switch $host + case 'github.com' 'www.github.com' + set provider github + case '*gitlab*' + set provider gitlab + case '*gitea*' 'codeberg.org' + set provider gitea + case '*bitbucket*' + set provider bitbucket + case '*' + set provider github # sensible default; override via browse.provider + end + end + + # ── Determine the default branch (for the "on default" check) ─ + set -l default_branch (git symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null \ + | string replace -r '^origin/' '') + if test -z "$default_branch" + for b in main master + if git rev-parse --verify --quiet refs/remotes/origin/$b >/dev/null 2>&1 + set default_branch $b + break + end + end + end + test -z "$default_branch"; and set default_branch main + + # ── Determine the branch to link to ────────────────────────── + set -l branch (git symbolic-ref --quiet --short HEAD 2>/dev/null) + if test -n "$branch" + # Prefer the local remote-tracking ref (offline, fast); fall back to a + # networked ls-remote before giving up on the current branch. + if not git rev-parse --verify --quiet refs/remotes/origin/$branch >/dev/null 2>&1 + if not git ls-remote --exit-code --heads origin $branch >/dev/null 2>&1 + set branch $default_branch + end + end + else + set branch $default_branch # detached HEAD + end + + # ── Sub-directory relative to repo root ────────────────────── + set -l prefix "" + if not set -q _flag_root + set prefix (git rev-parse --show-prefix 2>/dev/null | string trim -r -c /) + end + + # ── Assemble the provider-specific URL ─────────────────────── + set -l seg + switch $provider + case gitlab + set seg "/-/tree/$branch" + case gitea + set seg "/src/branch/$branch" + case bitbucket + set seg "/src/$branch" + case '*' # github and default + set seg "/tree/$branch" + end + + set -l url $base + if test -n "$prefix" + set url "$base$seg/$prefix" + else if test "$branch" != "$default_branch" + set url "$base$seg" + end + + if set -q _flag_print + echo $url + return 0 + end + + open-url $url + return $status +end