Elixir Desktop

repository·main·Indexed 23 days ago

https://github.com/elixir-desktop/desktop

A library for building native-like desktop and mobile applications for Windows, MacOS, Linux, iOS, and Android using Phoenix LiveView. It provides a platform-agnostic API for managing windows, webviews, menus, and notifications via multiple backends, including wxWidgets, JSON/TCP for mobile, and a browser-based fallback. Includes tools for environment verification, custom backend implementation, and generating platform-specific installers via `mix desktop.installer`.

Tokens
9K
Snippets
23
Records
54
Agent score
83%

What's inside elixir-desktop

  1. Understand the test architecture layers

    main

    The testing strategy is organized into four distinct layers to balance speed and coverage:

    LayerCommandDescription
    L0 Static guardsmix test.guardUses Credo/static analysis to catch misuse of and/or in Window logic without running code.
    L1 Unit (no wx)mix test.fastTests the Router, Browser backend contracts, and pure Window helpers.
    L2 Unit (mocked)mix test.fastTests the Bridge wire format and handle_cast logic using mocks (e.g., Bridge.Mock).
    L3 Integration (:wx)xvfb-run mix test.wxTests the real wx environment, including connection, locale, and window lifecycle.
    L4 App smokemanual/CI nightlyFull application supervision order tests (e.g., desktop-example-app).
  2. Configure the mobile (Android/iOS) bridge

    main

    On mobile targets, desktop uses Desktop.Backend.Json to communicate with the native host app via a JSON protocol over TCP.

    • Port Configuration: Set the BRIDGE_PORT environment variable to the port your native host app is listening on. Use 0 for an in-process mock transport (useful for testing).
    • Custom Events: To trigger app-level native events (like sharing or saving) from Elixir, use Desktop.Platform.System.custom_event/2. This sends the event over the same wire format used by the legacy Bridge package.

    Note: The separate bridge hex package is no longer required as transport is built into Desktop.Bridge.Transport.

  3. Avoid common boolean logic regressions in Window code

    main

    A known regression pattern in the project involves using non-boolean operands with and or or operators, which can cause {:badbool, ...} errors.

    Risk Patterns to Avoid

    Do not use if with and/or where the left-hand side (LHS) is a non-comparison value like a module atom, a wx ref, or a PID:

    • if <non_boolean> and|or <expr>
    • if frame and not is_shown? (where frame is a wx_ref)
    • if menubar and frame (where menubar is a module atom)

    Safe Patterns

    It is safe to use if with truthy values if you are not using and/or operators, or if you use explicit comparisons:

    • if frame, do: (Accepts truthy values)
    • if title != old and frame != nil (Uses explicit comparisons)
    • if caps.window and not OS.mobile?() (Both sides are booleans)
  4. Use version managers (asdf or mise) for Erlang and Elixir

    main

    Elixir Desktop uses a .tool-versions file to pin Erlang and Elixir versions. You can use asdf or mise to manage these automatically.

    Using mise:

    curl https://mise.run | sh
    mise install

    Using asdf:

    asdf plugin update --all
    asdf install erlang 24.0.1
    asdf install elixir 1.14.0-otp-24

    Note for automation: If writing shell wrappers (like run_mix scripts), do not hard-code paths to asdf. Instead, invoke mix through your activated environment or use mise exec -- mix ... / asdf exec mix ....

    # asdf example
    asdf plugin update --all
    asdf install erlang 24.0.1
    asdf install elixir 1.14.0-otp-24
  5. Convert a Phoenix app into a Desktop app

    main

    To transform an existing Phoenix application into a desktop application using elixir-desktop, follow these steps:

    1. Add the dependency: Add {:desktop, github: "elixir-desktop/desktop"} to your mix.exs.
    2. Update the Endpoint:
      • Change your endpoint module to use Desktop.Endpoint instead of Phoenix.Endpoint.
      • Add plug Desktop.Auth to the endpoint pipeline to restrict access to the Desktop app's WebView.
    3. Configure the Endpoint: Set the http port to 0 (to allow automatic port selection) and ensure server: true is set in your configuration (config/dev.exs or config/runtime.exs).
    4. Add the Window specification: Add {Desktop.Window, [app: :your_app, id: DemoWindow, url: &YourWeb.Endpoint.url/0]} to your application's supervision tree.
    # 1. mix.exs
    defp deps do
      [
        {:desktop, github: "elixir-desktop/desktop"}
      ]
    end
    
    # 2. Endpoint configuration
    defmodule DemoWeb.Endpoint do
      use Desktop.Endpoint, otp_app: :demo
      plug Desktop.Auth
      # ...
    end
    
    # 3. Application supervision tree
    defmodule Demo.Application do
      def start(_type, _args) do
        children = [
          DemoWeb.Endpoint,
          {
            Desktop.Window,
            [
              app: :demo,
              id: DemoWindow,
              url: &DemoWeb.Endpoint.url/0
            ]
          }
        ]
        # ...
      end
    end
    
    # 4. config/dev.exs
    config :demo, DemoWeb.Endpoint,
      http: [ip: {127, 0, 0, 1}, port: 0],
      server: true
  6. Create a distributable installer

    main

    To package your application for end-users, do not use mix release. While mix release creates a standard Erlang release, it lacks the platform-specific packaging and UI components required for a Desktop application.

    Instead, use the desktop.installer task to generate platform-specific installers:

    mix desktop.installer

    Supported Output Formats

    After running the command, check your project's build directory for the following files:

    • Windows: .exe installer
    • macOS: .dmg or .app bundle
    • Linux: .AppImage, .deb, or .rpm package
  7. Choose a platform backend

    main

    All UI operations in elixir-desktop are delegated to a single backend module via Desktop.Platform. You can select a backend automatically or explicitly via configuration.

    Automatic Selection

    By default (config :desktop, :backend, :auto), the library selects a backend based on the following priority:

    1. Mobile: If config :desktop, :mobile_target, true is set at compile time or Desktop.OS.mobile?/0 returns true (via ELIXIR_DESKTOP_OS), it uses Desktop.Backend.Json.
    2. No wxWidgets: If the NO_WX environment variable is set or OTP :wx is unavailable, it uses Desktop.Backend.Browser.
    3. Default: Otherwise, it uses Desktop.Backend.Wx.

    Explicit Configuration

    You can force a specific backend in your config/config.exs:

    • :wx: Native wxWidgets window + webview.
    • :json: JSON/TCP bridge (used for mobile native hosts).
    • :browser: Opens URLs in the OS default browser; no native window is created.
    • Custom: Provide a module that implements the Desktop.Platform.Window, Content, Notification, Media, System, and Menu behaviours.

    Note: You must restart the application after changing backend configuration.

    # Explicitly setting the backend in config/config.exs
    config :desktop, :backend, :wx      # native wxWidgets window + webview
    config :desktop, :backend, :json    # JSON/TCP bridge (mobile native host)
    config :desktop, :backend, :browser # OS default browser, no native window
    
    # Custom backend implementation
    config :desktop, :backend, MyApp.DesktopBackend
  8. Set up Elixir Desktop on GNU/Linux

    main

    It is recommended to use the Erlang Solutions packages for Erlang.

    To compile NIFs, you must install a C compiler and specific dependencies. On Debian/Ubuntu-based systems, run:

    sudo apt install inotify-tools libtool automake libgmp-dev make libwxgtk-webview3.0-gtk3-dev libssl-dev libncurses5-dev curl git
  9. Set up Elixir Desktop on Windows

    main

    Elixir Desktop uses msys2 on Windows to produce native applications. Follow these steps:

    1. Install Erlang 24: Download from erlang.org or build from source.
    2. Install msys2: Download from msys2.org.
    3. Install msys2 dependencies: Open an msys2 64-bit shell and run the pacman commands provided below.
    4. Install Elixir 1.12+: Download the precompiled zip and add the bin directory to your PATH via .bashrc.
    5. Install Node.js: Get npm and Node 12.x (or higher) from nodejs.org and add the binary path to your msys2 PATH.
    6. Install NSIS: Required for building installers. Get it from nsis.sourceforge.io.
    # Install msys2 packages in an msys2 64-bit shell
    pacman -Syu
    pacman -S --noconfirm pacman-mirrors pkg-config
    pacman -S --noconfirm --needed base-devel autoconf automake make libtool mingw-w64-x86_64-toolchain mingw-w64-x86_64-openssl mingw-w64-x86_64-libtool git
    
    # Install Elixir 1.12+ in an msys2 64-bit shell
    mkdir $HOME/elixir && cd $HOME/elixir
    wget https://github.com/elixir-lang/elixir/releases/download/v1.12/Precompiled.zip
    unzip Precompiled.zip
    echo "export PATH=\"$HOME/elixir/bin:\$PATH\"" >> ~/.bashrc
    export PATH="$HOME/elixir/bin:$PATH"
  10. Get started with Elixir Desktop

    main

    To build native-like Elixir apps for Windows, MacOS, Linux, iOS, and Android using Phoenix LiveView, follow these steps:

    1. Prepare your environment: Follow the Getting your Environment Ready Guide.
    2. Verify toolchain: If using .tool-versions, run mix desktop.check_toolchain to ensure your OTP and Elixir versions match.
    3. Run an example: Use the desktop-example-app as a starting point.
    4. Build your first app: Follow the Your first Desktop App guide.
  11. Set up Elixir Desktop on MacOS

    main
    1. Install Elixir: Use Homebrew to install Elixir, which will also fetch Erlang.
      brew install elixir
    2. Install Xcode Command Line Tools: Required for building NIFs and packaging releases. Open Xcode -> Preferences -> General -> Downloads -> Components and install Command Line Tools.
    3. Install Node.js/npm: Use nvm to install version 12.16.1.
      brew install nvm
      nvm install v12.16.1

    Using custom wxWidgets (Advanced)

    If you encounter visual issues with native Menus or Taskbar Icons, you can build a custom version of wxWidgets from the master branch and rebuild Erlang using kerl with the --with-wxdir flag.

    # Install Node via nvm
    brew install nvm
    nvm install v12.16.1
    
    # Example: Building custom wxWidgets
    git clone https://github.com/wxWidgets/wxWidgets.git
    cd wxWidgets
    git checkout master
    ./configure --prefix=/usr/local/wxWidgets --enable-clipboard --enable-controls \
          --enable-dataviewctrl --enable-display \
          --enable-dnd --enable-graphics_ctx \
          --enable-std_string --enable-svg \
          --enable-unicode --enable-webview \
          --with-expat --with-libjpeg \
          --with-libpng --with-libtiff \
          --with-opengl --with-zlib \
          --disable-precomp-headers --disable-monolithic
    
    make -j4
  12. Run without wxWidgets

    main

    If you do not have wxWidgets installed or are running in a headless environment (like CI), you can run your application using the browser backend.

    1. Using Environment Variables: Set NO_WX=1. With :auto backend configuration, this forces the use of Desktop.Backend.Browser. In this mode, URLs open in the OS default browser, and window/menu APIs degrade gracefully (notifications are logged to the console).
    2. Headless Linux Testing: If you want to test the Wx backend on a headless Linux server, use xvfb-run to provide a virtual display:
    xvfb-run -a mix phx.server