From 43bc23722af196c73dad1df18569967d24f7794f Mon Sep 17 00:00:00 2001 From: Nazmul Idris Date: Thu, 1 Sep 2022 16:12:44 -0700 Subject: [PATCH] WIP editor_engine.rs and editor_buffer.rs Caret - Add TWCommand to show & set caret (GlobalCursor) - Add support for LocalPaintedEffect caret TWCommandQueue - add macro to make it easy to join and drop other queues into one Position - add methods to check for bounds when adding rows and cols Unicode - Vec to / from String Debug - Add GetSize derive macro for various structs Testing - Add tests for editor buffer & engine - No such thing as visible for testing in integration tests Ergonomics - Add better macro for debug: call_if_debug_true! Documentation - Better code examples for macros Here's the design doc for this feature: - https://github.com/r3bl-org/r3bl_rs_utils/issues/23 - https://github.com/r3bl-org/r3bl_rs_utils/issues/30 --- .gitignore | 1 + .vscode/bookmarks.json | 34 ++++ .vscode/settings.json | 1 + TODO.todo | 38 ++++ core/src/decl_macros.rs | 21 ++ core/src/tui_core/dimens/base_units.rs | 8 + core/src/tui_core/dimens/position.rs | 34 +++- .../tui_core/graphemes/unicode_string_ext.rs | 104 +++++++++- core/src/tui_core/lolcat/cat.rs | 2 +- core/src/tui_core/lolcat/control.rs | 2 +- macro/src/lib.rs | 16 ++ macro/src/make_style/codegen.rs | 2 +- macro/src/make_style/syntax_parse.rs | 20 +- .../async_event_stream_ext.rs | 4 +- src/tui/crossterm_helpers/keypress.rs | 5 +- src/tui/crossterm_helpers/tw_command.rs | 159 +++++++++++---- src/tui/ed/editor_buffer.rs | 74 ++++++- src/tui/ed/editor_engine.rs | 192 ++++++++++++++---- src/tui/terminal_window/main_event_loop.rs | 3 +- tests/test_editor_buffer.rs | 86 ++++++++ 20 files changed, 684 insertions(+), 122 deletions(-) create mode 100644 .vscode/bookmarks.json create mode 100644 tests/test_editor_buffer.rs 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..b0f26327 --- /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": 234, + "column": 6, + "label": "" + } + ] + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index b73354f0..66e44c3c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -24,6 +24,7 @@ "keyb", "keyevent", "Keypress", + "keypresses", "lazyfield", "lazymemovalues", "litint", 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/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/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/async_event_stream_ext.rs b/src/tui/crossterm_helpers/async_event_stream_ext.rs index 50e98df8..d01cf35a 100644 --- a/src/tui/crossterm_helpers/async_event_stream_ext.rs +++ b/src/tui/crossterm_helpers/async_event_stream_ext.rs @@ -74,13 +74,13 @@ impl EventStreamExt for EventStream { match input_event { Ok(input_event) => Some(input_event), Err(e) => { - call_if_true!(DEBUG, log_no_err!(ERROR, "Error: {:?}", e)); + call_if_debug_true!(log_no_err!(ERROR, "Error: {:?}", e)); None } } } Some(Err(e)) => { - call_if_true!(DEBUG, log_no_err!(ERROR, "Error: {:?}", e)); + call_if_debug_true!(log_no_err!(ERROR, "Error: {:?}", e)); None } _ => None, 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..2e2fb69f 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