Writing desktop application is not an easy task. But why don’t use Elixir, Phoenix and LiveView? We will figure out how we can create a desktop application using those technologies.
We will use the Phoenix framework. All the details on how to install phoenix can be found here: https://hexdocs.pm/phoenix/installation.html
Let’s get started!
mix phx.new elixir_desktop_application --no-ecto
cd elixir_desktop_application
So far so good. To make things hot we will use LiveView. Here is the installation guide. After settings up Phoenix and LiveView, we will push things to git.
git init .
git add .
git commit -m "Initial commit"
Moving further We’ll a build simple file browser. Out root LiveView will look like this:
scope "/", ElixirDesktopApplicationWeb do
pipe_through :browser
live "/", FileBrowserLive
end
And write a simple file browser live view:
defmodule ElixirDesktopApplicationWeb.FileBrowserLive do
use Phoenix.LiveView
def render(assigns) do
~L"""
<h3><%= @current %></h3>
<ul>
<li phx-click="cd" phx-value=".."><b>..</b></li>
<%= for entry <- @dirs do %>
<li phx-click="cd" phx-value="<%= entry %>"><b><%= entry %></b></li>
<% end %>
<%= for entry <- @files do %>
<li><%= entry %></li>
<% end %>
</ul>
"""
end
def mount(_session, socket) do
socket =
socket
|> assign(current: Path.expand("."))
|> ls()
{:ok, socket}
end
def handle_event("cd", param, socket) do
socket =
socket
|> assign(current: path(socket, param))
|> ls()
{:noreply, socket}
end
defp ls(socket) do
case File.ls(socket.assigns.current) do
{:ok, entries} ->
{dirs, files} = Enum.split_with(entries, &File.dir?(path(socket, &1)))
assign(socket, dirs: Enum.sort(dirs), files: Enum.sort(files))
_ ->
socket
end
end
defp path(socket, param) do
Path.expand(socket.assigns.current <> "/" <> param)
end
end
Take a look at http://localhost:4000/ such a nice file browser huh?
To make our app desktop we need a window. What we can do about it? There is something we can use webengine_kioks. Let use it!
We need to add it as dependency:
{:webengine_kiosk, "~> 0.2"}
And insert it to our supervision tree:
defmodule ElixirDesktopApplication.Application do
...
def start(_type, _args) do
# List all child processes to be supervised
children = [
# Start the endpoint when the application starts
ElixirDesktopApplicationWeb.Endpoint,
# Starts a worker by calling: ElixirDesktopApplication.Worker.start_link(arg)
# {ElixirDesktopApplication.Worker, arg},
{WebengineKiosk, {[homepage: "http://localhost:4000", fullscreen: false], name: MyKiosk}}
]
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: ElixirDesktopApplication.Supervisor]
Supervisor.start_link(children, opts)
end
...
end
Easy peasy! Is it works? Let start our application:
mix phx.server
Wow! The window appeared with our browser. Nice! But we can go further! The erlang release.
But wait! I can’t quit! Hit ctrl-c
in your terminal and add the quit button:
In lib/elixir_desktop_application_web/live/file_browser_live.ex
:
def render(assigns) do
~L"""
<h4 phx-click="quit">quit</h4>
"""
...
end
def handle_event("quit", _param, _socket) do
System.halt(0)
end
We can close application now.
OTP releases are included in elixir 1.9. Out build script can look like this:
#!/bin/bash
export SECRET_KEY_BASE=ENhzfdb/3IwgO8LX0QHfYqPfpU6I8kyrPG348vFwRkzxG0CjN7+egBO/F0RJnjxE
export MIX_ENV=prod
mix deps.get --only prod
mix compile
npm run deploy --prefix ./assets
mix phx.digest
mix release --overwrite
And few more tweaks to our application. Inconfig/prod.exs
:
config :elixir_desktop_application, ElixirDesktopApplicationWeb.Endpoint,
cache_static_manifest: "priv/static/cache_manifest.json",
server: true
Finally, create the build!
./build.sh
And run!
_build/prod/rel/elixir_desktop_application/bin/elixir_desktop_application start
Success!
Yay! Everything still works. But this is not an app, right? Right. We will try to create an OSX app…
Updated build script looks like this:
#!/bin/bash
export SECRET_KEY_BASE=ENhzfdb/3IwgO8LX0QHfYqPfpU6I8kyrPG348vFwRkzxG0CjN7+egBO/F0RJnjxE
export MIX_ENV=prod
mix deps.get --only prod
mix compile
npm run deploy --prefix ./assets
mix phx.digest
mix release --overwrite
APP=FileBrowser
APP_DIR="${APP}.app/Contents/MacOS"
RELEASE=_build/prod/rel/elixir_desktop_application
rm -rf $APP.app
mkdir -p $APP_DIR
echo "cp -r $RELEASE $APP_DIR"
cp -r $RELEASE $APP_DIR
echo "#!/bin/bash" > $APP_DIR/$APP
echo 'DIR="\$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )' >> $APP_DIR/$APP
echo '$DIR/elixir_desktop_application/bin/elixir_desktop_application start' >> $APP_DIR/$APP
chmod +x $APP_DIR/$APP
Just run the script, click on FileBrowser.app … and … voila! But…
Although it all works just fine it’s not ready to distribute! There are a few things we need to look at!
We’re still communicating with HTTP. The browser talks to the server over HTTP. That’s bad. You can’t run a few instances at the same time. Is there a solution? Yes, it is! We can communicate over a port instead of HTTP. With the phoenix framework, it can be done. Also, some changes to WebEngineKiosk are required.
This application will work fine on your computer, but not on another. QT5 libraries need to be installed. We can include those libraries in our bundle and relink the binary.
What about other platforms? Erlang releases works on Windows and Linux. It shouldn’t be hard to make create releases for those platforms as well.
This is just proof of concept. But it turns out that we are not far away from the framework like Electron for Elixir. Will you be interested in such a thing? Are you interested in writing the desktop application in Elixir with a little bit of Javascript? What do you think?
P.S. The source code is here: https://github.com/fazibear/elixir_desktop_application