ターミナルでチャットできるGoベースの対話型シェル「chatsh」を作った
はじめに
開発作業をしているときにせんようClientに移らずに「ターミナル自体がチャットルームになったらどうだろう?」という発想から、Goでファイルシステムに似せたなリアルタイムチャットシェルのchatshを作りました。
chatshとは
chatshは、ターミナル内でファイルシステム風の操作でチャットルームを管理できる対話型シェルです。
主な特徴:
-
ls
,cd
,mkdir
といった標準的なシェルコマンドでチャットルームを操作 -
vim
コマンドでTUIベースのリアルタイムチャット - gRPCの双方向ストリーミングによる低遅延通信
- ファイルシステムの階層構造でチャットルームを管理
# インストール
$ brew install ponyo877/tap/chatsh
# 使用例
$ chatsh
❯ mkdir project-team
❯ cd project-team
❯ touch daily-standup
❯ vim daily-standup # チャットルーム開始
技術選定
Go + gRPC
リアルタイム性を重視して、gRPCの双方向ストリーミングRPCを採用しました。WebSocketと比較してプロトコルレベルでの型安全性と効率的なシリアライゼーションが決め手でした。
service ChatshService {
rpc StreamMessage(stream ClientMessage) returns (stream ServerMessage);
}
message ClientMessage {
oneof payload {
Join join = 1;
Chat chat = 2;
Tail tail = 3;
}
}
ファイルシステムメタファー
チャットルームの管理にファイルシステムの概念を導入。直感的な操作と学習コストの削減を狙いました。
SQLiteで階層構造を実現:
-- ディレクトリ構造
CREATE TABLE directories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
parent_id INTEGER NOT NULL REFERENCES directories(id), -- 親Dir
owner_token TEXT NOT NULL REFERENCES users(token),
path TEXT NOT NULL, -- CLI高速化ための絶対PATH
created_at DATETIME NOT NULL,
UNIQUE (parent_id, name),
UNIQUE (path)
);
-- チャットルーム(ファイルを模している)
CREATE TABLE rooms (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
directory_id INTEGER NOT NULL REFERENCES directories(id), -- 親Dir
owner_token TEXT NOT NULL REFERENCES users(token),
path TEXT NOT NULL, -- CLI高速化ための絶対PATH
created_at DATETIME NOT NULL,
UNIQUE (directory_id, name),
UNIQUE (path)
);
Cloud Run + Litestream
データ永続化にLitestreamを使用してSQLiteファイルをGoogle Cloud Storageにレプリケーション。Cloud SQLを使わずコストを大幅削減しました。
終始以下のkokiさんの記事を参考にさせていただきました。
実装のポイント
双方向ストリーミング通信
クライアント側ではgRPCストリームを確立し、goroutineで非同期にメッセージを受信:
func runChatUI(client pb.ChatshServiceClient, roomPath string) error {
stream, err := client.StreamMessage(ctx)
if err != nil {
return err
}
// メッセージ受信用goroutine
go func() {
for {
msg, err := stream.Recv()
if err == io.EOF {
return
}
// UI更新
updateChatView(msg)
}
}()
// メッセージ送信処理
// ...
}
TUIの実装
tviewを使用してリッチなターミナルUIを構築
textView := tview.NewTextView().
SetDynamicColors(true).
SetScrollable(true)
inputField := tview.NewInputField().
SetLabel("❯ ").
SetAcceptanceFunc(tview.InputFieldMaxLength(256))
flex := tview.NewFlex().
SetDirection(tview.FlexRow).
AddItem(textView, 0, 1, false).
AddItem(inputField, 1, 0, true)
CLI框架
cobraでサブコマンドを構築し、go-promptで対話型モードとTAB補完を実装
func completer(d prompt.Document) []prompt.Suggest {
// gRPCでディレクトリ情報取得
res, _ := client.ListNodes(ctx, &pb.ListNodesRequest{Path: currentPath})
var suggestions []prompt.Suggest
for _, entry := range res.Entries {
suggestions = append(suggestions, prompt.Suggest{
Text: entry.Name,
})
}
return suggestions
}
自動リリース
GoReleaserでGitHubリリースとHomebrew Tapを自動化:
# .goreleaser.yaml
builds:
- binary: chatsh
goos: [linux, darwin, windows]
goarch: [amd64, arm64]
brews:
- repository:
owner: ponyo877
name: homebrew-tap
homepage: https://github.com/ponyo877/chatsh
こちらもkokiさんの記事を参考にしました、もうkokiさん信者です。
まとめ
chatshを通じて、以下の技術を実践的に学べました:
- gRPCの双方向ストリーミング
- Goでのリアルタイム通信
- TUIライブラリの活用
- ファイルシステムメタファーを用いたUX設計
- Cloud Run + Litestreamによる低コスト構成
ターミナルネイティブなコミュニケーション体験として、新しい開発ワークフローの可能性を提示できたのではないかと思います。
興味のある方はぜひ試してみてください:
brew install ponyo877/tap/chatsh
また、Asakusa.goでも発表させていただきました!
会場を提供してくださっているドクターズプライムさんのイベントレポートでも取り上げていただけました!
ありがとうございます!
Discussion