use orfail::OrFail;
use tuinix::{KeyCode, KeyInput, TerminalPosition, TerminalStyle};
use unicode_width::UnicodeWidthStr;
use crate::{
app::{AppState, Focus},
canvas::{Canvas, Token},
git::GrepArg,
};
#[derive(Debug, Default)]
pub struct CommandEditorWidget {
original_text: String,
index: usize,
available_cols: usize,
}
impl CommandEditorWidget {
const ROW_OFFSET: usize = 1;
const COL_OFFSET: usize = "$ git".len();
pub fn set_available_cols(&mut self, cols: usize) {
self.available_cols = cols;
}
pub fn handle_focus_change(&mut self, state: &mut AppState) {
let Some(arg) = state.focused_arg_mut() else {
return;
};
self.original_text = arg.text.clone();
self.index = arg.len();
state.dirty = true;
}
pub fn handle_key_input(
&mut self,
state: &mut AppState,
input: KeyInput,
) -> orfail::Result<()> {
match (input.ctrl, input.code) {
(_, KeyCode::Enter) => {
state.regrep().or_fail()?;
state.focus = Focus::SearchResult;
state.dirty = true;
}
(_, KeyCode::Tab) => {
state.regrep().or_fail()?;
state.dirty = true;
}
(true, KeyCode::Char('g')) => {
let arg = state.focused_arg_mut().or_fail()?;
arg.text = self.original_text.clone();
state.regrep().or_fail()?;
state.focus = Focus::SearchResult;
state.dirty = true;
}
(false, KeyCode::Char(c))
if c.is_alphanumeric() || c.is_ascii_graphic() || c == ' ' =>
{
state.focused_arg_mut().or_fail()?.insert(self.index, c);
self.index += c.len_utf8();
state.dirty = true;
}
(false, KeyCode::Backspace) | (true, KeyCode::Char('h')) => {
let arg = state.focused_arg_mut().or_fail()?;
if let Some(c) = arg.prev_char(self.index) {
self.index -= c.len_utf8();
arg.remove(self.index).or_fail()?;
state.dirty = true;
}
}
(false, KeyCode::Delete) | (true, KeyCode::Char('d')) => {
let arg = state.focused_arg_mut().or_fail()?;
if arg.remove(self.index).is_some() {
state.dirty = true;
}
}
(false, KeyCode::Left) | (true, KeyCode::Char('b')) => {
let arg = state.focused_arg_mut().or_fail()?;
if let Some(c) = arg.prev_char(self.index) {
self.index -= c.len_utf8();
state.dirty = true;
}
}
(false, KeyCode::Right) | (true, KeyCode::Char('f')) => {
let arg = state.focused_arg_mut().or_fail()?;
if let Some(c) = arg.next_char(self.index) {
self.index += c.len_utf8();
state.dirty = true;
}
}
(true, KeyCode::Char('a')) => {
if self.index > 0 {
self.index = 0;
state.dirty = true;
}
}
(true, KeyCode::Char('e')) => {
let arg = state.focused_arg_mut().or_fail()?;
if self.index < arg.len() {
self.index = state.grep.pattern.len();
state.dirty = true;
}
}
_ => {}
}
Ok(())
}
pub fn render(&self, state: &AppState, canvas: &mut Canvas) {
if state.focus.is_editing() {
canvas.drawln(Token::with_style(
"[COMMAND]: editing…",
TerminalStyle::new().bold(),
));
} else {
canvas.drawln(Token::with_style("[COMMAND]", TerminalStyle::new()));
}
canvas.draw(Token::new("$ git"));
self.render_grep_args(state, canvas, &state.grep.args(state.focus));
}
fn render_grep_args(&self, state: &AppState, canvas: &mut Canvas, args: &[GrepArg]) {
let multiline = self.is_multiline(state);
for arg in args {
let focused = arg.kind.is_focused(state.focus);
if multiline && arg.multiline_head {
canvas.newline();
canvas.set_cursor_col(Self::COL_OFFSET);
}
let style = if focused {
TerminalStyle::new().bold()
} else {
TerminalStyle::new()
};
canvas.draw(Token::with_style(
format!(" {}", arg.maybe_quoted_text(state.focus)),
style,
));
}
canvas.newline();
}
pub fn update_cursor_position(&self, state: &mut AppState) {
if !state.focus.is_editing() {
state.show_terminal_cursor = None;
return;
}
let multiline = self.is_multiline(state);
let mut pos = TerminalPosition::row_col(Self::ROW_OFFSET, Self::COL_OFFSET);
for arg in state.grep.args(state.focus) {
let focused = arg.kind.is_focused(state.focus);
if multiline && arg.multiline_head {
pos.row += 1;
pos.col = Self::COL_OFFSET;
}
pos.col += 1;
if focused {
pos.col += arg.text[0..self.index].width();
state.show_terminal_cursor = Some(pos);
return;
} else {
pos.col += arg.width(state.focus);
}
}
}
fn is_multiline(&self, state: &AppState) -> bool {
let cols = Self::COL_OFFSET
+ state
.grep
.args(state.focus)
.iter()
.map(|a| a.width(state.focus) + 1) .sum::<usize>();
cols > self.available_cols
}
}