├── .gitignore ├── assets ├── demo.gif └── v1_0_0.gif ├── src ├── lib.rs ├── ui.rs ├── ui │ ├── theme │ │ ├── light.rs │ │ └── dark.rs │ ├── theme.rs │ ├── keymap_popup.rs │ ├── search_popup.rs │ ├── bottom_bar.rs │ ├── scroll_offset_list.rs │ ├── context_viewer.rs │ ├── result_list.rs │ └── input_handler.rs ├── ig │ ├── grep_match.rs │ ├── sink.rs │ ├── file_entry.rs │ ├── search_config.rs │ └── searcher.rs ├── main.rs ├── ig.rs ├── app.rs ├── editor.rs └── args.rs ├── HomebrewFormula └── igrep.rb ├── Cargo.toml ├── LICENSE ├── .github └── workflows │ └── ci.yml ├── CHANGELOG.md ├── README.md └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /assets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/konradsz/igrep/HEAD/assets/demo.gif -------------------------------------------------------------------------------- /assets/v1_0_0.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/konradsz/igrep/HEAD/assets/v1_0_0.gif -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod app; 2 | pub mod args; 3 | pub mod editor; 4 | pub mod ig; 5 | pub mod ui; 6 | -------------------------------------------------------------------------------- /src/ui.rs: -------------------------------------------------------------------------------- 1 | pub mod bottom_bar; 2 | pub mod context_viewer; 3 | pub mod input_handler; 4 | pub mod keymap_popup; 5 | pub mod result_list; 6 | pub mod search_popup; 7 | pub mod theme; 8 | 9 | mod scroll_offset_list; 10 | -------------------------------------------------------------------------------- /src/ui/theme/light.rs: -------------------------------------------------------------------------------- 1 | use super::Theme; 2 | use ratatui::style::Color; 3 | 4 | pub struct Light; 5 | 6 | impl Theme for Light { 7 | fn highlight_color(&self) -> Color { 8 | Color::Rgb(220, 220, 220) 9 | } 10 | 11 | fn context_viewer_theme(&self) -> &str { 12 | "base16-ocean.light" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/ig/grep_match.rs: -------------------------------------------------------------------------------- 1 | pub struct GrepMatch { 2 | pub line_number: u64, 3 | pub text: String, 4 | pub match_offsets: Vec<(usize, usize)>, 5 | } 6 | 7 | impl GrepMatch { 8 | pub fn new(line_number: u64, text: String, match_offsets: Vec<(usize, usize)>) -> Self { 9 | Self { 10 | line_number, 11 | text, 12 | match_offsets, 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /HomebrewFormula/igrep.rb: -------------------------------------------------------------------------------- 1 | class Igrep < Formula 2 | version "1.3.0" 3 | desc "Interactive Grep" 4 | homepage "https://github.com/konradsz/igrep" 5 | url "https://github.com/konradsz/igrep/releases/download/v#{version}/igrep-v#{version}-x86_64-apple-darwin.tar.gz" 6 | sha256 "33908e25d904d7652f2bc749a16beaae86b9529f83aeae8ca6834dddfc2b0a9d" 7 | 8 | def install 9 | bin.install "ig" 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /src/ui/theme/dark.rs: -------------------------------------------------------------------------------- 1 | use super::Theme; 2 | use ratatui::style::Color; 3 | 4 | pub struct Dark; 5 | 6 | impl Theme for Dark { 7 | fn highlight_color(&self) -> Color { 8 | Color::Rgb(58, 58, 58) 9 | } 10 | 11 | fn context_viewer_theme(&self) -> &str { 12 | "base16-ocean.dark" 13 | } 14 | 15 | fn bottom_bar_color(&self) -> Color { 16 | Color::Rgb(58, 58, 58) 17 | } 18 | 19 | fn bottom_bar_font_color(&self) -> Color { 20 | Color::Rgb(147, 147, 147) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "igrep" 3 | version = "1.3.0" 4 | authors = ["Konrad Szymoniak "] 5 | license = "MIT" 6 | description = "Interactive Grep" 7 | homepage = "https://github.com/konradsz/igrep" 8 | documentation = "https://github.com/konradsz/igrep" 9 | repository = "https://github.com/konradsz/igrep" 10 | keywords = ["cli", "tui", "grep"] 11 | categories = ["command-line-utilities"] 12 | edition = "2021" 13 | 14 | [[bin]] 15 | name = "ig" 16 | path = "src/main.rs" 17 | 18 | [dependencies] 19 | grep = "0.3.1" 20 | ignore = "0.4.22" 21 | clap = { version = "4.5.4", features = ["derive", "env"] } 22 | crossterm = "0.27.0" 23 | ratatui = { version = "0.26.2", default-features = false, features = [ 24 | 'crossterm', 25 | ] } 26 | unicode-width = "0.1.12" 27 | itertools = "0.13.0" 28 | anyhow = "1.0.83" 29 | strum = { version = "0.26.2", features = ["derive"] } 30 | syntect = "5.2.0" 31 | which = "6.0.3" 32 | 33 | [dev-dependencies] 34 | lazy_static = "1.4.0" 35 | test-case = "3.3.1" 36 | mockall = "0.12.1" 37 | 38 | [build-dependencies] 39 | anyhow = "1.0.83" 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Konrad Szymoniak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/ig/sink.rs: -------------------------------------------------------------------------------- 1 | use grep::{ 2 | matcher::Matcher, 3 | searcher::{Searcher, Sink, SinkMatch}, 4 | }; 5 | 6 | use super::grep_match::GrepMatch; 7 | 8 | pub(crate) struct MatchesSink<'a, M> 9 | where 10 | M: Matcher, 11 | { 12 | matcher: M, 13 | matches_in_entry: &'a mut Vec, 14 | } 15 | 16 | impl<'a, M> MatchesSink<'a, M> 17 | where 18 | M: Matcher, 19 | { 20 | pub(crate) fn new(matcher: M, matches_in_entry: &'a mut Vec) -> Self { 21 | Self { 22 | matcher, 23 | matches_in_entry, 24 | } 25 | } 26 | } 27 | 28 | impl Sink for MatchesSink<'_, M> 29 | where 30 | M: Matcher, 31 | { 32 | type Error = std::io::Error; 33 | 34 | fn matched(&mut self, _: &Searcher, sink_match: &SinkMatch) -> Result { 35 | let line_number = sink_match 36 | .line_number() 37 | .ok_or(std::io::ErrorKind::InvalidData)?; 38 | let text = std::str::from_utf8(sink_match.bytes()); 39 | 40 | let mut offsets = vec![]; 41 | self.matcher 42 | .find_iter(sink_match.bytes(), |m| { 43 | offsets.push((m.start(), m.end())); 44 | true 45 | }) 46 | .ok(); 47 | 48 | if let Ok(t) = text { 49 | self.matches_in_entry 50 | .push(GrepMatch::new(line_number, t.into(), offsets)); 51 | }; 52 | 53 | Ok(true) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/ig/file_entry.rs: -------------------------------------------------------------------------------- 1 | use super::grep_match::GrepMatch; 2 | 3 | pub enum EntryType { 4 | Header(String), 5 | Match(u64, String, Vec<(usize, usize)>), 6 | } 7 | 8 | pub struct FileEntry(Vec); 9 | 10 | impl FileEntry { 11 | pub fn new(name: String, matches: Vec) -> Self { 12 | Self( 13 | std::iter::once(EntryType::Header(name)) 14 | .chain(matches.into_iter().map(|m| { 15 | let mut text = String::new(); 16 | let mut ofs = m.match_offsets; 17 | let mut pos = 0; 18 | for c in m.text.chars() { 19 | pos += 1; 20 | if c != '\t' { 21 | text.push(c); 22 | } else { 23 | text.push_str(" "); 24 | for p in &mut ofs { 25 | if p.0 >= pos { 26 | p.0 += 1; 27 | p.1 += 1; 28 | } 29 | } 30 | } 31 | } 32 | EntryType::Match(m.line_number, text, ofs) 33 | })) 34 | .collect(), 35 | ) 36 | } 37 | 38 | pub fn get_matches_count(&self) -> usize { 39 | self.0 40 | .iter() 41 | .filter(|&e| matches!(e, EntryType::Match(_, _, _))) 42 | .count() 43 | } 44 | 45 | pub fn get_entries(self) -> Vec { 46 | self.0 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: CI 4 | 5 | jobs: 6 | check: 7 | name: Check 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout sources 11 | uses: actions/checkout@v2 12 | 13 | - name: Install stable toolchain 14 | uses: actions-rs/toolchain@v1 15 | with: 16 | profile: minimal 17 | toolchain: stable 18 | override: true 19 | 20 | - name: Run cargo check 21 | uses: actions-rs/cargo@v1 22 | with: 23 | command: check 24 | 25 | test: 26 | name: Test Suite 27 | runs-on: ubuntu-latest 28 | steps: 29 | - name: Checkout sources 30 | uses: actions/checkout@v2 31 | 32 | - name: Install stable toolchain 33 | uses: actions-rs/toolchain@v1 34 | with: 35 | profile: minimal 36 | toolchain: stable 37 | override: true 38 | 39 | - name: Run cargo test 40 | uses: actions-rs/cargo@v1 41 | with: 42 | command: test 43 | 44 | lints: 45 | name: Lints 46 | runs-on: ubuntu-latest 47 | steps: 48 | - name: Checkout sources 49 | uses: actions/checkout@v2 50 | 51 | - name: Install stable toolchain 52 | uses: actions-rs/toolchain@v1 53 | with: 54 | profile: minimal 55 | toolchain: stable 56 | override: true 57 | components: rustfmt, clippy 58 | 59 | - name: Run cargo fmt 60 | uses: actions-rs/cargo@v1 61 | with: 62 | command: fmt 63 | args: --all -- --check 64 | 65 | - name: Run cargo clippy 66 | uses: actions-rs/cargo@v1 67 | with: 68 | command: clippy 69 | args: --all-targets -- -D warnings 70 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use igrep::{ 3 | app::App, 4 | args::Args, 5 | editor::EditorCommand, 6 | ig, 7 | ui::{ 8 | context_viewer::ContextViewer, 9 | theme::{dark::Dark, light::Light, Theme, ThemeVariant}, 10 | }, 11 | }; 12 | use std::io::Write; 13 | 14 | fn main() -> Result<()> { 15 | let args = Args::parse_cli_and_config_file(); 16 | 17 | if args.type_list { 18 | use itertools::Itertools; 19 | let mut builder = ignore::types::TypesBuilder::new(); 20 | builder.add_defaults(); 21 | for definition in builder.definitions() { 22 | writeln!( 23 | std::io::stdout(), 24 | "{}: {}", 25 | definition.name(), 26 | definition.globs().iter().format(", "), 27 | )?; 28 | } 29 | return Ok(()); 30 | } 31 | 32 | let paths = if args.paths.is_empty() { 33 | vec!["./".into()] 34 | } else { 35 | args.paths 36 | }; 37 | 38 | let search_config = ig::SearchConfig::from(args.pattern.unwrap(), paths)? 39 | .case_insensitive(args.ignore_case) 40 | .case_smart(args.smart_case) 41 | .search_hidden(args.search_hidden) 42 | .follow_links(args.follow_links) 43 | .word_regexp(args.word_regexp) 44 | .fixed_strings(args.fixed_strings) 45 | .multi_line(args.multi_line) 46 | .globs(args.glob)? 47 | .file_types(args.type_matching, args.type_not)? 48 | .sort_by(args.sort_by, args.sort_by_reverse)?; 49 | 50 | let theme: Box = match args.theme { 51 | ThemeVariant::Light => Box::new(Light), 52 | ThemeVariant::Dark => Box::new(Dark), 53 | }; 54 | let mut app = App::new( 55 | search_config, 56 | EditorCommand::new(args.editor.custom_command, args.editor.editor)?, 57 | ContextViewer::new(args.context_viewer), 58 | theme, 59 | ); 60 | app.run()?; 61 | 62 | Ok(()) 63 | } 64 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v1.3.0 (2024-09-08) 2 | *** 3 | - locate editor executable using `which` crate 4 | - add `-w`/`--word-regexp` arg 5 | - add option to open Context Viewer at startup 6 | - add follow symlinks option 7 | - ability to specify custom command 8 | - read custom editor from environment 9 | - add `less` as an editor option 10 | - add a keybindings popup 11 | - add keybindings for changing context viewer size 12 | - fix flushing infinitely when opening nvim with no results found 13 | 14 | ## v1.2.0 (2023-08-08) 15 | *** 16 | - support multiple search paths 17 | - Ctrl+c closes an application 18 | - allow to change search pattern without closing an app 19 | 20 | ## v1.1.0 (2023-01-29) 21 | *** 22 | - add error handling in case of editor process spawning failure 23 | - improve performance by handling multiple file entries every redraw 24 | - add support for Sublime Text, Micro, Intellij, Goland, Pycharm 25 | - use `helix` as a binary name when `helix` is set as an editor of choice 26 | - prefer $VISUAL variable over $EDITOR when determining text editor to use 27 | 28 | ## v1.0.0 (2023-01-08) 29 | *** 30 | - add context viewer 31 | - add support for opening files in Helix 32 | 33 | ## v0.5.1 (2022-08-01) 34 | *** 35 | - add support for opening files in VS Code Insiders 36 | 37 | ## v0.5.0 (2022-04-24) 38 | *** 39 | - add theme for light environments 40 | - support for ripgrep's configuration file 41 | - add Scoop package 42 | 43 | ## v0.4.0 (2022-03-16) 44 | *** 45 | - improve clarity of using multi character input 46 | - add support for opening files in VS Code 47 | - add support for opening files in emacs and emacsclient 48 | 49 | ## v0.3.0 (2022-03-08) 50 | *** 51 | - use $EDITOR as a fallback variable 52 | - fix Initial console modes not set error on Windows 53 | - make igrep available on Homebrew 54 | 55 | ## v0.2.0 (2022-03-02) 56 | *** 57 | - allow to specify editor using `IGREP_EDITOR` environment variable 58 | - add `nvim` as an alias for `neovim` 59 | - support for searching hidden files/directories via `-.`/`--hidden` options 60 | 61 | ## v0.1.2 (2022-02-19) 62 | *** 63 | Initial release. Provides basic set of functionalities. 64 | -------------------------------------------------------------------------------- /src/ui/theme.rs: -------------------------------------------------------------------------------- 1 | pub mod dark; 2 | pub mod light; 3 | 4 | use clap::ValueEnum; 5 | use ratatui::style::{Color, Modifier, Style}; 6 | use strum::Display; 7 | 8 | #[derive(Display, Copy, Clone, Debug, ValueEnum)] 9 | #[strum(serialize_all = "lowercase")] 10 | pub enum ThemeVariant { 11 | Light, 12 | Dark, 13 | } 14 | 15 | pub trait Theme { 16 | // Matches list styles 17 | fn background_color(&self) -> Style { 18 | Style::default() 19 | } 20 | 21 | fn list_font_color(&self) -> Style { 22 | Style::default() 23 | } 24 | 25 | fn file_path_color(&self) -> Style { 26 | Style::default().fg(Color::LightMagenta) 27 | } 28 | 29 | fn line_number_color(&self) -> Style { 30 | Style::default().fg(Color::Green) 31 | } 32 | 33 | fn match_color(&self) -> Style { 34 | Style::default().fg(Color::Red) 35 | } 36 | 37 | fn highlight_color(&self) -> Color; 38 | 39 | // Context viewer styles 40 | fn context_viewer_theme(&self) -> &str; 41 | 42 | // Bottom bar styles 43 | fn bottom_bar_color(&self) -> Color { 44 | Color::Reset 45 | } 46 | 47 | fn bottom_bar_font_color(&self) -> Color { 48 | Color::Reset 49 | } 50 | 51 | fn bottom_bar_style(&self) -> Style { 52 | Style::default() 53 | .bg(self.bottom_bar_color()) 54 | .fg(self.bottom_bar_font_color()) 55 | } 56 | 57 | fn searching_state_style(&self) -> Style { 58 | Style::default() 59 | .add_modifier(Modifier::BOLD) 60 | .bg(Color::Rgb(255, 165, 0)) 61 | .fg(Color::Black) 62 | } 63 | 64 | fn error_state_style(&self) -> Style { 65 | Style::default() 66 | .add_modifier(Modifier::BOLD) 67 | .bg(Color::Red) 68 | .fg(Color::Black) 69 | } 70 | 71 | fn finished_state_style(&self) -> Style { 72 | Style::default() 73 | .add_modifier(Modifier::BOLD) 74 | .bg(Color::Green) 75 | .fg(Color::Black) 76 | } 77 | 78 | fn invalid_input_color(&self) -> Color { 79 | Color::Red 80 | } 81 | 82 | // Search popup style 83 | fn search_popup_border(&self) -> Style { 84 | Style::default().fg(Color::Green) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/ui/keymap_popup.rs: -------------------------------------------------------------------------------- 1 | use ratatui::{ 2 | layout::{Alignment, Rect}, 3 | text::Text, 4 | widgets::{Block, Borders, Clear, Padding, Paragraph}, 5 | Frame, 6 | }; 7 | 8 | use super::theme::Theme; 9 | 10 | include!(concat!(env!("OUT_DIR"), "/keybindings.rs")); 11 | 12 | pub struct KeymapPopup { 13 | visible: bool, 14 | scroll_y: u16, 15 | scroll_x: u16, 16 | content: Text<'static>, 17 | } 18 | 19 | impl KeymapPopup { 20 | pub fn new() -> Self { 21 | Self { 22 | visible: false, 23 | scroll_y: 0, 24 | scroll_x: 0, 25 | content: Text::from(KEYBINDINGS_TABLE), 26 | } 27 | } 28 | 29 | pub fn toggle(&mut self) { 30 | self.visible = !self.visible; 31 | if self.visible { 32 | self.scroll_y = 0; 33 | self.scroll_x = 0; 34 | } 35 | } 36 | 37 | pub fn go_down(&mut self) { 38 | self.scroll_y = self.scroll_y.saturating_add(1); 39 | } 40 | 41 | pub fn go_up(&mut self) { 42 | self.scroll_y = self.scroll_y.saturating_sub(1); 43 | } 44 | 45 | pub fn go_right(&mut self) { 46 | self.scroll_x = self.scroll_x.saturating_add(1); 47 | } 48 | 49 | pub fn go_left(&mut self) { 50 | self.scroll_x = self.scroll_x.saturating_sub(1); 51 | } 52 | 53 | pub fn draw(&mut self, frame: &mut Frame, theme: &dyn Theme) { 54 | if !self.visible { 55 | return; 56 | } 57 | 58 | let popup_area = Self::get_popup_area(frame.size()); 59 | 60 | let max_y = KEYBINDINGS_LEN.saturating_sub(popup_area.height - 4); 61 | self.scroll_y = self.scroll_y.min(max_y); 62 | let max_x = KEYBINDINGS_LINE_LEN.saturating_sub(popup_area.width - 4); 63 | self.scroll_x = self.scroll_x.min(max_x); 64 | 65 | let paragraph = Paragraph::new(self.content.clone()) 66 | .block( 67 | Block::default() 68 | .borders(Borders::ALL) 69 | .border_style(theme.search_popup_border()) 70 | .title(concat!( 71 | " ", 72 | env!("CARGO_PKG_NAME"), 73 | " ", 74 | env!("CARGO_PKG_VERSION"), 75 | " " 76 | )) 77 | .title_alignment(Alignment::Center) 78 | .padding(Padding::uniform(1)), 79 | ) 80 | .scroll((self.scroll_y, self.scroll_x)); 81 | 82 | frame.render_widget(Clear, popup_area); 83 | frame.render_widget(paragraph, popup_area); 84 | } 85 | 86 | fn get_popup_area(frame_size: Rect) -> Rect { 87 | let height = (KEYBINDINGS_LEN + 4).min((frame_size.height as f64 * 0.8) as u16); 88 | let y = (frame_size.height - height) / 2; 89 | 90 | let width = (KEYBINDINGS_LINE_LEN + 4).min((frame_size.width as f64 * 0.8) as u16); 91 | let x = (frame_size.width - width) / 2; 92 | 93 | Rect { 94 | x, 95 | y, 96 | width, 97 | height, 98 | } 99 | } 100 | } 101 | 102 | impl Default for KeymapPopup { 103 | fn default() -> Self { 104 | Self::new() 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/ig.rs: -------------------------------------------------------------------------------- 1 | pub mod file_entry; 2 | pub mod grep_match; 3 | pub mod search_config; 4 | mod searcher; 5 | mod sink; 6 | 7 | use std::process::ExitStatus; 8 | use std::sync::mpsc; 9 | 10 | use crate::editor::EditorCommand; 11 | use crate::ui::result_list::ResultList; 12 | pub use search_config::SearchConfig; 13 | pub use search_config::SortKey; 14 | use searcher::Event; 15 | 16 | use self::file_entry::FileEntry; 17 | 18 | #[derive(PartialEq, Eq)] 19 | pub enum State { 20 | Idle, 21 | Searching, 22 | OpenFile(bool), 23 | Error(String), 24 | Exit, 25 | } 26 | 27 | pub struct Ig { 28 | tx: mpsc::Sender, 29 | rx: mpsc::Receiver, 30 | state: State, 31 | editor_command: EditorCommand, 32 | } 33 | 34 | impl Ig { 35 | pub fn new(editor_command: EditorCommand) -> Self { 36 | let (tx, rx) = mpsc::channel(); 37 | 38 | Self { 39 | tx, 40 | rx, 41 | state: State::Idle, 42 | editor_command, 43 | } 44 | } 45 | 46 | fn try_spawn_editor(&self, file_name: &str, line_number: u64) -> anyhow::Result { 47 | let mut editor_process = self.editor_command.spawn(file_name, line_number)?; 48 | editor_process.wait().map_err(anyhow::Error::from) 49 | } 50 | 51 | pub fn open_file_if_requested(&mut self, selected_entry: Option<(String, u64)>) { 52 | if let State::OpenFile(idle) = self.state { 53 | if let Some((ref file_name, line_number)) = selected_entry { 54 | match self.try_spawn_editor(file_name, line_number) { 55 | Ok(_) => self.state = if idle { State::Idle } else { State::Searching }, 56 | Err(_) => { 57 | self.state = State::Error(format!( 58 | "Failed to open editor '{}'. Is it installed?", 59 | self.editor_command, 60 | )) 61 | } 62 | } 63 | } else { 64 | self.state = if idle { State::Idle } else { State::Searching }; 65 | } 66 | } 67 | } 68 | 69 | pub fn handle_searcher_event(&mut self) -> Option { 70 | while let Ok(event) = self.rx.try_recv() { 71 | match event { 72 | Event::NewEntry(e) => return Some(e), 73 | Event::SearchingFinished => self.state = State::Idle, 74 | Event::Error => self.state = State::Exit, 75 | } 76 | } 77 | 78 | None 79 | } 80 | 81 | pub fn search(&mut self, search_config: SearchConfig, result_list: &mut ResultList) { 82 | if self.state == State::Idle { 83 | *result_list = ResultList::default(); 84 | self.state = State::Searching; 85 | searcher::search(search_config, self.tx.clone()); 86 | } 87 | } 88 | 89 | pub fn open_file(&mut self) { 90 | self.state = State::OpenFile(self.state == State::Idle); 91 | } 92 | 93 | pub fn exit(&mut self) { 94 | self.state = State::Exit; 95 | } 96 | 97 | pub fn is_idle(&self) -> bool { 98 | self.state == State::Idle 99 | } 100 | 101 | pub fn is_searching(&self) -> bool { 102 | self.state == State::Searching 103 | } 104 | 105 | pub fn last_error(&self) -> Option<&str> { 106 | if let State::Error(err) = &self.state { 107 | Some(err) 108 | } else { 109 | None 110 | } 111 | } 112 | 113 | pub fn exit_requested(&self) -> bool { 114 | self.state == State::Exit 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/ui/search_popup.rs: -------------------------------------------------------------------------------- 1 | use ratatui::{ 2 | layout::{Alignment, Constraint, Direction, Layout, Rect}, 3 | style::Stylize, 4 | text::{Line, Text}, 5 | widgets::{Block, Borders, Clear, Paragraph}, 6 | Frame, 7 | }; 8 | 9 | use super::theme::Theme; 10 | 11 | #[derive(Default)] 12 | pub struct SearchPopup { 13 | visible: bool, 14 | pattern: String, 15 | cursor_position: usize, 16 | } 17 | 18 | impl SearchPopup { 19 | pub fn toggle(&mut self) { 20 | self.visible = !self.visible; 21 | } 22 | 23 | pub fn set_pattern(&mut self, pattern: String) { 24 | self.pattern = pattern; 25 | self.cursor_position = self.pattern.len(); 26 | } 27 | 28 | pub fn get_pattern(&self) -> String { 29 | self.pattern.clone() 30 | } 31 | 32 | pub fn insert_char(&mut self, c: char) { 33 | self.pattern.insert(self.cursor_position, c); 34 | self.move_cursor_right(); 35 | } 36 | 37 | pub fn remove_char(&mut self) { 38 | self.move_cursor_left(); 39 | if !self.pattern.is_empty() { 40 | self.pattern.remove(self.cursor_position); 41 | } 42 | } 43 | 44 | pub fn delete_char(&mut self) { 45 | if self.cursor_position < self.pattern.len() { 46 | self.pattern.remove(self.cursor_position); 47 | } 48 | } 49 | 50 | pub fn move_cursor_left(&mut self) { 51 | if self.cursor_position > 0 { 52 | self.cursor_position -= 1; 53 | } 54 | } 55 | 56 | pub fn move_cursor_right(&mut self) { 57 | if self.cursor_position < self.pattern.len() { 58 | self.cursor_position += 1; 59 | } 60 | } 61 | 62 | pub fn draw(&self, frame: &mut Frame, theme: &dyn Theme) { 63 | if !self.visible { 64 | return; 65 | } 66 | 67 | let block = Block::default() 68 | .borders(Borders::ALL) 69 | .border_style(theme.search_popup_border()) 70 | .bold() 71 | .title(" Regex Pattern ") 72 | .title_alignment(Alignment::Center); 73 | let popup_area = Self::get_popup_area(frame.size(), 50); 74 | frame.render_widget(Clear, popup_area); 75 | 76 | frame.render_widget(block, popup_area); 77 | 78 | let mut text_area = popup_area; 79 | text_area.y += 1; // one line below the border 80 | text_area.x += 2; // two chars to the right 81 | 82 | let max_text_width = text_area.width as usize - 4; 83 | let pattern = if self.pattern.len() > max_text_width { 84 | format!( 85 | "…{}", 86 | &self.pattern[self.pattern.len() - max_text_width + 1..] 87 | ) 88 | } else { 89 | self.pattern.clone() 90 | }; 91 | 92 | let text = Text::from(Line::from(pattern.as_str())); 93 | let pattern_text = Paragraph::new(text); 94 | frame.render_widget(pattern_text, text_area); 95 | frame.set_cursor( 96 | std::cmp::min( 97 | text_area.x + self.cursor_position as u16, 98 | text_area.x + text_area.width - 4, 99 | ), 100 | text_area.y, 101 | ); 102 | } 103 | 104 | fn get_popup_area(frame_size: Rect, width_percent: u16) -> Rect { 105 | const POPUP_HEIGHT: u16 = 3; 106 | let top_bottom_margin = (frame_size.height - POPUP_HEIGHT) / 2; 107 | let popup_layout = Layout::default() 108 | .direction(Direction::Vertical) 109 | .constraints( 110 | [ 111 | Constraint::Length(top_bottom_margin), 112 | Constraint::Length(POPUP_HEIGHT), 113 | Constraint::Length(top_bottom_margin), 114 | ] 115 | .as_ref(), 116 | ) 117 | .split(frame_size); 118 | 119 | Layout::default() 120 | .direction(Direction::Horizontal) 121 | .constraints( 122 | [ 123 | Constraint::Percentage((100 - width_percent) / 2), 124 | Constraint::Percentage(width_percent), 125 | Constraint::Percentage((100 - width_percent) / 2), 126 | ] 127 | .as_ref(), 128 | ) 129 | .split(popup_layout[1])[1] 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/ig/search_config.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use ignore::{ 3 | overrides::{Override, OverrideBuilder}, 4 | types::{Types, TypesBuilder}, 5 | }; 6 | use std::path::PathBuf; 7 | 8 | use crate::args::SortKeyArg; 9 | 10 | #[derive(Clone, Copy)] 11 | pub enum SortKey { 12 | Path, 13 | PathReversed, 14 | Modified, 15 | ModifiedReversed, 16 | Created, 17 | CreatedReversed, 18 | Accessed, 19 | AccessedReversed, 20 | } 21 | 22 | #[derive(Clone)] 23 | pub struct SearchConfig { 24 | pub pattern: String, 25 | pub paths: Vec, 26 | pub case_insensitive: bool, 27 | pub case_smart: bool, 28 | pub overrides: Override, 29 | pub types: Types, 30 | pub search_hidden: bool, 31 | pub follow_links: bool, 32 | pub word_regexp: bool, 33 | pub sort_by: Option, 34 | pub fixed_strings: bool, 35 | pub multi_line: bool, 36 | } 37 | 38 | impl SearchConfig { 39 | pub fn from(pattern: String, paths: Vec) -> Result { 40 | let mut builder = TypesBuilder::new(); 41 | builder.add_defaults(); 42 | let types = builder.build()?; 43 | 44 | Ok(Self { 45 | pattern, 46 | paths, 47 | case_insensitive: false, 48 | case_smart: false, 49 | overrides: Override::empty(), 50 | types, 51 | search_hidden: false, 52 | follow_links: false, 53 | word_regexp: false, 54 | fixed_strings: false, 55 | multi_line: false, 56 | sort_by: None, 57 | }) 58 | } 59 | 60 | pub fn case_insensitive(mut self, case_insensitive: bool) -> Self { 61 | self.case_insensitive = case_insensitive; 62 | self 63 | } 64 | 65 | pub fn case_smart(mut self, case_smart: bool) -> Self { 66 | self.case_smart = case_smart; 67 | self 68 | } 69 | 70 | pub fn globs(mut self, globs: Vec) -> Result { 71 | let mut builder = OverrideBuilder::new(std::env::current_dir()?); 72 | for glob in globs { 73 | builder.add(&glob)?; 74 | } 75 | self.overrides = builder.build()?; 76 | Ok(self) 77 | } 78 | 79 | pub fn file_types( 80 | mut self, 81 | file_types: Vec, 82 | file_types_not: Vec, 83 | ) -> Result { 84 | let mut builder = TypesBuilder::new(); 85 | builder.add_defaults(); 86 | for file_type in file_types { 87 | builder.select(&file_type); 88 | } 89 | for file_type in file_types_not { 90 | builder.negate(&file_type); 91 | } 92 | self.types = builder.build()?; 93 | Ok(self) 94 | } 95 | 96 | pub fn sort_by( 97 | mut self, 98 | sort_by: Option, 99 | sort_by_reversed: Option, 100 | ) -> Result { 101 | if let Some(arg) = sort_by { 102 | match arg { 103 | SortKeyArg::Path => self.sort_by = Some(SortKey::Path), 104 | SortKeyArg::Modified => self.sort_by = Some(SortKey::Modified), 105 | SortKeyArg::Created => self.sort_by = Some(SortKey::Created), 106 | SortKeyArg::Accessed => self.sort_by = Some(SortKey::Accessed), 107 | } 108 | }; 109 | if let Some(arg) = sort_by_reversed { 110 | match arg { 111 | SortKeyArg::Path => self.sort_by = Some(SortKey::PathReversed), 112 | SortKeyArg::Modified => self.sort_by = Some(SortKey::ModifiedReversed), 113 | SortKeyArg::Created => self.sort_by = Some(SortKey::CreatedReversed), 114 | SortKeyArg::Accessed => self.sort_by = Some(SortKey::AccessedReversed), 115 | } 116 | }; 117 | Ok(self) 118 | } 119 | 120 | pub fn search_hidden(mut self, search_hidden: bool) -> Self { 121 | self.search_hidden = search_hidden; 122 | self 123 | } 124 | 125 | pub fn follow_links(mut self, follow_links: bool) -> Self { 126 | self.follow_links = follow_links; 127 | self 128 | } 129 | 130 | pub fn word_regexp(mut self, word_regexp: bool) -> Self { 131 | self.word_regexp = word_regexp; 132 | self 133 | } 134 | 135 | pub fn fixed_strings(mut self, fixed_strings: bool) -> Self { 136 | self.fixed_strings = fixed_strings; 137 | self 138 | } 139 | 140 | pub fn multi_line(mut self, multi_line: bool) -> Self { 141 | self.multi_line = multi_line; 142 | self 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/ui/bottom_bar.rs: -------------------------------------------------------------------------------- 1 | use ratatui::{ 2 | layout::{Alignment, Constraint, Direction, Layout, Rect}, 3 | style::Style, 4 | text::Span, 5 | widgets::Paragraph, 6 | Frame, 7 | }; 8 | 9 | use crate::ig::Ig; 10 | 11 | use super::{ 12 | input_handler::{InputHandler, InputState}, 13 | result_list::ResultList, 14 | theme::Theme, 15 | }; 16 | 17 | pub fn draw( 18 | frame: &mut Frame, 19 | area: Rect, 20 | result_list: &ResultList, 21 | ig: &Ig, 22 | input_handler: &InputHandler, 23 | theme: &dyn Theme, 24 | ) { 25 | let selected_info_text = render_selected_info_text(result_list); 26 | 27 | let hsplit = Layout::default() 28 | .direction(Direction::Horizontal) 29 | .constraints( 30 | [ 31 | Constraint::Length(12), 32 | Constraint::Min(1), 33 | Constraint::Length(2), 34 | Constraint::Length(selected_info_text.len() as u16), 35 | ] 36 | .as_ref(), 37 | ) 38 | .split(area); 39 | 40 | draw_app_status(frame, hsplit[0], ig, theme); 41 | draw_search_result_summary(frame, hsplit[1], ig, result_list, theme); 42 | draw_current_input(frame, hsplit[2], input_handler, theme); 43 | draw_selected_info(frame, hsplit[3], selected_info_text, theme); 44 | } 45 | 46 | fn draw_app_status(frame: &mut Frame, area: Rect, ig: &Ig, theme: &dyn Theme) { 47 | let (app_status_text, app_status_style) = if ig.is_searching() { 48 | ("SEARCHING", theme.searching_state_style()) 49 | } else if ig.last_error().is_some() { 50 | ("ERROR", theme.error_state_style()) 51 | } else { 52 | ("FINISHED", theme.finished_state_style()) 53 | }; 54 | let app_status = Span::styled(app_status_text, app_status_style); 55 | 56 | frame.render_widget( 57 | Paragraph::new(app_status) 58 | .style(Style::default().bg(app_status_style.bg.expect("Background not set"))) 59 | .alignment(Alignment::Center), 60 | area, 61 | ); 62 | } 63 | 64 | fn draw_search_result_summary( 65 | frame: &mut Frame, 66 | area: Rect, 67 | ig: &Ig, 68 | result_list: &ResultList, 69 | theme: &dyn Theme, 70 | ) { 71 | let search_result = Span::raw(if ig.is_searching() { 72 | "".into() 73 | } else if let Some(err) = ig.last_error() { 74 | format!(" {err}") 75 | } else { 76 | let total_no_of_matches = result_list.get_total_number_of_matches(); 77 | if total_no_of_matches == 0 { 78 | " No matches found.".into() 79 | } else { 80 | let no_of_files = result_list.get_total_number_of_file_entries(); 81 | 82 | let matches_str = if total_no_of_matches == 1 { 83 | "match" 84 | } else { 85 | "matches" 86 | }; 87 | let files_str = if no_of_files == 1 { "file" } else { "files" }; 88 | 89 | let filtered_count = result_list.get_filtered_matches_count(); 90 | let filtered_str = if filtered_count != 0 { 91 | format!(" ({filtered_count} filtered out)") 92 | } else { 93 | String::default() 94 | }; 95 | 96 | format!(" Found {total_no_of_matches} {matches_str} in {no_of_files} {files_str}{filtered_str}.") 97 | } 98 | }); 99 | 100 | frame.render_widget( 101 | Paragraph::new(search_result) 102 | .style(theme.bottom_bar_style()) 103 | .alignment(Alignment::Left), 104 | area, 105 | ); 106 | } 107 | 108 | fn draw_current_input( 109 | frame: &mut Frame, 110 | area: Rect, 111 | input_handler: &InputHandler, 112 | theme: &dyn Theme, 113 | ) { 114 | let (current_input_content, current_input_color) = match input_handler.get_state() { 115 | InputState::Valid => (String::default(), theme.bottom_bar_font_color()), 116 | InputState::Incomplete(input) => (input.to_owned(), theme.bottom_bar_font_color()), 117 | InputState::Invalid(input) => (input.to_owned(), theme.invalid_input_color()), 118 | }; 119 | let current_input = Span::styled( 120 | current_input_content, 121 | Style::default() 122 | .bg(theme.bottom_bar_color()) 123 | .fg(current_input_color), 124 | ); 125 | 126 | frame.render_widget( 127 | Paragraph::new(current_input) 128 | .style(theme.bottom_bar_style()) 129 | .alignment(Alignment::Right), 130 | area, 131 | ); 132 | } 133 | 134 | fn render_selected_info_text(result_list: &ResultList) -> String { 135 | let current_no_of_matches = result_list.get_current_number_of_matches(); 136 | let current_match_index = result_list.get_current_match_index(); 137 | let width = current_no_of_matches.to_string().len(); 138 | format!(" | {current_match_index: >width$}/{current_no_of_matches} ") 139 | } 140 | 141 | fn draw_selected_info( 142 | frame: &mut Frame, 143 | area: Rect, 144 | selected_info_text: String, 145 | theme: &dyn Theme, 146 | ) { 147 | let selected_info = Span::styled(selected_info_text, theme.bottom_bar_style()); 148 | 149 | frame.render_widget( 150 | Paragraph::new(selected_info) 151 | .style(theme.bottom_bar_style()) 152 | .alignment(Alignment::Right), 153 | area, 154 | ); 155 | } 156 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # igrep - Interactive Grep 2 | Runs [grep](https://crates.io/crates/grep) ([ripgrep's](https://github.com/BurntSushi/ripgrep/) library) in the background, allows interactively pick its results and open selected match in text editor of choice (vim by default). 3 | 4 | `igrep` supports macOS and Linux. Reportedly it works on Windows as well. 5 | 6 | 7 | 8 | ## Usage 9 | `ig [OPTIONS] [PATHS]...` 10 | 11 | ### Args 12 | ``` 13 | Regular expression used for searching. 14 | ... Files or directories to search. Directories are searched recursively. 15 | If not specified, searching starts from current directory. 16 | ``` 17 | 18 | ### Options 19 | ``` 20 | -., --hidden Search hidden files and directories. By default, hidden files and 21 | directories are skipped. 22 | --editor Text editor used to open selected match. 23 | [possible values: check supported text editors section] 24 | --context-viewer Context viewer position at startup [default: none] 25 | [possible values: none, vertical, horizontal] 26 | --custom-command Custom command used to open selected match. 27 | Must contain {file_name} and {line_number} tokens (check Custom Command section). 28 | -g, --glob Include files and directories for searching that match the given glob. 29 | Multiple globs may be provided. 30 | -h, --help Print help information 31 | -i, --ignore-case Searches case insensitively. 32 | -L, --follow Follow symbolic links while traversing directories 33 | -S, --smart-case Searches case insensitively if the pattern is all lowercase. 34 | Search case sensitively otherwise. 35 | -t, --type Only search files matching TYPE. 36 | Multiple types may be provided. 37 | -T, --type-not Do not search files matching TYPE-NOT. 38 | Multiple types-not may be provided. 39 | --theme UI color theme [default: dark] [possible values: light, dark] 40 | --type-list Show all supported file types and their corresponding globs. 41 | -V, --version Print version information. 42 | -w, --word-regexp Only show matches surrounded by word boundaries 43 | -F, --fixed-strings Exact matches with no regex. Useful when searching for a string full of delimiters. 44 | --sort Sort results by [path, modified, accessed, created], see ripgrep for details 45 | --sortr Sort results reverse by [path, modified, accessed, created], see ripgrep for details 46 | ``` 47 | NOTE: `ig` respects `ripgrep`'s [configuration file](https://github.com/BurntSushi/ripgrep/blob/master/GUIDE.md#configuration-file) if `RIPGREP_CONFIG_PATH` environment variable is set and reads all supported options from it. 48 | 49 | ## Keybindings 50 | 51 | 52 | | Key | Action | 53 | | ------------------------ | -------------------------------------- | 54 | | `q`, `Esc`, `Ctrl+c` | Quit | 55 | | | | 56 | | `?`, `F1` | Open/close the keymap popup | 57 | | `Down`, `j` | Scroll down in the keymap popup | 58 | | `Up`, `k` | Scroll up in the keymap popup | 59 | | `Right`, `l` | Scroll right in the keymap popup | 60 | | `Left`, `h` | Scroll left in the keymap popup | 61 | | | | 62 | | `Down`, `j` | Select next match | 63 | | `Up`,`k` | Select previous match | 64 | | `Right`, `l`, `PageDown` | Select match in next file | 65 | | `Left`, `h`, `PageUp` | Select match in previous file | 66 | | `gg`, `Home` | Jump to the first match | 67 | | `Shift-g`, `End` | Jump to the last match | 68 | | `Enter` | Open current file | 69 | | `dd`, `Delete` | Filter out selected match | 70 | | `dw` | Filter out all matches in current file | 71 | | `v` | Toggle vertical context viewer | 72 | | `s` | Toggle horizontal context viewer | 73 | | `+` | Increase context viewer size | 74 | | `-` | Decrease context viewer size | 75 | | `F5`, `/` | Open search pattern popup | 76 | | `n` | Sort search results by name | 77 | | `m` | Sort search results by time modified | 78 | | `c` | Sort search results by time created | 79 | | `a` | Sort search results by time accessed | 80 | 81 | 82 | ## Supported text editors 83 | `igrep` supports Vim, Neovim, nano, VS Code (stable and insiders), Emacs, EmacsClient, Helix, SublimeText, Micro, Intellij, Goland, Pycharm and Less. If your beloved editor is missing on this list and you still want to use `igrep` please file an issue or use [custom command](#custom-command). 84 | 85 | ## Specifying text editor 86 | ### Builtin editors 87 | To specify builtin editor, use one of the following (listed in order of their precedence): 88 | - `--editor` option, 89 | - `$IGREP_EDITOR` variable, 90 | - `$VISUAL` variable, 91 | - `$EDITOR` variable. 92 | 93 | Higher priority option overrides lower one. If neither of these options is set, vim is used as a default. 94 | 95 | ### Custom Command 96 | Users can provide their own command used to open selected match using `--custom-command` option. It must contain {file_name} and {line_number} tokens. Example command used to open file in Vim looks as follows: 97 | 98 | `--custom-command "vim +{line_number} {file_name}"` 99 | 100 | The same argument can also be passed via the `$IGREP_CUSTOM_EDITOR` environment variable. Example: 101 | 102 | `IGREP_CUSTOM_EDITOR="vim +{line_number} {file_name}"` 103 | 104 | ## Installation 105 | ### Prebuilt binaries 106 | `igrep` binaries can be downloaded from [GitHub](https://github.com/konradsz/igrep/releases). 107 | ### Homebrew 108 | ```zsh 109 | brew install igrep 110 | ``` 111 | ### Scoop 112 | ``` 113 | scoop install igrep 114 | ``` 115 | ### Arch Linux 116 | ``` 117 | pacman -S igrep 118 | ``` 119 | ### Alpine Linux 120 | 121 | `igrep` is available for [Alpine Edge](https://pkgs.alpinelinux.org/packages?name=igrep&branch=edge). It can be installed via [apk](https://wiki.alpinelinux.org/wiki/Alpine_Package_Keeper) after enabling the [testing repository](https://wiki.alpinelinux.org/wiki/Repositories). 122 | 123 | ``` 124 | apk add igrep 125 | ``` 126 | 127 | ### Build from source 128 | Build and install from source using Rust toolchain by running: `cargo install igrep`. 129 | -------------------------------------------------------------------------------- /src/ig/searcher.rs: -------------------------------------------------------------------------------- 1 | use super::{file_entry::FileEntry, sink::MatchesSink, SearchConfig}; 2 | use crate::ig::SortKey; 3 | use grep::{ 4 | matcher::LineTerminator, 5 | regex::RegexMatcherBuilder, 6 | searcher::{BinaryDetection, SearcherBuilder}, 7 | }; 8 | use ignore::WalkBuilder; 9 | use std::cmp::Ordering; 10 | use std::{path::Path, sync::mpsc}; 11 | 12 | pub enum Event { 13 | NewEntry(FileEntry), 14 | SearchingFinished, 15 | Error, 16 | } 17 | 18 | pub fn search(config: SearchConfig, tx: mpsc::Sender) { 19 | std::thread::spawn(move || { 20 | let path_searchers = config 21 | .paths 22 | .clone() 23 | .into_iter() 24 | .map(|path| { 25 | let config = config.clone(); 26 | let tx = tx.clone(); 27 | std::thread::spawn(move || run(&path, config, tx)) 28 | }) 29 | .collect::>(); 30 | 31 | for searcher in path_searchers { 32 | if searcher.join().is_err() { 33 | tx.send(Event::Error).ok(); 34 | return; 35 | } 36 | } 37 | 38 | tx.send(Event::SearchingFinished).ok(); 39 | }); 40 | } 41 | 42 | fn run(path: &Path, config: SearchConfig, tx: mpsc::Sender) { 43 | let grep_searcher = SearcherBuilder::new() 44 | .binary_detection(BinaryDetection::quit(b'\x00')) 45 | .line_terminator(LineTerminator::byte(b'\n')) 46 | .line_number(true) 47 | .multi_line(config.multi_line) 48 | .build(); 49 | 50 | let mut regex_matcher_builder = RegexMatcherBuilder::new(); 51 | regex_matcher_builder 52 | .case_insensitive(config.case_insensitive) 53 | .case_smart(config.case_smart) 54 | .word(config.word_regexp) 55 | .fixed_strings(config.fixed_strings) 56 | .multi_line(config.multi_line); 57 | 58 | // INFO: enable this for non-multiline pattern. 59 | // HACK: without disabling this we will occur the NotAllowed("\n"). 60 | if !config.multi_line { 61 | regex_matcher_builder.line_terminator(Some(b'\n')); 62 | } 63 | let matcher = regex_matcher_builder 64 | .build(&config.pattern) 65 | .expect("Cannot build RegexMatcher"); 66 | 67 | let mut builder = WalkBuilder::new(path); 68 | let walker = builder 69 | .overrides(config.overrides.clone()) 70 | .types(config.types.clone()) 71 | .hidden(!config.search_hidden) 72 | .follow_links(config.follow_links); 73 | 74 | // if no sort is specified the faster parallel search is used 75 | match config.sort_by { 76 | None => { 77 | let walk_parallel = walker.build_parallel(); 78 | 79 | walk_parallel.run(move || { 80 | let tx = tx.clone(); 81 | let matcher = matcher.clone(); 82 | let mut grep_searcher = grep_searcher.clone(); 83 | 84 | Box::new(move |result| { 85 | let dir_entry = match result { 86 | Ok(entry) => { 87 | if !entry.file_type().is_some_and(|ft| ft.is_file()) { 88 | return ignore::WalkState::Continue; 89 | } 90 | entry 91 | } 92 | Err(_) => return ignore::WalkState::Continue, 93 | }; 94 | let mut matches_in_entry = Vec::new(); 95 | let sr = MatchesSink::new(&matcher, &mut matches_in_entry); 96 | grep_searcher 97 | .search_path(&matcher, dir_entry.path(), sr) 98 | .ok(); 99 | 100 | if !matches_in_entry.is_empty() { 101 | tx.send(Event::NewEntry(FileEntry::new( 102 | dir_entry.path().to_string_lossy().into_owned(), 103 | matches_in_entry, 104 | ))) 105 | .ok(); 106 | } 107 | 108 | ignore::WalkState::Continue 109 | }) 110 | }); 111 | } 112 | Some(key) => { 113 | let walk_sorted = 114 | match key { 115 | SortKey::Path => walker.sort_by_file_name(|a, b| a.cmp(b)), 116 | SortKey::PathReversed => walker.sort_by_file_name(|a, b| b.cmp(a)), 117 | SortKey::Modified => walker 118 | .sort_by_file_path(|a, b| compare_metadata(a, b, |m| m.modified(), false)), 119 | SortKey::ModifiedReversed => walker 120 | .sort_by_file_path(|a, b| compare_metadata(a, b, |m| m.modified(), true)), 121 | SortKey::Created => walker 122 | .sort_by_file_path(|a, b| compare_metadata(a, b, |m| m.created(), false)), 123 | SortKey::CreatedReversed => walker 124 | .sort_by_file_path(|a, b| compare_metadata(a, b, |m| m.created(), true)), 125 | SortKey::Accessed => walker 126 | .sort_by_file_path(|a, b| compare_metadata(a, b, |m| m.accessed(), false)), 127 | SortKey::AccessedReversed => walker 128 | .sort_by_file_path(|a, b| compare_metadata(a, b, |m| m.accessed(), true)), 129 | }; 130 | 131 | for result in walk_sorted.build() { 132 | let tx = tx.clone(); 133 | let matcher = matcher.clone(); 134 | let mut grep_searcher = grep_searcher.clone(); 135 | 136 | let dir_entry = match result { 137 | Ok(entry) => { 138 | if !entry.file_type().is_some_and(|ft| ft.is_file()) { 139 | continue; 140 | } 141 | entry 142 | } 143 | Err(_) => continue, 144 | }; 145 | let mut matches_in_entry = Vec::new(); 146 | let sr = MatchesSink::new(&matcher, &mut matches_in_entry); 147 | grep_searcher 148 | .search_path(&matcher, dir_entry.path(), sr) 149 | .ok(); 150 | 151 | if !matches_in_entry.is_empty() { 152 | tx.send(Event::NewEntry(FileEntry::new( 153 | dir_entry.path().to_string_lossy().into_owned(), 154 | matches_in_entry, 155 | ))) 156 | .ok(); 157 | } 158 | 159 | continue; 160 | } 161 | } 162 | } 163 | } 164 | 165 | fn compare_metadata(lhs: &Path, rhs: &Path, extractor: F, reversed: bool) -> Ordering 166 | where 167 | F: Fn(&std::fs::Metadata) -> std::io::Result, 168 | T: Ord, 169 | { 170 | let metadata_lhs = lhs.metadata().expect("cannot get metadata from file"); 171 | let metadata_rhs = rhs.metadata().expect("cannot get metadata from file"); 172 | let time_lhs = extractor(&metadata_lhs).expect("cannot get time of file"); 173 | let time_rhs = extractor(&metadata_rhs).expect("cannot get time of file"); 174 | if reversed { 175 | time_rhs.cmp(&time_lhs) 176 | } else { 177 | time_lhs.cmp(&time_rhs) 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/ui/scroll_offset_list.rs: -------------------------------------------------------------------------------- 1 | use ratatui::{ 2 | buffer::Buffer, 3 | layout::{Corner, Rect}, 4 | style::Style, 5 | text::Text, 6 | widgets::{Block, StatefulWidget, Widget}, 7 | }; 8 | use std::iter::Iterator; 9 | use unicode_width::UnicodeWidthStr; 10 | 11 | #[derive(Default, Debug, Copy, Clone)] 12 | pub struct ListState { 13 | offset: usize, 14 | selected: Option, 15 | } 16 | 17 | impl ListState { 18 | pub fn select(&mut self, index: Option) { 19 | self.selected = index; 20 | if index.is_none() { 21 | self.offset = 0; 22 | } 23 | } 24 | 25 | pub fn selected(&self) -> Option { 26 | self.selected 27 | } 28 | } 29 | 30 | #[derive(Debug, Clone)] 31 | pub struct ListItem<'a> { 32 | content: Text<'a>, 33 | style: Style, 34 | } 35 | 36 | impl<'a> ListItem<'a> { 37 | pub fn new(content: T) -> ListItem<'a> 38 | where 39 | T: Into>, 40 | { 41 | ListItem { 42 | content: content.into(), 43 | style: Style::default(), 44 | } 45 | } 46 | 47 | pub fn height(&self) -> usize { 48 | self.content.height() 49 | } 50 | } 51 | 52 | #[derive(Default, Debug, Clone)] 53 | pub struct ScrollOffset { 54 | top: usize, 55 | bottom: usize, 56 | } 57 | 58 | impl ScrollOffset { 59 | pub fn top(mut self, offset: usize) -> Self { 60 | self.top = offset; 61 | self 62 | } 63 | 64 | pub fn bottom(mut self, offset: usize) -> Self { 65 | self.bottom = offset; 66 | self 67 | } 68 | } 69 | 70 | #[derive(Debug, Clone)] 71 | pub struct List<'a> { 72 | block: Option>, 73 | items: Vec>, 74 | /// Style used as a base style for the widget 75 | style: Style, 76 | start_corner: Corner, 77 | /// Style used to render selected item 78 | highlight_style: Style, 79 | /// Symbol in front of the selected item (Shift all items to the right) 80 | highlight_symbol: Option<&'a str>, 81 | scroll_offset: ScrollOffset, 82 | } 83 | 84 | impl<'a> List<'a> { 85 | pub fn new(items: T) -> List<'a> 86 | where 87 | T: Into>>, 88 | { 89 | List { 90 | block: None, 91 | style: Style::default(), 92 | items: items.into(), 93 | start_corner: Corner::TopLeft, 94 | highlight_style: Style::default(), 95 | highlight_symbol: None, 96 | scroll_offset: ScrollOffset::default(), 97 | } 98 | } 99 | 100 | pub fn block(mut self, block: Block<'a>) -> List<'a> { 101 | self.block = Some(block); 102 | self 103 | } 104 | 105 | pub fn style(mut self, style: Style) -> List<'a> { 106 | self.style = style; 107 | self 108 | } 109 | 110 | pub fn highlight_style(mut self, style: Style) -> List<'a> { 111 | self.highlight_style = style; 112 | self 113 | } 114 | 115 | pub fn scroll_offset(mut self, scroll_offset: ScrollOffset) -> List<'a> { 116 | self.scroll_offset = scroll_offset; 117 | self 118 | } 119 | } 120 | 121 | impl StatefulWidget for List<'_> { 122 | type State = ListState; 123 | 124 | fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { 125 | buf.set_style(area, self.style); 126 | let list_area = match self.block.take() { 127 | Some(b) => { 128 | let inner_area = b.inner(area); 129 | b.render(area, buf); 130 | inner_area 131 | } 132 | None => area, 133 | }; 134 | 135 | if list_area.width < 1 || list_area.height < 1 { 136 | return; 137 | } 138 | 139 | if self.items.is_empty() { 140 | return; 141 | } 142 | let list_height = list_area.height as usize; 143 | 144 | let mut start = state.offset; 145 | let mut end = state.offset; 146 | let mut height = 0; 147 | for item in self.items.iter().skip(state.offset) { 148 | if height + item.height() > list_height { 149 | break; 150 | } 151 | height += item.height(); 152 | end += 1; 153 | } 154 | 155 | let selected = state.selected.unwrap_or(0).min(self.items.len() - 1); 156 | while selected >= end { 157 | height = height.saturating_add(self.items[end].height()); 158 | end += 1; 159 | while height > list_height { 160 | height = height.saturating_sub(self.items[start].height()); 161 | start += 1; 162 | } 163 | } 164 | while selected < start { 165 | start -= 1; 166 | height = height.saturating_add(self.items[start].height()); 167 | while height > list_height { 168 | end -= 1; 169 | height = height.saturating_sub(self.items[end].height()); 170 | } 171 | } 172 | state.offset = start; 173 | 174 | if selected - state.offset < self.scroll_offset.top { 175 | state.offset = state.offset.saturating_sub(1); 176 | } 177 | 178 | if selected >= list_height + state.offset - self.scroll_offset.bottom 179 | && selected < height - self.scroll_offset.bottom 180 | { 181 | state.offset += 1; 182 | } 183 | 184 | let highlight_symbol = self.highlight_symbol.unwrap_or(""); 185 | let blank_symbol = " ".repeat(highlight_symbol.width()); 186 | 187 | let mut current_height = 0; 188 | let has_selection = state.selected.is_some(); 189 | for (i, item) in self 190 | .items 191 | .iter_mut() 192 | .enumerate() 193 | .skip(state.offset) 194 | .take(end - start) 195 | { 196 | let (x, y) = match self.start_corner { 197 | Corner::BottomLeft => { 198 | current_height += item.height() as u16; 199 | (list_area.left(), list_area.bottom() - current_height) 200 | } 201 | _ => { 202 | let pos = (list_area.left(), list_area.top() + current_height); 203 | current_height += item.height() as u16; 204 | pos 205 | } 206 | }; 207 | let area = Rect { 208 | x, 209 | y, 210 | width: list_area.width, 211 | height: item.height() as u16, 212 | }; 213 | let item_style = self.style.patch(item.style); 214 | buf.set_style(area, item_style); 215 | 216 | let is_selected = state.selected.map(|s| s == i).unwrap_or(false); 217 | let elem_x = if has_selection { 218 | let symbol = if is_selected { 219 | highlight_symbol 220 | } else { 221 | &blank_symbol 222 | }; 223 | let (x, _) = buf.set_stringn(x, y, symbol, list_area.width as usize, item_style); 224 | x 225 | } else { 226 | x 227 | }; 228 | let max_element_width = (list_area.width - (elem_x - x)) as usize; 229 | for (j, line) in item.content.lines.iter().enumerate() { 230 | buf.set_line(elem_x, y + j as u16, line, max_element_width as u16); 231 | } 232 | if is_selected { 233 | buf.set_style(area, self.highlight_style); 234 | } 235 | } 236 | } 237 | } 238 | 239 | impl Widget for List<'_> { 240 | fn render(self, area: Rect, buf: &mut Buffer) { 241 | let mut state = ListState::default(); 242 | StatefulWidget::render(self, area, buf, &mut state); 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /src/ui/context_viewer.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | borrow::BorrowMut, 3 | cmp::max, 4 | io::BufRead, 5 | path::{Path, PathBuf}, 6 | }; 7 | 8 | use clap::ValueEnum; 9 | use itertools::Itertools; 10 | use ratatui::{ 11 | layout::{Constraint, Direction, Layout, Rect}, 12 | style::{Color, Style}, 13 | text::{Line, Span}, 14 | widgets::{Block, BorderType, Borders, Paragraph}, 15 | Frame, 16 | }; 17 | use syntect::{ 18 | easy::HighlightFile, 19 | highlighting::{self, ThemeSet}, 20 | parsing::SyntaxSet, 21 | }; 22 | 23 | use super::{result_list::ResultList, theme::Theme}; 24 | 25 | #[derive(Default, Clone, Debug, PartialEq, Eq, ValueEnum)] 26 | pub enum ContextViewerPosition { 27 | #[default] 28 | None, 29 | Vertical, 30 | Horizontal, 31 | } 32 | 33 | #[derive(Debug)] 34 | pub struct ContextViewer { 35 | highlighted_file_path: PathBuf, 36 | file_highlighted: Vec>, 37 | syntax_set: SyntaxSet, 38 | theme_set: ThemeSet, 39 | position: ContextViewerPosition, 40 | size: u16, 41 | } 42 | 43 | impl ContextViewer { 44 | const MIN_SIZE: u16 = 20; 45 | const MAX_SIZE: u16 = 80; 46 | const SIZE_CHANGE_DELTA: u16 = 5; 47 | 48 | pub fn new(position: ContextViewerPosition) -> Self { 49 | Self { 50 | highlighted_file_path: Default::default(), 51 | file_highlighted: Default::default(), 52 | syntax_set: SyntaxSet::load_defaults_newlines(), 53 | theme_set: highlighting::ThemeSet::load_defaults(), 54 | position, 55 | size: 50, 56 | } 57 | } 58 | 59 | pub fn toggle_vertical(&mut self) { 60 | match self.position { 61 | ContextViewerPosition::None => self.position = ContextViewerPosition::Vertical, 62 | ContextViewerPosition::Vertical => self.position = ContextViewerPosition::None, 63 | ContextViewerPosition::Horizontal => self.position = ContextViewerPosition::Vertical, 64 | } 65 | } 66 | 67 | pub fn toggle_horizontal(&mut self) { 68 | match self.position { 69 | ContextViewerPosition::None => self.position = ContextViewerPosition::Horizontal, 70 | ContextViewerPosition::Vertical => self.position = ContextViewerPosition::Horizontal, 71 | ContextViewerPosition::Horizontal => self.position = ContextViewerPosition::None, 72 | } 73 | } 74 | 75 | pub fn increase_size(&mut self) { 76 | self.size = (self.size + Self::SIZE_CHANGE_DELTA).min(Self::MAX_SIZE); 77 | } 78 | 79 | pub fn decrease_size(&mut self) { 80 | self.size = (self.size - Self::SIZE_CHANGE_DELTA).max(Self::MIN_SIZE); 81 | } 82 | 83 | pub fn update_if_needed(&mut self, file_path: impl AsRef, theme: &dyn Theme) { 84 | if self.position == ContextViewerPosition::None 85 | || self.highlighted_file_path == file_path.as_ref() 86 | { 87 | return; 88 | } 89 | 90 | self.highlighted_file_path = file_path.as_ref().into(); 91 | self.file_highlighted.clear(); 92 | 93 | let mut highlighter = HighlightFile::new( 94 | file_path, 95 | &self.syntax_set, 96 | &self.theme_set.themes[theme.context_viewer_theme()], 97 | ) 98 | .expect("Failed to create line highlighter"); 99 | let mut line = String::new(); 100 | 101 | while highlighter 102 | .reader 103 | .read_line(&mut line) 104 | .expect("Not valid UTF-8") 105 | > 0 106 | { 107 | let regions: Vec<(highlighting::Style, &str)> = highlighter 108 | .highlight_lines 109 | .highlight_line(&line, &self.syntax_set) 110 | .expect("Failed to highlight line"); 111 | 112 | let span_vec = regions 113 | .into_iter() 114 | .map(|(style, substring)| (style, substring.to_string())) 115 | .collect(); 116 | 117 | self.file_highlighted.push(span_vec); 118 | line.clear(); // read_line appends so we need to clear between lines 119 | } 120 | } 121 | 122 | pub fn split_view(&self, view_area: Rect) -> (Rect, Option) { 123 | match self.position { 124 | ContextViewerPosition::None => (view_area, None), 125 | ContextViewerPosition::Vertical => { 126 | let chunks = Layout::default() 127 | .direction(Direction::Horizontal) 128 | .constraints([ 129 | Constraint::Percentage(100 - self.size), 130 | Constraint::Percentage(self.size), 131 | ]) 132 | .split(view_area); 133 | 134 | let (left, right) = (chunks[0], chunks[1]); 135 | (left, Some(right)) 136 | } 137 | ContextViewerPosition::Horizontal => { 138 | let chunks = Layout::default() 139 | .direction(Direction::Vertical) 140 | .constraints([ 141 | Constraint::Percentage(100 - self.size), 142 | Constraint::Percentage(self.size), 143 | ]) 144 | .split(view_area); 145 | 146 | let (top, bottom) = (chunks[0], chunks[1]); 147 | (top, Some(bottom)) 148 | } 149 | } 150 | } 151 | 152 | pub fn draw(&self, frame: &mut Frame, area: Rect, result_list: &ResultList, theme: &dyn Theme) { 153 | let block_widget = Block::default() 154 | .borders(Borders::ALL) 155 | .border_type(BorderType::Rounded); 156 | 157 | if let Some((_, line_number)) = result_list.get_selected_entry() { 158 | let height = area.height as u64; 159 | let first_line_index = line_number.saturating_sub(height / 2); 160 | 161 | let paragraph_widget = Paragraph::new(self.get_styled_spans( 162 | first_line_index as usize, 163 | height as usize, 164 | area.width as usize, 165 | line_number as usize, 166 | theme, 167 | )) 168 | .block(block_widget); 169 | 170 | frame.render_widget(paragraph_widget, area); 171 | } else { 172 | frame.render_widget(block_widget, area); 173 | } 174 | } 175 | 176 | fn get_styled_spans( 177 | &self, 178 | first_line_index: usize, 179 | height: usize, 180 | width: usize, 181 | match_index: usize, 182 | theme: &dyn Theme, 183 | ) -> Vec> { 184 | let mut styled_spans = self 185 | .file_highlighted 186 | .iter() 187 | .skip(first_line_index.saturating_sub(1)) 188 | .take(height) 189 | .map(|line| { 190 | line.iter() 191 | .map(|(highlight_style, substring)| { 192 | let fg = highlight_style.foreground; 193 | let substring_without_tab = substring.replace('\t', " "); 194 | Span::styled( 195 | substring_without_tab, 196 | Style::default().fg(Color::Rgb(fg.r, fg.g, fg.b)), 197 | ) 198 | }) 199 | .collect_vec() 200 | }) 201 | .map(Line::from) 202 | .collect_vec(); 203 | 204 | let match_offset = match_index - max(first_line_index, 1); 205 | let styled_line = &mut styled_spans[match_offset]; 206 | let line_width = styled_line.width(); 207 | let span_vec = &mut styled_line.spans; 208 | 209 | if line_width < width { 210 | span_vec.push(Span::raw(" ".repeat(width - line_width))); 211 | } 212 | 213 | for span in span_vec.iter_mut() { 214 | let current_style = span.style; 215 | span.borrow_mut().style = current_style.bg(theme.highlight_color()); 216 | } 217 | 218 | styled_spans 219 | } 220 | } 221 | 222 | #[cfg(test)] 223 | mod tests { 224 | use super::*; 225 | use test_case::test_case; 226 | 227 | #[test_case(ContextViewerPosition::None => ContextViewerPosition::Vertical)] 228 | #[test_case(ContextViewerPosition::Vertical => ContextViewerPosition::None)] 229 | #[test_case(ContextViewerPosition::Horizontal => ContextViewerPosition::Vertical)] 230 | fn toggle_vertical(initial_position: ContextViewerPosition) -> ContextViewerPosition { 231 | let mut context_viewer = ContextViewer::new(initial_position); 232 | context_viewer.toggle_vertical(); 233 | context_viewer.position 234 | } 235 | 236 | #[test_case(ContextViewerPosition::None => ContextViewerPosition::Horizontal)] 237 | #[test_case(ContextViewerPosition::Vertical => ContextViewerPosition::Horizontal)] 238 | #[test_case(ContextViewerPosition::Horizontal => ContextViewerPosition::None)] 239 | fn toggle_horizontal(initial_position: ContextViewerPosition) -> ContextViewerPosition { 240 | let mut context_viewer = ContextViewer::new(initial_position); 241 | context_viewer.toggle_horizontal(); 242 | context_viewer.position 243 | } 244 | 245 | #[test] 246 | fn increase_size() { 247 | let mut context_viewer = ContextViewer::new(ContextViewerPosition::None); 248 | let default_size = context_viewer.size; 249 | context_viewer.increase_size(); 250 | assert_eq!( 251 | context_viewer.size, 252 | default_size + ContextViewer::SIZE_CHANGE_DELTA 253 | ); 254 | 255 | context_viewer.size = ContextViewer::MAX_SIZE; 256 | context_viewer.increase_size(); 257 | assert_eq!(context_viewer.size, ContextViewer::MAX_SIZE); 258 | } 259 | 260 | #[test] 261 | fn decrease_size() { 262 | let mut context_viewer = ContextViewer::new(ContextViewerPosition::None); 263 | let default_size = context_viewer.size; 264 | context_viewer.decrease_size(); 265 | assert_eq!( 266 | context_viewer.size, 267 | default_size - ContextViewer::SIZE_CHANGE_DELTA 268 | ); 269 | 270 | context_viewer.size = ContextViewer::MIN_SIZE; 271 | context_viewer.decrease_size(); 272 | assert_eq!(context_viewer.size, ContextViewer::MIN_SIZE); 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /src/app.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | editor::EditorCommand, 3 | ig::{Ig, SearchConfig, SortKey}, 4 | ui::{ 5 | bottom_bar, context_viewer::ContextViewer, input_handler::InputHandler, 6 | keymap_popup::KeymapPopup, result_list::ResultList, search_popup::SearchPopup, 7 | theme::Theme, 8 | }, 9 | }; 10 | use anyhow::Result; 11 | use crossterm::{ 12 | event::{DisableMouseCapture, EnableMouseCapture}, 13 | execute, 14 | terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, 15 | }; 16 | use ratatui::{ 17 | backend::CrosstermBackend, 18 | layout::{Constraint, Direction, Layout}, 19 | Frame, Terminal, 20 | }; 21 | use std::path::PathBuf; 22 | 23 | pub struct App { 24 | search_config: SearchConfig, 25 | ig: Ig, 26 | theme: Box, 27 | result_list: ResultList, 28 | context_viewer: ContextViewer, 29 | search_popup: SearchPopup, 30 | keymap_popup: KeymapPopup, 31 | } 32 | 33 | impl App { 34 | pub fn new( 35 | search_config: SearchConfig, 36 | editor_command: EditorCommand, 37 | context_viewer: ContextViewer, 38 | theme: Box, 39 | ) -> Self { 40 | let theme = theme; 41 | Self { 42 | search_config, 43 | ig: Ig::new(editor_command), 44 | theme, 45 | context_viewer, 46 | result_list: ResultList::default(), 47 | search_popup: SearchPopup::default(), 48 | keymap_popup: KeymapPopup::default(), 49 | } 50 | } 51 | 52 | pub fn run(&mut self) -> Result<()> { 53 | let mut input_handler = InputHandler::default(); 54 | self.ig 55 | .search(self.search_config.clone(), &mut self.result_list); 56 | 57 | loop { 58 | let backend = CrosstermBackend::new(std::io::stdout()); 59 | let mut terminal = Terminal::new(backend)?; 60 | terminal.hide_cursor()?; 61 | 62 | enable_raw_mode()?; 63 | execute!( 64 | terminal.backend_mut(), 65 | // NOTE: This is necessary due to upstream `crossterm` requiring that we "enable" 66 | // mouse handling first, which saves some state that necessary for _disabling_ 67 | // mouse events. 68 | EnableMouseCapture, 69 | EnterAlternateScreen, 70 | DisableMouseCapture 71 | )?; 72 | 73 | while self.ig.is_searching() || self.ig.last_error().is_some() || self.ig.is_idle() { 74 | terminal.draw(|f| Self::draw(f, self, &input_handler))?; 75 | 76 | while let Some(entry) = self.ig.handle_searcher_event() { 77 | self.result_list.add_entry(entry); 78 | } 79 | 80 | input_handler.handle_input(self)?; 81 | 82 | if let Some((file_name, _)) = self.result_list.get_selected_entry() { 83 | self.context_viewer 84 | .update_if_needed(PathBuf::from(file_name), self.theme.as_ref()); 85 | } 86 | } 87 | 88 | self.ig 89 | .open_file_if_requested(self.result_list.get_selected_entry()); 90 | 91 | if self.ig.exit_requested() { 92 | execute!(terminal.backend_mut(), LeaveAlternateScreen)?; 93 | disable_raw_mode()?; 94 | break; 95 | } 96 | } 97 | 98 | Ok(()) 99 | } 100 | 101 | fn draw(frame: &mut Frame, app: &mut App, input_handler: &InputHandler) { 102 | let chunks = Layout::default() 103 | .direction(Direction::Vertical) 104 | .constraints([Constraint::Min(1), Constraint::Length(1)].as_ref()) 105 | .split(frame.size()); 106 | 107 | let (view_area, bottom_bar_area) = (chunks[0], chunks[1]); 108 | let (list_area, context_viewer_area) = app.context_viewer.split_view(view_area); 109 | 110 | app.result_list.draw(frame, list_area, app.theme.as_ref()); 111 | 112 | if let Some(cv_area) = context_viewer_area { 113 | app.context_viewer 114 | .draw(frame, cv_area, &app.result_list, app.theme.as_ref()); 115 | } 116 | 117 | bottom_bar::draw( 118 | frame, 119 | bottom_bar_area, 120 | &app.result_list, 121 | &app.ig, 122 | input_handler, 123 | app.theme.as_ref(), 124 | ); 125 | 126 | app.search_popup.draw(frame, app.theme.as_ref()); 127 | app.keymap_popup.draw(frame, app.theme.as_ref()); 128 | } 129 | } 130 | 131 | impl Application for App { 132 | fn is_searching(&self) -> bool { 133 | self.ig.is_searching() 134 | } 135 | 136 | fn on_next_match(&mut self) { 137 | self.result_list.next_match(); 138 | } 139 | 140 | fn on_previous_match(&mut self) { 141 | self.result_list.previous_match(); 142 | } 143 | 144 | fn on_next_file(&mut self) { 145 | self.result_list.next_file(); 146 | } 147 | 148 | fn on_previous_file(&mut self) { 149 | self.result_list.previous_file(); 150 | } 151 | 152 | fn on_top(&mut self) { 153 | self.result_list.top(); 154 | } 155 | 156 | fn on_bottom(&mut self) { 157 | self.result_list.bottom(); 158 | } 159 | 160 | fn on_remove_current_entry(&mut self) { 161 | self.result_list.remove_current_entry(); 162 | } 163 | 164 | fn on_remove_current_file(&mut self) { 165 | self.result_list.remove_current_file(); 166 | } 167 | 168 | fn on_toggle_context_viewer_vertical(&mut self) { 169 | self.context_viewer.toggle_vertical(); 170 | } 171 | 172 | fn on_toggle_context_viewer_horizontal(&mut self) { 173 | self.context_viewer.toggle_horizontal(); 174 | } 175 | 176 | fn on_increase_context_viewer_size(&mut self) { 177 | self.context_viewer.increase_size(); 178 | } 179 | 180 | fn on_decrease_context_viewer_size(&mut self) { 181 | self.context_viewer.decrease_size(); 182 | } 183 | 184 | fn on_toggle_sort_name(&mut self) { 185 | match self.search_config.sort_by { 186 | Some(SortKey::Path) => self.search_config.sort_by = Some(SortKey::PathReversed), 187 | Some(_) => self.search_config.sort_by = Some(SortKey::Path), 188 | None => self.search_config.sort_by = Some(SortKey::Path), 189 | } 190 | self.ig 191 | .search(self.search_config.clone(), &mut self.result_list); 192 | } 193 | 194 | fn on_toggle_sort_mtime(&mut self) { 195 | match self.search_config.sort_by { 196 | Some(SortKey::Modified) => self.search_config.sort_by = Some(SortKey::ModifiedReversed), 197 | Some(_) => self.search_config.sort_by = Some(SortKey::Modified), 198 | None => self.search_config.sort_by = Some(SortKey::Modified), 199 | } 200 | self.ig 201 | .search(self.search_config.clone(), &mut self.result_list); 202 | } 203 | 204 | fn on_toggle_sort_ctime(&mut self) { 205 | match self.search_config.sort_by { 206 | Some(SortKey::Created) => self.search_config.sort_by = Some(SortKey::CreatedReversed), 207 | Some(_) => self.search_config.sort_by = Some(SortKey::Created), 208 | None => self.search_config.sort_by = Some(SortKey::Created), 209 | } 210 | self.ig 211 | .search(self.search_config.clone(), &mut self.result_list); 212 | } 213 | 214 | fn on_toggle_sort_atime(&mut self) { 215 | match self.search_config.sort_by { 216 | Some(SortKey::Modified) => self.search_config.sort_by = Some(SortKey::ModifiedReversed), 217 | Some(_) => self.search_config.sort_by = Some(SortKey::Modified), 218 | None => self.search_config.sort_by = Some(SortKey::Modified), 219 | } 220 | self.ig 221 | .search(self.search_config.clone(), &mut self.result_list); 222 | } 223 | 224 | fn on_open_file(&mut self) { 225 | self.ig.open_file(); 226 | } 227 | 228 | fn on_search(&mut self) { 229 | let pattern = self.search_popup.get_pattern(); 230 | self.search_config.pattern = pattern; 231 | self.ig 232 | .search(self.search_config.clone(), &mut self.result_list); 233 | } 234 | 235 | fn on_exit(&mut self) { 236 | self.ig.exit(); 237 | } 238 | 239 | fn on_toggle_popup(&mut self) { 240 | self.search_popup 241 | .set_pattern(self.search_config.pattern.clone()); 242 | self.search_popup.toggle(); 243 | } 244 | 245 | fn on_char_inserted(&mut self, c: char) { 246 | self.search_popup.insert_char(c); 247 | } 248 | 249 | fn on_char_removed(&mut self) { 250 | self.search_popup.remove_char(); 251 | } 252 | 253 | fn on_char_deleted(&mut self) { 254 | self.search_popup.delete_char(); 255 | } 256 | 257 | fn on_char_left(&mut self) { 258 | self.search_popup.move_cursor_left(); 259 | } 260 | 261 | fn on_char_right(&mut self) { 262 | self.search_popup.move_cursor_right(); 263 | } 264 | 265 | fn on_toggle_keymap(&mut self) { 266 | self.keymap_popup.toggle(); 267 | } 268 | 269 | fn on_keymap_up(&mut self) { 270 | self.keymap_popup.go_up(); 271 | } 272 | 273 | fn on_keymap_down(&mut self) { 274 | self.keymap_popup.go_down(); 275 | } 276 | 277 | fn on_keymap_left(&mut self) { 278 | self.keymap_popup.go_left(); 279 | } 280 | 281 | fn on_keymap_right(&mut self) { 282 | self.keymap_popup.go_right(); 283 | } 284 | } 285 | 286 | #[cfg_attr(test, mockall::automock)] 287 | pub trait Application { 288 | fn is_searching(&self) -> bool; 289 | fn on_next_match(&mut self); 290 | fn on_previous_match(&mut self); 291 | fn on_next_file(&mut self); 292 | fn on_previous_file(&mut self); 293 | fn on_top(&mut self); 294 | fn on_bottom(&mut self); 295 | fn on_remove_current_entry(&mut self); 296 | fn on_remove_current_file(&mut self); 297 | fn on_toggle_context_viewer_vertical(&mut self); 298 | fn on_toggle_context_viewer_horizontal(&mut self); 299 | fn on_increase_context_viewer_size(&mut self); 300 | fn on_decrease_context_viewer_size(&mut self); 301 | fn on_toggle_sort_name(&mut self); 302 | fn on_toggle_sort_mtime(&mut self); 303 | fn on_toggle_sort_ctime(&mut self); 304 | fn on_toggle_sort_atime(&mut self); 305 | fn on_open_file(&mut self); 306 | fn on_search(&mut self); 307 | fn on_exit(&mut self); 308 | fn on_toggle_popup(&mut self); 309 | fn on_char_inserted(&mut self, c: char); 310 | fn on_char_removed(&mut self); 311 | fn on_char_deleted(&mut self); 312 | fn on_char_left(&mut self); 313 | fn on_char_right(&mut self); 314 | fn on_toggle_keymap(&mut self); 315 | fn on_keymap_up(&mut self); 316 | fn on_keymap_down(&mut self); 317 | fn on_keymap_left(&mut self); 318 | fn on_keymap_right(&mut self); 319 | } 320 | -------------------------------------------------------------------------------- /src/editor.rs: -------------------------------------------------------------------------------- 1 | use crate::args::{EDITOR_ENV, IGREP_EDITOR_ENV, VISUAL_ENV}; 2 | use anyhow::{anyhow, Result}; 3 | use clap::ValueEnum; 4 | use itertools::Itertools; 5 | use std::{ 6 | fmt::{self, Debug, Display, Formatter}, 7 | process::{Child, Command}, 8 | }; 9 | use strum::Display; 10 | 11 | #[derive(Display, Default, PartialEq, Eq, Copy, Clone, Debug, ValueEnum)] 12 | #[strum(serialize_all = "lowercase")] 13 | pub enum Editor { 14 | #[default] 15 | Vim, 16 | Neovim, 17 | Nvim, 18 | Nano, 19 | Code, 20 | Vscode, 21 | CodeInsiders, 22 | Emacs, 23 | Emacsclient, 24 | Hx, 25 | Helix, 26 | Subl, 27 | SublimeText, 28 | Micro, 29 | Intellij, 30 | Goland, 31 | Pycharm, 32 | Less, 33 | } 34 | 35 | #[derive(Debug)] 36 | pub enum EditorCommand { 37 | Builtin(Editor), 38 | Custom(String, String), 39 | } 40 | 41 | impl EditorCommand { 42 | pub fn new(custom_command: Option, editor_cli: Option) -> Result { 43 | if let Some(custom_command) = custom_command { 44 | let (program, args) = custom_command.split_once(' ').ok_or( 45 | anyhow!("Expected program and its arguments") 46 | .context(format!("Incorrect editor command: '{custom_command}'")), 47 | )?; 48 | 49 | if args.matches("{file_name}").count() != 1 { 50 | return Err(anyhow!("Expected one occurrence of '{{file_name}}'.") 51 | .context(format!("Incorrect editor command: '{custom_command}'"))); 52 | } 53 | 54 | if args.matches("{line_number}").count() != 1 { 55 | return Err(anyhow!("Expected one occurrence of '{{line_number}}'.") 56 | .context(format!("Incorrect editor command: '{custom_command}'"))); 57 | } 58 | 59 | return Ok(EditorCommand::Custom(program.into(), args.into())); 60 | } 61 | 62 | let add_error_context = |e: String, env_value: String, env_name: &str| { 63 | let possible_variants = Editor::value_variants() 64 | .iter() 65 | .map(Editor::to_string) 66 | .join(", "); 67 | anyhow!(e).context(format!( 68 | "\"{env_value}\" read from ${env_name}, possible variants: [{possible_variants}]", 69 | )) 70 | }; 71 | 72 | let read_from_env = |name| { 73 | std::env::var(name).ok().map(|value| { 74 | Editor::from_str(&extract_editor_name(&value), false) 75 | .map_err(|error| add_error_context(error, value, name)) 76 | }) 77 | }; 78 | 79 | Ok(EditorCommand::Builtin( 80 | editor_cli 81 | .map(Ok) 82 | .or_else(|| read_from_env(IGREP_EDITOR_ENV)) 83 | .or_else(|| read_from_env(VISUAL_ENV)) 84 | .or_else(|| read_from_env(EDITOR_ENV)) 85 | .unwrap_or(Ok(Editor::default()))?, 86 | )) 87 | } 88 | 89 | pub fn spawn(&self, file_name: &str, line_number: u64) -> Result { 90 | let path = which::which(self.program())?; 91 | let mut command = Command::new(path); 92 | command.args(self.args(file_name, line_number)); 93 | command.spawn().map_err(anyhow::Error::from) 94 | } 95 | 96 | fn program(&self) -> &str { 97 | match self { 98 | EditorCommand::Builtin(editor) => match editor { 99 | Editor::Vim => "vim", 100 | Editor::Neovim | Editor::Nvim => "nvim", 101 | Editor::Nano => "nano", 102 | Editor::Code | Editor::Vscode => "code", 103 | Editor::CodeInsiders => "code-insiders", 104 | Editor::Emacs => "emacs", 105 | Editor::Emacsclient => "emacsclient", 106 | Editor::Hx => "hx", 107 | Editor::Helix => "helix", 108 | Editor::Subl | Editor::SublimeText => "subl", 109 | Editor::Micro => "micro", 110 | Editor::Intellij => "idea", 111 | Editor::Goland => "goland", 112 | Editor::Pycharm => "pycharm", 113 | Editor::Less => "less", 114 | }, 115 | EditorCommand::Custom(program, _) => program, 116 | } 117 | } 118 | 119 | fn args(&self, file_name: &str, line_number: u64) -> Box> { 120 | match self { 121 | EditorCommand::Builtin(editor) => match editor { 122 | Editor::Vim 123 | | Editor::Neovim 124 | | Editor::Nvim 125 | | Editor::Nano 126 | | Editor::Micro 127 | | Editor::Less => { 128 | Box::new([format!("+{line_number}"), file_name.into()].into_iter()) 129 | } 130 | Editor::Code | Editor::Vscode | Editor::CodeInsiders => { 131 | Box::new(["-g".into(), format!("{file_name}:{line_number}")].into_iter()) 132 | } 133 | Editor::Emacs | Editor::Emacsclient => Box::new( 134 | ["-nw".into(), format!("+{line_number}"), file_name.into()].into_iter(), 135 | ), 136 | Editor::Hx | Editor::Helix | Editor::Subl | Editor::SublimeText => { 137 | Box::new([format!("{file_name}:{line_number}")].into_iter()) 138 | } 139 | Editor::Intellij | Editor::Goland | Editor::Pycharm => Box::new( 140 | ["--line".into(), format!("{line_number}"), file_name.into()].into_iter(), 141 | ), 142 | }, 143 | EditorCommand::Custom(_, args) => { 144 | let args = args.replace("{file_name}", file_name); 145 | let args = args.replace("{line_number}", &line_number.to_string()); 146 | 147 | let args = args.split_whitespace().map(ToOwned::to_owned).collect_vec(); 148 | Box::new(args.into_iter()) 149 | } 150 | } 151 | } 152 | } 153 | 154 | impl Display for EditorCommand { 155 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 156 | write!(f, "{}", self.program()) 157 | } 158 | } 159 | 160 | fn extract_editor_name(input: &str) -> String { 161 | let mut split = input.rsplit('/'); 162 | split.next().unwrap().into() 163 | } 164 | 165 | #[cfg(test)] 166 | mod tests { 167 | use super::EditorCommand::Builtin; 168 | use super::*; 169 | use crate::args::EditorOpt; 170 | use clap::Parser; 171 | use lazy_static::lazy_static; 172 | use test_case::test_case; 173 | 174 | lazy_static! { 175 | static ref SERIAL_TEST: std::sync::Mutex<()> = Default::default(); 176 | } 177 | 178 | #[test_case("non_builtin_editor" => matches Err(_); "editor name only")] 179 | #[test_case("non_builtin_editor {file_name}" => matches Err(_); "no line number")] 180 | #[test_case("non_builtin_editor {line_number}" => matches Err(_); "no file name")] 181 | #[test_case("non_builtin_editor {file_name} {file_name} {line_number}" => matches Err(_); "file name twice")] 182 | #[test_case("non_builtin_editor {file_name} {line_number} {line_number}" => matches Err(_); "line number twice")] 183 | #[test_case("non_builtin_editor{file_name} {line_number}" => matches Err(_); "program not separated from arg")] 184 | #[test_case("non_builtin_editor {file_name}:{line_number}" => matches Ok(_); "correct command with one arg")] 185 | #[test_case("non_builtin_editor {file_name} {line_number}" => matches Ok(_); "correct command with two args")] 186 | fn parsing_custom_command(command: &str) -> Result { 187 | EditorCommand::new(Some(command.into()), None) 188 | } 189 | 190 | #[test_case(Some("nano"), Some("vim"), None, Some("neovim") => matches Ok(Builtin(Editor::Nano)); "cli")] 191 | #[test_case(None, Some("nano"), None, Some("neovim") => matches Ok(Builtin(Editor::Nano)); "igrep env")] 192 | #[test_case(None, None, Some("nano"), Some("helix") => matches Ok(Builtin(Editor::Nano)); "visual env")] 193 | #[test_case(None, None, None, Some("nano") => matches Ok(Builtin(Editor::Nano)); "editor env")] 194 | #[test_case(Some("unsupported-editor"), None, None, None => matches Err(_); "unsupported cli")] 195 | #[test_case(None, Some("unsupported-editor"), None, None => matches Err(_); "unsupported igrep env")] 196 | #[test_case(None, None, None, Some("unsupported-editor") => matches Err(_); "unsupported editor env")] 197 | #[test_case(None, None, None, None => matches Ok(Builtin(Editor::Vim)); "default editor")] 198 | #[test_case(None, Some("/usr/bin/nano"), None, None => matches Ok(Builtin(Editor::Nano)); "igrep env path")] 199 | #[test_case(None, None, None, Some("/usr/bin/nano") => matches Ok(Builtin(Editor::Nano)); "editor env path")] 200 | fn editor_options_precedence( 201 | cli_option: Option<&str>, 202 | igrep_editor_env: Option<&str>, 203 | visual_env: Option<&str>, 204 | editor_env: Option<&str>, 205 | ) -> Result { 206 | let _guard = SERIAL_TEST.lock().unwrap(); 207 | std::env::remove_var(IGREP_EDITOR_ENV); 208 | std::env::remove_var(VISUAL_ENV); 209 | std::env::remove_var(EDITOR_ENV); 210 | 211 | let opt = if let Some(cli_option) = cli_option { 212 | EditorOpt::try_parse_from(["test", "--editor", cli_option]) 213 | } else { 214 | EditorOpt::try_parse_from(["test"]) 215 | }; 216 | 217 | if let Some(igrep_editor_env) = igrep_editor_env { 218 | std::env::set_var(IGREP_EDITOR_ENV, igrep_editor_env); 219 | } 220 | 221 | if let Some(visual_env) = visual_env { 222 | std::env::set_var(VISUAL_ENV, visual_env); 223 | } 224 | 225 | if let Some(editor_env) = editor_env { 226 | std::env::set_var(EDITOR_ENV, editor_env); 227 | } 228 | 229 | EditorCommand::new(None, opt?.editor) 230 | } 231 | 232 | const FILE_NAME: &str = "file_name"; 233 | const LINE_NUMBER: u64 = 123; 234 | 235 | #[test] 236 | fn custom_command() { 237 | let editor_command = EditorCommand::new( 238 | Some("non_builtin_editor -@{file_name} {line_number}".into()), 239 | None, 240 | ) 241 | .unwrap(); 242 | 243 | assert_eq!(editor_command.program(), "non_builtin_editor"); 244 | assert_eq!( 245 | editor_command.args(FILE_NAME, LINE_NUMBER).collect_vec(), 246 | vec![format!("-@{FILE_NAME}"), LINE_NUMBER.to_string()] 247 | ) 248 | } 249 | 250 | #[test_case(Editor::Vim => format!("vim +{LINE_NUMBER} {FILE_NAME}"); "vim command")] 251 | #[test_case(Editor::Neovim => format!("nvim +{LINE_NUMBER} {FILE_NAME}"); "neovim command")] 252 | #[test_case(Editor::Nvim => format!("nvim +{LINE_NUMBER} {FILE_NAME}"); "nvim command")] 253 | #[test_case(Editor::Nano => format!("nano +{LINE_NUMBER} {FILE_NAME}"); "nano command")] 254 | #[test_case(Editor::Code => format!("code -g {FILE_NAME}:{LINE_NUMBER}"); "code command")] 255 | #[test_case(Editor::Vscode => format!("code -g {FILE_NAME}:{LINE_NUMBER}"); "vscode command")] 256 | #[test_case(Editor::CodeInsiders => format!("code-insiders -g {FILE_NAME}:{LINE_NUMBER}"); "code-insiders command")] 257 | #[test_case(Editor::Emacs => format!("emacs -nw +{LINE_NUMBER} {FILE_NAME}"); "emacs command")] 258 | #[test_case(Editor::Emacsclient => format!("emacsclient -nw +{LINE_NUMBER} {FILE_NAME}"); "emacsclient command")] 259 | #[test_case(Editor::Hx => format!("hx {FILE_NAME}:{LINE_NUMBER}"); "hx command")] 260 | #[test_case(Editor::Helix => format!("helix {FILE_NAME}:{LINE_NUMBER}"); "helix command")] 261 | #[test_case(Editor::Subl => format!("subl {FILE_NAME}:{LINE_NUMBER}"); "subl command")] 262 | #[test_case(Editor::SublimeText => format!("subl {FILE_NAME}:{LINE_NUMBER}"); "sublime text command")] 263 | #[test_case(Editor::Micro => format!("micro +{LINE_NUMBER} {FILE_NAME}"); "micro command")] 264 | #[test_case(Editor::Intellij => format!("idea --line {LINE_NUMBER} {FILE_NAME}"); "intellij command")] 265 | #[test_case(Editor::Goland => format!("goland --line {LINE_NUMBER} {FILE_NAME}"); "goland command")] 266 | #[test_case(Editor::Pycharm => format!("pycharm --line {LINE_NUMBER} {FILE_NAME}"); "pycharm command")] 267 | #[test_case(Editor::Less => format!("less +{LINE_NUMBER} {FILE_NAME}"); "less command")] 268 | fn builtin_editor_command(editor: Editor) -> String { 269 | let editor_command = EditorCommand::new(None, Some(editor)).unwrap(); 270 | format!( 271 | "{} {}", 272 | editor_command.program(), 273 | editor_command.args(FILE_NAME, LINE_NUMBER).join(" ") 274 | ) 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /src/ui/result_list.rs: -------------------------------------------------------------------------------- 1 | use std::cmp; 2 | 3 | use ratatui::{ 4 | layout::Rect, 5 | style::Style, 6 | text::{Line, Span}, 7 | widgets::{Block, BorderType, Borders}, 8 | Frame, 9 | }; 10 | 11 | use crate::ig::file_entry::{EntryType, FileEntry}; 12 | 13 | use super::{ 14 | scroll_offset_list::{List, ListItem, ListState, ScrollOffset}, 15 | theme::Theme, 16 | }; 17 | 18 | #[derive(Default)] 19 | pub struct ResultList { 20 | entries: Vec, 21 | state: ListState, 22 | file_entries_count: usize, 23 | matches_count: usize, 24 | filtered_matches_count: usize, 25 | } 26 | 27 | impl ResultList { 28 | pub fn add_entry(&mut self, entry: FileEntry) { 29 | self.file_entries_count += 1; 30 | self.matches_count += entry.get_matches_count(); 31 | 32 | self.entries.append(&mut entry.get_entries()); 33 | 34 | if self.state.selected().is_none() { 35 | self.next_match(); 36 | } 37 | } 38 | 39 | pub fn iter(&self) -> std::slice::Iter<'_, EntryType> { 40 | self.entries.iter() 41 | } 42 | 43 | pub fn is_empty(&self) -> bool { 44 | self.entries.is_empty() 45 | } 46 | 47 | pub fn next_match(&mut self) { 48 | if self.is_empty() { 49 | return; 50 | } 51 | 52 | let index = match self.state.selected() { 53 | Some(i) => { 54 | if i == self.entries.len() - 1 { 55 | i 56 | } else { 57 | match self.entries[i + 1] { 58 | EntryType::Header(_) => i + 2, 59 | EntryType::Match(_, _, _) => i + 1, 60 | } 61 | } 62 | } 63 | None => 1, 64 | }; 65 | 66 | self.state.select(Some(index)); 67 | } 68 | 69 | pub fn previous_match(&mut self) { 70 | if self.is_empty() { 71 | return; 72 | } 73 | 74 | let index = match self.state.selected() { 75 | Some(i) => { 76 | if i == 1 { 77 | 1 78 | } else { 79 | match self.entries[i - 1] { 80 | EntryType::Header(_) => i - 2, 81 | EntryType::Match(_, _, _) => i - 1, 82 | } 83 | } 84 | } 85 | None => 1, 86 | }; 87 | 88 | self.state.select(Some(index)); 89 | } 90 | 91 | pub fn next_file(&mut self) { 92 | if self.is_empty() { 93 | return; 94 | } 95 | 96 | let index = match self.state.selected() { 97 | Some(i) => { 98 | let mut next_index = i; 99 | loop { 100 | if next_index == self.entries.len() - 1 { 101 | next_index = i; 102 | break; 103 | } 104 | 105 | next_index += 1; 106 | match self.entries[next_index] { 107 | EntryType::Header(_) => { 108 | next_index += 1; 109 | break; 110 | } 111 | EntryType::Match(_, _, _) => continue, 112 | } 113 | } 114 | next_index 115 | } 116 | None => 1, 117 | }; 118 | 119 | self.state.select(Some(index)); 120 | } 121 | 122 | pub fn previous_file(&mut self) { 123 | if self.is_empty() { 124 | return; 125 | } 126 | 127 | let index = match self.state.selected() { 128 | Some(i) => { 129 | let mut next_index = i; 130 | let mut first_header_visited = false; 131 | loop { 132 | if next_index == 1 { 133 | break; 134 | } 135 | 136 | next_index -= 1; 137 | match self.entries[next_index] { 138 | EntryType::Header(_) => { 139 | if !first_header_visited { 140 | first_header_visited = true; 141 | next_index -= 1; 142 | } else { 143 | next_index += 1; 144 | break; 145 | } 146 | } 147 | EntryType::Match(_, _, _) => continue, 148 | } 149 | } 150 | next_index 151 | } 152 | None => 1, 153 | }; 154 | 155 | self.state.select(Some(index)); 156 | } 157 | 158 | pub fn top(&mut self) { 159 | if self.is_empty() { 160 | return; 161 | } 162 | 163 | self.state.select(Some(1)); 164 | } 165 | 166 | pub fn bottom(&mut self) { 167 | if self.is_empty() { 168 | return; 169 | } 170 | 171 | self.state.select(Some(self.entries.len() - 1)); 172 | } 173 | 174 | pub fn remove_current_entry(&mut self) { 175 | if self.is_empty() { 176 | return; 177 | } 178 | 179 | if self.is_last_match_in_file() { 180 | self.remove_current_file(); 181 | } else { 182 | self.remove_current_entry_and_select_previous(); 183 | } 184 | } 185 | 186 | pub fn remove_current_file(&mut self) { 187 | if self.is_empty() { 188 | return; 189 | } 190 | 191 | let selected_index = self.state.selected().expect("Nothing selected"); 192 | 193 | let mut current_file_header_index = 0; 194 | for index in (0..selected_index).rev() { 195 | if self.is_header(index) { 196 | current_file_header_index = index; 197 | break; 198 | } 199 | } 200 | 201 | let mut next_file_header_index = self.entries.len(); 202 | for index in selected_index..self.entries.len() { 203 | if self.is_header(index) { 204 | next_file_header_index = index; 205 | break; 206 | } 207 | } 208 | 209 | let span = next_file_header_index - current_file_header_index; 210 | for _ in 0..span { 211 | self.entries.remove(current_file_header_index); 212 | } 213 | 214 | self.filtered_matches_count += span - 1; 215 | 216 | if self.entries.is_empty() { 217 | self.state.select(None); 218 | } else if selected_index != 1 { 219 | self.state.select(Some(cmp::max( 220 | current_file_header_index.saturating_sub(1), 221 | 1, 222 | ))); 223 | } 224 | } 225 | 226 | fn is_header(&self, index: usize) -> bool { 227 | matches!(self.entries[index], EntryType::Header(_)) 228 | } 229 | 230 | fn is_last_match_in_file(&self) -> bool { 231 | let current_index = self.state.selected().expect("Nothing selected"); 232 | 233 | self.is_header(current_index - 1) 234 | && (current_index == self.entries.len() - 1 || self.is_header(current_index + 1)) 235 | } 236 | 237 | fn remove_current_entry_and_select_previous(&mut self) { 238 | let selected_index = self.state.selected().expect("Nothing selected"); 239 | self.entries.remove(selected_index); 240 | self.filtered_matches_count += 1; 241 | 242 | if selected_index >= self.entries.len() || self.is_header(selected_index) { 243 | self.state.select(Some(selected_index - 1)); 244 | } 245 | } 246 | 247 | pub fn get_selected_entry(&self) -> Option<(String, u64)> { 248 | match self.state.selected() { 249 | Some(i) => { 250 | let mut line_number: Option = None; 251 | for index in (0..=i).rev() { 252 | match &self.entries[index] { 253 | EntryType::Header(name) => { 254 | return Some(( 255 | name.to_owned(), 256 | line_number.expect("Line number not specified"), 257 | )); 258 | } 259 | EntryType::Match(number, _, _) => { 260 | if line_number.is_none() { 261 | line_number = Some(*number); 262 | } 263 | } 264 | } 265 | } 266 | None 267 | } 268 | None => None, 269 | } 270 | } 271 | 272 | pub fn get_current_match_index(&self) -> usize { 273 | match self.state.selected() { 274 | Some(selected) => { 275 | self.entries 276 | .iter() 277 | .take(selected) 278 | .filter(|&e| matches!(e, EntryType::Match(_, _, _))) 279 | .count() 280 | + 1 281 | } 282 | None => 0, 283 | } 284 | } 285 | 286 | pub fn get_current_number_of_matches(&self) -> usize { 287 | self.entries 288 | .iter() 289 | .filter(|&e| matches!(e, EntryType::Match(_, _, _))) 290 | .count() 291 | } 292 | 293 | pub fn get_total_number_of_matches(&self) -> usize { 294 | self.matches_count 295 | } 296 | 297 | pub fn get_total_number_of_file_entries(&self) -> usize { 298 | self.file_entries_count 299 | } 300 | 301 | pub fn get_filtered_matches_count(&self) -> usize { 302 | self.filtered_matches_count 303 | } 304 | 305 | pub fn draw(&mut self, frame: &mut Frame, area: Rect, theme: &dyn Theme) { 306 | let files_list: Vec = self 307 | .iter() 308 | .map(|e| match e { 309 | EntryType::Header(h) => { 310 | let h = h.trim_start_matches("./"); 311 | ListItem::new(Span::styled(h, theme.file_path_color())) 312 | } 313 | EntryType::Match(n, t, offsets) => { 314 | let line_number = Span::styled(format!(" {n}: "), theme.line_number_color()); 315 | 316 | let mut spans = vec![line_number]; 317 | 318 | let mut current_position = 0; 319 | for offset in offsets { 320 | let before_match = 321 | Span::styled(&t[current_position..offset.0], theme.list_font_color()); 322 | let actual_match = 323 | Span::styled(&t[offset.0..offset.1], theme.match_color()); 324 | 325 | // set current position to the end of current match 326 | current_position = offset.1; 327 | 328 | spans.push(before_match); 329 | spans.push(actual_match); 330 | } 331 | 332 | // push remaining text of a line 333 | spans.push(Span::styled( 334 | &t[current_position..], 335 | theme.list_font_color(), 336 | )); 337 | 338 | ListItem::new(Line::from(spans)) 339 | } 340 | }) 341 | .collect(); 342 | 343 | let list_widget = List::new(files_list) 344 | .block( 345 | Block::default() 346 | .borders(Borders::ALL) 347 | .border_type(BorderType::Rounded), 348 | ) 349 | .style(theme.background_color()) 350 | .highlight_style(Style::default().bg(theme.highlight_color())) 351 | .scroll_offset(ScrollOffset::default().top(1).bottom(0)); 352 | 353 | let mut state = self.state; 354 | frame.render_stateful_widget(list_widget, area, &mut state); 355 | self.state = state; 356 | } 357 | } 358 | 359 | #[cfg(test)] 360 | mod tests { 361 | use crate::ig::grep_match::GrepMatch; 362 | 363 | use super::*; 364 | 365 | #[test] 366 | fn test_empty_list() { 367 | let mut list = ResultList::default(); 368 | assert_eq!(list.state.selected(), None); 369 | list.next_match(); 370 | assert_eq!(list.state.selected(), None); 371 | list.previous_match(); 372 | assert_eq!(list.state.selected(), None); 373 | } 374 | 375 | #[test] 376 | fn test_add_entry() { 377 | let mut list = ResultList::default(); 378 | list.add_entry(FileEntry::new( 379 | "entry1".into(), 380 | vec![GrepMatch::new(0, "e1m1".into(), vec![])], 381 | )); 382 | assert_eq!(list.entries.len(), 2); 383 | assert_eq!(list.state.selected(), Some(1)); 384 | 385 | list.add_entry(FileEntry::new( 386 | "entry2".into(), 387 | vec![ 388 | GrepMatch::new(0, "e1m2".into(), vec![]), 389 | GrepMatch::new(0, "e2m2".into(), vec![]), 390 | ], 391 | )); 392 | assert_eq!(list.entries.len(), 5); 393 | assert_eq!(list.state.selected(), Some(1)); 394 | } 395 | } 396 | -------------------------------------------------------------------------------- /src/args.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | editor::Editor, 3 | ui::{context_viewer::ContextViewerPosition, theme::ThemeVariant}, 4 | }; 5 | use clap::{CommandFactory, Parser, ValueEnum}; 6 | use std::{ 7 | ffi::OsString, 8 | fs::File, 9 | io::{self, BufRead, BufReader}, 10 | iter::once, 11 | path::PathBuf, 12 | }; 13 | use strum::Display; 14 | 15 | pub const IGREP_CUSTOM_EDITOR_ENV: &str = "IGREP_CUSTOM_EDITOR"; 16 | pub const IGREP_EDITOR_ENV: &str = "IGREP_EDITOR"; 17 | pub const EDITOR_ENV: &str = "EDITOR"; 18 | pub const RIPGREP_CONFIG_PATH_ENV: &str = "RIPGREP_CONFIG_PATH"; 19 | pub const VISUAL_ENV: &str = "VISUAL"; 20 | 21 | #[derive(Parser, Debug)] 22 | #[clap(author, version, about, long_about = None)] 23 | pub struct Args { 24 | /// Regular expression used for searching. 25 | #[arg(group = "pattern_or_type", required = true)] 26 | pub pattern: Option, 27 | /// Files or directories to search. Directories are searched recursively. 28 | /// If not specified, searching starts from current directory. 29 | pub paths: Vec, 30 | #[clap(flatten)] 31 | pub editor: EditorOpt, 32 | /// UI color theme. 33 | #[clap(long, default_value_t = ThemeVariant::Dark)] 34 | pub theme: ThemeVariant, 35 | /// Searches case insensitively. 36 | #[clap(short = 'i', long)] 37 | pub ignore_case: bool, 38 | /// Searches case insensitively if the pattern is all lowercase. 39 | /// Search case sensitively otherwise. 40 | #[clap(short = 'S', long)] 41 | pub smart_case: bool, 42 | /// Search hidden files and directories. 43 | /// By default, hidden files and directories are skipped. 44 | #[clap(short = '.', long = "hidden")] 45 | pub search_hidden: bool, 46 | /// Follow symbolic links while traversing directories. 47 | #[clap(short = 'L', long = "follow")] 48 | pub follow_links: bool, 49 | /// Only show matches surrounded by word boundaries. 50 | #[clap(short = 'w', long = "word-regexp")] 51 | pub word_regexp: bool, 52 | /// Exact matches with no regex. Useful when searching for a string full of delimiters. 53 | #[clap(short = 'F', long = "fixed-strings")] 54 | pub fixed_strings: bool, 55 | /// Search with pattern contains newline character ('\n'). 56 | #[clap(short = 'U', long = "multiline")] 57 | pub multi_line: bool, 58 | /// Include files and directories for searching that match the given glob. 59 | /// Multiple globs may be provided. 60 | #[clap(short, long)] 61 | pub glob: Vec, 62 | /// Show all supported file types and their corresponding globs. 63 | #[arg(group = "pattern_or_type", required = true)] 64 | #[clap(long)] 65 | pub type_list: bool, 66 | /// Only search files matching TYPE. Multiple types may be provided. 67 | #[clap(short = 't', long = "type")] 68 | pub type_matching: Vec, 69 | /// Do not search files matching TYPE-NOT. Multiple types-not may be provided. 70 | #[clap(short = 'T', long)] 71 | pub type_not: Vec, 72 | /// Context viewer position at startup 73 | #[clap(long, value_enum, default_value_t = ContextViewerPosition::None)] 74 | pub context_viewer: ContextViewerPosition, 75 | /// Sort results, see ripgrep for details 76 | #[clap(long = "sort")] 77 | pub sort_by: Option, 78 | /// Sort results reverse, see ripgrep for details 79 | #[clap(long = "sortr")] 80 | pub sort_by_reverse: Option, 81 | } 82 | 83 | #[derive(Parser, Debug)] 84 | pub struct EditorOpt { 85 | /// Text editor used to open selected match. 86 | #[arg(group = "editor_command")] 87 | #[clap(long)] 88 | pub editor: Option, 89 | 90 | /// Custom command used to open selected match. Must contain {file_name} and {line_number} tokens. 91 | #[arg(group = "editor_command")] 92 | #[clap(long, env = IGREP_CUSTOM_EDITOR_ENV)] 93 | pub custom_command: Option, 94 | } 95 | 96 | #[derive(Clone, ValueEnum, Display, Debug, PartialEq)] 97 | pub enum SortKeyArg { 98 | Path, 99 | Modified, 100 | Created, 101 | Accessed, 102 | } 103 | 104 | impl Args { 105 | pub fn parse_cli_and_config_file() -> Self { 106 | // first validate if CLI arguments are valid 107 | Args::parse_from(std::env::args_os()); 108 | 109 | let mut args_os: Vec<_> = std::env::args_os().collect(); 110 | let to_ignore = args_os 111 | .iter() 112 | .filter_map(|arg| { 113 | let arg = arg.to_str().expect("Not valid UTF-8"); 114 | arg.starts_with('-') 115 | .then(|| arg.trim_start_matches('-').to_owned()) 116 | }) 117 | .collect::>(); 118 | 119 | // then extend them with those from config file 120 | args_os.extend(Self::parse_config_file(to_ignore)); 121 | 122 | Args::parse_from(args_os) 123 | } 124 | 125 | fn parse_config_file(to_ignore: Vec) -> Vec { 126 | match std::env::var_os(RIPGREP_CONFIG_PATH_ENV) { 127 | None => Vec::default(), 128 | Some(config_path) => match File::open(config_path) { 129 | Ok(file) => { 130 | let supported_arguments = Self::collect_supported_arguments(); 131 | let to_ignore = Self::pair_ignored(to_ignore, &supported_arguments); 132 | Self::parse_from_reader(file, supported_arguments, to_ignore) 133 | } 134 | Err(_) => Vec::default(), 135 | }, 136 | } 137 | } 138 | 139 | fn pair_ignored( 140 | to_ignore: Vec, 141 | supported_arguments: &[(Option, Option)], 142 | ) -> Vec { 143 | to_ignore 144 | .iter() 145 | .filter(|i| { 146 | supported_arguments 147 | .iter() 148 | .any(|arg| arg.0.as_ref() == Some(i) || arg.1.as_ref() == Some(i)) 149 | }) 150 | .flat_map(|i| { 151 | match supported_arguments 152 | .iter() 153 | .find(|arg| arg.0.as_ref() == Some(i) || arg.1.as_ref() == Some(i)) 154 | { 155 | Some(arg) => Box::new(once(arg.0.clone()).chain(once(arg.1.clone()))) 156 | as Box>, 157 | None => Box::new(once(None)), 158 | } 159 | }) 160 | .flatten() 161 | .collect() 162 | } 163 | 164 | fn collect_supported_arguments() -> Vec<(Option, Option)> { 165 | Args::command() 166 | .get_arguments() 167 | .filter_map(|arg| match (arg.get_long(), arg.get_short()) { 168 | (None, None) => None, 169 | (l, s) => Some((l.map(|l| l.to_string()), s.map(|s| s.to_string()))), 170 | }) 171 | .collect::>() 172 | } 173 | 174 | fn parse_from_reader( 175 | reader: R, 176 | supported: Vec<(Option, Option)>, 177 | to_ignore: Vec, 178 | ) -> Vec { 179 | let reader = BufReader::new(reader); 180 | let mut ignore_next_line = false; 181 | 182 | reader 183 | .lines() 184 | .filter_map(|line| { 185 | let line = line.expect("Not valid UTF-8"); 186 | let line = line.trim(); 187 | if line.is_empty() || line.starts_with('#') { 188 | return None; 189 | } 190 | 191 | if let Some(long) = line.strip_prefix("--") { 192 | ignore_next_line = false; 193 | let long = long.split_terminator('=').next().expect("Empty line"); 194 | if supported.iter().any(|el| el.0 == Some(long.to_string())) 195 | && !to_ignore.contains(&long.to_owned()) 196 | { 197 | return Some(OsString::from(line)); 198 | } 199 | if !line.contains('=') { 200 | ignore_next_line = true; 201 | } 202 | None 203 | } else if let Some(short) = line.strip_prefix('-') { 204 | ignore_next_line = false; 205 | let short = short.split_terminator('=').next().expect("Empty line"); 206 | if supported.iter().any(|el| el.1 == Some(short.to_string())) 207 | && !to_ignore.contains(&short.to_owned()) 208 | { 209 | return Some(OsString::from(line)); 210 | } 211 | if !line.contains('=') { 212 | ignore_next_line = true; 213 | } 214 | None 215 | } else { 216 | if ignore_next_line { 217 | ignore_next_line = false; 218 | return None; 219 | } 220 | Some(OsString::from(line)) 221 | } 222 | }) 223 | .collect() 224 | } 225 | } 226 | 227 | #[cfg(test)] 228 | mod tests { 229 | use super::*; 230 | use std::collections::HashSet; 231 | 232 | #[test] 233 | fn ripgrep_example_config() { 234 | let supported_args = vec![ 235 | (Some("glob".to_owned()), Some("g".to_owned())), 236 | (Some("smart-case".to_owned()), None), 237 | ]; 238 | let input = "\ 239 | # Don't let ripgrep vomit really long lines to my terminal, and show a preview. 240 | --max-columns=150 241 | --max-columns-preview 242 | 243 | # Add my 'web' type. 244 | --type-add 245 | web:*.{html,css,js}* 246 | 247 | # Using glob patterns to include/exclude files or folders 248 | -g=!git/* 249 | 250 | # or 251 | --glob 252 | !git/* 253 | 254 | # Set the colors. 255 | --colors=line:none 256 | --colors=line:style:bold 257 | 258 | # Because who cares about case!? 259 | --smart-case"; 260 | 261 | let args = Args::parse_from_reader(input.as_bytes(), supported_args, vec![]) 262 | .into_iter() 263 | .map(|s| s.into_string().unwrap()) 264 | .collect::>(); 265 | assert_eq!(args, ["-g=!git/*", "--glob", "!git/*", "--smart-case"]); 266 | } 267 | 268 | #[test] 269 | fn trim_whitespaces() { 270 | let supported_args = vec![(Some("sup".to_owned()), Some("s".to_owned()))]; 271 | 272 | let input = "\ 273 | # comment 274 | --sup=value\n\r\ 275 | -s \n\ 276 | value 277 | --unsup 278 | 279 | # --comment 280 | value 281 | -s"; 282 | let args = Args::parse_from_reader(input.as_bytes(), supported_args, vec![]) 283 | .into_iter() 284 | .map(|s| s.into_string().unwrap()) 285 | .collect::>(); 286 | assert_eq!(args, ["--sup=value", "-s", "value", "-s"]); 287 | } 288 | 289 | #[test] 290 | fn skip_line_after_ignored_option() { 291 | let supported_args = vec![ 292 | (Some("aaa".to_owned()), Some("a".to_owned())), 293 | (Some("bbb".to_owned()), Some("b".to_owned())), 294 | ]; 295 | 296 | let input = "\ 297 | --aaa 298 | value 299 | --bbb 300 | value 301 | "; 302 | let args = Args::parse_from_reader( 303 | input.as_bytes(), 304 | supported_args.clone(), 305 | vec!["aaa".to_owned()], 306 | ) 307 | .into_iter() 308 | .map(|s| s.into_string().unwrap()) 309 | .collect::>(); 310 | assert_eq!(args, ["--bbb", "value"]); 311 | 312 | let input = "\ 313 | -a 314 | value 315 | -b 316 | value 317 | "; 318 | let args = Args::parse_from_reader(input.as_bytes(), supported_args, vec!["a".to_owned()]) 319 | .into_iter() 320 | .map(|s| s.into_string().unwrap()) 321 | .collect::>(); 322 | assert_eq!(args, ["-b", "value"]); 323 | } 324 | 325 | #[test] 326 | fn do_not_skip_line_after_ignored_option_if_value_inline() { 327 | let supported_args = vec![ 328 | (Some("aaa".to_owned()), Some("a".to_owned())), 329 | (Some("bbb".to_owned()), Some("b".to_owned())), 330 | ]; 331 | 332 | let input = "\ 333 | --aaa=value 334 | --bbb 335 | value 336 | "; 337 | let args = Args::parse_from_reader( 338 | input.as_bytes(), 339 | supported_args.clone(), 340 | vec!["aaa".to_owned()], 341 | ) 342 | .into_iter() 343 | .map(|s| s.into_string().unwrap()) 344 | .collect::>(); 345 | assert_eq!(args, ["--bbb", "value"]); 346 | 347 | let input = "\ 348 | -a=value 349 | -b 350 | value 351 | "; 352 | let args = Args::parse_from_reader(input.as_bytes(), supported_args, vec!["a".to_owned()]) 353 | .into_iter() 354 | .map(|s| s.into_string().unwrap()) 355 | .collect::>(); 356 | assert_eq!(args, ["-b", "value"]); 357 | } 358 | 359 | #[test] 360 | fn do_not_skip_line_after_ignored_flag() { 361 | let supported_args = vec![ 362 | (Some("aaa".to_owned()), Some("a".to_owned())), 363 | (Some("bbb".to_owned()), Some("b".to_owned())), 364 | ]; 365 | 366 | let input = "\ 367 | --aaa 368 | --bbb 369 | value 370 | "; 371 | let args = Args::parse_from_reader( 372 | input.as_bytes(), 373 | supported_args.clone(), 374 | vec!["aaa".to_owned()], 375 | ) 376 | .into_iter() 377 | .map(|s| s.into_string().unwrap()) 378 | .collect::>(); 379 | assert_eq!(args, ["--bbb", "value"]); 380 | 381 | let input = "\ 382 | -a 383 | -b 384 | value 385 | "; 386 | let args = Args::parse_from_reader(input.as_bytes(), supported_args, vec!["a".to_owned()]) 387 | .into_iter() 388 | .map(|s| s.into_string().unwrap()) 389 | .collect::>(); 390 | assert_eq!(args, ["-b", "value"]); 391 | } 392 | 393 | #[test] 394 | fn pair_ignored() { 395 | let to_ignore = Args::pair_ignored( 396 | vec![ 397 | "a".to_owned(), 398 | "bbb".to_owned(), 399 | "ddd".to_owned(), 400 | "e".to_owned(), 401 | ], 402 | &vec![ 403 | (Some("aaa".to_owned()), Some("a".to_owned())), 404 | (Some("bbb".to_owned()), Some("b".to_owned())), 405 | (Some("ccc".to_owned()), Some("c".to_owned())), 406 | (Some("ddd".to_owned()), None), 407 | (None, Some("e".to_owned())), 408 | ], 409 | ); 410 | 411 | let extended: HashSet = HashSet::from_iter(to_ignore); 412 | let expected: HashSet = HashSet::from([ 413 | "aaa".to_owned(), 414 | "a".to_owned(), 415 | "bbb".to_owned(), 416 | "b".to_owned(), 417 | "ddd".to_owned(), 418 | "e".to_owned(), 419 | ]); 420 | 421 | assert_eq!(extended, expected); 422 | } 423 | } 424 | -------------------------------------------------------------------------------- /src/ui/input_handler.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use crossterm::event::{poll, read, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; 3 | use std::time::Duration; 4 | 5 | use crate::app::Application; 6 | 7 | #[derive(Default)] 8 | pub struct InputHandler { 9 | input_buffer: String, 10 | input_state: InputState, 11 | input_mode: InputMode, 12 | } 13 | 14 | #[derive(Clone, Debug, Default, PartialEq, Eq)] 15 | pub enum InputState { 16 | #[default] 17 | Valid, 18 | Incomplete(String), 19 | Invalid(String), 20 | } 21 | 22 | #[derive(Clone, Debug, Default, PartialEq, Eq)] 23 | pub enum InputMode { 24 | #[default] 25 | Normal, 26 | TextInsertion, 27 | Keymap, 28 | } 29 | 30 | impl InputHandler { 31 | pub fn handle_input(&mut self, app: &mut A) -> Result<()> { 32 | let poll_timeout = if app.is_searching() { 33 | Duration::from_millis(1) 34 | } else { 35 | Duration::from_millis(100) 36 | }; 37 | 38 | if poll(poll_timeout)? { 39 | let read_event = read()?; 40 | if let Event::Key(key_event) = read_event { 41 | // The following line needs to be amended if and when enabling the 42 | // `KeyboardEnhancementFlags::REPORT_EVENT_TYPES` flag on unix. 43 | let event_kind_enabled = cfg!(target_family = "windows"); 44 | let process_event = !event_kind_enabled || key_event.kind != KeyEventKind::Release; 45 | 46 | if process_event { 47 | match self.input_mode { 48 | InputMode::Normal => self.handle_key_in_normal_mode(key_event, app), 49 | InputMode::TextInsertion => { 50 | self.handle_key_in_text_insertion_mode(key_event, app) 51 | } 52 | InputMode::Keymap => self.handle_key_in_keymap_mode(key_event, app), 53 | } 54 | } 55 | } 56 | } 57 | 58 | Ok(()) 59 | } 60 | 61 | fn handle_key_in_normal_mode(&mut self, key_event: KeyEvent, app: &mut A) { 62 | match key_event { 63 | KeyEvent { 64 | code: KeyCode::Char('c'), 65 | modifiers: KeyModifiers::CONTROL, 66 | .. 67 | } => app.on_exit(), 68 | KeyEvent { 69 | code: KeyCode::Char(character), 70 | .. 71 | } => self.handle_char_input(character, app), 72 | _ => self.handle_non_char_input(key_event.code, app), 73 | } 74 | } 75 | 76 | fn handle_key_in_text_insertion_mode( 77 | &mut self, 78 | key_event: KeyEvent, 79 | app: &mut A, 80 | ) { 81 | match key_event { 82 | KeyEvent { 83 | code: KeyCode::Esc, .. 84 | } 85 | | KeyEvent { 86 | code: KeyCode::Char('c'), 87 | modifiers: KeyModifiers::CONTROL, 88 | .. 89 | } 90 | | KeyEvent { 91 | code: KeyCode::F(5), 92 | .. 93 | } => { 94 | self.input_mode = InputMode::Normal; 95 | app.on_toggle_popup(); 96 | } 97 | KeyEvent { 98 | code: KeyCode::Char(c), 99 | modifiers: modifier, 100 | .. 101 | } => { 102 | if modifier == KeyModifiers::SHIFT { 103 | app.on_char_inserted(c.to_ascii_uppercase()); 104 | } else if modifier == KeyModifiers::NONE { 105 | app.on_char_inserted(c); 106 | } 107 | } 108 | KeyEvent { 109 | code: KeyCode::Backspace, 110 | .. 111 | } => app.on_char_removed(), 112 | KeyEvent { 113 | code: KeyCode::Delete, 114 | .. 115 | } => app.on_char_deleted(), 116 | KeyEvent { 117 | code: KeyCode::Left, 118 | .. 119 | } => app.on_char_left(), 120 | KeyEvent { 121 | code: KeyCode::Right, 122 | .. 123 | } => app.on_char_right(), 124 | KeyEvent { 125 | code: KeyCode::Enter, 126 | .. 127 | } => { 128 | self.input_mode = InputMode::Normal; 129 | app.on_search(); 130 | app.on_toggle_popup(); 131 | } 132 | _ => (), 133 | } 134 | } 135 | 136 | fn handle_key_in_keymap_mode(&mut self, key_event: KeyEvent, app: &mut A) { 137 | match key_event { 138 | KeyEvent { 139 | code: KeyCode::Up, .. 140 | } 141 | | KeyEvent { 142 | code: KeyCode::Char('k'), 143 | .. 144 | } => app.on_keymap_up(), 145 | KeyEvent { 146 | code: KeyCode::Down, 147 | .. 148 | } 149 | | KeyEvent { 150 | code: KeyCode::Char('j'), 151 | .. 152 | } => app.on_keymap_down(), 153 | KeyEvent { 154 | code: KeyCode::Left, 155 | .. 156 | } 157 | | KeyEvent { 158 | code: KeyCode::Char('h'), 159 | .. 160 | } => app.on_keymap_left(), 161 | KeyEvent { 162 | code: KeyCode::Right, 163 | .. 164 | } 165 | | KeyEvent { 166 | code: KeyCode::Char('l'), 167 | .. 168 | } => app.on_keymap_right(), 169 | _ => { 170 | self.input_mode = InputMode::Normal; 171 | app.on_toggle_keymap(); 172 | } 173 | } 174 | } 175 | 176 | fn handle_char_input(&mut self, character: char, app: &mut A) { 177 | self.input_buffer.push(character); 178 | self.input_state = InputState::Valid; 179 | 180 | let consume_buffer_and_execute = |buffer: &mut String, op: &mut dyn FnMut()| { 181 | buffer.clear(); 182 | op(); 183 | }; 184 | 185 | match self.input_buffer.as_str() { 186 | // navigation 187 | "j" => consume_buffer_and_execute(&mut self.input_buffer, &mut || app.on_next_match()), 188 | "k" => { 189 | consume_buffer_and_execute(&mut self.input_buffer, &mut || app.on_previous_match()) 190 | } 191 | "l" => consume_buffer_and_execute(&mut self.input_buffer, &mut || app.on_next_file()), 192 | "h" => { 193 | consume_buffer_and_execute(&mut self.input_buffer, &mut || app.on_previous_file()) 194 | } 195 | "gg" => consume_buffer_and_execute(&mut self.input_buffer, &mut || app.on_top()), 196 | "G" => consume_buffer_and_execute(&mut self.input_buffer, &mut || app.on_bottom()), 197 | // deletion 198 | "dd" => consume_buffer_and_execute(&mut self.input_buffer, &mut || { 199 | app.on_remove_current_entry() 200 | }), 201 | "dw" => consume_buffer_and_execute(&mut self.input_buffer, &mut || { 202 | app.on_remove_current_file() 203 | }), 204 | // viewer 205 | "v" => consume_buffer_and_execute(&mut self.input_buffer, &mut || { 206 | app.on_toggle_context_viewer_vertical() 207 | }), 208 | "s" => consume_buffer_and_execute(&mut self.input_buffer, &mut || { 209 | app.on_toggle_context_viewer_horizontal() 210 | }), 211 | "+" => consume_buffer_and_execute(&mut self.input_buffer, &mut || { 212 | app.on_increase_context_viewer_size() 213 | }), 214 | "-" => consume_buffer_and_execute(&mut self.input_buffer, &mut || { 215 | app.on_decrease_context_viewer_size() 216 | }), 217 | // sort 218 | "n" => consume_buffer_and_execute(&mut self.input_buffer, &mut || { 219 | app.on_toggle_sort_name() 220 | }), 221 | "m" => consume_buffer_and_execute(&mut self.input_buffer, &mut || { 222 | app.on_toggle_sort_mtime() 223 | }), 224 | "a" => consume_buffer_and_execute(&mut self.input_buffer, &mut || { 225 | app.on_toggle_sort_atime() 226 | }), 227 | "c" => consume_buffer_and_execute(&mut self.input_buffer, &mut || { 228 | app.on_toggle_sort_ctime() 229 | }), 230 | // misc 231 | "q" => consume_buffer_and_execute(&mut self.input_buffer, &mut || app.on_exit()), 232 | "?" => { 233 | consume_buffer_and_execute(&mut self.input_buffer, &mut || app.on_toggle_keymap()) 234 | } 235 | "/" => { 236 | self.input_mode = InputMode::TextInsertion; 237 | consume_buffer_and_execute(&mut self.input_buffer, &mut || app.on_toggle_popup()) 238 | } 239 | // buffer for multikey inputs 240 | "g" => self.input_state = InputState::Incomplete("g…".into()), 241 | "d" => self.input_state = InputState::Incomplete("d…".into()), 242 | buf => { 243 | self.input_state = InputState::Invalid(buf.into()); 244 | self.input_buffer.clear(); 245 | } 246 | } 247 | } 248 | 249 | fn handle_non_char_input(&mut self, key_code: KeyCode, app: &mut A) { 250 | self.input_buffer.clear(); 251 | 252 | match key_code { 253 | KeyCode::Down => app.on_next_match(), 254 | KeyCode::Up => app.on_previous_match(), 255 | KeyCode::Right | KeyCode::PageDown => app.on_next_file(), 256 | KeyCode::Left | KeyCode::PageUp => app.on_previous_file(), 257 | KeyCode::Home => app.on_top(), 258 | KeyCode::End => app.on_bottom(), 259 | KeyCode::Delete => app.on_remove_current_entry(), 260 | KeyCode::Enter => app.on_open_file(), 261 | KeyCode::F(1) => { 262 | self.input_mode = InputMode::Keymap; 263 | app.on_toggle_keymap(); 264 | } 265 | KeyCode::F(5) => { 266 | self.input_mode = InputMode::TextInsertion; 267 | app.on_toggle_popup(); 268 | } 269 | KeyCode::Esc => { 270 | if matches!(self.input_state, InputState::Valid) 271 | || matches!(self.input_state, InputState::Invalid(_)) 272 | { 273 | app.on_exit(); 274 | } 275 | } 276 | _ => (), 277 | } 278 | 279 | self.input_state = InputState::Valid; 280 | } 281 | 282 | pub fn get_state(&self) -> &InputState { 283 | &self.input_state 284 | } 285 | } 286 | 287 | #[cfg(test)] 288 | mod tests { 289 | use crate::app::MockApplication; 290 | 291 | use super::*; 292 | use crossterm::event::KeyCode::{Char, Esc}; 293 | use test_case::test_case; 294 | 295 | fn handle_key(key_code: KeyCode, app: &mut A) { 296 | let mut input_handler = InputHandler::default(); 297 | handle(&mut input_handler, key_code, app); 298 | } 299 | 300 | fn handle_key_series(key_codes: &[KeyCode], app: &mut A) { 301 | let mut input_handler = InputHandler::default(); 302 | for key_code in key_codes { 303 | handle(&mut input_handler, *key_code, app); 304 | } 305 | } 306 | 307 | fn handle(input_handler: &mut InputHandler, key_code: KeyCode, app: &mut A) { 308 | match key_code { 309 | Char(character) => input_handler.handle_char_input(character, app), 310 | _ => input_handler.handle_non_char_input(key_code, app), 311 | } 312 | } 313 | 314 | fn handle_key_keymap_mode(key_event: KeyEvent, app: &mut A) { 315 | let mut input_handler = InputHandler { 316 | input_mode: InputMode::Keymap, 317 | ..Default::default() 318 | }; 319 | input_handler.handle_key_in_keymap_mode(key_event, app); 320 | } 321 | 322 | #[test_case(KeyCode::Down; "down")] 323 | #[test_case(Char('j'); "j")] 324 | fn next_match(key_code: KeyCode) { 325 | let mut app_mock = MockApplication::default(); 326 | app_mock.expect_on_next_match().once().return_const(()); 327 | handle_key(key_code, &mut app_mock); 328 | } 329 | 330 | #[test_case(KeyCode::Up; "up")] 331 | #[test_case(Char('k'); "k")] 332 | fn previous_match(key_code: KeyCode) { 333 | let mut app_mock = MockApplication::default(); 334 | app_mock.expect_on_previous_match().once().return_const(()); 335 | handle_key(key_code, &mut app_mock); 336 | } 337 | 338 | #[test_case(KeyCode::Right; "right")] 339 | #[test_case(KeyCode::PageDown; "page down")] 340 | #[test_case(Char('l'); "l")] 341 | fn next_file(key_code: KeyCode) { 342 | let mut app_mock = MockApplication::default(); 343 | app_mock.expect_on_next_file().once().return_const(()); 344 | handle_key(key_code, &mut app_mock); 345 | } 346 | 347 | #[test_case(KeyCode::Left; "left")] 348 | #[test_case(KeyCode::PageUp; "page up")] 349 | #[test_case(Char('h'); "h")] 350 | fn previous_file(key_code: KeyCode) { 351 | let mut app_mock = MockApplication::default(); 352 | app_mock.expect_on_previous_file().once().return_const(()); 353 | handle_key(key_code, &mut app_mock); 354 | } 355 | 356 | #[test_case(&[KeyCode::Home]; "home")] 357 | #[test_case(&[Char('g'), Char('g')]; "gg")] 358 | fn top(key_codes: &[KeyCode]) { 359 | let mut app_mock = MockApplication::default(); 360 | app_mock.expect_on_top().once().return_const(()); 361 | handle_key_series(key_codes, &mut app_mock); 362 | } 363 | 364 | #[test_case(KeyCode::End; "end")] 365 | #[test_case(Char('G'); "G")] 366 | fn bottom(key_code: KeyCode) { 367 | let mut app_mock = MockApplication::default(); 368 | app_mock.expect_on_bottom().once().return_const(()); 369 | handle_key(key_code, &mut app_mock); 370 | } 371 | 372 | #[test_case(&[KeyCode::Delete]; "delete")] 373 | #[test_case(&[Char('d'), Char('d')]; "dd")] 374 | #[test_case(&[Char('g'), Char('d'), Char('w'), Char('d'), Char('d')]; "gdwdd")] 375 | fn remove_current_entry(key_codes: &[KeyCode]) { 376 | let mut app_mock = MockApplication::default(); 377 | app_mock 378 | .expect_on_remove_current_entry() 379 | .once() 380 | .return_const(()); 381 | handle_key_series(key_codes, &mut app_mock); 382 | } 383 | 384 | #[test_case(&[Char('d'), Char('w')]; "dw")] 385 | #[test_case(&[Char('w'), Char('d'), Char('w')]; "wdw")] 386 | fn remove_current_file(key_codes: &[KeyCode]) { 387 | let mut app_mock = MockApplication::default(); 388 | app_mock 389 | .expect_on_remove_current_file() 390 | .once() 391 | .return_const(()); 392 | handle_key_series(key_codes, &mut app_mock); 393 | } 394 | 395 | #[test] 396 | fn toggle_vertical_context_viewer() { 397 | let mut app_mock = MockApplication::default(); 398 | app_mock 399 | .expect_on_toggle_context_viewer_vertical() 400 | .once() 401 | .return_const(()); 402 | handle_key(KeyCode::Char('v'), &mut app_mock); 403 | } 404 | 405 | #[test] 406 | fn toggle_horizontal_context_viewer() { 407 | let mut app_mock = MockApplication::default(); 408 | app_mock 409 | .expect_on_toggle_context_viewer_horizontal() 410 | .once() 411 | .return_const(()); 412 | handle_key(KeyCode::Char('s'), &mut app_mock); 413 | } 414 | 415 | #[test] 416 | fn open_file() { 417 | let mut app_mock = MockApplication::default(); 418 | app_mock.expect_on_open_file().once().return_const(()); 419 | handle_key(KeyCode::Enter, &mut app_mock); 420 | } 421 | 422 | #[test_case(KeyCode::F(5))] 423 | #[test_case(KeyCode::Char('/'))] 424 | fn search(key_code: KeyCode) { 425 | let mut app_mock = MockApplication::default(); 426 | app_mock.expect_on_toggle_popup().once().return_const(()); 427 | handle_key(key_code, &mut app_mock); 428 | } 429 | 430 | #[test_case(KeyCode::F(1))] 431 | #[test_case(KeyCode::Char('?'))] 432 | fn keymap_open(key_code: KeyCode) { 433 | let mut app_mock = MockApplication::default(); 434 | app_mock.expect_on_toggle_keymap().once().return_const(()); 435 | handle_key(key_code, &mut app_mock); 436 | } 437 | 438 | #[test_case(KeyEvent::new(KeyCode::F(1), KeyModifiers::NONE))] 439 | #[test_case(KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE))] 440 | #[test_case(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE))] 441 | #[test_case(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE))] 442 | #[test_case(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL))] 443 | fn keymap_close(event: KeyEvent) { 444 | let mut app_mock = MockApplication::default(); 445 | app_mock.expect_on_toggle_keymap().once().return_const(()); 446 | handle_key_keymap_mode(event, &mut app_mock); 447 | } 448 | 449 | #[test_case(KeyEvent::new(KeyCode::Up, KeyModifiers::NONE))] 450 | #[test_case(KeyEvent::new(KeyCode::Char('k'), KeyModifiers::NONE))] 451 | fn keymap_up(event: KeyEvent) { 452 | let mut app_mock = MockApplication::default(); 453 | app_mock.expect_on_keymap_up().once().return_const(()); 454 | handle_key_keymap_mode(event, &mut app_mock); 455 | } 456 | 457 | #[test_case(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE))] 458 | #[test_case(KeyEvent::new(KeyCode::Char('j'), KeyModifiers::NONE))] 459 | fn keymap_down(event: KeyEvent) { 460 | let mut app_mock = MockApplication::default(); 461 | app_mock.expect_on_keymap_down().once().return_const(()); 462 | handle_key_keymap_mode(event, &mut app_mock); 463 | } 464 | 465 | #[test_case(KeyEvent::new(KeyCode::Left, KeyModifiers::NONE))] 466 | #[test_case(KeyEvent::new(KeyCode::Char('h'), KeyModifiers::NONE))] 467 | fn keymap_left(event: KeyEvent) { 468 | let mut app_mock = MockApplication::default(); 469 | app_mock.expect_on_keymap_left().once().return_const(()); 470 | handle_key_keymap_mode(event, &mut app_mock); 471 | } 472 | 473 | #[test_case(KeyEvent::new(KeyCode::Right, KeyModifiers::NONE))] 474 | #[test_case(KeyEvent::new(KeyCode::Char('l'), KeyModifiers::NONE))] 475 | fn keymap_right(event: KeyEvent) { 476 | let mut app_mock = MockApplication::default(); 477 | app_mock.expect_on_keymap_right().once().return_const(()); 478 | handle_key_keymap_mode(event, &mut app_mock); 479 | } 480 | 481 | #[test_case(&[Char('q')]; "q")] 482 | #[test_case(&[Esc]; "empty input state")] 483 | #[test_case(&[Char('b'), Char('e'), Esc]; "invalid input state")] 484 | #[test_case(&[Char('d'), Esc, Esc]; "clear incomplete state first")] 485 | fn exit(key_codes: &[KeyCode]) { 486 | let mut app_mock = MockApplication::default(); 487 | app_mock.expect_on_exit().once().return_const(()); 488 | handle_key_series(key_codes, &mut app_mock); 489 | } 490 | } 491 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "adler" 7 | version = "1.0.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 10 | 11 | [[package]] 12 | name = "ahash" 13 | version = "0.8.11" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" 16 | dependencies = [ 17 | "cfg-if", 18 | "once_cell", 19 | "version_check", 20 | "zerocopy", 21 | ] 22 | 23 | [[package]] 24 | name = "aho-corasick" 25 | version = "1.1.3" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 28 | dependencies = [ 29 | "memchr", 30 | ] 31 | 32 | [[package]] 33 | name = "allocator-api2" 34 | version = "0.2.18" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" 37 | 38 | [[package]] 39 | name = "anstream" 40 | version = "0.6.14" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" 43 | dependencies = [ 44 | "anstyle", 45 | "anstyle-parse", 46 | "anstyle-query", 47 | "anstyle-wincon", 48 | "colorchoice", 49 | "is_terminal_polyfill", 50 | "utf8parse", 51 | ] 52 | 53 | [[package]] 54 | name = "anstyle" 55 | version = "1.0.7" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" 58 | 59 | [[package]] 60 | name = "anstyle-parse" 61 | version = "0.2.4" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" 64 | dependencies = [ 65 | "utf8parse", 66 | ] 67 | 68 | [[package]] 69 | name = "anstyle-query" 70 | version = "1.0.3" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" 73 | dependencies = [ 74 | "windows-sys 0.52.0", 75 | ] 76 | 77 | [[package]] 78 | name = "anstyle-wincon" 79 | version = "3.0.3" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" 82 | dependencies = [ 83 | "anstyle", 84 | "windows-sys 0.52.0", 85 | ] 86 | 87 | [[package]] 88 | name = "anyhow" 89 | version = "1.0.83" 90 | source = "registry+https://github.com/rust-lang/crates.io-index" 91 | checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3" 92 | 93 | [[package]] 94 | name = "autocfg" 95 | version = "1.3.0" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 98 | 99 | [[package]] 100 | name = "base64" 101 | version = "0.21.7" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" 104 | 105 | [[package]] 106 | name = "bincode" 107 | version = "1.3.3" 108 | source = "registry+https://github.com/rust-lang/crates.io-index" 109 | checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" 110 | dependencies = [ 111 | "serde", 112 | ] 113 | 114 | [[package]] 115 | name = "bitflags" 116 | version = "1.3.2" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 119 | 120 | [[package]] 121 | name = "bitflags" 122 | version = "2.5.0" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" 125 | 126 | [[package]] 127 | name = "bstr" 128 | version = "1.9.1" 129 | source = "registry+https://github.com/rust-lang/crates.io-index" 130 | checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706" 131 | dependencies = [ 132 | "memchr", 133 | "regex-automata", 134 | "serde", 135 | ] 136 | 137 | [[package]] 138 | name = "cassowary" 139 | version = "0.3.0" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" 142 | 143 | [[package]] 144 | name = "castaway" 145 | version = "0.2.2" 146 | source = "registry+https://github.com/rust-lang/crates.io-index" 147 | checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc" 148 | dependencies = [ 149 | "rustversion", 150 | ] 151 | 152 | [[package]] 153 | name = "cc" 154 | version = "1.0.97" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4" 157 | 158 | [[package]] 159 | name = "cfg-if" 160 | version = "1.0.0" 161 | source = "registry+https://github.com/rust-lang/crates.io-index" 162 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 163 | 164 | [[package]] 165 | name = "clap" 166 | version = "4.5.4" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" 169 | dependencies = [ 170 | "clap_builder", 171 | "clap_derive", 172 | ] 173 | 174 | [[package]] 175 | name = "clap_builder" 176 | version = "4.5.2" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" 179 | dependencies = [ 180 | "anstream", 181 | "anstyle", 182 | "clap_lex", 183 | "strsim", 184 | ] 185 | 186 | [[package]] 187 | name = "clap_derive" 188 | version = "4.5.4" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" 191 | dependencies = [ 192 | "heck 0.5.0", 193 | "proc-macro2", 194 | "quote", 195 | "syn", 196 | ] 197 | 198 | [[package]] 199 | name = "clap_lex" 200 | version = "0.7.0" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" 203 | 204 | [[package]] 205 | name = "colorchoice" 206 | version = "1.0.1" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" 209 | 210 | [[package]] 211 | name = "compact_str" 212 | version = "0.7.1" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" 215 | dependencies = [ 216 | "castaway", 217 | "cfg-if", 218 | "itoa", 219 | "ryu", 220 | "static_assertions", 221 | ] 222 | 223 | [[package]] 224 | name = "crc32fast" 225 | version = "1.4.0" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" 228 | dependencies = [ 229 | "cfg-if", 230 | ] 231 | 232 | [[package]] 233 | name = "crossbeam-deque" 234 | version = "0.8.5" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" 237 | dependencies = [ 238 | "crossbeam-epoch", 239 | "crossbeam-utils", 240 | ] 241 | 242 | [[package]] 243 | name = "crossbeam-epoch" 244 | version = "0.9.18" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 247 | dependencies = [ 248 | "crossbeam-utils", 249 | ] 250 | 251 | [[package]] 252 | name = "crossbeam-utils" 253 | version = "0.8.19" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" 256 | 257 | [[package]] 258 | name = "crossterm" 259 | version = "0.27.0" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" 262 | dependencies = [ 263 | "bitflags 2.5.0", 264 | "crossterm_winapi", 265 | "libc", 266 | "mio", 267 | "parking_lot", 268 | "signal-hook", 269 | "signal-hook-mio", 270 | "winapi", 271 | ] 272 | 273 | [[package]] 274 | name = "crossterm_winapi" 275 | version = "0.9.1" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 278 | dependencies = [ 279 | "winapi", 280 | ] 281 | 282 | [[package]] 283 | name = "deranged" 284 | version = "0.3.11" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" 287 | dependencies = [ 288 | "powerfmt", 289 | ] 290 | 291 | [[package]] 292 | name = "downcast" 293 | version = "0.11.0" 294 | source = "registry+https://github.com/rust-lang/crates.io-index" 295 | checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" 296 | 297 | [[package]] 298 | name = "either" 299 | version = "1.11.0" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" 302 | 303 | [[package]] 304 | name = "encoding_rs" 305 | version = "0.8.34" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" 308 | dependencies = [ 309 | "cfg-if", 310 | ] 311 | 312 | [[package]] 313 | name = "encoding_rs_io" 314 | version = "0.1.7" 315 | source = "registry+https://github.com/rust-lang/crates.io-index" 316 | checksum = "1cc3c5651fb62ab8aa3103998dade57efdd028544bd300516baa31840c252a83" 317 | dependencies = [ 318 | "encoding_rs", 319 | ] 320 | 321 | [[package]] 322 | name = "equivalent" 323 | version = "1.0.1" 324 | source = "registry+https://github.com/rust-lang/crates.io-index" 325 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 326 | 327 | [[package]] 328 | name = "errno" 329 | version = "0.3.9" 330 | source = "registry+https://github.com/rust-lang/crates.io-index" 331 | checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" 332 | dependencies = [ 333 | "libc", 334 | "windows-sys 0.52.0", 335 | ] 336 | 337 | [[package]] 338 | name = "flate2" 339 | version = "1.0.30" 340 | source = "registry+https://github.com/rust-lang/crates.io-index" 341 | checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" 342 | dependencies = [ 343 | "crc32fast", 344 | "miniz_oxide", 345 | ] 346 | 347 | [[package]] 348 | name = "fnv" 349 | version = "1.0.7" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 352 | 353 | [[package]] 354 | name = "fragile" 355 | version = "2.0.0" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" 358 | 359 | [[package]] 360 | name = "globset" 361 | version = "0.4.14" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" 364 | dependencies = [ 365 | "aho-corasick", 366 | "bstr", 367 | "log", 368 | "regex-automata", 369 | "regex-syntax", 370 | ] 371 | 372 | [[package]] 373 | name = "grep" 374 | version = "0.3.1" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "6e2b024ec1e686cb64d78beb852030b0e632af93817f1ed25be0173af0e94939" 377 | dependencies = [ 378 | "grep-cli", 379 | "grep-matcher", 380 | "grep-printer", 381 | "grep-regex", 382 | "grep-searcher", 383 | ] 384 | 385 | [[package]] 386 | name = "grep-cli" 387 | version = "0.1.10" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "ea40788c059ab8b622c4d074732750bfb3bd2912e2dd58eabc11798a4d5ad725" 390 | dependencies = [ 391 | "bstr", 392 | "globset", 393 | "libc", 394 | "log", 395 | "termcolor", 396 | "winapi-util", 397 | ] 398 | 399 | [[package]] 400 | name = "grep-matcher" 401 | version = "0.1.7" 402 | source = "registry+https://github.com/rust-lang/crates.io-index" 403 | checksum = "47a3141a10a43acfedc7c98a60a834d7ba00dfe7bec9071cbfc19b55b292ac02" 404 | dependencies = [ 405 | "memchr", 406 | ] 407 | 408 | [[package]] 409 | name = "grep-printer" 410 | version = "0.2.1" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "743c12a03c8aee38b6e5bd0168d8ebb09345751323df4a01c56e792b1f38ceb2" 413 | dependencies = [ 414 | "bstr", 415 | "grep-matcher", 416 | "grep-searcher", 417 | "log", 418 | "serde", 419 | "serde_json", 420 | "termcolor", 421 | ] 422 | 423 | [[package]] 424 | name = "grep-regex" 425 | version = "0.1.12" 426 | source = "registry+https://github.com/rust-lang/crates.io-index" 427 | checksum = "f748bb135ca835da5cbc67ca0e6955f968db9c5df74ca4f56b18e1ddbc68230d" 428 | dependencies = [ 429 | "bstr", 430 | "grep-matcher", 431 | "log", 432 | "regex-automata", 433 | "regex-syntax", 434 | ] 435 | 436 | [[package]] 437 | name = "grep-searcher" 438 | version = "0.1.13" 439 | source = "registry+https://github.com/rust-lang/crates.io-index" 440 | checksum = "ba536ae4f69bec62d8839584dd3153d3028ef31bb229f04e09fb5a9e5a193c54" 441 | dependencies = [ 442 | "bstr", 443 | "encoding_rs", 444 | "encoding_rs_io", 445 | "grep-matcher", 446 | "log", 447 | "memchr", 448 | "memmap2", 449 | ] 450 | 451 | [[package]] 452 | name = "hashbrown" 453 | version = "0.14.5" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 456 | dependencies = [ 457 | "ahash", 458 | "allocator-api2", 459 | ] 460 | 461 | [[package]] 462 | name = "heck" 463 | version = "0.4.1" 464 | source = "registry+https://github.com/rust-lang/crates.io-index" 465 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 466 | 467 | [[package]] 468 | name = "heck" 469 | version = "0.5.0" 470 | source = "registry+https://github.com/rust-lang/crates.io-index" 471 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 472 | 473 | [[package]] 474 | name = "home" 475 | version = "0.5.9" 476 | source = "registry+https://github.com/rust-lang/crates.io-index" 477 | checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" 478 | dependencies = [ 479 | "windows-sys 0.52.0", 480 | ] 481 | 482 | [[package]] 483 | name = "ignore" 484 | version = "0.4.22" 485 | source = "registry+https://github.com/rust-lang/crates.io-index" 486 | checksum = "b46810df39e66e925525d6e38ce1e7f6e1d208f72dc39757880fcb66e2c58af1" 487 | dependencies = [ 488 | "crossbeam-deque", 489 | "globset", 490 | "log", 491 | "memchr", 492 | "regex-automata", 493 | "same-file", 494 | "walkdir", 495 | "winapi-util", 496 | ] 497 | 498 | [[package]] 499 | name = "igrep" 500 | version = "1.3.0" 501 | dependencies = [ 502 | "anyhow", 503 | "clap", 504 | "crossterm", 505 | "grep", 506 | "ignore", 507 | "itertools 0.13.0", 508 | "lazy_static", 509 | "mockall", 510 | "ratatui", 511 | "strum", 512 | "syntect", 513 | "test-case", 514 | "unicode-width", 515 | "which", 516 | ] 517 | 518 | [[package]] 519 | name = "indexmap" 520 | version = "2.2.6" 521 | source = "registry+https://github.com/rust-lang/crates.io-index" 522 | checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" 523 | dependencies = [ 524 | "equivalent", 525 | "hashbrown", 526 | ] 527 | 528 | [[package]] 529 | name = "indoc" 530 | version = "2.0.5" 531 | source = "registry+https://github.com/rust-lang/crates.io-index" 532 | checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" 533 | 534 | [[package]] 535 | name = "is_terminal_polyfill" 536 | version = "1.70.0" 537 | source = "registry+https://github.com/rust-lang/crates.io-index" 538 | checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" 539 | 540 | [[package]] 541 | name = "itertools" 542 | version = "0.12.1" 543 | source = "registry+https://github.com/rust-lang/crates.io-index" 544 | checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" 545 | dependencies = [ 546 | "either", 547 | ] 548 | 549 | [[package]] 550 | name = "itertools" 551 | version = "0.13.0" 552 | source = "registry+https://github.com/rust-lang/crates.io-index" 553 | checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" 554 | dependencies = [ 555 | "either", 556 | ] 557 | 558 | [[package]] 559 | name = "itoa" 560 | version = "1.0.11" 561 | source = "registry+https://github.com/rust-lang/crates.io-index" 562 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 563 | 564 | [[package]] 565 | name = "lazy_static" 566 | version = "1.4.0" 567 | source = "registry+https://github.com/rust-lang/crates.io-index" 568 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 569 | 570 | [[package]] 571 | name = "libc" 572 | version = "0.2.154" 573 | source = "registry+https://github.com/rust-lang/crates.io-index" 574 | checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" 575 | 576 | [[package]] 577 | name = "line-wrap" 578 | version = "0.2.0" 579 | source = "registry+https://github.com/rust-lang/crates.io-index" 580 | checksum = "dd1bc4d24ad230d21fb898d1116b1801d7adfc449d42026475862ab48b11e70e" 581 | 582 | [[package]] 583 | name = "linked-hash-map" 584 | version = "0.5.6" 585 | source = "registry+https://github.com/rust-lang/crates.io-index" 586 | checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" 587 | 588 | [[package]] 589 | name = "linux-raw-sys" 590 | version = "0.4.14" 591 | source = "registry+https://github.com/rust-lang/crates.io-index" 592 | checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" 593 | 594 | [[package]] 595 | name = "lock_api" 596 | version = "0.4.12" 597 | source = "registry+https://github.com/rust-lang/crates.io-index" 598 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 599 | dependencies = [ 600 | "autocfg", 601 | "scopeguard", 602 | ] 603 | 604 | [[package]] 605 | name = "log" 606 | version = "0.4.21" 607 | source = "registry+https://github.com/rust-lang/crates.io-index" 608 | checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" 609 | 610 | [[package]] 611 | name = "lru" 612 | version = "0.12.3" 613 | source = "registry+https://github.com/rust-lang/crates.io-index" 614 | checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" 615 | dependencies = [ 616 | "hashbrown", 617 | ] 618 | 619 | [[package]] 620 | name = "memchr" 621 | version = "2.7.2" 622 | source = "registry+https://github.com/rust-lang/crates.io-index" 623 | checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" 624 | 625 | [[package]] 626 | name = "memmap2" 627 | version = "0.9.4" 628 | source = "registry+https://github.com/rust-lang/crates.io-index" 629 | checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" 630 | dependencies = [ 631 | "libc", 632 | ] 633 | 634 | [[package]] 635 | name = "miniz_oxide" 636 | version = "0.7.2" 637 | source = "registry+https://github.com/rust-lang/crates.io-index" 638 | checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" 639 | dependencies = [ 640 | "adler", 641 | ] 642 | 643 | [[package]] 644 | name = "mio" 645 | version = "0.8.11" 646 | source = "registry+https://github.com/rust-lang/crates.io-index" 647 | checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" 648 | dependencies = [ 649 | "libc", 650 | "log", 651 | "wasi", 652 | "windows-sys 0.48.0", 653 | ] 654 | 655 | [[package]] 656 | name = "mockall" 657 | version = "0.12.1" 658 | source = "registry+https://github.com/rust-lang/crates.io-index" 659 | checksum = "43766c2b5203b10de348ffe19f7e54564b64f3d6018ff7648d1e2d6d3a0f0a48" 660 | dependencies = [ 661 | "cfg-if", 662 | "downcast", 663 | "fragile", 664 | "lazy_static", 665 | "mockall_derive", 666 | "predicates", 667 | "predicates-tree", 668 | ] 669 | 670 | [[package]] 671 | name = "mockall_derive" 672 | version = "0.12.1" 673 | source = "registry+https://github.com/rust-lang/crates.io-index" 674 | checksum = "af7cbce79ec385a1d4f54baa90a76401eb15d9cab93685f62e7e9f942aa00ae2" 675 | dependencies = [ 676 | "cfg-if", 677 | "proc-macro2", 678 | "quote", 679 | "syn", 680 | ] 681 | 682 | [[package]] 683 | name = "num-conv" 684 | version = "0.1.0" 685 | source = "registry+https://github.com/rust-lang/crates.io-index" 686 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 687 | 688 | [[package]] 689 | name = "once_cell" 690 | version = "1.19.0" 691 | source = "registry+https://github.com/rust-lang/crates.io-index" 692 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 693 | 694 | [[package]] 695 | name = "onig" 696 | version = "6.4.0" 697 | source = "registry+https://github.com/rust-lang/crates.io-index" 698 | checksum = "8c4b31c8722ad9171c6d77d3557db078cab2bd50afcc9d09c8b315c59df8ca4f" 699 | dependencies = [ 700 | "bitflags 1.3.2", 701 | "libc", 702 | "once_cell", 703 | "onig_sys", 704 | ] 705 | 706 | [[package]] 707 | name = "onig_sys" 708 | version = "69.8.1" 709 | source = "registry+https://github.com/rust-lang/crates.io-index" 710 | checksum = "7b829e3d7e9cc74c7e315ee8edb185bf4190da5acde74afd7fc59c35b1f086e7" 711 | dependencies = [ 712 | "cc", 713 | "pkg-config", 714 | ] 715 | 716 | [[package]] 717 | name = "parking_lot" 718 | version = "0.12.2" 719 | source = "registry+https://github.com/rust-lang/crates.io-index" 720 | checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" 721 | dependencies = [ 722 | "lock_api", 723 | "parking_lot_core", 724 | ] 725 | 726 | [[package]] 727 | name = "parking_lot_core" 728 | version = "0.9.10" 729 | source = "registry+https://github.com/rust-lang/crates.io-index" 730 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 731 | dependencies = [ 732 | "cfg-if", 733 | "libc", 734 | "redox_syscall", 735 | "smallvec", 736 | "windows-targets 0.52.5", 737 | ] 738 | 739 | [[package]] 740 | name = "paste" 741 | version = "1.0.15" 742 | source = "registry+https://github.com/rust-lang/crates.io-index" 743 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 744 | 745 | [[package]] 746 | name = "pkg-config" 747 | version = "0.3.30" 748 | source = "registry+https://github.com/rust-lang/crates.io-index" 749 | checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" 750 | 751 | [[package]] 752 | name = "plist" 753 | version = "1.6.1" 754 | source = "registry+https://github.com/rust-lang/crates.io-index" 755 | checksum = "d9d34169e64b3c7a80c8621a48adaf44e0cf62c78a9b25dd9dd35f1881a17cf9" 756 | dependencies = [ 757 | "base64", 758 | "indexmap", 759 | "line-wrap", 760 | "quick-xml", 761 | "serde", 762 | "time", 763 | ] 764 | 765 | [[package]] 766 | name = "powerfmt" 767 | version = "0.2.0" 768 | source = "registry+https://github.com/rust-lang/crates.io-index" 769 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 770 | 771 | [[package]] 772 | name = "predicates" 773 | version = "3.1.0" 774 | source = "registry+https://github.com/rust-lang/crates.io-index" 775 | checksum = "68b87bfd4605926cdfefc1c3b5f8fe560e3feca9d5552cf68c466d3d8236c7e8" 776 | dependencies = [ 777 | "anstyle", 778 | "predicates-core", 779 | ] 780 | 781 | [[package]] 782 | name = "predicates-core" 783 | version = "1.0.6" 784 | source = "registry+https://github.com/rust-lang/crates.io-index" 785 | checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" 786 | 787 | [[package]] 788 | name = "predicates-tree" 789 | version = "1.0.9" 790 | source = "registry+https://github.com/rust-lang/crates.io-index" 791 | checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" 792 | dependencies = [ 793 | "predicates-core", 794 | "termtree", 795 | ] 796 | 797 | [[package]] 798 | name = "proc-macro2" 799 | version = "1.0.82" 800 | source = "registry+https://github.com/rust-lang/crates.io-index" 801 | checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b" 802 | dependencies = [ 803 | "unicode-ident", 804 | ] 805 | 806 | [[package]] 807 | name = "quick-xml" 808 | version = "0.31.0" 809 | source = "registry+https://github.com/rust-lang/crates.io-index" 810 | checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" 811 | dependencies = [ 812 | "memchr", 813 | ] 814 | 815 | [[package]] 816 | name = "quote" 817 | version = "1.0.36" 818 | source = "registry+https://github.com/rust-lang/crates.io-index" 819 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 820 | dependencies = [ 821 | "proc-macro2", 822 | ] 823 | 824 | [[package]] 825 | name = "ratatui" 826 | version = "0.26.2" 827 | source = "registry+https://github.com/rust-lang/crates.io-index" 828 | checksum = "a564a852040e82671dc50a37d88f3aa83bbc690dfc6844cfe7a2591620206a80" 829 | dependencies = [ 830 | "bitflags 2.5.0", 831 | "cassowary", 832 | "compact_str", 833 | "crossterm", 834 | "indoc", 835 | "itertools 0.12.1", 836 | "lru", 837 | "paste", 838 | "stability", 839 | "strum", 840 | "unicode-segmentation", 841 | "unicode-width", 842 | ] 843 | 844 | [[package]] 845 | name = "redox_syscall" 846 | version = "0.5.1" 847 | source = "registry+https://github.com/rust-lang/crates.io-index" 848 | checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" 849 | dependencies = [ 850 | "bitflags 2.5.0", 851 | ] 852 | 853 | [[package]] 854 | name = "regex-automata" 855 | version = "0.4.6" 856 | source = "registry+https://github.com/rust-lang/crates.io-index" 857 | checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" 858 | dependencies = [ 859 | "aho-corasick", 860 | "memchr", 861 | "regex-syntax", 862 | ] 863 | 864 | [[package]] 865 | name = "regex-syntax" 866 | version = "0.8.3" 867 | source = "registry+https://github.com/rust-lang/crates.io-index" 868 | checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" 869 | 870 | [[package]] 871 | name = "rustix" 872 | version = "0.38.34" 873 | source = "registry+https://github.com/rust-lang/crates.io-index" 874 | checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" 875 | dependencies = [ 876 | "bitflags 2.5.0", 877 | "errno", 878 | "libc", 879 | "linux-raw-sys", 880 | "windows-sys 0.52.0", 881 | ] 882 | 883 | [[package]] 884 | name = "rustversion" 885 | version = "1.0.17" 886 | source = "registry+https://github.com/rust-lang/crates.io-index" 887 | checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" 888 | 889 | [[package]] 890 | name = "ryu" 891 | version = "1.0.18" 892 | source = "registry+https://github.com/rust-lang/crates.io-index" 893 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 894 | 895 | [[package]] 896 | name = "same-file" 897 | version = "1.0.6" 898 | source = "registry+https://github.com/rust-lang/crates.io-index" 899 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 900 | dependencies = [ 901 | "winapi-util", 902 | ] 903 | 904 | [[package]] 905 | name = "scopeguard" 906 | version = "1.2.0" 907 | source = "registry+https://github.com/rust-lang/crates.io-index" 908 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 909 | 910 | [[package]] 911 | name = "serde" 912 | version = "1.0.202" 913 | source = "registry+https://github.com/rust-lang/crates.io-index" 914 | checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395" 915 | dependencies = [ 916 | "serde_derive", 917 | ] 918 | 919 | [[package]] 920 | name = "serde_derive" 921 | version = "1.0.202" 922 | source = "registry+https://github.com/rust-lang/crates.io-index" 923 | checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" 924 | dependencies = [ 925 | "proc-macro2", 926 | "quote", 927 | "syn", 928 | ] 929 | 930 | [[package]] 931 | name = "serde_json" 932 | version = "1.0.117" 933 | source = "registry+https://github.com/rust-lang/crates.io-index" 934 | checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" 935 | dependencies = [ 936 | "itoa", 937 | "ryu", 938 | "serde", 939 | ] 940 | 941 | [[package]] 942 | name = "signal-hook" 943 | version = "0.3.17" 944 | source = "registry+https://github.com/rust-lang/crates.io-index" 945 | checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" 946 | dependencies = [ 947 | "libc", 948 | "signal-hook-registry", 949 | ] 950 | 951 | [[package]] 952 | name = "signal-hook-mio" 953 | version = "0.2.3" 954 | source = "registry+https://github.com/rust-lang/crates.io-index" 955 | checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" 956 | dependencies = [ 957 | "libc", 958 | "mio", 959 | "signal-hook", 960 | ] 961 | 962 | [[package]] 963 | name = "signal-hook-registry" 964 | version = "1.4.2" 965 | source = "registry+https://github.com/rust-lang/crates.io-index" 966 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 967 | dependencies = [ 968 | "libc", 969 | ] 970 | 971 | [[package]] 972 | name = "smallvec" 973 | version = "1.13.2" 974 | source = "registry+https://github.com/rust-lang/crates.io-index" 975 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 976 | 977 | [[package]] 978 | name = "stability" 979 | version = "0.2.0" 980 | source = "registry+https://github.com/rust-lang/crates.io-index" 981 | checksum = "2ff9eaf853dec4c8802325d8b6d3dffa86cc707fd7a1a4cdbf416e13b061787a" 982 | dependencies = [ 983 | "quote", 984 | "syn", 985 | ] 986 | 987 | [[package]] 988 | name = "static_assertions" 989 | version = "1.1.0" 990 | source = "registry+https://github.com/rust-lang/crates.io-index" 991 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 992 | 993 | [[package]] 994 | name = "strsim" 995 | version = "0.11.1" 996 | source = "registry+https://github.com/rust-lang/crates.io-index" 997 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 998 | 999 | [[package]] 1000 | name = "strum" 1001 | version = "0.26.2" 1002 | source = "registry+https://github.com/rust-lang/crates.io-index" 1003 | checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" 1004 | dependencies = [ 1005 | "strum_macros", 1006 | ] 1007 | 1008 | [[package]] 1009 | name = "strum_macros" 1010 | version = "0.26.2" 1011 | source = "registry+https://github.com/rust-lang/crates.io-index" 1012 | checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" 1013 | dependencies = [ 1014 | "heck 0.4.1", 1015 | "proc-macro2", 1016 | "quote", 1017 | "rustversion", 1018 | "syn", 1019 | ] 1020 | 1021 | [[package]] 1022 | name = "syn" 1023 | version = "2.0.64" 1024 | source = "registry+https://github.com/rust-lang/crates.io-index" 1025 | checksum = "7ad3dee41f36859875573074334c200d1add8e4a87bb37113ebd31d926b7b11f" 1026 | dependencies = [ 1027 | "proc-macro2", 1028 | "quote", 1029 | "unicode-ident", 1030 | ] 1031 | 1032 | [[package]] 1033 | name = "syntect" 1034 | version = "5.2.0" 1035 | source = "registry+https://github.com/rust-lang/crates.io-index" 1036 | checksum = "874dcfa363995604333cf947ae9f751ca3af4522c60886774c4963943b4746b1" 1037 | dependencies = [ 1038 | "bincode", 1039 | "bitflags 1.3.2", 1040 | "flate2", 1041 | "fnv", 1042 | "once_cell", 1043 | "onig", 1044 | "plist", 1045 | "regex-syntax", 1046 | "serde", 1047 | "serde_derive", 1048 | "serde_json", 1049 | "thiserror", 1050 | "walkdir", 1051 | "yaml-rust", 1052 | ] 1053 | 1054 | [[package]] 1055 | name = "termcolor" 1056 | version = "1.4.1" 1057 | source = "registry+https://github.com/rust-lang/crates.io-index" 1058 | checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" 1059 | dependencies = [ 1060 | "winapi-util", 1061 | ] 1062 | 1063 | [[package]] 1064 | name = "termtree" 1065 | version = "0.4.1" 1066 | source = "registry+https://github.com/rust-lang/crates.io-index" 1067 | checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" 1068 | 1069 | [[package]] 1070 | name = "test-case" 1071 | version = "3.3.1" 1072 | source = "registry+https://github.com/rust-lang/crates.io-index" 1073 | checksum = "eb2550dd13afcd286853192af8601920d959b14c401fcece38071d53bf0768a8" 1074 | dependencies = [ 1075 | "test-case-macros", 1076 | ] 1077 | 1078 | [[package]] 1079 | name = "test-case-core" 1080 | version = "3.3.1" 1081 | source = "registry+https://github.com/rust-lang/crates.io-index" 1082 | checksum = "adcb7fd841cd518e279be3d5a3eb0636409487998a4aff22f3de87b81e88384f" 1083 | dependencies = [ 1084 | "cfg-if", 1085 | "proc-macro2", 1086 | "quote", 1087 | "syn", 1088 | ] 1089 | 1090 | [[package]] 1091 | name = "test-case-macros" 1092 | version = "3.3.1" 1093 | source = "registry+https://github.com/rust-lang/crates.io-index" 1094 | checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb" 1095 | dependencies = [ 1096 | "proc-macro2", 1097 | "quote", 1098 | "syn", 1099 | "test-case-core", 1100 | ] 1101 | 1102 | [[package]] 1103 | name = "thiserror" 1104 | version = "1.0.60" 1105 | source = "registry+https://github.com/rust-lang/crates.io-index" 1106 | checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18" 1107 | dependencies = [ 1108 | "thiserror-impl", 1109 | ] 1110 | 1111 | [[package]] 1112 | name = "thiserror-impl" 1113 | version = "1.0.60" 1114 | source = "registry+https://github.com/rust-lang/crates.io-index" 1115 | checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524" 1116 | dependencies = [ 1117 | "proc-macro2", 1118 | "quote", 1119 | "syn", 1120 | ] 1121 | 1122 | [[package]] 1123 | name = "time" 1124 | version = "0.3.36" 1125 | source = "registry+https://github.com/rust-lang/crates.io-index" 1126 | checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" 1127 | dependencies = [ 1128 | "deranged", 1129 | "itoa", 1130 | "num-conv", 1131 | "powerfmt", 1132 | "serde", 1133 | "time-core", 1134 | "time-macros", 1135 | ] 1136 | 1137 | [[package]] 1138 | name = "time-core" 1139 | version = "0.1.2" 1140 | source = "registry+https://github.com/rust-lang/crates.io-index" 1141 | checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" 1142 | 1143 | [[package]] 1144 | name = "time-macros" 1145 | version = "0.2.18" 1146 | source = "registry+https://github.com/rust-lang/crates.io-index" 1147 | checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" 1148 | dependencies = [ 1149 | "num-conv", 1150 | "time-core", 1151 | ] 1152 | 1153 | [[package]] 1154 | name = "unicode-ident" 1155 | version = "1.0.12" 1156 | source = "registry+https://github.com/rust-lang/crates.io-index" 1157 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 1158 | 1159 | [[package]] 1160 | name = "unicode-segmentation" 1161 | version = "1.11.0" 1162 | source = "registry+https://github.com/rust-lang/crates.io-index" 1163 | checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" 1164 | 1165 | [[package]] 1166 | name = "unicode-width" 1167 | version = "0.1.12" 1168 | source = "registry+https://github.com/rust-lang/crates.io-index" 1169 | checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" 1170 | 1171 | [[package]] 1172 | name = "utf8parse" 1173 | version = "0.2.1" 1174 | source = "registry+https://github.com/rust-lang/crates.io-index" 1175 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 1176 | 1177 | [[package]] 1178 | name = "version_check" 1179 | version = "0.9.4" 1180 | source = "registry+https://github.com/rust-lang/crates.io-index" 1181 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 1182 | 1183 | [[package]] 1184 | name = "walkdir" 1185 | version = "2.5.0" 1186 | source = "registry+https://github.com/rust-lang/crates.io-index" 1187 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 1188 | dependencies = [ 1189 | "same-file", 1190 | "winapi-util", 1191 | ] 1192 | 1193 | [[package]] 1194 | name = "wasi" 1195 | version = "0.11.0+wasi-snapshot-preview1" 1196 | source = "registry+https://github.com/rust-lang/crates.io-index" 1197 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1198 | 1199 | [[package]] 1200 | name = "which" 1201 | version = "6.0.3" 1202 | source = "registry+https://github.com/rust-lang/crates.io-index" 1203 | checksum = "b4ee928febd44d98f2f459a4a79bd4d928591333a494a10a868418ac1b39cf1f" 1204 | dependencies = [ 1205 | "either", 1206 | "home", 1207 | "rustix", 1208 | "winsafe", 1209 | ] 1210 | 1211 | [[package]] 1212 | name = "winapi" 1213 | version = "0.3.9" 1214 | source = "registry+https://github.com/rust-lang/crates.io-index" 1215 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1216 | dependencies = [ 1217 | "winapi-i686-pc-windows-gnu", 1218 | "winapi-x86_64-pc-windows-gnu", 1219 | ] 1220 | 1221 | [[package]] 1222 | name = "winapi-i686-pc-windows-gnu" 1223 | version = "0.4.0" 1224 | source = "registry+https://github.com/rust-lang/crates.io-index" 1225 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1226 | 1227 | [[package]] 1228 | name = "winapi-util" 1229 | version = "0.1.8" 1230 | source = "registry+https://github.com/rust-lang/crates.io-index" 1231 | checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" 1232 | dependencies = [ 1233 | "windows-sys 0.52.0", 1234 | ] 1235 | 1236 | [[package]] 1237 | name = "winapi-x86_64-pc-windows-gnu" 1238 | version = "0.4.0" 1239 | source = "registry+https://github.com/rust-lang/crates.io-index" 1240 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1241 | 1242 | [[package]] 1243 | name = "windows-sys" 1244 | version = "0.48.0" 1245 | source = "registry+https://github.com/rust-lang/crates.io-index" 1246 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1247 | dependencies = [ 1248 | "windows-targets 0.48.5", 1249 | ] 1250 | 1251 | [[package]] 1252 | name = "windows-sys" 1253 | version = "0.52.0" 1254 | source = "registry+https://github.com/rust-lang/crates.io-index" 1255 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1256 | dependencies = [ 1257 | "windows-targets 0.52.5", 1258 | ] 1259 | 1260 | [[package]] 1261 | name = "windows-targets" 1262 | version = "0.48.5" 1263 | source = "registry+https://github.com/rust-lang/crates.io-index" 1264 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1265 | dependencies = [ 1266 | "windows_aarch64_gnullvm 0.48.5", 1267 | "windows_aarch64_msvc 0.48.5", 1268 | "windows_i686_gnu 0.48.5", 1269 | "windows_i686_msvc 0.48.5", 1270 | "windows_x86_64_gnu 0.48.5", 1271 | "windows_x86_64_gnullvm 0.48.5", 1272 | "windows_x86_64_msvc 0.48.5", 1273 | ] 1274 | 1275 | [[package]] 1276 | name = "windows-targets" 1277 | version = "0.52.5" 1278 | source = "registry+https://github.com/rust-lang/crates.io-index" 1279 | checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" 1280 | dependencies = [ 1281 | "windows_aarch64_gnullvm 0.52.5", 1282 | "windows_aarch64_msvc 0.52.5", 1283 | "windows_i686_gnu 0.52.5", 1284 | "windows_i686_gnullvm", 1285 | "windows_i686_msvc 0.52.5", 1286 | "windows_x86_64_gnu 0.52.5", 1287 | "windows_x86_64_gnullvm 0.52.5", 1288 | "windows_x86_64_msvc 0.52.5", 1289 | ] 1290 | 1291 | [[package]] 1292 | name = "windows_aarch64_gnullvm" 1293 | version = "0.48.5" 1294 | source = "registry+https://github.com/rust-lang/crates.io-index" 1295 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1296 | 1297 | [[package]] 1298 | name = "windows_aarch64_gnullvm" 1299 | version = "0.52.5" 1300 | source = "registry+https://github.com/rust-lang/crates.io-index" 1301 | checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" 1302 | 1303 | [[package]] 1304 | name = "windows_aarch64_msvc" 1305 | version = "0.48.5" 1306 | source = "registry+https://github.com/rust-lang/crates.io-index" 1307 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1308 | 1309 | [[package]] 1310 | name = "windows_aarch64_msvc" 1311 | version = "0.52.5" 1312 | source = "registry+https://github.com/rust-lang/crates.io-index" 1313 | checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" 1314 | 1315 | [[package]] 1316 | name = "windows_i686_gnu" 1317 | version = "0.48.5" 1318 | source = "registry+https://github.com/rust-lang/crates.io-index" 1319 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1320 | 1321 | [[package]] 1322 | name = "windows_i686_gnu" 1323 | version = "0.52.5" 1324 | source = "registry+https://github.com/rust-lang/crates.io-index" 1325 | checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" 1326 | 1327 | [[package]] 1328 | name = "windows_i686_gnullvm" 1329 | version = "0.52.5" 1330 | source = "registry+https://github.com/rust-lang/crates.io-index" 1331 | checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" 1332 | 1333 | [[package]] 1334 | name = "windows_i686_msvc" 1335 | version = "0.48.5" 1336 | source = "registry+https://github.com/rust-lang/crates.io-index" 1337 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1338 | 1339 | [[package]] 1340 | name = "windows_i686_msvc" 1341 | version = "0.52.5" 1342 | source = "registry+https://github.com/rust-lang/crates.io-index" 1343 | checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" 1344 | 1345 | [[package]] 1346 | name = "windows_x86_64_gnu" 1347 | version = "0.48.5" 1348 | source = "registry+https://github.com/rust-lang/crates.io-index" 1349 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1350 | 1351 | [[package]] 1352 | name = "windows_x86_64_gnu" 1353 | version = "0.52.5" 1354 | source = "registry+https://github.com/rust-lang/crates.io-index" 1355 | checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" 1356 | 1357 | [[package]] 1358 | name = "windows_x86_64_gnullvm" 1359 | version = "0.48.5" 1360 | source = "registry+https://github.com/rust-lang/crates.io-index" 1361 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1362 | 1363 | [[package]] 1364 | name = "windows_x86_64_gnullvm" 1365 | version = "0.52.5" 1366 | source = "registry+https://github.com/rust-lang/crates.io-index" 1367 | checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" 1368 | 1369 | [[package]] 1370 | name = "windows_x86_64_msvc" 1371 | version = "0.48.5" 1372 | source = "registry+https://github.com/rust-lang/crates.io-index" 1373 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1374 | 1375 | [[package]] 1376 | name = "windows_x86_64_msvc" 1377 | version = "0.52.5" 1378 | source = "registry+https://github.com/rust-lang/crates.io-index" 1379 | checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" 1380 | 1381 | [[package]] 1382 | name = "winsafe" 1383 | version = "0.0.19" 1384 | source = "registry+https://github.com/rust-lang/crates.io-index" 1385 | checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" 1386 | 1387 | [[package]] 1388 | name = "yaml-rust" 1389 | version = "0.4.5" 1390 | source = "registry+https://github.com/rust-lang/crates.io-index" 1391 | checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" 1392 | dependencies = [ 1393 | "linked-hash-map", 1394 | ] 1395 | 1396 | [[package]] 1397 | name = "zerocopy" 1398 | version = "0.7.34" 1399 | source = "registry+https://github.com/rust-lang/crates.io-index" 1400 | checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" 1401 | dependencies = [ 1402 | "zerocopy-derive", 1403 | ] 1404 | 1405 | [[package]] 1406 | name = "zerocopy-derive" 1407 | version = "0.7.34" 1408 | source = "registry+https://github.com/rust-lang/crates.io-index" 1409 | checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" 1410 | dependencies = [ 1411 | "proc-macro2", 1412 | "quote", 1413 | "syn", 1414 | ] 1415 | --------------------------------------------------------------------------------