diff --git a/.gitignore b/.gitignore index 81928531..86ef9574 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ Cargo.lock **/target docs/*.bkp docs/*.dtmp +.idea diff --git a/.vscode/bookmarks.json b/.vscode/bookmarks.json new file mode 100644 index 00000000..9df6dfd5 --- /dev/null +++ b/.vscode/bookmarks.json @@ -0,0 +1,34 @@ +{ + "files": [ + { + "path": "src/tui/crossterm_helpers/tw_command.rs", + "bookmarks": [ + { + "line": 319, + "column": 4, + "label": "" + }, + { + "line": 326, + "column": 4, + "label": "" + }, + { + "line": 486, + "column": 8, + "label": "" + } + ] + }, + { + "path": "src/tui/terminal_window/main_event_loop.rs", + "bookmarks": [ + { + "line": 231, + "column": 6, + "label": "" + } + ] + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index b73354f0..3dc6561c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -15,7 +15,11 @@ "dialup", "dimens", "drawio", + "editorbuffer", + "editorcomponent", + "editorengine", "enumflags", + "globalcursor", "idents", "InputEvent", "insertanchor", @@ -24,9 +28,11 @@ "keyb", "keyevent", "Keypress", + "keypresses", "lazyfield", "lazymemovalues", "litint", + "localpaintedeffect", "middlewares", "neovim", "notcurses", diff --git a/README.md b/README.md index c706e46c..0ab5025a 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,8 @@

# Context - + @@ -49,8 +49,8 @@ it. 3. integrations w/ calendar, email, contacts APIs # About this library crate: r3bl_rs_utils - + This crate is the first thing that's described above. It provides lots of useful functionality to help you build TUI (text user interface) apps, along w/ general niceties & ergonomics that all @@ -147,8 +147,8 @@ Table of contents:
## tui - + You can build fully async TUI apps with a modern API that brings the best of reactive & unidirectional data flow architecture from frontend web development (React, Redux, CSS, flexbox) to @@ -173,8 +173,8 @@ Here are some framework highlights: - Support for Unicode grapheme clusters in strings. ### Life of an input event - + There is a clear separation of concerns in this module. To illustrate what goes where, and how things work let's look at an example that puts the main event loop front and center & deals w/ how @@ -225,8 +225,8 @@ Now that we have seen this whirlwind overview of the life of an input event, let details in each of the sections below. ### The window - + The main building blocks of a TUI app are: @@ -243,8 +243,8 @@ The main building blocks of a TUI app are: we have to deal with [FlexBox], [Component], and [crate::Style]. ### Layout and styling - + Inside of your [App] if you want to use flexbox like layout and CSS like styling you can think of composing your code in the following way: @@ -261,8 +261,8 @@ composing your code in the following way: dispatch actions to the store, and even have async middleware! ### Component, ComponentRegistry, focus management, and event routing - + Typically your [App] will look like this: @@ -291,16 +291,16 @@ Another thing to keep in mind is that the [App] and [TerminalWindow] is persiste re-renders. The Redux store is also persistent between re-renders. ### Input event specificity - + [TerminalWindow] gives [Component] first dibs when it comes to handling input events. If it punts handling this event, it will be handled by the default input event handler. And if nothing there matches this event, then it is simply dropped. ### Redux for state management - + If you use Redux for state management, then you will create a [crate::redux] [crate::Store] that is passed into the [TerminalWindow]. Here's an example of this. @@ -345,28 +345,28 @@ async fn create_store() -> Store { ``` ### Grapheme support - + Unicode is supported (to an extent). There are some caveats. The [crate::UnicodeStringExt] trait has lots of great information on this graphemes and what is supported and what is not. ### Lolcat support - + An implementation of [crate::lolcat::cat] w/ a color wheel is provided. ### Examples to get you started - + 1. [Code example of an address book using Redux](https://github.com/r3bl-org/address-book-with-redux-tui). 2. [Code example of TUI apps using Redux](https://github.com/r3bl-org/r3bl-cmdr). ## redux - + `Store` is thread safe and asynchronous (using Tokio). You have to implement `async` traits in order to use it, by defining your own reducer, subscriber, and middleware trait objects. You also have to @@ -382,8 +382,8 @@ Tokio executor / runtime, without which you will get a panic when `spawn_dispatc called. ### Middlewares - + Your middleware (`async` trait implementations) will be run concurrently or in parallel via Tokio tasks. You get to choose which `async` trait to implement to do one or the other. And regardless of @@ -403,16 +403,16 @@ call). are added to the store via a call to `add_middleware(...)`. ### Subscribers - + The subscribers will be run asynchronously via Tokio tasks. They are all run together concurrently but not in parallel, using [`futures::join_all()`](https://docs.rs/futures/latest/futures/future/fn.join_all.html). ### Reducers - + The reducer functions are also are `async` functions that are run in the tokio runtime. They're also run one after another in the order in which they're added. @@ -447,8 +447,8 @@ run one after another in the order in which they're added. - It returns nothing `()`. ### Summary - + Here's the gist of how to make & use one of these: @@ -468,8 +468,8 @@ Here's the gist of how to make & use one of these: `Box::new($YOUR_STRUCT))`. ### Examples - + > 💡 There are lots of examples in the > [tests](https://github.com/r3bl-org/r3bl-rs-utils/blob/main/tests/test_redux.rs) for this library @@ -701,28 +701,28 @@ assert_eq!(store.get_state().stack.len(), 0); ``` ## Macros - + ### Declarative - + There are quite a few declarative macros that you will find in the library. They tend to be used internally in the implementation of the library itself. Here are some that are actually externally exposed via `#[macro_export]`. #### assert_eq2! - + Similar to [`assert_eq!`] but automatically prints the left and right hand side variables if the assertion fails. Useful for debugging tests, since the cargo would just print out the left and right values w/out providing information on what variables were being compared. #### throws! - + Wrap the given `block` or `stmt` so that it returns a `Result<()>`. It is just syntactic sugar that helps having to write `Ok(())` repeatedly at the end of each block. Here's an example. @@ -760,8 +760,8 @@ fn test_simple_2_col_layout() -> CommonResult<()> { ``` #### throws_with_return! - + This is very similar to [`throws!`](#throws) but it also returns the result of the block. @@ -775,8 +775,8 @@ fn test_simple_2_col_layout() -> CommonResult { ``` #### log! - + You can use this macro to dump log messages at 3 levels to a file. By default this file is named `log.txt` and is dumped in the current directory. Here's how you can use it. @@ -862,8 +862,8 @@ Please check out the source [here](https://github.com/r3bl-org/r3bl-rs-utils/blob/main/src/utils/file_logging.rs). #### log_no_err! - + This macro is very similar to the [log!](#log) macro, except that it won't return any error if the underlying logging system fails. It will simply print a message to `stderr`. Here's an example. @@ -876,8 +876,8 @@ pub fn log_state(&self, msg: &str) { ``` #### debug_log_no_err! - + This is a really simple macro to make it effortless to debug into a log file. It outputs `DEBUG` level logs. It takes a single identifier as an argument, or any number of them. It simply dumps an @@ -891,8 +891,8 @@ debug_log_no_err!(my_string); ``` #### trace_log_no_err! - + This is very similar to [debug_log_no_err!](#debuglognoerr) except that it outputs `TRACE` level logs. @@ -903,8 +903,8 @@ trace_log_no_err!(my_string); ``` #### make_api_call_for! - + This macro makes it easy to create simple HTTP GET requests using the `reqwest` crate. It generates an `async` function called `make_request()` that returns a `CommonResult` where `T` is the type @@ -945,8 +945,8 @@ You can find lots of [examples here](https://github.com/r3bl-org/address-book-with-redux-tui/blob/main/src/tui/middlewares). #### fire_and_forget! - + This is a really simple wrapper around `tokio::spawn()` for the given block. Its just syntactic sugar. Here's an example of using it for a non-`async` block. @@ -978,8 +978,8 @@ pub fn foo() { ``` #### call_if_true! - + Syntactic sugar to run a conditional statement. Here's an example. @@ -997,8 +997,8 @@ call_if_true!( ``` #### debug! - + This is a really simple macro to make it effortless to use the color console logger. It takes a single identifier as an argument, or any number of them. It simply dumps an arrow symbol, followed @@ -1030,8 +1030,8 @@ debug!(OK_RAW &msg); ``` #### with! - + This is a macro that takes inspiration from the `with` scoping function in Kotlin. It just makes it easier to express a block of code that needs to run after an expression is evaluated and saved to a @@ -1060,8 +1060,8 @@ It does the following: 2. Runs the `$code` block. #### with_mut! - + This macro is just like [`with!`](#with) but it takes a mutable reference to the `$id` variable. Here's a code example. @@ -1082,8 +1082,8 @@ with_mut! { ``` #### with_mut_returns! - + This macro is just like [`with_mut!`](#withmutreturns) except that it returns the value of the `$code` block. Here's a code example. @@ -1099,8 +1099,8 @@ let tw_queue = with_mut_returns! { ``` #### unwrap_option_or_run_fn_returning_err! - + This macro can be useful when you are working w/ an expression that returns an `Option` and if that `Option` is `None` then you want to abort and return an error immediately. The idea is that you are @@ -1123,8 +1123,8 @@ pub fn from( ``` #### unwrap_option_or_compute_if_none! - + This macro is basically a way to compute something lazily when it (the `Option`) is set to `None`. Unwrap the `$option`, and if `None` then run the `$next` closure which must return a value that is @@ -1146,16 +1146,16 @@ fn test_unwrap_option_or_compute_if_none() { ``` ### Procedural - + All the procedural macros are organized in 3 crates [using an internal or core crate](https://developerlife.com/2022/03/30/rust-proc-macro/#add-an-internal-or-core-crate): the public crate, an internal or core crate, and the proc macro crate. #### Builder derive macro - + This derive macro makes it easy to generate builders when annotating a `struct` or `enum`. It generates It has full support for generics. It can be used like this. @@ -1181,8 +1181,8 @@ assert_eq!(my_pt.y, 2); ``` #### make_struct_safe_to_share_and_mutate! - + This function like macro (with custom syntax) makes it easy to manage shareability and interior mutability of a struct. We call this pattern the "manager" of "things"). @@ -1226,8 +1226,8 @@ async fn test_custom_syntax_no_where_clause() { ``` #### make_safe_async_fn_wrapper! - + This function like macro (with custom syntax) makes it easy to share functions and lambdas that are async. They should be safe to share between threads and they should support either being invoked or @@ -1271,8 +1271,8 @@ make_safe_async_fn_wrapper! { ``` ## tree_memory_arena (non-binary tree data structure) - + [`Arena`] and [`MTArena`] types are the implementation of a [non-binary tree](https://en.wikipedia.org/wiki/Binary_tree#Non-binary_trees) data structure that is @@ -1398,12 +1398,12 @@ let arena = MTArena::::new(); > [here](https://github.com/r3bl-org/r3bl-rs-utils/blob/main/tests/tree_memory_arena_test.rs). ## utils - + ### CommonResult and CommonError - + These two structs make it easier to work w/ `Result`s. They are just syntactic sugar and helper structs. You will find them used everywhere in the @@ -1434,8 +1434,8 @@ impl Stylesheet { ``` ### LazyField - + This combo of struct & trait object allows you to create a lazy field that is only evaluated when it is first accessed. You have to provide a trait implementation that computes the value of the field @@ -1469,8 +1469,8 @@ fn test_lazy_field() { ``` ### LazyMemoValues - + This struct allows users to create a lazy hash map. A function must be provided that computes the values when they are first requested. These values are cached for the lifetime this struct. Here's @@ -1500,8 +1500,8 @@ assert_eq!(arc_atomic_count.load(SeqCst), 1); // Doesn't change. ``` ### tty - + This module contains a set of functions to make it easier to work with terminals. @@ -1548,8 +1548,8 @@ Here's a list of functions available in this module: - `is_stdin_piped()` ### safe_unwrap - + Functions that make it easy to unwrap a value safely. These functions are provided to improve the ergonomics of using wrapped values in Rust. Examples of wrapped values are `>`, and @@ -1595,8 +1595,8 @@ Here's a list of type aliases provided for better readability: - `WriteGuarded` ### color_text - + ANSI colorized text helper methods. Here's an example. @@ -1628,8 +1628,8 @@ Here's a list of functions available in this module: - `style_error()` ## Stability - + 🧑‍🔬 This library is in active development. @@ -1641,15 +1641,15 @@ Here's a list of functions available in this module: 3. There are extensive tests for code that is production ready. ## Issues, comments, feedback, and PRs - + Please report any issues to the [issue tracker](https://github.com/r3bl-org/r3bl-rs-utils/issues). And if you have any feature requests, feel free to add them there too 👍. ## Notes - + Here are some notes on using experimental / unstable features in Tokio. diff --git a/TODO.todo b/TODO.todo index 164c5e41..57ba6818 100644 --- a/TODO.todo +++ b/TODO.todo @@ -1,3 +1,41 @@ ╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮ │ r3bl_rs_utils │ ╯ ╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ +editor engine (https://github.com/r3bl-org/r3bl_rs_utils/issues/23): + ✔ wire up the engine into framework @done(22-08-31 13:56) + ✔ fancy debug impl for editor buffer @done(22-08-31 13:57) + ✔ insert content: @done(22-09-01 11:46) + ✔ type characters & store in buffer @done(22-09-01 11:46) + ✔ add tests for editor buffer @done(22-09-01 11:46) + ✔ paint caret: @done(22-09-02 09:43) + ✔ use cursor show / hide @done(22-09-02 08:19) + ✔ use reverse / invert colors to paint the caret (so there can be many) @done(22-09-02 09:42) + ✔ bounds check max rows when painting content @done(22-09-02 11:29) + ✔ implement render clipping: @done(22-09-02 13:10) + ✔ figure out how to incorporate row & col bounds checking to implement clipping @done(22-09-02 13:10) + ☐ insert content: + ☐ handle new lines + ☐ move cursor: + ☐ left/right arrow key move in buffer + ☐ up/down arrow key move in buffer + ☐ delete content: + ☐ delete/backspace edit buffer line + ☐ delete/backspace lines + ☐ scrolling + ☐ left/right + ☐ up/down + ☐ keyboard shortcut to save/load buffer to/from file + ☐ highlight and selection: + ☐ add support for text selection highlighting + ☐ selection, copy, paste + ☐ multiple carets: + ☐ add support for multiple carets & network service providers to move them + +framework: + ☐ https://github.com/r3bl-org/r3bl_rs_utils/issues/28 + ☐ https://github.com/r3bl-org/r3bl_rs_utils/issues/27 + ☐ https://github.com/r3bl-org/r3bl_rs_utils/issues/24 + ☐ https://github.com/r3bl-org/r3bl_rs_utils/issues/26 + +writing: + ☐ https://github.com/r3bl-org/r3bl_rs_utils/issues/19 \ No newline at end of file diff --git a/core/src/decl_macros.rs b/core/src/decl_macros.rs index 4b05d972..ce28e9c5 100644 --- a/core/src/decl_macros.rs +++ b/core/src/decl_macros.rs @@ -143,6 +143,27 @@ macro_rules! call_if_true { }}; } +/// Syntactic sugar to run a conditional statement. Here's an example. +/// ```rust +/// const DEBUG: bool = true; +/// call_if_debug_true!( +/// eprintln!( +/// "{} {} {}\r", +/// r3bl_rs_utils::style_error("▶"), +/// r3bl_rs_utils::style_prompt($msg), +/// r3bl_rs_utils::style_dimmed(&format!("{:#?}", $err)) +/// ) +/// ); +/// ``` +#[macro_export] +macro_rules! call_if_debug_true { + ($block: expr) => {{ + if DEBUG { + $block + } + }}; +} + /// This is a really simple macro to make it effortless to use the color console /// logger. It takes a single identifier as an argument, or any number of them. /// It simply dumps an arrow symbol, followed by the identifier ([stringify]'d) diff --git a/core/src/tui_core/dimens/base_units.rs b/core/src/tui_core/dimens/base_units.rs index 0b030a30..329a9986 100644 --- a/core/src/tui_core/dimens/base_units.rs +++ b/core/src/tui_core/dimens/base_units.rs @@ -25,3 +25,11 @@ macro_rules! convert_to_base_unit { $self.try_into().unwrap_or($self as UnitType) }; } + +/// Converts a [UnitType] to ([i32], [usize]), etc. +#[macro_export] +macro_rules! convert_from_base_unit { + ($self:expr) => { + $self.try_into().unwrap_or($self as usize) + }; +} diff --git a/core/src/tui_core/dimens/position.rs b/core/src/tui_core/dimens/position.rs index 836134a2..9b702d48 100644 --- a/core/src/tui_core/dimens/position.rs +++ b/core/src/tui_core/dimens/position.rs @@ -90,24 +90,38 @@ impl From for (UnitType, UnitType) { } impl Position { - /// Add given `col` value to `self`. - pub fn add_col(&mut self, value: usize) -> Self { - let value: UnitType = value as UnitType; + /// Add given `col` count to `self`. + pub fn add_cols(&mut self, num_cols_to_add: usize) -> Self { + let value: UnitType = convert_to_base_unit!(num_cols_to_add); self.col += value; *self } - /// Add given `row` value to `self`. - pub fn add_row(&mut self, value: usize) -> Self { - let value = value as UnitType; + /// Add given `col` count to `self` w/ bounds check for max cols. + pub fn add_cols_with_bounds(&mut self, num_cols_to_add: usize, box_bounds_size: Size) -> Self { + let value: UnitType = convert_to_base_unit!(num_cols_to_add); + let max: UnitType = box_bounds_size.cols; + + if (self.col + value) >= max { + self.col = max + } else { + self.col += value; + } + + *self + } + + /// Add given `row` count to `self`. + pub fn add_rows(&mut self, num_rows_to_add: usize) -> Self { + let value = convert_to_base_unit!(num_rows_to_add); self.row += value; *self } - /// Add given `row` value to `self` w/ bounds check for max rows. - pub fn add_row_with_bounds(&mut self, value: usize, box_bounding_size: Size) -> Self { - let value: UnitType = value as UnitType; - let max: UnitType = box_bounding_size.rows; + /// Add given `row` count to `self` w/ bounds check for max rows. + pub fn add_rows_with_bounds(&mut self, num_rows_to_add: usize, box_bounds_size: Size) -> Self { + let value: UnitType = convert_to_base_unit!(num_rows_to_add); + let max: UnitType = box_bounds_size.rows; if (self.row + value) >= max { self.row = max diff --git a/core/src/tui_core/dimens/size.rs b/core/src/tui_core/dimens/size.rs index 2918b280..e911d399 100644 --- a/core/src/tui_core/dimens/size.rs +++ b/core/src/tui_core/dimens/size.rs @@ -110,6 +110,10 @@ impl Debug for Size { } } +/// Example: +/// ```ignore +/// let size: Size = size!(col: 10, row: 10); +/// ``` #[macro_export] macro_rules! size { ( diff --git a/core/src/tui_core/graphemes/unicode_string_ext.rs b/core/src/tui_core/graphemes/unicode_string_ext.rs index c69835c4..006ff017 100644 --- a/core/src/tui_core/graphemes/unicode_string_ext.rs +++ b/core/src/tui_core/graphemes/unicode_string_ext.rs @@ -16,9 +16,9 @@ */ use unicode_segmentation::UnicodeSegmentation; -use unicode_width::UnicodeWidthStr; +use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; -use crate::{convert_to_base_unit, Size, UnitType}; +use crate::{convert_to_base_unit, CommonResult, Size, UnitType}; /// A grapheme cluster is a user-perceived character. Rust uses `UTF-8` to /// represent text in `String`. So each character takes up 8 bits or one byte. @@ -203,15 +203,6 @@ impl UnicodeStringExt for String { } } -#[derive(Debug, Clone)] -pub struct UnicodeString { - pub string: String, - pub vec_segment: Vec, - pub byte_size: usize, - pub grapheme_cluster_segment_count: usize, - pub display_width: UnitType, -} - #[derive(Debug, Clone)] pub struct GraphemeClusterSegment { /// The actual grapheme cluster `&str`. Eg: "H", "📦", "🙏🏽". @@ -228,7 +219,51 @@ pub struct GraphemeClusterSegment { pub display_col_offset: UnitType, } +#[derive(Debug, Clone)] +pub struct UnicodeString { + pub string: String, + pub vec_segment: Vec, + pub byte_size: usize, + pub grapheme_cluster_segment_count: usize, + pub display_width: UnitType, +} + +/// Convert [char] to [GraphemeClusterSegment]. +impl From for GraphemeClusterSegment { + fn from(character: char) -> Self { + let my_string: String = character.into(); + my_string.unicode_string().vec_segment[0].clone() + } +} + +/// Convert [&str] to [GraphemeClusterSegment]. +impl From<&str> for GraphemeClusterSegment { + fn from(chunk: &str) -> Self { + let my_string: String = chunk.to_string(); + my_string.unicode_string().vec_segment[0].clone() + } +} + +/// Convert [Vec] to [String]. +fn to_string(vec_grapheme_cluster_segment: Vec) -> String { + let mut my_string = String::new(); + for grapheme_cluster_segment in vec_grapheme_cluster_segment { + my_string.push_str(&grapheme_cluster_segment.string); + } + my_string +} + impl UnicodeString { + pub fn char_display_width(character: char) -> usize { + let display_width: usize = UnicodeWidthChar::width(character).unwrap_or(0); + display_width + } + + pub fn str_display_width(string: &str) -> usize { + let display_width: usize = UnicodeWidthStr::width(string); + display_width + } + pub fn truncate_to_fit_size(&self, size: Size) -> &str { let display_cols: UnitType = size.cols; self.truncate_to_fit_display_cols(display_cols) @@ -249,10 +284,12 @@ impl UnicodeString { &self.string[..string_end_byte_index] } + /// `local_index` is the index of the grapheme cluster in the `vec_segment`. pub fn at_logical_index(&self, logical_index: usize) -> Option<&GraphemeClusterSegment> { self.vec_segment.get(logical_index) } + /// `display_col` is the col index in the terminal where this grapheme cluster can be displayed. pub fn at_display_col(&self, display_col: UnitType) -> Option<&GraphemeClusterSegment> { self.vec_segment.iter().find(|&grapheme_cluster_segment| { let segment_display_col_start: UnitType = grapheme_cluster_segment.display_col_offset; @@ -262,17 +299,62 @@ impl UnicodeString { }) } + /// Convert a `display_col` to a `logical_index`. + /// - `local_index` is the index of the grapheme cluster in the `vec_segment`. + /// - `display_col` is the col index in the terminal where this grapheme cluster can be displayed. pub fn logical_index_at_display_col(&self, display_col: UnitType) -> Option { self .at_display_col(display_col) .map(|segment| segment.logical_index) } + /// Convert a `logical_index` to a `display_col`. + /// - `local_index` is the index of the grapheme cluster in the `vec_segment`. + /// - `display_col` is the col index in the terminal where this grapheme cluster can be displayed. pub fn display_col_at_logical_index(&self, logical_index: usize) -> Option { self .at_logical_index(logical_index) .map(|segment| segment.display_col_offset) } + + /// Returns a new ([String], [UnitType]) tuple and does not modify + /// [self.string](UnicodeString::string). + pub fn insert_char_at_display_col( + &self, display_col: UnitType, chunk: &str, + ) -> CommonResult<(String, UnitType)> { + let maybe_logical_index = self.logical_index_at_display_col(display_col); + match maybe_logical_index { + // Insert somewhere inside bounds of self.string. + Some(logical_index) => { + // Convert the character into a grapheme cluster. + let character_g_c_s: GraphemeClusterSegment = chunk.into(); + let character_display_width: UnitType = character_g_c_s.unicode_width; + + // Insert this grapheme cluster to self.vec_segment. + let mut vec_segment_clone = self.vec_segment.clone(); + vec_segment_clone.insert(logical_index, character_g_c_s); + + // Generate a new string from self.vec_segment and return it and the unicode width of the + // character. + let new_string = to_string(vec_segment_clone); + + // In the caller - update the caret position based on the unicode width of the character. + Ok((new_string, character_display_width)) + } + // Add to end of self.string. + None => { + // Push character to the end of the cloned string. + let mut new_string = self.string.clone(); + new_string.push_str(chunk); + + // Get the unicode width of the character. + let character_display_width = UnicodeString::str_display_width(chunk); + + // In the caller - update the caret position based on the unicode width of the character. + Ok((new_string, convert_to_base_unit!(character_display_width))) + } + } + } } pub fn try_strip_ansi(text: &str) -> Option { diff --git a/core/src/tui_core/lolcat/cat.rs b/core/src/tui_core/lolcat/cat.rs index fbb2067c..43262100 100644 --- a/core/src/tui_core/lolcat/cat.rs +++ b/core/src/tui_core/lolcat/cat.rs @@ -20,9 +20,9 @@ use std::{fmt::Display, thread::sleep, time::Duration}; +use get_size::GetSize; use rand::{thread_rng, Rng}; use serde::*; -use get_size::GetSize; use crate::*; diff --git a/core/src/tui_core/lolcat/control.rs b/core/src/tui_core/lolcat/control.rs index 767ef29d..05213cdb 100644 --- a/core/src/tui_core/lolcat/control.rs +++ b/core/src/tui_core/lolcat/control.rs @@ -18,9 +18,9 @@ use std::fmt::Display; use atty::Stream; +use get_size::GetSize; use rand::random; use serde::*; -use get_size::GetSize; /// A struct to contain info we need to print with every character. #[derive(Debug, Clone, Copy, Serialize, Deserialize, GetSize)] diff --git a/docs/dd_editor_component.md b/docs/dd_editor_component.md new file mode 100644 index 00000000..488df477 --- /dev/null +++ b/docs/dd_editor_component.md @@ -0,0 +1,243 @@ +# Design document for editor component, Aug 28 2022 + + + + + +- [Goal](#goal) +- [Timeline & features](#timeline--features) +- [Milestones](#milestones) +- [Resources](#resources) +- [Proposed solution - add an EditorEngine field to the EditorComponent and add an EditorBuffer field to the State](#proposed-solution---add-an-editorengine-field-to-the-editorcomponent-and-add-an-editorbuffer-field-to-the-state) + - [Scope](#scope) + - [Constraints](#constraints) + - [Solution overview](#solution-overview) +- [Painting caret using cursor and another approach](#painting-caret-using-cursor-and-another-approach) + - [**GlobalCursor** - Use the terminal's cursor show / hide.](#globalcursor---use-the-terminals-cursor-show--hide) + - [**LocalPaintedEffect** - Paint the character at the cursor w/ the colors inverted or some](#localpaintedeffect---paint-the-character-at-the-cursor-w-the-colors-inverted-or-some) + - [Using both](#using-both) + + + +## Goal + + + +Create an editor component that is very similar to +[`micro` text editor](https://micro-editor.github.io/). But it must live in the tui layout engine: + +1. Meaning that it can be fit in different shaped boxes in the main terminal window. +2. We can't assume that it will take up 100% height & width of the terminal since there are other UI + components on the same "screen" / terminal window. + +## Timeline & features + + + +1. Editor component that can fit in a `TWBox` and is implemented as a `Component`. Example of a + `Component` + [`column_render_component.rs`](https://github.com/r3bl-org/r3bl-cmdr/blob/main/src/ex_app_with_layout/column_render_component.rs). +2. Supports editing but not saving. +3. Supports focus management, so there may be multiple editor components in a single `TWApp`. +4. Support unicode grapheme clusters (cursor navigation). +5. Rudimentary support for syntax highlighting (using an extensible backend to implement support for + various languages / file formats). + +Timeline is about 6 weeks. + +## Milestones + + + +1. Start building an example in `r3bl-cmdr` repo for editor component. +2. Create an app that has a 2 column layout, w/ a different editor component in each column. +3. Get the code solid in the example. Then migrate the code w/ tests into the `tui` module of + `r3bl_rs_utils` repo. + +## Resources + + + +This is a great series of videos on syntax highlighting & unicode support in editors: + +- https://www.youtube.com/playlist?list=PLP2yfE2-FXdQw0I6O4YdIX_mzBeF5TDdv +- There are a lot of videos here, but they're organized by topic +- Topics include: unicode handling, text wrapping, and syntax highlighting engines + +Here are a few resources related to editors, and syntax highlighting: + +- https://github.com/ndd7xv/heh +- https://docs.rs/syntect/latest/syntect/ +- https://github.com/zee-editor/zee +- https://github.com/helix-editor/helix + +Here are some other TUI frameworks: + +- https://dioxuslabs.com/ +- https://github.com/veeso/tui-realm/blob/main/docs/en/get-started.md + +## Proposed solution - add an `EditorEngine` field to the `EditorComponent` and add an `EditorBuffer` field to the `State` + + + +![editor_component drawio](https://raw.githubusercontent.com/r3bl-org/r3bl_rs_utils/main/docs/editor_component.drawio.svg) + +### Scope + + + +The goal is to create a reusable editor component. This example +[here](https://github.com/r3bl-org/r3bl-cmdr/tree/main/src/ex_editor) is a very simple application, +w/ a single column layout that takes up the full height & width of the terminal window. The goal is +to add a reusable editor component to this example to get the most basic editor functionality +created. + +### Constraints + + + +The application has a `State` and `Action` that are specific to the `AppWithLayout` (which +implements the `App` trait). + +1. The `launcher.rs` creates the store, and app itself, and passes it to `main_event_loop` to get + everything started. +2. This means that the `EditorComponent` struct which implements `Component` trait is actually + directly coupled to the `App` itself. So something else has to be reusable, since + `EditorComponent` can't be. + +The `EditorComponent` struct might be a good place to start looking for possible solutions. + +- This struct can hold data in its own memory. It already has a `Lolcat` struct inside of it. +- It also implements the `Component` trait. +- However, for the reusable editor component we need the data representing the document being edited + to be stored in the `State` and not inside of the `EditorComponent` itself (like the `lolcat` + field). + +### Solution overview + + + +1. Add two new structs: + + 1. `EditorEngine` -> **This goes in `EditorComponent`** + - Contains the logic to process keypresses and modify an editor buffer. + 2. `EditorBuffer` -> **This goes in the `State`** + - Contains the data that represents the document being edited. This can also contain the + undo/redo history. + +2. Here are the connection points w/ the impl of `Component` in `EditorComponent`: + 1. `handle_event(input_event: &InputEvent, state: &S, shared_store: &SharedStore)` + - Can simply relay the arguments to `EditorEngine::apply(state.editor_buffer, input_event)` + which will return another `EditorBuffer`. + - Return value can be dispatched to the store via an action + `UpdateEditorBuffer(EditorBuffer)`. + 2. `render(has_focus: &HasFocus, current_box: &FlexBox, state: &S, shared_store: &SharedStore)` + - Can simply relay the arguments to `EditorEngine::render(state.editor_buffer)` + - Which will return a `TWCommandQueue`. + +Sample code: + +```rust +pub struct EditorEngine; +impl EditorEngine { + fn async apply( + editor_buffer: &EditorBuffer, input_event: &InputEvent + ) -> EditorBuffer { + todo!(); + } + + fn async render( + editor_buffer: &EditorBuffer, has_focus: &HasFocus, current_box: &FlexBox + ) -> TWCommandQueue { + todo!(); + } +} + +pub struct EditorBuffer { + // TODO +} +``` + +These commits are related to the work described here: + +1. [Add EditorEngine & EditorBuffer skeleton](https://github.com/r3bl-org/r3bl_rs_utils/commit/6dea59b68f90330b3e95639751f92a18bf28bee4) +2. [Add EditorEngine & EditorBuffer integration for editor component](https://github.com/r3bl-org/r3bl-cmdr/commit/1041c1f7cfee91f9ca0166384dabeb8fe6b21a01) + +## Painting caret (using cursor and another approach) + + + +> Definitions +> +> **`Caret`** - the block that is visually displayed in a terminal which represents the insertion +> point for whatever is in focus. While only one insertion point is editable for the local user, +> there may be multiple of them, in which case there has to be a way to distinguish a local caret +> from a remote one (this can be done w/ bg color). +> +> **`Cursor`** - the global "thing" provided in terminals that shows by blinking usually where the +> cursor is. This cursor is moved around and then paint operations are performed on various +> different areas in a terminal window to paint the output of render operations. + +There are two ways of showing cursors which are quite different (each w/ very different +constraints). + +### 1. **`GlobalCursor`** - Use the terminal's cursor show / hide. + + + +1. Both [termion::cursor](https://docs.rs/termion/1.5.6/termion/cursor/index.html) and + [crossterm::cursor](https://docs.rs/crossterm/0.25.0/crossterm/cursor/index.html) support this. + The cursor has lots of effects like blink, etc. +2. The downside is that there is one global cursor for any given terminal window. And this cursor + is constantly moved around in order to paint anything (eg: + `MoveTo(col, row), SetColor, PaintText(...)` sequence). +3. So it must be guaranteed by + [TWCommandQueue via TWCommand::ShowCaretAtPosition???To(...)](https://github.com/r3bl-org/r3bl_rs_utils/blob/main/src/tui/crossterm_helpers/tw_command.rs#L171). + The downside here too is that there's a chance that different components and render functions + will clobber this value that's already been set. There's currently a weak warning that's + displayed after the 1st time this value is set which isn't robust either. +4. This is what that code looks like: + ```rust + // Approach 1 - using cursor show / hide. + tw_command_queue! { + queue push + TWCommand::ShowCaretAtPositionRelTo(box_origin_pos, editor_buffer.caret) + }; + ``` + +### 2. **`LocalPaintedEffect`** - Paint the character at the cursor w/ the colors inverted (or some + + + +other bg color) giving the visual effect of a cursor. + +1. This has the benefit that we can display multiple cursors in the app, since this is not global, + rather it is component specific. For the use case requiring google docs style multi user editing + where multiple cursors need to be shown, this approach can be used in order to implement that. + Each user for eg can get a different caret background color to differentiate their caret from + others. +2. The downside is that it isn't possible to blink the cursor or have all the other "standard" + cursor features that are provided by the actual global cursor (discussed above). +3. This is what that code looks like: + ```rust + // Approach 2 - painting the editor_buffer.caret position w/ reverse. + tw_command_queue! { + queue push + TWCommand::MoveCursorPositionRelTo(box_origin_pos, editor_buffer.caret), + TWCommand::PrintWithAttributes( + editor_buffer.get_char_at_caret().unwrap_or(DEFAULT_CURSOR_CHAR).into(), + style! { attrib: [reverse] }.into()), + TWCommand::MoveCursorPositionRelTo(box_origin_pos, editor_buffer.caret) + }; + ``` + +### Using both + + + +It might actually be necessary to use both `GlobalCursor` and `LocalPaintedEffect` approaches +simultaneously. + +1. `GlobalCursor` might be used to show the local user's caret since it blinks, etc +2. `LocalPaintedEffect` might be used to show remote user's carets since it doesn't blink and + supports a multitude of background colors that can be applied to distinguish users. diff --git a/macro/src/lib.rs b/macro/src/lib.rs index 1f6c6714..ab5fc3ff 100644 --- a/macro/src/lib.rs +++ b/macro/src/lib.rs @@ -37,6 +37,22 @@ pub fn derive_macro_builder(input: TokenStream) -> TokenStream { builder::derive_proc_macro_impl(input) } +/// Example. +/// +/// ``` +/// style! { +/// id: "my_style", /* Optional. */ +/// attrib: [dim, bold] /* Optional. */ +/// padding: 10, /* Optional. */ +/// color_fg: TWColor::Blue, /* Optional. */ +/// color_bg: TWColor::Red, /* Optional. */ +/// } +/// ``` +/// +/// `color_fg` and `color_bg` can take any of the following: +/// 1. Color enum value. +/// 2. Rgb value. +/// 3. Variable holding either of the above.qq #[proc_macro] pub fn style(input: TokenStream) -> TokenStream { make_style::fn_proc_macro_impl(input) } diff --git a/macro/src/make_style/codegen.rs b/macro/src/make_style/codegen.rs index 02da41eb..40b3e4be 100644 --- a/macro/src/make_style/codegen.rs +++ b/macro/src/make_style/codegen.rs @@ -63,7 +63,7 @@ pub(crate) fn code_gen( }; quote! { - r3bl_rs_utils::Style { + Style { id: #id.to_string(), bold: #has_attrib_bold, dim: #has_attrib_dim, diff --git a/macro/src/make_style/syntax_parse.rs b/macro/src/make_style/syntax_parse.rs index e56e012a..8299a516 100644 --- a/macro/src/make_style/syntax_parse.rs +++ b/macro/src/make_style/syntax_parse.rs @@ -26,11 +26,11 @@ use crate::utils::IdentExt; /// /// ``` /// style! { -/// id: "my_style", /* Optional. */ -/// attrib: [dim, bold] /* Optional. */ -/// padding: 10, /* Optional. */ -/// color_fg: Color::Blue, /* Optional. */ -/// color_bg: Color::Red, /* Optional. */ +/// id: "my_style", /* Optional. */ +/// attrib: [dim, bold] /* Optional. */ +/// padding: 10, /* Optional. */ +/// color_fg: TWColor::Blue, /* Optional. */ +/// color_bg: TWColor::Red, /* Optional. */ /// } /// ``` /// @@ -83,7 +83,7 @@ fn parse_optional_id(input: &ParseStream, metadata: &mut StyleMetadata) -> Resul let id = input.parse::()?; metadata.id = id; } - call_if_true!(DEBUG, println!("🚀 id: {:?}", metadata.id)); + call_if_debug_true!(println!("🚀 id: {:?}", metadata.id)); Ok(()) } @@ -118,7 +118,7 @@ fn parse_optional_attrib(input: &ParseStream, metadata: &mut StyleMetadata) -> R } } - call_if_true!(DEBUG, println!("🚀 attrib_vec: {:?}", metadata.attrib_vec)); + call_if_debug_true!(println!("🚀 attrib_vec: {:?}", metadata.attrib_vec)); } Ok(()) } @@ -132,7 +132,7 @@ fn parse_optional_padding(input: &ParseStream, metadata: &mut StyleMetadata) -> let lit_int = input.parse::()?; let padding_int: UnitType = lit_int.base10_parse().unwrap(); metadata.padding = Some(padding_int); - call_if_true!(DEBUG, println!("🚀 padding: {:?}", &metadata.padding)); + call_if_debug_true!(println!("🚀 padding: {:?}", &metadata.padding)); } Ok(()) } @@ -145,7 +145,7 @@ fn parse_optional_color_fg(input: &ParseStream, metadata: &mut StyleMetadata) -> input.parse::()?; let color_expr = input.parse::()?; metadata.color_fg = Some(color_expr); - call_if_true!(DEBUG, println!("🚀 color_fg: {:#?}", metadata.color_fg)); + call_if_debug_true!(println!("🚀 color_fg: {:#?}", metadata.color_fg)); } Ok(()) @@ -159,7 +159,7 @@ fn parse_optional_color_bg(input: &ParseStream, metadata: &mut StyleMetadata) -> input.parse::()?; let color_expr = input.parse::()?; metadata.color_bg = Some(color_expr); - call_if_true!(DEBUG, println!("🚀 color_bg: {:#?}", metadata.color_bg)); + call_if_debug_true!(println!("🚀 color_bg: {:#?}", metadata.color_bg)); } Ok(()) diff --git a/src/tui/crossterm_helpers/keypress.rs b/src/tui/crossterm_helpers/keypress.rs index 242d1e4b..a76d3038 100644 --- a/src/tui/crossterm_helpers/keypress.rs +++ b/src/tui/crossterm_helpers/keypress.rs @@ -229,6 +229,7 @@ pub enum SpecialKey { /// KeyCode::Char](https://docs.rs/crossterm/latest/crossterm/event/enum.KeyCode.html#variant.Char) pub mod convert_key_event { use super::*; + impl TryFrom for Keypress { type Error = (); /// Convert [KeyEvent] to [Keypress]. @@ -437,7 +438,3 @@ pub mod convert_key_event { }) } } - -// Re-export so this is visible for testing. -#[allow(unused_imports)] -pub(crate) use convert_key_event::*; diff --git a/src/tui/crossterm_helpers/tw_command.rs b/src/tui/crossterm_helpers/tw_command.rs index 63d175cd..94294685 100644 --- a/src/tui/crossterm_helpers/tw_command.rs +++ b/src/tui/crossterm_helpers/tw_command.rs @@ -29,6 +29,7 @@ use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; use crate::*; + const DEBUG: bool = false; // ╭┄┄┄┄┄┄┄╮ @@ -40,22 +41,30 @@ const DEBUG: bool = false; /// Paste docs: #[macro_export] macro_rules! exec { - ($cmd: expr, $log_msg: expr) => {{ + ( + $arg_cmd: expr, + $arg_log_msg: expr + ) => {{ // Generate a new function that returns [CommonResult]. This needs to be called. The only // purpose of this generated method is to handle errors that may result from calling log! macro // when there are issues accessing the log file for whatever reason. let _fn_wrap_for_logging_err = || -> CommonResult<()> { throws!({ // Execute the command. - if let Err(err) = $cmd { + if let Err(err) = $arg_cmd { call_if_true!( DEBUG, - log!(ERROR, "crossterm: ❌ Failed to {} due to {}", $log_msg, err) + log!( + ERROR, + "crossterm: ❌ Failed to {} due to {}", + $arg_log_msg, + err + ) ); } else { call_if_true! { DEBUG, - log!(INFO, "crossterm: ✅ {} successfully", $log_msg) + log!(INFO, "crossterm: ✅ {} successfully", $arg_log_msg) }; } }) @@ -66,8 +75,8 @@ macro_rules! exec { if let Err(logging_err) = _fn_wrap_for_logging_err() { let msg = format!( "❌ Failed to log exec output of {}, {}", - stringify!($cmd), - $log_msg + stringify!($arg_cmd), + $arg_log_msg ); call_if_true! { DEBUG, @@ -124,13 +133,26 @@ macro_rules! tw_command_queue { queue } }; - // Add a bunch of TWCommands $element* to the existing $queue & return nothing. + // Add a bunch of TWCommands $element+ to the existing $queue & return nothing. ($queue:ident push $($element:expr),+) => { $( /* Each repeat will contain the following statement, with $element replaced. */ $queue.push($element); )* }; + // Add a bunch of TWCommandQueues $element+ to the new queue, drop them, and return queue. + (@join_and_drop $($element:expr),+) => {{ + let mut queue = TWCommandQueue::default(); + $( + /* Each repeat will contain the following statement, with $element replaced. */ + queue.join_into($element); + )* + queue + }}; + // New. + (@new) => { + TWCommandQueue::default() + }; } // ╭┄┄┄┄┄┄┄┄┄┄┄╮ @@ -159,6 +181,8 @@ pub enum TWCommand { PrintWithAttributes(String, Option