hugoの予約投稿をtauriで作る2
こんにちは、ナナオです。 前回の続きです。 hugoを予約投稿するための超ミニマムなCMSを作った hugoの予約投稿アプリをtauriで作る 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(()) } } } これで既存の機能を移し替えることができました。 ...
tigの使い方
こんにちは、ナナオです。 今回は私が愛用しているtigの使い方を説明していこうと思います。 tigとは tigはターミナルなどで動作する、gitのTUIツールです。 コミットやステージング、スタッシュなどの操作をTUIで実行できるのが特徴です。 同じようなTUIツールだと、最近はlazygitなどが有名ですね。 インストール インストールはbrewで行いました。 brew install tig あとは任意のgitリポジトリのディレクトリでコマンドを実行します。 tig 以下のような画面になります。 この状態でsキーを押すと、ステージングファイルなどを見ることができます。 qキーを押すことで前の画面に戻れます。 カーソルの移動はvimっぽくjkキーで行えます。 また、.tigrcというファイルをホームディレクトリに配置することで、独自のキーカスタマイズも可能です。 私はこれを利用して、リポジトリへのプッシュやgit flowを行えるようにしています。 git-flowを行えるようにする.tigrc↓ dotfiles/tigrc.git-flow at master · darcyparker/dotfiles · GitHub fetch / pull / pushを行う.tigrc↓ Tig で Git を自由自在に操作するための .tigrc 設定例 #hub - Qiita # F で fetch (default: :toggle file-name / :toggle commit-title-refs) bind generic F ?git fetch %(remote) bind main F ?git fetch %(remote) # Alt-f で :toggle file-name / :toggle commit-title-refs bind generic <Esc>f :toggle file-name bind main <Esc>f :toggle commit-title-refs # U で pull bind generic U ?git pull %(remote) # Alt-u で該当ブランチを更新 bind main <Esc>u ?sh -c "git checkout %(branch) && git pull %(remote) --ff-only && git checkout -" bind diff <Esc>u ?sh -c "git checkout %(branch) && git pull %(remote) --ff-only && git checkout -" bind refs <Esc>u ?sh -c "git checkout %(branch) && git pull %(remote) --ff-only && git checkout -" # P で remote への push bind generic P ?git push -u %(remote) %(repo:head) # S で stash save bind generic S ?git stash save "%(prompt Enter stash comment: )" # Bindings for git-flow. # # Flow bindings start with the capital F and then follow the first character of # each operation. If executed from the refs view, the operations (that make # sense to) work on the selected branch. Otherwise, they work on the currently # checked out branch. # # Commands that finish a flow require confirmation to run. Commands that create # a new flow prompt for user input and run when that input is accepted with no # confirmation prompt. # # Note: Bindings assume the standard git-flow paths of feature, release, hotfix # and support. # # To use these keybindings copy the file to your HOME directory and include it # from your ~/.tigrc file: # # $ cp contrib/git-flow.tigrc ~/.tigrc.git-flow # $ echo "source ~/.tigrc.git-flow" >> ~/.tigrc # Get rid of default bindings for F, as that will be the entry point for all # git-flow related commands with this binding. bind main F none bind generic F none # General bind generic Fi ?git flow init # Feature bind generic Ffl !git flow feature bind generic Ffs !git flow feature start "%(prompt New feature name: )" bind generic Fff ?sh -c "git flow feature finish `echo %(repo:head) | sed -e s/feature.//`" bind refs Fff ?sh -c "git flow feature finish `echo %(branch) | sed -e s/feature.//`" # Release bind generic Frl !git flow release bind generic Frs !git flow release start "%(prompt New release name: )" bind generic Frf ?sh -c "git flow release finish `echo %(repo:head) | sed -e s/release.//`" bind refs Frf ?sh -c "git flow release finish `echo %(branch) | sed -e s/release.//`" # Hot Fix bind generic Fhl !git flow hotfix bind generic Fhs !git flow hotfix start "%(prompt New hotfix name: )" bind generic Fhf ?sh -c "git flow hotfix finish `echo %(repo:head) | sed -e s/hotfix.//`" bind refs Fhf ?sh -c "git flow hotfix finish `echo %(branch) | sed -e s/hotfix.//`" # Support bind generic Fsl !git flow support bind refs Fss !git flow support start "%(prompt New support name: )" %(branch)% まとめ カスタマイズもしやすいtig、おすすめです。 ...
家で豚骨ラーメンを作った
こんにちは、ナナオです。 ラーメンを作りました。 家ラーメンは何回か作ったことがありますが、今日は割とうまくできたほうかな、と思います。 (厨房は毎度のごとくギットギトになる…) 麺は肉のハナマサで買った超極太麺を使って、スープは豚骨をベースにモミジ、豚肩肉、豚バラをにんにくショウガ長ネギと一緒に煮込んだ感じです。 味は結構雑味が多いですが、優しめのスープになっています。 雑味が多いあたり、丁寧さを省いた性格がそのまま出た気がします。
zsh syntax highlighting: unhandled ZLE widget 'menu search'の直し方
こんにちは、ナナオです。 以前antidote + starshipで環境を整えたのですが、その後zshを使っていたら突然以下のようなエラー(?)になりました。 zsh-syntax-highlighting: unhandled ZLE widget 'menu-search' zsh-syntax-highlighting: (This is sometimes caused by doing `bindkey <keys> menu-search` without creating the 'menu-search' widget with `zle -N` or `zle -C`.) zsh-syntax-highlighting: unhandled ZLE widget 'recent-paths' zsh-syntax-highlighting: (This is sometimes caused by doing `bindkey <keys> recent-paths` without creating the 'recent-paths' widget with `zle -N` or `zle -C`.) なんだこれー?と思って調べてみたら、どうやら読み込み順が悪かった模様。。 unhandled ZLE widget · Issue #951 · zsh-users/zsh-syntax-highlighting · GitHub 私の.zsh_plugins.txtは以下のように定義していました。 ohmyzsh/ohmyzsh path:plugins/magic-enter ohmyzsh/ohmyzsh path:plugins/kubectl ohmyzsh/ohmyzsh path:plugins/terraform hlissner/zsh-autopair marlonrichert/zsh-edit zsh-users/zsh-completions marlonrichert/zsh-autocomplete zsh-users/zsh-autosuggestions zsh-users/zsh-history-substring-search zsh-users/zsh-syntax-highlighting どうやらzsh-syntax-highlightingはzsh-autocompleteよりも前に定義していなければいけない模様。 ...
pythonは鮮度が命なんだよォ!
こんにちは、ナナオです。 ここ5年ほどpythonを触ってきて、Webバックエンドを中心に、良いところも悪いところも一通り踏んできました。 そんな中、新規プロジェクトでpythonを導入する一番のメリットが何かと言われたら何でしょうか? チームメンバーがpythonを使っているから?社内の既存プロジェクトがpythonばかりだから? たしかにそれも理由の一つにはなるでしょう。ですが、プロジェクトのことを考えたときにメンバーが使っているからというのがメリットになるでしょうか? 新しい言語のラーニングコストと、プロジェクトそのもののコスト(運用、パフォーマンスなど)を天秤にかければ、ラーニングコストはそこまで大きなコストではないです。 pythonを採用する最も大きなメリット、それは実装速度です。 とにかくすぐ実装できる コードの制約があまりなく、すぐ実行可能なのはpythonです。これはスクリプト言語の強みでもあります。 バックエンドは以下のように数行で実装できます。 FastAPI from fastapi import FastAPI app = FastAPI() @app.get("/") def read_root(): return {"Hello": "World"} @app.get("/items/{item_id}") def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} pythonになじみのない方向けに説明すると、@はデコレーターといって、簡単に言えばラッパー関数を端的に表現したものになります。 https://zenn.dev/ryo_kawamata/articles/learn_decorator_in_python ただ、Rustでも数行で書くことはできます。 GitHub - actix/actix-web: Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust. use actix_web::{get, web, App, HttpServer, Responder}; #[get("/hello/{name}")] async fn greet(name: web::Path<String>) -> impl Responder { format!("Hello {name}!") } #[actix_web::main] // or #[tokio::main] async fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new().service(greet) }) .bind(("127.0.0.1", 8080))? .run() .await } 型情報があるというのはいいことです。スレッドセーフなのも最高。Rustは最高の言語のひとつだと思います。 ...
hiberfil.sysという謎のファイルが圧迫していたので削除した
どうも、ナナオです。 実は今私が使っているノートPC、容量が128GBしかないんです。 それなのにDockerとかk8sとか入れているもんだから、とにかく容量が不足してしょうがない。。 流石にそろそろ増設しようとは思っていますが、それまでの間容量を減らすためにWizTreeというアプリで何が容量を食っているのか観察したところ、hiberfil.sysというファイルで10GB近く容量を食っていることに気づきました。 今回はこのファイルの正体と、削除する方法を備忘録として書いておきます。 hiberfil.sysの正体 結論から言えば、コンピュータを休止状態にするために必要なファイルです。 メモリの内容を休止状態後も保持するためにあるみたいですね。 hiberfil.sysを削除する このファイル、単に削除してもすぐ復活してしまうので、休止状態を一度オフにしてあげる必要があります。 管理者権限でコマンドプロンプトを立ち上げ、以下のコマンドを実行します。 powercfg.exe /hibernate off 再度有効にする場合はonで実行すればいいだけです。 powercfg.exe /hibernate on 参考 休止状態を無効にして再び有効にする方法 - Windows Client | Microsoft Learn
hugoの予約投稿アプリをtauriで作る
こんにちは、ナナオです。 今回は、前回作ったhugoの予約投稿ツールをtauriでUI作って見やすくしようよ、という記事になります。 Githubはこちら↓ GitHub - satodaiki/hugo-local-cms: hugoの予約投稿管理 tauriのインストールと起動まで まずはプロジェクトにtauriを入れます。 やり方はこちらを参考にします。 プロジェクトの作成 | Tauri フロントエンドのビルドにはviteを利用します。 ❯ npm create vite@latest . > npx > "create-vite" . │ ◇ Current directory is not empty. Please choose how to proceed: │ Ignore files and continue │ ◇ Select a framework: │ React │ ◇ Select a variant: │ TypeScript │ ◇ Use rolldown-vite (Experimental)?: │ No │ ◇ Install with npm and start now? │ Yes │ ◇ Scaffolding project in /home/nanao/Project/personal/hugo-local-cms... │ ◇ Installing dependencies with npm... added 175 packages, and audited 176 packages in 14s 45 packages are looking for funding run `npm fund` for details found 0 vulnerabilities │ ◇ Starting dev server... React + TypeScriptで実装することにしました。 ...
Pythonでデータモデルを扱いたい時のTips
こんにちは、ナナオです。 ドメイン駆動開発しているとデータモデルを作る機会は多いと思いますが、Pythonではどのようにデータモデルを表現すればいいのか、というところを軽くまとめました。 とりあえず使うなら -> dataclasses 個人開発などでとりあえずデータモデルを定義したいだけならdataclassで事足りると思います。 dataclasses --- データクラス — Python 3.13.11 ドキュメント import uuid from dataclasses import dataclass @dataclass class User: id: uuid.UUID name: str # 使い方 user = User( id=uuid.uuid4(), name="nanao", ) なんといっても標準ライブラリで、かつ使いやすいのが特徴ですね。 特にDB連携なんかもしないならこれでいいと思います。 データベースと連携するなら -> SQLAlchemy DB連携を考えるなら外せないのがSQLAlchemyですね。 SQLAlchemy - The Database Toolkit for Python from typing import List from typing import Optional from sqlalchemy import ForeignKey from sqlalchemy import String from sqlalchemy.orm import DeclarativeBase from sqlalchemy.orm import Mapped from sqlalchemy.orm import mapped_column from sqlalchemy.orm import relationship class Base(DeclarativeBase): pass class User(Base): __tablename__ = "user_account" id: Mapped[int] = mapped_column(primary_key=True) name: Mapped[str] = mapped_column(String(30)) 本番利用も考えるなら -> pydantic こちらのライブラリはバリデーションの豊富さなどの観点から、さながらdataclassesの進化版といったところでしょうか。 ...
ディスク増設したあと、Cドライブを拡張する方法
こんにちは、ナナオです。 最近、128GB -> 500GBにディスクを増設したのですが、ディスククローンまではいつもこれを使っていて問題なかったのですが、Cドライブの拡張にちょっと手こずったので備忘録として残しておきます。 なんで手こずった Cドライブの拡張にはwindowsでは「ディスクの管理」というユーティリティがあるので、ここからディスクの拡張などはできるはずなのですが、図のようになっていました。 図のように、Cドライブの隣の隣に拡張された未割当の領域があります。 「なんだ、拡張できそうじゃん」となりますが、未割当領域はCドライブに隣接していないと拡張できないのです。。 「じゃあ隣まで持って来ればいいじゃん!」となりますが、このユーティリティからだとその操作ができないんですよね。なんでやねん。 じゃあどうするか ということで、diskpartコマンドを使って解決します。 管理者権限でコマンドプロンプトを開き、以下のコマンドを実行します。 diskpart 実行するとdiskpartツールが実行されます。 ここでlist diskを実行して、ディスクを選択します。 私の場合ディスクは一つしかないので、0を選択します。 DISKPART> list disk ディスク 状態 サイズ 空き ダイナ GPT ### ミック ------------ ------------- ------- ------- --- --- ディスク 0 オンライン 476 GB 357 GB * DISKPART> select disk 0 ディスク 0 が選択されました。 次にlist partitionでパーティションの一覧を見ます。 DISKPART> list partition Partition ### Type Size Offset ------------- ------------------ ------- ------- Partition 1 システム 200 MB 1024 KB Partition 2 予約済み 16 MB 201 MB Partition 3 プライマリ 118 GB 217 MB Partition 4 回復 783 MB 118 GB 大体さきほどのディスクの管理の画面で見た情報と変わらないですね。 ...
ryeからuvに乗り換える
こんにちは、ナナオです。 よく使っているノートPCでは完全にuvしか使っていないのですが、デスクトップではまだryeを使っていたのでもうuvに乗り換えてしまおうと思います。 (将来的に置き換えられるらしいからね。。) Rye and uv · astral-sh/rye · Discussion #1342 · GitHub ryeのアンインストール rye自身がアンインストールコマンドを用意しています。 rye self uninstall コマンドを実行後、Don't forget to remove the sourcing of $HOME/.rye/env from your shell config.と言われるので、言われるがままにこのディレクトリも削除します。 rm -rf ~/.rye また、.zshrcなどに定義したryeの設定も削除します。 # 以下の行を削除 source "$HOME/.rye/env" これでアンインストールは完了です! uvのインストール 続けてuvをインストールしましょう。 せっかくなので前回入れたmiseを使ってインストールします。 ❯ mise install uv mise uv@0.9.25 ❯ mise use -g uv mise ~/.config/mise/config.toml tools: uv@0.9.25 インストールできました。 ryeを使っていたプロジェクトではどうするか uv syncを実行してください。 ❯ uv sync Using CPython 3.10.6 interpreter at: /home/nanao/.local/share/mise/installs/python/3.10.6/bin/python3.10 Creating virtual environment at: .venv Resolved 1 package in 2ms Built python-playground @ file:///home/nanao/Project/study/python-playground Prepared 1 package in 597ms Installed 1 package in 1ms + python-playground==0.1.0 (from file:///home/nanao/Project/study/python-playground) これで今まで通り問題なく動くはずです。 ...