Rust製 GUI IRCクライアント自作

Posted April 6, 2022 by Yuri ‐ 2 min read

美味しいジェラートのつもりで作ったけれど、気づいたら放置。

自分の数々の駄作プロジェクトについて語りたいシリーズを始めようと思う。 自分の駄作を話すのは恥ずかしいのだけれど、自分がどこが分かっていないかがハッキリすると期待して。

駄作プロジェクト紹介第一弾は!

Rust製 GUI IRC(Internet Relay Chat Protocol)クライアント自作です。

わーぱちぱち。

コードはここ👉gelato🍨

A GUI IRC Client. Be Pop and Cool!

とまぁ、実装する前に書いたので飛ばしたセリフが書いてありますが、現時点の実装状況は別にクールでも何でもありません。

Rustで何か作りたい思いついちゃったGUI IRCクライアントアイデアを実現したい気持ち

が先行して、Rustの文法を勉強する前に書き始めちゃったので、実装の一部分が謎な状態。 現在は文法を勉強する前に書き始めたことを反省しており、本やドキュメントなどのを通しで見てから、再実装に取り掛かろうと思ったのも束の間、 お買い物にゲームにファッションに電子工作に自作OSに時間を取られ、放置中〜
でも皆に紹介ぐらいしてもいいのかもしれない。謎実装は通信の部分なので面白いかもしれないし…

初期実装衝動

  • 「え"っ!IRC:rfc1459ってTCPソケットに"文字列<スペース>文字列<スペース>文字列"を流し込んでいるだけなの!?HTTPより簡単そう。記事感謝
  • limechatさん今も愛されているのいいなー!でもLast Commitが2018年で止まっている模様?(※開発開始2020.03当時。現在確認したところ、2020.07から再開している様子…?)」
  • 「Rust製クロスコンパイル環境対応GUIアプリケーションライブラリicedさん、かっこよ…好き…(最新リリース時間経っているから少し心配🥺)」
  • 入社自己紹介でうっかりRustに興味あるって言ってしまったせいで、「まだRust入門しないんですかって聞かれたらどうしよう😅」
  • 「何この職場IRCに重要な情報が流れていくんですが…(現在は廃止)メンションはいらないけど今のセンパイの発言キープしたい!いろいろな機能を実装したい」
  • 「こうちょちょっと複数パネルとかサクッと作れるといいよね。やっぱり私はweechatさん激推しだわ。自作プラグインでカスタマイズも出来るし…でもCUIなせいでコピペが難しい…」
  • 「検索も出来ると嬉しいし、ミュートとか、あとカラバリ豊富なテーマがあるといいかもしれない」
  • 「icedライブラリを使った、カラフルテーマで、キープボタンがあって、プラグインなんかも作れたらいい感じ…」
  • 「そうだ!gelatoって名前にすれば、flavorとかchipとかtoppingみたいなtermが使えていい感じですわ!作るしかありませんわ!!!」

仕事が忙しければ忙しいほど、頭がアイデアを出してくる体質なんとかならないんですかね。

基本方針

  • GUIはiced 0.0.3のexampleのtodo,download_progress,pane_grid,scrollableを参考に作成。
  • IRC部分はTCPソケット自分で書くか迷ったけれど、大量のIRCのコマンド仕分け(LOGINとかPINGとかJOINとか)を自分で実装する手間を惜しんだので、既存のRust IRC 0.14ライブラリを使用して、もし気が向いたら自分で書くように置き換えようかなと思っていました。ただ、今から考えると、このIRCライブラリとicedの合体で苦労して怪しい実装 悪魔的融合👿 になっているので、TCPソケット通信部分を自分で書いてしまった方が楽かもしれない。開発再開するとしたら、機能つけたい気持ちをグッと抑えて、ブランチ切ってTCPソケット書きから始めるかな…

現在の進捗

  • 上の展望ほとんど達成していない。一応メッセージが送れるので、辛うじてIRCクライアントの体を保っている程度の能力。
  • ホスト・ポート・ユーザ名・入りたい複数のルーム名をsetting.jsonに書いておくと、一応メッセージが送れます。
  • パネルをボタンクリックで増やして、クリックしたパネルがアクティブモードになって、アクティブモードになっているルームに対してメッセージが送れます。
  • ネット上のIRCのホストに向けてメッセージを送れるので、そこは大変楽しいです。
  • 日本語も送れるけど、日本語書き込み中の推測文字を表示するIMEが出てこない。これは、RustのGUI系ライブラリ側の問題で日本のRustceanの皆様も取り組まれている模様。Tracking issue for IME / composition support #1497
  • PINGとかプライベートメッセージなどの具体的なコマンドで分岐して機能を実行する実装はまだやっていません。単に文字列を送り受け取るのみの状態。
  • GUI操作楽しいー!けれど、これはicedさんのおかげで私のおかげではない。
  • 色合いはポップでクールじゃん?(※じゃんは神奈川方言らしい)

技術的なsomething…

Client Stream

icedはdocにあるように、ElmアーキテクチャにインスパイアされたGUIアプリケーションライブラリであり下記の4つを意識する。

  • State — the state of your application
  • Messages — user interactions or meaningful events that you care about
  • View logic — a way to display your state as widgets that may produce messages on user interaction
  • Update logic — a way to react to messages and update your state

Stateというアプリケーションで必要な全ての情報の状態を管理する構造体があって、例えば画面クリックされるとMessageが飛ぶように設定しておくと、MessageをUpdate関数が受け取って、Stateが更新され、ViewはそのStateに対応した表示をしているというスタイル。

外部のイベントをリッスンする機能として、subscriptionが提供されている。 外部の非同期処理を実行しつつ、細かくsubscription用のメッセージを飛ばしてくれる…イメージでいる。(並列化されているかどうか実装確認したいところ)

実装する上で参考にしたのは、iced/example/download_progress

このサンプルは、RustのポピュラーなHTTPクライアントreqwestでファイルをダウンロードしつつ、ダウンロードが何%ぐらいなのかを表示する。 もし、subscriptionでのステートがダウンロード中モードで、chunkにデータが残っていれば、パーセンテージを計算して、第一引数にメッセージとして送りたいものを書き、第二引数に更新後のsubscriptionステートをダウンロード中モードで書いておくと、またこの処理をループすることができ、chunkが空なら終了のメッセージを送って、修了処理をする…と解釈した。(多分)

State::Downloading {
    mut response,
    total,
    downloaded,
} => match response.chunk().await {
    Ok(Some(chunk)) => {
        let downloaded = downloaded + chunk.len() as u64;

        let percentage = (downloaded as f32 / total as f32) * 100.0;

        (
        Some((id, Progress::Advanced(percentage))),
        State::Downloading {
        response,
        total,
        downloaded,},
        )
    }
    Ok(None) => (Some((id, Progress::Finished)), State::Finished),
    Err(_) => (Some((id, Progress::Errored)), State::Finished),
},

つまり、chunkをどうにかして、Rust IRCライブラリで使われているirc::client::ClientStreamから捻り出せたらいいのかな〜と雑な考えを始め、知恵の輪ガチャガチャしていたら、取れるようになりました…というのがこちらの実装。辛い。改善できそう。

SubscribeIrcState::Incoming {
    client_stream,
    mut message_text,
} => {
    let cloneclient = client_stream.clone();
    match (&mut cloneclient.expect("cloneclient error").lock().await)
        .next()
        .await
        .transpose()
    {
        Ok(Some(chunk)) => {
            message_text = chunk.to_string();
            Some((
                Progress::Advanced(message_text.clone()),
                SubscribeIrcState::Incoming {
                    client_stream,
                    message_text,
                },
            ))
        }
        Ok(None) => Some((Progress::Finished, SubscribeIrcState::Finished)),
        Err(_) => Some((Progress::Errored, SubscribeIrcState::Finished)),
    }
}

一点言い訳があって、使わせてもらっているライブラリのirc::client::ClientStreamがClone実装が無くて苦しかった。

当時、「通信部分の実装で悩んでいる」と友人に相談したところ、select, epollでファイルディスクリプタをモニタリングする概念を教えてもらったので、「selectの気持ち!」と叫びながらその概念を頭に浮かべて何とかしようとしていました。やるべきなのはもっと別なことな気がしているけれど、こういう知識の広がりが好きです。

とはいえ、組み合わせるのであれば組み合わせるなりの設計のやり方や、理解の解像度がまだ足りていない感覚があります。自分のここを何とかしたいです。 あと、subscription周辺にsome_inputという謎の変数が残してしまっている…

Clientの保持の仕方

アプリ全体の話に移ります。 アプリの全体の状態はenumのAppで管理しています。 GUI起動時に設定ファイルを読み込んでいる時のLoading、読み終わった後の設定画面中のLoaded、メイン画面表示させながらIRC接続Subscriptionを開始させるIrcConnecting、IRC接続をやめた時のIrcFinished、だったような。

pub enum App {
    Loading,
    Loaded(State),
    IrcConnecting(State),
    IrcFinished(State),
}

設計誰かに相談したいなー

  • 設定画面読み込み中->設定読み込み終了
  • IRCクライアント接続中->IRCクライアント接続終了
  • メインの画面表示中

この3つの状態変化が混ざった状態をどう分けるのがベストか、整理できていません。 本当はlimechatみたいに起動したらいきなりメインの画面を表示させていて、設定ファイルの読み書きは任意のタイミングで発動できると嬉しい…泣

pub struct State {
    pub irc_config: irc::client::prelude::Config,
    pub setting: SettingState,
    pub main: MainContents,
    pub pane: PaneState,
    sender: Option<Arc<Mutex<irc::client::Sender>>>,
    client_stream: Option<Arc<Mutex<irc::client::ClientStream>>>,
    pub error: Option<String>,
}

senderとclient_streamを2つ保持しているのはどう考えても無駄なのですが、所有権パズルに苦しんだ末の状態です。所有権を理解してここを改善したいです。

Option<Arc<Mutexirc::client::ClientStream»に関しては、Slackのrust-jpコミュニティのtatsuya6502さんに助けていただきました。ありがとうございます。

やっぱり苦しい思いをしたくなければ、高まる開発意欲を抑えて、Rustのドキュメントを熟読してから、開発開始すると良いと思います。

あと便利そうなRust Memory Container Cheat-sheetも教えていただきました。Rustライフの助けになりそう〜〜

まぁ大体この辺りで疲れて、「もっと簡単なRustのプログラムに書き慣れた方がイイのでは?」と思いました。(自明)

万が一このコードとかicedに興味を持った人が居たら、チャットとかでお話ししましょう〜🌟