|
|
@@ -2,6 +2,7 @@ mod cli;
|
|
|
mod window_customizer;
|
|
|
|
|
|
use cli::{get_sidecar_path, install_cli, sync_cli};
|
|
|
+use futures::FutureExt;
|
|
|
use std::{
|
|
|
collections::VecDeque,
|
|
|
net::{SocketAddr, TcpListener},
|
|
|
@@ -9,10 +10,9 @@ use std::{
|
|
|
time::{Duration, Instant},
|
|
|
};
|
|
|
use tauri::{
|
|
|
- path::BaseDirectory, AppHandle, LogicalSize, Manager, RunEvent, WebviewUrl, WebviewWindow,
|
|
|
+ path::BaseDirectory, AppHandle, LogicalSize, Manager, RunEvent, State, WebviewUrl,
|
|
|
+ WebviewWindow,
|
|
|
};
|
|
|
-use tauri_plugin_clipboard_manager::ClipboardExt;
|
|
|
-use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogResult};
|
|
|
use tauri_plugin_shell::process::{CommandChild, CommandEvent};
|
|
|
use tauri_plugin_shell::ShellExt;
|
|
|
use tokio::net::TcpSocket;
|
|
|
@@ -20,7 +20,26 @@ use tokio::net::TcpSocket;
|
|
|
use crate::window_customizer::PinchZoomDisablePlugin;
|
|
|
|
|
|
#[derive(Clone)]
|
|
|
-struct ServerState(Arc<Mutex<Option<CommandChild>>>);
|
|
|
+struct ServerState {
|
|
|
+ child: Arc<Mutex<Option<CommandChild>>>,
|
|
|
+ status: futures::future::Shared<tokio::sync::oneshot::Receiver<Result<(), String>>>,
|
|
|
+}
|
|
|
+
|
|
|
+impl ServerState {
|
|
|
+ pub fn new(
|
|
|
+ child: Option<CommandChild>,
|
|
|
+ status: tokio::sync::oneshot::Receiver<Result<(), String>>,
|
|
|
+ ) -> Self {
|
|
|
+ Self {
|
|
|
+ child: Arc::new(Mutex::new(child)),
|
|
|
+ status: status.shared(),
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ pub fn set_child(&self, child: Option<CommandChild>) {
|
|
|
+ *self.child.lock().unwrap() = child;
|
|
|
+ }
|
|
|
+}
|
|
|
|
|
|
#[derive(Clone)]
|
|
|
struct LogState(Arc<Mutex<VecDeque<String>>>);
|
|
|
@@ -35,7 +54,7 @@ fn kill_sidecar(app: AppHandle) {
|
|
|
};
|
|
|
|
|
|
let Some(server_state) = server_state
|
|
|
- .0
|
|
|
+ .child
|
|
|
.lock()
|
|
|
.expect("Failed to acquire mutex lock")
|
|
|
.take()
|
|
|
@@ -49,8 +68,7 @@ fn kill_sidecar(app: AppHandle) {
|
|
|
println!("Killed server");
|
|
|
}
|
|
|
|
|
|
-#[tauri::command]
|
|
|
-async fn copy_logs_to_clipboard(app: AppHandle) -> Result<(), String> {
|
|
|
+async fn get_logs(app: AppHandle) -> Result<String, String> {
|
|
|
let log_state = app.try_state::<LogState>().ok_or("Log state not found")?;
|
|
|
|
|
|
let logs = log_state
|
|
|
@@ -58,25 +76,16 @@ async fn copy_logs_to_clipboard(app: AppHandle) -> Result<(), String> {
|
|
|
.lock()
|
|
|
.map_err(|_| "Failed to acquire log lock")?;
|
|
|
|
|
|
- let log_text = logs.iter().cloned().collect::<Vec<_>>().join("");
|
|
|
-
|
|
|
- app.clipboard()
|
|
|
- .write_text(log_text)
|
|
|
- .map_err(|e| format!("Failed to copy to clipboard: {}", e))?;
|
|
|
-
|
|
|
- Ok(())
|
|
|
+ Ok(logs.iter().cloned().collect::<Vec<_>>().join(""))
|
|
|
}
|
|
|
|
|
|
#[tauri::command]
|
|
|
-async fn get_logs(app: AppHandle) -> Result<String, String> {
|
|
|
- let log_state = app.try_state::<LogState>().ok_or("Log state not found")?;
|
|
|
-
|
|
|
- let logs = log_state
|
|
|
- .0
|
|
|
- .lock()
|
|
|
- .map_err(|_| "Failed to acquire log lock")?;
|
|
|
-
|
|
|
- Ok(logs.iter().cloned().collect::<Vec<_>>().join(""))
|
|
|
+async fn ensure_server_started(state: State<'_, ServerState>) -> Result<(), String> {
|
|
|
+ state
|
|
|
+ .status
|
|
|
+ .clone()
|
|
|
+ .await
|
|
|
+ .map_err(|_| "Failed to get server status".to_string())?
|
|
|
}
|
|
|
|
|
|
fn get_sidecar_port() -> u32 {
|
|
|
@@ -209,9 +218,8 @@ pub fn run() {
|
|
|
.plugin(PinchZoomDisablePlugin)
|
|
|
.invoke_handler(tauri::generate_handler![
|
|
|
kill_sidecar,
|
|
|
- copy_logs_to_clipboard,
|
|
|
- get_logs,
|
|
|
- install_cli
|
|
|
+ install_cli,
|
|
|
+ ensure_server_started
|
|
|
])
|
|
|
.setup(move |app| {
|
|
|
let app = app.handle().clone();
|
|
|
@@ -219,94 +227,93 @@ pub fn run() {
|
|
|
// Initialize log state
|
|
|
app.manage(LogState(Arc::new(Mutex::new(VecDeque::new()))));
|
|
|
|
|
|
+ // Get port and create window immediately for faster perceived startup
|
|
|
+ let port = get_sidecar_port();
|
|
|
+
|
|
|
+ let primary_monitor = app.primary_monitor().ok().flatten();
|
|
|
+ let size = primary_monitor
|
|
|
+ .map(|m| m.size().to_logical(m.scale_factor()))
|
|
|
+ .unwrap_or(LogicalSize::new(1920, 1080));
|
|
|
+
|
|
|
+ // Create window immediately with serverReady = false
|
|
|
+ let mut window_builder =
|
|
|
+ WebviewWindow::builder(&app, "main", WebviewUrl::App("/".into()))
|
|
|
+ .title("OpenCode")
|
|
|
+ .inner_size(size.width as f64, size.height as f64)
|
|
|
+ .decorations(true)
|
|
|
+ .zoom_hotkeys_enabled(true)
|
|
|
+ .disable_drag_drop_handler()
|
|
|
+ .initialization_script(format!(
|
|
|
+ r#"
|
|
|
+ window.__OPENCODE__ ??= {{}};
|
|
|
+ window.__OPENCODE__.updaterEnabled = {updater_enabled};
|
|
|
+ window.__OPENCODE__.port = {port};
|
|
|
+ "#
|
|
|
+ ));
|
|
|
+
|
|
|
+ #[cfg(target_os = "macos")]
|
|
|
{
|
|
|
- let app = app.clone();
|
|
|
- tauri::async_runtime::spawn(async move {
|
|
|
- let port = get_sidecar_port();
|
|
|
-
|
|
|
- let should_spawn_sidecar = !is_server_running(port).await;
|
|
|
-
|
|
|
- let child = if should_spawn_sidecar {
|
|
|
- let child = spawn_sidecar(&app, port);
|
|
|
-
|
|
|
- let timestamp = Instant::now();
|
|
|
- loop {
|
|
|
- if timestamp.elapsed() > Duration::from_secs(7) {
|
|
|
- let res = app.dialog()
|
|
|
- .message("Failed to spawn OpenCode Server. Copy logs using the button below and send them to the team for assistance.")
|
|
|
- .title("Startup Failed")
|
|
|
- .buttons(MessageDialogButtons::OkCancelCustom("Copy Logs And Exit".to_string(), "Exit".to_string()))
|
|
|
- .blocking_show_with_result();
|
|
|
-
|
|
|
- if matches!(&res, MessageDialogResult::Custom(name) if name == "Copy Logs And Exit") {
|
|
|
- match copy_logs_to_clipboard(app.clone()).await {
|
|
|
- Ok(()) => println!("Logs copied to clipboard successfully"),
|
|
|
- Err(e) => println!("Failed to copy logs to clipboard: {}", e),
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- app.exit(1);
|
|
|
-
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- tokio::time::sleep(Duration::from_millis(10)).await;
|
|
|
-
|
|
|
- if is_server_running(port).await {
|
|
|
- // give the server a little bit more time to warm up
|
|
|
- tokio::time::sleep(Duration::from_millis(10)).await;
|
|
|
-
|
|
|
- break;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- println!("Server ready after {:?}", timestamp.elapsed());
|
|
|
-
|
|
|
- Some(child)
|
|
|
- } else {
|
|
|
- None
|
|
|
- };
|
|
|
-
|
|
|
- let primary_monitor = app.primary_monitor().ok().flatten();
|
|
|
- let size = primary_monitor
|
|
|
- .map(|m| m.size().to_logical(m.scale_factor()))
|
|
|
- .unwrap_or(LogicalSize::new(1920, 1080));
|
|
|
-
|
|
|
- let mut window_builder =
|
|
|
- WebviewWindow::builder(&app, "main", WebviewUrl::App("/".into()))
|
|
|
- .title("OpenCode")
|
|
|
- .inner_size(size.width as f64, size.height as f64)
|
|
|
- .decorations(true)
|
|
|
- .zoom_hotkeys_enabled(true)
|
|
|
- .disable_drag_drop_handler()
|
|
|
- .initialization_script(format!(
|
|
|
- r#"
|
|
|
- window.__OPENCODE__ ??= {{}};
|
|
|
- window.__OPENCODE__.updaterEnabled = {updater_enabled};
|
|
|
- window.__OPENCODE__.port = {port};
|
|
|
- "#
|
|
|
- ));
|
|
|
-
|
|
|
- #[cfg(target_os = "macos")]
|
|
|
- {
|
|
|
- window_builder = window_builder
|
|
|
- .title_bar_style(tauri::TitleBarStyle::Overlay)
|
|
|
- .hidden_title(true);
|
|
|
- }
|
|
|
-
|
|
|
- window_builder.build().expect("Failed to create window");
|
|
|
-
|
|
|
- app.manage(ServerState(Arc::new(Mutex::new(child))));
|
|
|
- });
|
|
|
+ window_builder = window_builder
|
|
|
+ .title_bar_style(tauri::TitleBarStyle::Overlay)
|
|
|
+ .hidden_title(true);
|
|
|
}
|
|
|
|
|
|
+ let window = window_builder.build().expect("Failed to create window");
|
|
|
+
|
|
|
+ let (tx, rx) = tokio::sync::oneshot::channel();
|
|
|
+ app.manage(ServerState::new(None, rx));
|
|
|
+
|
|
|
{
|
|
|
- let app = app.clone();
|
|
|
- tauri::async_runtime::spawn(async move {
|
|
|
- if let Err(e) = sync_cli(app) {
|
|
|
- eprintln!("Failed to sync CLI: {e}");
|
|
|
- }
|
|
|
- });
|
|
|
+ let app = app.clone();
|
|
|
+ tauri::async_runtime::spawn(async move {
|
|
|
+ let should_spawn_sidecar = !is_server_running(port).await;
|
|
|
+
|
|
|
+ let (child, res) = if should_spawn_sidecar {
|
|
|
+ let child = spawn_sidecar(&app, port);
|
|
|
+
|
|
|
+ let timestamp = Instant::now();
|
|
|
+ let res = loop {
|
|
|
+ if timestamp.elapsed() > Duration::from_secs(7) {
|
|
|
+ break Err(format!(
|
|
|
+ "Failed to spawn OpenCode Server. Logs:\n{}",
|
|
|
+ get_logs(app.clone()).await.unwrap()
|
|
|
+ ));
|
|
|
+ }
|
|
|
+
|
|
|
+ tokio::time::sleep(Duration::from_millis(10)).await;
|
|
|
+
|
|
|
+ if is_server_running(port).await {
|
|
|
+ // give the server a little bit more time to warm up
|
|
|
+ tokio::time::sleep(Duration::from_millis(10)).await;
|
|
|
+
|
|
|
+ break Ok(());
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ println!("Server ready after {:?}", timestamp.elapsed());
|
|
|
+
|
|
|
+ (Some(child), res)
|
|
|
+ } else {
|
|
|
+ (None, Ok(()))
|
|
|
+ };
|
|
|
+
|
|
|
+ app.state::<ServerState>().set_child(child);
|
|
|
+
|
|
|
+ if res.is_ok() {
|
|
|
+ let _ = window.eval("window.__OPENCODE__.serverReady = true;");
|
|
|
+ }
|
|
|
+
|
|
|
+ let _ = tx.send(res);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ {
|
|
|
+ let app = app.clone();
|
|
|
+ tauri::async_runtime::spawn(async move {
|
|
|
+ if let Err(e) = sync_cli(app) {
|
|
|
+ eprintln!("Failed to sync CLI: {e}");
|
|
|
+ }
|
|
|
+ });
|
|
|
}
|
|
|
|
|
|
Ok(())
|