term: Implement new output widget system
We implement a new `Element` trait which allows for more complex output to be drawn, such as vertical and horizontal stacks. We adapt `Table` to work within this system. We refresh the output of `rad patch show` to use these new tools.
This commit is contained in:
parent
73221e43d7
commit
ab74e54949
|
|
@ -17,7 +17,7 @@ The issue is now listed under our project.
|
||||||
|
|
||||||
```
|
```
|
||||||
$ rad issue list
|
$ rad issue list
|
||||||
2e8c1bf3fe0532a314778357c886608a966a34bd "flux capacitor underpowered"
|
2e8c1bf3fe0532a314778357c886608a966a34bd "flux capacitor underpowered" ❲unassigned❳
|
||||||
```
|
```
|
||||||
|
|
||||||
Great! Now we've documented the issue for ourselves and others.
|
Great! Now we've documented the issue for ourselves and others.
|
||||||
|
|
|
||||||
|
|
@ -58,10 +58,14 @@ Define power requirements 191a14e520f R0 3e674d1 (flux-capacitor-power) ahead 1,
|
||||||
Nothing to show.
|
Nothing to show.
|
||||||
|
|
||||||
$ rad patch show 191a14e520f2eeff7c0e3ee0a5523c5217eecb89
|
$ rad patch show 191a14e520f2eeff7c0e3ee0a5523c5217eecb89
|
||||||
|
╭────────────────────────────────────────────────────────────────────╮
|
||||||
Define power requirements
|
│ Title Define power requirements │
|
||||||
|
│ Patch 191a14e520f2eeff7c0e3ee0a5523c5217eecb89 │
|
||||||
See details.
|
│ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │
|
||||||
|
│ Status open │
|
||||||
|
│ │
|
||||||
|
│ See details. │
|
||||||
|
╰────────────────────────────────────────────────────────────────────╯
|
||||||
|
|
||||||
commit 3e674d1a1df90807e934f9ae5da2591dd6848a33
|
commit 3e674d1a1df90807e934f9ae5da2591dd6848a33
|
||||||
Author: radicle <radicle@localhost>
|
Author: radicle <radicle@localhost>
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ The issue is now listed under our project.
|
||||||
|
|
||||||
```
|
```
|
||||||
$ rad issue list
|
$ rad issue list
|
||||||
b05e945bb63c11bf80320f4e26ad1d1f7c51f755 "flux capacitor underpowered"
|
b05e945bb63c11bf80320f4e26ad1d1f7c51f755 "flux capacitor underpowered" ❲unassigned❳
|
||||||
```
|
```
|
||||||
|
|
||||||
Great! Now we've documented the issue for ourselves and others.
|
Great! Now we've documented the issue for ourselves and others.
|
||||||
|
|
|
||||||
|
|
@ -58,10 +58,14 @@ Define power requirements a07ef7743a3 R0 3e674d1 (flux-capacitor-power) ahead 1,
|
||||||
Nothing to show.
|
Nothing to show.
|
||||||
|
|
||||||
$ rad patch show a07ef7743a32a2e902672ea3526d1db6ee08108a
|
$ rad patch show a07ef7743a32a2e902672ea3526d1db6ee08108a
|
||||||
|
╭────────────────────────────────────────────────────────────────────╮
|
||||||
Define power requirements
|
│ Title Define power requirements │
|
||||||
|
│ Patch a07ef7743a32a2e902672ea3526d1db6ee08108a │
|
||||||
See details.
|
│ Author did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk │
|
||||||
|
│ Status open │
|
||||||
|
│ │
|
||||||
|
│ See details. │
|
||||||
|
╰────────────────────────────────────────────────────────────────────╯
|
||||||
|
|
||||||
commit 3e674d1a1df90807e934f9ae5da2591dd6848a33
|
commit 3e674d1a1df90807e934f9ae5da2591dd6848a33
|
||||||
Author: radicle <radicle@localhost>
|
Author: radicle <radicle@localhost>
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ use radicle_crypto::Verified;
|
||||||
|
|
||||||
use crate::terminal as term;
|
use crate::terminal as term;
|
||||||
use crate::terminal::args::{Args, Error, Help};
|
use crate::terminal::args::{Args, Error, Help};
|
||||||
|
use crate::terminal::Element;
|
||||||
use crate::terminal::Interactive;
|
use crate::terminal::Interactive;
|
||||||
|
|
||||||
pub const HELP: Help = Help {
|
pub const HELP: Help = Help {
|
||||||
|
|
@ -403,7 +404,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
state,
|
state,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
t.render();
|
t.print();
|
||||||
}
|
}
|
||||||
Operation::Commit { id, rev } => {
|
Operation::Commit { id, rev } => {
|
||||||
let mut proposal = proposals.get_mut(&id)?;
|
let mut proposal = proposals.get_mut(&id)?;
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ use radicle::prelude::Did;
|
||||||
|
|
||||||
use crate::terminal as term;
|
use crate::terminal as term;
|
||||||
use crate::terminal::args::{Args, Error, Help};
|
use crate::terminal::args::{Args, Error, Help};
|
||||||
|
use crate::terminal::Element;
|
||||||
|
|
||||||
use radicle::cob::common::{Reaction, Tag};
|
use radicle::cob::common::{Reaction, Tag};
|
||||||
use radicle::cob::issue;
|
use radicle::cob::issue;
|
||||||
|
|
@ -314,10 +315,14 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
t.push([
|
t.push([
|
||||||
id.to_string(),
|
id.to_string(),
|
||||||
format!("{:?}", issue.title()),
|
format!("{:?}", issue.title()),
|
||||||
assigned.to_string(),
|
if assigned.is_empty() {
|
||||||
|
String::from("❲unassigned❳")
|
||||||
|
} else {
|
||||||
|
assigned.to_string()
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
t.render();
|
t.print();
|
||||||
}
|
}
|
||||||
Operation::Delete { id } => {
|
Operation::Delete { id } => {
|
||||||
issues.remove(&id, &signer)?;
|
issues.remove(&id, &signer)?;
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ use crate::terminal as term;
|
||||||
use crate::terminal::args::{Args, Error, Help};
|
use crate::terminal::args::{Args, Error, Help};
|
||||||
|
|
||||||
use radicle::storage::{ReadRepository, ReadStorage};
|
use radicle::storage::{ReadRepository, ReadStorage};
|
||||||
|
use term::Element;
|
||||||
|
|
||||||
pub const HELP: Help = Help {
|
pub const HELP: Help = Help {
|
||||||
name: "ls",
|
name: "ls",
|
||||||
|
|
@ -52,13 +53,13 @@ pub fn run(_options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
let Ok(proj) = repo.project_of(profile.id()) else { return };
|
let Ok(proj) = repo.project_of(profile.id()) else { return };
|
||||||
let head = term::format::oid(head);
|
let head = term::format::oid(head);
|
||||||
table.push([
|
table.push([
|
||||||
term::format::bold(proj.name()),
|
term::format::bold(proj.name().to_owned()),
|
||||||
term::format::tertiary(id.urn().as_str()),
|
term::format::tertiary(id.urn()),
|
||||||
term::format::secondary(head.as_str()),
|
term::format::secondary(head),
|
||||||
term::format::italic(proj.description()),
|
term::format::italic(proj.description().to_owned()),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
table.render();
|
table.print();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,25 @@
|
||||||
use radicle::node;
|
use radicle::node;
|
||||||
|
|
||||||
use crate::terminal as term;
|
use crate::terminal as term;
|
||||||
|
use crate::terminal::Element;
|
||||||
|
|
||||||
pub fn run<S: node::routing::Store>(routing: &S) -> anyhow::Result<()> {
|
pub fn run<S: node::routing::Store>(routing: &S) -> anyhow::Result<()> {
|
||||||
let mut t = term::Table::new(term::table::TableOptions::default());
|
let mut t = term::Table::new(term::table::TableOptions::default());
|
||||||
t.push(["RID", "NID"]);
|
t.push([
|
||||||
t.push(["---", "---"]);
|
term::format::default(String::from("RID")),
|
||||||
|
term::format::default(String::from("NID")),
|
||||||
|
]);
|
||||||
|
t.push([
|
||||||
|
term::format::default(String::from("---")),
|
||||||
|
term::format::default(String::from("---")),
|
||||||
|
]);
|
||||||
for (id, node) in routing.entries()? {
|
for (id, node) in routing.entries()? {
|
||||||
t.push([
|
t.push([
|
||||||
term::format::highlight(id).to_string(),
|
term::format::highlight(id.to_string()),
|
||||||
term::format::node(&node),
|
term::format::default(term::format::node(&node)),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
t.render();
|
t.print();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ use radicle::node::tracking;
|
||||||
use radicle::prelude::Did;
|
use radicle::prelude::Did;
|
||||||
|
|
||||||
use crate::terminal as term;
|
use crate::terminal as term;
|
||||||
|
use term::Element;
|
||||||
|
|
||||||
use super::TrackingMode;
|
use super::TrackingMode;
|
||||||
|
|
||||||
|
|
@ -15,33 +16,55 @@ pub fn run(store: &tracking::store::Config, mode: TrackingMode) -> anyhow::Resul
|
||||||
|
|
||||||
fn print_repos(store: &tracking::store::Config) -> anyhow::Result<()> {
|
fn print_repos(store: &tracking::store::Config) -> anyhow::Result<()> {
|
||||||
let mut t = term::Table::new(term::table::TableOptions::default());
|
let mut t = term::Table::new(term::table::TableOptions::default());
|
||||||
t.push(["RID", "Scope", "Policy"]);
|
t.push([
|
||||||
t.push(["---", "-----", "------"]);
|
term::format::default(String::from("RID")),
|
||||||
|
term::format::default(String::from("Scope")),
|
||||||
|
term::format::default(String::from("Policy")),
|
||||||
|
]);
|
||||||
|
t.push([
|
||||||
|
term::format::default(String::from("---")),
|
||||||
|
term::format::default(String::from("-----")),
|
||||||
|
term::format::default(String::from("------")),
|
||||||
|
]);
|
||||||
for tracking::Repo { id, scope, policy } in store.repo_policies()? {
|
for tracking::Repo { id, scope, policy } in store.repo_policies()? {
|
||||||
|
let id = id.to_string();
|
||||||
|
let scope = scope.to_string();
|
||||||
|
let policy = policy.to_string();
|
||||||
|
|
||||||
t.push([
|
t.push([
|
||||||
term::format::highlight(id.to_string()),
|
term::format::highlight(id),
|
||||||
term::format::secondary(scope.to_string()),
|
term::format::secondary(scope),
|
||||||
term::format::secondary(policy.to_string()),
|
term::format::secondary(policy),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
t.render();
|
t.print();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_nodes(store: &tracking::store::Config) -> anyhow::Result<()> {
|
fn print_nodes(store: &tracking::store::Config) -> anyhow::Result<()> {
|
||||||
let mut t = term::Table::new(term::table::TableOptions::default());
|
let mut t = term::Table::new(term::table::TableOptions::default());
|
||||||
t.push(["DID", "Alias", "Policy"]);
|
t.push([
|
||||||
t.push(["---", "-----", "------"]);
|
term::format::default(String::from("DID")),
|
||||||
|
term::format::default(String::from("Alias")),
|
||||||
|
term::format::default(String::from("Policy")),
|
||||||
|
]);
|
||||||
|
t.push([
|
||||||
|
term::format::default(String::from("---")),
|
||||||
|
term::format::default(String::from("-----")),
|
||||||
|
term::format::default(String::from("------")),
|
||||||
|
]);
|
||||||
for tracking::Node { id, alias, policy } in store.node_policies()? {
|
for tracking::Node { id, alias, policy } in store.node_policies()? {
|
||||||
t.push([
|
t.push([
|
||||||
term::format::highlight(Did::from(id).to_string()),
|
term::format::highlight(Did::from(id).to_string()),
|
||||||
match alias {
|
match alias {
|
||||||
None => term::format::secondary("n/a".to_string()),
|
None => term::format::secondary(String::from("n/a")),
|
||||||
Some(alias) => term::format::secondary(alias),
|
Some(alias) => term::format::secondary(alias),
|
||||||
},
|
},
|
||||||
term::format::secondary(policy.to_string()),
|
term::format::secondary(policy.to_string()),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
t.render();
|
t.print();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,11 @@ use super::*;
|
||||||
use radicle::cob::patch;
|
use radicle::cob::patch;
|
||||||
use radicle::git;
|
use radicle::git;
|
||||||
use radicle::storage::git::Repository;
|
use radicle::storage::git::Repository;
|
||||||
|
use radicle_term::{
|
||||||
|
label,
|
||||||
|
table::{Table, TableOptions},
|
||||||
|
Element, Paint, VStack,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::terminal as term;
|
use crate::terminal as term;
|
||||||
|
|
||||||
|
|
@ -40,15 +45,40 @@ pub fn run(
|
||||||
anyhow::bail!("Patch `{patch_id}` not found");
|
anyhow::bail!("Patch `{patch_id}` not found");
|
||||||
};
|
};
|
||||||
|
|
||||||
term::blank();
|
let mut attrs = Table::<2, Paint<String>>::new(TableOptions {
|
||||||
term::info!("{}", term::format::bold(patch.title()));
|
spacing: 2,
|
||||||
term::blank();
|
..TableOptions::default()
|
||||||
|
});
|
||||||
|
attrs.push([
|
||||||
|
term::format::tertiary("Title".to_owned()),
|
||||||
|
term::format::bold(patch.title().to_owned()),
|
||||||
|
]);
|
||||||
|
attrs.push([
|
||||||
|
term::format::tertiary("Patch".to_owned()),
|
||||||
|
term::format::default(patch_id.to_string()),
|
||||||
|
]);
|
||||||
|
attrs.push([
|
||||||
|
term::format::tertiary("Author".to_owned()),
|
||||||
|
term::format::default(patch.author().id().to_string()),
|
||||||
|
]);
|
||||||
|
attrs.push([
|
||||||
|
term::format::tertiary("Status".to_owned()),
|
||||||
|
term::format::default(patch.state().to_string()),
|
||||||
|
]);
|
||||||
|
|
||||||
let description = patch.description().trim();
|
let description = patch.description().trim();
|
||||||
if !description.is_empty() {
|
let meta = VStack::default()
|
||||||
term::blob(description);
|
.border(Some(term::ansi::Color::Blue))
|
||||||
term::blank();
|
.child(attrs)
|
||||||
}
|
.blank()
|
||||||
|
.children(if !description.is_empty() {
|
||||||
|
Some(label(term::format::dim(description)))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
});
|
||||||
|
|
||||||
|
meta.print();
|
||||||
|
term::blank();
|
||||||
|
|
||||||
show_patch_diff(&patch, storage, workdir)?;
|
show_patch_diff(&patch, storage, workdir)?;
|
||||||
term::blank();
|
term::blank();
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ use radicle::Profile;
|
||||||
|
|
||||||
use crate::terminal as term;
|
use crate::terminal as term;
|
||||||
use crate::terminal::args::{Args, Error, Help};
|
use crate::terminal::args::{Args, Error, Help};
|
||||||
|
use crate::terminal::Element as _;
|
||||||
|
|
||||||
pub const HELP: Help = Help {
|
pub const HELP: Help = Help {
|
||||||
name: "self",
|
name: "self",
|
||||||
|
|
@ -143,7 +144,7 @@ fn all(profile: &Profile) -> anyhow::Result<()> {
|
||||||
term::format::tertiary(node_path.join("radicle.sock").display()).to_string(),
|
term::format::tertiary(node_path.join("radicle.sock").display()).to_string(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
table.render();
|
table.print();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use radicle::git;
|
use radicle::git;
|
||||||
|
|
||||||
use crate::terminal as term;
|
use crate::terminal as term;
|
||||||
|
use crate::terminal::Element;
|
||||||
|
|
||||||
/// The user supplied `Patch` description.
|
/// The user supplied `Patch` description.
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
|
@ -59,7 +60,7 @@ pub fn list_commits(commits: &[git::raw::Commit]) -> anyhow::Result<()> {
|
||||||
term::format::italic(String::from_utf8_lossy(message).to_string()),
|
term::format::italic(String::from_utf8_lossy(message).to_string()),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
table.render();
|
table.print();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,15 @@ impl From<&str> for Paint<String> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<Paint<&str>> for Paint<String> {
|
||||||
|
fn from(paint: Paint<&str>) -> Self {
|
||||||
|
Self {
|
||||||
|
item: paint.item.to_owned(),
|
||||||
|
style: paint.style,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<T> Paint<T> {
|
impl<T> Paint<T> {
|
||||||
/// Constructs a new `Paint` structure encapsulating `item` with no set
|
/// Constructs a new `Paint` structure encapsulating `item` with no set
|
||||||
/// styling.
|
/// styling.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,187 @@
|
||||||
|
use std::fmt;
|
||||||
|
use std::ops::Deref;
|
||||||
|
use std::vec;
|
||||||
|
|
||||||
|
use crate::cell::Cell;
|
||||||
|
use crate::Label;
|
||||||
|
|
||||||
|
/// A text element that has a size and can be rendered to the terminal.
|
||||||
|
pub trait Element: fmt::Debug {
|
||||||
|
/// Get the size of the element, in rows and columns.
|
||||||
|
fn size(&self) -> Size;
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
/// Render the element as lines of text that can be printed.
|
||||||
|
fn render(&self) -> Vec<Line>;
|
||||||
|
|
||||||
|
/// Get the number of columns occupied by this element.
|
||||||
|
fn columns(&self) -> usize {
|
||||||
|
self.size().cols
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the number of rows occupied by this element.
|
||||||
|
fn rows(&self) -> usize {
|
||||||
|
self.size().rows
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Print this element to stdout.
|
||||||
|
fn print(&self) {
|
||||||
|
for line in self.render() {
|
||||||
|
println!("{line}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
/// Return a string representation of this element.
|
||||||
|
fn display(&self) -> String {
|
||||||
|
let mut out = String::new();
|
||||||
|
for line in self.render() {
|
||||||
|
out.extend(line.into_iter().map(|l| l.to_string()));
|
||||||
|
out.push('\n');
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Element> Element for &T {
|
||||||
|
fn size(&self) -> Size {
|
||||||
|
self.deref().size()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&self) -> Vec<Line> {
|
||||||
|
self.deref().render()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print(&self) {
|
||||||
|
self.deref().print()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A line of text that has styling and can be displayed.
|
||||||
|
#[derive(Clone, Default, Debug)]
|
||||||
|
pub struct Line {
|
||||||
|
items: Vec<Label>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Line {
|
||||||
|
/// Create a new line.
|
||||||
|
pub fn new(item: impl Into<Label>) -> Self {
|
||||||
|
Self {
|
||||||
|
items: vec![item.into()],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add an item to this line.
|
||||||
|
pub fn item(mut self, item: impl Into<Label>) -> Self {
|
||||||
|
self.push(item);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add multiple items to this line.
|
||||||
|
pub fn extend(mut self, items: impl IntoIterator<Item = Label>) -> Self {
|
||||||
|
self.items.extend(items.into_iter());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add an item to this line.
|
||||||
|
pub fn push(&mut self, item: impl Into<Label>) {
|
||||||
|
self.items.push(item.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pad this line to occupy the given width.
|
||||||
|
pub fn pad(&mut self, width: usize) {
|
||||||
|
let w = Element::columns(self);
|
||||||
|
let pad = width - w;
|
||||||
|
|
||||||
|
self.items.push(Label::new(" ".repeat(pad).as_str()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Truncate this line to the given width.
|
||||||
|
pub fn truncate(&mut self, width: usize, delim: &str) {
|
||||||
|
while self.columns() > width {
|
||||||
|
let total = self.columns();
|
||||||
|
|
||||||
|
if total - self.items.last().map_or(0, Cell::width) > width {
|
||||||
|
self.items.pop();
|
||||||
|
} else if let Some(item) = self.items.last_mut() {
|
||||||
|
*item = item.truncate(width - (total - Cell::width(item)), delim);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoIterator for Line {
|
||||||
|
type Item = Label;
|
||||||
|
type IntoIter = vec::IntoIter<Label>;
|
||||||
|
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
self.items.into_iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Label> for Line {
|
||||||
|
fn from(label: Label) -> Self {
|
||||||
|
Self { items: vec![label] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Element for Line {
|
||||||
|
fn size(&self) -> Size {
|
||||||
|
Size::new(self.items.iter().map(Cell::width).sum(), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&self) -> Vec<Line> {
|
||||||
|
vec![self.clone()]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Line {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
for item in &self.items {
|
||||||
|
write!(f, "{item}")?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Size of a text element, in columns and rows.
|
||||||
|
#[derive(Clone, Debug, Default, Copy, PartialEq, Eq)]
|
||||||
|
pub struct Size {
|
||||||
|
/// Columns occupied.
|
||||||
|
pub cols: usize,
|
||||||
|
/// Rows occupied.
|
||||||
|
pub rows: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Size {
|
||||||
|
/// Create a new [`Size`].
|
||||||
|
pub fn new(cols: usize, rows: usize) -> Self {
|
||||||
|
Self { cols, rows }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_truncate() {
|
||||||
|
let line = Line::default().item("banana").item("peach").item("apple");
|
||||||
|
|
||||||
|
let mut actual = line.clone();
|
||||||
|
actual.truncate(9, "…");
|
||||||
|
assert_eq!(actual.to_string(), "bananape…");
|
||||||
|
|
||||||
|
let mut actual = line.clone();
|
||||||
|
actual.truncate(7, "…");
|
||||||
|
assert_eq!(actual.to_string(), "banana…");
|
||||||
|
|
||||||
|
let mut actual = line.clone();
|
||||||
|
actual.truncate(1, "…");
|
||||||
|
assert_eq!(actual.to_string(), "…");
|
||||||
|
|
||||||
|
let mut actual = line;
|
||||||
|
actual.truncate(0, "…");
|
||||||
|
assert_eq!(actual.to_string(), "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
use crate::Paint;
|
use crate::Paint;
|
||||||
|
|
||||||
|
pub fn default<D: std::fmt::Display>(msg: D) -> Paint<D> {
|
||||||
|
Paint::new(msg)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn wrap<D: std::fmt::Display>(msg: D) -> Paint<D> {
|
pub fn wrap<D: std::fmt::Display>(msg: D) -> Paint<D> {
|
||||||
Paint::wrapping(msg)
|
Paint::wrapping(msg)
|
||||||
}
|
}
|
||||||
|
|
@ -28,6 +32,10 @@ pub fn yellow<D: std::fmt::Display>(msg: D) -> Paint<D> {
|
||||||
Paint::yellow(msg)
|
Paint::yellow(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn faint<D: std::fmt::Display>(msg: D) -> Paint<D> {
|
||||||
|
Paint::fixed(236, msg)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn highlight<D: std::fmt::Debug + std::fmt::Display>(input: D) -> Paint<D> {
|
pub fn highlight<D: std::fmt::Debug + std::fmt::Display>(input: D) -> Paint<D> {
|
||||||
Paint::green(input).bold()
|
Paint::green(input).bold()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
use crate::{Element, Line, Size};
|
||||||
|
|
||||||
|
/// Horizontal stack of [`Element`] objects that implements [`Element`].
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
pub struct HStack<'a> {
|
||||||
|
elems: Vec<Box<dyn Element + 'a>>,
|
||||||
|
width: usize,
|
||||||
|
height: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> HStack<'a> {
|
||||||
|
/// Add an element to the stack.
|
||||||
|
pub fn child(mut self, child: impl Element + 'a) -> Self {
|
||||||
|
self.width += child.columns();
|
||||||
|
self.height = self.height.max(child.rows());
|
||||||
|
self.elems.push(Box::new(child));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Element for HStack<'a> {
|
||||||
|
fn size(&self) -> Size {
|
||||||
|
Size::new(self.width, self.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&self) -> Vec<Line> {
|
||||||
|
fn rearrange(input: Vec<Vec<Line>>) -> Vec<Line> {
|
||||||
|
let max_len = input.iter().map(|v| v.len()).max().unwrap_or(0);
|
||||||
|
|
||||||
|
(0..max_len)
|
||||||
|
.map(|i| {
|
||||||
|
Line::default().extend(
|
||||||
|
input
|
||||||
|
.iter()
|
||||||
|
.filter_map(move |v| v.get(i))
|
||||||
|
.flat_map(|l| l.clone().into_iter()),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
rearrange(self.elems.iter().map(|e| e.render()).collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use crate::{cell::Cell, Element, Line, Paint, Size};
|
||||||
|
|
||||||
|
/// A styled string that does not contain any `'\n'` and implements [`Element`] and [`Cell`].
|
||||||
|
#[derive(Clone, Default, Debug)]
|
||||||
|
pub struct Label(Paint<String>);
|
||||||
|
|
||||||
|
impl Label {
|
||||||
|
/// Create a new label.
|
||||||
|
pub fn new(s: &str) -> Self {
|
||||||
|
Self(Paint::new(s.replace('\n', " ")))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a blank label.
|
||||||
|
pub fn blank() -> Self {
|
||||||
|
Self(Paint::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Element for Label {
|
||||||
|
fn size(&self) -> Size {
|
||||||
|
Size::new(self.0.width(), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&self) -> Vec<Line> {
|
||||||
|
vec![Line::new(self.clone())]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Label {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cell for Label {
|
||||||
|
type Padded = Self;
|
||||||
|
type Truncated = Self;
|
||||||
|
|
||||||
|
fn pad(&self, width: usize) -> Self::Padded {
|
||||||
|
Self(self.0.pad(width))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn truncate(&self, width: usize, delim: &str) -> Self::Truncated {
|
||||||
|
Self(self.0.truncate(width, delim))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn width(&self) -> usize {
|
||||||
|
Cell::width(&self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Paint<String>> for Label {
|
||||||
|
fn from(paint: Paint<String>) -> Self {
|
||||||
|
Self(Paint {
|
||||||
|
item: paint.item.replace('\n', " "),
|
||||||
|
style: paint.style,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Paint<&str>> for Label {
|
||||||
|
fn from(paint: Paint<&str>) -> Self {
|
||||||
|
Self(Paint {
|
||||||
|
item: paint.item.replace('\n', " "),
|
||||||
|
style: paint.style,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for Label {
|
||||||
|
fn from(value: String) -> Self {
|
||||||
|
Label::from(value.as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for Label {
|
||||||
|
fn from(value: &str) -> Self {
|
||||||
|
Self(Paint::new(value.replace('\n', " ")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new label from a [`Paint`] object.
|
||||||
|
pub fn label(s: impl Into<Paint<String>>) -> Label {
|
||||||
|
Label(s.into())
|
||||||
|
}
|
||||||
|
|
@ -2,19 +2,26 @@ pub mod ansi;
|
||||||
pub mod cell;
|
pub mod cell;
|
||||||
pub mod command;
|
pub mod command;
|
||||||
pub mod editor;
|
pub mod editor;
|
||||||
|
pub mod element;
|
||||||
pub mod format;
|
pub mod format;
|
||||||
|
pub mod hstack;
|
||||||
pub mod io;
|
pub mod io;
|
||||||
|
pub mod label;
|
||||||
pub mod spinner;
|
pub mod spinner;
|
||||||
pub mod table;
|
pub mod table;
|
||||||
pub mod textbox;
|
pub mod vstack;
|
||||||
|
|
||||||
|
pub use ansi::Color;
|
||||||
pub use ansi::{paint, Paint};
|
pub use ansi::{paint, Paint};
|
||||||
pub use editor::Editor;
|
pub use editor::Editor;
|
||||||
|
pub use element::{Element, Line, Size};
|
||||||
|
pub use hstack::HStack;
|
||||||
pub use inquire::ui::Styled;
|
pub use inquire::ui::Styled;
|
||||||
pub use io::*;
|
pub use io::*;
|
||||||
|
pub use label::{label, Label};
|
||||||
pub use spinner::{spinner, Spinner};
|
pub use spinner::{spinner, Spinner};
|
||||||
pub use table::Table;
|
pub use table::Table;
|
||||||
pub use textbox::TextBox;
|
pub use vstack::{VStack, VStackOptions};
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
||||||
pub enum Interactive {
|
pub enum Interactive {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
//! t.push(["pest", "biological control"]);
|
//! t.push(["pest", "biological control"]);
|
||||||
//! t.push(["aphid", "lacewing"]);
|
//! t.push(["aphid", "lacewing"]);
|
||||||
//! t.push(["spider mite", "ladybug"]);
|
//! t.push(["spider mite", "ladybug"]);
|
||||||
//! t.render();
|
//! t.print();
|
||||||
//! ```
|
//! ```
|
||||||
//! Output:
|
//! Output:
|
||||||
//! ``` plain
|
//! ``` plain
|
||||||
|
|
@ -16,10 +16,13 @@
|
||||||
//! aphid ladybug
|
//! aphid ladybug
|
||||||
//! spider mite persimilis
|
//! spider mite persimilis
|
||||||
//! ```
|
//! ```
|
||||||
use std::io;
|
use std::fmt;
|
||||||
|
|
||||||
use crate as term;
|
use crate as term;
|
||||||
use crate::cell::Cell;
|
use crate::cell::Cell;
|
||||||
|
use crate::{Label, Line, Size};
|
||||||
|
|
||||||
|
pub use crate::Element;
|
||||||
|
|
||||||
/// Used to specify maximum width or height.
|
/// Used to specify maximum width or height.
|
||||||
#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)]
|
#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)]
|
||||||
|
|
@ -28,99 +31,106 @@ pub struct Max {
|
||||||
height: Option<usize>,
|
height: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug)]
|
||||||
pub struct TableOptions {
|
pub struct TableOptions {
|
||||||
/// Whether the table should be allowed to overflow.
|
/// Whether the table should be allowed to overflow.
|
||||||
pub overflow: bool,
|
pub overflow: bool,
|
||||||
/// The maximum width and height.
|
/// The maximum width and height.
|
||||||
pub max: Max,
|
pub max: Max,
|
||||||
|
/// Horizontal spacing between table cells.
|
||||||
|
pub spacing: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TableOptions {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
overflow: false,
|
||||||
|
max: Max::default(),
|
||||||
|
spacing: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Table<const W: usize> {
|
pub struct Table<const W: usize, T> {
|
||||||
rows: Vec<[String; W]>,
|
rows: Vec<[T; W]>,
|
||||||
widths: [usize; W],
|
widths: [usize; W],
|
||||||
|
width: usize,
|
||||||
opts: TableOptions,
|
opts: TableOptions,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<const W: usize> Default for Table<W> {
|
impl<const W: usize, T> Default for Table<W, T> {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
rows: Vec::new(),
|
rows: Vec::new(),
|
||||||
widths: [0; W],
|
widths: [0; W],
|
||||||
|
width: 0,
|
||||||
opts: TableOptions::default(),
|
opts: TableOptions::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<const W: usize> Table<W> {
|
impl<const W: usize, T: Cell + fmt::Debug> Element for Table<W, T>
|
||||||
|
where
|
||||||
|
T::Padded: Into<Label>,
|
||||||
|
{
|
||||||
|
fn size(&self) -> Size {
|
||||||
|
Table::size(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&self) -> Vec<Line> {
|
||||||
|
let mut lines = Vec::new();
|
||||||
|
let width = self.opts.max.width.or_else(term::columns);
|
||||||
|
|
||||||
|
for row in &self.rows {
|
||||||
|
let mut line = Line::default();
|
||||||
|
|
||||||
|
for (i, cell) in row.iter().enumerate() {
|
||||||
|
let pad = if i == row.len() - 1 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
self.widths[i] + self.opts.spacing
|
||||||
|
};
|
||||||
|
line.push(cell.pad(pad));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(width) = width {
|
||||||
|
line.truncate(width, "…");
|
||||||
|
};
|
||||||
|
lines.push(line);
|
||||||
|
}
|
||||||
|
lines
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const W: usize, T: Cell> Table<W, T> {
|
||||||
pub fn new(opts: TableOptions) -> Self {
|
pub fn new(opts: TableOptions) -> Self {
|
||||||
Self {
|
Self {
|
||||||
rows: Vec::new(),
|
rows: Vec::new(),
|
||||||
widths: [0; W],
|
widths: [0; W],
|
||||||
|
width: 0,
|
||||||
opts,
|
opts,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn push(&mut self, row: [impl Cell; W]) {
|
pub fn size(&self) -> Size {
|
||||||
let row = row.map(|s| s.to_string());
|
Size::new(self.width, self.rows.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push(&mut self, row: [T; W]) {
|
||||||
for (i, cell) in row.iter().enumerate() {
|
for (i, cell) in row.iter().enumerate() {
|
||||||
self.widths[i] = self.widths[i].max(cell.width());
|
self.widths[i] = self.widths[i].max(cell.width());
|
||||||
}
|
}
|
||||||
|
self.width =
|
||||||
|
self.width.max(row.iter().map(Cell::width).sum()) + (W - 1) * self.opts.spacing;
|
||||||
self.rows.push(row);
|
self.rows.push(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render(self) {
|
|
||||||
self.write(io::stdout()).ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn write<T: io::Write>(self, mut writer: T) -> io::Result<()> {
|
|
||||||
let width = self.opts.max.width.or_else(term::columns);
|
|
||||||
|
|
||||||
for row in &self.rows {
|
|
||||||
let mut output = String::new();
|
|
||||||
let cells = row.len();
|
|
||||||
|
|
||||||
for (i, cell) in row.iter().enumerate() {
|
|
||||||
if i == cells - 1 || self.opts.overflow {
|
|
||||||
output.push_str(cell.to_string().as_str());
|
|
||||||
} else {
|
|
||||||
output.push_str(cell.pad(self.widths[i]).as_str());
|
|
||||||
output.push(' ');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let output = output.trim_end();
|
|
||||||
writeln!(
|
|
||||||
writer,
|
|
||||||
"{}",
|
|
||||||
if let Some(width) = width {
|
|
||||||
output.truncate(width, "…")
|
|
||||||
} else {
|
|
||||||
output.into()
|
|
||||||
}
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn render_tree(self) {
|
|
||||||
for (r, row) in self.rows.iter().enumerate() {
|
|
||||||
if r != self.rows.len() - 1 {
|
|
||||||
print!("├── ");
|
|
||||||
} else {
|
|
||||||
print!("└── ");
|
|
||||||
}
|
|
||||||
for (i, cell) in row.iter().enumerate() {
|
|
||||||
print!("{} ", cell.pad(self.widths[i]));
|
|
||||||
}
|
|
||||||
println!();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
|
use crate::Element;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
|
@ -139,16 +149,14 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_table() {
|
fn test_table() {
|
||||||
let mut s = Vec::new();
|
|
||||||
let mut t = Table::new(TableOptions::default());
|
let mut t = Table::new(TableOptions::default());
|
||||||
|
|
||||||
t.push(["pineapple", "rosemary"]);
|
t.push(["pineapple", "rosemary"]);
|
||||||
t.push(["apples", "pears"]);
|
t.push(["apples", "pears"]);
|
||||||
t.write(&mut s).unwrap();
|
|
||||||
|
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
String::from_utf8_lossy(&s),
|
t.display(),
|
||||||
[
|
[
|
||||||
"pineapple rosemary\n",
|
"pineapple rosemary\n",
|
||||||
"apples pears\n"
|
"apples pears\n"
|
||||||
|
|
@ -158,7 +166,6 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_table_truncate() {
|
fn test_table_truncate() {
|
||||||
let mut s = Vec::new();
|
|
||||||
let mut t = Table::new(TableOptions {
|
let mut t = Table::new(TableOptions {
|
||||||
max: Max {
|
max: Max {
|
||||||
width: Some(16),
|
width: Some(16),
|
||||||
|
|
@ -169,11 +176,10 @@ mod test {
|
||||||
|
|
||||||
t.push(["pineapple", "rosemary"]);
|
t.push(["pineapple", "rosemary"]);
|
||||||
t.push(["apples", "pears"]);
|
t.push(["apples", "pears"]);
|
||||||
t.write(&mut s).unwrap();
|
|
||||||
|
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
String::from_utf8_lossy(&s),
|
t.display(),
|
||||||
[
|
[
|
||||||
"pineapple rosem…\n",
|
"pineapple rosem…\n",
|
||||||
"apples pears\n"
|
"apples pears\n"
|
||||||
|
|
@ -183,16 +189,14 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_table_unicode() {
|
fn test_table_unicode() {
|
||||||
let mut s = Vec::new();
|
|
||||||
let mut t = Table::new(TableOptions::default());
|
let mut t = Table::new(TableOptions::default());
|
||||||
|
|
||||||
t.push(["🍍pineapple", "__rosemary", "__sage"]);
|
t.push(["🍍pineapple", "__rosemary", "__sage"]);
|
||||||
t.push(["__pears", "🍎apples", "🍌bananas"]);
|
t.push(["__pears", "🍎apples", "🍌bananas"]);
|
||||||
t.write(&mut s).unwrap();
|
|
||||||
|
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
String::from_utf8_lossy(&s),
|
t.display(),
|
||||||
[
|
[
|
||||||
"🍍pineapple __rosemary __sage\n",
|
"🍍pineapple __rosemary __sage\n",
|
||||||
"__pears 🍎apples 🍌bananas\n"
|
"__pears 🍎apples 🍌bananas\n"
|
||||||
|
|
@ -202,7 +206,6 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_table_unicode_truncate() {
|
fn test_table_unicode_truncate() {
|
||||||
let mut s = Vec::new();
|
|
||||||
let mut t = Table::new(TableOptions {
|
let mut t = Table::new(TableOptions {
|
||||||
max: Max {
|
max: Max {
|
||||||
width: Some(16),
|
width: Some(16),
|
||||||
|
|
@ -213,11 +216,10 @@ mod test {
|
||||||
|
|
||||||
t.push(["🍍pineapple", "__rosemary"]);
|
t.push(["🍍pineapple", "__rosemary"]);
|
||||||
t.push(["__pears", "🍎apples"]);
|
t.push(["__pears", "🍎apples"]);
|
||||||
t.write(&mut s).unwrap();
|
|
||||||
|
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
String::from_utf8_lossy(&s),
|
t.display(),
|
||||||
[
|
[
|
||||||
"🍍pineapple __r…\n",
|
"🍍pineapple __r…\n",
|
||||||
"__pears 🍎a…\n"
|
"__pears 🍎a…\n"
|
||||||
|
|
|
||||||
|
|
@ -1,67 +0,0 @@
|
||||||
use std::fmt;
|
|
||||||
|
|
||||||
use crate as term;
|
|
||||||
use crate::cell::Cell as _;
|
|
||||||
|
|
||||||
pub struct TextBox {
|
|
||||||
pub body: String,
|
|
||||||
first: bool,
|
|
||||||
last: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TextBox {
|
|
||||||
pub fn new(body: String) -> Self {
|
|
||||||
Self {
|
|
||||||
body,
|
|
||||||
first: true,
|
|
||||||
last: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Is this text box the last one in the list?
|
|
||||||
pub fn last(mut self, connect: bool) -> Self {
|
|
||||||
self.last = connect;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Is this text box the first one in the list?
|
|
||||||
pub fn first(mut self, connect: bool) -> Self {
|
|
||||||
self.first = connect;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for TextBox {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
let mut width = self.body.lines().map(|l| l.width()).max().unwrap_or(0) + 2;
|
|
||||||
if let Some(max) = term::columns() {
|
|
||||||
if width + 2 > max {
|
|
||||||
width = max - 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let (connector, header_width) = if !self.first {
|
|
||||||
("┴", width - 1)
|
|
||||||
} else {
|
|
||||||
("", width)
|
|
||||||
};
|
|
||||||
writeln!(f, "┌{}{}┐", connector, "─".repeat(header_width))?;
|
|
||||||
|
|
||||||
for l in self.body.lines() {
|
|
||||||
writeln!(f, "│ {}│", l.pad(width - 1))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let (connector, footer_width) = if !self.last {
|
|
||||||
("┬", width - 1)
|
|
||||||
} else {
|
|
||||||
("", width)
|
|
||||||
};
|
|
||||||
|
|
||||||
writeln!(f, "└{}{}┘", connector, "─".repeat(footer_width))?;
|
|
||||||
|
|
||||||
if !self.last {
|
|
||||||
writeln!(f, " │")?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
use crate::{Color, Element, Label, Line, Paint, Size};
|
||||||
|
|
||||||
|
/// Options for [`VStack`].
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
pub struct VStackOptions {
|
||||||
|
border: Option<Color>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Vertical stack of [`Element`] objects that implements [`Element`].
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
pub struct VStack<'a> {
|
||||||
|
elems: Vec<Box<dyn Element + 'a>>,
|
||||||
|
opts: VStackOptions,
|
||||||
|
width: usize,
|
||||||
|
height: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> VStack<'a> {
|
||||||
|
/// Add an element to the stack.
|
||||||
|
pub fn child(mut self, child: impl Element + 'a) -> Self {
|
||||||
|
self.width = self.width.max(child.columns());
|
||||||
|
self.height += child.rows();
|
||||||
|
self.elems.push(Box::new(child));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a blank line to the stack.
|
||||||
|
pub fn blank(self) -> Self {
|
||||||
|
self.child(Label::blank())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add multiple elements to the stack.
|
||||||
|
pub fn children<E: Element + 'a>(self, children: impl IntoIterator<Item = E>) -> Self {
|
||||||
|
let mut vstack = self;
|
||||||
|
|
||||||
|
for child in children.into_iter() {
|
||||||
|
vstack = vstack.child(child);
|
||||||
|
}
|
||||||
|
vstack
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set or unset the outer border.
|
||||||
|
pub fn border(mut self, color: Option<Color>) -> Self {
|
||||||
|
self.opts.border = color;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Element for VStack<'a> {
|
||||||
|
fn size(&self) -> Size {
|
||||||
|
if self.opts.border.is_some() {
|
||||||
|
Size::new(self.width + 4, self.height + 2)
|
||||||
|
} else {
|
||||||
|
Size::new(self.width, self.height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&self) -> Vec<Line> {
|
||||||
|
let mut lines = Vec::new();
|
||||||
|
|
||||||
|
if let Some(color) = self.opts.border {
|
||||||
|
lines.push(
|
||||||
|
Line::default()
|
||||||
|
.item(Paint::new("╭").fg(color))
|
||||||
|
.item(Paint::new("─".repeat(self.width + 2)).fg(color))
|
||||||
|
.item(Paint::new("╮").fg(color)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for elem in &self.elems {
|
||||||
|
for mut line in elem.render() {
|
||||||
|
if let Some(color) = self.opts.border {
|
||||||
|
line.pad(self.width);
|
||||||
|
lines.push(
|
||||||
|
Line::default()
|
||||||
|
.item(Paint::new("│ ").fg(color))
|
||||||
|
.extend(line)
|
||||||
|
.item(Paint::new(" │").fg(color)),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
lines.push(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(color) = self.opts.border {
|
||||||
|
lines.push(
|
||||||
|
Line::default()
|
||||||
|
.item(Paint::new("╰").fg(color))
|
||||||
|
.item(Paint::new("─".repeat(self.width + 2)).fg(color))
|
||||||
|
.item(Paint::new("╯").fg(color)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
lines.into_iter().flat_map(|h| h.render()).collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -411,6 +411,16 @@ pub enum State {
|
||||||
Archived,
|
Archived,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for State {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Proposed => write!(f, "open"),
|
||||||
|
Self::Draft => write!(f, "draft"),
|
||||||
|
Self::Archived => write!(f, "archived"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A merged patch revision.
|
/// A merged patch revision.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue