はじめに
この記事は Slint Advent Calendar 2024 22日目の記事です。
昨日は @hermit4 さんによる Slintのバックエンドとレンダラー でした。
公式ドキュメントの Backends & Renderers の内容が日本語でよくまとまっていてとてもありがたいです。
Slint をシェル(bash 等)から動かそう
Slint ではフロントエンドを .slint ファイルに記述し、Rust や C++、JavaScript、Python で書かれたバックエンドから利用するのが一般的なやり方です。
Slint には Slint Viewer と呼ばれる .slint ファイルのビューアーが用意されていて、それを利用することで、簡単なものであれば(もしくはすごく頑張れば)シェルスクリプトでも GUI アプリケーションの作成が可能です。
公式のサンプルアプリにも Slint Bash example というものがあり、今回はこれをアレンジして遊んでいます。
Slint Viewer の仕様
Slit Viewer は Rust のパッケージマネージャーを利用して、以下のコマンドでインストールが可能です。
$ cargo install slint-viewer
以下の引数が利用可能です。
-
--auto-reload
: Automatically watch the file system, and reload when it changes -
--save-data <file>
: When exiting, write the value of public properties to a json file. Only property whose types can be serialized to json will be written. This option is incompatible with --auto-reload -
--load-data <file>
: Load the values of public properties from a json file. -
-I <path>
: Add an include path to look for imported .slint files or images. -
-L <library=path>
: Add a library path to look for @library imports. -
--style <style>
: Set the style. Defaults to native if the Qt backend is compiled, otherwise fluent -
--backend <backend>
: Override the Slint rendering backend -
--on <callback> <handler>
: Set a callback handler, see callback handler -
--component <name>
: Load the component with the given name. If not specified, load the last exported component
シェルスクリプトで GUI を表示したい場合の入出力は --load-data
と --save-data
で行うことができます。
export component MyApp inherits Window {
callback open-url(string);
//...
}
のようなコールバックが呼ばれた際の処理は以下のように行うことが可能です。
$ slint-viewer --on open-url 'xdg-open $1' main.slint
表示するエレメントが Dialog を継承している場合には、利用する StandardButton によって slint-viewer
の終了コードが以下のように変わります。
-
ok
やyes
、close
の場合は正常終了(=0) -
cancel
やno
の場合は異常終了(=1)
.slint で ログイン画面を作る
ユーザー名とパスワードの入力ダイアログを以下のように作りました。
import { LineEdit, VerticalBox, GridBox, StandardButton } from "std-widgets.slint";
export component LoginForm inherits Dialog {
title: "Login";
preferred-width: 300px;
forward-focus: username;
GridBox {
Row {
Text { text: @tr("Username:"); }
username := LineEdit {}
}
Row {
Text { text: @tr("Password:"); }
password := LineEdit { input-type: InputType.password; }
}
}
StandardButton { kind: ok; enabled: username.text != "" && password.text != ""; }
StandardButton { kind: cancel; }
}
SlintPad で動作確認することが可能です。
シェルスクリプトを作成する
上記の login.slint
と同じディレクトリに以下の login.sh
を作成します。
#!/bin/sh
slint-viewer `dirname $0`/login.slint
実行してみましょう。
$ chmod +x login.sh
$ ./login.sh
ちゃんと画面が表示されましたね。
入力結果の取得
それでは次に、入力された値を取得してみましょう。
#!/bin/sh
JSON=$(slint-viewer --save-data - `dirname $0`/login.slint)
if [ $? -ne 0 ]; then
echo "Failed to login"
exit 1
fi
echo $JSON
--save-data
オプションに -
を渡すことで、.slint からロードされた
コンポーネントのルート要素のパブリックなアウトプット対応のプロパティの情報が JSON 形式で標準出力に出力されます。
.slint 側に出力するプロパティを作成しましょう。
export component LoginForm inherits Dialog {
...
out property<string> username <=> username.text;
out property<string> password <=> password.text;
...
}
プロパティの宣言に、出力用の out
をつけています。
$ ./login.sh
{ "password": "p@ssword", "username": "user" }
コマンドラインの JSON のパーサー jq を利用して、スクリプトの変数に代入してみましょう。
#!/bin/sh
JSON=$(slint-viewer --save-data - `dirname $0`/login.slint)
if [ $? -ne 0 ]; then
echo "Failed to login"
exit 1
fi
USERNAME=$(jq -r ".username" <<< "$JSON")
PASSWORD=$(jq -r ".password" <<< "$JSON")
echo "UserName: $USERNAME"
echo "Password: $PASSWORD"
$ ./login.sh
UserName: user
Password: p@ssword
初期値の設定
毎回ユーザー名を入力するのが面倒くさいので、whoami
の結果をデフォルトとして設定しましょう。
#!/bin/sh
USERNAME=$(whoami)
JSON=$(slint-viewer --load-data - --save-data - `dirname $0`/login.slint <<EOF
{
"username": "$USERNAME",
"password": ""
}
EOF
)
...
.slint 側も、入力を受け付けるように変更しましょう。
export component LoginForm inherits Dialog {
...
in-out property<string> username <=> username.text;
in-out property<string> password <=> password.text;
...
}
おわりに
今回は Slint のバックエンドのロジックをシェルスクリプトで書いて遊んでみました。
簡単なものなら簡単に作れてとても便利な気がします。
明日は @rarirure さんによる RustとSlintで作る動的棒グラフ・ゲージチャート です。お楽しみに!
余談
今回使用した .slint のコードでは、ユーザー名とパスワードの入力状態に応じて OK
ボタンの有効無効を切り替えています。
StandardButton { kind: ok; enabled: username.text != "" && password.text != ""; }
.is_empty()
と書けたらよかったんですが、そういう機能は無かったので、空文字列との比較で実現しています。
また、ユーザー名もパスワードも通常は文字数の制限(とくに下限)があるので、文字数に応じた処理を行いたかったのですが、len()
や length()
のような機能もなく、今回は見送りました。
そういうのが用意されていない理由があるのか、ちょっと調べたけど分からなかったので、(バグレポ書いたり議論したりをすっとばしていますが)以下のプルリクエストを作成してみました。
メインの開発者は大体ドイツに住んでいるようで、もうクリスマス休暇で仕事はしてないっぽいので、年が明けたらレビューしてもらって、できれば本体に取り込んでもらいたいなと思っています。