From 1f0fe74ac10c2cb6c0203afa9efad3dd24eec257 Mon Sep 17 00:00:00 2001
From: Federico Terzi <federicoterzi96@gmail.com>
Date: Sun, 28 Mar 2021 18:22:50 +0200
Subject: [PATCH] feat(core): implement basic cli handler structure and path
 handler

---
 Cargo.lock                 |  76 ++++++++
 espanso/Cargo.toml         |   7 +-
 espanso/src/cli/mod.rs     |  65 +++++++
 espanso/src/cli/path.rs    |  59 ++++++
 espanso/src/logging/mod.rs |  89 +++++++++
 espanso/src/main.rs        | 388 ++++++++++++++++++++++++-------------
 espanso/src/main_old2.rs   |  75 -------
 7 files changed, 549 insertions(+), 210 deletions(-)
 create mode 100644 espanso/src/cli/mod.rs
 create mode 100644 espanso/src/cli/path.rs
 create mode 100644 espanso/src/logging/mod.rs
 delete mode 100644 espanso/src/main_old2.rs

diff --git a/Cargo.lock b/Cargo.lock
index 1c17b12..5fed2e9 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -9,6 +9,15 @@ dependencies = [
  "memchr",
 ]
 
+[[package]]
+name = "ansi_term"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
+dependencies = [
+ "winapi",
+]
+
 [[package]]
 name = "anyhow"
 version = "1.0.38"
@@ -27,6 +36,17 @@ version = "0.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
 
+[[package]]
+name = "atty"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "winapi",
+]
+
 [[package]]
 name = "autocfg"
 version = "1.0.1"
@@ -93,6 +113,21 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "clap"
+version = "2.33.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"
+dependencies = [
+ "ansi_term",
+ "atty",
+ "bitflags 1.2.1",
+ "strsim",
+ "textwrap",
+ "unicode-width",
+ "vec_map",
+]
+
 [[package]]
 name = "const_fn"
 version = "0.4.5"
@@ -250,6 +285,8 @@ dependencies = [
 name = "espanso"
 version = "1.0.0"
 dependencies = [
+ "anyhow",
+ "clap",
  "espanso-clipboard",
  "espanso-config",
  "espanso-detect",
@@ -258,8 +295,11 @@ dependencies = [
  "espanso-match",
  "espanso-path",
  "espanso-ui",
+ "lazy_static",
+ "log",
  "maplit",
  "simplelog",
+ "thiserror",
 ]
 
 [[package]]
@@ -448,6 +488,15 @@ dependencies = [
  "unicode-segmentation",
 ]
 
+[[package]]
+name = "hermit-abi"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "itertools"
 version = "0.10.0"
@@ -867,6 +916,12 @@ dependencies = [
  "termcolor",
 ]
 
+[[package]]
+name = "strsim"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
+
 [[package]]
 name = "strum"
 version = "0.8.0"
@@ -947,6 +1002,15 @@ dependencies = [
  "winapi-util",
 ]
 
+[[package]]
+name = "textwrap"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
+dependencies = [
+ "unicode-width",
+]
+
 [[package]]
 name = "thiserror"
 version = "1.0.23"
@@ -993,6 +1057,12 @@ version = "1.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796"
 
+[[package]]
+name = "unicode-width"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
+
 [[package]]
 name = "unicode-xid"
 version = "0.0.4"
@@ -1005,6 +1075,12 @@ version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
 
+[[package]]
+name = "vec_map"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
+
 [[package]]
 name = "version_check"
 version = "0.9.2"
diff --git a/espanso/Cargo.toml b/espanso/Cargo.toml
index 4843267..f6a2412 100644
--- a/espanso/Cargo.toml
+++ b/espanso/Cargo.toml
@@ -23,4 +23,9 @@ espanso-clipboard = { path = "../espanso-clipboard" }
 espanso-info = { path = "../espanso-info" } 
 espanso-path = { path = "../espanso-path" } 
 maplit = "1.0.2"
-simplelog = "0.9.0"
\ No newline at end of file
+simplelog = "0.9.0"
+log = "0.4.14"
+anyhow = "1.0.38"
+thiserror = "1.0.23"
+clap = "2.33.3"
+lazy_static = "1.4.0"
\ No newline at end of file
diff --git a/espanso/src/cli/mod.rs b/espanso/src/cli/mod.rs
new file mode 100644
index 0000000..df2cb10
--- /dev/null
+++ b/espanso/src/cli/mod.rs
@@ -0,0 +1,65 @@
+/*
+ * 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 clap::ArgMatches;
+use espanso_config::{config::ConfigStore, matches::store::MatchStore};
+use espanso_path::Paths;
+
+pub mod log;
+pub mod path;
+
+pub struct CliModule {
+  pub enable_logs: bool,
+  pub requires_paths: bool,
+  pub requires_config: bool,
+  pub subcommand: String,
+  pub entry: fn(CliModuleArgs),
+}
+
+impl Default for CliModule {
+  fn default() -> Self {
+    Self {
+      enable_logs: false,
+      requires_paths: false, 
+      requires_config: false, 
+      subcommand: "".to_string(), 
+      entry: |_| {},
+    }
+  }
+}
+
+pub struct CliModuleArgs {
+  pub config_store: Option<Box<dyn ConfigStore>>,
+  pub match_store: Option<Box<dyn MatchStore>>,
+  pub is_legacy_config: bool,
+  pub paths: Option<Paths>,
+  pub cli_args: Option<ArgMatches<'static>>,
+}
+
+impl Default for CliModuleArgs {
+  fn default() -> Self {
+    Self {
+      config_store: None,
+      match_store: None,
+      is_legacy_config: false,
+      paths: None,
+      cli_args: None,
+    }
+  }
+}
diff --git a/espanso/src/cli/path.rs b/espanso/src/cli/path.rs
new file mode 100644
index 0000000..e5a276b
--- /dev/null
+++ b/espanso/src/cli/path.rs
@@ -0,0 +1,59 @@
+/*
+ * 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 super::{CliModule, CliModuleArgs};
+
+pub fn new() -> CliModule {
+  CliModule {
+    requires_paths: true,
+    requires_config: true,
+    subcommand: "path".to_string(),
+    entry: path_main,
+    ..Default::default()
+  }
+}
+
+fn path_main(args: CliModuleArgs) {
+  let paths = args.paths.expect("missing paths argument");
+  let cli_args = args.cli_args.expect("missing cli_args argument");
+
+  if cli_args.subcommand_matches("config").is_some() {
+    println!("{}", paths.config.to_string_lossy());
+  } else if cli_args.subcommand_matches("packages").is_some() {
+    println!("{}", paths.packages.to_string_lossy());
+  } else if cli_args.subcommand_matches("data").is_some() || cli_args.subcommand_matches("runtime").is_some() {
+    println!("{}", paths.runtime.to_string_lossy());
+  } else if cli_args.subcommand_matches("default").is_some() {
+    if args.is_legacy_config {
+      println!("{}", paths.config.join("default.yml").to_string_lossy());
+    } else {
+      println!("{}", paths.config.join("config").join("default.yml").to_string_lossy());
+    }
+  } else if cli_args.subcommand_matches("base").is_some() {
+    if args.is_legacy_config {
+      eprintln!("base config not available when using legacy configuration format");
+    } else {
+      println!("{}", paths.config.join("match").join("base.yml").to_string_lossy());
+    }
+  } else {
+    println!("Config: {}", paths.config.to_string_lossy());
+    println!("Packages: {}", paths.packages.to_string_lossy());
+    println!("Runtime: {}", paths.runtime.to_string_lossy());
+  }
+}
diff --git a/espanso/src/logging/mod.rs b/espanso/src/logging/mod.rs
new file mode 100644
index 0000000..ff3c52d
--- /dev/null
+++ b/espanso/src/logging/mod.rs
@@ -0,0 +1,89 @@
+/*
+ * 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 anyhow::Result;
+use std::{
+  fs::{File, OpenOptions},
+  path::Path,
+};
+use std::{
+  io::Write,
+  sync::{Arc, Mutex},
+};
+
+/// This struct can be passed as an output to the logger to "defer" the
+/// decision of the output file
+#[derive(Clone)]
+pub(crate) struct FileProxy {
+  output: Arc<Mutex<Option<File>>>,
+}
+
+impl FileProxy {
+  pub fn new() -> Self {
+    Self {
+      output: Arc::new(Mutex::new(None)),
+    }
+  }
+
+  pub fn set_output_file(&self, path: &Path) -> Result<()> {
+    let log_file = OpenOptions::new()
+      .read(true)
+      .write(true)
+      .create(true)
+      .append(true)
+      .open(path)?;
+    let mut lock = self.output.lock().expect("unable to obtain FileProxy lock");
+    *lock = Some(log_file);
+    Ok(())
+  }
+}
+
+impl Write for FileProxy {
+  fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
+    match self.output.lock() {
+      Ok(lock) => {
+        if let Some(mut output) = lock.as_ref() {
+          output.write(buf)
+        } else {
+          Ok(0)
+        }
+      }
+      Err(_) => Err(std::io::Error::new(
+        std::io::ErrorKind::Other,
+        "lock poison error",
+      )),
+    }
+  }
+
+  fn flush(&mut self) -> std::io::Result<()> {
+    match self.output.lock() {
+      Ok(lock) => {
+        if let Some(mut output) = lock.as_ref() {
+          output.flush()
+        } else {
+          Ok(())
+        }
+      }
+      Err(_) => Err(std::io::Error::new(
+        std::io::ErrorKind::Other,
+        "lock poison error",
+      )),
+    }
+  }
+}
diff --git a/espanso/src/main.rs b/espanso/src/main.rs
index 7e8ccb9..cc5048c 100644
--- a/espanso/src/main.rs
+++ b/espanso/src/main.rs
@@ -1,144 +1,264 @@
-use std::{path::PathBuf, time::Duration};
+/*
+ * 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_detect::{
-  event::{InputEvent, Status},
-  get_source,
-  hotkey::HotKey,
-  SourceCreationOptions,
-};
-use espanso_inject::{get_injector, keys, Injector};
-use espanso_ui::{event::UIEvent::*, icons::TrayIcon, menu::*};
-use simplelog::{CombinedLogger, Config, LevelFilter, TermLogger, TerminalMode};
+#[macro_use]
+extern crate lazy_static;
+
+use clap::{App, AppSettings, Arg, SubCommand};
+use cli::{CliModule, CliModuleArgs};
+use logging::FileProxy;
+use simplelog::{CombinedLogger, Config, LevelFilter, TermLogger, TerminalMode, WriteLogger};
+
+mod cli;
+mod engine;
+mod logging;
+mod util;
+
+const VERSION: &str = env!("CARGO_PKG_VERSION");
+const LOG_FILE_NAME: &str = "espanso.log";
+
+lazy_static! {
+  static ref CLI_HANDLERS: Vec<CliModule> = vec![
+    cli::path::new(),
+  ];
+}
 
 fn main() {
-  println!("Hello, world!z");
+  // TODO: attach console
 
-  CombinedLogger::init(vec![
-    TermLogger::new(LevelFilter::Debug, Config::default(), TerminalMode::Mixed),
-    // WriteLogger::new(
-    //   LevelFilter::Info,
-    //   Config::default(),
-    //   File::create("my_rust_binary.log").unwrap(),
-    // ),
-  ])
-  .unwrap();
-  
-  let paths = espanso_path::resolve_paths();
-  println!("paths: {:?}", paths);
-  let config = espanso_config::load_legacy(&paths.config, &paths.packages).unwrap();
+  let install_subcommand = SubCommand::with_name("install")
+    .about("Install a package. Equivalent to 'espanso package install'")
+    .arg(
+      Arg::with_name("external")
+        .short("e")
+        .long("external")
+        .required(false)
+        .takes_value(false)
+        .help("Allow installing packages from non-verified repositories."),
+    )
+    .arg(Arg::with_name("package_name").help("Package name"))
+    .arg(
+      Arg::with_name("repository_url")
+        .help("(Optional) Link to GitHub repository")
+        .required(false)
+        .default_value("hub"),
+    )
+    .arg(
+      Arg::with_name("proxy")
+        .help("Use a proxy, should be used as --proxy=https://proxy:1234")
+        .required(false)
+        .long("proxy")
+        .takes_value(true),
+    );
 
-  // let icon_paths = vec![
-  //   (
-  //     espanso_ui::icons::TrayIcon::Normal,
-  //     r"C:\Users\Freddy\AppData\Local\espanso\espanso.ico".to_string(),
-  //   ),
-  //   (
-  //     espanso_ui::icons::TrayIcon::Disabled,
-  //     r"C:\Users\Freddy\AppData\Local\espanso\espansored.ico".to_string(),
-  //   ),
-  // ];
-  let icon_paths = vec![
-    (
-      espanso_ui::icons::TrayIcon::Normal,
-      r"/Users/freddy/Library/Application Support/espanso/icon.png".to_string(),
-    ),
-    (
-      espanso_ui::icons::TrayIcon::Disabled,
-      r"/Users/freddy/Library/Application Support/espanso/icondisabled.png".to_string(),
-    ),
-  ];
+  let uninstall_subcommand = SubCommand::with_name("uninstall")
+    .about("Remove an installed package. Equivalent to 'espanso package uninstall'")
+    .arg(Arg::with_name("package_name").help("Package name"));
 
-  // let (remote, mut eventloop) = espanso_ui::win32::create(espanso_ui::win32::Win32UIOptions {
-  //   show_icon: true,
-  //   icon_paths: &icon_paths,
-  //   notification_icon_path: r"C:\Users\Freddy\Insync\Development\Espanso\Images\icongreensmall.png"
-  //     .to_string(),
-  // });
-  // let (remote, mut eventloop) = espanso_ui::mac::create(espanso_ui::mac::MacUIOptions {
-  //   show_icon: true,
-  //   icon_paths: &icon_paths,
-  // });
-  let (remote, mut eventloop) = espanso_ui::create_ui(espanso_ui::UIOptions {
-    notification_icon_path: Some(
-      r"C:\Users\Freddy\Insync\Development\Espanso\Images\icongreensmall.png".to_string(),
-    ),
-    ..Default::default()
-  })
-  .unwrap();
+  let mut clap_instance = App::new("espanso")
+        .version(VERSION)
+        .author("Federico Terzi")
+        .about("A Privacy-first, Cross-platform Text Expander")
+        .arg(Arg::with_name("v")
+            .short("v")
+            .multiple(true)
+            .help("Sets the level of verbosity"))
+        .subcommand(SubCommand::with_name("cmd")
+            .about("Send a command to the espanso daemon.")
+            .subcommand(SubCommand::with_name("exit")
+                .about("Terminate the daemon."))
+            .subcommand(SubCommand::with_name("enable")
+                .about("Enable the espanso replacement engine."))
+            .subcommand(SubCommand::with_name("disable")
+                .about("Disable the espanso replacement engine."))
+            .subcommand(SubCommand::with_name("toggle")
+                .about("Toggle the status of the espanso replacement engine."))
+        )
+        .subcommand(SubCommand::with_name("edit")
+            .about("Open the default text editor to edit config files and reload them automatically when exiting")
+            .arg(Arg::with_name("config")
+                .help("Defaults to \"default\". The configuration file name to edit (without the .yml extension)."))
+            .arg(Arg::with_name("norestart")
+                .short("n")
+                .long("norestart")
+                .required(false)
+                .takes_value(false)
+                .help("Avoid restarting espanso after editing the file"))
+        )
+        .subcommand(SubCommand::with_name("detect")
+            .about("Tool to detect current window properties, to simplify filters creation."))
+        .subcommand(SubCommand::with_name("daemon")
+            .about("Start the daemon without spawning a new process."))
+        .subcommand(SubCommand::with_name("register")
+            .about("MacOS and Linux only. Register espanso in the system daemon manager."))
+        .subcommand(SubCommand::with_name("unregister")
+            .about("MacOS and Linux only. Unregister espanso from the system daemon manager."))
+        .subcommand(SubCommand::with_name("log")
+            .about("Print the latest daemon logs."))
+        .subcommand(SubCommand::with_name("start")
+            .about("Start the daemon spawning a new process in the background."))
+        .subcommand(SubCommand::with_name("stop")
+            .about("Stop the espanso daemon."))
+        .subcommand(SubCommand::with_name("restart")
+            .about("Restart the espanso daemon."))
+        .subcommand(SubCommand::with_name("status")
+            .about("Check if the espanso daemon is running or not."))
+        .subcommand(SubCommand::with_name("path")
+            .about("Prints all the espanso directory paths to easily locate configuration and matches.")
+            .subcommand(SubCommand::with_name("config")
+                .about("Print the current config folder path."))
+            .subcommand(SubCommand::with_name("packages")
+                .about("Print the current packages folder path."))
+            .subcommand(SubCommand::with_name("data")
+                .about("Print the current data folder path.")
+                .setting(AppSettings::Hidden))  // Legacy path
+            .subcommand(SubCommand::with_name("runtime")
+                .about("Print the current runtime folder path."))
+            .subcommand(SubCommand::with_name("default")
+                .about("Print the default configuration file path."))
+            .subcommand(SubCommand::with_name("base")
+                .about("Print the default match file path."))
+        )
+        .subcommand(SubCommand::with_name("match")
+            .about("List and execute matches from the CLI")
+            .subcommand(SubCommand::with_name("list")
+                .about("Print all matches to standard output")
+                .arg(Arg::with_name("json")
+                    .short("j")
+                    .long("json")
+                    .help("Return the matches as json")
+                    .required(false)
+                    .takes_value(false)
+                )
+                .arg(Arg::with_name("onlytriggers")
+                    .short("t")
+                    .long("onlytriggers")
+                    .help("Print only triggers without replacement")
+                    .required(false)
+                    .takes_value(false)
+                )
+                .arg(Arg::with_name("preservenewlines")
+                    .short("n")
+                    .long("preservenewlines")
+                    .help("Preserve newlines when printing replacements")
+                    .required(false)
+                    .takes_value(false)
+                )
+            )
+            .subcommand(SubCommand::with_name("exec")
+                .about("Triggers the expansion of the given match")
+                .arg(Arg::with_name("trigger")
+                    .help("The trigger of the match to be expanded")
+                )
+            )
+        )
+        // Package manager
+        .subcommand(SubCommand::with_name("package")
+            .about("Espanso package manager commands")
+            .subcommand(install_subcommand.clone())
+            .subcommand(uninstall_subcommand.clone())
+            .subcommand(SubCommand::with_name("list")
+                .about("List all installed packages")
+                .arg(Arg::with_name("full")
+                    .help("Print all package info")
+                    .long("full")))
 
-  eventloop.initialize().unwrap();
+            .subcommand(SubCommand::with_name("refresh")
+                .about("Update espanso package index"))
+        )
+        .subcommand(SubCommand::with_name("worker")
+            .setting(AppSettings::Hidden)
+            .arg(Arg::with_name("reload")
+                .short("r")
+                .long("reload")
+                .required(false)
+                .takes_value(false))
+        )
+        .subcommand(install_subcommand)
+        .subcommand(uninstall_subcommand);
 
-  let handle = std::thread::spawn(move || {
-    let injector = get_injector(Default::default()).unwrap();
-    let mut source = get_source(SourceCreationOptions {
-      //use_evdev: true,
-      hotkeys: vec![
-        HotKey::new(1, "CTRL+SPACE").unwrap(),
-        //HotKey::new(1, "OPTION+SPACE").unwrap(),
-        HotKey::new(2, "CTRL+OPTION+3").unwrap(),
-      ],
-      ..Default::default()
-    })
-    .unwrap();
-    let clipboard = espanso_clipboard::get_clipboard(Default::default()).unwrap();
-    let provider = espanso_info::get_provider().unwrap();
-    source.initialize().unwrap();
-    source
-      .eventloop(Box::new(move |event: InputEvent| {
-        println!("ev {:?}", event);
-        match event {
-          InputEvent::Mouse(_) => {}
-          InputEvent::Keyboard(evt) => {
-            if evt.key == espanso_detect::event::Key::Escape && evt.status == Status::Released {
-              //remote.update_tray_icon(espanso_ui::icons::TrayIcon::Disabled);
-              //remote.show_notification("Espanso is running!");
-              injector
-                .send_string("Hey guys! @", Default::default())
-                .expect("error");
-              //std::thread::sleep(std::time::Duration::from_secs(2));
-              //injector.send_key_combination(&[keys::Key::Control, keys::Key::V], Default::default()).unwrap();
-            }
-            if evt.key == espanso_detect::event::Key::Shift && evt.status == Status::Released {
-              println!("info {:?}", provider.get_info());
-            }
-          }
-          InputEvent::HotKey(hotkey) => {
-            if hotkey.hotkey_id == 2 {
-              println!("clip {:?}", clipboard.get_text());
-            } else if hotkey.hotkey_id == 1 {
-              //clipboard.set_text("test text").unwrap();
-              clipboard.set_html("<i>test text</i>", Some("test text fallback")).unwrap();
-              //clipboard.set_image(&PathBuf::from(r"C:\Users\Freddy\Insync\Development\Espanso\Images\icongreen.png")).unwrap();
-            }
-          }
-        }
-      }))
-      .unwrap();
-  });
+  // TODO: explain that the start and restart commands are only meaningful
+  // when using the system daemon manager on macOS and Linux
 
-  eventloop.run(Box::new(move |event| {
-    println!("ui {:?}", event);
-    let menu = Menu::from(vec![
-      MenuItem::Simple(SimpleMenuItem::new("open", "Open")),
-      MenuItem::Separator,
-      MenuItem::Sub(SubMenuItem::new(
-        "Sub",
-        vec![
-          MenuItem::Simple(SimpleMenuItem::new("sub1", "Sub 1")),
-          MenuItem::Simple(SimpleMenuItem::new("sub2", "Sub 2")),
-        ],
-      )),
-    ])
-    .unwrap();
-    match event {
-      TrayIconClick => {
-        remote.show_context_menu(&menu);
-      }
-      ContextMenuClick(raw_id) => {
-        //remote.update_tray_icon(TrayIcon::Disabled);
-        remote.show_notification("Hello there!");
-        println!("item {:?}", menu.get_item_id(raw_id));
-      }
+  let matches = clap_instance.clone().get_matches();
+  let log_level = match matches.occurrences_of("v") {
+    0 => LevelFilter::Warn,
+    1 => LevelFilter::Info,
+    _ => LevelFilter::Debug,
+  };
+
+  let handler = CLI_HANDLERS
+    .iter()
+    .find(|cli| matches.subcommand_matches(&cli.subcommand).is_some());
+
+  if let Some(handler) = handler {
+    let log_proxy = FileProxy::new();
+    if handler.enable_logs {
+      CombinedLogger::init(vec![
+        TermLogger::new(log_level, Config::default(), TerminalMode::Mixed),
+        WriteLogger::new(log_level, Config::default(), log_proxy.clone()),
+      ])
+      .expect("unable to initialize logs");
     }
-  }));
-}
+
+    let mut cli_args: CliModuleArgs = CliModuleArgs::default();
+
+    if handler.requires_paths || handler.requires_config {
+      // TODO: here take into account env variable and/or command line flag
+      let paths = espanso_path::resolve_paths();
+
+      if handler.requires_config {
+        let (config_store, match_store, is_legacy_config) =
+          if espanso_config::is_legacy_config(&paths.config) {
+            let (config_store, match_store) =
+              espanso_config::load_legacy(&paths.config, &paths.packages)
+                .expect("unable to load legacy config");
+            (config_store, match_store, true)
+          } else {
+            let (config_store, match_store) =
+              espanso_config::load(&paths.config).expect("unable to load config");
+            (config_store, match_store, false)
+          };
+
+        cli_args.is_legacy_config = is_legacy_config;
+        cli_args.config_store = Some(config_store);
+        cli_args.match_store = Some(match_store);
+      }
+
+      if handler.enable_logs {
+        log_proxy
+          .set_output_file(&paths.runtime.join(LOG_FILE_NAME))
+          .expect("unable to set up log output file");
+      }
+
+      cli_args.paths = Some(paths);
+    }
+
+    if let Some(args) = matches.subcommand_matches(&handler.subcommand) {
+      cli_args.cli_args = Some(args.clone());
+    }
+
+    (handler.entry)(cli_args)
+  } else {
+    clap_instance
+      .print_long_help()
+      .expect("unable to print help");
+    println!();
+  }
+}
\ No newline at end of file
diff --git a/espanso/src/main_old2.rs b/espanso/src/main_old2.rs
deleted file mode 100644
index e51724e..0000000
--- a/espanso/src/main_old2.rs
+++ /dev/null
@@ -1,75 +0,0 @@
-use espanso_detect::event::{InputEvent, Status};
-use espanso_ui::{linux::LinuxUIOptions, menu::*};
-use simplelog::{CombinedLogger, Config, LevelFilter, TermLogger, TerminalMode};
-
-fn main() {
-  println!("Hello, world!z");
-  CombinedLogger::init(vec![
-    TermLogger::new(LevelFilter::Debug, Config::default(), TerminalMode::Mixed),
-    // WriteLogger::new(
-    //   LevelFilter::Info,
-    //   Config::default(),
-    //   File::create("my_rust_binary.log").unwrap(),
-    // ),
-  ])
-  .unwrap();
-
-  let icon_paths = vec![
-    (
-      espanso_ui::icons::TrayIcon::Normal,
-      r"C:\Users\Freddy\AppData\Local\espanso\espanso.ico".to_string(),
-    ),
-    (
-      espanso_ui::icons::TrayIcon::Disabled,
-      r"C:\Users\Freddy\AppData\Local\espanso\espansored.ico".to_string(),
-    ),
-  ];
-
- 
-  // let (remote, mut eventloop) = espanso_ui::win32::create(espanso_ui::win32::Win32UIOptions {
-  //   show_icon: true,
-  //   icon_paths: &icon_paths,
-  //   notification_icon_path: r"C:\Users\Freddy\Insync\Development\Espanso\Images\icongreensmall.png"
-  //     .to_string(),
-  // });
-  let (remote, mut eventloop) = espanso_ui::linux::create(LinuxUIOptions {
-    notification_icon_path: r"/home/freddy/insync/Development/Espanso/Images/icongreensmall.png".to_owned(),
-  });
-
-  let handle = std::thread::spawn(move || {
-    //let mut source = espanso_detect::win32::Win32Source::new();
-    let mut source = espanso_detect::evdev::EVDEVSource::new();
-    source.initialize();
-    source.eventloop(Box::new(move |event: InputEvent| {
-      println!("ev {:?}", event);
-      match event {
-        InputEvent::Mouse(_) => {}
-        InputEvent::Keyboard(evt) => {
-          if evt.key == espanso_detect::event::Key::Shift && evt.status == Status::Pressed {
-            //remote.update_tray_icon(espanso_ui::icons::TrayIcon::Disabled);
-            remote.show_notification("Espanso is running!");
-          }
-        }
-      }
-    }));
-  });
-
-  // eventloop.initialize();
-  // eventloop.run(Box::new(move |event| {
-  //   println!("ui {:?}", event);
-  //   let menu = Menu::from(vec![
-  //     MenuItem::Simple(SimpleMenuItem::new("open", "Open")),
-  //     MenuItem::Separator,
-  //     MenuItem::Sub(SubMenuItem::new(
-  //       "Sub",
-  //       vec![
-  //         MenuItem::Simple(SimpleMenuItem::new("sub1", "Sub 1")),
-  //         MenuItem::Simple(SimpleMenuItem::new("sub2", "Sub 2")),
-  //       ],
-  //     )),
-  //   ])
-  //   .unwrap();
-  //   remote.show_context_menu(&menu);
-  // }))
-  eventloop.run();
-}