Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
cli: Add `rad config` command
cloudhead committed 2 years ago
commit ad7a3addb695b7e72d20e75cfbece02f5bcc4114
parent 59a08f21a44538d824cb866e5dfe50f29206c0d0
14 files changed +215 -54
modified Cargo.lock
@@ -1669,17 +1669,6 @@ dependencies = [
]

[[package]]
-
name = "json-color"
-
version = "0.7.1"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "e6dc8c55175cad7234a98cc3e31ba3009e276800271692ed3ad2c2f1c574b6e8"
-
dependencies = [
-
 "colored",
-
 "serde",
-
 "serde_json",
-
]
-

-
[[package]]
name = "keccak"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2298,7 +2287,6 @@ dependencies = [
 "anyhow",
 "chrono",
 "git-ref-format",
-
 "json-color",
 "lexopt",
 "localtime",
 "log",
@@ -2326,6 +2314,7 @@ dependencies = [
 "tree-sitter-go",
 "tree-sitter-highlight",
 "tree-sitter-html",
+
 "tree-sitter-json",
 "tree-sitter-md",
 "tree-sitter-python",
 "tree-sitter-ruby",
@@ -3541,6 +3530,16 @@ dependencies = [
]

[[package]]
+
name = "tree-sitter-json"
+
version = "0.20.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "50d82d2e33ee675dc71289e2ace4f8f9cf96d36d81400e9dae5ea61edaf5dea6"
+
dependencies = [
+
 "cc",
+
 "tree-sitter",
+
]
+

+
[[package]]
name = "tree-sitter-md"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3592,9 +3591,9 @@ dependencies = [

[[package]]
name = "tree-sitter-typescript"
-
version = "0.20.2"
+
version = "0.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "079c695c32d39ad089101c66393aeaca30e967fba3486a91f573d2f0e12d290a"
+
checksum = "a75049f0aafabb2aac205d7bb24da162b53dcd0cfb326785f25a2f32efa8071a"
dependencies = [
 "cc",
 "tree-sitter",
modified radicle-cli/Cargo.toml
@@ -14,7 +14,6 @@ path = "src/main.rs"
anyhow = { version = "1" }
chrono = { version = "0.4.26", default-features = false, features = ["clock", "std"] }
git-ref-format = { version = "0.3.0", features = ["macro"] }
-
json-color = { version = "0.7" }
lexopt = { version = "0.2" }
localtime = { version = "1.2.0" }
log = { version = "0.4", features = ["std"] }
@@ -32,8 +31,9 @@ thiserror = { version = "1" }
timeago = { version = "0.3", default-features = false }
tree-sitter = { version = "0.20.0" }
tree-sitter-highlight = { version = "0.20" }
+
tree-sitter-json = { version = "0.20.1" }
tree-sitter-rust = { version = "0.20" }
-
tree-sitter-typescript = { version = "0.20" }
+
tree-sitter-typescript = { version = "0.20.3" }
# N.b. This crate has a C++ token scanner that causes problems when building
# for the musl target. Hence it is optional for now.
tree-sitter-html = { version = "0.19", optional = true }
added radicle-cli/examples/rad-config.md
@@ -0,0 +1,43 @@
+
The `rad config` command is used to manage the local user configuration.
+
In its simplest form, `rad config` prints the current configuration.
+

+
```
+
$ rad config
+
{
+
  "publicExplorer": "https://app.radicle.xyz/nodes/$host/$rid",
+
  "preferredSeeds": [],
+
  "cli": {
+
    "hints": false
+
  },
+
  "node": {
+
    "alias": "alice",
+
    "listen": [],
+
    "peers": {
+
      "type": "dynamic",
+
      "target": 8
+
    },
+
    "connect": [],
+
    "externalAddresses": [],
+
    "network": "main",
+
    "relay": true,
+
    "limits": {
+
      "routingMaxSize": 1000,
+
      "routingMaxAge": 604800,
+
      "gossipMaxAge": 1209600,
+
      "fetchConcurrency": 1,
+
      "rate": {
+
        "inbound": {
+
          "fillRate": 0.2,
+
          "capacity": 32
+
        },
+
        "outbound": {
+
          "fillRate": 1.0,
+
          "capacity": 64
+
        }
+
      }
+
    },
+
    "policy": "block",
+
    "scope": "trusted"
+
  }
+
}
+
```
modified radicle-cli/src/commands.rs
@@ -10,6 +10,8 @@ pub mod rad_clean;
pub mod rad_clone;
#[path = "commands/cob.rs"]
pub mod rad_cob;
+
#[path = "commands/config.rs"]
+
pub mod rad_config;
#[path = "commands/diff.rs"]
pub mod rad_diff;
#[path = "commands/fork.rs"]
added radicle-cli/src/commands/config.rs
@@ -0,0 +1,58 @@
+
#![allow(clippy::or_fun_call)]
+
use std::ffi::OsString;
+

+
use anyhow::anyhow;
+

+
use crate::terminal as term;
+
use crate::terminal::args::{Args, Error, Help};
+
use crate::terminal::Element as _;
+

+
pub const HELP: Help = Help {
+
    name: "config",
+
    description: "Manage your local radicle configuration",
+
    version: env!("CARGO_PKG_VERSION"),
+
    usage: r#"
+
Usage
+

+
    rad config [<option>...]
+

+
    If no argument is specified, prints the current radicle configuration as JSON.
+

+
Options
+

+
    --help    Print help
+

+
"#,
+
};
+

+
pub struct Options {}
+

+
impl Args for Options {
+
    fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
+
        use lexopt::prelude::*;
+

+
        let mut parser = lexopt::Parser::from_args(args);
+

+
        #[allow(clippy::never_loop)]
+
        while let Some(arg) = parser.next()? {
+
            match arg {
+
                Long("help") | Short('h') => {
+
                    return Err(Error::Help.into());
+
                }
+
                _ => return Err(anyhow!(arg.unexpected())),
+
            }
+
        }
+

+
        Ok((Options {}, vec![]))
+
    }
+
}
+

+
pub fn run(_options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
+
    let profile = ctx.profile()?;
+
    let path = profile.home.config();
+
    let output = term::json::to_pretty(&profile.config, path.as_path())?;
+

+
    output.print();
+

+
    Ok(())
+
}
modified radicle-cli/src/commands/inspect.rs
@@ -1,12 +1,11 @@
#![allow(clippy::or_fun_call)]
use std::collections::HashMap;
use std::ffi::OsString;
-
use std::path::PathBuf;
+
use std::path::{Path, PathBuf};
use std::str::FromStr;

use anyhow::{anyhow, Context as _};
use chrono::prelude::*;
-
use json_color::{Color, Colorizer};

use radicle::identity::Id;
use radicle::identity::Identity;
@@ -14,9 +13,11 @@ use radicle::node::tracking::Policy;
use radicle::node::AliasStore as _;
use radicle::storage::refs::RefsAt;
use radicle::storage::{ReadRepository, ReadStorage};
+
use radicle_term::Element;

use crate::terminal as term;
use crate::terminal::args::{Args, Error, Help};
+
use crate::terminal::json;

pub const HELP: Help = Help {
    name: "inspect",
@@ -154,16 +155,10 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
            refs(&repo)?;
        }
        Target::Payload => {
-
            println!(
-
                "{}",
-
                colorizer().colorize_json_str(&serde_json::to_string_pretty(&project.payload)?)?
-
            );
+
            json::to_pretty(&project.payload, Path::new("radicle.json"))?.print();
        }
        Target::Identity => {
-
            println!(
-
                "{}",
-
                colorizer().colorize_json_str(&serde_json::to_string_pretty(&project.doc)?)?
-
            );
+
            json::to_pretty(&project.doc, Path::new("radicle.json"))?.print();
        }
        Target::Sigrefs => {
            for remote in repo.remote_ids()? {
@@ -265,11 +260,10 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
                    }
                    term::blank();
                }
-

-
                let json = colorizer().colorize_json_str(&serde_json::to_string_pretty(&doc)?)?;
-
                for line in json.lines() {
+
                for line in json::to_pretty(&doc, Path::new("radicle.json"))? {
                    println!(" {line}");
                }
+

                println!();
            }
        }
@@ -281,17 +275,6 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
    Ok(())
}

-
// Used for JSON Colorizing
-
fn colorizer() -> Colorizer {
-
    Colorizer::new()
-
        .null(Color::Cyan)
-
        .boolean(Color::Cyan)
-
        .number(Color::Magenta)
-
        .string(Color::Green)
-
        .key(Color::Blue)
-
        .build()
-
}
-

fn refs(repo: &radicle::storage::git::Repository) -> anyhow::Result<()> {
    let mut refs = Vec::new();
    for r in repo.references()? {
modified radicle-cli/src/main.rs
@@ -132,6 +132,13 @@ fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option<anyhow::Error>>
                args.to_vec(),
            );
        }
+
        "config" => {
+
            term::run_command_args::<rad_config::Options, _>(
+
                rad_config::HELP,
+
                rad_config::run,
+
                args.to_vec(),
+
            );
+
        }
        "diff" => {
            term::run_command_args::<rad_diff::Options, _>(
                rad_diff::HELP,
modified radicle-cli/src/terminal.rs
@@ -6,6 +6,7 @@ pub use io::signer;
pub mod comment;
pub mod highlight;
pub mod issue;
+
pub mod json;
pub mod patch;

use std::ffi::OsString;
modified radicle-cli/src/terminal/highlight.rs
@@ -16,6 +16,7 @@ const HIGHLIGHTS: &[&str] = &[
    "float.literal",
    "keyword",
    "label",
+
    "number",
    "operator",
    "property",
    "punctuation",
@@ -72,12 +73,14 @@ impl Theme {
            "keyword" => self.color("red"),
            "comment" => self.color("grey"),
            "constant" => self.color("orange"),
+
            "number" => self.color("blue"),
            "string" => self.color("teal"),
+
            "string.special" => self.color("green"),
            "function" => self.color("purple"),
            "operator" => self.color("blue"),
            // Eg. `true` and `false` in rust.
            "constant.builtin" => self.color("blue"),
-
            "type.builtin" => self.color("cyan"),
+
            "type.builtin" => self.color("teal"),
            "punctuation.bracket" | "punctuation.delimiter" => term::Color::default(),
            // Eg. the '#' in Markdown titles.
            "punctuation.special" => self.color("dim"),
@@ -124,14 +127,17 @@ impl Builder {
        for event in highlights {
            match event? {
                ts::HighlightEvent::Source { start, end } => {
-
                    let range = &code[start..end];
-

-
                    for byte in range {
+
                    for (i, byte) in code.iter().enumerate().skip(start).take(end - start) {
                        if *byte == b'\n' {
                            self.advance();
                            // Start on new line.
                            self.lines.push(term::Line::from(self.line.clone()));
                            self.line.clear();
+
                        } else if i == code.len() - 1 {
+
                            // File has no `\n` at the end.
+
                            self.label.push(*byte);
+
                            self.advance();
+
                            self.lines.push(term::Line::from(self.line.clone()));
                        } else {
                            // Add to existing label.
                            self.label.push(*byte);
@@ -140,10 +146,11 @@ impl Builder {
                }
                ts::HighlightEvent::HighlightStart(h) => {
                    let name = HIGHLIGHTS[h.0];
+
                    let style =
+
                        term::Style::default().fg(theme.highlight(name).unwrap_or_default());

                    self.advance();
-
                    self.styles
-
                        .push(term::Style::default().fg(theme.highlight(name).unwrap_or_default()));
+
                    self.styles.push(style);
                }
                ts::HighlightEvent::HighlightEnd => {
                    self.advance();
@@ -193,7 +200,8 @@ impl Highlighter {
    fn detect(&mut self, path: &Path, _code: &[u8]) -> Option<&mut ts::HighlightConfiguration> {
        match path.extension().and_then(|e| e.to_str()) {
            Some("rs") => self.config("rust"),
-
            Some("ts" | "js" | "json") => self.config("typescript"),
+
            Some("ts" | "js") => self.config("typescript"),
+
            Some("json") => self.config("json"),
            Some("sh" | "bash") => self.config("shell"),
            Some("md" | "markdown") => self.config("markdown"),
            Some("go") => self.config("go"),
@@ -220,6 +228,15 @@ impl Highlighter {
                )
                .expect("Highlighter::config: highlight configuration must be valid")
            })),
+
            "json" => Some(self.configs.entry(language).or_insert_with(|| {
+
                ts::HighlightConfiguration::new(
+
                    tree_sitter_json::language(),
+
                    tree_sitter_json::HIGHLIGHT_QUERY,
+
                    "",
+
                    "",
+
                )
+
                .expect("Highlighter::config: highlight configuration must be valid")
+
            })),
            "typescript" => Some(self.configs.entry(language).or_insert_with(|| {
                ts::HighlightConfiguration::new(
                    tree_sitter_typescript::language_typescript(),
added radicle-cli/src/terminal/json.rs
@@ -0,0 +1,15 @@
+
use std::path::Path;
+

+
use crate::terminal as term;
+

+
/// Pretty-print a JSON value with syntax highlighting.
+
pub fn to_pretty(value: &impl serde::Serialize, path: &Path) -> anyhow::Result<Vec<term::Line>> {
+
    let json = serde_json::to_string_pretty(&value)?;
+
    let mut highlighter = term::highlight::Highlighter::default();
+

+
    if let Some(highlighted) = highlighter.highlight(path, json.as_bytes())? {
+
        Ok(highlighted)
+
    } else {
+
        Ok(json.split('\n').map(term::Line::new).collect())
+
    }
+
}
modified radicle-cli/tests/commands.rs
@@ -194,6 +194,21 @@ fn rad_inspect() {
}

#[test]
+
fn rad_config() {
+
    let mut environment = Environment::new();
+
    let profile = environment.profile("alice");
+
    let working = tempfile::tempdir().unwrap();
+

+
    test(
+
        "examples/rad-config.md",
+
        working.path(),
+
        Some(&profile.home),
+
        [],
+
    )
+
    .unwrap();
+
}
+

+
#[test]
fn rad_checkout() {
    let mut environment = Environment::new();
    let profile = environment.profile("alice");
modified radicle-node/src/test/environment.rs
@@ -115,6 +115,7 @@ impl Environment {
            node: node::Config::new(alias.clone()),
            cli: cli::Config { hints: false },
            public_explorer: profile::Explorer::default(),
+
            preferred_seeds: vec![],
        };
        config.write(&home.config()).unwrap();

modified radicle-term/src/element.rs
@@ -291,6 +291,26 @@ impl Element for Line {
    }
}

+
impl Element for Vec<Line> {
+
    fn size(&self, parent: Constraint) -> Size {
+
        let width = self
+
            .iter()
+
            .map(|e| e.columns(parent))
+
            .max()
+
            .unwrap_or_default();
+
        let height = self.len();
+

+
        Size::new(width, height)
+
    }
+

+
    fn render(&self, parent: Constraint) -> Vec<Line> {
+
        self.iter()
+
            .cloned()
+
            .flat_map(|l| l.render(parent))
+
            .collect()
+
    }
+
}
+

impl fmt::Display for Line {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        for item in &self.items {
modified radicle/src/profile.rs
@@ -163,11 +163,6 @@ pub enum ConfigError {
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Config {
-
    /// Node configuration.
-
    pub node: node::Config,
-
    /// CLI configuration.
-
    #[serde(default)]
-
    pub cli: cli::Config,
    /// Public explorer. This is used for generating links.
    #[serde(default)]
    pub public_explorer: Explorer,
@@ -175,16 +170,21 @@ pub struct Config {
    /// and in other situations when a seed needs to be chosen.
    #[serde(default)]
    pub preferred_seeds: Vec<node::config::ConnectAddress>,
+
    /// CLI configuration.
+
    #[serde(default)]
+
    pub cli: cli::Config,
+
    /// Node configuration.
+
    pub node: node::Config,
}

impl Config {
    /// Create a new, default configuration.
    pub fn new(alias: Alias) -> Self {
        Self {
-
            node: node::Config::new(alias),
-
            cli: cli::Config::default(),
            public_explorer: Explorer::default(),
            preferred_seeds: vec![node::config::seeds::RADICLE_COMMUNITY_NODE.clone()],
+
            cli: cli::Config::default(),
+
            node: node::Config::new(alias),
        }
    }