Skip to content

Releases: mariolonghi/browser-tab-counter

v0.11.0

Choose a tag to compare

@github-actions github-actions released this 06 Aug 14:45
v0.11.0: handle very large tab counts (fixes #1)

Reported: with ~1600 tabs open, the export stalled and eventually crashed.
Reproduced against a real 1613-tab load. Four separate problems, only one of
which was the export.

1. The export made one Apple Event round-trip per property, per tab
   (title/URL/loading inside the tab loop): ~4800 IPC calls at 1600 tabs.
   Measured 53 ms/tab — 11.2 s at 213 tabs, ~85 s at 1613 (still running when
   killed at 31 s). Now uses bulk access, `title of every tab of w`, which is
   one round-trip per property per window. Cost scales with windows, not tabs.
   Applied to both the Chromium and Safari scripts.

2. A timeout silently returned an empty list, so past roughly 380 tabs the app
   reported "No open tabs found in the running browsers" — a failed read was
   indistinguishable from having no tabs. Failures now raise ExportError, which
   the existing dialog surfaces. Gather timeout 20 s -> 60 s; count timeout
   5 s -> 20 s (a browser with 1600 tabs needs 3-4 s just to answer a bulk
   count).

3. The counter was slower than its own poll timer, which is likely what the
   report actually experienced. running_process_names() asked System Events for
   the process list on every poll: 2248 ms measured. Combined with the count,
   count_all() took 4.72 s against a 4 s timer. Now uses NSWorkspace, a local
   API call needing no Automation permission: 44 ms. System Events kept as a
   fallback when PyObjC isn't available.

4. No re-entrancy guard, so slow polls stacked up and piled concurrent
   AppleScript calls on each other. refresh() now skips a tick if one is still
   in flight; the next catches up.

Results at 1613 tabs:
  export gather      ~85 s (timed out, returned nothing)  ->  2.0 s
  full export        failed                               ->  2.8 s
  count_all()        4.72 s (vs a 4 s timer)              ->  1.83 s
  process lookup     2248 ms                              ->  44 ms

A hypothesis that proved wrong, recorded so nobody re-does it: AppleScript's
`outp & ...` string building was assumed to be quadratic. Benchmarked at 1600
concatenations in 0.08 s. The IPC round-trips were the whole story.

Tests: a forced timeout must raise rather than return empty, and the bulk
query's true/false booleans must still map to the yes/no the CSV uses.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

v0.10.1

Choose a tag to compare

@github-actions github-actions released this 31 Jul 16:55
v0.10.1: update verification fails closed; CI test suite green

The new checks workflow caught a real bug on its first run, in code I wrote.

verify_app() shells out to codesign and spctl. If those tools can't be run at
all — a non-macOS host, a stripped system — subprocess raised a bare
FileNotFoundError instead of our UpdateError. Two consequences:

  * the user would have seen "Update failed: [Errno 2] No such file or
    directory: 'codesign'" instead of a comprehensible message, and
  * more importantly the failure mode was unclear at the exact moment it
    matters most: when we cannot verify an update we must refuse it.

_run_check() now wraps every verification call, converts OSError /
SubprocessError into a clean UpdateError, and adds a timeout. Verified it fails
CLOSED: with the tools removed from PATH, verify_app refuses the bundle rather
than letting an unverified one through.

Also fixes CI. I had claimed the test suite was stdlib-only and therefore safe
to run on Linux; that was wrong for test_selfupdate, which reached codesign
through verify_app. Checking only on macOS masked it. With the fail-closed fix
the test now passes on both platforms — confirmed by running the whole suite
with the macOS tools removed from PATH, and again normally.

New user-facing string translated into all six languages.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

v0.10.0

Choose a tag to compare

@github-actions github-actions released this 31 Jul 16:49
v0.10.0: French, Portuguese and Dutch; locale + test checks in CI

Completes localisations that were half-landed, and closes the gap that let them
land half-finished.

The problem: a language has to exist in three places — the .lproj permission
prompt, CFBundleLocalizations in setup.py, and the UI catalogues in
src/translations.py. fr/pt/nl had the first two but not the third, so macOS
offered the language in the per-app picker and showed a translated permission
prompt while every menu and dialog stayed English. check_locales.py reported
"OK: 7 locales" because it validated .lproj against the plist and had no
knowledge of the UI catalogues.

- src/translations.py: full French, Brazilian-Portuguese and Dutch catalogues
  (93 strings each). Portuguese follows pt-BR ("abas", "salvar") to match the
  wording already in pt.lproj.
- packaging/check_locales.py: also parses src/translations.py (via AST, never
  executing it) and fails when a language is advertised without a UI catalogue,
  or has a catalogue without a prompt. Verified it would have caught the state
  above: removing fr/pt/nl from CATALOGUES now produces three explicit errors.
- CI: the i18n workflow only triggered on packaging/**, so editing
  translations.py — the likeliest source of drift — never ran the check. It also
  never ran the test suite. Replaced with checks.yml, which runs locale parity
  AND all four suites on src/**, tests/** and packaging/**. Both are
  stdlib-only, so no dependency install and it finishes in seconds.
- requirements.txt: certifi refreshed (2026.6.17 -> 2026.7.22) and its floor
  raised from >=2024.0.0. It ships the CA roots that verify the self-update
  download, so a stale bundle mildly weakens that check.

Checked the 0.9.0 -> 0.10.0 version jump, which is the classic comparison trap
(string compare says "0.10.0" < "0.9.0"): updates._vtuple compares numerically,
so v0.9.0 users are correctly offered v0.10.0 and v0.10.0 doesn't downgrade.

Reviewed and left alone: check_locales.py parses setup.py via AST rather than
executing it, handles UTF-16 BOM, strips comments, and catches duplicate keys
and empty values; two suspected weaknesses (a "//" inside a value, and drift
detection) both probed and found sound. Workflow posture is correct —
contents: read, a concurrency group, no secrets.

Full review written up in the dossier (security-review.md, "Second review").

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

v0.9.0

Choose a tag to compare

@github-actions github-actions released this 29 Jul 15:55
v0.9.0: Swedish, Spanish and German

The app now speaks English, Swedish, Spanish and German, picking whichever the
Mac prefers. Menus, every dialog and button, the About panel, notifications,
update and self-update messages, and the HTML report (headings, column names,
meta line) are translated; dates are localised too.

- src/i18n.py: language detection + _() / ngettext() / localised dates.
  Detection reads macOS AppleLanguages (honours the per-app language override)
  and deliberately ignores Python's locale module, which reports "C" in our
  frozen app and would have shipped English to everyone.
- src/translations.py: dict catalogues keyed by the English source string.
  Chosen over gettext/.mo — 83 strings don't justify a compile step, and dicts
  stay reviewable in a diff. Anything missing falls back to English.
- packaging/locales: an InfoPlist.strings per language. The Automation
  permission prompt is rendered by macOS, so it needs .lproj files rather than
  gettext; CFBundleLocalizations declares the languages so macOS offers the
  per-app override in System Settings.
- Plurals: English "tab(s)" hides a problem the translations can't — one tab
  read "1 flikar" / "1 pestañas" / "1 Tabs". Added singular/plural variants for
  the menu total, the export dialog and the HTML meta line.
- CSV column headers stay English (machine-readable); the HTML report's visible
  headers are translated.
- tests/test_i18n.py: every used string is translated, no stale entries,
  placeholders survive translation, plurals differ, unknown strings fall back.

Verified: all suites pass; the .lproj files ship in the bundle; and with the
app's own preference domain in the search list (what the running app sees)
detection resolves to sv and the UI renders in Swedish.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

v0.8.0

Choose a tag to compare

@github-actions github-actions released this 29 Jul 15:12
v0.8.0: choose your export format (CSV or a sortable HTML report)

'Export open tabs to CSV…' becomes 'Export open tabs…' and asks for a format
first: Spreadsheet (CSV), Web page (HTML), or Cancel. The choice drives the file
extension; the rest of the flow (re-poll, timestamped name, reveal in Finder) is
unchanged.

New HTML report (tabexport.write_html) is a self-contained single file:
- same columns as the CSV, reordered for reading (title/url before the flags,
  which otherwise pushed them behind seven metadata columns)
- clickable links, opened with rel=noopener noreferrer
- click a heading to sort, click again to reverse, with a ▲/▼ indicator;
  window/tab sort numerically (1,2,3,10,20 — not lexicographically)
- sticky header, zebra striping, light/dark aware, long text ellipsized with the
  full value as a tooltip, table wrapped in overflow-x
- loads nothing external; the one ~20-line script does only the sorting

Injection safety: tab titles/URLs are untrusted text going into HTML, so every
value is html.escape'd and only http(s) URLs are linkified — a javascript: URL
is shown as text, never as an href. Regression-tested.

Both formats now end with a provenance note (app name, version, repo) read from
appinfo.py via a shared disclaimer_text(), so it can't drift from the build. In
the CSV it's a blank row plus one trailing line; data rows still parse intact.

Tests: HTML escaping/linkifying, HTML-vs-CSV column parity, CSV footer safety.
README updated. Rationale and the keep-the-script decision are recorded in the
dossier (build-log Phase 11).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

v0.7.0

Choose a tag to compare

@github-actions github-actions released this 29 Jul 13:03
v0.7.0: security hardening, clickable history sparkline, leaner CI

Security review of the whole codebase, CI and git history (no secrets ever
leaked; details in the dossier's security-review.md). Twelve fixes:

Security
- CI: scope the signing-cert secret to the steps that need it (it was job-wide,
  so every step incl. the third-party release action could read it).
- CI: pin softprops/action-gh-release to a commit SHA (supply chain).
- selfupdate: allow-list the download URL (https + GitHub hosts only) and cap
  the download at 200 MB, streamed.
- selfupdate: add 'codesign --verify --deep --strict' so the signature's
  integrity is checked explicitly, alongside spctl notarization + Team ID pin.
- tabcount: bounds-check the pure-Python LZ4 decoder — a bad match offset used
  to silently produce garbage via negative indexing; now raises, output capped.
- permissions: open_website() only accepts https:// URLs.

Bugs
- login_item: XML-escape plist values (a path with & produced a malformed
  plist launchd silently ignored). Verified with plutil -lint.
- prefs: atomic write (temp + rename) so a crash can't truncate prefs.json.
- selfupdate: don't unmount twice on error paths.
- updates: cap the release-API response read at 2 MB.
- app: Save-dialog title is per-action ('Save Tab History' vs 'Export Open Tabs').

UI / CI
- Menu history line capped at 45 chars; clicking the sparkline now saves the
  history CSV (replaces the 'Reveal tab-history file' item).
- CI: concurrency group cancels superseded runs; drop redundant pip upgrade.
- README: mockup + docs updated for the clickable sparkline.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

v0.6.2

Choose a tag to compare

@github-actions github-actions released this 29 Jul 10:43

Fixes a crash in the in-app updater (and hardens the whole app) against non-ASCII text.

What was wrong: launched from Finder, the app ran under a C/POSIX locale, so it decoded command output and files as ASCII and crashed on non-ASCII characters (em dashes, accents, emoji) — including during Update now. The whole app now runs in UTF-8 mode and every text path decodes UTF-8 explicitly.

⚠️ If you're on v0.6.1 or earlier: the fix is in the updater, so Update now can't bring you to v0.6.2 — please download this DMG and install it manually once (drag to Applications). After that, in-app updates work normally.

v0.6.1

Choose a tag to compare

@github-actions github-actions released this 29 Jul 10:00
v0.6.1: default Launch-at-Login on first run; cap menu width; README …

v0.6.0

Choose a tag to compare

@github-actions github-actions released this 29 Jul 09:33
v0.6.0: in-app self-update from the About panel

About → 'Update now' downloads the latest release, verifies it, swaps the app in
place, and relaunches — no manual drag-to-Applications, no admin (the app is
user-owned).

- src/selfupdate.py: download (certifi HTTPS) → mount → VERIFY (spctl notarized
  + codesign TeamIdentifier pinned to ZWXAL8XA46; a tampered/fake update can't
  install) → stage → detached helper waits for quit, ditto-swaps the bundle,
  clears quarantine, relaunches. Falls back to the release page when running
  from source or a read-only install.
- src/updates.py: expose the DMG asset browser_download_url.
- src/app.py: About shows 'Update now to vX' when a self-update can apply, else
  the old 'Download update' page button.
- tests/test_selfupdate.py; README documents it; version → 0.6.0.

Verified end-to-end: a running v0.6.0 downloaded the notarized v0.5.0, verified
it, replaced itself and relaunched; verify correctly rejects wrong-team and
non-notarized apps.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

v0.5.0

Choose a tag to compare

@github-actions github-actions released this 29 Jul 09:10
v0.5.0: export open tabs to CSV

- src/tabexport.py: on-demand snapshot of every open tab across running
  browsers -> CSV (browser, window, tab, active, pinned, loading, window_mode,
  last_accessed, title, url). Safari/Chromium via AppleScript, Firefox via the
  session file. Read-only, local, nothing persisted; private windows excluded.
- app.py: 'Export open tabs to CSV…' menu item -> re-poll -> gather -> native
  Save panel (app-name + timestamp filename) -> reveal in Finder.
- FIX: force UTF-8 decoding in tabcount._osascript. A py2app app launched from
  Finder runs under a C/POSIX locale, so subprocess text mode defaulted to ASCII
  and crashed on non-ASCII tab titles (em dashes, ellipses, emoji). Reproduced
  and fixed; also hardens counting. _osascript gains a timeout arg (export=20s).
- tests/test_tabexport.py; README documents the feature; version -> 0.5.0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>