应用概述
Wallpaper Todo 是一个 Windows 程序,用于配合计划任务定时将待办事项“印”在当日的 Bing 图片上并自动设置为壁纸。
待办事项应通过 url 参数传入,其格式为:
{
"todo": [
{"title": "任务1", "isFinished": false}
]
}
定时参数(秒)通过 interval 传入,下面是一个例子:
cargo run -- --url http://127.0.0.1:8080/todo --interval 60
如果不传递 url,则只设置 Bing 壁纸,如果不传递 interval,或传入的 interval 为 0,则不设置定时任务。
核心代码
//! # Wallpaper Todo
//!
//! 将待办事项附着于壁纸上,并定时更新。
//! 当 interval 为 0 时,只执行一次,反之则定时更新。
//! 当 url 为空时,使用默认的 bing 壁纸。
//!
//! ```bash
//! wallpaper_todo.exe --url "https://localhost:8080/todo" --interval 60
//! ```
use std::{
fs::File,
io::{Error, Write},
path::{Path, PathBuf},
};
use chrono;
use clap::Parser;
use font_kit::family_name::FamilyName;
use font_kit::properties::Properties;
use font_kit::source::SystemSource;
use image::Rgba;
use imageproc::drawing::{draw_line_segment_mut, draw_text_mut};
use reqwest;
use rusttype::{Font, Scale};
use serde::Deserialize;
use serde_json;
use tokio::time::{interval, Duration};
use wallpaper;
async fn parse_get_wallpaper_url() -> String {
let resp = reqwest::get("https://cn.bing.com/HPImageArchive.aspx?format=js&idx=0&n=1")
.await
.unwrap()
.text()
.await
.unwrap();
let json: serde_json::Value = serde_json::from_str(&resp).unwrap();
let images = json["images"].as_array().unwrap();
let image = images[0].as_object().unwrap();
let url = image["urlbase"].as_str().unwrap();
let url = format!("https://www.bing.com{}_UHD.jpg&rf=LaDigue_UHD.jpg&pid=hp&rs=1&c=4&qlt=100&uhd=1&uhdwidth=3840&uhdheight=2160", url);
return url;
}
fn image_exist() -> (bool, PathBuf) {
let cache_dir = dirs::cache_dir().ok_or("no cache dir").unwrap();
let now = chrono::Local::now();
let date = now.format("%Y%m%d").to_string();
let file_path = cache_dir.join(format!("{}_wallpaper.jpg", date));
return (file_path.exists(), file_path);
}
async fn download_image_with_cache(url: &str) -> Result<String, Error> {
let (exist, file_path) = image_exist();
if !exist {
let mut file = File::create(&file_path)?;
let res = reqwest::get(url).await.unwrap();
let bytes = res.bytes().await.unwrap();
file.write(&bytes.as_ref()).unwrap();
}
Ok(file_path
.to_str()
.to_owned()
.ok_or("no file path")
.unwrap()
.into())
}
#[tokio::test]
async fn test_download_image() {
let res = download_image_with_cache(&parse_get_wallpaper_url().await).await;
println!("{:?}", res);
}
async fn download_and_set_wallpaper() {
let _url = parse_get_wallpaper_url().await;
println!("setting wallpaper from {}", _url);
let now = chrono::Local::now();
let date = now.format("%Y%m%d").to_string();
if Path::new(&format!("{}.set", date)).exists() {
println!("{} already set", date);
return;
}
let path = download_image_with_cache(&_url).await.unwrap();
wallpaper::set_from_path(&path).unwrap();
wallpaper::set_mode(wallpaper::Mode::Crop).unwrap();
File::create(format!("{}.set", date)).unwrap();
}
async fn fetch_todo(url: &str) -> Result<Info, Error> {
let res = reqwest::get(url).await.unwrap().text().await.unwrap();
let info = serde_json::from_str::<Info>(&res);
if let Err(e) = info {
return Err(e.into());
}
return Ok(info.unwrap());
}
#[derive(Debug, Deserialize)]
struct Info {
workStatus: String,
offWork: bool,
todo: Vec<Todo>,
}
#[derive(Debug, Deserialize)]
struct Todo {
title: String,
isFinished: bool,
create_at: String,
}
fn render_image(path: &str, text: Vec<Todo>) -> Result<String, Box<dyn std::error::Error>> {
let mut img = image::open(path).unwrap().to_rgba8();
let (width, height) = img.dimensions();
let font = SystemSource::new()
.select_best_match(
&[FamilyName::Title("微软雅黑".to_owned())],
&Properties::new(),
)
.unwrap()
.load()
.unwrap();
let font_data = font.copy_font_data().unwrap().to_vec();
let font = Font::try_from_vec(font_data).unwrap();
let font_size = height as f32 / 60.0; // 设置字体大小
let scale = Scale {
x: font_size,
y: font_size,
};
let line_height = font_size * 1.0; //设置行高
let (margin_x, margin_y) = (40, 75); // 设置 y 轴边距
let total_height = line_height * text.len() as f32;
let mut y = height as i32 - total_height as i32 - margin_y;
let max_width = text
.iter()
.map(|line| {
let glyphs: Vec<_> = font
.layout(&line.title, scale, rusttype::point(0.0, 0.0))
.collect();
glyphs
.last()
.map(|g| g.position().x + g.unpositioned().h_metrics().advance_width)
.unwrap_or(0.0)
})
.max_by(|a, b| a.partial_cmp(b).unwrap())
.unwrap_or(0.0);
let x = width as i32 - max_width as i32 - margin_x;
for line in text {
if line.isFinished {
draw_text_mut(
&mut img,
Rgba([255u8, 255u8, 255u8, 12u8]), // 白色半透明
x,
y,
scale,
&font,
&line.title,
);
let line_width = font
.layout(&line.title, scale, rusttype::point(0.0, 0.0))
.last()
.map(|g| g.position().x + g.unpositioned().h_metrics().advance_width)
.unwrap_or(0.0);
let start_x = x as f32;
let end_x = start_x + line_width;
let line_y = y as f32 + font_size / 2.0; // 删除线位于文字中间
draw_line_segment_mut(
&mut img,
(start_x, line_y),
(end_x, line_y),
Rgba([255u8, 255u8, 255u8, 255u8]), // 半透明白色
);
} else {
draw_text_mut(
&mut img,
Rgba([255u8, 255u8, 255u8, 255u8]), // 白色
x,
y,
scale,
&font,
&line.title,
);
}
y += line_height as i32;
}
let cache_dir = dirs::cache_dir().ok_or("no cache dir").unwrap();
let file_path = cache_dir.join("wallpaper_todo.jpg");
img.save(&file_path)?;
Ok(file_path.to_str().unwrap().to_string())
}
#[derive(Parser, Debug)]
#[clap(author, version, about, long_about = None)]
struct Args {
#[clap(short, long, value_parser, default_value = "")]
url: String,
#[clap(short, long, value_parser, default_value_t = 0)]
interval: u64,
}
#[tokio::main]
async fn main() {
let args = Args::parse();
let mut interval = interval(Duration::from_secs(
if args.interval <= 0 { 300 } else { 0 }
));
loop {
if args.interval != 0 {
interval.tick().await;
}
println!("time to fetch todo and set wallpaper");
let bing_image_url = parse_get_wallpaper_url().await;
let mut path = download_image_with_cache(&bing_image_url).await.unwrap();
if !args.url.is_empty() {
let todo = fetch_todo(&args.url).await;
if todo.is_err() {
println!(
"error fetch todo from url, response should be like: todo: [[title, isFinished]]"
);
continue;
}
let todo = todo.unwrap().todo;
if !todo.is_empty() {
println!("todo is not empty, render new image");
let res = render_image(&path, todo);
path = res.unwrap();
} else {
println!("todo is empty, use bing image directly");
}
}
wallpaper::set_from_path(&path).unwrap();
wallpaper::set_mode(wallpaper::Mode::Crop).unwrap();
if args.interval == 0 {
break;
} else {
println!("done, waiting {} seconds", args.interval);
}
}
}
程序依赖:
tokio = { version = "1.28", features = ["full"] }
reqwest = { version = "0.11", features = ["json"] }
base64 = "0.21"
clap = { version = "3.0", features = ["derive"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
url = "2.3"
wallpaper = { version = "3", features = ["from_url"] }
chrono = "0.4.24"
image = "0.24.6"
imageproc = "0.23.0"
rusttype = "0.9.3"
font-kit = "0.11.0"
dirs = "1.0.5"
定时执行
在 Windows 系统中,可使用计划任务来定时运行此程序,简单来说,只需要点击 Windows 徽标键,搜索并打开 “计算机管理” 程序,在左侧列表找到 “系统工具 > 任务计划程序”, 右侧操作列表点按 “创建任务按钮”。
输入任务名称,点选 “隐藏”,转到 “触发器” 页,新建触发器,选择每天运行一次,设置任务随机延迟为 30 秒,设置重复任务间隔为 5 分钟,勾选 “已启用”。
转到 “操作” 页, 填入程序为 C:\Windows\System32\cmd.exe
,参数为 /c start /min "" "C:\tool\wallpaper-todo.exe" --url="https://YOUR_TODO_API"
,其中程序根据 cmd.exe 所在路径天界,参数根据实际待办事项 API 地址修改。
执行结果可通过 “历史记录” 栏查看: