Slint で書いたログイン画面を bash から表示する

# はじめに

この記事は [Slint Advent Calendar 2024](https://qiita.com/advent-calendar/2024/slint) 22日目の記事です。

昨日は @hermit4 さんによる [Slintのバックエンドとレンダラー](https://qiita.com/hermit4/items/262b316c5369db879d87) でした。

公式ドキュメントの [Backends & Renderers](https://docs.slint.dev/latest/docs/slint/guide/backends-and-renderers/backends_and_renderers/) の内容が日本語でよくまとまっていてとてもありがたいです。

# Slint をシェル(bash 等)から動かそう

Slint ではフロントエンドを .slint ファイルに記述し、Rust や C++、JavaScript、Python で書かれたバックエンドから利用するのが一般的なやり方です。

Slint には [Slint Viewer](https://github.com/slint-ui/slint/tree/master/tools/viewer) と呼ばれる .slint ファイルのビューアーが用意されていて、それを利用することで、簡単なものであれば（もしくはすごく頑張れば）シェルスクリプトでも GUI アプリケーションの作成が可能です。

公式のサンプルアプリにも [Slint Bash example](https://github.com/slint-ui/slint/tree/master/examples/bash) というものがあり、今回はこれをアレンジして遊んでいます。


## Slint Viewer の仕様

Slit Viewer は Rust のパッケージマネージャーを利用して、以下のコマンドでインストールが可能です。

```console
$ 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` で行うことができます。

```qml: main.slint
export component MyApp inherits Window {
  callback open-url(string);
  //...
}
```
のようなコールバックが呼ばれた際の処理は以下のように行うことが可能です。
```console
$ slint-viewer --on open-url 'xdg-open $1' main.slint
```

表示するエレメントが [Dialog](https://docs.slint.dev/latest/docs/slint/reference/window/dialog/) を継承している場合には、利用する [StandardButton](https://docs.slint.dev/latest/docs/slint/reference/std-widgets/basic-widgets/standardbutton/) によって `slint-viewer` の終了コードが以下のように変わります。

- `ok` や `yes`、`close` の場合は正常終了(=0)
- `cancel` や `no` の場合は異常終了(=1)


## .slint で ログイン画面を作る

ユーザー名とパスワードの入力ダイアログを以下のように作りました。

```qml:login.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](https://slintpad.com/?snippet=import+%7B+LineEdit%2C+VerticalBox%2C+GridBox%2C+StandardButton+%7D+from+%22std-widgets.slint%22%3B%0A%0Aexport+component+LoginForm+inherits+Dialog+%7B%0A++++title%3A+%22Login%22%3B%0A++++preferred-width%3A+300px%3B%0A++++forward-focus%3A+username%3B%0A++++GridBox+%7B%0A++++++++Row+%7B%0A++++++++++++Text+%7B+text%3A+%40tr%28%22Username%3A%22%29%3B+%7D%0A++++++++++++username+%3A%3D+LineEdit+%7B%7D%0A++++++++%7D%0A++++++++Row+%7B%0A++++++++++++Text+%7B+text%3A+%40tr%28%22Password%3A%22%29%3B+%7D%0A++++++++++++password+%3A%3D+LineEdit+%7B+input-type%3A+InputType.password%3B+%7D%0A++++++++%7D%0A++++%7D%0A++++StandardButton+%7B+kind%3A+ok%3B+enabled%3A+username.text+%21%3D+%22%22+%26%26+password.text+%21%3D+%22%22%3B+%7D%0A++++StandardButton+%7B+kind%3A+cancel%3B+%7D%0A%7D&style=native) で動作確認することが可能です。


## シェルスクリプトを作成する

上記の `login.slint` と同じディレクトリに以下の `login.sh` を作成します。

```bash:login.sh
#!/bin/sh

slint-viewer `dirname $0`/login.slint
```

実行してみましょう。

```console
$ chmod +x login.sh
$ ./login.sh
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13867/54df4f69-b839-de9e-5ca4-1413a6787afb.png)

ちゃんと画面が表示されましたね。

## 入力結果の取得

それでは次に、入力された値を取得してみましょう。

```bash: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 側に出力するプロパティを作成しましょう。

```qml:login.slint
export component LoginForm inherits Dialog {
    ...
    out property<string> username <=> username.text;
    out property<string> password <=> password.text;
    ...
}
```

プロパティの宣言に、出力用の `out` をつけています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13867/bf0c1b37-9f46-c24a-6210-2ee6bb8df90d.png)

```console
$ ./login.sh
{ "password": "p@ssword", "username": "user" }
```

コマンドラインの JSON のパーサー [jq](https://jqlang.github.io/jq/) を利用して、スクリプトの変数に代入してみましょう。

```bash:login.sh
#!/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"
```

```console
$ ./login.sh
UserName: user
Password: p@ssword
```

## 初期値の設定

毎回ユーザー名を入力するのが面倒くさいので、`whoami` の結果をデフォルトとして設定しましょう。

```bash:login.sh
#!/bin/sh

USERNAME=$(whoami)
JSON=$(slint-viewer --load-data - --save-data - `dirname $0`/login.slint <<EOF
{
    "username": "$USERNAME",
    "password": ""
}
EOF
)

...
```

.slint 側も、入力を受け付けるように変更しましょう。

```qml:login.slint
export component LoginForm inherits Dialog {
    ...
    in-out property<string> username <=> username.text;
    in-out property<string> password <=> password.text;
    ...
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13867/74e71151-fe8b-96ef-23c6-32c414865802.png)


# おわりに

今回は Slint のバックエンドのロジックをシェルスクリプトで書いて遊んでみました。

簡単なものなら簡単に作れてとても便利な気がします。

明日は @rarirure さんによる [RustとSlintで作る動的棒グラフ・ゲージチャート](https://qiita.com/rarirure/items/f1559f5d2ec3ccd98b04) です。お楽しみに！


# 余談

今回使用した .slint のコードでは、ユーザー名とパスワードの入力状態に応じて `OK` ボタンの有効無効を切り替えています。

```qml
    StandardButton { kind: ok; enabled: username.text != "" && password.text != ""; }
```

`.is_empty()` と書けたらよかったんですが、そういう機能は無かったので、空文字列との比較で実現しています。

また、ユーザー名もパスワードも通常は文字数の制限（とくに下限）があるので、文字数に応じた処理を行いたかったのですが、`len()` や `length()` のような機能もなく、今回は見送りました。

そういうのが用意されていない理由があるのか、ちょっと調べたけど分からなかったので、（バグレポ書いたり議論したりをすっとばしていますが）以下のプルリクエストを作成してみました。

https://github.com/slint-ui/slint/pull/7192

メインの開発者は大体ドイツに住んでいるようで、もうクリスマス休暇で仕事はしてないっぽいので、年が明けたらレビューしてもらって、できれば本体に取り込んでもらいたいなと思っています。