tokuhirom's Blog

neojot を Svelte 5 にあげた

こういうエラーが出て詰んだので対応。

https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes

マイグレーションガイドが丁寧に書かれていたのでこの通りに対応していく。

const app = new App({ target: document.getElementById("app") });

みたいに書いていたところを

const app = mount(App, { target: document.getElementById("app") });

と書くように変わったようだ。

最近のソフトウェアはマイグレーションガイドが丁寧なので助かるぜ。。

PR はこちら。 https://github.com/tokuhirom/neojot/pull/459

python の romkan がインストールできない

Collecting romkan
  Using cached romkan-0.2.1.tar.gz (10 kB)
  Preparing metadata (setup.py) ... error
  error: subprocess-exited-with-error

  × python setup.py egg_info did not run successfully.
  │ exit code: 1
  ╰─> [6 lines of output]
      Traceback (most recent call last):
        File "<string>", line 2, in <module>
        File "<pip-setuptools-caller>", line 34, in <module>
        File "/private/var/folders/kg/nqb8t2f1253_kh0r_rl06pcm0000gq/T/pip-install-cmsaoeh6/romkan_3dc774240ae4470fa4013b902bdab42c/setup.py", line 8, in <module>
          import os, json, imp
      ModuleNotFoundError: No module named 'imp'
      [end of output]

  note: This error originates from a subprocess, and is likely not a problem with pip.
error: metadata-generation-failed

× Encountered error while generating package metadata.
╰─> See above for output.

note: This is an issue with the package mentioned above, not pip.
hint: See above for details.

となって悲しい。

https://github.com/soimort/python-romkan/issues/17

https://docs.python.org/3.11/library/imp.html

imp というモジュールが、python 3.12で消えたようだ。

romkan に関して言うと、そんなに大きいライブラリではないのでコピペして使うのが良いかもしれず。。

Pure Kotlin な形態素解析機 Momiji をリリースしました

https://github.com/tokuhirom/momiji

Motivation

Pure kotlin な形態素解析機が欲しかったから。なぜ pure kotlin がいいかというと、kotlin multiplatform な環境で形態素解析したかったから。

実践・自然言語処理シリーズ2 形態素解析の理論と実装 が網羅的で、どういうふうに mecab が作られてるかを解説してくれてるので、この通りに実装すればまぁ普通にできる。

形態素解析は、ラティス構築してビタビアルゴリズムで最小コスト経路をたどればできるということは知っていたので、というかまぁ IME 作るのとやってることはほとんど一緒なので、やればできるということは知っていたので、やってみたという感じ。

kotlin multiplatform で動くからもちろん JS でも動くので、デモサイトも作ってみました。 https://tokuhirom.github.io/momiji/

どう作ったか

mecab の辞書をパースして、ラティスを構築してビタビアルゴリズムでコスト最小な経路を求めた。

mecab の辞書のパース

mecab のバイナリ辞書のパースは sys.dic, unk.dic, char.bin, matrix.bin の4つのファイルをパースできれば、ひと通りの事ができる。

バイナリファイルはすべてリトルエンディアン。

sys.dic / unk.dic のパース

sys.dic はシステム辞書で、unk.dic は未定義語のコストが入っている特殊辞書。フォーマットは同じ。

            val magic = byteReader.readUInt()
            val version = byteReader.readUInt()
            val type = byteReader.readUInt()
            val lexsize = byteReader.readUInt()
            val lsize = byteReader.readUInt()
            val rsize = byteReader.readUInt()
            val dsize = byteReader.readUInt()
            val tsize = byteReader.readUInt()
            val fsize = byteReader.readUInt()
            val dummy = byteReader.readUInt()

という感じでヘッダ領域がある。magic は 0xef718f77u とファイルサイズの xor で、これにより辞書が壊れていないかを確認できる。

その後に 32 byte の charset 領域がアリ、charset が格納されているはずだが、現実には "yes" という文字列が入っている。./cofigure の結果の charset じゃない文字列とかが入っちゃってるのかなぁと思いつつ、現実的には utf-8 決め打ちするので無視して問題ない。

その後、dsize バイト分の領域に、Darts の double array が格納されている。

そして、tsize 個のトークンが格納されている。Token は以下のようなフォーマット。

data class Token(
    val lcAttr: UShort,
    val rcAttr: UShort,
    val posid: UShort,
    val wcost: Short,
    val feature: UInt,
    val compound: UInt,
) {
    companion object {
        const val SIZE = 2 * 4 + 4 * 2
    }
}

あとは fsize バイトの feature が入っている。

char.bin

未定義語を判定するための文字種定義。例えばカタカナの連続だったら一つの未定義語とする、みたいな処理をするために使う。

32bit unsigned int で char category の数が格納されている。char category は 32 バイトの固定長文字列で、null terminated である。その後、0xFFFF 個の unsigned int が格納されております。

bit field になってるけど以下のような構造。

data class CharInfo(
    val type: Int,
    val defaultType: Int,
    val length: Int,
    val group: Boolean,
    val invoke: Boolean,
)

matrix.bin

連接コストが入ってる。

ヘッダに unsigned short 2つで lsize と rsize が入っている。ここから、lsize * rsize 個の short が入っている。 連接コストは matrix[leftContextId + lsize * rightContextId] のようにして取得可能。

Darts の実装

なんか common prefix search したいわけだが、、 https://blog.64p.org/entry/2024/07/24/010456 先日、KDary という実装を作ってできるようにしたんだけど、mecab のバイナリ辞書には Darts のバイナリが埋まってたので、これをそのまま使ったほうが効率がいいという話に。。

https://github.com/tokuhirom/momiji/blob/main/momiji-core/src/commonMain/kotlin/io/github/tokuhirom/momiji/core/dict/Darts.kt

Darts のバイナリを使って common prefix search するだけならこれだけで実装できた。

ラティスの構築

https://github.com/tokuhirom/momiji/blob/main/momiji-core/src/commonMain/kotlin/io/github/tokuhirom/momiji/core/LatticeBuilder.kt

なんかこういう感じでやったらできた。

ビタビアルゴリズムの実装

https://github.com/tokuhirom/momiji/blob/main/momiji-core/src/commonMain/kotlin/io/github/tokuhirom/momiji/core/Lattice.kt#L48-L85

ガーッとラティスをたどりながら最小コストを計算していって、最後に逆向きに最小コスト経路をたどっていくだけなので簡単。

まとめ

形態素解析って大変なのは辞書作るところなので、すでにある辞書を利用する分には簡単に実装できた。コード量としても大した事ない感じ。 あと、工藤さんの本がめっちゃ丁寧なのでかいてある通りに実装すれば普通に動いた。

kotlin-wrappers の 1.0.0-pre.757 から 1.0.0-pre.758 での不具合を報告して修正済みになった話

kotlin-wrappers の 1.0.0-pre.757 から 1.0.0-pre.758 までのバージョンで、もんだいがあった..

println(process.platform)

というコンパイルが以下のようなエラーでコンパイル出来なくなったのだ。

> Task :compileProductionExecutableKotlinJs FAILED
e: java.lang.IllegalStateException: IrTypeParameterPublicSymbolImpl for [ node.stream/StreamOptions.Companion.invoke|invoke(web.abort.AbortSignal?;kotlin.Double?;kotlin.Boolean?;kotlin.Function2<kotlin.Throwable?,kotlin.Function1<kotlin.Throwable?,kotlin.Unit>,kotlin.Unit>;kotlin.Boolean?;kotlin.Function1<kotlin.Function1<kotlin.Throwable?,kotlin.Unit>,kotlin.Unit>;kotlin.Boolean?){0§<node.stream.Stream>}[0] <- Local[<TP>,0|TYPE_PARAMETER name:T index:0 variance: superTypes:[node.stream.Stream] reified:false] ] is already bound: TYPE_PARAMETER name:T index:0 variance: superTypes:[node.stream.Stream] reified:false
        at org.jetbrains.kotlin.ir.symbols.impl.IrBindablePublicSymbolBase.bind(IrPublicSymbolBase.kt:69)
        at org.jetbrains.kotlin.ir.declarations.impl.IrTypeParameterImpl.<init>(IrTypeParameterImpl.kt:48)
        at org.jetbrains.kotlin.ir.declarations.impl.AbstractIrFactoryImpl.createTypeParameter(IrFactoryImpl.kt:380)
        at org.jetbrains.kotlin.ir.declarations.impl.IrFactoryImplForJsIC.createTypeParameter(IrFactoryImplForJsIC.kt:361)
        at org.jetbrains.kotlin.backend.common.serialization.IrDeclarationDeserializer.deserializeIrTypeParameter$lambda$8(IrDeclarationDeserializer.kt:286)
        at org.jetbrains.kotlin.ir.util.SymbolTable.declareScopedTypeParameter(SymbolTable.kt:421)
        at org.jetbrains.kotlin.backend.common.serialization.IrDeclarationDeserializer.deserializeIrTypeParameter(IrDeclarationDeserializer.kt:308)
        at org.jetbrains.kotlin.backend.common.serialization.IrDeclarationDeserializer.deserializeIrTypeParameter$default(IrDeclarationDeserializer.kt:279)
        at org.jetbrains.kotlin.backend.common.serialization.IrDeclarationDeserializer.deserializeTypeParameters(IrDeclarationDeserializer.kt:465)
        at org.jetbrains.kotlin.backend.common.serialization.IrDeclarationDeserializer.access$deserializeTypeParameters(IrDeclarationDeserializer.kt:68)
        at org.jetbrains.kotlin.backend.common.serialization.IrDeclarationDeserializer.deserializeIrFunction$ir_serialization_common(IrDeclarationDeserializer.kt:1204)
        at org.jetbrains.kotlin.backend.common.serialization.IrDeclarationDeserializer.deserializeDeclaration(IrDeclarationDeserializer.kt:821)
        at org.jetbrains.kotlin.backend.common.serialization.IrDeclarationDeserializer.deserializeDeclaration$default(IrDeclarationDeserializer.kt:815)
        at org.jetbrains.kotlin.backend.common.serialization.IrDeclarationDeserializer.deserializeIrClass(IrDeclarationDeserializer.kt:390)
        at org.jetbrains.kotlin.backend.common.serialization.IrDeclarationDeserializer.deserializeDeclaration(IrDeclarationDeserializer.kt:820)
        at org.jetbrains.kotlin.backend.common.serialization.IrDeclarationDeserializer.deserializeDeclaration$default(IrDeclarationDeserializer.kt:815)
        at org.jetbrains.kotlin.backend.common.serialization.IrFileDeserializer.deserializeDeclaration(IrFileDeserializer.kt:40)
        at org.jetbrains.kotlin.backend.common.serialization.FileDeserializationState.deserializeAllFileReachableTopLevel(IrFileDeserializer.kt:127)

https://github.com/JetBrains/kotlin-wrappers/commit/9efe9b6a1aceefb8b86114b02667660cb4009e35 でワークアラウンドがコミットされた。この変更がコミットされた 1.0.0-pre.759 がリリース済み。が、なんかまだ maven repository で見えない。

本質的にはコンパイラのバグなので、YouTrack に登録されている。すでに修正済みなので、次回のリリースで直るだろう。 https://youtrack.jetbrains.com/issue/KT-68943/JsPlainObject-breaks-when-interface-has-type-parameters

JJUG CCC 2024 Spring 参加レポート

はじめに

2024年の春、昨日開催されたJJUG CCC 2024 Spring に参加してきました!家庭の事情で午後からの参加となりましたが、Javaコミュニティの熱気を肌で感じながら、最新技術の情報や多くの開発者との交流を楽しむことができました。この記事では、キーノート、ブースでの会話、懇親会、そしてアンカンファレンスの様子をレポートします。

キーノートセッション

Java First. Java Always.

最初のキーノートは、「Java First. Java Always.」というタイトルで、OracleのJavaエキスパートたちが登壇しました。Javaの最新機能を紹介しながら、次世代アプリケーション開発の未来を描く内容でした。特に、オンプレミスからクラウドまで対応するJavaの強みが強調されており、開発者として非常に励みになりました。

FFMでJITコンパイラを作ってみた

次に聞いたのは、「FFMでJITコンパイラを作ってみた」というセッション。Java 22で正式に導入されたForeign Function & Memory API(FFM)を使って、JITコンパイラを実現するという興味深い内容でした。ffmasmライブラリを使って、Javaからネイティブコードを動的に生成・呼び出す方法が紹介され、FFMの新たな可能性を感じました。

ブース訪問

イベント中にFindy、Samuraism、Azulのブースを訪れ、それぞれの担当者とお話をしました。

懇親会

懇親会にも参加しました。LT大会が非常に盛り上がっており、参加者同士の活発な交流が見られました。変に食事があるよりも、乾き物しかない開場の方がかえって懇親しやすいと感じました。皆がリラックスして会話を楽しめる雰囲気がとても良かったです。

アンカンファレンス

アンカンファレンスにも参加しました。今回はフレームワークの話とAIの話が中心でした。

フレームワークの話

Spring Boot 3系への移行が大きなトピックでした。Jakarta EEへの移行が主な変更点であるため、実際のアップデート内容はそこまで大きくないとの意見も。QuarkusやMicroProfileも選択肢として挙がり、開発者たちの関心の高さが伺えました。

AIの話

AIを用いたコーディングについて話し合いました。特にGitHub Copilotのデモが盛り上がり、ほとんどの参加者が実際に使用していることが印象的でした。寺田さんが別のイベントの動画を流しながら、GitHub Copilotのデモについて詳しく解説してくれました。今後の進歩に大きな期待が寄せられています。

まとめ

JJUG CCC 2024 Springは、Javaコミュニティの最新情報や技術トレンドを学ぶ絶好の機会でした。キーノートやブースでの交流、懇親会、そしてアンカンファレンスを通じて、多くの刺激を受けました。これからもJavaの進化と共に、開発者として成長していきたいと思います。


参加者の皆さん、そして運営スタッフの皆さん、お疲れ様でした。また次回のイベントでお会いしましょう!

tauri v2 では SystemTray は TrayIcon という名前に変わっている。

https://v2.tauri.app/start/migrate/from-tauri-1/#migrate-to-tray-icon-module

v2 だと SystemTray は TrayIcon に変わっている。 マイグレーションガイドの通りにやれば、動きそう。

僕が neojot で tauri v2 beta を使い始めた頃には、ドキュメントが皆無だったが、最近は migration guide が充実しているので良い。

kotlin-power-assert-demo

kotlin 公式で power-assert が出てた。これは最高っぽい。 (まだできたてホヤホヤ Experimental です)

実際動かしてみて、その結果をレポジトリに置いておいた。 https://github.com/tokuhirom/kotlin-power-assert-demo

assertj/assertK などのアサーションライブラリは便利なのだのだけど、、

そこで、power-assert ですよ。

assertK でこう書いてたコードが

    val people = listOf(Person(name = "Sue"), Person(name = "Bob"))
    assertThat(people)
        .extracting(Person::name)
        .containsExactly("Sue", "Bob")

こう書いたら

    @Test
    fun testComplex() {
        val people = listOf(Person("Sue"), Person("Bob"))
        assert(people.map { it.name } == listOf("Susie", "Bob"))
    }

こうだ!

    Assertion failed
    assert(people.map { it.name } == listOf("Susie", "Bob"))
           |      |               |  |
           |      |               |  [Susie, Bob]
           |      |               false
           |      [Sue, Bob]
           [Person(name=Sue), Person(name=Bob)]

最高!

なお、multiplatform にも対応しているみたい。 とくに、kotlin/js とかでテスト書く時にはコレ使うと便利そうねぇ。 (kotlinx.test は便利だがマッチャの種類が少なめ。そこをこれでカバーできそう。 )

kotlinx.serialization のこと

kotlinx.serialization は、kotlin 用のシリアライゼーションライブラリである。 gradle のプラグインが提供されていて、@Serializable アノテーションがついているクラスに対するシリアライザを生成してくれる。 プラグインのソースコードは https://github.com/JetBrains/kotlin/tree/master/plugins/kotlinx-serialization にある。なにかコード生成ライブラリ使ってるのかと思ったら kotlin そのものの機能を使ってそう。

kotlinx.serialization のドキュメントは以下の場所にある。Polymorphism のような機能があって Jackson に出来ることは一通りできそう。 https://github.com/Kotlin/kotlinx.serialization/blob/4bf4113c2c6936e841caf2a8670382e701e7fcc2/docs/serialization-guide.md

たとえば以下のように使うのだが、、

package org.example

import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json

@Serializable
data class Greeting(val greeting: String)

fun main() {
    println("Hello World!")
    println(Json.encodeToString(Greeting("Hello, Kotlin/JS!")))
}

生成されるコードは以下のようになっている。Greeting$serializer.serialize が生成されてるので、実行時には高速に処理ができそうだ。 (kotlin のデコンパイルを IDEA がサポートしてほしいなぁと思いつつ幾星霜)

// IntelliJ API Decompiler stub source generated from a class file
// Implementation of methods is not available

package org.example

@kotlinx.serialization.Serializable public final data class Greeting public constructor(greeting: kotlin.String) {
    public companion object {
        public final fun serializer(): kotlinx.serialization.KSerializer<org.example.Greeting> { /* compiled code */ }
    }

    internal constructor(seen0: kotlin.Int, greeting: kotlin.String?, serializationConstructorMarker: kotlinx.serialization.internal.SerializationConstructorMarker?) { /* compiled code */ }

    public final val greeting: kotlin.String /* compiled code */

    public final operator fun component1(): kotlin.String { /* compiled code */ }

    public open operator fun equals(other: kotlin.Any?): kotlin.Boolean { /* compiled code */ }

    public open fun hashCode(): kotlin.Int { /* compiled code */ }

    public open fun toString(): kotlin.String { /* compiled code */ }

    @kotlin.jvm.JvmStatic internal final fun `write$Self`(self: org.example.Greeting, output: kotlinx.serialization.encoding.CompositeEncoder, serialDesc: kotlinx.serialization.descriptors.SerialDescriptor): kotlin.Unit { /* compiled code */ }

    @kotlin.Deprecated public object `$serializer` : kotlinx.serialization.internal.GeneratedSerializer<org.example.Greeting> {
        public final val descriptor: kotlinx.serialization.descriptors.SerialDescriptor /* compiled code */

        public final fun childSerializers(): kotlin.Array<kotlinx.serialization.KSerializer<*>> { /* compiled code */ }

        public final fun deserialize(decoder: kotlinx.serialization.encoding.Decoder): org.example.Greeting { /* compiled code */ }

        public final fun serialize(encoder: kotlinx.serialization.encoding.Encoder, value: org.example.Greeting): kotlin.Unit { /* compiled code */ }
    }
}

さて、実際速度的にはどうなのかな? というのが気になりますよね。 kotlinx.serialization のレポジトリ、ちゃんとベンチマークコードが入っているのに、結果をのせてないところが奥ゆかしいですね。 https://github.com/Kotlin/kotlinx.serialization/blob/4bf4113c2c6936e841caf2a8670382e701e7fcc2/benchmark/src/jmh/kotlin/kotlinx/benchmarks/json/JacksonComparisonBenchmark.kt

./gradlew jmh

とかすれば、結果が得られるので、手元で動かして結果を見てみると良さそう。

Benchmark                                               Mode  Cnt      Score      Error   Units
JacksonComparisonBenchmark.jacksonFromString           thrpt   10    731.704 ±   17.971  ops/ms
JacksonComparisonBenchmark.jacksonSmallToString        thrpt   10   9779.674 ± 3045.866  ops/ms
JacksonComparisonBenchmark.jacksonToString             thrpt   10   1811.471 ±   55.360  ops/ms
JacksonComparisonBenchmark.jacksonToStringWithEscapes  thrpt   10   1508.877 ±  122.538  ops/ms
JacksonComparisonBenchmark.kotlinFromStream            thrpt   10   1080.475 ±   82.937  ops/ms
JacksonComparisonBenchmark.kotlinFromString            thrpt   10   1375.436 ±   89.197  ops/ms
JacksonComparisonBenchmark.kotlinSmallToOkio           thrpt   10   8141.993 ±  111.842  ops/ms
JacksonComparisonBenchmark.kotlinSmallToStream         thrpt   10  10325.260 ± 2784.316  ops/ms
JacksonComparisonBenchmark.kotlinSmallToString         thrpt   10  10832.368 ±  306.662  ops/ms
JacksonComparisonBenchmark.kotlinToOkio                thrpt   10    898.711 ±   26.997  ops/ms
JacksonComparisonBenchmark.kotlinToStream              thrpt   10   1011.233 ±   62.126  ops/ms
JacksonComparisonBenchmark.kotlinToString              thrpt   10   1912.123 ±  192.133  ops/ms
JacksonComparisonBenchmark.kotlinToStringWithEscapes   thrpt   10   1336.079 ±  303.871  ops/ms

結果はこんな感じになる。 chatgpt で markdown に整形した結果が以下。そんなにすごい差があるわけでもない。

kotlinx.rpc 使ってみたよ

kotlinx.rpc が楽しそうなので触ってみたのでメモ。

https://github.com/Kotlin/kotlinx-rpc/tree/main/samples/ktor-web-app ここにあるサンプルがわかりやすくてすぐ動くので、これを触って

path とかゴチャゴチャ悩まずに kotlin の interface だけ定義すれば良いのは楽かも。 とはいえ、websocket ベースだとログとか悩まないといけないという面もあり、難しいところ。

gRPC サポートはいって安定したら、サーバー間通信に使うと良いかも。

rustmigemo と surrogate pair と

rustmigemo という、migemo を rust で実現できる便利な crate がある。 これを利用しようとしたところ、入力文字列として "h" とか "s" を入れたときに、panic することがわかった。 これは、h とか s とかで始まる文字列として、surrogate pair が含まれているからだということが、panic したところに printf debug していたらわかった。

surrogate pair で検索するような文字を、migemo で探したいことはほぼないと思うので、実際にどのような文字が含まれているかを調べてみることにした。

python で以下のようなスクリプトをさらっと書いて、辞書ファイルをスキャンしてみる。

import codecs
import sys

def is_surrogate_pair(char):
    """
    Checks if a character is in the surrogate-pair range.
    """
    return 0x010000 <= ord(char) <= 0x10FFFF

infile = sys.argv[1]
with codecs.open(infile, 'r') as file:
    for line in file:
        has_surrogate_pair = False
        for char in line:
            if is_surrogate_pair(char):
                print("U+%X" % ord(char))
                line = line.replace(char, f'**{char}**')
                has_surrogate_pair = True
        if has_surrogate_pair:
            print(line)

実行したところ、以下のような文字だということがわかった。

U+20B9F
しか    仕懸    仕掛    仕替    併      兪      叱      史家    呵      四角    始華    子夏    子華    市価    市花    師家    志嘉    志賀    歯科    死花    然
        知客    確      確然    私家    糸価    紙価    紙花    緊      而      聢      芝河    詆      詞花    詞華    詩家    詩歌    賜暇    蹙      顰      飾
        **𠮟**

U+20B9F
しっ    七      卓      叱      執      失      嫉      尻      悉      櫛      湿      漆      濕      疾      確      緊      質      **𠮟**

U+20B9F
しつ    七      仕付    仕詰    叱      喞      執      失      嫉      室      志都    悉      桎      湿      漆      濕      為付    為詰    瑟      疾      膝
        蟋      貭      質      躾      隰      隲      **𠮟**

U+27631
ふき    不帰    不羈    不覊    不記    不諱    不軌    付記    吹      富喜    富貴    布岐    腐気    苳      葺      蕗      袘      附記    **𧘱**

人生で一度も使おうと思ったことがない漢字なので、これはまぁ検索出来なくてもいいだろうという気分になった。

サロゲートペアを含む文字を除外する処理を辞書作成スクリプトから除外すればいいな、と思ったが、、 yet-another-migemo-dict ではビルドツールが全部含まれてるわけじゃなくて手である程度ビルドする感じになっていたので、めんどくさいからスクリプトを書いて、再生成可能なようにした。

https://github.com/oguna/yet-another-migemo-dict/pull/3

Codemirror6 で @codemirror/lang-legacy-modes と @codemirror/lang-markdown を組み合わせる

https://discuss.codemirror.net/t/codemirror-lang-markdown-with-codemirror-legacy-modes/7925

Codemirror6 では、一部のメジャーなプログラミング言語はサポートされているが、5時代にはサポートされていたのに6ではサポートされていないプログラミング言語も多い。 そういったプログラミング言語は @codemirror/lang-legacy-modes を使うことで、利用可能だ。 @codemirror/lang-legacy-modes のドキュメントを参照すれば、Codemirror6 でどのように使うかは書いてある。

しかしながら、Codemirror6 の lang-markdown に対してどのように使うかは書いてなかった。

そこで、フォーラムで質問したところ、@marjin 氏から即座にレスがついたので大変ありがたかった。

@codemirror/language-data というパッケージを使うと、必要なプログラミング言語の文法を動的に読んでくれるらしく、lang-legacy-modes の中身もすぐに使えるようだ。

最高!

tauri 2.0.0 beta1 に移行した

tauri 2.0.0 beta1 が出た。今後は大きい breaking changes はなさそうってことなので、移行してみる。 趣味で作ってるメモアプリを tauri 2.0.0 beta1 に移行した。

趣味だから、まぁ beta でもいいかな的なやつです。

npm install @tauri-apps/cli@next
npm run tauri migrate

ってやると、一通り自動マイグレーションされるので、それでだいたいいける。 が、微妙にぶっ壊れるので、新規で skelton 作って、それとの差分を見ながら調整していくみたいな作業はやっぱり必要。

fs まわり

[dependencies]
tauri-plugin-fs = "2.0.0-beta.0"

というふうに rust 側の Cargo.toml に依存追加する。

src-tauri/src/main.rs に以下のように書いて、プラグインを有効化する必要がある。

fn main() {
    tauri::Builder::default()
        .plugin(tauri_plugin_fs::init())
}

JS の API で、renameFile が rename になったり、removeFile が remove になったりと、メソッド名もそこそこ変わっている。 各APIの dir 指定が baseDir という名前に変わってる。rename の基底ディレクトリが newPathBaseDir と oldPathBaseDir に別れたの最高っぽい。

あとは、 credentails 周りが変わってる。 migration ツールが src-tauri/capabilities/migratred.json に権限リストを移行させてくれるが、権限の名前が変わってるので、そのままだと起動しなくなる。

例えば以下のようなエラーになる。

Permission fs:allow-read-file not found, expected one of app:default, app:allow-app-hide, ...

appdata なら appdata に読み書きする権限が、個別で用意されるようになってるんで、ここのへんのフォルダだけ触る場合は以下のように書いたらよろしい。

{
  "fs:allow-appdata-write-recursive",
  "fs:allow-appdata-read-recursive"
}

メニューが便利

Menu が rust ではなく JS 側でできるっぽいがドキュメントがまだなさそう。 情報が全く見当たらない。これはしばらく待ってから対応したいところ。

SQL が使える

sqlite が組み込みになっている。便利。

まとめ

わりと簡単に移行できる。

QMK firmware の開発メモ

QMKファームウェアを開発するには、どうするべきか

brew install qmk/qmk/qmk とすると、qmk がインストールできる。これは、めちゃくちゃ時間かかる 。数時間かかるので注意。

qmk のビルドは、ln してやる。make SKIP_GIT=true keyball/keyball44:tokuhirom とかする。

qmk flash ... とすると、書き込むことができる。キーボードのリセットボタンを押すと自動的に書き込みが開始される。最初は pro micro web writer で書いていたが、自分でファームウェア書くならCLIで書いた方がいい。

debug の方法

qmk console とすると、コンソールをモニタリングできる。

rules.mk に CONSOLE_ENABLE = yes に書く。

void keyboard_post_init_user(void) {
  // Customise these values to desired behaviour
  debug_enable=true;
  debug_matrix=true;
  //debug_keyboard=true;
  //debug_mouse=true;
}

のように書いて、デバッグ機能を有効化する。

bool process_record_user(uint16_t keycode, keyrecord_t *record) {
  // コンソールが有効化されている場合、マトリックス上の位置とキー押下状態を出力します
#ifdef CONSOLE_ENABLE
    uprintf("KL: kc: %u, col: %u, row: %u, pressed: %u\n", keycode, record->event.key.col, record->event.key.row, record->event.pressed);
#endif 
  return true;
}

のようにして、printf debug できる。

https://github.com/samhocevar-forks/qmk-firmware/blob/master/docs/ja/newbs_testing_debugging.md

QMK で増井さんの Dynamic Macro を実装してみた

https://github.com/tokuhirom/qmk-onemoretime

Keyboard のファームウェアとして、QMK が有名なわけだけど、その上で増井さんの Dynamic Macro を実装してみた。1ファイルのCライブラリとして実装してるので、自分のファームウェアに簡単に組み込めると思う。

DynamicMacro というのは、

Dynamic Macroの原理は非常に単純で、

    「同じ編集操作を2回繰り返したあとで[CTRL]+[t]を押すと繰り返された操作が再実行される」

というものです。「二度あることは三度ある」と言うように、同じことが二度あればもう一度あるのは世の中でごく普通のことです。二度実行した操作をもう一度実行することもよくあることですので、この方法はたいへん効果的です。

というようなやつ。

なんで作ったの?

なんかできそうなのでやってみただけです。 動いてみるとなんか楽しいです。

どう作ったの?

キーが押されたときに、リングバッファに押したキーを保存しておく。

任意のショートカットキーが押されたタイミングで、繰り返しコマンドになっている区間を検出して、処理する。

難しかったことは?

モディファイヤーキーの処理が難しい。キー送出している間はモディファイヤーキーを押してることにしちゃいけないので、状態を変えてもどしたりした。

改善点は?

メモリの要素をナイーブに舐めてるけど、ちゃんと memcmp とかにしたほうがエコかもしれない。が、ちょっとめんどくさいのでナイーブに実装した。

なんでこんな名前なの?

DynamicMacroという名前は QMK そのもので使われているから。Staticにハードコードされてない、キーボード側でマクロ記録開始!とかマクロ再生!!とかできるようなやつが DynamicMacro なのである。

もう一回実行するというところが本質かなと思うのでこういうネーミングとしてみた。

まとめ

気軽に組み込めると思うので、お気軽に試してみてね。

Jetpack Compose for Desktop で Caused by: java.lang.ClassNotFoundException: javax.naming.NamingException って言われるとき

アプリが完成したなぁ、とおもっていざ ./gradlew packageDmg したとき。

Caused by: java.lang.ClassNotFoundException: javax.naming.NamingException

というエラーがでて起動しなかったりする。

これは、javax.naming などのモジュールが参照できてないので、参照先を追加していくしかない。

compose.desktop {
    application {
        nativeDistributions {
            modules("java.naming")
            // alternatively: includeAllModules = true
        }
    }
}

のように追加していけば OK

ローラーマウスモバイルの COPY/PASTEをリマップする

[ローラーマウスモバイル]のCOPY/PASTEボタン、COPY/PASTE に使いたい感じは全くしないので、[Karabiner Elements]でリマップする。

~/.config/karabiner/assets/complex_modifications/roller_mouse.json に以下のように書く。

{
  "title": "ローラーマウスモバイル用設定",
  "rules": [
    {
      "description": "C-c を option-shift-↓にマッピング。C-v を option-shift-↑にマッピング (v4)",
      "manipulators": [
        {
          "type": "basic",
          "from": {
            "key_code": "c",
            "modifiers": {
              "mandatory": ["control"]
            }
          },
          "to": [
            {
              "key_code": "down_arrow",
              "modifiers": ["left_option", "left_shift"]
            }
          ],
          "conditions": [
            {
              "type": "device_if",
              "identifiers": [
                {
                  "vendor_id": 2867,
                  "product_id": 12288
                }
              ]
            }
          ]
        },
        {
          "type": "basic",
          "from": {
            "key_code": "v",
            "modifiers": {
              "mandatory": ["control"]
            }
          },
          "to": [
            {
              "key_code": "up_arrow",
              "modifiers": ["left_option", "left_shift"]
            }
          ],
          "conditions": [
            {
              "type": "device_if",
              "identifiers": [
                {
                  "vendor_id": 2867,
                  "product_id": 12288
                }
              ]
            }
          ]
        }
      ]
    }
  ]
}

これで、Slack の「未読のメッセージにジャンプする」が出来て便利。

2つのディレクトリの jar ファイルを比較するスクリプト

ChatGPT、こういうのの生成はやたら得意なので、ChatGPT で生成した。 こういう単純なスクリプトは手で書くより早いよねぇ。

import os
import sys
import subprocess
import difflib
import zipfile
import subprocess
import tempfile
from pathlib import Path

def get_git_commit_hash(directory):
    """Return the latest git commit hash of the specified directory."""
    try:
        commit_hash = subprocess.check_output(['git', '-C', directory, 'rev-parse', 'HEAD']).decode('utf-8').strip()
        return commit_hash
    except subprocess.CalledProcessError:
        print(f"Error: Failed to retrieve git commit hash for {directory}")
        sys.exit(1)

def get_jar_files(directory):
    """Return a list of jar files in the specified directory."""
    basedir = Path(directory)
    return [f.relative_to(basedir) for f in basedir.rglob("*.jar") if f.is_file()]

def are_jars_identical(jar1_path, jar2_path):
    """Check if the contents of the two JAR files are identical and display unified diff for .html files if they're different."""

    def is_binary(content):
        """Determine if the given content is binary."""
        return b'\x00' in content

    def get_javap_output(class_file_path):
        """Get the bytecode dump using javap."""
        try:
            output = subprocess.check_output(['javap', '-verbose', '-c', class_file_path], stderr=subprocess.STDOUT)
            return output.decode('utf-8')
        except subprocess.CalledProcessError as e:
            return str(e)

    with zipfile.ZipFile(jar1_path, 'r') as jar1, zipfile.ZipFile(jar2_path, 'r') as jar2:
        jar1_files = set(jar1.namelist())
        jar2_files = set(jar2.namelist())

        if jar1_files != jar2_files:
            return False, "File lists are different"

        for file_name in sorted(jar1_files):
            with jar1.open(file_name) as file1, jar2.open(file_name) as file2:
                file1_contents = file1.read()
                file2_contents = file2.read()

                if file1_contents != file2_contents:
                    # If the files are .html and not binary, display the unified diff
                    if file_name.endswith('.class'):
                        # If the files are .class files, get the javap output and compare
                        with tempfile.NamedTemporaryFile(suffix='.class', delete=True) as tmp1, tempfile.NamedTemporaryFile(suffix='.class', delete=True) as tmp2:
                            tmp1.write(file1_contents)
                            tmp2.write(file2_contents)
                            tmp1.flush()
                            tmp2.flush()

                            javap_output1 = get_javap_output(tmp1.name)
                            javap_output2 = get_javap_output(tmp2.name)

                            diff = difflib.unified_diff(
                                javap_output1.splitlines(),
                                javap_output2.splitlines(),
                                fromfile=f"{jar1_path}/{file_name}",
                                tofile=f"{jar2_path}/{file_name}"
                            )
                            return False, file_name + "\n" + "\n".join(diff)
                    elif file_name.endswith('.html') and not is_binary(file1_contents) and not is_binary(file2_contents):
                        diff = difflib.unified_diff(
                            file1_contents.decode().splitlines(),
                            file2_contents.decode().splitlines(),
                            fromfile=f"{jar1_path}/{file_name}",
                            tofile=f"{jar2_path}/{file_name}"
                        )
                        return False, "\n".join(diff)
                    return False, f"File contents are different: {file_name}"

    return True, ""

def main():
    if len(sys.argv) != 3:
        print("Usage: script_name directory1 directory2")
        sys.exit(1)

    dir1, dir2 = sys.argv[1], sys.argv[2]

    # Display git commit hashes for both directories
    commit_hash_dir1 = get_git_commit_hash(dir1)
    commit_hash_dir2 = get_git_commit_hash(dir2)

    print(f"Git commit hash for directory 1: {dir1} {commit_hash_dir1}")
    print(f"Git commit hash for directory 2: {dir2} {commit_hash_dir2}\n\n\n")

    jar_files_dir1 = set(get_jar_files(dir1))
    jar_files_dir2 = set(get_jar_files(dir2))

    common_jar_files = jar_files_dir1.intersection(jar_files_dir2)

    for jar_file in common_jar_files:
        if "-sources.jar" in str(jar_file):
            continue
        file1_path = os.path.join(dir1, jar_file)
        file2_path = os.path.join(dir2, jar_file)

        is_same, reason = are_jars_identical(file1_path, file2_path)
        if not is_same:
            print(f"## {jar_file} has different contents.")
            print(reason)
            print("\n\n")
        else:
            #print(f"{jar_file} has the same contents.")
            pass

    # Report files only in directory 1
    unique_files_dir1 = jar_files_dir1 - common_jar_files
    for jar_file in unique_files_dir1:
        print(f"{jar_file} exists only in directory 1.")

    # Report files only in directory 2
    unique_files_dir2 = jar_files_dir2 - common_jar_files
    for jar_file in unique_files_dir2:
        print(f"{jar_file} exists only in directory 2.")

if __name__ == "__main__":
    main()

自作ブログの構成更新

k8s を趣味でも使うか、、と思ってこのブログでも DigitalOcean の k8s を使うようにしてみたのだが、DigitalOcean への月々の支払いが $100/mo とかになってきて、さすがにたけーなと。円安だし。。

というわけで、もう少し安くすることを考えてみた。

DigitalOcean Apps というのがあったので、これを使うようにしてみる。これは AWS App Runner みたいなやつで、簡単に docker image をホスティングできるし、TLS 終端とかもいい感じにやってくれる。

といった感じのオペレーションをしたらとりあえず完了。

これでとりあえず $40/mo ぐらいにはなった。本当は、admin サイトと web サイトを統合して一つの app に入れるようにしたらもう少しコストを圧縮可能だが、、アプリのコードをそこそこいじらないと行けないと思うので、一旦ここまで。

kravis をやめて letsPlot に移行した

どちらも ggplot2 を意識したインターフェース。

というか kravis は ggplot2 の wrapper なので、ggplot2 の実装をそのまま使えて便利っちゃ便利。 一方で、ggplot2 をインストールしなきゃいけないので面倒なこともある。docker で動かすこともできるのだが、動作にちょっとコツが必要。

kravis は krangl と同じ作者なので、相性がよくて便利だったのだが、kotlinx-dataframe 時代になったのでその優位性は失われた。

letsPlot kotlin は kotlin で実装されてるのでインストールが楽。 昔は letsPlot は機能が少なすぎたが、最近は letsPlot は機能がだいぶ増えてきたので letsPlot に移行することにする。

    implementation("org.jetbrains.lets-plot:lets-plot-common:3.2.0")
    implementation("org.jetbrains.lets-plot:lets-plot-image-export:3.2.0")
    implementation("org.jetbrains.lets-plot:lets-plot-kotlin-jvm:4.4.1")

とかやる。

    private fun renderSummary(df: DataFrame<Any?>) {
        val p = letsPlot(df.toMap()) +
            geomBar(alpha = 0.3) { x = "year"; fill = "severity" } +
            facetWrap(facets = arrayOf("product")) +
            themeLight()

        ggsave(p, "./reports/summary.png", path = ".")
    }

みたいに実装する。ggsave() とか ggplot2 感がすごい。

LocalDate をそのまま入れておくと描画できないとか微妙に癖はあるが。。 datalore との相性も jetbrains がメンテしているだけあって良い気がするので、今後はこっちをつかっていきたい所存。

fsmonitor のソケットが原因で generateGitProperties がエラーになる

	> java.io.IOException: Cannot snapshot .git/fsmonitor--daemon.ipc: not a regular file

というエラーが generateGitProperties タスクで発生する。

rm -rf .git/**/fsmonitor-* をした上で、git config --global fsmonitor.socketDir "$HOME/.git-fsmonitor" としたほうが良いと思われる。

これでダメなら git config --global core.fsmonitor false か。