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)