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"
|
||||
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]]
|
||||
name = "const-oid"
|
||||
version = "0.9.1"
|
||||
|
|
@ -643,18 +630,6 @@ dependencies = [
|
|||
"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]]
|
||||
name = "diff"
|
||||
version = "0.1.13"
|
||||
|
|
@ -682,6 +657,12 @@ dependencies = [
|
|||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dyn-clone"
|
||||
version = "1.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9b0705efd4599c15a38151f4721f7bc388306f61084d3bfd50bd07fbca5cb60"
|
||||
|
||||
[[package]]
|
||||
name = "ec25519"
|
||||
version = "0.1.0"
|
||||
|
|
@ -757,12 +738,6 @@ dependencies = [
|
|||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "encode_unicode"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.2.8"
|
||||
|
|
@ -1173,18 +1148,6 @@ dependencies = [
|
|||
"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]]
|
||||
name = "inout"
|
||||
version = "0.1.3"
|
||||
|
|
@ -1194,6 +1157,23 @@ dependencies = [
|
|||
"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]]
|
||||
name = "instant"
|
||||
version = "0.1.12"
|
||||
|
|
@ -1447,6 +1427,15 @@ dependencies = [
|
|||
"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]]
|
||||
name = "noise-framework"
|
||||
version = "0.2.0"
|
||||
|
|
@ -1558,10 +1547,10 @@ dependencies = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "number_prefix"
|
||||
version = "0.4.0"
|
||||
name = "numtoa"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
|
||||
checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
|
|
@ -1833,12 +1822,12 @@ version = "0.8.0"
|
|||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"console",
|
||||
"dialoguer",
|
||||
"indicatif",
|
||||
"concolor",
|
||||
"inquire",
|
||||
"json-color",
|
||||
"lexopt",
|
||||
"log",
|
||||
"once_cell",
|
||||
"pretty_assertions",
|
||||
"radicle",
|
||||
"radicle-cli-test",
|
||||
|
|
@ -1850,8 +1839,11 @@ dependencies = [
|
|||
"serde_yaml",
|
||||
"similar",
|
||||
"tempfile",
|
||||
"termion 2.0.1",
|
||||
"thiserror",
|
||||
"timeago",
|
||||
"unicode-segmentation",
|
||||
"unicode-width",
|
||||
"ureq",
|
||||
"zeroize",
|
||||
]
|
||||
|
|
@ -2064,6 +2056,7 @@ dependencies = [
|
|||
"anyhow",
|
||||
"git-ref-format",
|
||||
"radicle",
|
||||
"radicle-cli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2146,6 +2139,15 @@ dependencies = [
|
|||
"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]]
|
||||
name = "regex"
|
||||
version = "1.7.1"
|
||||
|
|
@ -2397,12 +2399,6 @@ dependencies = [
|
|||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shell-words"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.1.0"
|
||||
|
|
@ -2632,6 +2628,30 @@ dependencies = [
|
|||
"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]]
|
||||
name = "thiserror"
|
||||
version = "1.0.38"
|
||||
|
|
@ -2919,6 +2939,12 @@ dependencies = [
|
|||
"tinyvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.10"
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use snapbox::{Assert, Substitutions};
|
|||
use thiserror::Error;
|
||||
|
||||
/// Error lines in the CLI are prefixed with this string.
|
||||
const ERROR_PREFIX: &str = "==";
|
||||
const ERROR_PREFIX: &str = "✗";
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
|
|
|
|||
|
|
@ -13,18 +13,21 @@ path = "src/main.rs"
|
|||
[dependencies]
|
||||
anyhow = { version = "1" }
|
||||
chrono = { version = "0.4", default-features = false, features = ["clock", "std"] }
|
||||
console = { version = "0.15" }
|
||||
dialoguer = { version = "0.10.0" }
|
||||
indicatif = { version = "0.16.2" }
|
||||
concolor = { version = "0", features = ["api_unstable"] }
|
||||
inquire = { version = "0.5.3", default-features = false, features = ["termion", "editor"] }
|
||||
json-color = { version = "0.7" }
|
||||
lexopt = { version = "0.2" }
|
||||
log = { version = "0.4", features = ["std"] }
|
||||
once_cell = { version = "1.13" }
|
||||
serde = { version = "1.0" }
|
||||
serde_json = { version = "1" }
|
||||
serde_yaml = { version = "0.8" }
|
||||
similar = { version = "2.2.1" }
|
||||
termion = { version = "2" }
|
||||
thiserror = { version = "1" }
|
||||
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"] }
|
||||
zeroize = { version = "1.1" }
|
||||
|
||||
|
|
@ -46,6 +49,6 @@ path = "../radicle-crypto"
|
|||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = { version = "1.3.0" }
|
||||
tempfile = { version = "3.3.0" }
|
||||
radicle = { path = "../radicle", 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
|
||||
|
||||
ok Creating your 🌱 Ed25519 keypair...
|
||||
ok Profile did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi created.
|
||||
✓ Creating your 🌱 Ed25519 keypair...
|
||||
✓ 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:
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ $ rad checkout rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
|
|||
|
||||
Initializing local checkout for 🌱 rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji (heartwood)
|
||||
|
||||
ok Performing checkout...
|
||||
✓ Performing checkout...
|
||||
|
||||
🌱 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
|
||||
ok Tracking relationship established for rad:zVNuptPuk5XauitpCWSNVCXGGfXW
|
||||
== Clone failed no seeds found for rad:zVNuptPuk5XauitpCWSNVCXGGfXW
|
||||
✓ Tracking relationship established 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
|
||||
ok Tracking relationship established for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
|
||||
ok Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MknSL…StBU8Vi..
|
||||
ok Forking under z6Mkt67…v4N1tRk..
|
||||
ok Creating checkout in ./heartwood..
|
||||
ok Remote z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi created
|
||||
ok Remote-tracking branch z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master created for z6MknSL…StBU8Vi
|
||||
✓ Tracking relationship established for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
|
||||
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MknSL…StBU8Vi..
|
||||
✓ Forking under z6Mkt67…v4N1tRk..
|
||||
✓ Creating checkout in ./heartwood..
|
||||
✓ Remote z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi created
|
||||
✓ Remote-tracking branch z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master created for z6MknSL…StBU8Vi
|
||||
|
||||
🌱 Project successfully cloned under [..]/heartwood/
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ work.
|
|||
```
|
||||
$ rad delegate add did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG --to rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
|
||||
Added delegate 'did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG'
|
||||
ok Update successful!
|
||||
✓ Update successful!
|
||||
```
|
||||
|
||||
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
|
||||
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
|
||||
ok Identity proposal '57332790a2eabc0b2fd8c7ff48c3579d5812d405' created 🌱
|
||||
✓ Identity proposal '57332790a2eabc0b2fd8c7ff48c3579d5812d405' created 🌱
|
||||
title: Add Alice
|
||||
description: Add Alice as a delegate
|
||||
status: open
|
||||
status: ❲open❳
|
||||
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
|
||||
Document Diff
|
||||
|
|
@ -43,15 +43,15 @@ keys: []
|
|||
|
||||
Quorum Reached
|
||||
|
||||
✗ no
|
||||
👎 no
|
||||
```
|
||||
|
||||
```
|
||||
$ 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
|
||||
description: Add Bob as a delegate
|
||||
status: open
|
||||
status: ❲open❳
|
||||
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
|
||||
Document Diff
|
||||
|
|
@ -85,7 +85,7 @@ keys: []
|
|||
|
||||
Quorum Reached
|
||||
|
||||
✗ no
|
||||
👎 no
|
||||
```
|
||||
|
||||
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
|
||||
ok Accepted proposal ✓
|
||||
✓ Accepted proposal ✓
|
||||
title: Add Alice
|
||||
description: Add Alice as a delegate
|
||||
status: open
|
||||
status: ❲open❳
|
||||
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
|
||||
Document Diff
|
||||
|
|
@ -133,15 +133,15 @@ keys: []
|
|||
|
||||
Quorum Reached
|
||||
|
||||
✓ yes
|
||||
👍 yes
|
||||
```
|
||||
|
||||
```
|
||||
$ rad id commit 57332790a2eabc0b2fd8c7ff48c3579d5812d405 --rev z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/1 --no-confirm
|
||||
ok Committed new identity '29ae4b72f5a315328f06fbd68dc1c396a2d5c45e' 🌱
|
||||
✓ Committed new identity '29ae4b72f5a315328f06fbd68dc1c396a2d5c45e' 🌱
|
||||
title: Add Alice
|
||||
description: Add Alice as a delegate
|
||||
status: committed
|
||||
status: ❲committed❳
|
||||
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
|
||||
Document Diff
|
||||
|
|
@ -177,20 +177,20 @@ keys: []
|
|||
|
||||
Quorum Reached
|
||||
|
||||
✓ yes
|
||||
👍 yes
|
||||
```
|
||||
|
||||
Now, when we go to accept the second proposal:
|
||||
|
||||
```
|
||||
$ rad id accept c3698d4e85f9d4c0ee536b34d6122fc7c81f7e2e --rev z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/1 --no-confirm
|
||||
** Warning: Revision is out of date
|
||||
** Warning: d96f425412c9f8ad5d9a9a05c9831d0728e2338d =/= 475cdfbc8662853dd132ec564e4f5eb0f152dd7f
|
||||
=> Consider using 'rad id rebase' to update the proposal to the latest identity
|
||||
ok Accepted proposal ✓
|
||||
! Warning: Revision is out of date
|
||||
! Warning: d96f425412c9f8ad5d9a9a05c9831d0728e2338d =/= 475cdfbc8662853dd132ec564e4f5eb0f152dd7f
|
||||
👉 Consider using 'rad id rebase' to update the proposal to the latest identity
|
||||
✓ Accepted proposal ✓
|
||||
title: Add Bob
|
||||
description: Add Bob as a delegate
|
||||
status: open
|
||||
status: ❲open❳
|
||||
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
|
||||
Document Diff
|
||||
|
|
@ -226,7 +226,7 @@ keys: []
|
|||
|
||||
Quorum Reached
|
||||
|
||||
✓ yes
|
||||
👍 yes
|
||||
```
|
||||
|
||||
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
|
||||
** Warning: Revision is out of date
|
||||
** Warning: d96f425412c9f8ad5d9a9a05c9831d0728e2338d =/= 475cdfbc8662853dd132ec564e4f5eb0f152dd7f
|
||||
=> Consider using 'rad id rebase' to update the proposal to the latest identity
|
||||
== Id failed
|
||||
the identity hashes do match 'd96f425412c9f8ad5d9a9a05c9831d0728e2338d =/= 475cdfbc8662853dd132ec564e4f5eb0f152dd7f' for the revision 'z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/1'
|
||||
! Warning: Revision is out of date
|
||||
! Warning: d96f425412c9f8ad5d9a9a05c9831d0728e2338d =/= 475cdfbc8662853dd132ec564e4f5eb0f152dd7f
|
||||
👉 Consider using 'rad id rebase' to update the proposal to the latest identity
|
||||
✗ Id failed: 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:
|
||||
|
||||
```
|
||||
$ rad id rebase c3698d4e85f9d4c0ee536b34d6122fc7c81f7e2e --rev z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/1 --no-confirm
|
||||
ok Identity proposal 'c3698d4e85f9d4c0ee536b34d6122fc7c81f7e2e' rebased 🌱
|
||||
ok Revision 'z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/4'
|
||||
✓ Identity proposal 'c3698d4e85f9d4c0ee536b34d6122fc7c81f7e2e' rebased 🌱
|
||||
✓ Revision 'z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/4'
|
||||
title: Add Bob
|
||||
description: Add Bob as a delegate
|
||||
status: open
|
||||
status: ❲open❳
|
||||
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
|
||||
Document Diff
|
||||
|
|
@ -288,18 +287,18 @@ keys: []
|
|||
|
||||
Quorum Reached
|
||||
|
||||
✗ no
|
||||
👎 no
|
||||
```
|
||||
|
||||
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
|
||||
ok Identity proposal 'c3698d4e85f9d4c0ee536b34d6122fc7c81f7e2e' updated 🌱
|
||||
ok Revision 'z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/6'
|
||||
✓ Identity proposal 'c3698d4e85f9d4c0ee536b34d6122fc7c81f7e2e' updated 🌱
|
||||
✓ Revision 'z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/6'
|
||||
title: Add Bob
|
||||
description: Add Bob as a delegate
|
||||
status: open
|
||||
status: ❲open❳
|
||||
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
|
||||
Document Diff
|
||||
|
|
@ -333,7 +332,7 @@ keys: []
|
|||
|
||||
Quorum Reached
|
||||
|
||||
✗ no
|
||||
👎 no
|
||||
```
|
||||
|
||||
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
|
||||
ok Accepted proposal ✓
|
||||
✓ Accepted proposal ✓
|
||||
title: Add Bob
|
||||
description: Add Bob as a delegate
|
||||
status: open
|
||||
status: ❲open❳
|
||||
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
|
||||
Document Diff
|
||||
|
|
@ -382,15 +381,15 @@ keys: []
|
|||
|
||||
Quorum Reached
|
||||
|
||||
✓ yes
|
||||
👍 yes
|
||||
```
|
||||
|
||||
```
|
||||
$ rad id commit c3698d4e85f9d4c0ee536b34d6122fc7c81f7e2e --rev z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/6 --no-confirm
|
||||
ok Committed new identity '60de897bc24898f6908fd1272633c0b15aa4096f' 🌱
|
||||
✓ Committed new identity '60de897bc24898f6908fd1272633c0b15aa4096f' 🌱
|
||||
title: Add Bob
|
||||
description: Add Bob as a delegate
|
||||
status: committed
|
||||
status: ❲committed❳
|
||||
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
|
||||
Document Diff
|
||||
|
|
@ -426,5 +425,5 @@ keys: []
|
|||
|
||||
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
|
||||
ok Identity proposal '06d9efa2a9aad06bfdf25a25690e1ec7db2c3c39' created 🌱
|
||||
✓ Identity proposal '06d9efa2a9aad06bfdf25a25690e1ec7db2c3c39' created 🌱
|
||||
title: Add Bob
|
||||
description: Add Bob as a delegate
|
||||
status: open
|
||||
status: ❲open❳
|
||||
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
|
||||
Document Diff
|
||||
|
|
@ -51,7 +51,7 @@ keys: []
|
|||
|
||||
Quorum Reached
|
||||
|
||||
✗ no
|
||||
👎 no
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
✗ no
|
||||
👎 no
|
||||
|
||||
Let's see what happens when we reject the change:
|
||||
|
||||
```
|
||||
$ rad id reject 06d9efa2a9aad06bfdf25a25690e1ec7db2c3c39 --rev z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/1 --no-confirm
|
||||
ok Rejected proposal ✗
|
||||
✓ Rejected proposal 👎
|
||||
title: Add Bob
|
||||
description: Add Bob as a delegate
|
||||
status: open
|
||||
status: ❲open❳
|
||||
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
|
||||
Document Diff
|
||||
|
|
@ -129,7 +129,7 @@ keys: [
|
|||
|
||||
Quorum Reached
|
||||
|
||||
✗ no
|
||||
👎 no
|
||||
```
|
||||
|
||||
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
|
||||
ok Accepted proposal ✓
|
||||
✓ Accepted proposal ✓
|
||||
title: Add Bob
|
||||
description: Add Bob as a delegate
|
||||
status: open
|
||||
status: ❲open❳
|
||||
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
|
||||
Document Diff
|
||||
|
|
@ -185,7 +185,7 @@ keys: []
|
|||
|
||||
Quorum Reached
|
||||
|
||||
✓ yes
|
||||
👍 yes
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
✓ yes
|
||||
👍 yes
|
||||
|
||||
At this point, we can commit the proposal and update the identity:
|
||||
|
||||
```
|
||||
$ rad id commit 06d9efa2a9aad06bfdf25a25690e1ec7db2c3c39 --rev z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/1 --no-confirm
|
||||
ok Committed new identity 'c96e764965aaeff1c6ea3e5b97e2b9828773c8b0' 🌱
|
||||
✓ Committed new identity 'c96e764965aaeff1c6ea3e5b97e2b9828773c8b0' 🌱
|
||||
title: Add Bob
|
||||
description: Add Bob as a delegate
|
||||
status: committed
|
||||
status: ❲committed❳
|
||||
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
|
||||
Document Diff
|
||||
|
|
@ -247,7 +247,7 @@ keys: []
|
|||
|
||||
Quorum Reached
|
||||
|
||||
✓ yes
|
||||
👍 yes
|
||||
```
|
||||
|
||||
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
|
||||
ok Identity proposal 'dc00640d3152ea5f1df59f39f2f5983d2ad21810' created 🌱
|
||||
✓ Identity proposal 'dc00640d3152ea5f1df59f39f2f5983d2ad21810' created 🌱
|
||||
title: Update threshold
|
||||
description: Update to safer threshold
|
||||
status: open
|
||||
status: ❲open❳
|
||||
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
|
||||
Document Diff
|
||||
|
|
@ -292,17 +292,17 @@ keys: []
|
|||
|
||||
Quorum Reached
|
||||
|
||||
✗ no
|
||||
👎 no
|
||||
```
|
||||
|
||||
But we change our minds and decide to close the proposal instead:
|
||||
|
||||
```
|
||||
$ rad id close dc00640d3152ea5f1df59f39f2f5983d2ad21810 --no-confirm
|
||||
ok Closed identity proposal 'dc00640d3152ea5f1df59f39f2f5983d2ad21810'
|
||||
✓ Closed identity proposal 'dc00640d3152ea5f1df59f39f2f5983d2ad21810'
|
||||
title: Update threshold
|
||||
description: Update to safer threshold
|
||||
status: closed
|
||||
status: ❲closed❳
|
||||
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
|
||||
Document Diff
|
||||
|
|
@ -336,7 +336,7 @@ keys: []
|
|||
|
||||
Quorum Reached
|
||||
|
||||
✗ no
|
||||
👎 no
|
||||
```
|
||||
|
||||
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
|
||||
06d9efa2a9aad06bfdf25a25690e1ec7db2c3c39 "Add Bob" committed
|
||||
dc00640d3152ea5f1df59f39f2f5983d2ad21810 "Update threshold" closed
|
||||
06d9efa2a9aad06bfdf25a25690e1ec7db2c3c39 "Add Bob" ❲committed❳
|
||||
dc00640d3152ea5f1df59f39f2f5983d2ad21810 "Update threshold" ❲closed❳
|
||||
```
|
||||
|
||||
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
|
||||
title: Update threshold
|
||||
description: Update to safer threshold
|
||||
status: closed
|
||||
status: ❲closed❳
|
||||
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
|
||||
Document Diff
|
||||
|
|
@ -393,7 +393,7 @@ keys: []
|
|||
|
||||
Quorum Reached
|
||||
|
||||
✗ no
|
||||
👎 no
|
||||
```
|
||||
|
||||
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 .
|
||||
|
||||
ok Project heartwood created
|
||||
✓ Project heartwood created
|
||||
{
|
||||
"name": "heartwood",
|
||||
"description": "Radicle Heartwood Protocol & Stack",
|
||||
"defaultBranch": "master"
|
||||
}
|
||||
ok Syncing inventory..
|
||||
✓ Syncing inventory..
|
||||
|
||||
Your project id is rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji. You can show it any time by running:
|
||||
rad .
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ $ rad init --name heartwood --description "Radicle Heartwood Protocol & Stack" -
|
|||
|
||||
Initializing local 🌱 project in .
|
||||
|
||||
ok Project heartwood created
|
||||
✓ Project heartwood created
|
||||
{
|
||||
"name": "heartwood",
|
||||
"description": "Radicle Heartwood Protocol & Stack",
|
||||
|
|
|
|||
|
|
@ -29,8 +29,8 @@ $ rad patch open --message "define power requirements" --no-confirm
|
|||
|
||||
🌱 Creating patch for heartwood
|
||||
|
||||
ok Pushing HEAD to storage...
|
||||
ok Analyzing remotes...
|
||||
✓ Pushing HEAD to storage...
|
||||
✓ Analyzing remotes...
|
||||
|
||||
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master (f2de534) <- z6MknSL…StBU8Vi/flux-capacitor-power (3e674d1)
|
||||
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.
|
||||
|
|
@ -53,13 +53,13 @@ It will now be listed as one of the project's open patches.
|
|||
```
|
||||
$ rad patch
|
||||
|
||||
- YOU PROPOSED -
|
||||
❲YOU PROPOSED❳
|
||||
|
||||
define power requirements d4ef85f57a8 R0 3e674d1 (flux-capacitor-power) ahead 1, behind 0
|
||||
└─ * opened by did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (you) [..]
|
||||
└─ * patch id d4ef85f57a849bd845915d7a66a2192cd23811f6
|
||||
|
||||
- OTHERS PROPOSED -
|
||||
❲OTHERS PROPOSED❳
|
||||
|
||||
Nothing to show.
|
||||
|
||||
|
|
@ -98,14 +98,14 @@ $ rad patch update --message "Add README, just for the fun" --no-confirm d4ef85f
|
|||
|
||||
🌱 Updating patch for heartwood
|
||||
|
||||
ok Pushing HEAD to storage...
|
||||
ok Analyzing remotes...
|
||||
✓ Pushing HEAD to storage...
|
||||
✓ Analyzing remotes...
|
||||
|
||||
d4ef85f57a8 R0 (3e674d1) -> R1 (27857ec)
|
||||
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
|
||||
ok Performing patch checkout...
|
||||
ok Switched to branch patch/d4ef85f57a8
|
||||
✓ Performing patch checkout...
|
||||
✓ Switched to branch patch/d4ef85f57a8
|
||||
```
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@ Now let's delete the `heartwood` project:
|
|||
|
||||
```
|
||||
$ 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: Make sure to untrack this repository when your node is running
|
||||
ok Successfully removed project rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from storage
|
||||
! 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
|
||||
✓ Successfully removed project rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from storage
|
||||
```
|
||||
|
||||
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 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 profile = Profile::init(home, passphrase)?;
|
||||
spinner.finish();
|
||||
|
||||
term::success!(
|
||||
"Profile {} created.",
|
||||
term::format::highlight(profile.did().to_string())
|
||||
);
|
||||
|
||||
term::blank();
|
||||
term::info!(
|
||||
"Your radicle Node ID is {}. This identifies your device.",
|
||||
term::format::highlight(profile.id().to_string())
|
||||
"Your Radicle ID is {}. This identifies your device.",
|
||||
term::format::highlight(profile.did())
|
||||
);
|
||||
|
||||
term::blank();
|
||||
|
|
@ -108,6 +106,7 @@ pub fn init(options: Options) -> anyhow::Result<()> {
|
|||
pub fn authenticate(profile: &Profile, options: Options) -> anyhow::Result<()> {
|
||||
let agent = ssh::agent::Agent::connect()?;
|
||||
|
||||
// TODO: Only show this if we're not authenticated.
|
||||
term::headline(&format!(
|
||||
"🌱 Authenticating as {}",
|
||||
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,
|
||||
// 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 mut agent = ssh::agent::Agent::connect()?;
|
||||
let secret = profile
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ pub fn execute(options: Options, profile: &Profile) -> anyhow::Result<PathBuf> {
|
|||
.identity_of(&remote)
|
||||
.context("project could not be found in local storage")?;
|
||||
let payload = doc.project()?;
|
||||
let path = PathBuf::from(payload.name().clone());
|
||||
let path = PathBuf::from(payload.name());
|
||||
|
||||
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 payload = serde_json::to_string_pretty(&project.payload)?;
|
||||
match term::Editor::new().edit(&payload)? {
|
||||
Some(updated_payload) => {
|
||||
match term::Editor::new().extension("json").edit(payload) {
|
||||
Ok(Some(updated_payload)) => {
|
||||
project.payload = serde_json::from_str(&updated_payload)?;
|
||||
project.sign(&signer).and_then(|(_, sig)| {
|
||||
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!");
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ pub struct Metadata {
|
|||
impl Metadata {
|
||||
fn edit(self) -> anyhow::Result<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")?),
|
||||
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?");
|
||||
if yes {
|
||||
proposal.reject(rid, &signer)?;
|
||||
term::success!("Rejected proposal ✗");
|
||||
term::success!("Rejected proposal 👎");
|
||||
print(&proposal, &previous, None)?;
|
||||
}
|
||||
}
|
||||
|
|
@ -578,9 +578,9 @@ fn print_revision(revision: &identity::Revision, previous: &Identity<Oid>) -> an
|
|||
print!(
|
||||
"{}",
|
||||
if revision.is_quorum_reached(previous) {
|
||||
term::format::positive("✓ yes")
|
||||
term::format::positive("👍 yes")
|
||||
} else {
|
||||
term::format::negative("✗ no")
|
||||
term::format::negative("👎 no")
|
||||
}
|
||||
);
|
||||
term::blank();
|
||||
|
|
|
|||
|
|
@ -162,9 +162,9 @@ pub fn init(options: Options, profile: &profile::Profile) -> anyhow::Result<()>
|
|||
term::headline(&format!(
|
||||
"Initializing local 🌱 project in {}",
|
||||
if path == cwd {
|
||||
term::format::highlight(".")
|
||||
term::format::highlight(".").to_string()
|
||||
} 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 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
|
||||
.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(|| {
|
||||
if interactive.yes() {
|
||||
term::text_input("Default branch", Some(head)).unwrap()
|
||||
term::input("Default branch", Some(head)).unwrap()
|
||||
} else {
|
||||
head
|
||||
}
|
||||
|
|
|
|||
|
|
@ -218,8 +218,8 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
|||
}
|
||||
Operation::React { id, reaction } => {
|
||||
if let Ok(mut issue) = issues.get_mut(&id) {
|
||||
let comment_id = term::comment_select(&issue).unwrap();
|
||||
issue.react(comment_id, reaction, &signer)?;
|
||||
let (comment_id, _) = term::comment_select(&issue).unwrap();
|
||||
issue.react(*comment_id, reaction, &signer)?;
|
||||
}
|
||||
}
|
||||
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())
|
||||
);
|
||||
|
||||
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 frontmatter = false;
|
||||
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);
|
||||
table.push([
|
||||
term::format::bold(proj.name()),
|
||||
term::format::tertiary(id.urn()),
|
||||
term::format::secondary(head),
|
||||
term::format::tertiary(id.urn().as_str()),
|
||||
term::format::secondary(head.as_str()),
|
||||
term::format::italic(proj.description()),
|
||||
]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -222,8 +222,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
|||
.dim()
|
||||
.italic()
|
||||
.to_string(),
|
||||
MergeStyle::Commit => term::format::style(merge_style.to_string())
|
||||
.yellow()
|
||||
MergeStyle::Commit => term::format::yellow(merge_style.to_string())
|
||||
.italic()
|
||||
.to_string(),
|
||||
};
|
||||
|
|
@ -312,19 +311,13 @@ fn merge_commit(
|
|||
writeln!(&mut merge_msg, "{}", MERGE_HELP_MSG.trim())?;
|
||||
|
||||
// Offer user the chance to edit the message before committing.
|
||||
let merge_msg = match term::Editor::new()
|
||||
.require_save(true)
|
||||
.trim_newlines(true)
|
||||
.extension(".git-commit")
|
||||
.edit(&merge_msg)
|
||||
.unwrap()
|
||||
{
|
||||
Some(s) => s
|
||||
let merge_msg = match term::Editor::new().extension("git-commit").edit(merge_msg) {
|
||||
Ok(Some(s)) => s
|
||||
.lines()
|
||||
.filter(|l| !l.starts_with('#'))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n"),
|
||||
None => anyhow::bail!("user aborted merge"),
|
||||
_ => anyhow::bail!("user aborted merge"),
|
||||
};
|
||||
|
||||
// Empty message aborts merge.
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@ pub fn pretty_sync_status(
|
|||
) -> anyhow::Result<String> {
|
||||
let (a, b) = repo.graph_ahead_behind(revision_oid, head_oid)?;
|
||||
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);
|
||||
|
|
@ -143,7 +143,7 @@ pub fn pretty_commit_version(
|
|||
revision_oid: &Oid,
|
||||
repo: &Option<git::raw::Repository>,
|
||||
) -> 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![];
|
||||
|
||||
if let Some(repo) = repo {
|
||||
|
|
@ -216,6 +216,8 @@ pub fn push_to_storage(
|
|||
if options.verbose {
|
||||
spinner.finish();
|
||||
term::blob(output);
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
spinner.finish();
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ use radicle::git;
|
|||
use radicle::prelude::*;
|
||||
use radicle::profile::Profile;
|
||||
use radicle::storage::git::Repository;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::terminal as term;
|
||||
|
||||
|
|
@ -39,10 +40,7 @@ pub fn run(
|
|||
}
|
||||
}
|
||||
term::blank();
|
||||
term::print(format!(
|
||||
"-{}-",
|
||||
term::format::badge_secondary("YOU PROPOSED")
|
||||
));
|
||||
term::print(term::format::badge_secondary("YOU PROPOSED"));
|
||||
|
||||
if own.is_empty() {
|
||||
term::blank();
|
||||
|
|
@ -55,10 +53,7 @@ pub fn run(
|
|||
}
|
||||
}
|
||||
term::blank();
|
||||
term::print(format!(
|
||||
"-{}-",
|
||||
term::format::badge_secondary("OTHERS PROPOSED")
|
||||
));
|
||||
term::print(term::format::badge_secondary("OTHERS PROPOSED"));
|
||||
|
||||
if other.is_empty() {
|
||||
term::blank();
|
||||
|
|
@ -94,11 +89,9 @@ fn print(
|
|||
)];
|
||||
|
||||
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(
|
||||
&patch.timestamp(),
|
||||
)));
|
||||
author_info.push(term::format::dim(term::format::timestamp(&patch.timestamp())).to_string());
|
||||
|
||||
let (_, revision) = patch
|
||||
.latest()
|
||||
|
|
@ -120,17 +113,17 @@ fn print(
|
|||
let mut badges = Vec::new();
|
||||
|
||||
if peer.delegate {
|
||||
badges.push(term::format::secondary("(delegate)"));
|
||||
badges.push(term::format::secondary("(delegate)").to_string());
|
||||
}
|
||||
if peer.id == *whoami {
|
||||
badges.push(term::format::secondary("(you)"));
|
||||
badges.push(term::format::secondary("(you)").to_string());
|
||||
}
|
||||
|
||||
timeline.push((
|
||||
merge.timestamp,
|
||||
format!(
|
||||
"{}{} by {} {}",
|
||||
" ".repeat(term::text_width(prefix)),
|
||||
" ".repeat(prefix.width()),
|
||||
term::format::secondary(term::format::dim("✓ merged")),
|
||||
term::format::tertiary(peer.id),
|
||||
badges.join(" "),
|
||||
|
|
@ -147,17 +140,17 @@ fn print(
|
|||
let mut badges = Vec::new();
|
||||
|
||||
if peer.delegate {
|
||||
badges.push(term::format::secondary("(delegate)"));
|
||||
badges.push(term::format::secondary("(delegate)").to_string());
|
||||
}
|
||||
if peer.id == *whoami {
|
||||
badges.push(term::format::secondary("(you)"));
|
||||
badges.push(term::format::secondary("(you)").to_string());
|
||||
}
|
||||
|
||||
timeline.push((
|
||||
review.timestamp(),
|
||||
format!(
|
||||
"{}{} by {} {}",
|
||||
" ".repeat(term::text_width(prefix)),
|
||||
" ".repeat(prefix.width()),
|
||||
verdict,
|
||||
term::format::tertiary(reviewer),
|
||||
badges.join(" "),
|
||||
|
|
|
|||
|
|
@ -78,33 +78,45 @@ fn all(profile: &Profile) -> anyhow::Result<()> {
|
|||
let mut table = term::Table::default();
|
||||
|
||||
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();
|
||||
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);
|
||||
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);
|
||||
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();
|
||||
table.push([
|
||||
"Storage (git)",
|
||||
&term::format::tertiary(storage_path.display()),
|
||||
term::format::style("Storage (git)").to_string(),
|
||||
term::format::tertiary(storage_path.display()).to_string(),
|
||||
]);
|
||||
|
||||
let keys_path = profile.home.keys();
|
||||
table.push([
|
||||
"Storage (keys)",
|
||||
&term::format::tertiary(keys_path.display()),
|
||||
term::format::style("Storage (keys)").to_string(),
|
||||
term::format::tertiary(keys_path.display()).to_string(),
|
||||
]);
|
||||
|
||||
let node_path = profile.home.node();
|
||||
table.push([
|
||||
"Node (socket)",
|
||||
&term::format::tertiary(node_path.join("radicle.sock").display()),
|
||||
term::format::style("Node (socket)").to_string(),
|
||||
term::format::tertiary(node_path.join("radicle.sock").display()).to_string(),
|
||||
]);
|
||||
|
||||
table.render();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
pub mod ansi;
|
||||
pub mod args;
|
||||
pub mod cell;
|
||||
pub mod cob;
|
||||
pub mod command;
|
||||
pub mod editor;
|
||||
pub mod format;
|
||||
pub mod io;
|
||||
pub mod patch;
|
||||
|
|
@ -11,12 +14,12 @@ pub mod textbox;
|
|||
use std::ffi::OsString;
|
||||
use std::process;
|
||||
|
||||
use dialoguer::console::style;
|
||||
use radicle::profile::Profile;
|
||||
|
||||
pub use ansi::{paint, Paint};
|
||||
pub use args::{Args, Error, Help};
|
||||
pub use console::measure_text_width as text_width;
|
||||
pub use dialoguer::Editor;
|
||||
pub use editor::Editor;
|
||||
pub use inquire::ui::Styled;
|
||||
pub use io::*;
|
||||
pub use spinner::{spinner, Spinner};
|
||||
pub use table::Table;
|
||||
|
|
@ -97,14 +100,14 @@ where
|
|||
};
|
||||
eprintln!(
|
||||
"{} {} {} {}",
|
||||
style("==").red(),
|
||||
style("Error:").red(),
|
||||
style(format!("rad-{}:", help.name)).red(),
|
||||
style(&err).red()
|
||||
Paint::red("=="),
|
||||
Paint::red("Error:"),
|
||||
Paint::red(format!("rad-{}:", help.name)),
|
||||
Paint::red(err.to_string()),
|
||||
);
|
||||
|
||||
if let Some(Error::WithHint { hint, .. }) = err.downcast_ref::<Error>() {
|
||||
eprintln!("{}", style(hint).yellow());
|
||||
eprintln!("{}", Paint::yellow(hint));
|
||||
}
|
||||
|
||||
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};
|
||||
|
||||
pub use dialoguer::console::style;
|
||||
pub use crate::terminal::{style, Paint};
|
||||
|
||||
use radicle::cob::{ObjectId, Timestamp};
|
||||
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 {
|
||||
style(msg).red().bright().to_string()
|
||||
pub fn negative<D: std::fmt::Display>(msg: D) -> Paint<D> {
|
||||
Paint::red(msg).bold()
|
||||
}
|
||||
|
||||
pub fn positive<D: std::fmt::Display>(msg: D) -> String {
|
||||
style(msg).green().bright().to_string()
|
||||
pub fn positive<D: std::fmt::Display>(msg: D) -> Paint<D> {
|
||||
Paint::green(msg).bold()
|
||||
}
|
||||
|
||||
pub fn secondary<D: std::fmt::Display>(msg: D) -> String {
|
||||
style(msg).blue().bright().to_string()
|
||||
pub fn secondary<D: std::fmt::Display>(msg: D) -> Paint<D> {
|
||||
Paint::blue(msg).bold()
|
||||
}
|
||||
|
||||
pub fn tertiary<D: std::fmt::Display>(msg: D) -> String {
|
||||
style(msg).cyan().to_string()
|
||||
pub fn tertiary<D: std::fmt::Display>(msg: D) -> Paint<D> {
|
||||
Paint::cyan(msg)
|
||||
}
|
||||
|
||||
pub fn tertiary_bold<D: std::fmt::Display>(msg: D) -> String {
|
||||
style(msg).cyan().bold().to_string()
|
||||
pub fn tertiary_bold<D: std::fmt::Display>(msg: D) -> Paint<D> {
|
||||
Paint::cyan(msg).bold()
|
||||
}
|
||||
|
||||
pub fn yellow<D: std::fmt::Display>(msg: D) -> String {
|
||||
style(msg).yellow().to_string()
|
||||
pub fn yellow<D: std::fmt::Display>(msg: D) -> Paint<D> {
|
||||
Paint::yellow(msg)
|
||||
}
|
||||
|
||||
pub fn highlight<D: std::fmt::Display>(input: D) -> String {
|
||||
style(input).green().bright().to_string()
|
||||
pub fn highlight<D: std::fmt::Debug + std::fmt::Display>(input: D) -> Paint<D> {
|
||||
Paint::green(input).bold()
|
||||
}
|
||||
|
||||
pub fn badge_primary<D: std::fmt::Display>(input: D) -> String {
|
||||
style(format!(" {input} ")).magenta().reverse().to_string()
|
||||
pub fn badge_primary<D: std::fmt::Display>(input: D) -> Paint<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 {
|
||||
style(format!(" {input} ")).green().reverse().to_string()
|
||||
pub fn badge_positive<D: std::fmt::Display>(input: D) -> Paint<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 {
|
||||
style(format!(" {input} ")).red().reverse().to_string()
|
||||
pub fn badge_negative<D: std::fmt::Display>(input: D) -> Paint<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 {
|
||||
style(format!(" {input} ")).blue().reverse().to_string()
|
||||
pub fn badge_secondary<D: std::fmt::Display>(input: D) -> Paint<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 {
|
||||
style(input).white().bright().bold().to_string()
|
||||
pub fn bold<D: std::fmt::Display>(input: D) -> Paint<D> {
|
||||
Paint::white(input).bold()
|
||||
}
|
||||
|
||||
pub fn dim<D: std::fmt::Display>(input: D) -> String {
|
||||
style(input).dim().to_string()
|
||||
pub fn dim<D: std::fmt::Display>(input: D) -> Paint<D> {
|
||||
Paint::new(input).dim()
|
||||
}
|
||||
|
||||
pub fn italic<D: std::fmt::Display>(input: D) -> String {
|
||||
style(input).italic().dim().to_string()
|
||||
pub fn italic<D: std::fmt::Display>(input: D) -> Paint<D> {
|
||||
Paint::new(input).italic().dim()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,39 @@
|
|||
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::thread::CommentId;
|
||||
use radicle::crypto::ssh::keystore::Passphrase;
|
||||
use radicle::cob::thread::{Comment, CommentId};
|
||||
use radicle::crypto::ssh::keystore::{MemorySigner, Passphrase};
|
||||
use radicle::crypto::Signer;
|
||||
use radicle::profile;
|
||||
use radicle::profile::Profile;
|
||||
|
||||
use radicle_crypto::ssh::keystore::MemorySigner;
|
||||
|
||||
use super::command;
|
||||
use super::format;
|
||||
use super::spinner::spinner;
|
||||
use super::Error;
|
||||
use super::{style, Paint};
|
||||
|
||||
pub const ERROR_PREFIX: Paint<&str> = Paint::red("✗");
|
||||
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_rules! info {
|
||||
($($arg:tt)*) => ({
|
||||
|
|
@ -45,17 +60,15 @@ pub use success;
|
|||
pub use tip;
|
||||
|
||||
pub fn success_args(args: fmt::Arguments) {
|
||||
println!("{} {args}", style("ok").green().reverse());
|
||||
println!("{} {args}", Paint::green("✓"));
|
||||
}
|
||||
|
||||
pub fn tip_args(args: fmt::Arguments) {
|
||||
println!("{} {}", style("=>").blue(), style(format!("{args}")).dim());
|
||||
println!("👉 {}", style(format!("{args}")).italic());
|
||||
}
|
||||
|
||||
pub fn width() -> Option<usize> {
|
||||
console::Term::stdout()
|
||||
.size_checked()
|
||||
.map(|(_, cols)| cols as usize)
|
||||
pub fn columns() -> Option<usize> {
|
||||
termion::terminal_size().map(|(cols, _)| cols as usize).ok()
|
||||
}
|
||||
|
||||
pub fn headline(headline: &str) {
|
||||
|
|
@ -66,7 +79,7 @@ pub fn headline(headline: &str) {
|
|||
|
||||
pub fn header(header: &str) {
|
||||
println!();
|
||||
println!("{}", style(format::yellow(header)).bold().underlined());
|
||||
println!("{}", style(format::yellow(header)).bold().underline());
|
||||
println!();
|
||||
}
|
||||
|
||||
|
|
@ -95,9 +108,9 @@ pub fn help(name: &str, version: &str, description: &str, usage: &str) {
|
|||
pub fn usage(name: &str, usage: &str) {
|
||||
println!(
|
||||
"{} {}\n{}",
|
||||
style("==").red(),
|
||||
style(format!("Error: rad-{name}: invalid usage")).red(),
|
||||
style(prefixed(TAB, usage)).red().dim()
|
||||
ERROR_PREFIX,
|
||||
Paint::red(format!("Error: rad-{name}: invalid usage")),
|
||||
Paint::red(prefixed(TAB, usage)).dim()
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -115,57 +128,39 @@ pub fn subcommand(msg: impl fmt::Display) {
|
|||
|
||||
pub fn warning(warning: &str) {
|
||||
println!(
|
||||
"{} {} {}",
|
||||
style("**").yellow(),
|
||||
style("Warning:").yellow().bold(),
|
||||
style(warning).yellow()
|
||||
"{} {} {warning}",
|
||||
Paint::yellow("!"),
|
||||
Paint::yellow("Warning:").bold(),
|
||||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
let err = error.to_string();
|
||||
let err = err.trim_end();
|
||||
let separator = if err.len() > 160 || err.contains('\n') {
|
||||
"\n"
|
||||
} else {
|
||||
" "
|
||||
};
|
||||
let separator = if err.contains('\n') { ":\n" } else { ": " };
|
||||
|
||||
println!(
|
||||
"{} {}{}{}",
|
||||
style("==").red(),
|
||||
style(header).red().reverse(),
|
||||
separator,
|
||||
style(error).red().bold(),
|
||||
"{ERROR_PREFIX} {}{}{error}",
|
||||
Paint::red(header).bold(),
|
||||
Paint::red(separator),
|
||||
);
|
||||
|
||||
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>() {
|
||||
println!("{} {}", style("==").yellow(), style(hint).yellow(),);
|
||||
println!("{} {}", Paint::yellow("×"), Paint::yellow(hint));
|
||||
blank();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ask<D: fmt::Display>(prompt: D, default: bool) -> bool {
|
||||
dialoguer::Confirm::new()
|
||||
.with_prompt(format!("{} {}", style(" ⤷".to_owned()).cyan(), prompt))
|
||||
.wait_for_newline(false)
|
||||
.default(true)
|
||||
.default(default)
|
||||
.interact()
|
||||
let prompt = format!("{} {}", Paint::blue("?".to_owned()), prompt);
|
||||
|
||||
Confirm::new(&prompt)
|
||||
.with_default(default)
|
||||
.prompt()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
|
|
@ -182,209 +177,95 @@ pub fn signer(profile: &Profile) -> anyhow::Result<Box<dyn Signer>> {
|
|||
if let Ok(signer) = profile.signer() {
|
||||
return Ok(signer);
|
||||
}
|
||||
|
||||
let passphrase = secret_input();
|
||||
let passphrase = passphrase()?;
|
||||
let spinner = spinner("Unsealing key...");
|
||||
let signer = MemorySigner::load(&profile.keystore, passphrase)?;
|
||||
|
||||
spinner.finish();
|
||||
|
||||
Ok(signer.boxed())
|
||||
}
|
||||
|
||||
pub fn theme() -> ColorfulTheme {
|
||||
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>
|
||||
pub fn input<S, E>(message: &str, default: Option<S>) -> anyhow::Result<S>
|
||||
where
|
||||
S: fmt::Display + std::str::FromStr<Err = E> + Clone,
|
||||
E: fmt::Debug + fmt::Display,
|
||||
{
|
||||
let theme = theme();
|
||||
let mut input: Input<S> = Input::with_theme(&theme);
|
||||
|
||||
let input = CustomType::<S>::new(message).with_render_config(*CONFIG);
|
||||
let value = match default {
|
||||
Some(default) => input
|
||||
.with_prompt(message)
|
||||
.with_initial_text(default.to_string())
|
||||
.interact_text()?,
|
||||
None => input.with_prompt(message).interact_text()?,
|
||||
Some(default) => input.with_default(default).prompt()?,
|
||||
None => input.prompt()?,
|
||||
};
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct Optional<T> {
|
||||
option: Option<T>,
|
||||
}
|
||||
|
||||
impl<T: fmt::Display> fmt::Display for Optional<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if let Some(val) = &self.option {
|
||||
write!(f, "{val}")
|
||||
} else {
|
||||
write!(f, "")
|
||||
}
|
||||
pub fn passphrase() -> Result<Passphrase, anyhow::Error> {
|
||||
if let Some(p) = profile::env::passphrase() {
|
||||
Ok(p)
|
||||
} else {
|
||||
Ok(Passphrase::from(
|
||||
Password::new("Passphrase:")
|
||||
.with_render_config(*CONFIG)
|
||||
.with_display_mode(inquire::PasswordDisplayMode::Masked)
|
||||
.without_confirmation()
|
||||
.prompt()?,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: FromStr> FromStr for Optional<T> {
|
||||
type Err = <T as FromStr>::Err;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
if s.is_empty() {
|
||||
return Ok(Optional { option: None });
|
||||
}
|
||||
let val: T = s.parse()?;
|
||||
|
||||
Ok(Self { option: Some(val) })
|
||||
pub fn passphrase_confirm() -> Result<Passphrase, anyhow::Error> {
|
||||
if let Some(p) = profile::env::passphrase() {
|
||||
Ok(p)
|
||||
} else {
|
||||
Ok(Passphrase::from(
|
||||
Password::new("Passphrase:")
|
||||
.with_render_config(*CONFIG)
|
||||
.with_display_mode(inquire::PasswordDisplayMode::Masked)
|
||||
.with_custom_confirmation_message("Repeat passphrase:")
|
||||
.with_custom_confirmation_error_message("The passphrases don't match.")
|
||||
.prompt()?,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn text_input_optional<S, E>(
|
||||
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> {
|
||||
pub fn passphrase_stdin() -> Result<Passphrase, anyhow::Error> {
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
|
||||
Ok(Passphrase::from(input.trim_end().to_owned()))
|
||||
}
|
||||
|
||||
pub fn read_passphrase(stdin: bool, confirm: bool) -> Result<Passphrase, anyhow::Error> {
|
||||
let passphrase = match profile::env::read_passphrase() {
|
||||
Some(input) => input,
|
||||
None => {
|
||||
if stdin {
|
||||
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>
|
||||
pub fn select<'a, T>(
|
||||
prompt: &str,
|
||||
options: &'a [T],
|
||||
active: &'a T,
|
||||
) -> Result<Option<&'a T>, InquireError>
|
||||
where
|
||||
T: fmt::Display + Eq + PartialEq,
|
||||
{
|
||||
let theme = theme();
|
||||
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 {
|
||||
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>
|
||||
where
|
||||
T: fmt::Display + Eq + PartialEq,
|
||||
{
|
||||
let theme = theme();
|
||||
let active = options.iter().position(|o| o == active);
|
||||
let mut selection = dialoguer::Select::with_theme(&theme);
|
||||
pub fn comment_select(issue: &Issue) -> Option<(&CommentId, &Comment)> {
|
||||
let comments = issue.comments().collect::<Vec<_>>();
|
||||
let selection = Select::new(
|
||||
"Which comment do you want to react to?",
|
||||
(0..comments.len()).collect(),
|
||||
)
|
||||
.with_render_config(*CONFIG)
|
||||
.with_formatter(&|i| comments[i.index].1.body().to_owned())
|
||||
.prompt()
|
||||
.ok()?;
|
||||
|
||||
selection.with_prompt(prompt);
|
||||
|
||||
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)
|
||||
comments.get(selection).copied()
|
||||
}
|
||||
|
||||
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 {
|
||||
use std::fmt::Write as _;
|
||||
|
||||
|
|
@ -406,44 +283,44 @@ pub mod proposal {
|
|||
identity::Identity,
|
||||
};
|
||||
|
||||
use super::{super::format, theme};
|
||||
use super::*;
|
||||
use crate::terminal::format;
|
||||
|
||||
pub fn revision_select(
|
||||
proposal: &Proposal,
|
||||
) -> Option<(&identity::RevisionId, &identity::Revision)> {
|
||||
let selection = dialoguer::Select::with_theme(&theme())
|
||||
.with_prompt("Which revision do you want to select?")
|
||||
.items(
|
||||
&proposal
|
||||
.revisions()
|
||||
.map(|(rid, _)| rid.to_string())
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.default(0)
|
||||
.interact_opt()
|
||||
.unwrap();
|
||||
let revisions = proposal.revisions().collect::<Vec<_>>();
|
||||
let selection = Select::new(
|
||||
"Which revision do you want to select?",
|
||||
(0..revisions.len()).collect(),
|
||||
)
|
||||
.with_vim_mode(true)
|
||||
.with_formatter(&|ix| revisions[ix.index].0.to_string())
|
||||
.with_render_config(*CONFIG)
|
||||
.prompt()
|
||||
.ok()?;
|
||||
|
||||
selection.and_then(|n| proposal.revisions().nth(n))
|
||||
revisions.get(selection).copied()
|
||||
}
|
||||
|
||||
pub fn revision_commit_select<'a>(
|
||||
proposal: &'a Proposal,
|
||||
previous: &'a Identity<Oid>,
|
||||
) -> Option<(&'a identity::RevisionId, &'a identity::Revision)> {
|
||||
let selection = dialoguer::Select::with_theme(&theme())
|
||||
.with_prompt("Which revision do you want to commit?")
|
||||
.items(
|
||||
&proposal
|
||||
.revisions()
|
||||
.filter(|(_, r)| r.is_quorum_reached(previous))
|
||||
.map(|(rid, _)| rid.to_string())
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.default(0)
|
||||
.interact_opt()
|
||||
.unwrap();
|
||||
let revisions = proposal
|
||||
.revisions()
|
||||
.filter(|(_, r)| r.is_quorum_reached(previous))
|
||||
.collect::<Vec<_>>();
|
||||
let selection = Select::new(
|
||||
"Which revision do you want to commit?",
|
||||
(0..revisions.len()).collect(),
|
||||
)
|
||||
.with_formatter(&|ix| revisions[ix.index].0.to_string())
|
||||
.with_render_config(*CONFIG)
|
||||
.prompt()
|
||||
.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> {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
use radicle::git;
|
||||
|
||||
use crate::terminal as term;
|
||||
use crate::terminal::cell::Cell as _;
|
||||
|
||||
/// The user supplied `Patch` description.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
|
|
@ -18,15 +19,14 @@ impl Message {
|
|||
pub fn get(self, help: &str) -> String {
|
||||
let comment = match self {
|
||||
Message::Edit => term::Editor::new()
|
||||
.require_save(true)
|
||||
.trim_newlines(true)
|
||||
.extension(".markdown")
|
||||
.extension("markdown")
|
||||
.edit(help)
|
||||
.unwrap(),
|
||||
.ok()
|
||||
.flatten(),
|
||||
Message::Blank => None,
|
||||
Message::Text(c) => Some(c),
|
||||
};
|
||||
let comment = comment.unwrap_or_default().replace(help, "");
|
||||
let comment = comment.unwrap_or_default();
|
||||
let comment = comment.trim();
|
||||
|
||||
comment.to_owned()
|
||||
|
|
@ -57,7 +57,7 @@ pub fn list_commits(commits: &[git::raw::Commit]) -> anyhow::Result<()> {
|
|||
.unwrap_or_else(|| commit.message_bytes());
|
||||
table.push([
|
||||
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();
|
||||
|
|
@ -100,6 +100,6 @@ pub fn print_title_desc(title: &str, description: &str) {
|
|||
term::blank();
|
||||
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 indicatif::{ProgressBar, ProgressFinish, ProgressStyle};
|
||||
use std::io::Write;
|
||||
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 {
|
||||
progress: ProgressBar,
|
||||
message: String,
|
||||
/// How much time to wait between spinner animation updates.
|
||||
pub const DEFAULT_TICK: time::Duration = time::Duration::from_millis(99);
|
||||
/// 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 {
|
||||
fn drop(&mut self) {
|
||||
// TODO: Set error that will be output on fail.
|
||||
if !self.progress.is_finished() {
|
||||
self.set_failed();
|
||||
impl Progress {
|
||||
fn new(message: Paint<String>) -> Self {
|
||||
Self {
|
||||
state: State::Running { cursor: 0 },
|
||||
message,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Spinner {
|
||||
pub fn finish(&self) {
|
||||
self.progress.finish_and_clear();
|
||||
term::success!("{}", &self.message);
|
||||
}
|
||||
enum State {
|
||||
Running { cursor: usize },
|
||||
Canceled,
|
||||
Done,
|
||||
Error,
|
||||
}
|
||||
|
||||
pub fn done(self) {
|
||||
self.progress.finish_and_clear();
|
||||
term::info!("{}", &self.message);
|
||||
}
|
||||
/// A progress spinner.
|
||||
pub struct Spinner {
|
||||
progress: Arc<Mutex<Progress>>,
|
||||
handle: ManuallyDrop<thread::JoinHandle<()>>,
|
||||
}
|
||||
|
||||
pub fn failed(mut self) {
|
||||
self.set_failed();
|
||||
}
|
||||
|
||||
pub fn error(mut self, msg: impl ToString) {
|
||||
let msg = msg.to_string();
|
||||
|
||||
self.message = format!("{} error: {}", self.message, msg);
|
||||
self.set_failed();
|
||||
}
|
||||
|
||||
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 Drop for Spinner {
|
||||
fn drop(&mut self) {
|
||||
if let Ok(mut progress) = self.progress.lock() {
|
||||
if let State::Running { .. } = progress.state {
|
||||
progress.state = State::Canceled;
|
||||
}
|
||||
}
|
||||
unsafe { ManuallyDrop::take(&mut self.handle) }
|
||||
.join()
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
let message = message.to_string();
|
||||
let style = ProgressStyle::default_spinner()
|
||||
.tick_strings(&[
|
||||
&style("\\ ").yellow().to_string(),
|
||||
&style("| ").yellow().to_string(),
|
||||
&style("/ ").yellow().to_string(),
|
||||
&style("| ").yellow().to_string(),
|
||||
])
|
||||
.template("{spinner} {msg}")
|
||||
.on_finish(ProgressFinish::AndClear);
|
||||
let progress = Arc::new(Mutex::new(Progress::new(Paint::new(message))));
|
||||
let handle = thread::spawn({
|
||||
let progress = progress.clone();
|
||||
|
||||
let progress = ProgressBar::new(!0);
|
||||
progress.set_style(style);
|
||||
progress.enable_steady_tick(99);
|
||||
progress.set_message(message.clone());
|
||||
move || {
|
||||
let mut stdout = io::stdout();
|
||||
let mut stderr = termion::cursor::HideCursor::from(io::stderr());
|
||||
|
||||
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(["spider mite", "ladybug"]);
|
||||
//! t.render();
|
||||
//! // pest biological control
|
||||
//! // aphid ladybug
|
||||
//! // spider mite persimilis
|
||||
//! ```
|
||||
|
||||
use std::fmt::Write;
|
||||
//! Output:
|
||||
//! ``` plain
|
||||
//! pest biological control
|
||||
//! aphid ladybug
|
||||
//! spider mite persimilis
|
||||
//! ```
|
||||
use std::io;
|
||||
|
||||
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)]
|
||||
pub struct TableOptions {
|
||||
/// Whether the table should be allowed to overflow.
|
||||
pub overflow: bool,
|
||||
/// The maximum width and height.
|
||||
pub max: Max,
|
||||
}
|
||||
|
||||
#[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());
|
||||
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);
|
||||
}
|
||||
|
||||
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 {
|
||||
let mut output = String::new();
|
||||
|
|
@ -66,27 +83,25 @@ impl<const W: usize> Table<W> {
|
|||
|
||||
for (i, cell) in row.iter().enumerate() {
|
||||
if i == cells - 1 || self.opts.overflow {
|
||||
write!(output, "{cell}").ok();
|
||||
output.push_str(cell.to_string().as_str());
|
||||
} else {
|
||||
write!(
|
||||
output,
|
||||
"{} ",
|
||||
console::pad_str(cell, self.widths[i], console::Alignment::Left, None)
|
||||
)
|
||||
.ok();
|
||||
output.push_str(cell.pad(self.widths[i]).as_str());
|
||||
output.push(' ');
|
||||
}
|
||||
}
|
||||
|
||||
let output = output.trim_end();
|
||||
println!(
|
||||
writeln!(
|
||||
writer,
|
||||
"{}",
|
||||
if let Some(width) = width {
|
||||
console::truncate_str(output, width - 1, "…")
|
||||
output.truncate(width, "…")
|
||||
} else {
|
||||
output.into()
|
||||
}
|
||||
);
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn render_tree(self) {
|
||||
|
|
@ -97,12 +112,116 @@ impl<const W: usize> Table<W> {
|
|||
print!("└── ");
|
||||
}
|
||||
for (i, cell) in row.iter().enumerate() {
|
||||
print!(
|
||||
"{} ",
|
||||
console::pad_str(cell, self.widths[i], console::Alignment::Left, None)
|
||||
);
|
||||
print!("{} ", cell.pad(self.widths[i]));
|
||||
}
|
||||
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 crate::terminal as term;
|
||||
use crate::terminal::cell::Cell as _;
|
||||
|
||||
pub struct TextBox {
|
||||
pub body: String,
|
||||
|
|
@ -32,14 +33,8 @@ impl TextBox {
|
|||
|
||||
impl fmt::Display for TextBox {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut width = self
|
||||
.body
|
||||
.lines()
|
||||
.map(console::measure_text_width)
|
||||
.max()
|
||||
.unwrap_or(0)
|
||||
+ 2;
|
||||
if let Some(max) = term::width() {
|
||||
let mut width = self.body.lines().map(|l| l.width()).max().unwrap_or(0) + 2;
|
||||
if let Some(max) = term::columns() {
|
||||
if width + 2 > max {
|
||||
width = max - 2
|
||||
}
|
||||
|
|
@ -53,11 +48,7 @@ impl fmt::Display for TextBox {
|
|||
writeln!(f, "┌{}{}┐", connector, "─".repeat(header_width))?;
|
||||
|
||||
for l in self.body.lines() {
|
||||
writeln!(
|
||||
f,
|
||||
"│ {}│",
|
||||
console::pad_str(l, width - 1, console::Alignment::Left, Some("…"))
|
||||
)?;
|
||||
writeln!(f, "│ {}│", l.pad(width - 1))?;
|
||||
}
|
||||
|
||||
let (connector, footer_width) = if !self.last {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@ git-ref-format = { version = "0", features = ["serde", "macro"] }
|
|||
version = "0"
|
||||
path = "../radicle"
|
||||
|
||||
[dependencies.radicle-cli]
|
||||
version = "0"
|
||||
path = "../radicle-cli"
|
||||
|
||||
[[bin]]
|
||||
name = "rad-init"
|
||||
path = "src/rad-init.rs"
|
||||
|
|
@ -32,3 +36,7 @@ path = "src/rad-push.rs"
|
|||
[[bin]]
|
||||
name = "rad-agent"
|
||||
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]
|
||||
pub fn name(&self) -> &String {
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn description(&self) -> &String {
|
||||
pub fn description(&self) -> &str {
|
||||
&self.description
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ pub mod env {
|
|||
/// Passphrase for the encrypted radicle secret key.
|
||||
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 {
|
||||
return None;
|
||||
};
|
||||
|
|
@ -109,7 +109,7 @@ impl Profile {
|
|||
}
|
||||
|
||||
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)?;
|
||||
return Ok(signer.boxed());
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue