Skip to content

Commit a38b827

Browse files
committed
Add example of an output / emit API
1 parent b7d29f2 commit a38b827

File tree

9 files changed

+314
-34
lines changed

9 files changed

+314
-34
lines changed

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ tempfile = "3.20.0"
9797
kdl = "6.5.0"
9898
libc = "0.2.62"
9999
cbindgen = "0.29.2"
100+
arc-swap = "1.8.0"
100101

101102
[workspace.lints.rust]
102103
rust_2018_idioms = { level = "warn", priority = -1 }

moss/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ triggers = { path = "../crates/triggers" }
1515
tui = { path = "../crates/tui" }
1616
vfs = { path = "../crates/vfs" }
1717

18+
arc-swap.workspace = true
1819
blsforme.workspace = true
1920
bytes.workspace = true
2021
camino.workspace = true

moss/src/cli/mod.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ use clap_complete::{
1010
shells::{Bash, Fish, Zsh},
1111
};
1212
use clap_mangen::Man;
13-
use moss::{Installation, installation};
13+
use moss::{
14+
Installation, installation,
15+
output::{self, DefaultOutput, TracingOutput},
16+
};
1417
use thiserror::Error;
1518
use tracing_common::{self, logging::LogConfig, logging::init_log_with_config};
1619
use tui::Styled;
@@ -101,6 +104,14 @@ fn command() -> Command {
101104
.value_name("DIR")
102105
.hide(true),
103106
)
107+
.arg(
108+
Arg::new("silent")
109+
.short('s')
110+
.long("silent")
111+
.global(true)
112+
.help("Suppress all output")
113+
.action(ArgAction::SetTrue),
114+
)
104115
.arg_required_else_help(true)
105116
.subcommand(boot::command())
106117
.subcommand(cache::command())
@@ -159,11 +170,19 @@ pub fn process() -> Result<(), Error> {
159170
let matches = command().get_matches_from(args);
160171

161172
let show_version = matches.get_one::<bool>("version").is_some_and(|v| *v);
173+
let silent = matches.get_one::<bool>("silent").is_some_and(|v| *v);
162174

163175
if show_version {
164176
println!("moss {}", tools_buildinfo::get_full_version());
165177
}
166178

179+
if silent {
180+
// We still output logging, that's controlled w/ a separate flag
181+
output::install_emitter(TracingOutput::default());
182+
} else {
183+
output::install_emitter(DefaultOutput::default());
184+
}
185+
167186
if let Some(log_config) = matches.get_one::<LogConfig>("log") {
168187
init_log_with_config(log_config.clone());
169188
}

moss/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ pub mod db;
1717
pub mod dependency;
1818
pub mod environment;
1919
pub mod installation;
20+
pub mod output;
2021
pub mod package;
2122
pub mod registry;
2223
pub mod repository;

moss/src/output.rs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// SPDX-FileCopyrightText: Copyright © 2020-2026 Serpent OS Developers
2+
//
3+
// SPDX-License-Identifier: MPL-2.0
4+
5+
use std::sync::{Arc, OnceLock};
6+
7+
use crate::repository;
8+
9+
pub use self::tracing::TracingOutput;
10+
pub use self::tui::TuiOutput;
11+
12+
mod tracing;
13+
mod tui;
14+
15+
/// Default emitter used if [`install_emitter`] isn't called with a
16+
/// custom [`Emit`] implementation.
17+
pub type DefaultOutput = Chain<TracingOutput, TuiOutput>;
18+
19+
/// Global emitter either defaulted or installed via [`install_emitter`]
20+
static EMITTER: OnceLock<Arc<dyn Emitter>> = OnceLock::new();
21+
22+
/// Install a global emitter that can be used with [`emit`]. If not called,
23+
/// [`DefaultOutput`] is used.
24+
///
25+
/// This can only be called once. Future calls have no effect.
26+
pub fn install_emitter(emitter: impl Emitter + 'static) {
27+
let _ = EMITTER.set(Arc::new(emitter));
28+
}
29+
30+
/// Get access to the global emitter
31+
pub fn emitter() -> &'static dyn Emitter {
32+
EMITTER.get_or_init(|| Arc::new(DefaultOutput::default())).as_ref()
33+
}
34+
35+
/// Emit an event for output
36+
#[macro_export]
37+
macro_rules! emit {
38+
($($tt:tt)*) => {
39+
$crate::output::Event::emit(($($tt)*), $crate::output::emitter());
40+
};
41+
}
42+
43+
/// Defines how events are emitted to some output
44+
pub trait Emitter: Send + Sync {
45+
fn emit(&self, _event: &InternalEvent) {}
46+
}
47+
48+
/// An emittable event
49+
pub trait Event: Sized {
50+
fn emit(self, _emitter: &dyn Emitter) {}
51+
}
52+
53+
/// An internal `moss` library event
54+
pub enum InternalEvent {
55+
RepositoryManager(repository::manager::OutputEvent),
56+
}
57+
58+
pub trait EmitExt: Emitter + Sized {
59+
fn chain<U>(self, other: U) -> Chain<Self, U>
60+
where
61+
U: Emitter + Sized,
62+
{
63+
Chain { a: self, b: other }
64+
}
65+
}
66+
67+
/// Do nothing with / suppress all output
68+
#[derive(Debug, Clone, Copy)]
69+
pub struct NoopOutput;
70+
71+
impl Emitter for NoopOutput {}
72+
73+
/// Chains multiple emitters together
74+
#[derive(Clone, Default)]
75+
pub struct Chain<A, B> {
76+
a: A,
77+
b: B,
78+
}
79+
80+
impl<A, B> Emitter for Chain<A, B>
81+
where
82+
A: Emitter,
83+
B: Emitter,
84+
{
85+
fn emit(&self, event: &InternalEvent) {
86+
self.a.emit(event);
87+
self.b.emit(event);
88+
}
89+
}

moss/src/output/tracing.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
use tracing::info;
2+
3+
use crate::{output, repository};
4+
5+
/// Tracing output
6+
#[derive(Debug, Clone, Default)]
7+
pub struct TracingOutput {
8+
_tracing: TracingState,
9+
}
10+
11+
impl output::Emitter for TracingOutput {
12+
fn emit(&self, event: &output::InternalEvent) {
13+
match event {
14+
output::InternalEvent::RepositoryManager(event) => match event {
15+
repository::manager::OutputEvent::RefreshStarted { num_to_refresh } => {
16+
info!(
17+
target: "repository_manager",
18+
num_repositories = %num_to_refresh,
19+
"Refreshing repositories"
20+
);
21+
}
22+
repository::manager::OutputEvent::RefreshRepoStarted(id) => {
23+
info!(
24+
target: "repository_manager",
25+
repo_id = %id,
26+
"Refreshing repository"
27+
);
28+
}
29+
repository::manager::OutputEvent::RefreshRepoFinished(id) => {
30+
info!(
31+
target: "repository_manager",
32+
repo_id = %id,
33+
"Repository refreshed"
34+
);
35+
}
36+
repository::manager::OutputEvent::RefreshFinished { elapsed } => {
37+
info!(
38+
target: "repository_manager",
39+
elapsed_seconds = %elapsed.as_secs_f32(),
40+
"All repositories refreshed"
41+
);
42+
}
43+
},
44+
}
45+
}
46+
}
47+
48+
#[derive(Debug, Clone, Default)]
49+
struct TracingState {
50+
// spans: Arc<ArcSwap<HashMap<TypeId, tracing::Span>>>,
51+
}

moss/src/output/tui.rs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
use std::{collections::HashMap, fmt, sync::Arc, time::Duration};
2+
3+
use arc_swap::ArcSwap;
4+
use tui::{MultiProgress, ProgressBar, ProgressStyle, Styled};
5+
6+
use crate::{output, repository};
7+
8+
/// Textual output to stdout / stderr
9+
#[derive(Debug, Clone, Default)]
10+
pub struct TuiOutput {
11+
progress: ProgressState,
12+
}
13+
14+
impl output::Emitter for TuiOutput {
15+
fn emit(&self, event: &output::InternalEvent) {
16+
match event {
17+
output::InternalEvent::RepositoryManager(event) => self.emit_repository_manager(event),
18+
}
19+
}
20+
}
21+
22+
impl TuiOutput {
23+
fn emit_repository_manager(&self, event: &repository::manager::OutputEvent) {
24+
match event {
25+
repository::manager::OutputEvent::RefreshStarted { .. } => {
26+
self.progress.multi_start();
27+
}
28+
repository::manager::OutputEvent::RefreshRepoStarted(id) => {
29+
let id = id.to_string();
30+
let pb = self.progress.multi_add_pb(
31+
&id,
32+
ProgressBar::new_spinner()
33+
.with_style(
34+
ProgressStyle::with_template(" {spinner} {wide_msg}")
35+
.unwrap()
36+
.tick_chars("--=≡■≡=--"),
37+
)
38+
.with_message(format!("{} {id}", "Refreshing".blue())),
39+
);
40+
pb.enable_steady_tick(Duration::from_millis(150));
41+
}
42+
repository::manager::OutputEvent::RefreshRepoFinished(id) => {
43+
let id = id.to_string();
44+
self.progress
45+
.multi_pb_println(&id, format_args!("{} {id}", "Refreshed".green()));
46+
self.progress.multi_remove_pb(&id);
47+
}
48+
repository::manager::OutputEvent::RefreshFinished { .. } => {
49+
self.progress.multi_finish();
50+
}
51+
}
52+
}
53+
}
54+
55+
#[derive(Debug, Clone, Default)]
56+
struct ProgressState {
57+
// ArcSwap provides lock-free safe usage in sync & async environments
58+
mpb: Arc<ArcSwap<MultiProgress>>,
59+
pbs: Arc<ArcSwap<HashMap<String, ProgressBar>>>,
60+
}
61+
62+
impl ProgressState {
63+
fn multi_start(&self) {
64+
self.mpb.store(Arc::new(MultiProgress::new()));
65+
self.pbs.store(Arc::new(HashMap::new()));
66+
}
67+
68+
fn multi_add_pb(&self, id: &str, pb: ProgressBar) -> ProgressBar {
69+
let pb = self.mpb.load().add(pb);
70+
self.pbs.rcu(|pbs| {
71+
let mut pbs = (**pbs).clone();
72+
pbs.insert(id.to_owned(), pb.clone());
73+
Arc::new(pbs)
74+
});
75+
pb
76+
}
77+
78+
fn multi_pb_println(&self, id: &str, args: fmt::Arguments<'_>) {
79+
let pbs = self.pbs.load();
80+
pbs.get(id).expect("pb exists").suspend(|| println!("{args}"));
81+
}
82+
83+
fn multi_remove_pb(&self, id: &str) {
84+
self.pbs.rcu(|pbs| {
85+
let mut pbs = (**pbs).clone();
86+
pbs.remove(id);
87+
Arc::new(pbs)
88+
});
89+
}
90+
91+
fn multi_finish(&self) {
92+
self.mpb.store(Arc::new(MultiProgress::new()));
93+
self.pbs.store(Arc::new(HashMap::new()));
94+
}
95+
}

0 commit comments

Comments
 (0)