+ weekdays + includes/excludes + upd deps

This commit is contained in:
Dmitry Belyaev 2025-01-28 11:09:57 +03:00
parent 529ca478b1
commit 4ced16e8da
Signed by: b4tman
GPG Key ID: 41A00BF15EA7E5F3
5 changed files with 402 additions and 189 deletions

82
Cargo.lock generated
View File

@ -99,7 +99,7 @@ dependencies = [
"iana-time-zone-haiku", "iana-time-zone-haiku",
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",
"windows-core", "windows-core 0.52.0",
] ]
[[package]] [[package]]
@ -123,9 +123,9 @@ dependencies = [
[[package]] [[package]]
name = "itertools" name = "itertools"
version = "0.10.5" version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
dependencies = [ dependencies = [
"either", "either",
] ]
@ -142,7 +142,7 @@ dependencies = [
[[package]] [[package]]
name = "laika" name = "laika"
version = "0.1.0" version = "0.1.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@ -266,23 +266,22 @@ dependencies = [
[[package]] [[package]]
name = "sysinfo" name = "sysinfo"
version = "0.28.4" version = "0.33.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4c2f3ca6693feb29a89724516f016488e9aafc7f37264f898593ee4b942f31b" checksum = "4fc858248ea01b66f19d8e8a6d55f41deaf91e9d495246fd01368d99935c6c01"
dependencies = [ dependencies = [
"cfg-if",
"core-foundation-sys", "core-foundation-sys",
"libc", "libc",
"memchr",
"ntapi", "ntapi",
"once_cell", "windows",
"winapi",
] ]
[[package]] [[package]]
name = "toml" name = "toml"
version = "0.7.8" version = "0.8.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
dependencies = [ dependencies = [
"serde", "serde",
"serde_spanned", "serde_spanned",
@ -301,9 +300,9 @@ dependencies = [
[[package]] [[package]]
name = "toml_edit" name = "toml_edit"
version = "0.19.15" version = "0.22.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"serde", "serde",
@ -398,6 +397,16 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 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]] [[package]]
name = "windows-core" name = "windows-core"
version = "0.52.0" version = "0.52.0"
@ -407,6 +416,49 @@ dependencies = [
"windows-targets", "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]] [[package]]
name = "windows-targets" name = "windows-targets"
version = "0.52.6" version = "0.52.6"
@ -473,9 +525,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.5.40" version = "0.6.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" checksum = "ad699df48212c6cc6eb4435f35500ac6fd3b9913324f938aea302022ce19d310"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]

View File

@ -1,6 +1,6 @@
[package] [package]
name = "laika" name = "laika"
version = "0.1.0" version = "0.1.1"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -8,10 +8,10 @@ edition = "2021"
[dependencies] [dependencies]
anyhow = "1.0.95" anyhow = "1.0.95"
chrono = "0.4.24" chrono = "0.4.24"
itertools = "0.10.5" itertools = "0.14.0"
serde = { version = "1.0.160", features = ["derive"] } serde = { version = "1.0.160", features = ["derive"] }
sysinfo = { version = "0.28.4", default-features = false } sysinfo = { version = "0.33.1", default-features = false, features = ["system"]}
toml = "0.7.3" toml = "0.8.19"
[profile.release] [profile.release]
opt-level = "s" opt-level = "s"

127
src/config.rs Normal file
View File

@ -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<SystemTime>,
/// 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<Vec<String>>,
/// weekdays (Mon,Tue,Wed | Пн,Вт,Ср)
pub(crate) weekdays: Weekdays,
/// exclude dates
pub(crate) exclude_dates: Vec<LocalDate>,
/// include dates
pub(crate) include_dates: Vec<LocalDate>,
/// 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<P: AsRef<Path>>(path: P) -> Result<Self> {
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<PathBuf> {
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
}
}

184
src/datetime.rs Normal file
View File

@ -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<Tz: TimeZone> From<DateTime<Tz>> for TimeHM {
fn from(value: DateTime<Tz>) -> 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<Self> {
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<String> for TimeHM {
type Error = anyhow::Error;
fn try_from(value: String) -> Result<Self, Self::Error> {
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<Self> {
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<String> for Weekdays {
type Error = anyhow::Error;
fn try_from(value: String) -> Result<Self, Self::Error> {
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<Self, Self::Err> {
let date = NaiveDateTime::parse_from_str(s, "%d.%m.%Y")?;
Ok(LocalDate { inner: date.date() })
}
}
impl TryFrom<String> for LocalDate {
type Error = anyhow::Error;
fn try_from(value: String) -> Result<Self, Self::Error> {
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<Local>) -> bool {
self.is_date(value.date_naive())
}
}
pub(crate) trait HasToday {
fn has_today(&self) -> bool;
}
impl HasToday for Vec<LocalDate> {
fn has_today(&self) -> bool {
let now = Local::now();
self.iter().any(|x| x.is_date_of(now))
}
}

View File

@ -1,172 +1,13 @@
#![windows_subsystem = "windows"] #![windows_subsystem = "windows"]
use chrono::prelude::*; use chrono::{DateTime, Local};
use chrono::{DateTime, Local, TimeZone}; use config::Config;
use itertools::Itertools; use datetime::{HasToday, TimeHM};
use serde::Deserialize; use std::{process::Command, thread, time::Duration};
use std::path::Path; use sysinfo::{ProcessRefreshKind, RefreshKind, System, UpdateKind};
use std::str::FromStr;
use std::time::SystemTime;
use std::{thread, time::Duration};
use sysinfo::{System, SystemExt};
use anyhow::{anyhow, Result}; mod config;
use std::{env, fs, path::PathBuf, process::Command}; mod datetime;
/// 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<Tz: TimeZone> From<DateTime<Tz>> for TimeHM {
fn from(value: DateTime<Tz>) -> 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<Self> {
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<String> for TimeHM {
type Error = anyhow::Error;
fn try_from(value: String) -> Result<Self, Self::Error> {
value.parse()
}
}
/// Application config
#[derive(Deserialize, Default, Debug)]
struct Config {
/// modification time
#[serde(skip)]
modified: Option<SystemTime>,
/// 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<Vec<String>>,
/// start time
start: TimeHM,
/// end time
end: TimeHM,
/// check delay (seconds)
delay: u32,
}
fn default_true() -> bool {
true
}
impl Config {
fn read<P: AsRef<Path>>(path: P) -> Result<Self> {
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<PathBuf> {
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
}
}
struct Laika { struct Laika {
config: Config, config: Config,
@ -179,9 +20,15 @@ impl From<Config> for Laika {
} }
impl Laika { impl Laika {
fn is_in_time(&self, value: TimeHM) -> bool { fn is_today_enabled(&self) -> bool {
self.config.start <= value && value <= self.config.end (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 { fn is_active(&self) -> bool {
let now: DateTime<Local> = Local::now(); let now: DateTime<Local> = Local::now();
self.config.active && self.config.is_valid() && self.is_in_time(now.into()) self.config.active && self.config.is_valid() && self.is_in_time(now.into())
@ -194,8 +41,11 @@ impl Laika {
if name.is_empty() { if name.is_empty() {
return false; return false;
} }
let sp = System::new_all(); let sp = System::new_with_specifics(
let procs = sp.processes_by_exact_name(&name); RefreshKind::nothing()
.with_processes(ProcessRefreshKind::nothing().with_exe(UpdateKind::OnlyIfNotSet)),
);
let procs = sp.processes_by_exact_name(name.as_ref());
procs.count() > 0 procs.count() > 0
} }
fn relaunch_target(&self) { fn relaunch_target(&self) {