白昼夢中遊行症

Obsidianでタスクをチケット化して管理する環境を作る

作る、というか作りました。
作ったうえで、1か月ほど利用しています。
まあまあ使えるものになったのではないかと思われるので、今のところこんな感じにやってますってのをまとめてみます。
これが万人に対して参考になるとは思いませんが、Obsidianはこういう使い方もできる、という一例を示すことができればと思います。

タスク管理に必要な情報

タスク管理のためには、いくつか必要な情報があります。
たとえば、それが何というタスクであるかを簡潔に示すタイトルは必須でしょうし、それがまだ手を付けられていないものなのか、それとも進行中なのか、終わったのか、放棄したのか、行き詰っているのか、などといった状況を示すものも必要でしょう。
分類好きであればそれがどのようなジャンルのタスクであるかを示すためのタグも要るでしょうし、それがタスクのためのノートであることを示すものも、Obsidianを完全にタスク管理のためだけに使用するのでなければ必要でしょう。

こうした情報はフロントマターに埋め込むことができます。
それぞれのキーにあらかじめプリセットした値を入れたフロントマターをテンプレート化し、ノート作成後にそれを呼び出せば簡単にできます。

ただ、タスクに通し番号を付ける場合、単純なテンプレートでは対応できません。
通し番号もまた、タスク管理のために必要な情報と私は考えます。
通し番号はそれぞれのタスクに一意に付与されるため、タスクのエイリアスとして機能しますし、タスクを作成するたびに番号が大きくなっていくことは、モチベーションにつながります。
自分が今までこなしてきたタスクの数が数字として表れる。
これ以上に気分をよくしてくれるものは(それほど多く)ないでしょう。

Templaterスクリプト

しかしながら、上述したように通し番号を付けるとなると、通常のテンプレートでは対応できません。
そのため、 Templater プラグインを使用します。 Templater プラグインでは、テンプレートの中にスクリプトを仕込むことができます。
これによって、タスクに対して通し番号を付与することができます。 以下のようなファイルを用意します。

createNewTaskFileWithNumberAndUserInput.md

<%*
// 次のファイル番号を取得する関数
async function getNextFileNumber() {
    // 検索対象のタグを指定する
    const tagName = 'task';
    // ディレクトリ内のファイルを取得する
    const files = app.vault.getMarkdownFiles();
    let maxNumber = 0;
    // 各ファイルについて処理する
    for (const file of files) {
        // ファイルのキャッシュされたメタデータを取得する
        const cache = app.metadataCache.getFileCache(file);
        // ファイルに指定されたタグがある場合
        const hasTagInFrontmatter = cache && cache.frontmatter && cache.frontmatter.tags && cache.frontmatter.tags.includes(tagName);
        const hasTagInTags = cache && cache.tags && cache.tags.some(tag => tag.tag === `#${tagName}`);
        if (hasTagInFrontmatter || hasTagInTags) {
            // ファイル名の先頭6文字を数値に変換する
            const number = parseInt(file.basename.slice(0, 6), 10);
            // 最大値を更新する
            if (number > maxNumber) {
                maxNumber = number;
            }
        }
    }
    // 最大値より1大きい値を返す
    return maxNumber + 1;
}

// 次のファイル番号を取得する
const nextFileNumber = await getNextFileNumber();
// ユーザーに文字列の入力を求める  
const userInput = await tp.system.prompt('文字列を入力してください');  
if (userInput === null || userInput === '') {
  // userInputが空の場合の処理
  new Notice("入力がなかったためタスク作成がキャンセルされました");
} else {
// userInputが空でない場合の処理
// 新しいファイル名を作成する(6桁の数字_入力文字列)
const newFileName = `${nextFileNumber.toString().padStart(6, '0')}_${userInput}.md`;

// 現在の日時を取得してフォーマット(moment.jsを使用)
const formattedDate = moment().format('YYYY-MM-DD HH:mm (UTCZ)');

// 挿入する内容を定義
const newFileContent = `---
created: ${formattedDate}
number: ${nextFileNumber}
title: "${userInput}"
tags:
  - task
aliases:
  - "t${nextFileNumber}"
  - "${userInput}"
priority: Middle
status: New
---

# ${userInput}

`;

// 新しいファイルを作成する
const newFile = await app.vault.create(newFileName, newFileContent);

// 現在アクティブなリーフを取得する
const activeLeaf = app.workspace.activeLeaf;

// 新しいファイルを開く
if (activeLeaf) {
  await activeLeaf.openFile(newFile);
}

// 現在アクティブなリーフが存在し、MarkdownViewの場合
if (activeLeaf && activeLeaf.view && activeLeaf.view.getViewType() === 'markdown') {
  // ファイルの最終行を取得する
  const lastLine = activeLeaf.view.editor.lastLine();
  // カーソルを最終行に移動する
  activeLeaf.view.editor.setCursor(lastLine, 0);
} //if(現在アクティブなリーフが存在し、MarkdownViewの場合)終わり
} //userInputが空でない場合の処理終わり
%>

これを Templater プラグインで実行します。
すると、プロンプトが出て入力を求められます。
そこに入力を行うと、「{通し番号}_{プロンプトで入力した内容}」のファイル名のノートが作成され、フロントマターなどのテンプレートが適用されます。

テンプレートの内容を変更したい場合は newFileContent 変数の内容を変更してください。
また、ファイル名のフォーマットを変更したければ、 newFileName 変数を変更すれば自分好みにできます。 1

とにかく、これでタスクファイルが作成されました。

フロントマターの更新

フロントマターの中に、 statuspriority などのキーがあり、それらにはあらかじめデフォルトの値がつけられています。
これらは、状況に応じて更新しましょう。これを更新することで、そのタスクの状態を管理することができます。
更新する際に、手動で書き換えるのが面倒であれば、これもスクリプトによって自動化できます。
Templater で、以下のようなファイルを実行します。

updateStatus.md

<%*
// フロントマターを更新する関数
async function updateFrontmatterValue(values) {
    // アクティブなファイルを取得
    const activeFile = this.app.workspace.getActiveFile();
    // フロントマターを取得
    const frontMatter = this.app.metadataCache.getFileCache(activeFile).frontmatter;
    // キーと値を定義
    const key = "status";
    // ユーザーが選択した値をフロントマターに設定
    const value = await tp.system.suggester(values, values, false, `Select a value for ${key}`);
    // ユーザーが値を選択しなかった場合、メッセージを出してreturnで処理を終わる
    if (value === null) {
        new Notice('値が入力されませんでした。');
        return;
    }   
    // statusの値を更新する
    frontMatter[key] = value;
    // positionキーを削除
    delete frontMatter.position;
    // フロントマターの値を文字列に変換する関数
    const stringifyValue = (key, value) => {
        // aliasesキーの値が配列の場合、各要素をダブルクオートで囲む
        if (key === "aliases" && Array.isArray(value)) {
            return `[${value.map(v => `"${v}"`).join(", ")}]`;
        // 日時を表すキーの値はダブルクオートを外す
        } else if (["created", "deadline", "updated", "completed at"].includes(key)) {
            return value;
        // 値が配列の場合、再帰的に処理する
        } else if (Array.isArray(value)) {
            return `[${value.map(v => stringifyValue(key, v)).join(", ")}]`;
        // 値が文字列の場合、ダブルクオートで囲む
        } else if (typeof value === "string") {
            return `"${value}"`;
        // 値がオブジェクトの場合、JSON文字列に変換する
        } else if (typeof value === "object") {
            return JSON.stringify(value);
        // それ以外の場合、値をそのまま返す
        } else {
            return value;
        }
    };
    // フロントマターを更新
    await this.app.vault.modify(activeFile, "---\n" + Object.entries(frontMatter).map(([key, value]) => `${key}: ${stringifyValue(key, value)}`).join("\n") + "\n---\n" + (await this.app.vault.read(activeFile)).replace(/^---[\s\S]+?---\n/, ""));

}


function handleTaskTag() {
  // taskタグが選択された場合の処理
  const suggestedValues = ["New", "Ongoing", "Done", "Completed", "Abandoned", "Pending"];
  updateFrontmatterValue(suggestedValues)
}

// 指定のタグがあるかどうかを見る関数 
function hasTag(file, tagName) { 
    const cache = app.metadataCache.getFileCache(file); 
    const hasTagInFrontmatter = cache && cache.frontmatter && cache.frontmatter.tags && cache.frontmatter.tags.includes(tagName); 
    const hasTagInTags = cache && cache.tags && cache.tags.some(tag => tag.tag === `#${tagName}`); 
    return hasTagInFrontmatter || hasTagInTags; }

// アクティブなファイルを取得
const activeFile = this.app.workspace.getActiveFile();
// 処理に関係のあるタグを列挙しておく
const tags = ['task'];
// アクティブなタグから、処理に関係のあるタグを抜き出す
const activeTags = tags.filter(tag => hasTag(activeFile, tag));

// 処理に関係のあるタグの数を数える
if (activeTags.length > 1) {
  // アクティブなファイルに複数のタグがある場合の処理
  // 対象のタグから選ぶ
  const tag = await tp.system.suggester(activeTags, activeTags);
  // switchで選ばれたタグに応じた処理を行う
  switch (value) {
    case 'task':
      handleTaskTag();
      break;
  }
} else if (activeTags.length === 1) {
  // アクティブなファイルに1つのタグがある場合の処理
  const tag = activeTags[0];
  // switchで一致したタグに応じた処理を行う
  switch (tag) {
    case 'task':
      handleTaskTag();
      break;
  }
} else {
  // アクティブなファイルに指定されたタグがない場合の処理
  new Notice('指定されたタグがありません。');
}

%>

status キーに入るであろう値がサジェストされるので、入れたい値を選んでください。
自動的にその値に置き換わります。

本来、updateStatus にはもう少しいろいろな機能を持たせているのですが、混乱を避けるためここではそれを簡略化しています。そのため、いろいろと不要なコードもありますが、ひとまずはこれで動きます。

priority キーの値を更新する、updatePriority というのも私は作っていましたが、私自身この値を用意したにもかかわらず今のところ使っていません。なので、その更新用のツールである updatePriority もまた、ほとんど使っていません。
必要であれば updateStatus を書き換えれば作れますので、試してみてください。

エイリアスについて

テンプレートで追加しているフロントマターには aliases キーというものがあります。
ここに値を入力すると、ファイルがその名称としても認識されるます。
タスクのエイリアスにはデフォルトの値として、「t41」などといった「t+通し番号」と、「プロンプトで入力した内容」の二つを設定しています。
断続的に取り組む必要のあるタスクであれば、番号で覚えておけば QuickSwitcher ですぐにそのチケットを開くことができます。
エイリアスの番号を頭に「t」のついたものではなく、ただの通し番号にしてもいいでしょう。
私の場合、このほかにも通し番号を付けて管理しているノートがあるため、区別するために「t」を接頭辞としてつけています。

dataviewで一覧化する

このように、タスクのチケットを作成しました。
タスクのチケットには、statuspriority などのキーがあり、それぞれのタスクの状態ごとに値が入ります。
こうしたメタデータが活きるのは、検索して一覧化する際です。
dataview というプラグインでは、メタデータの値などで検索をかけ、条件に当てはまるファイルを一覧表示できます。

例えば、保留や中止、完了していないタスクの一覧を取得したいとします。
その場合、dataview プラグインを入れた状態で下記のようなコードブロックを貼り付けます。
すると、プレビューでその通りの条件のものが表示されます。

```dataview
table status, priority
from #task 
where status != "Pending"
where status != "Completed"
where status != "Abandoned"
sort number desc
```

dataviewの表示(イメージ)

dataview プラグインでは、javascript を使用することもできます。
そのため、頑張ればUI上で絞り込み条件を指定して、それに合致したタスクを表示するようにもできます。
私もいろいろと javascript や dataview プラグインについて調べ、AIに少し書かせたりもして、そういったものを作りました。
ただ、ここでそれを提示すると情報過多になってしまいますし、作ったもののそれほど利用しておらず、また実際の利用上では上記のような dataview クエリだけでも十分なので、ここではイメージ図を提示するに留めておきます。2

テキストボックスに入力することでタイトル検索できます。また、Advanced SearchボタンでStatusキーの値による絞り込みオプションを開閉できます。

使用方法

具体的な使用方法についても少し説明します。
仕事ではなくても、人は日常的にいろいろなタスクをこなしています。 例えば「今日の夕飯を作る」とか、あるいは「風呂に入る」ことだってタスクです。3
こうしたタスクについて個別にチケットを作り対応に当たる、というのがタスクのチケット管理です。
もっとわかりやすい例でいえば、何か問題が起きて、それに対応しなければならない、というケースでしょう。
問題への対処というのはタスクの典型例でしょう。なので、それを例にお話しします。

何か問題が起きたら、その問題に対処する、といった内容のタスクファイルを発行します。
そして、そのタスクファイルにて、

  • どのような問題があるのか
  • 何が原因であると考えられるか
  • それに対する対応として何が考えられるか
  • どのように対応したか
  • 対応した内容に問題はなかったか

などの作業ログを残しながら、そのタスクを解消します。
こうすることで、過去にどのような問題があって、それにどのような対応をしたか、というログを自然と残すことができます。
また、タスクに取り組むにあたり調べたことや学習したこと、どういう手段がうまくいき、どういう手段がうまくいかなかったか、などの知見を蓄積することもできます。

こうした蓄積が将来、自身を助けるかもしれませんし、そうでもないかもしれません。
過度な期待は禁物です。そうしたことは、期待しているうちにはやってこないものです。
しかし、やはり助けになることはあるでしょう。
これは私の方法についてというよりも、PKM (Personal Knolwedge Management) と呼ばれる取り組み一般について言えることです。

とにかく、あまり気負うことなく気楽にやっていきましょう。

結び

そもそも、タスクをチケットとして管理する、とはどういうことなのでしょうか。
それはだれが始めた方法で、どのようにすべきで、どのような効用があるのでしょうか。
本来は、そのことについても説明するべきなのかもしれません。
しかし、私はあえてこの説明を省いていました。
なぜなら、私はそれを知らないからです。
私も、特別にこの方法について学んだことはなく、実際の使用により身に着け、少しずつ研ぎ澄ましているところなのです。

このように実践の中で方法を確立することの利点はいくつかあります。
私が特に重視するのは、こうすることによって気楽にその方法に取り組めるということです。
気楽に取り組めるということはどういうことか。
それに取り組む際の心理的なハードルをあまり高めないで済むということです。
何か新しいことを意気込んだとして、それについてしっかりとしたプランを立てたとして、始める前からいろいろとルールを決めてしまうと、いざ始めたとき、そうしたルールに体が慣れていないために、大きなストレスを感じてしまいます。
大きなストレスを感じると、あまりやりたくなくなります。
そうなると、どんなに優れた方法であっても続きません。

それに、事前に考えた方法がそのままうまくいくことは非常にまれです。
実際に取り組んでいると、どうも思ったようにいかない。そんなことの繰り返しです。
そうすると、いずれにしても立ち止まり、その方法を点検する必要が出てきます。
であるならば、入り口は気楽でもいいのではないでしょうか。

そもそも、なにかをする方法というのは座学を受けたり頭で考えて身につくものではありません。
実際に手を動かして身に着けるものです。
また、誰かの方法がそのまま自分にもぴったり当てはまるわけでもありません。
これまた、実際に手を動かして得たフィードバックをもとに最適化していく必要があります。

とにかく手を動かすのです。
そのうち、しっくりくるタイミングが来るでしょう。
私も1か月利用してきて、だいぶしっくりきたなと感じています。
まだまだ、とも感じています。
そのことが楽しくもあります。
この楽しさは、続けられているからにほかなりません。
続けるには、気楽さが第一だと思います。


  1. たとえば、ファイル名に通し番号を付けたくなければ、 newFileName 変数の定義部分を以下のように変更してみてください。

    // 新しいファイル名を作成する(6桁の数字_入力文字列)  
    const newFileName = `${userInput}.md`;  
    

    ファイル名が「{プロンプトで入力した内容}」のノートが作成されるようになります。

    ただし、その場合は

    if (hasTagInFrontmatter || hasTagInTags) {
            // ファイル名の先頭6文字を数値に変換する
            const number = parseInt(file.basename.slice(0, 6), 10);
            // 最大値を更新する
            if (number > maxNumber) {
                maxNumber = number;
            }
    

    のあたりで、ファイル名の先頭の数列ではなく number キーの値を取得して、それをもとに maxNumber を出す必要がありますね。(でないと、毎回通し番号1のタスクが作成されてしまいます)
    そういえば、「ファイル名の最初の数字なくてもよくね?」と思ったとき、このあたりに手を加えるのが面倒でそれを見送ったのでした。

  2. どんなふうにやってるのか見てみたいという人がいるのであれば(いないと思うけど)また別の記事で公開します。そうでなくても気が向いたら公開するかもしれません。コードが全然整理されておらず非常に汚いので、公開するなら整理したいですが。
  3. とはいえ、「風呂に入る」ことについてわざわざチケットとして管理する必要はないかもしれません。
    というのも、そのタスクをこなすために何か新しいことを学んだり、あるいは自分の持つ技術や状況を整理する必要がないからです。
    もっとも、風呂に入るためにドラム缶を探してきて、川から水を汲んできて、薪を集めて火を起こして、それで湯を沸かし……といったことを突発的にしなくてはならず、そのためにもろもろの調べごとをする必要があるのであれば、話は変わりますが。