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.last_command.is_empty() {
125 continue;
126 }
127
128 if shell.command_running {
129 let _ = shell.read_output(0.1);
130 }
131
132 if !shell.command_running {
133 finished.push(id.clone());
134 }
135 }
136
137 for id in finished {
138 self.remove_shell(&id);
139 }
140 }
141
142 pub fn get_running_info(&mut self) -> String {
144 self.prune_finished_shells();
145
146 if self.shells.is_empty() {
147 return "No command running in background.\n".to_string();
148 }
149
150 let mut running = Vec::new();
151 for (id, shell_arc) in &self.shells {
152 if let Ok(guard) = shell_arc.try_lock() {
153 if let Some(bash) = guard.as_ref() {
154 if bash.command_running {
155 running
156 .push(format!("Command: {}, bg_command_id: {}", bash.last_command, id));
157 }
158 }
159 } else {
160 running.push(format!("Command: <busy>, bg_command_id: {id}"));
161 }
162 }
163
164 if running.is_empty() {
165 "No command running in background.\n".to_string()
166 } else {
167 format!("Following background commands are attached:\n{}\n", running.join("\n"))
168 }
169 }
170}
171
172lazy_static::lazy_static! {
174 static ref BG_SHELL_MANAGER: StdMutex<BackgroundShellManager> = StdMutex::new(BackgroundShellManager::new());
175}
176
177fn get_status(
181 bash_state: &BashState,
182 is_bg: bool,
183 bg_id: Option<&str>,
184 is_running: bool,
185 running_for: Option<&str>,
186) -> String {
187 let mut status = "\n\n---\n\n".to_string();
188
189 if is_bg {
190 if let Some(id) = bg_id {
191 let _ = writeln!(status, "bg_command_id = {id}");
192 }
193 }
194
195 if is_running {
196 status.push_str("status = still running\n");
197 if let Some(duration) = running_for {
198 let _ = writeln!(status, "running for = {duration}");
199 }
200 } else {
201 status.push_str("status = process exited\n");
202 }
203
204 let _ = writeln!(status, "cwd = {}", bash_state.cwd.display());
205
206 if !is_bg {
207 if let Ok(mut manager) = BG_SHELL_MANAGER.lock() {
209 status.push_str("This is the main shell. ");
210 status.push_str(&manager.get_running_info());
211 }
212 }
213
214 status.trim_end().to_string()
215}
216
217fn wcgw_incremental_text(text: &str, last_pending_output: &str) -> String {
219 let text =
220 if text.len() > MAX_OUTPUT_LENGTH { &text[text.len() - MAX_OUTPUT_LENGTH..] } else { text };
221
222 if last_pending_output.is_empty() {
223 let rendered = render_terminal_output(text);
224 return rstrip_lines(&rendered).trim_start().to_string();
225 }
226
227 let last_rendered = render_terminal_output(last_pending_output);
228 if last_rendered.is_empty() {
229 return rstrip_lines(&render_terminal_output(text));
230 }
231
232 let text_after_last = if text.len() > last_pending_output.len() {
234 &text[last_pending_output.len()..]
235 } else {
236 text
237 };
238
239 let combined = format!("{}\n{}", last_rendered.join("\n"), text_after_last);
240 let new_rendered = render_terminal_output(&combined);
241
242 let incremental = get_incremental_output(&last_rendered, &new_rendered);
244 rstrip_lines(&incremental)
245}
246
247fn extract_prompt_cwd(output: &str) -> Option<PathBuf> {
248 let stripped = strip_ansi_codes(output);
249 let prompt_regex = Regex::new(r"◉ (?P<cwd>[^\r\n]*?)──➤").ok()?;
250
251 prompt_regex
252 .captures_iter(&stripped)
253 .filter_map(|captures| captures.name("cwd").map(|cwd| cwd.as_str().trim()))
254 .filter(|cwd| !cwd.is_empty())
255 .last()
256 .map(PathBuf::from)
257}
258
259fn rstrip_lines(lines: &[String]) -> String {
261 lines.iter().map(|line| line.trim_end()).collect::<Vec<_>>().join("\n")
262}
263
264fn get_incremental_output(old_output: &[String], new_output: &[String]) -> Vec<String> {
266 if old_output.is_empty() {
267 return new_output.to_vec();
268 }
269
270 let nold = old_output.len();
271 let nnew = new_output.len();
272
273 for i in (0..nnew).rev() {
275 if new_output[i] != old_output[nold - 1] {
276 continue;
277 }
278
279 let mut matched = true;
280 for j in (0..i).rev() {
281 let old_idx = (nold as i64 - 1 + j as i64 - i as i64) as isize;
282 if old_idx < 0 {
283 break;
284 }
285 if new_output[j] != old_output[old_idx as usize] {
286 matched = false;
287 break;
288 }
289 }
290
291 if matched {
292 return new_output[i + 1..].to_vec();
293 }
294 }
295
296 new_output.to_vec()
297}
298
299fn send_utf8_in_byte_chunks(shell: &mut PtyShell, text: &str, chunk_size: usize) -> Result<()> {
300 let mut start = 0;
301
302 while start < text.len() {
303 let mut end = (start + chunk_size).min(text.len());
304 while !text.is_char_boundary(end) {
305 end -= 1;
306 }
307 if end == start {
308 end = text[start..].char_indices().nth(1).map_or(text.len(), |(idx, _)| start + idx);
309 }
310
311 shell.send_text(&text[start..end]).map_err(|e| {
312 WinxError::CommandExecutionError(format!("Failed to write PTY input: {e}"))
313 })?;
314 start = end;
315 }
316
317 Ok(())
318}
319
320#[allow(dead_code)]
322fn is_status_check_action(action: &BashCommandAction) -> bool {
323 match action {
324 BashCommandAction::StatusCheck { .. } => true,
325 BashCommandAction::SendSpecials { send_specials, .. } => {
326 send_specials.len() == 1 && send_specials[0] == SpecialKey::Enter
327 }
328 BashCommandAction::SendAscii { send_ascii, .. } => {
329 send_ascii.len() == 1 && send_ascii[0] == 10 }
331 _ => false,
332 }
333}
334
335#[tracing::instrument(level = "info", skip(bash_state_arc, bash_command))]
342pub async fn handle_tool_call(
343 bash_state_arc: &Arc<Mutex<Option<BashState>>>,
344 bash_command: BashCommand,
345) -> Result<String> {
346 info!("BashCommand tool called with: {:?}", bash_command);
347
348 let thread_id = normalize_thread_id(&bash_command.thread_id);
349
350 if thread_id.is_empty() {
352 error!("Empty thread_id provided in BashCommand");
353 return Err(WinxError::ThreadIdMismatch(
354 "Error: No saved bash state found for thread ID \"\". Please initialize first with this ID.".to_string()
355 ));
356 }
357
358 let mut bash_state: BashState;
360 {
361 let bash_state_guard = bash_state_arc.lock().await;
362
363 let Some(state) = &*bash_state_guard else {
364 error!("BashState not initialized");
365 return Err(WinxError::BashStateNotInitialized);
366 };
367
368 bash_state = state.clone();
369 }
370
371 if thread_id != bash_state.current_thread_id {
373 if !bash_state.load_state_from_disk(&thread_id).unwrap_or(false) {
375 return Err(WinxError::ThreadIdMismatch(format!(
376 "Error: No saved bash state found for thread_id `{thread_id}`. Please initialize first with this ID."
377 )));
378 }
379 }
380
381 let timeout_s = bash_command
384 .wait_for_seconds
385 .map_or(DEFAULT_TIMEOUT, |t| f64::from(t).max(0.0))
386 .min(TIMEOUT_WHILE_OUTPUT);
387
388 let result = execute_bash_action(&mut bash_state, &bash_command.action_json, timeout_s).await;
390
391 {
392 let mut bash_state_guard = bash_state_arc.lock().await;
393 if let Some(state) = bash_state_guard.as_mut() {
394 state.cwd.clone_from(&bash_state.cwd);
395 }
396 }
397
398 match result {
400 Ok(mut output) => {
401 if let BashCommandAction::Command { ref command, .. } = bash_command.action_json {
402 let cmd_trimmed = command.trim();
403 if output.starts_with(cmd_trimmed) {
404 output = output[cmd_trimmed.len()..].to_string();
405 }
406 }
407 Ok(output)
408 }
409 Err(e) => Err(e),
410 }
411}
412
413async fn execute_bash_action(
415 bash_state: &mut BashState,
416 action: &BashCommandAction,
417 timeout_s: f64,
418) -> Result<String> {
419 let mut is_bg = false;
420 let mut bg_id: Option<String> = None;
421
422 let bg_shell: Option<SharedPtyShell> = match action {
424 BashCommandAction::Command { .. } => None, BashCommandAction::StatusCheck { bg_command_id, .. }
426 | BashCommandAction::SendText { bg_command_id, .. }
427 | BashCommandAction::SendSpecials { bg_command_id, .. }
428 | BashCommandAction::SendAscii { bg_command_id, .. } => {
429 if let Some(id) = bg_command_id {
430 let mut manager = BG_SHELL_MANAGER.lock().map_err(|e| {
431 WinxError::BashStateLockError(format!("Failed to lock bg manager: {e}"))
432 })?;
433 manager.prune_finished_shells();
434
435 if let Some(shell) = manager.get_shell(id) {
436 is_bg = true;
437 bg_id = Some(id.clone());
438 Some(shell)
439 } else {
440 let error = format!(
442 "No shell found running with command id {}.\n{}",
443 id,
444 manager.get_running_info()
445 );
446 return Err(WinxError::CommandExecutionError(error));
447 }
448 } else {
449 None
450 }
451 }
452 };
453
454 match action {
456 BashCommandAction::Command { command, is_background } => {
457 execute_command(bash_state, command, *is_background, timeout_s).await
458 }
459 BashCommandAction::StatusCheck { .. } => {
460 execute_status_check(bash_state, bg_shell, is_bg, bg_id.as_deref(), timeout_s).await
461 }
462 BashCommandAction::SendText { send_text, .. } => {
463 execute_send_text(bash_state, send_text, bg_shell, is_bg, bg_id.as_deref(), timeout_s)
464 .await
465 }
466 BashCommandAction::SendSpecials { send_specials, .. } => {
467 execute_send_specials(
468 bash_state,
469 send_specials,
470 bg_shell,
471 is_bg,
472 bg_id.as_deref(),
473 timeout_s,
474 )
475 .await
476 }
477 BashCommandAction::SendAscii { send_ascii, .. } => {
478 execute_send_ascii(bash_state, send_ascii, bg_shell, is_bg, bg_id.as_deref(), timeout_s)
479 .await
480 }
481 }
482}
483
484async fn execute_command(
486 bash_state: &mut BashState,
487 command: &str,
488 is_background: bool,
489 timeout_s: f64,
490) -> Result<String> {
491 debug!("Processing Command action: {}", command);
492
493 if !bash_state.is_command_allowed(command) {
495 error!("Command '{}' not allowed in current mode", command);
496 return Err(WinxError::CommandNotAllowed(
497 "Error: BashCommand not allowed in current mode".to_string(),
498 ));
499 }
500
501 let command = command.trim();
503 crate::utils::bash_parser::assert_single_statement(command)?;
504
505 if is_background {
507 return execute_in_background(bash_state, command, timeout_s).await;
508 }
509
510 {
512 let bash_guard = bash_state.pty_shell.lock().await;
513
514 if let Some(ref bash) = *bash_guard {
515 if bash.command_running {
516 return Err(WinxError::CommandExecutionError(WAITING_INPUT_MESSAGE.to_string()));
517 }
518 }
519 }
520
521 if bash_state.pty_shell.lock().await.is_none() {
523 bash_state
524 .init_pty_shell()
525 .await
526 .map_err(|e| WinxError::CommandExecutionError(format!("Failed to init bash: {e}")))?;
527 }
528
529 {
534 let mut bash_guard = bash_state.pty_shell.lock().await;
535
536 let bash = bash_guard.as_mut().ok_or(WinxError::BashStateNotInitialized)?;
537
538 bash.output_buffer.clear();
539 bash.output_truncated = false;
540 send_utf8_in_byte_chunks(bash, command, COMMAND_CHUNK_SIZE)?;
542
543 bash.send_special_key("Enter").map_err(|e| {
545 WinxError::CommandExecutionError(format!("Failed to send newline: {e}"))
546 })?;
547
548 bash.last_command = command.to_string();
549 bash.command_running = true;
550 }
551
552 let shell_arc = bash_state.pty_shell.clone();
554 wait_for_output(bash_state, &shell_arc, timeout_s, false, None, false).await
555}
556
557async fn wait_for_output(
561 bash_state: &mut BashState,
562 shell_arc: &SharedPtyShell,
563 timeout_s: f64,
564 is_bg: bool,
565 bg_id: Option<&str>,
566 is_status_check: bool,
567) -> Result<String> {
568 let start = Instant::now();
569 let wait = timeout_s.min(TIMEOUT_WHILE_OUTPUT);
570 let mut last_pending_output = String::new();
571 let mut complete = false;
572
573 sleep(Duration::from_secs_f64(wait.min(DEFAULT_TIMEOUT))).await;
575
576 let mut output = {
578 let mut bash_guard = shell_arc.lock().await;
579
580 if let Some(bash) = bash_guard.as_mut() {
581 let (out, done) = bash.read_output(0.5).map_err(|e| {
582 WinxError::CommandExecutionError(format!("Failed to read output: {e}"))
583 })?;
584 complete = done;
585 out
586 } else {
587 String::new()
588 }
589 };
590
591 if !complete && is_status_check {
594 let mut remaining = TIMEOUT_WHILE_OUTPUT - wait;
595 let mut patience = OUTPUT_WAIT_PATIENCE;
596
597 let incremental = wcgw_incremental_text(&output, &last_pending_output);
598 if incremental.is_empty() {
599 patience -= 1;
600 }
601
602 let mut last_incremental = incremental;
603
604 while remaining > 0.0 && patience > 0 {
605 sleep(Duration::from_secs_f64(wait.min(remaining))).await;
606
607 let (new_output, done) = {
608 let mut bash_guard = shell_arc.lock().await;
609
610 if let Some(bash) = bash_guard.as_mut() {
611 bash.read_output(0.5).map_err(|e| {
612 WinxError::CommandExecutionError(format!("Failed to read output: {e}"))
613 })?
614 } else {
615 (String::new(), true)
616 }
617 };
618
619 if done {
620 complete = true;
621 output = new_output;
622 break;
623 }
624
625 let new_incremental = wcgw_incremental_text(&new_output, &last_pending_output);
627 if new_incremental == last_incremental {
628 patience -= 1;
629 } else {
630 patience = OUTPUT_WAIT_PATIENCE; }
632 last_incremental = new_incremental;
633
634 output = new_output;
635 remaining -= wait;
636 }
637
638 if !complete {
639 last_pending_output = output.clone();
641 }
642 }
643
644 if complete {
645 if let Some(cwd) = extract_prompt_cwd(&output) {
646 bash_state.cwd = cwd;
647 }
648 }
649
650 let rendered = wcgw_incremental_text(&output, &last_pending_output);
652
653 let rendered = if rendered.len() > MAX_OUTPUT_LENGTH {
655 format!("(...truncated)\n{}", &rendered[rendered.len() - MAX_OUTPUT_LENGTH..])
656 } else {
657 rendered
658 };
659
660 let running_for = if complete {
662 None
663 } else {
664 Some(format!("{} seconds", (start.elapsed().as_secs() + timeout_s as u64)))
665 };
666
667 let status = get_status(bash_state, is_bg, bg_id, !complete, running_for.as_deref());
669 Ok(format!("{rendered}{status}"))
670}
671
672async fn execute_status_check(
674 bash_state: &mut BashState,
675 bg_shell: Option<SharedPtyShell>,
676 is_bg: bool,
677 bg_id: Option<&str>,
678 timeout_s: f64,
679) -> Result<String> {
680 debug!("Processing StatusCheck action");
681
682 let shell_arc = bg_shell.unwrap_or_else(|| bash_state.pty_shell.clone());
685
686 let is_running = {
688 let guard = shell_arc.lock().await;
689 if let Some(ref bash) = *guard {
690 bash.command_running
691 } else {
692 false
693 }
694 };
695
696 if !is_running && !is_bg {
698 let mut manager = BG_SHELL_MANAGER.lock().map_err(|e| {
699 WinxError::BashStateLockError(format!("Failed to lock bg manager: {e}"))
700 })?;
701 let error =
702 format!("No running command to check status of.\n{}", manager.get_running_info());
703 return Err(WinxError::CommandExecutionError(error));
704 }
705
706 wait_for_output(bash_state, &shell_arc, timeout_s, is_bg, bg_id, true).await
708}
709
710async fn execute_send_text(
712 bash_state: &mut BashState,
713 text: &str,
714 bg_shell: Option<SharedPtyShell>,
715 is_bg: bool,
716 bg_id: Option<&str>,
717 timeout_s: f64,
718) -> Result<String> {
719 debug!("Processing SendText action: {}", text);
720
721 if text.is_empty() {
723 return Err(WinxError::CommandExecutionError(
724 "Failure: send_text cannot be empty".to_string(),
725 ));
726 }
727
728 let shell_arc = bg_shell.unwrap_or_else(|| bash_state.pty_shell.clone());
730
731 {
733 let mut guard = shell_arc.lock().await;
734
735 let bash = guard.as_mut().ok_or(WinxError::BashStateNotInitialized)?;
736
737 send_utf8_in_byte_chunks(bash, text, TEXT_CHUNK_SIZE)?;
739
740 bash.send_special_key("Enter").map_err(|e| {
742 WinxError::CommandExecutionError(format!("Failed to send newline: {e}"))
743 })?;
744 }
745
746 wait_for_output(bash_state, &shell_arc, timeout_s, is_bg, bg_id, false).await
748}
749
750async fn execute_send_specials(
752 bash_state: &mut BashState,
753 keys: &[SpecialKey],
754 bg_shell: Option<SharedPtyShell>,
755 is_bg: bool,
756 bg_id: Option<&str>,
757 timeout_s: f64,
758) -> Result<String> {
759 debug!("Processing SendSpecials action: {:?}", keys);
760
761 if keys.is_empty() {
763 return Err(WinxError::CommandExecutionError(
764 "Failure: send_specials cannot be empty".to_string(),
765 ));
766 }
767
768 let shell_arc = bg_shell.unwrap_or_else(|| bash_state.pty_shell.clone());
769 let mut is_interrupt = false;
770
771 {
772 let mut guard = shell_arc.lock().await;
773
774 let bash = guard.as_mut().ok_or(WinxError::BashStateNotInitialized)?;
775
776 for key in keys {
778 match key {
779 SpecialKey::KeyUp => {
780 bash.send_special_key("KeyUp").map_err(|e| {
782 WinxError::CommandExecutionError(format!("Failed to send KeyUp: {e}"))
783 })?;
784 }
785 SpecialKey::KeyDown => {
786 bash.send_special_key("KeyDown").map_err(|e| {
788 WinxError::CommandExecutionError(format!("Failed to send KeyDown: {e}"))
789 })?;
790 }
791 SpecialKey::KeyLeft => {
792 bash.send_special_key("KeyLeft").map_err(|e| {
794 WinxError::CommandExecutionError(format!("Failed to send KeyLeft: {e}"))
795 })?;
796 }
797 SpecialKey::KeyRight => {
798 bash.send_special_key("KeyRight").map_err(|e| {
800 WinxError::CommandExecutionError(format!("Failed to send KeyRight: {e}"))
801 })?;
802 }
803 SpecialKey::Enter => {
804 bash.send_special_key("Enter").map_err(|e| {
806 WinxError::CommandExecutionError(format!("Failed to send Enter: {e}"))
807 })?;
808 }
809 SpecialKey::CtrlC => {
810 bash.send_interrupt().map_err(|e| {
812 WinxError::CommandExecutionError(format!("Failed to send interrupt: {e}"))
813 })?;
814 is_interrupt = true;
815 }
816 SpecialKey::CtrlD => {
817 bash.send_eof().map_err(|e| {
819 WinxError::CommandExecutionError(format!("Failed to send Ctrl+D: {e}"))
820 })?;
821 is_interrupt = true;
822 }
823 SpecialKey::CtrlZ => {
824 bash.send_suspend().map_err(|e| {
826 WinxError::CommandExecutionError(format!("Failed to send Ctrl+Z: {e}"))
827 })?;
828 }
829 }
830 }
831 }
832
833 let mut output =
835 wait_for_output(bash_state, &shell_arc, timeout_s, is_bg, bg_id, false).await?;
836
837 if is_interrupt && output.contains("status = still running") {
839 output.push_str("\n---\n----\nFailure interrupting.\nYou may want to try Ctrl-c again or program specific exit interactive commands.\n");
840 }
841
842 Ok(output)
843}
844
845async fn execute_send_ascii(
847 bash_state: &mut BashState,
848 ascii_codes: &[u8],
849 bg_shell: Option<SharedPtyShell>,
850 is_bg: bool,
851 bg_id: Option<&str>,
852 timeout_s: f64,
853) -> Result<String> {
854 debug!("Processing SendAscii action: {:?}", ascii_codes);
855
856 if ascii_codes.is_empty() {
858 return Err(WinxError::CommandExecutionError(
859 "Failure: send_ascii cannot be empty".to_string(),
860 ));
861 }
862
863 let shell_arc = bg_shell.unwrap_or_else(|| bash_state.pty_shell.clone());
864 let mut is_interrupt = false;
865
866 {
867 let mut guard = shell_arc.lock().await;
868
869 let bash = guard.as_mut().ok_or(WinxError::BashStateNotInitialized)?;
870
871 for &code in ascii_codes {
873 bash.send_bytes(&[code]).map_err(|e| {
875 WinxError::CommandExecutionError(format!("Failed to write ASCII code: {e}"))
876 })?;
877
878 if code == 3 {
880 is_interrupt = true;
881 }
882 }
883 }
884
885 let mut output =
887 wait_for_output(bash_state, &shell_arc, timeout_s, is_bg, bg_id, false).await?;
888
889 if is_interrupt && output.contains("status = still running") {
891 output.push_str("\n---\n----\nFailure interrupting.\nYou may want to try Ctrl-c again or program specific exit interactive commands.\n");
892 }
893
894 Ok(output)
895}
896
897async fn execute_in_background(
899 bash_state: &mut BashState,
900 command: &str,
901 timeout_s: f64,
902) -> Result<String> {
903 debug!("Executing command in background: {}", command);
904
905 let restricted_mode =
907 matches!(bash_state.bash_command_mode.bash_mode, crate::types::BashMode::RestrictedMode);
908
909 let bg_id = {
910 let mut manager = BG_SHELL_MANAGER.lock().map_err(|e| {
911 WinxError::BashStateLockError(format!("Failed to lock bg manager: {e}"))
912 })?;
913 manager.start_new_shell(&bash_state.cwd, restricted_mode)?
914 };
915
916 let shell_arc = {
918 let manager = BG_SHELL_MANAGER.lock().map_err(|e| {
919 WinxError::BashStateLockError(format!("Failed to lock bg manager: {e}"))
920 })?;
921 manager.get_shell(&bg_id).ok_or_else(|| {
922 WinxError::CommandExecutionError("Failed to get background shell".to_string())
923 })?
924 };
925
926 {
928 let mut guard = shell_arc.lock().await;
929 let bash = guard.as_mut().ok_or(WinxError::BashStateNotInitialized)?;
930 bash.send_command(command).map_err(|e| {
931 WinxError::CommandExecutionError(format!("Failed to send bg command: {e}"))
932 })?;
933 }
934 debug!("bg[{}]: send_command returned, replying with bg_command_id", bg_id);
935
936 let _ = timeout_s;
937 let _ = shell_arc;
938 Ok(get_status(bash_state, true, Some(&bg_id), true, None))
939}
940
941#[allow(dead_code)]
945#[tracing::instrument(level = "debug", skip(command, cwd))]
946async fn execute_simple_command(command: &str, cwd: &Path) -> Result<String> {
947 debug!("Executing command: {}", command);
948
949 let start_time = Instant::now();
950 let mut cmd = Command::new("sh");
951 cmd.arg("-c")
952 .arg(command)
953 .current_dir(cwd)
954 .stdin(Stdio::null())
955 .stdout(Stdio::piped())
956 .stderr(Stdio::piped());
957
958 let output = cmd.output().context("Failed to execute command")?;
959 let elapsed = start_time.elapsed();
960
961 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
962 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
963
964 let raw_result = format!("{stdout}{stderr}");
965 let mut result = raw_result.clone();
966 if !raw_result.is_empty() {
967 let rendered_lines = render_terminal_output(&raw_result);
968 if rendered_lines.is_empty() {
969 result = strip_ansi_codes(&raw_result);
971 } else {
972 result = rendered_lines.join("\n");
973 }
974 }
975
976 if result.len() > MAX_OUTPUT_LENGTH {
977 result = format!("(...truncated)\n{}", &result[result.len() - MAX_OUTPUT_LENGTH..]);
978 }
979
980 let exit_status = if output.status.success() {
981 "Command completed successfully".to_string()
982 } else {
983 format!("Command failed with status: {}", output.status)
984 };
985
986 let current_dir = std::env::current_dir()
987 .map_or_else(|_| "Unknown".to_string(), |p| p.to_string_lossy().into_owned());
988
989 debug!("Command executed in {:.2?}", elapsed);
990 Ok(format!("{result}\n\n---\n\nstatus = {exit_status}\ncwd = {current_dir}\n"))
991}
992
993#[allow(dead_code)]
995#[tracing::instrument(level = "debug", skip(command, cwd, screen_name))]
996async fn execute_in_screen(command: &str, cwd: &Path, screen_name: &str) -> Result<String> {
997 debug!("Executing command in screen session '{}': {}", screen_name, command);
998
999 let screen_check = Command::new("which")
1000 .arg("screen")
1001 .output()
1002 .context("Failed to check for screen command")?;
1003
1004 if !screen_check.status.success() {
1005 warn!("Screen command not found, falling back to direct execution");
1006 return execute_simple_command(command, cwd).await;
1007 }
1008
1009 let _cleanup = Command::new("screen").args(["-X", "-S", screen_name, "quit"]).output();
1010
1011 let screen_cmd = format!(
1012 "screen -dmS {} bash -c '{} ; ec=$? ; echo \"Command completed with exit code: $ec\" ; sleep 1 ; exit $ec'",
1013 screen_name,
1014 command.replace('\'', "'\\''")
1015 );
1016
1017 let screen_start = Command::new("sh")
1018 .arg("-c")
1019 .arg(&screen_cmd)
1020 .current_dir(cwd)
1021 .output()
1022 .context("Failed to start screen session")?;
1023
1024 if !screen_start.status.success() {
1025 let stderr = String::from_utf8_lossy(&screen_start.stderr).to_string();
1026 error!("Failed to start screen session: {}", stderr);
1027 return Err(WinxError::CommandExecutionError(format!(
1028 "Failed to start screen session: {stderr}"
1029 )));
1030 }
1031
1032 sleep(Duration::from_millis(300)).await;
1033
1034 let screen_check =
1035 Command::new("screen").args(["-ls"]).output().context("Failed to list screen sessions")?;
1036
1037 let screen_list = String::from_utf8_lossy(&screen_check.stdout).to_string();
1038
1039 let current_dir = std::env::current_dir()
1040 .map_or_else(|_| "Unknown".to_string(), |p| p.to_string_lossy().into_owned());
1041
1042 Ok(format!(
1043 "Started command in background screen session '{screen_name}'.\n\
1044 Use status_check to get output.\n\n\
1045 Screen sessions:\n{screen_list}\n\
1046 ---\n\n\
1047 status = running in background\n\
1048 cwd = {current_dir}\n"
1049 ))
1050}
1051
1052#[allow(dead_code)]
1054fn special_key_to_screen_input(key: SpecialKey) -> String {
1055 match key {
1056 SpecialKey::Enter => String::from("\r"),
1057 SpecialKey::KeyUp => String::from("\x1b[A"),
1058 SpecialKey::KeyDown => String::from("\x1b[B"),
1059 SpecialKey::KeyLeft => String::from("\x1b[D"),
1060 SpecialKey::KeyRight => String::from("\x1b[C"),
1061 SpecialKey::CtrlC => String::from("\x03"),
1062 SpecialKey::CtrlD => String::from("\x04"),
1063 SpecialKey::CtrlZ => String::from("\x1a"),
1064 }
1065}