こんにちは、ナナオです。

前回の続きです。

windows側で起動できるようにする

前回は画面表示はできたけど表示がおかしい感じになっていました。

おそらくWSLで起動したことが原因なので、修正します。

windows側のディレクトリに移動して、powershellで再度tauri-cliをインストールします。

cargo install tauri-cli

再度起動してみます。

cargo tauri dev

今度はちゃんと表示されました。

hugoディレクトリの監視とデプロイを行うようにする

今の状態だと予約投稿する記事の管理はGUIでできるようになりましたが、予約投稿自体ができていません。

ということで少し実装を修正します。

lib.rsを以下のように修正します。

use chrono::{Local, NaiveDateTime};
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::thread;
use std::time::Duration;

#[derive(Debug, Serialize, Deserialize, Clone)]
struct ScheduledPost {
    date: String,
    file: String,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
struct Config {
    workdir: String,
    scheduled: Vec<ScheduledPost>,
}

const CONFIG_PATH: &str = "../config.yaml"; // パスは適宜調整

#[tauri::command]
fn get_config() -> Result<Config, String> {
    let content = fs::read_to_string(CONFIG_PATH).map_err(|e| e.to_string())?;
    let config: Config = serde_yaml::from_str(&content).map_err(|e| e.to_string())?;
    Ok(config)
}

#[tauri::command]
fn save_config(config: Config) -> Result<(), String> {
    let yaml = serde_yaml::to_string(&config).map_err(|e| e.to_string())?;
    fs::write(CONFIG_PATH, yaml).map_err(|e| e.to_string())?;
    Ok(())
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .setup(|app| {
            if cfg!(debug_assertions) {
                app.handle().plugin(
                    tauri_plugin_log::Builder::default()
                        .level(log::LevelFilter::Info)
                        .build(),
                )?;
            }
            // スレッドを立てて監視を開始
            thread::spawn(|| {
                start_monitoring();
            });
            Ok(())
        })
        .invoke_handler(tauri::generate_handler![get_config, save_config])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

fn start_monitoring() {
    println!("Hugo Local CMS: 予約投稿監視を開始しました...");

    // 処理済みファイルを記録するSet (日付+ファイル名 をキーにする)
    let mut processed: HashSet<String> = HashSet::new();

    loop {
        println!("{} ファイルチェック開始", Local::now().to_rfc3339());
        // 設定ファイルの読み込み
        let config = match load_config(CONFIG_PATH) {
            Ok(cfg) => cfg,
            Err(e) => {
                eprintln!("設定読み込みエラー: {}", e);
                thread::sleep(Duration::from_secs(10));
                continue;
            }
        };

        let now = chrono::Local::now().naive_local();

        for post in config.scheduled {
            let key = format!("{}{}", post.date, post.file);

            // 既に処理済みならスキップ
            if processed.contains(&key) {
                continue;
            }

            // 日付パース (フォーマット: 2026/01/20 18:45:00)
            let pub_date = match NaiveDateTime::parse_from_str(&post.date, "%Y/%m/%d %H:%M:%S") {
                Ok(dt) => dt,
                Err(e) => {
                    eprintln!("日付パースエラー ({}): {}", post.file, e);
                    continue;
                }
            };

            // 時間チェック
            if now > pub_date {
                println!("⏰ 予約時刻になりました: {}", post.file);

                // publish処理を実行
                publish(&config.workdir, &post.file, &post.date);

                // 処理済みリストに追加
                processed.insert(key);
            }
        }

        println!("{} ファイルチェック終了", Local::now().to_rfc3339());
        // 30秒待機
        thread::sleep(Duration::from_secs(30));
    }
}

fn load_config(path: &str) -> Result<Config, Box<dyn std::error::Error>> {
    let content = fs::read_to_string(path)?;
    let config: Config = serde_yaml::from_str(&content)?;
    Ok(config)
}

fn publish(work_dir: &str, relative_path: &str, target_date_str: &str) {
    let full_path = Path::new(work_dir).join(relative_path);

    // 1. ファイルのドラフト解除
    if let Err(e) = modify_draft_status(&full_path, target_date_str) {
        eprintln!("❌ ファイル修正失敗 [{:?}]: {}", full_path, e);
        return;
    }

    // 2. Hugoコマンド実行
    execute_hugo(work_dir);
}

fn modify_draft_status(path: &PathBuf, target_date_str: &str) -> std::io::Result<()> {
    // ファイル読み込み
    let content = fs::read_to_string(path)?;

    // 書き換えるコンテンツ
    let mut new_content = content;

    // 正規表現: 行頭の "draft = true" (スペース許容) を検索
    // (?m) はマルチラインモード有効化
    let re = Regex::new(r"(?m)^(draft\s*:\s*)true").unwrap();

    if re.is_match(&new_content) {
        // 置換実行 ( ${1} は "draft = " の部分 )
        new_content = re.replace(&new_content, "${1}false").to_string();
        println!(
            "📝 Draftフラグを解除しました: {:?}",
            path.file_name().unwrap()
        );
    }

    // dateの書き換え
    // --- 日付のフォーマット変換 ---
    // config.yaml の "2026/01/20 18:45:00" -> "2026-01-20T18:45:00+09:00"
    let dt = NaiveDateTime::parse_from_str(target_date_str, "%Y/%m/%d %H:%M:%S").unwrap();
    let formatted_date = format!("{}T{}+09:00", dt.date(), dt.time());

    let re = Regex::new(r"(?m)^(\s*date\s*:\s*).*").unwrap();

    if re.is_match(&new_content) {
        let replacement = format!("${{1}}{}", formatted_date);
        new_content = re.replace(&new_content, replacement.as_str()).to_string();
        println!("📅 日付を更新しました: {}", formatted_date);
    }

    // 書き込み
    fs::write(path, new_content.as_bytes())?;
    println!("📝 Frontmatterの更新完了: {:?}", path.file_name().unwrap());

    Ok(())
}

fn execute_hugo(work_dir: &str) {
    println!("🏗️ Hugoビルドを開始します...");

    // 1. hugo
    if run_command(work_dir, "hugo", &[]).is_err() {
        return;
    }

    // 2. hugo deploy
    if run_command(work_dir, "hugo", &["deploy"]).is_err() {
        return;
    }

    println!("✅ Hugoのビルドとデプロイが完了しました。");
}

fn run_command(dir: &str, command: &str, args: &[&str]) -> Result<(), ()> {
    println!("🚀 実行中: {} {:?} (in {})", command, args, dir);

    let status = Command::new(command)
        .args(args)
        .current_dir(dir) // 作業ディレクトリ指定
        .status(); // 実行して終了ステータスを待つ

    match status {
        Ok(s) if s.success() => Ok(()),
        Ok(s) => {
            eprintln!("エラー: コマンドが失敗しました (Exit Code: {:?})", s.code());
            Err(())
        }
        Err(e) => {
            eprintln!("エラー: コマンドを実行できませんでした: {}", e);
            Err(())
        }
    }
}

これで既存の機能を移し替えることができました。

ファイル選択を行えるようにする

予約投稿したいmdファイルの選択を行えるようにします。

ライブラリをインストールします。

cargo tauri add fs

ディレクトリ読み込みの権限を付与します。

  "permissions": [
    "core:default",
    {
      "identifier": "fs:allow-read-dir",
      "allow": [{ "path": "$HOME/**/*" }]
    }
  ]

App.tsxの初期化関数から呼び出せるか確認してみます。

  const loadConfig = async () => {
    try {
      const res = await invoke<Config>("get_config");
      
      // 以下の二行を追加
      const entries = await readDir(`${res.workdir}\\content`);
      console.log("entries: ", entries);
      
      setConfig(res);
    } catch (err) {
      console.error("設定の読み込みに失敗:", err);
    }
  };

呼び出せることを確認しました。

まとめ

tauriから呼び出せるようにしただけで結構疲れたので、一旦ここまでにします。

次回こそはファイル一覧から選択できるようにしたいと思います。