+ weekdays + includes/excludes + upd deps

This commit is contained in:
2025-01-28 11:09:57 +03:00
parent 529ca478b1
commit 4ced16e8da
5 changed files with 402 additions and 189 deletions

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"]
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<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
}
}
mod config;
mod datetime;
struct Laika {
config: Config,
@@ -179,9 +20,15 @@ impl From<Config> 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> = 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) {