diff --git a/Cargo.lock b/Cargo.lock index 312caf9..6669165 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "ahash" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a824f2aa7e75a0c98c5a504fceb80649e9c35265d44525b5f94de4771a395cd" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + [[package]] name = "anstream" version = "0.6.4" @@ -77,6 +88,7 @@ version = "3.0.0-alpha.3" dependencies = [ "anyhow", "clap", + "config", "mio", "nix", "reqwest", @@ -102,6 +114,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "async-trait" +version = "0.1.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdf6721fb0140e4f897002dd086c06f6c27775df19cfe1fccb21181a48fd2c98" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -223,6 +246,21 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "config" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23738e11972c7643e4ec947840fc463b6a571afcd3e735bdfce7d03c7a784aca" +dependencies = [ + "async-trait", + "lazy_static", + "nom", + "pathdiff", + "rust-ini", + "serde", + "toml", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -248,6 +286,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "dlv-list" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" + [[package]] name = "encoding_rs" version = "0.8.33" @@ -405,6 +449,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + [[package]] name = "hashbrown" version = "0.14.3" @@ -521,7 +574,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.14.3", ] [[package]] @@ -545,6 +598,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + [[package]] name = "libc" version = "0.2.149" @@ -585,6 +644,12 @@ dependencies = [ "unicase", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.7.1" @@ -626,6 +691,16 @@ dependencies = [ "libc", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "num_cpus" version = "1.16.0" @@ -657,6 +732,22 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "ordered-multimap" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a" +dependencies = [ + "dlv-list", + "hashbrown 0.12.3", +] + +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + [[package]] name = "percent-encoding" version = "2.3.1" @@ -778,6 +869,16 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "rust-ini" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -1075,6 +1176,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + [[package]] name = "tower-service" version = "0.3.2" diff --git a/Cargo.toml b/Cargo.toml index a2a0ab2..97b8e6f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,3 +23,4 @@ signal-hook = "0.3.17" uuid = { version = "1.6.1", features = ["v4"] } reqwest = { version = "0.11.23", default-features = false, features = ["blocking", "rustls-tls", "multipart", "gzip", "json"] } rustyline = "13.0.0" +config = { version = "0.13.4", default-features = false, features = ["toml", "ini"] } diff --git a/src/cmd/auth.rs b/src/cmd/auth.rs index ed8e425..94dd1c4 100644 --- a/src/cmd/auth.rs +++ b/src/cmd/auth.rs @@ -1,4 +1,4 @@ -use crate::util; +use crate::{config::Config, util}; use anyhow::{anyhow, Result}; use clap::Args; use reqwest::Url; @@ -7,8 +7,8 @@ use reqwest::Url; pub struct Cli {} impl Cli { - pub fn run(self, server_url: &Option) -> Result<()> { - let auth_url = auth_url(server_url)?; + pub fn run(self, config: &Config) -> Result<()> { + let auth_url = auth_url(config.server_url())?; let server_hostname = auth_url.host().ok_or(anyhow!("invalid server URL"))?; println!("Open the following URL in a web browser to authenticate this asciinema CLI with your {server_hostname} user account:\n"); @@ -19,8 +19,8 @@ impl Cli { } } -fn auth_url(server_url: &Option) -> Result { - let mut url = util::get_server_url(server_url.as_ref())?; +fn auth_url(server_url: Option<&String>) -> Result { + let mut url = util::get_server_url(server_url)?; url.set_path(&format!("connect/{}", util::get_install_id()?)); Ok(url) diff --git a/src/cmd/upload.rs b/src/cmd/upload.rs index 8e0f3f0..261c6a8 100644 --- a/src/cmd/upload.rs +++ b/src/cmd/upload.rs @@ -1,3 +1,4 @@ +use crate::config::Config; use crate::util; use anyhow::{anyhow, Result}; use clap::Args; @@ -21,12 +22,12 @@ struct UploadResponse { } impl Cli { - pub fn run(self, server_url: &Option) -> Result<()> { + pub fn run(self, config: &Config) -> Result<()> { let client = Client::new(); let form = Form::new().file("asciicast", self.filename)?; let response = client - .post(api_url(server_url)?) + .post(api_url(config.server_url())?) .multipart(form) .basic_auth(get_username(), Some(util::get_install_id()?)) .header(header::USER_AGENT, build_user_agent()) @@ -56,8 +57,8 @@ impl Cli { } } -fn api_url(server_url: &Option) -> Result { - let mut url = util::get_server_url(server_url.as_ref())?; +fn api_url(server_url: Option<&String>) -> Result { + let mut url = util::get_server_url(server_url)?; url.set_path("api/asciicasts"); Ok(url) diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..10c6b02 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,92 @@ +use anyhow::{anyhow, Result}; +use serde::Deserialize; +use std::env; +use std::path::{Path, PathBuf}; + +#[derive(Debug, Deserialize)] +#[allow(unused)] +pub struct Config { + server: Server, + api: Api, + cmd: Cmd, +} + +#[derive(Debug, Deserialize)] +#[allow(unused)] +pub struct Server { + url: Option, +} + +#[derive(Debug, Deserialize)] +#[allow(unused)] +pub struct Api { + url: Option, +} + +#[derive(Debug, Deserialize)] +#[allow(unused)] +pub struct Cmd { + rec: Rec, + play: Play, +} + +#[derive(Debug, Deserialize, Default)] +#[allow(unused)] +pub struct Rec { + pub input: bool, + pub command: Option, + pub env: String, + pub idle_time_limit: Option, + pub prefix_key: Option, + pub pause_key: String, + pub add_marker_key: Option, +} + +#[derive(Debug, Deserialize)] +#[allow(unused)] +pub struct Play { + pub speed: f64, + pub idle_time_limit: Option, + pub pause_key: String, + pub step_key: String, + pub next_marker_key: String, +} + +impl Config { + pub fn new(server_url: Option) -> Result { + let user_config_file = home()?.join("config.toml"); + + let mut config = config::Config::builder() + .set_default("server.url", None::>)? + .set_default("api.url", None::>)? + .set_default("cmd.rec.input", false)? + .set_default("cmd.rec.env", "SHELL,TERM")? + .set_default("cmd.rec.pause_key", "C-\\")? + .set_default("cmd.play.speed", 1.0)? + .set_default("cmd.play.pause_key", " ")? + .set_default("cmd.play.step_key", ".")? + .set_default("cmd.play.next_marker_key", "]")? + .add_source( + config::File::with_name(&user_config_file.to_string_lossy()).required(false), + ) + .add_source(config::Environment::with_prefix("asciinema").separator("_")); + + if let Some(url) = server_url { + config = config.set_override("server.url", Some(url))?; + } + + Ok(config.build()?.try_deserialize()?) + } + + pub fn server_url(&self) -> Option<&String> { + self.server.url.as_ref().or(self.api.url.as_ref()) + } +} + +pub fn home() -> Result { + env::var("ASCIINEMA_CONFIG_HOME") + .map(PathBuf::from) + .or(env::var("XDG_CONFIG_HOME").map(|home| Path::new(&home).join("asciinema"))) + .or(env::var("HOME").map(|home| Path::new(&home).join(".config").join("asciinema"))) + .map_err(|_| anyhow!("need $HOME or $XDG_CONFIG_HOME or $ASCIINEMA_CONFIG_HOME")) +} diff --git a/src/main.rs b/src/main.rs index a022d69..6b0b466 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,11 @@ mod cmd; +mod config; mod format; mod locale; mod pty; mod recorder; mod util; +use crate::config::Config; use anyhow::Result; use clap::{Parser, Subcommand}; @@ -40,13 +42,13 @@ enum Commands { fn main() -> Result<()> { let cli = Cli::parse(); - let server_url = &cli.server_url; + let config = Config::new(cli.server_url.clone())?; match cli.command { Commands::Record(record) => record.run(), Commands::Play(play) => play.run(), Commands::Cat(cat) => cat.run(), - Commands::Upload(upload) => upload.run(server_url), - Commands::Auth(auth) => auth.run(server_url), + Commands::Upload(upload) => upload.run(&config), + Commands::Auth(auth) => auth.run(&config), } } diff --git a/src/util.rs b/src/util.rs index fd22039..b0e90fb 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,3 +1,4 @@ +use crate::config; use anyhow::{anyhow, bail, Result}; use reqwest::Url; use std::{env, fs, io::ErrorKind, path::Path, path::PathBuf}; @@ -29,7 +30,7 @@ fn create_install_id() -> Result { } fn read_legacy_install_id() -> Result> { - let path = config_home()?.join(INSTALL_ID_FILENAME); + let path = config::home()?.join(INSTALL_ID_FILENAME); match fs::read_to_string(path) { Ok(s) => Ok(Some(s.trim().to_string())), @@ -44,17 +45,14 @@ fn read_legacy_install_id() -> Result> { } } -pub fn get_server_url(server_url: Option) -> Result { - let mut url_opt = server_url - .map(|s| s.to_string()) - .or(env::var("ASCIINEMA_SERVER_URL").ok()) - .or(env::var("ASCIINEMA_API_URL").ok()); +pub fn get_server_url(server_url: Option<&String>) -> Result { + let mut url = server_url.cloned(); - if url_opt.is_none() { - url_opt = read_state_file(DEFAULT_SERVER_URL_FILENAME)?; + if url.is_none() { + url = read_state_file(DEFAULT_SERVER_URL_FILENAME)?; } - match url_opt { + match url { Some(url) => Ok(Url::parse(&url)?), None => { @@ -106,14 +104,6 @@ fn write_state_file(filename: &str, contents: &str) -> Result<()> { Ok(()) } -fn config_home() -> Result { - env::var("ASCIINEMA_CONFIG_HOME") - .map(PathBuf::from) - .or(env::var("XDG_CONFIG_HOME").map(|home| Path::new(&home).join("asciinema"))) - .or(env::var("HOME").map(|home| Path::new(&home).join(".config").join("asciinema"))) - .map_err(|_| anyhow!("need $HOME or $XDG_CONFIG_HOME or $ASCIINEMA_CONFIG_HOME")) -} - fn state_home() -> Result { env::var("ASCIINEMA_STATE_HOME") .map(PathBuf::from)