Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Tavern ~ Drive your Emacs with Elixir

tavern.livemd

Tavern ~ Drive your Emacs with Elixir

path = Path.expand("workspace/tavern", "~/")

Mix.install([
  # git: "https://github.com/MinasMazar/tavern.git"}
  {:tavern, "~> 0.1", path: path, env: :prod}
])

Start

Application.start(:tavern)

Emacs code

(use-package websocket
  :config
  (require 'websocket)

  (defvar tavern-endpoint "ws://localhost:9069/tavern"
    "Endpoint of tavern elixir application")

  (defun tavern-start ()
    "Start webserver connection to Tavern (Elixir WS server)"
    (interactive)
    (setq tavern--websocket
          (websocket-open
           tavern-endpoint
           :on-message
           (lambda (_websocket frame)
             (let ((body (websocket-frame-text frame)))
               (tavern-eval body)))
					;(message "[tavern] message received: %S" body)))
           :on-close (lambda (_websocket) (message "websocket closed")))))

  (defun tavern-send (payload)
    (interactive "sMessage: ")
    (unless (tavern-open-connection)
      (tavern-start))
    (websocket-send-text tavern--websocket payload))

  (defun tavern-open-connection ()
    (websocket-openp tavern--websocket))

  (defun tavern-eval (string)
    "Evaluate elisp code stored in a string."
    ;; (message (format "tavern> evaluating %s" string))
    (eval (car (read-from-string string))))

  (defun tavern--pong ()
    (tavern-send "tavern-pong"))

  (setq tavern--websocket nil))

Bot modules

defmodule Tavern.Bot.Alarm do
  use GenServer

  def init(state) do
    Tavern.emacs_eval([:"use-package", :"alarm-clock"])
    {:ok, state}
  end

  def handle_info({:message, _}, state), do: {:noreply, state}
end
defmodule Tavern.Bot.Notification do
  def notify(msg) do
    Tavern.emacs_eval([:message, "%s", msg])
  end
end
defmodule Tavern.Bot.Query do
  def ask(prompt, options) do
    Tavern.emacs_eval(
      with_current_frame do
        [:"completing-read", prompt, {:quote, options}]
      end
    )
  end

  def with_current_frame(do: body) do
    [:"with-selected-frame", [:"selected-frame"], body]
  end
end

Usage (a simple bot)

This dumb bot is going to print the UTC timestamp in a buffer named <*tavern*> in your Emacs active session every 5 seconds, for 2 minutes.

defmodule Tavern.Bot do
  require Logger
  import Tavern.Bot.Notification
  import Tavern.Bot.Query
  use GenServer

  @ask_timeout 24 * 60 * 1000
  def start do
    case start_link() do
      {:ok, pid} -> pid
      {:error, {:already_started, pid}} -> pid
    end
  end

  def start_link do
    GenServer.start_link(__MODULE__, nil, name: __MODULE__)
  end

  def init(_) do
    :timer.send_interval(@ask_timeout, :tick)
    Tavern.subscribe()
    {:ok, %{buffer_name: "*tavern*"}}
  end

  def handle_info(:tick, state) do
    ask("Are you ok?", ["sure", "not really"])
    |> handle_answer()
    |> notify()

    {:noreply, state}
  end

  def handle_info({:message, message}, state) do
    state =
      try do
        {evaluated, _} = Code.eval_string(message)
        Logger.info("Evaluated Elixir code from Emacs: #{inspect(evaluated)}")
        state
      rescue
        CompileError -> state
        _ -> state
      end

    {:noreply, state}
  end

  def handle_answer(answer) do
    Logger.debug(answer)
    "ok!"
  end
end

pid = Tavern.Bot.start()
# send(pid, :tick)

Elfeed bot

defmodule Tavern.Bots.Elfeed do
  require Logger
  import Tavern.Bot.Notification
  use GenServer

  def start_link do
    with {:ok, pid} <- GenServer.start_link(__MODULE__, nil, name: __MODULE__) do
      pid
    else
      {:error, {:already_started, pid}} -> pid
    end
  end

  def stop, do: GenServer.stop(__MODULE__)

  def init(state) do
    :timer.send_interval(31 * 60 * 1_000, :tick)
    Process.send_after(self(), :tick, 3 * 1_000)
    {:ok, state}
  end

  def handle_info(:tick, state) do
    Tavern.emacs_eval([:"elfeed-update"])
    notify("Elfeed update!")
    {:noreply, state}
  end
end

pid = Tavern.Bots.Elfeed.start_link()
Process.sleep(:infinity)

Debug section

# Tavern.Api.EmacsClient.emacs_eval("nil")
# Tavern.Api.send_message(~w[(message "hi there!")], :emacsclient)
# Tavern.Api.send_message(~w[(message "hi there!")], :ws)