Files
zesdex/apps/interfaces/tui/src/view/input.rs
T
asepharyana 7b0b53671f style: format seluruh workspace dengan cargo fmt
Menyeragamkan format kode sesuai rustfmt (126 file). Sebelumnya
lefthook pre-commit 'cargo fmt --check' akan gagal pada commit apa pun.
2026-08-27 22:10:28 +07:00

128 lines
4.7 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Chat input box and autocomplete dropdown rendering.
//! Implements the Component trait.
use super::theme::Theme;
use crate::components::Component;
use ratatui::layout::Rect;
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, BorderType, Borders, Paragraph};
use ratatui::Frame;
#[derive(Default)]
pub struct InputComponent;
impl Component for InputComponent {
fn draw(&mut self, frame: &mut Frame, area: Rect, state: &crate::state::AppStateRest) {
if state.input.autocomplete_visible && !state.input.autocomplete_candidates.is_empty() {
let n = state.input.autocomplete_candidates.len().min(10) as u16;
let dropdown_height = n + 2;
let dropdown_area = Rect {
x: area.x,
y: area.y.saturating_sub(dropdown_height),
width: area.width.min(45),
height: dropdown_height,
};
let dropdown_title = match state.input.autocomplete_kind {
crate::state::AutocompleteKind::Command => " ⌘ Commands ",
crate::state::AutocompleteKind::FileMention => " 📁 Files ",
};
let dropdown_block = Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(Theme::BORDER))
.title(Span::styled(
dropdown_title,
Style::default()
.fg(Theme::PRIMARY)
.add_modifier(Modifier::BOLD),
))
.style(Style::default().bg(Theme::SURFACE_ELEVATED));
let mut lines: Vec<Line> = Vec::new();
let selected = state.input.autocomplete_idx;
for (i, candidate) in state
.input
.autocomplete_candidates
.iter()
.enumerate()
.take(10)
{
let prefix = if i == selected { " ▸ " } else { " " };
let style = if i == selected {
Style::default()
.fg(Theme::TEXT)
.bg(Theme::HIGHLIGHT_DIM)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(Theme::TEXT)
};
let label = format!("{prefix}{candidate}");
lines.push(Line::from(Span::styled(label, style)));
}
let dropdown = Paragraph::new(lines).block(dropdown_block);
frame.render_widget(ratatui::widgets::Clear, dropdown_area);
frame.render_widget(dropdown, dropdown_area);
}
let block = Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(Theme::BORDER_FOCUSED))
.style(Style::default().bg(Theme::BG));
let input_text = &state.input.buffer;
let cursor_pos = state.input.cursor;
let prompt = Span::styled(
" ",
Style::default()
.fg(Theme::PRIMARY)
.add_modifier(Modifier::BOLD),
);
let mut spans = vec![prompt];
if input_text.is_empty() {
spans.push(Span::styled(
"Type a message, @file, or /command...",
Style::default()
.fg(Theme::TEXT_DIM)
.add_modifier(Modifier::ITALIC),
));
} else {
let (before, after) = input_text.split_at(cursor_pos);
spans.push(Span::raw(before.to_string()));
let (cursor_char, after_char) = if after.is_empty() {
(" ".to_string(), "")
} else {
let c = after.chars().next().unwrap();
let char_len = c.len_utf8();
(c.to_string(), &after[char_len..])
};
// Blinking cursor logic based on tick count
let cursor_style = if state.misc.tick_count % 10 < 5 {
Style::default()
.bg(Theme::PRIMARY)
.fg(Theme::BG)
.add_modifier(Modifier::BOLD)
} else {
Style::default()
.bg(Theme::TEXT_DIM)
.fg(Theme::BG)
.add_modifier(Modifier::BOLD)
};
spans.push(Span::styled(cursor_char, cursor_style));
if !after_char.is_empty() {
spans.push(Span::raw(after_char.to_string()));
}
}
let line = Line::from(spans);
let paragraph = Paragraph::new(line).block(block);
frame.render_widget(paragraph, area);
}
}