├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── ci.yaml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── auto_pair.rs ├── cursor_style.rs ├── custom_prompt.rs ├── drop_down_auto_complete.rs ├── hex_color_highlighter.rs ├── input_filter.rs ├── key_bindings.rs ├── keyword_highlighter.rs ├── keyword_hinter.rs ├── matching_brackets_highlighters.rs ├── surround_selection.rs ├── text_prompt.rs └── visual_selection.rs ├── media ├── auto_complete_demo.gif └── line_editor_demo.gif └── src ├── autopair └── mod.rs ├── completion └── mod.rs ├── core ├── editor.rs ├── event.rs ├── input_filter.rs ├── keybindings.rs ├── mod.rs ├── style.rs └── styled_buffer.rs ├── engine.rs ├── highlighter └── mod.rs ├── hinter └── mod.rs ├── lib.rs ├── prompt └── mod.rs └── view ├── base.rs ├── drop_down_list_view.rs ├── list_view.rs ├── mod.rs └── styled_editor_view.rs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: amrdeveloper -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 1. Go to '...' 15 | 2. Click on '....' 16 | 3. Scroll down to '....' 17 | 4. See error 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Screenshots** 23 | If applicable, add screenshots to help explain your problem. 24 | 25 | **LineEditor (please complete the following information):** 26 | - Version [e.g. 1.0.0] 27 | 28 | **Additional context** 29 | Add any other context about the problem here. 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | env: 12 | CARGO_TERM_COLOR: always 13 | 14 | jobs: 15 | fmt: 16 | name: Rustfmt 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v3 20 | - uses: dtolnay/rust-toolchain@stable 21 | - run: cargo fmt --all -- --check 22 | 23 | clippy: 24 | name: Clippy 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v3 28 | - uses: dtolnay/rust-toolchain@stable 29 | - run: cargo clippy -- -D warnings 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Change Log 2 | ========== 3 | 4 | Version 0.4.0 *(2024-10-26)* 5 | ----------------------------- 6 | 7 | - Fix Paste Edit command 8 | 9 | Version 0.3.0 *(2024-10-26)* 10 | ----------------------------- 11 | 12 | * Update Crossterm version. 13 | * Fix clearing the buffer after success command. 14 | 15 | Version 0.2.0 *(2023-12-29)* 16 | ----------------------------- 17 | 18 | * Implement Cut, Copy, Paste. 19 | * Supports InputFilter with Mixed and Custom rules. 20 | * Implement ListView, DropDownListView. 21 | 22 | Version 0.1.0 *(2023-12-21)* 23 | ----------------------------- 24 | 25 | * Syntax highlighter 26 | * Hinter 27 | * Auto Pair complete 28 | * Key bindings 29 | * Custom Prompt 30 | * Visual Selection -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | 3 | ### Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ### Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ### Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ### Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ### Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at amr96@programmer.net. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ### Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Please provide issue report in the format that we request, EACH DETAIL IS A HUGE HELP. 4 | 5 | Issues that are not following the guidelines, 6 | will be processed as last priority or never or simply closed as invalid. 7 | 8 | ## Contributing Guide 9 | 10 | Please note that PRs are looked only for approved issues. -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lineeditor" 3 | version = "0.4.0" 4 | edition = "2021" 5 | repository = "https://github.com/amrdeveloper/lineeditor" 6 | license = "MIT" 7 | authors = ["AmrDeveloper"] 8 | description = "A cross platform rich line editor" 9 | readme = "README.md" 10 | keywords = ["line-editor", "gitql", "cli", "rich-editor"] 11 | categories = ["command-line-utilities"] 12 | 13 | [lib] 14 | doctest = true 15 | 16 | [dependencies] 17 | crossterm = "0.28.1" 18 | clipboard = "0.5.0" 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Amr Hesham 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

LineEditor


2 | 3 |

4 | Crates.io 5 | Deps 6 | GitHub release 7 | GitHub issues 8 | GitHub 9 |

10 | 11 |

12 | A new cross platform Line editor implementation inspired by reedline, rustyline and other cool line editors, designed with the goal to provides the core components that allows you to build your custom line editor with as mush as customization options. 13 |

14 | 15 |

16 | animated 17 |

18 | 19 |

20 | animated 21 |

22 | 23 | > [!IMPORTANT] 24 | > LineEditor API is still experimental and may be changed from version to version until finishing the design and implementations of the basics features 25 | 26 | ### Basic example 27 | 28 | ```rs 29 | let prompt = StringPrompt::new("prompt> ".to_string()); 30 | let line_editor = LineEditor::new(Box::new(prompt)); 31 | match line_editor.read_line() { 32 | Ok(LineEditorResult::Success(buffer)) => { 33 | 34 | } 35 | _ => {} 36 | } 37 | ``` 38 | 39 | ### Customization examples 40 | - [Text Prompt](/examples/text_prompt.rs) 41 | - [Custom Prompt](/examples/custom_prompt.rs) 42 | - [Cursor style](/examples/cursor_style.rs) 43 | - [Input Filter](/examples/input_filter.rs) 44 | - [Key bindings](/examples/key_bindings.rs) 45 | - [DropDown AutoComplete](/examples/drop_down_auto_complete.rs) 46 | - [Keywords Highlighter](/examples/keyword_highlighter.rs) 47 | - [Matching Brackets Highlighter](/examples/matching_brackets_highlighter.rs) 48 | - [Hex Color Highlighter](/examples/hex_color_highlighter.rs) 49 | - [Keywords Hinter](/examples/keyword_hinter.rs) 50 | - [Auto Pair complete](/examples/auto_pair.rs) 51 | - [Visual Selection](/examples/visual_selection.rs) 52 | - [Auto Surround Selection](/examples/surround_selection.rs) 53 | 54 | ### License 55 | ``` 56 | MIT License 57 | 58 | Copyright (c) 2023 Amr Hesham 59 | 60 | Permission is hereby granted, free of charge, to any person obtaining a copy 61 | of this software and associated documentation files (the "Software"), to deal 62 | in the Software without restriction, including without limitation the rights 63 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 64 | copies of the Software, and to permit persons to whom the Software is 65 | furnished to do so, subject to the following conditions: 66 | 67 | The above copyright notice and this permission notice shall be included in all 68 | copies or substantial portions of the Software. 69 | 70 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 71 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 72 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 73 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 74 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 75 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 76 | SOFTWARE. 77 | ``` -------------------------------------------------------------------------------- /examples/auto_pair.rs: -------------------------------------------------------------------------------- 1 | use lineeditor::DefaultAutoPair; 2 | use lineeditor::LineEditor; 3 | use lineeditor::LineEditorResult; 4 | use lineeditor::StringPrompt; 5 | 6 | fn main() { 7 | let prompt = StringPrompt::new("prompt> ".to_string()); 8 | let mut line_editor = LineEditor::new(Box::new(prompt)); 9 | line_editor.set_auto_pair(Some(Box::::default())); 10 | 11 | let bindings = line_editor.keybinding(); 12 | bindings.register_common_control_bindings(); 13 | 14 | match line_editor.read_line() { 15 | Ok(LineEditorResult::Success(line)) => { 16 | println!("Line {}", line); 17 | } 18 | _ => {} 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/cursor_style.rs: -------------------------------------------------------------------------------- 1 | use lineeditor::LineEditor; 2 | use lineeditor::LineEditorResult; 3 | use lineeditor::SetCursorStyle; 4 | use lineeditor::StringPrompt; 5 | 6 | fn main() { 7 | let prompt = StringPrompt::new("prompt> ".to_string()); 8 | let mut line_editor = LineEditor::new(Box::new(prompt)); 9 | line_editor.set_cursor_style(Some(SetCursorStyle::BlinkingBlock)); 10 | 11 | let bindings = line_editor.keybinding(); 12 | bindings.register_common_control_bindings(); 13 | 14 | match line_editor.read_line() { 15 | Ok(LineEditorResult::Success(line)) => { 16 | println!("Line {}", line); 17 | } 18 | _ => {} 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/custom_prompt.rs: -------------------------------------------------------------------------------- 1 | use lineeditor::styled_buffer::StyledBuffer; 2 | use lineeditor::LineEditor; 3 | use lineeditor::LineEditorResult; 4 | use lineeditor::Prompt; 5 | 6 | pub struct CurrentPathPrompt {} 7 | 8 | impl Prompt for CurrentPathPrompt { 9 | fn prompt(&self) -> StyledBuffer { 10 | let path = if let Ok(current_dir) = std::env::current_dir() { 11 | current_dir.to_string_lossy().to_string() 12 | } else { 13 | "".to_owned() 14 | }; 15 | let mut styled_buffer = StyledBuffer::default(); 16 | styled_buffer.insert_string(&format!("📁 {}> ", path)); 17 | styled_buffer 18 | } 19 | } 20 | 21 | fn main() { 22 | let prompt = CurrentPathPrompt {}; 23 | let mut line_editor = LineEditor::new(Box::new(prompt)); 24 | 25 | let bindings = line_editor.keybinding(); 26 | bindings.register_common_control_bindings(); 27 | 28 | match line_editor.read_line() { 29 | Ok(LineEditorResult::Success(line)) => { 30 | println!("Line {}", line); 31 | } 32 | _ => {} 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/drop_down_auto_complete.rs: -------------------------------------------------------------------------------- 1 | use lineeditor::event::LineEditorEvent; 2 | use lineeditor::keybindings::KeyCombination; 3 | use lineeditor::styled_buffer::StyledBuffer; 4 | use lineeditor::Completer; 5 | use lineeditor::KeyModifiers; 6 | use lineeditor::LineEditor; 7 | use lineeditor::LineEditorResult; 8 | use lineeditor::Span; 9 | use lineeditor::StringPrompt; 10 | use lineeditor::Suggestion; 11 | 12 | const GITQL_RESERVED_KEYWORDS: [&str; 31] = [ 13 | "set", "select", "distinct", "from", "group", "where", "having", "offset", "limit", "order", 14 | "by", "case", "when", "then", "else", "end", "between", "in", "is", "not", "like", "glob", 15 | "or", "and", "xor", "true", "false", "null", "as", "asc", "desc", 16 | ]; 17 | 18 | pub struct FixedCompleter {} 19 | 20 | impl Completer for FixedCompleter { 21 | fn complete(&self, input: &StyledBuffer) -> Vec { 22 | let mut suggestions: Vec = vec![]; 23 | if input.position() != input.len() { 24 | return suggestions; 25 | } 26 | 27 | if let Some(keyword) = input.last_alphabetic_keyword() { 28 | for reserved_keyword in GITQL_RESERVED_KEYWORDS { 29 | if reserved_keyword.starts_with(&keyword) { 30 | let suggestion = Suggestion { 31 | content: StyledBuffer::from(reserved_keyword), 32 | span: Span { 33 | start: input.len() - keyword.len(), 34 | end: input.len(), 35 | }, 36 | }; 37 | suggestions.push(suggestion); 38 | } 39 | } 40 | } 41 | suggestions 42 | } 43 | } 44 | 45 | fn main() { 46 | let prompt = StringPrompt::new("prompt> ".to_string()); 47 | let mut line_editor = LineEditor::new(Box::new(prompt)); 48 | 49 | line_editor.set_completer(Box::new(FixedCompleter {})); 50 | 51 | let bindings = line_editor.keybinding(); 52 | 53 | bindings.register_binding( 54 | KeyCombination { 55 | key_kind: lineeditor::KeyEventKind::Press, 56 | modifier: KeyModifiers::NONE, 57 | key_code: lineeditor::KeyCode::Tab, 58 | }, 59 | LineEditorEvent::ToggleAutoComplete, 60 | ); 61 | bindings.register_common_control_bindings(); 62 | bindings.register_common_navigation_bindings(); 63 | bindings.register_common_edit_bindings(); 64 | bindings.register_common_selection_bindings(); 65 | 66 | match line_editor.read_line() { 67 | Ok(LineEditorResult::Success(line)) => { 68 | println!("Line {}", line); 69 | } 70 | _ => {} 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /examples/hex_color_highlighter.rs: -------------------------------------------------------------------------------- 1 | use lineeditor::style::Style; 2 | use lineeditor::styled_buffer::StyledBuffer; 3 | use lineeditor::Color; 4 | use lineeditor::Highlighter; 5 | use lineeditor::LineEditor; 6 | use lineeditor::LineEditorResult; 7 | use lineeditor::StringPrompt; 8 | 9 | #[derive(Default)] 10 | pub struct HexColorHighlighter {} 11 | 12 | impl Highlighter for HexColorHighlighter { 13 | fn highlight(&self, buffer: &mut StyledBuffer) { 14 | let lines = buffer.buffer().clone(); 15 | let mut i: usize = 0; 16 | 17 | loop { 18 | if i >= lines.len() { 19 | break; 20 | } 21 | 22 | // Highlight String literal 23 | if lines[i] == '"' { 24 | i += 1; 25 | 26 | while i < lines.len() && lines[i] != '"' { 27 | i += 1; 28 | } 29 | 30 | if i < lines.len() && lines[i] == '"' { 31 | i += 1; 32 | } 33 | 34 | continue; 35 | } 36 | 37 | // Highlight hex value background with it value 38 | if lines[i] == '#' && i + 6 < lines.len() { 39 | let start = i; 40 | i += 1; 41 | let hex_value = &lines[i..i + 6]; 42 | for ch in hex_value { 43 | if !ch.is_ascii_hexdigit() { 44 | return; 45 | } 46 | } 47 | let hex_string = hex_value.iter().cloned().collect::(); 48 | if let Ok(hex_value) = usize::from_str_radix(&hex_string, 16) { 49 | let red = (hex_value >> 16) & 0xFF; 50 | let green = (hex_value >> 8) & 0xFF; 51 | let blue = hex_value & 0xFF; 52 | let mut style = Style::default(); 53 | style.set_background_color(Color::Rgb { 54 | r: red as u8, 55 | g: green as u8, 56 | b: blue as u8, 57 | }); 58 | buffer.style_range(start, start + 7, style); 59 | } 60 | } 61 | 62 | i += 1; 63 | } 64 | } 65 | } 66 | 67 | fn main() { 68 | let prompt = StringPrompt::new("prompt> ".to_string()); 69 | let mut line_editor = LineEditor::new(Box::new(prompt)); 70 | 71 | line_editor.add_highlighter(Box::::default()); 72 | 73 | let bindings = line_editor.keybinding(); 74 | bindings.register_common_control_bindings(); 75 | bindings.register_common_navigation_bindings(); 76 | bindings.register_common_edit_bindings(); 77 | bindings.register_common_selection_bindings(); 78 | 79 | match line_editor.read_line() { 80 | Ok(LineEditorResult::Success(line)) => { 81 | println!("Line {}", line); 82 | } 83 | _ => {} 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /examples/input_filter.rs: -------------------------------------------------------------------------------- 1 | use lineeditor::input_filter::InputFilter; 2 | use lineeditor::LineEditor; 3 | use lineeditor::LineEditorResult; 4 | use lineeditor::StringPrompt; 5 | 6 | fn main() { 7 | let prompt = StringPrompt::new("prompt> ".to_string()); 8 | let mut line_editor = LineEditor::new(Box::new(prompt)); 9 | 10 | line_editor.set_input_filter(InputFilter::Options(vec![ 11 | Box::new(InputFilter::Digit), 12 | Box::new(InputFilter::Whitespace), 13 | ])); 14 | 15 | let bindings = line_editor.keybinding(); 16 | bindings.register_common_control_bindings(); 17 | bindings.register_common_navigation_bindings(); 18 | bindings.register_common_edit_bindings(); 19 | bindings.register_common_selection_bindings(); 20 | 21 | match line_editor.read_line() { 22 | Ok(LineEditorResult::Success(line)) => { 23 | println!("Line {}", line); 24 | } 25 | _ => {} 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/key_bindings.rs: -------------------------------------------------------------------------------- 1 | use lineeditor::LineEditor; 2 | use lineeditor::LineEditorResult; 3 | use lineeditor::StringPrompt; 4 | 5 | fn main() { 6 | let prompt = StringPrompt::new("prompt> ".to_string()); 7 | let mut line_editor = LineEditor::new(Box::new(prompt)); 8 | 9 | let bindings = line_editor.keybinding(); 10 | bindings.register_common_control_bindings(); 11 | bindings.register_common_navigation_bindings(); 12 | bindings.register_common_edit_bindings(); 13 | bindings.register_common_selection_bindings(); 14 | 15 | match line_editor.read_line() { 16 | Ok(LineEditorResult::Success(line)) => { 17 | println!("Line {}", line); 18 | } 19 | _ => {} 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/keyword_highlighter.rs: -------------------------------------------------------------------------------- 1 | use lineeditor::style::Style; 2 | use lineeditor::styled_buffer::StyledBuffer; 3 | use lineeditor::Color; 4 | use lineeditor::Highlighter; 5 | use lineeditor::LineEditor; 6 | use lineeditor::LineEditorResult; 7 | use lineeditor::StringPrompt; 8 | 9 | const GITQL_RESERVED_KEYWORDS: [&str; 31] = [ 10 | "set", "select", "distinct", "from", "group", "where", "having", "offset", "limit", "order", 11 | "by", "case", "when", "then", "else", "end", "between", "in", "is", "not", "like", "glob", 12 | "or", "and", "xor", "true", "false", "null", "as", "asc", "desc", 13 | ]; 14 | 15 | #[derive(Default)] 16 | pub struct GitQLHighlighter {} 17 | 18 | impl Highlighter for GitQLHighlighter { 19 | fn highlight(&self, buffer: &mut StyledBuffer) { 20 | let lines = buffer.buffer().clone(); 21 | let mut i: usize = 0; 22 | 23 | let mut keyword_style = Style::default(); 24 | keyword_style.set_foreground_color(Color::Magenta); 25 | 26 | let mut string_style = Style::default(); 27 | string_style.set_foreground_color(Color::Yellow); 28 | 29 | loop { 30 | if i >= lines.len() { 31 | break; 32 | } 33 | 34 | // Highlight String literal 35 | if lines[i] == '"' { 36 | buffer.style_char(i, string_style.clone()); 37 | i += 1; 38 | 39 | while i < lines.len() && lines[i] != '"' { 40 | buffer.style_char(i, string_style.clone()); 41 | i += 1; 42 | } 43 | 44 | if i < lines.len() && lines[i] == '"' { 45 | buffer.style_char(i, string_style.clone()); 46 | i += 1; 47 | } 48 | 49 | continue; 50 | } 51 | 52 | // Highlight reserved keyword 53 | if lines[i].is_alphabetic() { 54 | let start = i; 55 | let mut keyword = String::new(); 56 | while i < lines.len() && (lines[i].is_alphanumeric() || lines[i] == '_') { 57 | keyword.push(lines[i]); 58 | i += 1; 59 | } 60 | 61 | keyword = keyword.to_lowercase(); 62 | if GITQL_RESERVED_KEYWORDS.contains(&keyword.as_str()) { 63 | buffer.style_range(start, i, keyword_style.clone()) 64 | } 65 | continue; 66 | } 67 | 68 | i += 1; 69 | } 70 | } 71 | } 72 | 73 | fn main() { 74 | let prompt = StringPrompt::new("prompt> ".to_string()); 75 | let mut line_editor = LineEditor::new(Box::new(prompt)); 76 | 77 | let bindings = line_editor.keybinding(); 78 | bindings.register_common_control_bindings(); 79 | 80 | line_editor.add_highlighter(Box::::default()); 81 | 82 | match line_editor.read_line() { 83 | Ok(LineEditorResult::Success(line)) => { 84 | println!("Line {}", line); 85 | } 86 | _ => {} 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /examples/keyword_hinter.rs: -------------------------------------------------------------------------------- 1 | use lineeditor::style::Style; 2 | use lineeditor::styled_buffer::StyledBuffer; 3 | use lineeditor::Color; 4 | use lineeditor::Hinter; 5 | use lineeditor::LineEditor; 6 | use lineeditor::LineEditorResult; 7 | use lineeditor::StringPrompt; 8 | 9 | const GITQL_RESERVED_KEYWORDS: [&str; 31] = [ 10 | "set", "select", "distinct", "from", "group", "where", "having", "offset", "limit", "order", 11 | "by", "case", "when", "then", "else", "end", "between", "in", "is", "not", "like", "glob", 12 | "or", "and", "xor", "true", "false", "null", "as", "asc", "desc", 13 | ]; 14 | 15 | #[derive(Default)] 16 | pub struct GitQLHinter {} 17 | 18 | impl Hinter for GitQLHinter { 19 | fn hint(&self, buffer: &mut StyledBuffer) -> Option { 20 | if let Some(keyword) = buffer.last_alphabetic_keyword() { 21 | let keyword_lower = keyword.to_lowercase(); 22 | for word in GITQL_RESERVED_KEYWORDS { 23 | if word.starts_with(&keyword_lower) { 24 | let hint = &word[keyword.len()..]; 25 | let mut styled_buffer = StyledBuffer::default(); 26 | let mut style = Style::default(); 27 | style.set_foreground_color(Color::DarkGrey); 28 | styled_buffer.insert_styled_string(hint, style); 29 | return Some(styled_buffer); 30 | } 31 | } 32 | } 33 | None 34 | } 35 | } 36 | 37 | fn main() { 38 | let prompt = StringPrompt::new("prompt> ".to_string()); 39 | let mut line_editor = LineEditor::new(Box::new(prompt)); 40 | line_editor.add_hinter(Box::::default()); 41 | 42 | let bindings = line_editor.keybinding(); 43 | bindings.register_common_control_bindings(); 44 | 45 | match line_editor.read_line() { 46 | Ok(LineEditorResult::Success(line)) => { 47 | println!("Line {}", line); 48 | } 49 | _ => {} 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /examples/matching_brackets_highlighters.rs: -------------------------------------------------------------------------------- 1 | use lineeditor::style::Style; 2 | use lineeditor::styled_buffer::StyledBuffer; 3 | use lineeditor::Color; 4 | use lineeditor::Highlighter; 5 | use lineeditor::LineEditor; 6 | use lineeditor::LineEditorResult; 7 | use lineeditor::StringPrompt; 8 | 9 | #[derive(Default)] 10 | pub struct MatchingBracketsHighlighter {} 11 | 12 | impl Highlighter for MatchingBracketsHighlighter { 13 | fn highlight(&self, buffer: &mut StyledBuffer) { 14 | let colors = vec![Color::Red, Color::Blue, Color::Yellow, Color::Green]; 15 | let mut brackets_stack: Vec = vec![]; 16 | let mut current_color_index = 0; 17 | 18 | let lines = buffer.buffer().clone(); 19 | let mut i: usize = 0; 20 | loop { 21 | if i >= lines.len() { 22 | break; 23 | } 24 | 25 | if lines[i] == '"' { 26 | i += 1; 27 | while i < lines.len() && lines[i] != '"' { 28 | i += 1; 29 | } 30 | 31 | if i < lines.len() { 32 | i += 1; 33 | } 34 | continue; 35 | } 36 | 37 | if lines[i] == '(' || lines[i] == '<' || lines[i] == '[' || lines[i] == '{' { 38 | if current_color_index >= colors.len() { 39 | current_color_index = 0; 40 | } 41 | 42 | let color = colors[current_color_index]; 43 | current_color_index += 1; 44 | 45 | brackets_stack.push(color); 46 | 47 | let mut style = Style::default(); 48 | style.set_foreground_color(color); 49 | buffer.style_char(i, style); 50 | i += 1; 51 | continue; 52 | } 53 | 54 | if lines[i] == ')' || lines[i] == '>' || lines[i] == ']' || lines[i] == '}' { 55 | let color = if brackets_stack.is_empty() { 56 | colors[0] 57 | } else { 58 | brackets_stack.pop().unwrap() 59 | }; 60 | 61 | let mut style = Style::default(); 62 | style.set_foreground_color(color); 63 | buffer.style_char(i, style); 64 | 65 | i += 1; 66 | continue; 67 | } 68 | i += 1; 69 | } 70 | } 71 | } 72 | 73 | fn main() { 74 | let prompt = StringPrompt::new("gql> ".to_string()); 75 | let mut line_editor = LineEditor::new(Box::new(prompt)); 76 | line_editor.add_highlighter(Box::::default()); 77 | 78 | let bindings = line_editor.keybinding(); 79 | bindings.register_common_control_bindings(); 80 | 81 | match line_editor.read_line() { 82 | Ok(LineEditorResult::Success(line)) => { 83 | println!("Line {}", line); 84 | } 85 | _ => {} 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /examples/surround_selection.rs: -------------------------------------------------------------------------------- 1 | use lineeditor::style::Style; 2 | use lineeditor::LineEditor; 3 | use lineeditor::LineEditorResult; 4 | use lineeditor::StringPrompt; 5 | 6 | fn main() { 7 | let prompt = StringPrompt::new("prompt> ".to_string()); 8 | let mut line_editor = LineEditor::new(Box::new(prompt)); 9 | 10 | let mut style = Style::default(); 11 | style.set_background_color(lineeditor::Color::Cyan); 12 | line_editor.set_visual_selection_style(Some(style)); 13 | line_editor.enable_surround_selection(true); 14 | 15 | let bindings = line_editor.keybinding(); 16 | bindings.register_common_control_bindings(); 17 | bindings.register_common_navigation_bindings(); 18 | bindings.register_common_edit_bindings(); 19 | bindings.register_common_selection_bindings(); 20 | 21 | match line_editor.read_line() { 22 | Ok(LineEditorResult::Success(line)) => { 23 | println!("Line {}", line); 24 | } 25 | _ => {} 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/text_prompt.rs: -------------------------------------------------------------------------------- 1 | use lineeditor::LineEditor; 2 | use lineeditor::LineEditorResult; 3 | use lineeditor::StringPrompt; 4 | 5 | fn main() { 6 | let prompt = StringPrompt::new("prompt> ".to_string()); 7 | let mut line_editor = LineEditor::new(Box::new(prompt)); 8 | 9 | let bindings = line_editor.keybinding(); 10 | bindings.register_common_control_bindings(); 11 | 12 | match line_editor.read_line() { 13 | Ok(LineEditorResult::Success(line)) => { 14 | println!("Line {}", line); 15 | } 16 | _ => {} 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/visual_selection.rs: -------------------------------------------------------------------------------- 1 | use lineeditor::style::Style; 2 | use lineeditor::LineEditor; 3 | use lineeditor::LineEditorResult; 4 | use lineeditor::StringPrompt; 5 | 6 | fn main() { 7 | let prompt = StringPrompt::new("prompt> ".to_string()); 8 | let mut line_editor = LineEditor::new(Box::new(prompt)); 9 | 10 | let mut style = Style::default(); 11 | style.set_background_color(lineeditor::Color::Cyan); 12 | line_editor.set_visual_selection_style(Some(style)); 13 | 14 | let bindings = line_editor.keybinding(); 15 | bindings.register_common_control_bindings(); 16 | bindings.register_common_navigation_bindings(); 17 | bindings.register_common_edit_bindings(); 18 | bindings.register_common_selection_bindings(); 19 | 20 | match line_editor.read_line() { 21 | Ok(LineEditorResult::Success(line)) => { 22 | println!("Line {}", line); 23 | } 24 | _ => {} 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /media/auto_complete_demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmrDeveloper/Lineeditor/bb7228fc78e2c54c7d2d29e60b31d66cf11b7ee8/media/auto_complete_demo.gif -------------------------------------------------------------------------------- /media/line_editor_demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmrDeveloper/Lineeditor/bb7228fc78e2c54c7d2d29e60b31d66cf11b7ee8/media/line_editor_demo.gif -------------------------------------------------------------------------------- /src/autopair/mod.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::styled_buffer::StyledBuffer; 4 | 5 | /// List of common pairs 6 | pub const DEFAULT_PAIRS: &[(char, char)] = &[ 7 | ('(', ')'), 8 | ('{', '}'), 9 | ('[', ']'), 10 | ('\'', '\''), 11 | ('"', '"'), 12 | ('`', '`'), 13 | ]; 14 | 15 | /// The Auto pair trait. Implementers of this trait will take the current styled buffer and then 16 | /// modify it, which represents the contents of the original line 17 | pub trait AutoPair { 18 | /// The action that will handle the current styled buffer as a line 19 | fn complete_pair(&self, buffer: &mut StyledBuffer); 20 | } 21 | 22 | /// Auto pair complete that depend on a map of pairs 23 | pub struct DefaultAutoPair { 24 | pairs: HashMap, 25 | } 26 | 27 | /// Create instance of DefaultAutoPair with default pairs [`DEFAULT_PAIRS`] 28 | impl Default for DefaultAutoPair { 29 | fn default() -> Self { 30 | let mut pairs = HashMap::with_capacity(DEFAULT_PAIRS.len()); 31 | for pair in DEFAULT_PAIRS { 32 | pairs.insert(pair.0, pair.1); 33 | } 34 | Self { pairs } 35 | } 36 | } 37 | 38 | impl DefaultAutoPair { 39 | /// Create instance of DefaultAutoPair with custom pairs 40 | pub fn with_pairs(pairs: HashMap) -> Self { 41 | Self { pairs } 42 | } 43 | } 44 | 45 | impl AutoPair for DefaultAutoPair { 46 | /// Complete the pair if it exists on the pairs map and cursor is at the end 47 | fn complete_pair(&self, buffer: &mut StyledBuffer) { 48 | if buffer.position() == buffer.len() { 49 | if let Some(last_char) = buffer.buffer().last() { 50 | if let Some(pair) = self.pairs.get(last_char) { 51 | buffer.insert_char(*pair); 52 | buffer.move_char_left(); 53 | } 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/completion/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::styled_buffer::StyledBuffer; 2 | 3 | /// A span of source code, with positions 4 | pub struct Span { 5 | pub start: usize, 6 | pub end: usize, 7 | } 8 | 9 | impl Span { 10 | pub fn new(start: usize, end: usize) -> Self { 11 | Span { start, end } 12 | } 13 | } 14 | 15 | /// Suggestion returned by the Completer 16 | pub struct Suggestion { 17 | /// Suggestion content and styles 18 | pub content: StyledBuffer, 19 | /// Replacement span 20 | pub span: Span, 21 | } 22 | 23 | /// The Completer trait, Implementers of this trait will return a list of suggestions as styled buffers 24 | pub trait Completer { 25 | /// The action that will return a list of suggestions 26 | fn complete(&self, input: &StyledBuffer) -> Vec; 27 | } 28 | -------------------------------------------------------------------------------- /src/core/editor.rs: -------------------------------------------------------------------------------- 1 | use crate::event::MovementCommand; 2 | 3 | use super::event::EditCommand; 4 | use super::styled_buffer::StyledBuffer; 5 | 6 | /// Wrapper for the Buffer to make it easy to run edit commands 7 | pub struct Editor { 8 | buffer: StyledBuffer, 9 | } 10 | 11 | /// Create a new instance of [`Editor`] 12 | impl Default for Editor { 13 | fn default() -> Self { 14 | Self { 15 | buffer: Default::default(), 16 | } 17 | } 18 | } 19 | 20 | impl Editor { 21 | /// Get the current [`StyledBuffer`] 22 | pub fn styled_buffer(&mut self) -> &mut StyledBuffer { 23 | &mut self.buffer 24 | } 25 | 26 | /// Apply [`EditCommand`] to the current buffer 27 | pub fn run_edit_commands(&mut self, command: &EditCommand) { 28 | match command { 29 | EditCommand::InsertChar(c) => self.buffer.insert_char(*c), 30 | EditCommand::InsertString(s) => self.buffer.insert_string(s), 31 | EditCommand::DeleteLeftChar => self.buffer.delete_left_char(), 32 | EditCommand::DeleteRightChar => self.buffer.delete_right_char(), 33 | EditCommand::DeleteSpan(from, to) => self.buffer.delete_range(*from, *to), 34 | EditCommand::Clear => self.buffer.clear(), 35 | } 36 | } 37 | 38 | /// Apply [`MovementCommand`] to the current buffer 39 | pub fn run_movement_commands(&mut self, command: &MovementCommand) { 40 | match command { 41 | MovementCommand::MoveToStart => self.buffer.move_to_start(), 42 | MovementCommand::MoveToEnd => self.buffer.move_to_end(), 43 | MovementCommand::MoveLeftChar => self.buffer.move_char_left(), 44 | MovementCommand::MoveRightChar => self.buffer.move_char_right(), 45 | MovementCommand::MoveLeftWord => self.buffer.move_word_left(), 46 | MovementCommand::MoveRightWord => self.buffer.move_word_right(), 47 | MovementCommand::MoveToPosition(position) => self.buffer.set_position(*position), 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/core/event.rs: -------------------------------------------------------------------------------- 1 | /// Editing actions which can be mapped to key bindings. 2 | /// 3 | /// Executed by `Editor::run_edit_commands()` 4 | #[derive(Clone)] 5 | pub enum EditCommand { 6 | /// Insert a character at the current insertion point 7 | InsertChar(char), 8 | 9 | /// Insert a string at the current insertion point 10 | InsertString(String), 11 | 12 | /// Backspace delete from the current insertion point 13 | DeleteLeftChar, 14 | 15 | /// Delete in-place from the current insertion point 16 | DeleteRightChar, 17 | 18 | /// Delete in-place range 19 | DeleteSpan(usize, usize), 20 | 21 | /// Clear the current buffer 22 | Clear, 23 | } 24 | 25 | /// Movements actions which can be mapped to key bindings. 26 | #[derive(Clone)] 27 | pub enum MovementCommand { 28 | /// Move to the start of the buffer 29 | MoveToStart, 30 | 31 | /// Move to the end of the buffer 32 | MoveToEnd, 33 | 34 | /// Move one character to the left 35 | MoveLeftChar, 36 | 37 | /// Move one character to the right 38 | MoveRightChar, 39 | 40 | /// Move one word to the left 41 | MoveLeftWord, 42 | 43 | /// Move one word to the right 44 | MoveRightWord, 45 | 46 | /// Move to position 47 | MoveToPosition(usize), 48 | } 49 | 50 | /// LineEditor supported actions. 51 | #[derive(Clone)] 52 | pub enum LineEditorEvent { 53 | /// No op event 54 | None, 55 | 56 | /// Handle enter event 57 | Enter, 58 | 59 | /// Esc event 60 | Esc, 61 | 62 | /// Handle unconditional submit event 63 | Submit, 64 | 65 | /// Run these commands in the editor 66 | Edit(Vec), 67 | 68 | /// Run movements commands in the editor 69 | Movement(Vec), 70 | 71 | /// Move up to the previous line, if multiline, or up into the historic buffers 72 | Up, 73 | 74 | /// Move down to the next line, if multiline, or down through the historic buffers 75 | Down, 76 | 77 | /// Move right to the next column, completion entry, or complete hint 78 | Right, 79 | 80 | /// Move left to the next column, or completion entry 81 | Left, 82 | 83 | /// Select one character to the right 84 | SelectRight, 85 | 86 | /// Select one character to the left 87 | SelectLeft, 88 | 89 | /// Select all buffer 90 | SelectAll, 91 | 92 | /// Cut the selected text into clipboard 93 | CutSelected, 94 | 95 | /// Copy the selected text into clipboard 96 | CopySelected, 97 | 98 | /// Paste text from clipboard into selection or at insertion point 99 | Paste, 100 | 101 | /// Delete char from the left or delete selected range 102 | Backspace, 103 | 104 | /// Delete char from the right or delete selected range 105 | Delete, 106 | 107 | /// Show or Hide Auto Complete view depend on the state 108 | ToggleAutoComplete, 109 | } 110 | -------------------------------------------------------------------------------- /src/core/input_filter.rs: -------------------------------------------------------------------------------- 1 | /// Input filter used to ignore any input character that not matching the rules 2 | /// You can Mix one or more rules to make your own custom rules 3 | /// 4 | /// ## Examples of mixed rules 5 | /// 6 | /// ``` 7 | /// // To Make rules that accept any alphabetic character or whitespace 8 | /// InputFilter::Options(vec![Box::new(InputFilter::Alphabetic), Box::new(InputFilter::Whitespace)]) 9 | /// 10 | /// // To Make rules that accept any Text but not accept Punctuation 11 | /// InputFilter::Options(vec![Box::new(InputFilter::Alphabetic), Box::new(Not(Box::new(InputFilter::Punctuation)))]) 12 | /// ``` 13 | /// 14 | pub enum InputFilter { 15 | /// A-Z and a-z 16 | Alphabetic, 17 | /// A-Z, a-z and 0-9 18 | AlphaNumeric, 19 | /// 0-9 20 | Digit, 21 | /// Any character 22 | Text, 23 | /// a-f, A-F and 0-9 24 | HexDigit, 25 | /// Whitespace 26 | Whitespace, 27 | /// Punctuation 28 | Punctuation, 29 | /// Allow everything except One InputFilter 30 | Not(Box), 31 | /// Valid if one of the char is matching at least one of the InputFilters 32 | Options(Vec>), 33 | /// User defined input filter function 34 | Custom(fn(char) -> bool), 35 | } 36 | 37 | /// Input Filter function that returns true if character is matching the rules of the given InputFilter 38 | pub fn filter_input(ch: char, input_filter: &InputFilter) -> bool { 39 | match input_filter { 40 | InputFilter::Alphabetic => ch.is_alphabetic(), 41 | InputFilter::AlphaNumeric => ch.is_alphanumeric(), 42 | InputFilter::Text => true, 43 | InputFilter::Digit => ch.is_numeric(), 44 | InputFilter::HexDigit => ch.is_ascii_hexdigit(), 45 | InputFilter::Whitespace => ch.is_whitespace(), 46 | InputFilter::Punctuation => ch.is_ascii_punctuation(), 47 | InputFilter::Not(filter) => !filter_input(ch, filter), 48 | InputFilter::Options(input_filters) => { 49 | for filter in input_filters { 50 | if filter_input(ch, filter) { 51 | return true; 52 | } 53 | } 54 | false 55 | } 56 | InputFilter::Custom(function) => function(ch), 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/core/keybindings.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::hash::Hash; 3 | 4 | use crossterm::event::KeyCode; 5 | use crossterm::event::KeyEvent; 6 | use crossterm::event::KeyEventKind; 7 | use crossterm::event::KeyModifiers; 8 | 9 | use crate::event::MovementCommand; 10 | 11 | use super::event::LineEditorEvent; 12 | 13 | /// Represent the key combination 14 | #[derive(Clone, PartialEq, Eq, Hash, Debug)] 15 | pub struct KeyCombination { 16 | pub key_kind: KeyEventKind, 17 | pub modifier: KeyModifiers, 18 | pub key_code: KeyCode, 19 | } 20 | 21 | /// Create KeyCombination from crossterm KeyEvent 22 | impl From for KeyCombination { 23 | fn from(key_event: KeyEvent) -> Self { 24 | KeyCombination { 25 | key_kind: key_event.kind, 26 | modifier: key_event.modifiers, 27 | key_code: key_event.code, 28 | } 29 | } 30 | } 31 | 32 | /// Map of keybindings and [`LineEditorEvent`] 33 | pub struct Keybindings { 34 | /// Defines a keybinding for a reedline event 35 | pub bindings: HashMap, 36 | } 37 | 38 | /// Create a new instance of [`Keybindings`] 39 | impl Default for Keybindings { 40 | fn default() -> Self { 41 | Keybindings { 42 | bindings: HashMap::new(), 43 | } 44 | } 45 | } 46 | 47 | impl Keybindings { 48 | /// Register an [`LineEditorEvent`] for specific key combination 49 | pub fn register_binding(&mut self, key_combination: KeyCombination, event: LineEditorEvent) { 50 | self.bindings.insert(key_combination, event); 51 | } 52 | 53 | /// Find a keybinding based on the modifier and keycode 54 | pub fn find_binding(&self, key_combination: KeyCombination) -> Option { 55 | self.bindings.get(&key_combination).cloned() 56 | } 57 | 58 | /// Get assigned keybindings 59 | pub fn keybindings(&self) -> &HashMap { 60 | &self.bindings 61 | } 62 | 63 | /// Register basic functionality to Control 64 | /// 65 | /// `Enter`, `Esc` 66 | pub fn register_common_control_bindings(&mut self) { 67 | self.register_binding( 68 | KeyCombination { 69 | key_kind: KeyEventKind::Press, 70 | modifier: KeyModifiers::NONE, 71 | key_code: KeyCode::Enter, 72 | }, 73 | LineEditorEvent::Enter, 74 | ); 75 | 76 | self.register_binding( 77 | KeyCombination { 78 | key_kind: KeyEventKind::Press, 79 | modifier: KeyModifiers::NONE, 80 | key_code: KeyCode::Esc, 81 | }, 82 | LineEditorEvent::Esc, 83 | ); 84 | } 85 | 86 | /// Register basic functionality to Navigation 87 | /// 88 | /// `Up`, `Down`, `Right`, `Left` Arrow keys 89 | /// `CTRL + Right`, `CTRL + Left` 90 | /// `Home`, `End` 91 | pub fn register_common_navigation_bindings(&mut self) { 92 | self.register_binding( 93 | KeyCombination { 94 | key_kind: KeyEventKind::Press, 95 | modifier: KeyModifiers::NONE, 96 | key_code: KeyCode::Up, 97 | }, 98 | LineEditorEvent::Up, 99 | ); 100 | 101 | self.register_binding( 102 | KeyCombination { 103 | key_kind: KeyEventKind::Press, 104 | modifier: KeyModifiers::NONE, 105 | key_code: KeyCode::Down, 106 | }, 107 | LineEditorEvent::Down, 108 | ); 109 | 110 | self.register_binding( 111 | KeyCombination { 112 | key_kind: KeyEventKind::Press, 113 | modifier: KeyModifiers::NONE, 114 | key_code: KeyCode::Left, 115 | }, 116 | LineEditorEvent::Left, 117 | ); 118 | 119 | self.register_binding( 120 | KeyCombination { 121 | key_kind: KeyEventKind::Press, 122 | modifier: KeyModifiers::NONE, 123 | key_code: KeyCode::Right, 124 | }, 125 | LineEditorEvent::Right, 126 | ); 127 | 128 | self.register_binding( 129 | KeyCombination { 130 | key_kind: KeyEventKind::Press, 131 | modifier: KeyModifiers::NONE, 132 | key_code: KeyCode::Home, 133 | }, 134 | LineEditorEvent::Movement(vec![MovementCommand::MoveToStart]), 135 | ); 136 | 137 | self.register_binding( 138 | KeyCombination { 139 | key_kind: KeyEventKind::Press, 140 | modifier: KeyModifiers::NONE, 141 | key_code: KeyCode::End, 142 | }, 143 | LineEditorEvent::Movement(vec![MovementCommand::MoveToEnd]), 144 | ); 145 | 146 | self.register_binding( 147 | KeyCombination { 148 | key_kind: KeyEventKind::Press, 149 | modifier: KeyModifiers::CONTROL, 150 | key_code: KeyCode::Left, 151 | }, 152 | LineEditorEvent::Movement(vec![MovementCommand::MoveLeftWord]), 153 | ); 154 | 155 | self.register_binding( 156 | KeyCombination { 157 | key_kind: KeyEventKind::Press, 158 | modifier: KeyModifiers::CONTROL, 159 | key_code: KeyCode::Right, 160 | }, 161 | LineEditorEvent::Movement(vec![MovementCommand::MoveRightWord]), 162 | ); 163 | } 164 | 165 | /// Register basic functionality to edit 166 | /// 167 | /// `Delete`, `Backspace` and the basic variants do delete words 168 | pub fn register_common_edit_bindings(&mut self) { 169 | self.register_binding( 170 | KeyCombination { 171 | key_kind: KeyEventKind::Press, 172 | modifier: KeyModifiers::NONE, 173 | key_code: KeyCode::Backspace, 174 | }, 175 | LineEditorEvent::Backspace, 176 | ); 177 | 178 | self.register_binding( 179 | KeyCombination { 180 | key_kind: KeyEventKind::Press, 181 | modifier: KeyModifiers::NONE, 182 | key_code: KeyCode::Delete, 183 | }, 184 | LineEditorEvent::Delete, 185 | ); 186 | } 187 | 188 | /// Register basic functionality to selection 189 | /// 190 | /// Select right and left and select all 191 | pub fn register_common_selection_bindings(&mut self) { 192 | self.register_binding( 193 | KeyCombination { 194 | key_kind: KeyEventKind::Press, 195 | modifier: KeyModifiers::SHIFT, 196 | key_code: KeyCode::Left, 197 | }, 198 | LineEditorEvent::SelectLeft, 199 | ); 200 | 201 | self.register_binding( 202 | KeyCombination { 203 | key_kind: KeyEventKind::Press, 204 | modifier: KeyModifiers::SHIFT, 205 | key_code: KeyCode::Right, 206 | }, 207 | LineEditorEvent::SelectRight, 208 | ); 209 | 210 | self.register_binding( 211 | KeyCombination { 212 | key_kind: KeyEventKind::Press, 213 | modifier: KeyModifiers::CONTROL, 214 | key_code: KeyCode::Char('a'), 215 | }, 216 | LineEditorEvent::SelectAll, 217 | ); 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/core/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod editor; 2 | pub mod event; 3 | pub mod input_filter; 4 | pub mod keybindings; 5 | pub mod style; 6 | pub mod styled_buffer; 7 | -------------------------------------------------------------------------------- /src/core/style.rs: -------------------------------------------------------------------------------- 1 | use crossterm::style::Attribute; 2 | use crossterm::style::Color; 3 | 4 | /// Represent the foreground, background colors and attributes 5 | #[derive(Clone)] 6 | pub struct Style { 7 | /// Optional foreground color 8 | foreground: Option, 9 | /// Optional background color 10 | background: Option, 11 | /// Set of attributes like Bold, Italic, Undercurled...etc. 12 | attributes: Vec, 13 | } 14 | 15 | /// Create default instance of Style 16 | impl Default for Style { 17 | fn default() -> Self { 18 | Style { 19 | foreground: None, 20 | background: None, 21 | attributes: vec![], 22 | } 23 | } 24 | } 25 | 26 | impl Style { 27 | /// Get the style foreground color 28 | pub fn set_foreground_color(&mut self, color: Color) { 29 | self.foreground = Some(color); 30 | } 31 | 32 | /// Get the style foreground color 33 | pub fn foreground_color(&self) -> &Option { 34 | &self.foreground 35 | } 36 | 37 | /// Get the style background color 38 | pub fn set_background_color(&mut self, color: Color) { 39 | self.background = Some(color); 40 | } 41 | 42 | /// Get the style background color 43 | pub fn background_color(&self) -> &Option { 44 | &self.background 45 | } 46 | 47 | /// Get the style attributes 48 | pub fn attributes(&self) -> &Vec { 49 | &self.attributes 50 | } 51 | 52 | /// Add attribute to this style 53 | pub fn add_attribute(&mut self, attribute: Attribute) { 54 | self.attributes.push(attribute); 55 | } 56 | 57 | /// Remove all attributes for this style 58 | pub fn clear_attributes(&mut self) { 59 | self.attributes.clear(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/core/styled_buffer.rs: -------------------------------------------------------------------------------- 1 | use super::style::Style; 2 | 3 | /// Memory representation of the lines and styles 4 | pub struct StyledBuffer { 5 | /// The buffer as list of characters 6 | buffer: Vec, 7 | /// The styles for each character on the buffer 8 | styles: Vec