Compare commits
9 Commits
45c8675373
...
e52101da7c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e52101da7c | ||
|
|
35e80f1e14 | ||
|
|
fe28002782 | ||
|
|
2733b4edf1 | ||
|
|
e70b3933ae | ||
|
|
00af76a8ce | ||
|
|
877677219e | ||
|
|
88d0d17c51 | ||
|
|
636e5e1fec |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1 +1,3 @@
|
||||
/target
|
||||
|
||||
Cargo.lock
|
||||
7
Cargo.lock
generated
Normal file
7
Cargo.lock
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "health-guard"
|
||||
version = "0.1.0"
|
||||
@@ -1,3 +1,9 @@
|
||||
# hguard
|
||||
|
||||
A Software Guarding Your Health
|
||||
|
||||
## Progress
|
||||
- Now working on **hguard-core**
|
||||
- Preparing to work on **hguard-cli**
|
||||
|
||||
After developing main contents, Tray Icon and GUI Windows will be considered
|
||||
|
||||
84
docs/blog.mdx
Normal file
84
docs/blog.mdx
Normal file
@@ -0,0 +1,84 @@
|
||||
---
|
||||
title: 【开源项目】HGuard: 一个桌面健康守护者
|
||||
date: 2026-02-26
|
||||
tags: [开源项目, 健康, AI]
|
||||
---
|
||||
|
||||
# 一、项目背景
|
||||
|
||||
身体是革命的本钱。对于久坐办公的人群而言,身体健康更需要引起重视。写代码、写论文、查资料、看文档……
|
||||
有时候一抬头,已经过去三四个小时。这样的生活方式是极其不健康的。为了缓解工作学习对身体带来的负担,
|
||||
我开始做一个属于自己的小工具:它叫 **HealthGuard**,简称**HGuard**。
|
||||
|
||||
---
|
||||
|
||||
# 二、项目现状
|
||||
|
||||
目前的V1.0版本是一个后台运行的托盘程序,通过hguard-tray运行,所有的提醒都会记录在一个log文件中。
|
||||
可以在`config.toml`配置文件中写入需要提醒的任务,程序启动的时候会读取配置文件并按时提醒:
|
||||
弹出提示并播放预先设定好的音频。
|
||||
|
||||
详细信息可以到如下网址进行查看
|
||||
https://huajishe.fun/git/e2hang/HuajisheTools/releases/tag/HealthDaemon
|
||||
|
||||
同步开发仓库如下
|
||||
https://huajishe.fun/git/e2hang/hguard
|
||||
|
||||
---
|
||||
|
||||
# 三、项目前景
|
||||
|
||||
## 想法
|
||||
|
||||
单纯的提醒工具是没有灵魂的,如果把它变成一个桌面上的电子桌宠,又能打趣地提醒我该起立喝水了,
|
||||
还可以实时查看我的屏幕,并且根据我的屏幕变化随时作出反应,这将是极好的。
|
||||
这样就可以把单纯的提醒工具变成一个写代码、读文献时的伙伴,既可以帮你理解内容,查看你的代码问题,
|
||||
还可以进行健康提醒,极大地提高工作效率。
|
||||
|
||||
## 实现步骤
|
||||
|
||||
### 第一阶段(现在)
|
||||
|
||||
- 后台常驻
|
||||
- 可配置提醒
|
||||
- 稳定运行
|
||||
|
||||
### 第二阶段
|
||||
|
||||
- 桌面悬浮小窗口
|
||||
- 一个简单可见的形象,类似Live2d
|
||||
|
||||
### 第三阶段
|
||||
|
||||
- 接入 AI(本地LLM或云端API) 文本生成
|
||||
- 让提醒变得更自然
|
||||
|
||||
### 第四阶段
|
||||
|
||||
- 可选的屏幕内容理解
|
||||
- 更情境化的互动
|
||||
|
||||
每一步都可以独立完成、独立运行,慢慢进化。
|
||||
|
||||
---
|
||||
|
||||
## 四、实现难点与注意事项
|
||||
|
||||
### 本地LLM或云端API
|
||||
|
||||
本地LLM消耗的资源会很多,比较好的选择还是以云端API通过服务器返回结果
|
||||
|
||||
### 屏幕理解部分
|
||||
|
||||
采用OCR或多模态模型处理,处理后返回本地系统
|
||||
|
||||
### 隐私与边界
|
||||
|
||||
屏幕理解功能:
|
||||
- 功能默认关闭
|
||||
- 明确提示是否上传数据
|
||||
- 用户可完全控制
|
||||
|
||||
---
|
||||
|
||||
如想讨论相关内容,可以致信huajishe@gmail.com,或在git中留言
|
||||
347
docs/plan.mdx
Normal file
347
docs/plan.mdx
Normal file
@@ -0,0 +1,347 @@
|
||||
|
||||
````mdx
|
||||
---
|
||||
title: 从托盘提醒到 AI 桌宠:HGuard 的设计与进化路线
|
||||
date: 2026-02-26
|
||||
tags: [Rust, 桌面应用, 系统设计, AI, 桌宠]
|
||||
---
|
||||
|
||||
# 从托盘提醒到 AI 桌宠:HGuard 的设计与进化路线
|
||||
|
||||
HGuard 最初只是一个非常简单的想法:
|
||||
|
||||
> 我需要一个能提醒我起立和喝水的工具。
|
||||
|
||||
作为一个长期写代码、写算法、折腾系统的人,我发现“久坐”不是偶然,而是一种常态。现有工具要么太重,要么过于商业化,要么缺少可扩展性。
|
||||
|
||||
于是我决定自己写一个。
|
||||
|
||||
但在实现基础功能之后,我意识到:
|
||||
**它不应该只是一个提醒工具。**
|
||||
|
||||
我希望它最终成为:
|
||||
|
||||
- 一个后台健康守护系统
|
||||
- 一个可交互的桌面存在体
|
||||
- 一个具备 AI 对话能力的桌宠
|
||||
- 一个能够理解当前屏幕内容的上下文助手
|
||||
|
||||
这篇文章记录 HGuard 当前的实现状态,以及未来明确、可执行的演进路线。
|
||||
|
||||
---
|
||||
|
||||
# 一、当前版本:一个稳定的后台健康守护核心
|
||||
|
||||
当前版本的 HGuard 已实现以下能力:
|
||||
|
||||
## 1. 托盘常驻
|
||||
|
||||
- 程序以托盘应用形式运行
|
||||
- 不打扰桌面工作
|
||||
- 支持随时退出或暂停
|
||||
|
||||
## 2. 可配置任务系统
|
||||
|
||||
使用 `config.toml` 管理任务,例如:
|
||||
|
||||
```toml
|
||||
[[tasks]]
|
||||
name = "stand_up"
|
||||
interval_minutes = 60
|
||||
message = "该起立活动一下了"
|
||||
|
||||
[[tasks]]
|
||||
name = "drink_water"
|
||||
interval_minutes = 20
|
||||
message = "喝水时间到"
|
||||
````
|
||||
|
||||
优点:
|
||||
|
||||
* 用户无需重新编译程序
|
||||
* 扩展任务非常简单
|
||||
* 未来可支持更多任务类型
|
||||
|
||||
## 3. 异步调度引擎
|
||||
|
||||
核心采用 async 架构,调度结构大致如下:
|
||||
|
||||
```
|
||||
engine
|
||||
├── scheduler
|
||||
├── task
|
||||
├── notifier
|
||||
```
|
||||
|
||||
### engine
|
||||
|
||||
* 启动系统
|
||||
* 加载配置
|
||||
* 初始化任务
|
||||
|
||||
### scheduler
|
||||
|
||||
* 管理任务周期
|
||||
* 基于时间轮或 interval 驱动
|
||||
|
||||
### task
|
||||
|
||||
* 定义任务状态
|
||||
* 管理运行记录
|
||||
|
||||
### notifier
|
||||
|
||||
* 触发提醒
|
||||
* 未来可扩展为多种通知形式
|
||||
|
||||
这种结构保证:
|
||||
|
||||
* 调度逻辑与通知逻辑解耦
|
||||
* 可随时添加新类型任务
|
||||
* 未来可平滑接入 AI 模块
|
||||
|
||||
当前版本定位非常明确:
|
||||
|
||||
> 一个稳定、轻量、后台运行的健康守护内核。
|
||||
|
||||
---
|
||||
|
||||
# 二、架构升级目标:模块分层与 AI 解耦
|
||||
|
||||
未来 HGuard 不会把 AI 写进核心模块,而是采用分层架构。
|
||||
|
||||
计划结构:
|
||||
|
||||
```
|
||||
core/
|
||||
engine.rs
|
||||
scheduler.rs
|
||||
task.rs
|
||||
notifier.rs
|
||||
|
||||
ai/
|
||||
llm_client.rs
|
||||
context_memory.rs
|
||||
screen_analyzer.rs
|
||||
|
||||
ui/
|
||||
desktop_window.rs
|
||||
interaction.rs
|
||||
live2d_renderer.rs
|
||||
|
||||
system/
|
||||
screen_capture.rs
|
||||
permission.rs
|
||||
```
|
||||
|
||||
原则:
|
||||
|
||||
* Core 不依赖 AI
|
||||
* AI 作为增强能力存在
|
||||
* UI 与 Core 通过 IPC 通信
|
||||
|
||||
这样可以保证:
|
||||
|
||||
* 即使 AI 模块崩溃,健康提醒仍然运行
|
||||
* 可以替换不同 LLM API
|
||||
* 可关闭屏幕分析功能
|
||||
|
||||
---
|
||||
|
||||
# 三、阶段性演进路线(可实施版本)
|
||||
|
||||
## Phase 1(已完成)
|
||||
|
||||
* 托盘运行
|
||||
* 可配置任务
|
||||
* 异步调度
|
||||
* 基础通知系统
|
||||
|
||||
目标:稳定运行。
|
||||
|
||||
---
|
||||
|
||||
## Phase 2:桌面悬浮窗口
|
||||
|
||||
目标:让 HGuard “可见”。
|
||||
|
||||
### 实现步骤:
|
||||
|
||||
1. 使用 Tauri 或 winit 创建无边框窗口
|
||||
2. 设置透明背景
|
||||
3. 支持置顶
|
||||
4. 支持拖动
|
||||
5. 显示当前任务状态
|
||||
|
||||
这个阶段不接入 AI,只做 UI 可视化。
|
||||
|
||||
---
|
||||
|
||||
## Phase 3:接入 AI 文本生成
|
||||
|
||||
目标:让提醒“更有情绪”。
|
||||
|
||||
### 实现步骤:
|
||||
|
||||
1. 新建 ai/llm_client.rs
|
||||
2. 支持 API Key 配置
|
||||
3. 设计基础 Prompt:
|
||||
|
||||
```
|
||||
你是一个桌面健康助理。
|
||||
用户喜欢写代码。
|
||||
提醒语气略带调侃但不过分。
|
||||
```
|
||||
|
||||
4. 每次提醒时:
|
||||
|
||||
* 传入任务类型
|
||||
* 生成变体提醒语
|
||||
|
||||
例如:
|
||||
|
||||
> 你已经连续写了 60 分钟代码了,是时候站起来救救你的脊椎了。
|
||||
|
||||
---
|
||||
|
||||
## Phase 4:屏幕理解能力
|
||||
|
||||
目标:实现“上下文感知”。
|
||||
|
||||
### 技术路线:
|
||||
|
||||
1. 每 N 秒截屏(默认关闭)
|
||||
2. OCR 提取文本
|
||||
3. 或调用多模态模型
|
||||
4. 生成 contextual 对话
|
||||
|
||||
示例:
|
||||
|
||||
* 当前在 VSCode
|
||||
* 正在编辑 Rust 文件
|
||||
* 超过 2 小时未切换窗口
|
||||
|
||||
生成:
|
||||
|
||||
> 你是真的喜欢 Rust,还是只是逃避现实?
|
||||
|
||||
⚠ 必须提供隐私开关。
|
||||
|
||||
---
|
||||
|
||||
## Phase 5:Live2D 桌宠
|
||||
|
||||
目标:可视化人格。
|
||||
|
||||
### 技术选型:
|
||||
|
||||
推荐:
|
||||
|
||||
* 前端 + Live2D SDK
|
||||
* Rust 后端通过 IPC 通信
|
||||
|
||||
实现:
|
||||
|
||||
* 不同情绪切换动画
|
||||
* 提醒时触发动作
|
||||
* 被点击时触发对话
|
||||
|
||||
---
|
||||
|
||||
## Phase 6:情绪系统与记忆系统
|
||||
|
||||
定义简单状态机:
|
||||
|
||||
* Idle
|
||||
* Reminder
|
||||
* Playful
|
||||
* Concerned
|
||||
|
||||
记录:
|
||||
|
||||
* 今日提醒次数
|
||||
* 忽略次数
|
||||
* 活跃时长
|
||||
|
||||
形成持续人格。
|
||||
|
||||
---
|
||||
|
||||
# 四、性能与隐私设计原则
|
||||
|
||||
在设计 AI 桌宠时必须考虑:
|
||||
|
||||
## 1. 不高频截图
|
||||
|
||||
默认关闭屏幕理解。
|
||||
|
||||
## 2. 本地优先
|
||||
|
||||
优先本地 OCR 或模型。
|
||||
|
||||
## 3. 明确告知用户
|
||||
|
||||
说明:
|
||||
|
||||
* 截图是否上传
|
||||
* 是否保存数据
|
||||
* 是否长期存储
|
||||
|
||||
健康工具不应成为监控工具。
|
||||
|
||||
---
|
||||
|
||||
# 五、最终愿景
|
||||
|
||||
HGuard 的终极形态不是一个简单工具,而是:
|
||||
|
||||
* 一个长期运行的后台守护系统
|
||||
* 一个具备情境感知能力的桌面助手
|
||||
* 一个拥有轻量人格的陪伴型 AI
|
||||
|
||||
它不会干扰工作。
|
||||
|
||||
但它会存在。
|
||||
|
||||
---
|
||||
|
||||
# 六、当前进度总结
|
||||
|
||||
已完成:
|
||||
|
||||
* 核心调度系统
|
||||
* 配置驱动任务
|
||||
* 托盘常驻
|
||||
* 异步架构
|
||||
|
||||
正在进行:
|
||||
|
||||
* UI 可视化方案设计
|
||||
* AI 接口抽象
|
||||
|
||||
下一步:
|
||||
|
||||
* 实现桌面悬浮窗口
|
||||
* 接入基础 LLM API
|
||||
|
||||
---
|
||||
|
||||
# 七、为什么要做这个项目?
|
||||
|
||||
因为我发现:
|
||||
|
||||
健康不是“知道该做什么”,
|
||||
而是需要一个系统长期提醒你。
|
||||
|
||||
而陪伴也不是“热闹”,
|
||||
而是一个持续存在的反馈体。
|
||||
|
||||
HGuard 试图把这两件事结合起来。
|
||||
|
||||
这是一个长期项目。
|
||||
|
||||
它不会一蹴而就。
|
||||
|
||||
但它会持续进化。
|
||||
|
||||
```
|
||||
23
src/core/log.rs
Normal file
23
src/core/log.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
pub struct log {
|
||||
title:String,
|
||||
message:String,
|
||||
}
|
||||
|
||||
impl log {
|
||||
pub fn new(title:&str, message:&str) -> Self {
|
||||
Self{
|
||||
title:title.to_string(),
|
||||
message:message.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait error {
|
||||
pub fn ret(&self);
|
||||
|
||||
pub fn del(&self);
|
||||
}
|
||||
|
||||
impl error for log {
|
||||
|
||||
}
|
||||
141
src/core/notifier.rs
Normal file
141
src/core/notifier.rs
Normal file
@@ -0,0 +1,141 @@
|
||||
use anyhow::Result;
|
||||
use chrono::Local;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
use tracing::{error, warn};
|
||||
|
||||
use crate::task::Task;
|
||||
|
||||
// Syscall
|
||||
pub fn send_system_notification(task: &Task) -> Result<()> {
|
||||
notify_rust::Notification::new()
|
||||
.summary(&task.title)
|
||||
.body(&task.body)
|
||||
.appname("健康守护助手")
|
||||
.sound_name("default")
|
||||
.timeout(notify_rust::Timeout::Milliseconds(8000))
|
||||
.show()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Play Sound .path
|
||||
pub fn play_sound_file(path: &str) {
|
||||
use rodio::{Decoder, OutputStream, Sink};
|
||||
use std::fs::File;
|
||||
use std::io::BufReader;
|
||||
|
||||
let path = path.to_string();
|
||||
std::thread::spawn(move || {
|
||||
let result = (|| -> Result<()> {
|
||||
let (_stream, handle) = OutputStream::try_default()?;
|
||||
let sink = Sink::try_new(&handle)?;
|
||||
let file = File::open(&path)?;
|
||||
let source = Decoder::new(BufReader::new(file))?;
|
||||
sink.append(source);
|
||||
sink.sleep_until_end();
|
||||
Ok(())
|
||||
})();
|
||||
|
||||
if let Err(e) = result {
|
||||
warn!("音频文件播放失败({}),降级为内置蜂鸣", e);
|
||||
play_beep();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Beep Sound
|
||||
pub fn play_beep() {
|
||||
use rodio::{OutputStream, Sink, Source};
|
||||
use rodio::source::SineWave;
|
||||
|
||||
std::thread::spawn(|| {
|
||||
if let Ok((_stream, handle)) = OutputStream::try_default() {
|
||||
if let Ok(sink) = Sink::try_new(&handle) {
|
||||
for _ in 0..3 {
|
||||
let tone = SineWave::new(660.0)
|
||||
.take_duration(Duration::from_millis(500))
|
||||
.amplify(0.6);
|
||||
let gap = SineWave::new(1.0)
|
||||
.take_duration(Duration::from_millis(150))
|
||||
.amplify(0.0);
|
||||
sink.append(tone);
|
||||
sink.append(gap);
|
||||
}
|
||||
sink.sleep_until_end();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Play Sound
|
||||
pub fn play_sound(sound_file: &str) {
|
||||
if sound_file.is_empty() {
|
||||
play_beep();
|
||||
} else {
|
||||
play_sound_file(sound_file);
|
||||
}
|
||||
}
|
||||
|
||||
//File Checker
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FileLogger {
|
||||
log_path: PathBuf,
|
||||
lock: Arc<Mutex<()>>,
|
||||
}
|
||||
|
||||
impl FileLogger {
|
||||
/// Init Logs
|
||||
pub fn new(log_dir: &str) -> Self {
|
||||
let dir = if log_dir.is_empty() {
|
||||
dirs::data_local_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join("health-guardian")
|
||||
} else {
|
||||
PathBuf::from(log_dir)
|
||||
};
|
||||
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
let log_path = dir.join("health-guardian.log");
|
||||
let logger = Self { log_path, lock: Arc::new(Mutex::new(())) };
|
||||
logger.write_session_header();
|
||||
logger
|
||||
}
|
||||
|
||||
fn write_session_header(&self) {
|
||||
let now = Local::now();
|
||||
let line = format!(
|
||||
"\n{sep}\n 会话开始 {ts}\n{sep}\n",
|
||||
sep = "═".repeat(72),
|
||||
ts = now.format("%Y-%m-%d %H:%M:%S"),
|
||||
);
|
||||
self.append(&line);
|
||||
}
|
||||
|
||||
/// Record Trigger (Once)
|
||||
pub fn log_fire(&self, task: &Task, count: u32) {
|
||||
let now = Local::now();
|
||||
let line = format!(
|
||||
"[{ts}] #{count:>4} {name:<12} {title}\n",
|
||||
ts = now.format("%Y-%m-%d %H:%M:%S"),
|
||||
count = count,
|
||||
name = task.name,
|
||||
title = task.title,
|
||||
);
|
||||
self.append(&line);
|
||||
}
|
||||
|
||||
fn append(&self, content: &str) {
|
||||
let _g = self.lock.lock().unwrap();
|
||||
match std::fs::OpenOptions::new()
|
||||
.create(true).append(true).open(&self.log_path)
|
||||
{
|
||||
Ok(mut f) => { let _ = f.write_all(content.as_bytes()); }
|
||||
Err(e) => error!("日志写入失败: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &PathBuf { &self.log_path }
|
||||
}
|
||||
67
src/core/state.rs
Normal file
67
src/core/state.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use crate::task::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct StateInner {
|
||||
stats: HashMap<String, TaskRuntime>,
|
||||
running: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AppState {
|
||||
inner: Arc<RwLock<StateInner>>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: Arc::new(RwLock::new(
|
||||
StateInner {
|
||||
stats: HashMap::new(),
|
||||
running: true,
|
||||
}
|
||||
)),
|
||||
}
|
||||
}
|
||||
// Initial Counting Statics
|
||||
pub fn register_task(&self, stats: TaskRuntime) {
|
||||
let mut w = self.inner.write().unwrap();
|
||||
w.stats.insert(stats.tasks.name.clone(), stats);
|
||||
}
|
||||
|
||||
// Update Record
|
||||
pub fn record(&self, name:&str, next: chrono::DateTime<chrono::Local>) {
|
||||
let mut w = self.inner.write().unwrap();
|
||||
if let Some(s) = w.stats.get_mut(name) {
|
||||
s.record(next);
|
||||
}
|
||||
}
|
||||
|
||||
// Read Snapshots(All tasks)
|
||||
pub fn snapshot(&self) -> Vec<TaskRuntime> {
|
||||
let r = self.inner.read().unwrap();
|
||||
r.stats.values().cloned().collect();
|
||||
}
|
||||
|
||||
// Read Snapshots(Single tasks)
|
||||
pub fn get_stats(&self, name:&str) -> Option<TaskRuntime> {
|
||||
let r = self.inner.read().unwrap();
|
||||
r.stats.get(name).cloned()
|
||||
}
|
||||
|
||||
// Is Running?
|
||||
pub fn is_running(&self) -> bool {
|
||||
self.inner.read().unwrap().running
|
||||
}
|
||||
|
||||
// Request Stop
|
||||
pub fn request_stop(&self) {
|
||||
self.inner.write().unwrap().running = false;
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AppState {
|
||||
fn default() -> Self { Self::new() }
|
||||
}
|
||||
@@ -1,22 +1,95 @@
|
||||
use std::time::{Duration, Instant};
|
||||
use chrono::{DateTime, Local};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// Time Length
|
||||
pub enum Interval {
|
||||
Hours(u64),
|
||||
Minutes(u64),
|
||||
Seconds(u64),
|
||||
}
|
||||
// Unified to minutes, no *secs* or *hours*
|
||||
impl Interval {
|
||||
pub fn to_min(self) -> u64 {
|
||||
match self {
|
||||
Interval::Hours => self / 60,
|
||||
Interval::Minutes => self,
|
||||
Interval::Seconds => self * 60,
|
||||
}
|
||||
}
|
||||
pub fn display(self) {
|
||||
match self {
|
||||
Interval::Hours => format!("Every {} minutes", self.to_min()),
|
||||
Interval::Minutes => format!("Every {} minutes", self),
|
||||
Interval::Seconds => format!("Every {} minutes", self.to_min()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Task - Pure Data
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct Task {
|
||||
name:String,
|
||||
interval:Duration, //Use min, u64 should be divided by 60
|
||||
message:String,
|
||||
last_trigger:Instant,
|
||||
pub name:String,
|
||||
pub interval:Duration, //Use min, u64 should be divided by 60
|
||||
pub title:String,
|
||||
pub body:String,
|
||||
pub last_trigger:Instant,
|
||||
pub sound:bool,
|
||||
pub enable:bool,
|
||||
}
|
||||
|
||||
impl Task {
|
||||
pub fn new(name:&str, interval_mins:u64, message:&str) -> Self {
|
||||
pub fn new(name:&str,
|
||||
interval_mins:u64,
|
||||
title:&str,
|
||||
body:&str,
|
||||
sound:bool,
|
||||
enable:bool ) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
interval: Duration::from_secs(interval_mins * 60),
|
||||
message: message.to_string(),
|
||||
title: title.to_string(),
|
||||
body: body.to_string(),
|
||||
last_trigger: Instant::now(),
|
||||
sound: sound,
|
||||
enable: enable,
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn trigger() {
|
||||
|
||||
// Enum of Status
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub enum TaskStatus {
|
||||
Idle,
|
||||
Running,
|
||||
Paused,
|
||||
Error(String), // with error messages
|
||||
}
|
||||
|
||||
// Main: TaskRuntime
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct TaskRuntime {
|
||||
pub task: Task,
|
||||
pub task_status: TaskStatus,
|
||||
pub count: u32,
|
||||
pub last_time: Option<DateTime<Local>>,
|
||||
pub new_time: DateTime<Local>,
|
||||
}
|
||||
|
||||
impl TaskRuntime {
|
||||
pub fn new(task:&Task, task_status:&TaskStatus) -> Self {
|
||||
let nxt = Local::now() + chrono::Duration::minutes(task.initial_delay_minutes as i64);
|
||||
Self {
|
||||
task:task,
|
||||
task_status:task_status,
|
||||
count:0,
|
||||
last_time: None,
|
||||
new_time: nxt,
|
||||
}
|
||||
}
|
||||
pub fn record(&mut self, next: DataTime<Local>) {
|
||||
self.count += 1;
|
||||
self.last_time = Some(Local::now());
|
||||
self.next_time = next;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user