1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

iPhoneのショートカット機能を使って、GitHubのリポジトリにクリップボードからメモを保存するTips📝

Posted at

はじめに

iPhoneのショートカット機能は便利であることを知っていましたが、使いこなせてはいませんでした。
調べるとHTTPリクエストの送信や、データの変換など何かと便利ですね。

今回はこのショートカット機能を使って、GitHubのリポジトリにiPhoneのクリップボードの内容をマークダウンとしてファイルを手軽に保存する方法を紹介します。

きっかけ

最近はChatGPTGeminiClaudeによる調べ物が圧倒的に増えています。
また自分のプログラミング学習においても、「あ!これそういえばわかっていなかったな」という情報も気軽に聞いてしまえるほどです。

しかし調べ物を検証するためには、PCもといまとまった時間が必要になります。
一度チャットを投げて結果を見るも、あとでまた確認するというシーンが私は多いです。

そのため一時的にメモ📝を限りなく少ないアクションで残したいという気持ちがありました。

クリップボードからペーストできるから必要ないのでは?

クリップボードから値を取得して、Notionやらお気に入りのメモアプリに値を貼り付ければ話が早いのですが、貼り付け先のアプリを立ち上げるのも私は億劫に感じます。
クリップボード📋のマークをタップしたら、限りなく少ない導線で保存できることが個人的な理想です。

ここは私が非常に面倒くさがりな性格のため、このような結論に至っています。

事前準備

今回試す自動化には下記が必要になります。

  • ショートカット機能が使用できるiOS端末 iPhoneを想定
  • GitHubのリポジトリ (プライベート)を想定
  • GitHub個人用アクセス トークン

個人用アクセス トークンの取得方法

GitHubのWebページ、右側サイドパネルから[Setting]を選択します。

image.png

[Setting]の中から[Developer settings]をクリックします。

image.png

[Personal access tokens]から[Fine-grained tokens]をクリックすると設定画面に遷移します。

image.png

ここから[Generate new token]をクリックし、必要な権限の範囲有効期限を設定します。

image.png

必要な権限はContents Read and writeが該当します。

アクセス トークンは、パスワードと同じように扱ってください。

試してみたショートカット

iPhoneのショートカット機能の内容は下記のようなものです。
かなり簡易的なもとになっています。

  1. リッチテキストからマークダウンを作成

    • クリップボードが変換元
  2. Base64でエンコード

    • (1)リッチテキストからマークダウンを作成の値を参照
    • 行区切りを指定しない
  3. 日付を書式設定

    • 現在の日付が変換元
    • 日付の書式 yyyyMMdd-HHmmss
    • 地域: 日本
  4. 「Webの内容を取得」アクション

{
   "message": "コメント",
   "content": "(2)のアクションで取得したbase64文字列",
   "branch": "mainかmaster"
}

驚いたこととしてリッチテキストからマークダウンを作成のような変換アクションが、標準でショートカットの機能に含まれています。
markitdownのようなライブラリを使用せずとも使えることはうれしい限りですね。

またBase64エンコードといった機能も備わっており、いろいろな自動化が試せそうだと今更知りました。

設定のポイントを見ていきます。

1. リッチテキストからマークダウンを作成

変数の選択画面でクリップボードを選択します。

image.png

ショートカットの機能の中にはクリップボードの内容の取得といったアクションが存在しますが、今回のケースでは不要です。

クリップボードの内容はテキストとして解釈しています。

image.png

私のようなChatGPTClaudeGeminiの出力結果を保存したいという方をいったん想定し、リッチテキストからマークダウンを作成を採用していますが、ほかの書式やデータ型に対応させたい場合は条件分岐やスクリプトが必要になるかもしれません。

2. Base64でエンコード

こちらもアクションで備わっています。デフォルトで行区切りが設定されてしまっているため、指定しないに変更してください。

image.png

3. 日付を書式設定 - ファイル名の設定

ファイル名を設定するために現在日時を取得します。

image.png

日付を書式設定というアクションをもとに、yyyyMMdd-HHmmssでテキストを生成します。

image.png

4. Webの内容を取得

こちらがいわゆるHTTP要求の送信に該当します。

メソッドはPUTです。

GitHub REST APIについては下記が公式のドキュメントに該当します。
GitHub REST API に関するドキュメント

URLはhttps://api.github.com/repos/{ユーザーの名前}/{リポジトリの名前}/contents/{ファイル名称}

ヘッダーは下記の通りです。

キー バリュー
Authorization Bearer {事前準備で取得した個人用アクセス トークン}
Accept application/vnd.github.v3+json

実際の設定は上記をコピペしていくと簡単に済みます。
本文はJSONを直接打つのではなく、キーとバリューをガイドに沿って入力していきます。

image.png

{
   "message": "コメント",
   "content": "(2)のアクションで取得したbase64文字列",
   "branch": "mainかmaster"
}

これでほぼ完成です。

ChatGPTの出力で検証する

Gitの基本的な考え方、コマンドを初心者向けにわかりやすく説明してください。出力にはコマンドを含め、どのようなシーンで利用するのか教えてください。
上記のプロンプトで検証してみましょう。

image.png

クリップボードにコピーします

image.png

この状態でショートカットを実行すると

image.png

リポジトリにマークダウンファイルが、上手く保存されました🙌

image.png

おまけ: READMEに目次を作ってもらう

Claudeの力を利用して、GitHub Actionsを使った目次の自動生成も仕込みます。
リポジトリの.github/workflowsに下記のymlファイルを設定してください。

generate-toc.yml
name: Generate Markdown Table of Contents

on:
  push:
    branches: [ main, master ]
    paths: ['**/*.md']
  pull_request:
    branches: [ main, master ]
    paths: ['**/*.md']
  workflow_dispatch:

jobs:
  generate-toc:
    runs-on: ubuntu-latest
    
    permissions:
      contents: write
      
    steps:
    - name: Checkout repository
      uses: actions/checkout@v4
      with:
        fetch-depth: 0
        
    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: '18'
        
    - name: Generate Table of Contents
      run: |
        cat > generate-toc.js << 'EOF'
        const fs = require('fs');
        const path = require('path');
        
        function findMarkdownFiles(dir, fileList = []) {
          const files = fs.readdirSync(dir);
          
          files.forEach(file => {
            const filePath = path.join(dir, file);
            const stat = fs.statSync(filePath);
            
            if (stat.isDirectory()) {
              if (!file.startsWith('.') && file !== 'node_modules') {
                findMarkdownFiles(filePath, fileList);
              }
            } else if (file.endsWith('.md') && file.toLowerCase() !== 'readme.md') {
              fileList.push(filePath);
            }
          });
          
          return fileList;
        }
        
        // Markdownファイルからタイトルを抽出
        function extractTitle(filePath) {
          try {
            const content = fs.readFileSync(filePath, 'utf8');
            const h1Match = content.match(/^#\s+(.+)$/m);
            if (h1Match) {
              return h1Match[1].trim();
            }
            
            return path.basename(filePath, '.md');
          } catch (error) {
            return path.basename(filePath, '.md');
          }
        }
        
        function generateTOC() {
          const markdownFiles = findMarkdownFiles('.');
          
          if (markdownFiles.length === 0) {
            console.log('No markdown files found.');
            return;
          }
          
          const filesByDir = {};
          
          markdownFiles.forEach(filePath => {
            const dir = path.dirname(filePath);
            if (!filesByDir[dir]) {
              filesByDir[dir] = [];
            }
            
            const title = extractTitle(filePath);
            const relativePath = filePath.replace(/\\/g, '/'); // Windows対応
            
            filesByDir[dir].push({
              title,
              path: relativePath,
              filename: path.basename(filePath)
            });
          });
          
          let toc = '# 📚 Documentation Table of Contents\n\n';
          toc += `> 自動生成された目次 - 最終更新: ${new Date().toLocaleDateString('ja-JP')}\n\n`;
          
          if (filesByDir['.']) {
            toc += '## 📄 Root Directory\n\n';
            filesByDir['.'].forEach(file => {
              toc += `- [${file.title}](./${file.path})\n`;
            });
            toc += '\n';
          }
          
          Object.keys(filesByDir)
            .filter(dir => dir !== '.')
            .sort()
            .forEach(dir => {
              const dirName = dir.replace(/^\.\//, '');
              toc += `## 📁 ${dirName}\n\n`;
              
              filesByDir[dir].forEach(file => {
                toc += `- [${file.title}](./${file.path})\n`;
              });
              toc += '\n';
            });
          
          const totalFiles = markdownFiles.length;
          const totalDirs = Object.keys(filesByDir).length;
          
          toc += '---\n\n';
          toc += '## 📊 Statistics\n\n';
          toc += `- **Total Markdown files**: ${totalFiles}\n`;
          toc += `- **Directories**: ${totalDirs}\n`;
          toc += `- **Last updated**: ${new Date().toLocaleString('ja-JP')}\n\n`;
          
          let existingContent = '';
          if (fs.existsSync('README.md')) {
            existingContent = fs.readFileSync('README.md', 'utf8');
            
            const startMarker = '<!-- TOC_START -->';
            const endMarker = '<!-- TOC_END -->';
            
            if (existingContent.includes(startMarker) && existingContent.includes(endMarker)) {
              const beforeToc = existingContent.substring(0, existingContent.indexOf(startMarker));
              const afterToc = existingContent.substring(existingContent.indexOf(endMarker) + endMarker.length);
              existingContent = beforeToc + afterToc;
            }
          }
          
          const newReadme = existingContent.trim() ? 
            `${existingContent.trim()}\n\n<!-- TOC_START -->\n${toc}<!-- TOC_END -->\n` :
            `<!-- TOC_START -->\n${toc}<!-- TOC_END -->\n`;
          
          fs.writeFileSync('README.md', newReadme);
          console.log(`✅ Table of Contents generated successfully!`);
          console.log(`📝 Found ${totalFiles} markdown files in ${totalDirs} directories`);
        }
        
        generateTOC();
        EOF
        
        node generate-toc.js
        
    - name: Check for changes
      id: verify-changed-files
      run: |
        if [ -n "$(git status --porcelain)" ]; then
          echo "changed=true" >> $GITHUB_OUTPUT
        else
          echo "changed=false" >> $GITHUB_OUTPUT
        fi
        
    - name: Commit and push changes
      if: steps.verify-changed-files.outputs.changed == 'true'
      run: |
        git config --local user.email "action@github.com"
        git config --local user.name "GitHub Action"
        git add README.md
        git commit -m "📚 Auto-update Table of Contents [skip ci]"
        git push

上記を実行するためにリポジトリSettingActionsGeneralを開きます。
Workflow permissionsRead and write permissionsの権限を付与してください。

こちらでファイルが追加される都度、下記のような目次が自動生成されます。

image.png

おわりに

メモ保存ツールとしてプライベートリポジトリの使用をしてみましたが、GitHubでなくてもほかのツールで応用できそうですね。
身近な端末からこのような自動化ができることはとてもうれしいです。

AIはアイディアアクセラレータですので、どんどんこのような自動化を試していきたいと思います!

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?