11use serde:: { Deserialize , Serialize } ;
22use serde_json:: { Map as JsonMap , Value as JsonValue } ;
3- use std:: collections:: HashMap ;
4- use std:: sync:: atomic:: { AtomicU64 , Ordering } ;
5- use std:: sync:: mpsc;
6- use std:: sync:: { Arc , Mutex } ;
7- use std:: time:: Duration ;
8- use std:: path:: { Path , PathBuf } ;
3+ use std:: {
4+ collections:: HashMap ,
5+ env,
6+ fs:: { create_dir_all, OpenOptions } ,
7+ io:: Write ,
8+ path:: { Path , PathBuf } ,
9+ sync:: atomic:: { AtomicU64 , Ordering } ,
10+ sync:: mpsc,
11+ sync:: { Arc , Mutex } ,
12+ time:: { Duration , SystemTime , UNIX_EPOCH } ,
13+ } ;
914use tauri:: { AppHandle , Emitter , Manager } ;
1015use tauri_plugin_shell:: { process:: CommandEvent , ShellExt } ;
1116
@@ -23,6 +28,62 @@ struct SidecarState {
2328 next_request_id : AtomicU64 ,
2429}
2530
31+ const LOG_FILE_ENV : & str = "MOUSETERM_LOG_FILE" ;
32+
33+ fn log_timestamp ( ) -> u64 {
34+ SystemTime :: now ( )
35+ . duration_since ( UNIX_EPOCH )
36+ . map ( |duration| duration. as_secs ( ) )
37+ . unwrap_or_default ( )
38+ }
39+
40+ fn default_log_path ( ) -> PathBuf {
41+ if let Some ( path) = env:: var_os ( LOG_FILE_ENV ) {
42+ return PathBuf :: from ( path) ;
43+ }
44+
45+ #[ cfg( target_os = "windows" ) ]
46+ if let Some ( local_app_data) = env:: var_os ( "LOCALAPPDATA" ) {
47+ return PathBuf :: from ( local_app_data)
48+ . join ( "MouseTerm" )
49+ . join ( "mouseterm.log" ) ;
50+ }
51+
52+ env:: temp_dir ( ) . join ( "mouseterm.log" )
53+ }
54+
55+ fn init_log ( ) {
56+ let path = default_log_path ( ) ;
57+ if let Some ( parent) = path. parent ( ) {
58+ let _ = create_dir_all ( parent) ;
59+ }
60+
61+ if let Ok ( mut file) = OpenOptions :: new ( )
62+ . create ( true )
63+ . write ( true )
64+ . truncate ( true )
65+ . open ( & path)
66+ {
67+ let _ = writeln ! (
68+ file,
69+ "[{}] MouseTerm log started at {}" ,
70+ log_timestamp( ) ,
71+ path. display( )
72+ ) ;
73+ }
74+ }
75+
76+ fn append_log ( message : impl AsRef < str > ) {
77+ let path = default_log_path ( ) ;
78+ if let Some ( parent) = path. parent ( ) {
79+ let _ = create_dir_all ( parent) ;
80+ }
81+
82+ if let Ok ( mut file) = OpenOptions :: new ( ) . create ( true ) . append ( true ) . open ( path) {
83+ let _ = writeln ! ( file, "[{}] {}" , log_timestamp( ) , message. as_ref( ) ) ;
84+ }
85+ }
86+
2687#[ derive( Serialize , Deserialize , Clone ) ]
2788struct PtySpawnOptions {
2889 cols : Option < u16 > ,
@@ -39,7 +100,10 @@ fn request_from_sidecar(
39100 event : & str ,
40101 data : JsonValue ,
41102) -> Result < JsonValue , String > {
42- let request_id = format ! ( "req-{}" , state. next_request_id. fetch_add( 1 , Ordering :: Relaxed ) ) ;
103+ let request_id = format ! (
104+ "req-{}" ,
105+ state. next_request_id. fetch_add( 1 , Ordering :: Relaxed )
106+ ) ;
43107 let ( tx, rx) = mpsc:: channel ( ) ;
44108 state
45109 . pending_requests
@@ -73,11 +137,7 @@ fn request_from_sidecar(
73137// ── Tauri commands ──────────────────────────────────────────────────────────
74138
75139#[ tauri:: command]
76- fn pty_spawn (
77- state : tauri:: State < ' _ , SidecarState > ,
78- id : String ,
79- options : Option < PtySpawnOptions > ,
80- ) {
140+ fn pty_spawn ( state : tauri:: State < ' _ , SidecarState > , id : String , options : Option < PtySpawnOptions > ) {
81141 let msg = serde_json:: json!( {
82142 "event" : "pty:spawn" ,
83143 "data" : { "id" : id, "options" : options }
@@ -95,12 +155,7 @@ fn pty_write(state: tauri::State<'_, SidecarState>, id: String, data: String) {
95155}
96156
97157#[ tauri:: command]
98- fn pty_resize (
99- state : tauri:: State < ' _ , SidecarState > ,
100- id : String ,
101- cols : u16 ,
102- rows : u16 ,
103- ) {
158+ fn pty_resize ( state : tauri:: State < ' _ , SidecarState > , id : String , cols : u16 , rows : u16 ) {
104159 let msg = serde_json:: json!( {
105160 "event" : "pty:resize" ,
106161 "data" : { "id" : id, "cols" : cols, "rows" : rows }
@@ -124,16 +179,23 @@ fn pty_request_init(state: tauri::State<'_, SidecarState>) {
124179}
125180
126181#[ tauri:: command]
127- fn pty_get_cwd ( state : tauri:: State < ' _ , SidecarState > , id : String ) -> Result < Option < String > , String > {
182+ fn pty_get_cwd (
183+ state : tauri:: State < ' _ , SidecarState > ,
184+ id : String ,
185+ ) -> Result < Option < String > , String > {
128186 let response = request_from_sidecar ( & state, "pty:getCwd" , serde_json:: json!( { "id" : id } ) ) ?;
129187 Ok ( response
130188 . get ( "cwd" )
131189 . and_then ( |cwd| cwd. as_str ( ) . map ( String :: from) ) )
132190}
133191
134192#[ tauri:: command]
135- fn pty_get_scrollback ( state : tauri:: State < ' _ , SidecarState > , id : String ) -> Result < Option < String > , String > {
136- let response = request_from_sidecar ( & state, "pty:getScrollback" , serde_json:: json!( { "id" : id } ) ) ?;
193+ fn pty_get_scrollback (
194+ state : tauri:: State < ' _ , SidecarState > ,
195+ id : String ,
196+ ) -> Result < Option < String > , String > {
197+ let response =
198+ request_from_sidecar ( & state, "pty:getScrollback" , serde_json:: json!( { "id" : id } ) ) ?;
137199 Ok ( response
138200 . get ( "data" )
139201 . and_then ( |data| data. as_str ( ) . map ( String :: from) ) )
@@ -165,15 +227,17 @@ fn get_default_shell() -> ShellInfo {
165227 . unwrap_or_else ( |_| String :: from ( "C:\\ Windows\\ System32\\ cmd.exe" ) ) ;
166228
167229 #[ cfg( not( target_os = "windows" ) ) ]
168- let shell_path = std:: env:: var ( "SHELL" )
169- . unwrap_or_else ( |_| String :: from ( "/bin/sh" ) ) ;
230+ let shell_path = std:: env:: var ( "SHELL" ) . unwrap_or_else ( |_| String :: from ( "/bin/sh" ) ) ;
170231
171232 let name = Path :: new ( & shell_path)
172233 . file_name ( )
173234 . map ( |n| n. to_string_lossy ( ) . into_owned ( ) )
174235 . unwrap_or_else ( || shell_path. clone ( ) ) ;
175236
176- ShellInfo { name, path : shell_path }
237+ ShellInfo {
238+ name,
239+ path : shell_path,
240+ }
177241}
178242
179243fn resolve_sidecar_path ( resource_dir : Option < PathBuf > , manifest_dir : & Path ) -> PathBuf {
@@ -190,20 +254,25 @@ fn resolve_sidecar_path(resource_dir: Option<PathBuf>, manifest_dir: &Path) -> P
190254 manifest_dir. join ( ".." ) . join ( "sidecar" ) . join ( "main.js" )
191255}
192256
193- fn start_sidecar ( app : & AppHandle ) -> SidecarState {
257+ fn start_sidecar ( app : & AppHandle ) -> Result < SidecarState , String > {
194258 let sidecar_path = resolve_sidecar_path (
195259 app. path ( ) . resource_dir ( ) . ok ( ) ,
196260 Path :: new ( env ! ( "CARGO_MANIFEST_DIR" ) ) ,
197261 ) ;
262+ append_log ( format ! (
263+ "[sidecar] resolved script: {}" ,
264+ sidecar_path. display( )
265+ ) ) ;
198266
199267 let ( mut rx, mut child) = app
200268 . shell ( )
201269 . sidecar ( "node" )
202- . expect ( "failed to resolve bundled Node.js runtime" )
270+ . map_err ( |err| format ! ( "failed to resolve bundled Node.js runtime: {err}" ) ) ?
203271 . arg ( & sidecar_path)
204272 . set_raw_out ( false )
205273 . spawn ( )
206- . expect ( "failed to start Node.js sidecar" ) ;
274+ . map_err ( |err| format ! ( "failed to start Node.js sidecar: {err}" ) ) ?;
275+ append_log ( "[sidecar] spawned Node.js runtime" ) ;
207276
208277 let handle = app. clone ( ) ;
209278 let pending_requests: PendingRequests = Arc :: new ( Mutex :: new ( HashMap :: new ( ) ) ) ;
@@ -214,14 +283,21 @@ fn start_sidecar(app: &AppHandle) -> SidecarState {
214283 while let Some ( event) = rx. recv ( ) . await {
215284 match event {
216285 CommandEvent :: Stdout ( line) => {
217- let Ok ( line) = String :: from_utf8 ( line) else { continue } ;
286+ let Ok ( line) = String :: from_utf8 ( line) else {
287+ append_log ( "[sidecar stdout] invalid UTF-8" ) ;
288+ continue ;
289+ } ;
218290 let Ok ( mut msg) = serde_json:: from_str :: < serde_json:: Value > ( & line) else {
291+ append_log ( format ! ( "[sidecar stdout] {}" , line. trim_end( ) ) ) ;
219292 continue ;
220293 } ;
221- let Some ( event) = msg. get ( "event" ) . and_then ( |e| e. as_str ( ) ) . map ( String :: from) else {
294+ let Some ( event) = msg. get ( "event" ) . and_then ( |e| e. as_str ( ) ) . map ( String :: from)
295+ else {
296+ append_log ( "[sidecar stdout] JSON line missing event" ) ;
222297 continue ;
223298 } ;
224- let data = msg. as_object_mut ( )
299+ let data = msg
300+ . as_object_mut ( )
225301 . and_then ( |m| m. remove ( "data" ) )
226302 . unwrap_or ( serde_json:: Value :: Null ) ;
227303
@@ -241,18 +317,23 @@ fn start_sidecar(app: &AppHandle) -> SidecarState {
241317 }
242318 CommandEvent :: Stderr ( line) => {
243319 if let Ok ( line) = String :: from_utf8 ( line) {
244- eprintln ! ( "[sidecar] {}" , line. trim_end( ) ) ;
320+ let message = format ! ( "[sidecar] {}" , line. trim_end( ) ) ;
321+ eprintln ! ( "{message}" ) ;
322+ append_log ( message) ;
245323 }
246324 }
247325 CommandEvent :: Error ( err) => {
248- eprintln ! ( "[sidecar] {}" , err) ;
326+ let message = format ! ( "[sidecar] {err}" ) ;
327+ eprintln ! ( "{message}" ) ;
328+ append_log ( message) ;
249329 }
250330 CommandEvent :: Terminated ( payload) => {
251- eprintln ! (
331+ let message = format ! (
252332 "[sidecar] exited (code: {:?}, signal: {:?})" ,
253- payload. code,
254- payload. signal
333+ payload. code, payload. signal
255334 ) ;
335+ eprintln ! ( "{message}" ) ;
336+ append_log ( message) ;
256337 break ;
257338 }
258339 _ => { }
@@ -266,22 +347,26 @@ fn start_sidecar(app: &AppHandle) -> SidecarState {
266347 std:: thread:: spawn ( move || {
267348 while let Ok ( msg) = writer_rx. recv ( ) {
268349 match msg {
269- SidecarMsg :: Shutdown => break ,
350+ SidecarMsg :: Shutdown => {
351+ append_log ( "[sidecar] shutdown requested" ) ;
352+ break ;
353+ }
270354 SidecarMsg :: Json ( line) => {
271355 let payload = format ! ( "{}\n " , line) ;
272356 if child. write ( payload. as_bytes ( ) ) . is_err ( ) {
357+ append_log ( "[sidecar] stdin write failed" ) ;
273358 break ;
274359 }
275360 }
276361 }
277362 }
278363 } ) ;
279364
280- SidecarState {
365+ Ok ( SidecarState {
281366 tx,
282367 pending_requests,
283368 next_request_id : AtomicU64 :: new ( 0 ) ,
284- }
369+ } )
285370}
286371
287372// ── App entry point ─────────────────────────────────────────────────────────
@@ -292,8 +377,15 @@ pub fn run() {
292377 . plugin ( tauri_plugin_shell:: init ( ) )
293378 . plugin ( tauri_plugin_updater:: Builder :: new ( ) . build ( ) )
294379 . setup ( |app| {
295- let sidecar_state = start_sidecar ( app. handle ( ) ) ;
380+ init_log ( ) ;
381+ append_log ( "[app] setup started" ) ;
382+
383+ let sidecar_state = start_sidecar ( app. handle ( ) ) . map_err ( |err| {
384+ append_log ( format ! ( "[sidecar] {err}" ) ) ;
385+ std:: io:: Error :: new ( std:: io:: ErrorKind :: Other , err)
386+ } ) ?;
296387 app. manage ( sidecar_state) ;
388+ append_log ( "[app] sidecar state registered" ) ;
297389
298390 // On non-macOS, remove native decorations for a fully custom title bar.
299391 // macOS uses titleBarStyle "Overlay" from config instead, which preserves
@@ -380,6 +472,9 @@ mod tests {
380472
381473 let resolved = resolve_sidecar_path ( None , manifest_dir) ;
382474
383- assert_eq ! ( resolved, manifest_dir. join( ".." ) . join( "sidecar" ) . join( "main.js" ) ) ;
475+ assert_eq ! (
476+ resolved,
477+ manifest_dir. join( ".." ) . join( "sidecar" ) . join( "main.js" )
478+ ) ;
384479 }
385480}
0 commit comments