ブラウザベースのMarkdown viewerとしてmoを作った

OpinionatedなMarkdown viewer

世の中にMarkdown viewerはたくさんありますが、自分がほしかったのは以下の特性を持つものでした。

  • CLIからMarkdownを開きたい
  • Markdownはブラウザで見たい(ローカルWebサーバ方式)
  • Markdownを開くたびに別プロセス/別ポートが使用されないようにしたい
  • グルーピングもしたい

また、コーディングエージェントがMarkdownドキュメントを書くようになり、CLIからファイルを追加できることがキーになってきました。 エージェントの行動を邪魔しないインターフェイスが求められています。

というわけで、OpinionatedなMarkdown viewerを作ってみました。

mo

github.com

moMarkdownファイルをブラウザで openするCLIツールです。Markdownファイルを渡すとブラウザが開いてレンダリングされたドキュメントが表示されます。

(Go + 組み込みReact SPAの)単一バイナリで、依存関係なしにインストールできます。

使い方

基本はファイルを渡すだけです。

$ mo README.md
mo: serving at http://localhost:6275 (pid 12345)

複数ファイルも渡せます。

$ mo README.md CHANGELOG.md docs/*.md

グループ

--target (-t) フラグでファイルをグループに分けられます。グループごとにURLパスとサイドバーが分かれます。

$ mo spec.md --target design      # http://localhost:6275/design
$ mo api.md --target design       # design グループに追加
$ mo notes.md --target plans      # http://localhost:6275/plans

設計ドキュメントは"design"グループ、コーディングエージェントが書いたプランファイルは"plans"グループ、という使い分けもできます。

moの仕組み

mo は結構私のユースケースに合わせて特徴的な動きをします。

mo はデフォルトでバックグラウンドでサーバーを起動します。最初の mo コマンドでサーバーが立ち上がり、シェルはすぐに返ってくるので、そのまま作業を続けられます。

$ mo README.md
mo: serving at http://localhost:6275 (pid 12345)
$ # シェルがすぐ使える

これが個人的にめちゃくちゃ使いやすいです。

コーディングエージェントにも使わせやすいですし、スクリプトにも組み込みやすいです。ブラウザオープンも --open--no-open オプションで制御可能です。

2回目以降の mo コマンドの実行も、同じポートですでにサーバーが動いている場合新しいサーバーは起動せず既存のサーバーにファイルを追加します。

$ mo README.md          # サーバー起動してファイル追加
$ mo CHANGELOG.md       # 既存のサーバーにファイル追加
$ mo docs/*.md          # さらに追加

つまり、mo コマンドを叩くたびにプロセスやポートが増えていくことはありません。1つのサーバーに対してファイルをどんどん追加していく設計です。

Webアプリケーションを1つ起動してそこにファイルを追加していくイメージです。Webアプリケーションを開発した経験のある私には馴染みのある動きです。

なお、完全に別のセッションが必要な場合は別ポートを使えば可能です。

$ mo draft.md -p 6276

サーバーの管理もCLIから行えます。これもWebサーバを管理したことがある人には馴染みがあるのではないでしょうか。

$ mo --status              # 稼働中のmoサーバーを一覧表示
$ mo --shutdown            # デフォルトポートのサーバーをシャットダウン
$ mo --shutdown -p 6276    # 特定ポートのサーバーをシャットダウン

Web UIからサーバーをリスタートすることもできます。

画面右下のリスタートボタンをクリックすると、開いているファイルやグループの構成を保ったままサーバーが再起動します。

mo をバージョンアップした後、ファイルを開き直すことなく新しいバージョンに切り替えられるので便利です。

Viewerとしての機能

Markdownのレンダリングは基本的なものには対応しています。

  • GitHub Flavored Markdown(テーブル、タスクリスト、脚注など)
  • Shiki によるシンタックスハイライト
  • Mermaid の図表レンダリング
  • GitHub Alerts(admonitions)

UI

UI周りの機能としては以下があります。

  • ダーク / ライトテーマ切り替え
  • 目次パネル(右側)
  • Rawマークダウン表示の切り替え
  • コンテンツコピー(Markdown / テキスト / HTML形式を選択可)
  • ドラッグ&ドロップでのファイル並び替え

UIはGitHubのMarkdownレンダリングに寄せたデザインにしています。最終的にGitHubでどう見えるかがわかることが重要ですし、何よりGitHubの見た目が一番慣れているので読みやすいです。

サイドバーのファイル一覧には、フラットビューとツリービューの2つの表示モードを用意しています。

フラットビューはファイル名だけを並べたシンプルな一覧で、ドラッグ&ドロップで自由に並び替えられます。少数のファイルをサッと確認したいときはこれで十分です。

一方、docs/*.md のように同じディレクトリの下に大量のファイルを開いた場合、フラットビューだと一覧が長くなって見づらくなります。ツリービューに切り替えると、ディレクトリ階層で折りたたんで表示できるので、ファイルが多くても整理しやすくなります。

ライブリロード

mo は開いたファイルの変更を監視しています。

ファイルが更新されると、SSE(Server-Sent Events)経由でブラウザに通知が飛び、自動でコンテンツが再取得・再描画されます。

アーキテクチャ

GoのHTTPサーバーにReact SPAを go:embed で組み込んでいます。go generate でフロントエンドをビルドしてから go:embed でバイナリに埋め込むので、最終成果物は単一バイナリになります。

バックエンドはGoで、ファイル管理・API・SSE・ファイル監視を担当しています。フロントエンドはVite + React + TypeScript + Tailwind CSSで、react-markdownを使ってMarkdownをレンダリングしています。

インストール

homebrew tap:

$ brew install k1LoW/tap/mo

manually:

releases page からダウンロードできます。

go install は残念ながらできません。

References

mo のアイデアは yusukebe/gh-markdown-preview をベースにしています。gh-markdown-previewはずっと愛用してきたツールで、CLIからMarkdownをブラウザでプレビューするという体験の原点です。そこに自分がほしかった機能を足していった結果、別のツールとして形になりました。

まとめ

mo を使うことで、複数のMarkdownファイルを持続的にプレビューできるようになりました。

特に気に入っているのは、バックグラウンドでサーバーが動いていて、mo コマンドでファイルをどんどん追加できるところです。シェルを掴まないのも推しポイントです。

是非使ってみてください。

octocov.dev でカバレッジレポートを確認する

octocovはコードメトリクス(カバレッジ、Code to Test Ratio、テスト実行時間)を収集するツールキットです。以前もこのブログで紹介しました。

k1low.hatenablog.com

k1low.hatenablog.com

今回、octocovのサイトとして octocov.dev を作りました。

octocov.dev

ランディングページとしてoctocovの紹介(ほぼリンクだけ)を掲載しつつ、実験的機能としてカバレッジレポートをWebブラウザで閲覧できる機能も載せています。というか、これが作りたくて octocov.dev を作りました。

カバレッジレポート閲覧機能

octocovには artifact:// というdatastore設定があり、GitHub Actionsのアーティファクトにレポートを保存できます。外部サービスのアカウント不要で使えるので、個人的にはこれがお気に入りです。

CI上ではPRコメントやJob Summaryでレポート自体は確認できるので普段は困らないのですが、「任意のタイミングでサッと確認したい」「任意のソースコードのカバレッジを行単位で確認したい」となるとCLIとしての octocov コマンドを使用するしかありませんでした。

octocov.devでGitHubにサインインすると、artifact:// datastoreに保存されたカバレッジレポートをブラウザで閲覧できます。

前提として、リポジトリに以下のような .octocov.yml が設定されている必要があります。

# .octocov.yml
report:
  datastores:
    - artifact://owner/repo

そして octocov-action を使ってGitHub Actionsのアーティファクトにレポートが保存されている必要があります。

github.com

使い方

サインイン後、ヘッダーの検索バーに owner/repo を入力するとレポートページに遷移します。

レポートサマリでは、コードカバレッジ、Code to Test ratio、テスト実行時間、そしてファイル一覧が表示されます。

ファイル一覧からファイルをクリックすると、ソースコードビューに遷移します。行ごとのカバレッジが色分けで表示されます(緑=カバー済み)。シンタックスハイライトも有効なので、コードも読みやすいと思います。

スコープとプライバシー

サインイン時に public_repo(パブリックリポジトリのみ)と repo(プライベートリポジトリも含む)のスコープを選択できます。

データの取り扱いについて明確にしておくと、サーバーが保持するのはセッション(GitHubアクセストークンとユーザー情報)のみです。カバレッジデータはブラウザのIndexedDBに、ソースコードはキャッシュせず毎回GitHub APIから取得します。ログアウト時にはブラウザキャッシュを全削除します。

技術スタック

バックエンドはCloudflare Workers + Hono、フロントエンドはReact + Tailwind CSS v4で、@cloudflare/vite-plugin を使って単一のViteビルドに統合しています。シンタックスハイライトにはShikiを使用しています。

認証はGitHub OAuth + PKCEで、セッション管理にCloudflare KV(8時間TTL)を使っています。

パフォーマンス確保のため、クライアント側ではIndexedDBにカバレッジデータをキャッシュしています。

一方、サーバー側にはカバレッジデータのキャッシュは持たせていません。また、ソースコードはどこにも持たせていません(都度フェッチ)。これはプライベートリポジトリのデータが意図せず漏洩するリスクを避けるためです。

まとめ

octocov CLIはターミナルで、octocov-actionはCIで、そしてoctocov.dev はブラウザで。octocovのカバレッジレポートを確認する手段がまた一つ増えました。

CoverallsやCodecovのような専用カバレッジサービスの代替ではなく、octocovの artifact:// datastoreを使っているユーザー向けの軽量なビューアという位置づけです。

よかったら使ってみてください。

tmuxのウィンドウで動かしているコーディングエージェントのステータスを確認できるtcmuxを作った

普段使っているtmuxにClaude Codeの情報がほしい

私は普段からtmuxを使っています。Emacsを開いているウィンドウ、シェルを開いているウィンドウ、特定の作業をしているブランチのウィンドウ、別のリポジトリのウィンドウといった感じで複数のウィンドウを行き来しながら作業しています。

最近はClaude Codeを使うことが増えてきて、tmuxのウィンドウのいくつかでClaude Codeが動いている状態になりました。あるウィンドウではバグ修正を依頼して、別のウィンドウでは新機能の実装検討を依頼して、さらに別のウィンドウではテストを書かせている、といった具合です。

ここで問題がひとつ。「どのウィンドウのClaude Codeが今どういう状態なのかがわからない」

Claude Codeは、処理中なのか、完了して入力待ちなのか、あるいは許可を求めて止まっているのか、実際にそのウィンドウに切り替えないとわかりません。許可を求めて止まっているのに気づかず放置してしまうこともあります。

Claude Code専用の管理ツールを作るという手もありますが、自分がやりたいのは 普段使っているtmuxにClaude Codeの情報がのっている状態 です。Emacsやシェルといったウィンドウはそのままでいいので、Claude Codeが動いているウィンドウだけステータスが見えればいい。

というわけで、tmuxのウィンドウ一覧にClaude Codeのステータスを表示するツールを作りました。

tcmux

github.com

当初は "terminal と Claude Code の mux viewer" ということで tcmux という名前にしましたが、GitHub Copilot CLIにも対応したので、今は "terminal coding agent mux viewer" ということにしています。tmuxのウィンドウ一覧やセッション一覧にコーディングエージェントのステータスを付加して表示するCLIツールです。

それだけのツールです。

使い方

list-windows (lsw)

tcmux list-windows でClaude Codeが起動しているウィンドウの一覧を表示できます。

$ tcmux list-windows
0: editor (1 panes) ✻ Fix login bug [Idle]
2: server (2 panes) ✻ Add API endpoint [Running (1m 30s)], ✻ Write tests [Idle]
5: docs (1 panes) ✻ Update README [Idle]
7: review (1 panes) ✻ Review PR [Waiting]

各行の末尾にClaude Codeのステータスが表示されます。これで、どのウィンドウが処理中で、どのウィンドウが入力待ちかが一目でわかります。

-A オプションをつけると、Claude Codeが起動していないウィンドウも含めて全て表示されます。

$ tcmux list-windows -A
0: editor (1 panes) ✻ Fix login bug [Idle]
1: shell (1 panes)
2: server (2 panes) ✻ Add API endpoint [Running (1m 30s)], ✻ Write tests [Idle]
3: logs (1 panes)
4: htop (1 panes)
5: docs (1 panes) ✻ Update README [Idle]
7: review (1 panes) ✻ Review PR [Waiting]

-F オプションには tmux list-windows と互換があるので自由にフォーマット変更が可能です。

オプション 説明
-A, --all-windows Claude Codeが起動していないウィンドウも含めて全て表示
-a, --all-sessions 全セッションのウィンドウを表示
-t, --target-session 対象セッションを指定
-F, --format 出力フォーマットを指定(tmux互換+tcmux拡張)

list-sessions (ls)

tcmux list-sessions でtmuxセッションの一覧を、Claude Codeのステータス集計とともに表示できます。

$ tcmux list-sessions
dev: 7 windows (attached) - 3 Idle, 1 Running, 1 Waiting
main: 2 windows - 1 Idle
work: 1 window

複数のセッションを使い分けている場合に便利です。

ステータス検出

tcmuxは以下のステータスを検出します。

ステータス 説明
Idle プロンプト待ち。Claude Codeが次の入力を待っている状態
Running 処理中
Waiting ユーザー入力待ち。許可ダイアログや確認プロンプトが表示されている状態

また、モードも検出して表示します。

モード 説明
plan mode Plan modeがONの状態
accept edits Accept editsがONの状態

例えば、Plan modeがONで処理中の場合は [Running (30s, plan mode)] のように表示されます。

GitHub Copilot CLIにも対応した

tcmuxはClaude Code向けに作り始めましたが、ターミナルで動くコーディングエージェントはClaude Codeだけではありません。私は、主にレビュータスク目的でGitHub Copilot CLIも同様にtmux上で動かすことがあります。

そこで、GitHub Copilot CLIのステータス検出にも対応しました。検出するステータス(Idle / Running / Waiting)はClaude Codeと同じです。エージェントの種類はアイコンで区別できます。

アイコン エージェント
Claude Code
GitHub Copilot CLI

Claude CodeとCopilot CLIを混在させて使っている場合はこんな感じの表示になります。

$ tcmux list-windows
0: editor (1 panes) ✻ Fix login bug [Idle]
2: server (2 panes) ✻ Add API endpoint [Running (1m 30s)], ⬢ Write tests [Idle]
5: docs (1 panes) ⬢ Update README [Running]
7: review (1 panes) ✻ Review PR [Waiting]

レシピ

自分が実際に使っている設定を紹介します。

tmuxのウィンドウ選択で 、.tmux.conftmux list-windows の代わりに tcmux list-windows -A を使うと、ウィンドウ選択時にClaude Codeのステータスが表示されて便利です。

Before:

bind-key w run-shell "tmux list-windows | fzf --tmux | cut -d: -f1 | xargs tmux select-window -t"

After:

bind-key w run-shell "tcmux list-windows -A --color=always | fzf --ansi --tmux | cut -d: -f1 | xargs tmux select-window -t"

基本 tmux list-windows の置き換えのためだけに作ったので実質これが全てかもしれません。

Waitingのウィンドウがあれば一目でわかるので、許可を求められているClaude Codeを放置してしまうことがなくなりました。

自分はもう少し見た目を調整して使っています。

bind -r w run-shell "tcmux lsw -A --color=always | fzf --ansi --layout reverse --tmux 80%,50% --color='pointer:24' | cut -d: -f 1 | xargs tmux select-window -t"

冒頭のポストのイメージのように表示されます。

インストール

homebrew tap:

$ brew install k1LoW/tap/tcmux

go install:

$ go install github.com/k1LoW/tcmux@latest

まとめ

tcmuxを使うことで、tmux上で複数のClaude Codeを並行して動かしているときに、各インスタンスの状態を把握しやすくなりました。

特に「許可待ちで止まっているClaude Codeに気づかない」問題が解消されたのが大きいです。GitHub Copilot CLIにも対応しているので、複数のコーディングエージェントを混在させて使っている場合でも、アイコンで区別しつつ同じインターフェースでステータスを確認できます。

2025年の振り返りと2026年の抱負

明けました

2025年の振り返り

2025年2月からテイラー株式会社で働いています。

note.com

現在はTailor Platformの開発をメインにしていますが、入社エントリで書いたような感想は依然としてそのままで1年弱怒涛の日々でした。

入社エントリの最後に書いていた「プロジェクト」も https://docs.tailor.tech/ の1セクション(サブサブセクションくらい)を担うくらいの機能(サービス)としてリリースできました。 まだ改善の余地はありますが、開発体験の一端をサポートできてよかったです。

2025年はAIエージェントを使った開発もスタンダードになりました。開発能力を押し上げられる感覚がありました。

テイラー株式会社での日々

2025年の後半は私はPlatformの開発を主としていましたが、それ以外にもパッと思いつくだけでも以下のような開発領域があります。

  • Platformを使ったアプリケーションの開発(Professional Service)
  • Platformのコンソール画面の開発
  • PlatformのSDKの開発
  • Platformを使ったアプリケーションのフロントエンドのフレームワークの開発
  • Platformを使ったアプリケーション開発の開発フローの開発
  • Platformを支えるプラットフォームの開発・運用(SRE)

それぞれの領域にプロフェッショナルがいて、日々開発が進んでいます。

ふわっとした開発領域はありますがそれでチームが閉じているという印象はなく、「あらゆるアイデアをデプロイする」を実現するためのエコシステムをそれぞれの視点で成長させていっているイメージです。それぞれの視点のキャリブレーションはCTOがしているイメージ(そういえばどの領域にもCTOがいる気がする)。

Platformの開発は、あくまでイメージなのですが、日々「ある程度規模の大きくなったOSSの開発に参加している感じ」です。

コードにオーナー(オーサー)がいてメンテナがいて、Pull Requestを出してレビューがある。機能要望や提案がIssueやDesign docを通じてあり、適切に取り込まれる。

提案から実装までの流れが気持ち良いので、ついついいろいろ考えて(開発して)しまいます。

メンバーとのコミュニケーションはいつも学びがあります。

提案段階でもらったフィードバックで大きく方針を変えて大きく改善されることも1度や2度ではないですし、Pull Requestに対するレビューは必ずより良いものにつながります*1

なんというかrunnやdeckで体験したことがあるコントリビュータとの濃いめのコミュニケーションみたいなものがずっと続いている感じなんですよね。うーん説明が難しい。

つまり、かなり楽しくやっているということです。

もし興味がありましたら質問などお声がけください。

note.com

OSS

環境が変わったことから、「まずは手元の開発体験を助けるもの」みたいなものが多かった印象です。VSCode Extensionsまである。

deck、今年だったのか。。。個人的に欲しい機能は完全に揃ったので、あとはバグ修正をきっちりやっていきたいですね。

あと、2024年にできなかったrunn v1も無事リリースしました。

発表

今年は6本でした。私は基本的に発表は「いろいろな話題を話す」方針なのですが、いい感じにバラけたかなあと思っています。

2026年は発表は控える方向ですが2027年はまた頑張りたい!!*2

2026年の抱負

今年は「地に足をつける」です。 2026年は個人的特大イベントがあるので、そのイベントに振り回されないようしっかりを踏ん張りたいと思います。

いや、それがなくてもその日暮らし的に生きている気がするので、しっかりと地に足をつけていきたいです。なんか「ルーティーン」みたいなのを手に入れたい。

今年もどうぞよろしくお願いします。

*1:単純なものは全てAIエージェントで事前に終わっているのですが、それでも

*2:Fukuoka.goはできれば参加したい!

PHPカンファレンス福岡の記憶

これは、PHPカンファレンス福岡について語ってほしい! Advent Calendar 2025 の14日目のエントリです。

adventar.org

はじめに

私は性質として過去の記憶が結構消失してしまうようです。脳の問題なのか性格の問題なのかはわかりませんが、昔からそうです。個人的にはしょうがないと諦めています。

なのでここから書く内容は勘違いが混じっている可能性が高いです。人の数だけPHPカンファレンス福岡があるということでご了承ください。

過去のエントリからの記憶のサルベージです。

PHPカンファレンス福岡2015

k1low.hatenablog.com

なんと私、運営スタッフをやっています。エントリにも書いていますが、役に立たない系運営スタッフだった模様。今でもその自覚があります。カンファレンス運営は能力です。

当時「マスクド」は別のPHPer界隈でも存在していたのですが、その「マスクド」が偶然の被りだったことは覚えています。奇跡の被りでテンションが上がりました。

スポンサーの話。

多分10年経っていて隠すことではないし記憶も定かではないので適当に言うと、初年度からはじまっている株式会社Fusicのスポンサードは withelmo が動いた + 会社のGoサインがすぐ出たからだったと思います*1

スポンサードの会社判断が早いと、実は社内にも良い効果があると思っていて、「そういうカンファレンスに行って交流すること知見を得ることが良いことなんだ」と社内に暗黙の理解をさせることができるんですよね。

今年2025年のFusicのスポンサーセッションはとても良かったです。「カンファレンスとスポンサーの良い関係というのがずっと続いていたんだろうなあ」と部外者ながら思いました。

speakerdeck.com

PHPカンファレンス福岡2016

k1low.hatenablog.com

そういえば私運営スタッフでは印刷係でした。ネームカードに貼るステッカーを手作りで作成しはじめたのっていつだったかな。。。覚えていない。A4のシール用紙にいい感じにマークが印刷できるように何度も微調整をしていた気がします。

2016の発表資料で紹介したツールkomaの名前元ネタであるうちの犬、狛次郎は10歳などとうに過ぎていますが今も元気です。

PHPカンファレンス福岡2017

k1low.hatenablog.com

私、Ask the Speakerを早速体験していますね。Ask the Speaker、最初は誰も来ないかもしれないとドキドキするのですが、結局色々話すことができて良い思い出しかないです。

そういえばこのときだったっけな、清家くんがカンファレンスチケット購入忘れてたの。そのseike460くんはPHPカンファレンス福岡2023の実行委員長です。

この年まで私は運営スタッフでした。3年!

PHPカンファレンス福岡2018

tech.pepabo.com

2018はGMOペパボ株式会社のパートナー*2としてスポンサーブースに立ったりなどしていました(写真に証拠として残っていました)。

プロポーザルは落ちて、リジェクトコンで発表させてもらったりしていました。

【非公式】PHPカンファレンス福岡2018前夜祭リジェクトコン #phpconfuk_rej - connpass

リジェクトコンなのに3トラック!!すごい!

たまたまではあるとは思うのですが、Fusicと(GMOペパボの)ロリポップ!マネージドクラウドが同時にスポンサーしていて、とても誇らしかったのを覚えています。

PHPカンファレンス福岡2019

note.com

個人ブログに単独エントリがなかったので、もしかして、、、と思ったらやはり「マネクラからの挑戦状」の年でした。

これの「Webサイトコードゴルフチャレンジ」ですが、思いついちゃったのでしょうがない。仕組みを作るのがちょっと時間がかかりましたがやりました。

「サイトの表示を一切変えずに」って今で言うとビジュアルリグレッションテストですね。

PHPアプリケーション脆弱性修正チャレンジ」も好評でした。mrtc0 さんありがとう!

PHPカンファレンス福岡2023

k1low.hatenablog.com

@nojimage さんがニコニコしているのも「あーPHPカンファレンス福岡だ」と思いました。

PHPカンファレンス福岡を実感するための個人的なルーティーンの1つが「nojimageさんとお話しする」でした。

nojimageさんは(個人的)福岡の2大CakePHP Bakerの1人で*3当時から本当にお世話になっていました。

nojimageさんが時々お話しされる実装や設計のお話は、いつも私は数年遅れて実感していました。やはりレベルの高い人とお話しすると得られるものが多いです(数年遅れるのは私の練度の問題)。

最近のPHPカンファレンス福岡では私がPHPから離れているので軽い近況の話が多かったですが、ずっと尊敬している方です。

PHPカンファレンス福岡が終わってしまったので、何かまたきっかけを作りたいなあと思っています。

PHPカンファレンス福岡2024

k1low.hatenablog.com

2024はプロポーザル落ちて完全に参加者としてウロウロしていた年でした。めちゃくちゃ面白いですね!!!!!それなのにプロポーザル出すのなんでなんだろう。。。?謎です。

全国からお話ししたい人、質問したい人が来てくれるわけで、「東京、いつもズルい!」と思うと共に「PHPカンファレンス福岡ありがとう!!!!!!!!!!!」となりました。PHPカンファレンス福岡ありがとう!

これを本当に実感した年だった気がします。

いつも各所で言っているのですが、PHPカンファレンス福岡とかPHPerKaigiとかPHPカンファレンスとかPHPカンファレンス(いろいろ)とか、「設計」とか「組織」とかそういう方面の練度が高い人がいてめちゃくちゃ勉強になるんですよね。「PHP」というプレフィックスは本当にあの「プログラミング言語PHP」の意味なのかいつも疑問に思っています。

PHPプレフィックス」なカンファレンスの多くはPHPer以外でも知見が得られるカンファレンスだともっと認知されて欲しいですね。

PHPカンファレンス福岡2025

phpcon.fukuoka.jp

最後の年でしたが、普通にPHPカンファレンス福岡を楽しみました。

受付時に、akase244さんに「今からでも(運営スタッフ)間に合う」と言っていただいたのは嬉しかったです。そういえば運営スタッフやっていたんだよなあ。

終わりに

次の週の別のカンファレンスで akase224 / cakephper のお二人に会いましたが、お二人ともとても笑顔でした。

良いPHPカンファレンス福岡の10年だったのではないでしょうか。

私にとっては「地元」で「ホーム」でした。10年、10年かあ。

個人的な話ですが、来年はちょっと家庭の事情がありカンファレンスの遠方参加は控えることになりそうです*4

カンファレンスの楽しさは忘れられないですし、「PHPカンファレンス福岡フォロワーカンファレンス」も多くありますし、また再来年とかに参加できればなあと思っています。

カンファレンスいいですよね。PHPカンファレンス福岡も良かった。良かった。

*1:当時は私Fusicに所属していました

*2:社員のことをパートナーと呼びます

*3:もう1人はryuringさん

*4:ネガティブな話題ではありません。念の為

github.com/k1LoW/errors に errors.Join でまとめられた error を分割する Errors 関数を追加した

私はZero dependencyな error パッケージ github.com/k1LoW/errors をメンテナンスしています。

github.com

以前書いたエントリはこちら。

k1low.hatenablog.com

今回、github.com/k1LoW/errors に errors.Join でまとめられた error[]error に分割できる errors.Errors 関数を追加しました。

なぜ Errors 関数を追加したか

Go 1.20 で errors.Join が追加されました。複数のエラーを1つにまとめることができる便利な関数です。

err := errors.Join(errA, errB, errC)

しかし、まとめられたエラーを取り出す方法は用意されていません。

errors.Join が返すエラーは Unwrap() []error メソッドを実装しているので自分で取り出すことはできますが、ネストしたケースに対応しようとすると少し面倒です。

// これはネストしていない場合のみ
if je, ok := err.(interface{ Unwrap() []error }); ok {
    errs := je.Unwrap()
    // ...
}

そこで、再帰的にすべての error を取り出してフラットな []error として返す errors.Errors 関数を追加しました。

使い方

使い方はシンプルです。

import "github.com/k1LoW/errors"

errA := errors.New("error a")
errB := errors.New("error b")
errC := errors.New("error c")

// errors.Join でまとめる
joined := errors.Join(errA, errors.Join(errB, errC))

// errors.Errors で分割
errs := errors.Errors(joined)
// errs[0] == errA
// errs[1] == errB
// errs[2] == errC

ネストした errors.Join にも対応しており、すべてのエラーをフラットに取り出せます。

errors.Join されていない普通のエラーを渡した場合は、そのエラーだけを含む []error を返します。

err := errors.New("single error")
errs := errors.Errors(err)
// errs[0] == err
// len(errs) == 1

実装

実装もシンプルです。

// Errors returns all joined errors in the given error.
func Errors(err error) []error {
  je, ok := err.(joinError)
  if !ok {
    return []error{err}
  }
  errs := je.Unwrap()
  var splitted []error
  for _, e := range errs {
    splitted = append(splitted, Errors(e)...)
  }
  return splitted
}

Unwrap() []error を持つエラーかどうかを判定し、持っていれば再帰的に各エラーを展開していきます。持っていなければそのまま返します。

ユースケース

どういうときに使うかというと、例えば複数のエラーをまとめて返す処理があり、それを受け取った側でエラーごとに異なる処理をしたい場合などです。

// 複数のバリデーションエラーをまとめて返す
func validate(input Input) error {
    var errs []error
    if input.Name == "" {
        errs = append(errs, &ValidationError{Field: "name", Message: "required"})
    }
    if input.Age < 0 {
        errs = append(errs, &ValidationError{Field: "age", Message: "must be positive"})
    }
    return errors.Join(errs...)
}

// 受け取った側でエラーごとに処理
err := validate(input)
if err != nil {
    for _, e := range errors.Errors(err) {
        var ve *ValidationError
        if errors.As(e, &ve) {
            // フィールドごとにエラーメッセージを表示
            fmt.Printf("%s: %s\n", ve.Field, ve.Message)
        }
    }
}

go.uber.org/multierrgithub.com/hashicorp/go-multierror も同じようなエラーの分割する機能はありますが、github.com/k1LoW/errors はZero dependencyですし、標準のerrorsパッケージと互換があり、(github.com/hashicorp/go-multierrorでは必要な)型アサーションも必要ありません。

まとめ

github.com/k1LoW/errors パッケージに errors.Errors 関数を追加しました。

依然としてZero dependencyなので使い勝手は良いのではないかと思います。

是非採用のご検討のほどよろしくお願いいたします。

YAPC::Fukuoka 2025に参加した #yapcjapan

YAPC::Fukuoka 2025に参加してきました。

yapcjapan.org

私の発表の最初で「本編参加は初」というような内容をのたまっていたのですが、それは大きな間違いで、初参加はYAPC::Fukuoka 2017 HAKATAで、ちゃんと前夜祭だけではなく本編も満喫していました。 関係者の皆様、聴講者の皆様大変申し訳ありません。 (どう考えても2017のキーノートセッションの内容に覚えがある。。と思って調べまくったら上記の事実が自身のブログに残っていました。。。)

今回は2Daysということで、しっかり楽しむぞと、朝から福工大キャンパスへ。

私自身としてはYAPC本編初登壇で、しかも1日目午前中だったので気が気ではありませんでした。

ちょうどRFC関係の発表が2連続ということで、RFC後半戦としてRFC9111について発表してきました。

speakerdeck.com

当時はAIもなくかなり頑張ったのでどうしてもどこかで紹介をしたいと思っていたのをYAPCで発表できて良かったです。RFC2連続の効果か、席もほぼ埋まっていてしっかり緊張して話すことになりました。

お昼といろんな方と学食で食べた後に、Songmuさんのdeck高速化の発表*1を聞きながらXで副音声ポストをしていました。

PHPカンファレンス福岡2025では、deck作者としての発表をしたのですが、コントリビュータとしてのSongmuさんからのお話を聞けて答え合わせができた気分でした。あと、Songmuさんが施したdeck高速化のアプローチ、今回の発表でやっと完全に理解できました。ありがとうございました。

その後も発表を聴かせてもらったり、美味しいコーヒーをいただいたり、非公式懇親会で美味しいクラフトビールをいただいたりして、Day1終了。


Day2は子供の行事と重なったので午後から参加させていただきました*2

キーノートはP山さん。私も一緒に働いたことがあります。とても良い発表でした*3

「やる奴はやる。やらない奴はやらない。」

どこかの漫画にあるようなセリフですが、それがそのまましっくりくるのがP山さんです。「もしかしたら漫画の登場人物なのかもしれない」と思えるようなエピソードがたくさんあります。

私自身、P山さんに「突き動かされ」ている1人です。今でも自身の行動を振り返るときに「もしP山」が湧き起こり、恥ずかしくなったり、気を引き締めたりすることがあります。

そういう人のあのような発表なので多分多くの人に響いたんではないかと思います。

良い場面に出くわすことができました。


YAPC::Fukuoka 2025、楽しかったです。運営の皆様、スポンサー、登壇者の皆様ありがとうございました!

*1:mizzyさんの発表も聞きたかった。。。かなり良かったという話でかなり動画が待ち遠しい

*2:Day2の午前中にもみたい発表が多くあったのですが、動画公開を楽しみにしようと思っています。

*3:発表後、本人はとても謙遜していましたが