Watch
1
0
Fork
You've already forked forgejo-sidetree
0
No description
  • JavaScript 57%
  • CSS 28.3%
  • HTML 9.4%
  • Go Template 5.3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Daniel Baumann daf580fff8
Adding debian version 0.2.1+dfsg-1.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2026-10-05 11:56:23 +02:00
debian Adding debian version 0.2.1+dfsg-1. 2026-10-05 11:56:23 +02:00
screenshots Adding upstream version 0.2.1+dfsg. 2026-10-05 11:48:45 +02:00
scripts Adding upstream version 0.2.1+dfsg. 2026-10-05 11:48:45 +02:00
src Adding upstream version 0.2.1+dfsg. 2026-10-05 11:48:45 +02:00
.gitignore Adding upstream version 0.2.1+dfsg. 2026-10-05 11:48:45 +02:00
AGENTS.md Adding upstream version 0.2.1+dfsg. 2026-10-05 11:48:45 +02:00
package-lock.json Adding upstream version 0.2.1+dfsg. 2026-10-05 11:48:45 +02:00
package.json Adding upstream version 0.2.1+dfsg. 2026-10-05 11:48:45 +02:00
README.md Adding upstream version 0.2.1+dfsg. 2026-10-05 11:48:45 +02:00

Forgejo Sidetree

A browser extension that adds a GitHub-style file tree sidebar to Forgejo repository pages. Supports both Chrome and Firefox (Manifest V3).

Screenshots

Dark theme Light theme
Dark Light

Features

  • Inline sidebar — file tree injected into the page layout as a flex child, no floating overlay, no fixed positioning
  • Custom Forgejo instance — configure any Forgejo hostname via the options page (default: empty, must be set before first use)
  • API token support — optional token for private repository access, stored in storage.sync
  • Directory-aware tree — shows all top-level directories via non-recursive git/trees/{sha} (no 1000-entry truncation)
  • Lazy subdirectory loading — expanding a directory fetches its contents via repos/{owner}/{repo}/contents/{path} (60s cache)
  • Auto-expand current path — on page load and SPA navigation, the current directory's ancestor chain is automatically expanded
  • File page support — when viewing a file, the sidebar shows its parent directory with the file highlighted (detected from API response, not DOM class)
  • Resizable sidebar — drag the divider between sidebar and content (16px hot zone, col-resize cursor on hover, ::before separator line)
  • Collapsible sidebar — toggle button in the repo toolbar (near branch selector), keyboard shortcut: Ctrl+Shift+E, toolbar icon click
  • Sticky sidebar — position: sticky; top: 0 keeps the sidebar visible while scrolling; parent overflow: hidden is auto-cleared
  • No install-time site access — the extension requests no host permissions up front; the options page asks for the one Forgejo host you name, and the sidebar is injected on demand into that origin only
  • Forgejo theme integration — uses Forgejo's CSS variables for seamless visual blending
  • Octicon SVG icons — folder (closed/open), file, and chevron icons via inline SVG data URIs (no emoji dependency)
  • Custom application icon — a node-and-branch tree (one root node branching to two children) drawn in Forgejo's own logo palette (#f60 root and trunk, #d40000 branches and child nodes), kept as icons/icon.svg and rasterised at 16/48/128 px with every edge on a whole pixel so it stays crisp in the toolbar

Design History

Iteration Approach Problem
v1 Floating sidebar panel, toggle button Floating overlay, not inline
v2 DOM-injected inline sidebar (flex wrapper) Content pushed to far right
v3 Inner flex wrapper, per-directory API Root URL not handled
v4 git/trees/{sha}?recursive=1 full tree <1000 files: instant; >=1000: truncated
v5 Root tree + per-dir lazy (unified) Every expand needs a network call (rejected)
v6 Smart truncation detection Can't detect completely missing dirs
v7 (current) Non-recursive root tree + per-dir lazy on expand Root always complete, expand is 1 network call

Why v7 won

  • git/trees/{sha} (no ?recursive=1) returns the root-level tree entries with no truncation — all top-level directories are always visible
  • Each directory expansion triggers one contents/{path} call, cached for 60s
  • Small repos (<1000 files) would have been faster with recursive, but the UX consistency of "all root dirs always visible" was preferred over the truncation headache

Build

Built with a minimal toolchain — one dev dependency:

npm install
npm run build          # both Firefox + Chrome

One dev dependency: web-ext (Firefox packaging). Manifest templates are rendered by scripts/render-manifest.js (stdlib node, no Mustache). Chrome zipping uses python3 -m zipfile (stdlib, no zip/7z).

Output

dist/
├── firefox.zip        # Signed Firefox extension (web-ext build)
└── chrome.zip         # Chrome extension (zip of src/)

Both zips are committed to the repo for easy sideloading.

Architecture

Cross-Browser Compatibility

Aspect Chrome Firefox
API namespace chrome.* browser.* (native)
Shim if (typeof browser === 'undefined') { var browser = chrome; } in each JS file N/A
Background service_worker scripts (array)
Manifest manifest.chrome.json.tmpl manifest.firefox.json.tmpl (with browser_specific_settings.gecko)
Scrollbar ::-webkit-scrollbar scrollbar-width: thin
Packaging cp + python3 -m zipfile web-ext build

Dependencies

  • web-ext — Firefox extension building
  • Everything else: stdlib (node.js, python3 -m zipfile, bash)

File Structure

forgejo-sidetree/
├── screenshots/               # Extension screenshots
├── scripts/
│   └── render-manifest.js     # Manifest template renderer (node stdlib, 17 lines)
├── src/
│   ├── background.js          # action.onClicked + commands.onCommand handlers
│   ├── content.css            # Sidebar styles (inline flex, sticky, Octicon SVG icons)
│   ├── content.js             # All extension logic (URL parsing, API, tree rendering)
│   ├── icons/                 # icon.svg source + 16/48/128 PNG
│   ├── manifest.chrome.json.tmpl  # → dist/chrome/manifest.json (built)
│   ├── manifest.firefox.json.tmpl # → src/manifest.json (built)
│   ├── options.html           # Settings page (hostname + API token)
│   └── options.js             # Settings logic
├── package.json               # One devDependency: web-ext
├── .gitignore
├── AGENTS.md -> README.md
└── README.md

content.js — Key Design Decisions

Settings (options page)

  • forgejoHost — hostname of the Forgejo instance (e.g. git.example.com). Default: empty.
  • forgejoToken — optional API token for private repo access.
  • Both stored in browser.storage.sync. Read once at startup via loadSettings().
  • Saving the options page also requests access: permissions.request() for https://<host>/* and http://<host>/*, then permissions.remove() for a previous host so a changed hostname leaves no standing grant behind.
  • The sidebar is injected by background.js from tabs.onUpdated, not by a static content_scripts entry — the hostname is unknown at install time. content.js sets globalThis.__fstLoaded on first run and throws on a second, so reloads cannot register every listener twice.
  • If unconfigured, a banner appears in the bottom-right corner with a link to the options page.
  • browser.runtime.onInstalled opens the options page automatically on first install.

API Strategy

init()
  └─ loadSettings() ── Read host + token from storage.sync
  ├─ (unconfigured) ── showConfigBanner(), return early
  └─ parseURL() ────── Currently: hardcoded FORGEJO_HOST.
  │                     Options page → storage.sync for dynamic host (wip)
  └─ fetchTreeRoot()  ── GET /repos/{owner}/{repo}/git/trees/{sha}
  │                       Returns ALL root-level entries (no limit)
  ├─ renderItems()    ── Render root items in #fst-tree
  └─ expandChain()    ── Walk currentPath from root, for each ancestor:
       └─ fetchDir()  ──── GET /repos/{owner}/{repo}/contents/{path}?ref={branch}
                            Fetches subdirectory contents on demand

API calls include Authorization: token <api_token> when configured.

File vs Directory Detection

The API response from contents/{path} returns an array for directories and a single object for files. The content script checks Array.isArray(items) to distinguish. If it's a single file object, the parent directory is extracted and re-fetched, with the file name highlighted in the tree.

Drag Resize

  • 16px hot zone between sidebar and content
  • #fst-drag-handle element — position: relative in flex flow
  • ::before pseudo-element draws the 1px separator line (--color-secondary)
  • cursor: col-resize on the handle — visible on hover across the full 16px zone
  • mousedown on element → mousemove/mouseup on document
  • Range: 180px–600px, default 280px

Sidebar Toggle

  • Toggle button — injected into .repo-button-row before .js-branch-tag-selector, uses Forgejo's ui basic small compact button class with Octicon sidebar-expand / sidebar-collapse SVG icons
  • Toolbar icon — browser.action.onClicked sends toggle message to content script
  • Keyboard shortcut — Ctrl+Shift+E via commands.onCommand('toggle-sidebar') + keydown fallback
  • .fst-wrapper-collapsed class: sidebar width → 0, drag handle hidden, content gets 14px left padding
  • Button removed on navigation away from tree pages

Keyboard shortcut fallback mechanism: The content script queries browser.commands.getAll() via the background at startup. If the toggle-sidebar command has a shortcut assigned (user set one in browser settings), the keydown fallback is skipped — the commands API handles it. If no shortcut is assigned (common on Edge unpacked), the keydown fallback registers Ctrl+Shift+E directly. A 200ms debounce prevents double-triggering.

Sticky Sidebar

  • position: sticky; top: 0; height: 100vh
  • At page top: sidebar occupies its natural position (below the Code tab, inside .page-content)
  • When scrolling past header: sticky kicks in, sidebar sticks flush to viewport top (no gap)
  • Parent overflow: hidden is cleared via JS walk-up to prevent sticky breakage
  • Sidebar is in normal flex flow (not position: fixed), so footer and Code tab are never covered

SPA Navigation

  • MutationObserver watches window.location.href changes
  • Same repo+branch: only update expand state + highlight (no API calls, no DOM rebuild)
  • Different repo/branch: full re-init (fetch new root tree, rebuild sidebar)
  • Non-tree page: remove sidebar entirely, restore .ui.container inline styles

content.css — Key Design Decisions

Property Value Why
Sidebar bg var(--color-body) Matches page background, not a separate panel
Node height 28px Compact, matches Forgejo row density
Depth spacer 14px per level Tighter indentation than default
Font size 13px Matches Forgejo body text
Drag handle 16px Wide enough for easy mouse targeting
Separator line 1px ::before Merged with drag handle, no duplicate border
Position sticky; top: 0 Follows page flow, doesn't cover footer/Code tab
Icons Octicon SVG masks No emoji dependency, theme-adapting color

Usage

  1. Install the extension (load unpacked in Chrome, or install the XPI in Firefox)
  2. Right-click the extension icon → Options → set your Forgejo hostname (required before first use; opens automatically on first install)
  3. Navigate to https://{your-forgejo-instance}/{owner}/{repo}/src/branch/{branch}/...
  4. The file tree sidebar appears on the left side of the page
  5. Click directories to expand/collapse
  6. Click files to navigate
  7. Drag the divider to resize the sidebar
  8. Click the sidebar toggle button in the toolbar (near branch selector) to collapse/expand

Keyboard Shortcuts

  • Ctrl+Shift+E — toggle sidebar. Customize or disable at chrome://extensions/shortcuts, edge://extensions/shortcuts, or about:addons (Firefox).
  • Toolbar icon click — also toggles the sidebar.

Ponytail Simplifications

The project was simplified using the ponytail (lazy senior dev) approach:

Before After Savings
9 devDependencies 1 (web-ext) 8 removed, 132M→90M node_modules
mustache (template) scripts/render-manifest.js (17 lines, node stdlib) No external dep
npm-run-all && shell chaining No external dep
addons-linter, eslint, stylelint Removed No config files, no lint deps
background.js (sidebar router, 23 lines) Simplified to 18-line action.onClicked + commands.onCommand Smaller background
sidebar.html + sidebar.js Removed Firefox sidebar_action dropped

Known Limitations

  • Branch switching within the same repo triggers a full re-init (root tree re-fetch)
  • File tree doesn't show at repo root URL (/{owner}/{repo}) — only on /src/branch/... pages
  • Large subdirectories (>1000 files at a single level) are fully loaded on expand — no internal pagination
  • Forgejo host is configured via the options page (default: empty, must be set before use). Saving also grants access to that one host — the extension requests no site access at install time.
  • Edge unpacked extensions: the commands API may not auto-register suggested_key. A keydown fallback listener fills this gap. If you customize the shortcut in edge://extensions/shortcuts, the fallback automatically disables itself.

Changelog

Version history, written for AMO / CWS review. Versions match package.json and the manifest version field.

0.2.1 — 2026-10-01

Changed

  • The manifest no longer declares browser_specific_settings.gecko_android. That sub-key is how an extension tells AMO it supports Firefox for Android; with it present, AMO listed this extension as Available on Firefox for Android and offered an Android QR code. It was added in 0.1.1 to silence a linter warning — Android needs data_collection_permissions, which landed in Android 142 while desktop got it in 140 — but the cost was advertising a platform this extension does not support: the sidebar is injected into Forgejo's desktop page layout, which is not what Firefox for Android renders. Dropping the key is the only way to undo that, because AMO locks its Android compatibility controls for any version whose manifest carries gecko_android.
  • The desktop strict_min_version rises from 140 to 142. That is what keeps the lint run clean without claiming Android support: the warning compares the desktop floor against the Android version that introduced data_collection_permissions, so a floor below 142 recreates it (verified — keeping 140 alongside the removed key reproduces KEY_FIREFOX_ANDROID_UNSUPPORTED_BY_MIN_VERSION). 142 is well behind current releases, so the practical effect is nil.

Notes for reviewers

  • No permission changes in this release: still storage + scripting, with host access granted per-host from the options page.
  • Not offering the extension on Firefox for Android is deliberate. The content script targets .page-content.repository.file.list and the options page carries no viewport handling, so neither is usable on mobile — listing it there only invites installs that cannot work.
  • Verified with web-ext lint on the packaged xpi: 0 errors, 0 notices, 0 warnings.

0.2.0 — 2026-10-01

Changed

  • The extension no longer asks for access to every site at install time. It declared content_scripts matching https://*/* and http://*/* plus the same patterns as host_permissions, so installing it produced the browser's "Access your data for all websites" prompt. Nothing needed that breadth: the Forgejo hostname is chosen by the user at setup, so it was there only because a manifest cannot know the hostname in advance. The manifest now declares those same patterns as optional_host_permissions, declares no content_scripts at all, and background.js injects the sidebar on demand into the single origin the user has actually granted, through scripting.executeScript and scripting.insertCSS.
  • The options page asks for access when you save. permissions.request() is called from the Save button's click handler — it only works from a user gesture, so it is the first thing awaited there — for https://<your-host>/* and http://<your-host>/*. Declining leaves the hostname unsaved and the sidebar off. Changing the hostname calls permissions.remove() for the previous host, so a site you no longer use does not keep standing access.
  • The options page reports "No access to <host> yet — save to grant it" on load when the stored hostname has no matching grant. That is the state an existing install lands in after this update.
  • tabs is no longer requested. tabs.onUpdated and tabs.sendMessage need no permission, and the only URL the background reads is one it already holds host access to. The permission set is now storage + scripting.
  • The token field's placeholder no longer implies a ghp_ prefix, which is GitHub's format rather than Forgejo's.

Notes for reviewers

  • This release strictly reduces what the extension can reach. Removed: the https://*/* and http://*/* host permissions, the content_scripts entry, and tabs. Added: scripting, which on-demand injection requires, and optional_host_permissions carrying the same broad patterns — a user can grant at most one specific host, and only from the options page.
  • The broad patterns in optional_host_permissions are unavoidable rather than careless: a self-hosted Forgejo can live on any domain and the hostname is not known until the user types it. Nothing is requested before that point, and permissions.request() is always called with one concrete origin (https://git.example.com/*), never with the whole pattern.
  • Injection is idempotent by two means: background.js probes the page for globalThis.__fstLoaded before injecting, and content.js sets that flag and throws if it runs a second time, so a reload cannot register every listener twice. Verified against a live Forgejo page by reloading it three times: wrapper, toggle button and tree-node counts held at 1 / 1 / 17 throughout.
  • Verification used the packaged build unpacked from dist/chrome.zip, not the source directory. With no host grant, a real Forgejo directory page shows no #fst-wrapper and no .fst-node. After the grant, the sidebar renders 17 nodes and the toggle button. The options Save flow reports success and stores the hostname, and the stored grant is confirmed with permissions.contains().
  • Upgrade note for existing installs: the old broad grant disappears with the update, so the sidebar stays off until the options page is opened and Save is pressed once. The options page states this on load rather than failing silently.
  • Still no remote code — no eval, no script loaded over the network. data_collection_permissions remains {"required": ["none"]}.

0.1.1 — 2026-10-01

Fixed

  • Sidebars broke on repositories containing a path with a quote or backslash. The path was interpolated raw into a querySelector attribute selector. Measured in Chromium: [data-path="weird"name"] throws SyntaxError (the sidebar's whole tree is replaced by the error message, because the throw happens inside init()'s try block), and [data-path="back\slash"] silently matches nothing (the auto-expand chain stops at that level — the sidebar renders but never expands to the page you are on). All three selector sites now pass the path through CSS.escape().

Changed

  • Internal refactor of content.js, no behavior change: one expandNode() now backs directory expand, the auto-expand chain, and SPA re-expansion, replacing three near-identical copies; the five module-scope variables collapsed into a single state object; node depth is read from data-depth rather than parsed back out of the inline --fst-depth custom property; the content container's style overrides are driven by one map shared by inject and teardown.
  • Removed dead code: an unused isFile heuristic in the SPA branch, and a fst-loading class toggle that had no matching CSS rule (only the #fst-loading placeholder exists).
  • The options reminder banner's styles moved from JavaScript into content.css.
  • New application icon. The placeholder green triangle PNG is replaced by an SVG-derived mark — a node-and-branch tree (one root node branching to two children), kept as icons/icon.svg and rasterised at 16/48/128 px. The palette is taken from Forgejo's own logo SVG (public/assets/img/logo.svg): orange #f60 for the root node and trunk, red #d40000 for the branches and their child nodes. That logo draws a commit graph — three nodes and two branching paths — in the same stroke language, so the mark reads as part of the same family. Measured against real toolbar backgrounds, the two colours are complementary: red is 4.92:1 on the light toolbar while orange is 5.02:1 on the dark one, so at least one half of the mark stays clearly visible in either theme. The mark is drawn as a tree rather than as a container: at 16px a filled panel with rows inside reads as a document or a list, while a node-and-branch shape reads as a hierarchy — the same visual language Octotree uses. Edges sit on whole pixels at 16px, so the toolbar size stays sharp instead of greying out from antialiasing.
  • package.json author is now tianheg; the retired tianheg.xyz URL is gone.

Notes for reviewers

  • No new permissions: still storage + tabs, with https://*/* and http://*/* host permissions.
  • No remote code — no eval, no script loaded from the network. Everything runs from the packaged files.
  • data_collection_permissions remains {"required": ["none"]}.
  • Verifying this release: the build was loaded into Chromium next to 0.1.0, and live DOM snapshots were diffed across eight interaction steps (initial render, expand, collapse, toolbar toggle, SPA navigation between paths, navigation to a non-tree page). Zero differences; viewport screenshots of both builds were byte-identical.

0.1.0 — 2026-06-19

First release.

Added

  • GitHub-style file tree sidebar on Forgejo repository pages, injected inline into the page layout as a flex child — no floating overlay, no fixed positioning
  • Directory-aware tree: non-recursive git/trees/{sha} for the root level (sidesteps the 1000-entry truncation that silently drops whole directories), lazy per-directory contents/{path} on expand, 60-second cache
  • Auto-expand of the current path's ancestor chain on load and on SPA navigation; file pages show the parent directory with the file highlighted
  • Resizable sidebar (drag divider, 180–600px) and collapsible via the toolbar button, the extension icon, or Ctrl+Shift+E
  • Sticky sidebar that follows the page while scrolling, clearing the parent overflow: hidden chain that would otherwise break position: sticky
  • Options page for any Forgejo hostname, plus an optional API token for private repositories (stored in storage.sync); a reminder banner appears until the hostname is set
  • Octicon SVG icons rendered through CSS masks — no emoji, no icon font, no network request
  • One source tree building both Chrome and Firefox packages (Manifest V3, browser/chrome shim, web-ext packaging)

Fixed during 0.1.0 development

  • Drag handle invisible and untargetable — an empty flex child collapses to zero height under align-items: flex-start
  • position: sticky silently broken by an ancestor overflow: hidden
  • Sidebar DOM discarded on SPA navigation; manual expand/collapse state not preserved
  • Toggle double-firing when both the commands API and the keydown fallback are live (200 ms debounce)
  • Toolbar icon dead on unpacked Edge/Chrome builds (added action.onClicked, plus the keydown fallback)
  • Firefox manifest validation: data_collection_permissions placement and strict_min_version format
  • Chrome build shipped without background.js

License

MPL-2.0