1use std::collections::{HashMap, HashSet, VecDeque};
2use std::path::Path;
3use std::time::Instant;
4
5use ratatui::layout::Rect;
6use ratatui::text::Line;
7
8use crate::agent::{AgentEvent, QuestionResponder, TodoItem};
9use crate::config::CursorShape;
10use crate::tui::theme::Theme;
11use crate::tui::tools::{StreamSegment, ToolCallDisplay, ToolCategory, extract_tool_detail};
12use crate::tui::widgets::{
13 AgentSelector, AsidePopup, CommandPalette, FilePicker, HelpPopup, LoginPopup,
14 MessageContextMenu, ModelSelector, SessionSelector, ThinkingLevel, ThinkingSelector,
15 WelcomeScreen,
16};
17
18type ModelFetchReceiver =
19 tokio::sync::oneshot::Receiver<(Vec<(String, Vec<String>)>, String, String)>;
20
21pub struct ChatMessage {
22 pub role: String,
23 pub content: String,
24 pub tool_calls: Vec<ToolCallDisplay>,
25 pub thinking: Option<String>,
26 pub model: Option<String>,
27 pub segments: Option<Vec<StreamSegment>>,
29 pub chips: Option<Vec<InputChip>>,
31}
32
33pub struct TokenUsage {
34 pub input_tokens: u32,
35 pub output_tokens: u32,
36 pub total_cost: f64,
37}
38
39impl Default for TokenUsage {
40 fn default() -> Self {
41 Self {
42 input_tokens: 0,
43 output_tokens: 0,
44 total_cost: 0.0,
45 }
46 }
47}
48
49#[derive(Debug, Clone)]
50pub struct PasteBlock {
51 pub start: usize,
52 pub end: usize,
53 pub line_count: usize,
54}
55
56#[derive(Debug, Clone, PartialEq)]
57pub enum ChipKind {
58 File,
59 Skill,
60}
61
62#[derive(Debug, Clone)]
63pub struct InputChip {
64 pub start: usize,
65 pub end: usize,
66 pub kind: ChipKind,
67}
68
69#[derive(Debug, Clone)]
70pub struct ImageAttachment {
71 pub path: String,
72 pub media_type: String,
73 pub data: String,
74}
75
76#[derive(Debug, Clone, Copy, PartialEq)]
77pub enum StatusLevel {
78 Error,
79 Info,
80 Success,
81}
82
83pub struct StatusMessage {
84 pub text: String,
85 pub level: StatusLevel,
86 pub created: Instant,
87}
88
89impl StatusMessage {
90 pub fn error(text: impl Into<String>) -> Self {
91 Self {
92 text: text.into(),
93 level: StatusLevel::Error,
94 created: Instant::now(),
95 }
96 }
97
98 pub fn info(text: impl Into<String>) -> Self {
99 Self {
100 text: text.into(),
101 level: StatusLevel::Info,
102 created: Instant::now(),
103 }
104 }
105
106 pub fn success(text: impl Into<String>) -> Self {
107 Self {
108 text: text.into(),
109 level: StatusLevel::Success,
110 created: Instant::now(),
111 }
112 }
113
114 pub fn expired(&self) -> bool {
115 let ttl = match self.level {
116 StatusLevel::Error => std::time::Duration::from_secs(8),
117 StatusLevel::Info => std::time::Duration::from_secs(3),
118 StatusLevel::Success => std::time::Duration::from_secs(4),
119 };
120 self.created.elapsed() > ttl
121 }
122}
123
124const IMAGE_EXTENSIONS: &[&str] = &["png", "jpg", "jpeg", "gif", "webp", "bmp", "svg"];
125
126#[derive(Default)]
127pub struct TextSelection {
128 pub anchor: Option<(u16, u32)>,
129 pub end: Option<(u16, u32)>,
130 pub active: bool,
131}
132
133impl TextSelection {
134 pub fn start(&mut self, col: u16, visual_row: u32) {
135 self.anchor = Some((col, visual_row));
136 self.end = Some((col, visual_row));
137 self.active = true;
138 }
139
140 pub fn update(&mut self, col: u16, visual_row: u32) {
141 self.end = Some((col, visual_row));
142 }
143
144 pub fn clear(&mut self) {
145 self.anchor = None;
146 self.end = None;
147 self.active = false;
148 }
149
150 pub fn ordered(&self) -> Option<((u16, u32), (u16, u32))> {
151 let a = self.anchor?;
152 let e = self.end?;
153 if a.1 < e.1 || (a.1 == e.1 && a.0 <= e.0) {
154 Some((a, e))
155 } else {
156 Some((e, a))
157 }
158 }
159
160 pub fn is_empty_selection(&self) -> bool {
161 match (self.anchor, self.end) {
162 (Some(a), Some(e)) => a == e,
163 _ => true,
164 }
165 }
166}
167
168pub fn media_type_for_path(path: &str) -> Option<String> {
169 let ext = Path::new(path).extension()?.to_str()?.to_lowercase();
170 match ext.as_str() {
171 "png" => Some("image/png".into()),
172 "jpg" | "jpeg" => Some("image/jpeg".into()),
173 "gif" => Some("image/gif".into()),
174 "webp" => Some("image/webp".into()),
175 "bmp" => Some("image/bmp".into()),
176 "svg" => Some("image/svg+xml".into()),
177 _ => None,
178 }
179}
180
181pub fn is_image_path(path: &str) -> bool {
182 Path::new(path)
183 .extension()
184 .and_then(|e| e.to_str())
185 .map(|e| IMAGE_EXTENSIONS.contains(&e.to_lowercase().as_str()))
186 .unwrap_or(false)
187}
188
189pub fn normalize_paste_path(s: &str) -> Option<String> {
192 let s = s.trim().trim_matches('"').trim_matches('\'');
193 if s.is_empty() {
194 return None;
195 }
196 if let Ok(u) = url::Url::parse(s)
197 && u.scheme() == "file"
198 {
199 let path = u.path();
200 if path.is_empty() || path == "/" {
201 return None;
202 }
203 return Some(path.to_string());
204 }
205 if s.starts_with('/') || s.starts_with('~') || s.starts_with("./") {
206 return Some(s.to_string());
207 }
208 None
209}
210
211pub const PASTE_COLLAPSE_THRESHOLD: usize = 5;
212
213#[derive(Debug)]
214pub struct PendingQuestion {
215 pub question: String,
216 pub options: Vec<String>,
217 pub selected: usize,
218 pub custom_input: String,
219 pub responder: Option<QuestionResponder>,
220}
221
222pub struct SubagentState {
223 pub id: String,
224 pub description: String,
225 pub output: String,
226 pub current_tool: Option<String>,
227 pub current_tool_detail: Option<String>,
228 pub tools_completed: usize,
229 pub background: bool,
230}
231
232pub struct SubagentToolEntry {
233 pub name: String,
234 pub detail: String,
235 pub done: bool,
236 pub is_error: bool,
237}
238
239pub struct BackgroundSubagentInfo {
240 pub id: String,
241 pub description: String,
242 pub output: String,
243 pub tools_completed: usize,
244 pub done: bool,
245 pub started: Instant,
246 pub finished_at: Option<Instant>,
247 pub current_tool: Option<String>,
248 pub current_tool_detail: Option<String>,
249 pub tool_history: Vec<SubagentToolEntry>,
250 pub tokens: u32,
251 pub cost: f64,
252 pub text_lines: Vec<String>,
253}
254
255#[derive(Debug)]
256pub struct PendingPermission {
257 pub tool_name: String,
258 pub input_summary: String,
259 pub selected: usize,
260 pub responder: Option<QuestionResponder>,
261}
262
263pub struct QueuedMessage {
264 pub text: String,
265 pub images: Vec<(String, String)>,
266}
267
268#[derive(PartialEq, Clone, Copy)]
269pub enum AppMode {
270 Normal,
271 Insert,
272}
273
274#[derive(Default)]
275pub struct LayoutRects {
276 pub header: Rect,
277 pub messages: Rect,
278 pub input: Rect,
279 pub status: Rect,
280 pub model_selector: Option<Rect>,
281 pub agent_selector: Option<Rect>,
282 pub command_palette: Option<Rect>,
283 pub thinking_selector: Option<Rect>,
284 pub session_selector: Option<Rect>,
285 pub help_popup: Option<Rect>,
286 pub context_menu: Option<Rect>,
287 pub question_popup: Option<Rect>,
288 pub permission_popup: Option<Rect>,
289 pub file_picker: Option<Rect>,
290 pub login_popup: Option<Rect>,
291 pub welcome_screen: Option<Rect>,
292 pub aside_popup: Option<Rect>,
293 pub subagent_panel: Option<Rect>,
294}
295
296pub struct RenderCache {
297 pub lines: Vec<Line<'static>>,
298 pub line_to_msg: Vec<usize>,
299 pub line_to_tool: Vec<Option<(usize, usize)>>,
300 pub total_visual: u32,
301 pub width: u16,
302 pub wrap_heights: Vec<u32>,
303}
304
305pub struct MessageCache {
306 pub lines: Vec<Line<'static>>,
307 pub line_to_msg: Vec<usize>,
308 pub line_to_tool: Vec<Option<(usize, usize)>>,
309 pub message_count: usize,
310 pub width: u16,
311 pub expanded_snapshot: HashSet<(usize, usize)>,
312 pub thinking_expanded: bool,
313}
314
315pub struct SegmentCache {
316 pub lines: Vec<Line<'static>>,
317 pub line_to_tool: Vec<Option<(usize, usize)>>,
318 pub segment_count: usize,
319 pub width: u16,
320 pub prev_was_tool: bool,
321 pub tool_idx_base: usize,
322}
323
324pub struct App {
325 pub messages: Vec<ChatMessage>,
326 pub input: String,
327 pub cursor_pos: usize,
328 pub input_selection: Option<(usize, usize)>,
329 pub scroll_offset: u32,
330 pub max_scroll: u32,
331 pub is_streaming: bool,
332 pub current_response: String,
333 pub current_thinking: String,
334 pub should_quit: bool,
335 pub mode: AppMode,
336 pub usage: TokenUsage,
337 pub model_name: String,
338 pub provider_name: String,
339 pub agent_name: String,
340 pub theme: Theme,
341 pub tick_count: u64,
342 pub layout: LayoutRects,
343
344 pub pending_tool_name: Option<String>,
345 pub pending_tool_input: String,
346 pub current_tool_calls: Vec<ToolCallDisplay>,
347 pub streaming_segments: Vec<StreamSegment>,
348 pub status_message: Option<StatusMessage>,
349 pub model_selector: ModelSelector,
350 pub agent_selector: AgentSelector,
351 pub command_palette: CommandPalette,
352 pub thinking_selector: ThinkingSelector,
353 pub session_selector: SessionSelector,
354 pub help_popup: HelpPopup,
355 pub streaming_started: Option<Instant>,
356
357 pub thinking_expanded: bool,
358 pub thinking_budget: u32,
359 pub auto_opened_thinking: bool,
360 pub thinking_collapse_at: Option<Instant>,
361 pub last_escape_time: Option<Instant>,
362 pub follow_bottom: bool,
363
364 pub paste_blocks: Vec<PasteBlock>,
365 pub attachments: Vec<ImageAttachment>,
366 pub conversation_title: Option<String>,
367 pub vim_mode: bool,
368
369 pub selection: TextSelection,
370
371 pub content_width: u16,
372
373 pub context_window: u32,
374 pub last_input_tokens: u32,
375
376 pub esc_hint_until: Option<Instant>,
377 pub todos: Vec<TodoItem>,
378 pub message_line_map: Vec<usize>,
379 pub tool_line_map: Vec<Option<(usize, usize)>>,
380 pub expanded_tool_calls: HashSet<(usize, usize)>,
381 pub context_menu: MessageContextMenu,
382 pub pending_question: Option<PendingQuestion>,
383 pub pending_permission: Option<PendingPermission>,
384 pub message_queue: VecDeque<QueuedMessage>,
385 pub history: Vec<String>,
386 pub history_index: Option<usize>,
387 pub history_draft: String,
388 pub skill_entries: Vec<(String, String)>,
389 pub custom_command_names: Vec<String>,
390 pub rename_input: String,
391 pub rename_visible: bool,
392 pub favorite_models: Vec<String>,
393 pub file_picker: FilePicker,
394 pub login_popup: LoginPopup,
395 pub welcome_screen: WelcomeScreen,
396 pub aside_popup: AsidePopup,
397 pub chips: Vec<InputChip>,
398 pub active_subagent: Option<SubagentState>,
399 pub background_subagents: Vec<BackgroundSubagentInfo>,
400 pub subagent_panel_expanded: bool,
401
402 pub render_dirty: bool,
403 pub render_cache: Option<RenderCache>,
404 pub message_cache: Option<MessageCache>,
405 pub segment_cache: Option<SegmentCache>,
406 pub tool_call_complete_ticks: HashMap<(usize, usize), u64>,
407 pub input_at_top: bool,
408
409 pub cached_model_groups: Option<Vec<(String, Vec<String>)>>,
410 pub model_fetch_rx: Option<ModelFetchReceiver>,
411
412 pub cursor_shape: CursorShape,
413 pub cursor_blink: bool,
414 pub cursor_shape_normal: Option<CursorShape>,
415 pub cursor_blink_normal: Option<bool>,
416}
417impl App {
418 #[allow(clippy::too_many_arguments)]
419 pub fn new(
420 model_name: String,
421 provider_name: String,
422 agent_name: String,
423 theme_name: &str,
424 vim_mode: bool,
425 cursor_shape: CursorShape,
426 cursor_blink: bool,
427 cursor_shape_normal: Option<CursorShape>,
428 cursor_blink_normal: Option<bool>,
429 ) -> Self {
430 Self {
431 messages: Vec::new(),
432 input: String::new(),
433 cursor_pos: 0,
434 input_selection: None,
435 scroll_offset: 0,
436 max_scroll: 0,
437 is_streaming: false,
438 current_response: String::new(),
439 current_thinking: String::new(),
440 should_quit: false,
441 mode: AppMode::Insert,
442 usage: TokenUsage::default(),
443 model_name,
444 provider_name,
445 agent_name,
446 theme: Theme::from_config(theme_name),
447 tick_count: 0,
448 layout: LayoutRects::default(),
449 pending_tool_name: None,
450 pending_tool_input: String::new(),
451 current_tool_calls: Vec::new(),
452 streaming_segments: Vec::new(),
453 status_message: None,
454 model_selector: ModelSelector::new(),
455 agent_selector: AgentSelector::new(),
456 command_palette: CommandPalette::new(),
457 thinking_selector: ThinkingSelector::new(),
458 session_selector: SessionSelector::new(),
459 help_popup: HelpPopup::new(),
460 streaming_started: None,
461 thinking_expanded: false,
462 thinking_budget: 0,
463 auto_opened_thinking: false,
464 thinking_collapse_at: None,
465 last_escape_time: None,
466 follow_bottom: true,
467 paste_blocks: Vec::new(),
468 attachments: Vec::new(),
469 conversation_title: None,
470 vim_mode,
471 selection: TextSelection::default(),
472
473 content_width: 0,
474 context_window: 0,
475 last_input_tokens: 0,
476 esc_hint_until: None,
477 todos: Vec::new(),
478 message_line_map: Vec::new(),
479 tool_line_map: Vec::new(),
480 expanded_tool_calls: HashSet::new(),
481 context_menu: MessageContextMenu::new(),
482 pending_question: None,
483 pending_permission: None,
484 message_queue: VecDeque::new(),
485 history: Vec::new(),
486 history_index: None,
487 history_draft: String::new(),
488 skill_entries: Vec::new(),
489 custom_command_names: Vec::new(),
490 rename_input: String::new(),
491 rename_visible: false,
492 favorite_models: Vec::new(),
493 file_picker: FilePicker::new(),
494 login_popup: LoginPopup::new(),
495 welcome_screen: WelcomeScreen::new(),
496 aside_popup: AsidePopup::new(),
497 chips: Vec::new(),
498 active_subagent: None,
499 background_subagents: Vec::new(),
500 subagent_panel_expanded: false,
501 render_dirty: true,
502 render_cache: None,
503 message_cache: None,
504 segment_cache: None,
505 tool_call_complete_ticks: HashMap::new(),
506 input_at_top: false,
507 cached_model_groups: None,
508 model_fetch_rx: None,
509 cursor_shape,
510 cursor_blink,
511 cursor_shape_normal,
512 cursor_blink_normal,
513 }
514 }
515
516 pub fn mark_dirty(&mut self) {
517 self.render_dirty = true;
518 }
519
520 pub fn streaming_elapsed_secs(&self) -> Option<f64> {
521 self.streaming_started
522 .map(|start| start.elapsed().as_secs_f64())
523 }
524
525 pub fn thinking_level(&self) -> ThinkingLevel {
526 ThinkingLevel::from_budget(self.thinking_budget)
527 }
528
529 pub fn handle_agent_event(&mut self, event: AgentEvent) {
530 match event {
531 AgentEvent::TextDelta(text) => {
532 self.current_response.push_str(&text);
533 self.mark_dirty();
534 }
535 AgentEvent::ThinkingDelta(text) => {
536 let was_empty = self.current_thinking.is_empty();
537 self.current_thinking.push_str(&text);
538 if was_empty {
539 self.thinking_expanded = true;
540 self.auto_opened_thinking = true;
541 }
542 self.mark_dirty();
543 }
544 AgentEvent::TextComplete(text) => {
545 if !text.is_empty()
546 || !self.current_response.is_empty()
547 || !self.streaming_segments.is_empty()
548 {
549 if !self.current_response.is_empty() {
550 self.streaming_segments
551 .push(StreamSegment::Text(std::mem::take(
552 &mut self.current_response,
553 )));
554 }
555 let content: String = self
556 .streaming_segments
557 .iter()
558 .filter_map(|s| {
559 if let StreamSegment::Text(t) = s {
560 Some(t.as_str())
561 } else {
562 None
563 }
564 })
565 .collect();
566 let content = if content.is_empty() {
567 text.clone()
568 } else {
569 content
570 };
571 let thinking = if self.current_thinking.is_empty() {
572 None
573 } else {
574 Some(self.current_thinking.clone())
575 };
576 self.messages.push(ChatMessage {
577 role: "assistant".to_string(),
578 content,
579 tool_calls: std::mem::take(&mut self.current_tool_calls),
580 thinking,
581 model: Some(self.model_name.clone()),
582 segments: Some(std::mem::take(&mut self.streaming_segments)),
583 chips: None,
584 });
585 self.mark_dirty();
586 }
587 self.current_response.clear();
588 self.current_thinking.clear();
589 self.streaming_segments.clear();
590 self.segment_cache = None;
591 self.is_streaming = false;
592 self.streaming_started = None;
593 if self.auto_opened_thinking {
594 self.thinking_collapse_at =
595 Some(Instant::now() + std::time::Duration::from_secs(4));
596 }
597 self.scroll_to_bottom();
598 }
599 AgentEvent::ToolCallStart { name, .. } => {
600 self.pending_tool_name = Some(name);
601 self.pending_tool_input.clear();
602 self.mark_dirty();
603 }
604 AgentEvent::ToolCallInputDelta(delta) => {
605 self.pending_tool_input.push_str(&delta);
606 self.mark_dirty();
607 }
608 AgentEvent::ToolCallExecuting { name, input, .. } => {
609 self.pending_tool_name = Some(name.clone());
610 self.pending_tool_input = input;
611 self.mark_dirty();
612 }
613 AgentEvent::ToolCallResult {
614 name,
615 output,
616 is_error,
617 ..
618 } => {
619 if !self.current_response.is_empty() {
620 self.streaming_segments
621 .push(StreamSegment::Text(std::mem::take(
622 &mut self.current_response,
623 )));
624 }
625 let input = std::mem::take(&mut self.pending_tool_input);
626 let category = ToolCategory::from_name(&name);
627 let detail = extract_tool_detail(&name, &input);
628 let display = ToolCallDisplay {
629 name: name.clone(),
630 input,
631 output: Some(output),
632 is_error,
633 category,
634 detail,
635 };
636 let should_auto_expand = matches!(
637 display.category,
638 ToolCategory::MultiEdit | ToolCategory::Patch | ToolCategory::FileWrite
639 );
640 let tool_idx = self
641 .streaming_segments
642 .iter()
643 .filter(|s| matches!(s, StreamSegment::ToolCall(_)))
644 .count();
645 self.current_tool_calls.push(display.clone());
646 self.streaming_segments
647 .push(StreamSegment::ToolCall(display));
648 if should_auto_expand {
649 let stream_msg_idx = self.messages.len();
650 self.expanded_tool_calls.insert((stream_msg_idx, tool_idx));
651 }
652 self.pending_tool_name = None;
653 self.mark_dirty();
654 }
655 AgentEvent::Done { usage } => {
656 self.is_streaming = false;
657 self.streaming_started = None;
658 self.last_input_tokens = usage.input_tokens;
659 self.usage.input_tokens += usage.input_tokens;
660 self.usage.output_tokens += usage.output_tokens;
661 if self.auto_opened_thinking && self.thinking_collapse_at.is_none() {
662 self.thinking_expanded = false;
663 self.auto_opened_thinking = false;
664 }
665 self.scroll_to_bottom();
666 }
667 AgentEvent::Error(msg) => {
668 self.is_streaming = false;
669 self.streaming_started = None;
670 self.status_message = Some(StatusMessage::error(msg));
671 }
672 AgentEvent::Compacting => {
673 self.messages.push(ChatMessage {
674 role: "compact".to_string(),
675 content: "\u{26a1} context compacted".to_string(),
676 tool_calls: Vec::new(),
677 thinking: None,
678 model: None,
679 segments: None,
680 chips: None,
681 });
682 }
683 AgentEvent::TitleGenerated(title) => {
684 self.conversation_title = Some(title);
685 }
686 AgentEvent::Compacted { messages_removed } => {
687 if let Some(last) = self.messages.last_mut()
688 && last.role == "compact"
689 {
690 last.content = format!(
691 "\u{26a1} compacted \u{2014} {} messages summarized",
692 messages_removed
693 );
694 self.message_cache = None;
695 }
696 }
697 AgentEvent::TodoUpdate(items) => {
698 self.todos = items;
699 }
700 AgentEvent::Question {
701 question,
702 options,
703 responder,
704 ..
705 } => {
706 self.pending_question = Some(PendingQuestion {
707 question,
708 options,
709 selected: 0,
710 custom_input: String::new(),
711 responder: Some(responder),
712 });
713 }
714 AgentEvent::PermissionRequest {
715 tool_name,
716 input_summary,
717 responder,
718 } => {
719 self.pending_permission = Some(PendingPermission {
720 tool_name,
721 input_summary,
722 selected: 0,
723 responder: Some(responder),
724 });
725 }
726 AgentEvent::SubagentStart {
727 id,
728 description,
729 background,
730 } => {
731 if background {
732 self.background_subagents.push(BackgroundSubagentInfo {
733 id,
734 description,
735 output: String::new(),
736 tools_completed: 0,
737 done: false,
738 started: Instant::now(),
739 finished_at: None,
740 current_tool: None,
741 current_tool_detail: None,
742 tool_history: Vec::new(),
743 tokens: 0,
744 cost: 0.0,
745 text_lines: Vec::new(),
746 });
747 } else {
748 self.active_subagent = Some(SubagentState {
749 id,
750 description,
751 output: String::new(),
752 current_tool: None,
753 current_tool_detail: None,
754 tools_completed: 0,
755 background: false,
756 });
757 }
758 }
759 AgentEvent::SubagentDelta { id, text } => {
760 if let Some(ref mut state) = self.active_subagent
761 && state.id == id
762 {
763 state.output.push_str(&text);
764 } else if let Some(bg) = self.background_subagents.iter_mut().find(|b| b.id == id) {
765 bg.output.push_str(&text);
766 let lines: Vec<String> = bg.output.lines().map(|l| l.to_string()).collect();
767 let start = lines.len().saturating_sub(20);
768 bg.text_lines = lines[start..].to_vec();
769 }
770 }
771 AgentEvent::SubagentToolStart {
772 id,
773 tool_name,
774 detail,
775 } => {
776 if let Some(ref mut state) = self.active_subagent
777 && state.id == id
778 {
779 state.current_tool = Some(tool_name);
780 state.current_tool_detail = Some(detail);
781 } else if let Some(bg) = self.background_subagents.iter_mut().find(|b| b.id == id) {
782 bg.current_tool = Some(tool_name.clone());
783 bg.current_tool_detail = Some(detail.clone());
784 bg.tool_history.push(SubagentToolEntry {
785 name: tool_name,
786 detail,
787 done: false,
788 is_error: false,
789 });
790 }
791 }
792 AgentEvent::SubagentToolComplete { id, .. } => {
793 if let Some(ref mut state) = self.active_subagent
794 && state.id == id
795 {
796 state.current_tool = None;
797 state.current_tool_detail = None;
798 state.tools_completed += 1;
799 } else if let Some(bg) = self.background_subagents.iter_mut().find(|b| b.id == id) {
800 bg.current_tool = None;
801 bg.current_tool_detail = None;
802 bg.tools_completed += 1;
803 if let Some(entry) = bg.tool_history.iter_mut().rev().find(|e| !e.done) {
804 entry.done = true;
805 }
806 }
807 }
808 AgentEvent::SubagentComplete { id, .. } => {
809 if self.active_subagent.as_ref().is_some_and(|s| s.id == id) {
810 self.active_subagent = None;
811 }
812 }
813 AgentEvent::SubagentBackgroundDone {
814 id, description, ..
815 } => {
816 if let Some(bg) = self.background_subagents.iter_mut().find(|b| b.id == id) {
817 bg.done = true;
818 bg.finished_at = Some(Instant::now());
819 }
820 self.status_message = Some(StatusMessage::success(format!(
821 "Background subagent done: {}",
822 description
823 )));
824 }
825 AgentEvent::MemoryExtracted {
826 added,
827 updated,
828 deleted,
829 } => {
830 let parts: Vec<String> = [
831 (added > 0).then(|| format!("+{added}")),
832 (updated > 0).then(|| format!("~{updated}")),
833 (deleted > 0).then(|| format!("-{deleted}")),
834 ]
835 .into_iter()
836 .flatten()
837 .collect();
838 if !parts.is_empty() {
839 self.status_message = Some(StatusMessage::success(format!(
840 "memory {}",
841 parts.join(" ")
842 )));
843 }
844 }
845 AgentEvent::AsideDelta(text) => {
846 self.aside_popup.response.push_str(&text);
847 }
848 AgentEvent::AsideDone => {
849 self.aside_popup.done = true;
850 }
851 AgentEvent::AsideError(msg) => {
852 self.aside_popup.response = format!("Error: {msg}");
853 self.aside_popup.done = true;
854 }
855 }
856 self.mark_dirty();
857 }
858
859 pub fn take_input(&mut self) -> Option<String> {
860 let trimmed = self.input.trim().to_string();
861 if trimmed.is_empty() && self.attachments.is_empty() {
862 return None;
863 }
864 let display = if self.attachments.is_empty() {
865 trimmed.clone()
866 } else {
867 let att_names: Vec<String> = self
868 .attachments
869 .iter()
870 .map(|a| {
871 Path::new(&a.path)
872 .file_name()
873 .map(|f| f.to_string_lossy().to_string())
874 .unwrap_or_else(|| a.path.clone())
875 })
876 .collect();
877 if trimmed.is_empty() {
878 format!("[{}]", att_names.join(", "))
879 } else {
880 format!("{} [{}]", trimmed, att_names.join(", "))
881 }
882 };
883 let chips = std::mem::take(&mut self.chips);
884 self.messages.push(ChatMessage {
885 role: "user".to_string(),
886 content: display,
887 tool_calls: Vec::new(),
888 thinking: None,
889 model: None,
890 segments: None,
891 chips: if chips.is_empty() { None } else { Some(chips) },
892 });
893 self.input.clear();
894 self.cursor_pos = 0;
895 self.paste_blocks.clear();
896 self.history.push(trimmed.clone());
897 self.history_index = None;
898 self.history_draft.clear();
899 self.is_streaming = true;
900 self.streaming_started = Some(Instant::now());
901 self.current_response.clear();
902 self.current_thinking.clear();
903 self.current_tool_calls.clear();
904 self.streaming_segments.clear();
905 self.status_message = None;
906 self.scroll_to_bottom();
907 self.mark_dirty();
908 Some(trimmed)
909 }
910
911 pub fn take_attachments(&mut self) -> Vec<ImageAttachment> {
912 std::mem::take(&mut self.attachments)
913 }
914
915 pub fn queue_input(&mut self) -> bool {
916 let trimmed = self.input.trim().to_string();
917 if trimmed.is_empty() && self.attachments.is_empty() {
918 return false;
919 }
920 let display = if self.attachments.is_empty() {
921 trimmed.clone()
922 } else {
923 let names: Vec<String> = self
924 .attachments
925 .iter()
926 .map(|a| {
927 Path::new(&a.path)
928 .file_name()
929 .map(|f| f.to_string_lossy().to_string())
930 .unwrap_or_else(|| a.path.clone())
931 })
932 .collect();
933 if trimmed.is_empty() {
934 format!("[{}]", names.join(", "))
935 } else {
936 format!("{} [{}]", trimmed, names.join(", "))
937 }
938 };
939 let chips = std::mem::take(&mut self.chips);
940 self.messages.push(ChatMessage {
941 role: "user".to_string(),
942 content: display,
943 tool_calls: Vec::new(),
944 thinking: None,
945 model: None,
946 segments: None,
947 chips: if chips.is_empty() { None } else { Some(chips) },
948 });
949 let images: Vec<(String, String)> = self
950 .attachments
951 .drain(..)
952 .map(|a| (a.media_type, a.data))
953 .collect();
954 self.history.push(trimmed.clone());
955 self.history_index = None;
956 self.history_draft.clear();
957 self.message_queue.push_back(QueuedMessage {
958 text: trimmed,
959 images,
960 });
961 self.input.clear();
962 self.cursor_pos = 0;
963 self.paste_blocks.clear();
964 self.scroll_to_bottom();
965 self.mark_dirty();
966 true
967 }
968
969 pub fn input_height(&self, width: u16) -> u16 {
970 if self.is_streaming && self.input.is_empty() && self.attachments.is_empty() {
971 return 1;
972 }
973 let w = width as usize;
974 if w < 4 {
975 return 1;
976 }
977 let has_input = !self.input.is_empty() || !self.attachments.is_empty();
978 if !has_input {
979 return 1;
980 }
981 let mut visual = 0usize;
982 if !self.attachments.is_empty() {
983 visual += 1;
984 }
985 let display = self.display_input();
986 if display.is_empty() {
987 if self.attachments.is_empty() {
988 visual += 1;
989 }
990 } else {
991 for line in display.split('\n') {
992 let total = 2 + line.chars().count();
993 visual += if total == 0 {
994 1
995 } else {
996 total.div_ceil(w).max(1)
997 };
998 }
999 }
1000 (visual as u16).clamp(1, 12)
1001 }
1002
1003 pub fn handle_paste(&mut self, text: String) {
1004 self.delete_input_selection();
1005 let line_count = text.lines().count();
1006 let start = self.cursor_pos.min(self.input.len());
1007 let len = text.len();
1008 if !self.input.is_char_boundary(start) {
1009 self.cursor_pos = self.input.len();
1010 return;
1011 }
1012 self.input.insert_str(start, &text);
1013 self.adjust_chips(start, 0, len);
1014 self.cursor_pos = start + len;
1015 if line_count >= PASTE_COLLAPSE_THRESHOLD {
1016 self.paste_blocks.push(PasteBlock {
1017 start,
1018 end: start + len,
1019 line_count,
1020 });
1021 }
1022 }
1023
1024 pub fn paste_block_at_cursor(&self) -> Option<usize> {
1025 self.paste_blocks
1026 .iter()
1027 .position(|pb| self.cursor_pos > pb.start && self.cursor_pos <= pb.end)
1028 }
1029
1030 pub fn delete_paste_block(&mut self, idx: usize) {
1031 let pb = self.paste_blocks.remove(idx);
1032 let start = pb.start.min(self.input.len());
1033 let end = pb.end.min(self.input.len()).max(start);
1034 let len = end - start;
1035 self.input.replace_range(start..end, "");
1036 self.cursor_pos = start;
1037 for remaining in &mut self.paste_blocks {
1038 if remaining.start >= pb.end {
1039 remaining.start -= len;
1040 remaining.end -= len;
1041 }
1042 }
1043 }
1044
1045 pub fn chip_at_cursor(&self) -> Option<usize> {
1046 self.chips
1047 .iter()
1048 .position(|c| self.cursor_pos > c.start && self.cursor_pos <= c.end)
1049 }
1050
1051 pub fn delete_chip(&mut self, idx: usize) {
1052 let chip = self.chips.remove(idx);
1053 let start = chip.start.min(self.input.len());
1054 let end = chip.end.min(self.input.len()).max(start);
1055 let len = end - start;
1056 self.input.replace_range(start..end, "");
1057 self.cursor_pos = start;
1058 self.adjust_chips(start, len, 0);
1059 }
1060
1061 pub fn adjust_chips(&mut self, edit_start: usize, old_len: usize, new_len: usize) {
1062 let edit_end = edit_start + old_len;
1063 let delta = new_len as isize - old_len as isize;
1064 self.chips.retain_mut(|c| {
1065 if c.start >= edit_end {
1066 c.start = (c.start as isize + delta) as usize;
1067 c.end = (c.end as isize + delta) as usize;
1068 true
1069 } else {
1070 c.end <= edit_start
1071 }
1072 });
1073 }
1074
1075 pub fn add_image_attachment(&mut self, path: &str) -> Result<(), String> {
1076 let resolved = if path.starts_with('~') {
1077 if let Ok(home) = std::env::var("HOME") {
1078 path.replacen('~', &home, 1)
1079 } else {
1080 path.to_string()
1081 }
1082 } else {
1083 path.to_string()
1084 };
1085
1086 let fs_path = Path::new(&resolved);
1087 if !fs_path.exists() {
1088 return Err(format!("file not found: {}", path));
1089 }
1090
1091 let media_type = media_type_for_path(&resolved)
1092 .ok_or_else(|| format!("unsupported image format: {}", path))?;
1093
1094 let data = std::fs::read(fs_path).map_err(|e| format!("failed to read {}: {}", path, e))?;
1095 let encoded = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &data);
1096
1097 if self.attachments.iter().any(|a| a.path == resolved) {
1098 return Ok(());
1099 }
1100
1101 self.attachments.push(ImageAttachment {
1102 path: resolved,
1103 media_type,
1104 data: encoded,
1105 });
1106 Ok(())
1107 }
1108
1109 pub fn insert_file_reference(&mut self, path: &str) {
1110 let text = format!("@{} ", path);
1111 let start = self.cursor_pos;
1112 let len = text.len();
1113 self.input.insert_str(start, &text);
1114 self.adjust_chips(start, 0, len);
1115 let chip_end = start + 1 + path.len();
1116 self.chips.push(InputChip {
1117 start,
1118 end: chip_end,
1119 kind: ChipKind::File,
1120 });
1121 self.cursor_pos = start + len;
1122 }
1123
1124 pub fn display_cursor_pos(&self) -> usize {
1125 if self.paste_blocks.is_empty() {
1126 return self.cursor_pos;
1127 }
1128 let mut sorted: Vec<&PasteBlock> = self.paste_blocks.iter().collect();
1129 sorted.sort_by_key(|pb| pb.start);
1130 let cursor = self.cursor_pos;
1131 let mut raw_pos = 0usize;
1132 let mut display_pos = 0usize;
1133 for pb in &sorted {
1134 if cursor <= pb.start {
1135 display_pos += cursor - raw_pos;
1136 return display_pos;
1137 }
1138 display_pos += pb.start - raw_pos;
1139 let marker_len = format!("[pasted {} lines]", pb.line_count).len();
1140 if cursor <= pb.end {
1141 display_pos += marker_len;
1142 return display_pos;
1143 }
1144 display_pos += marker_len;
1145 raw_pos = pb.end;
1146 }
1147 display_pos += cursor - raw_pos;
1148 display_pos
1149 }
1150
1151 pub fn display_input(&self) -> String {
1152 if self.paste_blocks.is_empty() {
1153 return self.input.clone();
1154 }
1155 let mut result = String::new();
1156 let mut pos = 0;
1157 let mut sorted_blocks: Vec<&PasteBlock> = self.paste_blocks.iter().collect();
1158 sorted_blocks.sort_by_key(|pb| pb.start);
1159 for pb in sorted_blocks {
1160 if pb.start > pos {
1161 result.push_str(&self.input[pos..pb.start]);
1162 }
1163 result.push_str(&format!("[pasted {} lines]", pb.line_count));
1164 pos = pb.end;
1165 }
1166 if pos < self.input.len() {
1167 result.push_str(&self.input[pos..]);
1168 }
1169 result
1170 }
1171
1172 pub fn scroll_up(&mut self, n: u32) {
1173 self.follow_bottom = false;
1174 self.scroll_offset = self.scroll_offset.saturating_sub(n);
1175 }
1176
1177 pub fn scroll_down(&mut self, n: u32) {
1178 let target = self.scroll_offset.saturating_add(n);
1179 if target >= self.max_scroll {
1180 self.follow_bottom = true;
1181 self.scroll_offset = self.max_scroll;
1182 } else {
1183 self.scroll_offset = target;
1184 }
1185 }
1186
1187 pub fn scroll_to_top(&mut self) {
1188 self.follow_bottom = false;
1189 self.scroll_offset = 0;
1190 }
1191
1192 pub fn scroll_to_bottom(&mut self) {
1193 self.follow_bottom = true;
1194 self.scroll_offset = self.max_scroll;
1195 }
1196
1197 pub fn clear_conversation(&mut self) {
1198 self.messages.clear();
1199 self.current_response.clear();
1200 self.current_thinking.clear();
1201 self.current_tool_calls.clear();
1202 self.streaming_segments.clear();
1203 self.scroll_offset = 0;
1204 self.max_scroll = 0;
1205 self.follow_bottom = true;
1206 self.usage = TokenUsage::default();
1207 self.last_input_tokens = 0;
1208 self.status_message = None;
1209 self.paste_blocks.clear();
1210 self.chips.clear();
1211 self.attachments.clear();
1212 self.conversation_title = None;
1213 self.selection.clear();
1214
1215 self.todos.clear();
1216 self.message_line_map.clear();
1217 self.tool_line_map.clear();
1218 self.expanded_tool_calls.clear();
1219 self.esc_hint_until = None;
1220 self.context_menu.close();
1221 self.pending_question = None;
1222 self.pending_permission = None;
1223 self.active_subagent = None;
1224 self.background_subagents.clear();
1225 self.message_queue.clear();
1226 self.render_cache = None;
1227 self.message_cache = None;
1228 self.segment_cache = None;
1229 self.tool_call_complete_ticks.clear();
1230 self.auto_opened_thinking = false;
1231 self.thinking_collapse_at = None;
1232 self.mark_dirty();
1233 }
1234
1235 pub fn insert_char(&mut self, c: char) {
1236 let pos = self.cursor_pos.min(self.input.len());
1237 if !self.input.is_char_boundary(pos) {
1238 self.cursor_pos = self.input.len();
1239 return;
1240 }
1241 self.input.insert(pos, c);
1242 let len = c.len_utf8();
1243 self.adjust_chips(pos, 0, len);
1244 self.cursor_pos = pos + len;
1245 }
1246
1247 pub fn delete_char_before(&mut self) {
1248 self.cursor_pos = self.cursor_pos.min(self.input.len());
1249 if self.cursor_pos > 0 {
1250 if !self.input.is_char_boundary(self.cursor_pos) {
1251 self.cursor_pos = self.input.len();
1252 return;
1253 }
1254 let prev = self.input[..self.cursor_pos]
1255 .chars()
1256 .last()
1257 .map(|c| c.len_utf8())
1258 .unwrap_or(0);
1259 self.cursor_pos -= prev;
1260 self.input.remove(self.cursor_pos);
1261 self.adjust_chips(self.cursor_pos, prev, 0);
1262 }
1263 }
1264
1265 pub fn clear_input_selection(&mut self) {
1266 self.input_selection = None;
1267 }
1268
1269 pub fn input_selection_range(&self) -> Option<(usize, usize)> {
1270 let (anchor, _) = self.input_selection?;
1271 let start = anchor.min(self.cursor_pos);
1272 let end = anchor.max(self.cursor_pos);
1273 if start == end {
1274 return None;
1275 }
1276 Some((start, end))
1277 }
1278
1279 pub fn copy_input_selection(&self) -> Option<String> {
1280 let (start, end) = self.input_selection_range()?;
1281 Some(self.input[start..end].to_string())
1282 }
1283
1284 pub fn delete_input_selection(&mut self) -> bool {
1285 let Some((start, end)) = self.input_selection_range() else {
1286 return false;
1287 };
1288 let old_len = end - start;
1289 self.input.replace_range(start..end, "");
1290 self.adjust_chips(start, old_len, 0);
1291 self.cursor_pos = start;
1292 self.input_selection = None;
1293 true
1294 }
1295
1296 pub fn select_left(&mut self) {
1297 if self.input_selection.is_none() {
1298 self.input_selection = Some((self.cursor_pos, self.cursor_pos));
1299 }
1300 if self.cursor_pos > 0 {
1301 let prev = self.input[..self.cursor_pos]
1302 .chars()
1303 .last()
1304 .map(|c| c.len_utf8())
1305 .unwrap_or(0);
1306 self.cursor_pos -= prev;
1307 }
1308 }
1309
1310 pub fn select_right(&mut self) {
1311 if self.input_selection.is_none() {
1312 self.input_selection = Some((self.cursor_pos, self.cursor_pos));
1313 }
1314 if self.cursor_pos < self.input.len() {
1315 let next = self.input[self.cursor_pos..]
1316 .chars()
1317 .next()
1318 .map(|c| c.len_utf8())
1319 .unwrap_or(0);
1320 self.cursor_pos += next;
1321 }
1322 }
1323
1324 pub fn select_home(&mut self) {
1325 if self.input_selection.is_none() {
1326 self.input_selection = Some((self.cursor_pos, self.cursor_pos));
1327 }
1328 self.cursor_pos = 0;
1329 }
1330
1331 pub fn select_end(&mut self) {
1332 if self.input_selection.is_none() {
1333 self.input_selection = Some((self.cursor_pos, self.cursor_pos));
1334 }
1335 self.cursor_pos = self.input.len();
1336 }
1337
1338 pub fn select_all_input(&mut self) {
1339 self.input_selection = Some((0, 0));
1340 self.cursor_pos = self.input.len();
1341 }
1342
1343 pub fn move_cursor_left(&mut self) {
1344 if self.cursor_pos > 0 {
1345 let prev = self.input[..self.cursor_pos]
1346 .chars()
1347 .last()
1348 .map(|c| c.len_utf8())
1349 .unwrap_or(0);
1350 self.cursor_pos -= prev;
1351 }
1352 }
1353
1354 pub fn move_cursor_right(&mut self) {
1355 if self.cursor_pos < self.input.len() {
1356 let next = self.input[self.cursor_pos..]
1357 .chars()
1358 .next()
1359 .map(|c| c.len_utf8())
1360 .unwrap_or(0);
1361 self.cursor_pos += next;
1362 }
1363 }
1364
1365 pub fn move_cursor_home(&mut self) {
1366 self.cursor_pos = 0;
1367 }
1368
1369 pub fn move_cursor_end(&mut self) {
1370 self.cursor_pos = self.input.len();
1371 }
1372
1373 pub fn delete_word_before(&mut self) {
1374 if self.cursor_pos == 0 {
1375 return;
1376 }
1377 let before = &self.input[..self.cursor_pos];
1378 let trimmed = before.trim_end();
1379 let new_end = if trimmed.is_empty() {
1380 0
1381 } else if let Some(pos) = trimmed.rfind(|c: char| c.is_whitespace()) {
1382 pos + trimmed[pos..]
1383 .chars()
1384 .next()
1385 .map(|c| c.len_utf8())
1386 .unwrap_or(1)
1387 } else {
1388 0
1389 };
1390 let old_len = self.cursor_pos - new_end;
1391 self.input.replace_range(new_end..self.cursor_pos, "");
1392 self.adjust_chips(new_end, old_len, 0);
1393 self.cursor_pos = new_end;
1394 }
1395
1396 pub fn delete_to_end(&mut self) {
1397 let old_len = self.input.len() - self.cursor_pos;
1398 self.input.truncate(self.cursor_pos);
1399 self.adjust_chips(self.cursor_pos, old_len, 0);
1400 }
1401
1402 pub fn delete_to_start(&mut self) {
1403 let old_len = self.cursor_pos;
1404 self.input.replace_range(..self.cursor_pos, "");
1405 self.adjust_chips(0, old_len, 0);
1406 self.cursor_pos = 0;
1407 }
1408
1409 pub fn extract_selected_text(&self) -> Option<String> {
1410 let ((sc, sr), (ec, er)) = self.selection.ordered()?;
1411 let cache = self.render_cache.as_ref()?;
1412 if cache.lines.is_empty() || self.content_width == 0 {
1413 return None;
1414 }
1415 let mut text = String::new();
1416 for row in sr..=er {
1417 if row as usize >= cache.lines.len() {
1418 break;
1419 }
1420 let line_text: String = cache.lines[row as usize]
1421 .spans
1422 .iter()
1423 .map(|s| s.content.as_ref())
1424 .collect();
1425 let chars: Vec<char> = line_text.chars().collect();
1426 let start_col = if row == sr {
1427 (sc as usize).min(chars.len())
1428 } else {
1429 0
1430 };
1431 let end_col = if row == er {
1432 (ec as usize).min(chars.len())
1433 } else {
1434 chars.len()
1435 };
1436 if start_col <= end_col {
1437 let s = start_col.min(chars.len());
1438 let e = end_col.min(chars.len());
1439 text.extend(&chars[s..e]);
1440 }
1441 if row < er {
1442 text.push('\n');
1443 }
1444 }
1445 Some(text)
1446 }
1447
1448 pub fn move_cursor_up(&mut self) -> bool {
1449 let before = &self.input[..self.cursor_pos];
1450 let line_start = before.rfind('\n').map(|p| p + 1).unwrap_or(0);
1451 if line_start == 0 {
1452 return false;
1453 }
1454 let col = before[line_start..].chars().count();
1455 let prev_end = line_start - 1;
1456 let prev_start = self.input[..prev_end]
1457 .rfind('\n')
1458 .map(|p| p + 1)
1459 .unwrap_or(0);
1460 let prev_line = &self.input[prev_start..prev_end];
1461 let target_col = col.min(prev_line.chars().count());
1462 let offset: usize = prev_line
1463 .chars()
1464 .take(target_col)
1465 .map(|c| c.len_utf8())
1466 .sum();
1467 self.cursor_pos = prev_start + offset;
1468 true
1469 }
1470
1471 pub fn move_cursor_down(&mut self) -> bool {
1472 let after = &self.input[self.cursor_pos..];
1473 let next_nl = after.find('\n');
1474 let Some(nl_offset) = next_nl else {
1475 return false;
1476 };
1477 let before = &self.input[..self.cursor_pos];
1478 let line_start = before.rfind('\n').map(|p| p + 1).unwrap_or(0);
1479 let col = before[line_start..].chars().count();
1480 let next_start = self.cursor_pos + nl_offset + 1;
1481 let next_end = self.input[next_start..]
1482 .find('\n')
1483 .map(|p| next_start + p)
1484 .unwrap_or(self.input.len());
1485 let next_line = &self.input[next_start..next_end];
1486 let target_col = col.min(next_line.chars().count());
1487 let offset: usize = next_line
1488 .chars()
1489 .take(target_col)
1490 .map(|c| c.len_utf8())
1491 .sum();
1492 self.cursor_pos = next_start + offset;
1493 true
1494 }
1495
1496 pub fn history_prev(&mut self) {
1497 if self.history.is_empty() {
1498 return;
1499 }
1500 match self.history_index {
1501 None => {
1502 self.history_draft = self.input.clone();
1503 self.history_index = Some(self.history.len() - 1);
1504 }
1505 Some(0) => return,
1506 Some(i) => {
1507 self.history_index = Some(i - 1);
1508 }
1509 }
1510 self.input = self.history[self.history_index.unwrap()].clone();
1511 self.cursor_pos = self.input.len();
1512 self.paste_blocks.clear();
1513 self.chips.clear();
1514 }
1515
1516 pub fn history_next(&mut self) {
1517 let Some(idx) = self.history_index else {
1518 return;
1519 };
1520 if idx + 1 >= self.history.len() {
1521 self.history_index = None;
1522 self.input = std::mem::take(&mut self.history_draft);
1523 } else {
1524 self.history_index = Some(idx + 1);
1525 self.input = self.history[idx + 1].clone();
1526 }
1527 self.cursor_pos = self.input.len();
1528 self.paste_blocks.clear();
1529 self.chips.clear();
1530 }
1531}
1532
1533pub fn copy_to_clipboard(text: &str) {
1534 let encoded =
1535 base64::Engine::encode(&base64::engine::general_purpose::STANDARD, text.as_bytes());
1536 let osc = format!("\x1b]52;c;{}\x07", encoded);
1537 let _ = std::io::Write::write_all(&mut std::io::stderr(), osc.as_bytes());
1538
1539 #[cfg(target_os = "macos")]
1540 {
1541 use std::process::{Command, Stdio};
1542 if let Ok(mut child) = Command::new("pbcopy").stdin(Stdio::piped()).spawn() {
1543 if let Some(ref mut stdin) = child.stdin {
1544 let _ = std::io::Write::write_all(stdin, text.as_bytes());
1545 }
1546 let _ = child.wait();
1547 }
1548 }
1549
1550 #[cfg(target_os = "linux")]
1551 {
1552 use std::process::{Command, Stdio};
1553 let result = Command::new("xclip")
1554 .args(["-selection", "clipboard"])
1555 .stdin(Stdio::piped())
1556 .spawn();
1557 if let Ok(mut child) = result {
1558 if let Some(ref mut stdin) = child.stdin {
1559 let _ = std::io::Write::write_all(stdin, text.as_bytes());
1560 }
1561 let _ = child.wait();
1562 }
1563 }
1564}