应用概述

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 地址修改。

执行结果可通过 “历史记录” 栏查看:

有类似需求?联系微信 CorkineMa免费获取建议和报价折扣。