refactor: rebuild desktop app with Tauri

This commit is contained in:
leookun
2026-08-24 02:49:00 +08:00
parent a3ec2a0dfc
commit 4053a7fb20
808 changed files with 87359 additions and 181491 deletions
+229
View File
@@ -0,0 +1,229 @@
use std::{
process::Command,
sync::{
atomic::{AtomicBool, Ordering},
Mutex,
},
time::Duration,
};
use axum::{
extract::{Extension, Json},
http::StatusCode,
routing::post,
Router,
};
use tauri::{
async_runtime::JoinHandle, webview::Color, AppHandle, Manager, RunEvent, WebviewUrl,
WebviewWindowBuilder,
};
use tauri_plugin_opener::OpenerExt;
use tokio_util::sync::CancellationToken;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
#[cfg(dev)]
use cursor_server::config::ConsoleSource;
use cursor_server::{App, Config, Result};
#[cfg(not(dev))]
use crate::frontend;
use crate::tray;
pub(crate) const MAIN_WINDOW_LABEL: &str = "main";
struct DesktopRuntime {
shutdown: CancellationToken,
server: Mutex<Option<JoinHandle<Result<()>>>>,
exiting: AtomicBool,
}
#[tauri::command]
fn open_terminal_with_command(command: String) -> tauri::Result<()> {
#[cfg(target_os = "macos")]
{
let _ = command;
Command::new("open").args(["-a", "Terminal"]).status()?;
Ok(())
}
#[cfg(target_os = "windows")]
{
Command::new("cmd")
.args(["/C", "start", "cmd", "/K", &command])
.spawn()?;
Ok(())
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
{
let _ = command;
Err(tauri::Error::from(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"terminal guidance is unsupported on this platform",
)))
}
}
#[derive(serde::Deserialize)]
struct OpenExternalUrlRequest {
url: String,
}
fn open_external_url(app: &AppHandle, url: &str) -> std::result::Result<(), String> {
let parsed = url::Url::parse(url).map_err(|error| format!("invalid URL: {error}"))?;
if !matches!(parsed.scheme(), "http" | "https") || parsed.host_str().is_none() {
return Err("only absolute HTTP and HTTPS URLs are allowed".into());
}
app.opener()
.open_url(parsed.as_str(), None::<&str>)
.map_err(|error| format!("failed to open URL: {error}"))
}
async fn open_external_url_handler(
Extension(app): Extension<AppHandle>,
Json(request): Json<OpenExternalUrlRequest>,
) -> std::result::Result<StatusCode, (StatusCode, String)> {
open_external_url(&app, &request.url)
.map(|_| StatusCode::NO_CONTENT)
.map_err(|error| (StatusCode::BAD_REQUEST, error))
}
fn desktop_api_router(app: AppHandle) -> Router {
Router::new()
.route(
"/__byok-api__/api/desktop/open-external-url",
post(open_external_url_handler),
)
.layer(Extension(app))
}
fn create_main_window(app: &AppHandle, address: std::net::SocketAddr) -> tauri::Result<()> {
let url = format!("http://{address}/__byok-api__/")
.parse()
.expect("local frontend URL");
let builder = WebviewWindowBuilder::new(app, MAIN_WINDOW_LABEL, WebviewUrl::External(url))
.title("Cursor BYOK")
.inner_size(820.0, 558.0)
.min_inner_size(820.0, 558.0)
.center()
.background_color(Color(20, 20, 20, 255))
.decorations(cfg!(target_os = "macos"))
.shadow(true)
.resizable(true);
#[cfg(target_os = "macos")]
let builder = builder
.title_bar_style(tauri::TitleBarStyle::Overlay)
.hidden_title(true);
builder.build()?;
Ok(())
}
pub fn run() {
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "cursor_server=info".into()),
)
.with(tracing_subscriber::fmt::layer())
.init();
let app = tauri::Builder::default()
.invoke_handler(tauri::generate_handler![open_terminal_with_command])
.plugin(tauri_plugin_single_instance::init(|app, _, _| {
tray::show_main_window(app);
}))
.plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_updater::Builder::new().build())
.setup(|app| {
app.handle().plugin(tauri_plugin_autostart::init(
tauri_plugin_autostart::MacosLauncher::LaunchAgent,
None,
))?;
let config = Config::desktop()?;
#[cfg(dev)]
let config = {
let mut config = config;
config.console = Some(ConsoleSource::Proxy(
"http://127.0.0.1:1420"
.parse()
.expect("Vite development URL"),
));
config
};
let server = tauri::async_runtime::block_on(App::new(config))?
.merge_router(desktop_api_router(app.handle().clone()));
#[cfg(not(dev))]
let server = server.merge_router(frontend::router(app.handle().clone()));
let listener = tauri::async_runtime::block_on(server.bind())?;
let address = listener.local_addr()?;
tauri::async_runtime::block_on(server.harness().cleanup_stale_settings())?;
let shutdown = CancellationToken::new();
let server_shutdown = shutdown.clone();
let app_handle = app.handle().clone();
let task = tauri::async_runtime::spawn(async move {
let result = server.serve_on(listener, server_shutdown).await;
if let Err(error) = &result {
tracing::error!(%error, "desktop server stopped unexpectedly");
app_handle.exit(1);
}
result
});
app.manage(DesktopRuntime {
shutdown,
server: Mutex::new(Some(task)),
exiting: AtomicBool::new(false),
});
create_main_window(app.handle(), address)?;
tray::create(app)?;
Ok(())
})
.build(tauri::generate_context!())
.expect("failed to build Cursor BYOK desktop app");
app.run(|app, event| match event {
RunEvent::WindowEvent {
label,
event: tauri::WindowEvent::CloseRequested { api, .. },
..
} if label == MAIN_WINDOW_LABEL => {
let runtime = app.state::<DesktopRuntime>();
if !runtime.exiting.load(Ordering::Acquire) {
api.prevent_close();
if let Some(window) = app.get_webview_window(MAIN_WINDOW_LABEL) {
let _ = window.hide();
}
}
}
RunEvent::ExitRequested { api, .. } => {
let runtime = app.state::<DesktopRuntime>();
if !runtime.exiting.swap(true, Ordering::AcqRel) {
api.prevent_exit();
runtime.shutdown.cancel();
let server = runtime.server.lock().expect("server lock poisoned").take();
let app = app.clone();
tauri::async_runtime::spawn(async move {
if let Some(server) = server {
match tokio::time::timeout(Duration::from_secs(11), server).await {
Ok(Ok(Ok(()))) => {}
Ok(Ok(Err(error))) => {
tracing::error!(%error, "desktop server shutdown failed")
}
Ok(Err(error)) => tracing::error!(%error, "desktop server task failed"),
Err(_) => tracing::warn!("desktop server shutdown timed out"),
}
}
app.exit(0);
});
}
}
#[cfg(target_os = "macos")]
RunEvent::Reopen {
has_visible_windows: false,
..
} => tray::show_main_window(app),
_ => {}
});
}
+43
View File
@@ -0,0 +1,43 @@
//! Serves Tauri's embedded frontend assets through the local HTTP server.
use axum::{
body::Body,
extract::State,
http::{header, HeaderValue, Response, StatusCode, Uri},
routing::get,
Router,
};
use tauri::AppHandle;
pub(crate) fn router(app: AppHandle) -> Router {
Router::new()
.route("/__byok-api__/", get(asset))
.route("/__byok-api__/{*path}", get(asset))
.with_state(app)
}
async fn asset(State(app): State<AppHandle>, uri: Uri) -> Response<Body> {
let path = uri
.path()
.strip_prefix("/__byok-api__/")
.filter(|path| !path.is_empty())
.unwrap_or("index.html");
let Some(asset) = app.asset_resolver().get(path.to_string()) else {
return Response::builder()
.status(StatusCode::NOT_FOUND)
.body(Body::empty())
.expect("static not-found response");
};
let mut response = Response::new(Body::from(asset.bytes));
response.headers_mut().insert(
header::CONTENT_TYPE,
HeaderValue::from_str(&asset.mime_type)
.unwrap_or_else(|_| HeaderValue::from_static("application/octet-stream")),
);
if let Some(csp) = asset.csp_header.and_then(|value| value.parse().ok()) {
response
.headers_mut()
.insert(header::CONTENT_SECURITY_POLICY, csp);
}
response
}
+6
View File
@@ -0,0 +1,6 @@
mod desktop;
#[cfg(not(dev))]
mod frontend;
mod tray;
pub use desktop::run;
+5
View File
@@ -0,0 +1,5 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
cursor_byok_desktop::run();
}
+55
View File
@@ -0,0 +1,55 @@
use tauri::{
menu::{Menu, MenuItem, PredefinedMenuItem},
tray::TrayIconBuilder,
App, AppHandle, Manager,
};
#[cfg(target_os = "windows")]
use tauri::tray::{MouseButton, MouseButtonState, TrayIconEvent};
use crate::desktop::MAIN_WINDOW_LABEL;
const OPEN_MENU_ID: &str = "tray-open";
const QUIT_MENU_ID: &str = "tray-quit";
pub fn create(app: &mut App) -> tauri::Result<()> {
let open = MenuItem::with_id(app, OPEN_MENU_ID, "打开 Cursor BYOK", true, None::<&str>)?;
let separator = PredefinedMenuItem::separator(app)?;
let quit = MenuItem::with_id(app, QUIT_MENU_ID, "退出", true, None::<&str>)?;
let menu = Menu::with_items(app, &[&open, &separator, &quit])?;
TrayIconBuilder::with_id("main")
.icon(tauri::include_image!("./icons/32x32.png"))
.tooltip("Cursor BYOK")
.menu(&menu)
.show_menu_on_left_click(cfg!(target_os = "macos"))
.on_menu_event(|app, event| match event.id().as_ref() {
OPEN_MENU_ID => show_main_window(app),
QUIT_MENU_ID => app.exit(0),
_ => {}
})
.on_tray_icon_event(|tray, event| {
#[cfg(target_os = "windows")]
if let TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Up,
..
} = event
{
show_main_window(tray.app_handle());
}
#[cfg(not(target_os = "windows"))]
let _ = (tray, event);
})
.build(app)?;
Ok(())
}
pub fn show_main_window(app: &AppHandle) {
if let Some(window) = app.get_webview_window(MAIN_WINDOW_LABEL) {
let _ = window.unminimize();
let _ = window.show();
let _ = window.set_focus();
}
}