Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
233 changes: 83 additions & 150 deletions Cargo.lock

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ version = "0.0.0"
clap = { version = "=4.5.60", features = ["cargo"] }
crossterm = "=0.29.0"
dirs = "=6.0.0"
is_executable = "=1.0.5"
regex = "=1.12.3"
serde = { version = "=1.0.228", features = ["derive"] }
serde_json = "=1.0.149"
time = { version = "=0.3.47", features = ["formatting", "parsing"] }
is_executable = "=1.0.6"
regex = "=1.13.1"
serde = { version = "=1.0.229", features = ["derive"] }
serde_json = "=1.0.151"
time = { version = "=0.3.55", features = ["formatting", "parsing"] }

[profile.release]
# Perform Link Time Optimization
Expand Down
7 changes: 5 additions & 2 deletions src/communication/handlers/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,10 @@ impl CommandHandler {
}

// Remove "r " from the string
let parts = command[2..].split(',');
let Some(rest) = command.get(2..) else {
return Err(LogriaError::InvalidCommand(format!("{command:?}")));
};
let parts = rest.split(',');
let mut out_l: Vec<usize> = vec![];

// Not for_each because we may need to bail early
Expand All @@ -85,7 +88,7 @@ impl CommandHandler {
(_, _) => {
return Err(LogriaError::InvalidCommand(format!(
"range invalid: {:?}",
&range
range
)));
}
}
Expand Down
78 changes: 39 additions & 39 deletions src/communication/handlers/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,51 +224,51 @@ impl ProcessorMethods for ParserHandler {
/// Parse messages, loading the buffer of parsed messages in the main window
fn process_matches(&mut self, window: &mut MainWindow) -> Result<()> {
// Only process if the parser is set up properly
if let ParserState::Full = window.config.parser_state {
if self.parser.is_some() {
let mut wrote_progress = false;
// Start from where we left off to the most recent message
let start = window.config.last_index_processed;
let end = window.previous_messages().len();

let last = end.checked_sub(1).unwrap_or(end);
for index in start..end {
if window.config.aggregation_enabled {
match self.aggregate_handle(
&window.previous_messages()[index],
&window.config.num_to_aggregate,
index == last,
) {
Ok(aggregated_messages) => {
if !aggregated_messages.is_empty() {
window.config.auxiliary_messages.clear();
window.config.auxiliary_messages.extend(aggregated_messages);
}
if let ParserState::Full = window.config.parser_state
&& self.parser.is_some()
{
let mut wrote_progress = false;
// Start from where we left off to the most recent message
let start = window.config.last_index_processed;
let end = window.previous_messages().len();

let last = end.checked_sub(1).unwrap_or(end);
for index in start..end {
if window.config.aggregation_enabled {
match self.aggregate_handle(
&window.previous_messages()[index],
&window.config.num_to_aggregate,
index == last,
) {
Ok(aggregated_messages) => {
if !aggregated_messages.is_empty() {
window.config.auxiliary_messages.clear();
window.config.auxiliary_messages.extend(aggregated_messages);
}
Err(why) => {
// If the message failed parsing, it might just be a different format, so we ignore it
// If the parser is in an invalid state, alert the user
if let LogriaError::InvalidParserState(error) = why {
window.write_to_command_line(&error)?;
}
}
Err(why) => {
// If the message failed parsing, it might just be a different format, so we ignore it
// If the parser is in an invalid state, alert the user
if let LogriaError::InvalidParserState(error) = why {
window.write_to_command_line(&error)?;
}
}
} else if let Ok(Some(message)) = self.parse(
window.config.parser_index,
&window.previous_messages()[index],
) {
window.config.auxiliary_messages.push(message);
}
} else if let Ok(Some(message)) = self.parse(
window.config.parser_index,
&window.previous_messages()[index],
) {
window.config.auxiliary_messages.push(message);
}

// Update the user interface with the current state
wrote_progress = update_progress(window, start, end, index)?;
// Update the user interface with the current state
wrote_progress = update_progress(window, start, end, index)?;

// Update the last spot so we know where to start next time
window.config.last_index_processed = index + 1;
}
if wrote_progress {
window.write_status()?;
}
// Update the last spot so we know where to start next time
window.config.last_index_processed = index + 1;
}
if wrote_progress {
window.write_status()?;
}
}
Ok(())
Expand Down
2 changes: 1 addition & 1 deletion src/communication/handlers/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ pub fn update_progress(
index: usize,
) -> Result<bool> {
// Update the user interface with the current state
if end - start > THRESHOLD && (index % STEP == 0 || index == end - 1) {
if end - start > THRESHOLD && (index.is_multiple_of(STEP) || index == end - 1) {
let word = if index == end - 1 {
"Processed"
} else {
Expand Down
46 changes: 36 additions & 10 deletions src/communication/handlers/user_input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,16 @@ impl UserInputHandler {
(self.last_write - 1) as usize
}

/// Index of the char at the cursor, `None` when the cursor sits past the end of the content
fn index_at_cursor(&self) -> Option<usize> {
let index = self.position_as_index();
(index < self.content.len()).then_some(index)
}

/// Remove char 1 to the left of the cursor
fn backspace(&mut self, window: &mut MainWindow) -> Result<()> {
if self.last_write >= 1 && !self.content.is_empty() {
self.content
.remove(self.position_as_index().saturating_sub(1));
if self.last_write > 1 && !self.content.is_empty() {
self.content.remove(self.position_as_index() - 1);
self.move_left()?;
self.write(window)?;
}
Expand All @@ -97,8 +102,8 @@ impl UserInputHandler {

/// Remove char 1 to the right of the cursor
fn delete(&mut self, window: &mut MainWindow) -> Result<()> {
if self.last_write < self.x() && !self.content.is_empty() {
self.content.remove(self.position_as_index());
if let Some(index) = self.index_at_cursor() {
self.content.remove(index);
self.write(window)?;
}
Ok(())
Expand All @@ -121,21 +126,23 @@ impl UserInputHandler {

/// Get the next item in the history tape if it exists
fn tape_forward(&mut self, window: &mut MainWindow) -> Result<()> {
let content = self.history.scroll_forward();
self.tape_render(window, &content)?;
if let Some(content) = self.history.scroll_forward() {
self.tape_render(window, &content)?;
}
Ok(())
}

/// Get the previous item in the history tape if it exists
fn tape_back(&mut self, window: &mut MainWindow) -> Result<()> {
let content = self.history.scroll_back();
self.tape_render(window, &content)?;
if let Some(content) = self.history.scroll_back() {
self.tape_render(window, &content)?;
}
Ok(())
}

/// Render the new choice
fn tape_render(&mut self, window: &mut MainWindow, content: &str) -> Result<()> {
self.last_write = content.len() as u16 + 1;
self.last_write = content.chars().count() as u16 + 1;
window.write_to_command_line(content)?;
self.content = content.chars().collect();
queue!(
Expand Down Expand Up @@ -206,3 +213,22 @@ impl Handler for UserInputHandler {
Ok(())
}
}

#[cfg(test)]
mod tests {
use crate::communication::handlers::{handler::Handler, user_input::UserInputHandler};

#[test]
fn cursor_index_bounded_by_content() {
let mut handler = UserInputHandler::new();

// "abc" with the cursor past the end: the state that crashed forward-delete
handler.content = vec!['a', 'b', 'c'];
handler.last_write = 4;
assert_eq!(handler.index_at_cursor(), None);

// Cursor between 'a' and 'b': forward-delete removes 'b'
handler.last_write = 2;
assert_eq!(handler.index_at_cursor(), Some(1));
}
}
24 changes: 12 additions & 12 deletions src/communication/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,19 +246,19 @@ pub fn build_streams_from_input(
match determine_stream_type(command) {
SessionType::Command => {
// None indicates default poll rate
match CommandInput::build(command.to_owned(), command.to_owned()) {
Ok(stream) => streams.push(stream),
Err(why) => return Err(why),
{
let stream = CommandInput::build(command.to_owned(), command.to_owned())?;
streams.push(stream)
}
stream_types.insert(SessionType::Command);
}
SessionType::File => {
// None indicates default poll rate
let path = Path::new(command);
let name = path.file_name().unwrap().to_str().unwrap().to_string();
match FileInput::build(name, command.to_owned()) {
Ok(stream) => streams.push(stream),
Err(why) => return Err(why),
{
let stream = FileInput::build(name, command.to_owned())?;
streams.push(stream)
}
stream_types.insert(SessionType::File);
}
Expand Down Expand Up @@ -292,19 +292,19 @@ pub fn build_streams_from_session(session: Session) -> Result<Vec<InputStream>,
SessionType::Command => {
let mut streams: Vec<InputStream> = vec![];
for command in session.commands {
match CommandInput::build(command.clone(), command.clone()) {
Ok(stream) => streams.push(stream),
Err(why) => return Err(why),
{
let stream = CommandInput::build(command.clone(), command.clone())?;
streams.push(stream)
}
}
Ok(streams)
}
SessionType::File => {
let mut streams: Vec<InputStream> = vec![];
for command in session.commands {
match FileInput::build(command.clone(), command.clone()) {
Ok(stream) => streams.push(stream),
Err(why) => return Err(why),
{
let stream = FileInput::build(command.clone(), command.clone())?;
streams.push(stream)
}
}
Ok(streams)
Expand Down
41 changes: 31 additions & 10 deletions src/communication/reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ impl MainWindow {

/// Determine the start and end indexes we need to render in the window
pub fn determine_render_position(&mut self) -> (usize, usize) {
let mut end: usize = 0;
let end: usize;
let mut rows: usize = 0;
let message_pointer_length = self.number_of_messages();

Expand Down Expand Up @@ -341,10 +341,9 @@ impl MainWindow {
// If we have over-scrolled, go back
if self.config.current_end > message_pointer_length {
self.config.current_end = message_pointer_length;
} else {
// Since current_end can be zero, we have to use the number of messages
end = message_pointer_length;
}
// Since current_end can be zero, we have to use the number of messages
end = message_pointer_length;
}
}
ScrollState::Centered => {
Expand Down Expand Up @@ -445,7 +444,8 @@ impl MainWindow {
let clean_message = ANSI_COLOR_REGEX.replace_all(message.as_bytes(), "".as_bytes());

// Store some vectors of char bytes so we don't have to cast to a string every loop
let mut new_msg: Vec<u8> = vec![];
// Pre-allocate with room for the message plus color escape codes
let mut new_msg: Vec<u8> = Vec::with_capacity(clean_message.len() + 64);
let mut last_end = 0;

// Replace matched patterns with highlighted matched patterns
Expand Down Expand Up @@ -477,7 +477,10 @@ impl MainWindow {
let clean_message = ANSI_COLOR_REGEX.replace_all(message.as_bytes(), "".as_bytes());

// Store some vectors of char bytes so we don't have to cast to a string every loop
let mut new_msg: Vec<u8> = vec![];
// Pre-allocate with room for the message plus color escape codes
let mut new_msg: Vec<u8> = Vec::with_capacity(
clean_message.len() + colors::HIGHLIGHT_COLOR.len() + colors::RESET_COLOR.len(),
);
new_msg.extend_from_slice(colors::HIGHLIGHT_COLOR.as_bytes());
new_msg.extend_from_slice(&clean_message);
new_msg.extend_from_slice(colors::RESET_COLOR.as_bytes());
Expand Down Expand Up @@ -548,6 +551,9 @@ impl MainWindow {
// Cast to usize so we can reference this instead of casting every time we need
let width = self.config.width as usize;

// Reusable padding buffer: grows as needed, sliced to exact size each iteration
let mut padding_buf = String::new();

// Render each message from bottom to top
for index in (start..end).rev() {
// Get the next message from the message pointer
Expand All @@ -568,7 +574,12 @@ impl MainWindow {

// See method docs for note on why we need this padding
let message_padding_size = (width * message_rows) - message_length;
let padding = " ".repeat(message_padding_size);
if padding_buf.len() < message_padding_size {
padding_buf.extend(std::iter::repeat_n(
' ',
message_padding_size - padding_buf.len(),
));
}

let msg: Cow<str> =
if self.config.highlight_match && self.config.regex_pattern.is_some() {
Expand All @@ -589,7 +600,7 @@ impl MainWindow {
stdout,
cursor::MoveTo(0, current_row),
style::Print(&msg),
style::Print(padding)
style::Print(&padding_buf[..message_padding_size])
)?;
}

Expand Down Expand Up @@ -791,6 +802,11 @@ impl MainWindow {
pub fn start(&mut self, commands: Option<Vec<String>>) -> Result<()> {
self.validate_environment();

let previous_hook = panic::take_hook();
panic::set_hook(Box::new(move |info| {
MainWindow::restore_terminal();
previous_hook(info);
}));
// Build the app
if let Some(c) = commands {
// Build streams from the command used to launch Logria
Expand Down Expand Up @@ -827,10 +843,15 @@ impl MainWindow {
Ok(())
}

/// Restore terminal state
pub fn restore_terminal() {
let _ = execute!(stdout(), cursor::Show, Clear(ClearType::All));
let _ = disable_raw_mode();
}

/// Immediately exit the program
pub fn quit(&mut self) -> Result<()> {
execute!(stdout(), cursor::Show, Clear(ClearType::All))?;
disable_raw_mode()?;
Self::restore_terminal();
for stream in &self.config.streams {
stream.should_die.store(true, Ordering::Relaxed);
}
Expand Down
2 changes: 1 addition & 1 deletion src/constants/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ pub mod patterns {

use regex::bytes::Regex;

pub const ANSI_COLOR_PATTERN: &str = r"(?-u)(\x9b|\x1b\[)[0-?]*[ -/]*[@-~]";
pub const ANSI_COLOR_PATTERN: &str = r"(?-u)\x1b\[[0-?]*[ -/]*[@-~]";

pub static ANSI_COLOR_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(ANSI_COLOR_PATTERN).unwrap());
Expand Down
Loading
Loading