Skip to content

Commit

Permalink
[tui] refactor tui main_event_loop to use DI, for input & output devi…
Browse files Browse the repository at this point in the history
…ce & return state

- Introduce OutputDevice struct
- Introduce InputDevice struct
- Clean up Flush trait
  • Loading branch information
nazmulidris committed Oct 6, 2024
1 parent de56b48 commit 1dcda26
Show file tree
Hide file tree
Showing 46 changed files with 1,273 additions and 785 deletions.
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
- [r3bl_macro](#r3bl_macro)
- [next-release-macro](#next-release-macro)
- [r3bl_test_fixtures](#r3bl_test_fixtures)
- [next-release-test-fixtures](#next-release-test-fixtures)
- [v0.0.3 2024-09-12](#v003-2024-09-12)
- [v0.0.2 2024-07-13](#v002-2024-07-13)
- [v0.0.1 2024-07-12](#v001-2024-07-12)
Expand Down Expand Up @@ -387,6 +388,17 @@ reflect how the functionality is used in the real world.
of this functionality have emerged, including crates that are created by "3rd party
developers" (people not R3BL and not part of `r3bl-open-core` repo).

- Added:
- Provide a totally new interface for the `main_event_loop()` that allows for more
flexibility in how the event loop is run, using dependency injection. This is a
breaking change, but it is needed to make the codebase more maintainable and possible
to test end to end. This new change introduces the concept of providing some
dependencies to the function itself in order to use it: state, input device, output
device, and app. The function now returns these dependencies as well, so that they can
be used to create a running pipeline of small "applets" where all of these
dependencies are passed around, allowing a new generation of experiences to be built,
that are not monolithic, but are composable and testable.

### v0.5.9 (2024-09-12)

- Updated:
Expand Down Expand Up @@ -732,6 +744,12 @@ in the real world.
[`nazmulidris/rust-scratch/tcp-api-server`](https://github.com/nazmulidris/rust-scratch/)
repo. This allows customization of the miette global report handler at the process
level. Useful for apps that need to override the default report handler formatting.
- Add `OutputDevice` that abstracts away the output device (eg: `stdout`, `stderr`,
`SharedWriter`, etc.). This is useful for end to end testing, and adapting to a variety of
different input and output devices (in the future).
- Add `InputDevice` that abstracts away the input device (eg: `stdin`). This is useful
for end to end testing. This is useful for end to end testing, and adapting to a
variety of different input and output devices (in the future).

## `r3bl_analytics_schema`

Expand Down Expand Up @@ -776,6 +794,16 @@ Deleted:

## `r3bl_test_fixtures`

### next-release-test-fixtures

- Changed:
- Some type aliases were defined here redundantly, since they were also defined in
`r3bl_core` crate. Remove these duplicate types and add a dependency to `r3bl_core`
crate.

- Added:
- Add constructor and tests for `OutputDevice` struct for `StdoutMock`.

### v0.0.3 (2024-09-12)

- Updated:
Expand Down
4 changes: 3 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion cmdr/src/edi/launcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,6 @@ pub async fn run_app(maybe_file_path: Option<String>) -> CommonResult<()> {
)];

// Create a window.
TerminalWindow::main_event_loop(app, exit_keys, state).await?;
_ = TerminalWindow::main_event_loop(app, exit_keys, state).await?;
})
}
2 changes: 0 additions & 2 deletions core/src/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,9 @@ pub mod common_enums;
pub mod common_math;
pub mod common_result_and_error;
pub mod miette_setup_global_report_handler;
pub mod type_aliases;

// Re-export.
pub use common_enums::*;
pub use common_math::*;
pub use common_result_and_error::*;
pub use miette_setup_global_report_handler::*;
pub use type_aliases::*;
43 changes: 43 additions & 0 deletions core/src/terminal_io/input_device.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright (c) 2024 R3BL LLC
* All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

use crossterm::event::EventStream;
use futures_util::{FutureExt, StreamExt};
use miette::IntoDiagnostic;

use crate::{CrosstermEventResult, PinnedInputStream};

pub struct InputDevice {
pub resource: PinnedInputStream<CrosstermEventResult>,
}

impl InputDevice {
pub fn new_event_stream() -> InputDevice {
InputDevice {
resource: Box::pin(EventStream::new()),
}
}
}

impl InputDevice {
pub async fn next(&mut self) -> miette::Result<crossterm::event::Event> {
match self.resource.next().fuse().await {
Some(it) => it.into_diagnostic(),
None => miette::bail!("Failed to get next event from input source."),
}
}
}
6 changes: 6 additions & 0 deletions core/src/terminal_io/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,15 @@
*/

// Attach sources.
pub mod input_device;
pub mod output_device;
pub mod pretty_print;
pub mod shared_writer;
pub mod type_aliases;

// Re-export.
pub use input_device::*;
pub use output_device::*;
pub use pretty_print::*;
pub use shared_writer::*;
pub use type_aliases::*;
74 changes: 74 additions & 0 deletions core/src/terminal_io/output_device.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright (c) 2024 R3BL LLC
* All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

use std::sync::Arc;

use crate::{SafeRawTerminal, SendRawTerminal, StdMutex};

pub type LockedOutputDevice<'a> = &'a mut dyn std::io::Write;

/// Macro to simplify locking and getting a mutable reference to the output device.
///
/// Usage example:
/// ```rust
/// use r3bl_core::{output_device_as_mut, OutputDevice, LockedOutputDevice};
/// let device = OutputDevice::new_stdout();
/// let mut_ref: LockedOutputDevice<'_> = output_device_as_mut!(device);
/// let _ = mut_ref.write_all(b"Hello, world!\n");
/// ```
#[macro_export]
macro_rules! output_device_as_mut {
($device:expr) => {
&mut *$device.lock()
};
}

/// This struct represents an output device that can be used to write to the terminal. It
/// is safe to clone. In order to write to it, see the examples in [Self::lock()] or
/// [output_device_as_mut] macro.
#[derive(Clone)]
pub struct OutputDevice {
pub resource: SafeRawTerminal,
}

impl OutputDevice {
pub fn new_stdout() -> Self {
Self {
resource: Arc::new(StdMutex::new(std::io::stdout())),
}
}
}

impl OutputDevice {
/// Locks the output device for writing. To use it, use the following code:
///
/// ```rust
/// use r3bl_core::{OutputDevice, LockedOutputDevice};
///
/// let device = OutputDevice::new_stdout();
/// let mut_ref: LockedOutputDevice<'_> = &mut *device.lock();
/// let _ = mut_ref.write_all(b"Hello, world!\n");
/// ```
///
/// This method returns a [`std::sync::MutexGuard`] which provides a mechanism to
/// access the underlying resource in a thread-safe manner. The `MutexGuard` ensures
/// that the resource is locked for the duration of the guard's lifetime, preventing
/// other threads from accessing it simultaneously.
pub fn lock(&self) -> std::sync::MutexGuard<'_, SendRawTerminal> {
self.resource.lock().unwrap()
}
}
File renamed without changes.
8 changes: 8 additions & 0 deletions run
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ def main [...args: string] {
"clean" => {clean}
"install-cargo-tools" => {install-cargo-tools}
"test" => {test}
"watch-all-tests" => {watch-all-tests}
"docs" => {docs}
"check" => {check}
"check-watch" => {check-watch}
Expand Down Expand Up @@ -82,6 +83,7 @@ def print-help [command: string] {
print $' (ansi green)clean(ansi reset)'
print $' (ansi green)install-cargo-tools(ansi reset)'
print $' (ansi green)test(ansi reset)'
print $' (ansi green)watch-all-tests(ansi reset)'
print $' (ansi green)docs(ansi reset)'
print $' (ansi green)check(ansi reset)'
print $' (ansi green)check-watch(ansi reset)'
Expand All @@ -100,6 +102,12 @@ def print-help [command: string] {
}
}

def watch-all-tests [] {
cargo watch -x 'test --workspace --quiet --color always -- --test-threads 4' -c -q --delay 2
# cargo watch -x 'test --workspace' -c -q --delay 2
# cargo watch --exec check --exec 'test --quiet --color always -- --test-threads 4' --clear --quiet --delay 2
}

# https://thelinuxcode.com/create-ramdisk-linux/
def ramdisk-create [] {
# Use findmnt -t tmpfs target_foo to check if the ramdisk is mounted.
Expand Down
3 changes: 2 additions & 1 deletion terminal_async/src/readline_impl/readline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,8 @@ impl Readline {
pub fn new(
prompt: String,
safe_raw_terminal: SafeRawTerminal,
/* move */ pinned_input_stream: PinnedInputStream<CrosstermEventResult>,
/* move */
pinned_input_stream: PinnedInputStream<CrosstermEventResult>,
) -> Result<(Self, SharedWriter), ReadlineError> {
// Line control channel - signals are send to this channel to control `LineState`.
// A task is spawned to monitor this channel.
Expand Down
2 changes: 2 additions & 0 deletions test_fixtures/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ homepage = "https://r3bl.com"
license = "Apache-2.0"

[dependencies]
r3bl_core = { path = "../core", version = "0.9.16" }

# Async stream for DI and testing.
futures-core = "0.3.30"
async-stream = "0.3.5"
Expand Down
3 changes: 1 addition & 2 deletions test_fixtures/src/async_stream_fixtures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@
use std::time::Duration;

use async_stream::stream;

use super::PinnedInputStream;
use r3bl_core::PinnedInputStream;

pub fn gen_input_stream<T>(generator_vec: Vec<T>) -> PinnedInputStream<T>
where
Expand Down
9 changes: 1 addition & 8 deletions test_fixtures/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,16 +193,9 @@
//! }
//! ```
use std::pin::Pin;

use futures_core::Stream;

// Type aliases.
pub type StdMutex<T> = std::sync::Mutex<T>;
pub type PinnedInputStream<T> = Pin<Box<dyn Stream<Item = T>>>;

// Attach sources.
pub mod async_stream_fixtures;
pub mod output_device_fixtures;
pub mod stdout_fixtures;

// Re-export.
Expand Down
50 changes: 50 additions & 0 deletions test_fixtures/src/output_device_fixtures.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright (c) 2024 R3BL LLC
* All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

use std::sync::Arc;

use r3bl_core::{OutputDevice, StdMutex};

use crate::StdoutMock;

impl StdoutMock {
pub fn new_output_device() -> (OutputDevice, StdoutMock) {
let stdout_mock = StdoutMock::default();
let this = OutputDevice {
resource: Arc::new(StdMutex::new(stdout_mock.clone())),
};
(this, stdout_mock)
}
}

#[cfg(test)]
mod tests {
use r3bl_core::{output_device_as_mut, LockedOutputDevice};

use crate::StdoutMock;

#[test]
fn test_output_device() {
let (device, mock) = StdoutMock::new_output_device();
let mut_ref: LockedOutputDevice<'_> = output_device_as_mut!(device);
let _ = mut_ref.write_all(b"Hello, world!\n");
assert_eq!(
mock.get_copy_of_buffer_as_string_strip_ansi(),
"Hello, world!\n"
);
}
}
3 changes: 1 addition & 2 deletions test_fixtures/src/stdout_fixtures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,9 @@
use std::{io::{Result, Write},
sync::Arc};

use r3bl_core::StdMutex;
use strip_ansi_escapes::strip;

use super::StdMutex;

/// You can safely clone this struct, since it only contains an `Arc<StdMutex<Vec<u8>>>`. The
/// inner `buffer` will not be cloned, just the [Arc] will be cloned.
#[derive(Clone)]
Expand Down
1 change: 1 addition & 0 deletions tui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ pretty_assertions = "1.4.0"
# - This is not a dependency for the library, and is not used when the library is
# published or used as a dependency.
r3bl_terminal_async = { path = "../terminal_async" }
r3bl_test_fixtures = { path = "../test_fixtures" }

# For assert_eq2! macro.
pretty_assertions = "1.4.0"
Expand Down
2 changes: 1 addition & 1 deletion tui/examples/demo/ex_app_no_layout/launcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,6 @@ pub async fn run_app() -> CommonResult<()> {
vec![InputEvent::Keyboard(keypress! { @char 'x' })];

// Create a window.
TerminalWindow::main_event_loop(app, exit_keys, State::default()).await?
_ = TerminalWindow::main_event_loop(app, exit_keys, State::default()).await?
});
}
2 changes: 1 addition & 1 deletion tui/examples/demo/ex_app_with_1col_layout/launcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,6 @@ pub async fn run_app() -> CommonResult<()> {
vec![InputEvent::Keyboard(keypress! { @char 'x' })];

// Create a window.
TerminalWindow::main_event_loop(app, exit_keys, State::default()).await?
_ = TerminalWindow::main_event_loop(app, exit_keys, State::default()).await?
});
}
Loading

0 comments on commit 1dcda26

Please sign in to comment.