use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use tui_textarea::{CursorMove, TextArea};
pub(super) const COMMAND_BG: Color = Color::Rgb(82, 82, 82);
pub(super) const INACTIVE_COMMAND_BG: Color = Color::Rgb(45, 45, 45);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum CommandKind {
Bang,
Ex,
}
impl CommandKind {
pub(super) fn marker(self) -> char {
match self {
Self::Bang => '!',
Self::Ex => ':',
}
}
pub(super) fn marker_style(self) -> Style {
self.marker_style_with_focus(true)
}
fn marker_style_with_focus(self, focused: bool) -> Style {
let color = if focused {
match self {
Self::Bang => crate::theme::color(crate::theme::Role::CommandBang),
Self::Ex => crate::theme::color(crate::theme::Role::CommandEx),
}
} else {
Color::DarkGray
};
Style::default().fg(color).add_modifier(Modifier::BOLD)
}
}
pub(super) fn command_line(kind: CommandKind, tail: &str) -> Line<'static> {
command_line_with_focus(kind, tail, true)
}
pub(super) fn command_line_with_focus(
kind: CommandKind,
tail: &str,
focused: bool,
) -> Line<'static> {
Line::from(vec![
Span::styled(
kind.marker().to_string(),
kind.marker_style_with_focus(focused),
),
Span::styled(
tail.to_string(),
Style::default().fg(if focused {
Color::White
} else {
Color::DarkGray
}),
),
])
}
pub(super) fn command_background(focused: bool) -> Color {
if focused {
COMMAND_BG
} else {
INACTIVE_COMMAND_BG
}
}
pub(super) fn is_bang_escape(body: &str) -> bool {
crate::bang_command(body.trim()).is_some()
}
fn bang_prefix_chars(body: &str) -> Option<usize> {
if !is_bang_escape(body) {
return None;
}
let marker = body.chars().position(|c| !c.is_whitespace())?;
(body.chars().nth(marker) == Some('!')).then_some(marker + 1)
}
fn cursor_offset(lines: &[String], cursor: (usize, usize)) -> usize {
lines
.iter()
.take(cursor.0)
.map(|line| line.chars().count() + 1)
.sum::<usize>()
+ cursor.1
}
fn cursor_at_offset(lines: &[String], mut offset: usize) -> (usize, usize) {
for (row, line) in lines.iter().enumerate() {
let width = line.chars().count();
if offset <= width {
return (row, offset);
}
offset = offset.saturating_sub(width + 1);
}
let row = lines.len().saturating_sub(1);
(row, lines.get(row).map_or(0, |line| line.chars().count()))
}
pub(super) struct BangView<'a> {
pub(super) textarea: TextArea<'a>,
pub(super) cursor_on_marker: bool,
}
pub(super) fn bang_view<'a>(textarea: &TextArea<'a>) -> Option<BangView<'a>> {
let body = textarea.lines().join("\n");
let hidden = bang_prefix_chars(&body)?;
if textarea.is_selecting() {
return None;
}
let original_offset = cursor_offset(textarea.lines(), textarea.cursor());
let mut view = textarea.clone();
view.move_cursor(CursorMove::Jump(0, 0));
for _ in 0..hidden {
view.delete_next_char();
}
let cursor = cursor_at_offset(view.lines(), original_offset.saturating_sub(hidden));
view.move_cursor(CursorMove::Jump(cursor.0 as u16, cursor.1 as u16));
Some(BangView {
textarea: view,
cursor_on_marker: original_offset < hidden,
})
}
pub(super) fn cancel_hidden_bang_selection(textarea: &mut TextArea<'_>) {
if textarea.is_selecting() && is_bang_escape(&textarea.lines().join("\n")) {
textarea.cancel_selection();
}
}