zellij/zellij-utils/src/setup.rs
a-kenji bd8c834d7c
fix(options): handling and overwriting cli opts (#859)
* fix(options): handling ond verwriting cli opts

* previously it was only possible to turn off certain features with a
  command line option, now it is possible to also overwrite this
  behavior in a sane way, for that some breaking changes happened:

  following options got renamed and inverted:
  ```
  disable_mouse_mode -> mouse_mode
  no_pane_frames -> pane_frames
  ```

  following cli options got added:
  ```
  mouse-mode [bool]
  pane-frames [bool]
  simplified-ui [bool]
  ```

  the following cli flag got removed:
  ```
  simplified-ui
  ```

  They can be specified in the following way:
  ```
  zellij options --mouse-mode true
  ```
  in order to enable the mouse mode, even if it is turned off in the
  config file:
  ```
  mouse_mode: false
  ```

  The order is now as follows:
  1. corresponding flag (`disable-mouse-mode`)
  2. corresponding option (`mouse-mode`)
  3. corresponding config option (`mouse_mode`)

* add: options and flags for the same value conflict

* example:
  ```
  zellij options --mouse-mode true --disable-mouse-mode`
  ```
  ```
  $ error: The argument '--mouse-mode <mouse-mode>' cannot be used with '--disable-mouse-mode'
  ```
2021-11-11 17:13:34 +01:00

362 lines
12 KiB
Rust

use crate::{
cli::{CliArgs, Command},
consts::{
FEATURES, SYSTEM_DEFAULT_CONFIG_DIR, SYSTEM_DEFAULT_DATA_DIR_PREFIX, VERSION,
ZELLIJ_PROJ_DIR,
},
input::{
config::{Config, ConfigError},
layout::LayoutFromYaml,
options::Options,
},
};
use directories_next::BaseDirs;
use serde::{Deserialize, Serialize};
use std::{convert::TryFrom, io::Write, path::Path, path::PathBuf, process};
use structopt::StructOpt;
const CONFIG_LOCATION: &str = ".config/zellij";
const CONFIG_NAME: &str = "config.yaml";
static ARROW_SEPARATOR: &str = "";
#[cfg(not(test))]
/// Goes through a predefined list and checks for an already
/// existing config directory, returns the first match
pub fn find_default_config_dir() -> Option<PathBuf> {
default_config_dirs()
.into_iter()
.filter(|p| p.is_some())
.find(|p| p.clone().unwrap().exists())
.flatten()
}
#[cfg(test)]
pub fn find_default_config_dir() -> Option<PathBuf> {
None
}
/// Order in which config directories are checked
fn default_config_dirs() -> Vec<Option<PathBuf>> {
vec![
home_config_dir(),
Some(xdg_config_dir()),
Some(Path::new(SYSTEM_DEFAULT_CONFIG_DIR).to_path_buf()),
]
}
/// Looks for an existing dir, uses that, else returns a
/// dir matching the config spec.
pub fn get_default_data_dir() -> PathBuf {
vec![
xdg_data_dir(),
Path::new(SYSTEM_DEFAULT_DATA_DIR_PREFIX).join("share/zellij"),
]
.into_iter()
.find(|p| p.exists())
.unwrap_or_else(xdg_data_dir)
}
pub fn xdg_config_dir() -> PathBuf {
ZELLIJ_PROJ_DIR.config_dir().to_owned()
}
pub fn xdg_data_dir() -> PathBuf {
ZELLIJ_PROJ_DIR.data_dir().to_owned()
}
pub fn home_config_dir() -> Option<PathBuf> {
if let Some(user_dirs) = BaseDirs::new() {
let config_dir = user_dirs.home_dir().join(CONFIG_LOCATION);
Some(config_dir)
} else {
None
}
}
pub fn get_layout_dir(config_dir: Option<PathBuf>) -> Option<PathBuf> {
config_dir.map(|dir| dir.join("layouts"))
}
pub fn dump_asset(asset: &[u8]) -> std::io::Result<()> {
std::io::stdout().write_all(asset)?;
Ok(())
}
pub const DEFAULT_CONFIG: &[u8] = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/",
"assets/config/default.yaml"
));
pub const DEFAULT_LAYOUT: &[u8] = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/",
"assets/layouts/default.yaml"
));
pub const STRIDER_LAYOUT: &[u8] = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/",
"assets/layouts/strider.yaml"
));
pub const NO_STATUS_LAYOUT: &[u8] = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/",
"assets/layouts/disable-status-bar.yaml"
));
pub fn dump_default_config() -> std::io::Result<()> {
dump_asset(DEFAULT_CONFIG)
}
pub fn dump_specified_layout(layout: &str) -> std::io::Result<()> {
match layout {
"strider" => dump_asset(STRIDER_LAYOUT),
"default" => dump_asset(DEFAULT_LAYOUT),
"disable-status" => dump_asset(NO_STATUS_LAYOUT),
not_found => Err(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Layout: {} not found", not_found),
)),
}
}
#[derive(Debug, Default, Clone, StructOpt, Serialize, Deserialize)]
pub struct Setup {
/// Dump the default configuration file to stdout
#[structopt(long)]
pub dump_config: bool,
/// Disables loading of configuration file at default location,
/// loads the defaults that zellij ships with
#[structopt(long)]
pub clean: bool,
/// Checks the configuration of zellij and displays
/// currently used directories
#[structopt(long)]
pub check: bool,
/// Dump the specified layout file to stdout
#[structopt(long)]
pub dump_layout: Option<String>,
/// Generates completion for the specified shell
#[structopt(long)]
pub generate_completion: Option<String>,
}
impl Setup {
/// Entrypoint from main
/// Merges options from the config file and the command line options
/// into `[Options]`, the command line options superceding the config
/// file options:
/// 1. command line options (`zellij options`)
/// 2. config options (`config.yaml`)
pub fn from_options(
opts: &CliArgs,
) -> Result<(Config, Option<LayoutFromYaml>, Options), ConfigError> {
let clean = match &opts.command {
Some(Command::Setup(ref setup)) => setup.clean,
_ => false,
};
// setup functions that don't require deserialisation of the config
if let Some(Command::Setup(ref setup)) = &opts.command {
setup.from_cli().map_or_else(
|e| {
eprintln!("{:?}", e);
process::exit(1);
},
|_| {},
);
};
let config = if !clean {
match Config::try_from(opts) {
Ok(config) => config,
Err(e) => {
return Err(e);
}
}
} else {
Config::default()
};
let config_options = Options::from_cli(&config.options, opts.command.clone());
let layout_dir = config_options
.layout_dir
.clone()
.or_else(|| get_layout_dir(opts.config_dir.clone().or_else(find_default_config_dir)));
let layout_result = LayoutFromYaml::from_path_or_default(
opts.layout.as_ref(),
opts.layout_path.as_ref(),
layout_dir,
);
let layout = match layout_result {
None => None,
Some(Ok(layout)) => Some(layout),
Some(Err(e)) => {
return Err(e);
}
};
if let Some(Command::Setup(ref setup)) = &opts.command {
setup
.from_cli_with_options(opts, &config_options)
.map_or_else(
|e| {
eprintln!("{:?}", e);
process::exit(1);
},
|_| {},
);
};
Ok((config, layout, config_options))
}
/// General setup helpers
pub fn from_cli(&self) -> std::io::Result<()> {
if self.clean {
return Ok(());
}
if self.dump_config {
dump_default_config()?;
std::process::exit(0);
}
if let Some(shell) = &self.generate_completion {
Self::generate_completion(shell.into());
std::process::exit(0);
}
if let Some(layout) = &self.dump_layout {
dump_specified_layout(layout)?;
std::process::exit(0);
}
Ok(())
}
/// Checks the merged configuration
pub fn from_cli_with_options(
&self,
opts: &CliArgs,
config_options: &Options,
) -> std::io::Result<()> {
if self.check {
Setup::check_defaults_config(opts, config_options)?;
std::process::exit(0);
}
Ok(())
}
pub fn check_defaults_config(opts: &CliArgs, config_options: &Options) -> std::io::Result<()> {
let data_dir = opts.data_dir.clone().unwrap_or_else(get_default_data_dir);
let config_dir = opts.config_dir.clone().or_else(find_default_config_dir);
let plugin_dir = data_dir.join("plugins");
let layout_dir = config_options
.layout_dir
.clone()
.or_else(|| get_layout_dir(config_dir.clone()));
let system_data_dir = PathBuf::from(SYSTEM_DEFAULT_DATA_DIR_PREFIX).join("share/zellij");
let config_file = opts
.config
.clone()
.or_else(|| config_dir.clone().map(|p| p.join(CONFIG_NAME)));
// according to
// https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
let hyperlink_start = "\u{1b}]8;;";
let hyperlink_mid = "\u{1b}\\";
let hyperlink_end = "\u{1b}]8;;\u{1b}\\";
let mut message = String::new();
message.push_str(&format!("[Version]: {:?}\n", VERSION));
if let Some(config_dir) = config_dir {
message.push_str(&format!("[CONFIG DIR]: {:?}\n", config_dir));
} else {
message.push_str("[CONFIG DIR]: Not Found\n");
let mut default_config_dirs = default_config_dirs()
.iter()
.filter_map(|p| p.clone())
.collect::<Vec<PathBuf>>();
default_config_dirs.dedup();
message.push_str(
" On your system zellij looks in the following config directories by default:\n",
);
for dir in default_config_dirs {
message.push_str(&format!(" {:?}\n", dir));
}
}
if let Some(config_file) = config_file {
message.push_str(&format!("[CONFIG FILE]: {:?}\n", config_file));
match Config::new(&config_file) {
Ok(_) => message.push_str("[CONFIG FILE]: Well defined.\n"),
Err(e) => message.push_str(&format!("[CONFIG ERROR]: {}\n", e)),
}
} else {
message.push_str("[CONFIG FILE]: Not Found\n");
message.push_str(&format!(
" By default zellij looks for a file called [{}] in the configuration directory\n",
CONFIG_NAME
));
}
message.push_str(&format!("[DATA DIR]: {:?}\n", data_dir));
message.push_str(&format!("[PLUGIN DIR]: {:?}\n", plugin_dir));
if let Some(layout_dir) = layout_dir {
message.push_str(&format!("[LAYOUT DIR]: {:?}\n", layout_dir));
} else {
message.push_str("[CONFIG FILE]: Not Found\n");
}
message.push_str(&format!("[SYSTEM DATA DIR]: {:?}\n", system_data_dir));
message.push_str(&format!("[ARROW SEPARATOR]: {}\n", ARROW_SEPARATOR));
message.push_str(" Is the [ARROW_SEPARATOR] displayed correctly?\n");
message.push_str(" If not you may want to either start zellij with a compatible mode: 'zellij options --simplified-ui true'\n");
let mut hyperlink_compat = String::new();
hyperlink_compat.push_str(hyperlink_start);
hyperlink_compat.push_str("https://zellij.dev/documentation/compatibility.html#the-status-bar-fonts-dont-render-correctly");
hyperlink_compat.push_str(hyperlink_mid);
hyperlink_compat.push_str("https://zellij.dev/documentation/compatibility.html#the-status-bar-fonts-dont-render-correctly");
hyperlink_compat.push_str(hyperlink_end);
message.push_str(&format!(
" Or check the font that is in use:\n {}\n",
hyperlink_compat
));
message.push_str("[MOUSE INTERACTION]: \n");
message.push_str(" Can be temporarily disabled through pressing the [SHIFT] key.\n");
message.push_str(" If that doesn't fix any issues consider to disable the mouse handling of zellij: 'zellij options --disable-mouse-mode'\n");
message.push_str(&format!("[FEATURES]: {:?}\n", FEATURES));
let mut hyperlink = String::new();
hyperlink.push_str(hyperlink_start);
hyperlink.push_str("https://www.zellij.dev/documentation/");
hyperlink.push_str(hyperlink_mid);
hyperlink.push_str("zellij.dev/documentation");
hyperlink.push_str(hyperlink_end);
message.push_str(&format!("[DOCUMENTATION]: {}\n", hyperlink));
//printf '\e]8;;http://example.com\e\\This is a link\e]8;;\e\\\n'
std::io::stdout().write_all(message.as_bytes())?;
Ok(())
}
fn generate_completion(shell: String) {
let shell = match shell.as_ref() {
"bash" => structopt::clap::Shell::Bash,
"fish" => structopt::clap::Shell::Fish,
"zsh" => structopt::clap::Shell::Zsh,
"powerShell" => structopt::clap::Shell::PowerShell,
"elvish" => structopt::clap::Shell::Elvish,
other => {
eprintln!("Unsupported shell: {}", other);
std::process::exit(1);
}
};
let mut out = std::io::stdout();
CliArgs::clap().gen_completions_to("zellij", shell, &mut out);
}
}