Obsidian Templater EditRoutine
作成日: 2025/09/02
リピートタスク操作ツール — 仕様まとめ(実装版 / Obsidian Templater User Script)
【概要】
現在ノートと同じフォルダ内の「リピートタスク.md」を開かずに読み込み、行単位のタスクリストをUI表示。
ユーザーが選んだタスクに対して「編集/移動/削除」を実行。
処理完了後は、元ノートのカーソル/選択範囲を復元。
【前提・環境】
Obsidian + Templater(User Script)前提(app, tp を使用)。
ノートは UTF-8 想定。1行=1タスク扱い(空行は無視)。
HUD は Obsidian の Notice を利用。
【対象ファイルの特定】
現在編集中ノートのフォルダ配下の「リピートタスク.md」を対象。const repeatTaskFilePath = tp.file.folder(true) + '/リピートタスク.md'
【基本フロー】
1. 現ノートのカーソル位置・選択範囲を退避。
2. 「リピートタスク.md」を読み込み、全行を配列化(空行除外)。
3. 現ノートの選択テキストがあれば、includes で部分一致フィルタ(0件なら通知して終了)。
4. フィルタ済み一覧を「行全文 | 元配列インデックス」で suggester 表示 → 1件選択。
5. 「編集/移動/削除」いずれかを選択。
編集:入力ダイアログで文字列を上書き保存(空文字は不可)。
移動:基準タスク(“直後へ挿入”の相手)を選び、その直後に挿入。
削除:確認後に当該行を削除。
6. 保存:vault.modify で「リピートタスク.md」を即時上書き保存。
7. 復帰:元ノートの選択範囲/カーソルを復元(表示ノートは切り替えない設計)。
【UI仕様】
一覧UI: "行全文 | 元配列インデックス" の表示(選択結果は元インデックス値)。
成功時:Notice(成功)。未選択/キャンセル/不正操作は Notice(エラー/情報)。
【フィルタ仕様(任意)】
現ノート選択範囲が空でなければ includes で絞り込み(大文字小文字は区別)。
0件は保存・変更を行わず終了。
【エラー/ガード】
ターゲットファイルが無ければ自動作成(空)。
未選択/キャンセル/空文字編集は禁止・中断。
【既知の制約】
同一内容のタスクが複数ある場合、インデックスにより個別識別は可能だが、表示上は区別が付きづらい場合がある(表示強化は適宜拡張可)。
行頭の装飾は特別扱いしない(純粋に1行テキストとして扱う)。
code:js
<%*
/**
* リピートタスク操作ツール — テンプレ直書き版(MarkdownView未定義対策)
*/
const Notice = window.Notice || ((msg)=>alert(msg));
// ▼ アクティブMarkdownエディタ取得(MarkdownView を使わない)
const leaf = app.workspace?.activeLeaf;
const view = leaf?.view;
if (!view || typeof view.getViewType !== 'function' || view.getViewType() !== 'markdown' || !view.editor) {
new Notice("Markdownエディタで実行してください。");
return;
}
const editor = view.editor;
// 元の選択範囲を退避
const selFrom = editor.getCursor("from");
const selTo = editor.getCursor("to");
// ▼ フォルダと対象ファイル
const folderPath = tp.file.folder(true);
const repeatTaskFilePath = ${folderPath}/リピートタスク.md;
let repeatFile = app.vault.getAbstractFileByPath(repeatTaskFilePath);
if (!repeatFile) {
repeatFile = await app.vault.create(repeatTaskFilePath, "");
new Notice("「リピートタスク.md」を新規作成しました。");
}
// ▼ 読み込み → 行配列(空行除外)
let tasks = (await app.vault.read(repeatFile))
.split(/\r?\n/)
.filter(l => l.trim() !== "");
// ▼ フィルタ(選択文字列があれば部分一致)
const selectedText = editor.getSelection().trim();
let viewLines = tasks, viewIdxMap = tasks.map((_, i) => i);
if (selectedText) {
const f = [], m = [];
tasks.forEach((line, i) => { if (line.includes(selectedText)) { f.push(line); m.push(i); } });
if (f.length === 0) {
new Notice(対象なし: 「${selectedText}」);
editor.setSelection(selFrom, selTo);
return;
}
viewLines = f; viewIdxMap = m;
}
if (viewLines.length === 0) {
new Notice("リピートタスクがありません。");
editor.setSelection(selFrom, selTo);
return;
}
// ▼ タスク選択UI
const display = viewLines.map((line, k) => ${line} | ${viewIdxMap[k]});
const pickedIndex = await tp.system.suggester(display, viewIdxMap);
if (pickedIndex == null) {
new Notice("キャンセルしました。");
editor.setSelection(selFrom, selTo);
return;
}
// ▼ 操作選択
const action = await tp.system.suggester("編集","移動","削除", "edit","move","delete");
if (!action) {
new Notice("操作未選択。");
editor.setSelection(selFrom, selTo);
return;
}
// ▼ 保存関数
async function save(lines) {
await app.vault.modify(repeatFile, lines.join("\n") + "\n");
}
try {
if (action === "edit") {
const before = taskspickedIndex;
const after = await tp.system.prompt("タスク編集:", before);
if (after == null) {
new Notice("編集をキャンセルしました。");
} else {
const newText = String(after).replace(/\r?\n/g, " ").trim();
if (!newText) {
new Notice("空文字は保存できません。");
} else if (newText === before) {
new Notice("変更なし。");
} else {
taskspickedIndex = newText;
await save(tasks);
new Notice("編集完了");
}
}
}
if (action === "move") {
if (tasks.length <= 1) {
new Notice("移動できる他行がありません。");
} else {
const destIdxs = tasks.map((_, i) => i).filter(i => i !== pickedIndex);
const destDisp = destIdxs.map(i => ${tasks[i]} | ${i});
const destIndex = await tp.system.suggester(destDisp, destIdxs);
if (destIndex != null) {
const arr = tasks.slice();
const moved = arr.splice(pickedIndex, 1);
const insertPos = destIndex > pickedIndex ? destIndex : destIndex + 1;
arr.splice(insertPos, 0, moved);
tasks = arr;
await save(tasks);
new Notice("移動完了");
} else {
new Notice("移動をキャンセルしました。");
}
}
}
if (action === "delete") {
const confirm = await tp.system.suggester("削除する","キャンセル", true, false);
if (confirm) {
tasks.splice(pickedIndex, 1);
await save(tasks);
new Notice("削除完了");
} else {
new Notice("削除をキャンセルしました。");
}
}
} catch (e) {
console.error(e);
new Notice(エラー: ${e?.message ?? e});
} finally {
// 元の選択範囲へ復帰
editor.setSelection(selFrom, selTo);
}
%>