Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
.build
.git
.swiftpm
.appimage-tools
AppDir
*.AppImage
*.AppImage.sha256
59 changes: 48 additions & 11 deletions .github/workflows/linux-appimage.yml
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
# 在 tag 推送时为 Linux x86_64 构建 AppImage 并附加到对应 release。
# 从独立维护的 Linux 分支构建 x86_64 AppImage,并附加到已存在的共享 release。
#
# 流程:release.sh(Mac)构建 macOS 双变体 + 建 release + 推 tag → 本 workflow 被 tag 触发 →
# 在 swift:6.0-jammy 容器(glibc 2.35,覆盖 2022+ 发行版)里 checkout normal(含 Linux 源码)
# → 跑 scripts/build-appimage.sh → 把 TokenClock-x86_64.AppImage + .sha256 附加到该 release。
# 与 release.sh 之间是异步的(AppImage 约在发版后 5–10 分钟出现);softprops/action-gh-release
# 的 create-or-update 语义避免「release 还没建好」的竞态。
# linux-v1.4.0 映射到共享 release v1.4.0。构建源码和 workflow 都来自这个不可变的
# Linux tag,因此 Linux catalog、路径和 GTK 界面无需并入 macOS normal/main 分支。
name: linux-appimage

on:
push:
tags: ['v*']
workflow_dispatch: # 允许手动触发,便于在不打 tag 时验证
tags: ['linux-v*']
workflow_dispatch:
inputs:
release_tag:
description: 'Existing shared release tag, for example v1.4.0'
required: true
type: string

permissions:
contents: write # 附加资产到 release 需要
Expand All @@ -21,10 +23,44 @@ jobs:
container:
image: swift:6.0-jammy
steps:
- name: Checkout normal branch (Linux sources + build script)
- name: Checkout tagged Linux source
uses: actions/checkout@v4

- name: Map Linux tag to shared release tag
id: release
shell: bash
run: |
if [[ '${{ github.event_name }}' == 'workflow_dispatch' ]]; then
release_tag='${{ inputs.release_tag }}'
if [[ ! "$release_tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Unexpected shared release tag: $release_tag" >&2
exit 1
fi
else
linux_tag='${{ github.ref_name }}'
if [[ ! "$linux_tag" =~ ^linux-(v[0-9].*)$ ]]; then
echo "Unexpected Linux tag: $linux_tag" >&2
exit 1
fi
release_tag="${BASH_REMATCH[1]}"
fi
echo "tag=$release_tag" >> "$GITHUB_OUTPUT"

- name: Require an existing shared release
uses: actions/github-script@v7
env:
RELEASE_TAG: ${{ steps.release.outputs.tag }}
with:
ref: normal
script: |
try {
await github.rest.repos.getReleaseByTag({
owner: context.repo.owner,
repo: context.repo.repo,
tag: process.env.RELEASE_TAG,
});
} catch (error) {
core.setFailed(`Shared release ${process.env.RELEASE_TAG} does not exist: ${error.message}`);
}

- name: Build AppImage
run: |
Expand All @@ -34,7 +70,8 @@ jobs:
- name: Attach AppImage + SHA256 to the release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.ref_name }}
tag_name: ${{ steps.release.outputs.tag }}
fail_on_unmatched_files: true
files: |
TokenClock-x86_64.AppImage
TokenClock-x86_64.AppImage.sha256
11 changes: 11 additions & 0 deletions Dockerfile.linux
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ FROM builder AS tester
RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
curl \
adwaita-icon-theme \
fonts-noto-color-emoji \
fonts-noto-cjk \
librsvg2-common \
shared-mime-info \
xvfb \
&& rm -rf /var/lib/apt/lists/*

Expand All @@ -29,10 +34,16 @@ RUN runtime_mirror="$(printf '%s' "${UBUNTU_PORTS_MIRROR}" | sed 's@^https:@http
&& apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
ca-certificates \
adwaita-icon-theme \
fonts-noto-color-emoji \
fonts-noto-cjk \
libcurl4 \
libgtk-3-0 \
librsvg2-common \
libsqlite3-0 \
shared-mime-info \
&& rm -rf /var/lib/apt/lists/*

COPY --from=builder /src/.build/release/TokenClock /usr/local/bin/tokenclock-gui
COPY --from=builder /src/.build/release/TokenClock_TokenClock.resources /usr/local/bin/TokenClock_TokenClock.resources
ENTRYPOINT ["/usr/local/bin/tokenclock-gui"]
24 changes: 21 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ let linuxSources = [
"Services/AntigravityUsageService.swift",
"Services/AppPaths.swift",
"Services/ClaudeCodeUsageService.swift",
"Services/CodexQuotaService.swift",
"Services/ClineUsageService.swift",
"Services/CodexUsageService.swift",
"Services/ContinueUsageService.swift",
Expand All @@ -19,20 +20,30 @@ let linuxSources = [
"Services/GeminiUsageService.swift",
"Services/GrokUsageService.swift",
"Services/HermesUsageService.swift",
"Services/JSONLLineReader.swift",
"Services/HistoryStore.swift",
"Services/MockUsageService.swift",
"Services/ModelEmoji.swift",
"Services/ModelNormalizer.swift",
"Services/OpenClawUsageService.swift",
"Services/OpenCodeUsageService.swift",
"Services/PathConfig.swift",
"Services/PathDetector.swift",
"Services/QwenCodeUsageService.swift",
"Services/UsageAggregator.swift",
"Services/UsageServiceProtocol.swift",
"Linux/LinuxAPIServer.swift",
"Linux/LinuxApp.swift",
"Linux/LinuxAutostart.swift",
"Linux/LinuxClockTheme.swift",
"Linux/LinuxCustomTheme.swift",
"Linux/LinuxClockRenderer.swift",
"Linux/LinuxDetailsPanel.swift",
"Linux/LinuxSettingsWindow.swift",
"Linux/LinuxThemePicker.swift",
"Linux/LinuxWeatherService.swift",
"Linux/LinuxMain.swift",
"Linux/LinuxPathConfig.swift",
"Linux/LinuxPathDetector.swift",
"Linux/LinuxProviderCatalog.swift",
"Linux/LinuxUsageModel.swift",
]

Expand All @@ -59,17 +70,24 @@ let package = Package(
"Models/ClockFaceTheme.swift",
"Models/ClockSize.swift",
"Models/CustomThemeConfig.swift",
"Resources",
"Services/LaunchAgentHelper.swift",
"Services/PathConfig.swift",
"Services/PathDetector.swift",
"Services/UsageAPIServer.swift",
"Services/WeatherService.swift",
"ViewModel.swift",
"Views",
"main.swift",
],
sources: linuxSources,
resources: [.copy("Resources/glass_disc.png")],
swiftSettings: [.unsafeFlags(["-parse-as-library"])]
),
.testTarget(
name: "TokenClockTests",
dependencies: ["TokenClock"],
path: "Tests/TokenClockTests"
),
]
)
#else
Expand Down
36 changes: 35 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,41 @@ curl -fsSL https://raw.githubusercontent.com/Neo-Isshin/TokenClock/main/cli/inst

### Linux normal build

Linux intentionally ships the **normal classic dial only** (GTK3). It keeps the existing 14 local usage parsers, stores history under XDG data directories, registers XDG autostart, and exposes the same loopback API on `127.0.0.1:9988`. Liquid Glass, the macOS theme editor, and weather/location UI are not part of the Linux build.
Linux ships the **normal dial experience** in GTK3/Cairo: a transparent circular widget with the same on-dial information layout, all 8 built-in normal faces (Glass, Classic, Glacier, Midnight, Luxe, Antique, Railgun, and Sky), their hand shapes, numerals, ticks, and decorations. Left-click opens the themed session/model detail panel with percentages and weather forecast; right-click opens the visual face picker plus size, opacity, always-on-top, temperature, city, timezone, language, settings, XDG autostart, API copy, and About controls. The GTK settings window covers auto-detection, all 14 tool switches and data paths, live API configuration, usage thresholds, and saved custom faces. Automatic Linux weather uses wttr.in's IP-based location fallback; manually selected cities behave like macOS normal.

The Linux detail panel also includes **Codex Quota** immediately to the left of **By Percent**, showing available weekly/short-window allowance, reset time, plan, balance, and reset credits when present. Quota is fetched only when that view is opened (with a short cache and bounded fallback); it does not keep an `app-server` process or quota polling loop resident. Normal-alignment work also provides a compact 3×3 base-face picker with saved custom faces below it, persistent custom save/apply/delete/reset behavior, a 520×548 overview-and-disclosure settings window, and a Linux-specific About dialog. These are GTK/Cairo adaptations targeting the same normal workflow and visual character, not a claim of pixel-for-pixel AppKit rendering equivalence.

#### Linux provider catalog

Linux uses its own path catalog; it never imports macOS `~/Library/Application Support` or Windows `%APPDATA%`/`%LOCALAPPDATA%` locations. Resolution order is a saved custom path, the environment candidates shown below, the Linux default, then Linux-only alternates. XDG variables are used only where the provider or host application uses the XDG base-directory layout; an unset (or invalid relative) XDG directory falls back to the path after `:-`.

| Tool | Linux default / parser input | Environment candidates (in priority order) |
|------|------------------------------|--------------------------------------------|
| **OpenClaw** | `~/.openclaw/agents/*/sessions/*.jsonl` | `OPENCLAW_STATE_DIR`; `${OPENCLAW_HOME}/.openclaw` |
| **Claude Code** | `~/.claude/projects/**/*.jsonl` | `CLAUDE_CONFIG_DIR` |
| **Gemini CLI** | `~/.gemini/tmp/*/chats/session-*.(jsonl\|json)` | `${GEMINI_CLI_HOME}/.gemini`; `GEMINI_HOME`† |
| **Codex** | `~/.codex/sessions/**/rollout-*.jsonl` | `CODEX_HOME` |
| **Hermes** | `~/.hermes/state.db` (`sessions` table) | `HERMES_HOME` |
| **OpenCode** | `${XDG_DATA_HOME:-~/.local/share}/opencode/opencode.db` | `OPENCODE_DB` (file); `OPENCODE_HOME`†; `XDG_DATA_HOME` |
| **Qwen Code** | `~/.qwen/projects/*/chats/*.jsonl` | `QWEN_RUNTIME_DIR`; `QWEN_HOME` |
| **GitHub Copilot CLI** | `~/.copilot/session-state/*/events.jsonl` and optional OTel JSONL | `COPILOT_HOME`; `COPILOT_OTEL_FILE_EXPORTER_PATH` (file) |
| **Grok CLI** | `~/.grok/sessions/*/*/updates.jsonl` | `GROK_HOME`† |
| **Aider** | `${XDG_STATE_HOME:-~/.local/state}/aider/analytics.jsonl` (TokenClock convention) | `AIDER_ANALYTICS_LOG` (file); `AIDER_HOME`†; `XDG_STATE_HOME` |
| **Antigravity** | `~/.gemini/{antigravity-cli,antigravity-ide,antigravity}/conversations/*.db` | `ANTIGRAVITY_HOME`† |
| **Cline** | `${XDG_CONFIG_HOME:-~/.config}/Code/User/globalStorage/saoudrizwan.claude-dev/tasks/*/api_conversation.json` | `CLINE_HOME`†; `XDG_CONFIG_HOME` |
| **Continue** | `~/.continue/{dev_data,sessions}/*.jsonl` | `CONTINUE_HOME`† |
| **Cursor Agent** | `${XDG_CONFIG_HOME:-~/.config}/Cursor/User/globalStorage/state.vscdb`, then the authenticated Cursor usage API | `CURSOR_AGENT_HOME`†; `XDG_CONFIG_HOME` |

† TokenClock compatibility override, not a provider-documented environment contract. Custom and environment paths expand `~`, `$VAR`, and `${VAR}`. Cline also probes VSCodium, Code OSS, Cursor, VS Code Remote, and Cursor Remote global storage under their Linux user-data roots.

Detection reports three separate states internally: catalog entry declared, candidate path exists, and parser-readable source found. A path is counted as detected only after TokenClock can read a valid JSON/JSONL source or open the required SQLite table and columns.

Known limitations:

- Current OpenClaw releases can migrate transcripts to per-agent SQLite; TokenClock's OpenClaw parser still requires legacy JSONL transcripts.
- Aider does not create an analytics log by default. Start it with `--analytics-log <file>` or set `AIDER_ANALYTICS_LOG`; the XDG state path above is only TokenClock's Linux convention.
- Copilot session events may contain limited token detail. Full detail requires Copilot OTel file export; `COPILOT_OTEL_FILE_EXPORTER_PATH` is consumed directly.
- Cursor usage is not read from local token logs: TokenClock reads the local Cursor credential database and, when cloud fetching is enabled, calls Cursor's authenticated usage API.

**x86_64 — prebuilt AppImage (default):** the universal one-liner downloads a self-contained AppImage (GTK3 bundled, needs only glibc ≥ 2.35) — no Swift, no compilation, no dev headers:

Expand Down
36 changes: 35 additions & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,41 @@ curl -fsSL https://raw.githubusercontent.com/Neo-Isshin/TokenClock/main/cli/inst

### Linux normal 版

Linux 仅适配 **normal 经典不透明表盘**:使用 GTK3,复用现有 14 个本地用量解析器,历史数据按 XDG 目录保存,支持 XDG 登录自启动,并在 `127.0.0.1:9988` 提供同构 API。Linux 版不包含 Liquid Glass、macOS 主题编辑器和天气/定位界面。
Linux 使用 GTK3/Cairo 还原 **normal 表盘体验**:主窗口是透明的圆形小组件,盘面信息布局与 macOS normal 一致,并完整提供玻璃、经典、冰川、深夜、暗金、古风、超电磁炮、天空 8 套内置表盘及其指针形状、数字、刻度和装饰。左键展开与主题配套的详情面板,可按会话/模型分组、显示占比、展开来源并查看天气趋势;右键提供图形化表盘选择器、尺寸、透明度、置顶、温标、城市、时区、语言、设置、XDG 开机自启、复制 API 和关于。GTK 设置窗口包含自动探测、14 种工具开关与数据路径、API 即时配置、速率阈值和可保存的自定义表盘。Linux 自动天气使用 wttr.in 的 IP 定位回退,手选城市行为与 macOS normal 一致。

Linux 详情面板还在 **By Percent 左侧**加入 **Codex Quota**:在有数据时展示周额度/短周期额度的剩余量、重置时间、套餐、余额和 reset credits。额度只在用户打开该视图时按需获取,并使用短缓存和有界回退;不会常驻 `app-server` 子进程,也没有额度轮询循环。normal 对齐还包括紧凑的 3×3 基础表盘选择器(已保存自定义表盘排列在其后)、自定义表盘保存/应用/删除/重置与持久化、520×548 的概览式折叠设置窗口,以及 Linux 专属 About 信息。这些是对 normal 工作流与视觉气质的 GTK/Cairo 适配,不代表与 AppKit 像素级完全相同。

#### Linux provider catalog

Linux 使用独立的路径 catalog,不会导入 macOS 的 `~/Library/Application Support`,也不会导入 Windows 的 `%APPDATA%`/`%LOCALAPPDATA%`。解析顺序为:已保存的自定义路径、下表所列环境变量候选、Linux 默认路径、Linux 专属备选路径。只有 provider 或宿主应用遵循 XDG Base Directory 时才使用 XDG;XDG 变量未设置(或错误地设置成相对路径)时使用 `:-` 后的回退路径。

| 工具 | Linux 默认路径 / 解析器输入 | 环境变量候选(按优先级) |
|------|-----------------------------|--------------------------|
| **OpenClaw** | `~/.openclaw/agents/*/sessions/*.jsonl` | `OPENCLAW_STATE_DIR`;`${OPENCLAW_HOME}/.openclaw` |
| **Claude Code** | `~/.claude/projects/**/*.jsonl` | `CLAUDE_CONFIG_DIR` |
| **Gemini CLI** | `~/.gemini/tmp/*/chats/session-*.(jsonl\|json)` | `${GEMINI_CLI_HOME}/.gemini`;`GEMINI_HOME`† |
| **Codex** | `~/.codex/sessions/**/rollout-*.jsonl` | `CODEX_HOME` |
| **Hermes** | `~/.hermes/state.db`(`sessions` 表) | `HERMES_HOME` |
| **OpenCode** | `${XDG_DATA_HOME:-~/.local/share}/opencode/opencode.db` | `OPENCODE_DB`(文件);`OPENCODE_HOME`†;`XDG_DATA_HOME` |
| **Qwen Code** | `~/.qwen/projects/*/chats/*.jsonl` | `QWEN_RUNTIME_DIR`;`QWEN_HOME` |
| **GitHub Copilot CLI** | `~/.copilot/session-state/*/events.jsonl` 与可选的 OTel JSONL | `COPILOT_HOME`;`COPILOT_OTEL_FILE_EXPORTER_PATH`(文件) |
| **Grok CLI** | `~/.grok/sessions/*/*/updates.jsonl` | `GROK_HOME`† |
| **Aider** | `${XDG_STATE_HOME:-~/.local/state}/aider/analytics.jsonl`(TokenClock 约定) | `AIDER_ANALYTICS_LOG`(文件);`AIDER_HOME`†;`XDG_STATE_HOME` |
| **Antigravity** | `~/.gemini/{antigravity-cli,antigravity-ide,antigravity}/conversations/*.db` | `ANTIGRAVITY_HOME`† |
| **Cline** | `${XDG_CONFIG_HOME:-~/.config}/Code/User/globalStorage/saoudrizwan.claude-dev/tasks/*/api_conversation.json` | `CLINE_HOME`†;`XDG_CONFIG_HOME` |
| **Continue** | `~/.continue/{dev_data,sessions}/*.jsonl` | `CONTINUE_HOME`† |
| **Cursor Agent** | `${XDG_CONFIG_HOME:-~/.config}/Cursor/User/globalStorage/state.vscdb`,随后请求已认证的 Cursor usage API | `CURSOR_AGENT_HOME`†;`XDG_CONFIG_HOME` |

† TokenClock 兼容覆盖变量,不是 provider 官方公布的环境变量契约。自定义路径和环境变量路径支持展开 `~`、`$VAR`、`${VAR}`。Cline 还会探测 Linux 用户数据目录下的 VSCodium、Code OSS、Cursor、VS Code Remote 和 Cursor Remote global storage。

自动探测在内部区分三个状态:catalog 已声明、候选路径存在、解析器可读取。只有 TokenClock 能读取有效 JSON/JSONL,或能打开所需 SQLite 表及字段时,才计为探测成功。

已知限制:

- 当前 OpenClaw 可以把 transcript 迁移到每个 agent 的 SQLite 数据库;TokenClock 的 OpenClaw 解析器目前仍要求旧版 JSONL transcript。
- Aider 默认不会创建 analytics 日志,必须通过 `--analytics-log <file>` 或 `AIDER_ANALYTICS_LOG` 开启;上面的 XDG state 路径只是 TokenClock 的 Linux 约定。
- Copilot 的 session event 可能只包含有限的 token 明细;完整明细需要 Copilot OTel 文件,`COPILOT_OTEL_FILE_EXPORTER_PATH` 会被直接读取。
- Cursor 用量并非来自本地 token 日志:TokenClock 读取本地 Cursor 凭据数据库,并在启用云端获取时调用 Cursor 的已认证 usage API。

**x86_64 —— 预编译 AppImage(默认):** 通用一键命令会下载一个自带 GTK3 的 AppImage(仅要求 glibc ≥ 2.35),无需 Swift、无需编译、无需开发头文件:

Expand Down
Loading
Loading