Releases: mariolonghi/browser-tab-counter
Releases · mariolonghi/browser-tab-counter
Release list
v0.11.0
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
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
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
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
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
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
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.
v0.6.1
v0.6.1: default Launch-at-Login on first run; cap menu width; README …
v0.6.0
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
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>