feat(cli): add cli options to override configuration and cable directories

`tv --config-dir=/my/dir --cable-dir=/my/other/dir`
This commit is contained in:
alexandre pasmantier 2025-06-15 17:57:02 +02:00 committed by Alex Pasmantier
parent 666254498e
commit bc8d636005
9 changed files with 105 additions and 28 deletions

View File

@ -435,7 +435,8 @@ pub fn draw(c: &mut Criterion) {
b.to_async(&rt).iter_batched(
// FIXME: this is kind of hacky
|| {
let config = Config::new(&ConfigEnv::init().unwrap()).unwrap();
let config =
Config::new(&ConfigEnv::init().unwrap(), None).unwrap();
let backend = TestBackend::new(width, height);
let terminal = Terminal::new(backend).unwrap();
let (tx, _) = tokio::sync::mpsc::unbounded_channel();

View File

@ -118,7 +118,7 @@ where
/// Load cable channels from the config directory.
///
/// Cable is loaded by compiling all files located in the `cable/` subdirectory
/// of the user's configuration directory (+ defaults)
/// of the user's configuration directory, unless a custom directory is provided.
///
/// # Example:
/// ```ignore
@ -129,9 +129,15 @@ where
/// ├── channel_2.toml
/// └── ...
/// ```
pub fn load_cable() -> Option<Cable> {
let cable_dir = get_config_dir().join(CABLE_DIR_NAME);
debug!("Cable directory: {}", cable_dir.to_string_lossy());
pub fn load_cable<P>(cable_dir: Option<P>) -> Option<Cable>
where
P: AsRef<Path>,
{
let cable_dir = match cable_dir {
Some(dir) => PathBuf::from(dir.as_ref()),
None => get_config_dir().join(CABLE_DIR_NAME),
};
debug!("Using cable directory: {}", cable_dir.to_string_lossy());
let cable_files = get_cable_files(&cable_dir);
debug!("Found cable channel files: {:?}", cable_files);

View File

@ -146,6 +146,14 @@ pub struct Cli {
)]
pub ui_scale: u16,
/// Provide a custom configuration file to use.
#[arg(long, value_name = "PATH", verbatim_doc_comment)]
pub config_file: Option<String>,
/// Provide a custom cable directory to use.
#[arg(long, value_name = "PATH", verbatim_doc_comment)]
pub cable_dir: Option<String>,
#[command(subcommand)]
pub command: Option<Command>,
}

View File

@ -1,5 +1,5 @@
use rustc_hash::FxHashMap;
use std::path::Path;
use std::path::{Path, PathBuf};
use anyhow::{Result, anyhow};
use tracing::debug;
@ -11,6 +11,7 @@ use crate::{
},
cli::args::{Cli, Command},
config::{KeyBindings, get_config_dir, get_data_dir},
utils::paths::expand_tilde,
};
pub mod args;
@ -34,6 +35,8 @@ pub struct PostProcessedCli {
pub no_remote: bool,
pub no_help: bool,
pub ui_scale: u16,
pub config_file: Option<PathBuf>,
pub cable_dir: Option<PathBuf>,
}
impl Default for PostProcessedCli {
@ -55,6 +58,8 @@ impl Default for PostProcessedCli {
no_remote: false,
no_help: false,
ui_scale: 100,
config_file: None,
cable_dir: None,
}
}
}
@ -115,6 +120,8 @@ pub fn post_process(cli: Cli) -> PostProcessedCli {
no_remote: cli.no_remote,
no_help: cli.no_help,
ui_scale: cli.ui_scale,
config_file: cli.config_file.map(expand_tilde),
cable_dir: cli.cable_dir.map(expand_tilde),
}
}
@ -149,8 +156,9 @@ fn parse_keybindings_literal(
toml::from_str(&toml_definition).map_err(|e| anyhow!(e))
}
pub fn list_channels() {
let channels = cable::load_cable().expect("Failed to load cable channels");
pub fn list_channels(cable_dir: Option<&Path>) {
let channels = cable::load_cable::<&Path>(cable_dir)
.expect("Failed to load cable channels");
for c in channels.keys() {
println!("\t{c}");
}

View File

@ -122,16 +122,30 @@ const USER_CONFIG_ERROR_MSG: &str = "
impl Config {
#[allow(clippy::missing_panics_doc, clippy::missing_errors_doc)]
pub fn new(config_env: &ConfigEnv) -> Result<Self> {
pub fn new(
config_env: &ConfigEnv,
custom_config_file: Option<&Path>,
) -> Result<Self> {
// Load the default_config values as base defaults
let default_config: Config = default_config_from_file()?;
// if a config file exists, load it and merge it with the default configuration
if config_env.config_dir.join(CONFIG_FILE_NAME).is_file() {
debug!("Found config file at {:?}", config_env.config_dir);
if config_env.config_dir.join(CONFIG_FILE_NAME).is_file()
|| custom_config_file.is_some()
{
let config_file = if let Some(path) = custom_config_file {
debug!("Using custom configuration file at: {:?}", path);
path.to_path_buf()
} else {
let config_file = config_env.config_dir.join(CONFIG_FILE_NAME);
debug!(
"Using default configuration file at: {:?}",
config_file
);
config_file
};
let user_cfg: Config =
Self::load_user_config(&config_env.config_dir)?;
let user_cfg: Config = Self::load_user_config(&config_file)?;
// merge the user configuration with the default configuration
let final_cfg = Self::merge_with_default(default_config, user_cfg);
@ -156,12 +170,11 @@ impl Config {
}
}
fn load_user_config(config_dir: &Path) -> Result<Self> {
let path = config_dir.join(CONFIG_FILE_NAME);
let contents = std::fs::read_to_string(&path)?;
fn load_user_config(config_file: &Path) -> Result<Self> {
let contents = std::fs::read_to_string(config_file)?;
let user_cfg: Config = toml::from_str(&contents).context(format!(
"Error parsing configuration file: {}\n{}",
path.display(),
config_file.display(),
USER_CONFIG_ERROR_MSG,
))?;
Ok(user_cfg)
@ -320,7 +333,7 @@ mod tests {
let mut file = File::create(&config_file).unwrap();
file.write_all(DEFAULT_CONFIG.as_bytes()).unwrap();
let config = Config::load_user_config(config_dir).unwrap();
let config = Config::load_user_config(&config_file).unwrap();
assert_eq!(config.application.data_dir, get_data_dir());
assert_eq!(config.application.config_dir, get_config_dir());
assert_eq!(config, toml::from_str(DEFAULT_CONFIG).unwrap());
@ -338,7 +351,7 @@ mod tests {
_data_dir: get_data_dir(),
config_dir: config_dir.to_path_buf(),
};
let config = Config::new(&config_env).unwrap();
let config = Config::new(&config_env, None).unwrap();
let mut default_config: Config =
toml::from_str(DEFAULT_CONFIG).unwrap();
default_config.shell_integration.merge_triggers();
@ -392,7 +405,7 @@ mod tests {
_data_dir: get_data_dir(),
config_dir: config_dir.to_path_buf(),
};
let config = Config::new(&config_env).unwrap();
let config = Config::new(&config_env, None).unwrap();
let mut default_config: Config =
toml::from_str(DEFAULT_CONFIG).unwrap();
@ -452,7 +465,7 @@ mod tests {
config_dir: config_dir.to_path_buf(),
};
let config = Config::new(&config_env).unwrap();
let config = Config::new(&config_env, None).unwrap();
assert_eq!(
config.shell_integration.commands.iter().collect::<Vec<_>>(),
@ -479,7 +492,7 @@ mod tests {
config_dir: config_dir.to_path_buf(),
};
let config = Config::new(&config_env).unwrap();
let config = Config::new(&config_env, None).unwrap();
assert_eq!(
config.shell_integration.keybindings,

View File

@ -44,16 +44,17 @@ async fn main() -> Result<()> {
// load the configuration file
debug!("Loading configuration...");
let mut config = Config::new(&ConfigEnv::init()?)?;
let mut config =
Config::new(&ConfigEnv::init()?, args.config_file.as_deref())?;
// handle subcommands
debug!("Handling subcommands...");
if let Some(subcommand) = &args.command {
handle_subcommand(subcommand, &config)?;
handle_subcommand(subcommand, &config, &args)?;
}
debug!("Loading cable channels...");
let cable = load_cable().unwrap_or_else(|| exit(1));
let cable = load_cable(args.cable_dir.as_ref()).unwrap_or_else(|| exit(1));
// optionally change the working directory
args.working_directory.as_ref().map(set_current_dir);
@ -139,10 +140,14 @@ pub fn set_current_dir(path: &String) -> Result<()> {
Ok(())
}
pub fn handle_subcommand(command: &Command, config: &Config) -> Result<()> {
pub fn handle_subcommand(
command: &Command,
config: &Config,
args: &PostProcessedCli,
) -> Result<()> {
match command {
Command::ListChannels => {
list_channels();
list_channels(args.cable_dir.as_deref());
exit(0);
}
Command::InitShell { shell } => {

View File

@ -766,7 +766,7 @@ mod test {
use crate::{
action::Action,
cable::Cable,
config::{Binding, KeyBindings},
config::Binding,
event::Key,
television::{MatchingMode, Television},
};

View File

@ -6,6 +6,7 @@ pub mod hashmaps;
pub mod indices;
pub mod input;
pub mod metadata;
pub mod paths;
pub mod rocell;
pub mod shell;
pub mod stdin;

35
television/utils/paths.rs Normal file
View File

@ -0,0 +1,35 @@
use directories::UserDirs;
use std::path::{Path, PathBuf};
pub fn expand_tilde<P>(path: P) -> PathBuf
where
P: AsRef<Path>,
{
let path = path.as_ref();
if path.starts_with("~") {
let home = UserDirs::new()
.map(|dirs| dirs.home_dir().to_path_buf())
.unwrap_or_else(|| PathBuf::from("/"));
home.join(path.strip_prefix("~").unwrap())
} else {
path.to_path_buf()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_expand_tilde() {
assert_eq!(
expand_tilde("~/test").to_str().unwrap(),
&format!("{}/test", UserDirs::new().unwrap().home_dir().display())
);
assert_eq!(expand_tilde("test").to_str().unwrap(), "test");
assert_eq!(
expand_tilde("/absolute/path").to_str().unwrap(),
"/absolute/path"
);
}
}