πŸŽ™ A compact library for working with user output

Last update: May 15, 2022

πŸŽ™ Storyteller

A library for working with user output

Table of contents

Introduction

This library is intended to be used by tools, such as cli's, which have multiple user interface options through which they can communicate, while also having various separate commands (or flows) which require each to carefully specify their own output formatting.

The library consists of three primary building blocks, and a default implementation on top of these building blocks. It helps you setup your program architecture

The three building blocks are:

  • EventHandler: The event handler which deals with the user output, for example:

    • A handler which formats events as json-lines, and prints them to stderr
    • A handler which updates a progress bar
    • A handler which collects events for software testing
    • A handler which sends websocket messages for each event
    • A handler which updates a user interface
  • Reporter: Called during your program logic. Used to communicate user output to a user. The reporter is invoked with an Event during the programs logic, so you don't have to deal with formatting and display details in the middle of the program flow.

  • EventListener: Receives events, send by a reporter and runs the EventHandler. Usually spins upa separate thread so it won't block.

On top of these building blocks, a channel based implementation is provided which runs the EventHandler in a separate thread. To use this implementation, consult the docs for the ChannelReporter, and the ChannelEventListener.

In addition to these provided elements, you have to:

  • Define a type which can be used as Event
  • Define one or more EventHandlers (i.e. impl EventHandler<Event = YourEventType>).

Visualized introduction

Click here for a larger version. visualized introduction sketch (light svg, dark svg, light png, dark png)

Example

use std::io::{Stderr, Write};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use std::{io, thread};
use storyteller::{
    disconnect_channel, event_channel, ChannelEventListener, ChannelReporter, EventHandler,
    EventListener, Reporter,
};

// --- In the main function, we'll instantiate a Reporter, a Listener, and an EventHandler.
//     For the reporter and listener, we'll use implementations included with the library.
//     The EventHandler must be defined by us, and can be found below.
//     We also need to define our event type, which can also be found below.

// See the test function `bar` in src/tests.rs for an example where the handler is a progress bar.
fn main() {
    let (sender, receiver) = event_channel::<ExampleEvent>();
    let (disconnect_sender, disconnect_receiver) = disconnect_channel();

    // Handlers are implemented by you. Here you find one which writes jsonlines messages to stderr.
    // This can be anything, for example a progress bar (see src/tests.rs for an example of this),
    // a fake reporter which collects events for testing or maybe even a "MultiHandler<'h>" which
    // consists of a Vec<&'h dyn EventHandler> and executes multiple handlers under the hood.
    let handler = JsonHandler::default();

    // This one is included with the library. It just needs to be hooked up with a channel.
    let reporter = ChannelReporter::new(sender, disconnect_receiver);

    // This one is also included with the library. It also needs to be hooked up with a channel.
    // It's EventListener implementation spawns a thread in which event messages will be handled.
    // Events are send to this thread using channels, therefore the name ChannelEventListener ✨.
    let listener = ChannelEventListener::new(receiver, disconnect_sender);

    // Here we use the jsonlines handler we defined above, in combination with the default `EventListener`
    // implementation on the `ChannelEventListener` we used above.
    //
    // If we don't run the handler, we'll end up in an infinite loop, because our `reporter.disconnect()`
    // below will block until it receives a Disconnect message.
    // Besides, if we don't run the handler, we would not need this library =). 
    // 
    // Also: as described above, it spawns a thread which handles updates, so it won't block.
    listener.run_handler(handler);

    // Run your program's logic
    my_programming_logic(&reporter);

    // Finish handling reported events, then disconnect the channels.
    // If not called, events which have not been handled by the event handler yet may be discarded. 
    let _ = reporter.disconnect();
}

fn my_programming_logic(reporter: &ChannelReporter<ExampleEvent>) {
	#[allow(unused_must_use)] // sending events can fail, but we'll assume they won't for this example
    {
    	// These are the events we would call during the regular flow of our program, for example
	    // if we use the library in a package manager, before, during or after downloading dependencies.
	    // The use any-event-type-you-like nature allows you to go as crazy as you would like. 
        reporter.report_event(ExampleEvent::text("[status]\t\tOne"));
        reporter.report_event(ExampleEvent::event(MyEvent::Increment));
        reporter.report_event(ExampleEvent::event(MyEvent::Increment));
        reporter.report_event(ExampleEvent::text("[status::before]\tTwo before reset"));
        reporter.report_event(ExampleEvent::event(MyEvent::Reset));
        reporter.report_event(ExampleEvent::text("[status::after]\t\tTwo after reset"));
        reporter.report_event(ExampleEvent::event(MyEvent::Increment));
        reporter.report_event(ExampleEvent::event(MyEvent::Increment));
        reporter.report_event(ExampleEvent::event(MyEvent::Increment));
        reporter.report_event(ExampleEvent::event(MyEvent::Increment));
        reporter.report_event(ExampleEvent::event(MyEvent::Increment));
        reporter.report_event(ExampleEvent::event(MyEvent::Increment));
        reporter.report_event(ExampleEvent::text("[status]\t\tThree"));
        reporter.report_event(ExampleEvent::event(MyEvent::Increment));
        reporter.report_event(ExampleEvent::event(MyEvent::Increment));
        reporter.report_event(ExampleEvent::event(MyEvent::Increment));
        reporter.report_event(ExampleEvent::text("[status]\t\tFour"));
    }
}

// --- Here we define out Event Type.

// if we would have imported third-party libraries, we could have done: #[derive(serde::Serialize)]
enum ExampleEvent {
    Event(MyEvent),
    Text(String),
}

impl ExampleEvent {
    pub fn event(event: MyEvent) -> Self {
        Self::Event(event)
    }

    pub fn text<T: AsRef<str>>(text: T) -> Self {
        Self::Text(text.as_ref().to_string())
    }
}

impl ExampleEvent {
    // Here we create some json by hand, so you can copy the example without importing other libraries, but you can also
    // replace all of this by, say `serde_json`, and derive a complete json output of your `Event` definition all at once (by designβ„’ =)).
    pub fn to_json(&self) -> String {
        match self {
            Self::Event(event) => event.to_json(),
            Self::Text(msg) => format!("{{ \"event\" : \"message\", \"value\" : \"{}\" }}", msg),
        }
    }
}

enum MyEvent {
    Increment,
    Reset,
}

impl MyEvent {
    pub fn to_json(&self) -> String {
        match self {
            Self::Increment => format!("{{ \"event\" : \"increment\" }}"),
            Self::Reset => format!("{{ \"event\" : \"reset\" }}"),
        }
    }
}

// --- Here we define an Event Handler which deals with the user output.

struct JsonHandler {
    stream: Arc<Mutex<Stderr>>,
}

impl Default for JsonHandler {
    fn default() -> Self {
        Self {
            stream: Arc::new(Mutex::new(io::stderr())),
        }
    }
}

impl EventHandler for JsonHandler {
    type Event = ExampleEvent;

    fn handle(&self, event: Self::Event) {
        /* simulate some busy work, so we can more easily follow the user output */
        thread::sleep(Duration::from_secs(1));
        /* simulate some busy work */
        let message = event.to_json();

        let mut out = self.stream.lock().unwrap();
        let _ = writeln!(out, "{}", message);
        let _ = out.flush();
    }

    fn finish(&self) {
        let mut out = self.stream.lock().unwrap();

        let message = format!("{{ \"event\" : \"program-finished\", \"success\" : true }}");

        let _ = writeln!(out, "{}", message);
        let _ = out.flush();
    }
}

Origins

This library is a refined implementation based on an earlier experiment. It is intended to be used by, and was developed because of, cargo-msrv which has outgrown its current user output implementation.

Contributions

Contributions, feedback or other correspondence are more than welcome! Feel free to send a message or create an issue πŸ˜„ .

License

Licensed under either of

at your option.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

GitHub

https://github.com/foresterre/storyteller
You might also like...

A Text User Interface library for the Rust programming language

A Text User Interface library for the Rust programming language

Cursive Cursive is a TUI (Text User Interface) library for rust. It uses ncurses by default, but other backends are available. It allows you to build

May 20, 2022

A Text User Interface library for the Rust programming language

A Text User Interface library for the Rust programming language

Cursive Cursive is a TUI (Text User Interface) library for rust. It uses ncurses by default, but other backends are available. It allows you to build

May 26, 2022

Build terminal user interfaces and dashboards using Rust

Build terminal user interfaces and dashboards using Rust

tui-rs tui-rs is a Rust library to build rich terminal user interfaces and dashboards. It is heavily inspired by the Javascript library blessed-contri

May 23, 2022

A cross platform minimalistic text user interface

A cross platform minimalistic text user interface

titik Titik is a crossplatform TUI widget library with the goal of being able to interact intuitively on these widgets. It uses crossterm as the under

May 16, 2022

fd is a program to find entries in your filesystem. It is a simple, fast and user-friendly alternative to find

fd is a program to find entries in your filesystem. It is a simple, fast and user-friendly alternative to find

fd is a program to find entries in your filesystem. It is a simple, fast and user-friendly alternative to find. While it does not aim to support all of find's powerful functionality, it provides sensible (opinionated) defaults for a majority of use cases.

May 23, 2022

A user-friendly TUI client for Matrix written in Rust!

Konoha A user-friendly TUI client for Matrix written in Rust! Notice: The client is currently not usable and is only hosted on GitHub for version cont

Jan 5, 2022

Github user information on terminal :D

Github user information on terminal :D

octofetch Use this if youre too lazy to open github lol Installation Local install with cargo Run cargo install --git https://github.com/azur1s/octofe

Apr 11, 2022

a terminal user interface for lemmy

Lemmy-Terminal-Viewer Terminal User Interface for lemmy for Linux Terminals (should work in MacOs but i can't test) Install and Usage Linux Download l

Jan 22, 2022

koyo is a cli tool that lets you run commands as another user. It is similar to doas or sudo.

koyo is a cli tool that lets you run commands as another user. It is similar to doas or sudo.

Nov 27, 2021
Cross-platform Rust library for coloring and formatting terminal output
Cross-platform Rust library for coloring and formatting terminal output

Coloring terminal output Documentation term-painter is a cross-platform (i.e. also non-ANSI terminals) Rust library for coloring and formatting termin

Feb 6, 2022
A Rust program/library to write a Hello World message to the standard output.

hello-world Description hello-world is a Rust program and library that prints the line Hello, world! to the console. Why was this created? This progra

May 11, 2022
A cargo plugin to shrink cargo's output

cargo single-line A simple cargo plugin that shrinks the visible cargo output to a single line (okay, in the best case scenario). In principle, the pl

Sep 29, 2021
Grep with human-friendly search output
Grep with human-friendly search output

hgrep: Human-friendly GREP hgrep is a grep tool to search files with given pattern and print the matched code snippets with human-friendly syntax high

May 11, 2022
A syntax-highlighting pager for git, diff, and grep output
A syntax-highlighting pager for git, diff, and grep output

Get Started Install delta and add this to your ~/.gitconfig: [core] pager = delta [interactive] diffFilter = delta --color-only [delta]

May 19, 2022
A silly program written in Rust to output nonsensical sentences in the command line interface.

A silly program written in Rust to output nonsensical sentences in the command line interface.

Dec 13, 2021
Watch output and trigger on diff!

watchdiff Watch output and trigger on diff! Ever want to have watch output only tell you what changed? And not only what, but when? Now you can! Enter

Apr 6, 2022
A cargo subcommand that displays ghidra function output through the use of the rizin rz-ghidra project.

cargo-rz-ghidra A cargo subcommand that displays ghidra function output through the use of the rizin rz-ghidra project. Install cargo install --git ht

May 10, 2022
CLI tool that make it easier to perform multiple lighthouse runs towards a single target and output the result in a plotable format.

Lighthouse Aggregator CLI tool that make it easier to perform multiple lighthouse runs towards a single target and output the result in a "plotable" f

Jan 12, 2022
hexyl is a simple hex viewer for the terminal. It uses a colored output to distinguish different categories of bytes
hexyl is a simple hex viewer for the terminal. It uses a colored output to distinguish different categories of bytes

hexyl is a simple hex viewer for the terminal. It uses a colored output to distinguish different categories of bytes (NULL bytes, printable ASCII characters, ASCII whitespace characters, other ASCII characters and non-ASCII).

May 20, 2022