ターミナルカラー検出ロジックの簡素化とセンチネル処理の修正
ターミナル起動時のカラー検出において、DA1センチネルマーカーの消費漏れを修正し、チャネルベースの並行処理を同期的なバッファ読み取りループに置き換えることで、実装を大幅に簡素化しました。
背景
この変更は2つの問題を解決するために行われました。1つ目は、DA1センチネルマーカーが消費されないまま残り、ターミナル終了時にターミナルに漏れ出す不具合です。2つ目は、並行処理を用いた複雑な応答処理ロジックの存在で、これが保守性の低下につながっていました。
もともとの実装では colorResult 構造体とチャネルを使い、OSCシーケンスの応答を並行的に収集していました。応答ごとにゴルーチンでチャネルに送信する設計は、センチネルの到着を検知しつつ複数の応答を非同期に処理するために採用されたものですが、結果として制御フローが複雑になっていました。
この設計の複雑さを解消するため、「センチネルが来るまでバッファを読み続け、タイムアウト時にreaderを閉じる」という単純なループへの置き換えが行われています。
技術的な変更
palette_detect.go の実装が bufio.Reader を用いた同期的な読み取りループに刷新され、コードは156行の削除に対して99行の追加と、大幅な削減となっています。
変更前のアーキテクチャでは、colorResult チャネルを介してOSCレスポンスを並行処理していました。センチネル検知・タイムアウト・チャネルへの書き込みが絡み合い、制御パスが分散していました。
変更後のアーキテクチャでは、detector 構造体が bufio.Reader を保持し、DetectTerminalColors 関数内のループがバッファを逐次読み取りながらOSCシーケンスを解析します。DA1センチネルを受信するとループを終了し、タイムアウト時は tty をクローズすることでreaderに対してEOFを発生させ、ループを自然に終了させる設計です。これにより colorResult 構造体はDiffから完全に削除されています。
// 変更前: チャネルベースの並行処理
type colorResult struct {
index int
color colorful.Color
}
クリーンアップ処理も、defer による単純なクロージャから sync.OnceFunc を使った cleanup 関数に変更されています。
// 変更前: deferによるクリーンアップ
defer func() {
term.Restore(fd, oldState)
tty.Close()
}()
// 変更後: sync.OnceFuncによる安全なクリーンアップ
cleanup := sync.OnceFunc(func() {
term.Restore(fd, oldState)
tty.Close()
})
この変更により、タイムアウトによる強制終了パスと正常終了パスの両方で安全にリソース解放できるようになり、二重クローズも防止されます。またセンチネルマーカーはループ内で必ず消費されるようになり、ターミナルへの漏れ出しが解消されました。
テストコードも同様に整理され、チャネルを直接検証する個別テスト(TestParseOSCForeground 等)が廃止され、detector 構造体を生成するヘルパー newTestDetector を使った TestReadForegroundColor 等に置き換えられました。
// 変更後: detector構造体を使ったテストヘルパー
func newTestDetector(data string) *detector {
return &detector{reader: bufio.NewReader(strings.NewReader(data))}
}
func TestReadForegroundColor(t *testing.T) {
d := newTestDetector("\x1b]10;rgb:c0c0/caca/f5f5\x07")
da1, err := d.readNext()
require.NoError(t, err)
assert.False(t, da1)
assert.True(t, d.colors.Detected[sampleForeground])
assert.InDelta(t, 0.753, d.colors.Colors[sampleForeground].R, 0.01)
}
設計判断
並行処理から同期処理への切り替えが採用された点が、この変更の核心的な設計判断です。
OSCシーケンスの応答はターミナルから逐次到着するため、本質的に並行処理の恩恵が薄い処理です。チャネルを使ってゴルーチン間で結果を受け渡す設計は、タイムアウト処理やセンチネル検知との組み合わせで制御フローを複雑にしていました。bufio.Reader でバッファを逐次読み取るループは、同じ処理を単一のゴルーチン内で完結させます。
タイムアウト処理も tty.Close() によるEOF発生という方法が選ばれています。これは読み取りをブロックしているgoroutineをチャネルや context.Context で中断するよりもシンプルで、sync.OnceFunc と組み合わせることで二重クローズも防止しています。
まとめ
本PRは、並行処理の複雑さがもたらしていたセンチネル消費漏れというバグを、同期的なループへの設計変更で根本から解消した変更です。コード量を純減させながらバグを修正するというアプローチは、「シンプルな設計がバグを生みにくい」という原則を実践する好例といえます。