cli: Simplify & improve CLI output
Improvements to the CLI output as well as converging towards `termion` as a backend. The crates we were using for prompting and spinners were based on a terminal backend called `console`, which is not really used anywhere else. There were also limitations in terms of the output of these crates. Therefore, we switched to the `inquire` crate for prompting, using the `termion` backend, which is very standard. Additionally, we simplify the terminal output to use symbols: ! for Warning ✓ for Success ✗ for Error ... Instead of a mix of words and symbols. We also include a modified version of the `yansi` crate which plays well with our table formatting functions by automatically truncating and padding output in a unicode-aware way.
This commit is contained in:
parent
11da344e3a
commit
0a93fa833e
|
|
@ -402,19 +402,6 @@ version = "0.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "82a90734b3d5dcf656e7624cca6bce9c3a90ee11f900e80141a7427ccfb3d317"
|
checksum = "82a90734b3d5dcf656e7624cca6bce9c3a90ee11f900e80141a7427ccfb3d317"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "console"
|
|
||||||
version = "0.15.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c3d79fbe8970a77e3e34151cc13d3b3e248aa0faaecb9f6091fa07ebefe5ad60"
|
|
||||||
dependencies = [
|
|
||||||
"encode_unicode",
|
|
||||||
"lazy_static",
|
|
||||||
"libc",
|
|
||||||
"unicode-width",
|
|
||||||
"windows-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "const-oid"
|
name = "const-oid"
|
||||||
version = "0.9.1"
|
version = "0.9.1"
|
||||||
|
|
@ -643,18 +630,6 @@ dependencies = [
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dialoguer"
|
|
||||||
version = "0.10.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "af3c796f3b0b408d9fd581611b47fa850821fcb84aa640b83a3c1a5be2d691f2"
|
|
||||||
dependencies = [
|
|
||||||
"console",
|
|
||||||
"shell-words",
|
|
||||||
"tempfile",
|
|
||||||
"zeroize",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "diff"
|
name = "diff"
|
||||||
version = "0.1.13"
|
version = "0.1.13"
|
||||||
|
|
@ -682,6 +657,12 @@ dependencies = [
|
||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dyn-clone"
|
||||||
|
version = "1.0.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c9b0705efd4599c15a38151f4721f7bc388306f61084d3bfd50bd07fbca5cb60"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ec25519"
|
name = "ec25519"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
@ -757,12 +738,6 @@ dependencies = [
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "encode_unicode"
|
|
||||||
version = "0.3.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "errno"
|
name = "errno"
|
||||||
version = "0.2.8"
|
version = "0.2.8"
|
||||||
|
|
@ -1173,18 +1148,6 @@ dependencies = [
|
||||||
"hashbrown",
|
"hashbrown",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "indicatif"
|
|
||||||
version = "0.16.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2d207dc617c7a380ab07ff572a6e52fa202a2a8f355860ac9c38e23f8196be1b"
|
|
||||||
dependencies = [
|
|
||||||
"console",
|
|
||||||
"lazy_static",
|
|
||||||
"number_prefix",
|
|
||||||
"regex",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "inout"
|
name = "inout"
|
||||||
version = "0.1.3"
|
version = "0.1.3"
|
||||||
|
|
@ -1194,6 +1157,23 @@ dependencies = [
|
||||||
"generic-array",
|
"generic-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "inquire"
|
||||||
|
version = "0.5.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f3a94f0659efe59329832ba0452d3ec753145fc1fb12a8e1d60de4ccf99f5364"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"dyn-clone",
|
||||||
|
"lazy_static",
|
||||||
|
"newline-converter",
|
||||||
|
"tempfile",
|
||||||
|
"termion 1.5.6",
|
||||||
|
"thiserror",
|
||||||
|
"unicode-segmentation",
|
||||||
|
"unicode-width",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "instant"
|
name = "instant"
|
||||||
version = "0.1.12"
|
version = "0.1.12"
|
||||||
|
|
@ -1447,6 +1427,15 @@ dependencies = [
|
||||||
"socket2",
|
"socket2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "newline-converter"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1f71d09d5c87634207f894c6b31b6a2b2c64ea3bdcf71bd5599fdbbe1600c00f"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-segmentation",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "noise-framework"
|
name = "noise-framework"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
|
@ -1558,10 +1547,10 @@ dependencies = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "number_prefix"
|
name = "numtoa"
|
||||||
version = "0.4.0"
|
version = "0.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
|
checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
|
|
@ -1833,12 +1822,12 @@ version = "0.8.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
"console",
|
"concolor",
|
||||||
"dialoguer",
|
"inquire",
|
||||||
"indicatif",
|
|
||||||
"json-color",
|
"json-color",
|
||||||
"lexopt",
|
"lexopt",
|
||||||
"log",
|
"log",
|
||||||
|
"once_cell",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
"radicle",
|
"radicle",
|
||||||
"radicle-cli-test",
|
"radicle-cli-test",
|
||||||
|
|
@ -1850,8 +1839,11 @@ dependencies = [
|
||||||
"serde_yaml",
|
"serde_yaml",
|
||||||
"similar",
|
"similar",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
|
"termion 2.0.1",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"timeago",
|
"timeago",
|
||||||
|
"unicode-segmentation",
|
||||||
|
"unicode-width",
|
||||||
"ureq",
|
"ureq",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
@ -2064,6 +2056,7 @@ dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"git-ref-format",
|
"git-ref-format",
|
||||||
"radicle",
|
"radicle",
|
||||||
|
"radicle-cli",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2146,6 +2139,15 @@ dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_termios"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8440d8acb4fd3d277125b4bd01a6f38aee8d814b3b5fc09b3f2b825d37d3fe8f"
|
||||||
|
dependencies = [
|
||||||
|
"redox_syscall",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.7.1"
|
version = "1.7.1"
|
||||||
|
|
@ -2397,12 +2399,6 @@ dependencies = [
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "shell-words"
|
|
||||||
version = "1.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shlex"
|
name = "shlex"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
|
|
@ -2632,6 +2628,30 @@ dependencies = [
|
||||||
"winapi-util",
|
"winapi-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "termion"
|
||||||
|
version = "1.5.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "077185e2eac69c3f8379a4298e1e07cd36beb962290d4a51199acf0fdc10607e"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"numtoa",
|
||||||
|
"redox_syscall",
|
||||||
|
"redox_termios",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "termion"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "659c1f379f3408c7e5e84c7d0da6d93404e3800b6b9d063ba24436419302ec90"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"numtoa",
|
||||||
|
"redox_syscall",
|
||||||
|
"redox_termios",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.38"
|
version = "1.0.38"
|
||||||
|
|
@ -2919,6 +2939,12 @@ dependencies = [
|
||||||
"tinyvec",
|
"tinyvec",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-segmentation"
|
||||||
|
version = "1.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-width"
|
name = "unicode-width"
|
||||||
version = "0.1.10"
|
version = "0.1.10"
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ use snapbox::{Assert, Substitutions};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
/// Error lines in the CLI are prefixed with this string.
|
/// Error lines in the CLI are prefixed with this string.
|
||||||
const ERROR_PREFIX: &str = "==";
|
const ERROR_PREFIX: &str = "✗";
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
|
|
|
||||||
|
|
@ -13,18 +13,21 @@ path = "src/main.rs"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = { version = "1" }
|
anyhow = { version = "1" }
|
||||||
chrono = { version = "0.4", default-features = false, features = ["clock", "std"] }
|
chrono = { version = "0.4", default-features = false, features = ["clock", "std"] }
|
||||||
console = { version = "0.15" }
|
concolor = { version = "0", features = ["api_unstable"] }
|
||||||
dialoguer = { version = "0.10.0" }
|
inquire = { version = "0.5.3", default-features = false, features = ["termion", "editor"] }
|
||||||
indicatif = { version = "0.16.2" }
|
|
||||||
json-color = { version = "0.7" }
|
json-color = { version = "0.7" }
|
||||||
lexopt = { version = "0.2" }
|
lexopt = { version = "0.2" }
|
||||||
log = { version = "0.4", features = ["std"] }
|
log = { version = "0.4", features = ["std"] }
|
||||||
|
once_cell = { version = "1.13" }
|
||||||
serde = { version = "1.0" }
|
serde = { version = "1.0" }
|
||||||
serde_json = { version = "1" }
|
serde_json = { version = "1" }
|
||||||
serde_yaml = { version = "0.8" }
|
serde_yaml = { version = "0.8" }
|
||||||
similar = { version = "2.2.1" }
|
similar = { version = "2.2.1" }
|
||||||
|
termion = { version = "2" }
|
||||||
thiserror = { version = "1" }
|
thiserror = { version = "1" }
|
||||||
timeago = { version = "0.3", default-features = false }
|
timeago = { version = "0.3", default-features = false }
|
||||||
|
unicode-width = { version = "0.1.10", default-features = false }
|
||||||
|
unicode-segmentation = { version = "1.7.1" }
|
||||||
ureq = { version = "2.6.1", default-features = false, features = ["json"] }
|
ureq = { version = "2.6.1", default-features = false, features = ["json"] }
|
||||||
zeroize = { version = "1.1" }
|
zeroize = { version = "1.1" }
|
||||||
|
|
||||||
|
|
@ -46,6 +49,6 @@ path = "../radicle-crypto"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
pretty_assertions = { version = "1.3.0" }
|
pretty_assertions = { version = "1.3.0" }
|
||||||
tempfile = { version = "3.3.0" }
|
|
||||||
radicle = { path = "../radicle", features = ["test"] }
|
radicle = { path = "../radicle", features = ["test"] }
|
||||||
radicle-node = { path = "../radicle-node", features = ["test"] }
|
radicle-node = { path = "../radicle-node", features = ["test"] }
|
||||||
|
tempfile = { version = "3.3.0" }
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,10 @@ $ rad auth
|
||||||
|
|
||||||
Initializing your 🌱 profile and identity
|
Initializing your 🌱 profile and identity
|
||||||
|
|
||||||
ok Creating your 🌱 Ed25519 keypair...
|
✓ Creating your 🌱 Ed25519 keypair...
|
||||||
ok Profile did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi created.
|
✓ Your Radicle ID is did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi. This identifies your device.
|
||||||
|
|
||||||
Your radicle Node ID is z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi. This identifies your device.
|
👉 To create a radicle project, run `rad init` from a git repository.
|
||||||
|
|
||||||
=> To create a radicle project, run `rad init` from a git repository.
|
|
||||||
```
|
```
|
||||||
|
|
||||||
You can get the above information at all times using the `self` command:
|
You can get the above information at all times using the `self` command:
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ $ rad checkout rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
|
||||||
|
|
||||||
Initializing local checkout for 🌱 rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji (heartwood)
|
Initializing local checkout for 🌱 rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji (heartwood)
|
||||||
|
|
||||||
ok Performing checkout...
|
✓ Performing checkout...
|
||||||
|
|
||||||
🌱 Project checkout successful under ./heartwood
|
🌱 Project checkout successful under ./heartwood
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,6 @@ Trying to clone a repository that is not in our routing table returns an error:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ rad clone rad:zVNuptPuk5XauitpCWSNVCXGGfXW
|
$ rad clone rad:zVNuptPuk5XauitpCWSNVCXGGfXW
|
||||||
ok Tracking relationship established for rad:zVNuptPuk5XauitpCWSNVCXGGfXW
|
✓ Tracking relationship established for rad:zVNuptPuk5XauitpCWSNVCXGGfXW
|
||||||
== Clone failed no seeds found for rad:zVNuptPuk5XauitpCWSNVCXGGfXW
|
✗ Clone failed: no seeds found for rad:zVNuptPuk5XauitpCWSNVCXGGfXW
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,12 @@ To create a local copy of a repository on the radicle network, we use the
|
||||||
|
|
||||||
```
|
```
|
||||||
$ rad clone rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
|
$ rad clone rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
|
||||||
ok Tracking relationship established for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
|
✓ Tracking relationship established for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
|
||||||
ok Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MknSL…StBU8Vi..
|
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MknSL…StBU8Vi..
|
||||||
ok Forking under z6Mkt67…v4N1tRk..
|
✓ Forking under z6Mkt67…v4N1tRk..
|
||||||
ok Creating checkout in ./heartwood..
|
✓ Creating checkout in ./heartwood..
|
||||||
ok Remote z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi created
|
✓ Remote z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi created
|
||||||
ok Remote-tracking branch z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master created for z6MknSL…StBU8Vi
|
✓ Remote-tracking branch z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master created for z6MknSL…StBU8Vi
|
||||||
|
|
||||||
🌱 Project successfully cloned under [..]/heartwood/
|
🌱 Project successfully cloned under [..]/heartwood/
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ work.
|
||||||
```
|
```
|
||||||
$ rad delegate add did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG --to rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
|
$ rad delegate add did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG --to rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
|
||||||
Added delegate 'did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG'
|
Added delegate 'did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG'
|
||||||
ok Update successful!
|
✓ Update successful!
|
||||||
```
|
```
|
||||||
|
|
||||||
Let's convince ourselves that there's another delegate.
|
Let's convince ourselves that there's another delegate.
|
||||||
|
|
@ -35,7 +35,7 @@ the torch and remove ourselves from the delegate set.
|
||||||
```
|
```
|
||||||
$ rad delegate remove did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi --to rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
|
$ rad delegate remove did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi --to rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
|
||||||
Removed delegate 'did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi'
|
Removed delegate 'did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi'
|
||||||
ok Update successful!
|
✓ Update successful!
|
||||||
```
|
```
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,10 @@ delegates creating proposals concurrently.
|
||||||
|
|
||||||
```
|
```
|
||||||
$ rad id edit --title "Add Alice" --description "Add Alice as a delegate" --delegates did:key:z6MkedTZGJGqgQ2py2b8kGecfxdt2yRdHWF6JpaZC47fovFn --no-confirm
|
$ rad id edit --title "Add Alice" --description "Add Alice as a delegate" --delegates did:key:z6MkedTZGJGqgQ2py2b8kGecfxdt2yRdHWF6JpaZC47fovFn --no-confirm
|
||||||
ok Identity proposal '57332790a2eabc0b2fd8c7ff48c3579d5812d405' created 🌱
|
✓ Identity proposal '57332790a2eabc0b2fd8c7ff48c3579d5812d405' created 🌱
|
||||||
title: Add Alice
|
title: Add Alice
|
||||||
description: Add Alice as a delegate
|
description: Add Alice as a delegate
|
||||||
status: open
|
status: ❲open❳
|
||||||
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
|
|
||||||
Document Diff
|
Document Diff
|
||||||
|
|
@ -43,15 +43,15 @@ keys: []
|
||||||
|
|
||||||
Quorum Reached
|
Quorum Reached
|
||||||
|
|
||||||
✗ no
|
👎 no
|
||||||
```
|
```
|
||||||
|
|
||||||
```
|
```
|
||||||
$ rad id edit --title "Add Bob" --description "Add Bob as a delegate" --delegates did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG --no-confirm
|
$ rad id edit --title "Add Bob" --description "Add Bob as a delegate" --delegates did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG --no-confirm
|
||||||
ok Identity proposal 'c3698d4e85f9d4c0ee536b34d6122fc7c81f7e2e' created 🌱
|
✓ Identity proposal 'c3698d4e85f9d4c0ee536b34d6122fc7c81f7e2e' created 🌱
|
||||||
title: Add Bob
|
title: Add Bob
|
||||||
description: Add Bob as a delegate
|
description: Add Bob as a delegate
|
||||||
status: open
|
status: ❲open❳
|
||||||
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
|
|
||||||
Document Diff
|
Document Diff
|
||||||
|
|
@ -85,7 +85,7 @@ keys: []
|
||||||
|
|
||||||
Quorum Reached
|
Quorum Reached
|
||||||
|
|
||||||
✗ no
|
👎 no
|
||||||
```
|
```
|
||||||
|
|
||||||
Now, if the first proposal was accepted and committed before the
|
Now, if the first proposal was accepted and committed before the
|
||||||
|
|
@ -94,10 +94,10 @@ through that and see what happens.
|
||||||
|
|
||||||
```
|
```
|
||||||
$ rad id accept 57332790a2eabc0b2fd8c7ff48c3579d5812d405 --rev z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/1 --no-confirm
|
$ rad id accept 57332790a2eabc0b2fd8c7ff48c3579d5812d405 --rev z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/1 --no-confirm
|
||||||
ok Accepted proposal ✓
|
✓ Accepted proposal ✓
|
||||||
title: Add Alice
|
title: Add Alice
|
||||||
description: Add Alice as a delegate
|
description: Add Alice as a delegate
|
||||||
status: open
|
status: ❲open❳
|
||||||
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
|
|
||||||
Document Diff
|
Document Diff
|
||||||
|
|
@ -133,15 +133,15 @@ keys: []
|
||||||
|
|
||||||
Quorum Reached
|
Quorum Reached
|
||||||
|
|
||||||
✓ yes
|
👍 yes
|
||||||
```
|
```
|
||||||
|
|
||||||
```
|
```
|
||||||
$ rad id commit 57332790a2eabc0b2fd8c7ff48c3579d5812d405 --rev z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/1 --no-confirm
|
$ rad id commit 57332790a2eabc0b2fd8c7ff48c3579d5812d405 --rev z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/1 --no-confirm
|
||||||
ok Committed new identity '29ae4b72f5a315328f06fbd68dc1c396a2d5c45e' 🌱
|
✓ Committed new identity '29ae4b72f5a315328f06fbd68dc1c396a2d5c45e' 🌱
|
||||||
title: Add Alice
|
title: Add Alice
|
||||||
description: Add Alice as a delegate
|
description: Add Alice as a delegate
|
||||||
status: committed
|
status: ❲committed❳
|
||||||
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
|
|
||||||
Document Diff
|
Document Diff
|
||||||
|
|
@ -177,20 +177,20 @@ keys: []
|
||||||
|
|
||||||
Quorum Reached
|
Quorum Reached
|
||||||
|
|
||||||
✓ yes
|
👍 yes
|
||||||
```
|
```
|
||||||
|
|
||||||
Now, when we go to accept the second proposal:
|
Now, when we go to accept the second proposal:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ rad id accept c3698d4e85f9d4c0ee536b34d6122fc7c81f7e2e --rev z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/1 --no-confirm
|
$ rad id accept c3698d4e85f9d4c0ee536b34d6122fc7c81f7e2e --rev z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/1 --no-confirm
|
||||||
** Warning: Revision is out of date
|
! Warning: Revision is out of date
|
||||||
** Warning: d96f425412c9f8ad5d9a9a05c9831d0728e2338d =/= 475cdfbc8662853dd132ec564e4f5eb0f152dd7f
|
! Warning: d96f425412c9f8ad5d9a9a05c9831d0728e2338d =/= 475cdfbc8662853dd132ec564e4f5eb0f152dd7f
|
||||||
=> Consider using 'rad id rebase' to update the proposal to the latest identity
|
👉 Consider using 'rad id rebase' to update the proposal to the latest identity
|
||||||
ok Accepted proposal ✓
|
✓ Accepted proposal ✓
|
||||||
title: Add Bob
|
title: Add Bob
|
||||||
description: Add Bob as a delegate
|
description: Add Bob as a delegate
|
||||||
status: open
|
status: ❲open❳
|
||||||
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
|
|
||||||
Document Diff
|
Document Diff
|
||||||
|
|
@ -226,7 +226,7 @@ keys: []
|
||||||
|
|
||||||
Quorum Reached
|
Quorum Reached
|
||||||
|
|
||||||
✓ yes
|
👍 yes
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that a warning was emitted:
|
Note that a warning was emitted:
|
||||||
|
|
@ -239,22 +239,21 @@ If we attempt to commit this revision, the command will fail:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ rad id commit c3698d4e85f9d4c0ee536b34d6122fc7c81f7e2e --rev z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/1 --no-confirm
|
$ rad id commit c3698d4e85f9d4c0ee536b34d6122fc7c81f7e2e --rev z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/1 --no-confirm
|
||||||
** Warning: Revision is out of date
|
! Warning: Revision is out of date
|
||||||
** Warning: d96f425412c9f8ad5d9a9a05c9831d0728e2338d =/= 475cdfbc8662853dd132ec564e4f5eb0f152dd7f
|
! Warning: d96f425412c9f8ad5d9a9a05c9831d0728e2338d =/= 475cdfbc8662853dd132ec564e4f5eb0f152dd7f
|
||||||
=> Consider using 'rad id rebase' to update the proposal to the latest identity
|
👉 Consider using 'rad id rebase' to update the proposal to the latest identity
|
||||||
== Id failed
|
✗ Id failed: the identity hashes do match 'd96f425412c9f8ad5d9a9a05c9831d0728e2338d =/= 475cdfbc8662853dd132ec564e4f5eb0f152dd7f' for the revision 'z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/1'
|
||||||
the identity hashes do match 'd96f425412c9f8ad5d9a9a05c9831d0728e2338d =/= 475cdfbc8662853dd132ec564e4f5eb0f152dd7f' for the revision 'z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/1'
|
|
||||||
```
|
```
|
||||||
|
|
||||||
So, let's fix this by running a rebase on the proposal's revision:
|
So, let's fix this by running a rebase on the proposal's revision:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ rad id rebase c3698d4e85f9d4c0ee536b34d6122fc7c81f7e2e --rev z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/1 --no-confirm
|
$ rad id rebase c3698d4e85f9d4c0ee536b34d6122fc7c81f7e2e --rev z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/1 --no-confirm
|
||||||
ok Identity proposal 'c3698d4e85f9d4c0ee536b34d6122fc7c81f7e2e' rebased 🌱
|
✓ Identity proposal 'c3698d4e85f9d4c0ee536b34d6122fc7c81f7e2e' rebased 🌱
|
||||||
ok Revision 'z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/4'
|
✓ Revision 'z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/4'
|
||||||
title: Add Bob
|
title: Add Bob
|
||||||
description: Add Bob as a delegate
|
description: Add Bob as a delegate
|
||||||
status: open
|
status: ❲open❳
|
||||||
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
|
|
||||||
Document Diff
|
Document Diff
|
||||||
|
|
@ -288,18 +287,18 @@ keys: []
|
||||||
|
|
||||||
Quorum Reached
|
Quorum Reached
|
||||||
|
|
||||||
✗ no
|
👎 no
|
||||||
```
|
```
|
||||||
|
|
||||||
We can now update the proposal to have both keys in the delegates set:
|
We can now update the proposal to have both keys in the delegates set:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ rad id update c3698d4e85f9d4c0ee536b34d6122fc7c81f7e2e --rev z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/4 --delegates did:key:z6MkedTZGJGqgQ2py2b8kGecfxdt2yRdHWF6JpaZC47fovFn --no-confirm
|
$ rad id update c3698d4e85f9d4c0ee536b34d6122fc7c81f7e2e --rev z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/4 --delegates did:key:z6MkedTZGJGqgQ2py2b8kGecfxdt2yRdHWF6JpaZC47fovFn --no-confirm
|
||||||
ok Identity proposal 'c3698d4e85f9d4c0ee536b34d6122fc7c81f7e2e' updated 🌱
|
✓ Identity proposal 'c3698d4e85f9d4c0ee536b34d6122fc7c81f7e2e' updated 🌱
|
||||||
ok Revision 'z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/6'
|
✓ Revision 'z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/6'
|
||||||
title: Add Bob
|
title: Add Bob
|
||||||
description: Add Bob as a delegate
|
description: Add Bob as a delegate
|
||||||
status: open
|
status: ❲open❳
|
||||||
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
|
|
||||||
Document Diff
|
Document Diff
|
||||||
|
|
@ -333,7 +332,7 @@ keys: []
|
||||||
|
|
||||||
Quorum Reached
|
Quorum Reached
|
||||||
|
|
||||||
✗ no
|
👎 no
|
||||||
```
|
```
|
||||||
|
|
||||||
Finally, we can accept and commit this proposal, creating the final
|
Finally, we can accept and commit this proposal, creating the final
|
||||||
|
|
@ -343,10 +342,10 @@ $ rad id show c3698d4e85f9d4c0ee536b34d6122fc7c81f7e2e --revisions
|
||||||
|
|
||||||
```
|
```
|
||||||
$ rad id accept c3698d4e85f9d4c0ee536b34d6122fc7c81f7e2e --rev z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/6 --no-confirm
|
$ rad id accept c3698d4e85f9d4c0ee536b34d6122fc7c81f7e2e --rev z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/6 --no-confirm
|
||||||
ok Accepted proposal ✓
|
✓ Accepted proposal ✓
|
||||||
title: Add Bob
|
title: Add Bob
|
||||||
description: Add Bob as a delegate
|
description: Add Bob as a delegate
|
||||||
status: open
|
status: ❲open❳
|
||||||
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
|
|
||||||
Document Diff
|
Document Diff
|
||||||
|
|
@ -382,15 +381,15 @@ keys: []
|
||||||
|
|
||||||
Quorum Reached
|
Quorum Reached
|
||||||
|
|
||||||
✓ yes
|
👍 yes
|
||||||
```
|
```
|
||||||
|
|
||||||
```
|
```
|
||||||
$ rad id commit c3698d4e85f9d4c0ee536b34d6122fc7c81f7e2e --rev z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/6 --no-confirm
|
$ rad id commit c3698d4e85f9d4c0ee536b34d6122fc7c81f7e2e --rev z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/6 --no-confirm
|
||||||
ok Committed new identity '60de897bc24898f6908fd1272633c0b15aa4096f' 🌱
|
✓ Committed new identity '60de897bc24898f6908fd1272633c0b15aa4096f' 🌱
|
||||||
title: Add Bob
|
title: Add Bob
|
||||||
description: Add Bob as a delegate
|
description: Add Bob as a delegate
|
||||||
status: committed
|
status: ❲committed❳
|
||||||
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
|
|
||||||
Document Diff
|
Document Diff
|
||||||
|
|
@ -426,5 +425,5 @@ keys: []
|
||||||
|
|
||||||
Quorum Reached
|
Quorum Reached
|
||||||
|
|
||||||
✓ yes
|
👍 yes
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,10 @@ Let's add Bob as a delegate using their DID
|
||||||
|
|
||||||
```
|
```
|
||||||
$ rad id edit --title "Add Bob" --description "Add Bob as a delegate" --delegates did:key:z6MkedTZGJGqgQ2py2b8kGecfxdt2yRdHWF6JpaZC47fovFn --no-confirm
|
$ rad id edit --title "Add Bob" --description "Add Bob as a delegate" --delegates did:key:z6MkedTZGJGqgQ2py2b8kGecfxdt2yRdHWF6JpaZC47fovFn --no-confirm
|
||||||
ok Identity proposal '06d9efa2a9aad06bfdf25a25690e1ec7db2c3c39' created 🌱
|
✓ Identity proposal '06d9efa2a9aad06bfdf25a25690e1ec7db2c3c39' created 🌱
|
||||||
title: Add Bob
|
title: Add Bob
|
||||||
description: Add Bob as a delegate
|
description: Add Bob as a delegate
|
||||||
status: open
|
status: ❲open❳
|
||||||
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
|
|
||||||
Document Diff
|
Document Diff
|
||||||
|
|
@ -51,7 +51,7 @@ keys: []
|
||||||
|
|
||||||
Quorum Reached
|
Quorum Reached
|
||||||
|
|
||||||
✗ no
|
👎 no
|
||||||
```
|
```
|
||||||
|
|
||||||
Before moving on, let's take a few notes on this output. The first
|
Before moving on, let's take a few notes on this output. The first
|
||||||
|
|
@ -84,16 +84,16 @@ Finally, we can see whether the `Quorum` was reached:
|
||||||
|
|
||||||
Quorum Reached
|
Quorum Reached
|
||||||
|
|
||||||
✗ no
|
👎 no
|
||||||
|
|
||||||
Let's see what happens when we reject the change:
|
Let's see what happens when we reject the change:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ rad id reject 06d9efa2a9aad06bfdf25a25690e1ec7db2c3c39 --rev z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/1 --no-confirm
|
$ rad id reject 06d9efa2a9aad06bfdf25a25690e1ec7db2c3c39 --rev z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/1 --no-confirm
|
||||||
ok Rejected proposal ✗
|
✓ Rejected proposal 👎
|
||||||
title: Add Bob
|
title: Add Bob
|
||||||
description: Add Bob as a delegate
|
description: Add Bob as a delegate
|
||||||
status: open
|
status: ❲open❳
|
||||||
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
|
|
||||||
Document Diff
|
Document Diff
|
||||||
|
|
@ -129,7 +129,7 @@ keys: [
|
||||||
|
|
||||||
Quorum Reached
|
Quorum Reached
|
||||||
|
|
||||||
✗ no
|
👎 no
|
||||||
```
|
```
|
||||||
|
|
||||||
Our key was added to the `Rejected` set of `keys` and the `total`
|
Our key was added to the `Rejected` set of `keys` and the `total`
|
||||||
|
|
@ -146,10 +146,10 @@ Instead, let's accept the proposal:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ rad id accept 06d9efa2a9aad06bfdf25a25690e1ec7db2c3c39 --rev z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/1 --no-confirm
|
$ rad id accept 06d9efa2a9aad06bfdf25a25690e1ec7db2c3c39 --rev z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/1 --no-confirm
|
||||||
ok Accepted proposal ✓
|
✓ Accepted proposal ✓
|
||||||
title: Add Bob
|
title: Add Bob
|
||||||
description: Add Bob as a delegate
|
description: Add Bob as a delegate
|
||||||
status: open
|
status: ❲open❳
|
||||||
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
|
|
||||||
Document Diff
|
Document Diff
|
||||||
|
|
@ -185,7 +185,7 @@ keys: []
|
||||||
|
|
||||||
Quorum Reached
|
Quorum Reached
|
||||||
|
|
||||||
✓ yes
|
👍 yes
|
||||||
```
|
```
|
||||||
|
|
||||||
Our key has changed from the `Rejected` set to the `Accepted` set
|
Our key has changed from the `Rejected` set to the `Accepted` set
|
||||||
|
|
@ -202,16 +202,16 @@ As well as that, the `Quorum` has now been reached:
|
||||||
|
|
||||||
Quorum Reached
|
Quorum Reached
|
||||||
|
|
||||||
✓ yes
|
👍 yes
|
||||||
|
|
||||||
At this point, we can commit the proposal and update the identity:
|
At this point, we can commit the proposal and update the identity:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ rad id commit 06d9efa2a9aad06bfdf25a25690e1ec7db2c3c39 --rev z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/1 --no-confirm
|
$ rad id commit 06d9efa2a9aad06bfdf25a25690e1ec7db2c3c39 --rev z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/1 --no-confirm
|
||||||
ok Committed new identity 'c96e764965aaeff1c6ea3e5b97e2b9828773c8b0' 🌱
|
✓ Committed new identity 'c96e764965aaeff1c6ea3e5b97e2b9828773c8b0' 🌱
|
||||||
title: Add Bob
|
title: Add Bob
|
||||||
description: Add Bob as a delegate
|
description: Add Bob as a delegate
|
||||||
status: committed
|
status: ❲committed❳
|
||||||
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
|
|
||||||
Document Diff
|
Document Diff
|
||||||
|
|
@ -247,7 +247,7 @@ keys: []
|
||||||
|
|
||||||
Quorum Reached
|
Quorum Reached
|
||||||
|
|
||||||
✓ yes
|
👍 yes
|
||||||
```
|
```
|
||||||
|
|
||||||
Let's say we decide to also change the `threshold`, we can do so using
|
Let's say we decide to also change the `threshold`, we can do so using
|
||||||
|
|
@ -255,10 +255,10 @@ the `--threshold` option:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ rad id edit --title "Update threshold" --description "Update to safer threshold" --threshold 2 --no-confirm
|
$ rad id edit --title "Update threshold" --description "Update to safer threshold" --threshold 2 --no-confirm
|
||||||
ok Identity proposal 'dc00640d3152ea5f1df59f39f2f5983d2ad21810' created 🌱
|
✓ Identity proposal 'dc00640d3152ea5f1df59f39f2f5983d2ad21810' created 🌱
|
||||||
title: Update threshold
|
title: Update threshold
|
||||||
description: Update to safer threshold
|
description: Update to safer threshold
|
||||||
status: open
|
status: ❲open❳
|
||||||
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
|
|
||||||
Document Diff
|
Document Diff
|
||||||
|
|
@ -292,17 +292,17 @@ keys: []
|
||||||
|
|
||||||
Quorum Reached
|
Quorum Reached
|
||||||
|
|
||||||
✗ no
|
👎 no
|
||||||
```
|
```
|
||||||
|
|
||||||
But we change our minds and decide to close the proposal instead:
|
But we change our minds and decide to close the proposal instead:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ rad id close dc00640d3152ea5f1df59f39f2f5983d2ad21810 --no-confirm
|
$ rad id close dc00640d3152ea5f1df59f39f2f5983d2ad21810 --no-confirm
|
||||||
ok Closed identity proposal 'dc00640d3152ea5f1df59f39f2f5983d2ad21810'
|
✓ Closed identity proposal 'dc00640d3152ea5f1df59f39f2f5983d2ad21810'
|
||||||
title: Update threshold
|
title: Update threshold
|
||||||
description: Update to safer threshold
|
description: Update to safer threshold
|
||||||
status: closed
|
status: ❲closed❳
|
||||||
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
|
|
||||||
Document Diff
|
Document Diff
|
||||||
|
|
@ -336,7 +336,7 @@ keys: []
|
||||||
|
|
||||||
Quorum Reached
|
Quorum Reached
|
||||||
|
|
||||||
✗ no
|
👎 no
|
||||||
```
|
```
|
||||||
|
|
||||||
The proposal is now closed and cannot be committed. If at a later date
|
The proposal is now closed and cannot be committed. If at a later date
|
||||||
|
|
@ -348,8 +348,8 @@ Radicle identity, then we can use the list command:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ rad id list
|
$ rad id list
|
||||||
06d9efa2a9aad06bfdf25a25690e1ec7db2c3c39 "Add Bob" committed
|
06d9efa2a9aad06bfdf25a25690e1ec7db2c3c39 "Add Bob" ❲committed❳
|
||||||
dc00640d3152ea5f1df59f39f2f5983d2ad21810 "Update threshold" closed
|
dc00640d3152ea5f1df59f39f2f5983d2ad21810 "Update threshold" ❲closed❳
|
||||||
```
|
```
|
||||||
|
|
||||||
And if we want to view the latest state of any proposal we can use the
|
And if we want to view the latest state of any proposal we can use the
|
||||||
|
|
@ -359,7 +359,7 @@ show command:
|
||||||
$ rad id show dc00640d3152ea5f1df59f39f2f5983d2ad21810
|
$ rad id show dc00640d3152ea5f1df59f39f2f5983d2ad21810
|
||||||
title: Update threshold
|
title: Update threshold
|
||||||
description: Update to safer threshold
|
description: Update to safer threshold
|
||||||
status: closed
|
status: ❲closed❳
|
||||||
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
|
|
||||||
Document Diff
|
Document Diff
|
||||||
|
|
@ -393,7 +393,7 @@ keys: []
|
||||||
|
|
||||||
Quorum Reached
|
Quorum Reached
|
||||||
|
|
||||||
✗ no
|
👎 no
|
||||||
```
|
```
|
||||||
|
|
||||||
On a final note, these examples used `--no-confirm`. The default mode
|
On a final note, these examples used `--no-confirm`. The default mode
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,13 @@ $ rad init --name heartwood --description "Radicle Heartwood Protocol & Stack" -
|
||||||
|
|
||||||
Initializing local 🌱 project in .
|
Initializing local 🌱 project in .
|
||||||
|
|
||||||
ok Project heartwood created
|
✓ Project heartwood created
|
||||||
{
|
{
|
||||||
"name": "heartwood",
|
"name": "heartwood",
|
||||||
"description": "Radicle Heartwood Protocol & Stack",
|
"description": "Radicle Heartwood Protocol & Stack",
|
||||||
"defaultBranch": "master"
|
"defaultBranch": "master"
|
||||||
}
|
}
|
||||||
ok Syncing inventory..
|
✓ Syncing inventory..
|
||||||
|
|
||||||
Your project id is rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji. You can show it any time by running:
|
Your project id is rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji. You can show it any time by running:
|
||||||
rad .
|
rad .
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ $ rad init --name heartwood --description "Radicle Heartwood Protocol & Stack" -
|
||||||
|
|
||||||
Initializing local 🌱 project in .
|
Initializing local 🌱 project in .
|
||||||
|
|
||||||
ok Project heartwood created
|
✓ Project heartwood created
|
||||||
{
|
{
|
||||||
"name": "heartwood",
|
"name": "heartwood",
|
||||||
"description": "Radicle Heartwood Protocol & Stack",
|
"description": "Radicle Heartwood Protocol & Stack",
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,8 @@ $ rad patch open --message "define power requirements" --no-confirm
|
||||||
|
|
||||||
🌱 Creating patch for heartwood
|
🌱 Creating patch for heartwood
|
||||||
|
|
||||||
ok Pushing HEAD to storage...
|
✓ Pushing HEAD to storage...
|
||||||
ok Analyzing remotes...
|
✓ Analyzing remotes...
|
||||||
|
|
||||||
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master (f2de534) <- z6MknSL…StBU8Vi/flux-capacitor-power (3e674d1)
|
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master (f2de534) <- z6MknSL…StBU8Vi/flux-capacitor-power (3e674d1)
|
||||||
1 commit(s) ahead, 0 commit(s) behind
|
1 commit(s) ahead, 0 commit(s) behind
|
||||||
|
|
@ -45,7 +45,7 @@ No description provided.
|
||||||
╰───────────────────────────────────
|
╰───────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
ok Patch d4ef85f57a849bd845915d7a66a2192cd23811f6 created 🌱
|
✓ Patch d4ef85f57a849bd845915d7a66a2192cd23811f6 created 🌱
|
||||||
```
|
```
|
||||||
|
|
||||||
It will now be listed as one of the project's open patches.
|
It will now be listed as one of the project's open patches.
|
||||||
|
|
@ -53,13 +53,13 @@ It will now be listed as one of the project's open patches.
|
||||||
```
|
```
|
||||||
$ rad patch
|
$ rad patch
|
||||||
|
|
||||||
- YOU PROPOSED -
|
❲YOU PROPOSED❳
|
||||||
|
|
||||||
define power requirements d4ef85f57a8 R0 3e674d1 (flux-capacitor-power) ahead 1, behind 0
|
define power requirements d4ef85f57a8 R0 3e674d1 (flux-capacitor-power) ahead 1, behind 0
|
||||||
└─ * opened by did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (you) [..]
|
└─ * opened by did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (you) [..]
|
||||||
└─ * patch id d4ef85f57a849bd845915d7a66a2192cd23811f6
|
└─ * patch id d4ef85f57a849bd845915d7a66a2192cd23811f6
|
||||||
|
|
||||||
- OTHERS PROPOSED -
|
❲OTHERS PROPOSED❳
|
||||||
|
|
||||||
Nothing to show.
|
Nothing to show.
|
||||||
|
|
||||||
|
|
@ -98,14 +98,14 @@ $ rad patch update --message "Add README, just for the fun" --no-confirm d4ef85f
|
||||||
|
|
||||||
🌱 Updating patch for heartwood
|
🌱 Updating patch for heartwood
|
||||||
|
|
||||||
ok Pushing HEAD to storage...
|
✓ Pushing HEAD to storage...
|
||||||
ok Analyzing remotes...
|
✓ Analyzing remotes...
|
||||||
|
|
||||||
d4ef85f57a8 R0 (3e674d1) -> R1 (27857ec)
|
d4ef85f57a8 R0 (3e674d1) -> R1 (27857ec)
|
||||||
1 commit(s) ahead, 0 commit(s) behind
|
1 commit(s) ahead, 0 commit(s) behind
|
||||||
|
|
||||||
|
|
||||||
ok Patch d4ef85f57a849bd845915d7a66a2192cd23811f6 updated 🌱
|
✓ Patch d4ef85f57a849bd845915d7a66a2192cd23811f6 updated 🌱
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -122,6 +122,6 @@ Now, let's checkout the patch that we just created:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ rad patch checkout d4ef85f57a849bd845915d7a66a2192cd23811f6
|
$ rad patch checkout d4ef85f57a849bd845915d7a66a2192cd23811f6
|
||||||
ok Performing patch checkout...
|
✓ Performing patch checkout...
|
||||||
ok Switched to branch patch/d4ef85f57a8
|
✓ Switched to branch patch/d4ef85f57a8
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,9 @@ Now let's delete the `heartwood` project:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ rad rm rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --no-confirm
|
$ rad rm rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --no-confirm
|
||||||
** Warning: Failed to untrack repository: failed to connect to node: No such file or directory (os error 2)
|
! Warning: Failed to untrack repository: failed to connect to node: No such file or directory (os error 2)
|
||||||
** Warning: Make sure to untrack this repository when your node is running
|
! Warning: Make sure to untrack this repository when your node is running
|
||||||
ok Successfully removed project rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from storage
|
✓ Successfully removed project rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from storage
|
||||||
```
|
```
|
||||||
|
|
||||||
We can check our repositories again to see if it was deleted:
|
We can check our repositories again to see if it was deleted:
|
||||||
|
|
|
||||||
|
|
@ -80,20 +80,18 @@ pub fn init(options: Options) -> anyhow::Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
let home = profile::home()?;
|
let home = profile::home()?;
|
||||||
let passphrase = term::read_passphrase(options.stdin, true)?;
|
let passphrase = if options.stdin {
|
||||||
|
term::passphrase_stdin()
|
||||||
|
} else {
|
||||||
|
term::passphrase_confirm()
|
||||||
|
}?;
|
||||||
let spinner = term::spinner("Creating your 🌱 Ed25519 keypair...");
|
let spinner = term::spinner("Creating your 🌱 Ed25519 keypair...");
|
||||||
let profile = Profile::init(home, passphrase)?;
|
let profile = Profile::init(home, passphrase)?;
|
||||||
spinner.finish();
|
spinner.finish();
|
||||||
|
|
||||||
term::success!(
|
term::success!(
|
||||||
"Profile {} created.",
|
"Your Radicle ID is {}. This identifies your device.",
|
||||||
term::format::highlight(profile.did().to_string())
|
term::format::highlight(profile.did())
|
||||||
);
|
|
||||||
|
|
||||||
term::blank();
|
|
||||||
term::info!(
|
|
||||||
"Your radicle Node ID is {}. This identifies your device.",
|
|
||||||
term::format::highlight(profile.id().to_string())
|
|
||||||
);
|
);
|
||||||
|
|
||||||
term::blank();
|
term::blank();
|
||||||
|
|
@ -108,6 +106,7 @@ pub fn init(options: Options) -> anyhow::Result<()> {
|
||||||
pub fn authenticate(profile: &Profile, options: Options) -> anyhow::Result<()> {
|
pub fn authenticate(profile: &Profile, options: Options) -> anyhow::Result<()> {
|
||||||
let agent = ssh::agent::Agent::connect()?;
|
let agent = ssh::agent::Agent::connect()?;
|
||||||
|
|
||||||
|
// TODO: Only show this if we're not authenticated.
|
||||||
term::headline(&format!(
|
term::headline(&format!(
|
||||||
"🌱 Authenticating as {}",
|
"🌱 Authenticating as {}",
|
||||||
term::format::Identity::new(profile).styled()
|
term::format::Identity::new(profile).styled()
|
||||||
|
|
@ -119,7 +118,11 @@ pub fn authenticate(profile: &Profile, options: Options) -> anyhow::Result<()> {
|
||||||
|
|
||||||
// TODO: We should show the spinner on the passphrase prompt,
|
// TODO: We should show the spinner on the passphrase prompt,
|
||||||
// otherwise it seems like the passphrase is valid even if it isn't.
|
// otherwise it seems like the passphrase is valid even if it isn't.
|
||||||
let passphrase = term::read_passphrase(options.stdin, false)?;
|
let passphrase = if options.stdin {
|
||||||
|
term::passphrase_stdin()
|
||||||
|
} else {
|
||||||
|
term::passphrase()
|
||||||
|
}?;
|
||||||
let spinner = term::spinner("Unlocking...");
|
let spinner = term::spinner("Unlocking...");
|
||||||
let mut agent = ssh::agent::Agent::connect()?;
|
let mut agent = ssh::agent::Agent::connect()?;
|
||||||
let secret = profile
|
let secret = profile
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,7 @@ pub fn execute(options: Options, profile: &Profile) -> anyhow::Result<PathBuf> {
|
||||||
.identity_of(&remote)
|
.identity_of(&remote)
|
||||||
.context("project could not be found in local storage")?;
|
.context("project could not be found in local storage")?;
|
||||||
let payload = doc.project()?;
|
let payload = doc.project()?;
|
||||||
let path = PathBuf::from(payload.name().clone());
|
let path = PathBuf::from(payload.name());
|
||||||
|
|
||||||
transport::local::register(storage.clone());
|
transport::local::register(storage.clone());
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -71,8 +71,8 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
let repo = storage.repository(id)?;
|
let repo = storage.repository(id)?;
|
||||||
|
|
||||||
let payload = serde_json::to_string_pretty(&project.payload)?;
|
let payload = serde_json::to_string_pretty(&project.payload)?;
|
||||||
match term::Editor::new().edit(&payload)? {
|
match term::Editor::new().extension("json").edit(payload) {
|
||||||
Some(updated_payload) => {
|
Ok(Some(updated_payload)) => {
|
||||||
project.payload = serde_json::from_str(&updated_payload)?;
|
project.payload = serde_json::from_str(&updated_payload)?;
|
||||||
project.sign(&signer).and_then(|(_, sig)| {
|
project.sign(&signer).and_then(|(_, sig)| {
|
||||||
project.update(
|
project.update(
|
||||||
|
|
@ -83,7 +83,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
None => return Err(anyhow!("Operation aborted!")),
|
_ => return Err(anyhow!("Operation aborted!")),
|
||||||
}
|
}
|
||||||
|
|
||||||
term::success!("Update successful!");
|
term::success!("Update successful!");
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ pub struct Metadata {
|
||||||
impl Metadata {
|
impl Metadata {
|
||||||
fn edit(self) -> anyhow::Result<Self> {
|
fn edit(self) -> anyhow::Result<Self> {
|
||||||
let yaml = serde_yaml::to_string(&self)?;
|
let yaml = serde_yaml::to_string(&self)?;
|
||||||
match term::Editor::new().edit(&yaml)? {
|
match term::Editor::new().edit(yaml)? {
|
||||||
Some(meta) => Ok(serde_yaml::from_str(&meta).context("failed to parse proposal meta")?),
|
Some(meta) => Ok(serde_yaml::from_str(&meta).context("failed to parse proposal meta")?),
|
||||||
None => return Err(anyhow!("Operation aborted!")),
|
None => return Err(anyhow!("Operation aborted!")),
|
||||||
}
|
}
|
||||||
|
|
@ -261,7 +261,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
let yes = confirm(interactive, "Are you sure you want to reject?");
|
let yes = confirm(interactive, "Are you sure you want to reject?");
|
||||||
if yes {
|
if yes {
|
||||||
proposal.reject(rid, &signer)?;
|
proposal.reject(rid, &signer)?;
|
||||||
term::success!("Rejected proposal ✗");
|
term::success!("Rejected proposal 👎");
|
||||||
print(&proposal, &previous, None)?;
|
print(&proposal, &previous, None)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -578,9 +578,9 @@ fn print_revision(revision: &identity::Revision, previous: &Identity<Oid>) -> an
|
||||||
print!(
|
print!(
|
||||||
"{}",
|
"{}",
|
||||||
if revision.is_quorum_reached(previous) {
|
if revision.is_quorum_reached(previous) {
|
||||||
term::format::positive("✓ yes")
|
term::format::positive("👍 yes")
|
||||||
} else {
|
} else {
|
||||||
term::format::negative("✗ no")
|
term::format::negative("👎 no")
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
term::blank();
|
term::blank();
|
||||||
|
|
|
||||||
|
|
@ -162,9 +162,9 @@ pub fn init(options: Options, profile: &profile::Profile) -> anyhow::Result<()>
|
||||||
term::headline(&format!(
|
term::headline(&format!(
|
||||||
"Initializing local 🌱 project in {}",
|
"Initializing local 🌱 project in {}",
|
||||||
if path == cwd {
|
if path == cwd {
|
||||||
term::format::highlight(".")
|
term::format::highlight(".").to_string()
|
||||||
} else {
|
} else {
|
||||||
term::format::highlight(path.display())
|
term::format::highlight(path.display()).to_string()
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
|
|
||||||
|
|
@ -184,14 +184,14 @@ pub fn init(options: Options, profile: &profile::Profile) -> anyhow::Result<()>
|
||||||
|
|
||||||
let name = options.name.unwrap_or_else(|| {
|
let name = options.name.unwrap_or_else(|| {
|
||||||
let default = path.file_name().map(|f| f.to_string_lossy().to_string());
|
let default = path.file_name().map(|f| f.to_string_lossy().to_string());
|
||||||
term::text_input("Name", default).unwrap()
|
term::input("Name", default).unwrap()
|
||||||
});
|
});
|
||||||
let description = options
|
let description = options
|
||||||
.description
|
.description
|
||||||
.unwrap_or_else(|| term::text_input("Description", None).unwrap());
|
.unwrap_or_else(|| term::input("Description", None).unwrap());
|
||||||
let branch = options.branch.unwrap_or_else(|| {
|
let branch = options.branch.unwrap_or_else(|| {
|
||||||
if interactive.yes() {
|
if interactive.yes() {
|
||||||
term::text_input("Default branch", Some(head)).unwrap()
|
term::input("Default branch", Some(head)).unwrap()
|
||||||
} else {
|
} else {
|
||||||
head
|
head
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -218,8 +218,8 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
}
|
}
|
||||||
Operation::React { id, reaction } => {
|
Operation::React { id, reaction } => {
|
||||||
if let Ok(mut issue) = issues.get_mut(&id) {
|
if let Ok(mut issue) = issues.get_mut(&id) {
|
||||||
let comment_id = term::comment_select(&issue).unwrap();
|
let (comment_id, _) = term::comment_select(&issue).unwrap();
|
||||||
issue.react(comment_id, reaction, &signer)?;
|
issue.react(*comment_id, reaction, &signer)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Operation::Open { title, description } => {
|
Operation::Open { title, description } => {
|
||||||
|
|
@ -235,7 +235,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
description.unwrap_or("Enter a description...".to_owned())
|
description.unwrap_or("Enter a description...".to_owned())
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Some(text) = term::Editor::new().edit(&doc)? {
|
if let Ok(Some(text)) = term::Editor::new().edit(&doc) {
|
||||||
let mut meta = String::new();
|
let mut meta = String::new();
|
||||||
let mut frontmatter = false;
|
let mut frontmatter = false;
|
||||||
let mut lines = text.lines();
|
let mut lines = text.lines();
|
||||||
|
|
|
||||||
|
|
@ -53,8 +53,8 @@ pub fn run(_options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
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()),
|
||||||
term::format::tertiary(id.urn()),
|
term::format::tertiary(id.urn().as_str()),
|
||||||
term::format::secondary(head),
|
term::format::secondary(head.as_str()),
|
||||||
term::format::italic(proj.description()),
|
term::format::italic(proj.description()),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -222,8 +222,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
.dim()
|
.dim()
|
||||||
.italic()
|
.italic()
|
||||||
.to_string(),
|
.to_string(),
|
||||||
MergeStyle::Commit => term::format::style(merge_style.to_string())
|
MergeStyle::Commit => term::format::yellow(merge_style.to_string())
|
||||||
.yellow()
|
|
||||||
.italic()
|
.italic()
|
||||||
.to_string(),
|
.to_string(),
|
||||||
};
|
};
|
||||||
|
|
@ -312,19 +311,13 @@ fn merge_commit(
|
||||||
writeln!(&mut merge_msg, "{}", MERGE_HELP_MSG.trim())?;
|
writeln!(&mut merge_msg, "{}", MERGE_HELP_MSG.trim())?;
|
||||||
|
|
||||||
// Offer user the chance to edit the message before committing.
|
// Offer user the chance to edit the message before committing.
|
||||||
let merge_msg = match term::Editor::new()
|
let merge_msg = match term::Editor::new().extension("git-commit").edit(merge_msg) {
|
||||||
.require_save(true)
|
Ok(Some(s)) => s
|
||||||
.trim_newlines(true)
|
|
||||||
.extension(".git-commit")
|
|
||||||
.edit(&merge_msg)
|
|
||||||
.unwrap()
|
|
||||||
{
|
|
||||||
Some(s) => s
|
|
||||||
.lines()
|
.lines()
|
||||||
.filter(|l| !l.starts_with('#'))
|
.filter(|l| !l.starts_with('#'))
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join("\n"),
|
.join("\n"),
|
||||||
None => anyhow::bail!("user aborted merge"),
|
_ => anyhow::bail!("user aborted merge"),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Empty message aborts merge.
|
// Empty message aborts merge.
|
||||||
|
|
|
||||||
|
|
@ -127,7 +127,7 @@ pub fn pretty_sync_status(
|
||||||
) -> anyhow::Result<String> {
|
) -> anyhow::Result<String> {
|
||||||
let (a, b) = repo.graph_ahead_behind(revision_oid, head_oid)?;
|
let (a, b) = repo.graph_ahead_behind(revision_oid, head_oid)?;
|
||||||
if a == 0 && b == 0 {
|
if a == 0 && b == 0 {
|
||||||
return Ok(term::format::dim("up to date"));
|
return Ok(term::format::dim("up to date").to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
let ahead = term::format::positive(a);
|
let ahead = term::format::positive(a);
|
||||||
|
|
@ -143,7 +143,7 @@ pub fn pretty_commit_version(
|
||||||
revision_oid: &Oid,
|
revision_oid: &Oid,
|
||||||
repo: &Option<git::raw::Repository>,
|
repo: &Option<git::raw::Repository>,
|
||||||
) -> anyhow::Result<String> {
|
) -> anyhow::Result<String> {
|
||||||
let mut oid = term::format::secondary(term::format::oid(*revision_oid));
|
let mut oid = term::format::secondary(term::format::oid(*revision_oid)).to_string();
|
||||||
let mut branches: Vec<String> = vec![];
|
let mut branches: Vec<String> = vec![];
|
||||||
|
|
||||||
if let Some(repo) = repo {
|
if let Some(repo) = repo {
|
||||||
|
|
@ -216,6 +216,8 @@ pub fn push_to_storage(
|
||||||
if options.verbose {
|
if options.verbose {
|
||||||
spinner.finish();
|
spinner.finish();
|
||||||
term::blob(output);
|
term::blob(output);
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
spinner.finish();
|
spinner.finish();
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ use radicle::git;
|
||||||
use radicle::prelude::*;
|
use radicle::prelude::*;
|
||||||
use radicle::profile::Profile;
|
use radicle::profile::Profile;
|
||||||
use radicle::storage::git::Repository;
|
use radicle::storage::git::Repository;
|
||||||
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
use crate::terminal as term;
|
use crate::terminal as term;
|
||||||
|
|
||||||
|
|
@ -39,10 +40,7 @@ pub fn run(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
term::blank();
|
term::blank();
|
||||||
term::print(format!(
|
term::print(term::format::badge_secondary("YOU PROPOSED"));
|
||||||
"-{}-",
|
|
||||||
term::format::badge_secondary("YOU PROPOSED")
|
|
||||||
));
|
|
||||||
|
|
||||||
if own.is_empty() {
|
if own.is_empty() {
|
||||||
term::blank();
|
term::blank();
|
||||||
|
|
@ -55,10 +53,7 @@ pub fn run(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
term::blank();
|
term::blank();
|
||||||
term::print(format!(
|
term::print(term::format::badge_secondary("OTHERS PROPOSED"));
|
||||||
"-{}-",
|
|
||||||
term::format::badge_secondary("OTHERS PROPOSED")
|
|
||||||
));
|
|
||||||
|
|
||||||
if other.is_empty() {
|
if other.is_empty() {
|
||||||
term::blank();
|
term::blank();
|
||||||
|
|
@ -94,11 +89,9 @@ fn print(
|
||||||
)];
|
)];
|
||||||
|
|
||||||
if you {
|
if you {
|
||||||
author_info.push(term::format::secondary("(you)"));
|
author_info.push(term::format::secondary("(you)").to_string());
|
||||||
}
|
}
|
||||||
author_info.push(term::format::dim(term::format::timestamp(
|
author_info.push(term::format::dim(term::format::timestamp(&patch.timestamp())).to_string());
|
||||||
&patch.timestamp(),
|
|
||||||
)));
|
|
||||||
|
|
||||||
let (_, revision) = patch
|
let (_, revision) = patch
|
||||||
.latest()
|
.latest()
|
||||||
|
|
@ -120,17 +113,17 @@ fn print(
|
||||||
let mut badges = Vec::new();
|
let mut badges = Vec::new();
|
||||||
|
|
||||||
if peer.delegate {
|
if peer.delegate {
|
||||||
badges.push(term::format::secondary("(delegate)"));
|
badges.push(term::format::secondary("(delegate)").to_string());
|
||||||
}
|
}
|
||||||
if peer.id == *whoami {
|
if peer.id == *whoami {
|
||||||
badges.push(term::format::secondary("(you)"));
|
badges.push(term::format::secondary("(you)").to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
timeline.push((
|
timeline.push((
|
||||||
merge.timestamp,
|
merge.timestamp,
|
||||||
format!(
|
format!(
|
||||||
"{}{} by {} {}",
|
"{}{} by {} {}",
|
||||||
" ".repeat(term::text_width(prefix)),
|
" ".repeat(prefix.width()),
|
||||||
term::format::secondary(term::format::dim("✓ merged")),
|
term::format::secondary(term::format::dim("✓ merged")),
|
||||||
term::format::tertiary(peer.id),
|
term::format::tertiary(peer.id),
|
||||||
badges.join(" "),
|
badges.join(" "),
|
||||||
|
|
@ -147,17 +140,17 @@ fn print(
|
||||||
let mut badges = Vec::new();
|
let mut badges = Vec::new();
|
||||||
|
|
||||||
if peer.delegate {
|
if peer.delegate {
|
||||||
badges.push(term::format::secondary("(delegate)"));
|
badges.push(term::format::secondary("(delegate)").to_string());
|
||||||
}
|
}
|
||||||
if peer.id == *whoami {
|
if peer.id == *whoami {
|
||||||
badges.push(term::format::secondary("(you)"));
|
badges.push(term::format::secondary("(you)").to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
timeline.push((
|
timeline.push((
|
||||||
review.timestamp(),
|
review.timestamp(),
|
||||||
format!(
|
format!(
|
||||||
"{}{} by {} {}",
|
"{}{} by {} {}",
|
||||||
" ".repeat(term::text_width(prefix)),
|
" ".repeat(prefix.width()),
|
||||||
verdict,
|
verdict,
|
||||||
term::format::tertiary(reviewer),
|
term::format::tertiary(reviewer),
|
||||||
badges.join(" "),
|
badges.join(" "),
|
||||||
|
|
|
||||||
|
|
@ -78,33 +78,45 @@ fn all(profile: &Profile) -> anyhow::Result<()> {
|
||||||
let mut table = term::Table::default();
|
let mut table = term::Table::default();
|
||||||
|
|
||||||
let did = profile.did();
|
let did = profile.did();
|
||||||
table.push(["ID", &term::format::tertiary(did)]);
|
table.push([
|
||||||
|
term::format::style("ID").to_string(),
|
||||||
|
term::format::tertiary(did).to_string(),
|
||||||
|
]);
|
||||||
|
|
||||||
let node_id = profile.id();
|
let node_id = profile.id();
|
||||||
table.push(["Node ID", &term::format::tertiary(node_id)]);
|
table.push([
|
||||||
|
term::format::style("Node ID").to_string(),
|
||||||
|
term::format::tertiary(node_id).to_string(),
|
||||||
|
]);
|
||||||
|
|
||||||
let ssh_short = ssh::fmt::fingerprint(node_id);
|
let ssh_short = ssh::fmt::fingerprint(node_id);
|
||||||
table.push(["Key (hash)", &term::format::tertiary(ssh_short)]);
|
table.push([
|
||||||
|
term::format::style("Key (hash)").to_string(),
|
||||||
|
term::format::tertiary(ssh_short).to_string(),
|
||||||
|
]);
|
||||||
|
|
||||||
let ssh_long = ssh::fmt::key(node_id);
|
let ssh_long = ssh::fmt::key(node_id);
|
||||||
table.push(["Key (full)", &term::format::tertiary(ssh_long)]);
|
table.push([
|
||||||
|
term::format::style("Key (full)").to_string(),
|
||||||
|
term::format::tertiary(ssh_long).to_string(),
|
||||||
|
]);
|
||||||
|
|
||||||
let storage_path = profile.home.storage();
|
let storage_path = profile.home.storage();
|
||||||
table.push([
|
table.push([
|
||||||
"Storage (git)",
|
term::format::style("Storage (git)").to_string(),
|
||||||
&term::format::tertiary(storage_path.display()),
|
term::format::tertiary(storage_path.display()).to_string(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let keys_path = profile.home.keys();
|
let keys_path = profile.home.keys();
|
||||||
table.push([
|
table.push([
|
||||||
"Storage (keys)",
|
term::format::style("Storage (keys)").to_string(),
|
||||||
&term::format::tertiary(keys_path.display()),
|
term::format::tertiary(keys_path.display()).to_string(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let node_path = profile.home.node();
|
let node_path = profile.home.node();
|
||||||
table.push([
|
table.push([
|
||||||
"Node (socket)",
|
term::format::style("Node (socket)").to_string(),
|
||||||
&term::format::tertiary(node_path.join("radicle.sock").display()),
|
term::format::tertiary(node_path.join("radicle.sock").display()).to_string(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
table.render();
|
table.render();
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
|
pub mod ansi;
|
||||||
pub mod args;
|
pub mod args;
|
||||||
|
pub mod cell;
|
||||||
pub mod cob;
|
pub mod cob;
|
||||||
pub mod command;
|
pub mod command;
|
||||||
|
pub mod editor;
|
||||||
pub mod format;
|
pub mod format;
|
||||||
pub mod io;
|
pub mod io;
|
||||||
pub mod patch;
|
pub mod patch;
|
||||||
|
|
@ -11,12 +14,12 @@ pub mod textbox;
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
use std::process;
|
use std::process;
|
||||||
|
|
||||||
use dialoguer::console::style;
|
|
||||||
use radicle::profile::Profile;
|
use radicle::profile::Profile;
|
||||||
|
|
||||||
|
pub use ansi::{paint, Paint};
|
||||||
pub use args::{Args, Error, Help};
|
pub use args::{Args, Error, Help};
|
||||||
pub use console::measure_text_width as text_width;
|
pub use editor::Editor;
|
||||||
pub use dialoguer::Editor;
|
pub use inquire::ui::Styled;
|
||||||
pub use io::*;
|
pub use io::*;
|
||||||
pub use spinner::{spinner, Spinner};
|
pub use spinner::{spinner, Spinner};
|
||||||
pub use table::Table;
|
pub use table::Table;
|
||||||
|
|
@ -97,14 +100,14 @@ where
|
||||||
};
|
};
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"{} {} {} {}",
|
"{} {} {} {}",
|
||||||
style("==").red(),
|
Paint::red("=="),
|
||||||
style("Error:").red(),
|
Paint::red("Error:"),
|
||||||
style(format!("rad-{}:", help.name)).red(),
|
Paint::red(format!("rad-{}:", help.name)),
|
||||||
style(&err).red()
|
Paint::red(err.to_string()),
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Some(Error::WithHint { hint, .. }) = err.downcast_ref::<Error>() {
|
if let Some(Error::WithHint { hint, .. }) = err.downcast_ref::<Error>() {
|
||||||
eprintln!("{}", style(hint).yellow());
|
eprintln!("{}", Paint::yellow(hint));
|
||||||
}
|
}
|
||||||
|
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
|
|
@ -173,3 +176,7 @@ impl From<bool> for Interactive {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn style<T>(item: T) -> Paint<T> {
|
||||||
|
paint(item)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
//! A dead simple ANSI terminal color painting library.
|
||||||
|
//!
|
||||||
|
//! This library is a port of the `yansi` crate.
|
||||||
|
//! Copyright (c) 2017 Sergio Benitez
|
||||||
|
//!
|
||||||
|
mod color;
|
||||||
|
mod paint;
|
||||||
|
mod style;
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
|
mod windows;
|
||||||
|
|
||||||
|
pub use color::Color;
|
||||||
|
pub use paint::paint;
|
||||||
|
pub use paint::Paint;
|
||||||
|
pub use style::Style;
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use super::{Paint, Style};
|
||||||
|
|
||||||
|
/// An enum representing an ANSI color code.
|
||||||
|
#[derive(Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Copy, Clone)]
|
||||||
|
pub enum Color {
|
||||||
|
/// No color has been set. Nothing is changed when applied.
|
||||||
|
#[default]
|
||||||
|
Unset,
|
||||||
|
/// Black #0 (foreground code `30`, background code `40`).
|
||||||
|
Black,
|
||||||
|
/// Red: #1 (foreground code `31`, background code `41`).
|
||||||
|
Red,
|
||||||
|
/// Green: #2 (foreground code `32`, background code `42`).
|
||||||
|
Green,
|
||||||
|
/// Yellow: #3 (foreground code `33`, background code `43`).
|
||||||
|
Yellow,
|
||||||
|
/// Blue: #4 (foreground code `34`, background code `44`).
|
||||||
|
Blue,
|
||||||
|
/// Magenta: #5 (foreground code `35`, background code `45`).
|
||||||
|
Magenta,
|
||||||
|
/// Cyan: #6 (foreground code `36`, background code `46`).
|
||||||
|
Cyan,
|
||||||
|
/// White: #7 (foreground code `37`, background code `47`).
|
||||||
|
White,
|
||||||
|
/// A color number from 0 to 255, for use in 256-color terminals.
|
||||||
|
Fixed(u8),
|
||||||
|
/// A 24-bit RGB color, as specified by ISO-8613-3.
|
||||||
|
RGB(u8, u8, u8),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Color {
|
||||||
|
/// Constructs a new `Paint` structure that encapsulates `item` with the
|
||||||
|
/// foreground color set to the color `self`.
|
||||||
|
#[inline]
|
||||||
|
pub fn paint<T>(self, item: T) -> Paint<T> {
|
||||||
|
Paint::new(item).fg(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructs a new `Style` structure with the foreground color set to the
|
||||||
|
/// color `self`.
|
||||||
|
#[inline]
|
||||||
|
pub const fn style(self) -> Style {
|
||||||
|
Style::new(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn ansi_fmt(&self, f: &mut dyn fmt::Write) -> fmt::Result {
|
||||||
|
match *self {
|
||||||
|
Color::Unset => Ok(()),
|
||||||
|
Color::Black => write!(f, "0"),
|
||||||
|
Color::Red => write!(f, "1"),
|
||||||
|
Color::Green => write!(f, "2"),
|
||||||
|
Color::Yellow => write!(f, "3"),
|
||||||
|
Color::Blue => write!(f, "4"),
|
||||||
|
Color::Magenta => write!(f, "5"),
|
||||||
|
Color::Cyan => write!(f, "6"),
|
||||||
|
Color::White => write!(f, "7"),
|
||||||
|
Color::Fixed(num) => write!(f, "8;5;{num}"),
|
||||||
|
Color::RGB(r, g, b) => write!(f, "8;2;{r};{g};{b}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,252 @@
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
|
use super::color::Color;
|
||||||
|
use super::style::{Property, Style};
|
||||||
|
|
||||||
|
/// A structure encapsulating an item and styling.
|
||||||
|
#[derive(Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Copy, Clone)]
|
||||||
|
pub struct Paint<T> {
|
||||||
|
pub item: T,
|
||||||
|
pub style: Style,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Paint<&str> {
|
||||||
|
/// Return plain content.
|
||||||
|
pub fn content(&self) -> &str {
|
||||||
|
self.item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Paint<String> {
|
||||||
|
/// Return plain content.
|
||||||
|
pub fn content(&self) -> &str {
|
||||||
|
self.item.as_str()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<T> for Paint<T> {
|
||||||
|
fn from(value: T) -> Self {
|
||||||
|
Self::new(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for Paint<String> {
|
||||||
|
fn from(item: &str) -> Self {
|
||||||
|
Self::new(item.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Paint<T> {
|
||||||
|
/// Constructs a new `Paint` structure encapsulating `item` with no set
|
||||||
|
/// styling.
|
||||||
|
#[inline]
|
||||||
|
pub const fn new(item: T) -> Paint<T> {
|
||||||
|
Paint {
|
||||||
|
item,
|
||||||
|
style: Style {
|
||||||
|
foreground: Color::Unset,
|
||||||
|
background: Color::Unset,
|
||||||
|
properties: Property::new(),
|
||||||
|
wrap: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructs a new _wrapping_ `Paint` structure encapsulating `item` with
|
||||||
|
/// default styling.
|
||||||
|
///
|
||||||
|
/// A wrapping `Paint` converts all color resets written out by the internal
|
||||||
|
/// value to the styling of itself. This allows for seamless color wrapping
|
||||||
|
/// of other colored text.
|
||||||
|
///
|
||||||
|
/// # Performance
|
||||||
|
///
|
||||||
|
/// In order to wrap an internal value, the internal value must first be
|
||||||
|
/// written out to a local buffer and examined. As a result, displaying a
|
||||||
|
/// wrapped value is likely to result in a heap allocation and copy.
|
||||||
|
#[inline]
|
||||||
|
pub const fn wrapping(item: T) -> Paint<T> {
|
||||||
|
Paint::new(item).wrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructs a new `Paint` structure encapsulating `item` with the
|
||||||
|
/// foreground color set to the RGB color `r`, `g`, `b`.
|
||||||
|
#[inline]
|
||||||
|
pub const fn rgb(r: u8, g: u8, b: u8, item: T) -> Paint<T> {
|
||||||
|
Paint::new(item).fg(Color::RGB(r, g, b))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructs a new `Paint` structure encapsulating `item` with the
|
||||||
|
/// foreground color set to the fixed 8-bit color `color`.
|
||||||
|
#[inline]
|
||||||
|
pub const fn fixed(color: u8, item: T) -> Paint<T> {
|
||||||
|
Paint::new(item).fg(Color::Fixed(color))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn red(item: T) -> Paint<T> {
|
||||||
|
Paint::new(item).fg(Color::Red)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn black(item: T) -> Paint<T> {
|
||||||
|
Paint::new(item).fg(Color::Black)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn yellow(item: T) -> Paint<T> {
|
||||||
|
Paint::new(item).fg(Color::Yellow)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn green(item: T) -> Paint<T> {
|
||||||
|
Paint::new(item).fg(Color::Green)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn cyan(item: T) -> Paint<T> {
|
||||||
|
Paint::new(item).fg(Color::Cyan)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn blue(item: T) -> Paint<T> {
|
||||||
|
Paint::new(item).fg(Color::Blue)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn magenta(item: T) -> Paint<T> {
|
||||||
|
Paint::new(item).fg(Color::Magenta)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn white(item: T) -> Paint<T> {
|
||||||
|
Paint::new(item).fg(Color::White)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves the style currently set on `self`.
|
||||||
|
#[inline]
|
||||||
|
pub const fn style(&self) -> Style {
|
||||||
|
self.style
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves a borrow to the inner item.
|
||||||
|
#[inline]
|
||||||
|
pub const fn inner(&self) -> &T {
|
||||||
|
&self.item
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the style of `self` to `style`.
|
||||||
|
#[inline]
|
||||||
|
pub fn with_style(mut self, style: Style) -> Paint<T> {
|
||||||
|
self.style = style;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Makes `self` a _wrapping_ `Paint`.
|
||||||
|
///
|
||||||
|
/// A wrapping `Paint` converts all color resets written out by the internal
|
||||||
|
/// value to the styling of itself. This allows for seamless color wrapping
|
||||||
|
/// of other colored text.
|
||||||
|
///
|
||||||
|
/// # Performance
|
||||||
|
///
|
||||||
|
/// In order to wrap an internal value, the internal value must first be
|
||||||
|
/// written out to a local buffer and examined. As a result, displaying a
|
||||||
|
/// wrapped value is likely to result in a heap allocation and copy.
|
||||||
|
#[inline]
|
||||||
|
pub const fn wrap(mut self) -> Paint<T> {
|
||||||
|
self.style.wrap = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the foreground to `color`.
|
||||||
|
#[inline]
|
||||||
|
pub const fn fg(mut self, color: Color) -> Paint<T> {
|
||||||
|
self.style.foreground = color;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the background to `color`.
|
||||||
|
#[inline]
|
||||||
|
pub const fn bg(mut self, color: Color) -> Paint<T> {
|
||||||
|
self.style.background = color;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bold(mut self) -> Self {
|
||||||
|
self.style.properties.set(Property::BOLD);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dim(mut self) -> Self {
|
||||||
|
self.style.properties.set(Property::DIM);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn italic(mut self) -> Self {
|
||||||
|
self.style.properties.set(Property::ITALIC);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn underline(mut self) -> Self {
|
||||||
|
self.style.properties.set(Property::UNDERLINE);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn invert(mut self) -> Self {
|
||||||
|
self.style.properties.set(Property::INVERT);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn strikethrough(mut self) -> Self {
|
||||||
|
self.style.properties.set(Property::STRIKETHROUGH);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn blink(mut self) -> Self {
|
||||||
|
self.style.properties.set(Property::BLINK);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hidden(mut self) -> Self {
|
||||||
|
self.style.properties.set(Property::HIDDEN);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: UnicodeWidthStr> UnicodeWidthStr for Paint<T> {
|
||||||
|
fn width(&self) -> usize {
|
||||||
|
self.item.width()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn width_cjk(&self) -> usize {
|
||||||
|
self.item.width_cjk()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: fmt::Display> fmt::Display for Paint<T> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
if Paint::is_enabled() && self.style.wrap {
|
||||||
|
let mut prefix = String::new();
|
||||||
|
prefix.push_str("\x1B[0m");
|
||||||
|
self.style.fmt_prefix(&mut prefix)?;
|
||||||
|
self.style.fmt_prefix(f)?;
|
||||||
|
|
||||||
|
let item = format!("{}", self.item).replace("\x1B[0m", &prefix);
|
||||||
|
fmt::Display::fmt(&item, f)?;
|
||||||
|
self.style.fmt_suffix(f)
|
||||||
|
} else if Paint::is_enabled() {
|
||||||
|
self.style.fmt_prefix(f)?;
|
||||||
|
fmt::Display::fmt(&self.item, f)?;
|
||||||
|
self.style.fmt_suffix(f)
|
||||||
|
} else {
|
||||||
|
fmt::Display::fmt(&self.item, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Paint<()> {
|
||||||
|
/// Returns `true` if coloring is enabled and `false` otherwise.
|
||||||
|
pub fn is_enabled() -> bool {
|
||||||
|
concolor::get(concolor::Stream::Stdout).ansi_color()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shorthand for [`Paint::new`].
|
||||||
|
pub fn paint<T>(item: T) -> Paint<T> {
|
||||||
|
Paint::new(item)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,267 @@
|
||||||
|
use std::fmt::{self, Display};
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
use std::ops::BitOr;
|
||||||
|
|
||||||
|
use super::{Color, Paint};
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Copy, Clone)]
|
||||||
|
pub struct Property(u8);
|
||||||
|
|
||||||
|
impl Property {
|
||||||
|
pub const BOLD: Self = Property(1 << 0);
|
||||||
|
pub const DIM: Self = Property(1 << 1);
|
||||||
|
pub const ITALIC: Self = Property(1 << 2);
|
||||||
|
pub const UNDERLINE: Self = Property(1 << 3);
|
||||||
|
pub const BLINK: Self = Property(1 << 4);
|
||||||
|
pub const INVERT: Self = Property(1 << 5);
|
||||||
|
pub const HIDDEN: Self = Property(1 << 6);
|
||||||
|
pub const STRIKETHROUGH: Self = Property(1 << 7);
|
||||||
|
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Property(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub const fn contains(self, other: Property) -> bool {
|
||||||
|
(other.0 & self.0) == other.0
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn set(&mut self, other: Property) {
|
||||||
|
self.0 |= other.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn iter(self) -> Iter {
|
||||||
|
Iter {
|
||||||
|
index: 0,
|
||||||
|
properties: self,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BitOr for Property {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn bitor(self, rhs: Self) -> Self {
|
||||||
|
Property(self.0 | rhs.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Iter {
|
||||||
|
index: u8,
|
||||||
|
properties: Property,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Iterator for Iter {
|
||||||
|
type Item = usize;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
while self.index < 8 {
|
||||||
|
let index = self.index;
|
||||||
|
self.index += 1;
|
||||||
|
|
||||||
|
if self.properties.contains(Property(1 << index)) {
|
||||||
|
return Some(index as usize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents a set of styling options.
|
||||||
|
#[repr(packed)]
|
||||||
|
#[derive(Default, Debug, Eq, Ord, PartialOrd, Copy, Clone)]
|
||||||
|
pub struct Style {
|
||||||
|
pub(crate) foreground: Color,
|
||||||
|
pub(crate) background: Color,
|
||||||
|
pub(crate) properties: Property,
|
||||||
|
pub(crate) wrap: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for Style {
|
||||||
|
fn eq(&self, other: &Style) -> bool {
|
||||||
|
self.foreground == other.foreground
|
||||||
|
&& self.background == other.background
|
||||||
|
&& self.properties == other.properties
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hash for Style {
|
||||||
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
|
self.foreground.hash(state);
|
||||||
|
self.background.hash(state);
|
||||||
|
self.properties.hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn write_spliced<T: Display>(c: &mut bool, f: &mut dyn fmt::Write, t: T) -> fmt::Result {
|
||||||
|
if *c {
|
||||||
|
write!(f, ";{t}")
|
||||||
|
} else {
|
||||||
|
*c = true;
|
||||||
|
write!(f, "{t}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Style {
|
||||||
|
/// Default style with the foreground set to `color` and no other set
|
||||||
|
/// properties.
|
||||||
|
#[inline]
|
||||||
|
pub const fn new(color: Color) -> Style {
|
||||||
|
// Avoiding `Default::default` since unavailable as `const`
|
||||||
|
Self {
|
||||||
|
foreground: color,
|
||||||
|
background: Color::Unset,
|
||||||
|
properties: Property::new(),
|
||||||
|
wrap: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the foreground to `color`.
|
||||||
|
#[inline]
|
||||||
|
pub const fn fg(mut self, color: Color) -> Style {
|
||||||
|
self.foreground = color;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the background to `color`.
|
||||||
|
#[inline]
|
||||||
|
pub const fn bg(mut self, color: Color) -> Style {
|
||||||
|
self.background = color;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets `self` to be wrapping.
|
||||||
|
///
|
||||||
|
/// A wrapping `Style` converts all color resets written out by the internal
|
||||||
|
/// value to the styling of itself. This allows for seamless color wrapping
|
||||||
|
/// of other colored text.
|
||||||
|
///
|
||||||
|
/// # Performance
|
||||||
|
///
|
||||||
|
/// In order to wrap an internal value, the internal value must first be
|
||||||
|
/// written out to a local buffer and examined. As a result, displaying a
|
||||||
|
/// wrapped value is likely to result in a heap allocation and copy.
|
||||||
|
#[inline]
|
||||||
|
pub const fn wrap(mut self) -> Style {
|
||||||
|
self.wrap = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bold(mut self) -> Self {
|
||||||
|
self.properties.set(Property::BOLD);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dim(mut self) -> Self {
|
||||||
|
self.properties.set(Property::DIM);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn italic(mut self) -> Self {
|
||||||
|
self.properties.set(Property::ITALIC);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn underline(mut self) -> Self {
|
||||||
|
self.properties.set(Property::UNDERLINE);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn invert(mut self) -> Self {
|
||||||
|
self.properties.set(Property::INVERT);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn strikethrough(mut self) -> Self {
|
||||||
|
self.properties.set(Property::STRIKETHROUGH);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructs a new `Paint` structure that encapsulates `item` with the
|
||||||
|
/// style set to `self`.
|
||||||
|
#[inline]
|
||||||
|
pub fn paint<T>(self, item: T) -> Paint<T> {
|
||||||
|
Paint::new(item).with_style(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the foreground color of `self`.
|
||||||
|
#[inline]
|
||||||
|
pub const fn fg_color(&self) -> Color {
|
||||||
|
self.foreground
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the foreground color of `self`.
|
||||||
|
#[inline]
|
||||||
|
pub const fn bg_color(&self) -> Color {
|
||||||
|
self.background
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if `self` is wrapping.
|
||||||
|
#[inline]
|
||||||
|
pub const fn is_wrapping(&self) -> bool {
|
||||||
|
self.wrap
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn is_plain(&self) -> bool {
|
||||||
|
self == &Style::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes the ANSI code prefix for the currently set styles.
|
||||||
|
///
|
||||||
|
/// This method is intended to be used inside of [`fmt::Display`] and
|
||||||
|
/// [`fmt::Debug`] implementations for custom or specialized use-cases. Most
|
||||||
|
/// users should use [`Paint`] for all painting needs.
|
||||||
|
///
|
||||||
|
/// This method writes the ANSI code prefix irrespective of whether painting
|
||||||
|
/// is currently enabled or disabled. To write the prefix only if painting
|
||||||
|
/// is enabled, condition a call to this method on [`Paint::is_enabled()`].
|
||||||
|
pub fn fmt_prefix(&self, f: &mut dyn fmt::Write) -> fmt::Result {
|
||||||
|
// A user may just want a code-free string when no styles are applied.
|
||||||
|
if self.is_plain() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut splice = false;
|
||||||
|
write!(f, "\x1B[")?;
|
||||||
|
|
||||||
|
for i in self.properties.iter() {
|
||||||
|
let k = if i >= 5 { i + 2 } else { i + 1 };
|
||||||
|
write_spliced(&mut splice, f, k)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.background != Color::Unset {
|
||||||
|
write_spliced(&mut splice, f, "4")?;
|
||||||
|
self.background.ansi_fmt(f)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.foreground != Color::Unset {
|
||||||
|
write_spliced(&mut splice, f, "3")?;
|
||||||
|
self.foreground.ansi_fmt(f)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// All the codes end with an `m`.
|
||||||
|
write!(f, "m")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes the ANSI code suffix for the currently set styles.
|
||||||
|
///
|
||||||
|
/// This method is intended to be used inside of [`fmt::Display`] and
|
||||||
|
/// [`fmt::Debug`] implementations for custom or specialized use-cases. Most
|
||||||
|
/// users should use [`Paint`] for all painting needs.
|
||||||
|
///
|
||||||
|
/// This method writes the ANSI code suffix irrespective of whether painting
|
||||||
|
/// is currently enabled or disabled. To write the suffix only if painting
|
||||||
|
/// is enabled, condition a call to this method on [`Paint::is_enabled()`].
|
||||||
|
pub fn fmt_suffix(&self, f: &mut dyn fmt::Write) -> fmt::Result {
|
||||||
|
if self.is_plain() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
write!(f, "\x1B[0m")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,279 @@
|
||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
use super::Color::*;
|
||||||
|
use super::Paint;
|
||||||
|
|
||||||
|
/// Ensures tests are running serially.
|
||||||
|
static SERIAL: Mutex<()> = Mutex::new(());
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn colors_enabled() {
|
||||||
|
let _guard = SERIAL.lock();
|
||||||
|
|
||||||
|
concolor::set(concolor::ColorChoice::Always);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
Paint::new("text/plain").to_string(),
|
||||||
|
"text/plain".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Paint::red("hi").to_string(),
|
||||||
|
"\x1B[31mhi\x1B[0m".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Paint::black("hi").to_string(),
|
||||||
|
"\x1B[30mhi\x1B[0m".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Paint::yellow("hi").bold().to_string(),
|
||||||
|
"\x1B[1;33mhi\x1B[0m".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Paint::new("hi").fg(Yellow).bold().to_string(),
|
||||||
|
"\x1B[1;33mhi\x1B[0m".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Paint::blue("hi").underline().to_string(),
|
||||||
|
"\x1B[4;34mhi\x1B[0m".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Paint::green("hi").bold().underline().to_string(),
|
||||||
|
"\x1B[1;4;32mhi\x1B[0m".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Paint::green("hi").underline().bold().to_string(),
|
||||||
|
"\x1B[1;4;32mhi\x1B[0m".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Paint::magenta("hi").bg(White).to_string(),
|
||||||
|
"\x1B[47;35mhi\x1B[0m".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Paint::red("hi").bg(Blue).fg(Yellow).to_string(),
|
||||||
|
"\x1B[44;33mhi\x1B[0m".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Paint::cyan("hi").bg(Blue).fg(Yellow).to_string(),
|
||||||
|
"\x1B[44;33mhi\x1B[0m".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Paint::cyan("hi").bold().bg(White).to_string(),
|
||||||
|
"\x1B[1;47;36mhi\x1B[0m".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Paint::cyan("hi").underline().bg(White).to_string(),
|
||||||
|
"\x1B[4;47;36mhi\x1B[0m".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Paint::cyan("hi").bold().underline().bg(White).to_string(),
|
||||||
|
"\x1B[1;4;47;36mhi\x1B[0m".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Paint::cyan("hi").underline().bold().bg(White).to_string(),
|
||||||
|
"\x1B[1;4;47;36mhi\x1B[0m".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Paint::fixed(100, "hi").to_string(),
|
||||||
|
"\x1B[38;5;100mhi\x1B[0m".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Paint::fixed(100, "hi").bg(Magenta).to_string(),
|
||||||
|
"\x1B[45;38;5;100mhi\x1B[0m".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Paint::fixed(100, "hi").bg(Fixed(200)).to_string(),
|
||||||
|
"\x1B[48;5;200;38;5;100mhi\x1B[0m".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Paint::rgb(70, 130, 180, "hi").to_string(),
|
||||||
|
"\x1B[38;2;70;130;180mhi\x1B[0m".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Paint::rgb(70, 130, 180, "hi").bg(Blue).to_string(),
|
||||||
|
"\x1B[44;38;2;70;130;180mhi\x1B[0m".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Paint::blue("hi").bg(RGB(70, 130, 180)).to_string(),
|
||||||
|
"\x1B[48;2;70;130;180;34mhi\x1B[0m".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Paint::rgb(70, 130, 180, "hi")
|
||||||
|
.bg(RGB(5, 10, 15))
|
||||||
|
.to_string(),
|
||||||
|
"\x1B[48;2;5;10;15;38;2;70;130;180mhi\x1B[0m".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Paint::new("hi").bold().to_string(),
|
||||||
|
"\x1B[1mhi\x1B[0m".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Paint::new("hi").underline().to_string(),
|
||||||
|
"\x1B[4mhi\x1B[0m".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Paint::new("hi").bold().underline().to_string(),
|
||||||
|
"\x1B[1;4mhi\x1B[0m".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Paint::new("hi").dim().to_string(),
|
||||||
|
"\x1B[2mhi\x1B[0m".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Paint::new("hi").italic().to_string(),
|
||||||
|
"\x1B[3mhi\x1B[0m".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Paint::new("hi").blink().to_string(),
|
||||||
|
"\x1B[5mhi\x1B[0m".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Paint::new("hi").invert().to_string(),
|
||||||
|
"\x1B[7mhi\x1B[0m".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Paint::new("hi").hidden().to_string(),
|
||||||
|
"\x1B[8mhi\x1B[0m".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Paint::new("hi").strikethrough().to_string(),
|
||||||
|
"\x1B[9mhi\x1B[0m".to_string()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn colors_disabled() {
|
||||||
|
let _guard = SERIAL.lock();
|
||||||
|
|
||||||
|
concolor::set(concolor::ColorChoice::Never);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
Paint::new("text/plain").to_string(),
|
||||||
|
"text/plain".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(Paint::red("hi").to_string(), "hi".to_string());
|
||||||
|
assert_eq!(Paint::black("hi").to_string(), "hi".to_string());
|
||||||
|
assert_eq!(Paint::yellow("hi").bold().to_string(), "hi".to_string());
|
||||||
|
assert_eq!(
|
||||||
|
Paint::new("hi").fg(Yellow).bold().to_string(),
|
||||||
|
"hi".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(Paint::blue("hi").underline().to_string(), "hi".to_string());
|
||||||
|
assert_eq!(
|
||||||
|
Paint::green("hi").bold().underline().to_string(),
|
||||||
|
"hi".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Paint::green("hi").underline().bold().to_string(),
|
||||||
|
"hi".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(Paint::magenta("hi").bg(White).to_string(), "hi".to_string());
|
||||||
|
assert_eq!(
|
||||||
|
Paint::red("hi").bg(Blue).fg(Yellow).to_string(),
|
||||||
|
"hi".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Paint::cyan("hi").bg(Blue).fg(Yellow).to_string(),
|
||||||
|
"hi".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Paint::cyan("hi").bold().bg(White).to_string(),
|
||||||
|
"hi".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Paint::cyan("hi").underline().bg(White).to_string(),
|
||||||
|
"hi".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Paint::cyan("hi").bold().underline().bg(White).to_string(),
|
||||||
|
"hi".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Paint::cyan("hi").underline().bold().bg(White).to_string(),
|
||||||
|
"hi".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(Paint::fixed(100, "hi").to_string(), "hi".to_string());
|
||||||
|
assert_eq!(
|
||||||
|
Paint::fixed(100, "hi").bg(Magenta).to_string(),
|
||||||
|
"hi".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Paint::fixed(100, "hi").bg(Fixed(200)).to_string(),
|
||||||
|
"hi".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(Paint::rgb(70, 130, 180, "hi").to_string(), "hi".to_string());
|
||||||
|
assert_eq!(
|
||||||
|
Paint::rgb(70, 130, 180, "hi").bg(Blue).to_string(),
|
||||||
|
"hi".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Paint::blue("hi").bg(RGB(70, 130, 180)).to_string(),
|
||||||
|
"hi".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Paint::blue("hi").bg(RGB(70, 130, 180)).wrap().to_string(),
|
||||||
|
"hi".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Paint::rgb(70, 130, 180, "hi")
|
||||||
|
.bg(RGB(5, 10, 15))
|
||||||
|
.to_string(),
|
||||||
|
"hi".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(Paint::new("hi").bold().to_string(), "hi".to_string());
|
||||||
|
assert_eq!(Paint::new("hi").underline().to_string(), "hi".to_string());
|
||||||
|
assert_eq!(
|
||||||
|
Paint::new("hi").bold().underline().to_string(),
|
||||||
|
"hi".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(Paint::new("hi").dim().to_string(), "hi".to_string());
|
||||||
|
assert_eq!(Paint::new("hi").italic().to_string(), "hi".to_string());
|
||||||
|
assert_eq!(Paint::new("hi").blink().to_string(), "hi".to_string());
|
||||||
|
assert_eq!(Paint::new("hi").invert().to_string(), "hi".to_string());
|
||||||
|
assert_eq!(Paint::new("hi").hidden().to_string(), "hi".to_string());
|
||||||
|
assert_eq!(
|
||||||
|
Paint::new("hi").strikethrough().to_string(),
|
||||||
|
"hi".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Paint::new("hi").strikethrough().wrap().to_string(),
|
||||||
|
"hi".to_string()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wrapping() {
|
||||||
|
let _guard = SERIAL.lock();
|
||||||
|
let inner = || format!("{} b {}", Paint::red("a"), Paint::green("c"));
|
||||||
|
let inner2 = || format!("0 {} 1", Paint::magenta(&inner()).wrap());
|
||||||
|
|
||||||
|
concolor::set(concolor::ColorChoice::Always);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
Paint::new("text/plain").wrap().to_string(),
|
||||||
|
"text/plain".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(Paint::new(&inner()).wrap().to_string(), inner());
|
||||||
|
assert_eq!(
|
||||||
|
Paint::new(&inner()).wrap().to_string(),
|
||||||
|
"\u{1b}[31ma\u{1b}[0m b \u{1b}[32mc\u{1b}[0m".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Paint::new(&inner()).fg(Blue).wrap().to_string(),
|
||||||
|
"\u{1b}[34m\u{1b}[31ma\u{1b}[0m\u{1b}[34m b \
|
||||||
|
\u{1b}[32mc\u{1b}[0m\u{1b}[34m\u{1b}[0m"
|
||||||
|
.to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(Paint::new(&inner2()).wrap().to_string(), inner2());
|
||||||
|
assert_eq!(
|
||||||
|
Paint::new(&inner2()).wrap().to_string(),
|
||||||
|
"0 \u{1b}[35m\u{1b}[31ma\u{1b}[0m\u{1b}[35m b \
|
||||||
|
\u{1b}[32mc\u{1b}[0m\u{1b}[35m\u{1b}[0m 1"
|
||||||
|
.to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Paint::new(&inner2()).fg(Blue).wrap().to_string(),
|
||||||
|
"\u{1b}[34m0 \u{1b}[35m\u{1b}[31ma\u{1b}[0m\u{1b}[34m\u{1b}[35m b \
|
||||||
|
\u{1b}[32mc\u{1b}[0m\u{1b}[34m\u{1b}[35m\u{1b}[0m\u{1b}[34m 1\u{1b}[0m"
|
||||||
|
.to_string()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
#[cfg(windows)]
|
||||||
|
mod windows_console {
|
||||||
|
use std::os::raw::c_void;
|
||||||
|
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
type c_ulong = u32;
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
type c_int = i32;
|
||||||
|
type DWORD = c_ulong;
|
||||||
|
type LPDWORD = *mut DWORD;
|
||||||
|
type HANDLE = *mut c_void;
|
||||||
|
type BOOL = c_int;
|
||||||
|
|
||||||
|
const ENABLE_VIRTUAL_TERMINAL_PROCESSING: DWORD = 0x0004;
|
||||||
|
const STD_OUTPUT_HANDLE: DWORD = 0xFFFFFFF5;
|
||||||
|
const STD_ERROR_HANDLE: DWORD = 0xFFFFFFF4;
|
||||||
|
const INVALID_HANDLE_VALUE: HANDLE = -1isize as HANDLE;
|
||||||
|
const FALSE: BOOL = 0;
|
||||||
|
const TRUE: BOOL = 1;
|
||||||
|
|
||||||
|
// This is the win32 console API, taken from the 'winapi' crate.
|
||||||
|
extern "system" {
|
||||||
|
fn GetStdHandle(nStdHandle: DWORD) -> HANDLE;
|
||||||
|
fn GetConsoleMode(hConsoleHandle: HANDLE, lpMode: LPDWORD) -> BOOL;
|
||||||
|
fn SetConsoleMode(hConsoleHandle: HANDLE, dwMode: DWORD) -> BOOL;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn get_handle(handle_num: DWORD) -> Result<HANDLE, ()> {
|
||||||
|
match GetStdHandle(handle_num) {
|
||||||
|
handle if handle == INVALID_HANDLE_VALUE => Err(()),
|
||||||
|
handle => Ok(handle),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn enable_vt(handle: HANDLE) -> Result<(), ()> {
|
||||||
|
let mut dw_mode: DWORD = 0;
|
||||||
|
if GetConsoleMode(handle, &mut dw_mode) == FALSE {
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
|
||||||
|
dw_mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;
|
||||||
|
match SetConsoleMode(handle, dw_mode) {
|
||||||
|
result if result == TRUE => Ok(()),
|
||||||
|
_ => Err(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn enable_ansi_colors_raw() -> Result<bool, ()> {
|
||||||
|
let stdout_handle = get_handle(STD_OUTPUT_HANDLE)?;
|
||||||
|
let stderr_handle = get_handle(STD_ERROR_HANDLE)?;
|
||||||
|
|
||||||
|
enable_vt(stdout_handle)?;
|
||||||
|
if stdout_handle != stderr_handle {
|
||||||
|
enable_vt(stderr_handle)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn enable_ansi_colors() -> bool {
|
||||||
|
unsafe { enable_ansi_colors_raw().unwrap_or(false) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,146 @@
|
||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
use super::Paint;
|
||||||
|
|
||||||
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
|
/// Text that can be displayed on the terminal, measured, truncated and padded.
|
||||||
|
pub trait Cell: Display {
|
||||||
|
/// Type after truncation.
|
||||||
|
type Truncated: Cell;
|
||||||
|
/// Type after padding.
|
||||||
|
type Padded: Cell;
|
||||||
|
|
||||||
|
/// Cell display width in number of terminal columns.
|
||||||
|
fn width(&self) -> usize;
|
||||||
|
/// Truncate cell if longer than given width. Shows the delimiter if truncated.
|
||||||
|
fn truncate(&self, width: usize, delim: &str) -> Self::Truncated;
|
||||||
|
/// Pad the cell so that it is the given width, while keeping the content left-aligned.
|
||||||
|
fn pad(&self, width: usize) -> Self::Padded;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cell for Paint<String> {
|
||||||
|
type Truncated = Self;
|
||||||
|
type Padded = Self;
|
||||||
|
|
||||||
|
fn width(&self) -> usize {
|
||||||
|
UnicodeWidthStr::width(self.content())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn truncate(&self, width: usize, delim: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
item: self.item.truncate(width, delim),
|
||||||
|
style: self.style,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pad(&self, width: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
item: self.item.pad(width),
|
||||||
|
style: self.style,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cell for Paint<&str> {
|
||||||
|
type Truncated = Paint<String>;
|
||||||
|
type Padded = Paint<String>;
|
||||||
|
|
||||||
|
fn width(&self) -> usize {
|
||||||
|
Cell::width(self.item)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn truncate(&self, width: usize, delim: &str) -> Paint<String> {
|
||||||
|
Paint {
|
||||||
|
item: self.item.truncate(width, delim),
|
||||||
|
style: self.style,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pad(&self, width: usize) -> Paint<String> {
|
||||||
|
Paint {
|
||||||
|
item: self.item.pad(width),
|
||||||
|
style: self.style,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cell for String {
|
||||||
|
type Truncated = Self;
|
||||||
|
type Padded = Self;
|
||||||
|
|
||||||
|
fn width(&self) -> usize {
|
||||||
|
Cell::width(self.as_str())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn truncate(&self, width: usize, delim: &str) -> Self {
|
||||||
|
self.as_str().truncate(width, delim)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pad(&self, width: usize) -> Self {
|
||||||
|
self.as_str().pad(width)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cell for str {
|
||||||
|
type Truncated = String;
|
||||||
|
type Padded = String;
|
||||||
|
|
||||||
|
fn width(&self) -> usize {
|
||||||
|
UnicodeWidthStr::width(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn truncate(&self, width: usize, delim: &str) -> String {
|
||||||
|
use unicode_segmentation::UnicodeSegmentation as _;
|
||||||
|
|
||||||
|
if width < Cell::width(self) {
|
||||||
|
let d = Cell::width(delim);
|
||||||
|
if width < d {
|
||||||
|
// If we can't even fit the delimiter, just return an empty string.
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
// Find the unicode byte boundary where the display width is the largest,
|
||||||
|
// while being smaller than the given max width.
|
||||||
|
let mut cols = 0; // Number of visual columns we need.
|
||||||
|
let mut boundary = 0; // Boundary in bytes.
|
||||||
|
for g in self.graphemes(true) {
|
||||||
|
let c = Cell::width(g);
|
||||||
|
if cols + c + d > width {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
boundary += g.len();
|
||||||
|
cols += c;
|
||||||
|
}
|
||||||
|
format!("{}{delim}", &self[..boundary])
|
||||||
|
} else {
|
||||||
|
self.to_owned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pad(&self, max: usize) -> String {
|
||||||
|
let width = Cell::width(self);
|
||||||
|
|
||||||
|
if width < max {
|
||||||
|
format!("{self}{}", " ".repeat(max - width))
|
||||||
|
} else {
|
||||||
|
self.to_owned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Cell + ?Sized> Cell for &T {
|
||||||
|
type Truncated = T::Truncated;
|
||||||
|
type Padded = T::Padded;
|
||||||
|
|
||||||
|
fn width(&self) -> usize {
|
||||||
|
T::width(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn truncate(&self, width: usize, delim: &str) -> Self::Truncated {
|
||||||
|
T::truncate(self, width, delim)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pad(&self, width: usize) -> Self::Padded {
|
||||||
|
T::pad(self, width)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
use std::ffi::OsString;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::process;
|
||||||
|
use std::{env, fs, io};
|
||||||
|
|
||||||
|
pub const COMMENT_FILE: &str = "RAD_COMMENT";
|
||||||
|
|
||||||
|
/// Allows for text input in the configured editor.
|
||||||
|
pub struct Editor {
|
||||||
|
path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Editor {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
fs::remove_file(&self.path).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Editor {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Editor {
|
||||||
|
/// Create a new editor.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let path = env::temp_dir().join(COMMENT_FILE);
|
||||||
|
|
||||||
|
Self { path }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the file extension.
|
||||||
|
pub fn extension(mut self, ext: &str) -> Self {
|
||||||
|
let ext = ext.trim_start_matches('.');
|
||||||
|
|
||||||
|
self.path.set_extension(ext);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Open the editor and return the edited text.
|
||||||
|
///
|
||||||
|
/// If the text hasn't changed from the initial contents of the editor,
|
||||||
|
/// return `None`.
|
||||||
|
pub fn edit(&mut self, initial: impl ToString) -> io::Result<Option<String>> {
|
||||||
|
let initial = initial.to_string();
|
||||||
|
let mut file = fs::OpenOptions::new()
|
||||||
|
.write(true)
|
||||||
|
.create(true)
|
||||||
|
.open(&self.path)?;
|
||||||
|
|
||||||
|
if file.metadata()?.len() == 0 {
|
||||||
|
file.write_all(initial.as_bytes())?;
|
||||||
|
if !initial.ends_with('\n') {
|
||||||
|
file.write_all(b"\n")?;
|
||||||
|
}
|
||||||
|
file.flush()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(cmd) = self::default_editor() else {
|
||||||
|
return Err(
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::NotFound,
|
||||||
|
"editor not configured: the `EDITOR` environment variable is not set"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
process::Command::new(cmd).arg(&self.path).spawn()?.wait()?;
|
||||||
|
|
||||||
|
let text = fs::read_to_string(&self.path)?;
|
||||||
|
let text = text.strip_prefix(&initial).unwrap_or(&text);
|
||||||
|
|
||||||
|
if text.trim().is_empty() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
Ok(Some(text.to_owned()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the default editor command.
|
||||||
|
pub fn default_editor() -> Option<OsString> {
|
||||||
|
if let Ok(visual) = env::var("VISUAL") {
|
||||||
|
if !visual.is_empty() {
|
||||||
|
return Some(visual.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Ok(editor) = env::var("EDITOR") {
|
||||||
|
if !editor.is_empty() {
|
||||||
|
return Some(editor.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use std::{fmt, time};
|
use std::{fmt, time};
|
||||||
|
|
||||||
pub use dialoguer::console::style;
|
pub use crate::terminal::{style, Paint};
|
||||||
|
|
||||||
use radicle::cob::{ObjectId, Timestamp};
|
use radicle::cob::{ObjectId, Timestamp};
|
||||||
use radicle::node::NodeId;
|
use radicle::node::NodeId;
|
||||||
|
|
@ -88,58 +88,74 @@ impl<'a> fmt::Display for Identity<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn negative<D: std::fmt::Display>(msg: D) -> String {
|
pub fn negative<D: std::fmt::Display>(msg: D) -> Paint<D> {
|
||||||
style(msg).red().bright().to_string()
|
Paint::red(msg).bold()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn positive<D: std::fmt::Display>(msg: D) -> String {
|
pub fn positive<D: std::fmt::Display>(msg: D) -> Paint<D> {
|
||||||
style(msg).green().bright().to_string()
|
Paint::green(msg).bold()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn secondary<D: std::fmt::Display>(msg: D) -> String {
|
pub fn secondary<D: std::fmt::Display>(msg: D) -> Paint<D> {
|
||||||
style(msg).blue().bright().to_string()
|
Paint::blue(msg).bold()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tertiary<D: std::fmt::Display>(msg: D) -> String {
|
pub fn tertiary<D: std::fmt::Display>(msg: D) -> Paint<D> {
|
||||||
style(msg).cyan().to_string()
|
Paint::cyan(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tertiary_bold<D: std::fmt::Display>(msg: D) -> String {
|
pub fn tertiary_bold<D: std::fmt::Display>(msg: D) -> Paint<D> {
|
||||||
style(msg).cyan().bold().to_string()
|
Paint::cyan(msg).bold()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn yellow<D: std::fmt::Display>(msg: D) -> String {
|
pub fn yellow<D: std::fmt::Display>(msg: D) -> Paint<D> {
|
||||||
style(msg).yellow().to_string()
|
Paint::yellow(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn highlight<D: std::fmt::Display>(input: D) -> String {
|
pub fn highlight<D: std::fmt::Debug + std::fmt::Display>(input: D) -> Paint<D> {
|
||||||
style(input).green().bright().to_string()
|
Paint::green(input).bold()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn badge_primary<D: std::fmt::Display>(input: D) -> String {
|
pub fn badge_primary<D: std::fmt::Display>(input: D) -> Paint<String> {
|
||||||
style(format!(" {input} ")).magenta().reverse().to_string()
|
if Paint::is_enabled() {
|
||||||
|
Paint::magenta(format!(" {input} ")).invert()
|
||||||
|
} else {
|
||||||
|
Paint::new(format!("❲{input}❳"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn badge_positive<D: std::fmt::Display>(input: D) -> String {
|
pub fn badge_positive<D: std::fmt::Display>(input: D) -> Paint<String> {
|
||||||
style(format!(" {input} ")).green().reverse().to_string()
|
if Paint::is_enabled() {
|
||||||
|
Paint::green(format!(" {input} ")).invert()
|
||||||
|
} else {
|
||||||
|
Paint::new(format!("❲{input}❳"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn badge_negative<D: std::fmt::Display>(input: D) -> String {
|
pub fn badge_negative<D: std::fmt::Display>(input: D) -> Paint<String> {
|
||||||
style(format!(" {input} ")).red().reverse().to_string()
|
if Paint::is_enabled() {
|
||||||
|
Paint::red(format!(" {input} ")).invert()
|
||||||
|
} else {
|
||||||
|
Paint::new(format!("❲{input}❳"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn badge_secondary<D: std::fmt::Display>(input: D) -> String {
|
pub fn badge_secondary<D: std::fmt::Display>(input: D) -> Paint<String> {
|
||||||
style(format!(" {input} ")).blue().reverse().to_string()
|
if Paint::is_enabled() {
|
||||||
|
Paint::blue(format!(" {input} ")).invert()
|
||||||
|
} else {
|
||||||
|
Paint::new(format!("❲{input}❳"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn bold<D: std::fmt::Display>(input: D) -> String {
|
pub fn bold<D: std::fmt::Display>(input: D) -> Paint<D> {
|
||||||
style(input).white().bright().bold().to_string()
|
Paint::white(input).bold()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn dim<D: std::fmt::Display>(input: D) -> String {
|
pub fn dim<D: std::fmt::Display>(input: D) -> Paint<D> {
|
||||||
style(input).dim().to_string()
|
Paint::new(input).dim()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn italic<D: std::fmt::Display>(input: D) -> String {
|
pub fn italic<D: std::fmt::Display>(input: D) -> Paint<D> {
|
||||||
style(input).italic().dim().to_string()
|
Paint::new(input).italic().dim()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,39 @@
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use dialoguer::{console::style, console::Style, theme::ColorfulTheme, Input, Password};
|
use inquire::ui::{ErrorMessageRenderConfig, StyleSheet, Styled};
|
||||||
|
use inquire::InquireError;
|
||||||
|
use inquire::{ui::Color, ui::RenderConfig, Confirm, CustomType, Password, Select};
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
use radicle::cob::issue::Issue;
|
use radicle::cob::issue::Issue;
|
||||||
use radicle::cob::thread::CommentId;
|
use radicle::cob::thread::{Comment, CommentId};
|
||||||
use radicle::crypto::ssh::keystore::Passphrase;
|
use radicle::crypto::ssh::keystore::{MemorySigner, Passphrase};
|
||||||
use radicle::crypto::Signer;
|
use radicle::crypto::Signer;
|
||||||
use radicle::profile;
|
use radicle::profile;
|
||||||
use radicle::profile::Profile;
|
use radicle::profile::Profile;
|
||||||
|
|
||||||
use radicle_crypto::ssh::keystore::MemorySigner;
|
|
||||||
|
|
||||||
use super::command;
|
use super::command;
|
||||||
use super::format;
|
use super::format;
|
||||||
use super::spinner::spinner;
|
use super::spinner::spinner;
|
||||||
use super::Error;
|
use super::Error;
|
||||||
|
use super::{style, Paint};
|
||||||
|
|
||||||
|
pub const ERROR_PREFIX: Paint<&str> = Paint::red("✗");
|
||||||
pub const TAB: &str = " ";
|
pub const TAB: &str = " ";
|
||||||
|
|
||||||
|
/// Render configuration.
|
||||||
|
pub static CONFIG: Lazy<RenderConfig> = Lazy::new(|| RenderConfig {
|
||||||
|
prompt: StyleSheet::new().with_fg(Color::LightCyan),
|
||||||
|
prompt_prefix: Styled::new("?").with_fg(Color::LightBlue),
|
||||||
|
answered_prompt_prefix: Styled::new("✓").with_fg(Color::LightGreen),
|
||||||
|
answer: StyleSheet::new(),
|
||||||
|
highlighted_option_prefix: Styled::new("*").with_fg(Color::LightYellow),
|
||||||
|
help_message: StyleSheet::new().with_fg(Color::DarkGrey),
|
||||||
|
error_message: ErrorMessageRenderConfig::default_colored()
|
||||||
|
.with_prefix(Styled::new("✗").with_fg(Color::LightRed)),
|
||||||
|
..RenderConfig::default_colored()
|
||||||
|
});
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! info {
|
macro_rules! info {
|
||||||
($($arg:tt)*) => ({
|
($($arg:tt)*) => ({
|
||||||
|
|
@ -45,17 +60,15 @@ pub use success;
|
||||||
pub use tip;
|
pub use tip;
|
||||||
|
|
||||||
pub fn success_args(args: fmt::Arguments) {
|
pub fn success_args(args: fmt::Arguments) {
|
||||||
println!("{} {args}", style("ok").green().reverse());
|
println!("{} {args}", Paint::green("✓"));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tip_args(args: fmt::Arguments) {
|
pub fn tip_args(args: fmt::Arguments) {
|
||||||
println!("{} {}", style("=>").blue(), style(format!("{args}")).dim());
|
println!("👉 {}", style(format!("{args}")).italic());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn width() -> Option<usize> {
|
pub fn columns() -> Option<usize> {
|
||||||
console::Term::stdout()
|
termion::terminal_size().map(|(cols, _)| cols as usize).ok()
|
||||||
.size_checked()
|
|
||||||
.map(|(_, cols)| cols as usize)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn headline(headline: &str) {
|
pub fn headline(headline: &str) {
|
||||||
|
|
@ -66,7 +79,7 @@ pub fn headline(headline: &str) {
|
||||||
|
|
||||||
pub fn header(header: &str) {
|
pub fn header(header: &str) {
|
||||||
println!();
|
println!();
|
||||||
println!("{}", style(format::yellow(header)).bold().underlined());
|
println!("{}", style(format::yellow(header)).bold().underline());
|
||||||
println!();
|
println!();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -95,9 +108,9 @@ pub fn help(name: &str, version: &str, description: &str, usage: &str) {
|
||||||
pub fn usage(name: &str, usage: &str) {
|
pub fn usage(name: &str, usage: &str) {
|
||||||
println!(
|
println!(
|
||||||
"{} {}\n{}",
|
"{} {}\n{}",
|
||||||
style("==").red(),
|
ERROR_PREFIX,
|
||||||
style(format!("Error: rad-{name}: invalid usage")).red(),
|
Paint::red(format!("Error: rad-{name}: invalid usage")),
|
||||||
style(prefixed(TAB, usage)).red().dim()
|
Paint::red(prefixed(TAB, usage)).dim()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -115,57 +128,39 @@ pub fn subcommand(msg: impl fmt::Display) {
|
||||||
|
|
||||||
pub fn warning(warning: &str) {
|
pub fn warning(warning: &str) {
|
||||||
println!(
|
println!(
|
||||||
"{} {} {}",
|
"{} {} {warning}",
|
||||||
style("**").yellow(),
|
Paint::yellow("!"),
|
||||||
style("Warning:").yellow().bold(),
|
Paint::yellow("Warning:").bold(),
|
||||||
style(warning).yellow()
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn error(error: impl fmt::Display) {
|
pub fn error(error: impl fmt::Display) {
|
||||||
println!("{} {}", style("==").red(), style(error).red());
|
println!("{ERROR_PREFIX} {error}");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fail(header: &str, error: &anyhow::Error) {
|
pub fn fail(header: &str, error: &anyhow::Error) {
|
||||||
let err = error.to_string();
|
let err = error.to_string();
|
||||||
let err = err.trim_end();
|
let err = err.trim_end();
|
||||||
let separator = if err.len() > 160 || err.contains('\n') {
|
let separator = if err.contains('\n') { ":\n" } else { ": " };
|
||||||
"\n"
|
|
||||||
} else {
|
|
||||||
" "
|
|
||||||
};
|
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
"{} {}{}{}",
|
"{ERROR_PREFIX} {}{}{error}",
|
||||||
style("==").red(),
|
Paint::red(header).bold(),
|
||||||
style(header).red().reverse(),
|
Paint::red(separator),
|
||||||
separator,
|
|
||||||
style(error).red().bold(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let cause = error.root_cause();
|
|
||||||
if cause.to_string() != error.to_string() {
|
|
||||||
println!(
|
|
||||||
"{} {}",
|
|
||||||
style("==").red().dim(),
|
|
||||||
style(error.root_cause()).red().dim()
|
|
||||||
);
|
|
||||||
blank();
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(Error::WithHint { hint, .. }) = error.downcast_ref::<Error>() {
|
if let Some(Error::WithHint { hint, .. }) = error.downcast_ref::<Error>() {
|
||||||
println!("{} {}", style("==").yellow(), style(hint).yellow(),);
|
println!("{} {}", Paint::yellow("×"), Paint::yellow(hint));
|
||||||
blank();
|
blank();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ask<D: fmt::Display>(prompt: D, default: bool) -> bool {
|
pub fn ask<D: fmt::Display>(prompt: D, default: bool) -> bool {
|
||||||
dialoguer::Confirm::new()
|
let prompt = format!("{} {}", Paint::blue("?".to_owned()), prompt);
|
||||||
.with_prompt(format!("{} {}", style(" ⤷".to_owned()).cyan(), prompt))
|
|
||||||
.wait_for_newline(false)
|
Confirm::new(&prompt)
|
||||||
.default(true)
|
.with_default(default)
|
||||||
.default(default)
|
.prompt()
|
||||||
.interact()
|
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -182,209 +177,95 @@ pub fn signer(profile: &Profile) -> anyhow::Result<Box<dyn Signer>> {
|
||||||
if let Ok(signer) = profile.signer() {
|
if let Ok(signer) = profile.signer() {
|
||||||
return Ok(signer);
|
return Ok(signer);
|
||||||
}
|
}
|
||||||
|
let passphrase = passphrase()?;
|
||||||
let passphrase = secret_input();
|
|
||||||
let spinner = spinner("Unsealing key...");
|
let spinner = spinner("Unsealing key...");
|
||||||
let signer = MemorySigner::load(&profile.keystore, passphrase)?;
|
let signer = MemorySigner::load(&profile.keystore, passphrase)?;
|
||||||
|
|
||||||
spinner.finish();
|
spinner.finish();
|
||||||
|
|
||||||
Ok(signer.boxed())
|
Ok(signer.boxed())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn theme() -> ColorfulTheme {
|
pub fn input<S, E>(message: &str, default: Option<S>) -> anyhow::Result<S>
|
||||||
ColorfulTheme {
|
|
||||||
success_prefix: style("ok".to_owned()).for_stderr().green().reverse(),
|
|
||||||
prompt_prefix: style(" ⤷".to_owned()).cyan().dim().for_stderr(),
|
|
||||||
prompt_suffix: style("·".to_owned()).cyan().for_stderr(),
|
|
||||||
prompt_style: Style::new().cyan().bold().for_stderr(),
|
|
||||||
active_item_style: Style::new().for_stderr().yellow().reverse(),
|
|
||||||
active_item_prefix: style("*".to_owned()).yellow().for_stderr(),
|
|
||||||
picked_item_prefix: style("*".to_owned()).yellow().for_stderr(),
|
|
||||||
inactive_item_prefix: style(" ".to_string()).for_stderr(),
|
|
||||||
inactive_item_style: Style::new().yellow().for_stderr(),
|
|
||||||
error_prefix: style("⤹ Error:".to_owned()).red().for_stderr(),
|
|
||||||
success_suffix: style("·".to_owned()).cyan().for_stderr(),
|
|
||||||
|
|
||||||
..ColorfulTheme::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn text_input<S, E>(message: &str, default: Option<S>) -> anyhow::Result<S>
|
|
||||||
where
|
where
|
||||||
S: fmt::Display + std::str::FromStr<Err = E> + Clone,
|
S: fmt::Display + std::str::FromStr<Err = E> + Clone,
|
||||||
E: fmt::Debug + fmt::Display,
|
E: fmt::Debug + fmt::Display,
|
||||||
{
|
{
|
||||||
let theme = theme();
|
let input = CustomType::<S>::new(message).with_render_config(*CONFIG);
|
||||||
let mut input: Input<S> = Input::with_theme(&theme);
|
|
||||||
|
|
||||||
let value = match default {
|
let value = match default {
|
||||||
Some(default) => input
|
Some(default) => input.with_default(default).prompt()?,
|
||||||
.with_prompt(message)
|
None => input.prompt()?,
|
||||||
.with_initial_text(default.to_string())
|
|
||||||
.interact_text()?,
|
|
||||||
None => input.with_prompt(message).interact_text()?,
|
|
||||||
};
|
};
|
||||||
Ok(value)
|
Ok(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone)]
|
pub fn passphrase() -> Result<Passphrase, anyhow::Error> {
|
||||||
pub struct Optional<T> {
|
if let Some(p) = profile::env::passphrase() {
|
||||||
option: Option<T>,
|
Ok(p)
|
||||||
}
|
} else {
|
||||||
|
Ok(Passphrase::from(
|
||||||
impl<T: fmt::Display> fmt::Display for Optional<T> {
|
Password::new("Passphrase:")
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
.with_render_config(*CONFIG)
|
||||||
if let Some(val) = &self.option {
|
.with_display_mode(inquire::PasswordDisplayMode::Masked)
|
||||||
write!(f, "{val}")
|
.without_confirmation()
|
||||||
} else {
|
.prompt()?,
|
||||||
write!(f, "")
|
))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: FromStr> FromStr for Optional<T> {
|
pub fn passphrase_confirm() -> Result<Passphrase, anyhow::Error> {
|
||||||
type Err = <T as FromStr>::Err;
|
if let Some(p) = profile::env::passphrase() {
|
||||||
|
Ok(p)
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
} else {
|
||||||
if s.is_empty() {
|
Ok(Passphrase::from(
|
||||||
return Ok(Optional { option: None });
|
Password::new("Passphrase:")
|
||||||
}
|
.with_render_config(*CONFIG)
|
||||||
let val: T = s.parse()?;
|
.with_display_mode(inquire::PasswordDisplayMode::Masked)
|
||||||
|
.with_custom_confirmation_message("Repeat passphrase:")
|
||||||
Ok(Self { option: Some(val) })
|
.with_custom_confirmation_error_message("The passphrases don't match.")
|
||||||
|
.prompt()?,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn text_input_optional<S, E>(
|
pub fn passphrase_stdin() -> Result<Passphrase, anyhow::Error> {
|
||||||
message: &str,
|
|
||||||
initial: Option<String>,
|
|
||||||
) -> anyhow::Result<Option<S>>
|
|
||||||
where
|
|
||||||
S: fmt::Display + fmt::Debug + FromStr<Err = E> + Clone,
|
|
||||||
E: fmt::Debug + fmt::Display,
|
|
||||||
{
|
|
||||||
let theme = theme();
|
|
||||||
let mut input: Input<Optional<S>> = Input::with_theme(&theme);
|
|
||||||
|
|
||||||
if let Some(init) = initial {
|
|
||||||
input.with_initial_text(init);
|
|
||||||
}
|
|
||||||
let value = input
|
|
||||||
.with_prompt(message)
|
|
||||||
.allow_empty(true)
|
|
||||||
.interact_text()?;
|
|
||||||
|
|
||||||
Ok(value.option)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn secret_input() -> Passphrase {
|
|
||||||
secret_input_with_prompt("Passphrase")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: This prompt shows success just for entering a password,
|
|
||||||
// even if the password is later found out to be wrong.
|
|
||||||
// We should handle this differently.
|
|
||||||
pub fn secret_input_with_prompt(prompt: &str) -> Passphrase {
|
|
||||||
Passphrase::from(
|
|
||||||
Password::with_theme(&theme())
|
|
||||||
.allow_empty_password(true)
|
|
||||||
.with_prompt(prompt)
|
|
||||||
.interact()
|
|
||||||
.unwrap(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn secret_input_with_confirmation() -> Passphrase {
|
|
||||||
Passphrase::from(
|
|
||||||
Password::with_theme(&theme())
|
|
||||||
.with_prompt("Passphrase")
|
|
||||||
.with_confirmation("Repeat passphrase", "Error: the passphrases don't match.")
|
|
||||||
.interact()
|
|
||||||
.unwrap(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn secret_stdin() -> Result<Passphrase, anyhow::Error> {
|
|
||||||
let mut input = String::new();
|
let mut input = String::new();
|
||||||
std::io::stdin().read_line(&mut input)?;
|
std::io::stdin().read_line(&mut input)?;
|
||||||
|
|
||||||
Ok(Passphrase::from(input.trim_end().to_owned()))
|
Ok(Passphrase::from(input.trim_end().to_owned()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_passphrase(stdin: bool, confirm: bool) -> Result<Passphrase, anyhow::Error> {
|
pub fn select<'a, T>(
|
||||||
let passphrase = match profile::env::read_passphrase() {
|
prompt: &str,
|
||||||
Some(input) => input,
|
options: &'a [T],
|
||||||
None => {
|
active: &'a T,
|
||||||
if stdin {
|
) -> Result<Option<&'a T>, InquireError>
|
||||||
secret_stdin()?
|
|
||||||
} else if confirm {
|
|
||||||
secret_input_with_confirmation()
|
|
||||||
} else {
|
|
||||||
secret_input()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(passphrase)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn select<'a, T>(options: &'a [T], active: &'a T) -> Option<&'a T>
|
|
||||||
where
|
where
|
||||||
T: fmt::Display + Eq + PartialEq,
|
T: fmt::Display + Eq + PartialEq,
|
||||||
{
|
{
|
||||||
let theme = theme();
|
|
||||||
let active = options.iter().position(|o| o == active);
|
let active = options.iter().position(|o| o == active);
|
||||||
let mut selection = dialoguer::Select::with_theme(&theme);
|
let selection =
|
||||||
|
Select::new(prompt, options.iter().collect::<Vec<_>>()).with_render_config(*CONFIG);
|
||||||
|
|
||||||
if let Some(active) = active {
|
if let Some(active) = active {
|
||||||
selection.default(active);
|
selection.with_starting_cursor(active).prompt_skippable()
|
||||||
|
} else {
|
||||||
|
selection.prompt_skippable()
|
||||||
}
|
}
|
||||||
let result = selection
|
|
||||||
.items(&options.iter().map(|p| p.to_string()).collect::<Vec<_>>())
|
|
||||||
.interact_opt()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
result.map(|i| &options[i])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select_with_prompt<'a, T>(prompt: &str, options: &'a [T], active: &'a T) -> Option<&'a T>
|
pub fn comment_select(issue: &Issue) -> Option<(&CommentId, &Comment)> {
|
||||||
where
|
let comments = issue.comments().collect::<Vec<_>>();
|
||||||
T: fmt::Display + Eq + PartialEq,
|
let selection = Select::new(
|
||||||
{
|
"Which comment do you want to react to?",
|
||||||
let theme = theme();
|
(0..comments.len()).collect(),
|
||||||
let active = options.iter().position(|o| o == active);
|
)
|
||||||
let mut selection = dialoguer::Select::with_theme(&theme);
|
.with_render_config(*CONFIG)
|
||||||
|
.with_formatter(&|i| comments[i.index].1.body().to_owned())
|
||||||
|
.prompt()
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
selection.with_prompt(prompt);
|
comments.get(selection).copied()
|
||||||
|
|
||||||
if let Some(active) = active {
|
|
||||||
selection.default(active);
|
|
||||||
}
|
|
||||||
let result = selection
|
|
||||||
.items(&options.iter().map(|p| p.to_string()).collect::<Vec<_>>())
|
|
||||||
.interact_opt()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
result.map(|i| &options[i])
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn comment_select(issue: &Issue) -> Option<CommentId> {
|
|
||||||
let selection = dialoguer::Select::with_theme(&theme())
|
|
||||||
.with_prompt("Which comment do you want to react to?")
|
|
||||||
.item(issue.description().unwrap_or_default())
|
|
||||||
.items(
|
|
||||||
&issue
|
|
||||||
.comments()
|
|
||||||
.map(|(_, i)| i.body().to_owned())
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
)
|
|
||||||
.default(0)
|
|
||||||
.interact_opt()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
selection
|
|
||||||
.and_then(|n| issue.comments().nth(n))
|
|
||||||
.map(|(id, _)| *id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn markdown(content: &str) {
|
pub fn markdown(content: &str) {
|
||||||
|
|
@ -393,10 +274,6 @@ pub fn markdown(content: &str) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _info(args: std::fmt::Arguments) {
|
|
||||||
println!("{args}");
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod proposal {
|
pub mod proposal {
|
||||||
use std::fmt::Write as _;
|
use std::fmt::Write as _;
|
||||||
|
|
||||||
|
|
@ -406,44 +283,44 @@ pub mod proposal {
|
||||||
identity::Identity,
|
identity::Identity,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{super::format, theme};
|
use super::*;
|
||||||
|
use crate::terminal::format;
|
||||||
|
|
||||||
pub fn revision_select(
|
pub fn revision_select(
|
||||||
proposal: &Proposal,
|
proposal: &Proposal,
|
||||||
) -> Option<(&identity::RevisionId, &identity::Revision)> {
|
) -> Option<(&identity::RevisionId, &identity::Revision)> {
|
||||||
let selection = dialoguer::Select::with_theme(&theme())
|
let revisions = proposal.revisions().collect::<Vec<_>>();
|
||||||
.with_prompt("Which revision do you want to select?")
|
let selection = Select::new(
|
||||||
.items(
|
"Which revision do you want to select?",
|
||||||
&proposal
|
(0..revisions.len()).collect(),
|
||||||
.revisions()
|
)
|
||||||
.map(|(rid, _)| rid.to_string())
|
.with_vim_mode(true)
|
||||||
.collect::<Vec<_>>(),
|
.with_formatter(&|ix| revisions[ix.index].0.to_string())
|
||||||
)
|
.with_render_config(*CONFIG)
|
||||||
.default(0)
|
.prompt()
|
||||||
.interact_opt()
|
.ok()?;
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
selection.and_then(|n| proposal.revisions().nth(n))
|
revisions.get(selection).copied()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn revision_commit_select<'a>(
|
pub fn revision_commit_select<'a>(
|
||||||
proposal: &'a Proposal,
|
proposal: &'a Proposal,
|
||||||
previous: &'a Identity<Oid>,
|
previous: &'a Identity<Oid>,
|
||||||
) -> Option<(&'a identity::RevisionId, &'a identity::Revision)> {
|
) -> Option<(&'a identity::RevisionId, &'a identity::Revision)> {
|
||||||
let selection = dialoguer::Select::with_theme(&theme())
|
let revisions = proposal
|
||||||
.with_prompt("Which revision do you want to commit?")
|
.revisions()
|
||||||
.items(
|
.filter(|(_, r)| r.is_quorum_reached(previous))
|
||||||
&proposal
|
.collect::<Vec<_>>();
|
||||||
.revisions()
|
let selection = Select::new(
|
||||||
.filter(|(_, r)| r.is_quorum_reached(previous))
|
"Which revision do you want to commit?",
|
||||||
.map(|(rid, _)| rid.to_string())
|
(0..revisions.len()).collect(),
|
||||||
.collect::<Vec<_>>(),
|
)
|
||||||
)
|
.with_formatter(&|ix| revisions[ix.index].0.to_string())
|
||||||
.default(0)
|
.with_render_config(*CONFIG)
|
||||||
.interact_opt()
|
.prompt()
|
||||||
.unwrap();
|
.ok()?;
|
||||||
|
|
||||||
selection.and_then(|n| proposal.revisions().nth(n))
|
revisions.get(selection).copied()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn diff(proposal: &identity::Revision, previous: &Identity<Oid>) -> anyhow::Result<String> {
|
pub fn diff(proposal: &identity::Revision, previous: &Identity<Oid>) -> anyhow::Result<String> {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use radicle::git;
|
use radicle::git;
|
||||||
|
|
||||||
use crate::terminal as term;
|
use crate::terminal as term;
|
||||||
|
use crate::terminal::cell::Cell as _;
|
||||||
|
|
||||||
/// The user supplied `Patch` description.
|
/// The user supplied `Patch` description.
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
|
@ -18,15 +19,14 @@ impl Message {
|
||||||
pub fn get(self, help: &str) -> String {
|
pub fn get(self, help: &str) -> String {
|
||||||
let comment = match self {
|
let comment = match self {
|
||||||
Message::Edit => term::Editor::new()
|
Message::Edit => term::Editor::new()
|
||||||
.require_save(true)
|
.extension("markdown")
|
||||||
.trim_newlines(true)
|
|
||||||
.extension(".markdown")
|
|
||||||
.edit(help)
|
.edit(help)
|
||||||
.unwrap(),
|
.ok()
|
||||||
|
.flatten(),
|
||||||
Message::Blank => None,
|
Message::Blank => None,
|
||||||
Message::Text(c) => Some(c),
|
Message::Text(c) => Some(c),
|
||||||
};
|
};
|
||||||
let comment = comment.unwrap_or_default().replace(help, "");
|
let comment = comment.unwrap_or_default();
|
||||||
let comment = comment.trim();
|
let comment = comment.trim();
|
||||||
|
|
||||||
comment.to_owned()
|
comment.to_owned()
|
||||||
|
|
@ -57,7 +57,7 @@ pub fn list_commits(commits: &[git::raw::Commit]) -> anyhow::Result<()> {
|
||||||
.unwrap_or_else(|| commit.message_bytes());
|
.unwrap_or_else(|| commit.message_bytes());
|
||||||
table.push([
|
table.push([
|
||||||
term::format::secondary(term::format::oid(commit.id())),
|
term::format::secondary(term::format::oid(commit.id())),
|
||||||
term::format::italic(String::from_utf8_lossy(message)),
|
term::format::italic(String::from_utf8_lossy(message).to_string()),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
table.render();
|
table.render();
|
||||||
|
|
@ -100,6 +100,6 @@ pub fn print_title_desc(title: &str, description: &str) {
|
||||||
term::blank();
|
term::blank();
|
||||||
term::print(term::format::dim(format!(
|
term::print(term::format::dim(format!(
|
||||||
"╰{}",
|
"╰{}",
|
||||||
"─".repeat(term::text_width(title_pretty) - 1)
|
"─".repeat(title_pretty.to_string().width() - 1)
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,77 +1,171 @@
|
||||||
use dialoguer::console::style;
|
use std::io::Write;
|
||||||
use indicatif::{ProgressBar, ProgressFinish, ProgressStyle};
|
use std::mem::ManuallyDrop;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::{fmt, io, thread, time};
|
||||||
|
|
||||||
use crate::terminal as term;
|
use crate::terminal::io::ERROR_PREFIX;
|
||||||
|
use crate::terminal::Paint;
|
||||||
|
|
||||||
pub struct Spinner {
|
/// How much time to wait between spinner animation updates.
|
||||||
progress: ProgressBar,
|
pub const DEFAULT_TICK: time::Duration = time::Duration::from_millis(99);
|
||||||
message: String,
|
/// The spinner animation strings.
|
||||||
|
pub const DEFAULT_STYLE: [Paint<&'static str>; 4] = [
|
||||||
|
Paint::magenta("◢"),
|
||||||
|
Paint::cyan("◣"),
|
||||||
|
Paint::magenta("◤"),
|
||||||
|
Paint::blue("◥"),
|
||||||
|
];
|
||||||
|
|
||||||
|
struct Progress {
|
||||||
|
state: State,
|
||||||
|
message: Paint<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for Spinner {
|
impl Progress {
|
||||||
fn drop(&mut self) {
|
fn new(message: Paint<String>) -> Self {
|
||||||
// TODO: Set error that will be output on fail.
|
Self {
|
||||||
if !self.progress.is_finished() {
|
state: State::Running { cursor: 0 },
|
||||||
self.set_failed();
|
message,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Spinner {
|
enum State {
|
||||||
pub fn finish(&self) {
|
Running { cursor: usize },
|
||||||
self.progress.finish_and_clear();
|
Canceled,
|
||||||
term::success!("{}", &self.message);
|
Done,
|
||||||
}
|
Error,
|
||||||
|
}
|
||||||
|
|
||||||
pub fn done(self) {
|
/// A progress spinner.
|
||||||
self.progress.finish_and_clear();
|
pub struct Spinner {
|
||||||
term::info!("{}", &self.message);
|
progress: Arc<Mutex<Progress>>,
|
||||||
}
|
handle: ManuallyDrop<thread::JoinHandle<()>>,
|
||||||
|
}
|
||||||
|
|
||||||
pub fn failed(mut self) {
|
impl Drop for Spinner {
|
||||||
self.set_failed();
|
fn drop(&mut self) {
|
||||||
}
|
if let Ok(mut progress) = self.progress.lock() {
|
||||||
|
if let State::Running { .. } = progress.state {
|
||||||
pub fn error(mut self, msg: impl ToString) {
|
progress.state = State::Canceled;
|
||||||
let msg = msg.to_string();
|
}
|
||||||
|
}
|
||||||
self.message = format!("{} error: {}", self.message, msg);
|
unsafe { ManuallyDrop::take(&mut self.handle) }
|
||||||
self.set_failed();
|
.join()
|
||||||
}
|
.unwrap();
|
||||||
|
|
||||||
pub fn clear(self) {
|
|
||||||
self.progress.finish_and_clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn message(&mut self, msg: impl ToString) {
|
|
||||||
let msg = msg.to_string();
|
|
||||||
|
|
||||||
self.progress.set_message(msg.clone());
|
|
||||||
self.message = msg;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_failed(&mut self) {
|
|
||||||
self.progress.finish_and_clear();
|
|
||||||
term::println(style("!!").red().reverse(), &self.message);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Spinner {
|
||||||
|
/// Mark the spinner as successfully completed.
|
||||||
|
pub fn finish(self) {
|
||||||
|
if let Ok(mut progress) = self.progress.lock() {
|
||||||
|
progress.state = State::Done;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mark the spinner as failed. This cancels the spinner.
|
||||||
|
pub fn failed(self) {
|
||||||
|
if let Ok(mut progress) = self.progress.lock() {
|
||||||
|
progress.state = State::Error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cancel the spinner with an error.
|
||||||
|
pub fn error(self, msg: impl fmt::Display) {
|
||||||
|
if let Ok(mut progress) = self.progress.lock() {
|
||||||
|
progress.state = State::Error;
|
||||||
|
progress.message = Paint::new(format!(
|
||||||
|
"{} {} {}",
|
||||||
|
progress.message,
|
||||||
|
Paint::red("error:"),
|
||||||
|
msg
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the spinner's message.
|
||||||
|
pub fn message(&mut self, msg: impl fmt::Display) {
|
||||||
|
let msg = msg.to_string();
|
||||||
|
|
||||||
|
if let Ok(mut progress) = self.progress.lock() {
|
||||||
|
progress.message = Paint::new(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new spinner with the given message.
|
||||||
pub fn spinner(message: impl ToString) -> Spinner {
|
pub fn spinner(message: impl ToString) -> Spinner {
|
||||||
let message = message.to_string();
|
let message = message.to_string();
|
||||||
let style = ProgressStyle::default_spinner()
|
let progress = Arc::new(Mutex::new(Progress::new(Paint::new(message))));
|
||||||
.tick_strings(&[
|
let handle = thread::spawn({
|
||||||
&style("\\ ").yellow().to_string(),
|
let progress = progress.clone();
|
||||||
&style("| ").yellow().to_string(),
|
|
||||||
&style("/ ").yellow().to_string(),
|
|
||||||
&style("| ").yellow().to_string(),
|
|
||||||
])
|
|
||||||
.template("{spinner} {msg}")
|
|
||||||
.on_finish(ProgressFinish::AndClear);
|
|
||||||
|
|
||||||
let progress = ProgressBar::new(!0);
|
move || {
|
||||||
progress.set_style(style);
|
let mut stdout = io::stdout();
|
||||||
progress.enable_steady_tick(99);
|
let mut stderr = termion::cursor::HideCursor::from(io::stderr());
|
||||||
progress.set_message(message.clone());
|
|
||||||
|
|
||||||
Spinner { message, progress }
|
loop {
|
||||||
|
let Ok(mut progress) = progress.lock() else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
match &mut *progress {
|
||||||
|
Progress {
|
||||||
|
state: State::Running { cursor },
|
||||||
|
message,
|
||||||
|
} => {
|
||||||
|
let spinner = DEFAULT_STYLE[*cursor];
|
||||||
|
|
||||||
|
write!(
|
||||||
|
stderr,
|
||||||
|
"{}{}{spinner} {message}",
|
||||||
|
termion::cursor::Save,
|
||||||
|
termion::clear::AfterCursor,
|
||||||
|
)
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
write!(stderr, "{}", termion::cursor::Restore).ok();
|
||||||
|
|
||||||
|
*cursor += 1;
|
||||||
|
*cursor %= DEFAULT_STYLE.len();
|
||||||
|
}
|
||||||
|
Progress {
|
||||||
|
state: State::Done,
|
||||||
|
message,
|
||||||
|
} => {
|
||||||
|
write!(stderr, "{}", termion::clear::AfterCursor).ok();
|
||||||
|
writeln!(stdout, "{} {message}", Paint::green("✓")).ok();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Progress {
|
||||||
|
state: State::Canceled,
|
||||||
|
message,
|
||||||
|
} => {
|
||||||
|
write!(stderr, "{}", termion::clear::AfterCursor).ok();
|
||||||
|
writeln!(
|
||||||
|
stdout,
|
||||||
|
"{ERROR_PREFIX} {message} {}",
|
||||||
|
Paint::red("<canceled>")
|
||||||
|
)
|
||||||
|
.ok();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Progress {
|
||||||
|
state: State::Error,
|
||||||
|
message,
|
||||||
|
} => {
|
||||||
|
writeln!(stdout, "{ERROR_PREFIX} {message}").ok();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
drop(progress);
|
||||||
|
thread::sleep(DEFAULT_TICK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Spinner {
|
||||||
|
progress,
|
||||||
|
handle: ManuallyDrop::new(handle),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,18 +9,31 @@
|
||||||
//! t.push(["aphid", "lacewing"]);
|
//! t.push(["aphid", "lacewing"]);
|
||||||
//! t.push(["spider mite", "ladybug"]);
|
//! t.push(["spider mite", "ladybug"]);
|
||||||
//! t.render();
|
//! t.render();
|
||||||
//! // pest biological control
|
|
||||||
//! // aphid ladybug
|
|
||||||
//! // spider mite persimilis
|
|
||||||
//! ```
|
//! ```
|
||||||
|
//! Output:
|
||||||
use std::fmt::Write;
|
//! ``` plain
|
||||||
|
//! pest biological control
|
||||||
|
//! aphid ladybug
|
||||||
|
//! spider mite persimilis
|
||||||
|
//! ```
|
||||||
|
use std::io;
|
||||||
|
|
||||||
use crate::terminal as term;
|
use crate::terminal as term;
|
||||||
|
use crate::terminal::cell::Cell;
|
||||||
|
|
||||||
|
/// Used to specify maximum width or height.
|
||||||
|
#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)]
|
||||||
|
pub struct Max {
|
||||||
|
width: Option<usize>,
|
||||||
|
height: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct TableOptions {
|
pub struct TableOptions {
|
||||||
|
/// Whether the table should be allowed to overflow.
|
||||||
pub overflow: bool,
|
pub overflow: bool,
|
||||||
|
/// The maximum width and height.
|
||||||
|
pub max: Max,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|
@ -49,16 +62,20 @@ impl<const W: usize> Table<W> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn push(&mut self, row: [impl ToString; W]) {
|
pub fn push(&mut self, row: [impl Cell; W]) {
|
||||||
let row = row.map(|s| s.to_string());
|
let row = row.map(|s| s.to_string());
|
||||||
for (i, cell) in row.iter().enumerate() {
|
for (i, cell) in row.iter().enumerate() {
|
||||||
self.widths[i] = self.widths[i].max(console::measure_text_width(cell));
|
self.widths[i] = self.widths[i].max(cell.width());
|
||||||
}
|
}
|
||||||
self.rows.push(row);
|
self.rows.push(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render(self) {
|
pub fn render(self) {
|
||||||
let width = term::width(); // Terminal width.
|
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 {
|
for row in &self.rows {
|
||||||
let mut output = String::new();
|
let mut output = String::new();
|
||||||
|
|
@ -66,27 +83,25 @@ impl<const W: usize> Table<W> {
|
||||||
|
|
||||||
for (i, cell) in row.iter().enumerate() {
|
for (i, cell) in row.iter().enumerate() {
|
||||||
if i == cells - 1 || self.opts.overflow {
|
if i == cells - 1 || self.opts.overflow {
|
||||||
write!(output, "{cell}").ok();
|
output.push_str(cell.to_string().as_str());
|
||||||
} else {
|
} else {
|
||||||
write!(
|
output.push_str(cell.pad(self.widths[i]).as_str());
|
||||||
output,
|
output.push(' ');
|
||||||
"{} ",
|
|
||||||
console::pad_str(cell, self.widths[i], console::Alignment::Left, None)
|
|
||||||
)
|
|
||||||
.ok();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let output = output.trim_end();
|
let output = output.trim_end();
|
||||||
println!(
|
writeln!(
|
||||||
|
writer,
|
||||||
"{}",
|
"{}",
|
||||||
if let Some(width) = width {
|
if let Some(width) = width {
|
||||||
console::truncate_str(output, width - 1, "…")
|
output.truncate(width, "…")
|
||||||
} else {
|
} else {
|
||||||
output.into()
|
output.into()
|
||||||
}
|
}
|
||||||
);
|
)?;
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_tree(self) {
|
pub fn render_tree(self) {
|
||||||
|
|
@ -97,12 +112,116 @@ impl<const W: usize> Table<W> {
|
||||||
print!("└── ");
|
print!("└── ");
|
||||||
}
|
}
|
||||||
for (i, cell) in row.iter().enumerate() {
|
for (i, cell) in row.iter().enumerate() {
|
||||||
print!(
|
print!("{} ", cell.pad(self.widths[i]));
|
||||||
"{} ",
|
|
||||||
console::pad_str(cell, self.widths[i], console::Alignment::Left, None)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
println!();
|
println!();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_truncate() {
|
||||||
|
assert_eq!("🍍".truncate(1, "…"), String::from("…"));
|
||||||
|
assert_eq!("🍍".truncate(1, ""), String::from(""));
|
||||||
|
assert_eq!("🍍🍍".truncate(2, "…"), String::from("…"));
|
||||||
|
assert_eq!("🍍🍍".truncate(3, "…"), String::from("🍍…"));
|
||||||
|
assert_eq!("🍍".truncate(1, "🍎"), String::from(""));
|
||||||
|
assert_eq!("🍍".truncate(2, "🍎"), String::from("🍍"));
|
||||||
|
assert_eq!("🍍🍍".truncate(3, "🍎"), String::from("🍎"));
|
||||||
|
assert_eq!("🍍🍍🍍".truncate(4, "🍎"), String::from("🍍🍎"));
|
||||||
|
assert_eq!("hello".truncate(3, "…"), String::from("he…"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_table() {
|
||||||
|
let mut s = Vec::new();
|
||||||
|
let mut t = Table::new(TableOptions::default());
|
||||||
|
|
||||||
|
t.push(["pineapple", "rosemary"]);
|
||||||
|
t.push(["apples", "pears"]);
|
||||||
|
t.write(&mut s).unwrap();
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
assert_eq!(
|
||||||
|
String::from_utf8_lossy(&s),
|
||||||
|
[
|
||||||
|
"pineapple rosemary\n",
|
||||||
|
"apples pears\n"
|
||||||
|
].join("")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_table_truncate() {
|
||||||
|
let mut s = Vec::new();
|
||||||
|
let mut t = Table::new(TableOptions {
|
||||||
|
max: Max {
|
||||||
|
width: Some(16),
|
||||||
|
height: None,
|
||||||
|
},
|
||||||
|
..TableOptions::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
t.push(["pineapple", "rosemary"]);
|
||||||
|
t.push(["apples", "pears"]);
|
||||||
|
t.write(&mut s).unwrap();
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
assert_eq!(
|
||||||
|
String::from_utf8_lossy(&s),
|
||||||
|
[
|
||||||
|
"pineapple rosem…\n",
|
||||||
|
"apples pears\n"
|
||||||
|
].join("")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_table_unicode() {
|
||||||
|
let mut s = Vec::new();
|
||||||
|
let mut t = Table::new(TableOptions::default());
|
||||||
|
|
||||||
|
t.push(["🍍pineapple", "__rosemary", "__sage"]);
|
||||||
|
t.push(["__pears", "🍎apples", "🍌bananas"]);
|
||||||
|
t.write(&mut s).unwrap();
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
assert_eq!(
|
||||||
|
String::from_utf8_lossy(&s),
|
||||||
|
[
|
||||||
|
"🍍pineapple __rosemary __sage\n",
|
||||||
|
"__pears 🍎apples 🍌bananas\n"
|
||||||
|
].join("")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_table_unicode_truncate() {
|
||||||
|
let mut s = Vec::new();
|
||||||
|
let mut t = Table::new(TableOptions {
|
||||||
|
max: Max {
|
||||||
|
width: Some(16),
|
||||||
|
height: None,
|
||||||
|
},
|
||||||
|
..TableOptions::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
t.push(["🍍pineapple", "__rosemary"]);
|
||||||
|
t.push(["__pears", "🍎apples"]);
|
||||||
|
t.write(&mut s).unwrap();
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
assert_eq!(
|
||||||
|
String::from_utf8_lossy(&s),
|
||||||
|
[
|
||||||
|
"🍍pineapple __r…\n",
|
||||||
|
"__pears 🍎a…\n"
|
||||||
|
].join("")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
use crate::terminal as term;
|
use crate::terminal as term;
|
||||||
|
use crate::terminal::cell::Cell as _;
|
||||||
|
|
||||||
pub struct TextBox {
|
pub struct TextBox {
|
||||||
pub body: String,
|
pub body: String,
|
||||||
|
|
@ -32,14 +33,8 @@ impl TextBox {
|
||||||
|
|
||||||
impl fmt::Display for TextBox {
|
impl fmt::Display for TextBox {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
let mut width = self
|
let mut width = self.body.lines().map(|l| l.width()).max().unwrap_or(0) + 2;
|
||||||
.body
|
if let Some(max) = term::columns() {
|
||||||
.lines()
|
|
||||||
.map(console::measure_text_width)
|
|
||||||
.max()
|
|
||||||
.unwrap_or(0)
|
|
||||||
+ 2;
|
|
||||||
if let Some(max) = term::width() {
|
|
||||||
if width + 2 > max {
|
if width + 2 > max {
|
||||||
width = max - 2
|
width = max - 2
|
||||||
}
|
}
|
||||||
|
|
@ -53,11 +48,7 @@ impl fmt::Display for TextBox {
|
||||||
writeln!(f, "┌{}{}┐", connector, "─".repeat(header_width))?;
|
writeln!(f, "┌{}{}┐", connector, "─".repeat(header_width))?;
|
||||||
|
|
||||||
for l in self.body.lines() {
|
for l in self.body.lines() {
|
||||||
writeln!(
|
writeln!(f, "│ {}│", l.pad(width - 1))?;
|
||||||
f,
|
|
||||||
"│ {}│",
|
|
||||||
console::pad_str(l, width - 1, console::Alignment::Left, Some("…"))
|
|
||||||
)?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let (connector, footer_width) = if !self.last {
|
let (connector, footer_width) = if !self.last {
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,10 @@ git-ref-format = { version = "0", features = ["serde", "macro"] }
|
||||||
version = "0"
|
version = "0"
|
||||||
path = "../radicle"
|
path = "../radicle"
|
||||||
|
|
||||||
|
[dependencies.radicle-cli]
|
||||||
|
version = "0"
|
||||||
|
path = "../radicle-cli"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "rad-init"
|
name = "rad-init"
|
||||||
path = "src/rad-init.rs"
|
path = "src/rad-init.rs"
|
||||||
|
|
@ -32,3 +36,7 @@ path = "src/rad-push.rs"
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "rad-agent"
|
name = "rad-agent"
|
||||||
path = "src/rad-agent.rs"
|
path = "src/rad-agent.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "rad-cli-demo"
|
||||||
|
path = "src/rad-cli-demo.rs"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
use std::{thread, time};
|
||||||
|
|
||||||
|
use radicle_cli::terminal;
|
||||||
|
|
||||||
|
fn main() -> anyhow::Result<()> {
|
||||||
|
let demo = terminal::io::select(
|
||||||
|
"Choose something to try out:",
|
||||||
|
&[
|
||||||
|
"spinner",
|
||||||
|
"spinner-drop",
|
||||||
|
"spinner-error",
|
||||||
|
"editor",
|
||||||
|
"prompt",
|
||||||
|
],
|
||||||
|
&"spinner",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
match demo {
|
||||||
|
Some(&"editor") => {
|
||||||
|
let output = terminal::editor::Editor::new()
|
||||||
|
.extension("rs")
|
||||||
|
.edit("// Enter code here.");
|
||||||
|
|
||||||
|
match output {
|
||||||
|
Ok(Some(s)) => {
|
||||||
|
terminal::info!("You entered:");
|
||||||
|
terminal::blob(s);
|
||||||
|
}
|
||||||
|
Ok(None) => {
|
||||||
|
terminal::info!("You didn't enter anything.");
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(&"spinner") => {
|
||||||
|
let mut spinner = terminal::spinner("Spinning turbines..");
|
||||||
|
thread::sleep(time::Duration::from_secs(1));
|
||||||
|
spinner.message("Still spinning..");
|
||||||
|
thread::sleep(time::Duration::from_secs(1));
|
||||||
|
spinner.message("Almost done..");
|
||||||
|
thread::sleep(time::Duration::from_secs(1));
|
||||||
|
spinner.message("Done.");
|
||||||
|
|
||||||
|
spinner.finish();
|
||||||
|
}
|
||||||
|
Some(&"spinner-drop") => {
|
||||||
|
let _spinner = terminal::spinner("Spinning turbines..");
|
||||||
|
thread::sleep(time::Duration::from_secs(3));
|
||||||
|
}
|
||||||
|
Some(&"spinner-error") => {
|
||||||
|
let spinner = terminal::spinner("Spinning turbines..");
|
||||||
|
thread::sleep(time::Duration::from_secs(3));
|
||||||
|
spinner.error("broken turbine");
|
||||||
|
}
|
||||||
|
Some(&"prompt") => {
|
||||||
|
let fruit = terminal::io::select(
|
||||||
|
"Enter your favorite fruit:",
|
||||||
|
&["apple", "pear", "banana", "strawberry"],
|
||||||
|
&"apple",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
if let Some(fruit) = fruit {
|
||||||
|
terminal::success!("You have chosen '{fruit}'");
|
||||||
|
} else {
|
||||||
|
terminal::info!("Ok, bye.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
@ -178,12 +178,12 @@ impl Project {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn name(&self) -> &String {
|
pub fn name(&self) -> &str {
|
||||||
&self.name
|
&self.name
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn description(&self) -> &String {
|
pub fn description(&self) -> &str {
|
||||||
&self.description
|
&self.description
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ pub mod env {
|
||||||
/// Passphrase for the encrypted radicle secret key.
|
/// Passphrase for the encrypted radicle secret key.
|
||||||
pub const RAD_PASSPHRASE: &str = "RAD_PASSPHRASE";
|
pub const RAD_PASSPHRASE: &str = "RAD_PASSPHRASE";
|
||||||
|
|
||||||
pub fn read_passphrase() -> Option<super::Passphrase> {
|
pub fn passphrase() -> Option<super::Passphrase> {
|
||||||
let Ok(passphrase) = std::env::var(RAD_PASSPHRASE) else {
|
let Ok(passphrase) = std::env::var(RAD_PASSPHRASE) else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
|
|
@ -109,7 +109,7 @@ impl Profile {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn signer(&self) -> Result<Box<dyn Signer>, Error> {
|
pub fn signer(&self) -> Result<Box<dyn Signer>, Error> {
|
||||||
if let Some(passphrase) = env::read_passphrase() {
|
if let Some(passphrase) = env::passphrase() {
|
||||||
let signer = keystore::MemorySigner::load(&self.keystore, passphrase)?;
|
let signer = keystore::MemorySigner::load(&self.keystore, passphrase)?;
|
||||||
return Ok(signer.boxed());
|
return Ok(signer.boxed());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue