use crossterm::event::{KeyCode, KeyModifiers, MouseEventKind};
use ratatui::{
layout::{Constraint, Layout, Rect},
style::{Color, Modifier, Style},
text::{Span, Text},
widgets::{Block, Borders, Paragraph},
Frame,
};
use crate::state::{App, TerminalMode};
use crate::serial::InputMode;
use super::common::{
draw_error_popup, draw_info_bar, help_bar_height, help_spans, info_bar_spans, sep_span,
};
pub fn draw(app: &mut App, frame: &mut Frame) {
let info_height = help_bar_height(info_bar_spans(app), frame.area().width).0;
let help_height = help_bar_height(
help_spans_for_mode(&app.terminal_mode, app.keyboard_enhanced),
frame.area().width,
)
.0;
let [info_area, output_area, help_area] = Layout::vertical([
Constraint::Length(info_height),
Constraint::Min(0),
Constraint::Length(help_height),
])
.areas(frame.area());
let inner = Rect {
x: output_area.x + 1,
y: output_area.y + 1,
width: output_area.width.saturating_sub(2),
height: output_area.height.saturating_sub(2),
};
app.resize_parser(inner.height, inner.width);
app.viewport_height = inner.height as usize;
draw_info_bar(app, frame, info_area);
let all_lines: Vec<&str> = app
.scrollback
.iter()
.flat_map(|l| l.split_inclusive('\n'))
.flat_map(|l| l.strip_suffix('\n').or(Some(l)))
.collect();
let total = all_lines.len();
let height = inner.height as usize;
let max_offset = total.saturating_sub(height);
let scrolling = app.scroll_offset > 0;
let block = Block::new().borders(Borders::ALL);
frame.render_widget(block, output_area);
if scrolling {
let end = total.saturating_sub(app.scroll_offset.min(max_offset));
let start = end.saturating_sub(height);
let visible: Vec<&str> = all_lines[start..end].to_vec();
let text = visible.join("\n");
frame.render_widget(Paragraph::new(text), inner);
} else {
let screen = app.parser.screen();
let buf = frame.buffer_mut();
for row in 0..inner.height {
for col in 0..inner.width {
if let Some(cell) = screen.cell(row, col) {
let ch = cell.contents();
if ch.is_empty() {
continue;
}
let mut style = Style::default();
style = style.fg(vt100_color(cell.fgcolor()));
style = style.bg(vt100_color(cell.bgcolor()));
if cell.bold() {
style = style.add_modifier(Modifier::BOLD);
}
if cell.italic() {
style = style.add_modifier(Modifier::ITALIC);
}
if cell.underline() {
style = style.add_modifier(Modifier::UNDERLINED);
}
if cell.inverse() {
style = style.add_modifier(Modifier::REVERSED);
}
buf[(inner.x + col, inner.y + row)]
.set_symbol(ch)
.set_style(style);
}
}
}
let (crow, ccol) = screen.cursor_position();
let (mut col, mut row) = (ccol, crow);
if app.input_mode == InputMode::Line {
let style = Style::default().fg(Color::Cyan);
let mut utf8 = [0u8; 4];
for ch in app.line_buffer.chars() {
if col >= inner.width {
col = 0;
row = row.saturating_add(1);
}
if row >= inner.height {
break;
}
let symbol = ch.encode_utf8(&mut utf8);
buf[(inner.x + col, inner.y + row)]
.set_symbol(symbol)
.set_style(style);
col += 1;
}
}
frame.set_cursor_position((
(inner.x + col).min(inner.x + inner.width - 1),
(inner.y + row).min(inner.y + inner.height - 1),
));
}
let (_, help_lines) = help_bar_height(
help_spans_for_mode(&app.terminal_mode, app.keyboard_enhanced),
help_area.width,
);
let help = Paragraph::new(Text::from(help_lines)).block(Block::new().borders(Borders::ALL));
frame.render_widget(help, help_area);
draw_error_popup(app, frame);
}
pub fn handle_key(app: &mut App, code: KeyCode, modifiers: KeyModifiers) {
match app.terminal_mode {
TerminalMode::Insert => handle_insert_mode(app, code, modifiers),
TerminalMode::Control => handle_control_mode(app, code),
}
}
pub fn handle_mouse(app: &mut App, mouse: crossterm::event::MouseEvent) {
match mouse.kind {
MouseEventKind::ScrollUp => app.scroll(3),
MouseEventKind::ScrollDown => app.scroll(-3),
_ => {}
}
}
fn help_spans_for_mode(mode: &TerminalMode, keyboard_enhanced: bool) -> Vec<Span<'static>> {
match mode {
TerminalMode::Insert => {
let mut spans = vec![
Span::styled(
" INSERT ",
Style::default().fg(Color::Black).bg(Color::Green).bold(),
),
sep_span(),
];
let escape_hint: &'static str = if keyboard_enhanced {
"Ctrl+Esc"
} else {
"Ctrl+Space"
};
spans.extend(help_spans(&[(escape_hint, "control mode")]));
spans
}
TerminalMode::Control => {
let mut spans = vec![
Span::styled(
" CONTROL ",
Style::default().fg(Color::Black).bg(Color::Cyan).bold(),
),
sep_span(),
];
spans.extend(help_spans(&[
("a", "insert"),
("↑↓", "scroll"),
("Esc", "bottom"),
("f", "flush"),
("c", "copy"),
("+/-", "baud"),
("s", "settings"),
("Del", "port select"),
("q", "quit"),
]));
spans
}
}
}
fn handle_insert_mode(app: &mut App, code: KeyCode, modifiers: KeyModifiers) {
let is_escape =
(app.keyboard_enhanced && code == KeyCode::Esc && modifiers == KeyModifiers::CONTROL)
|| (code == KeyCode::Char(' ') && modifiers == KeyModifiers::CONTROL);
if is_escape {
app.terminal_mode = TerminalMode::Control;
return;
}
if app.input_mode == InputMode::Line && !modifiers.contains(KeyModifiers::CONTROL) {
match code {
KeyCode::Char(c) => app.line_buffer.push(c),
KeyCode::Backspace => {
app.line_buffer.pop();
}
KeyCode::Enter => app.send_line(),
_ => {}
}
return;
}
let key_event = crossterm::event::Event::Key(crossterm::event::KeyEvent::new(code, modifiers));
if let Ok(t_event) = terminput_crossterm::to_terminput(key_event) {
let mut buf = [0u8; 16];
if let Ok(n) = t_event.encode(&mut buf, terminput::Encoding::Xterm) {
let mut bytes = buf[..n].to_vec();
if app.local_echo {
app.echo_local(&bytes);
}
if bytes == [0x0d] {
use crate::serial::NewlineEncoding;
match app.outgoing_newline {
NewlineEncoding::CR => {}
NewlineEncoding::LF => bytes[0] = 0x0a,
NewlineEncoding::CRLF => bytes.push(0x0a),
}
}
app.send_bytes(bytes);
}
}
}
fn handle_control_mode(app: &mut App, code: KeyCode) {
match code {
KeyCode::Char('a') | KeyCode::Char('i') => {
app.scroll_to_bottom();
app.terminal_mode = TerminalMode::Insert;
}
KeyCode::Up | KeyCode::Char('k') => app.scroll(3),
KeyCode::Down | KeyCode::Char('j') => app.scroll(-3),
KeyCode::Esc => app.scroll_to_bottom(),
KeyCode::Backspace | KeyCode::Delete => app.disconnect(),
KeyCode::Char('f') => app.flush_screen(),
KeyCode::Char('c') => app.copy_to_clipboard(),
KeyCode::Char('+') => app.change_baud(1),
KeyCode::Char('-') => app.change_baud(-1),
KeyCode::Char('s') => app.show_settings = true,
KeyCode::Char('q') => {
app.disconnect();
app.exit = true;
}
_ => {}
}
}
fn vt100_color(color: vt100::Color) -> Color {
match color {
vt100::Color::Default => Color::Reset,
vt100::Color::Idx(i) => Color::Indexed(i),
vt100::Color::Rgb(r, g, b) => Color::Rgb(r, g, b),
}
}