diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore index b5143ed..bbf2a79 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,7 @@ # Dotenv files **/.env -**/.idea \ No newline at end of file +**/.idea + +# No Nix-Direnv +./.direnv diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..f52c5ed --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1771848320, + "narHash": "sha256-0MAd+0mun3K/Ns8JATeHT1sX28faLII5hVLq0L3BdZU=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "2fc6539b481e1d2569f25f8799236694180c0993", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..cbcb759 --- /dev/null +++ b/flake.nix @@ -0,0 +1,45 @@ +{ + description = "Rust-Flake by blckr"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + }; + + outputs = + { nixpkgs, ... }: + let + supportedSystems = [ + "aarch64-linux" + "x86_64-linux" + "aarch64-darwin" + "x86_64-darwin" + ]; + forAllSystems = nixpkgs.lib.genAttrs supportedSystems; + in + { + devShells = forAllSystems ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + in + { + default = pkgs.mkShellNoCC { + buildInputs = with pkgs; [ + rustc + cargo + gcc + rust-analyzer + rustfmt + clippy + gdb + openssl + openssl.dev + ]; + nativeBuildInputs = with pkgs; [ + pkg-config + ]; + }; + } + ); + }; +} diff --git a/src/core/parameters.rs b/src/core/parameters.rs index 6e4227e..bc62949 100644 --- a/src/core/parameters.rs +++ b/src/core/parameters.rs @@ -1,29 +1,9 @@ -use regex::Regex; - use crate::core::HoardCmd; use crate::gui::prompts::prompt_input; pub trait Parameterized { - /// Checks if the command string contains a specific token. - /// - /// This function takes a token and checks if the command string contains this token. - /// - /// # Arguments - /// - /// * `token` - A string slice that holds the token to be checked. - /// - /// # Returns - /// - /// This function returns a boolean. It returns true if the command string contains the token, - /// and false otherwise. - /// - /// # Example - /// - /// ``` - /// let command = HoardCmd::default()::with_command("echo $"); - /// assert!(command.is_parameterized("$")); - /// ``` - + fn escape_input(input: &str, start_token: &str, end_token: &str) -> String; + fn cleanup_escapes(&self, start_token: &str, end_token: &str) -> HoardCmd; fn is_parameterized(&self, token: &str) -> bool; /// Counts the number of occurrences of a specific token in the command string. /// @@ -140,12 +120,110 @@ impl Parameterized for HoardCmd { fn is_parameterized(&self, token: &str) -> bool { self.command.contains(token) } + // Escapet einen String so, dass get_parameter_count ihn komplett ignoriert + fn escape_input(input: &str, start_token: &str, end_token: &str) -> String { + let mut escaped = String::with_capacity(input.len() * 2); + let mut i = 0; + + while i < input.len() { + if input.as_bytes()[i] == b'\\' { + escaped.push_str("\\\\"); + i += 1; + continue; + } + + if input[i..].starts_with(start_token) { + escaped.push('\\'); + escaped.push_str(start_token); + i += start_token.len(); + continue; + } + + if !end_token.is_empty() && input[i..].starts_with(end_token) { + escaped.push('\\'); + escaped.push_str(end_token); + i += end_token.len(); + continue; + } + + let c = input[i..].chars().next().unwrap(); + escaped.push(c); + i += c.len_utf8(); + } + + escaped + } + + fn cleanup_escapes(&self, start_token: &str, end_token: &str) -> HoardCmd { + let s = &self.command; + let mut out = String::with_capacity(s.len()); + let mut i = 0; + + while i < s.len() { + if s.as_bytes()[i] == b'\\' { + i += 1; + if i < s.len() { + if s[i..].starts_with(start_token) { + out.push_str(start_token); + i += start_token.len(); + } else if s[i..].starts_with(end_token) && !end_token.is_empty() { + out.push_str(end_token); + i += end_token.len(); + } else if s.as_bytes()[i] == b'\\' { + out.push('\\'); + i += 1; + } else { + let c = s[i..].chars().next().unwrap(); + out.push(c); + i += c.len_utf8(); + } + } + continue; + } + + let c = s[i..].chars().next().unwrap(); + out.push(c); + i += c.len_utf8(); + } + + Self::default().with_command(&out) + } + fn get_parameter_count(&self, token: &str) -> usize { - self.command.matches(token).count() + let s = &self.command; + let mut count = 0; + let mut i = 0; + + while i < s.len() { + if s.as_bytes()[i] == b'\\' { + i += 1; + if i < s.len() { + if s[i..].starts_with(token) { + i += token.len(); + } else { + let c = s[i..].chars().next().unwrap(); + i += c.len_utf8(); + } + } + continue; + } + + if s[i..].starts_with(token) { + count += 1; + i += token.len(); + continue; + } + + let c = s[i..].chars().next().unwrap(); + i += c.len_utf8(); + } + count } + fn split(&self, token: &str) -> Vec { self.command.split(token).map(ToString::to_string).collect() } + fn split_inclusive_token(&self, token: &str) -> Vec { let split = self.split(token); let mut collected: Vec = Vec::new(); @@ -162,31 +240,139 @@ impl Parameterized for HoardCmd { } fn replace_parameter(&self, start_token: &str, end_token: &str, value: &str) -> Self { - let pattern = format!( - "{}.*?{}", - regex::escape(start_token), - regex::escape(end_token) - ); - let re = Regex::new(&pattern).unwrap(); - let replaced = re.replace_all(&self.command, value); - Self::default().with_command(&replaced) + let s = &self.command; + let mut out = String::with_capacity(s.len()); + let mut i = 0; + let mut replaced = false; + + while i < s.len() { + if s.as_bytes()[i] == b'\\' { + out.push('\\'); // Keep Backslash for final cleanup + i += 1; + if i < s.len() { + if s[i..].starts_with(start_token) { + out.push_str(start_token); + i += start_token.len(); + } else if s[i..].starts_with(end_token) && !end_token.is_empty() { + out.push_str(end_token); + i += end_token.len(); + } else { + let c = s[i..].chars().next().unwrap(); + out.push(c); + i += c.len_utf8(); + } + } + continue; + } + + if !replaced && s[i..].starts_with(start_token) { + let param_content_start = i + start_token.len(); + + let mut search_idx = param_content_start; + let mut found_end = None; + + while search_idx < s.len() { + if s.as_bytes()[search_idx] == b'\\' { + search_idx += 1; + if search_idx < s.len() { + let c = s[search_idx..].chars().next().unwrap(); + search_idx += c.len_utf8(); + } + continue; + } + + if !end_token.is_empty() && s[search_idx..].starts_with(end_token) { + found_end = Some(search_idx); + break; + } + + if s[search_idx..].starts_with(start_token) { + break; + } + + let c = s[search_idx..].chars().next().unwrap(); + search_idx += c.len_utf8(); + } + + if let Some(end_idx) = found_end { + out.push_str(value); + i = end_idx + end_token.len(); + replaced = true; + continue; + } else { + out.push_str(value); + i += start_token.len(); + replaced = true; + continue; + } + } + + let c = s[i..].chars().next().unwrap(); + out.push(c); + i += c.len_utf8(); + } + + Self::default().with_command(&out) } fn with_input_parameters(&mut self, token: &str, ending_token: &str) -> Self { + let s = &self.command; + let mut out = String::with_capacity(s.len()); + let mut i = 0; let mut param_count = 0; - while self.get_parameter_count(token) != 0 { - let prompt_dialog = format!( - "Enter parameter({}) nr {} \n~> {}\n", - token, - (param_count + 1), - self.command - ); - let parameter = prompt_input(&prompt_dialog, false, None); - self.command = self - .replace_parameter(token, ending_token, ¶meter) - .command; - param_count += 1; + + while i < s.len() { + if s.as_bytes()[i] == b'\\' { + if i + 1 < s.len() { + let next_pos = i + 1; + + if s[next_pos..].starts_with(token) { + out.push_str(token); + i = next_pos + token.len(); + continue; + } + + if s.as_bytes()[next_pos] == b'\\' { + out.push('\\'); + i = next_pos + 1; + continue; + } + } + out.push('\\'); + i += 1; + continue; + } + + if s[i..].starts_with(token) { + param_count += 1; + let param_content_start = i + token.len(); + + let current_preview = format!("{}{}[...]", out, &s[i..]); + + let prompt_dialog = format!( + "Enter parameter({}) nr {}\n~> {}\n", + token, param_count, current_preview + ); + + let user_input = prompt_input(&prompt_dialog, false, None); + + if let Some(end_offset) = s[param_content_start..].find(ending_token) { + out.push_str(&user_input); + i = param_content_start + end_offset + ending_token.len(); + continue; + } else { + out.push_str(&user_input); + i += token.len(); + continue; + } + } + + let c = s[i..].chars().next().unwrap(); + out.push(c); + i += c.len_utf8(); } + + self.command = out; self.clone() } } @@ -268,4 +454,37 @@ mod test_commands { let expected = HoardCmd::default().with_command("test1replacementtest3"); assert_eq!(expected, command.replace_parameter("#", "!", "replacement")); } + + #[test] + fn test_escape_double_backslash_before_token() { + let command = HoardCmd::default().with_command("wewantto\\\\#escape"); + // Backslash-Cleanup happens later + let expected = HoardCmd::default().with_command("wewantto\\\\replacementescape"); + assert_eq!(expected, command.replace_parameter("#", "!", "replacement")); + } + #[test] + fn test_escape_single_backslash_before_token() { + let command = HoardCmd::default().with_command("wewantto\\#escape"); + let expected = HoardCmd::default().with_command("wewantto\\#escape"); + assert_eq!(expected, command.replace_parameter("#", "!", "replacement")); + } + #[test] + fn test_escape_no_backslash_before_token() { + let command = HoardCmd::default().with_command("wewantto#!escape"); + let expected = HoardCmd::default().with_command("wewanttoreplacementescape"); + assert_eq!(expected, command.replace_parameter("#", "!", "replacement")); + } + #[test] + fn test_escape_backslash_before_token_with_end() { + let command = HoardCmd::default().with_command("wewantto\\##!escape"); + let expected = HoardCmd::default().with_command("wewantto\\#replacementescape"); + assert_eq!(expected, command.replace_parameter("#", "!", "replacement")); + } + #[test] + fn test_escape_backslash_before_multiple_token_with_end() { + let command = HoardCmd::default().with_command("wewantto\\##!escape##"); + // Only the first gets replaced in a single iteration + let expected = HoardCmd::default().with_command("wewantto\\#replacementescape##"); + assert_eq!(expected, command.replace_parameter("#", "!", "replacement")); + } } diff --git a/src/gui/parameter_input/controls.rs b/src/gui/parameter_input/controls.rs index 08d916a..0cc36a9 100644 --- a/src/gui/parameter_input/controls.rs +++ b/src/gui/parameter_input/controls.rs @@ -12,20 +12,40 @@ pub fn key_handler(input: Key, app: &mut State) -> Option { } Key::Char('\n') => { let command = app.selected_command.clone().unwrap(); - let parameter = app.input.clone(); + + let mut safe_parameter = app.input.clone(); + safe_parameter = safe_parameter.replace(&app.parameter_token, "\u{E000}"); + if !app.parameter_ending_token.is_empty() { + safe_parameter = safe_parameter.replace(&app.parameter_ending_token, "\u{E001}"); + } + let replaced_command = command.replace_parameter( &app.parameter_token, &app.parameter_ending_token, - ¶meter, + &safe_parameter, ); + app.input = String::new(); + if replaced_command.get_parameter_count(&app.parameter_token) == 0 { - return Some(replaced_command); + let mut final_command = replaced_command + .cleanup_escapes(&app.parameter_token, &app.parameter_ending_token); + + let mut restored_cmd = final_command.command.clone(); + restored_cmd = restored_cmd.replace('\u{E000}', &app.parameter_token); + if !app.parameter_ending_token.is_empty() { + restored_cmd = restored_cmd.replace('\u{E001}', &app.parameter_ending_token); + } + final_command.command = restored_cmd; + + return Some(final_command); } + app.selected_command = Some(replaced_command); app.provided_parameter_count += 1; None } + // Handle query input Key::Backspace => { app.input.pop(); diff --git a/src/gui/parameter_input/render.rs b/src/gui/parameter_input/render.rs index be2942c..e141075 100644 --- a/src/gui/parameter_input/render.rs +++ b/src/gui/parameter_input/render.rs @@ -1,6 +1,6 @@ use crate::config::HoardConfig; use crate::gui::commands_gui::State; -use crate::util::{split_with_delim, string_find_next, translate_number_to_nth}; +use crate::util::translate_number_to_nth; use ratatui::backend::TermionBackend; use ratatui::layout::{Alignment, Constraint, Direction, Layout}; use ratatui::style::{Color, Style}; @@ -65,46 +65,82 @@ pub fn draw( let token = config.parameter_token.as_ref().unwrap().as_str(); let ending_token = config.parameter_ending_token.as_ref().unwrap().as_str(); - // Named parameter ending with a space - let named_token = string_find_next(command_text, token, " "); - // Named parameter ending with ending token. If ending token is not used, `full_named_token` is an empty string - let mut full_named_token = string_find_next(command_text, token, ending_token); - full_named_token.push_str(ending_token); - // Select the split based on whether the ending token is part of the command or not - let split_token = if command_text.contains(ending_token) { - full_named_token - } else { - named_token - }; + let mut command_spans: Vec = Vec::new(); - let split_commands: Vec = split_with_delim(command_text, &split_token); - if token == split_token { - // If the next token to replace is not named - let command_parts = command_text.split_once(token); - let mut spans: Vec = if let Some((begin, end)) = command_parts { - vec![ - Span::styled(begin, command_style), - Span::styled(token, primary_style), - Span::styled(end, command_style), - ] - } else { - vec![Span::styled(command_text, command_style)] - }; - command_spans.append(&mut spans); - } else { - // if the next token to replaced is named, find all other occurrences and paint them too - let mut spans = split_commands - .iter() - .map(|e| { - if *e == split_token { - (e, primary_style) + + let mut i = 0; + let mut found_pos = None; + let bytes = command_text.as_bytes(); + + while i < command_text.len() { + if bytes[i] == b'\\' { + i += 1; + if i < command_text.len() { + if command_text[i..].starts_with(token) { + i += token.len(); } else { - (e, command_style) + let ch = command_text[i..].chars().next().unwrap(); + i += ch.len_utf8(); + } + } + continue; + } + if command_text[i..].starts_with(token) { + found_pos = Some(i); + break; + } + let ch = command_text[i..].chars().next().unwrap(); + i += ch.len_utf8(); + } + + if let Some(pos) = found_pos { + let mut full_param_len = token.len(); + + if !ending_token.is_empty() { + let rest = &command_text[pos + token.len()..]; + let mut search_idx = 0; + let mut found_end_at = None; + + while search_idx < rest.len() { + if rest.as_bytes()[search_idx] == b'\\' { + search_idx += 1; + if search_idx < rest.len() { + let ch = rest[search_idx..].chars().next().unwrap(); + search_idx += ch.len_utf8(); + } + continue; + } + if rest[search_idx..].starts_with(token) { + break; + } + if rest[search_idx..].starts_with(ending_token) { + found_end_at = Some(search_idx + ending_token.len()); + break; + } + if rest.as_bytes()[search_idx] == b' ' { + break; } - }) - .map(|(command, style)| Span::styled(command, style)) - .collect(); - command_spans.append(&mut spans); + + let ch = rest[search_idx..].chars().next().unwrap(); + search_idx += ch.len_utf8(); + } + + if let Some(offset) = found_end_at { + full_param_len = token.len() + offset; + } + } + + command_spans.push(Span::styled(&command_text[..pos], command_style)); + command_spans.push(Span::styled( + &command_text[pos..pos + full_param_len], + primary_style, + )); + command_spans.push(Span::styled( + &command_text[pos + full_param_len..], + command_style, + )); + } else { + command_spans.push(Span::styled(command_text, command_style)); } let command = Paragraph::new(Line::from(command_spans))