Files
asciinema/src/config.rs

293 lines
7.4 KiB
Rust
Raw Normal View History

use std::env;
use std::fs;
2023-12-28 16:30:09 +01:00
use std::io::ErrorKind;
use std::path::{Path, PathBuf};
2025-03-12 13:15:02 +01:00
use anyhow::{anyhow, bail, Result};
2025-05-07 11:30:17 +02:00
use config::{self, Environment, File};
2025-03-12 13:15:02 +01:00
use reqwest::Url;
use serde::Deserialize;
2023-12-28 16:30:09 +01:00
use uuid::Uuid;
use crate::status;
2023-12-28 16:30:09 +01:00
const DEFAULT_SERVER_URL: &str = "https://asciinema.org";
const INSTALL_ID_FILENAME: &str = "install-id";
pub type Key = Option<Vec<u8>>;
#[derive(Debug, Deserialize)]
#[allow(unused)]
pub struct Config {
server: Server,
2025-05-07 16:42:04 +02:00
pub recording: Recording,
pub playback: Playback,
pub notifications: Notifications,
}
#[derive(Debug, Deserialize)]
#[allow(unused)]
pub struct Server {
url: Option<String>,
}
2025-03-12 13:15:02 +01:00
#[derive(Debug, Clone, Deserialize, Default)]
#[allow(unused)]
2025-05-07 16:42:04 +02:00
pub struct Recording {
pub command: Option<String>,
2025-05-03 17:28:10 +02:00
pub rec_input: bool,
pub rec_env: Option<String>,
pub idle_time_limit: Option<f64>,
pub prefix_key: Option<String>,
pub pause_key: Option<String>,
pub add_marker_key: Option<String>,
}
2025-03-12 13:15:02 +01:00
#[derive(Debug, Clone, Deserialize)]
#[allow(unused)]
2025-05-07 16:42:04 +02:00
pub struct Playback {
pub speed: Option<f64>,
pub idle_time_limit: Option<f64>,
pub pause_key: Option<String>,
pub step_key: Option<String>,
pub next_marker_key: Option<String>,
}
#[derive(Debug, Deserialize)]
#[allow(unused)]
pub struct Notifications {
pub enabled: bool,
pub command: Option<String>,
}
impl Config {
pub fn new(server_url: Option<String>) -> Result<Self> {
let mut config = config::Config::builder()
.set_default("server.url", None::<Option<String>>)?
2025-05-07 16:42:04 +02:00
.set_default("playback.speed", None::<Option<f64>>)?
.set_default("recording.rec_input", false)?
.set_default("notifications.enabled", true)?
2025-05-07 11:30:17 +02:00
.add_source(File::with_name("/etc/asciinema/config.toml").required(false))
.add_source(File::with_name(&user_defaults_path()?.to_string_lossy()).required(false))
.add_source(File::with_name(&user_config_path()?.to_string_lossy()).required(false))
.add_source(Environment::with_prefix("asciinema").separator("_"));
if let Some(url) = server_url {
config = config.set_override("server.url", Some(url))?;
}
if let (Err(_), Ok(url)) = (
env::var("ASCIINEMA_SERVER_URL"),
env::var("ASCIINEMA_API_URL"),
) {
env::set_var("ASCIINEMA_SERVER_URL", url);
}
Ok(config.build()?.try_deserialize()?)
}
2023-12-28 16:30:09 +01:00
pub fn get_server_url(&self) -> Result<Url> {
match self.server.url.as_ref() {
Some(url) => Ok(parse_server_url(url)?),
2023-12-28 16:30:09 +01:00
None => {
let url = parse_server_url(&ask_for_server_url()?)?;
2023-12-28 16:30:09 +01:00
save_default_server_url(url.as_ref())?;
Ok(url)
}
}
}
pub fn get_install_id(&self) -> Result<String> {
let path = install_id_path()?;
if let Some(id) = read_install_id(&path)? {
Ok(id)
} else {
let id = create_install_id();
save_install_id(&path, &id)?;
Ok(id)
}
}
2025-03-12 13:15:02 +01:00
}
2025-05-07 16:42:04 +02:00
impl Recording {
2025-03-12 13:15:02 +01:00
pub fn prefix_key(&self) -> Result<Option<Key>> {
self.prefix_key.as_ref().map(parse_key).transpose()
}
2024-02-07 14:05:00 +01:00
2025-03-12 13:15:02 +01:00
pub fn pause_key(&self) -> Result<Option<Key>> {
self.pause_key.as_ref().map(parse_key).transpose()
2024-02-07 14:05:00 +01:00
}
2025-03-12 13:15:02 +01:00
pub fn add_marker_key(&self) -> Result<Option<Key>> {
self.add_marker_key.as_ref().map(parse_key).transpose()
2024-02-07 14:05:00 +01:00
}
2025-03-12 13:15:02 +01:00
}
2024-02-07 14:05:00 +01:00
2025-05-07 16:42:04 +02:00
impl Playback {
2025-03-12 13:15:02 +01:00
pub fn pause_key(&self) -> Result<Option<Key>> {
self.pause_key.as_ref().map(parse_key).transpose()
2024-02-07 14:05:00 +01:00
}
2025-03-12 13:15:02 +01:00
pub fn step_key(&self) -> Result<Option<Key>> {
self.step_key.as_ref().map(parse_key).transpose()
2024-02-07 14:05:00 +01:00
}
2025-02-22 12:34:48 +01:00
2025-03-12 13:15:02 +01:00
pub fn next_marker_key(&self) -> Result<Option<Key>> {
self.next_marker_key.as_ref().map(parse_key).transpose()
2025-02-22 12:34:48 +01:00
}
}
2023-12-28 16:30:09 +01:00
fn ask_for_server_url() -> Result<String> {
println!("No asciinema server configured for this CLI.");
2025-05-07 11:30:17 +02:00
let url = rustyline::DefaultEditor::new()?.readline_with_initial(
2023-12-28 16:30:09 +01:00
"Enter the server URL to use by default: ",
(DEFAULT_SERVER_URL, ""),
)?;
2025-05-07 11:30:17 +02:00
2023-12-28 16:30:09 +01:00
println!();
Ok(url)
}
fn save_default_server_url(url: &str) -> Result<()> {
let path = user_defaults_path()?;
if let Some(dir) = path.parent() {
fs::create_dir_all(dir)?;
}
fs::write(path, format!("[server]\nurl = \"{url}\"\n"))?;
Ok(())
}
fn parse_server_url(s: &str) -> Result<Url> {
let url = Url::parse(s)?;
if url.host().is_none() {
bail!("server URL is missing a host");
}
Ok(url)
}
2023-12-28 16:30:09 +01:00
fn read_install_id(path: &PathBuf) -> Result<Option<String>> {
match fs::read_to_string(path) {
Ok(s) => Ok(Some(s.trim().to_string())),
Err(e) => {
if e.kind() == ErrorKind::NotFound {
Ok(None)
} else {
bail!(e)
}
}
}
}
fn create_install_id() -> String {
Uuid::new_v4().to_string()
}
fn save_install_id(path: &PathBuf, id: &str) -> Result<()> {
if let Some(dir) = path.parent() {
fs::create_dir_all(dir)?;
}
2023-12-28 21:14:40 +01:00
fs::write(path, id)?;
2023-12-28 16:30:09 +01:00
Ok(())
}
pub fn user_config_path() -> Result<PathBuf> {
Ok(home()?.join("config.toml"))
}
fn legacy_user_config_path() -> Result<PathBuf> {
Ok(home()?.join("config"))
}
fn user_defaults_path() -> Result<PathBuf> {
Ok(home()?.join("defaults.toml"))
}
2023-12-28 16:30:09 +01:00
fn install_id_path() -> Result<PathBuf> {
Ok(home()?.join(INSTALL_ID_FILENAME))
}
fn home() -> Result<PathBuf> {
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 parse_key<S: AsRef<str>>(key: S) -> Result<Key> {
let key = key.as_ref();
let chars: Vec<char> = key.chars().collect();
match chars.len() {
0 => return Ok(None),
1 => {
let mut buf = [0; 4];
let str = chars[0].encode_utf8(&mut buf);
return Ok(Some(str.as_bytes().into()));
}
2 => {
if chars[0] == '^' && chars[1].is_ascii_alphabetic() {
let key = vec![chars[1].to_ascii_uppercase() as u8 - 0x40];
return Ok(Some(key));
}
}
3 => {
2025-06-23 14:24:08 +02:00
if chars[0].eq_ignore_ascii_case(&'C')
&& ['+', '-'].contains(&chars[1])
&& chars[2].is_ascii_alphabetic()
{
let key = vec![chars[2].to_ascii_uppercase() as u8 - 0x40];
return Ok(Some(key));
}
}
_ => (),
}
Err(anyhow!("invalid key definition '{key}'"))
}
pub fn check_legacy_config_file() {
let Ok(legacy_path) = legacy_user_config_path() else {
return;
};
let Ok(new_path) = user_config_path() else {
return;
};
if legacy_path.exists() && !new_path.exists() {
status::warning!(
"Your config file at {} uses the location and format from asciinema 2.x.",
legacy_path.to_string_lossy()
);
status::warning!(
"For asciinema 3.x (this version) create a new config file at {}.",
new_path.to_string_lossy()
);
status::warning!("Read the documentation (CLI -> Configuration) for details.\n");
}
}