diff --git a/Cargo.lock b/Cargo.lock index 7681e85..67846de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -99,7 +99,7 @@ dependencies = [ "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows-core", + "windows-core 0.52.0", ] [[package]] @@ -123,9 +123,9 @@ dependencies = [ [[package]] name = "itertools" -version = "0.10.5" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" dependencies = [ "either", ] @@ -142,7 +142,7 @@ dependencies = [ [[package]] name = "laika" -version = "0.1.0" +version = "0.1.1" dependencies = [ "anyhow", "chrono", @@ -266,23 +266,22 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.28.4" +version = "0.33.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4c2f3ca6693feb29a89724516f016488e9aafc7f37264f898593ee4b942f31b" +checksum = "4fc858248ea01b66f19d8e8a6d55f41deaf91e9d495246fd01368d99935c6c01" dependencies = [ - "cfg-if", "core-foundation-sys", "libc", + "memchr", "ntapi", - "once_cell", - "winapi", + "windows", ] [[package]] name = "toml" -version = "0.7.8" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" dependencies = [ "serde", "serde_spanned", @@ -301,9 +300,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.19.15" +version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ "indexmap", "serde", @@ -398,6 +397,16 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +dependencies = [ + "windows-core 0.57.0", + "windows-targets", +] + [[package]] name = "windows-core" version = "0.52.0" @@ -407,6 +416,49 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "windows-core" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-result", + "windows-targets", +] + +[[package]] +name = "windows-implement" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -473,9 +525,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.5.40" +version = "0.6.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +checksum = "ad699df48212c6cc6eb4435f35500ac6fd3b9913324f938aea302022ce19d310" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index 153b14d..1cd818f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "laika" -version = "0.1.0" +version = "0.1.1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -8,10 +8,10 @@ edition = "2021" [dependencies] anyhow = "1.0.95" chrono = "0.4.24" -itertools = "0.10.5" +itertools = "0.14.0" serde = { version = "1.0.160", features = ["derive"] } -sysinfo = { version = "0.28.4", default-features = false } -toml = "0.7.3" +sysinfo = { version = "0.33.1", default-features = false, features = ["system"]} +toml = "0.8.19" [profile.release] opt-level = "s" diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..f28c2d8 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,127 @@ +use crate::datetime::{LocalDate, TimeHM, Weekdays}; +use serde::Deserialize; +use std::path::Path; +use std::time::SystemTime; + +use anyhow::{anyhow, Result}; +use std::{env, fs, path::PathBuf}; + +/// Application config +#[derive(Deserialize, Default, Debug)] +pub(crate) struct Config { + /// modification time + #[serde(skip)] + pub(crate) modified: Option, + /// is application active + #[serde(default = "default_true")] + pub(crate) active: bool, + /// need to exit + #[serde(default)] + pub(crate) exit_now: bool, + /// target command + pub(crate) command: String, + /// target process name + #[serde(default)] + pub(crate) target: String, + /// workdir for launch target command + #[serde(default)] + pub(crate) workdir: String, + /// args for launch target command + #[serde(default)] + pub(crate) args: Option>, + /// weekdays (Mon,Tue,Wed | Пн,Вт,Ср) + pub(crate) weekdays: Weekdays, + /// exclude dates + pub(crate) exclude_dates: Vec, + /// include dates + pub(crate) include_dates: Vec, + /// start time + pub(crate) start: TimeHM, + /// end time + pub(crate) end: TimeHM, + /// check delay (seconds) + pub(crate) delay: u32, +} + +fn default_true() -> bool { + true +} + +impl Config { + pub(crate) fn read>(path: P) -> Result { + let data = fs::read_to_string(path).map_err(|e| anyhow!("can't read config: {:?}", e))?; + toml::from_str(&data).map_err(|e| anyhow!("can't parse config: {:?}", e)) + } + pub(crate) fn file_location() -> Result { + let res = env::current_exe() + .map_err(|e| anyhow!("can't get current exe path: {:?}", e))? + .with_extension("toml"); + Ok(res) + } + pub(crate) fn get() -> Self { + let path = Config::file_location(); + if let Err(_e) = path { + //println!("{}", _e); + return Config::default(); + } + + let path = path.unwrap(); + let cfg = Config::read(&path); + match cfg { + Err(_e) => { + //println!("{}", _e); + Config::default() + } + Ok(mut cfg) => { + let metadata = fs::metadata(&path); + if let Ok(meta) = metadata { + if let Ok(time) = meta.modified() { + cfg.modified = Some(time); + } + } + cfg + } + } + } + pub(crate) fn is_file_modified(&self) -> bool { + if self.modified.is_none() { + return true; + } + let path = Config::file_location(); + if let Err(_e) = path { + return false; + } + let path = path.unwrap(); + let metadata = fs::metadata(path); + let mut result = false; + if let Ok(meta) = metadata { + if let Ok(time) = meta.modified() { + result = self.modified.unwrap() < time + } + } + result + } + pub(crate) fn reload(&mut self) { + if !self.is_file_modified() { + return; + } + + let new_cfg = Self::get(); + *self = new_cfg; + } + pub(crate) fn target_name(&self) -> String { + if !self.target.is_empty() { + self.target.clone() + } else { + PathBuf::from(self.command.clone()) + .file_name() + .unwrap() + .to_os_string() + .into_string() + .unwrap() + } + } + pub(crate) fn is_valid(&self) -> bool { + (!self.command.is_empty()) && self.delay > 0 + } +} diff --git a/src/datetime.rs b/src/datetime.rs new file mode 100644 index 0000000..95cc26e --- /dev/null +++ b/src/datetime.rs @@ -0,0 +1,184 @@ +use chrono::prelude::*; +use chrono::{DateTime, Local, TimeZone}; +use itertools::Itertools; +use serde::Deserialize; +use std::fmt::Display; +use std::str::FromStr; + +use anyhow::{anyhow, Result}; + +const WEEKDAYS_STR_EN: [&str; 7] = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]; +const WEEKDAYS_STR_RU: [&str; 7] = ["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"]; + +/// Time with hours (24) and minutes (60) +#[derive(Deserialize, Default, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Debug)] +#[serde(try_from = "String")] +pub(crate) struct TimeHM { + /// hour num (0..23) + hour: u8, + /// minute num (0..59) + minute: u8, +} + +impl From> for TimeHM { + fn from(value: DateTime) -> TimeHM { + TimeHM { + hour: value.hour() as u8, + minute: value.minute() as u8, + } + } +} + +impl FromStr for TimeHM { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + if let Some((str_hour, str_minute)) = s.split(':').collect_tuple() { + let hour: u8 = str_hour.parse()?; + let minute: u8 = str_minute.parse()?; + Ok(TimeHM { hour, minute }) + } else { + Err(anyhow!("invalid time, must be hh:mm")) + } + } +} + +impl TryFrom for TimeHM { + type Error = anyhow::Error; + + fn try_from(value: String) -> Result { + value.parse() + } +} + +#[derive(Deserialize, Default, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Debug)] +#[serde(try_from = "String")] +pub(crate) struct Weekdays { + /// bits of weekdays + bits: u8, +} + +impl FromStr for Weekdays { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let mut bits: u8 = 0b10000000; + + let mut has_invalid = false; + let mut has_valid = false; + + for &weekdays_str in [&WEEKDAYS_STR_EN, &WEEKDAYS_STR_RU] { + let found = s.split(',').map(|p| { + weekdays_str + .iter() + .find_position(|&&x| x.to_lowercase() == p.trim().to_lowercase()) + }); + + for item in found { + if let Some((pos, _)) = item { + bits |= 1 << pos; + has_valid = true; + } else { + has_invalid = true; + } + } + } + + if has_valid || !has_invalid { + Ok(Weekdays { bits }) + } else { + Err(anyhow!("invalid time, must be hh:mm")) + } + } +} + +impl TryFrom for Weekdays { + type Error = anyhow::Error; + + fn try_from(value: String) -> Result { + value.parse() + } +} + +impl Weekdays { + pub(crate) fn is_weekday_enabled(&self, day: u8) -> bool { + (self.bits >> day) & 1 == 1 + } + + pub(crate) fn is_today_enabled(&self) -> bool { + let now = Local::now(); + let day = now.weekday().num_days_from_monday(); + self.is_weekday_enabled(day as u8) + } +} + +impl Display for Weekdays { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut res = String::new(); + for (i, item) in WEEKDAYS_STR_EN.iter().enumerate() { + if self.is_weekday_enabled(i as u8) { + res.push_str(&format!("{},", item)); + } + } + res.pop(); + write!(f, "{}", res) + } +} + +#[derive(Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone, Debug)] +#[serde(try_from = "String")] +pub(crate) struct LocalDate { + inner: NaiveDate, +} + +impl Default for LocalDate { + fn default() -> Self { + LocalDate { + inner: Local::now().date_naive(), + } + } +} + +impl Display for LocalDate { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.inner.format("%d.%m.%Y")) + } +} + +impl FromStr for LocalDate { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let date = NaiveDateTime::parse_from_str(s, "%d.%m.%Y")?; + Ok(LocalDate { inner: date.date() }) + } +} + +impl TryFrom for LocalDate { + type Error = anyhow::Error; + + fn try_from(value: String) -> Result { + value.parse() + } +} + +impl LocalDate { + pub(crate) fn is_date(&self, value: NaiveDate) -> bool { + self.inner == value + } + + pub(crate) fn is_date_of(&self, value: DateTime) -> bool { + self.is_date(value.date_naive()) + } +} + +pub(crate) trait HasToday { + fn has_today(&self) -> bool; +} + +impl HasToday for Vec { + fn has_today(&self) -> bool { + let now = Local::now(); + self.iter().any(|x| x.is_date_of(now)) + } +} diff --git a/src/main.rs b/src/main.rs index c94ecb4..45f85f8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,172 +1,13 @@ #![windows_subsystem = "windows"] -use chrono::prelude::*; -use chrono::{DateTime, Local, TimeZone}; -use itertools::Itertools; -use serde::Deserialize; -use std::path::Path; -use std::str::FromStr; -use std::time::SystemTime; -use std::{thread, time::Duration}; -use sysinfo::{System, SystemExt}; +use chrono::{DateTime, Local}; +use config::Config; +use datetime::{HasToday, TimeHM}; +use std::{process::Command, thread, time::Duration}; +use sysinfo::{ProcessRefreshKind, RefreshKind, System, UpdateKind}; -use anyhow::{anyhow, Result}; -use std::{env, fs, path::PathBuf, process::Command}; - -/// Time with hours (24) and minutes (60) -#[derive(Deserialize, Default, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Debug)] -#[serde(try_from = "String")] -struct TimeHM { - /// hour num (0..23) - hour: u8, - /// minute num (0..59) - minute: u8, -} - -impl From> for TimeHM { - fn from(value: DateTime) -> TimeHM { - TimeHM { - hour: value.hour() as u8, - minute: value.minute() as u8, - } - } -} - -impl FromStr for TimeHM { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - if let Some((str_hour, str_minute)) = s.split(':').collect_tuple() { - let hour: u8 = str_hour.parse()?; - let minute: u8 = str_minute.parse()?; - Ok(TimeHM { hour, minute }) - } else { - Err(anyhow!("invalid time, must be hh:mm")) - } - } -} - -impl TryFrom for TimeHM { - type Error = anyhow::Error; - - fn try_from(value: String) -> Result { - value.parse() - } -} - -/// Application config -#[derive(Deserialize, Default, Debug)] -struct Config { - /// modification time - #[serde(skip)] - modified: Option, - /// is application active - #[serde(default = "default_true")] - active: bool, - /// need to exit - #[serde(default)] - exit_now: bool, - /// target command - command: String, - /// target process name - #[serde(default)] - target: String, - /// workdir for launch target command - #[serde(default)] - workdir: String, - /// args for launch target command - #[serde(default)] - args: Option>, - /// start time - start: TimeHM, - /// end time - end: TimeHM, - /// check delay (seconds) - delay: u32, -} - -fn default_true() -> bool { - true -} - -impl Config { - fn read>(path: P) -> Result { - let data = fs::read_to_string(path).map_err(|e| anyhow!("can't read config: {:?}", e))?; - toml::from_str(&data).map_err(|e| anyhow!("can't parse config: {:?}", e)) - } - fn file_location() -> Result { - let res = env::current_exe() - .map_err(|e| anyhow!("can't get current exe path: {:?}", e))? - .with_extension("toml"); - Ok(res) - } - fn get() -> Self { - let path = Config::file_location(); - if let Err(_e) = path { - //println!("{}", _e); - return Config::default(); - } - - let path = path.unwrap(); - let cfg = Config::read(&path); - match cfg { - Err(_e) => { - //println!("{}", _e); - Config::default() - } - Ok(mut cfg) => { - let metadata = fs::metadata(&path); - if let Ok(meta) = metadata { - if let Ok(time) = meta.modified() { - cfg.modified = Some(time); - } - } - cfg - } - } - } - fn is_file_modified(&self) -> bool { - if self.modified.is_none() { - return true; - } - let path = Config::file_location(); - if let Err(_e) = path { - return false; - } - let path = path.unwrap(); - let metadata = fs::metadata(path); - let mut result = false; - if let Ok(meta) = metadata { - if let Ok(time) = meta.modified() { - result = self.modified.unwrap() < time - } - } - result - } - fn reload(&mut self) { - if !self.is_file_modified() { - return; - } - - let new_cfg = Self::get(); - *self = new_cfg; - } - fn target_name(&self) -> String { - if !self.target.is_empty() { - self.target.clone() - } else { - PathBuf::from(self.command.clone()) - .file_name() - .unwrap() - .to_os_string() - .into_string() - .unwrap() - } - } - fn is_valid(&self) -> bool { - (!self.command.is_empty()) && self.delay > 0 - } -} +mod config; +mod datetime; struct Laika { config: Config, @@ -179,9 +20,15 @@ impl From for Laika { } impl Laika { - fn is_in_time(&self, value: TimeHM) -> bool { - self.config.start <= value && value <= self.config.end + fn is_today_enabled(&self) -> bool { + (self.config.weekdays.is_today_enabled() && !self.config.exclude_dates.has_today()) + || self.config.include_dates.has_today() } + + fn is_in_time(&self, value: TimeHM) -> bool { + self.is_today_enabled() && self.config.start <= value && value <= self.config.end + } + fn is_active(&self) -> bool { let now: DateTime = Local::now(); self.config.active && self.config.is_valid() && self.is_in_time(now.into()) @@ -194,8 +41,11 @@ impl Laika { if name.is_empty() { return false; } - let sp = System::new_all(); - let procs = sp.processes_by_exact_name(&name); + let sp = System::new_with_specifics( + RefreshKind::nothing() + .with_processes(ProcessRefreshKind::nothing().with_exe(UpdateKind::OnlyIfNotSet)), + ); + let procs = sp.processes_by_exact_name(name.as_ref()); procs.count() > 0 } fn relaunch_target(&self) {