matrixcode-tui 0.4.27

MatrixCode TUI - Terminal UI library for AI Code Agent
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
//! Input area and queue rendering.

use ratatui::{
    layout::Rect,
    style::{Color, Modifier, Style},
    text::{Line, Span},
    widgets::Paragraph,
};

use crate::BORDER_PADDING;
use crate::app::TuiApp;
use crate::types::Activity;
use crate::utils::{truncate, truncate_visual, truncate_visual_end};

impl TuiApp {
    pub(crate) fn draw_queue(&self, f: &mut ratatui::Frame, area: Rect) {
        let mut spans: Vec<Span> = vec![
            Span::styled("", Style::default().fg(Color::Magenta)),
            Span::styled(
                format!("Queue ({}): ", self.pending_messages.len()),
                Style::default()
                    .fg(Color::Magenta)
                    .add_modifier(Modifier::BOLD),
            ),
        ];

        for (i, msg) in self.pending_messages.iter().enumerate() {
            if i > 0 {
                spans.push(Span::styled("", Style::default().fg(Color::DarkGray)));
            }
            let preview = msg.lines().next().unwrap_or("");
            let truncated = truncate(preview, 30);
            spans.push(Span::styled(
                format!("\"{}\"", truncated),
                Style::default().fg(Color::Yellow),
            ));
        }

        f.render_widget(Paragraph::new(Line::from(spans)), area);
    }

    pub(crate) fn draw_input(&self, f: &mut ratatui::Frame, area: Rect) {
        let frame_style = Style::default().fg(Color::DarkGray);
        let border = "".repeat(area.width as usize);
        f.render_widget(
            Paragraph::new(Line::from(Span::styled(border.clone(), frame_style))),
            Rect::new(area.x, area.y, area.width, 1),
        );
        f.render_widget(
            Paragraph::new(Line::from(Span::styled(border, frame_style))),
            Rect::new(area.x, area.bottom().saturating_sub(1), area.width, 1),
        );

        let area = Rect::new(
            area.x,
            area.y.saturating_add(1),
            area.width,
            area.height.saturating_sub(2),
        );
        if area.height == 0 {
            return;
        }

        let (prompt, prompt_color) = match self.activity {
            Activity::Idle => ("", Color::Yellow),
            Activity::Asking => ("", Color::Red),
            _ => ("", Color::Gray),
        };

        let max_w = (area.width as usize).saturating_sub(BORDER_PADDING);

        // History mode indicator
        let history_indicator = if self.history_index.is_some() {
            "📜 "
        } else {
            ""
        };

        // Ask mode handling
        if self.activity == Activity::Asking && self.waiting_for_ask {
            let mut lines: Vec<Line> = Vec::new();

            // First line: prompt and instructions
            let mut spans: Vec<Span> = vec![Span::styled(
                prompt,
                Style::default()
                    .fg(prompt_color)
                    .add_modifier(Modifier::BOLD),
            )];

            if !self.ask_options.is_empty() {
                // Show compact instruction
                spans.push(Span::styled(
                    if self.ask_multi_select {
                        "↑↓选择  Space切换  Enter确认"
                    } else {
                        "↑↓选择  Enter确认"
                    },
                    Style::default().fg(Color::DarkGray),
                ));
                lines.push(Line::from(spans));

                // Options display - use structured layout
                for (i, opt) in self.ask_options.iter().enumerate() {
                    let is_selected = self.ask_selected_index == i;
                    let is_checked = opt.selected;
                    let is_submit = opt.is_submit;
                    let is_other = opt.is_other;

                    // Build option line with visual indicators
                    let mut opt_spans: Vec<Span> = Vec::new();

                    // Selection arrow
                    opt_spans.push(Span::styled(
                        if is_selected { "" } else { "  " },
                        Style::default().fg(if is_selected {
                            Color::Cyan
                        } else {
                            Color::DarkGray
                        }),
                    ));

                    // Checkbox/radio indicator
                    if self.ask_multi_select {
                        let box_char = if is_checked { "" } else { "" };
                        let box_color = if is_checked {
                            Color::Green
                        } else {
                            Color::Gray
                        };
                        opt_spans.push(Span::styled(
                            format!("{} ", box_char),
                            Style::default().fg(box_color),
                        ));
                    } else {
                        // Single select: use radio-style
                        let radio_char = if is_checked { "" } else { "" };
                        let radio_color = if is_checked { Color::Cyan } else { Color::Gray };
                        opt_spans.push(Span::styled(
                            format!("{} ", radio_char),
                            Style::default().fg(radio_color),
                        ));
                    }

                    // Option label
                    let label_style = if is_submit {
                        Style::default()
                            .fg(if is_checked {
                                Color::Yellow
                            } else {
                                Color::White
                            })
                            .add_modifier(Modifier::BOLD)
                    } else if is_checked {
                        Style::default()
                            .fg(Color::Green)
                            .add_modifier(Modifier::BOLD)
                    } else if is_selected {
                        Style::default().fg(Color::White)
                    } else {
                        Style::default().fg(Color::Gray)
                    };
                    opt_spans.push(Span::styled(opt.label.clone(), label_style));

                    // Description (if available)
                    if let Some(desc) = &opt.description {
                        opt_spans.push(Span::styled(
                            format!(" {}", truncate(desc, 25)),
                            Style::default().fg(Color::DarkGray),
                        ));
                    }

                    // Other option hint
                    if is_other && is_selected && !is_checked {
                        opt_spans.push(Span::styled(
                            " ✏️自定义",
                            Style::default().fg(Color::Yellow),
                        ));
                    }

                    lines.push(Line::from(opt_spans));
                }
            } else {
                // Free text input mode - show user input with cursor
                if self.input.is_empty() {
                    spans.push(Span::styled("", Style::default().fg(Color::Cyan)));
                    spans.push(Span::styled(
                        "Type y/n, Enter to submit  ESC abort",
                        Style::default().fg(Color::DarkGray),
                    ));
                } else {
                    spans.push(Span::styled(
                        self.input.clone(),
                        Style::default().fg(Color::White),
                    ));
                    spans.push(Span::styled("", Style::default().fg(Color::Cyan)));
                    spans.push(Span::styled(
                        "  Enter to submit  ESC abort",
                        Style::default().fg(Color::DarkGray),
                    ));
                }
                lines.push(Line::from(spans));
            }

            // "Other" input mode: show input field
            if self.ask_other_input_active {
                lines.push(Line::styled(
                    "─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─",
                    Style::default().fg(Color::DarkGray),
                ));
                let input_line = if self.input.is_empty() {
                    Line::from(vec![
                        Span::styled("  ✏️ ", Style::default().fg(Color::Yellow)),
                        Span::styled("", Style::default().fg(Color::Cyan)),
                        Span::styled(" 输入自定义内容", Style::default().fg(Color::DarkGray)),
                    ])
                } else {
                    Line::from(vec![
                        Span::styled("  ✏️ ", Style::default().fg(Color::Yellow)),
                        Span::styled(&self.input, Style::default().fg(Color::White)),
                        Span::styled("", Style::default().fg(Color::Cyan)),
                    ])
                };
                lines.push(input_line);
                lines.push(Line::styled(
                    "  [Enter确认  Esc取消]",
                    Style::default().fg(Color::DarkGray),
                ));
            }

            f.render_widget(Paragraph::new(lines), area);
            return;
        }

        let is_multiline = self.input.contains('\n');

        if !is_multiline {
            let mut spans: Vec<Span> = vec![Span::styled(
                prompt,
                Style::default()
                    .fg(prompt_color)
                    .add_modifier(Modifier::BOLD),
            )];

            // History mode indicator
            if self.history_index.is_some() {
                spans.push(Span::styled(
                    history_indicator,
                    Style::default().fg(Color::DarkGray),
                ));
            }

            if self.input.is_empty() {
                spans.push(Span::styled("", Style::default().fg(Color::Cyan)));
                // Show helpful shortcuts hints
                if self.history_index.is_some() {
                    spans.push(Span::styled(
                        "↑↓ navigate  Enter use  Esc back",
                        Style::default().fg(Color::DarkGray),
                    ));
                } else {
                    spans.push(Span::styled(
                        " Ask anything... ",
                        Style::default().fg(Color::DarkGray),
                    ));
                    spans.push(Span::styled(
                        "(Ctrl+V paste │ ↑↓ history │ Shift+Enter newline)",
                        Style::default().fg(Color::DarkGray),
                    ));
                }
            } else {
                let display_width = max_w.saturating_sub(15);
                let before_cursor = &self.input[..self.cursor_pos];
                let after_cursor = &self.input[self.cursor_pos..];

                let before_vis_width: usize = before_cursor
                    .chars()
                    .map(|c| if c > '\u{7F}' { 2 } else { 1 })
                    .sum();
                let after_vis_width: usize = after_cursor
                    .chars()
                    .map(|c| if c > '\u{7F}' { 2 } else { 1 })
                    .sum();

                if before_vis_width + after_vis_width <= display_width {
                    spans.push(Span::styled(
                        before_cursor.to_string(),
                        Style::default().fg(Color::White),
                    ));
                    spans.push(Span::styled("", Style::default().fg(Color::Cyan)));
                    spans.push(Span::styled(
                        after_cursor.to_string(),
                        Style::default().fg(Color::White),
                    ));
                } else if before_vis_width < display_width {
                    spans.push(Span::styled(
                        before_cursor.to_string(),
                        Style::default().fg(Color::White),
                    ));
                    spans.push(Span::styled("", Style::default().fg(Color::Cyan)));
                    let remaining = display_width.saturating_sub(before_vis_width);
                    let truncated_after = truncate_visual(after_cursor, remaining);
                    spans.push(Span::styled(
                        truncated_after,
                        Style::default().fg(Color::White),
                    ));
                } else {
                    let start_width = display_width.saturating_sub(10);
                    let truncated_before = truncate_visual_end(before_cursor, start_width);
                    spans.push(Span::styled(
                        format!("{}", truncated_before),
                        Style::default().fg(Color::White),
                    ));
                    spans.push(Span::styled("", Style::default().fg(Color::Cyan)));
                    let remaining = display_width.saturating_sub(start_width + 1);
                    let truncated_after = truncate_visual(after_cursor, remaining);
                    spans.push(Span::styled(
                        truncated_after,
                        Style::default().fg(Color::White),
                    ));
                }
            }

            f.render_widget(Paragraph::new(Line::from(spans)), area);
        } else {
            // Multiline mode
            let mut lines: Vec<Line> = Vec::new();
            let input_lines: Vec<&str> = self.input.split('\n').collect();
            let cursor_line = self.input[..self.cursor_pos].matches('\n').count();
            let cursor_col_byte = self.input[..self.cursor_pos]
                .rfind('\n')
                .map(|i| self.cursor_pos - i - 1)
                .unwrap_or(self.cursor_pos);

            let total_lines_count = input_lines.len();
            // Use area height minus 1 for char count line if needed
            let show_char_count = self.input.chars().count() > 50 || total_lines_count > 1;
            let max_display_lines =
                (area.height as usize).saturating_sub(if show_char_count { 1 } else { 0 });

            for (i, line) in input_lines.iter().enumerate().take(max_display_lines) {
                let line_prompt = if i == 0 { prompt } else { "  " };
                let line_prompt_color = if i == 0 {
                    prompt_color
                } else {
                    Color::DarkGray
                };

                // Add line number indicator for multiline
                let line_num_hint = if i == cursor_line && total_lines_count > 1 {
                    format!("({}/{}) ", i + 1, total_lines_count)
                } else {
                    String::new()
                };

                if i == cursor_line {
                    let before = &line[..cursor_col_byte.min(line.len())];
                    let after = &line[cursor_col_byte.min(line.len())..];
                    lines.push(Line::from(vec![
                        Span::styled(
                            line_prompt,
                            Style::default()
                                .fg(line_prompt_color)
                                .add_modifier(Modifier::BOLD),
                        ),
                        Span::styled(line_num_hint, Style::default().fg(Color::DarkGray)),
                        Span::styled(before.to_string(), Style::default().fg(Color::White)),
                        Span::styled("", Style::default().fg(Color::Cyan)),
                        Span::styled(after.to_string(), Style::default().fg(Color::White)),
                    ]));
                } else {
                    lines.push(Line::from(vec![
                        Span::styled(
                            line_prompt,
                            Style::default()
                                .fg(line_prompt_color)
                                .add_modifier(Modifier::BOLD),
                        ),
                        Span::styled(truncate(line, max_w), Style::default().fg(Color::White)),
                    ]));
                }
            }

            let total_lines = input_lines.len();
            if total_lines > max_display_lines {
                lines.push(Line::styled(
                    format!("  … ({}/{} lines)", max_display_lines, total_lines),
                    Style::default().fg(Color::DarkGray),
                ));
            }

            // Show character count at the bottom for multiline input
            if show_char_count {
                lines.push(Line::styled(
                    format!(
                        "  {} chars, {} lines",
                        self.input.chars().count(),
                        total_lines_count
                    ),
                    Style::default().fg(Color::DarkGray),
                ));
            }

            f.render_widget(Paragraph::new(lines), area);
        }
    }
}