Skip to content
Merged
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
82 changes: 65 additions & 17 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
mod antigravity;
mod app_proxy;
mod client_config;
mod jsonc;
mod antigravity;
mod codex;
mod jsonc;
mod kiro;
mod logging;
mod proxy;
Expand Down Expand Up @@ -57,6 +57,7 @@ pub(crate) fn show_or_create_main_window(app: &tauri::AppHandle) {
let _ = window.unminimize();
let _ = window.show();
let _ = window.set_focus();
sync_main_window_menu_item(app);
return;
}

Expand All @@ -78,9 +79,38 @@ pub(crate) fn show_or_create_main_window(app: &tauri::AppHandle) {
return;
}
set_main_window_visibility(&app_handle, true);
sync_main_window_menu_item(&app_handle);
});
}

pub(crate) fn hide_main_window(app: &tauri::AppHandle) {
set_main_window_visibility(app, false);
if let Some(window) = app.get_webview_window(MAIN_WINDOW_LABEL) {
if let Err(err) = window.destroy() {
tracing::warn!(error = %err, "destroy window failed");
}
}
sync_main_window_menu_item(app);
}

pub(crate) fn toggle_main_window(app: &tauri::AppHandle) {
let visible = app
.get_webview_window(MAIN_WINDOW_LABEL)
.and_then(|window| window.is_visible().ok())
.unwrap_or(false);
if visible {
hide_main_window(app);
} else {
show_or_create_main_window(app);
}
}

fn sync_main_window_menu_item(app: &tauri::AppHandle) {
if let Some(tray_state) = app.try_state::<tray::TrayState>() {
tray_state.sync_main_window_menu_item(app);
}
}

fn is_autostart_launch() -> bool {
std::env::args().any(|arg| arg == "--autostart")
}
Expand All @@ -92,7 +122,9 @@ async fn read_proxy_config(app: tauri::AppHandle) -> Result<proxy::config::Confi
}

#[tauri::command]
async fn preview_client_setup(app: tauri::AppHandle) -> Result<client_config::ClientSetupInfo, String> {
async fn preview_client_setup(
app: tauri::AppHandle,
) -> Result<client_config::ClientSetupInfo, String> {
client_config::preview(app).await
}

Expand All @@ -104,7 +136,9 @@ async fn write_claude_code_settings(
}

#[tauri::command]
async fn write_codex_config(app: tauri::AppHandle) -> Result<client_config::ClientConfigWriteResult, String> {
async fn write_codex_config(
app: tauri::AppHandle,
) -> Result<client_config::ClientConfigWriteResult, String> {
client_config::write_codex_config(app).await
}

Expand Down Expand Up @@ -134,14 +168,19 @@ async fn write_proxy_config(
"write_proxy_config apply_config done"
);
let log_level = config.log_level;
let app_proxy_url = proxy::config::app_proxy_url_from_config(&config).ok().flatten();
let app_proxy_url = proxy::config::app_proxy_url_from_config(&config)
.ok()
.flatten();
let paths = app.state::<Arc<token_proxy_core::paths::TokenProxyPaths>>();
if let Err(err) = proxy::config::write_config(paths.inner().as_ref(), config).await {
tracing::error!(error = %err, "write_proxy_config save failed");
tray_state.apply_error("保存失败", &err);
return Err(err);
}
tracing::debug!(elapsed_ms = start.elapsed().as_millis(), "write_proxy_config saved");
tracing::debug!(
elapsed_ms = start.elapsed().as_millis(),
"write_proxy_config saved"
);
let reload_start = Instant::now();
logging_state.apply_level(log_level);
app_proxy::set(&app_proxy_state, app_proxy_url).await;
Expand All @@ -168,7 +207,6 @@ async fn write_proxy_config(
}
}


#[tauri::command]
async fn read_dashboard_snapshot(
app: tauri::AppHandle,
Expand Down Expand Up @@ -226,9 +264,7 @@ async fn kiro_import_ide(
if trimmed.is_empty() {
return Err("Directory is required.".to_string());
}
kiro_store
.import_ide_tokens(PathBuf::from(trimmed))
.await
kiro_store.import_ide_tokens(PathBuf::from(trimmed)).await
}

#[tauri::command]
Expand All @@ -240,9 +276,7 @@ async fn kiro_import_kam(
if trimmed.is_empty() {
return Err("File path is required.".to_string());
}
kiro_store
.import_kam_export(PathBuf::from(trimmed))
.await
kiro_store.import_kam_export(PathBuf::from(trimmed)).await
}

#[tauri::command]
Expand Down Expand Up @@ -687,6 +721,7 @@ pub fn run() {
if let Some(window) = app_handle.get_webview_window(MAIN_WINDOW_LABEL) {
let _ = window.hide();
}
sync_main_window_menu_item(&app_handle);
} else {
show_or_create_main_window(&app_handle);
}
Expand All @@ -696,17 +731,23 @@ pub fn run() {
tauri::WindowEvent::Focused(true) => {
if window.label() == MAIN_WINDOW_LABEL {
set_main_window_visibility(window.app_handle(), true);
sync_main_window_menu_item(window.app_handle());
}
}
tauri::WindowEvent::CloseRequested { api, .. } => {
let tray_state = window.app_handle().try_state::<tray::TrayState>();
if tray_state.as_ref().map(|state| state.should_quit()).unwrap_or(false) {
if tray_state
.as_ref()
.map(|state| state.should_quit())
.unwrap_or(false)
{
return;
}
// 关闭即销毁 WebView,后台核心继续运行。
api.prevent_close();
if window.label() == MAIN_WINDOW_LABEL {
set_main_window_visibility(window.app_handle(), false);
hide_main_window(window.app_handle());
return;
}
if let Err(err) = window.destroy() {
tracing::warn!(error = %err, "destroy window failed");
Expand Down Expand Up @@ -763,14 +804,21 @@ pub fn run() {
app.run(|app_handle, event| match event {
tauri::RunEvent::ExitRequested { api, .. } => {
let tray_state = app_handle.try_state::<tray::TrayState>();
if tray_state.as_ref().map(|state| state.should_quit()).unwrap_or(false) {
if tray_state
.as_ref()
.map(|state| state.should_quit())
.unwrap_or(false)
{
return;
}
// 仅关闭窗口时阻止退出,允许托盘“退出”彻底结束进程。
api.prevent_exit();
}
#[cfg(target_os = "macos")]
tauri::RunEvent::Reopen { has_visible_windows, .. } => {
tauri::RunEvent::Reopen {
has_visible_windows,
..
} => {
// 点击 Dock 重新打开时,恢复主窗口。
if !has_visible_windows {
show_or_create_main_window(app_handle);
Expand Down
91 changes: 68 additions & 23 deletions src-tauri/src/tray.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, RwLock};
use std::time::Instant;
#[cfg(target_os = "macos")]
use std::time::Duration;
use std::time::Instant;

use tauri::image::Image;
use tauri::menu::{Menu, MenuItem, PredefinedMenuItem};
use tauri::tray::{TrayIcon, TrayIconBuilder};
use tauri::{AppHandle, Manager};

use crate::show_or_create_main_window;
use crate::proxy::config::{TrayTokenRateConfig, TrayTokenRateFormat};
use crate::proxy::service::{ProxyServiceHandle, ProxyServiceState, ProxyServiceStatus};
#[cfg(target_os = "macos")]
Expand All @@ -26,6 +25,8 @@ const MENU_STOP: &str = "tray_stop_proxy";
const MENU_RESTART: &str = "tray_restart_proxy";
const MENU_STATUS: &str = "tray_status";
const MENU_QUIT: &str = "tray_quit";
const SHOW_MAIN_WINDOW_TEXT: &str = "显示主窗口";
const HIDE_MAIN_WINDOW_TEXT: &str = "隐藏主窗口";

#[derive(Clone)]
pub(crate) struct TrayState {
Expand All @@ -34,6 +35,7 @@ pub(crate) struct TrayState {

struct TrayStateInner {
tray: AppTrayIcon,
show_item: AppMenuItem,
start_item: AppMenuItem,
stop_item: AppMenuItem,
restart_item: AppMenuItem,
Expand Down Expand Up @@ -130,6 +132,17 @@ impl TrayState {
let _ = self.inner.status_item.set_enabled(false);
}

pub(crate) fn sync_main_window_menu_item(&self, app: &AppHandle) {
let visible = app
.get_webview_window(crate::MAIN_WINDOW_LABEL)
.and_then(|window| window.is_visible().ok())
.unwrap_or(false);
let _ = self
.inner
.show_item
.set_text(main_window_menu_text(visible));
}

#[cfg(target_os = "macos")]
async fn update_token_rate_title(&self) {
let config = {
Expand Down Expand Up @@ -174,14 +187,15 @@ impl TrayState {
if !self.is_token_rate_enabled() {
return;
}
let current = self
.inner
.token_rate_loop_active
.load(Ordering::SeqCst);
let current = self.inner.token_rate_loop_active.load(Ordering::SeqCst);
if current != 0 {
return;
}
let loop_id = self.inner.token_rate_loop_counter.fetch_add(1, Ordering::SeqCst) + 1;
let loop_id = self
.inner
.token_rate_loop_counter
.fetch_add(1, Ordering::SeqCst)
+ 1;
if self
.inner
.token_rate_loop_active
Expand Down Expand Up @@ -211,18 +225,12 @@ impl TrayState {
if self.should_quit() {
return false;
}
self.inner
.token_rate_loop_active
.load(Ordering::SeqCst)
== loop_id
self.inner.token_rate_loop_active.load(Ordering::SeqCst) == loop_id
}

#[cfg(target_os = "macos")]
fn finish_token_rate_loop(&self, loop_id: u64) {
let active = self
.inner
.token_rate_loop_active
.load(Ordering::SeqCst);
let active = self.inner.token_rate_loop_active.load(Ordering::SeqCst);
if active == loop_id {
self.inner.token_rate_loop_active.store(0, Ordering::SeqCst);
}
Expand All @@ -233,7 +241,13 @@ pub(crate) fn init_tray(
app: &AppHandle,
proxy_service: ProxyServiceHandle,
) -> Result<TrayState, Box<dyn std::error::Error>> {
let show_item = MenuItem::with_id(app, MENU_SHOW, "显示主窗口", true, None::<&str>)?;
let show_item = MenuItem::with_id(
app,
MENU_SHOW,
main_window_menu_text(false),
true,
None::<&str>,
)?;
let start_item = MenuItem::with_id(app, MENU_START, "启动代理", true, None::<&str>)?;
let stop_item = MenuItem::with_id(app, MENU_STOP, "停止代理", false, None::<&str>)?;
let restart_item = MenuItem::with_id(app, MENU_RESTART, "重启代理", false, None::<&str>)?;
Expand Down Expand Up @@ -267,6 +281,7 @@ pub(crate) fn init_tray(
let tray_state = TrayState {
inner: Arc::new(TrayStateInner {
tray,
show_item: show_item.clone(),
start_item: start_item.clone(),
stop_item: stop_item.clone(),
restart_item: restart_item.clone(),
Expand All @@ -286,15 +301,17 @@ pub(crate) fn init_tray(
let id = event.id().as_ref();
match id {
MENU_SHOW => {
show_or_create_main_window(app);
crate::toggle_main_window(app);
}
MENU_START => {
let app = app.clone();
let tray_state = tray_state_for_menu.clone();
let proxy_service = proxy_for_menu.clone();
tauri::async_runtime::spawn(async move {
let proxy_context =
app.state::<crate::proxy::service::ProxyContext>().inner().clone();
let proxy_context = app
.state::<crate::proxy::service::ProxyContext>()
.inner()
.clone();
match proxy_service.start(&proxy_context).await {
Ok(status) => tray_state.apply_status(&status),
Err(err) => tray_state.apply_error("启动失败", &err),
Expand All @@ -316,8 +333,10 @@ pub(crate) fn init_tray(
let tray_state = tray_state_for_menu.clone();
let proxy_service = proxy_for_menu.clone();
tauri::async_runtime::spawn(async move {
let proxy_context =
app.state::<crate::proxy::service::ProxyContext>().inner().clone();
let proxy_context = app
.state::<crate::proxy::service::ProxyContext>()
.inner()
.clone();
match proxy_service.restart(&proxy_context).await {
Ok(status) => tray_state.apply_status(&status),
Err(err) => tray_state.apply_error("重启失败", &err),
Expand All @@ -334,6 +353,7 @@ pub(crate) fn init_tray(

#[cfg(target_os = "macos")]
tray_state.ensure_token_rate_loop();
tray_state.sync_main_window_menu_item(app);

Ok(tray_state)
}
Expand Down Expand Up @@ -378,11 +398,19 @@ fn format_rate_title(snapshot: TokenRateSnapshot, format: TrayTokenRateFormat) -
let has_output = snapshot.output > 0;
let has_tokens = has_input || has_output;
// ↑ 显示 input(有 input 时)或连接数(无 input 时)
let input_display = if has_input { snapshot.input } else { snapshot.connections };
let input_display = if has_input {
snapshot.input
} else {
snapshot.connections
};
// ↓ 始终显示 output
let output_display = snapshot.output;
// total 显示总 token 数(有 token 时)或连接数(无 token 时)
let total_display = if has_tokens { snapshot.total } else { snapshot.connections };
let total_display = if has_tokens {
snapshot.total
} else {
snapshot.connections
};
match format {
TrayTokenRateFormat::Combined => format!("{total_display}"),
TrayTokenRateFormat::Split => format!("↑{input_display} ↓{output_display}"),
Expand Down Expand Up @@ -422,6 +450,14 @@ fn compact_error(err: &str) -> String {
output
}

fn main_window_menu_text(visible: bool) -> &'static str {
if visible {
HIDE_MAIN_WINDOW_TEXT
} else {
SHOW_MAIN_WINDOW_TEXT
}
}

fn load_tray_icon() -> Result<Image<'static>, Box<dyn std::error::Error>> {
let bytes: &[u8] = if cfg!(debug_assertions) {
&include_bytes!("../icons/icon-state.dev.png")[..]
Expand All @@ -430,3 +466,12 @@ fn load_tray_icon() -> Result<Image<'static>, Box<dyn std::error::Error>> {
};
Ok(Image::from_bytes(bytes)?)
}

#[cfg(test)]
mod tests {
#[test]
fn main_window_menu_text_reflects_visibility() {
assert_eq!(super::main_window_menu_text(false), "显示主窗口");
assert_eq!(super::main_window_menu_text(true), "隐藏主窗口");
}
}