feat(core): progress in the core implementation
This commit is contained in:
parent
459e414a09
commit
e643609d57
82
espanso/src/cli/worker/config.rs
Normal file
82
espanso/src/cli/worker/config.rs
Normal file
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* This file is part of espanso.
|
||||
*
|
||||
* Copyright (C) 2019-2021 Federico Terzi
|
||||
*
|
||||
* espanso is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* espanso is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::engine::process::MatchFilter;
|
||||
use espanso_config::{
|
||||
config::{AppProperties, Config, ConfigStore},
|
||||
matches::store::{MatchSet, MatchStore},
|
||||
};
|
||||
use espanso_info::{AppInfo, AppInfoProvider};
|
||||
use std::iter::FromIterator;
|
||||
|
||||
pub struct ConfigManager<'a> {
|
||||
config_store: &'a dyn ConfigStore,
|
||||
match_store: &'a dyn MatchStore,
|
||||
app_info_provider: &'a dyn AppInfoProvider,
|
||||
}
|
||||
|
||||
impl<'a> ConfigManager<'a> {
|
||||
pub fn new(
|
||||
config_store: &'a dyn ConfigStore,
|
||||
match_store: &'a dyn MatchStore,
|
||||
app_info_provider: &'a dyn AppInfoProvider,
|
||||
) -> Self {
|
||||
Self {
|
||||
config_store,
|
||||
match_store,
|
||||
app_info_provider,
|
||||
}
|
||||
}
|
||||
|
||||
fn active(&self) -> &'a dyn Config {
|
||||
let current_app = self.app_info_provider.get_info();
|
||||
let info = to_app_properties(¤t_app);
|
||||
self.config_store.active(&info)
|
||||
}
|
||||
|
||||
fn active_match_set(&self) -> MatchSet {
|
||||
let match_paths = self.active().match_paths();
|
||||
self.match_store.query(&match_paths)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> MatchFilter for ConfigManager<'a> {
|
||||
fn filter_active(&self, matches_ids: &[i32]) -> Vec<i32> {
|
||||
let ids_set: HashSet<i32> = HashSet::from_iter(matches_ids.iter().copied());
|
||||
let match_set = self.active_match_set();
|
||||
|
||||
match_set
|
||||
.matches
|
||||
.iter()
|
||||
.filter(|m| ids_set.contains(&m.id))
|
||||
.map(|m| m.id)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: test
|
||||
fn to_app_properties(info: &AppInfo) -> AppProperties {
|
||||
AppProperties {
|
||||
title: info.title.as_deref(),
|
||||
class: info.class.as_deref(),
|
||||
exec: info.exec.as_deref(),
|
||||
}
|
||||
}
|
72
espanso/src/cli/worker/matcher/convert.rs
Normal file
72
espanso/src/cli/worker/matcher/convert.rs
Normal file
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* This file is part of espanso.
|
||||
*
|
||||
* Copyright (C) 2019-2021 Federico Terzi
|
||||
*
|
||||
* espanso is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* espanso is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use espanso_config::{
|
||||
config::ConfigStore,
|
||||
matches::{
|
||||
store::{MatchSet, MatchStore},
|
||||
MatchCause,
|
||||
},
|
||||
};
|
||||
use espanso_match::rolling::{RollingMatch, StringMatchOptions};
|
||||
use std::iter::FromIterator;
|
||||
|
||||
pub struct MatchConverter<'a> {
|
||||
config_store: &'a dyn ConfigStore,
|
||||
match_store: &'a dyn MatchStore,
|
||||
}
|
||||
|
||||
impl<'a> MatchConverter<'a> {
|
||||
pub fn new(config_store: &'a dyn ConfigStore, match_store: &'a dyn MatchStore) -> Self {
|
||||
Self {
|
||||
config_store,
|
||||
match_store,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: test (might need to move the conversion logic into a separate function)
|
||||
pub fn get_rolling_matches(&self) -> Vec<RollingMatch<i32>> {
|
||||
let match_set = self.global_match_set();
|
||||
let mut matches = Vec::new();
|
||||
|
||||
for m in match_set.matches {
|
||||
if let MatchCause::Trigger(cause) = &m.cause {
|
||||
for trigger in cause.triggers.iter() {
|
||||
matches.push(RollingMatch::from_string(
|
||||
m.id,
|
||||
&trigger,
|
||||
&StringMatchOptions {
|
||||
case_insensitive: cause.propagate_case,
|
||||
preserve_case_markers: cause.propagate_case,
|
||||
left_word: cause.left_word,
|
||||
right_word: cause.right_word,
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
matches
|
||||
}
|
||||
|
||||
fn global_match_set(&self) -> MatchSet {
|
||||
let paths = self.config_store.get_all_match_paths();
|
||||
self.match_store.query(&Vec::from_iter(paths.into_iter()))
|
||||
}
|
||||
}
|
|
@ -22,6 +22,7 @@ use espanso_match::rolling::matcher::RollingMatcherState;
|
|||
use enum_as_inner::EnumAsInner;
|
||||
|
||||
pub mod rolling;
|
||||
pub mod convert;
|
||||
|
||||
#[derive(Clone, EnumAsInner)]
|
||||
pub enum MatcherState<'a> {
|
||||
|
|
|
@ -31,14 +31,8 @@ pub struct RollingMatcherAdapter {
|
|||
}
|
||||
|
||||
impl RollingMatcherAdapter {
|
||||
pub fn new() -> Self {
|
||||
// TODO: pass actual matches
|
||||
let matches = vec![
|
||||
RollingMatch::from_string(1, "esp", &Default::default()),
|
||||
RollingMatch::from_string(2, "test", &Default::default()),
|
||||
];
|
||||
|
||||
let matcher = RollingMatcher::new(&matches, Default::default());
|
||||
pub fn new(matches: &[RollingMatch<i32>]) -> Self {
|
||||
let matcher = RollingMatcher::new(matches, Default::default());
|
||||
|
||||
Self { matcher }
|
||||
}
|
||||
|
|
|
@ -19,10 +19,13 @@
|
|||
|
||||
use funnel::Source;
|
||||
use process::Matcher;
|
||||
use ui::selector::MatchSelectorAdapter;
|
||||
|
||||
use crate::engine::{Engine, funnel, process, dispatch};
|
||||
use super::{CliModule, CliModuleArgs};
|
||||
|
||||
mod ui;
|
||||
mod config;
|
||||
mod source;
|
||||
mod matcher;
|
||||
mod executor;
|
||||
|
@ -40,13 +43,21 @@ pub fn new() -> CliModule {
|
|||
}
|
||||
|
||||
fn worker_main(args: CliModuleArgs) {
|
||||
let detect_source = source::detect::init_and_spawn().unwrap(); // TODO: handle error
|
||||
let config_store = args.config_store.expect("missing config store in worker main");
|
||||
let match_store = args.match_store.expect("missing match store in worker main");
|
||||
|
||||
let app_info_provider = espanso_info::get_provider().expect("unable to initialize app info provider");
|
||||
let config_manager = config::ConfigManager::new(&*config_store, &*match_store, &*app_info_provider);
|
||||
let match_converter = matcher::convert::MatchConverter::new(&*config_store, &*match_store);
|
||||
|
||||
let detect_source = source::detect::init_and_spawn().expect("failed to initialize detector module");
|
||||
let sources: Vec<&dyn Source> = vec![&detect_source];
|
||||
let funnel = funnel::default(&sources);
|
||||
|
||||
let matcher = matcher::rolling::RollingMatcherAdapter::new();
|
||||
let matcher = matcher::rolling::RollingMatcherAdapter::new(&match_converter.get_rolling_matches());
|
||||
let matchers: Vec<&dyn Matcher<matcher::MatcherState>> = vec![&matcher];
|
||||
let mut processor = process::default(&matchers);
|
||||
let selector = MatchSelectorAdapter::new();
|
||||
let mut processor = process::default(&matchers, &config_manager, &selector);
|
||||
|
||||
let text_injector = executor::text_injector::TextInjectorAdapter::new();
|
||||
let dispatcher = dispatch::default(&text_injector);
|
||||
|
|
20
espanso/src/cli/worker/ui/mod.rs
Normal file
20
espanso/src/cli/worker/ui/mod.rs
Normal file
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* This file is part of espanso.
|
||||
*
|
||||
* Copyright (C) 2019-2021 Federico Terzi
|
||||
*
|
||||
* espanso is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* espanso is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
pub mod selector;
|
37
espanso/src/cli/worker/ui/selector.rs
Normal file
37
espanso/src/cli/worker/ui/selector.rs
Normal file
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* This file is part of espanso.
|
||||
*
|
||||
* Copyright (C) 2019-2021 Federico Terzi
|
||||
*
|
||||
* espanso is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* espanso is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use crate::engine::process::MatchSelector;
|
||||
|
||||
pub struct MatchSelectorAdapter {
|
||||
// TODO: pass Modulo search UI manager
|
||||
}
|
||||
|
||||
impl MatchSelectorAdapter {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl MatchSelector for MatchSelectorAdapter {
|
||||
fn select(&self, matches_ids: &[i32]) -> Option<i32> {
|
||||
// TODO: replace with actual selection
|
||||
Some(*matches_ids.first().unwrap())
|
||||
}
|
||||
}
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
#[derive(Debug)]
|
||||
pub struct TextInjectEvent {
|
||||
pub delete_count: i32,
|
||||
pub text: String,
|
||||
pub mode: TextInjectMode,
|
||||
}
|
||||
|
|
|
@ -21,12 +21,17 @@ use std::collections::HashMap;
|
|||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct MatchesDetectedEvent {
|
||||
pub results: Vec<MatchResult>,
|
||||
pub matches: Vec<DetectedMatch>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct MatchResult {
|
||||
pub struct DetectedMatch {
|
||||
pub id: i32,
|
||||
pub trigger: String,
|
||||
pub vars: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct MatchSelectedEvent {
|
||||
pub chosen: DetectedMatch,
|
||||
}
|
|
@ -19,7 +19,7 @@
|
|||
|
||||
pub mod keyboard;
|
||||
pub mod inject;
|
||||
pub mod matches_detected;
|
||||
pub mod matches;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Event {
|
||||
|
@ -29,7 +29,8 @@ pub enum Event {
|
|||
Keyboard(keyboard::KeyboardEvent),
|
||||
|
||||
// Internal
|
||||
MatchesDetected(matches_detected::MatchesDetectedEvent),
|
||||
MatchesDetected(matches::MatchesDetectedEvent),
|
||||
MatchSelected(matches::MatchSelectedEvent),
|
||||
|
||||
// Effects
|
||||
TextInject(inject::TextInjectEvent),
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
|
||||
use log::trace;
|
||||
|
||||
use super::{Event, Matcher, Middleware, Processor, middleware::matcher::MatchMiddleware};
|
||||
use super::{Event, MatchFilter, MatchSelector, Matcher, Middleware, Processor, middleware::match_select::MatchSelectMiddleware, middleware::matcher::MatchMiddleware};
|
||||
use std::collections::VecDeque;
|
||||
|
||||
pub struct DefaultProcessor<'a> {
|
||||
|
@ -27,13 +27,18 @@ pub struct DefaultProcessor<'a> {
|
|||
middleware: Vec<Box<dyn Middleware + 'a>>,
|
||||
}
|
||||
|
||||
impl <'a> DefaultProcessor<'a> {
|
||||
pub fn new<MatcherState>(matchers: &'a [&'a dyn Matcher<'a, MatcherState>]) -> DefaultProcessor<'a> {
|
||||
impl<'a> DefaultProcessor<'a> {
|
||||
pub fn new<MatcherState>(
|
||||
matchers: &'a [&'a dyn Matcher<'a, MatcherState>],
|
||||
match_filter: &'a dyn MatchFilter,
|
||||
match_selector: &'a dyn MatchSelector,
|
||||
) -> DefaultProcessor<'a> {
|
||||
Self {
|
||||
event_queue: VecDeque::new(),
|
||||
middleware: vec![
|
||||
Box::new(MatchMiddleware::new(matchers)),
|
||||
]
|
||||
Box::new(MatchSelectMiddleware::new(match_filter, match_selector)),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -66,7 +71,7 @@ impl <'a> DefaultProcessor<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
impl <'a> Processor for DefaultProcessor<'a> {
|
||||
impl<'a> Processor for DefaultProcessor<'a> {
|
||||
fn process(&mut self, event: Event) -> Vec<Event> {
|
||||
self.event_queue.push_front(event);
|
||||
|
||||
|
|
93
espanso/src/engine/process/middleware/match_select.rs
Normal file
93
espanso/src/engine/process/middleware/match_select.rs
Normal file
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* This file is part of espanso.
|
||||
*
|
||||
* Copyright (C) 2019-2021 Federico Terzi
|
||||
*
|
||||
* espanso is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* espanso is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use log::{debug, error};
|
||||
|
||||
use super::super::Middleware;
|
||||
use crate::engine::{
|
||||
event::{
|
||||
matches::{MatchSelectedEvent},
|
||||
Event,
|
||||
},
|
||||
process::{MatchFilter, MatchSelector},
|
||||
};
|
||||
|
||||
pub struct MatchSelectMiddleware<'a> {
|
||||
match_filter: &'a dyn MatchFilter,
|
||||
match_selector: &'a dyn MatchSelector,
|
||||
}
|
||||
|
||||
impl<'a> MatchSelectMiddleware<'a> {
|
||||
pub fn new(match_filter: &'a dyn MatchFilter, match_selector: &'a dyn MatchSelector) -> Self {
|
||||
Self {
|
||||
match_filter,
|
||||
match_selector,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Middleware for MatchSelectMiddleware<'a> {
|
||||
fn next(&self, event: Event, _: &dyn FnMut(Event)) -> Event {
|
||||
if let Event::MatchesDetected(m_event) = event {
|
||||
let matches_ids: Vec<i32> = m_event.matches.iter().map(|m| m.id).collect();
|
||||
|
||||
// Find the matches that are actually valid in the current context
|
||||
let valid_ids = self.match_filter.filter_active(&matches_ids);
|
||||
|
||||
return match valid_ids.len() {
|
||||
0 => Event::NOOP, // No valid matches, consume the event
|
||||
1 => {
|
||||
// Only one match, no need to show a selection dialog
|
||||
let m = m_event
|
||||
.matches
|
||||
.into_iter()
|
||||
.find(|m| m.id == *valid_ids.first().unwrap());
|
||||
if let Some(m) = m {
|
||||
Event::MatchSelected(MatchSelectedEvent { chosen: m })
|
||||
} else {
|
||||
error!("MatchSelectMiddleware could not find the correspondent match");
|
||||
Event::NOOP
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Multiple matches, we need to ask the user which one to use
|
||||
if let Some(selected_id) = self.match_selector.select(&valid_ids) {
|
||||
let m = m_event
|
||||
.matches
|
||||
.into_iter()
|
||||
.find(|m| m.id == selected_id);
|
||||
if let Some(m) = m {
|
||||
Event::MatchSelected(MatchSelectedEvent { chosen: m })
|
||||
} else {
|
||||
error!("MatchSelectMiddleware could not find the correspondent match");
|
||||
Event::NOOP
|
||||
}
|
||||
} else {
|
||||
debug!("MatchSelectMiddleware did not receive any match selection");
|
||||
Event::NOOP
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
event
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: test
|
|
@ -21,7 +21,7 @@ use log::trace;
|
|||
use std::{cell::RefCell, collections::VecDeque};
|
||||
|
||||
use super::super::Middleware;
|
||||
use crate::engine::{event::{Event, keyboard::{Key, Status}, matches_detected::{MatchResult, MatchesDetectedEvent}}, process::{Matcher, MatcherEvent}};
|
||||
use crate::engine::{event::{Event, keyboard::{Key, Status}, matches::{DetectedMatch, MatchesDetectedEvent}}, process::{Matcher, MatcherEvent}};
|
||||
|
||||
const MAX_HISTORY: usize = 3; // TODO: get as parameter
|
||||
|
||||
|
@ -85,12 +85,10 @@ impl<'a, State> Middleware for MatchMiddleware<'a, State> {
|
|||
matcher_states.pop_front();
|
||||
}
|
||||
|
||||
println!("results: {:?}", all_results);
|
||||
|
||||
if !all_results.is_empty() {
|
||||
return Event::MatchesDetected(MatchesDetectedEvent {
|
||||
results: all_results.into_iter().map(|result | {
|
||||
MatchResult {
|
||||
matches: all_results.into_iter().map(|result | {
|
||||
DetectedMatch {
|
||||
id: result.id,
|
||||
trigger: result.trigger,
|
||||
vars: result.vars,
|
||||
|
@ -133,3 +131,5 @@ fn is_invalidating_key(key: &Key) -> bool {
|
|||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: test
|
|
@ -18,3 +18,4 @@
|
|||
*/
|
||||
|
||||
pub mod matcher;
|
||||
pub mod match_select;
|
|
@ -17,13 +17,12 @@
|
|||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::{Event, event::keyboard::Key};
|
||||
use super::{event::keyboard::Key, Event};
|
||||
|
||||
mod middleware;
|
||||
mod default;
|
||||
mod middleware;
|
||||
|
||||
pub trait Middleware {
|
||||
fn next(&self, event: Event, dispatch: &dyn FnMut(Event)) -> Event;
|
||||
|
@ -36,7 +35,11 @@ pub trait Processor {
|
|||
// Dependency inversion entities
|
||||
|
||||
pub trait Matcher<'a, State> {
|
||||
fn process(&'a self, prev_state: Option<&State>, event: &MatcherEvent) -> (State, Vec<MatchResult>);
|
||||
fn process(
|
||||
&'a self,
|
||||
prev_state: Option<&State>,
|
||||
event: &MatcherEvent,
|
||||
) -> (State, Vec<MatchResult>);
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -52,6 +55,18 @@ pub struct MatchResult {
|
|||
pub vars: HashMap<String, String>,
|
||||
}
|
||||
|
||||
pub fn default<'a, MatcherState>(matchers: &'a [&'a dyn Matcher<'a, MatcherState>]) -> impl Processor + 'a {
|
||||
default::DefaultProcessor::new(matchers)
|
||||
pub trait MatchFilter {
|
||||
fn filter_active(&self, matches_ids: &[i32]) -> Vec<i32>;
|
||||
}
|
||||
|
||||
pub trait MatchSelector {
|
||||
fn select(&self, matches_ids: &[i32]) -> Option<i32>;
|
||||
}
|
||||
|
||||
pub fn default<'a, MatcherState>(
|
||||
matchers: &'a [&'a dyn Matcher<'a, MatcherState>],
|
||||
match_filter: &'a dyn MatchFilter,
|
||||
match_selector: &'a dyn MatchSelector,
|
||||
) -> impl Processor + 'a {
|
||||
default::DefaultProcessor::new(matchers, match_filter, match_selector)
|
||||
}
|
|
@ -201,8 +201,12 @@ fn main() {
|
|||
let log_level = match matches.occurrences_of("v") {
|
||||
0 => LevelFilter::Warn,
|
||||
1 => LevelFilter::Info,
|
||||
2 => LevelFilter::Debug,
|
||||
_ => LevelFilter::Trace,
|
||||
|
||||
// Trace mode is only available in debug mode for security reasons
|
||||
#[cfg(debug_assertions)]
|
||||
3 => LevelFilter::Trace,
|
||||
|
||||
_ => LevelFilter::Debug,
|
||||
};
|
||||
|
||||
let handler = CLI_HANDLERS
|
||||
|
|
Loading…
Reference in New Issue
Block a user