lib.rs 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. use std::{
  2. net::{SocketAddr, TcpListener},
  3. sync::{Arc, Mutex},
  4. time::{Duration, Instant},
  5. };
  6. #[cfg(target_os = "macos")]
  7. use tauri::TitleBarStyle;
  8. use tauri::{AppHandle, LogicalSize, Manager, RunEvent, WebviewUrl, WebviewWindow};
  9. use tauri_plugin_shell::process::{CommandChild, CommandEvent};
  10. use tauri_plugin_shell::ShellExt;
  11. use tokio::net::TcpSocket;
  12. #[derive(Clone)]
  13. struct ServerState(Arc<Mutex<Option<CommandChild>>>);
  14. #[tauri::command]
  15. fn kill_sidecar(app: AppHandle) {
  16. let Some(server_state) = app.try_state::<ServerState>() else {
  17. println!("Server not running");
  18. return;
  19. };
  20. let Some(server_state) = server_state
  21. .0
  22. .lock()
  23. .expect("Failed to acquire mutex lock")
  24. .take()
  25. else {
  26. println!("Server state missing");
  27. return;
  28. };
  29. let _ = server_state.kill();
  30. println!("Killed server");
  31. }
  32. fn get_sidecar_port() -> u16 {
  33. option_env!("OPENCODE_PORT")
  34. .map(|s| s.to_string())
  35. .or_else(|| std::env::var("OPENCODE_PORT").ok())
  36. .and_then(|port_str| port_str.parse().ok())
  37. .unwrap_or_else(|| {
  38. TcpListener::bind("127.0.0.1:0")
  39. .expect("Failed to bind to find free port")
  40. .local_addr()
  41. .expect("Failed to get local address")
  42. .port()
  43. })
  44. }
  45. fn get_user_shell() -> String {
  46. std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string())
  47. }
  48. fn spawn_sidecar(app: &AppHandle, port: u16) -> CommandChild {
  49. #[cfg(target_os = "windows")]
  50. let (mut rx, child) = app
  51. .shell()
  52. .sidecar("opencode-cli")
  53. .unwrap()
  54. .env("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY", "true")
  55. .env("OPENCODE_CLIENT", "desktop")
  56. .args(["serve", &format!("--port={port}")])
  57. .spawn()
  58. .expect("Failed to spawn opencode");
  59. #[cfg(not(target_os = "windows"))]
  60. let (mut rx, child) = {
  61. let sidecar_path = tauri::utils::platform::current_exe()
  62. .expect("Failed to get current exe")
  63. .parent()
  64. .expect("Failed to get parent dir")
  65. .join("opencode-cli");
  66. let shell = get_user_shell();
  67. app.shell()
  68. .command(&shell)
  69. .env("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY", "true")
  70. .env("OPENCODE_CLIENT", "desktop")
  71. .args([
  72. "-il",
  73. "-c",
  74. &format!("{} serve --port={}", sidecar_path.display(), port),
  75. ])
  76. .spawn()
  77. .expect("Failed to spawn opencode")
  78. };
  79. tauri::async_runtime::spawn(async move {
  80. while let Some(event) = rx.recv().await {
  81. match event {
  82. CommandEvent::Stdout(line_bytes) => {
  83. let line = String::from_utf8_lossy(&line_bytes);
  84. print!("{line}");
  85. }
  86. CommandEvent::Stderr(line_bytes) => {
  87. let line = String::from_utf8_lossy(&line_bytes);
  88. eprint!("{line}");
  89. }
  90. _ => {}
  91. }
  92. }
  93. });
  94. child
  95. }
  96. async fn is_server_running(port: u16) -> bool {
  97. TcpSocket::new_v4()
  98. .unwrap()
  99. .connect(SocketAddr::new(
  100. "127.0.0.1".parse().expect("Failed to parse IP"),
  101. port,
  102. ))
  103. .await
  104. .is_ok()
  105. }
  106. #[cfg_attr(mobile, tauri::mobile_entry_point)]
  107. pub fn run() {
  108. let updater_enabled = option_env!("TAURI_SIGNING_PRIVATE_KEY").is_some();
  109. let mut builder = tauri::Builder::default()
  110. .plugin(tauri_plugin_os::init())
  111. .plugin(tauri_plugin_window_state::Builder::new().build())
  112. .plugin(tauri_plugin_store::Builder::new().build())
  113. .plugin(tauri_plugin_dialog::init())
  114. .plugin(tauri_plugin_shell::init())
  115. .plugin(tauri_plugin_process::init())
  116. .plugin(tauri_plugin_opener::init())
  117. .invoke_handler(tauri::generate_handler![kill_sidecar])
  118. .setup(move |app| {
  119. let app = app.handle().clone();
  120. tauri::async_runtime::spawn(async move {
  121. let port = get_sidecar_port();
  122. let should_spawn_sidecar = !is_server_running(port).await;
  123. let child = if should_spawn_sidecar {
  124. let child = spawn_sidecar(&app, port);
  125. let timestamp = Instant::now();
  126. loop {
  127. if timestamp.elapsed() > Duration::from_secs(7) {
  128. todo!("Handle server spawn timeout");
  129. }
  130. tokio::time::sleep(Duration::from_millis(10)).await;
  131. if is_server_running(port).await {
  132. // give the server a little bit more time to warm up
  133. tokio::time::sleep(Duration::from_millis(10)).await;
  134. break;
  135. }
  136. }
  137. println!("Server ready after {:?}", timestamp.elapsed());
  138. Some(child)
  139. } else {
  140. None
  141. };
  142. let primary_monitor = app.primary_monitor().ok().flatten();
  143. let size = primary_monitor
  144. .map(|m| m.size().to_logical(m.scale_factor()))
  145. .unwrap_or(LogicalSize::new(1920, 1080));
  146. let mut window_builder =
  147. WebviewWindow::builder(&app, "main", WebviewUrl::App("/".into()))
  148. .title("OpenCode")
  149. .inner_size(size.width as f64, size.height as f64)
  150. .decorations(true)
  151. .zoom_hotkeys_enabled(true)
  152. .disable_drag_drop_handler()
  153. .initialization_script(format!(
  154. r#"
  155. window.__OPENCODE__ ??= {{}};
  156. window.__OPENCODE__.updaterEnabled = {updater_enabled};
  157. window.__OPENCODE__.port = {port};
  158. "#
  159. ));
  160. #[cfg(target_os = "macos")]
  161. {
  162. window_builder = window_builder
  163. .title_bar_style(TitleBarStyle::Overlay)
  164. .hidden_title(true);
  165. }
  166. window_builder.build().expect("Failed to create window");
  167. app.manage(ServerState(Arc::new(Mutex::new(child))));
  168. });
  169. Ok(())
  170. });
  171. if updater_enabled {
  172. builder = builder.plugin(tauri_plugin_updater::Builder::new().build());
  173. }
  174. builder
  175. .build(tauri::generate_context!())
  176. .expect("error while running tauri application")
  177. .run(|app, event| {
  178. if let RunEvent::Exit = event {
  179. println!("Received Exit");
  180. kill_sidecar(app.clone());
  181. }
  182. });
  183. }