{"id":13822311,"url":"https://github.com/foresterre/storyteller","last_synced_at":"2025-04-07T05:14:59.037Z","repository":{"id":37009631,"uuid":"488805955","full_name":"foresterre/storyteller","owner":"foresterre","description":"🎙 An event based library for architecting user output for multiple output destinations, in Rust","archived":false,"fork":false,"pushed_at":"2024-12-10T22:27:29.000Z","size":1851,"stargazers_count":31,"open_issues_count":2,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-06T05:58:20.216Z","etag":null,"topics":["architecture","architecture-components","crate","event","hacktoberfest","handler","library","output","rust","stdout","ui","user"],"latest_commit_sha":null,"homepage":"https://docs.rs/storyteller","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/foresterre.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE-APACHE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":["foresterre"],"buy_me_a_coffee":"foresterre","thanks_dev":"u/gh/foresterre"}},"created_at":"2022-05-05T02:25:54.000Z","updated_at":"2024-12-30T22:26:55.000Z","dependencies_parsed_at":"2024-08-04T08:08:17.905Z","dependency_job_id":"48c8c7c4-7ba5-4edb-a5f3-5c1a40fe6ef5","html_url":"https://github.com/foresterre/storyteller","commit_stats":{"total_commits":68,"total_committers":4,"mean_commits":17.0,"dds":0.4117647058823529,"last_synced_commit":"547e5e8f3ee90817a4d173975199da6779385527"},"previous_names":[],"tags_count":13,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/foresterre%2Fstoryteller","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/foresterre%2Fstoryteller/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/foresterre%2Fstoryteller/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/foresterre%2Fstoryteller/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/foresterre","download_url":"https://codeload.github.com/foresterre/storyteller/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247595335,"owners_count":20963943,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["architecture","architecture-components","crate","event","hacktoberfest","handler","library","output","rust","stdout","ui","user"],"created_at":"2024-08-04T08:01:53.978Z","updated_at":"2025-04-07T05:14:58.966Z","avatar_url":"https://github.com/foresterre.png","language":"Rust","funding_links":["https://github.com/sponsors/foresterre","https://buymeacoffee.com/foresterre","https://thanks.dev/u/gh/foresterre"],"categories":["Rust"],"sub_categories":[],"readme":"# 🎙 Storyteller\n_A library for working with user output_\n\n## Table of contents\n\n* 👋 [Introduction](#introduction)\n* 🖼 [Visualized introduction](#visualized-introduction)\n* 📄 [Examples](#examples)\n  * [Real world usage](#real-world-example)\n  * [Hello world example](#hello-world-example)\n* ❓ [Origins](#origins)\n* 💖 [Contributions \u0026 Feedback](#contributions)\n* 🧾 [License](#license)\n\n## Introduction\n\nThis library is intended to be used by tools, such as cli's, which have multiple user interface\noptions through which they can communicate, while also having various separate commands (or\nflows) which require each to carefully specify their own output formatting.\n\nThe library consists of three primary building blocks, and a default implementation on top\nof these building blocks. It helps you setup your program architecture \n\nThe three building blocks are:\n* `EventHandler`: The event handler which deals with the user output, for example:\n\t* A handler which formats events as json-lines, and prints them to stderr\n\t* A handler which updates a progress bar\n\t* A handler which collects events for software testing\n\t* A handler which sends websocket messages for each event\n\t* A handler which updates a user interface\n    \n* `EventReporter`: Called during your program logic. \n\tUsed 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.\n\n* `EventListener`: Receives events, send by a reporter and runs the `EventHandler`. Usually spins upa separate thread so it won't block.\n\nOn top of these building blocks, a channel based implementation is provided which runs the `EventHandler`\nin a separate thread. To use this implementation, consult the docs for the `ChannelReporter`, and the\n`ChannelEventListener`.\n\nIn addition to these provided elements, you have to:\n* Define a type which can be used as Event\n* Define one or more EventHandlers (i.e. `impl EventHandler\u003cEvent = YourEventType\u003e`).\n\n## Visualized introduction\n\nClick [here](https://raw.githubusercontent.com/foresterre/storyteller/main/docs/sketches/introduction_dark.svg) for a larger version. \n![visualized introduction sketch](docs/sketches/introduction_dark.png)\n([light svg](https://raw.githubusercontent.com/foresterre/storyteller/main/docs/sketches/introduction.svg), [dark svg](https://raw.githubusercontent.com/foresterre/storyteller/main/docs/sketches/introduction_dark.svg), [light png](https://raw.githubusercontent.com/foresterre/storyteller/main/docs/sketches/introduction.png), [dark png](https://raw.githubusercontent.com/foresterre/storyteller/main/docs/sketches/introduction_dark.png))\n\n## Examples\n\n### Real-world Example\n\nStoryteller is used by [cargo-msrv](https://github.com/foresterre/cargo-msrv/tree/44444c55608edb749c3cbcd5b6983d7f8846b452/src/reporter) since `v0.16`. To preview how events are specified, you could click around in its [event](https://github.com/foresterre/cargo-msrv/tree/44444c55608edb749c3cbcd5b6983d7f8846b452/src/reporter/event) module. In the [handler](https://github.com/foresterre/cargo-msrv/tree/44444c55608edb749c3cbcd5b6983d7f8846b452/src/reporter/handler) module, several handlers can be found, such as one which writes JSON, one which prints pretty human readable output, one which prints minimal final result output used by shell commands, one which discards output and one which is used for integration testing.\n\n### A taste of [storyteller](https://github.com/foresterre/storyteller)\n\n```rust\nuse std::cell::RefCell;\nuse std::hash::Hasher;\nuse std::io::{Stderr, Write};\nuse std::sync::atomic::{AtomicU32, AtomicUsize, Ordering};\nuse std::sync::{Arc, Mutex};\nuse std::time::Duration;\nuse std::{io, thread};\nuse storyteller::{EventHandler, FinishProcessing};\n\nuse storyteller::{\n    event_channel, ChannelEventListener, ChannelReporter, EventListener, EventReporter,\n};\n\n#[derive(serde::Serialize)]\n#[serde(tag = \"type\", rename_all = \"snake_case\")]\nenum Event {\n    DiceThrow { throw: u8 },\n    YouWin,\n    YouLose,\n}\n\n#[derive(Default)]\nstruct JsonHandler;\n\nimpl EventHandler for JsonHandler {\n    type Event = Event;\n\n    fn handle(\u0026self, event: Self::Event) {\n        let serialized_event = serde_json::to_string(\u0026event).unwrap();\n\n        eprintln!(\"{}\", serialized_event);\n    }\n}\n\n// See the test function `bar` in src/tests.rs for an example where the handler is a progress bar.\nfn main() {\n    let (sender, receiver) = event_channel::\u003cEvent\u003e();\n\n    // Handlers are implemented by you. Here you find one which writes jsonlines messages to stderr.\n    // This can be anything, for example a progress bar (see src/tests.rs for an example of this),\n    // a fake reporter which collects events for testing or maybe even a \"MultiHandler\u003c'h\u003e\" which\n    // consists of a Vec\u003c\u0026'h dyn EventHandler\u003e and executes multiple handlers under the hood.\n    let handler = JsonHandler::default();\n\n    // This one is included with the library. It just needs to be hooked up with a channel.\n    let reporter = ChannelReporter::new(sender);\n\n    // This one is also included with the library. It also needs to be hooked up with a channel.\n    let listener = ChannelEventListener::new(receiver);\n\n    // Here we use the JsonHandler we defined above, in combination with the default `EventListener`\n    // and  `ChannelEventListener` defined above.\n    //\n    // If we don't run the handler, we'll end up in an infinite loop, because our `reporter.disconnect()`\n    // below will block until it receives a Disconnect message.\n    let fin = listener.run_handler(Arc::new(handler));\n\n    // Now onto this program, let's play a game of dice!\n    for _ in 0..100 {\n        let dice = roll_dice();\n        reporter\n            .report_event(Event::DiceThrow { throw: dice })\n            .unwrap();\n\n        if dice \u003e= 3 {\n            reporter.report_event(Event::YouWin).unwrap();\n        } else {\n            reporter.report_event(Event::YouLose).unwrap();\n        }\n\n        thread::sleep(Duration::from_millis(100))\n    }\n\n    // Within the ChannelReporter, the sender is dropped, thereby disconnecting the channel\n    // Already sent events can still be processed.\n    let _ = reporter.disconnect();\n\n    // To keep the processing of already sent events alive, we block the handler\n    let _ = fin.finish_processing();\n}\n\nstatic SEED: AtomicU32 = AtomicU32::new(1);\n\nfn roll_dice() -\u003e u8 {\n    let mut random = SEED.load(Ordering::SeqCst);\n    random ^= random \u003c\u003c 13;\n    random ^= random \u003e\u003e 17;\n    random ^= random \u003c\u003c 5;\n    SEED.store(random, Ordering::SeqCst);\n\n    (random % 6 + 1) as u8\n}\n\n```\n\n## Origins\n\nThis library is a refined implementation based on an earlier [experiment](https://github.com/foresterre/rust-experiment-air3/blob/main/src/main.rs).\nIt is intended to be used by, and was developed because of, [cargo-msrv](https://github.com/foresterre/cargo-msrv) \nwhich has outgrown its current user output implementation.\n\n## Contributions\n\nContributions, feedback or other correspondence are more than welcome! Feel free to send a message or create an issue 😄.\n\n## License\n\nLicensed under either of\n\n* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)\n* MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)\n\nat your option.\n\n\n#### Contribution\n\nUnless you explicitly state otherwise, any contribution intentionally\nsubmitted for inclusion in the work by you, as defined in the Apache-2.0\nlicense, shall be dual licensed as above, without any additional terms or\nconditions.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fforesterre%2Fstoryteller","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fforesterre%2Fstoryteller","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fforesterre%2Fstoryteller/lists"}