はじめに
iPhoneのショートカット機能は便利であることを知っていましたが、使いこなせてはいませんでした。
調べるとHTTPリクエストの送信
や、データの変換
など何かと便利ですね。
今回はこのショートカット機能を使って、GitHub
のリポジトリにiPhone
のクリップボードの内容をマークダウンとしてファイルを手軽に保存する方法を紹介します。
きっかけ
最近はChatGPT
やGemini
、Claude
による調べ物が圧倒的に増えています。
また自分のプログラミング学習においても、「あ!これそういえばわかっていなかったな」という情報も気軽に聞いてしまえるほどです。
しかし調べ物を検証するためには、PC
もといまとまった時間が必要になります。
一度チャットを投げて結果を見るも、あとでまた確認するというシーンが私は多いです。
そのため一時的にメモ📝を限りなく少ないアクションで残したいという気持ちがありました。
クリップボードからペーストできるから必要ないのでは?
クリップボードから値を取得して、Notion
やらお気に入りのメモアプリに値を貼り付ければ話が早いのですが、貼り付け先のアプリを立ち上げるのも私は億劫に感じます。
クリップボード📋のマークをタップしたら、限りなく少ない導線で保存できることが個人的な理想です。
ここは私が非常に面倒くさがりな性格のため、このような結論に至っています。
事前準備
今回試す自動化には下記が必要になります。
- ショートカット機能が使用できるiOS端末
iPhone
を想定 -
GitHub
のリポジトリ(プライベート)を想定
-
GitHub
の個人用アクセス トークン
個人用アクセス トークンの取得方法
GitHubのWebページ、右側サイドパネルから[Setting]を選択します。
[Setting]の中から[Developer settings]をクリックします。
[Personal access tokens]から[Fine-grained tokens]をクリックすると設定画面に遷移します。
ここから[Generate new token]をクリックし、必要な権限の範囲
と有効期限
を設定します。
必要な権限はContents Read and write
が該当します。
アクセス トークンは、パスワードと同じように扱ってください。
試してみたショートカット
iPhone
のショートカット機能の内容は下記のようなものです。
かなり簡易的なもとになっています。
-
リッチテキストからマークダウンを作成
- クリップボードが変換元
-
Base64でエンコード
-
(1)リッチテキストからマークダウンを作成
の値を参照 - 行区切りを指定しない
-
-
日付を書式設定
- 現在の日付が変換元
- 日付の書式 yyyyMMdd-HHmmss
- 地域: 日本
-
「Webの内容を取得」アクション
- URL: https://api.github.com/repos/{username}/{repository}/contents/{filename}
- 方法: PUT
- ヘッダー:
- Authorization: Bearer {your_token}
- Accept: application/vnd.github.v3+json
{
"message": "コメント",
"content": "(2)のアクションで取得したbase64文字列",
"branch": "mainかmaster"
}
驚いたこととしてリッチテキストからマークダウンを作成
のような変換アクションが、標準でショートカットの機能に含まれています。
markitdownのようなライブラリを使用せずとも使えることはうれしい限りですね。
またBase64エンコード
といった機能も備わっており、いろいろな自動化が試せそうだと今更知りました。
設定のポイントを見ていきます。
1. リッチテキストからマークダウンを作成
変数
の選択画面でクリップボード
を選択します。
ショートカットの機能の中にはクリップボードの内容の取得
といったアクションが存在しますが、今回のケースでは不要です。
2. Base64でエンコード
こちらもアクションで備わっています。デフォルトで行区切り
が設定されてしまっているため、指定しない
に変更してください。
3. 日付を書式設定 - ファイル名の設定
ファイル名を設定するために現在日時を取得します。
日付を書式設定
というアクションをもとに、yyyyMMdd-HHmmss
でテキストを生成します。
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を直接打つのではなく、キーとバリューをガイドに沿って入力していきます。
{
"message": "コメント",
"content": "(2)のアクションで取得したbase64文字列",
"branch": "mainかmaster"
}
これでほぼ完成です。
ChatGPTの出力で検証する
Gitの基本的な考え方、コマンドを初心者向けにわかりやすく説明してください。出力にはコマンドを含め、どのようなシーンで利用するのか教えてください。
上記のプロンプトで検証してみましょう。
クリップボードにコピーします
この状態でショートカットを実行すると
リポジトリにマークダウンファイルが、上手く保存されました🙌
おまけ: READMEに目次を作ってもらう
Claude
の力を利用して、GitHub Actions
を使った目次の自動生成も仕込みます。
リポジトリの.github/workflows
に下記の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
上記を実行するためにリポジトリ
のSetting
、Actions
のGeneral
を開きます。
Workflow permissions
にRead and write permissions
の権限を付与してください。
こちらでファイルが追加される都度、下記のような目次が自動生成されます。
おわりに
メモ保存ツールとしてプライベートリポジトリの使用をしてみましたが、GitHubでなくてもほかのツールで応用できそうですね。
身近な端末からこのような自動化ができることはとてもうれしいです。
AIはアイディアアクセラレータですので、どんどんこのような自動化を試していきたいと思います!