mirror of
https://github.com/coastalwhite/lemurs
synced 2024-11-25 11:30:17 +00:00
Adjust method of variables parsing
This commit changes the method in which variables are supplied and parsed, requiring a separate file. For the configuration file, this is first parsed into a RoughConfig where variables might be inserted into any of the fields. Then, variables are interpreted into a PartialConfig.
This commit is contained in:
parent
b42e028d44
commit
a46b83ebe1
5 changed files with 417 additions and 146 deletions
|
@ -13,6 +13,7 @@ USAGE: lemurs [OPTIONS] [SUBCOMMAND]
|
|||
|
||||
OPTIONS:
|
||||
-c, --config <FILE> A file to replace the default configuration
|
||||
-v, --variables <FILE> A file to replace the set variables
|
||||
-h, --help Print help information
|
||||
--no-log
|
||||
--preview
|
||||
|
@ -34,6 +35,7 @@ pub struct Cli {
|
|||
pub no_log: bool,
|
||||
pub tty: Option<u8>,
|
||||
pub config: Option<PathBuf>,
|
||||
pub variables: Option<PathBuf>,
|
||||
pub command: Option<Commands>,
|
||||
}
|
||||
|
||||
|
@ -76,6 +78,7 @@ impl Cli {
|
|||
no_log: false,
|
||||
tty: None,
|
||||
config: None,
|
||||
variables: None,
|
||||
command: None,
|
||||
};
|
||||
|
||||
|
@ -104,6 +107,11 @@ impl Cli {
|
|||
let arg = PathBuf::from(arg);
|
||||
cli.config = Some(arg);
|
||||
}
|
||||
(_, "--variables") | (_, "-v") => {
|
||||
let (_, arg) = args.next().ok_or(CliError::MissingArgument("variables"))?;
|
||||
let arg = PathBuf::from(arg);
|
||||
cli.variables = Some(arg);
|
||||
}
|
||||
(_, arg) => return Err(CliError::InvalidArgument(arg.to_string())),
|
||||
}
|
||||
}
|
||||
|
|
449
src/config.rs
449
src/config.rs
|
@ -1,11 +1,9 @@
|
|||
use crossterm::event::KeyCode;
|
||||
use log::error;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{de::Error, Deserialize};
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Display;
|
||||
use std::fs::File;
|
||||
use std::io::{self, BufReader, Read};
|
||||
use std::io::Read;
|
||||
use std::path::Path;
|
||||
use std::process;
|
||||
use toml::Value;
|
||||
|
@ -156,8 +154,21 @@ macro_rules! merge_strategy {
|
|||
};
|
||||
}
|
||||
|
||||
macro_rules! var_replacement_strategy {
|
||||
($vars:ident, $value:ident, $field_type:ty) => {
|
||||
<$field_type as VariableInsertable>::insert($value, $vars)?
|
||||
};
|
||||
($vars:ident, $value:ident, $field_type:ty, $_:ty) => {
|
||||
$value.into_partial($vars)?
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! toml_config_struct {
|
||||
($struct_name:ident, $partial_struct_name:ident, $($field_name:ident => $field_type:ty $([$par_field_type:ty])?),+ $(,)?) => {
|
||||
($struct_name:ident, $partial_struct_name:ident, $rough_name:ident, $($field_name:ident => $field_type:ty $([$par_field_type:ty, $rough_field_type:ty])?),+ $(,)?) => {
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
struct $rough_name {
|
||||
$($field_name: Option<partial_struct_field!(PossibleVariable<$field_type>$(, $rough_field_type)?)>,)+
|
||||
}
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct $struct_name {
|
||||
$(pub $field_name: $field_type,)+
|
||||
|
@ -175,16 +186,29 @@ macro_rules! toml_config_struct {
|
|||
)+
|
||||
}
|
||||
}
|
||||
|
||||
impl $rough_name {
|
||||
pub fn into_partial(self, variables: &Variables) -> Result<$partial_struct_name, VariableInsertionError> {
|
||||
Ok($partial_struct_name {
|
||||
$(
|
||||
$field_name: match self.$field_name {
|
||||
Some(value) => Some(
|
||||
var_replacement_strategy!(variables, value, $field_type$(, $par_field_type)?)
|
||||
),
|
||||
None => None,
|
||||
},
|
||||
)+
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct VariablesConfig {
|
||||
#[serde(default)]
|
||||
pub variables: HashMap<String, toml::Value>,
|
||||
}
|
||||
#[serde(transparent)]
|
||||
pub struct Variables(toml::value::Table);
|
||||
|
||||
toml_config_struct! { Config, PartialConfig,
|
||||
toml_config_struct! { Config, PartialConfig, RoughConfig,
|
||||
tty => u8,
|
||||
|
||||
x11_display => String,
|
||||
|
@ -202,26 +226,26 @@ toml_config_struct! { Config, PartialConfig,
|
|||
|
||||
focus_behaviour => FocusBehaviour,
|
||||
|
||||
background => BackgroundConfig [PartialBackgroundConfig],
|
||||
background => BackgroundConfig [PartialBackgroundConfig, RoughBackgroundConfig],
|
||||
|
||||
power_controls => PowerControlConfig [PartialPowerControlConfig],
|
||||
environment_switcher => SwitcherConfig [PartialSwitcherConfig],
|
||||
username_field => UsernameFieldConfig [PartialUsernameFieldConfig],
|
||||
password_field => PasswordFieldConfig [PartialPasswordFieldConfig],
|
||||
power_controls => PowerControlConfig [PartialPowerControlConfig, RoughPowerControlConfig],
|
||||
environment_switcher => SwitcherConfig [PartialSwitcherConfig, RoughSwitcherConfig],
|
||||
username_field => UsernameFieldConfig [PartialUsernameFieldConfig, RoughUsernameFieldConfig],
|
||||
password_field => PasswordFieldConfig [PartialPasswordFieldConfig, RoughPasswordFieldConfig],
|
||||
}
|
||||
|
||||
toml_config_struct! { BackgroundStyleConfig, PartialBackgroundStyleConfig,
|
||||
toml_config_struct! { BackgroundStyleConfig, PartialBackgroundStyleConfig, RoughBackgroundStyleConfig,
|
||||
color => String,
|
||||
show_border => bool,
|
||||
border_color => String,
|
||||
}
|
||||
|
||||
toml_config_struct! { BackgroundConfig, PartialBackgroundConfig,
|
||||
toml_config_struct! { BackgroundConfig, PartialBackgroundConfig, RoughBackgroundConfig,
|
||||
show_background => bool,
|
||||
style => BackgroundStyleConfig [PartialBackgroundStyleConfig],
|
||||
style => BackgroundStyleConfig [PartialBackgroundStyleConfig, RoughBackgroundStyleConfig],
|
||||
}
|
||||
|
||||
toml_config_struct! { PowerControlConfig, PartialPowerControlConfig,
|
||||
toml_config_struct! { PowerControlConfig, PartialPowerControlConfig, RoughPowerControlConfig,
|
||||
allow_shutdown => bool,
|
||||
shutdown_hint => String,
|
||||
shutdown_hint_color => String,
|
||||
|
@ -239,7 +263,7 @@ toml_config_struct! { PowerControlConfig, PartialPowerControlConfig,
|
|||
hint_margin => u16,
|
||||
}
|
||||
|
||||
toml_config_struct! { SwitcherConfig, PartialSwitcherConfig,
|
||||
toml_config_struct! { SwitcherConfig, PartialSwitcherConfig, RoughSwitcherConfig,
|
||||
switcher_visibility => SwitcherVisibility,
|
||||
toggle_hint => String,
|
||||
toggle_hint_color => String,
|
||||
|
@ -287,7 +311,7 @@ toml_config_struct! { SwitcherConfig, PartialSwitcherConfig,
|
|||
no_envs_modifiers_focused => String,
|
||||
}
|
||||
|
||||
toml_config_struct! { InputFieldStyle, PartialInputFieldStyle,
|
||||
toml_config_struct! { InputFieldStyle, PartialInputFieldStyle, RoughInputFieldStyle,
|
||||
show_title => bool,
|
||||
title => String,
|
||||
|
||||
|
@ -306,14 +330,14 @@ toml_config_struct! { InputFieldStyle, PartialInputFieldStyle,
|
|||
max_width => u16,
|
||||
}
|
||||
|
||||
toml_config_struct! { UsernameFieldConfig, PartialUsernameFieldConfig,
|
||||
toml_config_struct! { UsernameFieldConfig, PartialUsernameFieldConfig, RoughUsernameFieldConfig,
|
||||
remember => bool,
|
||||
style => InputFieldStyle [PartialInputFieldStyle],
|
||||
style => InputFieldStyle [PartialInputFieldStyle, RoughInputFieldStyle],
|
||||
}
|
||||
|
||||
toml_config_struct! { PasswordFieldConfig, PartialPasswordFieldConfig,
|
||||
toml_config_struct! { PasswordFieldConfig, PartialPasswordFieldConfig, RoughPasswordFieldConfig,
|
||||
content_replacement_character => char,
|
||||
style => InputFieldStyle [PartialInputFieldStyle],
|
||||
style => InputFieldStyle [PartialInputFieldStyle, RoughInputFieldStyle],
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
|
@ -363,7 +387,6 @@ impl<'de> Deserialize<'de> for SwitcherVisibility {
|
|||
return Err(D::Error::custom(
|
||||
"Invalid key provided to toggle switcher visibility. Only F1-F12 are allowed"
|
||||
));
|
||||
|
||||
};
|
||||
|
||||
Self::Keybind(keycode)
|
||||
|
@ -381,112 +404,290 @@ impl Default for Config {
|
|||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
impl PartialConfig {
|
||||
/// Facilitates the loading of the entire configuration
|
||||
pub fn load(path: &Path) -> Result<Config, Box<dyn std::error::Error>> {
|
||||
let mut config = Config::default();
|
||||
pub fn from_file(
|
||||
path: &Path,
|
||||
variables: Option<&Variables>,
|
||||
) -> Result<PartialConfig, Box<dyn std::error::Error>> {
|
||||
let mut file = File::open(path)?;
|
||||
let mut contents = String::new();
|
||||
|
||||
let (config_str, var_config) = load_as_parts(path)?;
|
||||
let processed_config = apply_variables(config_str, &var_config)?;
|
||||
let partial = from_string::<PartialConfig, _>(&processed_config);
|
||||
config.merge_in_partial(partial);
|
||||
Ok(config)
|
||||
file.read_to_string(&mut contents)?;
|
||||
|
||||
match variables {
|
||||
Some(variables) => {
|
||||
let rough = toml::from_str::<RoughConfig>(&contents)?;
|
||||
Ok(rough.into_partial(variables)?)
|
||||
}
|
||||
None => Ok(toml::from_str::<PartialConfig>(&contents)?),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Variables {
|
||||
/// Facilitates the loading of the entire configuration
|
||||
pub fn from_file(path: &Path) -> Result<Variables, Box<dyn std::error::Error>> {
|
||||
let mut file = File::open(path)?;
|
||||
let mut contents = String::new();
|
||||
|
||||
file.read_to_string(&mut contents)?;
|
||||
|
||||
Ok(toml::from_str(&contents)?)
|
||||
}
|
||||
}
|
||||
|
||||
trait VariableInsertable: Sized {
|
||||
const DEPTH_LIMIT: u32 = 10;
|
||||
|
||||
fn insert(
|
||||
possible: PossibleVariable<Self>,
|
||||
variables: &Variables,
|
||||
) -> Result<Self, VariableInsertionError> {
|
||||
Self::insert_with_depth(possible, variables, 0)
|
||||
}
|
||||
fn insert_with_depth(
|
||||
value: PossibleVariable<Self>,
|
||||
variables: &Variables,
|
||||
depth: u32,
|
||||
) -> Result<Self, VariableInsertionError>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum PossibleVariable<T> {
|
||||
Value(T),
|
||||
Variable(String),
|
||||
}
|
||||
|
||||
impl<'de, T: Deserialize<'de>> TryFrom<toml::Value> for PossibleVariable<T> {
|
||||
type Error = &'static str;
|
||||
|
||||
fn try_from(value: toml::Value) -> Result<Self, Self::Error> {
|
||||
if let Ok(i) = value.clone().try_into() {
|
||||
return Ok(Self::Value(i));
|
||||
}
|
||||
|
||||
match value {
|
||||
Value::String(s) => Ok(PossibleVariable::Variable(s)),
|
||||
v => Err(v.type_str()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum VariableInsertionError {
|
||||
ImpossibleVariableCast {
|
||||
var_ident: String,
|
||||
expected_type: &'static str,
|
||||
},
|
||||
UnsetVariable {
|
||||
var_ident: String,
|
||||
},
|
||||
DepthLimitReached,
|
||||
InvalidType {
|
||||
expected: &'static str,
|
||||
gotten: &'static str,
|
||||
},
|
||||
UnexpectedVariableType {
|
||||
var_ident: String,
|
||||
expected_type: &'static str,
|
||||
},
|
||||
}
|
||||
|
||||
impl Display for VariableInsertionError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
VariableInsertionError::ImpossibleVariableCast {
|
||||
var_ident,
|
||||
expected_type,
|
||||
} => write!(
|
||||
f,
|
||||
"Impossible to use variable '{var_ident}' in string to cast to '{expected_type}'"
|
||||
),
|
||||
VariableInsertionError::UnsetVariable { var_ident } => {
|
||||
write!(f, "Variable '{var_ident}' is not set")
|
||||
},
|
||||
VariableInsertionError::DepthLimitReached => {
|
||||
write!(f, "Variable evaluation reached the depth limit")
|
||||
},
|
||||
VariableInsertionError::InvalidType { expected, gotten } => write!(f, "Expected type '{expected}'. Got type '{gotten}'."),
|
||||
VariableInsertionError::UnexpectedVariableType { var_ident, expected_type } => write!(f, "Needed to use variable '{var_ident}' as a '{expected_type}', but was unable to cast it as such."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for VariableInsertionError {}
|
||||
|
||||
macro_rules! non_string_var_insert {
|
||||
($($type:ty [$type_str:literal]),+ $(,)?) => {
|
||||
$(
|
||||
impl VariableInsertable for $type {
|
||||
fn insert_with_depth(
|
||||
value: PossibleVariable<Self>,
|
||||
variables: &Variables,
|
||||
depth: u32,
|
||||
) -> Result<Self, VariableInsertionError> {
|
||||
use VariableInsertionError as E;
|
||||
|
||||
if depth == Self::DEPTH_LIMIT {
|
||||
return Err(E::DepthLimitReached);
|
||||
}
|
||||
|
||||
match value {
|
||||
PossibleVariable::Variable(s) => {
|
||||
// Ignore surrounding spaces
|
||||
let s = s.trim();
|
||||
|
||||
let mut variter = VariableIterator::new(&s);
|
||||
|
||||
// No variable in string
|
||||
let var = variter.next().ok_or(E::InvalidType {
|
||||
expected: $type_str,
|
||||
gotten: "string",
|
||||
})?;
|
||||
|
||||
// Not whole string is variable
|
||||
if var.span() != (0..s.len()) {
|
||||
return Err(E::ImpossibleVariableCast {
|
||||
var_ident: var.ident().to_string(),
|
||||
expected_type: $type_str,
|
||||
});
|
||||
}
|
||||
|
||||
let value = <PossibleVariable<$type>>::try_from(
|
||||
variables
|
||||
.0
|
||||
.get(var.ident())
|
||||
.ok_or(E::UnsetVariable {
|
||||
var_ident: var.ident().to_string(),
|
||||
})?
|
||||
.clone(),
|
||||
)
|
||||
.map_err(|_| E::UnexpectedVariableType {
|
||||
var_ident: var.ident().to_string(),
|
||||
expected_type: $type_str,
|
||||
})?;
|
||||
|
||||
Self::insert_with_depth(value, variables, depth + 1)
|
||||
}
|
||||
PossibleVariable::Value(b) => Ok(b),
|
||||
}
|
||||
}
|
||||
}
|
||||
)+
|
||||
};
|
||||
}
|
||||
|
||||
non_string_var_insert! {
|
||||
bool ["boolean"],
|
||||
u8 ["unsigned 8-bit integer"],
|
||||
u16 ["unsigned 16-bit integer"],
|
||||
char ["character"],
|
||||
ShellLoginFlag ["shell login flag"],
|
||||
FocusBehaviour ["focus behavior"],
|
||||
SwitcherVisibility ["switcher visibility"],
|
||||
}
|
||||
|
||||
impl VariableInsertable for String {
|
||||
fn insert_with_depth(
|
||||
value: PossibleVariable<Self>,
|
||||
variables: &Variables,
|
||||
depth: u32,
|
||||
) -> Result<Self, VariableInsertionError> {
|
||||
use VariableInsertionError as E;
|
||||
|
||||
if depth == Self::DEPTH_LIMIT {
|
||||
return Err(E::DepthLimitReached);
|
||||
}
|
||||
|
||||
let mut s = match value {
|
||||
PossibleVariable::Value(s) | PossibleVariable::Variable(s) => s,
|
||||
};
|
||||
|
||||
loop {
|
||||
let Some(var) = VariableIterator::new(&s).next() else {
|
||||
break;
|
||||
};
|
||||
|
||||
let value = <PossibleVariable<String>>::try_from(
|
||||
variables
|
||||
.0
|
||||
.get(var.ident())
|
||||
.ok_or(E::UnsetVariable {
|
||||
var_ident: var.ident().to_string(),
|
||||
})?
|
||||
.clone(),
|
||||
)
|
||||
.map_err(|_| E::UnexpectedVariableType {
|
||||
var_ident: var.ident().to_string(),
|
||||
expected_type: "string",
|
||||
})?;
|
||||
|
||||
let insertion = Self::insert_with_depth(value.clone(), variables, depth + 1)?;
|
||||
s.replace_range(var.span(), &insertion);
|
||||
}
|
||||
|
||||
Ok(s)
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator over variables in a given string
|
||||
/// Assumes the presence of quotes
|
||||
struct VariableIterator<'a> {
|
||||
inner: &'a str,
|
||||
last: usize,
|
||||
offset: usize,
|
||||
}
|
||||
|
||||
struct Variable<'a> {
|
||||
start: usize,
|
||||
ident: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> Variable<'a> {
|
||||
const START_SYMBOL: &str = "$";
|
||||
|
||||
fn span(&self) -> std::ops::Range<usize> {
|
||||
self.start..self.start + Self::START_SYMBOL.len() + self.ident.len()
|
||||
}
|
||||
|
||||
fn ident(&self) -> &str {
|
||||
self.ident
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> VariableIterator<'a> {
|
||||
pub fn new(text: &'a str) -> Self {
|
||||
Self {
|
||||
inner: text,
|
||||
last: 0,
|
||||
offset: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<'a> Iterator for VariableIterator<'a> {
|
||||
type Item = (usize, usize);
|
||||
type Item = Variable<'a>;
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
const START_PAT: &str = "\"$";
|
||||
const START_PAT_LEN: usize = START_PAT.len();
|
||||
let s = &self.inner[self.offset..];
|
||||
|
||||
let start = match self.inner[self.last..].find(START_PAT) {
|
||||
Some(pos) => self.last + pos,
|
||||
let start = match s.find(Variable::START_SYMBOL) {
|
||||
Some(position) => position,
|
||||
None => return None,
|
||||
};
|
||||
|
||||
let end = self.inner[start + START_PAT_LEN..] // skip the "$ pattern
|
||||
// skip the "$ pattern
|
||||
let s = &s[start + Variable::START_SYMBOL.len()..];
|
||||
|
||||
// Find the first not variable token.
|
||||
let end = s
|
||||
.find(|c: char| !c.is_alphanumeric() && c != '_')
|
||||
.map(|index| start + index + START_PAT_LEN + 1)
|
||||
.unwrap_or_else(|| self.inner.len());
|
||||
self.last = end;
|
||||
.unwrap_or(s.len());
|
||||
|
||||
Some((start, end))
|
||||
}
|
||||
}
|
||||
let start = self.offset + start;
|
||||
self.offset = start + Variable::START_SYMBOL.len() + end;
|
||||
|
||||
/// Substitutes variables present in the configuration string
|
||||
fn apply_variables(config_str: String, var_config: &VariablesConfig) -> Result<String, VarError> {
|
||||
let mut output = String::new();
|
||||
let mut last = 0;
|
||||
let config_len = config_str.len();
|
||||
for (start, end) in VariableIterator::new(&config_str) {
|
||||
let var_ident = &config_str[start + 2..end - 1];
|
||||
let var_val = var_config.variables.get(var_ident).ok_or(VarError {
|
||||
variable: var_ident.to_owned(),
|
||||
pos: start,
|
||||
})?;
|
||||
output.push_str(&config_str[last..start]);
|
||||
let ident = &s[..end];
|
||||
|
||||
// any case that is not a string, will not be wrapped in brackets
|
||||
match var_val {
|
||||
Value::String(val) => output.push_str(&format!("\"{}\"", val)),
|
||||
_ => output.push_str(&var_val.to_string()),
|
||||
};
|
||||
last = end + 1;
|
||||
}
|
||||
|
||||
if last != config_len {
|
||||
output.push_str(&config_str[last..config_len]);
|
||||
}
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
/// Helper function to facilitate immediate deserialization of variables along passing the original file content
|
||||
fn load_as_parts(path: &Path) -> io::Result<(String, VariablesConfig)> {
|
||||
match read_to_string(path) {
|
||||
Ok(contents) => {
|
||||
let variables = from_string::<VariablesConfig, _>(&contents);
|
||||
Ok((contents, variables))
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
fn read_to_string(path: &Path) -> io::Result<String> {
|
||||
let file = File::open(path)?;
|
||||
|
||||
let mut buf_reader = BufReader::new(file);
|
||||
let mut contents = String::new();
|
||||
|
||||
buf_reader.read_to_string(&mut contents)?;
|
||||
Ok(contents)
|
||||
}
|
||||
|
||||
/// Generic configuration file loading
|
||||
fn from_string<T: DeserializeOwned, S: AsRef<str>>(contents: S) -> T {
|
||||
match toml::from_str::<T>(contents.as_ref()) {
|
||||
Ok(config) => config,
|
||||
Err(err) => {
|
||||
eprintln!("Given configuration file contains errors:");
|
||||
eprintln!("{err}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
Some(Variable { start, ident })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -496,23 +697,31 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_variable_iterator() {
|
||||
let test_cases = [
|
||||
macro_rules! assert_var_iter {
|
||||
(
|
||||
"TESTMEPLS \"$test5\" ME and \"$another\"",
|
||||
2,
|
||||
vec!["\"$test5\"", "\"$another\""],
|
||||
),
|
||||
(
|
||||
"\"$var1\" \"$var2\" $5var",
|
||||
2,
|
||||
vec!["\"$var1\"", "\"$var2\""],
|
||||
),
|
||||
];
|
||||
for (text, count, variables) in test_cases {
|
||||
let iter = VariableIterator::new(text);
|
||||
let collected_vars: Vec<_> = iter.map(|(start, end)| &text[start..end]).collect();
|
||||
assert_eq!(variables, collected_vars);
|
||||
assert_eq!(count, collected_vars.len());
|
||||
$s:literal,
|
||||
($($ident:literal),*)
|
||||
) => {
|
||||
let variables: Vec<String> = VariableIterator::new($s).map(|v| v.ident().to_string()).collect();
|
||||
let idents: &[&str] = &[$($ident),*];
|
||||
|
||||
eprintln!("variables = {variables:?}");
|
||||
eprintln!("ident = {idents:?}");
|
||||
|
||||
assert_eq!(
|
||||
&variables,
|
||||
idents,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
assert_var_iter!("", ());
|
||||
assert_var_iter!("abcdef", ());
|
||||
assert_var_iter!("$a", ("a"));
|
||||
assert_var_iter!("$a$b", ("a", "b"));
|
||||
assert_var_iter!("$a_c$b", ("a_c", "b"));
|
||||
assert_var_iter!("$a()$b", ("a", "b"));
|
||||
assert_var_iter!("$0 $1", ("0", "1"));
|
||||
assert_var_iter!("$var1 $var2 $var3 ", ("var1", "var2", "var3"));
|
||||
}
|
||||
}
|
||||
|
|
94
src/main.rs
94
src/main.rs
|
@ -6,7 +6,7 @@ use crossterm::{
|
|||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use log::{error, info};
|
||||
use log::{error, info, warn};
|
||||
use ratatui::backend::CrosstermBackend;
|
||||
use ratatui::Terminal;
|
||||
|
||||
|
@ -37,9 +37,77 @@ use self::{
|
|||
},
|
||||
};
|
||||
|
||||
const DEFAULT_VARIABLES_PATH: &str = "/etc/lemurs/variables.toml";
|
||||
const DEFAULT_CONFIG_PATH: &str = "/etc/lemurs/config.toml";
|
||||
const PREVIEW_LOG_PATH: &str = "lemurs.log";
|
||||
|
||||
fn merge_in_configuration(
|
||||
config: &mut Config,
|
||||
config_path: Option<&Path>,
|
||||
variables_path: Option<&Path>,
|
||||
) {
|
||||
let load_variables_path = variables_path.unwrap_or_else(|| Path::new(DEFAULT_VARIABLES_PATH));
|
||||
|
||||
let variables = match config::Variables::from_file(load_variables_path) {
|
||||
Ok(variables) => {
|
||||
info!(
|
||||
"Successfully loaded variables file from '{}'",
|
||||
load_variables_path.display()
|
||||
);
|
||||
|
||||
Some(variables)
|
||||
}
|
||||
Err(err) => {
|
||||
// If we have given it a specific config path, it should crash if this file cannot be
|
||||
// loaded. If it is the default config location just put a warning in the logs.
|
||||
if let Some(variables_path) = variables_path {
|
||||
eprintln!(
|
||||
"The variables file '{}' cannot be loaded.\nReason: {}",
|
||||
variables_path.display(),
|
||||
err
|
||||
);
|
||||
std::process::exit(1);
|
||||
} else {
|
||||
info!(
|
||||
"No variables file loaded from the default location ({}). Reason: {}",
|
||||
DEFAULT_CONFIG_PATH, err
|
||||
);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let load_config_path = config_path.unwrap_or_else(|| Path::new(DEFAULT_CONFIG_PATH));
|
||||
|
||||
match config::PartialConfig::from_file(load_config_path, variables.as_ref()) {
|
||||
Ok(partial_config) => {
|
||||
info!(
|
||||
"Successfully loaded configuration file from '{}'",
|
||||
load_config_path.display()
|
||||
);
|
||||
config.merge_in_partial(partial_config)
|
||||
}
|
||||
Err(err) => {
|
||||
// If we have given it a specific config path, it should crash if this file cannot be
|
||||
// loaded. If it is the default config location just put a warning in the logs.
|
||||
if let Some(config_path) = config_path {
|
||||
eprintln!(
|
||||
"The config file '{}' cannot be loaded.\nReason: {}",
|
||||
config_path.display(),
|
||||
err
|
||||
);
|
||||
std::process::exit(1);
|
||||
} else {
|
||||
warn!(
|
||||
"No configuration file loaded from the expected location ({}). Reason: {}",
|
||||
DEFAULT_CONFIG_PATH, err
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_logger(log_path: &str) {
|
||||
let log_file = Box::new(File::create(log_path).unwrap_or_else(|_| {
|
||||
eprintln!("Failed to open log file: '{log_path}'");
|
||||
|
@ -60,28 +128,8 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||
std::process::exit(2);
|
||||
});
|
||||
|
||||
// Load and setup configuration
|
||||
let mut should_crash = true;
|
||||
let cli_config_path = cli.config.as_deref();
|
||||
let load_config_path = cli_config_path.unwrap_or_else(|| {
|
||||
should_crash = false;
|
||||
Path::new(DEFAULT_CONFIG_PATH)
|
||||
});
|
||||
|
||||
let mut config = match Config::load(load_config_path) {
|
||||
Ok(config) => config,
|
||||
Err(err) => {
|
||||
eprintln!(
|
||||
"The config file '{}' cannot be loaded.\nReason: {}",
|
||||
load_config_path.display(),
|
||||
err
|
||||
);
|
||||
if should_crash {
|
||||
std::process::exit(1);
|
||||
}
|
||||
Config::default()
|
||||
}
|
||||
};
|
||||
let mut config = Config::default();
|
||||
merge_in_configuration(&mut config, cli.config.as_deref(), cli.variables.as_deref());
|
||||
|
||||
if let Some(cmd) = cli.command {
|
||||
match cmd {
|
||||
|
|
|
@ -146,11 +146,17 @@ impl LimitedOutputChild {
|
|||
let file = file_options.open(log_path)?;
|
||||
|
||||
let Some(stdout) = process.stdout.take() else {
|
||||
return Err(io::Error::new(io::ErrorKind::Other, "Failed to grab stdout"));
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"Failed to grab stdout",
|
||||
));
|
||||
};
|
||||
|
||||
let Some(stderr) = process.stderr.take() else {
|
||||
return Err(io::Error::new(io::ErrorKind::Other, "Failed to grab stderr"));
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"Failed to grab stderr",
|
||||
));
|
||||
};
|
||||
|
||||
let mut stdout_receiver = Receiver::from(stdout);
|
||||
|
|
|
@ -450,7 +450,7 @@ impl LoginForm {
|
|||
let Some(post_login_env) = environment else {
|
||||
status_message.set(ErrorStatusMessage::NoGraphicalEnvironment);
|
||||
send_ui_request(UIThreadRequest::Redraw);
|
||||
continue
|
||||
continue;
|
||||
};
|
||||
|
||||
match start_session(
|
||||
|
|
Loading…
Reference in a new issue