use crate::{
display::{
SuperConsole,
renderers::color::helpers::{
ClearLine, CursorDirection, EMPTY_HEADER, calculate_message_width,
calculate_tailer_width, erase_line, header_width, move_cursor, pad_to_width,
},
},
input::{InputProvider, TerminalInputEvent},
};
use owo_colors::OwoColorize;
use std::{
fmt::Write,
sync::atomic::{AtomicBool, Ordering},
};
use unicode_width::UnicodeWidthChar;
use valuable::Valuable;
type LineColumnType = (usize, usize);
#[derive(Clone, Debug, PartialEq, Eq, Valuable)]
pub struct TerminalState {
autocomplete_suggestion: Option<String>,
character_at: usize,
cursor_character_position: LineColumnType,
line_widths: Vec<InputLine>,
max_character_at: usize,
message_width: usize,
tailer_width: usize,
ps1: String,
}
impl TerminalState {
#[must_use]
pub fn new(ps1: String) -> Self {
let msg_width = calculate_message_width(40);
let tailer_width = calculate_tailer_width(40);
Self {
autocomplete_suggestion: None,
character_at: 0,
cursor_character_position: (0, 0),
line_widths: vec![InputLine {
char_count: 0,
total_width: 0,
}],
max_character_at: 0,
message_width: msg_width,
tailer_width,
ps1,
}
}
#[must_use]
pub fn clear_current_render(&self) -> String {
if self.cursor_character_position.0 == 0 && self.cursor_character_position.1 == 0 {
return String::with_capacity(0);
}
let mut clear_string = if self.cursor_character_position.0 + 1 < self.line_widths.len() {
move_cursor(
CursorDirection::Down,
self.line_widths.len() - (self.cursor_character_position.0 + 1),
)
} else {
String::new()
};
clear_string += &move_cursor(
CursorDirection::Left,
self.cursor_character_position.1 + header_width(),
);
for _ in 0..self.line_widths.len() {
clear_string += &erase_line(ClearLine::EntireLine);
clear_string += &move_cursor(CursorDirection::Up, 1);
}
clear_string += &erase_line(ClearLine::EntireLine);
clear_string
}
#[must_use]
pub fn render_current_standalone(
&mut self,
new_ps1: Option<&str>,
message_width: usize,
tailer_width: usize,
input: &str,
) -> String {
if (new_ps1.is_some() && Some(self.ps1.as_str()) != new_ps1)
|| self.message_width != message_width
|| self.tailer_width != tailer_width
|| self.cursor_character_position == (0, 0)
{
self.message_width = message_width;
self.tailer_width = tailer_width;
if let Some(new_ps) = new_ps1 {
new_ps.clone_into(&mut self.ps1);
}
let (linecol, lines) = Self::calculate_lines(
self.message_width,
&self.ps1,
input,
self.autocomplete_suggestion.as_deref(),
self.character_at,
);
self.cursor_character_position = linecol;
self.line_widths = lines;
Self::do_full_render(
self.message_width,
self.tailer_width,
&self.ps1,
input,
self.cursor_character_position,
&self.line_widths,
self.autocomplete_suggestion.as_deref(),
)
} else {
Self::do_full_render(
self.message_width,
self.tailer_width,
&self.ps1,
input,
self.cursor_character_position,
&self.line_widths,
self.autocomplete_suggestion.as_deref(),
)
}
}
#[must_use]
pub fn on_input_event(
&mut self,
input_provider: &dyn InputProvider,
input_event: TerminalInputEvent,
force_pause: &AtomicBool,
) -> String {
match input_event {
TerminalInputEvent::InputStarted => String::with_capacity(0),
TerminalInputEvent::ClearScreen => self.do_clear_screen(),
TerminalInputEvent::InputCancelled | TerminalInputEvent::InputFinished => {
self.on_input_reset()
}
TerminalInputEvent::InputChanged(cursor_char_position) => {
self.reflow_entire_input(cursor_char_position, input_provider)
}
TerminalInputEvent::InputAppend(new_character) => {
self.reflow_new_char(new_character, input_provider)
}
TerminalInputEvent::InputMassAppend(new_data) => {
self.reflow_string(&new_data, input_provider)
}
TerminalInputEvent::CursorMoveLeft(mut char_amount) => {
let mut move_left_amount = 0_usize;
let mut move_up_amount = 0_usize;
while char_amount > 0 {
char_amount -= 1;
if self.character_at == 0 {
break;
}
self.character_at -= 1;
if self.cursor_character_position.1 == 0 {
self.cursor_character_position.0 -= 1;
self.cursor_character_position.1 =
self.line_widths[self.cursor_character_position.0].char_count;
move_up_amount += 1;
} else {
self.cursor_character_position.1 -= 1;
move_left_amount += 1;
}
}
if move_up_amount > 0 {
let mut data =
move_cursor(CursorDirection::Left, self.message_width + header_width());
data += &move_cursor(CursorDirection::Up, move_up_amount);
data += &move_cursor(
CursorDirection::Right,
header_width() + self.cursor_character_position.1,
);
data
} else {
move_cursor(CursorDirection::Left, move_left_amount)
}
}
TerminalInputEvent::CursorMoveRight(mut char_amount) => {
let mut move_down_amount = 0_usize;
let mut move_right_amount = 0_usize;
while char_amount > 0 {
char_amount -= 1;
if self.character_at > self.max_character_at {
break;
}
self.character_at += 1;
let line = &self.line_widths[self.cursor_character_position.0];
if self.cursor_character_position.1 + 1 > line.char_count {
move_down_amount += 1;
move_right_amount = 0;
self.cursor_character_position.0 += 1;
self.cursor_character_position.1 = 1;
} else {
self.cursor_character_position.1 += 1;
move_right_amount += 1;
}
}
if move_down_amount > 0 {
let mut data =
move_cursor(CursorDirection::Left, self.message_width + header_width());
data += &move_cursor(CursorDirection::Down, move_down_amount);
data += &move_cursor(
CursorDirection::Right,
header_width() + self.cursor_character_position.1,
);
data
} else {
move_cursor(CursorDirection::Right, move_right_amount)
}
}
TerminalInputEvent::ToggleOutputPause => {
force_pause.fetch_not(Ordering::AcqRel);
String::with_capacity(0)
}
}
}
fn calculate_lines(
msg_width: usize,
ps1: &str,
input: &str,
autocomplete: Option<&str>,
input_char: usize,
) -> (LineColumnType, Vec<InputLine>) {
let mut line_col = (0_usize, 0_usize);
let mut input_lines = vec![InputLine {
char_count: 0,
total_width: 0,
}];
for character in ps1.chars() {
let my_input_line = input_lines
.last_mut()
.unwrap_or_else(|| unreachable!("input lines always > 0"));
let character_width = character.width().unwrap_or_default();
if character == '\n' {
line_col.0 += 1;
line_col.1 = 1;
input_lines.push(InputLine {
char_count: 1,
total_width: 1,
});
} else if my_input_line.total_width + character_width > msg_width {
line_col.0 += 1;
line_col.1 = 1;
input_lines.push(InputLine {
char_count: 1,
total_width: character_width,
});
} else {
line_col.1 += 1;
my_input_line.char_count += 1;
my_input_line.total_width += character_width;
}
}
for (idx, character) in input.chars().enumerate() {
let my_input_line = input_lines
.last_mut()
.unwrap_or_else(|| unreachable!("input lines always > 0"));
let character_width = character.width().unwrap_or_default();
if character == '\n' {
input_lines.push(InputLine {
char_count: 1,
total_width: 1,
});
if idx < input_char {
line_col.0 += 1;
line_col.1 = 1;
}
} else if my_input_line.total_width + character_width > msg_width {
input_lines.push(InputLine {
char_count: 1,
total_width: character_width,
});
if idx < input_char {
line_col.0 += 1;
line_col.1 = 1;
}
} else {
my_input_line.char_count += 1;
my_input_line.total_width += character_width;
if idx < input_char {
line_col.1 += 1;
}
}
}
for character in autocomplete.unwrap_or_default().chars() {
let my_input_line = input_lines
.last_mut()
.unwrap_or_else(|| unreachable!("input lines always > 0"));
let character_width = character.width().unwrap_or_default();
if character == '\n' {
input_lines.push(InputLine {
char_count: 1,
total_width: 1,
});
} else if my_input_line.total_width + character_width > msg_width {
input_lines.push(InputLine {
char_count: 1,
total_width: character_width,
});
} else {
my_input_line.char_count += 1;
my_input_line.total_width += character_width;
}
}
(line_col, input_lines)
}
fn do_clear_screen(&self) -> String {
let mut buff = move_cursor(
CursorDirection::Left,
header_width() + self.message_width + self.tailer_width,
);
let observed_terminal_height =
SuperConsole::<std::io::Stdout, std::io::Stderr>::terminal_height().unwrap_or(144);
buff += &move_cursor(CursorDirection::Down, usize::from(observed_terminal_height));
for _ in 0..=observed_terminal_height {
buff += &erase_line(ClearLine::EntireLine);
buff += &move_cursor(CursorDirection::Up, 1);
}
buff
}
fn do_full_render(
message_width: usize,
tailer_width: usize,
ps1: &str,
input: &str,
cursor_at: LineColumnType,
lines: &[InputLine],
autocomplete_suggestion: Option<&str>,
) -> String {
let mut final_render = String::new();
let mut autocomplete_iterator = autocomplete_suggestion.unwrap_or_default().chars();
let mut full_input_iterator = ps1.chars().chain(input.chars()).peekable();
final_render.push_str(EMPTY_HEADER);
let mut msg = String::new();
while msg.len() < message_width {
msg.push('-');
}
final_render.push_str(&msg);
final_render.push_str(&pad_to_width("|".to_owned(), tailer_width));
let mut italic_buff: Option<String> = None;
for line in lines {
final_render.push('\n');
final_render.push_str(EMPTY_HEADER);
let mut inner_line = String::new();
for _ in 0..line.char_count {
if let Some(regular_char) = full_input_iterator.next() {
if regular_char == '\u{1b}' && full_input_iterator.peek().is_none() {
} else {
inner_line.push(if regular_char == '\n' {
' '
} else {
regular_char
});
}
} else if let Some(other_char) = autocomplete_iterator.next() {
if let Some(italic_buff_add) = italic_buff.as_mut() {
italic_buff_add.push(if other_char == '\n' { ' ' } else { other_char });
} else {
italic_buff = Some(String::from(if other_char == '\n' {
' '
} else {
other_char
}));
}
}
}
if let Some(buff) = italic_buff.take() {
_ = write!(&mut inner_line, "{}", buff.italic().bright_black());
}
final_render.push_str(&pad_to_width(inner_line, message_width));
final_render.push_str(&pad_to_width("|".to_owned(), tailer_width));
}
final_render.push_str(&move_cursor(
CursorDirection::Left,
tailer_width + (message_width - cursor_at.1) - 1,
));
final_render.push_str(&move_cursor(
CursorDirection::Up,
(lines.len() - 1) - cursor_at.0,
));
final_render
}
fn on_input_reset(&mut self) -> String {
let mut data = self.clear_current_render();
self.character_at = 0_usize;
self.max_character_at = 0_usize;
self.cursor_character_position = (0_usize, 0_usize);
self.autocomplete_suggestion = None;
self.line_widths = vec![InputLine {
char_count: 0,
total_width: 0,
}];
data += &self.render_current_standalone(None, self.message_width, self.tailer_width, "");
data
}
fn reflow_entire_input(
&mut self,
cursor_char_position: usize,
input_provider: &dyn InputProvider,
) -> String {
let mut final_input = self.clear_current_render();
let input = input_provider.current_input();
if input.len() >= 3 && !input_provider.autocomplete_suggestion_pending() {
self.autocomplete_suggestion = input_provider.current_autocomplete_suggestion();
}
if input.len() < 3 || input_provider.autocomplete_suggestion_pending() {
self.autocomplete_suggestion = None;
}
let (linecol, lines) = Self::calculate_lines(
self.message_width,
&self.ps1,
&input,
self.autocomplete_suggestion.as_deref(),
cursor_char_position,
);
self.character_at = cursor_char_position;
self.cursor_character_position = linecol;
self.line_widths = lines;
self.max_character_at = input.len();
final_input +=
&self.render_current_standalone(None, self.message_width, self.tailer_width, &input);
final_input
}
fn reflow_new_char(
&mut self,
new_character: char,
input_provider: &dyn InputProvider,
) -> String {
self.character_at += 1;
self.max_character_at += 1;
let last_line = &mut self.line_widths[self.cursor_character_position.0];
let new_char_width = new_character.width().unwrap_or_default();
if input_provider.autocomplete_suggestion_pending() {
self.autocomplete_suggestion = None;
return self.reflow_entire_input(self.character_at, input_provider);
}
if input_provider.is_doing_history_search() {
return self.reflow_entire_input(self.character_at, input_provider);
}
if self.autocomplete_suggestion.is_some() {
let sugg = self.autocomplete_suggestion.as_deref().unwrap_or_default();
if sugg.starts_with(new_character) && sugg.len() != 1 {
self.autocomplete_suggestion = self
.autocomplete_suggestion
.take()
.map(|str| str.chars().skip(1).collect::<String>());
} else {
self.autocomplete_suggestion = None;
}
return self.reflow_entire_input(self.character_at, input_provider);
} else if self.character_at + 1 >= 3
&& let Some(sugg) = input_provider.current_autocomplete_suggestion()
{
self.autocomplete_suggestion = Some(sugg);
return self.reflow_entire_input(self.character_at + 1, input_provider);
}
if new_character == '\n' {
return self.reflow_entire_input(self.character_at + 1, input_provider);
}
if last_line.total_width + new_char_width > self.message_width {
self.cursor_character_position.0 += 1;
self.cursor_character_position.1 = 1;
self.line_widths.push(InputLine {
char_count: 1,
total_width: new_char_width,
});
let mut data = move_cursor(
CursorDirection::Right,
self.message_width + self.tailer_width,
);
data.push('\n');
data.push_str(EMPTY_HEADER);
data.push_str(&pad_to_width(
if new_character == '\u{1b}' {
String::new()
} else {
String::from(new_character)
},
self.message_width,
));
data.push_str(&pad_to_width("|".to_owned(), self.tailer_width));
data.push_str(&move_cursor(
CursorDirection::Left,
self.tailer_width + (self.message_width - self.cursor_character_position.1) - 1,
));
data
} else {
self.cursor_character_position.1 += 1;
let mut data = erase_line(ClearLine::CursorToEnd);
data += &pad_to_width(
if new_character == '\u{1b}' {
String::new()
} else {
String::from(new_character)
},
self.message_width - last_line.total_width,
);
last_line.char_count += 1;
last_line.total_width += new_char_width;
data += &pad_to_width("|".to_owned(), self.tailer_width);
data += &move_cursor(
CursorDirection::Left,
self.tailer_width + (self.message_width - self.cursor_character_position.1) - 1,
);
data
}
}
fn reflow_string(&mut self, new_data: &str, input_provider: &dyn InputProvider) -> String {
let mut result = String::new();
if input_provider.autocomplete_suggestion_pending() {
self.autocomplete_suggestion = None;
return self.reflow_entire_input(self.character_at, input_provider);
}
if input_provider.is_doing_history_search() {
return self.reflow_entire_input(self.character_at, input_provider);
}
if self.autocomplete_suggestion.is_some() {
let sugg = self.autocomplete_suggestion.as_deref().unwrap_or_default();
if sugg.starts_with(new_data) && sugg.len() < new_data.len() {
self.autocomplete_suggestion = self.autocomplete_suggestion.take().map(|str| {
str.chars()
.skip(new_data.chars().count())
.collect::<String>()
});
} else {
self.autocomplete_suggestion = None;
}
return self.reflow_entire_input(self.character_at, input_provider);
} else if self.character_at + new_data.chars().count() >= 3
&& let Some(sugg) = input_provider.current_autocomplete_suggestion()
{
self.autocomplete_suggestion = Some(sugg);
return self
.reflow_entire_input(self.character_at + new_data.chars().count(), input_provider);
}
if new_data.contains('\n') {
return self
.reflow_entire_input(self.character_at + new_data.chars().count(), input_provider);
}
let mut has_erased = false;
for new_character in new_data.chars() {
self.character_at += 1;
self.max_character_at += 1;
let last_line = &mut self.line_widths[self.cursor_character_position.0];
let new_char_width = new_character.width().unwrap_or_default();
if last_line.total_width + new_char_width > self.message_width {
if has_erased {
let mut current_width = last_line.total_width;
while current_width < self.message_width {
result.push(' ');
current_width += 1;
}
result.push_str(&pad_to_width("|".to_owned(), self.tailer_width));
} else {
has_erased = true;
result.push_str(&move_cursor(
CursorDirection::Right,
self.message_width + self.tailer_width,
));
}
result.push('\n');
result.push_str(EMPTY_HEADER);
result.push(new_character);
self.cursor_character_position.0 += 1;
self.cursor_character_position.1 = 1;
self.line_widths.push(InputLine {
char_count: 1,
total_width: new_char_width,
});
} else {
if !has_erased {
result += &erase_line(ClearLine::CursorToEnd);
has_erased = true;
}
last_line.char_count += 1;
last_line.total_width += new_char_width;
result.push(new_character);
self.cursor_character_position.1 += 1;
}
}
if has_erased {
let last_line = self
.line_widths
.last_mut()
.unwrap_or_else(|| unreachable!());
let mut current_width = last_line.total_width;
while current_width < self.message_width {
result.push(' ');
current_width += 1;
}
result.push_str(&pad_to_width("|".to_owned(), self.tailer_width));
result.push_str(&move_cursor(
CursorDirection::Left,
self.tailer_width + (self.message_width - self.cursor_character_position.1) - 1,
));
}
result
}
}
#[derive(Clone, Debug, PartialEq, Eq, Valuable)]
struct InputLine {
char_count: usize,
total_width: usize,
}