Packages

`forkpty(3)` bindings for elixir

Current section

Files

Jump to
expty lib expty_smartcell.ex
Raw

lib/expty_smartcell.ex

if !Code.ensure_loaded?(Kino.SmartCell) do
defmodule ExPTY.SmartCell do
end
else
defmodule ExPTY.SmartCell do
use Kino.JS, assets_path: "lib/assets"
use Kino.JS.Live
use Kino.SmartCell, name: "ExPTY"
require Logger
@impl true
def init(attrs, ctx) do
executable = attrs["executable"] || default_executable()
ctx =
assign(ctx,
executable: executable,
pty: nil,
pty_ref: nil,
started?: false
)
{:ok, ctx}
end
@impl true
def handle_connect(ctx) do
fields = %{
executable: ctx.assigns.executable,
started: ctx.assigns.started?
}
{:ok, fields, ctx}
end
@impl true
def handle_event("start_executable", data, ctx) do
if ctx.assigns.started? do
Logger.warning("Executable already started")
{:noreply, ctx}
else
executable =
case data do
%{"executable" => executable} ->
executable
_ ->
default_executable()
end
Logger.info("Starting executable: #{executable}")
{:ok, pty} = ExPTY.spawn(executable, [])
ExPTY.on_data(pty, fn _, _, data ->
broadcast_event(ctx, "data", %{data: Base.encode64(data)})
end)
self = self()
ExPTY.on_exit(pty, fn _, _, exit_code, _ ->
Process.send_after(self, {:executable_exited, exit_code}, 0)
end)
{:noreply,
assign(ctx,
pty: pty,
started?: true
)}
end
end
def handle_event("stop_executable", _, ctx) do
stop_executable(ctx)
{:noreply, assign(ctx, pty: nil, started?: false)}
end
def handle_event("on_key", %{"data" => data}, ctx) do
if is_pid(ctx.assigns.pty) do
case Base.decode64(data) do
{:ok, data} ->
ExPTY.write(ctx.assigns.pty, data)
_ ->
nil
end
end
{:noreply, ctx}
end
def handle_event("resize", %{"cols" => cols, "rows" => rows}, ctx)
when is_integer(cols) and cols > 0 and is_integer(rows) and rows > 0 do
if is_pid(ctx.assigns.pty) do
ExPTY.resize(ctx.assigns.pty, cols, rows)
else
Process.send_after(self(), {:resize, cols, rows}, 500)
end
{:noreply, ctx}
end
def handle_event(_unknown_event, _data, ctx) do
{:noreply, ctx}
end
@impl true
def handle_info({:resize, cols, rows}, ctx) do
if is_pid(ctx.assigns.pty) do
ExPTY.resize(ctx.assigns.pty, cols, rows)
end
{:noreply, ctx}
end
def handle_info({:executable_exited, code}, ctx) do
broadcast_event(ctx, "executable_exited", %{code: code})
{:noreply, assign(ctx, pty: nil, pty_ref: nil, started?: false)}
end
@impl true
def terminate(_, ctx) do
stop_executable(ctx)
:ok
end
@impl true
def to_attrs(ctx) do
%{
"executable" => ctx.assigns.executable
}
end
@impl true
def to_source(_attrs) do
""
end
defp default_executable do
case :os.type() do
{:unix, :darwin} ->
"zsh"
{:unix, _} ->
"bash"
{:win32, _} ->
"powershell.exe"
end
end
defp stop_executable(ctx) do
if is_pid(ctx.assigns.pty) do
# signal 9: SIGKILL
case :os.type() do
{:unix, _} ->
ExPTY.kill(ctx.assigns.pty, 9)
{:win32, _} ->
# TODO: implement ExPTY.Win.kill/2
nil
end
end
end
end
end