build: scaffold the Tauri v2 crate

Turn the package into a lib plus thin bin on Tauri v2 with a root-level
tauri.conf.json, capabilities, generated icons (keeping the hand-tuned
icon.ico) and a placeholder bubble page. The Win32 modules leave the
module tree until each one is ported; their sources stay on disk.

tauri-build now embeds the icon, a VERSIONINFO from the Cargo version and
a PerMonitorV2 manifest, replacing res/icon.rc and its stale 0.1.0.
This commit is contained in:
tiennm99 committed 2026-10-21 11:45:14 +07:00
1 parent bb09765be5
commit f64d81e6a1
18 files changed
+4656 -415

No files matched your search

+2
View File
@@ -1 +1,3 @@
/target
/node_modules
/gen
+6
View File
@@ -0,0 +1,6 @@
ui/
tests/
scripts/
plans/
docs/
node_modules/
Generated
+4421 -287
View File
File diff suppressed because it is too large. Load diff
+33 -17
View File
@@ -3,11 +3,23 @@ name = "claude-code-usage-bubble"
version = "0.5.0"
edition = "2021"
license = "Apache-2.0"
description = "Floating bubble showing Claude Code and Codex usage on Windows"
description = "Floating bubble showing Claude Code and Codex usage"
homepage = "https://github.com/tiennm99/claude-code-usage-bubble"
repository = "https://github.com/tiennm99/claude-code-usage-bubble"
[lib]
name = "claude_code_usage_bubble_lib"
path = "src/lib.rs"
[[bin]]
name = "claude-code-usage-bubble"
path = "src/main.rs"
[dependencies]
tauri = { version = "2", features = ["tray-icon", "macos-private-api"] }
tauri-plugin-single-instance = "2"
tauri-plugin-autostart = "2"
ureq = "3"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
dirs = "6"
@@ -18,27 +30,31 @@ tiny-skia = "0.11"
sha2 = "0.10"
self-replace = "1.5"
[dependencies.windows]
version = "0.58"
features = [
[target.'cfg(unix)'.dependencies]
libc = "0.2"
[target.'cfg(target_os = "linux")'.dependencies]
x11rb = "0.14"
libloading = "0.9"
[target.'cfg(windows)'.dependencies]
# Keep the major in step with the `windows` crate tauri pulls
# (`cargo tree -i windows`), so only one copy is compiled.
windows = { version = "0.62", features = [
"Win32_Foundation",
"Win32_Globalization",
"Win32_Graphics_Dwm",
"Win32_Graphics_Gdi",
"Win32_System_LibraryLoader",
"Win32_UI_Shell",
"Win32_UI_WindowsAndMessaging",
"Win32_System_Registry",
"Win32_System_Threading",
"Win32_Storage_FileSystem",
"Win32_Security",
"Win32_UI_HiDpi",
"Win32_UI_Input_KeyboardAndMouse",
"Win32_Networking_WinHttp",
]
"Win32_UI_WindowsAndMessaging",
"Win32_UI_Shell",
"Win32_Graphics_Dwm",
"Win32_Graphics_Gdi",
] }
# Same major as tauri's; used for the WebView2 runtime check.
webview2-com = "0.39"
[build-dependencies]
embed-resource = "3"
tauri-build = { version = "2", features = [] }
serde_json = "1"
[profile.release]
opt-level = "z"
+33 -12
View File
@@ -1,16 +1,37 @@
// Compiles `res/icon.rc` into the binary as Windows PE resources.
// `tauri-build` validates `tauri.conf.json` against the enabled crate
// features and generates the capability schemas under `gen/`. On Windows it
// also embeds the icon, a VERSIONINFO and the application manifest below.
//
// `embed-resource` shells out to `rc.exe` (when a Windows SDK is on PATH) or
// `windres` (MinGW) to produce a .res object that the linker bakes into the
// .exe alongside our Rust code. The .rc file references `src/icons/icon.ico`
// via a relative path; keep that path stable.
// `tauri.conf.json` has no `version`, so `Cargo.toml` stays the only version
// source. `tauri-build` writes VERSIONINFO only from the config version, so
// the Cargo version is merged in through `TAURI_CONFIG`, which `try_build`
// reads after the config file. Any `TAURI_CONFIG` passed by the CLI
// (`--config`) is kept.
//
// The manifest keeps tauri's default Common-Controls v6 dependency and adds
// PerMonitorV2 DPI awareness, so the process is DPI aware before any window
// exists instead of relying on a runtime call.
fn main() {
// `compile` returns a `CompilationResult` annotated `#[must_use]` on
// embed-resource 3.x. `manifest_optional()` collapses Linux/Mac no-op
// cases (where there's no `rc.exe`) into Ok while still surfacing real
// failures on Windows.
embed_resource::compile("res/icon.rc", embed_resource::NONE)
.manifest_optional()
.expect("Failed to compile Windows resources");
inject_cargo_version();
let windows = tauri_build::WindowsAttributes::new()
.app_manifest(include_str!("res/windows-app-manifest.xml"));
tauri_build::try_build(tauri_build::Attributes::new().windows_attributes(windows))
.expect("failed to run the tauri build script");
}
fn inject_cargo_version() {
let mut config = match std::env::var("TAURI_CONFIG") {
Ok(raw) => serde_json::from_str::<serde_json::Value>(&raw)
.expect("TAURI_CONFIG must be a JSON object"),
Err(_) => serde_json::json!({}),
};
let object = config
.as_object_mut()
.expect("TAURI_CONFIG must be a JSON object");
object
.entry("version")
.or_insert_with(|| env!("CARGO_PKG_VERSION").into());
std::env::set_var("TAURI_CONFIG", config.to_string());
}
+10
View File
@@ -0,0 +1,10 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Core IPC and window dragging for the bubble and panel windows. Plugins are driven from Rust only.",
"windows": ["bubble-*", "panel"],
"permissions": [
"core:default",
"core:window:allow-start-dragging"
]
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 515 B

BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

-32
View File
@@ -1,32 +0,0 @@
#include <winver.h>
1 ICON "..\\src\\icons\\icon.ico"
1 VERSIONINFO
FILEVERSION 0,1,0,0
PRODUCTVERSION 0,1,0,0
FILEFLAGSMASK 0x3fL
FILEFLAGS 0x0L
FILEOS 0x40004L
FILETYPE 0x1L
FILESUBTYPE 0x0L
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904E4"
BEGIN
VALUE "CompanyName", "tiennm99\0"
VALUE "FileDescription", "Claude Code Usage Bubble\0"
VALUE "FileVersion", "0.1.0\0"
VALUE "InternalName", "ClaudeCodeUsageBubble\0"
VALUE "LegalCopyright", "Copyright (C) 2026 tiennm99\0"
VALUE "OriginalFilename", "claude-code-usage-bubble.exe\0"
VALUE "ProductName", "Claude Code Usage Bubble\0"
VALUE "ProductVersion", "0.1.0\0"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1252
END
END
+20
View File
@@ -0,0 +1,20 @@
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<dependency>
<dependentAssembly>
<assemblyIdentity
type="win32"
name="Microsoft.Windows.Common-Controls"
version="6.0.0.0"
processorArchitecture="*"
publicKeyToken="6595b64144ccf1df"
language="*"
/>
</dependentAssembly>
</dependency>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness>
</windowsSettings>
</application>
</assembly>
+71
View File
@@ -0,0 +1,71 @@
//! Claude Code Usage Bubble on Tauri v2.
//!
//! The binary in `src/main.rs` only calls [`run`]. Every module is `pub` so
//! that items wired up incrementally do not trip `dead_code`.
pub mod diag;
use tauri::{RunEvent, WebviewUrl, WebviewWindowBuilder, WindowEvent};
/// Label of the scaffold window that proves the webview pipeline works.
const TEST_WINDOW_LABEL: &str = "bubble-test";
/// Starts the application and blocks until it exits.
pub fn run() {
let args: Vec<String> = std::env::args().collect();
let diagnose_enabled = args.iter().any(|a| a == "--diagnose");
match diag::init(diagnose_enabled) {
Ok(Some(path)) => log::info!("startup args={args:?} log_path={}", path.display()),
Ok(None) => {}
Err(err) => eprintln!("diagnostic log could not be opened: {err}"),
}
let app = tauri::Builder::default()
.setup(|app| {
let window = WebviewWindowBuilder::new(
app,
TEST_WINDOW_LABEL,
WebviewUrl::App("bubble.html".into()),
)
.title("Claude Code Usage Bubble")
.inner_size(220.0, 66.0)
.transparent(true)
.decorations(false)
.always_on_top(true)
.skip_taskbar(true)
.resizable(false)
.build()?;
// The process otherwise outlives its windows (see the exit
// handler below), so closing the only window must exit
// explicitly.
let handle = app.handle().clone();
window.on_window_event(move |event| {
if let WindowEvent::CloseRequested { .. } = event {
handle.exit(0);
}
});
Ok(())
})
.build(tauri::generate_context!());
let app = match app {
Ok(app) => app,
Err(err) => {
log::error!("tauri builder failed: {err}");
eprintln!("Claude Code Usage Bubble failed to start: {err}");
std::process::exit(1);
}
};
app.run(|_app, event| {
// Destroying the last window must not end the process; only an
// explicit `exit` (which carries an exit code) does.
if let RunEvent::ExitRequested {
code: None, api, ..
} = event
{
api.prevent_exit();
}
});
}
+4 -67
View File
@@ -1,70 +1,7 @@
#![windows_subsystem = "windows"]
// Several modules (creds, usage, update, os::dpi, …) expose API that the
// app surface doesn't fully call yet — they're scaffolding for in-progress
// port phases. Allow at the crate root rather than scattering attributes.
#![allow(dead_code)]
// Original infrastructure.
mod creds;
mod diag;
mod i18n;
mod net;
mod os;
mod tray;
mod update;
mod usage;
mod usage_color;
// Application surface.
mod app;
mod bubble;
mod panel;
mod settings;
// Release builds use the GUI subsystem so no console window opens with the
// app. Debug builds keep the console for log output.
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
let args: Vec<String> = std::env::args().collect();
let diagnose_enabled = args.iter().any(|a| a == "--diagnose");
if diagnose_enabled {
if let Ok(Some(path)) = diag::init(true) {
log::info!("startup args={args:?} log_path={}", path.display());
}
}
if let Some(exit_code) = update::run_cli(&args) {
if diagnose_enabled {
log::info!("cli mode exited with code {exit_code}");
}
std::process::exit(exit_code);
}
let wait_pid = args
.iter()
.position(|a| a == "--wait-pid")
.and_then(|i| args.get(i + 1))
.and_then(|s| s.parse::<u32>().ok());
if let Some(pid) = wait_pid {
if diagnose_enabled {
log::info!("waiting up to 5s for parent pid {pid} to exit");
}
update::handoff::wait_for_parent_exit(pid, 5_000);
}
let updated_to = args
.iter()
.position(|a| a == "--updated-to")
.and_then(|i| args.get(i + 1))
.cloned();
if diagnose_enabled {
log::info!("entering app::run (wait_pid={wait_pid:?} updated_to={updated_to:?})");
}
app::run(AppArgs {
wait_pid_present: wait_pid.is_some(),
updated_to,
});
}
pub struct AppArgs {
pub wait_pid_present: bool,
pub updated_to: Option<String>,
claude_code_usage_bubble_lib::run();
}
+25
View File
@@ -0,0 +1,25 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Claude Code Usage Bubble",
"mainBinaryName": "claude-code-usage-bubble",
"identifier": "io.github.tiennm99.claudecodeusagebubble",
"build": {
"frontendDist": "ui"
},
"app": {
"withGlobalTauri": true,
"macOSPrivateApi": true,
"windows": [],
"security": {
"csp": "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self'; connect-src ipc: http://ipc.localhost"
}
},
"bundle": {
"active": true,
"targets": ["appimage", "deb"],
"createUpdaterArtifacts": false,
"copyright": "Copyright (C) 2026 tiennm99",
"publisher": "tiennm99",
"icon": []
}
}
+31
View File
@@ -0,0 +1,31 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Claude Code Usage Bubble</title>
<style>
html,
body {
margin: 0;
height: 100%;
background: transparent;
overflow: hidden;
}
.pill {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
border-radius: 9999px;
background: #1f1f1f;
color: #f0f0f0;
font: 600 14px "Segoe UI", system-ui, sans-serif;
}
</style>
</head>
<body>
<div class="pill">Claude Code Usage Bubble</div>
</body>
</html>