use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use std::time::Instant;
use tui_textarea::{CursorMove, TextArea};
use ratatui::{
Frame,
layout::{Alignment, Rect},
style::{Color as RatatuiColor, Style as RatatuiStyle},
widgets::Paragraph,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EditMode {
Insert,
Normal,
Command,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InputAction {
Continue,
Submit,
Cancel,
}
pub struct HelixTextArea {
textarea: TextArea<'static>,
mode: EditMode,
command_buffer: String,
key_sequence: Vec<char>,
last_key_time: Instant,
show_line_numbers: bool,
is_maximized: bool,
}
impl HelixTextArea {
pub fn new(initial_value: String, show_line_numbers: bool, start_in_normal_mode: bool) -> Self {
let mut textarea = if initial_value.is_empty() {
TextArea::default()
} else {
TextArea::from(initial_value.lines().map(|s| s.to_string()))
};
textarea.set_style(
RatatuiStyle::default()
.fg(RatatuiColor::Rgb(236, 239, 244)) .bg(RatatuiColor::Rgb(46, 52, 64)),
);
textarea.set_cursor_style(
RatatuiStyle::default()
.bg(RatatuiColor::Rgb(136, 192, 208)) .fg(RatatuiColor::Rgb(46, 52, 64)),
);
if show_line_numbers {
textarea
.set_line_number_style(RatatuiStyle::default().fg(RatatuiColor::Rgb(76, 86, 106))); }
textarea.set_cursor_line_style(RatatuiStyle::default().bg(RatatuiColor::Rgb(59, 66, 82)));
Self {
textarea,
mode: if start_in_normal_mode {
EditMode::Normal } else {
EditMode::Insert },
command_buffer: String::new(),
key_sequence: Vec::new(),
last_key_time: Instant::now(),
show_line_numbers,
is_maximized: false, }
}
#[allow(dead_code)]
pub fn get_mode(&self) -> EditMode {
self.mode
}
pub fn is_maximized(&self) -> bool {
self.is_maximized
}
pub fn toggle_maximize(&mut self) {
self.is_maximized = !self.is_maximized;
}
pub fn get_content(&self) -> String {
self.textarea.lines().join("\n")
}
pub fn handle_key(&mut self, key: KeyEvent) -> InputAction {
if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('s') {
return InputAction::Submit;
}
match self.mode {
EditMode::Insert => self.handle_insert_mode(key),
EditMode::Normal => self.handle_normal_mode(key),
EditMode::Command => self.handle_command_mode(key),
}
}
fn handle_insert_mode(&mut self, key: KeyEvent) -> InputAction {
match key.code {
KeyCode::Esc => {
self.mode = EditMode::Normal;
InputAction::Continue
}
KeyCode::Char(c) => {
self.textarea.insert_char(c);
InputAction::Continue
}
KeyCode::Enter => {
self.textarea.insert_newline();
InputAction::Continue
}
KeyCode::Backspace => {
self.textarea.delete_char();
InputAction::Continue
}
KeyCode::Delete => {
self.textarea.delete_next_char();
InputAction::Continue
}
KeyCode::Left => {
self.textarea.move_cursor(CursorMove::Back);
InputAction::Continue
}
KeyCode::Right => {
self.textarea.move_cursor(CursorMove::Forward);
InputAction::Continue
}
KeyCode::Up => {
self.textarea.move_cursor(CursorMove::Up);
InputAction::Continue
}
KeyCode::Down => {
self.textarea.move_cursor(CursorMove::Down);
InputAction::Continue
}
KeyCode::Home => {
self.textarea.move_cursor(CursorMove::Head);
InputAction::Continue
}
KeyCode::End => {
self.textarea.move_cursor(CursorMove::End);
InputAction::Continue
}
_ => InputAction::Continue,
}
}
fn handle_normal_mode(&mut self, key: KeyEvent) -> InputAction {
if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {
return InputAction::Cancel;
}
if self.last_key_time.elapsed().as_millis() > 500 {
self.key_sequence.clear();
}
self.last_key_time = Instant::now();
match key.code {
KeyCode::Char('i') => {
self.mode = EditMode::Insert;
InputAction::Continue
}
KeyCode::Char('a') => {
self.textarea.move_cursor(CursorMove::Forward);
self.mode = EditMode::Insert;
InputAction::Continue
}
KeyCode::Char('I') => {
self.textarea.move_cursor(CursorMove::Head);
self.mode = EditMode::Insert;
InputAction::Continue
}
KeyCode::Char('A') => {
self.textarea.move_cursor(CursorMove::End);
self.mode = EditMode::Insert;
InputAction::Continue
}
KeyCode::Char('o') => {
self.textarea.move_cursor(CursorMove::End);
self.textarea.insert_newline();
self.mode = EditMode::Insert;
InputAction::Continue
}
KeyCode::Char('O') => {
self.textarea.move_cursor(CursorMove::Head);
self.textarea.insert_newline();
self.textarea.move_cursor(CursorMove::Up);
self.mode = EditMode::Insert;
InputAction::Continue
}
KeyCode::Char('h') | KeyCode::Left => {
self.textarea.move_cursor(CursorMove::Back);
InputAction::Continue
}
KeyCode::Char('j') | KeyCode::Down => {
self.textarea.move_cursor(CursorMove::Down);
InputAction::Continue
}
KeyCode::Char('k') | KeyCode::Up => {
self.textarea.move_cursor(CursorMove::Up);
InputAction::Continue
}
KeyCode::Char('l') | KeyCode::Right => {
self.textarea.move_cursor(CursorMove::Forward);
InputAction::Continue
}
KeyCode::Char('w') => {
self.textarea.move_cursor(CursorMove::WordForward);
InputAction::Continue
}
KeyCode::Char('b') => {
self.textarea.move_cursor(CursorMove::WordBack);
InputAction::Continue
}
KeyCode::Char('e') => {
self.textarea.move_cursor(CursorMove::WordEnd);
InputAction::Continue
}
KeyCode::Char('0') | KeyCode::Home => {
self.textarea.move_cursor(CursorMove::Head);
InputAction::Continue
}
KeyCode::Char('$') | KeyCode::End => {
self.textarea.move_cursor(CursorMove::End);
InputAction::Continue
}
KeyCode::Char('G') => {
self.textarea.move_cursor(CursorMove::Bottom);
InputAction::Continue
}
KeyCode::Char('x') => {
self.textarea.move_cursor(CursorMove::Head);
self.textarea.start_selection();
self.textarea.move_cursor(CursorMove::End);
InputAction::Continue
}
KeyCode::Char('X') => {
self.textarea.move_cursor(CursorMove::Head);
self.textarea.start_selection();
self.textarea.move_cursor(CursorMove::Down);
self.textarea.move_cursor(CursorMove::Head);
InputAction::Continue
}
KeyCode::Delete => {
self.textarea.delete_next_char();
InputAction::Continue
}
KeyCode::Backspace => {
self.textarea.delete_char();
InputAction::Continue
}
KeyCode::Char('u') => {
self.textarea.undo();
InputAction::Continue
}
KeyCode::Char('U') => {
self.textarea.redo();
InputAction::Continue
}
KeyCode::Char('p') => {
self.textarea.paste();
InputAction::Continue
}
KeyCode::Char('d') => {
self.textarea.cut();
InputAction::Continue
}
KeyCode::Char('m') => {
self.toggle_maximize();
InputAction::Continue
}
KeyCode::Char(':') => {
self.mode = EditMode::Command;
self.command_buffer.clear();
InputAction::Continue
}
KeyCode::Char(c) => {
self.key_sequence.push(c);
self.handle_key_sequence()
}
_ => InputAction::Continue,
}
}
fn handle_key_sequence(&mut self) -> InputAction {
match self.key_sequence.as_slice() {
['g', 'g'] => {
self.textarea.move_cursor(CursorMove::Top);
self.key_sequence.clear();
InputAction::Continue
}
['g', 'e'] => {
self.textarea.move_cursor(CursorMove::Bottom);
self.key_sequence.clear();
InputAction::Continue
}
['d', 'd'] => {
self.textarea.delete_line_by_head();
self.key_sequence.clear();
InputAction::Continue
}
['c', 'c'] => {
self.textarea.delete_line_by_head();
self.mode = EditMode::Insert;
self.key_sequence.clear();
InputAction::Continue
}
['y', 'y'] => {
self.textarea.copy();
self.key_sequence.clear();
InputAction::Continue
}
_ => {
if self.key_sequence.len() > 2 {
self.key_sequence.clear();
}
InputAction::Continue
}
}
}
fn handle_command_mode(&mut self, key: KeyEvent) -> InputAction {
match key.code {
KeyCode::Esc => {
self.mode = EditMode::Normal;
self.command_buffer.clear();
InputAction::Continue
}
KeyCode::Enter => {
let action = self.execute_command();
self.command_buffer.clear();
if action != InputAction::Continue {
action
} else {
self.mode = EditMode::Normal;
InputAction::Continue
}
}
KeyCode::Char(c) => {
self.command_buffer.push(c);
InputAction::Continue
}
KeyCode::Backspace => {
self.command_buffer.pop();
InputAction::Continue
}
_ => InputAction::Continue,
}
}
fn execute_command(&mut self) -> InputAction {
match self.command_buffer.trim() {
"w" | "write" => InputAction::Submit,
"q" | "quit" => InputAction::Cancel,
"wq" | "x" => InputAction::Submit,
"q!" => InputAction::Cancel,
_ => InputAction::Continue,
}
}
pub fn render(&mut self, f: &mut Frame, area: Rect) {
if self.show_line_numbers {
self.textarea
.set_line_number_style(RatatuiStyle::default().fg(RatatuiColor::Rgb(76, 86, 106)));
}
f.render_widget(&self.textarea, area);
}
pub fn render_mode_indicator(&self, f: &mut Frame, area: Rect) {
let (mode_text, style) = match self.mode {
EditMode::Insert => (
"-- INSERT --".to_string(),
RatatuiStyle::default().fg(RatatuiColor::Rgb(163, 190, 140)), ),
EditMode::Normal => (
"-- NORMAL --".to_string(),
RatatuiStyle::default().fg(RatatuiColor::Rgb(136, 192, 208)), ),
EditMode::Command => {
let text = format!(":{}█", self.command_buffer);
(
text,
RatatuiStyle::default().fg(RatatuiColor::Rgb(235, 203, 139)), )
}
};
let paragraph = Paragraph::new(mode_text)
.style(style)
.alignment(Alignment::Left);
f.render_widget(paragraph, area);
}
#[allow(dead_code)]
pub fn render_key_sequence(&self, f: &mut Frame, area: Rect) {
if !self.key_sequence.is_empty() && self.mode == EditMode::Normal {
let text = self.key_sequence.iter().collect::<String>();
let paragraph = Paragraph::new(text)
.style(RatatuiStyle::default().fg(RatatuiColor::Rgb(235, 203, 139))) .alignment(Alignment::Right);
f.render_widget(paragraph, area);
}
}
}