├── .gitignore
├── README.md
├── elm-package.json
├── examples
└── ipcRenderer
│ ├── Main.elm
│ ├── README.md
│ ├── elm-package.json
│ ├── index.html
│ ├── main.js
│ └── package.json
└── src
├── Electron
├── IpcRenderer.elm
├── Screen.elm
└── Shell.elm
└── Native
├── IpcRenderer.js
├── Screen.js
└── Shell.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # Generated output
2 | elm.js
3 |
4 | # ELM
5 | elm-stuff/
6 |
7 | # NODEJS
8 | node_modules/
9 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # elm-electron
2 |
3 | Elm-electron is a (work in progress) integration of electron for Elm.
4 |
5 | At the moment, it exposes a limited subset of the electron api. But it should be enough to get you started!
6 |
7 | ## How to
8 |
9 | **Nota** You can find an example of a project integrating `elm-electron` [there](https://github.com/oleiade/patron)
10 |
11 | elm-electron is mostly written using the Native elm api. Therefore it is not compatible with the `elm-package` command. As a result you will have to integrate the code by hand into your projects.
12 |
13 | To do so, you will need to integrate the `elm-electron` sources into your project, in this example we will assume you maintain your elm sources files in an `src/` folder.
14 |
15 | The folder hierarchy would probably look like:
16 |
17 | ```bash
18 | ├── Electron
19 | │ └── IpcRenderer.elm
20 | │ └── Screen.elm
21 | │ └── Shell.elm
22 | ├── Native
23 | │ └── IpcRenderer.js
24 | │ └── Screen.js
25 | │ └── Shell.js
26 | ├── MyFile.elm
27 | ├── MyOtherFile.elm
28 | └── MyOtherOtherFile.elm
29 | ```
30 |
31 | And you should make sure your `elm-package.json` file:
32 | * adds the `elm-electron` sources to *source-directories
33 | * exposes the `Electron` modules you intend to use
34 | * activates the usage of `native-modules`
35 |
36 | ```json
37 | "source-directories": [
38 | ".",
39 | "./src"
40 | ],
41 | "exposed-modules": [
42 | "Electron.IpcRenderer"
43 | ],
44 | "native-modules": true,
45 | "elm-version": "0.18.0 <= v < 0.19.0"
46 | ```
47 |
48 | You should be done and ready to rock. For more informations on how to actually use the library, please refer to the `examples/`
49 |
--------------------------------------------------------------------------------
/elm-package.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "1.0.0",
3 | "summary": "helpful summary of your project, less than 80 characters",
4 | "repository": "https://github.com/user/project.git",
5 | "license": "BSD3",
6 | "source-directories": [
7 | "./src"
8 | ],
9 | "exposed-modules": [
10 | "Electron.IpcRenderer",
11 | "Electron.Screen"
12 | ],
13 | "native-modules": true,
14 | "dependencies": {
15 | "elm-lang/core": "4.0.0 <= v < 5.0.0"
16 | },
17 | "elm-version": "0.18.0 <= v < 0.19.0"
18 | }
19 |
--------------------------------------------------------------------------------
/examples/ipcRenderer/Main.elm:
--------------------------------------------------------------------------------
1 | import Electron.IpcRenderer as IPC exposing (on, send)
2 |
3 | import Html exposing (Html, program, text, button, h2, div)
4 | import Html.Attributes exposing (class)
5 | import Html.Events exposing (onClick)
6 | import Json.Encode
7 | -- import Json.Decode as Decode exposing (Decoder, map, (:=))
8 | import Json.Decode exposing (int, string, float, nullable, map, Decoder)
9 | import Json.Decode.Pipeline exposing (decode, required, optional, hardcoded)
10 |
11 |
12 | main =
13 | program
14 | { init = init
15 | , view = view
16 | , update = update
17 | , subscriptions = subscriptions
18 | }
19 |
20 |
21 | -- MODEL
22 |
23 |
24 | type alias Model =
25 | { currentTime : String
26 | }
27 |
28 |
29 | init : ( Model, Cmd Msg )
30 | init =
31 | ({ currentTime = "None" }, Cmd.none)
32 |
33 |
34 | type alias TimeRequest =
35 | { format : String
36 | }
37 |
38 |
39 | encodeRequest : TimeRequest -> Json.Encode.Value
40 | encodeRequest request =
41 | Json.Encode.object
42 | [ ( "format", Json.Encode.string request.format ) ]
43 |
44 |
45 | type alias TimeResponse =
46 | { status : String
47 | , time : String
48 | }
49 |
50 |
51 | decodeResponse : Decoder TimeResponse
52 | decodeResponse =
53 | decode TimeResponse
54 | |> required "status" string
55 | |> required "time" string
56 |
57 |
58 | -- UPDATE
59 |
60 |
61 | type Msg
62 | = Send String
63 | | OnResponse TimeResponse
64 |
65 |
66 | update : Msg -> Model -> (Model, Cmd Msg)
67 | update msg model =
68 | case msg of
69 | Send format ->
70 | ( model, IPC.send "time-request" <| encodeRequest { format = format } )
71 | OnResponse response ->
72 | ( { model | currentTime = response.time }, Cmd.none )
73 |
74 |
75 | -- VIEW
76 |
77 |
78 | view : Model -> Html Msg
79 | view model =
80 | div []
81 | [ h2 [] [ text model.currentTime ]
82 | , button [ class "btn btn-default btn-lg btn-block", onClick (Send "timestamp") ] [ text "Get timestamp" ]
83 | , button [ class "btn btn-default btn-lg btn-block", onClick (Send "date") ] [ text "Get date" ]
84 | ]
85 |
86 |
87 |
88 | -- SUBSCRIPTIONS
89 |
90 |
91 | subscriptions : Model -> Sub Msg
92 | subscriptions model =
93 | Sub.batch
94 | [ IPC.on "time-response" (map OnResponse decodeResponse)
95 | ]
96 |
--------------------------------------------------------------------------------
/examples/ipcRenderer/README.md:
--------------------------------------------------------------------------------
1 | # ipcRenderer
2 |
3 | ## How to
4 |
5 | ```bash
6 | npm install
7 | elm-make Main.elm --output=elm.js && electron .
8 | ```
9 |
10 | ## Under the hood
11 |
12 | ### Main.js
13 |
14 | The entry point for this example is the `main.js` file. This is where the electron ipc communication is handled.
15 |
16 | ```js
17 | ipcMain.on('time-request', (event, arg) => {
18 | let t;
19 |
20 | if (arg['format'] == "timestamp") {
21 | t = Date.now().toString();
22 | } else {
23 | t = Date().toLocaleString();
24 | }
25 |
26 | event.sender.send('time-response', {
27 | status: "success",
28 | time: t
29 | });
30 | });
31 | ```
32 |
33 | In this example, we set the main process IPC listener to react on `time-request` channel events. Whenever an event comes in, it extracts the provided `format` and send back the time whether as a timestamp or a locale date string to the renderer process.
34 |
35 | ### Main.elm
36 |
37 | On Elm side, we want to send an IPC message to the main process (as we are living in a renderer process) on the `time-request` channel, whenever a button is pressed. And we want to listen for responses from the main process on the `time-response` channel, as well as decoding them.
38 |
39 | To do this we start by declaring our `Msg` type to provide a `Send` and an `OnResponse` messages
40 |
41 | ```elm
42 | type Msg
43 | = Send String
44 | | OnResponse TimeResponse
45 | ```
46 |
47 | As well as some types to represent the requests and the responses
48 |
49 | ```elm
50 | type alias TimeRequest =
51 | { format : String
52 | }
53 |
54 | type alias TimeResponse =
55 | { status : String
56 | , time : String
57 | }
58 | ```
59 |
60 | Then we want our view to integrate two buttons to get the date back from the main process in both possible formats.
61 | To do this we ensure the button's `onClick` emits a `Send` message with the desired date format to the update loop.
62 |
63 | ```elm
64 | view : Model -> Html Msg
65 | view model =
66 | div []
67 | [ h2 [] [ text model.currentTime ]
68 | , button [ class "btn btn-default btn-lg btn-block", onClick (Send "timestamp") ] [ text "Get timestamp" ]
69 | , button [ class "btn btn-default btn-lg btn-block", onClick (Send "date") ] [ text "Get date" ]
70 | ]
71 | ```
72 |
73 | Here is the core of the IPC communication happening. In the update loop, when a `Send` message is processed, we will use the *elm-electron* `IpcRenderer` constructs to send the message to the main process. We do so by providing the `Electron.IpcRenderer.send` function the channel it should send the message on, and a JSON encoded message.
74 |
75 | At this the point the message will be sent to the main process.
76 |
77 | We also handle the `OnResponse` message by updating the model currentTime with the date we received. More about this later on.
78 |
79 | ```elm
80 | update : Msg -> Model -> (Model, Cmd Msg)
81 | update msg model =
82 | case msg of
83 | Send format ->
84 | ( model, IPC.send "time-request" <| encodeRequest { format = format } )
85 | OnResponse response ->
86 | ( { model | currentTime = response.time }, Cmd.none )
87 | ```
88 |
89 | Now we are able ton send messages to the main process. But how do we listen for incoming responses? You will need to use the `Electron.IpcRenderer.on` function as a subscription.
90 |
91 | the `Electron.IpcRenderer.on` function takes a channel it should listen on, and a JSON decoder to deserialize the responses, as input.
92 |
93 | ```elm
94 | subscriptions : Model -> Sub Msg
95 | subscriptions model =
96 | Sub.batch
97 | [ IPC.on "time-response" (Decode.map OnResponse decodeResponse)
98 | ]
99 | ```
100 |
101 | In this example, we subscribe to responses incoming on the `time-response` channel, and decode them using our custom decodeResponse.
102 |
103 | ```elm
104 | decodeResponse : Decode.Decoder TimeResponse
105 | decodeResponse =
106 | Decode.object2 TimeResponse
107 | ("status" := Decode.string)
108 | ("time" := Decode.string)
109 | ```
110 |
--------------------------------------------------------------------------------
/examples/ipcRenderer/elm-package.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.1.0",
3 | "summary": "",
4 | "repository": "https://github.com/elm-electron/elm-electron.git",
5 | "license": "MIT",
6 | "source-directories": [
7 | ".",
8 | "../../src"
9 | ],
10 | "exposed-modules": [
11 | "Electron.IpcRenderer"
12 | ],
13 | "native-modules": true,
14 | "dependencies": {
15 | "NoRedInk/elm-decode-pipeline": "3.0.0 <= v < 4.0.0",
16 | "elm-lang/core": "5.0.0 <= v < 6.0.0",
17 | "elm-lang/html": "2.0.0 <= v < 3.0.0",
18 | "elm-lang/http": "1.0.0 <= v < 2.0.0"
19 | },
20 | "elm-version": "0.18.0 <= v < 0.19.0"
21 | }
22 |
--------------------------------------------------------------------------------
/examples/ipcRenderer/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Elm Electron
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/examples/ipcRenderer/main.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // Electron libraries
4 | const electron = require('electron');
5 | const {ipcMain} = require('electron');
6 |
7 |
8 | // Module to control application life.
9 | const app = electron.app;
10 | // Module to create native browser window.
11 | const BrowserWindow = electron.BrowserWindow;
12 |
13 | // Keep a global reference of the window object, if you don't, the window will
14 | // be closed automatically when the JavaScript object is garbage collected.
15 | let mainWindow;
16 |
17 | /**
18 | * [createWindow description]
19 | * @method createWindow
20 | */
21 | function createWindow() {
22 | // Create the browser window.
23 | mainWindow = new BrowserWindow({width: 800, height: 640});
24 | mainWindow.setMenu(null);
25 |
26 | // and load the index.html of the app.
27 | mainWindow.loadURL('file://' + __dirname + '/index.html');
28 |
29 | // Emitted when the window is closed.
30 | mainWindow.on('closed', function() {
31 | // Dereference the window object, usually you would store windows
32 | // in an array if your app supports multi windows, this is the time
33 | // when you should delete the corresponding element.
34 | mainWindow = null;
35 | });
36 | }
37 |
38 | /**
39 | * Initializes the app:
40 | * - Get or create user settings database
41 | * - Create app main windows
42 | */
43 | function initialize() {
44 | createWindow();
45 | };
46 |
47 | // This method will be called when Electron has finished
48 | // initialization and is ready to create browser windows.
49 | app.on('ready', initialize);
50 |
51 | // Quit when all windows are closed.
52 | app.on('window-all-closed', function() {
53 | // On OS X it is common for applications and their menu bar
54 | // to stay active until the user quits explicitly with Cmd + Q
55 | if (process.platform !== 'darwin') {
56 | app.quit();
57 | }
58 | });
59 |
60 | app.on('activate', function() {
61 | // On OS X it's common to re-create a window in the app when the
62 | // dock icon is clicked and there are no other windows open.
63 | if (mainWindow === null) {
64 | createWindow();
65 | }
66 | });
67 |
68 |
69 | ipcMain.on('time-request', (event, arg) => {
70 | let t;
71 |
72 | if (arg['format'] == "timestamp") {
73 | t = Date.now().toString();
74 | } else {
75 | t = Date().toLocaleString();
76 | }
77 |
78 | event.sender.send('time-response', {
79 | status: "success",
80 | time: t
81 | });
82 | });
83 |
--------------------------------------------------------------------------------
/examples/ipcRenderer/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "elm-electron-ipc",
3 | "version": "0.1.0",
4 | "description": "Example of how to use elm-electron for IPC communication",
5 | "main": "main.js",
6 | "scripts": {
7 | "start": "electron main.js"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/elm-electron/electron.git"
12 | },
13 | "keywords": [
14 | "Electron",
15 | "Elm"
16 | ],
17 | "author": "oleiade",
18 | "license": "MIT",
19 | "bugs": {
20 | "url": "https://github.com/elm-electron/electron/issues"
21 | },
22 | "homepage": "https://github.com/elm-electron/electron#readme",
23 | "devDependencies": {
24 | "devtron": "^1.4.0",
25 | "eslint": "^3.11.1",
26 | "eslint-config-google": "^0.7.1",
27 | "electron-debug": "^1.1.0"
28 | },
29 | "dependencies": {
30 | "electron-prebuilt": "^0.36.0"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Electron/IpcRenderer.elm:
--------------------------------------------------------------------------------
1 | effect module Electron.IpcRenderer
2 | where { subscription = MySub, command = MyCmd }
3 | exposing
4 | ( on
5 | , send
6 | )
7 |
8 | {-|
9 | # Subscriptions
10 | @docs on, send
11 | -}
12 |
13 | import Platform exposing (Router)
14 | import Native.IpcRenderer
15 | import Dict exposing (Dict)
16 | import Json.Decode as Decode exposing (Decoder, Value)
17 | import Process
18 | import Task exposing (Task)
19 |
20 |
21 | {-| Subscribe to an event incoming over ipcRenderer
22 | -}
23 | on : String -> Decoder msg -> Sub msg
24 | on eventName decoder =
25 | subscription (On eventName decoder)
26 |
27 |
28 | {-| Send a value over ipcRenderer
29 | -}
30 | send : String -> Value -> Cmd msg
31 | send eventName value =
32 | command (Send eventName value)
33 |
34 |
35 | type MySub msg
36 | = On String (Decoder msg)
37 |
38 |
39 | subMap : (a -> b) -> MySub a -> MySub b
40 | subMap mapper (On eventName decoder) =
41 | On eventName <| Decode.map mapper decoder
42 |
43 |
44 | type MyCmd msg
45 | = Send String Value
46 |
47 |
48 | cmdMap : (a -> b) -> MyCmd a -> MyCmd b
49 | cmdMap _ (Send eventName value) =
50 | Send eventName value
51 |
52 |
53 | type alias State msg =
54 | Dict String (Watcher msg)
55 |
56 |
57 | type alias Watcher msg =
58 | { decoders : List (Decoder msg)
59 | , pid : Process.Id
60 | }
61 |
62 |
63 | type alias SubDict msg =
64 | Dict String (List (Decoder msg))
65 |
66 |
67 | categorize : List (MySub msg) -> SubDict msg
68 | categorize subs =
69 | categorizeHelp subs Dict.empty
70 |
71 |
72 | categorizeHelp : List (MySub msg) -> SubDict msg -> SubDict msg
73 | categorizeHelp subs subDict =
74 | case subs of
75 | [] ->
76 | subDict
77 |
78 | (On eventName decoder) :: rest ->
79 | categorizeHelp rest
80 | <| Dict.update eventName (categorizeHelpHelp decoder) subDict
81 |
82 |
83 | categorizeHelpHelp : a -> Maybe (List a) -> Maybe (List a)
84 | categorizeHelpHelp value maybeValues =
85 | case maybeValues of
86 | Nothing ->
87 | Just [ value ]
88 |
89 | Just values ->
90 | Just (value :: values)
91 |
92 |
93 | init : Task Never (State msg)
94 | init =
95 | Task.succeed Dict.empty
96 |
97 |
98 | type alias Msg =
99 | { eventName : String
100 | , value : Value
101 | }
102 |
103 |
104 | onWatcherEffects :
105 | Router msg Msg
106 | -> List (MySub msg)
107 | -> Dict String (Watcher msg)
108 | -> Task Never (Dict String (Watcher msg))
109 | onWatcherEffects router newSubs oldState =
110 | let
111 | leftStep eventName watcher task =
112 | watcher.pid
113 | |> Process.kill
114 | |> Task.andThen (always task)
115 |
116 | bothStep eventName watcher decoders task =
117 | task
118 | |> Task.andThen (\state ->
119 | Task.succeed (Dict.insert eventName ({ watcher | decoders = decoders }) state))
120 |
121 | rightStep eventName decoders task =
122 | task
123 | |> Task.andThen (\state ->
124 | Process.spawn (Native.IpcRenderer.on eventName (Platform.sendToSelf router << Msg eventName))
125 | |> Task.andThen (\pid ->
126 | Task.succeed (Dict.insert eventName (Watcher decoders pid) state)))
127 | in
128 | Dict.merge leftStep
129 | bothStep
130 | rightStep
131 | oldState
132 | (categorize newSubs)
133 | (Task.succeed Dict.empty)
134 |
135 |
136 | onEffects :
137 | Router msg Msg
138 | -> List (MyCmd msg)
139 | -> List (MySub msg)
140 | -> State msg
141 | -> Task Never (State msg)
142 | onEffects router newCmds newSubs oldState =
143 | let
144 | updatedForWatchers =
145 | onWatcherEffects router newSubs oldState
146 |
147 | runCommand (Send eventName value) =
148 | Native.IpcRenderer.send eventName value
149 |
150 | runCommands =
151 | newCmds
152 | |> List.map runCommand
153 | |> Task.sequence
154 | in
155 | runCommands
156 | |> Task.andThen (always updatedForWatchers)
157 |
158 |
159 | onSelfMsg : Router msg Msg -> Msg -> State msg -> Task Never (State msg)
160 | onSelfMsg router msg state =
161 | case Dict.get msg.eventName state of
162 | Nothing ->
163 | Task.succeed state
164 |
165 | Just watcher ->
166 | let
167 | send decoder =
168 | msg.value
169 | |> Decode.decodeValue decoder
170 | |> Result.map (Platform.sendToApp router)
171 | |> Result.toMaybe
172 | in
173 | Task.sequence (List.filterMap send watcher.decoders)
174 | |> Task.andThen ((always (Task.succeed state)))
175 |
--------------------------------------------------------------------------------
/src/Electron/Screen.elm:
--------------------------------------------------------------------------------
1 | effect module Electron.Screen where { subscription = MySub } exposing
2 | ( displays
3 | , Rect
4 | , TouchSupport(..)
5 | , Display
6 | )
7 |
8 | {-|
9 | # Subscriptions
10 | @docs displays
11 |
12 | # Types
13 | @docs Rect, TouchSupport, Display
14 | -}
15 |
16 | import Platform exposing (Router)
17 | import Json.Decode as Decode exposing (Decoder, Value, andThen)
18 | import Json.Decode.Pipeline exposing (decode, required, optional, hardcoded)
19 | import Process
20 | import Task exposing (Task)
21 | import Native.Screen
22 |
23 |
24 | {-| Subscribe to changes in the state of the user's available displays
25 | -}
26 | displays : (List Display -> msg) -> Sub msg
27 | displays toMsg =
28 | subscription <| Displays toMsg
29 |
30 |
31 |
32 | {-| Denotes whether a display supports touch interactions and whether that
33 | capability is even known.
34 | -}
35 | type TouchSupport
36 | = Available
37 | | Unavailable
38 | | Unknown
39 |
40 | customDecoder decoder toResult =
41 | Decode.andThen
42 | (\a ->
43 | case toResult a of
44 | Ok b -> Decode.succeed b
45 | Err err -> Decode.fail err
46 | )
47 | decoder
48 |
49 | decodeTouchSupport : Decoder TouchSupport
50 | decodeTouchSupport =
51 | let
52 | parse value =
53 | case value of
54 | "available" ->
55 | Ok Available
56 | "unavailable" ->
57 | Ok Unavailable
58 | "unknown" ->
59 | Ok Unknown
60 | _ ->
61 | Err ("Unknown TouchSupport type: " ++ value)
62 | in
63 | customDecoder Decode.string parse
64 |
65 |
66 | {-| Describes an area of the screen in pixels
67 | -}
68 | type alias Rect =
69 | { x : Int
70 | , y : Int
71 | , width : Int
72 | , height : Int
73 | }
74 |
75 |
76 | decodeRect : Decoder Rect
77 | decodeRect =
78 | decode Rect
79 | |> required "x" Decode.int
80 | |> required "y" Decode.int
81 | |> required "width" Decode.int
82 | |> required "height" Decode.int
83 |
84 |
85 | {-| All available information about a user's display
86 | -}
87 | type alias Display =
88 | { id : Int
89 | , rotation : Int
90 | , scaleFactor : Float
91 | , touchSupport : TouchSupport
92 | , bounds : Rect
93 | , workArea : Rect
94 | , workAreaSize : Rect
95 | }
96 |
97 |
98 | decodeDisplay : Decoder Display
99 | decodeDisplay =
100 | decode Display
101 | |> required "id" Decode.int
102 | |> required "rotation" Decode.int
103 | |> required "scaleFactor" Decode.float
104 | |> required "touchSupport" decodeTouchSupport
105 | |> required "bounds" decodeRect
106 | |> required "workArea" decodeRect
107 | |> required "workAreaSize" decodeRect
108 |
109 |
110 | type alias Watcher a =
111 | { pid : Process.Id
112 | , listeners : List a
113 | }
114 |
115 |
116 | type MySub msg
117 | = Displays (List Display -> msg)
118 |
119 |
120 | subMap : (a -> b) -> MySub a -> MySub b
121 | subMap mapper (Displays toMsg) =
122 | Displays (toMsg >> mapper)
123 |
124 |
125 | type alias State msg =
126 | { displays : Maybe (Watcher (List Display -> msg))
127 | }
128 |
129 |
130 | init : Task Never (State msg)
131 | init =
132 | Task.succeed { displays = Nothing }
133 |
134 |
135 | type Msg =
136 | DisplaysMsg (List Display)
137 |
138 |
139 | onDisplaysEffects
140 | : Router msg Msg
141 | -> (List (List Display -> msg))
142 | -> Maybe (Watcher (List Display -> msg))
143 | -> Task Never (Maybe (Watcher (List Display -> msg)))
144 | onDisplaysEffects router newListeners maybeWatcher =
145 | case (newListeners, maybeWatcher) of
146 | ([], Nothing) ->
147 | Task.succeed Nothing
148 |
149 | ([], Just watcher) ->
150 | Process.kill watcher.pid
151 | |> Task.map (always Nothing)
152 |
153 | (subs, Nothing) ->
154 | Process.spawn (Native.Screen.onDisplaysChanged (Decode.list decodeDisplay) (DisplaysMsg >> Platform.sendToSelf router))
155 | |> Task.andThen (\pid -> (Task.succeed (Just (Watcher pid subs))))
156 |
157 | (subs, Just watcher) ->
158 | Task.succeed <| Just { watcher | listeners = subs }
159 |
160 |
161 | subsToDisplayListeners : List (MySub msg) -> List (List Display -> msg)
162 | subsToDisplayListeners subs =
163 | let
164 | extractListener sub =
165 | case sub of
166 | Displays listener ->
167 | Just listener
168 | in
169 | List.filterMap extractListener subs
170 |
171 |
172 | onEffects
173 | : Router msg Msg
174 | -> List (MySub msg)
175 | -> State msg
176 | -> Task Never (State msg)
177 | onEffects router newSubs oldState =
178 | onDisplaysEffects router (subsToDisplayListeners newSubs) oldState.displays
179 | |> Task.andThen (\maybeWatcher -> Task.succeed { oldState | displays = maybeWatcher })
180 |
181 |
182 | sendDisplaysMsg : Router msg Msg -> List Display -> (List Display -> msg) -> Task Never ()
183 | sendDisplaysMsg router latestDisplays listener =
184 | Platform.sendToApp router (listener latestDisplays)
185 |
186 |
187 | onSelfMsg : Router msg Msg -> Msg -> State msg -> Task Never (State msg)
188 | onSelfMsg router msg state =
189 | case msg of
190 | DisplaysMsg displays ->
191 | state.displays
192 | |> Maybe.map (\watcher -> Task.sequence <| List.map (sendDisplaysMsg router displays) watcher.listeners)
193 | |> Maybe.map (\task -> Task.map (always state) task)
194 | |> Maybe.withDefault (Task.succeed state)
195 |
--------------------------------------------------------------------------------
/src/Electron/Shell.elm:
--------------------------------------------------------------------------------
1 | effect module Electron.Shell
2 | where { command = MyCmd }
3 | exposing
4 | ( showItemInFolder
5 | , openItem
6 | , openExternal
7 | , moveItemToTrash
8 | , beep
9 | )
10 |
11 | import Task exposing (Task)
12 | import Platform exposing (Router)
13 | import Native.Shell
14 |
15 |
16 | -- PUBLIC CMDS
17 |
18 |
19 | showItemInFolder : String -> Cmd msg
20 | showItemInFolder fullPath =
21 | command <| ShowItemInFolder fullPath
22 |
23 |
24 | openItem : String -> Cmd msg
25 | openItem fullPath =
26 | command <| OpenItem fullPath
27 |
28 |
29 | openExternal : String -> Cmd msg
30 | openExternal url =
31 | command <| OpenExternal url
32 |
33 |
34 | moveItemToTrash : String -> Cmd msg
35 | moveItemToTrash fullPath =
36 | command <| MoveItemToTrash fullPath
37 |
38 |
39 | beep : Cmd msg
40 | beep =
41 | command Beep
42 |
43 |
44 |
45 | -- NATIVE WRAPPERS
46 |
47 |
48 | nativeShowItemInFolder : String -> Task Never ()
49 | nativeShowItemInFolder fullPath =
50 | Native.Shell.showItemInFolder fullPath
51 |
52 |
53 | nativeOpenItem : String -> Task Never ()
54 | nativeOpenItem fullPath =
55 | Native.Shell.openItem fullPath
56 |
57 |
58 | nativeOpenExternal : String -> Task Never ()
59 | nativeOpenExternal url =
60 | Native.Shell.openExternal url
61 |
62 |
63 | nativeMoveItemToTrash : String -> Task Never ()
64 | nativeMoveItemToTrash fullPath =
65 | Native.Shell.moveItemToTrash fullPath
66 |
67 |
68 | nativeBeep : Task Never ()
69 | nativeBeep =
70 | Native.Shell.beep
71 |
72 |
73 |
74 | -- EFFECT MANAGER
75 |
76 |
77 | type MyCmd msg
78 | = ShowItemInFolder String
79 | | OpenItem String
80 | | OpenExternal String
81 | | MoveItemToTrash String
82 | | Beep
83 |
84 |
85 | cmdMap : (a -> b) -> MyCmd a -> MyCmd b
86 | cmdMap mapper cmd =
87 | case cmd of
88 | ShowItemInFolder fullPath ->
89 | ShowItemInFolder fullPath
90 |
91 | OpenItem fullPath ->
92 | OpenItem fullPath
93 |
94 | OpenExternal url ->
95 | OpenExternal url
96 |
97 | MoveItemToTrash fullPath ->
98 | MoveItemToTrash fullPath
99 |
100 | Beep ->
101 | Beep
102 |
103 |
104 | cmdToTask : MyCmd msg -> Task Never ()
105 | cmdToTask cmd =
106 | case cmd of
107 | ShowItemInFolder fullPath ->
108 | nativeShowItemInFolder fullPath
109 |
110 | OpenItem fullPath ->
111 | nativeOpenItem fullPath
112 |
113 | OpenExternal url ->
114 | nativeOpenExternal url
115 |
116 | MoveItemToTrash fullPath ->
117 | nativeMoveItemToTrash fullPath
118 |
119 | Beep ->
120 | nativeBeep
121 |
122 |
123 | init : Task Never ()
124 | init =
125 | Task.succeed ()
126 |
127 |
128 | onEffects : Router msg Never -> List (MyCmd msg) -> () -> Task Never ()
129 | onEffects router cmds seed =
130 | case cmds of
131 | [] ->
132 | Task.succeed ()
133 |
134 | _ ->
135 | cmds
136 | |> List.map cmdToTask
137 | |> Task.sequence
138 | |> Task.map (\_ -> ())
139 |
140 |
141 | onSelfMsg : Platform.Router msg Never -> Never -> () -> Task Never ()
142 | onSelfMsg _ _ _ =
143 | Task.succeed ()
144 |
--------------------------------------------------------------------------------
/src/Native/IpcRenderer.js:
--------------------------------------------------------------------------------
1 | var _elm_electron$elm_electron$Native_IpcRenderer = (function () {
2 | var ipcRenderer = require('electron').ipcRenderer;
3 |
4 | function on(eventName, toTask) {
5 | return _elm_lang$core$Native_Scheduler.nativeBinding(function (callback) {
6 | function performTask(event, value) {
7 | _elm_lang$core$Native_Scheduler.rawSpawn(toTask(value));
8 | }
9 |
10 | ipcRenderer.on(eventName, performTask);
11 |
12 | return function () {
13 | ipcRenderer.removeListener(eventName, performTask);
14 | };
15 | });
16 | }
17 |
18 | function send(eventName, value) {
19 | return _elm_lang$core$Native_Scheduler.nativeBinding(function (callback) {
20 | ipcRenderer.send(eventName, value);
21 | callback(_elm_lang$core$Native_Scheduler.succeed(_elm_lang$core$Native_Utils.Tuple0));
22 | });
23 | }
24 |
25 | return {
26 | on: F2(on),
27 | send: F2(send),
28 | };
29 | }());
30 |
--------------------------------------------------------------------------------
/src/Native/Screen.js:
--------------------------------------------------------------------------------
1 | var _elm_electron$elm_electron$Native_Screen = (function () {
2 | var electronScreen = require('electron').screen
3 |
4 | function onDisplaysChanged(decoder, toTask) {
5 | return _elm_lang$core$Native_Scheduler.nativeBinding(function (callback) {
6 |
7 | function performTask() {
8 | var displays = electronScreen.getAllDisplays()
9 | var result = _elm_lang$core$Native_Json.run(decoder, displays)
10 | if (result.ctor === 'Ok') {
11 | _elm_lang$core$Native_Scheduler.rawSpawn(toTask(result))
12 | }
13 | }
14 |
15 | electronScreen.on('display-added', performTask)
16 | electronScreen.on('display-removed', performTask)
17 | electronScreen.on('display-metrics-changed', performTask)
18 |
19 | performTask()
20 |
21 | return function () {
22 | electronScreen.removeListener('display-added', performTask)
23 | electronScreen.removeListener('display-removed', performTask)
24 | electronScreen.removeListener('display-metrics-changed', performTask)
25 | }
26 | })
27 | }
28 |
29 | return {
30 | onDisplaysChanged: F2(onDisplaysChanged)
31 | }
32 | }())
33 |
--------------------------------------------------------------------------------
/src/Native/Shell.js:
--------------------------------------------------------------------------------
1 | var _elm_electron$elm_electron$Native_Shell = (function () {
2 | var shell = require('electron').shell
3 |
4 | function runAndSucceed(work) {
5 | return _elm_lang$core$Native_Scheduler.nativeBinding(function (callback) {
6 | work()
7 | callback(_elm_lang$core$Task.succeed(_elm_lang$core$Native_Utils.Tuple0));
8 | })
9 | }
10 |
11 | // showItemInFolder: String -> Task Never ()
12 | function showItemInFolder(fullPath) {
13 | return runAndSucceed(function () {
14 | shell.showItemInFolder(fullPath);
15 | })
16 | }
17 |
18 | // openItem: String -> Task Never ()
19 | function openItem(fullPath) {
20 | return runAndSucceed(function () {
21 | shell.openItem(fullPath)
22 | })
23 | }
24 |
25 | // openExternal: String -> Task Never ()
26 | function openExternal(url) {
27 | return runAndSucceed(function () {
28 | shell.openExternal(url);
29 | })
30 | }
31 |
32 | // moveItemToTrash: String -> Task Never ()
33 | function moveItemToTrash(fullPath) {
34 | return runAndSucceed(function () {
35 | shell.moveItemToTrash(fullPath)
36 | })
37 | }
38 |
39 | // beep: Task Never ()
40 | var beep = runAndSucceed(function () {
41 | shell.beep()
42 | })
43 |
44 | return {
45 | showItemInFolder: showItemInFolder,
46 | openItem: openItem,
47 | openExternal: openExternal,
48 | moveItemToTrash: moveItemToTrash,
49 | beep: beep,
50 | };
51 | }());
52 |
--------------------------------------------------------------------------------