Packages

Multi-surface application runtime for Elixir. One TEA module renders to terminal, browser (LiveView), SSH, and MCP (agents). 30+ widgets, flexbox + CSS grid, AI agent runtime, distributed swarm with CRDTs, time-travel debugging, session recording, sandboxed REPL, and agentic commerce.

Current section

Files

Jump to
raxol lib raxol playground demos vfs_demo.ex
Raw

lib/raxol/playground/demos/vfs_demo.ex

defmodule Raxol.Playground.Demos.VfsDemo do
@moduledoc "Playground demo: in-memory virtual file system with shell-like commands."
use Raxol.Core.Runtime.Application
alias Raxol.Commands.FileSystem
import Raxol.Playground.DemoHelpers,
only: [history_prev: 1, history_next: 1, effective_width: 2]
@visible_lines 14
@default_box_width 70
@box_height 16
@max_history 50
@impl true
def init(_context) do
%{
fs: seed_filesystem(),
input: "",
cursor: 0,
output: [{"# Virtual FS -- type 'help' for commands", :info}],
output_offset: 0,
input_history: [],
history_index: nil
}
end
@impl true
def update(message, model), do: {handle_key(message, model), []}
defp handle_key(key_match(:enter), model), do: exec_input(model)
defp handle_key(key_match(:up), model), do: history_prev(model)
defp handle_key(key_match(:down), model), do: history_next(model)
defp handle_key(key_match(:backspace), model) do
input = String.slice(model.input, 0..-2//1)
%{model | input: input, cursor: max(model.cursor - 1, 0)}
end
defp handle_key(key_match("l", ctrl: true), model),
do: %{model | output: [], output_offset: 0}
defp handle_key(key_match("u", ctrl: true), model),
do: %{model | input: "", cursor: 0}
defp handle_key(key_match("j", ctrl: true), model),
do: scroll_output(model, 1)
defp handle_key(key_match("k", ctrl: true), model),
do: scroll_output(model, -1)
defp handle_key(key_match(:char, char: ch), model) when byte_size(ch) == 1 do
%{model | input: model.input <> ch, cursor: model.cursor + 1}
end
defp handle_key(_other, model), do: model
@impl true
def view(model) do
box_width = effective_width(model, @default_box_width)
visible =
model.output
|> Enum.reverse()
|> Enum.drop(model.output_offset)
|> Enum.take(@visible_lines)
|> Enum.map(&output_line/1)
column style: %{gap: 0} do
[
text("Virtual File System", style: [:bold]),
text("cwd: #{FileSystem.pwd(model.fs)}", style: [:dim]),
divider(),
box style: %{
border: :single,
padding: 1,
width: box_width,
height: @box_height
} do
column style: %{gap: 0} do
if visible == [],
do: [text("(empty)", style: [:dim])],
else: visible
end
end,
prompt_line(model),
divider(),
text(
"[Enter] exec [Up/Down] history [Ctrl+L] clear [Ctrl+U] clear input",
style: [:dim]
),
text(
"Commands: ls | cd | cat | pwd | mkdir | rm | tree (try 'help')",
style: [:dim]
)
]
end
end
@impl true
def subscribe(_model), do: []
# -- Command execution --
defp exec_input(%{input: ""} = model), do: model
defp exec_input(model) do
cmd = String.trim(model.input)
{argv0, args} = parse_command(cmd)
{output_lines, new_fs} = dispatch(argv0, args, model.fs)
%{
model
| fs: new_fs,
input: "",
cursor: 0,
history_index: nil,
input_history: [cmd | model.input_history] |> Enum.take(@max_history)
}
|> add_output([{"$ #{cmd}", :input} | output_lines])
end
defp parse_command(cmd) do
case String.split(cmd, ~r/\s+/, trim: true) do
[] -> {"", []}
[argv0 | args] -> {argv0, args}
end
end
defp dispatch("ls", args, fs) do
dir = List.first(args, ".")
case FileSystem.ls(fs, dir) do
{:ok, entries} ->
{FileSystem.format_ls(entries, fs, dir), fs}
{:error, reason} ->
{[{error_msg("ls", reason), :error}], fs}
end
end
defp dispatch("cd", [], fs), do: {[{"cd: missing argument", :error}], fs}
defp dispatch("cd", [dir | _], fs) do
case FileSystem.cd(fs, dir) do
{:ok, new_fs} -> {[], new_fs}
{:error, reason} -> {[{error_msg("cd", reason), :error}], fs}
end
end
defp dispatch("cat", [], fs), do: {[{"cat: missing argument", :error}], fs}
defp dispatch("cat", [path | _], fs) do
case FileSystem.cat(fs, path) do
{:ok, content} ->
lines =
content
|> FileSystem.format_cat(@default_box_width - 4, @visible_lines)
|> Enum.map(fn {line, num} ->
{"#{String.pad_leading(Integer.to_string(num), 3)} | #{line}",
:file}
end)
{lines, fs}
{:error, reason} ->
{[{error_msg("cat", reason), :error}], fs}
end
end
defp dispatch("pwd", _args, fs) do
{[{FileSystem.pwd(fs), :result}], fs}
end
defp dispatch("mkdir", [], fs),
do: {[{"mkdir: missing argument", :error}], fs}
defp dispatch("mkdir", [dir | _], fs) do
case FileSystem.mkdir(fs, dir) do
{:ok, new_fs} -> {[{"created: #{dir}", :result}], new_fs}
{:error, reason} -> {[{error_msg("mkdir", reason), :error}], fs}
end
end
defp dispatch("rm", [], fs), do: {[{"rm: missing argument", :error}], fs}
defp dispatch("rm", [path | _], fs) do
case FileSystem.rm(fs, path) do
{:ok, new_fs} -> {[{"removed: #{path}", :result}], new_fs}
{:error, reason} -> {[{error_msg("rm", reason), :error}], fs}
end
end
defp dispatch("tree", args, fs) do
dir = Enum.at(args, 0, ".")
depth = parse_depth(Enum.at(args, 1))
case FileSystem.tree(fs, dir, depth) do
{:ok, tree_data} ->
lines = tree_data |> format_tree("") |> Enum.map(&{&1, :file})
{lines, fs}
{:error, reason} ->
{[{error_msg("tree", reason), :error}], fs}
end
end
defp dispatch("help", _args, fs) do
lines = [
{"ls [dir] -- list directory", :info},
{"cd <dir> -- change directory (supports .., -)", :info},
{"cat <file> -- show file content", :info},
{"pwd -- print working directory", :info},
{"mkdir <dir> -- create directory", :info},
{"rm <path> -- remove file or empty dir", :info},
{"tree [dir] [depth] -- show directory tree", :info}
]
{lines, fs}
end
defp dispatch(cmd, _args, fs) do
{[{"unknown command: #{cmd} (try 'help')", :error}], fs}
end
# -- Helpers --
defp error_msg(cmd, reason) do
"#{cmd}: #{reason |> Atom.to_string() |> String.replace("_", " ")}"
end
defp parse_depth(nil), do: 3
defp parse_depth(str) do
case Integer.parse(str) do
{n, ""} when n >= 0 -> n
_ -> 3
end
end
defp format_tree({name, :file, _}, prefix), do: [prefix <> name]
defp format_tree({name, :directory, children}, prefix) do
last_idx = length(children) - 1
child_lines =
children
|> Enum.with_index()
|> Enum.flat_map(fn {child, idx} ->
format_tree_child(child, prefix, idx == last_idx)
end)
[prefix <> name <> "/" | child_lines]
end
defp format_tree_child(child, prefix, is_last?) do
connector = if is_last?, do: "`-- ", else: "|-- "
continuation = if is_last?, do: " ", else: "| "
format_tree(child, prefix <> connector)
|> Enum.with_index()
|> Enum.map(fn
{line, 0} -> line
{line, _} -> prefix <> continuation <> String.trim_leading(line)
end)
end
# -- Seed data --
defp seed_filesystem do
fs = FileSystem.new()
{:ok, fs} = FileSystem.mkdir(fs, "/home")
{:ok, fs} = FileSystem.mkdir(fs, "/home/user")
{:ok, fs} = FileSystem.mkdir(fs, "/home/user/docs")
{:ok, fs} = FileSystem.mkdir(fs, "/home/user/projects")
{:ok, fs} =
FileSystem.create_file(
fs,
"/home/user/readme.txt",
"Welcome to Raxol VFS!\n\nThis is a virtual file system demo.\nAll data lives in memory."
)
{:ok, fs} =
FileSystem.create_file(
fs,
"/home/user/docs/notes.txt",
"- Learn Elixir\n- Build TUIs\n- Ship to Hex"
)
{:ok, fs} =
FileSystem.create_file(
fs,
"/home/user/projects/hello.exs",
~s[IO.puts("Hello from VFS!")]
)
{:ok, fs} = FileSystem.mkdir(fs, "/tmp")
{:ok, fs} = FileSystem.cd(fs, "/home/user")
fs
end
# history_prev/1 and history_next/1 imported from DemoHelpers
# -- Scroll --
defp scroll_output(model, delta) do
max_offset = max(0, length(model.output) - @visible_lines)
new_offset =
Raxol.Core.Utils.Math.clamp(model.output_offset + delta, 0, max_offset)
%{model | output_offset: new_offset}
end
# -- Output --
defp add_output(model, lines) do
new_output =
Enum.reduce(lines, model.output, fn line, acc -> [line | acc] end)
%{model | output: new_output, output_offset: 0}
end
# -- View helpers --
defp output_line({line, :input}), do: text(line, style: [:bold])
defp output_line({line, :result}), do: text(line, fg: :green)
defp output_line({line, :file}), do: text(line)
defp output_line({line, :directory}),
do: text(line, fg: :blue, style: [:bold])
defp output_line({line, :error}), do: text(line, fg: :red)
defp output_line({line, :info}), do: text(line, style: [:dim])
defp prompt_line(model) do
row style: %{gap: 0} do
[
text("vfs> ", style: [:bold], fg: :cyan),
text(model.input <> "_")
]
end
end
end