├── .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 | --------------------------------------------------------------------------------