Skip to content

Commit

Permalink
Merge pull request #14 from sourcefrog/no-tostring
Browse files Browse the repository at this point in the history
rm impl Model for Display
  • Loading branch information
sourcefrog authored Jan 15, 2024
2 parents f83ed78 + 921b291 commit 7b4e182
Show file tree
Hide file tree
Showing 9 changed files with 144 additions and 113 deletions.
94 changes: 45 additions & 49 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -1,57 +1,53 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
// https://github.com/microsoft/vscode-dev-containers/tree/v0.238.1/containers/rust
{
"name": "Rust",
"build": {
"dockerfile": "Dockerfile",
"args": {
// Use the VARIANT arg to pick a Debian OS version: buster, bullseye
// Use bullseye when on local on arm64/Apple Silicon.
"VARIANT": "bullseye"
}
},
"runArgs": [
"--cap-add=SYS_PTRACE",
"--security-opt",
"seccomp=unconfined"
],
"name": "Rust",
"build": {
"dockerfile": "Dockerfile",
"args": {
// Use the VARIANT arg to pick a Debian OS version: buster, bullseye
// Use bullseye when on local on arm64/Apple Silicon.
"VARIANT": "bullseye"
}
},
"runArgs": ["--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined"],

// Configure tool-specific properties.
"customizations": {
// Configure properties specific to VS Code.
"vscode": {
// Set *default* container specific settings.json values on container create.
"settings": {
"lldb.executable": "/usr/bin/lldb",
// VS Code don't watch files under ./target
"files.watcherExclude": {
"**/target/**": true
},
"rust-analyzer.checkOnSave.command": "clippy"
},

// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"vadimcn.vscode-lldb",
"mutantdino.resourcemonitor",
"rust-lang.rust-analyzer",
"tamasfe.even-better-toml",
"serayuzgur.crates"
]
}
},
// Configure tool-specific properties.
"customizations": {
// Configure properties specific to VS Code.
"vscode": {
// Set *default* container specific settings.json values on container create.
"settings": {
"lldb.executable": "/usr/bin/lldb",
// VS Code don't watch files under ./target
"files.watcherExclude": {
"**/target/**": true
},
"rust-analyzer.checkOnSave.command": "clippy"
},

// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"vadimcn.vscode-lldb",
"mutantdino.resourcemonitor",
"rust-lang.rust-analyzer",
"tamasfe.even-better-toml",
"serayuzgur.crates"
]
}
},

// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "rustc --version",
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],

// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "vscode",
"features": {
"git": "os-provided",
"github-cli": "latest",
"fish": "latest"
}
// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "rustc --version",

// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "vscode",
"features": {
"git": "os-provided",
"github-cli": "latest",
"ghcr.io/meaningful-ooo/devcontainer-features/fish:1": {}
}
}
4 changes: 4 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

- **Breaking change:** `Render` no longer has a blanket implementation for `Display`: you can no longer use a string, integer, or some object that implements `Display` as a model directly. You can instead implement `Render` explicitly, or opt in to this behavior using the new `nutmeg::models::DisplayModel`.

It seems that the `Display` implementation is often not a very satisfactory progress bar, and the presence of the blanket implementation causes confusing error messages when `Render` is not implemented correctly.

- Change back to `std::sync::Mutex` from `parking_lot`, to keep dependencies smaller.

## 0.1.4
Expand Down
14 changes: 11 additions & 3 deletions examples/bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,23 @@
use std::time::Instant;

struct IntModel(usize);

impl nutmeg::Model for IntModel {
fn render(&mut self, _width: usize) -> String {
format!("count: {}", self.0)
}
}

fn main() {
let start = Instant::now();
let view = nutmeg::View::new(0u64, nutmeg::Options::default());
let view = nutmeg::View::new(IntModel(0), nutmeg::Options::default());
let n = 10_000_000;
for i in 0..n {
view.update(|model| *model = i);
view.update(|IntModel(count)| *count = i);
}
view.message(format!(
"{}ms to send {} updates; average {}ns/update",
"{}ms to send {} updates; average {}ns/update\n",
start.elapsed().as_millis(),
n,
start.elapsed().as_nanos() / n as u128,
Expand Down
20 changes: 20 additions & 0 deletions examples/display_model.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//! Anything that implements Display can be wrapped in a DisplayModel.
use std::fs::read_dir;
use std::io;
use std::thread::sleep;
use std::time::Duration;

use nutmeg::models::DisplayModel;

fn main() -> io::Result<()> {
let options = nutmeg::Options::default();
let model = DisplayModel::new(String::new());
let view = nutmeg::View::new(model, options);
for p in read_dir(".")? {
let dir_entry = p?;
view.update(|DisplayModel(message)| *message = dir_entry.path().display().to_string());
sleep(Duration::from_millis(300));
}
Ok(())
}
10 changes: 6 additions & 4 deletions examples/print_holdoff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,24 @@ use std::thread;
use std::time;
use std::time::Duration;

use nutmeg::models::LinearModel;

fn main() -> io::Result<()> {
let options = nutmeg::Options::default()
.print_holdoff(Duration::from_millis(1000))
.update_interval(Duration::from_millis(0));
let mut view = nutmeg::View::new(0usize, options);
let mut view = nutmeg::View::new(LinearModel::new("Things", 50), options);
for _i in 0..5 {
for j in 0..4 {
writeln!(view, "message {j}")?;
thread::sleep(time::Duration::from_millis(100));
}
for j in 0..20 {
view.update(|state| {
view.update(|model| {
// Previous updates were applied even though
// they may not have been painted.
assert!(j == 0 || *state == (j - 1));
*state = j
assert!(j == 0 || model.done() == (j - 1));
model.set_done(j);
});
thread::sleep(time::Duration::from_millis(100));
}
Expand Down
17 changes: 0 additions & 17 deletions examples/string_as_model.rs

This file was deleted.

59 changes: 23 additions & 36 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,11 @@ processed, the amount of data transmitted or received, the currently active task
The Model can be any of these things, from simplest to most powerful:
1. Any type that implements [std::fmt::Display], such as a String or integer.
2. One of the provided [models].
3. An application-defined struct (or enum or other type) that implements [Model].
1. One of the provided [models].
2. An application-defined struct (or enum or other type) that implements [Model].
The model is responsible for rendering itself into a String, optionally with ANSI styling,
by implementing [Model::render] (or [std::fmt::Display]). Applications might
by implementing [Model::render]. Applications might
choose to use any of the Rust crates that can render ANSI control codes into a
string, such as yansi.
Expand Down Expand Up @@ -211,7 +210,6 @@ is welcome.

#![warn(missing_docs)]

use std::fmt::Display;
use std::io::{self, Write};
use std::sync::{Arc, Mutex};
use std::time::Instant;
Expand Down Expand Up @@ -292,29 +290,6 @@ pub trait Model {
}
}

/// Blanket implementation of Model for Display.
///
/// `self` is converted to a display string without regard for
/// the terminal width.
///
/// This allows direct use of e.g. a String or integer as a model
/// for very basic progress indications.
///
/// ```
/// use nutmeg::{Options, View};
///
/// let view = View::new(0, Options::default());
/// view.update(|model| *model += 1);
/// ```
impl<T> Model for T
where
T: Display,
{
fn render(&mut self, _width: usize) -> String {
self.to_string()
}
}

/// A view that draws and coordinates a progress bar on the terminal.
///
/// There should be only one `View` active on a terminal at any time, and
Expand Down Expand Up @@ -512,10 +487,13 @@ impl<M: Model> View<M> {
///
/// ```
/// use nutmeg::{Options, View};
/// use nutmeg::models::LinearModel;
///
/// let view = View::new(10, Options::default());
/// view.update(|model| *model += 3);
/// assert_eq!(view.inspect_model(|m| *m), 13);
/// let mut model = LinearModel::new("Things done", 100);
/// model.set_done(10);
/// let view = View::new(model, Options::default());
/// view.update(|model| model.increment(3));
/// assert_eq!(view.inspect_model(|m| m.done()), 13);
/// ```
pub fn inspect_model<F, R>(&self, f: F) -> R
where
Expand Down Expand Up @@ -549,10 +527,15 @@ impl<M: Model> View<M> {
///
/// ```
/// use nutmeg::{Options, View};
/// use nutmeg::models::LinearModel;
///
/// let view = View::new(0, Options::default());
/// // ...
/// view.message(format!("{} splines reticulated\n", 42));
/// let view = View::new(LinearModel::new("Splines reticulated", 100), Options::default());
/// for i in 0..20 {
/// view.update(|model| model.increment(1));
/// if i == 12 {
/// view.message("Some quality splines here!\n");
/// }
/// }
/// ```
pub fn message<S: AsRef<str>>(&self, message: S) {
self.message_bytes(message.as_ref().as_bytes())
Expand All @@ -566,8 +549,9 @@ impl<M: Model> View<M> {
///
/// ```
/// use nutmeg::{Options, View};
/// use nutmeg::models::LinearModel;
///
/// let view = View::new("model content", Options::default());
/// let view = View::new(LinearModel::new("Things done", 100), Options::default());
/// view.message_bytes(b"hello crow\n");
/// ```
pub fn message_bytes<S: AsRef<[u8]>>(&self, message: S) {
Expand All @@ -588,8 +572,11 @@ impl<M: Model> View<M> {
///
/// ```
/// use nutmeg::{Destination, Options, View};
/// use nutmeg::models::DisplayModel;
///
/// let view = View::new(0, Options::default().destination(Destination::Capture));
/// let view = View::new(
/// DisplayModel("unchanging message"),
/// Options::default().destination(Destination::Capture));
/// let output = view.captured_output();
/// view.message("Captured message\n");
/// drop(view);
Expand Down
29 changes: 29 additions & 0 deletions src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//! requirement to use them: they only implement the public [Model] interface.
use std::borrow::Cow;
use std::fmt::{Debug, Display};
use std::time::{Duration, Instant};

#[allow(unused)] // For docstrings
Expand Down Expand Up @@ -86,6 +87,7 @@ impl Model for StringPair {
/// progress.update(|model| model.increment(1));
/// }
/// ```
#[derive(Debug)]
pub struct LinearModel {
done: usize,
total: usize,
Expand All @@ -109,6 +111,16 @@ impl LinearModel {
self.total = total
}

/// Get the total number of things.
pub fn total(&self) -> usize {
self.total
}

/// Get the number of things done so far.
pub fn done(&self) -> usize {
self.done
}

/// Update the amount of work done.
///
/// This should normally be called from a callback passed to [View::update].
Expand Down Expand Up @@ -270,3 +282,20 @@ where
(self.render_fn)(&mut self.value)
}
}

/// A model that holds a single value and renders it using its `Display` implementation.
#[derive(Debug)]
pub struct DisplayModel<T: Display + Debug>(pub T);

impl<T: Display + Debug> DisplayModel<T> {
/// Construct a new model holding a value implementing `Display`.
pub fn new(value: T) -> DisplayModel<T> {
DisplayModel(value)
}
}

impl<T: Display + Debug> Model for DisplayModel<T> {
fn render(&mut self, _width: usize) -> String {
format!("{}", self.0)
}
}
Loading

0 comments on commit 7b4e182

Please sign in to comment.