Blog

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 か。

magic research をクリアした

https://mcolotto.github.io/magic-research-demo/website/

Android 版をシコシコとやってた。いわゆる放置ゲー的なやつだけど、この手のやつでは一番おもしろかった。 トータルで 49日かかりました。

何が面白いかと言うとですね。。ゲームバランスが絶妙で、ボスが絶妙に倒せないのと、戦術を工夫しないと先に進めないようになっててめっちゃ楽しい。 転生するたびに見える景色が変わっていく感じがたまらないですな。

Marvel's Spider-Man: Miles Morales

https://www.playstation.com/ja-jp/games/marvels-spider-man-miles-morales/

PS5でクリア。 正当な拡張コンテンツという感じ。Spider man 2 も発売決定してるのでそれも楽しみですね。

freemarker の js_string の歴史

freemarker の js_string がどのような変遷をたどってきたのかをまとめてみた。

2.3

Date of release: 2004-June-15

https://freemarker.apache.org/docs/versions_2_3.html

New built-ins for Java and JavaScript string escaping: j_string and js_string

2.3.1

Date of release: 2005-01-04

https://freemarker.apache.org/docs/versions_2_3_1.html

The js_string built-in now escapes > as > (to avoid ).

2.3.20

Date of release: 2013-06-27

https://freemarker.apache.org/docs/versions_2_3_20.html

Bug fix [390] (and other improvements): ?js_string and ?json_string didn't escape the u2028-u2029 line terminators (problem for JavaScript) and the u007F-u009F control characters (maybe a problem in JSON, depending on implementation). Furthermore, the escaping of , <, and > become safer in that now they are escaped whenever it can't be guaranteed that they won't be part of <!, ]]> or </. Earlier they were only escaped when it was known that they are part of these patterns, thus it was possible to assemble these patterns from two adjacent interpolations. Additionally, from now on <? and --> also count as dangerous patterns, and will trigger < and > escaping.

まとめ

2.3.20 以後、2023年4月現在の最新版である 2.3.32 まで変更はなし。

Java 9 以後での InputStream を String にするやり方

commons-io で IOUtils.toString(inputStream, StandardCharsets.UTF_8) としているケースは、Java 9 以後なら new String(inputStream.readAllBytes(), StandardCharsets.UTF_8) と書けて便利。

https://www.baeldung.com/convert-input-stream-to-string

Rust で整数を漢数字に変換したいよー

こんな感じで実装できる。真面目にやればもうちょいちゃんと書けるけどこんな感じで。。

use std::cmp::min;

const NUMS: [&str; 10] = ["", "一", "二", "三", "四", "五", "六", "七", "八", "九"];
const SUBS: [&str; 4] = ["", "十", "百", "千"];
const PARTS: [&str; 18] = [
    "",
    "万",
    "億",
    "兆",
    "京",
    "垓",
    "𥝱",
    "穣",
    "溝",
    "澗",
    "正",
    "載",
    "極",
    "恒河沙",
    "阿僧祇",
    "那由他",
    "不可思議",
    "無量大数",
];

fn int2kanji(i: i64) -> String {
    let s = i.to_string();
    let chars = s.bytes();
    let p = chars.into_iter().rev().enumerate().collect::<Vec<_>>();
    let mut buf: Vec<&'static str> = Vec::new();
    for (i, b) in p.clone() {
        let c = (b - 48) as usize; // 48 is '0'
        if i % 4 == 0
            && i > 0
            && (i..min(i + 4, s.len()))
                .map(|i| {
                    let (_, c) = p.get(i).unwrap();
                    *c
                })
                .any(|n| n != 48)
        {
            buf.push(PARTS[i / 4]);
        }
        if c != 0 {
            // その桁が 0 のときは区切りを追加しない
            buf.push(SUBS[i % 4]);
        }
        if !(i % 4 != 0 && c == 1) {
            // 十百千を表示したときで、一のときは追加しない。
            buf.push(NUMS[c]); // 48 is '0'
        }
    }
    buf.reverse();
    buf.join("")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_int2kanji() {
        assert_eq!(int2kanji(1), "一");
        assert_eq!(int2kanji(9), "九");
        assert_eq!(int2kanji(10), "十");
        assert_eq!(int2kanji(11), "十一");
        assert_eq!(int2kanji(21), "二十一");
        assert_eq!(int2kanji(99), "九十九");
        assert_eq!(int2kanji(100), "百");
        assert_eq!(int2kanji(999), "九百九十九");
        assert_eq!(int2kanji(1000), "千");
        assert_eq!(int2kanji(9999), "九千九百九十九");
        assert_eq!(int2kanji(10000), "一万");
        assert_eq!(int2kanji(10020), "一万二十");
        assert_eq!(int2kanji(1_000_020), "百万二十");
        assert_eq!(int2kanji(100_000_020), "一億二十");
        assert_eq!(int2kanji(1_0000_4423), "一億四千四百二十三");
        assert_eq!(int2kanji(1_8000_4423), "一億八千万四千四百二十三");
    }
}

最近またLinux用の日本語IMEを作っている

本件は mozc の ut がどうこうとかは関係なくて、ふと linux desktop を使おうと昨年末に思いまして、昨年末からちまちまやってます

https://github.com/tokuhirom/akaza

かな漢字変換って作るの難しいのかなぁ、と思ったので作ってみている。これはまさに Just for Fun でやっている。 わりと普通に自分で常用してる分には困らないかな、というところまできている。

以下は、思ってることの垂れ流しという感じで、まとまってないですが。

「日本語入力を支える技術」という本が 2018年に出ていて、この本の内容を読めば、だいたいエンジン部分は実装できる。Amazon のレビューではこの本よんでも実装できないって書いてあるけど、変換エンジン自体は実装できます。 UI が辛い。けど。

エンジンは、ビタビアルゴリズムで最小コスト法を実装する、とかであればプログラミングできる人なら割とまじで誰でも実装できる。

ただ、どうしてもかな漢字変換エンジンは大量の細かいオブジェクトを取り扱う必要が出てくるので、python とかで実装すると結構遅い。 2年前に一回 pure python で実装したが、おそすぎたのでコア部分を C++ で書き直したという経緯がある。 Python で書いてると、色々なアルゴリズムためす上でも、Python が遅いからだめなのか、アルゴリズムがそもそも遅くて無理なのかがよくわからなくなりがちなんだよな。

UI を Python で書いて、ロジックを C++ で実装したのだがどうもインストールまわりが煩雑になるし、Rust 触ってみたかったので rust で全面的に書き直した。 Pure Rust にしたことによって、SEGV したりして謎に死ぬことがなくなったので快適になったし、高速に動いている。所有権とかがまぁめんどくさいとかはありつつ、わりと慣れれば大丈夫になる。

基本的な方針としては、mozc に近い感じの構成になっていて、最小コスト法でビタビアルゴリズムで統計的かな漢字変換である。 個人で開発しているから Google と違って大量のウェブコーパスを利用できるわけではないから、Wikipedia と青空文庫を形態素解析してそれを学習元として利用している。 mozc は Google の大量の資源を利用できるから変換精度がいいんでしょ、と思う人が多いと思うんだけど、それはまったくもってそのとおり。ただ、書き言葉という点においては、wikipedia ぐらいの規模の言語資源があれば、それを使えば割とそこそこの変換精度は出る。逆に話し言葉はめちゃくちゃ弱いので、話し言葉とかくだけた書き方のコーパスがもっとほしい。 利用できる日本語のコーパスとしては BCCWJ がほぼ唯一に近いもので、BCCWJ を使えばもっと変換精度は向上するんだけど、、個人で利用しようとすると 25万円だかかかるので二の足を踏んでいる。

識別モデルを使う というのも試してみた。今どきだと機械学習的なアプローチがナウいかなぁと思い。。だが、どうしても個人のマシンで動かしていると時間がかかってしょうがない。金をかけられない個人の趣味開発においては、統計的かな漢字変換のほうが向いていると思う。 開発を継続的にやるには無料の環境で使えるのが大事かなと思っている。モデルデータを github actions とかで生成できるのが理想的で、github actions で生成するならチープなスペックのインスタンスで 回せるぐらいのものが良い。github actions は 6 時間しか回せない。

基本的に今は単語 Bi-gram でコスト計算している。tri-gram に拡張するとプログラムが複雑になりすぎるし、有効なケースも限定的なのでまぁ。あまり bi-gram で困ってないし、という。mozc のようにクラスNグラムにしたほうがいいのかなぁ、と思いつつ、単語Nグラムで困ってないのでまぁいいかなぁといったところ。単語Nグラムだと品詞のこととか考えなくていいのがとにかく良い。 日本語の品詞難しいよ〜。個人的には、かな漢字変換の辞書登録で品詞を入れさせられるの、だるいなと前々から思ってたし。

https://komachi.hatenablog.com/entry/20081127/p2 http://www.fenix.ne.jp/~G-HAL/soft/nosettle/anthy.html#patch13

↑このへんとかを見ても思うこと。個人的には SKK の学習の具合を気に入っている。システムで提供するモデルはもちろん大事なのだが、個人の語彙などはたかが知れているので、システムで提供するモデルは「ある程度学習が進むまで耐えられる」ぐらいの精度があればよく、細かいところは個人の学習データで補ってくれや。っていうふうにするしかないのかな、と思っている。 だって個人だと Google みたいな規模のコーパス使えないんだもの。

ユーザーの学習は、unigram と bigram をテキストファイルで ~/.local/share/ とかに落とすようになっていて、自分が変換した結果の確率が学習データにあればそっちが優先されるようになっている。ので、誤変換で確定しちゃっても、正しい変換で何回か確定すればそれが優先されるようになる。このへんの学習ロジックはめちゃくちゃシンプル。基本的にユーザーの語彙は wikipedia 全体の語彙よりも少ないから、分母が小さくなるので、ユーザーの学習結果の確率計算のほうがキツく効くっていう仕組みにしている。

ちなみに、この手のかな漢字変換エンジンって、どういうふうに考えて実装したか、みたいな日記を書いてる人が多くてそういうの読むのって面白いなぁと思う。僕はそのへんの経緯を github issues とかに書くのがいいのかなぁと思ったので github issues とか discussions とか wiki に書いている。バザール的に開発できるような感じの構成にしているつもり、ではある。 お気軽になにかアイデアとかあれば書いていただければ嬉しいし、PR 送っていただければなお嬉しい。

https://github.com/tokuhirom/akaza/discussions

今月中ぐらいに一旦スナップショットリリースを出来たらいいなぁと思っている。

cargo test で fork させたい

https://crates.io/crates/rusty-fork

cargo test はプロセスを共有する。これではテストをしづらいケースがある。例えば C のライブラリのラッパーなどの場合、初期化関数を何回も呼んではいけないケースがある。

rusty-fork crate を利用すると、以下のように書くだけで別プロセスでテストを実行させることが可能である。

use rusty_fork::rusty_fork_test;

rusty_fork_test! {
    #[test]
    fn my_test() {
        assert_eq!(2, 1 + 1);
    }

    // more tests...
}

jumpapp を i3 に適用して一発でアプリにフォーカスをあてる on Linux

jumpapp というコマンドがある。これを使うと該当のウィンドウが起動していなければ起動し、起動していればフォアグラウンドにもってくることができる。

僕の場合は i3wm を使っているので、.config/i3/config に以下のように設定する。

bindsym $mod+Ctrl+Shift+o exec "jumpapp obsidian"
bindsym $mod+Ctrl+Shift+c exec "jumpapp -c google-chrome google-chrome-stable"
bindsym $mod+Ctrl+Shift+t exec "jumpapp moderndeck"
bindsym $mod+Ctrl+Shift+w exec "jumpapp wezterm"
bindsym $mod+Ctrl+Shift+f exec "jumpapp -c franz franz"

Ultimate hacking keyboard のバンパーを Ctrl+Shift+Super に割り当てて、2つのキーを押すだけでアプリを起動できるようになった。便利。

arch linux で使うのに最適な twitter クライアントを探す

最近色々ある twitter ですが、なんだかんだ twitter クライアントを起動しておきたいなと思っている今日この頃なので、、

最初は CawBird を使っていたが、vim keybinding が使えなかったりしてちょっと不満。

なので、気合で自分にあうものを探してみようかなと。yay -Ss twitter として twitter 関連のものを探し、その中から GUI クライアントを絞り込むものとする。このキーワード検索では、どうしてもライブラリや CLI ツールなどが混ざってくるので、目で確認していく。

aur/birdie-git 1.1.r12.g20b3c83-1 (+0 0.00) (Orphaned)
    Twitter client for Linux
aur/v2cmod-z 2.19.1-8 (+0 0.00)
    A 2ch/5ch and Twitter browser
aur/moderndeck-bin 9.4.5-1 (+1 0.24)
    A beautiful, powerful Twitter client for desktop.
aur/mikutter-git 3.4.2.r0.gdf0a70c-1 (+2 0.00)
    a moest twitter client (Upstream version)
aur/alice-git latest-1 (+2 0.00)
    A minimal Twitter client based on GTK+3.
aur/nocturn 1.8.4-2 (+6 0.00)
    Desktop twitter client
aur/cawbird-git 1.5.r0.g0a60a0bc38-1 (+7 0.01)
    A fork of the Corebird GTK Twitter client that continues to work with Twitter
aur/choqok-git v1.6.0.r106.gd8a53169-1 (+35 0.00)
    Microblogging client for KDE with support for Pump.io, GNU social and Twitter.com - git version
aur/mikutter 5.0.4-1 (+39 0.00)
    a moest twitter client
community/cawbird 1.5-2 (644.4 KiB 2.0 MiB) (Installed)
    Native GTK Twitter client

cawbird

cawbird が微妙なのは、hjkl 移動できないという点とフォントがなんか汚い。 フォントは設定すればなんとかなりそうだが。。

cawbird で issue たっているが、あんまメンテナが乗り気じゃなさそうだった>ショートカット

birdie

インストール不能

nocturn

すでに試したのだが起動しなかった。

[1022523:1222/153244.185725:FATAL:gpu_data_manager_impl_private.cc(439)] GPU process isn't usable. Goodbye.
zsh: trace trap (core dumped)  nocturn

と言われてしまう。

これは nocturn --no-sandbox すれば起動はできる。

が、結局なんかしらんけどログインができない。

mikutter

twitter client ではなく mastdon クライアントになっていた

v2cmod-z

2ch クライアントがメインっぽい。。

alice-git

なんか github レポジトリが消えてる

choqok

動く。動くのがすごい、みたいになってるのがちょっとあれだけど。。 choqok は一番まともかな。ただ、twitter 特化じゃないのでなんか変。 fav するためのショートカットがなかったり、vim like keybindings がなかったりする。

これだったら cawbird でいいかな。

moderndeck

これが、基本的には twitterdeck なので体験が良い。

ということで、moderndeck を使うことにした。 Use os native title bar を appearance 設定で利用することで、ちゃんと i3 で枠がつくようになる。

というわけで、しばらくは moderndeck を使おうかなと思う。