12 个版本

0.2.6 2023 年 10 月 22 日
0.2.3 2023 年 9 月 10 日
0.2.2 2023 年 4 月 21 日
0.2.1 2023 年 3 月 6 日
0.1.3 2022 年 11 月 6 日

#512Rust 模式

34 每月下载量

Apache-2.0

76KB
465

上下文

R3BL TUI &

我们正在努力构建在 Rust 中具有丰富文本用户界面 (TUI) 的命令行应用程序。我们希望将终端作为一个生产力场所,并为它构建各种出色的应用程序。

  1. 🔮 我们不是仅仅构建一个应用程序,而是在构建一个库,以支持各种丰富的 TUI 开发,并有所创新:将适用于前端移动和 Web 开发世界的概念重新构思,以适应 TUI 和 Rust。

    • 例如 React、JSX、CSS 和 Redux,但使所有内容都异步(它们可以通过 Tokio 进行并行和并发运行)。
    • 甚至运行主事件循环的线程也不会阻塞,因为它也是异步的。
    • 使用过程宏来创建 DSL 以实现 CSS 和 JSX。
  2. 🌎 我们正在构建应用程序以增强开发者的生产力和工作流程。

    • 这里的想法不是在 Rust 中重建 tmux(将多个进程连接到单个终端窗口)。相反,我们旨在构建一组集成的“应用程序”(或“任务”),这些任务在同一个进程中运行,并将渲染到一个终端窗口。
    • 在这个终端窗口内部,我们可以实现诸如“应用程序”切换、路由、平铺布局、堆叠布局等,以便我们可以管理许多在同一个进程中、同一个窗口中运行的紧密集成的 TUI 应用程序。因此,您可以想象所有这些“应用程序”共享应用程序状态(即 Redux 存储)。每个“应用程序”也可能有自己的 Redux 存储。
    • 以下是我们想要构建的“应用程序”类型的一些示例
      1. 具有语法高亮的多人文本编辑器
      2. 与 GitHub 问题的集成
      3. 与日历、电子邮件、联系人API的集成

r3bl_redux

此库与上述第一点描述的内容相关。它提供了许多有用的功能,帮助您构建TUI(文本用户界面)应用,同时还包含所有Rustaceans(🦀)都可以享受的通用便利性和人体工程学🎉

redux

Store 是线程安全和异步的(使用Tokio)。您必须通过定义自己的还原器、订阅者和中间件特质对象来实现 async 特质才能使用它。您还必须提供Tokio运行时,因为这个库不会创建自己的运行时。然而,为了获得最佳效果,最好使用多线程的Tokio运行时。

一旦您设置了包含还原器、订阅者和中间件的Redux存储,您可以通过调用 spawn_dispatch_action!) 来使用它。这将启动一个并行的Tokio任务,该任务将运行中间件函数、还原器函数,最后是订阅者函数。因此,这不会阻塞调用此代码的任何代码的线程。该 spawn_dispatch_action!) 宏本身不是 async。因此,您可以从非 async 代码调用它,但是您仍然必须提供一个Tokio执行器/运行时,否则当调用 spawn_dispatch_action!) 时将引发panic。

中间件

您的中间件(async 特质实现)将通过Tokio任务并发或并行运行。您可以选择实现哪个 async 特质来执行一个或另一个。无论您实现哪种类型,都会在所有中间件执行完成后(对于特定的 spawn_dispatch_action!) 调用)将可选返回的 Action 发送到Redux存储。

  1. AsyncMiddlewareSpawns<State, Action> - 您的中间件必须使用 tokio::spawn 在一个 单独的线程 中运行 async 块,并返回一个包含 Option<Action>JoinHandle。提供了一个宏 fire_and_forget!,这样您就可以轻松地在 async 函数中生成并行代码块。这些通过调用 add_middleware_spawns) 添加到存储中。

  2. AsyncMiddleware<State, Action> - 它们将使用 futures::join_all) 并发运行。这些通过调用 add_middleware) 添加到存储中。

订阅者

订阅者将通过Tokio任务异步运行。它们都是一起并发运行,但不是并行运行,使用futures::join_all()

还原器

归约函数也是运行在tokio运行时的async函数。它们也是按照添加的顺序一个接一个地运行的。

您编写的任何使用Redux库的函数或块都必须标记为async。您必须通过使用#[tokio::main]宏来启动Tokio运行时。如果您使用默认运行时,Tokio将使用多个线程和其任务窃取实现来为您提供并行和并发行为。您也可以使用单线程运行时;这完全取决于您。

  1. 要创建中间件,您必须实现AsyncMiddleware<S,A>特质或AsyncMiddlewareSpawns<S,A>特质。请阅读AsyncMiddleware文档以了解两种情况的示例。该方法run()传递两个参数:状态State和操作Action

    1. 在您的run()实现中,对于AsyncMiddlewareSpawns<S,A>,您必须使用fire_and_forget!宏来包围您的代码。这将返回一个JoinHandle<Option<A>>
    2. 对于您的AsyncMiddleware<S,A>run()实现中,只需返回一个Option<A>>
  2. 要创建归约器,您必须实现AsyncReducer特质。

    • 这些应该是纯函数,并且简单地返回一个新的State对象。
    • 方法run()将传递两个参数:一个指向Action的引用和一个指向State的引用。
  3. 要创建订阅者,您必须实现AsyncSubscriber特质。

    • 方法run()将传递一个作为参数的State对象。
    • 它不返回任何内容()

总结

以下是如何创建和使用其中之一的基本方法

  1. 创建一个结构体。使其继承Default。或者,您也可以向此结构体添加自己的属性/字段,并自己构建它,甚至提供构造函数。
    • 该特质为您提供默认构造函数 new()
    • 只需按照此方法创建具有您自己属性的结构的构造函数即可。
  2. 在您的结构体上实现 AsyncMiddlewareAsyncMiddlewareSpawnsAsyncReducerAsyncSubscriber 特质。
  3. 使用 add_middleware()add_middleware_spawns()add_reducer()add_subscriber() 方法将此结构体注册到商店中。您可以注册任意数量的这些方法。
    • 如果您有一个没有属性的结构体,您可以直接使用默认的 ::new() 方法创建一个实例,并将其传递给 add_???() 方法。
    • 如果您有一个具有自定义属性的结构体,您可以实现自己的构造函数,或者将以下内容用作 add_???() 方法的参数:Box::new($YOUR_STRUCT))

示例

💡 在此库的测试以及使用它构建的CLI 应用程序中有很多示例。

以下是如何使用它的示例。让我们从导入语句开始。

/// Imports.
use async_trait::async_trait;
use r3bl_rs_utils::redux::{
  AsyncMiddlewareSpawns, AsyncMiddleware, AsyncReducer,
  AsyncSubscriber, Store, StoreStateMachine,
};
use std::sync::{Arc, Mutex};
use tokio::sync::RwLock;
  1. 请确保在您的 Cargo.toml 文件中安装了 tokioasync-trait 依赖项,以及 r3bl_rs_utils
  2. 以下是 Cargo.toml 的示例。

假设我们有一个以下动作枚举和状态结构体。

/// Action enum.
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum Action {
  Add(i32, i32),
  AddPop(i32),
  Clear,
  MiddlewareCreateClearAction,
  Noop,
}

impl Default for Action {
  fn default() -> Self {
    Action::Noop
  }
}

/// State.
#[derive(Clone, Default, PartialEq, Debug)]
pub struct State {
  pub stack: Vec<i32>,
}

以下是减少函数的示例。

/// Reducer function (pure).
#[derive(Default)]
struct MyReducer;

#[async_trait]
impl AsyncReducer<State, Action> for MyReducer {
  async fn run(
    &self,
    action: &Action,
    state: &mut State,
  ) {
    match action {
      Action::Add(a, b) => {
        let sum = a + b;
        state.stack = vec![sum];
      }
      Action::AddPop(a) => {
        let sum = a + state.stack[0];
        state.stack = vec![sum];
      }
      Action::Clear => State {
        state.stack.clear();
      },
      _ => {}
    }
  }
}

以下是异步订阅者函数的示例(在派发动作后并行执行)。以下示例使用捕获共享对象的lambda表达式。这可能是您在创建在封闭块或作用域中共享状态的订阅者时可能会遇到的一种常见模式。

/// This shared object is used to collect results from the subscriber
/// function & test it later.
let shared_object = Arc::new(Mutex::new(Vec::<i32>::new()));

#[derive(Default)]
struct MySubscriber {
  pub shared_object_ref: Arc<Mutex<Vec<i32>>>,
}

#[async_trait]
impl AsyncSubscriber<State> for MySubscriber {
  async fn run(
    &self,
    state: State,
  ) {
    let mut stack = self
      .shared_object_ref
      .lock()
      .unwrap();
    if !state.stack.is_empty() {
      stack.push(state.stack[0]);
    }
  }
}

let my_subscriber = MySubscriber {
  shared_object_ref: shared_object_ref.clone(),
};

以下是两种异步中间件函数的类型。一种返回一个动作(在中间件返回时将派发),另一种不返回任何内容(如将当前动作输出到控制台的日志中间件)。请注意,这两个函数都共享上述 shared_object 引用。

/// This shared object is used to collect results from the subscriber
/// function & test it later.
#[derive(Default)]
struct MwExampleNoSpawn {
  pub shared_object_ref: Arc<Mutex<Vec<i32>>>,
}

#[async_trait]
impl AsyncMiddleware<State, Action> for MwExampleNoSpawn {
  async fn run(
    &self,
    action: Action,
    _store_ref: Arc<RwLock<StoreStateMachine<State, Action>>>,
  ) {
    let mut stack = self
      .shared_object_ref
      .lock()
      .unwrap();
    match action {
      Action::MwExampleNoSpawn_Add(_, _) => stack.push(-1),
      Action::MwExampleNoSpawn_AddPop(_) => stack.push(-2),
      Action::MwExampleNoSpawn_Clear => stack.push(-3),
      _ => {}
    }
    None
  }
}

let mw_example_no_spawn = MwExampleNoSpawn {
  shared_object_ref: shared_object_ref.clone(),
};

/// This shared object is used to collect results from the subscriber
/// function & test it later.
#[derive(Default)]
struct MwExampleSpawns {
  pub shared_object_ref: Arc<Mutex<Vec<i32>>>,
}

#[async_trait]
impl AsyncMiddlewareSpawns<State, Action> for MwExampleSpawns {
  async fn run(
    &self,
    action: Action,
    store_ref: Arc<RwLock<StoreStateMachine<State, Action>>>,
  ) -> JoinHandle<Option<Action>> {
    fire_and_forget!(
      {
        let mut stack = self
          .shared_object_ref
          .lock()
          .unwrap();
        match action {
          Action::MwExampleSpawns_ModifySharedObject_ResetState => {
            shared_vec.push(-4);
            return Action::Reset.into();
          }
          _ => {}
        }
        None
      }
    );
  }
}

let mw_example_spawns = MwExampleSpawns {
  shared_object_ref: shared_object_ref.clone(),
};

以下是设置具有上述减少器、中间件和订阅者函数的商店的方法。

// Setup store.
let mut store = Store::<State, Action>::default();
store
  .add_reducer(MyReducer::new()) // Note the use of `::new()` here.
  .await
  .add_subscriber(Box::new(         // We aren't using `::new()` here
    my_subscriber,                  // because the struct has properties.
  ))
  .await
  .add_middleware_spawns(Box::new(  // We aren't using `::new()` here
    mw_example_spawns,              // because the struct has properties.
  ))
  .await
  .add_middleware(Box::new(         // We aren't using `::new()` here
    mw_example_no_spawn,            // because the struct has properties.
  ))
  .await;

最后,以下是如何在测试中派发动作的示例。您可以使用 spawn_dispatch_action!() 并行派发动作,它是一种“点火并忘记”的操作,意味着调用者不会阻塞或等待 spawn_dispatch_action!() 返回。

// Test reducer and subscriber by dispatching `Add`, `AddPop`, `Clear` actions in parallel.
spawn_dispatch_action!( store, Action::Add(1, 2) );
assert_eq!(shared_object.lock().unwrap().pop(), Some(3));

spawn_dispatch_action!( store, Action::AddPop(1) );
assert_eq!(shared_object.lock().unwrap().pop(), Some(4));

spawn_dispatch_action!( store, Action::Clear );
assert_eq!(store.get_state().stack.len(), 0);

依赖于此的其它库

此包是以下包的依赖

  1. r3bl_rs_utils 包(主库)

问题、评论、反馈和PR

如有任何问题,请向问题跟踪器报告。如果您有任何功能请求,也请随意添加👍。

依赖

~14–23MB
~228K SLoC