use std::fmt::Write as _;
use std::ops::{Deref, DerefMut};
use std::time::Instant;
use ansi_to_tui::IntoText;
use ratatui::{prelude::*, widgets::Widget};
use unicode_display_width::width as display_width;
use crate::helper::item::strip_ansi;
use crate::tui::BorderType;
use crate::tui::options::TuiLayout;
use crate::tui::statusline::{Info, InfoDisplay};
use crate::tui::util::style_line;
use crate::tui::widget::{SkimRender, SkimWidget};
use crate::{SkimOptions, theme::ColorTheme};
use std::sync::Arc;
const SPINNER_DURATION: u32 = 200;
const SPINNERS_UNICODE: [char; 10] = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
#[derive(Clone, Default)]
pub struct StatusInfo {
pub total: usize,
pub matched: usize,
pub processed: usize,
pub show_spinner: bool,
pub matcher_mode: String,
pub multi_selection: bool,
pub selected: usize,
pub current_item_idx: usize,
pub hscroll_offset: i64,
pub start: Option<Instant>,
pub inline_separator: String,
}
impl StatusInfo {
pub fn left_title(&self) -> String {
let mut parts = String::new();
if self.show_spinner
&& let Some(start) = self.start
{
let spinner_elapsed_ms = start.elapsed().as_millis();
let index =
((spinner_elapsed_ms / u128::from(SPINNER_DURATION)) % (SPINNERS_UNICODE.len() as u128)) as usize;
parts.push(SPINNERS_UNICODE[index]);
parts.push(' ');
} else {
parts.push_str(" ");
}
let _ = write!(parts, "{}/{}", self.matched, self.total);
if !self.matcher_mode.is_empty() {
let _ = write!(parts, "/{}", &self.matcher_mode);
}
if self.show_spinner && self.total > 0 && self.processed != self.total {
let pct = self.processed.saturating_mul(100) / self.total;
let _ = write!(parts, " ({pct}%)");
}
if self.multi_selection && self.selected > 0 {
let _ = write!(parts, " [{}]", self.selected);
}
parts
}
pub fn inline_separator_or_spinner(&self) -> String {
if self.show_spinner
&& let Some(start) = self.start
{
let spinner_elapsed_ms = start.elapsed().as_millis();
let index =
((spinner_elapsed_ms / u128::from(SPINNER_DURATION)) % (SPINNERS_UNICODE.len() as u128)) as usize;
format!(
"{}{}",
SPINNERS_UNICODE[index],
" ".repeat(display_width(&self.inline_separator).try_into().unwrap())
)
} else {
self.inline_separator.clone()
}
}
pub fn inline_status(&self) -> String {
let mut parts = String::new();
let _ = write!(parts, "{}/{}", self.matched, self.total);
if !self.matcher_mode.is_empty() {
let _ = write!(parts, "/{}", &self.matcher_mode);
}
if self.show_spinner && self.total > 0 {
let pct = self.processed.saturating_mul(100) / self.total;
let _ = write!(parts, " ({pct}%)");
}
if self.multi_selection && self.selected > 0 {
let _ = write!(parts, " [{}]", self.selected);
}
parts
}
pub fn right_title(&self) -> String {
format!("{}/{}", self.current_item_idx, self.hscroll_offset)
}
}
pub struct Input {
pub prompt: String,
alternate_prompt: String,
pub value: String,
alternate_value: String,
pub cursor_pos: u16,
pub alternate_cursor_pos: u16,
pub theme: Arc<ColorTheme>,
pub border: BorderType,
pub status_info: Option<StatusInfo>,
pub info: Info,
pub reverse: bool,
}
impl Default for Input {
fn default() -> Self {
Self::_default()
}
}
impl Input {
pub fn insert(&mut self, c: char) {
self.value.insert(self.cursor_pos.into(), c);
self.move_cursor(c.len_utf8().try_into().unwrap());
}
pub fn insert_str(&mut self, s: &str) {
self.value.insert_str(self.cursor_pos as usize, s);
self.move_cursor(
s.chars()
.count()
.try_into()
.expect("Failed to fit inserted str len into an i32"),
);
}
fn nchars(&self) -> usize {
self.value.chars().count()
}
pub fn delete(&mut self, offset: i32) -> Option<char> {
if self.value.is_empty() {
return None;
}
let new_pos = i32::from(self.cursor_pos) + offset;
if new_pos < 0 || usize::try_from(new_pos).map_or(true, |p| p >= self.value.len()) {
return None;
}
let pos = self.value.floor_char_boundary(new_pos.unsigned_abs() as usize);
let ch = self.value.remove(pos);
if offset < 0 {
self.move_cursor(-1);
}
Some(ch)
}
pub fn move_cursor(&mut self, offset: i32) {
if offset == 0 {
return;
}
if offset < 0 {
let new_pos = (i32::from(self.cursor_pos) + offset).max(0).unsigned_abs() as usize;
self.move_cursor_to(u16::try_from(self.value.floor_char_boundary(new_pos)).unwrap_or(u16::MAX));
} else {
let new_pos = (i32::from(self.cursor_pos) + offset).unsigned_abs() as usize;
self.move_cursor_to(u16::try_from(self.value.ceil_char_boundary(new_pos)).unwrap_or(u16::MAX));
}
}
pub fn move_cursor_to(&mut self, pos: u16) {
if self.value.is_char_boundary(pos as usize) {
self.cursor_pos = u16::clamp(pos, 0, u16::try_from(self.value.len()).unwrap_or(u16::MAX));
} else {
warn!("Invalid cursor pos");
}
}
pub fn move_to_end(&mut self) {
self.move_cursor_to(
self.value
.len()
.try_into()
.expect("Failed to fit input len into an u16"),
);
}
fn is_word_char(ch: char) -> bool {
ch.is_alphanumeric()
}
fn find_next_word_end(&self, start_pos: usize) -> usize {
let mut pos = start_pos;
while pos < self.nchars() {
let ch = self.value.chars().nth(pos).unwrap();
if Self::is_word_char(ch) {
break;
}
pos += 1;
}
while pos < self.nchars() {
let ch = self.value.chars().nth(pos).unwrap();
if !Self::is_word_char(ch) {
break;
}
pos += 1;
}
pos
}
fn find_compound_word_end(&self, start_pos: usize) -> usize {
let mut pos = start_pos;
while pos < self.nchars() {
let ch = self.value.chars().nth(pos).unwrap();
if !ch.is_whitespace() {
break;
}
pos += 1;
}
while pos < self.nchars() {
let ch = self.value.chars().nth(pos).unwrap();
if ch.is_whitespace() {
break;
}
pos += 1;
}
pos
}
fn find_prev_word_start(&self, start_pos: usize) -> usize {
if start_pos == 0 {
return 0;
}
let mut pos = start_pos;
pos = pos.saturating_sub(1);
while pos > 0 && !Self::is_word_char(self.value.chars().nth(pos).unwrap()) {
pos -= 1;
}
while pos > 0 && Self::is_word_char(self.value.chars().nth(pos - 1).unwrap()) {
pos -= 1;
}
pos
}
fn find_delete_backward_pos(&self, start_pos: usize) -> usize {
if start_pos == 0 {
return 0;
}
let mut pos = start_pos;
while pos > 0 {
let ch = self.value.chars().nth(pos - 1).unwrap();
if Self::is_word_char(ch) {
break;
}
pos -= 1;
}
while pos > 0 {
let ch = self.value.chars().nth(pos - 1).unwrap();
if !Self::is_word_char(ch) {
break;
}
pos -= 1;
}
pos
}
pub fn delete_backward_word(&mut self) -> String {
if self.cursor_pos == 0 {
return String::new();
}
let start_pos = self.find_delete_backward_pos(self.cursor_pos as usize);
let deleted = self.value[start_pos..self.cursor_pos as usize].to_string();
self.value = format!(
"{}{}",
&self.value[..start_pos],
&self.value[self.cursor_pos as usize..]
);
self.cursor_pos = u16::try_from(start_pos).unwrap_or(u16::MAX);
deleted
}
pub fn delete_backward_to_whitespace(&mut self) -> String {
if self.cursor_pos == 0 {
return String::new();
}
let mut pos = self.cursor_pos as usize;
while pos > 0 && self.value.chars().nth(pos - 1).unwrap_or_default().is_whitespace() {
pos -= 1;
}
while pos > 0 && !self.value.chars().nth(pos - 1).unwrap_or_default().is_whitespace() {
pos -= 1;
}
let deleted = self.value[pos..self.cursor_pos as usize].to_string();
self.value = format!("{}{}", &self.value[..pos], &self.value[self.cursor_pos as usize..]);
self.cursor_pos = u16::try_from(pos).unwrap_or(u16::MAX);
deleted
}
pub fn delete_forward_word(&mut self) -> String {
if self.cursor_pos as usize >= self.value.len() {
return String::new();
}
let end_pos = self.find_next_word_end(self.cursor_pos as usize);
let deleted = self.value[self.cursor_pos as usize..end_pos].to_string();
self.value = format!("{}{}", &self.value[..self.cursor_pos as usize], &self.value[end_pos..]);
deleted
}
pub fn move_cursor_forward_word(&mut self) {
let new_pos = self.find_compound_word_end(self.cursor_pos as usize);
self.cursor_pos = u16::try_from(new_pos).unwrap_or(u16::MAX);
}
pub fn move_cursor_backward_word(&mut self) {
let new_pos = self.find_prev_word_start(self.cursor_pos as usize);
self.cursor_pos = u16::try_from(new_pos).unwrap_or(u16::MAX);
}
pub fn delete_to_beginning(&mut self) -> String {
let deleted = self.value[..self.cursor_pos as usize].to_string();
self.value = self.value[self.cursor_pos as usize..].to_string();
self.cursor_pos = 0;
deleted
}
pub fn cursor_pos(&self) -> u16 {
(display_width(&self.value[..(self.cursor_pos as usize)]) + display_width(&strip_ansi(&self.prompt).0))
.try_into()
.expect("Failed to fit cursor char into an u16")
}
pub fn switch_mode(&mut self) {
std::mem::swap(&mut self.prompt, &mut self.alternate_prompt);
std::mem::swap(&mut self.value, &mut self.alternate_value);
std::mem::swap(&mut self.cursor_pos, &mut self.alternate_cursor_pos);
}
}
impl SkimWidget for Input {
fn from_options(options: &SkimOptions, theme: Arc<ColorTheme>) -> Self {
let mut res = Self {
theme,
border: options.border,
info: options.info.clone(),
reverse: options.layout == TuiLayout::Reverse,
prompt: String::new(),
alternate_prompt: String::new(),
value: String::new(),
alternate_value: String::new(),
cursor_pos: 0,
alternate_cursor_pos: 0,
status_info: None,
};
if options.interactive {
res.prompt.clone_from(&options.cmd_prompt);
res.alternate_prompt.clone_from(&options.prompt);
res.value = options.cmd_query.clone().unwrap_or_default();
res.alternate_value = options.query.clone().unwrap_or_default();
} else {
res.prompt.clone_from(&options.prompt);
res.alternate_prompt.clone_from(&options.cmd_prompt);
res.value = options.query.clone().unwrap_or_default();
res.alternate_value = options.cmd_query.clone().unwrap_or_default();
}
res.cursor_pos = u16::try_from(res.value.len()).unwrap_or(u16::MAX);
res.alternate_cursor_pos = u16::try_from(res.alternate_value.len()).unwrap_or(u16::MAX);
res
}
fn render(&mut self, area: Rect, buf: &mut Buffer) -> SkimRender {
use ratatui::layout::Alignment;
use ratatui::text::{Line, Span};
use ratatui::widgets::Paragraph;
use ratatui::widgets::{Block, Borders};
let mut line = self.prompt.into_text().map_or_else(
|_| Line::from(self.prompt.as_str()),
|t| t.lines.into_iter().next().unwrap_or_default(),
);
style_line(&mut line, self.theme.prompt);
line.push_span(Span::styled(&self.value, self.theme.query));
let mut block = Block::default();
if let Some(border_type) = self.border.into_ratatui() {
block = block
.borders(Borders::ALL)
.border_type(border_type)
.border_style(self.theme.border);
}
match self.info.display {
InfoDisplay::Inline | InfoDisplay::InlineRight => {
if let Some(ref status) = self.status_info {
let separator = status.inline_separator_or_spinner();
let inline_status = status.inline_status();
let right_status = status.right_title();
let prompt_width = display_width(&self.prompt);
let value_width = display_width(&self.value);
let separator_width = display_width(&separator); let inline_status_width = display_width(&inline_status);
let right_status_width = display_width(&right_status);
let used_width =
prompt_width + value_width + separator_width + inline_status_width + right_status_width;
let available_width = u64::from(area.width);
let padding_width = available_width.saturating_sub(used_width);
if self.info.display == InfoDisplay::InlineRight {
line.push_span(Span::raw(" ".repeat(usize::try_from(padding_width).unwrap() - 2)));
}
line.push_span(Span::styled(separator, self.theme.info));
line.push_span(Span::styled(inline_status, self.theme.info));
if self.info.display == InfoDisplay::Inline {
line.push_span(Span::raw(" ".repeat(padding_width.try_into().unwrap())));
} else {
line.push_span(Span::raw(" ".repeat(2)));
}
line.push_span(Span::styled(right_status, self.theme.info));
Paragraph::new(line)
.block(block)
.style(self.theme.normal)
.render(area, buf);
} else {
Paragraph::new(line)
.block(block)
.style(self.theme.normal)
.render(area, buf);
}
}
InfoDisplay::Default => {
if let Some(ref status) = self.status_info {
let left_title = status.left_title();
let right_title = status.right_title();
if self.reverse {
block = block
.title_bottom(Line::from(left_title).style(self.theme.info).alignment(Alignment::Left))
.title_bottom(
Line::from(right_title)
.style(self.theme.info)
.alignment(Alignment::Right),
);
} else {
block = block
.title_top(Line::from(left_title).style(self.theme.info).alignment(Alignment::Left))
.title_top(
Line::from(right_title)
.style(self.theme.info)
.alignment(Alignment::Right),
);
}
}
Paragraph::new(line)
.block(block)
.style(self.theme.normal)
.render(area, buf);
}
InfoDisplay::Hidden => {
Paragraph::new(line)
.block(block)
.style(self.theme.normal)
.render(area, buf);
}
}
SkimRender::default()
}
}
impl Deref for Input {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.value
}
}
impl DerefMut for Input {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.value
}
}