Elm×Electronでシンプルなタイマーを作る

# はじめに
これはElm/Electron初学者が シンプルなタイマーをElectronで構築するまでの話です。
#### [完成品](https://github.com/care0717/pomotodolo/tree/62f5eee92e2bd356aed10e5bbe2e9b49375670cc)（まだ途中ですが）
![sample.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216927/519d68fe-1516-890c-8dd2-be22b2d1b2f5.gif)

# 構成
今回はwebpackでElmをコンパイルして一つの`bundle.js`にまとめ、それをElectronのベースとなる[index.html](https://github.com/care0717/pomotodolo/blob/62f5eee92e2bd356aed10e5bbe2e9b49375670cc/index.html)から読み出すという構成を取っています。なので`webpack.config.js`は

```javascript:webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = (env, argv) => {
    return {
        entry: `${__dirname}/src/index.js`,
        output: {
            path: `${__dirname}/dist`,
            filename: 'bundle.js',
            libraryTarget: 'window',
        },
        module: {
            rules: [
                {
                    test: /\.(css|scss)$/,
                    loader: ['style-loader', 'css-loader', 'sass-loader'],
                },
                {
                    test:    /\.elm$/,
                    loader: 'elm-webpack-loader',
                    options: {
                        debug: (argv.mode !== 'production')
                    }
                }
            ],
        },
        devServer: {
            port: '8080',
            compress: true,
            watchContentBase: true,
        }
    };
};
```
という感じになります。[参考](https://github.com/ababup1192/elm-dev-env/blob/master/webpack.config.js)
またElectronのエントリーポイントとなる`index.js`は

```javascript:index.js
"use strict";

const { app, BrowserWindow } = require("electron");

app.on('window-all-closed', function() {
    if (process.platform != 'darwin') {
        app.quit();
    }
});

app.on('ready', function() {
    let mainWindow = new BrowserWindow({width: 240, height: 240});
    // 起動時にベースとなるhtml
    mainWindow.loadURL('file://' + __dirname + '/index.html');

    mainWindow.on('closed', function() {
        mainWindow = null;
    });
});
```
としています。また、デザインのために[Bulma](https://bulma.io/)とFASを導入しています。

# 実装
### src/Main.elm
ほぼすべての実装を[Main.elm](https://github.com/care0717/pomotodolo/tree/62f5eee92e2bd356aed10e5bbe2e9b49375670cc/src/Main.elm)に書いています。ぶっちゃけ実装は[この記事](https://qiita.com/smith-30/items/0b0df1197701fd6c4398)とほどんど同じですが、~~独自性を出すために~~タイマーが終了する際に[通知](https://developer.mozilla.org/ja/docs/Web/API/notification)を出すようにしています。 以下では上から部分的に紹介していきます。

```elm:src/Main.elm
port notifyUser : () -> Cmd msg

port notified : (Bool -> msg) -> Sub msg
```
いきなり結構重要なところですが、[Notification](https://developer.mozilla.org/ja/docs/Web/API/notification)を叩くために外部のjsを呼び出すということを行っています。なのでここでは[ports](https://guide.elm-lang.jp/interop/ports.html)を利用しています。実際にportsを使っている例としては[この記事](https://qiita.com/ababup1192/items/f27f9af282d9fa642eb5)が非常に参考になります。

```elm:src/Main.elm
initTime = 3

type alias Model =
    { timer : Int
    , isStart : Bool
    }

init : () -> ( Model, Cmd Msg )
init _ =
    ( Model initTime False
    , Cmd.none
    )
```
Modelとしては現在のTimerの値(`timer`)とTimerが動いているかどうか(`isStart`)を持っています。そしてinitで適切に初期化しています。

```elm:src/Main.elm
type Msg
    = DoTimer
    | Tick Time.Posix
    | Notification
    | Notified Bool
    | Reset

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        DoTimer ->
            let
                isStart =
                    not model.isStart
            in
            ( { model | isStart = isStart }, Cmd.none )

        Tick _ ->
            let
                c =
                    if model.isStart then
                        model.timer - 1

                    else
                        model.timer

                status =
                    if c < 0 then
                        update Notification model

                    else
                        ( { model | timer = c }
                        , Cmd.none
                        )
            in
            status

        Notification ->
            ( model, notifyUser () )

        -- イケてない
        Notified _ ->
            update Reset model

        Reset ->
            ( { model | timer = initTime, isStart = False }
            , Cmd.none
            )
```
メインロジックに当たります。ボタンを押すと`DoTimer`が発火し、止まっていれば`isStart`がtrueになります。すると毎秒発火している`Tick`において、`model.timer`の値が減っていきます。（要はカウントダウンが始まります）
もし0以下になれば、`Notification`が発火し、portsで設定していた`notifyUser`が呼ばれます。あとで実装を紹介しますが、`notifyUser`が終わる際に`Notified`を発火させ、タイマーがリセットされます。

```elm:src/Main.elm
subscriptions : Model -> Sub Msg
subscriptions _ =
    Sub.batch [ notified Notified, Time.every 1000 Tick ]
```
js側から送られてくる`notified`をSubしておきます。

### src/index.js
Elmにおけるベースとなるjavascriptです。

```javascript:src/index.js
document.addEventListener('DOMContentLoaded', function () {
    if (!Notification) {
        alert('Notification can not use.');
        return;
    }

    if (Notification.permission !== "granted") {
        Notification.requestPermission();
    }
});
function notify() {
    if (Notification.permission !== "granted")
        Notification.requestPermission();
    else {
        const notification = new Notification('Time\'s up', {
            body: "timer end"
        });
        notification.onclose = () => {}
    }
}
```
一応ブラウザを想定して`Notification.permission`を確認していますが、このあたりはmozilaのサイトを見て学ぶほうが良い気がします。（Electronだけならいらないはずです）

```javascript:src/index.js
app.ports.notifyUser.subscribe(_ => {
    notify()
    app.ports.notified.send(true) // イケてない
});
```
Elm側が`notifyUser`を呼び出したときに、実際に動く部分になります。`notify`を呼び出してpush通知を送ったあと、終わったことをElm側に通知しています。（何かを送らなければならないようなので後々のことも考えてbooleanを送っています。）

以上が実装の解説になります。

# まとめ
振り返ると全くElectron要素がないので詐欺タイトルっぽくなっていますが、一応動くものは完成しました。Elmは少し触っただけでしたが、portsを使うことでElmの世界を壊すことなく、Native javascriptの資産を使えるところも非常に良いと感じました。
実はtodo機能のついたpomodolo timerを作ろうとしているので、まだまだスタートラインにたったところです。やるきの続く限り今後も開発を続けていくと思うのでたまに覗いていただければと思います。