feat: clean-room rewrite — replace ported modules with original implementations

Every Rust module under src/ that previously contained upstream-derivative
code has been replaced by a from-scratch implementation:

  diag/    log + simplelog file appender (was: diagnose.rs)
  os/      color, dpi, registry, string, theme (was: theme.rs, native_interop.rs)
  net/     WinHTTP-based HTTP client (was: ureq + native-tls)
  i18n/    TOML-embedded locale tables (was: localization/*.rs)
  usage/   trait UsageProvider + ClaudeProvider + ChatGptProvider + refresh
           orchestrator + registry (was: poller.rs, models.rs)
  creds/   trait CredentialSource + local/WSL/Codex impls (was: poller.rs)
  tray/    stateless tray manager + tiny-skia anti-aliased badge renderer
           (was: tray_icon.rs)
  update/  release fetch + inline cmd /c handoff installer
           (was: updater.rs's helper-exe pattern)

Application files (app.rs, bubble.rs, panel.rs, settings.rs) migrated to
the new modules. main.rs declares only the new modules.

NOTICE deleted; LICENSE is plain Apache-2.0; README updated to credit
inspiration rather than claim derivation. Cargo.toml drops ureq + native-tls
+ winres in favour of log + simplelog + thiserror + toml + tiny-skia +
embed-resource. Build script swapped to embed-resource via res/icon.rc.

External contracts preserved unchanged: Anthropic + ChatGPT endpoints and
headers, ~/.claude/.credentials.json + Codex auth.json paths, WSL bridging
via wsl.exe, CLI-driven token refresh, GitHub Releases JSON shape, Windows
registry path for startup, single-instance mutex name.

Phase docs: plans/260516-0707-cleanroom-rewrite/.
This commit is contained in:
2026-05-16 10:09:43 +07:00
parent c0f3e3f860
commit aa6217d2cf
72 changed files with 6265 additions and 3788 deletions
+46
View File
@@ -0,0 +1,46 @@
// Color helpers for GDI.
//
// Win32 GDI stores colours as a 32-bit COLORREF in 0x00BBGGRR byte order
// (B in the low byte). We keep a normal `r,g,b` struct in code and convert
// at the FFI boundary.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Rgb {
pub r: u8,
pub g: u8,
pub b: u8,
}
impl Rgb {
pub const fn new(r: u8, g: u8, b: u8) -> Self {
Self { r, g, b }
}
/// Parse `#RRGGBB` or `RRGGBB`. Returns `None` on malformed input.
pub fn parse_hex(hex: &str) -> Option<Self> {
let s = hex.trim_start_matches('#');
if s.len() != 6 {
return None;
}
let r = u8::from_str_radix(&s[0..2], 16).ok()?;
let g = u8::from_str_radix(&s[2..4], 16).ok()?;
let b = u8::from_str_radix(&s[4..6], 16).ok()?;
Some(Self { r, g, b })
}
/// Pack into a Win32 COLORREF (`0x00BBGGRR`).
pub fn into_colorref(self) -> u32 {
(self.r as u32) | ((self.g as u32) << 8) | ((self.b as u32) << 16)
}
/// Linear interpolation between two colours. `t` is clamped to `[0, 1]`.
pub fn lerp(self, other: Rgb, t: f64) -> Rgb {
let t = t.clamp(0.0, 1.0);
let mix = |a: u8, b: u8| (a as f64 + (b as f64 - a as f64) * t).round() as u8;
Rgb {
r: mix(self.r, other.r),
g: mix(self.g, other.g),
b: mix(self.b, other.b),
}
}
}
+31
View File
@@ -0,0 +1,31 @@
// Per-window DPI helpers.
//
// The DPI of a window can differ from `GetDpiForSystem` on multi-monitor
// setups where the app is per-monitor DPI aware. Always prefer
// `GetDpiForWindow` for HWNDs that participate in the message loop.
use windows::Win32::Foundation::HWND;
use windows::Win32::UI::HiDpi::{GetDpiForSystem, GetDpiForWindow};
/// The default DPI Win32 reports for 100% scaling.
pub const BASE_DPI: u32 = 96;
/// DPI for the supplied window. Falls back to system DPI if the call fails.
pub fn for_window(hwnd: HWND) -> u32 {
let raw = unsafe { GetDpiForWindow(hwnd) };
if raw == 0 {
for_system()
} else {
raw.max(BASE_DPI)
}
}
/// Global system DPI. Cheap; safe to call from any thread.
pub fn for_system() -> u32 {
unsafe { GetDpiForSystem() }.max(BASE_DPI)
}
/// Scale a logical (96-DPI) pixel measurement to the given DPI.
pub fn scale(logical_px: i32, dpi: u32) -> i32 {
((logical_px as i64) * (dpi as i64) / (BASE_DPI as i64)) as i32
}
+14
View File
@@ -0,0 +1,14 @@
// `os` namespace: thin, typed wrappers over the slice of Win32 we use.
//
// Each submodule covers one concern (color conversion, UTF-16 strings,
// DPI math, registry I/O, theme detection). Nothing in here knows about
// the bubble UI or the polling loop — it's pure platform glue.
pub mod color;
pub mod dpi;
pub mod registry;
pub mod string;
pub mod theme;
pub use color::Rgb;
pub use string::to_utf16_nul;
+159
View File
@@ -0,0 +1,159 @@
// Typed wrapper over a tiny subset of the Win32 registry API.
//
// All operations target `HKEY_CURRENT_USER` keys by default (the app only
// reads/writes user-scoped state — startup entry, theme detection, etc.).
// Each call opens and closes the key internally; there is no caching so
// state changes by other processes are visible immediately.
use windows::core::PCWSTR;
use windows::Win32::Foundation::ERROR_SUCCESS;
use windows::Win32::System::Registry::{
RegCloseKey, RegDeleteValueW, RegOpenKeyExW, RegQueryValueExW, RegSetValueExW, HKEY,
HKEY_CURRENT_USER, KEY_READ, KEY_WRITE, REG_SZ,
};
use super::string::to_utf16_nul;
#[derive(Debug, thiserror::Error)]
pub enum RegistryError {
#[error("registry open failed for {key}: code {code}")]
Open { key: String, code: u32 },
#[error("registry write failed for {key}\\{value}: code {code}")]
Write { key: String, value: String, code: u32 },
}
/// Read a `REG_DWORD` value under `HKEY_CURRENT_USER\<subkey>`.
/// Returns `None` if the key or value does not exist.
pub fn read_u32(subkey: &str, value_name: &str) -> Option<u32> {
let subkey_w = to_utf16_nul(subkey);
let value_w = to_utf16_nul(value_name);
unsafe {
let mut hkey = HKEY::default();
let open = RegOpenKeyExW(
HKEY_CURRENT_USER,
PCWSTR::from_raw(subkey_w.as_ptr()),
0,
KEY_READ,
&mut hkey,
);
if open != ERROR_SUCCESS {
return None;
}
let mut data: u32 = 0;
let mut size: u32 = std::mem::size_of::<u32>() as u32;
let query = RegQueryValueExW(
hkey,
PCWSTR::from_raw(value_w.as_ptr()),
None,
None,
Some((&mut data as *mut u32) as *mut u8),
Some(&mut size),
);
let _ = RegCloseKey(hkey);
if query == ERROR_SUCCESS {
Some(data)
} else {
None
}
}
}
/// Test whether a value (any type) exists under `HKEY_CURRENT_USER\<subkey>`.
pub fn value_exists(subkey: &str, value_name: &str) -> bool {
let subkey_w = to_utf16_nul(subkey);
let value_w = to_utf16_nul(value_name);
unsafe {
let mut hkey = HKEY::default();
let open = RegOpenKeyExW(
HKEY_CURRENT_USER,
PCWSTR::from_raw(subkey_w.as_ptr()),
0,
KEY_READ,
&mut hkey,
);
if open != ERROR_SUCCESS {
return false;
}
let mut size: u32 = 0;
let query = RegQueryValueExW(
hkey,
PCWSTR::from_raw(value_w.as_ptr()),
None,
None,
None,
Some(&mut size),
);
let _ = RegCloseKey(hkey);
query == ERROR_SUCCESS
}
}
/// Write a string value as `REG_SZ` under `HKEY_CURRENT_USER\<subkey>`.
pub fn write_string(subkey: &str, value_name: &str, value: &str) -> Result<(), RegistryError> {
let subkey_w = to_utf16_nul(subkey);
let value_w = to_utf16_nul(value_name);
let data_w = to_utf16_nul(value);
unsafe {
let mut hkey = HKEY::default();
let open = RegOpenKeyExW(
HKEY_CURRENT_USER,
PCWSTR::from_raw(subkey_w.as_ptr()),
0,
KEY_WRITE,
&mut hkey,
);
if open != ERROR_SUCCESS {
return Err(RegistryError::Open {
key: subkey.to_string(),
code: open.0,
});
}
let bytes = std::slice::from_raw_parts(
data_w.as_ptr() as *const u8,
data_w.len() * std::mem::size_of::<u16>(),
);
let res = RegSetValueExW(
hkey,
PCWSTR::from_raw(value_w.as_ptr()),
0,
REG_SZ,
Some(bytes),
);
let _ = RegCloseKey(hkey);
if res == ERROR_SUCCESS {
Ok(())
} else {
Err(RegistryError::Write {
key: subkey.to_string(),
value: value_name.to_string(),
code: res.0,
})
}
}
}
/// Delete a value under `HKEY_CURRENT_USER\<subkey>`. Returns `Ok(())` even
/// if the value never existed.
pub fn delete_value(subkey: &str, value_name: &str) -> Result<(), RegistryError> {
let subkey_w = to_utf16_nul(subkey);
let value_w = to_utf16_nul(value_name);
unsafe {
let mut hkey = HKEY::default();
let open = RegOpenKeyExW(
HKEY_CURRENT_USER,
PCWSTR::from_raw(subkey_w.as_ptr()),
0,
KEY_WRITE,
&mut hkey,
);
if open != ERROR_SUCCESS {
return Err(RegistryError::Open {
key: subkey.to_string(),
code: open.0,
});
}
let _ = RegDeleteValueW(hkey, PCWSTR::from_raw(value_w.as_ptr()));
let _ = RegCloseKey(hkey);
Ok(())
}
}
+10
View File
@@ -0,0 +1,10 @@
// UTF-16 conversion helpers.
/// Encode a Rust `&str` as a NUL-terminated UTF-16 vector suitable for
/// passing to Win32 `PCWSTR`-typed parameters.
///
/// The result lives as long as the returned `Vec<u16>`; callers must keep
/// the vector alive across the FFI call.
pub fn to_utf16_nul(s: &str) -> Vec<u16> {
s.encode_utf16().chain(std::iter::once(0)).collect()
}
+16
View File
@@ -0,0 +1,16 @@
// Windows light/dark theme detection.
//
// Windows stores the current theme under
// `HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize`
// with a `SystemUsesLightTheme` DWORD: 1 means light, 0 means dark.
use super::registry;
const THEME_KEY: &str = r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize";
const LIGHT_VALUE: &str = "SystemUsesLightTheme";
/// `true` if the system is in dark mode. Defaults to dark when the registry
/// value is missing (matches Windows 11 first-boot behaviour).
pub fn is_dark() -> bool {
!matches!(registry::read_u32(THEME_KEY, LIGHT_VALUE), Some(1))
}