Open67

1Writer/Obsidianでタスクシュートやってみる

しのしの

このスクラップについて

最近よく使っているテキストエディターの1WriterとObsidianで行動ログやルーチン管理ができないかなぁと思い、ちょこちょことコードを書き溜めています。

タスクシュートみたいに、開始時刻・終了時刻が簡単に入れられて、実績時間や完了予定時刻が出せるのが理想です。

まとめ記事

バージョン1: テキストエディタで簡易タスクシュート - 運用とコード(Obsidian / 1Writer)|しの
バージョン2: テキストエディタで簡易タスクシュート - Dynamic Timetable / 1Writerで終了予定時刻|しの

しのしの

1Writer タスクの開始終了 v1

行頭がhh:mmでなければ、行頭に現在時刻を"hh:mm- "形式で追加
行頭がhh:mmならば、"hh:mm-"の直後に現在時刻を"hh:mm"形式で追加

// カーソルがある行の範囲を取得
const cursorLineRange = editor.getSelectedLineRange();
const start = cursorLineRange[0]; // 行の開始位置
const end = cursorLineRange[1];   // 行の終了位置

// カーソルがある行のテキストを取得
const currentLineText = editor.getTextInRange(start, end);

// 現在時刻を hh:mm 形式で取得
const currentTime = new Date();
const formattedTime = currentTime.toTimeString().slice(0, 5); // "hh:mm"形式

let updatedLineText;

if (/^\d{2}:\d{2}-/.test(currentLineText)) {
    // 行頭が hh:mm- で始まる場合、その直後に hh:mm を挿入
    updatedLineText = currentLineText.replace(/^(\d{2}:\d{2}-)/, `$1${formattedTime}`);
} else {
    // 行頭が hh:mm- で始まらない場合、行頭に "hh:mm- "(末尾に半角スペース)を追加
    updatedLineText = `${formattedTime}- ${currentLineText}`;
}

// カーソル行のテキストを更新
editor.replaceTextInRange(start, end, updatedLineText);

参考

ファイルからリストを作成して、リピートタスクを呼び出す〜1Writerカスタマイズ④〜 - iPhoneと本と数学となんやかんやと

メモ

下記をコピペして、ChatGPTに「下記が1Writerのドキュメントです。これを元に、正しいコードを書いて」と指示したら、一発で動くコードが返ってきた。
editor - JavaScript Documentation - 1Writer

しのしの

Obsidian タスクの開始終了 v1

下準備: Templaterを入れる

TemplaterでInstallを押すか、
Community plugins > "Community plugins"セクションの"Browse"ボタンを押す > Templaterを見つけてInstall

Community plugins > "Templater" section > Togglをオンにする

手順 1: TemplaterJS フォルダを作成し、JavaScriptファイルを配置する

  1. ObsidianのVaultに「TemplaterJS」というフォルダを作成します。このフォルダにJavaScriptファイルを配置します。
  2. そのフォルダ内に新しいファイルを作成し、例えば「timestampTool.js」と名付けます。

手順 2: timestampTool.js ファイルの内容を記述

次に、timestampTool.jsファイルに以下のコードを記述します。

async function timestampTool(tp) {
    const currentTime = new Date();
    const formattedTime = currentTime.toTimeString().slice(0, 5);

    // 現在のカーソル行を取得
    const editor = app.workspace.activeLeaf.view.editor;
    const cursorLine = editor.getCursor().line;
    const currentLineText = editor.getLine(cursorLine);

    // 正規表現で hh:mm- を探す
    const timeRegex = /^\d{2}:\d{2}-/;

    let updatedLineText;
    let cursorPosition;

    if (timeRegex.test(currentLineText)) {
        // 既に hh:mm- 形式がある場合、現在時刻を追加
        updatedLineText = currentLineText.replace(timeRegex, `$&${formattedTime}`);
        cursorPosition = `${formattedTime}`.length + `${formattedTime}`.length + 1;  // カーソルの新しい位置
    } else {
        // まだ hh:mm- 形式がない場合、行頭に現在時刻を追加
        updatedLineText = `${formattedTime}- ${currentLineText}`;
        cursorPosition = `${formattedTime}- `.length;  // カーソルの新しい位置
    }

    // 更新されたテキストをカーソル行に挿入
    editor.setLine(cursorLine, updatedLineText);
    
    // カーソルを新しい時刻の末尾に移動
    editor.setCursor({ line: cursorLine, ch: cursorPosition });
}

module.exports = timestampTool;

手順 3: Templaterコマンドを設定する

  1. Obsidianで「Templates」フォルダに新しいテンプレートファイルを作成し、例えば「InsertTimestamp.md」と名付けます。「Templates」フォルダがない場合、新たに作成します。
  2. このテンプレートファイルに以下の内容を追加します。
<%* tp.user.timestampTool(tp) %>

手順 4: Templaterプラグイン設定でTemplaterJSフォルダを指定

  1. Obsidianの設定に進み、「Templater」プラグインの設定を開きます。
  2. 「Template folder location」に、「Templates」フォルダを指定します。
  3. 「User Script Folder」に、先ほど作成した「TemplaterJS」フォルダのパスを指定します。
  4. 「Template hotkeys」で「Add new hotkeys for template」をクリックし、InsertTimestamp.mdを選択します。

手順 5: テンプレートを実行する

  1. Obsidianの任意のノートで、先ほど設定したテンプレートを挿入します。コマンドパレット(Ctrl + P)を開いて、「Templater: Insert Template」を選択し、「InsertTimestamp」を選択します。
  2. カーソル行に開始時刻や終了時刻が正しく挿入されるはずです。

手順 6: Hotkeyを設定する

Setting > Hotkey > 「Templater: Insert Template」を検索して、任意のホットキーを設定。

メモ

how-to-use-templater-js-scripts - shabeblogの内容をコピペし、このドキュメントを元にコードを書くようにChatGPTに指示

しのしの

Obsidian タスクの開始終了 v2 (行頭にステータス絵文字つける)

機能

開始時刻・終了時刻の打刻(現在時刻のみ)
🆕行頭のステータスアイコン(絵文字)の変更

行頭ステータス絵文字の仕様

  1. 行頭に「🔲」がある場合、それを削除します。
  2. 行頭に「➡️hh:mm- 」の形式で開始時刻がない場合、「➡️hh:mm- 」を自動的に追加します。
  3. 行頭が「➡️hh:mm- 」で始まる場合、「➡️」を「✅」に置き換え、「hh:mm- 」の後に終了時刻を追加します。
  4. 行頭が「hh:mm- 」で始まる場合、「✅」を追加し、「hh:mm- 」の後に終了時刻を追加します。

コード

async function timestampTool2(tp) {
    const currentTime = new Date();
    const formattedTime = currentTime.toTimeString().slice(0, 5);

    // 現在のカーソル行を取得
    const editor = app.workspace.activeLeaf.view.editor;
    const cursorLine = editor.getCursor().line;
    const currentLineText = editor.getLine(cursorLine);

    // 正規表現で "➡️hh:mm-" または "hh:mm-" を探す
    const arrowTimeRegex = /^➡️(\d{2}:\d{2})-/;
    const startTimeRegex = /^\d{2}:\d{2}-/;
    const taskSymbolRegex = /^🔲/;

    let updatedLineText;
    let cursorPosition;

    if (arrowTimeRegex.test(currentLineText)) {
        // "➡️hh:mm-" が既にある場合、"✅"に変更して、終了時刻を追加
        updatedLineText = currentLineText.replace(arrowTimeRegex, `✅$1-${formattedTime}`);
        cursorPosition = updatedLineText.indexOf(formattedTime) + formattedTime.length;
    } else if (startTimeRegex.test(currentLineText)) {
        // "hh:mm-" が既にある場合、行頭を"✅"に変更して、終了時刻を追加
        updatedLineText = currentLineText.replace(startTimeRegex, `✅$&${formattedTime}`);
        cursorPosition = updatedLineText.indexOf(formattedTime) + formattedTime.length;
    } else {
        // hh:mm- 形式がない場合、"➡️hh:mm- "を行頭に追加し、🔲があれば消す
        const lineWithoutTaskSymbol = currentLineText.replace(taskSymbolRegex, '').trim();
        updatedLineText = `➡️${formattedTime}- ${lineWithoutTaskSymbol}`;
        cursorPosition = `➡️${formattedTime}- `.length;
    }

    // 更新されたテキストをカーソル行に挿入
    editor.setLine(cursorLine, updatedLineText);
    
    // カーソルを新しい時刻の末尾に移動
    editor.setCursor({ line: cursorLine, ch: cursorPosition });
}

module.exports = timestampTool2;
しのしの

Obsidian タスクの開始終了 v3 (過去タスクをコピーして実行する)

機能

開始時刻・終了時刻の打刻(現在時刻のみ)
行頭のステータスアイコン(絵文字)の変更
🆕過去タスクをコピーして実行

過去タスクコピーの仕様

行のテキストに開始時刻 (hh:mm-) と終了時刻 (hh:mm) を自動的に追加します。
行頭に 🔲 がある場合、これを削除して ➡️hh:mm- 形式で開始時刻を挿入します。
既に hh:mm- 形式が存在する場合、行頭を ✅ に変更し、終了時刻を追加します。
行頭が ✅hh:mm-hh:mm の場合、そのタスクをコピーし、次の行に ➡️hh:mm- 形式で新しいタスクを作成します。

更新履歴

2024.8.16 11:07 タスクコピー時のカーソル位置を修正

コード

async function timestampTool3(tp) {
    const currentTime = new Date();
    const formattedTime = currentTime.toTimeString().slice(0, 5);

    // 現在のカーソル行を取得
    const editor = app.workspace.activeLeaf.view.editor;
    const cursorLine = editor.getCursor().line;
    const currentLineText = editor.getLine(cursorLine);

    // 正規表現で "➡️hh:mm-"、"✅hh:mm-"、または "✅hh:mm-hh:mm" を探す
    const completedTaskRegex = /^✅\d{2}:\d{2}-\d{2}:\d{2}/;
    const arrowTimeRegex = /^➡️(\d{2}:\d{2})-/;
    const startTimeRegex = /^\d{2}:\d{2}-/;
    const taskSymbolRegex = /^🔲/;

    let updatedLineText;
    let cursorPosition;

    if (completedTaskRegex.test(currentLineText)) {
        // "✅hh:mm-hh:mm" が既にある場合、タスクをコピーし次の行に新しいタスクを挿入
        const lineWithoutTimes = currentLineText.replace(completedTaskRegex, '').trim();
        const newLineText = `➡️${formattedTime}- ${lineWithoutTimes}`;
        
        // 現在の行をそのままにして、次の行に新しいタスクを挿入し、その後に空行を追加
        editor.replaceRange(`${newLineText}\n`, { line: cursorLine + 1, ch: 0 });
        cursorPosition = `➡️${formattedTime}- `.length;

        // カーソルを新しい行に移動
        editor.setCursor({ line: cursorLine + 1,  ch:`➡️${formattedTime}- `.length}); // 空行の位置に移動
    } else if (arrowTimeRegex.test(currentLineText)) {
        // "➡️hh:mm-" が既にある場合、"✅"に変更して、終了時刻を追加
        updatedLineText = currentLineText.replace(arrowTimeRegex, `✅$1-${formattedTime}`);
        cursorPosition = updatedLineText.indexOf(formattedTime) + formattedTime.length;
        editor.setLine(cursorLine, updatedLineText);
        editor.setCursor({ line: cursorLine, ch: cursorPosition });
    } else if (startTimeRegex.test(currentLineText)) {
        // "hh:mm-" が既にある場合、行頭を"✅"に変更して、終了時刻を追加
        updatedLineText = currentLineText.replace(startTimeRegex, `✅$&${formattedTime}`);
        cursorPosition = updatedLineText.indexOf(formattedTime) + formattedTime.length;
        editor.setLine(cursorLine, updatedLineText);
        editor.setCursor({ line: cursorLine, ch: cursorPosition });
    } else {
        // hh:mm- 形式がない場合、"➡️hh:mm- "を行頭に追加し、🔲があれば消す
        const lineWithoutTaskSymbol = currentLineText.replace(taskSymbolRegex, '').trim();
        updatedLineText = `➡️${formattedTime}- ${lineWithoutTaskSymbol}`;
        cursorPosition = `➡️${formattedTime}- `.length;
        editor.setLine(cursorLine, updatedLineText);
        editor.setCursor({ line: cursorLine, ch: cursorPosition });
    }
}

module.exports = timestampTool3;
しのしの

1Writer タスクの開始終了 v3 (v2なし)

機能

開始時刻・終了時刻の打刻(現在時刻のみ)
行頭のステータスアイコン(絵文字)の変更
過去タスクをコピーして実行

過去タスクコピーの仕様

行のテキストに開始時刻 (hh:mm-) と終了時刻 (hh:mm) を自動的に追加します。
行頭に 🔲 がある場合、これを削除して ➡️hh:mm- 形式で開始時刻を挿入します。
既に hh:mm- 形式が存在する場合、行頭を ✅ に変更し、終了時刻を追加します。
行頭が ✅hh:mm-hh:mm の場合、そのタスクをコピーし、次の行に ➡️hh:mm- 形式で新しいタスクを作成します。

function timestampTool3() {
    const currentTime = new Date();
    const formattedTime = currentTime.toTimeString().slice(0, 5);

    // 現在のカーソル行の範囲を取得
    const selectedLineRange = editor.getSelectedLineRange();
    const cursorLineStart = selectedLineRange[0];
    const cursorLineEnd = selectedLineRange[1];
    const currentLineText = editor.getTextInRange(cursorLineStart, cursorLineEnd);

    // 正規表現で "➡️hh:mm-"、"✅hh:mm-"、または "✅hh:mm-hh:mm" を探す
    const completedTaskRegex = /^\d{2}:\d{2}-\d{2}:\d{2}/;
    const arrowTimeRegex = /^➡️(\d{2}:\d{2})-/;
    const startTimeRegex = /^\d{2}:\d{2}-/;
    const taskSymbolRegex = /^🔲/;

    let updatedLineText;

    if (completedTaskRegex.test(currentLineText)) {
        // "✅hh:mm-hh:mm" が既にある場合、タスクをコピーし次の行に新しいタスクを挿入
        const lineWithoutTimes = currentLineText.replace(completedTaskRegex, '').trim();
        const newLineText = `➡️${formattedTime}- ${lineWithoutTimes}`;

        // 現在の行をそのままにして、次の行に新しいタスクを挿入し、その後に空行を追加
        editor.replaceTextInRange(cursorLineEnd, cursorLineEnd, `\n${newLineText}`);
        const newCursorPosition = cursorLineEnd + newLineText.length + 1; // 改行分も考慮
        editor.setSelectedRange(newCursorPosition);
    } else if (arrowTimeRegex.test(currentLineText)) {
        // "➡️hh:mm-" が既にある場合、"✅"に変更して、終了時刻を追加
        updatedLineText = currentLineText.replace(arrowTimeRegex, `✅$1-${formattedTime}`);
        editor.replaceTextInRange(cursorLineStart, cursorLineEnd, updatedLineText);
        editor.setSelectedRange(cursorLineStart + updatedLineText.length);
    } else if (startTimeRegex.test(currentLineText)) {
        // "hh:mm-" が既にある場合、行頭を"✅"に変更して、終了時刻を追加
        updatedLineText = currentLineText.replace(startTimeRegex, `✅$&${formattedTime}`);
        editor.replaceTextInRange(cursorLineStart, cursorLineEnd, updatedLineText);
        editor.setSelectedRange(cursorLineStart + updatedLineText.length);
    } else {
        // hh:mm- 形式がない場合、"➡️hh:mm- "を行頭に追加し、🔲があれば消す
        const lineWithoutTaskSymbol = currentLineText.replace(taskSymbolRegex, '').trim();
        updatedLineText = `➡️${formattedTime}- ${lineWithoutTaskSymbol}`;
        editor.replaceTextInRange(cursorLineStart, cursorLineEnd, updatedLineText);
        editor.setSelectedRange(cursorLineStart + updatedLineText.length);
    }
}

timestampTool3();
しのしの

Obsidian 行移動・行削除

Hotkeyの割り当て
Cmd + J: 行を下に移動
Cmd + K: 行を上に移動

Hotkey設定済みだった
Cmd + X: 行削除

しのしの

実装したい機能

✅ステータス表示
✅行の上下移動
✅過去タスクをコピーして実行
✅リピートタスク呼び出し
🔲タスク終了後は次の行にカーソル移動
✅リピートタスク作成ボタン
⚠️タスク開始時に完了タスクの末尾にタスクを移動
🔲開始予定時刻
🔲1日分のリピートタスクコピー 祝日対応/N日毎/曜日
➡️タスク完了時に所要時間出す

いまの運用

しのしの

Obsidianプラグイン TemplaterでのConsole Log確認方法

Windowsの場合、Ctrl + Shift + I で、Google Chromeと同じような開発者ツールが表示され、JavaScriptのConsoleログも見られる。

しのしの

Obsidian プロジェクト別集計 v1

概要

このスクリプトは、ObsidianのTemplaterプラグインを使用し、✅hh:mm-hh:mm [プロジェクト名] タスク名形式で記述された作業ログから、各プロジェクトごとの合計作業時間を時間単位で計算します。計算結果は、タブ区切りで整形され、クリップボードにコピーされます。

入力形式

各行は以下の形式で記述されます。

✅hh:mm-hh:mm [プロジェクト名] タスク名

[プロジェクト名] は省略可能です。その場合、プロジェクト名は "No Project" として処理されます。

出力形式

出力はタブ区切りのテキストで、以下の形式になります。

Project   Hours
Project A 2.50
Project B 2.00
No Project 1.50

Hoursは、プロジェクトごとの合計作業時間を小数点第2位まで表示したものです。

コード

async function projectSummary(tp) {


const projectDurations = {};

// テキストを行ごとに分割
const lines = tp.file.content.split('\n');

for (const line of lines) {
    // ✅hh:mm-hh:mm のパターンにマッチする行を取得
    const match = line.match(/^✅(\d{1,2}:\d{2})-(\d{1,2}:\d{2})\s*(\[(.*?)\])?\s*(.*)/);
    if (match) {
        const startTime = match[1];
        const endTime = match[2];
        const projectName = match[4] ? match[4] : "No Project"; // プロジェクト名が空の場合は "No Project"

        // 開始時刻と終了時刻をDateオブジェクトに変換
        const [startHour, startMinute] = startTime.split(':').map(Number);
        const [endHour, endMinute] = endTime.split(':').map(Number);

        const startDate = new Date(0, 0, 0, startHour, startMinute);
        const endDate = new Date(0, 0, 0, endHour, endMinute);

        // 時間差を分単位で計算
        let durationMinutes = (endDate - startDate) / 1000 / 60;
        if (durationMinutes < 0) {
            durationMinutes += 24 * 60; // 日をまたぐ場合の補正
        }

        // プロジェクトごとの集計
        if (!projectDurations[projectName]) {
            projectDurations[projectName] = 0;
        }
        projectDurations[projectName] += durationMinutes;
    }
}

// 結果の出力を準備してクリップボードにコピー
let output = "Project\tHours\n"; // ヘッダー行を追加
for (const [projectName, totalMinutes] of Object.entries(projectDurations)) {
    const totalHours = (totalMinutes / 60).toFixed(2); // 時間単位に変換し、小数点第2位まで
    output += `${projectName}\t${totalHours}\n`;
}

if (output.trim()) {
    // クリップボードにコピー
    await navigator.clipboard.writeText(output.trim());
    console.log("Output copied to clipboard:", output.trim());  // コピーした内容をログに表示
} else {
    console.log("No output to copy");  // 出力がない場合のログ
}

}


module.exports = projectSummary;
しのしの

1Writer メモの切り出し v1

選択範囲の内容で新しいノートが作成された後、元のノートに戻り、選択されたテキストを[[新しいノート名]]リンクに置き換えることができます。

// 選択範囲を取得
const selectedText = editor.getSelectedText();
const selectedRange = editor.getSelectedRange(); // [start, end]
const originalFolderPath = editor.getFolderPath(); // 元のノートのフォルダパスを取得
const originalFileName = editor.getFileName(); // 元のノートのファイル名を取得

if (selectedText.length > 0) {
    // 選択範囲の内容を行に分割
    const lines = selectedText.split('\n');
    
    // 最初の行をタイトルとして使用
    const title = lines[0].trim();
    
    // 新しいファイルを作成
    editor.newFile(selectedText, title, function() {
        // 新しいファイルが開かれた後に、元のノートに戻る
        const originalNotePath = `${originalFolderPath}/${originalFileName}`;
        
        editor.openFile(originalNotePath, 'edit', function() {
            // 元のノートに[[ノート名]]を挿入
            const link = `[[${title}]]`;
            editor.replaceTextInRange(selectedRange[0], selectedRange[1], link);
        });
    });
} else {
    // 選択範囲がない場合のエラーメッセージ
    app.showMessage("選択範囲がありません。", "Error");
}
しのしの

1Writer ルーチン作成

カーソル行のタスクを「リピートタスク.md」ファイルに追加するコード。

これで、リピートタスクを開いてタスクをコピペする手間が省け、ルーチン登録がしやすくなる。

カーソル行のテキストを取得し、特定の記号や時間パターンを処理し行頭を 🔁 に置き換えたタスクを生成。その後、「リピートタスク.md」ファイルにタスクを追加して保存します。

// カーソル行のテキストを取得
const cursorRange = editor.getSelectedLineRange();  // カーソル行の範囲を取得
const cursorLineText = editor.getTextInRange(cursorRange[0], cursorRange[1]);  // カーソル行のテキストを取得

// 行頭の特定の記号とその後の時間パターンに一致する正規表現
const timePattern = /^\d{2}:\d{2}-\d{2}:\d{2}\s*/;
const symbolAndTimePattern = /^[🔲➡️✅⏰]\d{2}:\d{2}-\d{2}:\d{2}\s*/;
const symbolPattern = /^[🔲➡️✅]/;

let newTask;
if (symbolAndTimePattern.test(cursorLineText)) {
    // 記号と時間パターンがある場合、その部分を削除し、行頭に🔁を追加
    newTask = '🔁' + cursorLineText.replace(symbolAndTimePattern, '').trim();
} else if (symbolPattern.test(cursorLineText)) {
    // 記号のみの場合、その記号を🔁に置き換える
    newTask = '🔁' + cursorLineText.replace(symbolPattern, '').trim();
} else {
    // パターンがない場合、行頭に🔁を追加
    newTask = '🔁' + cursorLineText.trim();
}

// リピートタスク.md のパスを定義
const repeatTaskFilePath = editor.getFolderPath() + "/リピートタスク.md";

// リピートタスク.md の内容を取得し、リストとして表示
editor.openFile(repeatTaskFilePath, 'edit', function() {
    const repeatTaskContent = editor.getText();
    const taskList = repeatTaskContent.split('\n').filter(line => line.trim() !== '');

    // リストとして表示し、追加位置を選択
    ui.list('タスクを挿入する場所を選択してください', taskList, false, (selectedValues, selectedIndices) => {
        if (selectedIndices !== undefined && selectedIndices.length > 0) {
            const insertIndex = selectedIndices[0] + 1; // 選択したタスクの下に挿入するため+1
            
            // 新しいタスクを選択した位置の下に挿入
            taskList.splice(insertIndex, 0, newTask);
            const updatedContent = taskList.join('\n');

            // リピートタスク.md の内容を更新
            editor.setText(updatedContent);
            ui.hudSuccess("リピートタスクに追加されました。");
        } else {
            ui.hudError("タスクの挿入がキャンセルされました。");
        }
    });
});
しのしの

1Writer タスクの開始終了 v4.1

機能

開始時刻・終了時刻の打刻(現在時刻のみ)
行頭のステータスアイコン(絵文字)の変更
過去タスクをコピーして実行
🆕行頭アイコンが🔁の場合にも対応

⚠️🔁記号が頭についていると、コピー後のタスク名前に�がつく。

行頭が🔁の場合の挙動

🔲と同じ扱い

コード

function timestampTool() {
    const currentTime = new Date();
    const formattedTime = currentTime.toTimeString().slice(0, 5);

    // 現在のカーソル行の範囲を取得
    const selectedLineRange = editor.getSelectedLineRange();
    const cursorLineStart = selectedLineRange[0];
    const cursorLineEnd = selectedLineRange[1];
    const currentLineText = editor.getTextInRange(cursorLineStart, cursorLineEnd);

    // 正規表現で各パターンを定義
    const completedTaskRegex = /^✅\d{2}:\d{2}-\d{2}:\d{2}/;
    const arrowTimeRegex = /^➡️(\d{2}:\d{2})-/;
    const startTimeRegex = /^\d{2}:\d{2}-/;
    const taskSymbolRegex = /^[🔲🔁]/; // 🔲 または 🔁 に一致

    let updatedLineText;

    if (completedTaskRegex.test(currentLineText)) {
        // "✅hh:mm-hh:mm" が既にある場合、新しいタスクを次の行に追加
        const lineWithoutTimes = currentLineText.replace(completedTaskRegex, '').trim();
        const newLineText = `➡️${formattedTime}- ${lineWithoutTimes}`;

        // 現在の行をそのままにして、次の行に新しいタスクを挿入
        editor.replaceTextInRange(cursorLineEnd, cursorLineEnd, `\n${newLineText}`);
        const newCursorPosition = cursorLineEnd + newLineText.length + 1; // 改行分も考慮
        editor.setSelectedRange(newCursorPosition);
    } else if (arrowTimeRegex.test(currentLineText)) {
        // "➡️hh:mm-" が既にある場合、"✅"に変更して終了時刻を追加
        updatedLineText = currentLineText.replace(arrowTimeRegex, `✅$1-${formattedTime}`);
        editor.replaceTextInRange(cursorLineStart, cursorLineEnd, updatedLineText);
        editor.setSelectedRange(cursorLineStart + updatedLineText.length);
    } else if (startTimeRegex.test(currentLineText)) {
        // "hh:mm-" が既にある場合、行頭を"✅"に変更して終了時刻を追加
        updatedLineText = currentLineText.replace(startTimeRegex, `✅$&${formattedTime}`);
        editor.replaceTextInRange(cursorLineStart, cursorLineEnd, updatedLineText);
        editor.setSelectedRange(cursorLineStart + updatedLineText.length);
    } else {
        // hh:mm- 形式がない場合、"➡️hh:mm- "を行頭に追加し、🔲または🔁があれば削除
        const lineWithoutTaskSymbol = currentLineText.replace(taskSymbolRegex, '').trim();
        updatedLineText = `➡️${formattedTime}- ${lineWithoutTaskSymbol}`;
        editor.replaceTextInRange(cursorLineStart, cursorLineEnd, updatedLineText);
        editor.setSelectedRange(cursorLineStart + updatedLineText.length);
    }
}

timestampTool();
しのしの

1Writer リピートタスクからタスク入力

ファイルからリストを作成して、リピートタスクを呼び出す〜1Writerカスタマイズ④〜 - iPhoneと本と数学となんやかんやとのコードを参考にさせていただきました。

編集中のノートに「リピートタスク.md」から選んだタスクをカーソル位置に挿入します。

// アクションを実行したあと、元のファイルに戻るため、今編集中ファイルのフォルダとファイル名、カーソル位置を取得
var folder = editor.getFolderPath();
var editingfile = editor.getFileName();
var cursorPosition = editor.getSelectedRange(); // カーソル位置を保存

// リピートタスクを記述しているファイルのファイル名
var openfilename = 'リピートタスク.md';

// リピートタスクの一覧が記述されているファイルを開く。フォルダは、そのファイルが保存されているものを指定してください。
editor.openFile(folder + '/' + openfilename, 'edit', call);

function call() {
    // 開いたファイルに記述されているテキストを取得
    var text = editor.getText();
    // 改行ごとに配列に格納
    const listData = text.split('\n');
    ui.hudDismiss();

    // 配列をリストに
    ui.list('Repeat', listData, false, selectedValues => {
        if (!selectedValues) {
            return;
        }
        const selectedText = selectedValues.join('\n');

        // はじめに編集中だったファイルを開く
        editor.openFile(folder + '/' + editingfile, 'edit', function() {
            // カーソル位置に選択したテキストを挿入
            editor.setSelectedRange(cursorPosition[0]); // 保存していたカーソル位置に戻る
            editor.replaceSelection(selectedText); // 選択されたテキストを挿入
            
            // 挿入されたテキストの直後にカーソルを移動
            editor.setSelectedRange(cursorPosition[0] + selectedText.length);
        });
    });
}

しのしの

1Writer リピートタスクのセット v1

リピートタスク.mdから行頭が🔁または⏰をコピーして、編集中ノートのカーソル位置に貼り付け

// ノート情報の保存
var folder = editor.getFolderPath();
var editingfile = editor.getFileName();
var cursorPosition = editor.getSelectedRange(); // カーソル位置を保存

// リピートタスクを記述しているファイルのファイル名
var openfilename = 'リピートタスク.md';

// リピートタスクのファイルを開く
editor.openFile(folder + '/' + openfilename, 'edit', call);

function call() {
    // ファイルのテキストを取得
    var text = editor.getText();
    // 🔁 または ⏰ で始まる行のみをフィルタリング
    const listData = text.split('\n').filter(line => line.startsWith('🔁') || line.startsWith('⏰'));
    ui.hudDismiss();

    if (listData.length === 0) {
        ui.alert("🔁または⏰で始まるタスクが見つかりませんでした。");
        return;
    }

    const selectedText = listData.join('\n'); // すべてのタスクを1つの文字列に結合

    // 元のノートに戻り、カーソル位置にすべてのタスクを挿入
    editor.openFile(folder + '/' + editingfile, 'edit', function() {
        editor.setSelectedRange(cursorPosition[0]); // カーソル位置に戻る
        editor.replaceSelection(selectedText); // 選択されたテキストを挿入
        editor.setSelectedRange(cursorPosition[0] + selectedText.length); // カーソルを挿入後の位置に移動
    });
}
しのしの

1Writer リピートタスクのセット v2

概要

ノートのタイトル日付(YYYY-MM-DD形式)から平日かどうかを判定し、平日の場合のみ、リピートタスクファイル(同じフォルダ内)から 🔁 や ⏰ で始まるタスクをフィルタリングして、現在のカーソル位置に挿入します。
なお、祝日判定はできません。

動作概要

日付判定: ノートタイトルの日付を解析し、平日かどうかを確認。
タスクのフィルタリング: 🔁 や ⏰ で始まるタスクをフィルタリングし、🔁平日 のタスクは平日の場合のみ含める。
タスクの挿入: フィルタリングされたタスクをカーソル位置に挿入し、平日 などの繰り返し条件を削除して挿入。

コード

// ノート情報の保存
var folder = editor.getFolderPath(); // 現在のノートのフォルダパスを取得
var editingfile = editor.getFileName();
var cursorPosition = editor.getSelectedRange(); // カーソル位置を保存

// ノートタイトルから日付部分を取得(例: "2023-08-20")
var datePattern = /^(\d{4})-(\d{2})-(\d{2})/;
var match = editingfile.match(datePattern);

if (!match) {
    ui.alert("ノートのタイトルに有効な日付が含まれていません。");
    return;
}

// 日付オブジェクトを作成
var noteDate = new Date(match[1], match[2] - 1, match[3]); // 月は0から始まるため-1

// ノートの日付から曜日を取得(0: 日曜, 1: 月曜, ..., 6: 土曜)
var dayOfWeek = noteDate.getDay();

// 平日かどうかを判定(1: 月曜 ~ 5: 金曜)
var isWeekday = (dayOfWeek >= 1 && dayOfWeek <= 5);

// リピートタスクを記述しているファイルのファイル名
var openfilename = 'リピートタスク.md';

// リピートタスクのファイルを開く(現在のフォルダ内)
editor.openFile(folder + '/' + openfilename, 'edit', call);

function call() {
    // ファイルのテキストを取得
    var text = editor.getText();

    // 🔁 または ⏰ で始まる行のみをフィルタリング
    let listData = text.split('\n').filter(line => {
        if (line.startsWith('🔁平日')) {
            return isWeekday; // ノートの日付が平日の場合のみ含める
        }
        return line.startsWith('🔁') || line.startsWith('⏰');
    }).map(line => {
        // 繰り返し条件("平日"および後続のスペース)を削除
        if (line.startsWith('🔁平日')) {
            return line.replace('平日 ', '').trim();
        }
        return line;
    });

    ui.hudDismiss();

    if (listData.length === 0) {
        ui.alert("🔁または⏰で始まるタスクが見つかりませんでした。");
        return;
    }

    const selectedText = listData.join('\n'); // すべてのタスクを1つの文字列に結合

    // 元のノートに戻り、カーソル位置にすべてのタスクを挿入
    editor.openFile(folder + '/' + editingfile, 'edit', function() {
        editor.setSelectedRange(cursorPosition[0]); // カーソル位置に戻る
        editor.replaceSelection(selectedText); // 選択されたテキストを挿入
        editor.setSelectedRange(cursorPosition[0] + selectedText.length); // カーソルを挿入後の位置に移動
    });
}
しのしの

1Writer タスクの開始終了 v5

機能

開始時刻・終了時刻の打刻(現在時刻のみ)
行頭のステータスアイコン(絵文字)の変更
過去タスクをコピーして実行
行頭アイコンが🔁の場合にも対応
🆕タスク実行時にタスクを移動

⚠️タスクコピー時のタスク移動がうまくいかない

タスク移動の仕様

新しいタスクは、最後の ✅ タスクの直後で、次の未完了タスク(🔲、⏰、🔁)の1行手前に移動します。
未完了タスクがない場合は、リストの最後に追加します。

履歴

2024.8.17 10:27 終了時刻入力後のカーソル位置変更

コード

function timestampTool() {
    const currentTime = new Date();
    const formattedTime = currentTime.toTimeString().slice(0, 5);

    // 現在のカーソル行の範囲を取得
    const selectedLineRange = editor.getSelectedLineRange();
    const cursorLineStart = selectedLineRange[0];
    const cursorLineEnd = selectedLineRange[1];
    let currentLineText = editor.getTextInRange(cursorLineStart, cursorLineEnd);

    // 正規表現で各パターンを定義
    const completedTaskRegex = /^\d{2}:\d{2}-\d{2}:\d{2}/;
    const arrowTimeRegex = /^➡️(\d{2}:\d{2})-/;
    const startTimeRegex = /^\d{2}:\d{2}-/;
    const taskSymbolRegex = /^[🔲🔁⏰]/; // 🔲 または 🔁 または ⏰ に一致

    let movedTask = "";

    if (completedTaskRegex.test(currentLineText)) {
        // "✅hh:mm-hh:mm" が既にある場合、新しいタスクを次の行に追加
        const lineWithoutTimes = currentLineText.replace(completedTaskRegex, '').trim();
        movedTask = `➡️${formattedTime}- ${lineWithoutTimes}`;
    } else if (arrowTimeRegex.test(currentLineText)) {
        // "➡️hh:mm-" が既にある場合、"✅"に変更して終了時刻を追加
        movedTask = currentLineText.replace(arrowTimeRegex, `✅$1-${formattedTime}`);
    } else if (startTimeRegex.test(currentLineText)) {
        // "hh:mm-" が既にある場合、行頭を"✅"に変更して終了時刻を追加
        movedTask = currentLineText.replace(startTimeRegex, `✅$&${formattedTime}`);
    } else {
        // hh:mm- 形式がない場合、"➡️hh:mm- "を行頭に追加し、🔲または🔁または⏰があれば削除
        const lineWithoutTaskSymbol = currentLineText.replace(taskSymbolRegex, '').trim();
        movedTask = `➡️${formattedTime}- ${lineWithoutTaskSymbol}`;
    }

    // 現在の行を削除する(行の終わりの改行も含めて削除)
    const lineEndPosition = cursorLineEnd + 1; // 行の終わり+改行
    editor.replaceTextInRange(cursorLineStart, lineEndPosition, "");

    // ドキュメント全体を取得し、行ごとに分割
    const allText = editor.getText();
    const lines = allText.split('\n');

    let lastCompletedTaskIndex = -1;
    let nextUncompletedTaskIndex = -1;

    // 行を上から順にチェック
    for (let i = 0; i < lines.length; i++) {
        if (completedTaskRegex.test(lines[i])) {
            lastCompletedTaskIndex = i;
        } else if (taskSymbolRegex.test(lines[i]) && lastCompletedTaskIndex !== -1) {
            nextUncompletedTaskIndex = i;
            break;
        }
    }

    // 次の未完了タスクが見つかった場合、その1行手前にタスクを挿入
    if (nextUncompletedTaskIndex !== -1) {
        lines.splice(nextUncompletedTaskIndex, 0, movedTask);
    } else {
        // 未完了タスクが見つからない場合、最後に追加
        lines.push(movedTask);
    }

    // 全体テキストを更新
    const updatedText = lines.join('\n');
    editor.setText(updatedText);

    // カーソルを移動したタスク行に設定
    const newCursorPosition = lines.indexOf(movedTask);
    const targetPosition = lines.slice(0, newCursorPosition).join('\n').length + movedTask.length + 1;
    editor.setSelectedRange(targetPosition);
}

timestampTool();

しのしの

1Writer タスクの開始終了 v6

機能

開始時刻(前回タスクの終了時刻)・終了時刻の打刻
行頭のステータスアイコン(絵文字)の変更
過去タスクをコピーして実行
行頭アイコンが🔁の場合にも対応
タスク実行時にタスクを移動
🆕前回タスクの完了時刻を開始時刻とする

⚠️タスクコピー時のタスク移動がうまくいかないことがある

function timestampTool() {
    const currentTime = new Date();
    const formattedTime = currentTime.toTimeString().slice(0, 5);

    // 現在のカーソル行の範囲を取得
    const selectedLineRange = editor.getSelectedLineRange();
    const cursorLineStart = selectedLineRange[0];
    const cursorLineEnd = selectedLineRange[1];
    let currentLineText = editor.getTextInRange(cursorLineStart, cursorLineEnd);

    // 正規表現で各パターンを定義
    const completedTaskRegex = /^\d{2}:\d{2}-(\d{2}:\d{2})/;
    const arrowTimeRegex = /^➡️(\d{2}:\d{2})-/;
    const startTimeRegex = /^(\d{2}:\d{2})-/;
    const taskSymbolRegex = /^[🔲🔁⏰]/; // 🔲 または 🔁 または ⏰ に一致

    // ドキュメント全体を取得し、行ごとに分割
    let allText = editor.getText();
    let lines = allText.split('\n');

    let latestEndTime = "00:00"; // 最も遅い終了時刻を初期化

    // 各行をチェックし、最も遅い終了時刻を特定
    lines.forEach(line => {
        const match = line.match(completedTaskRegex);
        if (match) {
            const endTime = match[1];
            if (endTime > latestEndTime) {
                latestEndTime = endTime;
            }
        }
    });

    let movedTask = "";

    if (completedTaskRegex.test(currentLineText)) {
        // "✅hh:mm-hh:mm" が既にある場合、新しいタスクを次の行に追加
        const lineWithoutTimes = currentLineText.replace(completedTaskRegex, '').trim();
        movedTask = `➡️${latestEndTime}- ${lineWithoutTimes}`;
    } else if (arrowTimeRegex.test(currentLineText)) {
        // "➡️hh:mm-" が既にある場合、"✅"に変更して終了時刻を追加
        movedTask = currentLineText.replace(arrowTimeRegex, `✅$1-${formattedTime}`);
    } else if (startTimeRegex.test(currentLineText)) {
        // "hh:mm-" が既にある場合、行頭を"✅"に変更して終了時刻を追加
        movedTask = currentLineText.replace(startTimeRegex, `✅$&${formattedTime}`);
    } else {
        // hh:mm- 形式がない場合、"➡️hh:mm- "を行頭に追加し、🔲または🔁または⏰があれば削除
        const lineWithoutTaskSymbol = currentLineText.replace(taskSymbolRegex, '').trim();
        movedTask = `➡️${latestEndTime}- ${lineWithoutTaskSymbol}`;
    }

    // 現在の行を削除する(行の終わりの改行も含めて削除)
    const lineEndPosition = cursorLineEnd + 1; // 行の終わり+改行
    editor.replaceTextInRange(cursorLineStart, lineEndPosition, "");

    // ドキュメント全体を取得し、行ごとに分割
    allText = editor.getText();
    lines = allText.split('\n');

    let lastCompletedTaskIndex = -1;
    let nextUncompletedTaskIndex = -1;

    // 行を上から順にチェック
    for (let i = 0; i < lines.length; i++) {
        if (completedTaskRegex.test(lines[i])) {
            lastCompletedTaskIndex = i;
        } else if (taskSymbolRegex.test(lines[i]) && lastCompletedTaskIndex !== -1) {
            nextUncompletedTaskIndex = i;
            break;
        }
    }

    // 次の未完了タスクが見つかった場合、その1行手前にタスクを挿入
    if (nextUncompletedTaskIndex !== -1) {
        lines.splice(nextUncompletedTaskIndex, 0, movedTask);
    } else {
        // 未完了タスクが見つからない場合、最後に追加
        lines.push(movedTask);
    }

    // 全体テキストを更新
    const updatedText = lines.join('\n');
    editor.setText(updatedText);

    // カーソルを移動したタスク行に設定
    const newCursorPosition = lines.indexOf(movedTask);
    const targetPosition = lines.slice(0, newCursorPosition).join('\n').length + movedTask.length + 1;
    editor.setSelectedRange(targetPosition);
}

timestampTool();

しのしの

1Writer 過去タスクリスト作成

過去タスク.mdに完了タスクをコピーし、行頭に実行日をつける。

コード

// Get the file name and folder path of the currently edited note
const noteTitle = editor.getFileName();
const folderPath = editor.getFolderPath();

// Extract the date (YYYY-MM-DD) from the note title
const date = noteTitle.match(/\d{4}-\d{2}-\d{2}/)[0];

// Regular expression to find tasks that start with ✅
const taskRegex = /^.*$/gm;

// Get all content from the currently edited note
const content = editor.getText();

// Extract all completed tasks (those starting with ✅)
const completedTasks = content.match(taskRegex) || [];

// Check if there are any completed tasks to append
if (completedTasks.length > 0) {
    // Prepend the date to each completed task
    const tasksToAppend = completedTasks.map(task => `${task.replace(/^/, `${date} `)}`).join('\n') + '\n';
    
    // Path to the "過去タスク.md" file in the same folder as the current note
    const filePath = folderPath + "/過去タスク.md";
    
    // Open the "過去タスク.md" file in edit mode, append the tasks, and save it
    editor.openFile(filePath, 'edit', function() {
        const fileContent = editor.getText();
        
        // Calculate the insertion point
        const insertionPoint = fileContent.length;
        
        // Append the tasks to the file content
        editor.setText(fileContent + '\n' + tasksToAppend);
        
        // Reopen the file to place the cursor at the correct location
        editor.setSelectedRange(insertionPoint + 1, insertionPoint + 1);
    });
} else {
    // Display an error HUD if no completed tasks are found
    ui.hudError("完了したタスクが見つかりませんでした。");
}
しのしの

1Writer ノート内の完了タスクの所要時間計算

変換前
✅08:00-09:15 朝の散歩
✅14:30-15:00 読書

変換後
✅08:00-09:15 (1:15) 朝の散歩
✅14:30-15:00 (0:30) 読書

// Regular expression to find tasks that start with ✅ and a time range (hh:mm-hh:mm)
const taskRegex = /^✅(\d{2}:\d{2})-(\d{2}:\d{2})(\s\(\d+:\d{2}\))?\s(.*)$/gm;

// Get all content from the currently edited note
let content = editor.getText();

// Function to calculate the duration in h:mm format
function calculateDuration(startTime, endTime) {
    const [startHour, startMinute] = startTime.split(':').map(Number);
    const [endHour, endMinute] = endTime.split(':').map(Number);

    let durationMinutes = (endHour * 60 + endMinute) - (startHour * 60 + startMinute);
    if (durationMinutes < 0) {
        // Handle cases where the end time is on the next day
        durationMinutes += 24 * 60;
    }

    const hours = Math.floor(durationMinutes / 60);
    const minutes = durationMinutes % 60;

    return `${hours}:${minutes.toString().padStart(2, '0')}`;
}

// Replace the content with updated tasks
content = content.replace(taskRegex, (match, startTime, endTime, existingDuration, taskDescription) => {
    const duration = calculateDuration(startTime, endTime);
    return `✅${startTime}-${endTime} (${duration}) ${taskDescription}`;
});

// Set the updated content back to the editor
editor.setText(content);
しのしの

1Writer ui.listの挙動

ui.list('リストのタイトル', ['title|value|subtitle'], false, function(selectedValues) {
    // 選択したアイテムに対する処理
});

しのしの

1Writer 過去タスク検索

過去タスク.mdのログをリスト表示
このリストは、検索窓で検索して絞り込める
リストのいずれかクリックするとリストが閉じる

// 過去タスクファイルのパス
const folderPath = editor.getFolderPath();
const filePath = folderPath + "/過去タスク.md";

// 現在のファイル名
const originalFileName = editor.getFileName();

// 現在のカーソル位置を保存
const originalCursorPosition = editor.getSelectedRange();

// 過去タスクファイルを開いて、内容を取得
editor.openFile(filePath, 'edit', function() {
    const fileContent = editor.getText();
    
    // タスクを格納する配列
    const tasks = [];
    
    // 正規表現でタスクを抽出
    const taskRegex = /^\s(\d{4}-\d{2}-\d{2})\s(\d{2}:\d{2}-\d{2}:\d{2})\s\((\d+:\d{2})\)\s(.*)$/gm;
    let match;
    
    // タスクが見つかるたびに配列に追加
    while ((match = taskRegex.exec(fileContent)) !== null) {
        const date = match[1];           // 日付
        const timeRange = match[2];      // 時間範囲
        const duration = match[3];       // 所要時間
        const taskName = match[4];       // タスク名

        // サブタイトルとして日時と所要時間を表示
        const subtitle = `${date} ${timeRange} (${duration})`;
        
        // タスクをリストに追加(タスク行全体を value として保持)
        tasks.push({ title: taskName, subtitle, value: match[0] });
    }
    
    if (tasks.length > 0) {
        // タスクリストを表示
        showTaskList(tasks);
    } else {
        ui.hudError('タスクが見つかりませんでした。');
    }
});

// タスクリストを表示する関数
function showTaskList(tasks) {
    const listItems = tasks.map(task => `${task.title}|${task.value}|${task.subtitle}`);
    
    ui.list('すべてのタスク', listItems, false, function() {
        // リストを閉じたときに元のファイルの元のカーソル位置に戻る
        editor.openFile(folderPath + '/' + originalFileName, 'edit', function() {
            editor.setSelectedRange(originalCursorPosition[0], originalCursorPosition[1]);
        });
    });
}
しのしの

1Writer タスクの開始終了 v7

機能

開始時刻(前回タスクの終了時刻)・終了時刻の打刻
行頭のステータスアイコン(絵文字)の変更
過去タスクをコピーして実行
行頭アイコンが🔁の場合にも対応
タスク実行時にタスクを移動
🆕タスク終了時に所要時間を入力

⚠️完了タスクをコピーして実行する際、選択した完了タスクの下にコメントがあるとタスク移動が適切に行われない。
⇒ コピーしたい完了タスクが完了タスクのうち一番したにある場合、かつ、コメントがある場合のみなので、運用でカバー。気づいたらコメントを上に手作業で移動する。

実行前
✅17:08-17:36 (0:28) リファクタリング
コメント
🔲未実行タスク

実行後 (期待する挙動)
✅17:08-17:36 (0:28) リファクタリング
コメント
➡️17:36- リファクタリング
🔲未実行タスク

実行後 (実際の挙動)
✅17:08-17:36 (0:28) リファクタリング
➡️17:36- リファクタリング
コメント
🔲未実行タスク

所要時間

(h:mm)形式
日付跨ぎなどは未考慮

出力例
✅14:04-14:08 (0:04) 読書

コード

function timestampTool() {
    const currentTime = new Date();
    const formattedTime = currentTime.toTimeString().slice(0, 5);

    // 現在のカーソル行の範囲を取得
    const selectedLineRange = editor.getSelectedLineRange();
    const cursorLineStart = selectedLineRange[0];
    const cursorLineEnd = selectedLineRange[1];
    let currentLineText = editor.getTextInRange(cursorLineStart, cursorLineEnd);

    // 正規表現で各パターンを定義
    const completedTaskWithOptionalDurationRegex = /^(\d{2}:\d{2})-(\d{2}:\d{2})(\s\(\d+:\d{2}\))?/;
    const arrowTimeRegex = /^➡️(\d{2}:\d{2})-/;
    const startTimeRegex = /^(\d{2}:\d{2})-/;
    const taskSymbolRegex = /^[🔲🔁⏰]/; // 🔲 または 🔁 または ⏰ に一致

    // ドキュメント全体を取得し、行ごとに分割
    let allText = editor.getText();
    let lines = allText.split('\n');

    let latestEndTime = "00:00"; // 最も遅い終了時刻を初期化

    // 各行をチェックし、最も遅い終了時刻を特定
    lines.forEach(line => {
        const match = line.match(completedTaskWithOptionalDurationRegex);
        if (match) {
            const endTime = match[2];
            if (endTime > latestEndTime) {
                latestEndTime = endTime;
            }
        }
    });

    let movedTask = "";

    if (completedTaskWithOptionalDurationRegex.test(currentLineText)) {
        // "✅hh:mm-hh:mm (h:mm)" が既にある場合、所要時間があってもなくても削除し、新しいタスクを次の行に追加
        currentLineText = currentLineText.replace(/\s\(\d+:\d{2}\)/, ''); // 所要時間を削除
        const lineWithoutTimes = currentLineText.replace(completedTaskWithOptionalDurationRegex, '').trim();
        movedTask = `➡️${latestEndTime}- ${lineWithoutTimes}`;

    } else if (arrowTimeRegex.test(currentLineText)) {
        // "➡️hh:mm-" が既にある場合、"✅"に変更して終了時刻と所要時間を追加
        movedTask = currentLineText.replace(arrowTimeRegex, (match, startTime) => {
            const duration = calculateDuration(startTime, formattedTime);
            return `${startTime}-${formattedTime} (${duration}) `;
        });

        // 現在の行を削除する処理を実行
        removeCurrentLine(cursorLineStart, cursorLineEnd);

    } else if (startTimeRegex.test(currentLineText)) {
        // "hh:mm-" が既にある場合、行頭を"✅"に変更して終了時刻と所要時間を追加
        movedTask = currentLineText.replace(startTimeRegex, (match, startTime) => {
            const duration = calculateDuration(startTime, formattedTime);
            return `${startTime}-${formattedTime} (${duration}) `;
        });

        // 現在の行を削除する処理を実行
        removeCurrentLine(cursorLineStart, cursorLineEnd);

    } else {
        // hh:mm- 形式がない場合、"➡️hh:mm- "を行頭に追加し、🔲または🔁または⏰があれば削除
        const lineWithoutTaskSymbol = currentLineText.replace(taskSymbolRegex, '').trim();
        movedTask = `➡️${latestEndTime}- ${lineWithoutTaskSymbol}`;

        // 現在の行を削除する処理を実行
        removeCurrentLine(cursorLineStart, cursorLineEnd);
    }

    // ドキュメント全体を取得し、行ごとに分割
    allText = editor.getText();
    lines = allText.split('\n');

    let lastCompletedTaskIndex = -1;
    let nextUncompletedTaskIndex = -1;

    // 行を上から順にチェック
    for (let i = 0; i < lines.length; i++) {
        if (completedTaskWithOptionalDurationRegex.test(lines[i])) {
            lastCompletedTaskIndex = i;
        } else if (taskSymbolRegex.test(lines[i]) && lastCompletedTaskIndex !== -1) {
            nextUncompletedTaskIndex = i;
            break;
        }
    }

    // 次の未完了タスクが見つかった場合、その1行手前にタスクを挿入
    if (nextUncompletedTaskIndex !== -1) {
        lines.splice(nextUncompletedTaskIndex, 0, movedTask);
    } else {
        // 未完了タスクが見つからない場合、最後に追加
        lines.push(movedTask);
    }

    // 全体テキストを更新
    const updatedText = lines.join('\n');
    editor.setText(updatedText);

    // カーソルを移動したタスク行に設定
    const newCursorPosition = lines.indexOf(movedTask);
    const targetPosition = lines.slice(0, newCursorPosition).join('\n').length + movedTask.length + 1;
    editor.setSelectedRange(targetPosition);
}

// 現在の行を削除する関数
function removeCurrentLine(cursorLineStart, cursorLineEnd) {
    const lineEndPosition = cursorLineEnd + 1; // 行の終わり+改行
    editor.replaceTextInRange(cursorLineStart, lineEndPosition, "");
}

// 所要時間を計算する関数
function calculateDuration(startTime, endTime) {
    const [startHour, startMinute] = startTime.split(':').map(Number);
    const [endHour, endMinute] = endTime.split(':').map(Number);

    let durationMinutes = (endHour * 60 + endMinute) - (startHour * 60 + startMinute);
    if (durationMinutes < 0) {
        // 終了時刻が開始時刻より前の場合、翌日にまたがっていると仮定
        durationMinutes += 24 * 60;
    }

    const hours = Math.floor(durationMinutes / 60);
    const minutes = durationMinutes % 60;

    return `${hours}:${minutes.toString().padStart(2, '0')}`;
}

timestampTool();
しのしの

1Writer タスクの開始終了 v7.1

期待する挙動

IF (hh:mm-hh:mmを含む) {
タスクをコピーして開始
行頭が✅➡️🔲🔁⏰であれば、削除
"hh:mm-hh:mm "部分を削除
行頭に"➡️hh:mm "(hh:mmは直前タスクの終了時刻)を設定
} ELSE IF (hh:mm-を含む) {
タスクの終了
行頭が✅➡️🔲🔁⏰であれば、削除
行頭に"✅hh:mm-hh:mm (h:mm)" (1つ目のhh:mmはあらかじめ入っていた値、2つ目のhh:mmは現在時刻、h:mmは終了時刻から開始時刻を引いた数)

} ELSE {
タスクの開始
行頭が✅🔲🔁⏰であれば、削除
行頭に"➡️hh:mm "(hh:mmは直前タスクの終了時刻)を設定
}

IF(タスクの終了){
カーソル行タスクの終了処理を実施
} ELSE IF (タスクの開始 OR タスクをコピーして開始) {
IF(次の未完了タスクが見つかった場合) {
その1行手前にタスクを挿入
} ELSE {
未完了タスクが見つからない場合、最後に追加
}
カーソル行タスクを削除
}

コード 開始時刻=現在時刻

function timestampTool() {
    const currentTime = new Date();
    const formattedTime = currentTime.toTimeString().slice(0, 5);

    // 現在のカーソル行の範囲を取得
    const selectedLineRange = editor.getSelectedLineRange();
    const cursorLineStart = selectedLineRange[0];
    const cursorLineEnd = selectedLineRange[1];
    let currentLineText = editor.getTextInRange(cursorLineStart, cursorLineEnd);

    // 正規表現で各パターンを定義
    const completedTaskRegex = /^\d{2}:\d{2}-(\d{2}:\d{2})/;
    const startTimeRegex = /^[➡️🔲🔁⏰✅]+(\d{2}:\d{2})-/;
    const taskSymbolRegex = /^[🔲🔁⏰]/; // 🔲 または 🔁 または ⏰ に一致

    // ドキュメント全体を取得し、行ごとに分割
    let allText = editor.getText();
    let lines = allText.split('\n');

    let latestEndTime = "00:00"; // 最も遅い終了時刻を初期化

    // 各行をチェックし、最も遅い終了時刻を特定
    lines.forEach(line => {
        const match = line.match(completedTaskRegex);
        if (match) {
            const endTime = match[1];
            if (endTime > latestEndTime) {
                latestEndTime = endTime;
            }
        }
    });

    let updatedTask = "";

    if (completedTaskRegex.test(currentLineText)) {
        // "✅hh:mm-hh:mm" が既にある場合、新しいタスクを次の行に追加
        const lineWithoutTimes = currentLineText.replace(completedTaskRegex, '').trim();
        updatedTask = `➡️${formattedTime}- ${lineWithoutTimes}`;
    } else if (startTimeRegex.test(currentLineText)) {
        updatedTask = currentLineText.replace(startTimeRegex, (match, startTime) => {
            const duration = calculateDuration(startTime, formattedTime);
            return `${startTime}-${formattedTime} (${duration})`;
        });
        editor.replaceTextInRange(cursorLineStart, cursorLineEnd, updatedTask);
        editor.setSelectedRange(cursorLineStart + updatedTask.length);
        return; // ここで処理を終了
    } else {
        // hh:mm- 形式がない場合、"➡️hh:mm- "を行頭に追加し、🔲または🔁または⏰があれば削除
        const lineWithoutTaskSymbol = currentLineText.replace(taskSymbolRegex, '').trim();
        updatedTask = `➡️${formattedTime}- ${lineWithoutTaskSymbol}`;
        // 現在の行を削除する(行の終わりの改行も含めて削除)
        const lineEndPosition = cursorLineEnd + 1; // 行の終わり+改行
        editor.replaceTextInRange(cursorLineStart, lineEndPosition, "");
    }

    

    // ドキュメント全体を取得し、行ごとに分割
    allText = editor.getText();
    lines = allText.split('\n');

    let lastCompletedTaskIndex = -1;
    let nextUncompletedTaskIndex = -1;

    // 行を上から順にチェック
    for (let i = 0; i < lines.length; i++) {
        if (completedTaskRegex.test(lines[i])) {
            lastCompletedTaskIndex = i;
        } else if (taskSymbolRegex.test(lines[i]) && lastCompletedTaskIndex !== -1) {
            nextUncompletedTaskIndex = i;
            break;
        }
    }

    // 次の未完了タスクが見つかった場合、その1行手前にタスクを挿入
    if (nextUncompletedTaskIndex !== -1) {
        lines.splice(nextUncompletedTaskIndex, 0, updatedTask);
    } else {
        // 未完了タスクが見つからない場合、最後に追加
        lines.push(updatedTask);
    }

    // 全体テキストを更新
    const updatedText = lines.join('\n');
    editor.setText(updatedText);

    // カーソルを移動したタスク行に設定
    const newCursorPosition = lines.indexOf(updatedTask);
    const targetPosition = lines.slice(0, newCursorPosition).join('\n').length + updatedTask.length + 1;
    editor.setSelectedRange(targetPosition);
}

// 所要時間を計算する関数
function calculateDuration(startTime, endTime) {
    const [startHour, startMinute] = startTime.split(':').map(Number);
    const [endHour, endMinute] = endTime.split(':').map(Number);

    let durationMinutes = (endHour * 60 + endMinute) - (startHour * 60 + startMinute);
    if (durationMinutes < 0) {
        // 終了時刻が開始時刻より前の場合、翌日にまたがっていると仮定
        durationMinutes += 24 * 60;
    }

    const hours = Math.floor(durationMinutes / 60);
    const minutes = durationMinutes % 60;

    return `${hours}:${minutes.toString().padStart(2, '0')}`;
}


timestampTool();

コード 開始時刻=終了時刻

function timestampTool() {
    const currentTime = new Date();
    const formattedTime = currentTime.toTimeString().slice(0, 5);

    // 現在のカーソル行の範囲を取得
    const selectedLineRange = editor.getSelectedLineRange();
    const cursorLineStart = selectedLineRange[0];
    const cursorLineEnd = selectedLineRange[1];
    let currentLineText = editor.getTextInRange(cursorLineStart, cursorLineEnd);

    // 正規表現で各パターンを定義
    const completedTaskRegex = /^\d{2}:\d{2}-(\d{2}:\d{2})/;
    const startTimeRegex = /^[➡️🔲🔁⏰✅]+(\d{2}:\d{2})-/;
    const taskSymbolRegex = /^[🔲🔁⏰]/; // 🔲 または 🔁 または ⏰ に一致


    // ドキュメント全体を取得し、行ごとに分割
    let allText = editor.getText();
    let lines = allText.split('\n');

    let latestEndTime = "00:00"; // 最も遅い終了時刻を初期化

    // 各行をチェックし、最も遅い終了時刻を特定
    lines.forEach(line => {
        const match = line.match(completedTaskRegex);
        if (match) {
            const endTime = match[1];
            if (endTime > latestEndTime) {
                latestEndTime = endTime;
            }
        }
    });

    if (completedTaskRegex.test(currentLineText)) {
        // "✅hh:mm-hh:mm" が既にある場合、新しいタスクを次の行に追加
        const lineWithoutTimes = currentLineText.replace(completedTaskRegex, '').trim();
        updatedTask = `➡️${latestEndTime}- ${lineWithoutTimes}`;
    } else if (startTimeRegex.test(currentLineText)) {
        // "[➡️]+hh:mm-" が既にある場合、"✅"に変更して終了時刻を追加        
        updatedTask = currentLineText.replace(startTimeRegex, (match, startTime) => {
            const duration = calculateDuration(startTime, formattedTime);
            return `${startTime}-${formattedTime} (${duration})`;
        });
        editor.replaceTextInRange(cursorLineStart, cursorLineEnd, updatedTask);
        editor.setSelectedRange(cursorLineStart + updatedTask.length);
        return; // ここで処理を終了
    } else {
        // hh:mm- 形式がない場合、"➡️hh:mm- "を行頭に追加し、🔲または🔁または⏰があれば削除
        const lineWithoutTaskSymbol = currentLineText.replace(taskSymbolRegex, '').trim();
        updatedTask = `➡️${latestEndTime}- ${lineWithoutTaskSymbol}`;
        // 現在の行を削除する(行の終わりの改行も含めて削除)
        const lineEndPosition = cursorLineEnd + 1; // 行の終わり+改行
        editor.replaceTextInRange(cursorLineStart, lineEndPosition, "");
    }

    

    // ドキュメント全体を取得し、行ごとに分割
    allText = editor.getText();
    lines = allText.split('\n');

    let lastCompletedTaskIndex = -1;
    let nextUncompletedTaskIndex = -1;

    // 行を上から順にチェック
    for (let i = 0; i < lines.length; i++) {
        if (completedTaskRegex.test(lines[i])) {
            lastCompletedTaskIndex = i;
        } else if (taskSymbolRegex.test(lines[i]) && lastCompletedTaskIndex !== -1) {
            nextUncompletedTaskIndex = i;
            break;
        }
    }

    // 次の未完了タスクが見つかった場合、その1行手前にタスクを挿入
    if (nextUncompletedTaskIndex !== -1) {
        lines.splice(nextUncompletedTaskIndex, 0, updatedTask);
    } else {
        // 未完了タスクが見つからない場合、最後に追加
        lines.push(updatedTask);
    }

    // 全体テキストを更新
    const updatedText = lines.join('\n');
    editor.setText(updatedText);

    // カーソルを移動したタスク行に設定
    const newCursorPosition = lines.indexOf(updatedTask);
    const targetPosition = lines.slice(0, newCursorPosition).join('\n').length + updatedTask.length + 1;
    editor.setSelectedRange(targetPosition);
}

// 所要時間を計算する関数
function calculateDuration(startTime, endTime) {
    const [startHour, startMinute] = startTime.split(':').map(Number);
    const [endHour, endMinute] = endTime.split(':').map(Number);

    let durationMinutes = (endHour * 60 + endMinute) - (startHour * 60 + startMinute);
    if (durationMinutes < 0) {
        // 終了時刻が開始時刻より前の場合、翌日にまたがっていると仮定
        durationMinutes += 24 * 60;
    }

    const hours = Math.floor(durationMinutes / 60);
    const minutes = durationMinutes % 60;

    return `${hours}:${minutes.toString().padStart(2, '0')}`;
}

timestampTool();
しのしの

現在時刻版 実行中の表示を✴️に変更

function timestampTool() {
    const currentTime = new Date();
    const formattedTime = currentTime.toTimeString().slice(0, 5);

    // 現在のカーソル行の範囲を取得
    const selectedLineRange = editor.getSelectedLineRange();
    const cursorLineStart = selectedLineRange[0];
    const cursorLineEnd = selectedLineRange[1];
    let currentLineText = editor.getTextInRange(cursorLineStart, cursorLineEnd);

    // 正規表現で各パターンを定義
    const completedTaskRegex = /^\d{2}:\d{2}-(\d{2}:\d{2})/;
    const startTimeRegex = /^[➡️🔲🔁⏰✅✴️]+(\d{2}:\d{2})-/;
    const taskSymbolRegex = /^[🔲🔁⏰✴️]/; // 🔲 または 🔁 または ⏰ または ✴️に一致

    // ドキュメント全体を取得し、行ごとに分割
    let allText = editor.getText();
    let lines = allText.split('\n');

    let latestEndTime = "00:00"; // 最も遅い終了時刻を初期化

    // 各行をチェックし、最も遅い終了時刻を特定
    lines.forEach(line => {
        const match = line.match(completedTaskRegex);
        if (match) {
            const endTime = match[1];
            if (endTime > latestEndTime) {
                latestEndTime = endTime;
            }
        }
    });

    let updatedTask = "";

    if (completedTaskRegex.test(currentLineText)) {
        // "✅hh:mm-hh:mm" が既にある場合、新しいタスクを次の行に追加
        const lineWithoutTimes = currentLineText.replace(completedTaskRegex, '').trim();
        updatedTask = `✴️${formattedTime}- ${lineWithoutTimes}`;
    } else if (startTimeRegex.test(currentLineText)) {
        updatedTask = currentLineText.replace(startTimeRegex, (match, startTime) => {
            const duration = calculateDuration(startTime, formattedTime);
            return `${startTime}-${formattedTime} (${duration})`;
        });
        editor.replaceTextInRange(cursorLineStart, cursorLineEnd, updatedTask);
        editor.setSelectedRange(cursorLineStart + updatedTask.length);
        return; // ここで処理を終了
    } else {
        // hh:mm- 形式がない場合、"✴️hh:mm- "を行頭に追加し、🔲または🔁または⏰または✴️があれば削除
        const lineWithoutTaskSymbol = currentLineText.replace(taskSymbolRegex, '').trim();
        updatedTask = `✴️${formattedTime}- ${lineWithoutTaskSymbol}`;
        // 現在の行を削除する(行の終わりの改行も含めて削除)
        const lineEndPosition = cursorLineEnd + 1; // 行の終わり+改行
        editor.replaceTextInRange(cursorLineStart, lineEndPosition, "");
    }

    

    // ドキュメント全体を取得し、行ごとに分割
    allText = editor.getText();
    lines = allText.split('\n');

    let lastCompletedTaskIndex = -1;
    let nextUncompletedTaskIndex = -1;

    // 行を上から順にチェック
    for (let i = 0; i < lines.length; i++) {
        if (completedTaskRegex.test(lines[i])) {
            lastCompletedTaskIndex = i;
        } else if (taskSymbolRegex.test(lines[i]) && lastCompletedTaskIndex !== -1) {
            nextUncompletedTaskIndex = i;
            break;
        }
    }

    // 次の未完了タスクが見つかった場合、その1行手前にタスクを挿入
    if (nextUncompletedTaskIndex !== -1) {
        lines.splice(nextUncompletedTaskIndex, 0, updatedTask);
    } else {
        // 未完了タスクが見つからない場合、最後に追加
        lines.push(updatedTask);
    }

    // 全体テキストを更新
    const updatedText = lines.join('\n');
    editor.setText(updatedText);

    // カーソルを移動したタスク行に設定
    const newCursorPosition = lines.indexOf(updatedTask);
    const targetPosition = lines.slice(0, newCursorPosition).join('\n').length + updatedTask.length + 1;
    editor.setSelectedRange(targetPosition);
}

// 所要時間を計算する関数
function calculateDuration(startTime, endTime) {
    const [startHour, startMinute] = startTime.split(':').map(Number);
    const [endHour, endMinute] = endTime.split(':').map(Number);

    let durationMinutes = (endHour * 60 + endMinute) - (startHour * 60 + startMinute);
    if (durationMinutes < 0) {
        // 終了時刻が開始時刻より前の場合、翌日にまたがっていると仮定
        durationMinutes += 24 * 60;
    }

    const hours = Math.floor(durationMinutes / 60);
    const minutes = durationMinutes % 60;

    return `${hours}:${minutes.toString().padStart(2, '0')}`;
}


timestampTool();
しのしの

1Writer メモの切り出し v2

選択行の1行目をタイトルにしたノートを作成、または上書き

URLスキームのappendを使うことで、指定したファイル名が既存のファイルの場合は上書き、そうでなければ新規作成を実現できた。

元ファイル側

[[タイトル]]

追記・新規作成側

### 元ファイルのノート名

ノートの内容
from [[元ファイルのノート名]]

コード

// 選択範囲を取得
const selectedText = editor.getSelectedText();
const selectedRange = editor.getSelectedRange(); // [start, end]
const originalFolderPath = editor.getFolderPath(); // 元のノートのフォルダパスを取得
const originalFileName = editor.getFileName(); // 元のノートのファイル名を取得

if (selectedText.length > 0) {
    // 選択範囲の内容を行に分割
    const lines = selectedText.split('\n');
    
    // 最初の行をタイトルとして使用
    const title = lines[0].trim();
    
    // 元のノートのタイトル(拡張子を除外)
    const originalTitle = originalFileName.replace(/\.md$/, '');
    
    // 新しいファイルのパスを構築
    const newFilePath = `${originalFolderPath}/${title}.md`;

    // 追記する内容を構築
    const header = `### ${originalTitle}\n\n`;
    const footer = `\n\nfrom [[${originalTitle}]]`;
    const contentToAppend = encodeURIComponent(header + selectedText + footer);

    // URL Scheme を使用してファイルに追記
    const appendURL = `onewriter://x-callback-url/append?path=${encodeURIComponent(newFilePath)}&text=${contentToAppend}`;
    
    // 1Writer の URL Scheme を実行
    app.openURL(appendURL);

    // 元のノートに戻り、リンクを挿入
    const originalNotePath = `${originalFolderPath}/${originalFileName}`;
    editor.openFile(originalNotePath, 'edit', function() {
        const link = `[[${title}]]`;
        editor.replaceTextInRange(selectedRange[0], selectedRange[1], link);

        // カーソル位置をリンクの文末に設定
        const newCursorPosition = selectedRange[0] + link.length;
        editor.setSelectedRange(newCursorPosition);
    });
} else {
    // 選択範囲がない場合のエラーメッセージ
    app.showMessage("選択範囲がありません。", "Error");
}

しのしの

絵文字処理の課題

Obsidianと1Writerで挙動が違う記号(同じに見えて何かが違う?)
✴️

1Writerでの絵文字の挙動
コピー時に�になる(異常)
📖🔁🔲

コピー時に�にならない(正常)
➡️✅⏰

しのしの

1Writer メモの切り出し v3

選択範囲がない場合は、カーソル行の末尾に、[[yyyy-MM-dd hhmmss]]を入れて、タイトルがyyyy-MM-dd hhmmssのノートを作るようにした

// 日付フォーマット用の関数
function formatDate(date) {
    const yyyy = date.getFullYear();
    const MM = String(date.getMonth() + 1).padStart(2, '0');
    const dd = String(date.getDate()).padStart(2, '0');
    const hh = String(date.getHours()).padStart(2, '0');
    const mm = String(date.getMinutes()).padStart(2, '0');
    const ss = String(date.getSeconds()).padStart(2, '0');
    return `${yyyy}-${MM}-${dd} ${hh}${mm}${ss}`;
}

// 現在の日時を取得してフォーマット
const now = new Date();
const formattedDate = formatDate(now);

// 選択範囲を取得
const selectedText = editor.getSelectedText();
const selectedRange = editor.getSelectedRange(); // [start, end]
const originalFolderPath = editor.getFolderPath(); // 元のノートのフォルダパスを取得
const originalFileName = editor.getFileName(); // 元のノートのファイル名を取得

if (selectedText.length > 0) {
    // 選択範囲がある場合、既存の処理
    const lines = selectedText.split('\n');
    const title = lines[0].trim();
    const originalTitle = originalFileName.replace(/\.md$/, '');
    const newFilePath = `${originalFolderPath}/${title}.md`;
    const header = `### ${originalTitle}\n\n`;
    const footer = `\n\nfrom [[${originalTitle}]]`;
    const contentToAppend = encodeURIComponent(header + selectedText + footer);
    const appendURL = `onewriter://x-callback-url/append?path=${encodeURIComponent(newFilePath)}&text=${contentToAppend}`;
    app.openURL(appendURL);

    // 元のノートにリンクを挿入
    const originalNotePath = `${originalFolderPath}/${originalFileName}`;
    editor.openFile(originalNotePath, 'edit', function() {
        const link = `[[${title}]]`;
        editor.replaceTextInRange(selectedRange[0], selectedRange[1], link);
        const newCursorPosition = selectedRange[0] + link.length;
        editor.setSelectedRange(newCursorPosition);
    });
} else {
    // 選択範囲がない場合
    const originalTitle = originalFileName.replace(/\.md$/, '');
    const newFilePath = `${originalFolderPath}/${formattedDate}.md`;
    const header = `### ${originalTitle}\n\n`;
    const contentToAppend = encodeURIComponent(header);
    const createURL = `onewriter://x-callback-url/append?path=${encodeURIComponent(newFilePath)}&text=${contentToAppend}`;
    app.openURL(createURL);

    // カーソル行の末尾にリンクを挿入
    const lineRange = editor.getSelectedLineRange();
    const link = ` [[${formattedDate}]]`;
    editor.replaceTextInRange(lineRange[1], lineRange[1], link);
    
    // カーソル位置をリンクの文末に設定
    const newCursorPosition = lineRange[1] + link.length;
    editor.setSelectedRange(newCursorPosition);
}
しのしの

秒まではいらないかな、と思ったので、秒は削除

// 日付フォーマット用の関数
function formatDate(date) {
    const yyyy = date.getFullYear();
    const MM = String(date.getMonth() + 1).padStart(2, '0');
    const dd = String(date.getDate()).padStart(2, '0');
    const hh = String(date.getHours()).padStart(2, '0');
    const mm = String(date.getMinutes()).padStart(2, '0');
    return `${yyyy}-${MM}-${dd} ${hh}${mm}`;
}

// 現在の日時を取得してフォーマット
const now = new Date();
const formattedDate = formatDate(now);

// 選択範囲を取得
const selectedText = editor.getSelectedText();
const selectedRange = editor.getSelectedRange(); // [start, end]
const originalFolderPath = editor.getFolderPath(); // 元のノートのフォルダパスを取得
const originalFileName = editor.getFileName(); // 元のノートのファイル名を取得

if (selectedText.length > 0) {
    // 選択範囲がある場合、既存の処理
    const lines = selectedText.split('\n');
    const title = lines[0].trim();
    const originalTitle = originalFileName.replace(/\.md$/, '');
    const newFilePath = `${originalFolderPath}/${title}.md`;
    const header = `### ${originalTitle}\n\n`;
    const footer = `\n\nfrom [[${originalTitle}]]`;
    const contentToAppend = encodeURIComponent(header + selectedText + footer);
    const appendURL = `onewriter://x-callback-url/append?path=${encodeURIComponent(newFilePath)}&text=${contentToAppend}`;
    app.openURL(appendURL);

    // 元のノートにリンクを挿入
    const originalNotePath = `${originalFolderPath}/${originalFileName}`;
    editor.openFile(originalNotePath, 'edit', function() {
        const link = `[[${title}]]`;
        editor.replaceTextInRange(selectedRange[0], selectedRange[1], link);
        const newCursorPosition = selectedRange[0] + link.length;
        editor.setSelectedRange(newCursorPosition);
    });
} else {
    // 選択範囲がない場合
    const originalTitle = originalFileName.replace(/\.md$/, '');
    const newFilePath = `${originalFolderPath}/${formattedDate}.md`;
    const header = `### ${originalTitle}\n\n`;
    const contentToAppend = encodeURIComponent(header);
    const createURL = `onewriter://x-callback-url/append?path=${encodeURIComponent(newFilePath)}&text=${contentToAppend}`;
    app.openURL(createURL);

    // カーソル行の末尾にリンクを挿入
    const lineRange = editor.getSelectedLineRange();
    const link = ` [[${formattedDate}]]`;
    editor.replaceTextInRange(lineRange[1], lineRange[1], link);
    
    // カーソル位置をリンクの文末に設定
    const newCursorPosition = lineRange[1] + link.length;
    editor.setSelectedRange(newCursorPosition);
}
しのしの

選択範囲がない場合に作成されるノートの中身は空白にし、ノートを開く

// 日付フォーマット用の関数
function formatDate(date) {
    const yyyy = date.getFullYear();
    const MM = String(date.getMonth() + 1).padStart(2, '0');
    const dd = String(date.getDate()).padStart(2, '0');
    const hh = String(date.getHours()).padStart(2, '0');
    const mm = String(date.getMinutes()).padStart(2, '0');
    return `${yyyy}-${MM}-${dd} ${hh}${mm}`;
}

// 現在の日時を取得してフォーマット
const now = new Date();
const formattedDate = formatDate(now);

// 選択範囲を取得
const selectedText = editor.getSelectedText();
const selectedRange = editor.getSelectedRange(); // [start, end]
const originalFolderPath = editor.getFolderPath(); // 元のノートのフォルダパスを取得
const originalFileName = editor.getFileName(); // 元のノートのファイル名を取得

if (selectedText.length > 0) {
    // 選択範囲がある場合、既存の処理
    const lines = selectedText.split('\n');
    const title = lines[0].trim();
    const originalTitle = originalFileName.replace(/\.md$/, '');
    const newFilePath = `${originalFolderPath}/${title}.md`;
    const header = `### ${originalTitle}\n\n`;
    const footer = `\n\nfrom [[${originalTitle}]]`;
    const contentToAppend = encodeURIComponent(header + selectedText + footer);
    const appendURL = `onewriter://x-callback-url/append?path=${encodeURIComponent(newFilePath)}&text=${contentToAppend}`;
    app.openURL(appendURL);

    // 元のノートにリンクを挿入
    const originalNotePath = `${originalFolderPath}/${originalFileName}`;
    editor.openFile(originalNotePath, 'edit', function() {
        const link = `[[${title}]]`;
        editor.replaceTextInRange(selectedRange[0], selectedRange[1], link);
        const newCursorPosition = selectedRange[0] + link.length;
        editor.setSelectedRange(newCursorPosition);
    });
} else {
    // 選択範囲がない場合
    const newFilePath = `${originalFolderPath}/${formattedDate}`;

    // カーソル行の末尾にリンクを挿入
    const lineRange = editor.getSelectedLineRange();
    const link = ` [[${formattedDate}]]`;
    editor.replaceTextInRange(lineRange[1], lineRange[1], link);
    
    // カーソル位置をリンクの文末に設定
    const newCursorPosition = lineRange[1] + link.length;
    editor.setSelectedRange(newCursorPosition);

    // 新しいノートを作成
    editor.newFile('',`${formattedDate}`)
}
しのしの

1Writerのノート作成に関する挙動メモ

URLスキーム + app.openURL('url')の場合は、バックグラウンドでノートが作成されるだけで、作成されたノートは勝手には開かない

editor.newFile([text], [name], [callback])は、ファイルが作成されるだけでなく、そのファイルが開かれる

しのしの

Obsidian プロジェクト別集計 v2

プロジェクト別の実績時間の集計値をカーソル位置に出力

インプットフォーマット

hh:mm-hh:mm #プロジェクト名

アウトプットイメージ(単位は時間)

#project_a    0.13
#project_b    1.21

コード(ProjectSummary.md)

<%*

// Initialize an object to hold time totals for each project
let timeTotals = {};

// Regular expression to match time entries with Unicode support
const regex = /(\d{2}:\d{2})-(\d{2}:\d{2})\s#([\p{L}\p{N}_]+)/gu;

// Get all the lines from the note
const content = tp.file.content;
const lines = content.split("\n");

lines.forEach(line => {
    let match;
    // Find all matches in the current line
    while ((match = regex.exec(line)) !== null) {
        let startTime = match[1];
        let endTime = match[2];
        let project = match[3];

        // Convert times to Date objects
        let start = new Date(`1970-01-01T${startTime}:00`);
        let end = new Date(`1970-01-01T${endTime}:00`);

        // Calculate the difference in hours
        let timeSpent = (end - start) / (1000 * 60 * 60); // time in hours

        // Add time to the appropriate project
        if (timeTotals[project]) {
            timeTotals[project] += timeSpent;
        } else {
            timeTotals[project] = timeSpent;
        }
    }
});

// Output the results in the desired format
let output = "";
for (let project in timeTotals) {
    output += `#${project}\t${timeTotals[project].toFixed(2)}\n`;
}

// Output the results in Templater
tR += output;

%>
しのしの

1Writer メモの切り出し v3

やりたいこと

---
tc: "タスク名"
---
{カーソル位置}

from [[2024-08-24]]
しのしの

1Writer リピートタスクのセット v3

平日に加えて、曜日での処理もできるようにした。

曜日の指定方法

例) 🔁木,金 タスク名

  1. 行頭が🔁または⏰
  2. その直後に、月火水木金土日、複数指定した場合は「,」区切り。カンマの前後にスペースは入れない。
  3. 半角スペースを開けて、その後にタスク名
// ノート情報の保存
var folder = editor.getFolderPath(); // 現在のノートのフォルダパスを取得
var editingfile = editor.getFileName();
var cursorPosition = editor.getSelectedRange(); // カーソル位置を保存

// ノートタイトルから日付部分を取得(例: "2023-08-20")
var datePattern = /^(\d{4})-(\d{2})-(\d{2})/;
var match = editingfile.match(datePattern);

if (!match) {
    ui.alert("ノートのタイトルに有効な日付が含まれていません。");
    return;
}

// 日付オブジェクトを作成
var noteDate = new Date(match[1], match[2] - 1, match[3]); // 月は0から始まるため-1

// ノートの日付から曜日を取得(0: 日曜, 1: 月曜, ..., 6: 土曜)
var dayOfWeek = noteDate.getDay();

// 曜日のマッピング(0: 日曜 ~ 6: 土曜)
var dayOfWeekMapping = ['日', '月', '火', '水', '木', '金', '土'];

// リピートタスクを記述しているファイルのファイル名
var openfilename = 'リピートタスク.md';

// リピートタスクのファイルを開く(現在のフォルダ内)
editor.openFile(folder + '/' + openfilename, 'edit', call);

function call() {
    // ファイルのテキストを取得
    var text = editor.getText();

    // 🔁 または ⏰ で始まる行のみをフィルタリング
    let listData = text.split('\n').filter(line => {
        if (line.startsWith('🔁平日')) {
            // 平日のみの場合、ノートの日付が平日なら含める
            return dayOfWeek >= 1 && dayOfWeek <= 5;
        }

        // 曜日が指定されているか確認
        let matchedDays = line.match(/🔁([月火水木金土日,]+)/);
        if (matchedDays) {
            let daysArray = matchedDays[1].split(',').map(day => day.trim());
            // 現在の日付の曜日が含まれているかを確認
            if (daysArray.includes(dayOfWeekMapping[dayOfWeek])) {
                return true;
            }
            return false; // 指定された曜日と一致しない場合は含めない
        }

        return line.startsWith('🔁') || line.startsWith('⏰');
    }).map(line => {
        // "平日 "や曜日指定部分を削除し、「🔁」は残す
        if (line.startsWith('🔁平日')) {
            return line.replace('平日 ', '').trim();
        }

        // 曜日が指定されている部分を削除し、行頭の「🔁」を残す
        line = line.replace(/🔁([月火水木金土日,]+)\s*/, '🔁').trim();

        return line;
    });

    ui.hudDismiss();

    if (listData.length === 0) {
        ui.alert("🔁または⏰で始まるタスクが見つかりませんでした。");
        return;
    }

    const selectedText = listData.join('\n'); // すべてのタスクを1つの文字列に結合

    // 元のノートに戻り、カーソル位置にすべてのタスクを挿入
    editor.openFile(folder + '/' + editingfile, 'edit', function() {
        editor.setSelectedRange(cursorPosition[0]); // カーソル位置に戻る
        editor.replaceSelection(selectedText); // 選択されたテキストを挿入
        editor.setSelectedRange(cursorPosition[0] + selectedText.length); // カーソルを挿入後の位置に移動
    });
}
しのしの

1Writer リピートタスクのセット v4

月日(MMDD)形式での指定を追加
, 区切りで複数指定も可

簡易テスト用のリピートタスク

2024/08/25(日) に実施

🔁日 テスト 残す
🔁月 テスト 削除
🔁日から始まるタスク 残す
🔁月から始まるタスク 残す
🔁日,月 テスト 残す
🔁0825 月日一つのテスト 残す
🔁0826 月日一つのテスト 削除
🔁0825,0826 月日2つのテスト 残す
🔁0826,0827 月日2つのテスト 削除

コード

// ノート情報の保存
var folder = editor.getFolderPath(); // 現在のノートのフォルダパスを取得
var editingfile = editor.getFileName();
var cursorPosition = editor.getSelectedRange(); // カーソル位置を保存

// ノートタイトルから日付部分を取得(例: "2023-08-20")
var datePattern = /^(\d{4})-(\d{2})-(\d{2})/;
var match = editingfile.match(datePattern);

if (!match) {
    ui.alert("ノートのタイトルに有効な日付が含まれていません。");
    return;
}

// 日付オブジェクトを作成
var noteDate = new Date(match[1], match[2] - 1, match[3]); // 月は0から始まるため-1

// MMDD形式で現在の日付を取得
var currentMMDD = ("0" + (noteDate.getMonth() + 1)).slice(-2) + ("0" + noteDate.getDate()).slice(-2);

// ノートの日付から曜日を取得(0: 日曜, 1: 月曜, ..., 6: 土曜)
var dayOfWeek = noteDate.getDay();

// 曜日のマッピング(0: 日曜 ~ 6: 土曜)
var dayOfWeekMapping = ['日', '月', '火', '水', '木', '金', '土'];

// リピートタスクを記述しているファイルのファイル名
var openfilename = 'リピートタスク.md';

// リピートタスクのファイルを開く(現在のフォルダ内)
editor.openFile(folder + '/' + openfilename, 'edit', call);

function call() {
    // ファイルのテキストを取得
    var text = editor.getText();

    // 🔁 または ⏰ で始まる行のみをフィルタリング
    let listData = text.split('\n').filter(line => {
        // MMDD形式のチェック(カンマ区切り対応)
        let matchedMMDD = line.match(/^🔁((\d{4},?)+)\s/);
        if (matchedMMDD) {
            let datesArray = matchedMMDD[1].split(',').map(date => date.trim());
            // 現在の日付がリストに含まれているか確認
            return datesArray.includes(currentMMDD);
        }

        // 平日指定の場合
        if (line.startsWith('🔁平日')) {
            return dayOfWeek >= 1 && dayOfWeek <= 5; // 平日なら含める
        }

        // 曜日が指定されているか確認
        let matchedDays = line.match(/🔁([月火水木金土日,]+)\s/);
        if (matchedDays) {
            let daysArray = matchedDays[1].split(',').map(day => day.trim());
            // 現在の日付の曜日が含まれているかを確認
            return daysArray.includes(dayOfWeekMapping[dayOfWeek]);
        }

        return line.startsWith('🔁') || line.startsWith('⏰');
    });

    // 条件部分の削除処理
    listData = listData.map(line => {
        // MMDD部分の削除
        line = line.replace(/^🔁((\d{4},?)+)\s/, '🔁').trim();

        // 平日部分の削除
        line = line.replace('平日 ', '').trim();

        // 曜日部分の削除
        line = line.replace(/🔁([月火水木金土日,]+)\s/, '🔁').trim();

        return line;
    });

    ui.hudDismiss();

    if (listData.length === 0) {
        ui.alert("🔁または⏰で始まるタスクが見つかりませんでした。");
        return;
    }

    const selectedText = listData.join('\n'); // すべてのタスクを1つの文字列に結合

    // 元のノートに戻り、カーソル位置にすべてのタスクを挿入
    editor.openFile(folder + '/' + editingfile, 'edit', function() {
        editor.setSelectedRange(cursorPosition[0]); // カーソル位置に戻る
        editor.replaceSelection(selectedText); // 選択されたテキストを挿入
        editor.setSelectedRange(cursorPosition[0] + selectedText.length); // カーソルを挿入後の位置に移動
    });
}
しのしの

Templaterの使い方

Documentation

Internal Functions - Templater

コードを書いたマークダウンファイルを作成

ファイル名.md

<%*

ここにJavaScriptを書く

%>

Templaterのプラグインで選択

Setting > Community plugins > Templater > Template hotkeys > Add new hotkey for template

しのしの

Obsidian メモ作成

タスク一覧側

✅08:31-08:34 タスク名 [[240902 0838]]

新規ノート


from [[2024-08-24]]

コード

<%*

function formatDate(date) {

    const yy = String(date.getFullYear()).slice(-2); // 2桁の年を取得
    const MM = String(date.getMonth() + 1).padStart(2, '0');
    const dd = String(date.getDate()).padStart(2, '0');
    const hh = String(date.getHours()).padStart(2, '0');
    const mm = String(date.getMinutes()).padStart(2, '0');

    return `${yy}${MM}${dd} ${hh}${mm}`;

}

// 現在のカーソル行を取得
const editor = app.workspace.activeLeaf.view.editor;
const cursorLine = editor.getCursor().line;
const currentLineText = editor.getLine(cursorLine);

let updatedLineText;
let cursorPosition;

// 現在の日時を取得してフォーマット
const now = new Date();
const formattedDate = formatDate(now);

// 現在のファイルに関する情報を取得
const originalFolderPath = tp.file.folder(true); // 元のノートのフォルダパスを取得
const originalFileName = tp.file.title; // 元のノートのファイル名を取得

try {

		// 新しいファイル名
		const newFilePath = `${formattedDate}`;
    
    // リンクを現在の行の末尾に挿入
    const link = `[[${formattedDate}]]`;
    const updatedLineText = currentLineText.replace(currentLineText, `${currentLineText} ${link}`);
    
    cursorPosition = updatedLineText.indexOf(currentLineText);
    editor.setLine(cursorLine, updatedLineText);
    editor.setCursor({ line: cursorLine, ch: cursorPosition });

    // 新しいノートを作成
    const content = "from [[" + originalFileName + "]]";
    await tp.file.create_new(content, newFilePath, true);

} catch (error) {
    console.error("Error processing without selection:", error);
}

%>
しのしの

過去のジャーナルへのリンク

テンプレート

<< [[2024-09-01]] | [[2024-09-03]] >>
<< [[2024-08-26]] | [[2024-09-09]] >>
<< [[2024-08-02]] | [[2024-10-02]] >>
<< [[2023-09-02]] | [[2025-09-02]] >>

Obsidianコード

<%*
const fileTitle = tp.file.title; // ファイルのタイトルを取得
const fileDate = moment(fileTitle, "YYYY-MM-DD"); // タイトルから日付を取得

// 日付リンクを生成
const dayBefore = fileDate.clone().subtract(1, 'day').format("YYYY-MM-DD");
const dayAfter = fileDate.clone().add(1, 'day').format("YYYY-MM-DD");
const weekBefore = fileDate.clone().subtract(7, 'days').format("YYYY-MM-DD");
const weekAfter = fileDate.clone().add(7, 'days').format("YYYY-MM-DD");
const monthBefore = fileDate.clone().subtract(1, 'month').format("YYYY-MM-DD");
const monthAfter = fileDate.clone().add(1, 'month').format("YYYY-MM-DD");
const yearBefore = fileDate.clone().subtract(1, 'year').format("YYYY-MM-DD");
const yearAfter = fileDate.clone().add(1, 'year').format("YYYY-MM-DD");
%>

<< [[<%* tR += dayBefore %>]] | [[<%* tR += dayAfter %>]] >>
<< [[<%* tR += weekBefore %>]] | [[<%* tR += weekAfter %>]] >>
<< [[<%* tR += monthBefore %>]] | [[<%* tR += monthAfter %>]] >>
<< [[<%* tR += yearBefore %>]] | [[<%* tR += yearAfter %>]] >>

1Writerコード

<%*
const fileTitle = tp.file.title; // ファイルのタイトルを取得
const fileDate = moment(fileTitle, "YYYY-MM-DD"); // タイトルから日付を取得

// 日付リンクを生成
const dayBefore = fileDate.clone().subtract(1, 'day').format("YYYY-MM-DD");
const dayAfter = fileDate.clone().add(1, 'day').format("YYYY-MM-DD");
const weekBefore = fileDate.clone().subtract(7, 'days').format("YYYY-MM-DD");
const weekAfter = fileDate.clone().add(7, 'days').format("YYYY-MM-DD");
const monthBefore = fileDate.clone().subtract(1, 'month').format("YYYY-MM-DD");
const monthAfter = fileDate.clone().add(1, 'month').format("YYYY-MM-DD");
const yearBefore = fileDate.clone().subtract(1, 'year').format("YYYY-MM-DD");
const yearAfter = fileDate.clone().add(1, 'year').format("YYYY-MM-DD");
%>

<< [[<%* tR += dayBefore %>]] | [[<%* tR += dayAfter %>]] >>
<< [[<%* tR += weekBefore %>]] | [[<%* tR += weekAfter %>]] >>
<< [[<%* tR += monthBefore %>]] | [[<%* tR += monthAfter %>]] >>
<< [[<%* tR += yearBefore %>]] | [[<%* tR += yearAfter %>]] >>
しのしの

Obsidian 終了予定時刻をポップアップ表示

インプット
記号(見積時間、分単位) タスク名

✅08:39-08:59 (20) 朝食
🔁(60)仕事
🔁(60)料理
🔁️(30)夕飯
🔁(12)お皿洗い

アウトプット

コード

<%*
const moment = tp.date.now("HH:mm");
const currentDate = new Date();

// 未完了タスク(🔲、⏰、🔁)を正規表現で抽出
const content = tp.file.content;
const unfinishedTasks = content.match(/(?:🔲||🔁)\((\d+)\)/g);

// 未完了タスクの所用時間を合計
let totalEstimate = 0;
if (unfinishedTasks) {
    totalEstimate = unfinishedTasks.reduce((sum, task) => {
        const estimate = parseInt(task.match(/\((\d+)\)/)[1], 10);
        return sum + estimate;
    }, 0);
}

// 合計時間を時間と分に変換
const hours = Math.floor(totalEstimate / 60);
const minutes = totalEstimate % 60;

// 現在時刻に合計所要時間を足して終了予定時刻を計算
const endDate = new Date(currentDate.getTime() + totalEstimate * 60000);
const endTime = endDate.toTimeString().substring(0, 5); // "HH:mm"形式で表示

// 結果をポップアップ表示
new Notice(`終了予定時刻: ${endTime}\n見積時間: ${hours}時間${minutes}`);
%>
しのしの

Obsidian Dynamic Timetable

Dynamic Timetableというタスクシュートができるプラグインを作っている方がいた。

Obsidian で TaskChute(タスクシュート)みたいなことをするためのプラグインを作った|L7Cy

Dynamic Timetableのインストール

  1. 設定で"Community plugins"をクリック。"Community plugins"セクションの"Browse"ボタンを押す。
  2. "Dynamic Timetable"を検索して"Install"ボタンを押す
  3. "Installed plugins"セクションで、"Dynamic Timetable"を探し、有効化。

Dynamic Timetableの設定

Task/Estimate Delimiterは\にした。使用頻度が少なく、1Writerから見ても邪魔にならないので。

タスク一覧のイメージ

- [x] 18:02-18:02 \5 完了タスク
- [ ] 18:02- \10 実行中タスク
- [ ] \15 未実行タスク

所感

シュミレーションができない問題に進展。
ただ、開始時刻や終了時刻が残らないので、別途記録できるようにコードを書くことにした。

しのしの

Obsidian 開始・終了

Dynamic Timetableのフォーマットに合わせて変更。
タスクの開始、終了、完了タスクのコピーが可能

<%* 
// 現在時刻を取得
const now = tp.date.now("HH:mm");

// 現在のエディタとカーソル行を取得
const editor = app.workspace.activeLeaf.view.editor;
const cursorLine = editor.getCursor().line;
const currentLineText = editor.getLine(cursorLine).trim();

// 1. [ ] のみの場合: 開始時刻を追加
if (/^- \[ \] (?!\d{2}:\d{2}-)/.test(currentLineText)) {
    // 時間がまだ追加されていない場合
    const updatedLine = currentLineText.replace("- [ ] ", `- [ ] ${now}- `);
    editor.setLine(cursorLine, updatedLine);
}

// 2. [ ] hh:mm- の場合: 終了時刻を追加して、完了状態にする
else if (/^- \[ \] \d{2}:\d{2}-/.test(currentLineText)) {
    // 終了時刻を追加して完了状態にする
    const updatedLine = currentLineText.replace(/^-\s\[ \]\s(\d{2}:\d{2})-/, `- [x] $1-${now}`);
    editor.setLine(cursorLine, updatedLine);
}

// 3. [x] hh:mm-hh:mm の場合: 行を複製し、複製した行を未完了状態にして、hh:mm-hh:mm の部分を消す
else if (/^- \[x\] \d{2}:\d{2}-\d{2}:\d{2}/.test(currentLineText)) {
    // 完了済みタスクの時間部分を除いて行を複製
    const timeRangeEnd = currentLineText.indexOf(" ", 16); // hh:mm-hh:mm の終了後の位置
    const taskDescription = currentLineText.slice(timeRangeEnd).trim(); // タスクの説明部分を取得
    const newLine = `- [ ] ${taskDescription}`; // 新しい未完了タスク行
    editor.replaceRange(newLine + "\n", { line: cursorLine + 1, ch: 0 }); // 次の行に追加
}
%>
しのしの

Obsidian Project Summary

Dynamic Timetableのフォーマットに合わせて変更

<%*
const completedTasks = tp.file.content.match(/- \[x\].*/g);

if (!completedTasks) {
  tR += "完了したタスクが見つかりませんでした。";
} else {
  const projectData = {};

  function parseTask(task) {
    const timePattern = /(\d{1,2}:\d{2})-(\d{1,2}:\d{2})/;
    const costPattern = /\\(\d+)/;
    const projectPattern = /#([^\s]+)/;

    const timeMatch = task.match(timePattern);
    const costMatch = task.match(costPattern);
    const projectMatch = task.match(projectPattern);

    let minutes = 0;
    if (timeMatch) {
      const start = timeMatch[1];
      const end = timeMatch[2];
      const [startHour, startMinute] = start.split(':').map(Number);
      const [endHour, endMinute] = end.split(':').map(Number);
      minutes = (endHour * 60 + endMinute) - (startHour * 60 + startMinute);
    }

    const cost = costMatch ? parseInt(costMatch[1], 10) : 0;
    const project = projectMatch ? projectMatch[1] : 'その他';

    return { project, minutes, cost };
  }

  for (const task of completedTasks) {
    const { project, minutes, cost } = parseTask(task);

    if (!projectData[project]) {
      projectData[project] = { totalMinutes: 0, totalCost: 0 };
    }

    projectData[project].totalMinutes += minutes;
    projectData[project].totalCost += cost;
  }

  const result = Object.entries(projectData).map(([project, data]) => {
    const hours = (data.totalMinutes / 60).toFixed(1); // 分を時間に変換
    return `${project}\t${hours}h`;
  }).join("\n");

  tR += result;
}
%>
しのしの

Obsidian 開始・終了

startTimeプロパティも同時に更新

<%* 
// 現在時刻を取得
const now = tp.date.now("HH:mm");
const fullNow = tp.date.now("HH:mm:ss");

// 現在のエディタとカーソル行を取得
const editor = app.workspace.activeLeaf.view.editor;
const cursorLine = editor.getCursor().line;
const currentLineText = editor.getLine(cursorLine).trim();

// タスクのプロパティが存在するかチェック
const propertyRegex = /^---\n([\s\S]+?)\n---/s;
let fileContent = app.workspace.activeLeaf.view.data;

let propertiesMatch = fileContent.match(propertyRegex);

if (propertiesMatch) {
    // プロパティブロックが存在する場合
    let properties = propertiesMatch[1];
    let updatedProperties;

    if (/startTime:\s*.+/g.test(properties)) {
        // "startTime" プロパティを更新
        updatedProperties = properties.replace(/startTime:\s*.*/g, `startTime: ${fullNow}`);
    } else {
        // "startTime" プロパティがない場合、追加
        updatedProperties = properties + `\nstartTime: ${fullNow}`;
    }

    // プロパティ部分のみを置換してファイルを更新
    const updatedFileContent = fileContent.replace(properties, updatedProperties);
    await app.vault.modify(app.workspace.getActiveFile(), updatedFileContent);

} else {
    // プロパティブロックがない場合、新しいプロパティブロックを追加
    const newProperties = `---\nstartTime: ${fullNow}\n---\n`;
    const updatedFileContent = newProperties + fileContent;
    await app.vault.modify(app.workspace.getActiveFile(), updatedFileContent);
}

// 1. [ ] のみの場合: 開始時刻を追加
if (/^- \[ \] (?!\d{2}:\d{2}-)/.test(currentLineText)) {
    const updatedLine = currentLineText.replace("- [ ] ", `- [ ] ${now}- `);
    editor.setLine(cursorLine, updatedLine);
}

// 2. [ ] hh:mm- の場合: 終了時刻を追加して、完了状態にする
else if (/^- \[ \] \d{2}:\d{2}-/.test(currentLineText)) {
    const updatedLine = currentLineText.replace(/^-\s\[ \]\s(\d{2}:\d{2})-/, `- [x] $1-${now}`);
    editor.setLine(cursorLine, updatedLine);
}

// 3. [x] hh:mm-hh:mm の場合: 行を複製し、複製した行を未完了状態にして、hh:mm-hh:mm の部分を消す
else if (/^- \[x\] \d{2}:\d{2}-\d{2}:\d{2}/.test(currentLineText)) {
    const timeRangeEnd = currentLineText.indexOf(" ", 16);
    const taskDescription = currentLineText.slice(timeRangeEnd).trim();
    const newLine = `- [ ] ${taskDescription}`;
    editor.replaceRange(newLine + "\n", { line: cursorLine + 1, ch: 0 });
}
%>
しのしの

1Writer 開始・終了

タスクの開始・終了とstartTimeプロパティの更新

// 現在時刻を取得
const now = new Date().toLocaleTimeString('ja-JP', { hour: '2-digit', minute: '2-digit' });
const fullNow = new Date().toLocaleTimeString('ja-JP', { hour: '2-digit', minute: '2-digit', second: '2-digit' });

// 現在の選択範囲とカーソル位置を保存
const initialCursorRange = editor.getSelectedRange();
const [start, end] = editor.getSelectedLineRange(); // 現在の選択行の範囲を取得
const currentLineText = editor.getTextInRange(start, end).trim(); // 行のテキストを取得

// ファイル全体のテキストを取得
let fileContent = editor.getText();

// タスクのプロパティが存在するかチェック
const propertyRegex = /^---\n([\s\S]+?)\n---/s;
let propertiesMatch = fileContent.match(propertyRegex);

if (propertiesMatch) {
    // プロパティブロックが存在する場合
    let properties = propertiesMatch[1];
    let updatedProperties;

    if (/startTime:\s*.+/g.test(properties)) {
        // "startTime" プロパティを更新
        updatedProperties = properties.replace(/startTime:\s*.*/g, `startTime: ${fullNow}`);
    } else {
        // "startTime" プロパティがない場合、追加
        updatedProperties = properties + `\nstartTime: ${fullNow}`;
    }

    // プロパティ部分のみを置換してテキストを更新
    fileContent = fileContent.replace(properties, updatedProperties);
    editor.setText(fileContent);

} else {
    // プロパティブロックがない場合、新しいプロパティブロックを追加
    const newProperties = `---\nstartTime: ${fullNow}\n---\n`;
    fileContent = newProperties + fileContent;
    editor.setText(fileContent);
}

// 1. [ ] のみの場合: 開始時刻を追加
if (/^- \[ \] (?!\d{2}:\d{2}-)/.test(currentLineText)) {
    const updatedLine = currentLineText.replace("- [ ] ", `- [ ] ${now}- `);
    editor.replaceTextInRange(start, end, updatedLine);
}

// 2. [ ] hh:mm- の場合: 終了時刻を追加して、完了状態にする
else if (/^- \[ \] \d{2}:\d{2}-/.test(currentLineText)) {
    const updatedLine = currentLineText.replace(/^-\s\[ \]\s(\d{2}:\d{2})-/, `- [x] $1-${now}`);
    editor.replaceTextInRange(start, end, updatedLine);
}

// 3. [x] hh:mm-hh:mm の場合: 行を複製し、複製した行を未完了状態にして、hh:mm-hh:mm の部分を消す
else if (/^- \[x\] \d{2}:\d{2}-\d{2}:\d{2}/.test(currentLineText)) {
    const timeRangeEnd = currentLineText.indexOf(" ", 16);
    const taskDescription = currentLineText.slice(timeRangeEnd).trim();
    const newLine = `- [ ] ${taskDescription}`;
    
    editor.replaceTextInRange(start, end, currentLineText); // 現在の行を保持
    editor.replaceTextInRange(end, end, "\n" + newLine); // 新しい行を追加
}

// 処理完了後、カーソル位置を元の位置に戻す
editor.setSelectedRange(initialCursorRange[0], initialCursorRange[1]);
しのしの

1Writer 終了予定時刻のリスト表示

// ノートの全テキストを取得
let noteText = editor.getText();

// 未完了タスクを正規表現で抽出(開始時刻があってもなくても対応)
let tasks = noteText.match(/- \[ \](?: (\d{2}:\d{2})-)? \\(\d+)(.*)/g);

if (tasks) {
    // 結果を格納するリスト
    let endTimes = [];
    let currentDateTime = new Date(); // 現在の時刻を基準にする

    tasks.forEach((task, index) => {
        // タスクから開始時刻、所要時間、タスク名を抽出
        let match = task.match(/(?: (\d{2}:\d{2})-)? \\(\d+)(.*)/);
        let startTime = match[1];  // 開始時刻 (optional)
        let duration = parseInt(match[2], 10);  // 所要時間
        let taskName = match[3].trim();  // タスク名

        // 開始時刻が指定されていない場合は、現在の累積時刻を基準にする
        if (startTime && index === 0) {
            // 最初のタスクで開始時刻が指定されている場合は、それを基準にする
            let [hours, minutes] = startTime.split(':').map(Number);
            currentDateTime.setHours(hours);
            currentDateTime.setMinutes(minutes);
        }

        // 所要時間を累積する
        currentDateTime = new Date(currentDateTime.getTime() + duration * 60000);
        let endTime = currentDateTime.toTimeString().slice(0, 5);  // "HH:MM"形式にフォーマット

        // 結果を「タスク名: 終了予定時刻」の形式でリストに追加
        endTimes.push(`${taskName}: ${endTime}`);
    });

    // 結果をリスト形式で表示
    ui.list("未完了タスクの終了予定時刻", endTimes);
} else {
    ui.alert("未完了タスクが見つかりませんでした。");
}
しのしの

Obsidian 見積時間の入力補助

\を入力し、\の直後にカーソルを移動

見積時間の入力イメージ

  • \5
  • 09:00- \100
  • 09:00-10:00 \100
  • \5 タスク名
  • 09:00- \100 タスク名
  • 09:00-10:00 \100 タスク名

テストパターン

- [ ] \5
- [ ] 09:00- \100
- [x] 09:00-10:00 \100
- [ ] \5 タスク名
- [ ] 09:00- \100 タスク名
- [x] 09:00-10:00 \100 タスク名
- [ ] 
- [ ] 09:00- 
- [x] 09:00-10:00
- [ ] タスク名
- [ ] 09:00- タスク名
- [x] 09:00-10:00 タスク名

コード

<%*
const editor = app.workspace.activeLeaf.view.editor; // 現在のエディタを取得
const cursor = editor.getCursor(); // カーソル位置を取得
const cursorLine = cursor.line; // カーソル行を取得
const currentLineText = editor.getLine(cursorLine); // カーソル行のテキストを取得

let updatedLine = currentLineText;
let newCursorPos = cursor.ch; // 新しいカーソル位置

// パターン2: - [ ] hh:mm- タスク名 → バックスラッシュをタスク名の直前に挿入
let pattern2WithTask = /^- \[ \] \d{2}:\d{2}-\s*(.*)$/;

// パターン3: - [x] hh:mm-hh:mm タスク名 → バックスラッシュをタスク名の直前に挿入
let pattern3WithTask = /^- \[x\] \d{2}:\d{2}-\d{2}:\d{2}\s*(.*)$/;

// パターン4: - [ ] タスク名 → タスク名の前にバックスラッシュを挿入
let pattern4WithTask = /^- \[ \]\s+(.*)$/;

// バックスラッシュが既に存在する場合の処理
let existingBackslashPattern = /\\(\d+)?/;

// 既存のバックスラッシュがある場合の処理を最優先
if (existingBackslashPattern.test(currentLineText)) {
    // 既存のバックスラッシュがある場合は、その直後の数字の末尾にカーソルを移動
    let match = existingBackslashPattern.exec(currentLineText);
    if (match && match[1]) {
        // バックスラッシュの後に数字がある場合
        newCursorPos = currentLineText.indexOf(match[1]) + match[1].length;
    } else {
        // 数字がない場合
        newCursorPos = currentLineText.indexOf("\\") + 1;
    }
} else if (pattern3WithTask.test(currentLineText)) {
    // パターン3に一致: バックスラッシュをタスク名の前に挿入
    updatedLine = currentLineText.replace(/^(- \[x\] \d{2}:\d{2}-\d{2}:\d{2})\s*(.*)$/, "$1 \\$2");
    newCursorPos = updatedLine.indexOf("\\") + 1;
} else if (pattern2WithTask.test(currentLineText)) {
    // パターン2に一致: バックスラッシュをタスク名の前に挿入
    updatedLine = currentLineText.replace(/^(- \[ \] \d{2}:\d{2}-)\s*(.*)$/, "$1 \\$2");
    newCursorPos = updatedLine.indexOf("\\") + 1;
} else if (pattern4WithTask.test(currentLineText)) {
    // パターン4に一致: タスク名の前にバックスラッシュを挿入
    updatedLine = currentLineText.replace(/^(- \[ \])\s*(.*)$/, "$1 \\$2");
    newCursorPos = updatedLine.indexOf("\\") + 1;
}

// 行を更新してカーソルを新しい位置に設定
editor.setLine(cursorLine, updatedLine);
editor.setCursor(cursorLine, newCursorPos);
%>
しのしの

1Writer 見積時間の入力補助

// エディタの現在のカーソル位置と行を取得
const cursor = editor.getSelectedRange();
const start = editor.getSelectedLineRange()[0];  // カーソル行の開始位置
const end = editor.getSelectedLineRange()[1];    // カーソル行の終了位置
const currentLineText = editor.getTextInRange(start, end); // 現在の行のテキスト

let updatedLine = currentLineText;
let newCursorPos = cursor.start;  // 新しいカーソル位置

// パターン2: - [ ] hh:mm- タスク名 → バックスラッシュをタスク名の直前に挿入
let pattern2WithTask = /^- \[ \] \d{2}:\d{2}-\s*(.*)$/;

// パターン3: - [x] hh:mm-hh:mm タスク名 → バックスラッシュをタスク名の直前に挿入
let pattern3WithTask = /^- \[x\] \d{2}:\d{2}-\d{2}:\d{2}\s*(.*)$/;

// パターン4: - [ ] タスク名 → タスク名の前にバックスラッシュを挿入
let pattern4WithTask = /^- \[ \]\s+(.*)$/;

// バックスラッシュが既に存在する場合の処理
let existingBackslashPattern = /\\(\d+)?/;

// 既存のバックスラッシュがある場合の処理を最優先
if (existingBackslashPattern.test(currentLineText)) {
    // 既存のバックスラッシュがある場合、その直後の数字の末尾にカーソルを移動
    let match = existingBackslashPattern.exec(currentLineText);
    if (match && match[1]) {
        newCursorPos = start + currentLineText.indexOf(match[1]) + match[1].length;
    } else {
        newCursorPos = start + currentLineText.indexOf("\\") + 1;
    }
} else if (pattern3WithTask.test(currentLineText)) {
    // パターン3に一致: バックスラッシュをタスク名の前に挿入
    updatedLine = currentLineText.replace(/^(- \[x\] \d{2}:\d{2}-\d{2}:\d{2})\s*(.*)$/, "$1 \\$2");
    newCursorPos = start + updatedLine.indexOf("\\") + 1;
} else if (pattern2WithTask.test(currentLineText)) {
    // パターン2に一致: バックスラッシュをタスク名の前に挿入
    updatedLine = currentLineText.replace(/^(- \[ \] \d{2}:\d{2}-)\s*(.*)$/, "$1 \\$2");
    newCursorPos = start + updatedLine.indexOf("\\") + 1;
} else if (pattern4WithTask.test(currentLineText)) {
    // パターン4に一致: タスク名の前にバックスラッシュを挿入
    updatedLine = currentLineText.replace(/^(- \[ \])\s*(.*)$/, "$1 \\$2");
    newCursorPos = start + updatedLine.indexOf("\\") + 1;
}

// 行を更新してカーソル位置を新しい位置に設定
editor.replaceTextInRange(start, end, updatedLine);
editor.setSelectedRange(newCursorPos, newCursorPos);
しのしの

1Writer リピートタスク一括コピー

- [ ] `- [ ]`から始まるもののみ、リピートタスクと判定してコピー
- [ ] !平: 平日
- [ ] !月,金: 曜日指定
- [ ] !0904,0905: 月日指定

テスト

- [ ] !水 テスト 残す
- [ ] !月 テスト 削除
- [ ] 水から始まるタスク 残す
- [ ] 月から始まるタスク 残す
- [ ] !水,月 テスト 残す
- [ ] !0904 月日一つのテスト 残す
- [ ] !0826 月日一つのテスト 削除
- [ ] !0825,0904 月日2つのテスト 残す
- [ ] !0826,0827 月日2つのテスト 削除

コード

// ノート情報の保存
var folder = editor.getFolderPath(); // 現在のノートのフォルダパスを取得
var editingfile = editor.getFileName();
var cursorPosition = editor.getSelectedRange(); // カーソル位置を保存

// ノートタイトルから日付部分を取得(例: "2023-08-20")
var datePattern = /^(\d{4})-(\d{2})-(\d{2})/;
var match = editingfile.match(datePattern);

if (!match) {
    ui.alert("ノートのタイトルに有効な日付が含まれていません。");
    return;
}

// 日付オブジェクトを作成
var noteDate = new Date(match[1], match[2] - 1, match[3]); // 月は0から始まるため-1

// MMDD形式で現在の日付を取得
var currentMMDD = ("0" + (noteDate.getMonth() + 1)).slice(-2) + ("0" + noteDate.getDate()).slice(-2);

// ノートの日付から曜日を取得(0: 日曜, 1: 月曜, ..., 6: 土曜)
var dayOfWeek = noteDate.getDay();

// 曜日のマッピング(0: 日曜 ~ 6: 土曜)
var dayOfWeekMapping = ['日', '月', '火', '水', '木', '金', '土'];

// リピートタスクを記述しているファイルのファイル名
var openfilename = 'リピートタスク.md';

// リピートタスクのファイルを開く(現在のフォルダ内)
editor.openFile(folder + '/' + openfilename, 'edit', call);

function call() {
    // ファイルのテキストを取得
    var text = editor.getText();

    // "- [ ] " で始まる行のみをフィルタリング
    let listData = text.split('\n').filter(line => {
        // タスクが "- [ ] " で始まっているか確認
        if (!line.startsWith('- [ ] ')) return false;

        // リピート条件が指定されているか確認(例: !木,金,MMDD,平)
        let matchedCondition = line.match(/!([月火水木金土日平,\d{4}]+)/);
        if (matchedCondition) {
            let conditions = matchedCondition[1].split(',').map(cond => cond.trim());

            // 曜日が指定されているか確認
            let dayMatch = conditions.some(cond => dayOfWeekMapping.includes(cond));
            if (dayMatch) return conditions.includes(dayOfWeekMapping[dayOfWeek]);

            // MMDD形式のチェック
            let dateMatch = conditions.some(cond => /^\d{4}$/.test(cond));
            if (dateMatch) return conditions.includes(currentMMDD);

            // "平" が指定された場合の処理(平日ならタスクを含める)
            if (conditions.includes('平')) {
                return dayOfWeek >= 1 && dayOfWeek <= 5; // 平日なら含める
            }

            return false; // 条件に合致しない場合は除外
        }

        // 条件が指定されていない場合は常にタスクを含める
        return true;
    });

    // 条件部分の削除処理
    listData = listData.map(line => {
        // リピート条件部分の削除(例: !木,金,0820,平)
        return line.replace(/!([月火水木金土日平,\d{4}]+)/, '').trim();
    });

    ui.hudDismiss();

    if (listData.length === 0) {
        ui.alert("リピート条件に一致するタスクが見つかりませんでした。");
        return;
    }

    const selectedText = listData.join('\n'); // すべてのタスクを1つの文字列に結合

    // 元のノートに戻り、カーソル位置にすべてのタスクを挿入
    editor.openFile(folder + '/' + editingfile, 'edit', function() {
        editor.setSelectedRange(cursorPosition[0]); // カーソル位置に戻る
        editor.replaceSelection(selectedText); // 選択されたテキストを挿入
        editor.setSelectedRange(cursorPosition[0] + selectedText.length); // カーソルを挿入後の位置に移動
    });
}
しのしの

1Writer リピートタスク一括コピー

セクションもコピーされるようにした。

**🗂️セクション名**

コード

// ノート情報の保存
var folder = editor.getFolderPath(); // 現在のノートのフォルダパスを取得
var editingfile = editor.getFileName();
var cursorPosition = editor.getSelectedRange(); // カーソル位置を保存

// ノートタイトルから日付部分を取得(例: "2023-08-20")
var datePattern = /^(\d{4})-(\d{2})-(\d{2})/;
var match = editingfile.match(datePattern);

if (!match) {
    ui.alert("ノートのタイトルに有効な日付が含まれていません。");
    return;
}

// 日付オブジェクトを作成
var noteDate = new Date(match[1], match[2] - 1, match[3]); // 月は0から始まるため-1

// MMDD形式で現在の日付を取得
var currentMMDD = ("0" + (noteDate.getMonth() + 1)).slice(-2) + ("0" + noteDate.getDate()).slice(-2);

// ノートの日付から曜日を取得(0: 日曜, 1: 月曜, ..., 6: 土曜)
var dayOfWeek = noteDate.getDay();

// 曜日のマッピング(0: 日曜 ~ 6: 土曜)
var dayOfWeekMapping = ['日', '月', '火', '水', '木', '金', '土'];

// リピートタスクを記述しているファイルのファイル名
var openfilename = 'リピートタスク.md';

// リピートタスクのファイルを開く(現在のフォルダ内)
editor.openFile(folder + '/' + openfilename, 'edit', call);

function call() {
    // ファイルのテキストを取得
    var text = editor.getText();

    // "- [ ] " または "🗂️" で始まる行のみをフィルタリング
    let listData = text.split('\n').filter(line => {
        // タスクが "- [ ] " または "🗂️" で始まっているか確認
        if (!(line.startsWith('- [ ] ') || line.startsWith('\**🗂️'))) return false;

        // リピート条件が指定されているか確認(例: !木,金,MMDD,平)
        let matchedCondition = line.match(/!([月火水木金土日平,\d{4}]+)/);
        if (matchedCondition) {
            let conditions = matchedCondition[1].split(',').map(cond => cond.trim());

            // 曜日が指定されているか確認
            let dayMatch = conditions.some(cond => dayOfWeekMapping.includes(cond));
            if (dayMatch) return conditions.includes(dayOfWeekMapping[dayOfWeek]);

            // MMDD形式のチェック
            let dateMatch = conditions.some(cond => /^\d{4}$/.test(cond));
            if (dateMatch) return conditions.includes(currentMMDD);

            // "平" が指定された場合の処理(平日ならタスクを含める)
            if (conditions.includes('平')) {
                return dayOfWeek >= 1 && dayOfWeek <= 5; // 平日なら含める
            }

            return false; // 条件に合致しない場合は除外
        }

        // 条件が指定されていない場合は常にタスクを含める
        return true;
    });

    // 条件部分の削除処理
    listData = listData.map(line => {
        // リピート条件部分の削除(例: !木,金,0820,平)
        return line.replace(/!([月火水木金土日平,\d{4}]+)/, '').trim();
    });

    ui.hudDismiss();

    if (listData.length === 0) {
        ui.alert("リピート条件に一致するタスクが見つかりませんでした。");
        return;
    }

    const selectedText = listData.join('\n'); // すべてのタスクを1つの文字列に結合

    // 元のノートに戻り、カーソル位置にすべてのタスクを挿入
    editor.openFile(folder + '/' + editingfile, 'edit', function() {
        editor.setSelectedRange(cursorPosition[0]); // カーソル位置に戻る
        editor.replaceSelection(selectedText); // 選択されたテキストを挿入
        editor.setSelectedRange(cursorPosition[0] + selectedText.length); // カーソルを挿入後の位置に移動
    });
}
しのしの

1Writer 終了予定時刻等表示

現在時刻基準
見積時間が入っていないタスクがあると挙動が変

// ノートの全テキストを取得
let noteText = editor.getText();

// 未完了タスクおよび🗂️で始まる行を正規表現で抽出
let tasks = noteText.match(/(?:- \[ \](?: (\d{2}:\d{2})-)? \\(\d+)(.*)|🗂️.*)/g);

if (tasks) {
    // 結果を格納するリスト
    let endTimes = [];
    let currentDateTime = new Date(); // 現在の時刻を基準にする

    tasks.forEach((task, index) => {
        // 🗂️で始まる行を処理
        if (task.startsWith("🗂️")) {
            endTimes.push(task);  // タスク名をそのままリストに追加
            return;  // 次のループへ
        }

        // タスクから開始時刻、所要時間、タスク名を抽出
        let match = task.match(/(?: (\d{2}:\d{2})-)? \\(\d+)(.*)/);
        let startTime = match[1];  // 開始時刻 (optional)
        let duration = parseInt(match[2], 10);  // 所要時間
        let taskName = match[3].trim();  // タスク名

        let baseTime;
        let actualStartTime;

        // 一つ目のタスクの処理
        if (index === 0 || !endTimes[endTimes.length - 1].startsWith("終了予定時刻")) {
            if (startTime) {
                // 開始時刻が指定されている場合
                let [hours, minutes] = startTime.split(':').map(Number);
                baseTime = new Date();
                baseTime.setHours(hours);
                baseTime.setMinutes(minutes);
                baseTime.setSeconds(0);
                baseTime.setMilliseconds(0);
            } else {
                // 開始時刻がない場合、現在時刻を基準にする
                baseTime = new Date(currentDateTime);
            }
        } else {
            // 2つ目以降のタスクは、前のタスクの終了予定時刻を基準にする
            baseTime = new Date(currentDateTime);
        }

        // 基準時刻に所要時間を追加して終了時刻を計算
        actualStartTime = new Date(baseTime);  // 実際の開始時刻
        let endDate = new Date(baseTime.getTime() + duration * 60000);

        // 計算した終了予定時刻が現在時刻より前の場合、終了時刻を現在時刻に設定
        if (endDate < new Date()) {
            endDate = new Date();
        }

        // "HH:MM"形式にフォーマット
        let startTimeStr = actualStartTime.toTimeString().slice(0, 5);
        let endTimeStr = endDate.toTimeString().slice(0, 5);

        // 結果を「タスク名: 開始時刻 - 終了予定時刻 (見積時間分)」の形式でリストに追加
        endTimes.push(`${taskName}||${startTimeStr} - ${endTimeStr} (${duration}分)`);

        // 次のタスクの基準時刻として設定
        currentDateTime = endDate;
    });

    // 結果をリスト形式で表示
    ui.list("未完了タスクと🗂️行の終了予定時刻", endTimes);
} else {
    ui.alert("未完了タスクが見つかりませんでした。");
}
しのしの

1Writer 終了予定時刻等表示

// ノートの全テキストを取得
let noteText = editor.getText();

// 未完了タスクおよび🗂️で始まる行を正規表現で抽出
let tasks = noteText.match(/(?:- \[ \](?: (\d{2}:\d{2})-)? \\(\d+)(.*)|🗂️.*)/g);

if (tasks) {
    // 結果を格納するリスト
    let endTimes = [];
    let currentDateTime = new Date(); // 現在の時刻を基準にする

    tasks.forEach((task, index) => {
        // 🗂️で始まる行を処理
        if (task.startsWith("🗂️")) {
            endTimes.push(task);  // タスク名をそのままリストに追加
            return;  // 次のループへ
        }

        // タスクから開始時刻、所要時間、タスク名を抽出
        let match = task.match(/(?: (\d{2}:\d{2})-)? \\(\d+)(.*)/);
        let startTime = match[1];  // 開始時刻 (optional)
        let duration = parseInt(match[2], 10);  // 所要時間
        let taskName = match[3].trim();  // タスク名

        let baseTime;
        let actualStartTime;
        let scheduleTimeWarning = '';

        // 一つ目のタスクの処理
        if (index === 0 || !endTimes[endTimes.length - 1].startsWith("終了予定時刻")) {
            if (startTime) {
                // 開始時刻が指定されている場合
                let [hours, minutes] = startTime.split(':').map(Number);
                baseTime = new Date();
                baseTime.setHours(hours);
                baseTime.setMinutes(minutes);
                baseTime.setSeconds(0);
                baseTime.setMilliseconds(0);
            } else {
                // 開始時刻がない場合、現在時刻を基準にする
                baseTime = new Date(currentDateTime);
            }
        } else {
            // 2つ目以降のタスクは、前のタスクの終了予定時刻を基準にする
            baseTime = new Date(currentDateTime);
        }

        // 基準時刻に所要時間を追加して終了時刻を計算
        actualStartTime = new Date(baseTime);  // 実際の開始時刻
        let endDate = new Date(baseTime.getTime() + duration * 60000);

        // 計算した終了予定時刻が現在時刻より前の場合、終了時刻を現在時刻に設定
        if (endDate < new Date()) {
            endDate = new Date();
        }

        // スケジュール時間のチェックと警告の設定
        let scheduleTimeMatch = taskName.match(/@(\d{2}:\d{2})/);
        if (scheduleTimeMatch) {
            let [schedHours, schedMinutes] = scheduleTimeMatch[1].split(':').map(Number);
            let scheduleTime = new Date();
            scheduleTime.setHours(schedHours);
            scheduleTime.setMinutes(schedMinutes);
            scheduleTime.setSeconds(0);
            scheduleTime.setMilliseconds(0);

            // 開始予定時刻がスケジュールされた時間より後なら警告をつける
            if (actualStartTime > scheduleTime) {
                scheduleTimeWarning = '⚠️';
            }
        }

        // "HH:MM"形式にフォーマット
        let startTimeStr = actualStartTime.toTimeString().slice(0, 5);
        let endTimeStr = endDate.toTimeString().slice(0, 5);

        // 残り時間の計算(現在時刻 - 開始時刻)
        let elapsedTime = Math.ceil((new Date() - actualStartTime) / 60000);

        // 超過時間のチェック
        let remainingTimeInfo = '';
        if (elapsedTime > duration) {
            let overdueMinutes = elapsedTime - duration;
            remainingTimeInfo = `⚠️超過${overdueMinutes}`;
        } else {
            let remainingTime = duration - elapsedTime;
            remainingTimeInfo = `➡️残り${remainingTime}`;
        }

        // 結果を「タスク名: 開始時刻 - 終了予定時刻 (見積時間分)」の形式でリストに追加
        let taskOutput = `${taskName}||${scheduleTimeWarning}${startTimeStr} - ${endTimeStr} (${duration}分)`;

        if (startTime) {
            taskOutput += ` ${remainingTimeInfo}`;
        }

        endTimes.push(taskOutput);

        // 次のタスクの基準時刻として設定
        currentDateTime = endDate;
    });

    // 結果をリスト形式で表示
    ui.list("未完了タスクと🗂️行の終了予定時刻", endTimes);
} else {
    ui.alert("未完了タスクが見つかりませんでした。");
}
しのしの

1Writer 開始・終了

アクション終了後のカーソル位置の修正

開始時: カーソル行の末尾
終了時: 次の行
コピー時: コピー先の行の末尾

// 現在時刻を取得
const now = new Date().toLocaleTimeString('ja-JP', { hour: '2-digit', minute: '2-digit' });
const fullNow = new Date().toLocaleTimeString('ja-JP', { hour: '2-digit', minute: '2-digit', second: '2-digit' });

// 現在の選択範囲とカーソル位置を保存
const initialCursorRange = editor.getSelectedRange();
const [start, end] = editor.getSelectedLineRange(); // 現在の選択行の範囲を取得
const currentLineText = editor.getTextInRange(start, end).trim(); // 行のテキストを取得

// ファイル全体のテキストを取得
let fileContent = editor.getText();

// タスクのプロパティが存在するかチェック
const propertyRegex = /^---\n([\s\S]+?)\n---/s;
let propertiesMatch = fileContent.match(propertyRegex);

if (propertiesMatch) {
    // プロパティブロックが存在する場合
    let properties = propertiesMatch[1];
    let updatedProperties;

    if (/startTime:\s*.+/g.test(properties)) {
        // "startTime" プロパティを更新
        updatedProperties = properties.replace(/startTime:\s*.*/g, `startTime: ${fullNow}`);
    } else {
        // "startTime" プロパティがない場合、追加
        updatedProperties = properties + `\nstartTime: ${fullNow}`;
    }

    // プロパティ部分のみを置換してテキストを更新
    fileContent = fileContent.replace(properties, updatedProperties);
    editor.setText(fileContent);

} else {
    // プロパティブロックがない場合、新しいプロパティブロックを追加
    const newProperties = `---\nstartTime: ${fullNow}\n---\n`;
    fileContent = newProperties + fileContent;
    editor.setText(fileContent);
}

// カーソルを設定するための変数
let newCursorPos = initialCursorRange[0];

// 1. [ ] のみの場合: 開始時刻を追加
if (/^- \[ \] (?!\d{2}:\d{2}-)/.test(currentLineText)) {
    const updatedLine = currentLineText.replace("- [ ] ", `- [ ] ${now}- `);
    editor.replaceTextInRange(start, end, updatedLine);
    
    // カーソルをカーソル行の末尾に移動
    newCursorPos = start + updatedLine.length;

}

// 2. [ ] hh:mm- の場合: 終了時刻を追加して、完了状態にする
else if (/^- \[ \] \d{2}:\d{2}-/.test(currentLineText)) {
    const updatedLine = currentLineText.replace(/^-\s\[ \]\s(\d{2}:\d{2})-/, `- [x] $1-${now}`);
    editor.replaceTextInRange(start, end, updatedLine);
    
    // カーソルを次の行に設定
    newCursorPos = end + updatedLine.length - currentLineText.length+1;

}

// 3. [x] hh:mm-hh:mm の場合: 行を複製し、複製した行を未完了状態にして、hh:mm-hh:mm の部分を消す
else if (/^- \[x\] \d{2}:\d{2}-\d{2}:\d{2}/.test(currentLineText)) {
    const timeRangeEnd = currentLineText.indexOf(" ", 16);
    const taskDescription = currentLineText.slice(timeRangeEnd).trim();
    const newLine = `- [ ] ${taskDescription}`;
    
    editor.replaceTextInRange(start, end, currentLineText); // 現在の行を保持
    editor.replaceTextInRange(end, end, "\n" + newLine); // 新しい行を追加
    
    // カーソルを次の行の末尾に設定
    newCursorPos = end + newLine.length + 1;
}

// 処理完了後、カーソル位置を設定
editor.setSelectedRange(newCursorPos, newCursorPos);
しのしの

Obsidian 開始・終了

処理後のカーソル位置を修正
⚠️タスクコピー時、もとの行の終了時刻が複製されてしまうことが時々ある。原因未特定。

<%* 
// 現在時刻を取得
const now = tp.date.now("HH:mm");
const fullNow = tp.date.now("HH:mm:ss");

// 現在のエディタとカーソル行を取得
const editor = app.workspace.activeLeaf.view.editor;
const cursorLine = editor.getCursor().line;
const cursorCh = editor.getCursor().ch; // カーソルの列位置
const currentLineText = editor.getLine(cursorLine).trim();

// タスクのプロパティが存在するかチェック
const propertyRegex = /^---\n([\s\S]+?)\n---/s;
let fileContent = app.workspace.activeLeaf.view.data;

let propertiesMatch = fileContent.match(propertyRegex);

if (propertiesMatch) {
    // プロパティブロックが存在する場合
    let properties = propertiesMatch[1];
    let updatedProperties;

    if (/startTime:\s*.+/g.test(properties)) {
        // "startTime" プロパティを更新
        updatedProperties = properties.replace(/startTime:\s*.*/g, `startTime: ${fullNow}`);
    } else {
        // "startTime" プロパティがない場合、追加
        updatedProperties = properties + `\nstartTime: ${fullNow}`;
    }

    // プロパティ部分のみを置換してファイルを更新
    const updatedFileContent = fileContent.replace(properties, updatedProperties);
    await app.vault.modify(app.workspace.getActiveFile(), updatedFileContent);

} else {
    // プロパティブロックがない場合、新しいプロパティブロックを追加
    const newProperties = `---\nstartTime: ${fullNow}\n---\n`;
    const updatedFileContent = newProperties + fileContent;
    await app.vault.modify(app.workspace.getActiveFile(), updatedFileContent);
}

// 1. [ ] のみの場合: 開始時刻を追加し、カーソルを元の位置 + 追加した文字数に移動
if (/^- \[ \] (?!\d{2}:\d{2}-)/.test(currentLineText)) {
    const updatedLine = currentLineText.replace("- [ ] ", `- [ ] ${now}- `);
    editor.setLine(cursorLine, updatedLine);

    // カーソルを元の位置 + 追加した文字数に移動
    const newCursorPos = cursorCh + now.length + 2; // 'hh:mm-' の長さ分移動
    editor.setCursor({ line: cursorLine, ch: newCursorPos });

} 

// 2. [ ] hh:mm- の場合: 終了時刻を追加して完了状態にし、カーソルを次の行の末尾に移動
else if (/^- \[ \] \d{2}:\d{2}-/.test(currentLineText)) {
    const updatedLine = currentLineText.replace(/^-\s\[ \]\s(\d{2}:\d{2})-/, `- [x] $1-${now}`);
    editor.setLine(cursorLine, updatedLine);

    // カーソルを次の行の末尾に移動
    const nextLineLength = editor.getLine(cursorLine + 1).length;
    editor.setCursor({ line: cursorLine + 1, ch: nextLineLength });

} 

// 3. [x] hh:mm-hh:mm の場合: 行を複製し、カーソルを新しい行の末尾に移動
else if (/^- \[x\] \d{2}:\d{2}-\d{2}:\d{2}/.test(currentLineText)) {
    const timeRangeEnd = currentLineText.indexOf(" ", 16);
    const taskDescription = currentLineText.slice(timeRangeEnd).trim();
    const newLine = `- [ ] ${taskDescription}`;
    editor.replaceRange(newLine + "\n", { line: cursorLine + 1, ch: 0 });

    // カーソルを新しい行の末尾に移動
    const newLineLength = newLine.length;
    editor.setCursor({ line: cursorLine + 1, ch: newLineLength });
}
%>
しのしの

1Writer カーソル行を削除

カーソル行を削除し、削除した行の下の行を選択

// カーソルがある行の範囲を取得
let lineRange = editor.getSelectedLineRange();
let start = lineRange[0];
let end = lineRange[1];

// 改行も含めて完全に行を削除
editor.replaceTextInRange(start, end + 1, "");

// 次の行の開始位置にカーソルを移動
editor.setSelectedRange(start);
しのしの

1Writer 開始・終了

空行でも実行できるように変更
- [ ] のみのとき実行できない問題は未解決

// 現在時刻を取得
const now = new Date().toLocaleTimeString('ja-JP', { hour: '2-digit', minute: '2-digit' });
const fullNow = new Date().toLocaleTimeString('ja-JP', { hour: '2-digit', minute: '2-digit', second: '2-digit' });

// 現在の選択範囲とカーソル位置を保存
const initialCursorRange = editor.getSelectedRange();
const [start, end] = editor.getSelectedLineRange(); // 現在の選択行の範囲を取得
const currentLineText = editor.getTextInRange(start, end).trim(); // 行のテキストを取得

// 行が空白の場合、新しいタスクを作成
if (!currentLineText) {
    const newTask = `- [ ] ${now}- `;
    editor.replaceTextInRange(start, end, newTask);
    editor.setSelectedRange(start + newTask.length, start + newTask.length);
    return;
}

// ファイル全体のテキストを取得
let fileContent = editor.getText();

// タスクのプロパティが存在するかチェック
const propertyRegex = /^---\n([\s\S]+?)\n---/s;
let propertiesMatch = fileContent.match(propertyRegex);

if (propertiesMatch) {
    // プロパティブロックが存在する場合
    let properties = propertiesMatch[1];
    let updatedProperties;

    if (/startTime:\s*.+/g.test(properties)) {
        // "startTime" プロパティを更新
        updatedProperties = properties.replace(/startTime:\s*.*/g, `startTime: ${fullNow}`);
    } else {
        // "startTime" プロパティがない場合、追加
        updatedProperties = properties + `\nstartTime: ${fullNow}`;
    }

    // プロパティ部分のみを置換してテキストを更新
    fileContent = fileContent.replace(properties, updatedProperties);
    editor.setText(fileContent);

} else {
    // プロパティブロックがない場合、新しいプロパティブロックを追加
    const newProperties = `---\nstartTime: ${fullNow}\n---\n`;
    fileContent = newProperties + fileContent;
    editor.setText(fileContent);
}

// カーソルを設定するための変数
let newCursorPos = initialCursorRange[0];

// 1. [ ] のみの場合: 開始時刻を追加
if (/^- \[ \] (?!\d{2}:\d{2}-)/.test(currentLineText)) {
    const updatedLine = currentLineText.replace("- [ ] ", `- [ ] ${now}- `);
    editor.replaceTextInRange(start, end, updatedLine);
    
    // カーソルをカーソル行の末尾に移動
    newCursorPos = start + updatedLine.length;

}

// 2. [ ] hh:mm- の場合: 終了時刻を追加して、完了状態にする
else if (/^- \[ \] \d{2}:\d{2}-/.test(currentLineText)) {
    const updatedLine = currentLineText.replace(/^-\s\[ \]\s(\d{2}:\d{2})-/, `- [x] $1-${now}`);
    editor.replaceTextInRange(start, end, updatedLine);
    
    // カーソルを次の行に設定
    newCursorPos = end + updatedLine.length - currentLineText.length + 1;

}

// 3. [x] hh:mm-hh:mm の場合: 行を複製し、複製した行を未完了状態にして、hh:mm-hh:mm の部分を消す
else if (/^- \[x\] \d{2}:\d{2}-\d{2}:\d{2}/.test(currentLineText)) {
    const timeRangeEnd = currentLineText.indexOf(" ", 16);
    const taskDescription = currentLineText.slice(timeRangeEnd).trim();
    const newLine = `- [ ] ${taskDescription}`;
    
    editor.replaceTextInRange(start, end, currentLineText); // 現在の行を保持
    editor.replaceTextInRange(end, end, "\n" + newLine); // 新しい行を追加
    
    // カーソルを次の行の末尾に設定
    newCursorPos = end + newLine.length + 1;
}

// 処理完了後、カーソル位置を設定
editor.setSelectedRange(newCursorPos, newCursorPos);
しのしの

1Writer リピートタスクの編集

// 1. リピートタスク.mdの内容を取得してリスト表示
let folderPath = editor.getFolderPath();
let repeatTaskFilePath = folderPath + '/リピートタスク.md';

var editingfile = editor.getFileName();
var cursorPosition = editor.getSelectedRange(); // カーソル位置を保存

let originalNoteContent = editor.getText(); // 現在編集中のノートの内容を保存
let originalSelectionRange = editor.getSelectedRange(); // 現在のカーソル位置を保存

// リピートタスク.mdを開いて内容を取得
editor.openFile(repeatTaskFilePath, 'edit', function() {
    let content = editor.getText(); // リピートタスク.mdの内容を取得
    let tasks = content.split('\n').filter(task => task.trim() !== '');

    // タスクリストを表示するためのデータを準備
    let listData = tasks.map((task, index) => `${task}|${index}`);
    
    // 2. 選択したタスクに対する処理を選択
    ui.list('リピートタスク', listData, false, function(selectedValues, selectedIndices) {
        if (!selectedValues) {
            ui.hudError('タスクが選択されていません');
            return;
        }

        let selectedTaskIndex = selectedIndices[0];
        let selectedTask = tasks[selectedTaskIndex];

        // 処理選択リストを表示
        let actions = ['編集', '移動', '削除'];
        ui.list('処理を選択してください', actions, false, function(actionValues, actionIndices) {
            let action = actions[actionIndices[0]];
            switch(action) {
                case '編集':
                    // 3.1 編集:タスク内容を編集
                    ui.input('タスクを編集', selectedTask, '', 'text', function(editedTask) {
                        tasks[selectedTaskIndex] = editedTask;
                        saveChangesToRepeatTaskFile(tasks);
                        ui.hudSuccess('タスクが編集されました');
                    });
                    break;

                case '移動':
                    // 3.2 移動:タスクの移動
                    ui.list('移動先を選択してください', listData, false, function(moveValues, moveIndices) {
                        let moveIndex = moveIndices[0];
                        
                        // 選択したタスクを削除し、指定したタスクの直後に挿入
                        let movedTask = tasks.splice(selectedTaskIndex, 1)[0];
                        let insertIndex = (moveIndex >= selectedTaskIndex) ? moveIndex : moveIndex + 1;
                        tasks.splice(insertIndex, 0, movedTask);
                        saveChangesToRepeatTaskFile(tasks);
                        ui.hudSuccess('タスクが移動されました');
                    });
                    break;

                case '削除':
                    // 3.3 削除:確認ポップアップを表示し、タスクを削除
                    ui.alert('本当に削除しますか?', '確認', '削除', 'キャンセル', function(confirmIndex) {
                        if (confirmIndex === 0) {
                            tasks.splice(selectedTaskIndex, 1);
                            saveChangesToRepeatTaskFile(tasks);
                            ui.hudSuccess('タスクが削除されました');
                        } else {
                            ui.hudDismiss();
                        }
                    });
                    break;

                default:
                    ui.hudError('不明な操作です');
            }
        });
    });

    // リピートタスク.mdの編集内容を保存し、元のノートに戻る
    function saveChangesToRepeatTaskFile(tasks) {
        editor.setText(tasks.join('\n')); // リピートタスク.mdの内容を更新
        editor.close(); // ファイルを閉じる

        // 元のノートを再度開き、元のカーソル位置を復元
		    editor.openFile(folderPath + '/' + editingfile, 'edit', function() {
		        editor.setSelectedRange(cursorPosition[0]); // カーソル位置に戻る
		    });
    }
});
しのしの

1Writer リピートタスクの一括コピー

!条件のあとのスペースが残ってしまう問題の解消

// ノート情報の保存
var folder = editor.getFolderPath(); // 現在のノートのフォルダパスを取得
var editingfile = editor.getFileName();
var cursorPosition = editor.getSelectedRange(); // カーソル位置を保存

// ノートタイトルから日付部分を取得(例: "2023-08-20")
var datePattern = /^(\d{4})-(\d{2})-(\d{2})/;
var match = editingfile.match(datePattern);

if (!match) {
    ui.alert("ノートのタイトルに有効な日付が含まれていません。");
    return;
}

// 日付オブジェクトを作成
var noteDate = new Date(match[1], match[2] - 1, match[3]); // 月は0から始まるため-1

// MMDD形式で現在の日付を取得
var currentMMDD = ("0" + (noteDate.getMonth() + 1)).slice(-2) + ("0" + noteDate.getDate()).slice(-2);

// ノートの日付から曜日を取得(0: 日曜, 1: 月曜, ..., 6: 土曜)
var dayOfWeek = noteDate.getDay();

// 曜日のマッピング(0: 日曜 ~ 6: 土曜)
var dayOfWeekMapping = ['日', '月', '火', '水', '木', '金', '土'];

// リピートタスクを記述しているファイルのファイル名
var openfilename = 'リピートタスク.md';

// リピートタスクのファイルを開く(現在のフォルダ内)
editor.openFile(folder + '/' + openfilename, 'edit', call);

function call() {
    // ファイルのテキストを取得
    var text = editor.getText();

    // "- [ ] " または "🗂️" で始まる行のみをフィルタリング
    let listData = text.split('\n').filter(line => {
        // タスクが "- [ ] " または "🗂️" で始まっているか確認
        if (!(line.startsWith('- [ ] ') || line.startsWith('\**🗂️'))) return false;

        // リピート条件が指定されているか確認(例: !木,金,MMDD,平)
        let matchedCondition = line.match(/!([月火水木金土日平,\d{4}]+)/);
        if (matchedCondition) {
            let conditions = matchedCondition[1].split(',').map(cond => cond.trim());

            // 曜日が指定されているか確認
            let dayMatch = conditions.some(cond => dayOfWeekMapping.includes(cond));
            if (dayMatch) return conditions.includes(dayOfWeekMapping[dayOfWeek]);

            // MMDD形式のチェック
            let dateMatch = conditions.some(cond => /^\d{4}$/.test(cond));
            if (dateMatch) return conditions.includes(currentMMDD);

            // "平" が指定された場合の処理(平日ならタスクを含める)
            if (conditions.includes('平')) {
                return dayOfWeek >= 1 && dayOfWeek <= 5; // 平日なら含める
            }

            return false; // 条件に合致しない場合は除外
        }

        // 条件が指定されていない場合は常にタスクを含める
        return true;
    });

    // 条件部分の削除処理
    listData = listData.map(line => {
        // リピート条件部分の削除(例: !木,金,0820,平)
        return line.replace(/!([月火水木金土日平,\d{4}]+)\s*/, '').trim();
    });

    ui.hudDismiss();

    if (listData.length === 0) {
        ui.alert("リピート条件に一致するタスクが見つかりませんでした。");
        return;
    }

    const selectedText = listData.join('\n'); // すべてのタスクを1つの文字列に結合

    // 元のノートに戻り、カーソル位置にすべてのタスクを挿入
    editor.openFile(folder + '/' + editingfile, 'edit', function() {
        editor.setSelectedRange(cursorPosition[0]); // カーソル位置に戻る
        editor.replaceSelection(selectedText); // 選択されたテキストを挿入
        editor.setSelectedRange(cursorPosition[0] + selectedText.length); // カーソルを挿入後の位置に移動
    });
}
しのしの

1Writer ルーチン登録

カーソル行をリピートタスクに追加

// カーソル行のテキストを取得
const cursorRange = editor.getSelectedLineRange();  // カーソル行の範囲を取得
const cursorLineText = editor.getTextInRange(cursorRange[0], cursorRange[1]);  // カーソル行のテキストを取得

// 行頭の特定の記号とその後の時間パターンに一致する正規表現
const timePattern = /^\d{2}:\d{2}-\d{2}:\d{2}\s*/;
const symbolAndTimePattern = /^- \[.*\]\s*\d{2}:\d{2}-\d{2}:\d{2}\s*/;
const symbolPattern = /^- \[.*\]\s*/;

let newTask;
if (symbolAndTimePattern.test(cursorLineText)) {
    // 記号と時間パターンがある場合、その部分を削除し、行頭に`- [ ] `を追加
    newTask = '' + cursorLineText.replace(symbolAndTimePattern, '- [ ] ').trim();
} else if (symbolPattern.test(cursorLineText)) {
    // 記号のみの場合、その記号を`- [ ] `に置き換える
    newTask = '' + cursorLineText.replace(symbolPattern, '- [ ] ').trim();
} else {
    // パターンがない場合、行頭に`- [ ] `を追加
    newTask = '- [ ] ' + cursorLineText.trim();
}

// 現在のファイル名とカーソル位置を保存
const originalFileName = editor.getFileName();
const originalFolderPath = editor.getFolderPath();
const originalCursorPosition = editor.getSelectedRange(); // [start, end]

// リピートタスク.md のパスを定義
const repeatTaskFilePath = originalFolderPath + "/リピートタスク.md";

// リピートタスク.md の内容を取得し、リストとして表示
editor.openFile(repeatTaskFilePath, 'edit', function() {
    const repeatTaskContent = editor.getText();
    const taskList = repeatTaskContent.split('\n').filter(line => line.trim() !== '');

    // リストとして表示し、追加位置を選択
    ui.list('タスクを挿入する場所を選択してください', taskList, false, (selectedValues, selectedIndices) => {
        if (selectedIndices !== undefined && selectedIndices.length > 0) {
            const insertIndex = selectedIndices[0] + 1; // 選択したタスクの下に挿入するため+1
            
            // 新しいタスクを選択した位置の下に挿入
            taskList.splice(insertIndex, 0, newTask);
            const updatedContent = taskList.join('\n');

            // リピートタスク.md の内容を更新
            editor.setText(updatedContent);
            ui.hudSuccess("リピートタスクに追加されました。");
        } else {
            ui.hudError("タスクの挿入がキャンセルされました。");
        }

        // 元のノートに戻り、カーソル位置を復元
        const originalNotePath = `${originalFolderPath}/${originalFileName}`;
        editor.openFile(originalNotePath, 'edit', function() {
            editor.setSelectedRange(originalCursorPosition[0], originalCursorPosition[1]);
        });
    });
});
しのしの

1Writer カーソル行を翌日にコピー

ログ部分を消してコピーするコードです。
⚠️実行中タスクではうまく動きません。

var dir = 'Dropbox/vault/private/notes/'

// 1. カーソル行のテキストを取得

// カーソル行のテキストを取得
const cursorRange = editor.getSelectedLineRange();  // カーソル行の範囲を取得
const cursorLineText = editor.getTextInRange(cursorRange[0], cursorRange[1]);  // カーソル行のテキストを取得

// 行頭の特定の記号とその後の時間パターンに一致する正規表現
const timePattern = /^\d{2}:\d{2}-\d{2}:\d{2}\s*/;
const symbolAndTimePattern = /^- \[.*\]\s*\d{2}:\d{2}-\d{2}:\d{2}\s*/;
const symbolPattern = /^- \[.*\]\s*/;

let newTask;
if (symbolAndTimePattern.test(cursorLineText)) {
    // 記号と時間パターンがある場合、その部分を削除し、行頭に`- [ ] `を追加
    newTask = '' + cursorLineText.replace(symbolAndTimePattern, '- [ ] ').trim();
} else if (symbolPattern.test(cursorLineText)) {
    // 記号のみの場合、その記号を`- [ ] `に置き換える
    newTask = '' + cursorLineText.replace(symbolPattern, '- [ ] ').trim();
} else {
    // パターンがない場合、行頭に`- [ ] `を追加
    newTask = '- [ ] ' + cursorLineText.trim();

}

// 2. 現在の日付を取得し、翌日の日付を計算
let today = new Date();
today.setDate(today.getDate() + 1); // 翌日を設定
let year = today.getFullYear();
let month = ("0" + (today.getMonth() + 1)).slice(-2); // 月を2桁に
let day = ("0" + today.getDate()).slice(-2); // 日を2桁に

// 3. ノートのファイル名(YYYY-MM-DD形式)を作成
let nextDayFileName = `${dir}${year}-${month}-${day}.md`;

// 4. URLスキームを使って、翌日のノートにテキストを追記
let textToAppend = encodeURIComponent(newTask); // URLエンコード
let urlScheme = `onewriter://x-callback-url/append?path=${nextDayFileName}&text=${textToAppend}`;

// 5. URLを開く(追記処理)
app.openURL(urlScheme);

// 6. 完了ポップアップ
ui.hudSuccess(`${newTask}を翌日にコピーしました。`);
しのしの

1Writer 終了時刻表示

リストのタイトルに、全てのタスクの終了時刻を表示

// ノートの全テキストを取得
let noteText = editor.getText();

// 未完了タスクおよび🗂️で始まる行を正規表現で抽出
let tasks = noteText.match(/(?:- \[ \](?: (\d{2}:\d{2})-)? \\(\d+)(.*)|🗂️.*)/g);

if (tasks) {
    // 結果を格納するリスト
    let endTimes = [];
    let currentDateTime = new Date(); // 現在の時刻を基準にする
    let lastEndTime = '';  // 最後のタスクの終了時刻を保持する変数

    tasks.forEach((task, index) => {
        // 🗂️で始まる行を処理
        if (task.startsWith("🗂️")) {
            endTimes.push(task);  // タスク名をそのままリストに追加
            return;  // 次のループへ
        }

        // タスクから開始時刻、所要時間、タスク名を抽出
        let match = task.match(/(?: (\d{2}:\d{2})-)? \\(\d+)(.*)/);
        let startTime = match[1];  // 開始時刻 (optional)
        let duration = parseInt(match[2], 10);  // 所要時間
        let taskName = match[3].trim();  // タスク名

        let baseTime;
        let actualStartTime;
        let scheduleTimeWarning = '';

        // 一つ目のタスクの処理
        if (index === 0 || !endTimes[endTimes.length - 1].startsWith("終了予定時刻")) {
            if (startTime) {
                // 開始時刻が指定されている場合
                let [hours, minutes] = startTime.split(':').map(Number);
                baseTime = new Date();
                baseTime.setHours(hours);
                baseTime.setMinutes(minutes);
                baseTime.setSeconds(0);
                baseTime.setMilliseconds(0);
            } else {
                // 開始時刻がない場合、現在時刻を基準にする
                baseTime = new Date(currentDateTime);
            }
        } else {
            // 2つ目以降のタスクは、前のタスクの終了予定時刻を基準にする
            baseTime = new Date(currentDateTime);
        }

        // 基準時刻に所要時間を追加して終了時刻を計算
        actualStartTime = new Date(baseTime);  // 実際の開始時刻
        let endDate = new Date(baseTime.getTime() + duration * 60000);

        // 計算した終了予定時刻が現在時刻より前の場合、終了時刻を現在時刻に設定
        if (endDate < new Date()) {
            endDate = new Date();
        }

        // スケジュール時間のチェックと警告の設定
        let scheduleTimeMatch = taskName.match(/@(\d{2}:\d{2})/);
        if (scheduleTimeMatch) {
            let [schedHours, schedMinutes] = scheduleTimeMatch[1].split(':').map(Number);
            let scheduleTime = new Date();
            scheduleTime.setHours(schedHours);
            scheduleTime.setMinutes(schedMinutes);
            scheduleTime.setSeconds(0);
            scheduleTime.setMilliseconds(0);

            // 開始予定時刻がスケジュールされた時間より後なら警告をつける
            if (actualStartTime > scheduleTime) {
                scheduleTimeWarning = '⚠️';
            }
        }

        // "HH:MM"形式にフォーマット
        let startTimeStr = actualStartTime.toTimeString().slice(0, 5);
        let endTimeStr = endDate.toTimeString().slice(0, 5);

        // 最後のタスクの終了時刻を更新
        lastEndTime = endTimeStr;

        // 残り時間の計算(現在時刻 - 開始時刻)
        let elapsedTime = Math.ceil((new Date() - actualStartTime) / 60000);

        // 超過時間のチェック
        let remainingTimeInfo = '';
        if (elapsedTime > duration) {
            let overdueMinutes = elapsedTime - duration;
            remainingTimeInfo = `⚠️超過${overdueMinutes}`;
        } else {
            let remainingTime = duration - elapsedTime;
            remainingTimeInfo = `➡️残り${remainingTime}`;
        }

        // 結果を「タスク名: 開始時刻 - 終了予定時刻 (見積時間分)」の形式でリストに追加
        let taskOutput = `${taskName}||${scheduleTimeWarning}${startTimeStr} - ${endTimeStr} (${duration}分)`;

        if (startTime) {
            taskOutput += ` ${remainingTimeInfo}`;
        }

        endTimes.push(taskOutput);

        // 次のタスクの基準時刻として設定
        currentDateTime = endDate;
    });

    // 結果をリスト形式で表示 (最後のタスクの終了時刻をタイトルに使用)
    ui.list(`終了時刻: ${lastEndTime}`, endTimes);
} else {
    ui.alert("未完了タスクが見つかりませんでした。");
}
しのしの

1Writer リピートタスク一括コピー

セクション表記を### 🗂️セクション名に変更したことな伴う変更。

// ノート情報の保存
var folder = editor.getFolderPath(); // 現在のノートのフォルダパスを取得
var editingfile = editor.getFileName();
var cursorPosition = editor.getSelectedRange(); // カーソル位置を保存

// ノートタイトルから日付部分を取得(例: "2023-08-20")
var datePattern = /^(\d{4})-(\d{2})-(\d{2})/;
var match = editingfile.match(datePattern);

if (!match) {
    ui.alert("ノートのタイトルに有効な日付が含まれていません。");
    return;
}

// 日付オブジェクトを作成
var noteDate = new Date(match[1], match[2] - 1, match[3]); // 月は0から始まるため-1

// MMDD形式で現在の日付を取得
var currentMMDD = ("0" + (noteDate.getMonth() + 1)).slice(-2) + ("0" + noteDate.getDate()).slice(-2);

// ノートの日付から曜日を取得(0: 日曜, 1: 月曜, ..., 6: 土曜)
var dayOfWeek = noteDate.getDay();

// 曜日のマッピング(0: 日曜 ~ 6: 土曜)
var dayOfWeekMapping = ['日', '月', '火', '水', '木', '金', '土'];

// リピートタスクを記述しているファイルのファイル名
var openfilename = 'リピートタスク.md';

// リピートタスクのファイルを開く(現在のフォルダ内)
editor.openFile(folder + '/' + openfilename, 'edit', call);

function call() {
    // ファイルのテキストを取得
    var text = editor.getText();

    // "- [ ] " または "🗂️" で始まる行のみをフィルタリング
    let listData = text.split('\n').filter(line => {
        // タスクが "- [ ] " または "🗂️" で始まっているか確認
        if (!(line.startsWith('- [ ] ') || line.startsWith('\### 🗂️'))) return false;

        // リピート条件が指定されているか確認(例: !木,金,MMDD,平)
        let matchedCondition = line.match(/!([月火水木金土日平,\d{4}]+)/);
        if (matchedCondition) {
            let conditions = matchedCondition[1].split(',').map(cond => cond.trim());

            // 曜日が指定されているか確認
            let dayMatch = conditions.some(cond => dayOfWeekMapping.includes(cond));
            if (dayMatch) return conditions.includes(dayOfWeekMapping[dayOfWeek]);

            // MMDD形式のチェック
            let dateMatch = conditions.some(cond => /^\d{4}$/.test(cond));
            if (dateMatch) return conditions.includes(currentMMDD);

            // "平" が指定された場合の処理(平日ならタスクを含める)
            if (conditions.includes('平')) {
                return dayOfWeek >= 1 && dayOfWeek <= 5; // 平日なら含める
            }

            return false; // 条件に合致しない場合は除外
        }

        // 条件が指定されていない場合は常にタスクを含める
        return true;
    });

    // 条件部分の削除処理
    listData = listData.map(line => {
        // リピート条件部分の削除(例: !木,金,0820,平)
        return line.replace(/!([月火水木金土日平,\d{4}]+)\s*/, '').trim();
    });

    ui.hudDismiss();

    if (listData.length === 0) {
        ui.alert("リピート条件に一致するタスクが見つかりませんでした。");
        return;
    }

    const selectedText = listData.join('\n'); // すべてのタスクを1つの文字列に結合

    // 元のノートに戻り、カーソル位置にすべてのタスクを挿入
    editor.openFile(folder + '/' + editingfile, 'edit', function() {
        editor.setSelectedRange(cursorPosition[0]); // カーソル位置に戻る
        editor.replaceSelection(selectedText); // 選択されたテキストを挿入
        editor.setSelectedRange(cursorPosition[0] + selectedText.length); // カーソルを挿入後の位置に移動
    });
}
しのしの

1Writer 過去データの検索

  1. ポップアップを表示し、キーワード入力を求める
  2. 過去の日次ノート(タイトルがYYYY-MM-DD形式のノート)を検索してリスト表示
// 過去データ検索

const maxDaysToSearch = 30;  // 検索範囲を1ヶ月に制限
const maxTasksToFind = 10;    // 検索するタスクの上限を設定

let originalFilePath;  // 元々開いていたファイルのパスを保持

// 検索キーワードをユーザーに入力させる
function promptUserForKeywords() {
    return new Promise((resolve) => {
        ui.input("タスク検索", "", "検索キーワードを入力してください(スペースで区切るとAND検索)", "default", (input) => {
            if (input) {
                resolve(input.split(" "));  // スペースで分割してキーワード配列に
            } else {
                resolve(null);  // キャンセルされた場合
            }
        });
    });
}

// 指定された日付のノートを開き、その内容を取得する
function getNoteForDate(folderPath, dateString) {
    return new Promise((resolve) => {
        const filePath = `${folderPath}/${dateString}.md`;  // 日付ベースのファイル名
        editor.openFile(filePath, "edit", (success) => {
            if (success) {
                const noteText = editor.getText();
                resolve(noteText);  // ファイルが開けた場合、内容を返す
            } else {
                resolve(null);  // ファイルが開けなかった場合、null を返す
            }
        });
    });
}

// 行がタスクかどうかを判定する関数(タスクは `- [x] ` で始まる)
function isTask(line) {
    return line.startsWith("- [x] ");
}

// 行にすべてのキーワードが含まれているかを判定(AND検索)
function containsKeywords(line, keywords) {
    return keywords.every(keyword => line.toLowerCase().includes(keyword.toLowerCase()));
}

// 指定フォルダ内でタスクを検索する
async function searchTasks(keywords, folderPath) {
    let foundTasks = [];
    let currentDate = new Date();  // 現在の日付を取得
    let daysChecked = 0;

    while (foundTasks.length < maxTasksToFind && daysChecked < maxDaysToSearch) {
        // YYYY-MM-DD形式で日付をフォーマット
        const dateString = currentDate.toISOString().split('T')[0];
        const note = await getNoteForDate(folderPath, dateString);
        
        if (note) {
            const lines = note.split('\n');
            for (const line of lines) {
                if (isTask(line) && containsKeywords(line, keywords)) {
                    foundTasks.push({ date: dateString, task: line });
                    if (foundTasks.length === maxTasksToFind) break;
                }
            }
        }

        // 日付を前日に設定
        currentDate.setDate(currentDate.getDate() - 1);
        daysChecked++;
    }

    return foundTasks;
}

// タスクをリスト形式で表示する
function displayTasks(tasks) {
    if (tasks.length === 0) {
        ui.hudError("該当するタスクが見つかりませんでした。");
    } else {
        const taskList = tasks.map(task => `${task.date}: ${task.task}`);
        ui.list("検索結果", taskList, false, (selectedTask) => {
            if (selectedTask) {
                ui.hudSuccess(`選択されたタスク: ${selectedTask}`);
            }
        });
    }
}

// 元々開いていたファイルを再度開く
function reopenOriginalFile() {
    if (originalFilePath) {
        editor.openFile(originalFilePath, "edit", (success) => {
            if (!success) {
                ui.hudError("元のファイルを開けませんでした。");
            }
        });
    }
}

// メイン処理
async function main() {
    const folderPath = editor.getFolderPath();  // パスを確認して指定する
    const filePath = editor.getFileName();
    originalFilePath = folderPath + "/" + filePath;  // 現在開いているファイルを保存

    const keywords = await promptUserForKeywords();
    if (!keywords) {
        ui.hudError("検索キーワードが入力されませんでした。");
        return;
    }

    // 手動でフォルダパスを指定する(必要に応じて)
    const tasks = await searchTasks(keywords, folderPath);  // タスクを検索
    displayTasks(tasks);  // 結果を表示

    // 最後に元々開いていたファイルを再度開く
    reopenOriginalFile();
}

// 実行
main();
しのしの

1Writer 過去タスク検索

  1. ポップアップを表示し、キーワード入力を求める
  2. 過去の日次ノート(タイトルがYYYY-MM-DD形式のノート)を検索してリスト表示

リスト表示で、1行目にタイトル、2行目に日付、開始時刻、終了時刻、実績値をサブタイトルとして表示。見積時間の表示はやめた。

// 過去データ検索

  

const maxDaysToSearch = 30; // 検索範囲を1ヶ月に制限

const maxTasksToFind = 10; // 検索するタスクの上限を設定

  

let originalFilePath; // 元々開いていたファイルのパスを保持

  

// 検索キーワードをユーザーに入力させる

function promptUserForKeywords() {

return new Promise((resolve) => {

ui.input("タスク検索", "", "検索キーワードを入力してください(スペースで区切るとAND検索)", "default", (input) => {

if (input) {

resolve(input.split(" ")); // スペースで分割してキーワード配列に

} else {

resolve(null); // キャンセルされた場合

}

});

});

}

  

// 指定された日付のノートを開き、その内容を取得する

function getNoteForDate(folderPath, dateString) {

return new Promise((resolve) => {

const filePath = `${folderPath}/${dateString}.md`; // 日付ベースのファイル名

editor.openFile(filePath, "edit", (success) => {

if (success) {

const noteText = editor.getText();

resolve(noteText); // ファイルが開けた場合、内容を返す

} else {

resolve(null); // ファイルが開けなかった場合、null を返す

}

});

});

}

  

// 行がタスクかどうかを判定する関数(タスクは `- [x] ` で始まる)

function isTask(line) {

return line.startsWith("- [x] ");

}

  

// 行にすべてのキーワードが含まれているかを判定(AND検索)

function containsKeywords(line, keywords) {

return keywords.every(keyword => line.toLowerCase().includes(keyword.toLowerCase()));

}

  

// 指定フォルダ内でタスクを検索する

async function searchTasks(keywords, folderPath) {

let foundTasks = [];

let currentDate = new Date(); // 現在の日付を取得

let daysChecked = 0;

  

while (foundTasks.length < maxTasksToFind && daysChecked < maxDaysToSearch) {

// YYYY-MM-DD形式で日付をフォーマット

const dateString = currentDate.toISOString().split('T')[0];

const note = await getNoteForDate(folderPath, dateString);

if (note) {

const lines = note.split('\n');

for (const line of lines) {

if (isTask(line) && containsKeywords(line, keywords)) {

foundTasks.push({ date: dateString, task: line });

if (foundTasks.length === maxTasksToFind) break;

}

}

}

  

// 日付を前日に設定

currentDate.setDate(currentDate.getDate() - 1);

daysChecked++;

}

  

return foundTasks;

}

  

// タスク行を解析し、開始時刻、終了時刻、実績時間、見積時間、タスク名を抽出する

function parseTaskLine(line) {

// 以下の正規表現を使って、タスクの各要素を抽出

const taskRegex = /^- \[x\] (\d{2}:\d{2})-(\d{2}:\d{2})(?:\s+(\d+)?\\?(\d+)?\s+)?(.+)$/;

const match = line.match(taskRegex);

  

if (match) {

return {

startTime: match[1] || null, // 開始時刻

endTime: match[2] || null, // 終了時刻

actualTime: match[3] || null, // 実績時間

estimatedTime: match[4] || null, // 見積時間

taskName: match[5] ? match[5].trim() : "不明" // タスク名

};

} else {

return null; // フォーマットが一致しない場合

}

}

  

// タスクをリスト形式で表示する

function displayTasks(tasks) {

if (tasks.length === 0) {

ui.hudError("該当するタスクが見つかりませんでした。");

} else {

const taskList = tasks.map(task => {

const parsedTask = parseTaskLine(task.task);

if (parsedTask) {

// タスク名をタイトルとして設定

const title = `${parsedTask.taskName}`;

  

// サブタイトル部分を、存在するデータだけで作成

let subtitleParts = [`${task.date}`];

if (parsedTask.startTime && parsedTask.endTime) {

subtitleParts.push(`${parsedTask.startTime}-${parsedTask.endTime}`);

}

if (parsedTask.actualTime) {

subtitleParts.push(`(${parsedTask.actualTime})`);

}

const subtitle = subtitleParts.join(' ');

  

return { title, subtitle };

} else {

return { title: `不明`, subtitle: `${task.date}` }; // 解析できない場合

}

});

  

ui.list("検索結果", taskList.map(t => `${t.title}||${t.subtitle}`), false, (selectedTask) => {

if (selectedTask) {

ui.hudSuccess(`選択されたタスク: ${selectedTask}`);

}

});

}

}

  

// 元々開いていたファイルを再度開く

function reopenOriginalFile() {

if (originalFilePath) {

editor.openFile(originalFilePath, "edit", (success) => {

if (!success) {

ui.hudError("元のファイルを開けませんでした。");

}

});

}

}

  

// メイン処理

async function main() {

const folderPath = editor.getFolderPath(); // パスを確認して指定する

const filePath = editor.getFileName();

originalFilePath = folderPath + "/" + filePath; // 現在開いているファイルを保存

  

const keywords = await promptUserForKeywords();

if (!keywords) {

ui.hudError("検索キーワードが入力されませんでした。");

return;

}

  

// 手動でフォルダパスを指定する(必要に応じて)

const tasks = await searchTasks(keywords, folderPath); // タスクを検索

displayTasks(tasks); // 結果を表示

  

// 最後に元々開いていたファイルを再度開く

reopenOriginalFile();

}

  

// 実行

main();
しのしの

Obsidian 開始・終了

これまでの処理が不安定だったので、1Writerのコードをもとに再作成。

<%*
const now = tp.date.now("HH:mm");
const fullNow = tp.date.now("HH:mm:ss");

// 現在のカーソル位置を取得
const initialCursorPos = this.app.workspace.activeEditor.editor.getCursor();
const currentLineText = this.app.workspace.activeEditor.editor.getLine(initialCursorPos.line).trim();

// 行が空白の場合、新しいタスクを作成
if (!currentLineText) {
    const newTask = `- [ ] ${now}- `;
    this.app.workspace.activeEditor.editor.replaceRange(newTask, {line: initialCursorPos.line, ch: 0}, {line: initialCursorPos.line, ch: currentLineText.length});
    this.app.workspace.activeEditor.editor.setCursor({line: initialCursorPos.line, ch: newTask.length});
    return;
}

// ファイル全体のテキストを取得
let fileContent = tp.file.content;

// タスクのプロパティが存在するかチェック
const propertyRegex = /^---\n([\s\S]+?)\n---/s;
let propertiesMatch = fileContent.match(propertyRegex);

if (propertiesMatch) {
    // プロパティブロックが存在する場合
    let properties = propertiesMatch[1];
    let updatedProperties;

    if (/startTime:\s*.+/g.test(properties)) {
        // "startTime" プロパティを更新
        updatedProperties = properties.replace(/startTime:\s*.*/g, `startTime: ${fullNow}`);
    } else {
        // "startTime" プロパティがない場合、追加
        updatedProperties = properties + `\nstartTime: ${fullNow}`;
    }

    // プロパティ部分のみを置換してテキストを更新
    const fileLines = fileContent.split("\n");
    let startLine = 0, endLine = 0;
    let inPropertiesBlock = false;

    for (let i = 0; i < fileLines.length; i++) {
        if (fileLines[i].startsWith("---")) {
            if (!inPropertiesBlock) {
                startLine = i;
                inPropertiesBlock = true;
            } else {
                endLine = i;
                break;
            }
        }
    }

    this.app.workspace.activeEditor.editor.replaceRange(
        `---\n${updatedProperties}\n---`,
        {line: startLine, ch: 0},
        {line: endLine, ch: fileLines[endLine].length}
    );

} else {
    // プロパティブロックがない場合、新しいプロパティブロックを追加
    const newProperties = `---\nstartTime: ${fullNow}\n---\n`;
    fileContent = newProperties + fileContent;
    await tp.file.write(fileContent);
}

// カーソル位置を取得し、行のテキストを処理
let newCursorPos = initialCursorPos;

// 1. [ ] のみの場合: 開始時刻を追加
if (/^- \[ \] (?!\d{2}:\d{2}-)/.test(currentLineText)) {
    const updatedLine = currentLineText.replace("- [ ] ", `- [ ] ${now}- `);
    this.app.workspace.activeEditor.editor.replaceRange(updatedLine, {line: initialCursorPos.line, ch: 0}, {line: initialCursorPos.line, ch: currentLineText.length});
    
    // カーソルをカーソル行の末尾に移動
    newCursorPos.ch = updatedLine.length;
}

// 2. [ ] hh:mm- の場合: 終了時刻を追加して、完了状態にする
else if (/^- \[ \] \d{2}:\d{2}-/.test(currentLineText)) {
    const updatedLine = currentLineText.replace(/^-\s\[ \]\s(\d{2}:\d{2})-/, `- [x] $1-${now}`);
    this.app.workspace.activeEditor.editor.replaceRange(updatedLine, {line: initialCursorPos.line, ch: 0}, {line: initialCursorPos.line, ch: currentLineText.length});
    
    // カーソルを次の行に設定
    newCursorPos.line += 1;
    newCursorPos.ch = 0;
}

// 3. [x] hh:mm-hh:mm の場合: 行を複製し、複製した行を未完了状態にして、hh:mm-hh:mm の部分を消す
else if (/^- \[x\] \d{2}:\d{2}-\d{2}:\d{2}/.test(currentLineText)) {
    const timeRangeEnd = currentLineText.indexOf(" ", 16);
    const taskDescription = currentLineText.slice(timeRangeEnd).trim();
    const newLine = `- [ ] ${taskDescription}`;
    
    // 現在の行を保持し、複製した行を追加
    this.app.workspace.activeEditor.editor.replaceRange(currentLineText, {line: initialCursorPos.line, ch: 0}, {line: initialCursorPos.line, ch: currentLineText.length});
    this.app.workspace.activeEditor.editor.replaceRange(newLine + "\n", {line: initialCursorPos.line + 1, ch: 0});
    
    // カーソルを次の行の末尾に設定
    newCursorPos.line += 1;
    newCursorPos.ch = newLine.length;
}

// 処理完了後、カーソル位置を設定
this.app.workspace.activeEditor.editor.setCursor(newCursorPos);
%>
しのしの

増えすぎてきたので、これ以降は、機能ごとにスクラップを分けようと思います。