Skip to content

Commit

Permalink
Merge pull request #19 from ynqa/dev-0.3.3/main
Browse files Browse the repository at this point in the history
v0.3.3
  • Loading branch information
ynqa authored Mar 27, 2024
2 parents 298e4e2 + f5b2c03 commit 33e8c8f
Show file tree
Hide file tree
Showing 9 changed files with 188 additions and 21 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "promkit"
version = "0.3.2"
version = "0.3.3"
authors = ["ynqa <un.pensiero.vano@gmail.com>"]
edition = "2021"
description = "A toolkit for building your own interactive command-line tools"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Put the package in your `Cargo.toml`.

```toml
[dependencies]
promkit = "0.3.2"
promkit = "0.3.3"
```

## Features
Expand Down
9 changes: 9 additions & 0 deletions src/core/cursor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,15 @@ impl<C: Len> Cursor<C> {
pub fn is_tail(&self) -> bool {
self.position == self.contents.len().saturating_sub(1)
}

pub fn move_to(&mut self, position: usize) -> bool {
if position < self.contents.len() {
self.position = position;
true
} else {
false
}
}
}

#[cfg(test)]
Expand Down
111 changes: 111 additions & 0 deletions src/core/text_editor.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::collections::HashSet;

use crate::{
core::cursor::Cursor,
grapheme::{Grapheme, Graphemes},
Expand Down Expand Up @@ -105,6 +107,75 @@ impl TextEditor {
*self = Self::default();
}

/// Erases the text from the current cursor position to the specified position,
/// considering whether pos is greater or smaller than the current position.
fn erase_to_position(&mut self, pos: usize) {
let current_pos = self.position();
if pos > current_pos {
self.0.contents_mut().drain(current_pos..pos);
} else {
self.0.contents_mut().drain(pos..current_pos);
self.0.move_to(pos);
}
}

/// Finds the nearest previous index of any character in `word_break_chars` from the cursor position.
fn find_previous_nearest_index(&self, word_break_chars: &HashSet<char>) -> usize {
let current_position = self.position();
self.text()
.chars()
.iter()
.enumerate()
.filter(|&(i, _)| i < current_position.saturating_sub(1))
.rev()
.find(|&(_, c)| word_break_chars.contains(c))
.map(|(i, _)| i + 1)
.unwrap_or(0)
}

/// Erases the text from the current cursor position to the nearest previous character in `word_break_chars`.
pub fn erase_to_previous_nearest(&mut self, word_break_chars: &HashSet<char>) {
let pos = self.find_previous_nearest_index(word_break_chars);
self.erase_to_position(pos);
}

/// Moves the cursor to the nearest previous character in `word_break_chars`.
pub fn move_to_previous_nearest(&mut self, word_break_chars: &HashSet<char>) {
let pos = self.find_previous_nearest_index(word_break_chars);
self.0.move_to(pos);
}

/// Finds the nearest next index of any character in `word_break_chars` from the cursor position.
fn find_next_nearest_index(&self, word_break_chars: &HashSet<char>) -> usize {
let current_position = self.position();
self.text()
.chars()
.iter()
.enumerate()
.filter(|&(i, _)| i > current_position)
.find(|&(_, c)| word_break_chars.contains(c))
.map(|(i, _)| {
if i < self.0.contents().len() - 1 {
i + 1
} else {
self.0.contents().len() - 1
}
})
.unwrap_or(self.0.contents().len() - 1)
}

/// Erases the text from the current cursor position to the nearest next character in `word_break_chars`.
pub fn erase_to_next_nearest(&mut self, word_break_chars: &HashSet<char>) {
let pos = self.find_next_nearest_index(word_break_chars);
self.erase_to_position(pos);
}

/// Moves the cursor to the nearest next character in `word_break_chars`.
pub fn move_to_next_nearest(&mut self, word_break_chars: &HashSet<char>) {
let pos = self.find_next_nearest_index(word_break_chars);
self.0.move_to(pos);
}

/// Moves the cursor to the beginning of the text.
pub fn move_to_head(&mut self) {
self.0.move_to_head()
Expand Down Expand Up @@ -199,6 +270,46 @@ mod test {
}
}

mod find_previous_nearest_index {
use std::collections::HashSet;

use crate::text_editor::test::new_with_position;

#[test]
fn test() {
let mut txt = new_with_position(String::from("koko momo jojo "), 11); // indicate `o`.
assert_eq!(10, txt.find_previous_nearest_index(&HashSet::from([' '])));
txt.0.move_to(10);
assert_eq!(5, txt.find_previous_nearest_index(&HashSet::from([' '])));
}

#[test]
fn test_with_no_target() {
let txt = new_with_position(String::from("koko momo jojo "), 7); // indicate `m`.
assert_eq!(0, txt.find_previous_nearest_index(&HashSet::from(['z'])));
}
}

mod find_next_nearest_index {
use std::collections::HashSet;

use crate::text_editor::test::new_with_position;

#[test]
fn test() {
let mut txt = new_with_position(String::from("koko momo jojo "), 7); // indicate `m`.
assert_eq!(10, txt.find_next_nearest_index(&HashSet::from([' '])));
txt.0.move_to(10);
assert_eq!(14, txt.find_next_nearest_index(&HashSet::from([' '])));
}

#[test]
fn test_with_no_target() {
let txt = new_with_position(String::from("koko momo jojo "), 7); // indicate `m`.
assert_eq!(14, txt.find_next_nearest_index(&HashSet::from(['z'])));
}
}

mod insert {
use crate::text_editor::test::new_with_position;

Expand Down
4 changes: 4 additions & 0 deletions src/core/text_editor/render.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::collections::HashSet;

use crate::{
crossterm::style::ContentStyle,
grapheme::{matrixify, StyledGraphemes},
Expand Down Expand Up @@ -36,6 +38,8 @@ pub struct Renderer {

/// Current edit mode, determining whether input inserts or overwrites existing text.
pub edit_mode: Mode,
/// Characters to be for word break.
pub word_break_chars: HashSet<char>,
/// Number of lines available for rendering.
pub lines: Option<usize>,
}
Expand Down
25 changes: 8 additions & 17 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,21 @@
//!
//! ```toml
//! [dependencies]
//! promkit = "0.3.2"
//! promkit = "0.3.3"
//! ```
//!
//! ## Features
//!
//! - Support cross-platform both UNIX and Windows owing to [crossterm](https://github.com/crossterm-rs/crossterm)
//! - Various building methods
//! - Preset; Support for quickly setting up a UI by providing simple parameters.
//! - [Readline](https://github.com/ynqa/promkit/tree/v0.3.2#readline)
//! - [Confirm](https://github.com/ynqa/promkit/tree/v0.3.2#confirm)
//! - [Password](https://github.com/ynqa/promkit/tree/v0.3.2#password)
//! - [Select](https://github.com/ynqa/promkit/tree/v0.3.2#select)
//! - [QuerySelect](https://github.com/ynqa/promkit/tree/v0.3.2#queryselect)
//! - [Checkbox](https://github.com/ynqa/promkit/tree/v0.3.2#checkbox)
//! - [Tree](https://github.com/ynqa/promkit/tree/v0.3.2#tree)
//! - [Readline](https://github.com/ynqa/promkit/tree/v0.3.3#readline)
//! - [Confirm](https://github.com/ynqa/promkit/tree/v0.3.3#confirm)
//! - [Password](https://github.com/ynqa/promkit/tree/v0.3.3#password)
//! - [Select](https://github.com/ynqa/promkit/tree/v0.3.3#select)
//! - [QuerySelect](https://github.com/ynqa/promkit/tree/v0.3.3#queryselect)
//! - [Checkbox](https://github.com/ynqa/promkit/tree/v0.3.3#checkbox)
//! - [Tree](https://github.com/ynqa/promkit/tree/v0.3.3#tree)
//! - Combining various UI components.
//! - They are provided with the same interface, allowing users to choose and
//! assemble them according to their preferences.
Expand Down Expand Up @@ -91,15 +91,6 @@
//! *promkit* introduces a step to align data with the screen size before rendering.
//! This approach ensures consistency in UI elements even when
//! the terminal size changes, providing a smoother user experience.
//!
//! ## License
//!
//! This project is licensed under the MIT License.
//! See the [LICENSE](https://github.com/ynqa/promkit/blob/main/LICENSE)
//! file for details.
//!
//! ## Stargazers over time
//! [![Stargazers over time](https://starchart.cc/ynqa/promkit.svg?variant=adaptive)](https://starchart.cc/ynqa/promkit)
pub use crossterm;
pub use serde_json;
Expand Down
1 change: 1 addition & 0 deletions src/preset/query_selector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ impl QuerySelector {
active_char_style: StyleBuilder::new().bgc(Color::DarkCyan).build(),
inactive_char_style: StyleBuilder::new().build(),
edit_mode: Default::default(),
word_break_chars: Default::default(),
lines: Default::default(),
},
listbox_renderer: listbox::Renderer {
Expand Down
9 changes: 9 additions & 0 deletions src/preset/readline.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::collections::HashSet;

use crate::{
crossterm::{
event::Event,
Expand Down Expand Up @@ -58,6 +60,7 @@ impl Default for Readline {
active_char_style: StyleBuilder::new().bgc(Color::DarkCyan).build(),
inactive_char_style: StyleBuilder::new().build(),
edit_mode: Default::default(),
word_break_chars: HashSet::from([' ']),
lines: Default::default(),
},
suggest: Default::default(),
Expand Down Expand Up @@ -144,6 +147,12 @@ impl Readline {
self
}

/// Sets the characters to be for word break.
pub fn word_break_chars(mut self, characters: HashSet<char>) -> Self {
self.text_editor_renderer.word_break_chars = characters;
self
}

/// Sets the number of lines available for rendering the text editor.
pub fn text_editor_lines(mut self, lines: usize) -> Self {
self.text_editor_renderer.lines = Some(lines);
Expand Down
46 changes: 44 additions & 2 deletions src/preset/readline/keymap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::{
///
/// | Key | Action
/// | :--------------------- | :-------------------------------------------
/// | <kbd>Enter</kbd> | Exit the editor
/// | <kbd>Enter</kbd> | Exit the editor if input is valid, otherwise show error message
/// | <kbd>Ctrl + C</kbd> | Interrupt the current operation
/// | <kbd>←</kbd> | Move the cursor one character to the left
/// | <kbd>→</kbd> | Move the cursor one character to the right
Expand All @@ -18,7 +18,11 @@ use crate::{
/// | <kbd>↓</kbd> | Recall the next entry from history
/// | <kbd>Backspace</kbd> | Delete the character before the cursor
/// | <kbd>Ctrl + U</kbd> | Delete all characters in the current line
/// | <kbd>TAB</kbd> | Autocomplete the current input based on available suggestions
/// | <kbd>Tab</kbd> | Autocomplete the current input based on available suggestions
/// | <kbd>Alt + B</kbd> | Move the cursor to the previous nearest character within set (default: whitespace)
/// | <kbd>Alt + F</kbd> | Move the cursor to the next nearest character within set (default: whitespace)
/// | <kbd>Ctrl + W</kbd> | Erase to the previous nearest character within set (default: whitespace)
/// | <kbd>Alt + D</kbd> | Erase to the next nearest character within set (default: whitespace)
pub fn default(
event: &Event,
renderer: &mut preset::readline::render::Renderer,
Expand Down Expand Up @@ -119,6 +123,25 @@ pub fn default(
state: KeyEventState::NONE,
}) => text_editor_after_mut.texteditor.move_to_tail(),

// Move cursor to the nearest character.
Event::Key(KeyEvent {
code: KeyCode::Char('b'),
modifiers: KeyModifiers::ALT,
kind: KeyEventKind::Press,
state: KeyEventState::NONE,
}) => text_editor_after_mut
.texteditor
.move_to_previous_nearest(&text_editor_after_mut.word_break_chars),

Event::Key(KeyEvent {
code: KeyCode::Char('f'),
modifiers: KeyModifiers::ALT,
kind: KeyEventKind::Press,
state: KeyEventState::NONE,
}) => text_editor_after_mut
.texteditor
.move_to_next_nearest(&text_editor_after_mut.word_break_chars),

// Erase char(s).
Event::Key(KeyEvent {
code: KeyCode::Backspace,
Expand All @@ -133,6 +156,25 @@ pub fn default(
state: KeyEventState::NONE,
}) => text_editor_after_mut.texteditor.erase_all(),

// Erase to the nearest character.
Event::Key(KeyEvent {
code: KeyCode::Char('w'),
modifiers: KeyModifiers::CONTROL,
kind: KeyEventKind::Press,
state: KeyEventState::NONE,
}) => text_editor_after_mut
.texteditor
.erase_to_previous_nearest(&text_editor_after_mut.word_break_chars),

Event::Key(KeyEvent {
code: KeyCode::Char('d'),
modifiers: KeyModifiers::ALT,
kind: KeyEventKind::Press,
state: KeyEventState::NONE,
}) => text_editor_after_mut
.texteditor
.erase_to_next_nearest(&text_editor_after_mut.word_break_chars),

// Choose history
Event::Key(KeyEvent {
code: KeyCode::Up,
Expand Down

0 comments on commit 33e8c8f

Please sign in to comment.