diff --git a/Cargo.toml b/Cargo.toml index 5db8c09..591b4ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ HexPatch is a binary patcher and editor with terminal user interface (TUI), it's capable of disassembling instructions and assembling patches. It supports a variety of architectures and file formats. Also, it can edit remote files via SSH.""" -version = "1.7.0" +version = "1.7.1" authors = ["Ettore Ricci"] edition = "2021" readme = "docs/README.md" diff --git a/docs/PLUGIN_API.md b/docs/PLUGIN_API.md index 59f0464..83c4272 100644 --- a/docs/PLUGIN_API.md +++ b/docs/PLUGIN_API.md @@ -210,7 +210,7 @@ For more information on the types, see the following sections. This type contains the settings of the application. A setting can be accessed using the `.` operator with its full name (dots are replaced with underscores). -e.g. `context.settings.color_address_selected` or `context.settings.key_up`. +e.g. `context.settings.color_address_selected`, `context.settings.key_up`, `context.settings.app_history_limit`. WARNING: You should get and set the setting altogether, e.g. `context.settings.color_address_selected = {fg = "Red"}`. Trying to set a single field will not work. diff --git a/src/app/app.rs b/src/app/app.rs index 4bc2126..1aa41d4 100644 --- a/src/app/app.rs +++ b/src/app/app.rs @@ -99,7 +99,7 @@ impl App { args: Args, terminal: &mut ratatui::Terminal, ) -> Result { - let mut logger = Logger::new(); + let mut logger = Logger::default(); let settings = match Settings::load_or_create(args.config.as_deref()) { Ok(settings) => settings, Err(e) => { @@ -110,6 +110,7 @@ impl App { Settings::default() } }; + logger.change_limit(settings.app.log_limit); Self::print_loading_status( &settings.color, &format!("Opening \"{}\"...", args.path), @@ -329,7 +330,7 @@ impl Default for App { plugin_manager: PluginManager::default(), filesystem: FileSystem::default(), header: Header::None, - logger: Logger::new(), + logger: Logger::default(), help_list: Self::help_list(&Settings::default().key), data: Data::default(), assembly_offsets: Vec::new(), diff --git a/src/app/data.rs b/src/app/data.rs index c2e7fb7..511ef3a 100644 --- a/src/app/data.rs +++ b/src/app/data.rs @@ -10,10 +10,10 @@ pub struct Data { } impl Data { - pub fn new(bytes: Vec) -> Self { + pub fn new(bytes: Vec, history_limit: usize) -> Self { Self { bytes, - history: History::default(), + history: History::with_limit(history_limit), dirty: false, } } @@ -109,7 +109,7 @@ mod tests { #[test] fn test_data_push_change() { - let mut data = Data::new(vec![0, 1, 2, 3, 4]); + let mut data = Data::new(vec![0, 1, 2, 3, 4], 0); assert_eq!(data.push_change(2, vec![9, 8, 7]), 3); assert_eq!(data.bytes(), &[0, 1, 9, 8, 7]); assert_eq!(data.push_change(2, vec![9, 8, 7]), 0); @@ -133,13 +133,13 @@ mod tests { #[test] #[should_panic] fn test_data_push_change_out_of_bounds() { - let mut data = Data::new(vec![0, 1, 2, 3, 4]); + let mut data = Data::new(vec![0, 1, 2, 3, 4], 0); data.push_change(5, vec![9, 8, 7]); } #[test] fn test_data_undo_redo() { - let mut data = Data::new(vec![0, 1, 2, 3, 4]); + let mut data = Data::new(vec![0, 1, 2, 3, 4], 0); data.push_change(2, vec![9, 8, 7]); assert_eq!(data.bytes(), &[0, 1, 9, 8, 7]); data.push_change(0, vec![9, 8]); @@ -158,7 +158,7 @@ mod tests { #[test] fn test_data_clear_history() { - let mut data = Data::new(vec![0, 1, 2, 3, 4]); + let mut data = Data::new(vec![0, 1, 2, 3, 4], 0); data.push_change(2, vec![9, 8, 7]); data.push_change(0, vec![9, 8]); data.push_change(4, vec![9]); diff --git a/src/app/files/files.rs b/src/app/files/files.rs index f2e5802..0de277b 100644 --- a/src/app/files/files.rs +++ b/src/app/files/files.rs @@ -189,7 +189,10 @@ impl App { } else { None }; - self.data = Data::new(self.filesystem.read(self.filesystem.pwd())?); + self.data = Data::new( + self.filesystem.read(self.filesystem.pwd())?, + self.settings.app.history_limit, + ); terminal = if let Some(terminal) = terminal { Self::print_loading_status(&self.settings.color, "Decoding binary data...", terminal)?; diff --git a/src/app/history/change.rs b/src/app/history/change.rs index 45fd72d..40a1fc1 100644 --- a/src/app/history/change.rs +++ b/src/app/history/change.rs @@ -1,4 +1,4 @@ -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Change { offset: usize, old: Vec, diff --git a/src/app/history/history.rs b/src/app/history/history.rs index 22ccf94..5527613 100644 --- a/src/app/history/history.rs +++ b/src/app/history/history.rs @@ -1,16 +1,32 @@ #![allow(clippy::module_inception)] +use std::collections::VecDeque; + use super::change::Change; #[derive(Debug, Clone, Default)] pub struct History { - changes: Vec, + limit: usize, + changes: VecDeque, current: usize, } impl History { + /// If limit is 0, there is no limit. + pub fn with_limit(limit: usize) -> Self { + Self { + limit, + changes: VecDeque::with_capacity(limit), + current: 0, + } + } + pub fn push(&mut self, change: Change) { self.changes.truncate(self.current); - self.changes.push(change); + if self.changes.len() >= self.limit && self.limit > 0 { + self.changes.remove(0); + self.current = self.current.saturating_sub(1); + } + self.changes.push_back(change); self.current += 1; } @@ -38,8 +54,63 @@ impl History { } } + pub fn change_limit(&mut self, limit: usize) { + self.limit = limit; + if self.changes.len() > limit && limit > 0 { + self.changes.drain(0..self.changes.len() - limit); + self.current = limit; + } + if let Some(additional) = limit.checked_sub(self.changes.capacity()) { + self.changes.reserve(additional); + } else { + self.changes.shrink_to_fit(); + } + } + pub fn clear(&mut self) { self.changes.clear(); self.current = 0; } } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_history_with_limit() { + let mut history = History::with_limit(2); + assert_eq!(history.limit, 2); + assert_eq!(history.changes.capacity(), 2); + + history.push(Change::new(0, &[0], &[1])); + assert_eq!(history.current, 1); + history.push(Change::new(0, &[1], &[0])); + assert_eq!(history.current, 2); + history.push(Change::new(0, &[0], &[1])); + assert_eq!(history.current, 2); + assert_eq!(history.changes.len(), 2); + + history.undo(&mut vec![0]); + assert_eq!(history.current, 1); + history.undo(&mut vec![0]); + assert_eq!(history.current, 0); + assert!(history.undo(&mut vec![0]).is_none()); + assert_eq!(history.current, 0); + } + + #[test] + fn test_history_change_limit() { + let mut history = History::with_limit(2); + history.push(Change::new(0, &[0], &[1])); + history.push(Change::new(0, &[1], &[2])); + history.push(Change::new(0, &[2], &[3])); + assert_eq!(history.changes.len(), 2); + assert_eq!(history.current, 2); + + history.change_limit(1); + assert_eq!(history.changes.len(), 1); + assert_eq!(history.current, 1); + assert_eq!(history.changes[0], Change::new(0, &[2], &[3])); + } +} diff --git a/src/app/log/logger.rs b/src/app/log/logger.rs index a89d4bf..cd90df7 100644 --- a/src/app/log/logger.rs +++ b/src/app/log/logger.rs @@ -1,23 +1,31 @@ -use std::ops::Index; +use std::{collections::VecDeque, ops::Index}; use crate::app::App; use super::{log_line::LogLine, notification::NotificationLevel}; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct Logger { - pub(super) log: Vec, + pub(super) limit: usize, + pub(super) log: VecDeque, pub(super) notification: NotificationLevel, } impl Logger { - pub fn new() -> Self { - Self::default() + pub fn with_limit(limit: usize) -> Self { + Self { + limit, + log: VecDeque::with_capacity(limit), + notification: NotificationLevel::None, + } } pub fn log(&mut self, level: NotificationLevel, message: &str) { self.notification.bump_notification_level(level); - self.log.push(LogLine::new(level, message.to_string())); + if self.log.len() >= self.limit && self.limit > 0 { + self.log.pop_front(); + } + self.log.push_back(LogLine::new(level, message.to_string())); } pub fn clear(&mut self) { @@ -45,24 +53,30 @@ impl Logger { self.notification.reset(); } + pub fn change_limit(&mut self, limit: usize) { + self.limit = limit; + if self.log.len() > limit && limit > 0 { + self.log.drain(0..self.log.len() - limit); + } + if let Some(additional) = limit.checked_sub(self.log.capacity()) { + self.log.reserve(additional); + } else { + self.log.shrink_to_fit(); + } + } + pub fn merge(&mut self, other: &Self) { for log_line in &other.log { - self.log.push(log_line.clone()); + if self.log.len() >= self.limit && self.limit > 0 { + self.log.pop_front(); + } + self.log.push_back(log_line.clone()); } self.notification .bump_notification_level(other.notification); } } -impl Default for Logger { - fn default() -> Self { - Self { - log: Vec::new(), - notification: NotificationLevel::None, - } - } -} - impl Index for Logger { type Output = LogLine; @@ -83,7 +97,7 @@ mod test { #[test] fn test_logger() { - let mut logger = Logger::new(); + let mut logger = Logger::default(); assert_eq!(logger.len(), 0); assert!(logger.is_empty()); logger.log(NotificationLevel::Error, "Test error message"); @@ -98,8 +112,8 @@ mod test { #[test] fn test_logger_merge() { - let mut logger1 = Logger::new(); - let mut logger2 = Logger::new(); + let mut logger1 = Logger::default(); + let mut logger2 = Logger::default(); logger1.log(NotificationLevel::Error, "Test error message"); logger2.log(NotificationLevel::Warning, "Test warning message"); logger1.merge(&logger2); @@ -110,4 +124,36 @@ mod test { assert_eq!(logger1[1].message, "Test warning message"); assert_eq!(logger1.get_notification_level(), NotificationLevel::Error); } + + #[test] + fn test_logger_with_limit() { + let mut logger = Logger::with_limit(5); + for i in 0..10 { + logger.log( + NotificationLevel::Error, + &format!("Test error message {}", i), + ); + } + assert_eq!(logger.len(), 5); + assert_eq!(logger[0].message, "Test error message 5"); + assert_eq!(logger[4].message, "Test error message 9"); + } + + #[test] + fn test_logger_change_limit() { + let mut logger = Logger::with_limit(2); + logger.log(NotificationLevel::Error, "Test error message 1"); + logger.log(NotificationLevel::Error, "Test error message 2"); + logger.log(NotificationLevel::Error, "Test error message 3"); + assert_eq!(logger.len(), 2); + assert_eq!(logger[0].message, "Test error message 2"); + assert_eq!(logger[1].message, "Test error message 3"); + logger.change_limit(3); + assert_eq!(logger.len(), 2); + assert_eq!(logger[0].message, "Test error message 2"); + assert_eq!(logger[1].message, "Test error message 3"); + logger.change_limit(1); + assert_eq!(logger.len(), 1); + assert_eq!(logger[0].message, "Test error message 3"); + } } diff --git a/src/app/log/notification.rs b/src/app/log/notification.rs index 5acb4b7..54fae65 100644 --- a/src/app/log/notification.rs +++ b/src/app/log/notification.rs @@ -1,7 +1,8 @@ use std::fmt::{Display, Formatter}; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum NotificationLevel { + #[default] None, Debug, Info, diff --git a/src/app/plugins/register_userdata.rs b/src/app/plugins/register_userdata.rs index fe541da..f6a426f 100644 --- a/src/app/plugins/register_userdata.rs +++ b/src/app/plugins/register_userdata.rs @@ -2,8 +2,8 @@ use mlua::{FromLua, IntoLua, Lua, UserDataFields, UserDataMethods}; use ratatui::text::Text; use crate::app::settings::{ - color_settings::ColorSettings, key_settings::KeySettings, settings_value::SettingsValue, - Settings, + app_settings::AppSettings, color_settings::ColorSettings, key_settings::KeySettings, + settings_value::SettingsValue, Settings, }; pub fn register_vec_u8(lua: &Lua) -> mlua::Result<()> { @@ -70,6 +70,7 @@ pub fn register_settings(lua: &Lua) -> mlua::Result<()> { lua.register_userdata_type(|data: &mut mlua::UserDataRegistry| { ColorSettings::register_userdata(data); KeySettings::register_userdata(data); + AppSettings::register_userdata(data); data.add_method("get_custom", |_lua, this, key: String| { Ok(this.custom.get(&key).cloned()) }); diff --git a/src/app/settings/app_settings.rs b/src/app/settings/app_settings.rs new file mode 100644 index 0000000..7d6de9a --- /dev/null +++ b/src/app/settings/app_settings.rs @@ -0,0 +1,47 @@ +use mlua::UserDataRegistry; +use serde::{Deserialize, Serialize}; + +use super::Settings; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(default)] +pub struct AppSettings { + pub history_limit: usize, + pub log_limit: usize, +} + +impl AppSettings { + pub fn register_userdata(data: &mut UserDataRegistry) { + mlua::UserDataFields::add_field_method_get(data, "app_history_limit", |_lua, settings| { + Ok(settings.app.history_limit) + }); + mlua::UserDataFields::add_field_method_set( + data, + "app_history_limit", + |_lua, settings, value| { + settings.app.history_limit = value; + Ok(()) + }, + ); + mlua::UserDataFields::add_field_method_get(data, "app_log_limit", |_lua, settings| { + Ok(settings.app.log_limit) + }); + mlua::UserDataFields::add_field_method_set( + data, + "app_log_limit", + |_lua, settings, value| { + settings.app.log_limit = value; + Ok(()) + }, + ); + } +} + +impl Default for AppSettings { + fn default() -> Self { + Self { + history_limit: 1024, + log_limit: 1024, + } + } +} diff --git a/src/app/settings/mod.rs b/src/app/settings/mod.rs index efe3b42..f771c2b 100644 --- a/src/app/settings/mod.rs +++ b/src/app/settings/mod.rs @@ -1,6 +1,7 @@ mod settings; pub use settings::Settings; +pub mod app_settings; pub mod color_settings; pub mod key_settings; #[macro_use] diff --git a/src/app/settings/settings.rs b/src/app/settings/settings.rs index d2928be..1227d00 100644 --- a/src/app/settings/settings.rs +++ b/src/app/settings/settings.rs @@ -6,7 +6,8 @@ use std::{ }; use super::{ - color_settings::ColorSettings, key_settings::KeySettings, settings_value::SettingsValue, + app_settings::AppSettings, color_settings::ColorSettings, key_settings::KeySettings, + settings_value::SettingsValue, }; #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Default)] @@ -14,6 +15,7 @@ use super::{ pub struct Settings { pub color: ColorSettings, pub key: KeySettings, + pub app: AppSettings, pub custom: HashMap, } diff --git a/test/default_settings.json b/test/default_settings.json index ac965e3..1497113 100644 --- a/test/default_settings.json +++ b/test/default_settings.json @@ -586,5 +586,9 @@ "state": "" } }, + "app": { + "history_limit": 1024, + "log_limit": 1024 + }, "custom": {} } \ No newline at end of file