Add script-vars
This commit is contained in:
parent
5f164650e9
commit
2451f6fd49
12 changed files with 215 additions and 65 deletions
7
Cargo.lock
generated
7
Cargo.lock
generated
|
@ -20,6 +20,12 @@ dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyhow"
|
||||||
|
version = "1.0.42"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "595d3cfa7a60d4555cb5067b99f07142a08ea778de5cf993f7b75c7d8fabc486"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ascii-canvas"
|
name = "ascii-canvas"
|
||||||
version = "3.0.0"
|
version = "3.0.0"
|
||||||
|
@ -195,6 +201,7 @@ checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
|
||||||
name = "eww_config"
|
name = "eww_config"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
"codespan-reporting",
|
"codespan-reporting",
|
||||||
"derive_more",
|
"derive_more",
|
||||||
"insta",
|
"insta",
|
||||||
|
|
|
@ -23,6 +23,8 @@ serde_json = "1.0"
|
||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
pretty_assertions = "0.7"
|
pretty_assertions = "0.7"
|
||||||
|
|
||||||
|
anyhow = "1"
|
||||||
|
|
||||||
|
|
||||||
simplexpr = { path = "../../projects/simplexpr" }
|
simplexpr = { path = "../../projects/simplexpr" }
|
||||||
|
|
||||||
|
|
|
@ -2,12 +2,16 @@ use std::collections::HashMap;
|
||||||
|
|
||||||
use simplexpr::SimplExpr;
|
use simplexpr::SimplExpr;
|
||||||
|
|
||||||
use super::{var::VarDefinition, widget_definition::WidgetDefinition, widget_use::WidgetUse};
|
use super::{
|
||||||
|
script_var_definition::ScriptVarDefinition, var_definition::VarDefinition, widget_definition::WidgetDefinition,
|
||||||
|
widget_use::WidgetUse,
|
||||||
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
|
config::script_var_definition::{PollScriptVar, TailScriptVar},
|
||||||
error::{AstError, AstResult, OptionAstErrorExt},
|
error::{AstError, AstResult, OptionAstErrorExt},
|
||||||
parser::{
|
parser::{
|
||||||
ast::{Ast, AstIterator, Span},
|
ast::{Ast, AstIterator, Span},
|
||||||
element::{Element, FromAst},
|
element::{Element, FromAst, FromAstElementContent},
|
||||||
},
|
},
|
||||||
spanned,
|
spanned,
|
||||||
value::{AttrName, VarName},
|
value::{AttrName, VarName},
|
||||||
|
@ -15,6 +19,7 @@ use crate::{
|
||||||
|
|
||||||
pub enum TopLevel {
|
pub enum TopLevel {
|
||||||
VarDefinition(VarDefinition),
|
VarDefinition(VarDefinition),
|
||||||
|
ScriptVarDefinition(ScriptVarDefinition),
|
||||||
WidgetDefinition(WidgetDefinition),
|
WidgetDefinition(WidgetDefinition),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,11 +27,21 @@ impl FromAst for TopLevel {
|
||||||
fn from_ast(e: Ast) -> AstResult<Self> {
|
fn from_ast(e: Ast) -> AstResult<Self> {
|
||||||
let span = e.span();
|
let span = e.span();
|
||||||
spanned!(e.span(), {
|
spanned!(e.span(), {
|
||||||
let list = e.as_list_ref()?;
|
let list = e.as_list()?;
|
||||||
match list.first().or_missing()?.as_symbol_ref()?.as_ref() {
|
let mut iter = AstIterator::new(list.into_iter());
|
||||||
"defwidget" => Self::WidgetDefinition(WidgetDefinition::from_ast(e)?),
|
let (sym_span, element_name) = iter.expect_symbol()?;
|
||||||
"defvar" => Self::VarDefinition(VarDefinition::from_ast(e)?),
|
match element_name.as_str() {
|
||||||
x => return Err(AstError::UnknownToplevel(Some(span), x.to_string())),
|
x if x == WidgetDefinition::get_element_name() => {
|
||||||
|
Self::WidgetDefinition(WidgetDefinition::from_tail(span, iter)?)
|
||||||
|
}
|
||||||
|
x if x == VarDefinition::get_element_name() => Self::VarDefinition(VarDefinition::from_tail(span, iter)?),
|
||||||
|
x if x == PollScriptVar::get_element_name() => {
|
||||||
|
Self::ScriptVarDefinition(ScriptVarDefinition::Poll(PollScriptVar::from_tail(span, iter)?))
|
||||||
|
}
|
||||||
|
x if x == TailScriptVar::get_element_name() => {
|
||||||
|
Self::ScriptVarDefinition(ScriptVarDefinition::Tail(TailScriptVar::from_tail(span, iter)?))
|
||||||
|
}
|
||||||
|
x => return Err(AstError::UnknownToplevel(Some(sym_span), x.to_string())),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -36,17 +51,22 @@ impl FromAst for TopLevel {
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
widget_definitions: HashMap<String, WidgetDefinition>,
|
widget_definitions: HashMap<String, WidgetDefinition>,
|
||||||
var_definitions: HashMap<VarName, VarDefinition>,
|
var_definitions: HashMap<VarName, VarDefinition>,
|
||||||
|
script_vars: HashMap<VarName, ScriptVarDefinition>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromAst for Config {
|
impl FromAst for Config {
|
||||||
fn from_ast(e: Ast) -> AstResult<Self> {
|
fn from_ast(e: Ast) -> AstResult<Self> {
|
||||||
let list = e.as_list()?;
|
let list = e.as_list()?;
|
||||||
let mut config = Self { widget_definitions: HashMap::new(), var_definitions: HashMap::new() };
|
let mut config =
|
||||||
|
Self { widget_definitions: HashMap::new(), var_definitions: HashMap::new(), script_vars: HashMap::new() };
|
||||||
for element in list {
|
for element in list {
|
||||||
match TopLevel::from_ast(element)? {
|
match TopLevel::from_ast(element)? {
|
||||||
TopLevel::VarDefinition(x) => {
|
TopLevel::VarDefinition(x) => {
|
||||||
config.var_definitions.insert(x.name.clone(), x);
|
config.var_definitions.insert(x.name.clone(), x);
|
||||||
}
|
}
|
||||||
|
TopLevel::ScriptVarDefinition(x) => {
|
||||||
|
config.script_vars.insert(x.name().clone(), x);
|
||||||
|
}
|
||||||
TopLevel::WidgetDefinition(x) => {
|
TopLevel::WidgetDefinition(x) => {
|
||||||
config.widget_definitions.insert(x.name.clone(), x);
|
config.widget_definitions.insert(x.name.clone(), x);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
mod config;
|
mod config;
|
||||||
|
pub mod script_var_definition;
|
||||||
pub mod validate;
|
pub mod validate;
|
||||||
pub mod var;
|
pub mod var_definition;
|
||||||
pub mod widget_definition;
|
pub mod widget_definition;
|
||||||
pub mod widget_use;
|
pub mod widget_use;
|
||||||
|
|
73
src/config/script_var_definition.rs
Normal file
73
src/config/script_var_definition.rs
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use simplexpr::{dynval::DynVal, SimplExpr};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
error::{AstError, AstResult},
|
||||||
|
parser::{
|
||||||
|
ast::{Ast, AstIterator, Span},
|
||||||
|
element::{Element, FromAst, FromAstElementContent},
|
||||||
|
},
|
||||||
|
spanned,
|
||||||
|
value::{AttrName, VarName},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum ScriptVarDefinition {
|
||||||
|
Poll(PollScriptVar),
|
||||||
|
Tail(TailScriptVar),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScriptVarDefinition {
|
||||||
|
pub fn name(&self) -> &VarName {
|
||||||
|
match self {
|
||||||
|
ScriptVarDefinition::Poll(x) => &x.name,
|
||||||
|
ScriptVarDefinition::Tail(x) => &x.name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum VarSource {
|
||||||
|
// TODO allow for other executors? (python, etc)
|
||||||
|
Shell(String),
|
||||||
|
Function(fn() -> Result<DynVal, Box<dyn std::error::Error>>),
|
||||||
|
}
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct PollScriptVar {
|
||||||
|
pub name: VarName,
|
||||||
|
pub command: VarSource,
|
||||||
|
pub interval: std::time::Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromAstElementContent for PollScriptVar {
|
||||||
|
fn get_element_name() -> &'static str {
|
||||||
|
"defpollvar"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_tail<I: Iterator<Item = Ast>>(span: Span, mut iter: AstIterator<I>) -> AstResult<Self> {
|
||||||
|
let (_, name) = iter.expect_symbol()?;
|
||||||
|
let attrs: HashMap<String, String> = iter.expect_key_values()?;
|
||||||
|
let interval = attrs.get("interval").unwrap();
|
||||||
|
let interval = crate::util::parse_duration(interval).map_err(|e| AstError::Other(Some(span), e.into()))?;
|
||||||
|
let (_, script) = iter.expect_value()?;
|
||||||
|
Ok(Self { name: VarName(name), command: VarSource::Shell(script.to_string()), interval })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct TailScriptVar {
|
||||||
|
pub name: VarName,
|
||||||
|
pub command: String,
|
||||||
|
}
|
||||||
|
impl FromAstElementContent for TailScriptVar {
|
||||||
|
fn get_element_name() -> &'static str {
|
||||||
|
"deftailvar"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_tail<I: Iterator<Item = Ast>>(span: Span, mut iter: AstIterator<I>) -> AstResult<Self> {
|
||||||
|
let (_, name) = iter.expect_symbol()?;
|
||||||
|
let (_, script) = iter.expect_value()?;
|
||||||
|
Ok(Self { name: VarName(name), command: script.to_string() })
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,34 +0,0 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use simplexpr::{dynval::DynVal, SimplExpr};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
error::AstResult,
|
|
||||||
parser::{
|
|
||||||
ast::{Ast, AstIterator, Span},
|
|
||||||
element::{Element, FromAst},
|
|
||||||
},
|
|
||||||
spanned,
|
|
||||||
value::{AttrName, VarName},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
|
||||||
pub struct VarDefinition {
|
|
||||||
pub name: VarName,
|
|
||||||
pub initial_value: DynVal,
|
|
||||||
pub span: Span,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromAst for VarDefinition {
|
|
||||||
fn from_ast(e: Ast) -> AstResult<Self> {
|
|
||||||
let span = e.span();
|
|
||||||
spanned!(e.span(), {
|
|
||||||
let list = e.as_list()?;
|
|
||||||
let mut iter = AstIterator::new(list.into_iter());
|
|
||||||
let _ = iter.expect_symbol()?;
|
|
||||||
let (_, name) = iter.expect_symbol()?;
|
|
||||||
let (_, initial_value) = iter.expect_value()?;
|
|
||||||
Self { name: VarName(name), initial_value, span }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
32
src/config/var_definition.rs
Normal file
32
src/config/var_definition.rs
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use simplexpr::{dynval::DynVal, SimplExpr};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
error::AstResult,
|
||||||
|
parser::{
|
||||||
|
ast::{Ast, AstIterator, Span},
|
||||||
|
element::{Element, FromAst, FromAstElementContent},
|
||||||
|
},
|
||||||
|
spanned,
|
||||||
|
value::{AttrName, VarName},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
|
pub struct VarDefinition {
|
||||||
|
pub name: VarName,
|
||||||
|
pub initial_value: DynVal,
|
||||||
|
pub span: Span,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromAstElementContent for VarDefinition {
|
||||||
|
fn get_element_name() -> &'static str {
|
||||||
|
"defvar"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_tail<I: Iterator<Item = Ast>>(span: Span, mut iter: AstIterator<I>) -> AstResult<Self> {
|
||||||
|
let (_, name) = iter.expect_symbol()?;
|
||||||
|
let (_, initial_value) = iter.expect_value()?;
|
||||||
|
Ok(Self { name: VarName(name), initial_value, span })
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,7 +6,7 @@ use crate::{
|
||||||
error::AstResult,
|
error::AstResult,
|
||||||
parser::{
|
parser::{
|
||||||
ast::{Ast, AstIterator, Span},
|
ast::{Ast, AstIterator, Span},
|
||||||
element::{Element, FromAst},
|
element::{Element, FromAst, FromAstElementContent},
|
||||||
},
|
},
|
||||||
spanned,
|
spanned,
|
||||||
value::{AttrName, VarName},
|
value::{AttrName, VarName},
|
||||||
|
@ -22,23 +22,18 @@ pub struct WidgetDefinition {
|
||||||
pub args_span: Span,
|
pub args_span: Span,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromAst for WidgetDefinition {
|
impl FromAstElementContent for WidgetDefinition {
|
||||||
fn from_ast(e: Ast) -> AstResult<Self> {
|
fn get_element_name() -> &'static str {
|
||||||
let span = e.span();
|
"defwidget"
|
||||||
spanned!(e.span(), {
|
}
|
||||||
let list = e.as_list()?;
|
|
||||||
let mut iter = AstIterator::new(list.into_iter());
|
|
||||||
|
|
||||||
let (_, def_type) = iter.expect_symbol()?;
|
|
||||||
assert!(def_type == "defwidget");
|
|
||||||
|
|
||||||
|
fn from_tail<I: Iterator<Item = Ast>>(span: Span, mut iter: AstIterator<I>) -> AstResult<Self> {
|
||||||
let (_, name) = iter.expect_symbol()?;
|
let (_, name) = iter.expect_symbol()?;
|
||||||
let (args_span, expected_args) = iter.expect_array()?;
|
let (args_span, expected_args) = iter.expect_array()?;
|
||||||
let expected_args = expected_args.into_iter().map(|x| x.as_symbol().map(AttrName)).collect::<AstResult<_>>()?;
|
let expected_args = expected_args.into_iter().map(|x| x.as_symbol().map(AttrName)).collect::<AstResult<_>>()?;
|
||||||
let widget = iter.expect_any().and_then(WidgetUse::from_ast)?;
|
let widget = iter.expect_any().and_then(WidgetUse::from_ast)?;
|
||||||
// TODO verify that this was the last element in the list
|
// TODO verify that this was the last element in the list
|
||||||
// iter.expect_done()?;
|
// iter.expect_done()?;
|
||||||
Self { name, expected_args, widget, span, args_span }
|
Ok(Self { name, expected_args, widget, span, args_span })
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
17
src/error.rs
17
src/error.rs
|
@ -20,6 +20,10 @@ pub enum AstError {
|
||||||
WrongExprType(Option<Span>, AstType, AstType),
|
WrongExprType(Option<Span>, AstType, AstType),
|
||||||
#[error("Expected to get a value, but got {1}")]
|
#[error("Expected to get a value, but got {1}")]
|
||||||
NotAValue(Option<Span>, AstType),
|
NotAValue(Option<Span>, AstType),
|
||||||
|
#[error("Expected element {1}, but read {2}")]
|
||||||
|
MismatchedElementName(Option<Span>, String, String),
|
||||||
|
#[error("{1}")]
|
||||||
|
Other(Option<Span>, Box<dyn std::error::Error>),
|
||||||
|
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
ValidationError(#[from] ValidationError),
|
ValidationError(#[from] ValidationError),
|
||||||
|
@ -35,6 +39,8 @@ impl AstError {
|
||||||
AstError::MissingNode(span) => *span,
|
AstError::MissingNode(span) => *span,
|
||||||
AstError::WrongExprType(span, ..) => *span,
|
AstError::WrongExprType(span, ..) => *span,
|
||||||
AstError::NotAValue(span, ..) => *span,
|
AstError::NotAValue(span, ..) => *span,
|
||||||
|
AstError::MismatchedElementName(span, ..) => *span,
|
||||||
|
AstError::Other(span, ..) => *span,
|
||||||
AstError::ValidationError(error) => None, // TODO none here is stupid
|
AstError::ValidationError(error) => None, // TODO none here is stupid
|
||||||
AstError::ParseError { file_id, source } => file_id.and_then(|id| get_parse_error_span(id, source)),
|
AstError::ParseError { file_id, source } => file_id.and_then(|id| get_parse_error_span(id, source)),
|
||||||
}
|
}
|
||||||
|
@ -67,9 +73,14 @@ fn get_parse_error_span(
|
||||||
pub fn spanned(span: Span, err: impl Into<AstError>) -> AstError {
|
pub fn spanned(span: Span, err: impl Into<AstError>) -> AstError {
|
||||||
use AstError::*;
|
use AstError::*;
|
||||||
match err.into() {
|
match err.into() {
|
||||||
AstError::UnknownToplevel(None, x) => AstError::UnknownToplevel(Some(span), x),
|
UnknownToplevel(None, x) => UnknownToplevel(Some(span), x),
|
||||||
AstError::MissingNode(None) => AstError::MissingNode(Some(span)),
|
MissingNode(None) => MissingNode(Some(span)),
|
||||||
AstError::WrongExprType(None, x, y) => AstError::WrongExprType(Some(span), x, y),
|
WrongExprType(None, x, y) => WrongExprType(Some(span), x, y),
|
||||||
|
UnknownToplevel(None, x) => UnknownToplevel(Some(span), x),
|
||||||
|
MissingNode(None) => MissingNode(Some(span)),
|
||||||
|
NotAValue(None, x) => NotAValue(Some(span), x),
|
||||||
|
MismatchedElementName(None, x, y) => MismatchedElementName(Some(span), x, y),
|
||||||
|
Other(None, x) => Other(Some(span), x),
|
||||||
x => x,
|
x => x,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,4 +6,5 @@ pub mod config;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod format_diagnostic;
|
pub mod format_diagnostic;
|
||||||
pub mod parser;
|
pub mod parser;
|
||||||
|
mod util;
|
||||||
pub mod value;
|
pub mod value;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use super::ast::{Ast, AstIterator, AstType, Span};
|
use super::ast::{Ast, AstIterator, AstType, Span};
|
||||||
use crate::{error::*, parser, spanned, value::AttrName};
|
use crate::{error::*, parser, spanned, value::AttrName};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use simplexpr::ast::SimplExpr;
|
use simplexpr::{ast::SimplExpr, dynval::DynVal};
|
||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, LinkedList},
|
collections::{HashMap, LinkedList},
|
||||||
iter::FromIterator,
|
iter::FromIterator,
|
||||||
|
@ -18,6 +18,34 @@ impl FromAst for Ast {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FromAst for String {
|
||||||
|
fn from_ast(e: Ast) -> AstResult<Self> {
|
||||||
|
Ok(e.as_value()?.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A trait that allows creating a type from the tail of a list-node.
|
||||||
|
/// I.e. to parse (foo [a b] (c d)), [from_tail] would just get [a b] (c d).
|
||||||
|
pub trait FromAstElementContent: Sized {
|
||||||
|
fn get_element_name() -> &'static str;
|
||||||
|
fn from_tail<I: Iterator<Item = Ast>>(span: Span, iter: AstIterator<I>) -> AstResult<Self>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: FromAstElementContent> FromAst for T {
|
||||||
|
fn from_ast(e: Ast) -> AstResult<Self> {
|
||||||
|
let span = e.span();
|
||||||
|
spanned!(e.span(), {
|
||||||
|
let list = e.as_list()?;
|
||||||
|
let mut iter = AstIterator::new(list.into_iter());
|
||||||
|
let (_, element_name) = iter.expect_symbol()?;
|
||||||
|
if Self::get_element_name() != element_name {
|
||||||
|
return Err(AstError::MismatchedElementName(Some(span), Self::get_element_name().to_string(), element_name));
|
||||||
|
}
|
||||||
|
Self::from_tail(span, iter)?
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl FromAst for SimplExpr {
|
impl FromAst for SimplExpr {
|
||||||
fn from_ast(e: Ast) -> AstResult<Self> {
|
fn from_ast(e: Ast) -> AstResult<Self> {
|
||||||
match e {
|
match e {
|
||||||
|
|
14
src/util.rs
Normal file
14
src/util.rs
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
pub fn parse_duration(s: &str) -> anyhow::Result<std::time::Duration> {
|
||||||
|
use std::time::Duration;
|
||||||
|
if s.ends_with("ms") {
|
||||||
|
Ok(Duration::from_millis(s.trim_end_matches("ms").parse()?))
|
||||||
|
} else if s.ends_with('s') {
|
||||||
|
Ok(Duration::from_secs(s.trim_end_matches('s').parse()?))
|
||||||
|
} else if s.ends_with('m') {
|
||||||
|
Ok(Duration::from_secs(s.trim_end_matches('m').parse::<u64>()? * 60))
|
||||||
|
} else if s.ends_with('h') {
|
||||||
|
Ok(Duration::from_secs(s.trim_end_matches('h').parse::<u64>()? * 60 * 60))
|
||||||
|
} else {
|
||||||
|
Err(anyhow::anyhow!("Failed to parse duration `{}`", s))
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue