標準愚痴出力

個人的なIT作業ログです。もしかしたら一般的に参考になることが書いているかもしれません(弱気

ターミナル用 JSON エディター Jegan を作りました

拙作の

  1. バイナリエディターBine
  2. CSV エディター Csvi
  3. ライブラリテスト用簡易テキストエディタ GM

に続く、ターミナルエディターシリーズ第4弾です。

純粋な JSON だけでなく、以下形式にも対応しています。

  • JSONL 形式
    (ログなどよく使われる。トップ項目が改行などで区切られて複数個存在する)
  • JavaScript代入式
    (X/Twitter などのアーカイブで使用されている方式)

これまでのターミナルエディターの定番の機能もサポートです。

  • 大きな JSON ファイルでもバックグラウンド読み込みを行い、高速に起動
  • 普通の JSON エディターだと正規化してしまって、関係ないところの書式がかなり変わったりしがちだが、本エディターはユーザが明示的に修正した箇所以外は一切変更しない(Minimal Diff)**
  • カーソル移動は vim ライク、項目編集は Emacs like な readline
  • 画面の必要最小限の行のみ使用
  • ファイル読み込み・標準入力読み込み両方に対応
    ( cat < old.json | jegan | cat > new.json といったことも可能 )
  • ユーザによる変更箇所を強調表示、u キーでUNDO 可能
  • Windows と UNIX系OS に対応

今回、バックグラウンド読み込みと Minimal Diff はかつてなく苦労しました。本当はそのあたりをつらつら詳しく記したいところですが、キリがないので今回は省略します。

この製品、3月末に開発に着手して、一ヶ月間、かなり集中して作り込んだんですが… そもそも需要があまりないようです。これは gmnlisp 以来の徒労に終わるかも。まぁ、せっかく作ったものなので、もし JSON ファイルを手作業で触る機会があるようでしたら試していただけますと幸いです。

ターミナル用CSVエディタ Csvi:大躍進の裏で起きた入力キーシーケンス分断問題

Csvi の大躍進

20〜30 あたりだった、GitHub の Stars が 2025年末時点で 88 、2026年2月には171 まで上がりました。宣伝が効を奏したようです。

特に Reddit 投稿が大きかったようです。これは mattn 先生の

https://x.com/mattn_jp/status/2010536235459362883:

某ブログサービスの翻訳のデフォルト設定の是非の話は別の話として置いとくとして、昔からずっと海外 OSS に関わって来た隅っこの1人としては、日本人が発してる技術情報や OSS には優秀な物も沢山あるのに、海外勢から読まれずに使われずに消えてく物が多すぎるんですよ。読み流されるならまだしも、そもそも読まれもしてない。
僕はよくOSS 作ったら Reddit に宣伝ポストするけど、アレやるとやらんのでは海外勢からのリーチが全然違う
もっと読まれる流れを作るべきだと思うし、もし SNS で逆張りして文句言ってるだけなのであれば(そうではなく信念がある人や自分で英訳したい人は別として)、今一度考えたほうがいいんじゃないかと個人的に思ってる。(前述の通りデフォ設定の話はしていない)

という御発言を踏まえたものでしたが、いざやってみたら「ほんまや…」という有様でした。

この手の投稿はやり直しが多分できませんので(嫌われたらリカバリが難しい)、慎重に行いました。ChatGPT に助けてもらいましたが、あくまでアドバイスや英語の支援とし、内容の決定・判断は自分で主体的に行うことに努めました。

(AI を使ったことは伏せるべきではないかとも思ったのですが、気を付けるべき点をどうやって知ったかを記すとなると避けては通れません。「丸投げ」ではなく自分の意思がきっちりと入っていることを強調すれば理解が得られることを期待し、その上で正直に書くようにしました)

  1. Reddit に対して、ツールの宣伝を、どこに、どうすべきか、ChatGPT にガイドしてもらう。曰く
    • Csvi のようなコマンドラインツールの宣伝ならば r/commandline がよい
    • r/commandline はツール紹介の場なので、くどくどと自己紹介や経緯など書かず、まずは端的にツールの特徴一覧とスクリーンショット、GitHub の URL だけを書けばよい。URL は GitHub Pages よりは GitHub 本体の方がよい
  2. 投稿内容は慎重に作成
    • まずは自分で投稿内容の素案を作る。いきなり英語はしんどいので、日本語で箇条書き
    • それを ChatGPT に英語化してもらう
    • その英語を読んで、自分の意図どおりかチェック
      • どういう意味か分からないものは GPT 自身に聞く
      • GPT は油断をすると冗長だったり、ズレた表現をしがちなので、違っていたら直してもらう
      • アポストロフィーやダッシュ、絵文字など GPT が使いがちな文字は他の表現に置き替える ( 使うなと言っても忘れた頃に使われ気味なので、チェックは欠かせない)
  3. 投稿後は反応すべてに対して誠実に返信
    • ニュアンスの解釈がたいへんだが、そこは GPT にいちいち解説してもらう。
    • 返信内容づくりは投稿内容作成と同様に GPT とのキャッチボール

結果は Upvotes 158, Downvotes 0, Comments 22 と期待以上のものとなりました。

これの反響か、Untitled Linux Show: More Time to Bake という、Linux の動画ニュースでも取り上げられました(238回;1月18日分)。00:53:04 のあたりですね (AI による文書おこし)

入力キーシーケンスの分断問題

ところで、00:54:20 からRob Campell さんから、このような指摘があります。

I'm not sure if I found a bug. But when I navigate around with the arrows, every once in a while, when I push the back arrow, it was deleting the whole line. So I'm going to dig into that and maybe submit a bug for that if I figure out. Because I don't know why it was doing that for me. So if you use this right now, use the HJKL to navigate around.

(機械翻訳) バグを見つけたかどうかはわかりません。しかし、矢印を使って移動するとき、時々、戻る矢印を押すと、行全体が削除されてしまいました。それで、私はそれを掘り下げて、もしわかったらバグを提出するつもりです。なぜなら、なぜそれが私にそのようなことをしたのかわからないからです。したがって、今これを使用する場合は、HJKL を使用して移動してください )

左矢印キーのキーは\x1B[D という複数バイトのシーケンスなんですが、入力に遅延が発生すると、読み切れなかった D を行削除と誤認してしまうという問題でした。

これに対し、v1.21.1 で次のような対応を行いました。

さて、この入力キーシーケンスが分断されて誤認識してしまうという問題は以前に NYAGOS + ConEmu でも時折、ヒストリ参照で「なぜか[A が入力される」という形で報告がありました。

つまり、事は Csvi 単体ではなく、自分の nyaosorg エコシステム全体にもあったということです。そこで、一文字キー入力のアダプターパッケージ go-ttyadapter にメスを入れることにしました。

"go-ttyadapter" は以下のようなサブパッケージを提供しています。

  • "tty8" - "github.com/mattn/go-tty" のラッパー (Windows 7,8 でも動く)
  • "tty10" - "golang.org/x/term" のラッパー
  • "auto" - オートパイロット機能を提供する。主にテスト用途

これらのラッパーは3種類の一文字キー入力パッケージに対して、同一の共通 interface を提供するものです。

type Tty interface {
    Open(onResize func(int, int)) error // 初期化: 引数は画面サイズが変った時に呼び出すコールバック関数。nil可能
    GetKey() (string, error) // 1文字キー入力
    Size() (int, int, error) // 画面サイズを取得
    Close() error
}

既存動作を替えるのはよくないので、今回は新パッケージを二つ加えました。

  • "tty8pe"
  • "tty10pe"

これらは tty8, tty10 に対して、\x1B などを受信したら後続するデータを必ず待つという動作を加えたものです。副作用として

  • Esc キー単品入力が受信不可になる
  • Esc + 1文字と、Alt + 1文字が等価になる

という変化を及ぼすため、キー入力体系全体の見直しが発生します。それはプログラムコードだけでなく、README などドキュメントなどにも及びます。

  • go get github.com/nyaosorg/go-ttyadapter を実行する
  • import 先を "github.com/nyaosorg/go-ttyadapter/tty8" → "github.com/nyaosorg/go-ttyadapter/tty8pe" に切り替え
  • できなくなった、Esc キー単体入力を Ctrl+G など別のキーにふりなおす
  • README.md , README_ja.md などの記載を改める
  • バージョンを更新する
  • go-ttyadapter を利用しているパッケージを import していたら、それも更新する

これを全部のパッケージでやります。うへぇ

こうして、1月13日の Reddit 投稿にはじまったフィーバーは 2月14日にようやく終わったのです…長かった(まぁ、フィーバーいうても、ほとんど独りで踊っていた感じなんですけどね)

jj の GitHub 認証まわりの最近の挙動

Git互換バージョン管理システム jjGitHub へコミットを push する時などに行う認証方法ですが、かつてはパスフレーズ入力を必要としない方式しかサポートしていませんでした。それが、いつの間にかフルサポートになっていた模様です。

いいわけになりますが、 リリースノートには Authentication という単語が全然でていませんでした。zenn.dev の自分の Book の見直しをしようと、最新公式ドキュメント を確認していて、今ごろ気付いてしまいました。 (英語は苦手なので許して)

過去ドキュメントのバックナンバーを見ると、バージョン 0.26.0 までは

  • Authentication: Partial. Only ssh-agent, a password-less key ( only ~/.ssh/id_rsa, ~/.ssh/id_ed25519 or ~/.ssh/id_ed25519_sk), or a credential.helper.

認証は部分サポート。ssh-agent, パスワードなしのキー (~/.ssh/id_rsa, ~/.ssh/id_ed25519 or ~/.ssh/id_ed25519_sk のみ ), もしくは credential.helper

だったのが、v0.27.0 から

  • Authentication: Yes. With the default authentication scheme, which uses git under the hood. With git.subprocess = false, only ssh-agent, a password-less key (only ~/.ssh/id_rsa, ~/.ssh/id_ed25519 or ~/.ssh/id_ed25519_sk), or a credential.helper are supported.

認証サポート。デフォルトでは内部で git を使用するデフォルトの認証スキームを使用。設定 git.subprocess = falsessh-agent, パスワードなしのキー (~/.ssh/id_rsa, ~/.ssh/id_ed25519 or ~/.ssh/id_ed25519_sk のみ ), もしくは credential.helper をサポート

v0.29.0

  • Authentication: Yes. git is used for remote operations under the hood.

認証サポート。デフォルトでは内部で git を使用するデフォルトの認証スキームを使用。

になってました。

まずは実験です。パスフレーズがちゃんとある ssh 鍵に差し替えて、ssh-agent を止めて、git cloneパスフレーズ入力を要求されることを確認した上で、jj git push をやってみたところ

> jj git push
Changes to push to origin:
  Move forward bookmark master from b84e02bf5017 to 522d4fc0791c
Error: Git process failed: External git program failed:
git@github.com: Permission denied (publickey).
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.

あれぇ。あいかわらずエラーが発生します。原因が分からないので

set "GIT_SSH_COMMAND=C:/Users/hymko/scoop/apps/git/current/usr/bin/ssh.exe -v"

と設定してみると

debug1: read_passphrase: can't open /dev/tty: No such device or address

と標準エラーに出てます。

どうやら、jj → git → ssh の過程で端末の引渡しがうまくいっていないようです。Linux 環境であれば普通端末の引渡しなど失敗することはないのですが、Windows では MSYS など UNIX エミュレーション下のプログラムとネイティブなプログラムが交差するので、ありえない話ではありません。実際、WSLのUbuntu 下では問題なくパスフレーズが表示されてしまいました。

今のところ言えることは

  • Windows環境では、Git で使える認証方法が、そのまま jj でも使えるようになっている
  • Windows環境では、端末の仕様により、jj 内でのパスフレーズ入力が失敗しがち
    →引き続き ssh-agent などあらかじめ認証を通しておくのが無難

といったところではないかと思われます。

Git互換分散バージョン管理システム jj で「jj git push --allow-new」が非推奨となった

従来はゼロコミット状態の GitHubjj から初回 push する場合

$ jj git init --colocate

$ gh repo create --public 20251226
✓ Created repository hymkor/20251226 on github.com
  https://github.com/hymkor/20251226

$ jj git remote add origin git@github.com:hymkor/20251226.git

$ jj commit -m "初回コミットのログ"

$ jj bookmark create master -r @-
Created 1 bookmarks pointing to ormyzlwk 61595706 master | Add .gitattributes

$ jj git push --allow-new

でよかった。これがいろいろ変わった:

  • v0.34.0

    Git-based repositories are now colocated by default. Configure git.colocate = false to keep the previous behavior.

    • jj git init で、これまでほぼ常に指定していた、git コマンドとの共存を指示するオプション --colocate が省略可能になり、デフォルトで有効になった ( Breaking changes とされている)
  • v0.35.0

    jj bookmark track can now associate new local bookmarks with remote. Tracked bookmarks can be pushed without --allow-new. #7072

    • jj bookmark track はリモートレポジトリにローカルの新bookmark を関連付けできるようになった。
    • track された bookmark は --allow-new なしで push することができる

--colocate が省略できるようになったのはよいが、初回に jj git push する時の --allow-new が非推奨となったのはとまどうところだ。次のような警告が出るようになってしまった。

$ jj git push --allow-new

Warning: --allow-new is deprecated, track bookmarks manually or configure remotes.<name>.auto-track-bookmarks instead.
Changes to push to origin:
  Add bookmark master to e77f6b7289bb

なぜ、こうすべきかについては #7072 で述べられているものの、話が長いこともあり、なかなか理解が難しい。リモート側に存在しない bookmark を track するとは?

理由はともかくとして、今後は次のような手順が推奨されるようだ。

$ jj bookmark track master@origin
Started tracking 1 remote bookmarks.

$ jj git push
Changes to push to origin:
  Add bookmark master to 615957069b23

しかしながら、bookmark を create して、track して、ようやく push というのはステップ数がやや多い。たまにしか行わない「レポジトリを作成する」作業では、すぐ忘れてしまいそうだ。

思うに別に --named オプションでもいいのではないだろうか?

$ jj git init

$ gh repo create --public 20251226
✓ Created repository hymkor/20251226 on github.com
  https://github.com/hymkor/20251226

$ jj commit -m "初回コミット"

$ jj git remote add origin git@github.com:hymkor/20251226.git

$ jj git push --named master=@-
Changes to push to origin:
  Add bookmark master to 47d0c5b9a7dd

jj git push --named BOOKMARK=REVSET はリモートレポジトリにローカルレポジトリの指定したコミットを先頭とする新bookmarkを作るサブコマンドだ。普段は PR 用のブランチを作成するためによく使うが、初回 push に使っても別にダメというわけではない。むしろ、こちらの方だと、jj bookmark create master も省略できる。

もしかして、今までは、こうするのが普通だったのだろうか?まぁ、少なくとも自分は今後これを主に使うことになりそうだ。

References

jj にタグ関連のサブコマンドが実装されていた

今まで jj にはタグ操作のサブコマンドが jj tag list くらいしか実装されていなかったため、リリースのためのタグを作成する際:

git tag v1.19.0
git push --tag

と git を併用しなければいけなかった。

だが、jj があずかり知らぬところで git コマンドが実行されたことを jj が検出すると、jj はレポジトリの整合性をとるため、git から見えないコミットを破棄する動作を行うことがある。たとえば、タグの打つ場所を間違えたので作成し直そうと git tag -d を実行すると、タグがなくなったコミットは git から見えないため、続く jj log などのタイミングで削除されてしまう。1

v0.35.0 では jj 自身によるタグ作成/削除のサブコマンドが用意され、安全性が改善された。

  • (作成) jj tag set TAG -r REVSET
  • (削除) jj tag delete TAG

だが、こうしたタグを GitHub へ反映させるコマンドは11月27日時点ではまだ実装されていないようだ。そこは引き続き git push --tag を使う必要がある。

References

ソフトウェア命名論争についてのポエム

cat alternative とされるコマンド群がある。

個人的な感覚だと、cat はファイルを連結するコマンドで、見るのは more や less のお仕事だから more alternative つーか、素直に pager と表現した方が…といつも考えてしまう。

だが、これは:

  • 技術者としては機能を正確に表す名前にしてほしい(cat の機能は連結である)
  • 利用者としてはどこで使うべきかが分かりやすい名前にしてほしい(catはファイルを見る時につかう)

という対立問題の現れでもある。たとえば、最近の MinGW の必須 DLL としては次のような3 つの DLL があるらしい (正確さは微妙だが、多分、このような小難しい名前だったと思う)

  • libwinpthread-1.dll
  • libgcc_s_seh-1.dll
  • libstdc++-6.dll

いや用途は分かるけど、これらが MinGW 必須DLLだとは分かりにくいヨ。Cygwin1.dll みたいに、MinGW1.dll とかにしてくれ…と思ったりする。

「それ bat/gat に対して言ってたことと違いますよね?」

あ、はい。


同氏は、かつてのUNIXツールは、grep(global regular expression print)、awk(開発者の頭文字)、sed(stream editor)など、機能や由来が直感的に分かる名前だったのに対し、現代では「Viper」「Cobra」「Melody」「Casbin」「Asynq」など、機能と無関係な名前が氾濫していると現状を憂いています。

なら、GNU ってなんだよ! GNU の G ってなんだよ。ふざけんな!

「あ、ひらきなおった」

うるせぇ。nyagos、ぶつけるぞ

テキストファイルの行処理のイディオム

テキストファイルなどを扱う時:

  • データ読み取り時にエラーが発生した時はただちに終わる
  • ただし、そのエラーが EOF の場合は、有効データがある可能性があるため、それを処理してから終わる

という定番処理がある。

これを素直に実装すると次のようになる。 ( 読み取り処理は fetch, 加工処理は work という関数で表現 )

for {
    data, err := fetch()
    if err != nil && !errors.Is(err, io.EOF) {
        return err
    }
    work(data)
    if errors.Is(err, io.EOF) {
        break
    }
}

これ、errors.Is 関数を二回呼んでいるので、コストがかかる。この無駄の削減 兼 定型処理のパターン化のため、次のようなヘルパー関数を書いてみた。

func fetchAndWork[T any](fetch func() (T, error), work func(T)) error {
    for {
        val, err := fetch()
        if err != nil {
            if errors.Is(err, io.EOF) {
                work(val)
                return nil
            }
            return err
        }
        work(val)
    }
}

なんとなくだが、よさそう。

これ、本当は csvi での処理の最適化のために考えたたのだけれども、csvi ではデータ読み取りと処理を別の goroutine で行い、値やエラーは channel 経由で受け渡しているので、残念ながら使えそうもない。

なので、ここで供養することにした。