cli: Add `rad config` command

Displays the current configuration.

Also move to our tree-sitter highlighter for pretty-printing JSON and
remove the `json-color` dependency.
This commit is contained in:
cloudhead 2023-11-27 13:56:43 +01:00
parent 59a08f21a4
commit ad7a3addb6
No known key found for this signature in database
14 changed files with 215 additions and 54 deletions

27
Cargo.lock generated
View File

@ -1668,17 +1668,6 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[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]] [[package]]
name = "keccak" name = "keccak"
version = "0.1.4" version = "0.1.4"
@ -2298,7 +2287,6 @@ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
"git-ref-format", "git-ref-format",
"json-color",
"lexopt", "lexopt",
"localtime", "localtime",
"log", "log",
@ -2326,6 +2314,7 @@ dependencies = [
"tree-sitter-go", "tree-sitter-go",
"tree-sitter-highlight", "tree-sitter-highlight",
"tree-sitter-html", "tree-sitter-html",
"tree-sitter-json",
"tree-sitter-md", "tree-sitter-md",
"tree-sitter-python", "tree-sitter-python",
"tree-sitter-ruby", "tree-sitter-ruby",
@ -3540,6 +3529,16 @@ dependencies = [
"tree-sitter", "tree-sitter",
] ]
[[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]] [[package]]
name = "tree-sitter-md" name = "tree-sitter-md"
version = "0.1.5" version = "0.1.5"
@ -3592,9 +3591,9 @@ dependencies = [
[[package]] [[package]]
name = "tree-sitter-typescript" name = "tree-sitter-typescript"
version = "0.20.2" version = "0.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "079c695c32d39ad089101c66393aeaca30e967fba3486a91f573d2f0e12d290a" checksum = "a75049f0aafabb2aac205d7bb24da162b53dcd0cfb326785f25a2f32efa8071a"
dependencies = [ dependencies = [
"cc", "cc",
"tree-sitter", "tree-sitter",

View File

@ -14,7 +14,6 @@ path = "src/main.rs"
anyhow = { version = "1" } anyhow = { version = "1" }
chrono = { version = "0.4.26", default-features = false, features = ["clock", "std"] } chrono = { version = "0.4.26", default-features = false, features = ["clock", "std"] }
git-ref-format = { version = "0.3.0", features = ["macro"] } git-ref-format = { version = "0.3.0", features = ["macro"] }
json-color = { version = "0.7" }
lexopt = { version = "0.2" } lexopt = { version = "0.2" }
localtime = { version = "1.2.0" } localtime = { version = "1.2.0" }
log = { version = "0.4", features = ["std"] } log = { version = "0.4", features = ["std"] }
@ -32,8 +31,9 @@ thiserror = { version = "1" }
timeago = { version = "0.3", default-features = false } timeago = { version = "0.3", default-features = false }
tree-sitter = { version = "0.20.0" } tree-sitter = { version = "0.20.0" }
tree-sitter-highlight = { version = "0.20" } tree-sitter-highlight = { version = "0.20" }
tree-sitter-json = { version = "0.20.1" }
tree-sitter-rust = { version = "0.20" } 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 # N.b. This crate has a C++ token scanner that causes problems when building
# for the musl target. Hence it is optional for now. # for the musl target. Hence it is optional for now.
tree-sitter-html = { version = "0.19", optional = true } tree-sitter-html = { version = "0.19", optional = true }

View File

@ -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"
}
}
```

View File

@ -10,6 +10,8 @@ pub mod rad_clean;
pub mod rad_clone; pub mod rad_clone;
#[path = "commands/cob.rs"] #[path = "commands/cob.rs"]
pub mod rad_cob; pub mod rad_cob;
#[path = "commands/config.rs"]
pub mod rad_config;
#[path = "commands/diff.rs"] #[path = "commands/diff.rs"]
pub mod rad_diff; pub mod rad_diff;
#[path = "commands/fork.rs"] #[path = "commands/fork.rs"]

View File

@ -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(())
}

View File

@ -1,12 +1,11 @@
#![allow(clippy::or_fun_call)] #![allow(clippy::or_fun_call)]
use std::collections::HashMap; use std::collections::HashMap;
use std::ffi::OsString; use std::ffi::OsString;
use std::path::PathBuf; use std::path::{Path, PathBuf};
use std::str::FromStr; use std::str::FromStr;
use anyhow::{anyhow, Context as _}; use anyhow::{anyhow, Context as _};
use chrono::prelude::*; use chrono::prelude::*;
use json_color::{Color, Colorizer};
use radicle::identity::Id; use radicle::identity::Id;
use radicle::identity::Identity; use radicle::identity::Identity;
@ -14,9 +13,11 @@ use radicle::node::tracking::Policy;
use radicle::node::AliasStore as _; use radicle::node::AliasStore as _;
use radicle::storage::refs::RefsAt; use radicle::storage::refs::RefsAt;
use radicle::storage::{ReadRepository, ReadStorage}; use radicle::storage::{ReadRepository, ReadStorage};
use radicle_term::Element;
use crate::terminal as term; use crate::terminal as term;
use crate::terminal::args::{Args, Error, Help}; use crate::terminal::args::{Args, Error, Help};
use crate::terminal::json;
pub const HELP: Help = Help { pub const HELP: Help = Help {
name: "inspect", name: "inspect",
@ -154,16 +155,10 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
refs(&repo)?; refs(&repo)?;
} }
Target::Payload => { Target::Payload => {
println!( json::to_pretty(&project.payload, Path::new("radicle.json"))?.print();
"{}",
colorizer().colorize_json_str(&serde_json::to_string_pretty(&project.payload)?)?
);
} }
Target::Identity => { Target::Identity => {
println!( json::to_pretty(&project.doc, Path::new("radicle.json"))?.print();
"{}",
colorizer().colorize_json_str(&serde_json::to_string_pretty(&project.doc)?)?
);
} }
Target::Sigrefs => { Target::Sigrefs => {
for remote in repo.remote_ids()? { for remote in repo.remote_ids()? {
@ -265,11 +260,10 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
} }
term::blank(); term::blank();
} }
for line in json::to_pretty(&doc, Path::new("radicle.json"))? {
let json = colorizer().colorize_json_str(&serde_json::to_string_pretty(&doc)?)?;
for line in json.lines() {
println!(" {line}"); println!(" {line}");
} }
println!(); println!();
} }
} }
@ -281,17 +275,6 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
Ok(()) 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<()> { fn refs(repo: &radicle::storage::git::Repository) -> anyhow::Result<()> {
let mut refs = Vec::new(); let mut refs = Vec::new();
for r in repo.references()? { for r in repo.references()? {

View File

@ -132,6 +132,13 @@ fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option<anyhow::Error>>
args.to_vec(), args.to_vec(),
); );
} }
"config" => {
term::run_command_args::<rad_config::Options, _>(
rad_config::HELP,
rad_config::run,
args.to_vec(),
);
}
"diff" => { "diff" => {
term::run_command_args::<rad_diff::Options, _>( term::run_command_args::<rad_diff::Options, _>(
rad_diff::HELP, rad_diff::HELP,

View File

@ -6,6 +6,7 @@ pub use io::signer;
pub mod comment; pub mod comment;
pub mod highlight; pub mod highlight;
pub mod issue; pub mod issue;
pub mod json;
pub mod patch; pub mod patch;
use std::ffi::OsString; use std::ffi::OsString;

View File

@ -16,6 +16,7 @@ const HIGHLIGHTS: &[&str] = &[
"float.literal", "float.literal",
"keyword", "keyword",
"label", "label",
"number",
"operator", "operator",
"property", "property",
"punctuation", "punctuation",
@ -72,12 +73,14 @@ impl Theme {
"keyword" => self.color("red"), "keyword" => self.color("red"),
"comment" => self.color("grey"), "comment" => self.color("grey"),
"constant" => self.color("orange"), "constant" => self.color("orange"),
"number" => self.color("blue"),
"string" => self.color("teal"), "string" => self.color("teal"),
"string.special" => self.color("green"),
"function" => self.color("purple"), "function" => self.color("purple"),
"operator" => self.color("blue"), "operator" => self.color("blue"),
// Eg. `true` and `false` in rust. // Eg. `true` and `false` in rust.
"constant.builtin" => self.color("blue"), "constant.builtin" => self.color("blue"),
"type.builtin" => self.color("cyan"), "type.builtin" => self.color("teal"),
"punctuation.bracket" | "punctuation.delimiter" => term::Color::default(), "punctuation.bracket" | "punctuation.delimiter" => term::Color::default(),
// Eg. the '#' in Markdown titles. // Eg. the '#' in Markdown titles.
"punctuation.special" => self.color("dim"), "punctuation.special" => self.color("dim"),
@ -124,14 +127,17 @@ impl Builder {
for event in highlights { for event in highlights {
match event? { match event? {
ts::HighlightEvent::Source { start, end } => { ts::HighlightEvent::Source { start, end } => {
let range = &code[start..end]; for (i, byte) in code.iter().enumerate().skip(start).take(end - start) {
for byte in range {
if *byte == b'\n' { if *byte == b'\n' {
self.advance(); self.advance();
// Start on new line. // Start on new line.
self.lines.push(term::Line::from(self.line.clone())); self.lines.push(term::Line::from(self.line.clone()));
self.line.clear(); 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 { } else {
// Add to existing label. // Add to existing label.
self.label.push(*byte); self.label.push(*byte);
@ -140,10 +146,11 @@ impl Builder {
} }
ts::HighlightEvent::HighlightStart(h) => { ts::HighlightEvent::HighlightStart(h) => {
let name = HIGHLIGHTS[h.0]; let name = HIGHLIGHTS[h.0];
let style =
term::Style::default().fg(theme.highlight(name).unwrap_or_default());
self.advance(); self.advance();
self.styles self.styles.push(style);
.push(term::Style::default().fg(theme.highlight(name).unwrap_or_default()));
} }
ts::HighlightEvent::HighlightEnd => { ts::HighlightEvent::HighlightEnd => {
self.advance(); self.advance();
@ -193,7 +200,8 @@ impl Highlighter {
fn detect(&mut self, path: &Path, _code: &[u8]) -> Option<&mut ts::HighlightConfiguration> { fn detect(&mut self, path: &Path, _code: &[u8]) -> Option<&mut ts::HighlightConfiguration> {
match path.extension().and_then(|e| e.to_str()) { match path.extension().and_then(|e| e.to_str()) {
Some("rs") => self.config("rust"), 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("sh" | "bash") => self.config("shell"),
Some("md" | "markdown") => self.config("markdown"), Some("md" | "markdown") => self.config("markdown"),
Some("go") => self.config("go"), Some("go") => self.config("go"),
@ -220,6 +228,15 @@ impl Highlighter {
) )
.expect("Highlighter::config: highlight configuration must be valid") .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(|| { "typescript" => Some(self.configs.entry(language).or_insert_with(|| {
ts::HighlightConfiguration::new( ts::HighlightConfiguration::new(
tree_sitter_typescript::language_typescript(), tree_sitter_typescript::language_typescript(),

View File

@ -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())
}
}

View File

@ -193,6 +193,21 @@ fn rad_inspect() {
test("examples/rad-inspect-noauth.md", working.path(), None, []).unwrap(); test("examples/rad-inspect-noauth.md", working.path(), None, []).unwrap();
} }
#[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] #[test]
fn rad_checkout() { fn rad_checkout() {
let mut environment = Environment::new(); let mut environment = Environment::new();

View File

@ -115,6 +115,7 @@ impl Environment {
node: node::Config::new(alias.clone()), node: node::Config::new(alias.clone()),
cli: cli::Config { hints: false }, cli: cli::Config { hints: false },
public_explorer: profile::Explorer::default(), public_explorer: profile::Explorer::default(),
preferred_seeds: vec![],
}; };
config.write(&home.config()).unwrap(); config.write(&home.config()).unwrap();

View File

@ -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 { impl fmt::Display for Line {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for item in &self.items { for item in &self.items {

View File

@ -163,11 +163,6 @@ pub enum ConfigError {
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Config { 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. /// Public explorer. This is used for generating links.
#[serde(default)] #[serde(default)]
pub public_explorer: Explorer, pub public_explorer: Explorer,
@ -175,16 +170,21 @@ pub struct Config {
/// and in other situations when a seed needs to be chosen. /// and in other situations when a seed needs to be chosen.
#[serde(default)] #[serde(default)]
pub preferred_seeds: Vec<node::config::ConnectAddress>, pub preferred_seeds: Vec<node::config::ConnectAddress>,
/// CLI configuration.
#[serde(default)]
pub cli: cli::Config,
/// Node configuration.
pub node: node::Config,
} }
impl Config { impl Config {
/// Create a new, default configuration. /// Create a new, default configuration.
pub fn new(alias: Alias) -> Self { pub fn new(alias: Alias) -> Self {
Self { Self {
node: node::Config::new(alias),
cli: cli::Config::default(),
public_explorer: Explorer::default(), public_explorer: Explorer::default(),
preferred_seeds: vec![node::config::seeds::RADICLE_COMMUNITY_NODE.clone()], preferred_seeds: vec![node::config::seeds::RADICLE_COMMUNITY_NODE.clone()],
cli: cli::Config::default(),
node: node::Config::new(alias),
} }
} }