📝

【Raycast】手軽にメモるための拡張機能を作る

2023/12/12に公開

以前、【Mac】いつでもメモをするためにという記事を書きました。
今回は、Raycastの拡張機能を自作して、すぐにメモることができる環境を作ります。
https://www.raycast.com/

今回作ったものは、こちらに公開しています。
https://github.com/k41531/snap-jot

拡張機能を作る

拡張機能の作り方は、公式ドキュメントにも丁寧に書かれていますので、参考にしてください。
https://developers.raycast.com/basics/create-your-first-extension#create-a-new-extension

準備

  • Raycastのアカウントが必要
    • Raycast自体は無料で使えますが、拡張機能を作るにはアカウントが必要です。
  • Node.js

プロジェクトの作成

Raycastを起動して、Create Extensionを選択、拡張機能を作るためのフォームで以下三つを適当に設定します。(今回はTemplateを使いませんでした。)

  • Extension Name
  • Command Name
  • Location

作成したら、プロジェクトフォルダが作られているはずなので、移動して以下のコマンドを実行する。

npm install
npm run dev

Raycastが起動すると、拡張機能が追加されているはずです。
まだ何も書いていないので、何も表示されません。

とりあえず、Hello, World!を表示させてみます。
srcフォルダに、tsxファイルが作成されていると思うので、それを編集します。

import { Detail } from "@raycast/api";

export default function Command() {
    return <Detail markdown={`# Hello World`} />;
}

Raycastを起動して、拡張機能を実行すると Hello World が表示されているはず。

コード

では、Hello,world は消して、ここから拡張機能を作っていきます。

最終的なコード
import { Form, ActionPanel, Action, getPreferenceValues, openExtensionPreferences } from "@raycast/api";
import { useForm } from "@raycast/utils";

interface Memo {
    memo: string;
}

interface Preferences {
    directory: string;
    format: string;
    prefix: string;
    template: string;
}

function formatDateTime(date: Date, format: string) {
    const tokens: { [key: string]: string } = { // Add index signature to tokens object
        YYYY: date.getFullYear().toString(),
        YY: date.getFullYear().toString().slice(-2),
        MM: (date.getMonth() + 1).toString().padStart(2, '0'),
        DD: date.getDate().toString().padStart(2, '0'),
        HH: date.getHours().toString().padStart(2, '0'),
        mm: date.getMinutes().toString().padStart(2, '0'),
        ss: date.getSeconds().toString().padStart(2, '0')
    };

    return format.replace(/YYYY|YY|MM|DD|HH|mm|ss/g, match => tokens[match]);
}

function replaceDatePlaceholders(date: Date, text: string): string {
    return text.replace(/{{date:([^}]+)}}/g, (_, dateFormat) => formatDateTime(date, dateFormat));
}


export default function Command() {
    const preferences = getPreferenceValues<Preferences>();
    const { directory, format, prefix, template } = preferences;

    function saveMemo(values: Memo) {
        const fs = require('fs');
        const path = require('path');
        const filePath = path.join(directory, formatDateTime(new Date(), format));
        let memo = formatDateTime(new Date(), prefix) + values.memo + "\n";
        // ファイルが存在しない場合は、ファイルの先頭にテンプレートを追加する
        if (!fs.existsSync(filePath)) {
            // テンプレートのファイルパスのファイルないの文字列を取得する
            const templateContent = fs.readFileSync(template, 'utf8');
            memo = replaceDatePlaceholders(new Date(), templateContent) + "\n" + memo;
        }
        fs.appendFileSync(filePath, memo);

    }
    const { handleSubmit, reset, itemProps } = useForm<Memo>({
        onSubmit(values) {
            saveMemo(values);
            reset();
        }
    });

    return (
        <Form
            enableDrafts={true}
            actions={
                <ActionPanel>
                    <Action.SubmitForm title="Submit" onSubmit={handleSubmit} />
                    <Action title="Open Extension Preferences" onAction={openExtensionPreferences} />
                </ActionPanel>
            }>
            <Form.TextArea title="Memo" {...itemProps.memo} />
        </Form>
    )
}

フォームの作成

まず、メモを入力するためのプロンプトを表示させます。

import { Form } from "@raycast/api";

export default function Command() {
    return (
        <Form>
            <Form.TextArea id="memo"/>
        </Form>
    )
}

ActionPanelとActionを追加すると、フォームの内容を送信できるようになります。

import { Form, ActionPanel, Action } from "@raycast/api";

export default function Command() {
    return (
        <Form
            actions={
                <ActionPanel>
                    <Action.SubmitForm title="Memo" onSubmit={(values) => console.log(values)} />
                </ActionPanel>
            }>
            <Form.TextArea id="memo" />
        </Form>
    )
}

ファイルに保存

これで、とりあえず入力したメモをファイルに保存することができます。

import { Form, ActionPanel, Action } from "@raycast/api";

type Memo = {
    memo: string;
}

export default function Command() {
    function saveMemo(values: Memo) {
        const fs = require('fs');
        const path = require('path');
        const filePath = path.join(process.env.HOME, 'memo.txt');
        const memo = values.memo + "\n";
        fs.appendFileSync(filePath, memo);
    }

    return (
        <Form
            actions={
                <ActionPanel>
                    <Action.SubmitForm title="Memo" onSubmit={(values: Memo) => saveMemo(values)} />
                </ActionPanel>
            }>
            <Form.TextArea id="memo" />
        </Form>
    )
}

拡張機能の設定

この拡張機能では、ファイルに保存する場所や、ファイル名のフォーマット、メモの先頭につける文字列を設定できるようにしたいです。
Raycastには、PreferencesAPIが用意されているので活用します。

package.jsoncommandsプロパティでPreferencesを設定することができます。
参考 : Manifest - Raycast API

  "commands": [
    {
      "name": "memo",
      "title": "memo",
      "description": "Create a memo with a timestamp",
      "mode": "view",
      "preferences": [
        {
          "type": "directory",
          "name": "directory",
          "title": "Directory path",
          "required": true,
          "description": "Specify a folder to save the memo",
          "defaultValue": ""
        },
        {
          "type": "textfield",
          "name": "format",
          "title": "File name format",
          "required": true,
          "description": "ex) YYYY-MM-DD.md -> 2023-12-11.md",
          "defaultValue": "YYYY-MM-DD.md"
        },
        {
          "type": "textfield",
          "name": "prefix",
          "title": "Memo prefix",
          "required": true,
          "description": "ex) - HH:mm  -> - 12:34 My first memo",
          "defaultValue": "- HH:mm "
        }
      ]
    }
  ]

設定された値を取得するには、getPreferenceValuesを使います。
ついで、拡張機能の画面から設定を開けるように、openExtensionPreferencesを使います。
参考 : Preferences - Raycast API

import { Form, ActionPanel, Action, getPreferenceValues, openExtensionPreferences } from "@raycast/api";

type Memo = {
    memo: string;
}

interface Preferences {
    directory: string;
    format: string;
    prefix: string;
}

function formatDateTime(date: Date, format: string) {
    const tokens: { [key: string]: string } = { 
        YYYY: date.getFullYear().toString(),
        YY: date.getFullYear().toString().slice(-2),
        MM: (date.getMonth() + 1).toString().padStart(2, '0'),
        DD: date.getDate().toString().padStart(2, '0'),
        HH: date.getHours().toString().padStart(2, '0'),
        mm: date.getMinutes().toString().padStart(2, '0'),
        ss: date.getSeconds().toString().padStart(2, '0')
    };

    return format.replace(/YYYY|YY|MM|DD|HH|mm|ss/g, match => tokens[match]);
}

export default function Command() {
    const preferences = getPreferenceValues<Preferences>();
    const { directory, format, prefix } = preferences;

    function saveMemo(values: Memo) {
        const fs = require('fs');
        const path = require('path');
        const filePath = path.join(directory, formatDateTime(new Date(), format));
        const memo = formatDateTime(new Date(), prefix) + values.memo + "\n";
        fs.appendFileSync(filePath, memo);
    }

    return (
        <Form
            enableDrafts={true}
            actions={
                <ActionPanel>
                    <Action.SubmitForm title="Memo" onSubmit={(values: Memo) => saveMemo(values)} />
                    <Action title="Open Extension Preferences" onAction={openExtensionPreferences} />
                </ActionPanel>
            }>
            <Form.TextArea id="memo" />
        </Form>
    )
}

フォームの内容をリセット

メモを送信した後にフォームの内容をリセットするには、@raycast/utilsuseFormを使います。
参考 : useForm - Raycast API

npm install --save @raycast/utils

itemPropsをちゃんと設定しないといけない点に注意。

import { Form, ActionPanel, Action, getPreferenceValues, openExtensionPreferences } from "@raycast/api";
import { useForm } from "@raycast/utils";

...省略...

export default function Command() {
    const preferences = getPreferenceValues<Preferences>();
    const { directory, format, prefix } = preferences;

    function saveMemo(values: Memo) {
        const fs = require('fs');
        const path = require('path');
        const filePath = path.join(directory, formatDateTime(new Date(), format));
        const memo = formatDateTime(new Date(), prefix) + values.memo + "\n";
        fs.appendFileSync(filePath, memo);

    }
    const { handleSubmit, reset, itemProps } = useForm<Memo>({
        onSubmit(values) {
            saveMemo(values);
            reset();
        }
    });

    return (
        <Form
            enableDrafts={true}
            actions={
                <ActionPanel>
                    <Action.SubmitForm title="Submit" onSubmit={handleSubmit} />
                    <Action title="Open Extension Preferences" onAction={openExtensionPreferences} />
                </ActionPanel>
            }>
            <Form.TextArea title="Memo" {...itemProps.memo} />
        </Form>
    )
}

ファイルのテンプレートを設定

ついで、ファイルが存在しない場合は、テンプレートをもとにファイルを作成するようにします。

Preferencesの設定

{
  "type": "file",
  "name": "template",
  "title": "Memo template",
  "required": true,
  "description": "If there is no file, create a file with the template written in it.",
  "defaultValue": ""
}
interface Preferences {
    directory: string;
    format: string;
    prefix: string;
    template: string;
}

ファイル名のフォーマットを設定するときに使った関数は、YYYYやMMなどの文字列をすべて置き換えてしまうので、明示的に日付だとわかるように{{date:YYYY-MM-DD}}のように書くことにします。(Obsidianのテンプレートを参考にしました。)

function replaceDatePlaceholders(date: Date, text: string): string {
    return text.replace(/{{date:([^}]+)}}/g, (_, dateFormat) => formatDateTime(date, dateFormat));
}
export default function Command() {
    const preferences = getPreferenceValues<Preferences>();
    const { directory, format, prefix, template } = preferences;

    function saveMemo(values: Memo) {
        const fs = require('fs');
        const path = require('path');
        const filePath = path.join(directory, formatDateTime(new Date(), format));
        let memo = formatDateTime(new Date(), prefix) + values.memo + "\n";
        if (!fs.existsSync(filePath)) {
            const templateContent = fs.readFileSync(template, 'utf8');
            memo = replaceDatePlaceholders(new Date(), templateContent) + "\n" + memo;
        }
        fs.appendFileSync(filePath, memo);

    }

これで、Raycastからすぐにメモができる拡張機能の完成です。

しっかりとAPIが用意されており、ドキュメントも充実しているので拡張機能はかなり作りやすいかと思います。
Publishは少し面倒だったので、今回はしませんでしたが、個人で使う用途であればPublishしなくても使えるので、ぜひ試してみてください。

Discussion