1use anyhow::Context as AnyhowContext;
8use rand::RngExt;
9use regex::Regex;
10use std::collections::HashMap;
11use std::fmt::Write as FmtWrite;
12use std::path::{Path, PathBuf};
13use std::process::{Command, Stdio};
14use std::sync::{Arc, Mutex as StdMutex};
15use std::time::{Duration, Instant};
16use tokio::sync::Mutex;
17use tokio::time::sleep;
18use tracing::{debug, error, info, warn};
19
20use crate::errors::{Result, WinxError};
21use crate::state::bash_state::BashState;
22use crate::state::pty::PtyShell;
23use crate::state::terminal::{render_terminal_output, strip_ansi_codes};
24use crate::types::{normalize_thread_id, BashCommand, BashCommandAction, SpecialKey};
25
26type SharedPtyShell = Arc<Mutex<Option<PtyShell>>>;
27
28const DEFAULT_TIMEOUT: f64 = 5.0;
32
33const TIMEOUT_WHILE_OUTPUT: f64 = 20.0;
35
36const OUTPUT_WAIT_PATIENCE: i32 = 3;
38
39const COMMAND_CHUNK_SIZE: usize = 64;
41
42const TEXT_CHUNK_SIZE: usize = 128;
44
45const MAX_OUTPUT_LENGTH: usize = 100_000;
47
48const WAITING_INPUT_MESSAGE: &str = "A command is already running. NOTE: You can't run multiple shell commands in main shell, likely a previous program hasn't exited.
501. Get its output using status check.
512. Use `send_ascii` or `send_specials` to give inputs to the running program OR
523. kill the previous program by sending ctrl+c first using `send_ascii` or `send_specials`
534. Interrupt and run the process in background
54";
55
56#[derive(Debug, Default)]
60pub struct BackgroundShellManager {
61 shells: HashMap<String, SharedPtyShell>,
62}
63
64impl BackgroundShellManager {
65 pub fn new() -> Self {
67 Self { shells: HashMap::new() }
68 }
69
70 pub fn start_new_shell(&mut self, working_dir: &Path, restricted_mode: bool) -> Result<String> {
72 let cid = format!("{:010x}", rand::rng().random::<u32>());
73
74 let shell = PtyShell::new(working_dir, restricted_mode).map_err(|e| {
75 WinxError::CommandExecutionError(format!("Failed to start background shell: {e}"))
76 })?;
77
78 self.shells.insert(cid.clone(), Arc::new(Mutex::new(Some(shell))));
79
80 info!("Started background shell with id: {}", cid);
81 Ok(cid)
82 }
83
84 pub fn get_shell(&self, bg_command_id: &str) -> Option<SharedPtyShell> {
86 self.shells.get(bg_command_id).cloned()
87 }
88
89 pub fn remove_shell(&mut self, bg_command_id: &str) -> bool {
91 if let Some(shell_arc) = self.shells.remove(bg_command_id) {
92 if let Ok(mut guard) = shell_arc.try_lock() {
93 *guard = None;
94 }
95 info!("Removed background shell: {}", bg_command_id);
96 true
97 } else {
98 false
99 }
100 }
101
102 fn prune_finished_shells(&mut self) {
103 let mut finished = Vec::new();
104
105 for (id, shell_arc) in &self.shells {
106 let Ok(mut guard) = shell_arc.try_lock() else {
107 continue;
108 };
109
110 let Some(shell) = guard.as_mut() else {
111 finished.push(id.clone());
112 continue;
113 };
114
115 if !shell.is_alive() {
116 finished.push(id.clone());
117 continue;
118 }
119
120 if shell.command_running {
121 let _ = shell.read_output(0.1);
122 }
123
124 if !shell.command_running {
125 finished.push(id.clone());
126 }
127 }
128
129 for id in finished {
130 self.remove_shell(&id);
131 }
132 }
133
134 pub fn get_running_info(&mut self) -> String {
136 self.prune_finished_shells();
137
138 if self.shells.is_empty() {
139 return "No command running in background.\n".to_string();
140 }
141
142 let mut running = Vec::new();
143 for (id, shell_arc) in &self.shells {
144 if let Ok(guard) = shell_arc.try_lock() {
145 if let Some(bash) = guard.as_ref() {
146 if bash.command_running {
147 running
148 .push(format!("Command: {}, bg_command_id: {}", bash.last_command, id));
149 }
150 }
151 } else {
152 running.push(format!("Command: <busy>, bg_command_id: {id}"));
153 }
154 }
155
156 if running.is_empty() {
157 "No command running in background.\n".to_string()
158 } else {
159 format!("Following background commands are attached:\n{}\n", running.join("\n"))
160 }
161 }
162}
163
164lazy_static::lazy_static! {
166 static ref BG_SHELL_MANAGER: StdMutex<BackgroundShellManager> = StdMutex::new(BackgroundShellManager::new());
167}
168
169fn get_status(
173 bash_state: &BashState,
174 is_bg: bool,
175 bg_id: Option<&str>,
176 is_running: bool,
177 running_for: Option<&str>,
178) -> String {
179 let mut status = "\n\n---\n\n".to_string();
180
181 if is_bg {
182 if let Some(id) = bg_id {
183 let _ = writeln!(status, "bg_command_id = {id}");
184 }
185 }
186
187 if is_running {
188 status.push_str("status = still running\n");
189 if let Some(duration) = running_for {
190 let _ = writeln!(status, "running for = {duration}");
191 }
192 } else {
193 status.push_str("status = process exited\n");
194 }
195
196 let _ = writeln!(status, "cwd = {}", bash_state.cwd.display());
197
198 if !is_bg {
199 if let Ok(mut manager) = BG_SHELL_MANAGER.lock() {
201 status.push_str("This is the main shell. ");
202 status.push_str(&manager.get_running_info());
203 }
204 }
205
206 status.trim_end().to_string()
207}
208
209fn wcgw_incremental_text(text: &str, last_pending_output: &str) -> String {
211 let text =
212 if text.len() > MAX_OUTPUT_LENGTH { &text[text.len() - MAX_OUTPUT_LENGTH..] } else { text };
213
214 if last_pending_output.is_empty() {
215 let rendered = render_terminal_output(text);
216 return rstrip_lines(&rendered).trim_start().to_string();
217 }
218
219 let last_rendered = render_terminal_output(last_pending_output);
220 if last_rendered.is_empty() {
221 return rstrip_lines(&render_terminal_output(text));
222 }
223
224 let text_after_last = if text.len() > last_pending_output.len() {
226 &text[last_pending_output.len()..]
227 } else {
228 text
229 };
230
231 let combined = format!("{}\n{}", last_rendered.join("\n"), text_after_last);
232 let new_rendered = render_terminal_output(&combined);
233
234 let incremental = get_incremental_output(&last_rendered, &new_rendered);
236 rstrip_lines(&incremental)
237}
238
239fn extract_prompt_cwd(output: &str) -> Option<PathBuf> {
240 let stripped = strip_ansi_codes(output);
241 let prompt_regex = Regex::new(r"◉ (?P<cwd>[^\r\n]*?)──➤").ok()?;
242
243 prompt_regex
244 .captures_iter(&stripped)
245 .filter_map(|captures| captures.name("cwd").map(|cwd| cwd.as_str().trim()))
246 .filter(|cwd| !cwd.is_empty())
247 .last()
248 .map(PathBuf::from)
249}
250
251fn rstrip_lines(lines: &[String]) -> String {
253 lines.iter().map(|line| line.trim_end()).collect::<Vec<_>>().join("\n")
254}
255
256fn get_incremental_output(old_output: &[String], new_output: &[String]) -> Vec<String> {
258 if old_output.is_empty() {
259 return new_output.to_vec();
260 }
261
262 let nold = old_output.len();
263 let nnew = new_output.len();
264
265 for i in (0..nnew).rev() {
267 if new_output[i] != old_output[nold - 1] {
268 continue;
269 }
270
271 let mut matched = true;
272 for j in (0..i).rev() {
273 let old_idx = (nold as i64 - 1 + j as i64 - i as i64) as isize;
274 if old_idx < 0 {
275 break;
276 }
277 if new_output[j] != old_output[old_idx as usize] {
278 matched = false;
279 break;
280 }
281 }
282
283 if matched {
284 return new_output[i + 1..].to_vec();
285 }
286 }
287
288 new_output.to_vec()
289}
290
291fn send_utf8_in_byte_chunks(shell: &mut PtyShell, text: &str, chunk_size: usize) -> Result<()> {
292 let mut start = 0;
293
294 while start < text.len() {
295 let mut end = (start + chunk_size).min(text.len());
296 while !text.is_char_boundary(end) {
297 end -= 1;
298 }
299 if end == start {
300 end = text[start..].char_indices().nth(1).map_or(text.len(), |(idx, _)| start + idx);
301 }
302
303 shell.send_text(&text[start..end]).map_err(|e| {
304 WinxError::CommandExecutionError(format!("Failed to write PTY input: {e}"))
305 })?;
306 start = end;
307 }
308
309 Ok(())
310}
311
312#[allow(dead_code)]
314fn is_status_check_action(action: &BashCommandAction) -> bool {
315 match action {
316 BashCommandAction::StatusCheck { .. } => true,
317 BashCommandAction::SendSpecials { send_specials, .. } => {
318 send_specials.len() == 1 && send_specials[0] == SpecialKey::Enter
319 }
320 BashCommandAction::SendAscii { send_ascii, .. } => {
321 send_ascii.len() == 1 && send_ascii[0] == 10 }
323 _ => false,
324 }
325}
326
327#[tracing::instrument(level = "info", skip(bash_state_arc, bash_command))]
334pub async fn handle_tool_call(
335 bash_state_arc: &Arc<Mutex<Option<BashState>>>,
336 bash_command: BashCommand,
337) -> Result<String> {
338 info!("BashCommand tool called with: {:?}", bash_command);
339
340 let thread_id = normalize_thread_id(&bash_command.thread_id);
341
342 if thread_id.is_empty() {
344 error!("Empty thread_id provided in BashCommand");
345 return Err(WinxError::ThreadIdMismatch(
346 "Error: No saved bash state found for thread ID \"\". Please initialize first with this ID.".to_string()
347 ));
348 }
349
350 let mut bash_state: BashState;
352 {
353 let bash_state_guard = bash_state_arc.lock().await;
354
355 let Some(state) = &*bash_state_guard else {
356 error!("BashState not initialized");
357 return Err(WinxError::BashStateNotInitialized);
358 };
359
360 bash_state = state.clone();
361 }
362
363 if thread_id != bash_state.current_thread_id {
365 if !bash_state.load_state_from_disk(&thread_id).unwrap_or(false) {
367 return Err(WinxError::ThreadIdMismatch(format!(
368 "Error: No saved bash state found for thread_id `{thread_id}`. Please initialize first with this ID."
369 )));
370 }
371 }
372
373 let timeout_s = bash_command
376 .wait_for_seconds
377 .map_or(DEFAULT_TIMEOUT, |t| f64::from(t).max(0.0))
378 .min(TIMEOUT_WHILE_OUTPUT);
379
380 let result = execute_bash_action(&mut bash_state, &bash_command.action_json, timeout_s).await;
382
383 {
384 let mut bash_state_guard = bash_state_arc.lock().await;
385 if let Some(state) = bash_state_guard.as_mut() {
386 state.cwd.clone_from(&bash_state.cwd);
387 }
388 }
389
390 match result {
392 Ok(mut output) => {
393 if let BashCommandAction::Command { ref command, .. } = bash_command.action_json {
394 let cmd_trimmed = command.trim();
395 if output.starts_with(cmd_trimmed) {
396 output = output[cmd_trimmed.len()..].to_string();
397 }
398 }
399 Ok(output)
400 }
401 Err(e) => Err(e),
402 }
403}
404
405async fn execute_bash_action(
407 bash_state: &mut BashState,
408 action: &BashCommandAction,
409 timeout_s: f64,
410) -> Result<String> {
411 let mut is_bg = false;
412 let mut bg_id: Option<String> = None;
413
414 let bg_shell: Option<SharedPtyShell> = match action {
416 BashCommandAction::Command { .. } => None, BashCommandAction::StatusCheck { bg_command_id, .. }
418 | BashCommandAction::SendText { bg_command_id, .. }
419 | BashCommandAction::SendSpecials { bg_command_id, .. }
420 | BashCommandAction::SendAscii { bg_command_id, .. } => {
421 if let Some(id) = bg_command_id {
422 let mut manager = BG_SHELL_MANAGER.lock().map_err(|e| {
423 WinxError::BashStateLockError(format!("Failed to lock bg manager: {e}"))
424 })?;
425 manager.prune_finished_shells();
426
427 if let Some(shell) = manager.get_shell(id) {
428 is_bg = true;
429 bg_id = Some(id.clone());
430 Some(shell)
431 } else {
432 let error = format!(
434 "No shell found running with command id {}.\n{}",
435 id,
436 manager.get_running_info()
437 );
438 return Err(WinxError::CommandExecutionError(error));
439 }
440 } else {
441 None
442 }
443 }
444 };
445
446 match action {
448 BashCommandAction::Command { command, is_background } => {
449 execute_command(bash_state, command, *is_background, timeout_s).await
450 }
451 BashCommandAction::StatusCheck { .. } => {
452 execute_status_check(bash_state, bg_shell, is_bg, bg_id.as_deref(), timeout_s).await
453 }
454 BashCommandAction::SendText { send_text, .. } => {
455 execute_send_text(bash_state, send_text, bg_shell, is_bg, bg_id.as_deref(), timeout_s)
456 .await
457 }
458 BashCommandAction::SendSpecials { send_specials, .. } => {
459 execute_send_specials(
460 bash_state,
461 send_specials,
462 bg_shell,
463 is_bg,
464 bg_id.as_deref(),
465 timeout_s,
466 )
467 .await
468 }
469 BashCommandAction::SendAscii { send_ascii, .. } => {
470 execute_send_ascii(bash_state, send_ascii, bg_shell, is_bg, bg_id.as_deref(), timeout_s)
471 .await
472 }
473 }
474}
475
476async fn execute_command(
478 bash_state: &mut BashState,
479 command: &str,
480 is_background: bool,
481 timeout_s: f64,
482) -> Result<String> {
483 debug!("Processing Command action: {}", command);
484
485 if !bash_state.is_command_allowed(command) {
487 error!("Command '{}' not allowed in current mode", command);
488 return Err(WinxError::CommandNotAllowed(
489 "Error: BashCommand not allowed in current mode".to_string(),
490 ));
491 }
492
493 let command = command.trim();
495 crate::utils::bash_parser::assert_single_statement(command)?;
496
497 if is_background {
499 return execute_in_background(bash_state, command, timeout_s).await;
500 }
501
502 {
504 let bash_guard = bash_state.pty_shell.lock().await;
505
506 if let Some(ref bash) = *bash_guard {
507 if bash.command_running {
508 return Err(WinxError::CommandExecutionError(WAITING_INPUT_MESSAGE.to_string()));
509 }
510 }
511 }
512
513 if bash_state.pty_shell.lock().await.is_none() {
515 bash_state
516 .init_pty_shell()
517 .await
518 .map_err(|e| WinxError::CommandExecutionError(format!("Failed to init bash: {e}")))?;
519 }
520
521 {
526 let mut bash_guard = bash_state.pty_shell.lock().await;
527
528 let bash = bash_guard.as_mut().ok_or(WinxError::BashStateNotInitialized)?;
529
530 bash.output_buffer.clear();
531 bash.output_truncated = false;
532 send_utf8_in_byte_chunks(bash, command, COMMAND_CHUNK_SIZE)?;
534
535 bash.send_special_key("Enter").map_err(|e| {
537 WinxError::CommandExecutionError(format!("Failed to send newline: {e}"))
538 })?;
539
540 bash.last_command = command.to_string();
541 bash.command_running = true;
542 }
543
544 let shell_arc = bash_state.pty_shell.clone();
546 wait_for_output(bash_state, &shell_arc, timeout_s, false, None, false).await
547}
548
549async fn wait_for_output(
553 bash_state: &mut BashState,
554 shell_arc: &SharedPtyShell,
555 timeout_s: f64,
556 is_bg: bool,
557 bg_id: Option<&str>,
558 is_status_check: bool,
559) -> Result<String> {
560 let start = Instant::now();
561 let wait = timeout_s.min(TIMEOUT_WHILE_OUTPUT);
562 let mut last_pending_output = String::new();
563 let mut complete = false;
564
565 sleep(Duration::from_secs_f64(wait.min(DEFAULT_TIMEOUT))).await;
567
568 let mut output = {
570 let mut bash_guard = shell_arc.lock().await;
571
572 if let Some(bash) = bash_guard.as_mut() {
573 let (out, done) = bash.read_output(0.5).map_err(|e| {
574 WinxError::CommandExecutionError(format!("Failed to read output: {e}"))
575 })?;
576 complete = done;
577 out
578 } else {
579 String::new()
580 }
581 };
582
583 if !complete && is_status_check {
586 let mut remaining = TIMEOUT_WHILE_OUTPUT - wait;
587 let mut patience = OUTPUT_WAIT_PATIENCE;
588
589 let incremental = wcgw_incremental_text(&output, &last_pending_output);
590 if incremental.is_empty() {
591 patience -= 1;
592 }
593
594 let mut last_incremental = incremental;
595
596 while remaining > 0.0 && patience > 0 {
597 sleep(Duration::from_secs_f64(wait.min(remaining))).await;
598
599 let (new_output, done) = {
600 let mut bash_guard = shell_arc.lock().await;
601
602 if let Some(bash) = bash_guard.as_mut() {
603 bash.read_output(0.5).map_err(|e| {
604 WinxError::CommandExecutionError(format!("Failed to read output: {e}"))
605 })?
606 } else {
607 (String::new(), true)
608 }
609 };
610
611 if done {
612 complete = true;
613 output = new_output;
614 break;
615 }
616
617 let new_incremental = wcgw_incremental_text(&new_output, &last_pending_output);
619 if new_incremental == last_incremental {
620 patience -= 1;
621 } else {
622 patience = OUTPUT_WAIT_PATIENCE; }
624 last_incremental = new_incremental;
625
626 output = new_output;
627 remaining -= wait;
628 }
629
630 if !complete {
631 last_pending_output = output.clone();
633 }
634 }
635
636 if complete {
637 if let Some(cwd) = extract_prompt_cwd(&output) {
638 bash_state.cwd = cwd;
639 }
640 }
641
642 let rendered = wcgw_incremental_text(&output, &last_pending_output);
644
645 let rendered = if rendered.len() > MAX_OUTPUT_LENGTH {
647 format!("(...truncated)\n{}", &rendered[rendered.len() - MAX_OUTPUT_LENGTH..])
648 } else {
649 rendered
650 };
651
652 let running_for = if complete {
654 None
655 } else {
656 Some(format!("{} seconds", (start.elapsed().as_secs() + timeout_s as u64)))
657 };
658
659 let status = get_status(bash_state, is_bg, bg_id, !complete, running_for.as_deref());
661 Ok(format!("{rendered}{status}"))
662}
663
664async fn execute_status_check(
666 bash_state: &mut BashState,
667 bg_shell: Option<SharedPtyShell>,
668 is_bg: bool,
669 bg_id: Option<&str>,
670 timeout_s: f64,
671) -> Result<String> {
672 debug!("Processing StatusCheck action");
673
674 let shell_arc = bg_shell.unwrap_or_else(|| bash_state.pty_shell.clone());
677
678 let is_running = {
680 let guard = shell_arc.lock().await;
681 if let Some(ref bash) = *guard {
682 bash.command_running
683 } else {
684 false
685 }
686 };
687
688 if !is_running && !is_bg {
690 let mut manager = BG_SHELL_MANAGER.lock().map_err(|e| {
691 WinxError::BashStateLockError(format!("Failed to lock bg manager: {e}"))
692 })?;
693 let error =
694 format!("No running command to check status of.\n{}", manager.get_running_info());
695 return Err(WinxError::CommandExecutionError(error));
696 }
697
698 wait_for_output(bash_state, &shell_arc, timeout_s, is_bg, bg_id, true).await
700}
701
702async fn execute_send_text(
704 bash_state: &mut BashState,
705 text: &str,
706 bg_shell: Option<SharedPtyShell>,
707 is_bg: bool,
708 bg_id: Option<&str>,
709 timeout_s: f64,
710) -> Result<String> {
711 debug!("Processing SendText action: {}", text);
712
713 if text.is_empty() {
715 return Err(WinxError::CommandExecutionError(
716 "Failure: send_text cannot be empty".to_string(),
717 ));
718 }
719
720 let shell_arc = bg_shell.unwrap_or_else(|| bash_state.pty_shell.clone());
722
723 {
725 let mut guard = shell_arc.lock().await;
726
727 let bash = guard.as_mut().ok_or(WinxError::BashStateNotInitialized)?;
728
729 send_utf8_in_byte_chunks(bash, text, TEXT_CHUNK_SIZE)?;
731
732 bash.send_special_key("Enter").map_err(|e| {
734 WinxError::CommandExecutionError(format!("Failed to send newline: {e}"))
735 })?;
736 }
737
738 wait_for_output(bash_state, &shell_arc, timeout_s, is_bg, bg_id, false).await
740}
741
742async fn execute_send_specials(
744 bash_state: &mut BashState,
745 keys: &[SpecialKey],
746 bg_shell: Option<SharedPtyShell>,
747 is_bg: bool,
748 bg_id: Option<&str>,
749 timeout_s: f64,
750) -> Result<String> {
751 debug!("Processing SendSpecials action: {:?}", keys);
752
753 if keys.is_empty() {
755 return Err(WinxError::CommandExecutionError(
756 "Failure: send_specials cannot be empty".to_string(),
757 ));
758 }
759
760 let shell_arc = bg_shell.unwrap_or_else(|| bash_state.pty_shell.clone());
761 let mut is_interrupt = false;
762
763 {
764 let mut guard = shell_arc.lock().await;
765
766 let bash = guard.as_mut().ok_or(WinxError::BashStateNotInitialized)?;
767
768 for key in keys {
770 match key {
771 SpecialKey::KeyUp => {
772 bash.send_special_key("KeyUp").map_err(|e| {
774 WinxError::CommandExecutionError(format!("Failed to send KeyUp: {e}"))
775 })?;
776 }
777 SpecialKey::KeyDown => {
778 bash.send_special_key("KeyDown").map_err(|e| {
780 WinxError::CommandExecutionError(format!("Failed to send KeyDown: {e}"))
781 })?;
782 }
783 SpecialKey::KeyLeft => {
784 bash.send_special_key("KeyLeft").map_err(|e| {
786 WinxError::CommandExecutionError(format!("Failed to send KeyLeft: {e}"))
787 })?;
788 }
789 SpecialKey::KeyRight => {
790 bash.send_special_key("KeyRight").map_err(|e| {
792 WinxError::CommandExecutionError(format!("Failed to send KeyRight: {e}"))
793 })?;
794 }
795 SpecialKey::Enter => {
796 bash.send_special_key("Enter").map_err(|e| {
798 WinxError::CommandExecutionError(format!("Failed to send Enter: {e}"))
799 })?;
800 }
801 SpecialKey::CtrlC => {
802 bash.send_interrupt().map_err(|e| {
804 WinxError::CommandExecutionError(format!("Failed to send interrupt: {e}"))
805 })?;
806 is_interrupt = true;
807 }
808 SpecialKey::CtrlD => {
809 bash.send_eof().map_err(|e| {
811 WinxError::CommandExecutionError(format!("Failed to send Ctrl+D: {e}"))
812 })?;
813 is_interrupt = true;
814 }
815 SpecialKey::CtrlZ => {
816 bash.send_suspend().map_err(|e| {
818 WinxError::CommandExecutionError(format!("Failed to send Ctrl+Z: {e}"))
819 })?;
820 }
821 }
822 }
823 }
824
825 let mut output =
827 wait_for_output(bash_state, &shell_arc, timeout_s, is_bg, bg_id, false).await?;
828
829 if is_interrupt && output.contains("status = still running") {
831 output.push_str("\n---\n----\nFailure interrupting.\nYou may want to try Ctrl-c again or program specific exit interactive commands.\n");
832 }
833
834 Ok(output)
835}
836
837async fn execute_send_ascii(
839 bash_state: &mut BashState,
840 ascii_codes: &[u8],
841 bg_shell: Option<SharedPtyShell>,
842 is_bg: bool,
843 bg_id: Option<&str>,
844 timeout_s: f64,
845) -> Result<String> {
846 debug!("Processing SendAscii action: {:?}", ascii_codes);
847
848 if ascii_codes.is_empty() {
850 return Err(WinxError::CommandExecutionError(
851 "Failure: send_ascii cannot be empty".to_string(),
852 ));
853 }
854
855 let shell_arc = bg_shell.unwrap_or_else(|| bash_state.pty_shell.clone());
856 let mut is_interrupt = false;
857
858 {
859 let mut guard = shell_arc.lock().await;
860
861 let bash = guard.as_mut().ok_or(WinxError::BashStateNotInitialized)?;
862
863 for &code in ascii_codes {
865 bash.send_bytes(&[code]).map_err(|e| {
867 WinxError::CommandExecutionError(format!("Failed to write ASCII code: {e}"))
868 })?;
869
870 if code == 3 {
872 is_interrupt = true;
873 }
874 }
875 }
876
877 let mut output =
879 wait_for_output(bash_state, &shell_arc, timeout_s, is_bg, bg_id, false).await?;
880
881 if is_interrupt && output.contains("status = still running") {
883 output.push_str("\n---\n----\nFailure interrupting.\nYou may want to try Ctrl-c again or program specific exit interactive commands.\n");
884 }
885
886 Ok(output)
887}
888
889async fn execute_in_background(
891 bash_state: &mut BashState,
892 command: &str,
893 timeout_s: f64,
894) -> Result<String> {
895 debug!("Executing command in background: {}", command);
896
897 let restricted_mode =
899 matches!(bash_state.bash_command_mode.bash_mode, crate::types::BashMode::RestrictedMode);
900
901 let bg_id = {
902 let mut manager = BG_SHELL_MANAGER.lock().map_err(|e| {
903 WinxError::BashStateLockError(format!("Failed to lock bg manager: {e}"))
904 })?;
905 manager.start_new_shell(&bash_state.cwd, restricted_mode)?
906 };
907
908 let shell_arc = {
910 let manager = BG_SHELL_MANAGER.lock().map_err(|e| {
911 WinxError::BashStateLockError(format!("Failed to lock bg manager: {e}"))
912 })?;
913 manager.get_shell(&bg_id).ok_or_else(|| {
914 WinxError::CommandExecutionError("Failed to get background shell".to_string())
915 })?
916 };
917
918 {
920 let mut guard = shell_arc.lock().await;
921 let bash = guard.as_mut().ok_or(WinxError::BashStateNotInitialized)?;
922 bash.send_command(command).map_err(|e| {
923 WinxError::CommandExecutionError(format!("Failed to send bg command: {e}"))
924 })?;
925 }
926 debug!("bg[{}]: send_command returned, replying with bg_command_id", bg_id);
927
928 let _ = timeout_s;
929 let _ = shell_arc;
930 Ok(get_status(bash_state, true, Some(&bg_id), true, None))
931}
932
933#[allow(dead_code)]
937#[tracing::instrument(level = "debug", skip(command, cwd))]
938async fn execute_simple_command(command: &str, cwd: &Path) -> Result<String> {
939 debug!("Executing command: {}", command);
940
941 let start_time = Instant::now();
942 let mut cmd = Command::new("sh");
943 cmd.arg("-c")
944 .arg(command)
945 .current_dir(cwd)
946 .stdin(Stdio::null())
947 .stdout(Stdio::piped())
948 .stderr(Stdio::piped());
949
950 let output = cmd.output().context("Failed to execute command")?;
951 let elapsed = start_time.elapsed();
952
953 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
954 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
955
956 let raw_result = format!("{stdout}{stderr}");
957 let mut result = raw_result.clone();
958 if !raw_result.is_empty() {
959 let rendered_lines = render_terminal_output(&raw_result);
960 if rendered_lines.is_empty() {
961 result = strip_ansi_codes(&raw_result);
963 } else {
964 result = rendered_lines.join("\n");
965 }
966 }
967
968 if result.len() > MAX_OUTPUT_LENGTH {
969 result = format!("(...truncated)\n{}", &result[result.len() - MAX_OUTPUT_LENGTH..]);
970 }
971
972 let exit_status = if output.status.success() {
973 "Command completed successfully".to_string()
974 } else {
975 format!("Command failed with status: {}", output.status)
976 };
977
978 let current_dir = std::env::current_dir()
979 .map_or_else(|_| "Unknown".to_string(), |p| p.to_string_lossy().into_owned());
980
981 debug!("Command executed in {:.2?}", elapsed);
982 Ok(format!("{result}\n\n---\n\nstatus = {exit_status}\ncwd = {current_dir}\n"))
983}
984
985#[allow(dead_code)]
987#[tracing::instrument(level = "debug", skip(command, cwd, screen_name))]
988async fn execute_in_screen(command: &str, cwd: &Path, screen_name: &str) -> Result<String> {
989 debug!("Executing command in screen session '{}': {}", screen_name, command);
990
991 let screen_check = Command::new("which")
992 .arg("screen")
993 .output()
994 .context("Failed to check for screen command")?;
995
996 if !screen_check.status.success() {
997 warn!("Screen command not found, falling back to direct execution");
998 return execute_simple_command(command, cwd).await;
999 }
1000
1001 let _cleanup = Command::new("screen").args(["-X", "-S", screen_name, "quit"]).output();
1002
1003 let screen_cmd = format!(
1004 "screen -dmS {} bash -c '{} ; ec=$? ; echo \"Command completed with exit code: $ec\" ; sleep 1 ; exit $ec'",
1005 screen_name,
1006 command.replace('\'', "'\\''")
1007 );
1008
1009 let screen_start = Command::new("sh")
1010 .arg("-c")
1011 .arg(&screen_cmd)
1012 .current_dir(cwd)
1013 .output()
1014 .context("Failed to start screen session")?;
1015
1016 if !screen_start.status.success() {
1017 let stderr = String::from_utf8_lossy(&screen_start.stderr).to_string();
1018 error!("Failed to start screen session: {}", stderr);
1019 return Err(WinxError::CommandExecutionError(format!(
1020 "Failed to start screen session: {stderr}"
1021 )));
1022 }
1023
1024 sleep(Duration::from_millis(300)).await;
1025
1026 let screen_check =
1027 Command::new("screen").args(["-ls"]).output().context("Failed to list screen sessions")?;
1028
1029 let screen_list = String::from_utf8_lossy(&screen_check.stdout).to_string();
1030
1031 let current_dir = std::env::current_dir()
1032 .map_or_else(|_| "Unknown".to_string(), |p| p.to_string_lossy().into_owned());
1033
1034 Ok(format!(
1035 "Started command in background screen session '{screen_name}'.\n\
1036 Use status_check to get output.\n\n\
1037 Screen sessions:\n{screen_list}\n\
1038 ---\n\n\
1039 status = running in background\n\
1040 cwd = {current_dir}\n"
1041 ))
1042}
1043
1044#[allow(dead_code)]
1046fn special_key_to_screen_input(key: SpecialKey) -> String {
1047 match key {
1048 SpecialKey::Enter => String::from("\r"),
1049 SpecialKey::KeyUp => String::from("\x1b[A"),
1050 SpecialKey::KeyDown => String::from("\x1b[B"),
1051 SpecialKey::KeyLeft => String::from("\x1b[D"),
1052 SpecialKey::KeyRight => String::from("\x1b[C"),
1053 SpecialKey::CtrlC => String::from("\x03"),
1054 SpecialKey::CtrlD => String::from("\x04"),
1055 SpecialKey::CtrlZ => String::from("\x1a"),
1056 }
1057}