@@ -2,6 +2,7 @@ use crate::config_handler::ConfigHandler;
22use crate :: log_handler:: LogHandler ;
33use crate :: monitoring:: Monitoring ;
44use crate :: process_manager:: ProcessManager ;
5+ use crate :: terminal:: TerminalManager ;
56use crate :: types:: { ProcessConfig , ProcessState } ;
67use chrono:: Local ;
78use serde_json:: json;
@@ -16,6 +17,8 @@ pub struct AppState {
1617 pub log_handler : Arc < LogHandler > ,
1718 /// Persistent sysinfo System — keeps prior CPU snapshot so delta is accurate.
1819 pub system : Arc < Mutex < System > > ,
20+ /// Integrated terminal sessions.
21+ pub terminal : Arc < Mutex < TerminalManager > > ,
1922}
2023
2124/// Spawn background threads that read stdout/stderr from a child process,
@@ -378,3 +381,164 @@ pub async fn stop_all(state: State<'_, AppState>, window: WebviewWindow) -> Resu
378381
379382 Ok ( ( ) )
380383}
384+
385+ // ═══════════════════════════════════════════════════════════════
386+ // Terminal commands
387+ // ═══════════════════════════════════════════════════════════════
388+
389+ /// Run a shell command in the given terminal session.
390+ /// Streams stdout/stderr via `terminal:output:{session_id}` events.
391+ /// Emits `terminal:done:{session_id}` when the process exits.
392+ #[ tauri:: command]
393+ pub async fn terminal_run (
394+ session_id : String ,
395+ command : String ,
396+ job_id : String ,
397+ state : State < ' _ , AppState > ,
398+ window : WebviewWindow ,
399+ ) -> Result < ( ) , String > {
400+ // Grab current CWD for this session
401+ let cwd = {
402+ let mut terminal = state. terminal . lock ( ) . map_err ( |e| e. to_string ( ) ) ?;
403+ terminal. get_cwd ( & session_id)
404+ } ;
405+
406+ // Spawn via PowerShell on Windows
407+ let mut child = std:: process:: Command :: new ( "powershell" )
408+ . args ( [ "-NoProfile" , "-NonInteractive" , "-Command" , & command] )
409+ . current_dir ( & cwd)
410+ . stdout ( std:: process:: Stdio :: piped ( ) )
411+ . stderr ( std:: process:: Stdio :: piped ( ) )
412+ . spawn ( )
413+ . map_err ( |e| format ! ( "Failed to spawn command: {}" , e) ) ?;
414+
415+ let stdout = child. stdout . take ( ) . expect ( "stdout piped" ) ;
416+ let stderr = child. stderr . take ( ) . expect ( "stderr piped" ) ;
417+
418+ // Wrap child so both streaming threads + kill command can access it
419+ let child_arc: std:: sync:: Arc < Mutex < Option < std:: process:: Child > > > =
420+ std:: sync:: Arc :: new ( Mutex :: new ( Some ( child) ) ) ;
421+
422+ // Register job
423+ {
424+ let mut terminal = state. terminal . lock ( ) . map_err ( |e| e. to_string ( ) ) ?;
425+ terminal. add_job ( job_id. clone ( ) , std:: sync:: Arc :: clone ( & child_arc) ) ;
426+ }
427+
428+ // ── stdout reader thread ──
429+ {
430+ let win = window. clone ( ) ;
431+ let sid = session_id. clone ( ) ;
432+ let jid = job_id. clone ( ) ;
433+ std:: thread:: spawn ( move || {
434+ let reader = std:: io:: BufReader :: new ( stdout) ;
435+ for line in reader. lines ( ) {
436+ if let Ok ( msg) = line {
437+ let ts = Local :: now ( ) . format ( "%H:%M:%S%.3f" ) . to_string ( ) ;
438+ let _ = win. emit (
439+ & format ! ( "terminal:output:{}" , sid) ,
440+ json ! ( { "jobId" : jid, "line" : msg, "isError" : false , "timestamp" : ts } ) ,
441+ ) ;
442+ }
443+ }
444+ } ) ;
445+ }
446+
447+ // ── stderr reader + exit-waiter thread ──
448+ {
449+ let win = window. clone ( ) ;
450+ let sid = session_id. clone ( ) ;
451+ let jid = job_id. clone ( ) ;
452+ let child_ref = std:: sync:: Arc :: clone ( & child_arc) ;
453+ let terminal_arc = std:: sync:: Arc :: clone ( & state. terminal ) ;
454+ std:: thread:: spawn ( move || {
455+ // Read stderr
456+ let reader = std:: io:: BufReader :: new ( stderr) ;
457+ for line in reader. lines ( ) {
458+ if let Ok ( msg) = line {
459+ let ts = Local :: now ( ) . format ( "%H:%M:%S%.3f" ) . to_string ( ) ;
460+ let _ = win. emit (
461+ & format ! ( "terminal:output:{}" , sid) ,
462+ json ! ( { "jobId" : jid, "line" : msg, "isError" : true , "timestamp" : ts } ) ,
463+ ) ;
464+ }
465+ }
466+
467+ // Wait for child to exit
468+ let exit_code = {
469+ let mut guard = child_ref. lock ( ) . unwrap_or_else ( |e| e. into_inner ( ) ) ;
470+ if let Some ( ref mut c) = * guard {
471+ c. wait ( ) . map ( |s| s. code ( ) ) . ok ( ) . flatten ( ) . unwrap_or ( -1 )
472+ } else {
473+ -1
474+ }
475+ } ;
476+
477+ // Remove completed job from manager
478+ if let Ok ( mut mgr) = terminal_arc. lock ( ) {
479+ mgr. remove_job ( & jid) ;
480+ }
481+
482+ let _ = win. emit (
483+ & format ! ( "terminal:done:{}" , sid) ,
484+ json ! ( { "jobId" : jid, "exitCode" : exit_code } ) ,
485+ ) ;
486+ } ) ;
487+ }
488+
489+ Ok ( ( ) )
490+ }
491+
492+ /// Kill a running terminal job.
493+ #[ tauri:: command]
494+ pub async fn terminal_kill (
495+ job_id : String ,
496+ state : State < ' _ , AppState > ,
497+ ) -> Result < ( ) , String > {
498+ let mut terminal = state. terminal . lock ( ) . map_err ( |e| e. to_string ( ) ) ?;
499+ terminal. kill_job ( & job_id)
500+ }
501+
502+ /// Update the working directory for a terminal session.
503+ /// Returns the resolved absolute CWD or an error.
504+ #[ tauri:: command]
505+ pub async fn terminal_set_cwd (
506+ session_id : String ,
507+ path : String ,
508+ state : State < ' _ , AppState > ,
509+ ) -> Result < String , String > {
510+ let mut terminal = state. terminal . lock ( ) . map_err ( |e| e. to_string ( ) ) ?;
511+ terminal. set_cwd ( & session_id, & path)
512+ }
513+
514+ /// Return the current working directory for a terminal session.
515+ #[ tauri:: command]
516+ pub async fn terminal_get_cwd (
517+ session_id : String ,
518+ state : State < ' _ , AppState > ,
519+ ) -> Result < String , String > {
520+ let mut terminal = state. terminal . lock ( ) . map_err ( |e| e. to_string ( ) ) ?;
521+ Ok ( terminal. get_cwd ( & session_id) )
522+ }
523+
524+ /// Promote a terminal command to a managed process.
525+ /// The running job (if any) is left untouched; a new process entry is created.
526+ #[ tauri:: command]
527+ pub async fn terminal_add_process (
528+ name : String ,
529+ command : String ,
530+ working_dir : Option < String > ,
531+ state : State < ' _ , AppState > ,
532+ ) -> Result < String , String > {
533+ let parts: Vec < & str > = command. trim ( ) . splitn ( 2 , ' ' ) . collect ( ) ;
534+ let exe = parts. first ( ) . copied ( ) . unwrap_or ( "" ) . to_string ( ) ;
535+ let args: Vec < String > = if parts. len ( ) > 1 {
536+ parts[ 1 ] . split_whitespace ( ) . map ( |s| s. to_string ( ) ) . collect ( )
537+ } else {
538+ vec ! [ ]
539+ } ;
540+
541+ let mut manager = state. manager . lock ( ) . map_err ( |e| e. to_string ( ) ) ?;
542+ let id = manager. add_process ( name, exe, args, working_dir, false ) ;
543+ Ok ( id)
544+ }
0 commit comments