MatrixChat.jl/src/repl.jl

882 lines
28 KiB
Julia

# include("repl.jl")
import REPL
import REPL: LineEdit, REPLCompletions
using Markdown
###########
# Options #
###########
struct OptionSpec
name::String
short_name::Union{Nothing,String}
api::Pair{Symbol, Any}
is_switch::Bool
end
@enum(OptionClass, OPT_ARG, OPT_SWITCH)
const OptionDeclaration = Tuple{Union{String,Vector{String}}, # name + short_name?
OptionClass, # arg or switch
Pair{Symbol, Any} # api keywords
}
function OptionSpec(x::OptionDeclaration)::OptionSpec
get_names(name::String) = (name, nothing)
function get_names(names::Vector{String})
@assert length(names) == 2
return (names[1], names[2])
end
is_switch = x[2] == OPT_SWITCH
api = x[3]
(name, short_name) = get_names(x[1])
#TODO assert matching lex regex
if !is_switch
@assert api.second === nothing || hasmethod(api.second, Tuple{String})
end
return OptionSpec(name, short_name, api, is_switch)
end
function OptionSpecs(decs::Vector{OptionDeclaration})::Dict{String, OptionSpec}
specs = Dict()
for x in decs
opt_spec = OptionSpec(x)
@assert get(specs, opt_spec.name, nothing) === nothing # don't overwrite
specs[opt_spec.name] = opt_spec
if opt_spec.short_name !== nothing
@assert get(specs, opt_spec.short_name, nothing) === nothing # don't overwrite
specs[opt_spec.short_name] = opt_spec
end
end
return specs
end
struct Option
val::String
argument::Union{Nothing,String}
end
Base.show(io::IO, opt::Option) = print(io, "--$(opt.val)", opt.argument == nothing ? "" : "=$(opt.argument)")
function parse_option(word::AbstractString)::Option
m = match(r"^(?: -([a-z]) | --([a-z]{2,})(?:\s*=\s*(\S*))? )$"ix, word)
m == nothing && matrixerror("malformed option: ", repr(word))
option_name = (m.captures[1] != nothing ? m.captures[1] : m.captures[2])
option_arg = (m.captures[3] == nothing ? nothing : String(m.captures[3]))
return Option(option_name, option_arg)
end
meta_option_declarations = OptionDeclaration[
("preview", OPT_SWITCH, :preview => true)
]
meta_option_specs = OptionSpecs(meta_option_declarations)
################
# Command Spec #
################
@enum(CommandKind, CMD_HELP, CMD_MSG, CMD_DIALOG_LIST, CMD_HISTORY, CMD_LOAD_GROUP_PHOTO)
@enum(ArgClass, ARG_RAW, ARG_PKG, ARG_VERSION, ARG_REV, ARG_ALL)
struct ArgSpec
count::Pair
parser::Function
parser_keys::Vector{Pair{Symbol, Any}}
end
const CommandDeclaration = Tuple{CommandKind,
Vector{String}, # names
Union{Nothing,Function}, # handler
Tuple{Pair, # count
Function, # parser
Vector{Pair{Symbol, Any}}, # parser keys
}, # arguments
Vector{OptionDeclaration}, # options
String, #description
Union{Nothing}, #help
}
struct CommandSpec
kind::CommandKind
canonical_name::String
short_name::Union{Nothing,String}
handler::Union{Nothing,Function}
argument_spec::ArgSpec
option_specs::Dict{String, OptionSpec}
description::String
help::Union{Nothing}
end
function SuperSpecs(foo)::Dict{String,Dict{String,CommandSpec}}
super_specs = Dict()
for x in foo
sub_specs = CommandSpecs(x.second)
for name in x.first
@assert get(super_specs, name, nothing) === nothing
super_specs[name] = sub_specs
end
end
return super_specs
end
# populate a dictionary: command_name -> command_spec
function CommandSpecs(declarations::Vector{CommandDeclaration})::Dict{String,CommandSpec}
specs = Dict()
for dec in declarations
names = dec[2]
spec = CommandSpec(dec[1],
names[1],
length(names) == 2 ? names[2] : nothing,
dec[3],
ArgSpec(dec[4]...),
OptionSpecs(dec[5]),
dec[6],
dec[end])
for name in names
# TODO regex check name
@assert get(specs, name, nothing) === nothing
specs[name] = spec
end
end
return specs
end
###################
# Package parsing #
###################
let uuid = raw"(?i)[0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12}(?-i)",
name = raw"(\w+)(?:\.jl)?"
global const name_re = Regex("^$name\$")
global const uuid_re = Regex("^$uuid\$")
global const name_uuid_re = Regex("^$name\\s*=\\s*($uuid)\$")
end
# packages can be identified through: uuid, name, or name+uuid
# additionally valid for add/develop are: local path, url
function parse_package(word::AbstractString; add_or_develop=false)::PackageSpec
if add_or_develop && casesensitive_isdir(expanduser(word))
return PackageSpec(Types.GitRepo(expanduser(word)))
elseif occursin(uuid_re, word)
return PackageSpec(UUID(word))
elseif occursin(name_re, word)
return PackageSpec(String(match(name_re, word).captures[1]))
elseif occursin(name_uuid_re, word)
m = match(name_uuid_re, word)
return PackageSpec(String(m.captures[1]), UUID(m.captures[2]))
elseif add_or_develop
# Guess it is a url then
return PackageSpec(Types.GitRepo(word))
else
matrixerror("`$word` cannot be parsed as a package")
end
end
################
# REPL parsing #
################
mutable struct Statement
command::Union{Nothing,CommandSpec}
options::Vector{String}
arguments::Vector{String}
meta_options::Vector{String}
Statement() = new(nothing, [], [], [])
end
struct QuotedWord
word::String
isquoted::Bool
end
function unwrap_option(option::String)
if startswith(option, "--")
return length(option) == 2 ? "" : option[3:end]
elseif length(option) == 2
return option[end]
end
end
wrap_option(option::String) =
length(option) == 1 ? "-$option" : "--$option"
function _statement(words)
is_option(word) = first(word) == '-'
word = popfirst!(words)
# meta options
while is_option(word)
if isempty(words)
if unwrap_option(word) in keys(meta_option_specs)
return :cmd, "", nothing, true
else
return :meta, word, nothing, true
end
end
word = popfirst!(words)
end
# command
if word in keys(super_specs) # have a super command
super_name = word
super = super_specs[word]
if isempty(words)
return :sub, "", super_name, true
end
word = popfirst!(words)
command = get(super, word, nothing)
if command === nothing
if isempty(words)
return :sub, word, super_name, true
else
return nothing
end
end
elseif get(super_specs["matrix"], word, nothing) !== nothing # given a "matrix" command
command = get(super_specs["matrix"], word, nothing)
elseif isempty(words) # try to complete the super command
return :cmd, word, nothing, true
else
return nothing
end
if isempty(words)
return :arg, "", command, true
end
word = words[end]
manifest = any(x->x in ["--manifest", "-m"], filter(is_option, words))
return is_option(word) ?
(:opt, word, command, true) :
(:arg, word, command, !manifest)
end
function parse(cmd::String; for_completions=false)
# replace new lines with ; to support multiline commands
cmd = replace(replace(cmd, "\r\n" => "; "), "\n" => "; ")
# tokenize accoring to whitespace / quotes
# WIP
qwords = parse_quotes(cmd)
# tokenzie unquoted tokens according to matrix REPL syntax
words = lex(qwords)
# break up words according to ";"(doing this early makes subsequent processing easier)
word_groups = group_words(words)
# create statements
if for_completions
if length(word_groups) > 0
return _statement(word_groups[end])
else
return []
end
end
return map(Statement, word_groups)
end
# vector of words -> structured statement
# minimal checking is done in this phase
function Statement(words)::Statement
is_option(word) = first(word) == '-'
statement = Statement()
word = popfirst!(words)
# meta options
while is_option(word)
push!(statement.meta_options, word)
isempty(words) && matrixerror("no command specified")
word = popfirst!(words)
end
# command
# special handling for `preview`, just convert it to a meta option under the hood
if word == "preview"
if !("--preview" in statement.meta_options)
push!(statement.meta_options, "--preview")
end
isempty(words) && matrixerror("preview requires a command")
word = popfirst!(words)
end
if word in keys(super_specs)
super = super_specs[word]
isempty(words) && matrixerror("no subcommand specified")
word = popfirst!(words)
else
super = super_specs["matrix"]
end
command = get(super, word, nothing)
command !== nothing || matrixerror("expected command. instead got [$word]")
statement.command = command
# command arguments
for word in words
push!((is_option(word) ? statement.options : statement.arguments), word)
end
return statement
end
# break up words according to `;`(doing this early makes subsequent processing easier)
# the final group does not require a trailing `;`
function group_words(words)::Vector{Vector{String}}
statements = Vector{String}[]
x = String[]
for word in words
if word == ";"
isempty(x) ? matrixerror("empty statement") : push!(statements, x)
x = String[]
else
push!(x, word)
end
end
isempty(x) || push!(statements, x)
return statements
end
const lex_re = r"^[\?\./\+\-](?!\-) | ((git|ssh|http(s)?)|(git@[\w\-\.]+))(:(//)?)([\w\.@\:/\-~]+)(\.git)(/)? | [^@\#\s;]+\s*=\s*[^@\#\s;]+ | \#\s*[^@\#\s;]* | @\s*[^@\#\s;]* | [^@\#\s;]+|;"x
function lex(qwords::Vector{QuotedWord})::Vector{String}
words = String[]
for qword in qwords
if qword.isquoted
push!(words, qword.word)
else
append!(words, map(m->m.match, eachmatch(lex_re, qword.word)))
end
end
return words
end
function parse_quotes(cmd::String)::Vector{QuotedWord}
in_doublequote = false
in_singlequote = false
qwords = QuotedWord[]
token_in_progress = Char[]
push_token!(is_quoted) = begin
push!(qwords, QuotedWord(String(token_in_progress), is_quoted))
empty!(token_in_progress)
end
for c in cmd
if c == '"'
if in_singlequote # raw char
push!(token_in_progress, c)
else # delimiter
in_doublequote ? push_token!(true) : push_token!(false)
in_doublequote = !in_doublequote
end
elseif c == '\''
if in_doublequote # raw char
push!(token_in_progress, c)
else # delimiter
in_singlequote ? push_token!(true) : push_token!(false)
in_singlequote = !in_singlequote
end
else
push!(token_in_progress, c)
end
end
if (in_doublequote || in_singlequote)
matrixerror("unterminated quote")
else
push_token!(false)
end
# to avoid complexity in the main loop, empty tokens are allowed above and
# filtered out before returning
return filter(x->!isempty(x.word), qwords)
end
##############
# MatrixCommand #
##############
function APIOptions(options::Vector{Option},
specs::Dict{String, OptionSpec},
)::Dict{Symbol, Any}
keyword_vec = map(options) do opt
spec = specs[opt.val]
# opt is switch
spec.is_switch && return spec.api
# no opt wrapper -> just use raw argument
spec.api.second === nothing && return spec.api.first => opt.argument
# given opt wrapper
return spec.api.first => spec.api.second(opt.argument)
end
return Dict(keyword_vec)
end
# Only for PkgSpec
function word2token(word::AbstractString)::Token
if first(word) == '@'
return VersionRange(word[2:end])
elseif first(word) == '#'
return Rev(word[2:end])
else
return String(word)
end
end
function parse_matrix(raw_args::Vector{String}; valid=[], add_or_dev=false)
args::Vector{PkgToken} = map(word2token, raw_args)
enforce_argument_order(args)
# enforce spec
push!(valid, String) # always want at least PkgSpec identifiers
if !all(x->typeof(x) in valid, args)
matrixerror("invalid token")
end
# convert to final arguments
return package_args(args; add_or_dev=add_or_dev)
end
function enforce_argument(raw_args::Vector{String}, spec::ArgSpec)
args = spec.parser(raw_args; spec.parser_keys...)
# enforce_argument_count(spec.count, args)
return args
end
function enforce_option(option::String, specs::Dict{String,OptionSpec})::Option
opt = parse_option(option)
spec = get(specs, opt.val, nothing)
spec !== nothing ||
matrixerror("option '$(opt.val)' is not a valid option")
if spec.is_switch
opt.argument === nothing ||
matrixerror("option '$(opt.val)' does not take an argument, but '$(opt.argument)' given")
else # option takes an argument
opt.argument !== nothing ||
matrixerror("option '$(opt.val)' expects an argument, but no argument given")
end
return opt
end
function enforce_meta_options(options::Vector{String}, specs::Dict{String,OptionSpec})::Vector{Option}
meta_opt_names = keys(specs)
return map(options) do opt
tok = enforce_option(opt, specs)
tok.val in meta_opt_names ||
matrixerror("option '$opt' is not a valid meta option.")
#TODO hint that maybe they intended to use it as a command option
return tok
end
end
function enforce_opts(options::Vector{String}, specs::Dict{String,OptionSpec})::Vector{Option}
unique_keys = Symbol[]
get_key(opt::Option) = specs[opt.val].api.first
# final parsing
toks = map(x->enforce_option(x,specs),options)
# checking
for opt in toks
# valid option
opt.val in keys(specs) ||
matrixerror("option '$(opt.val)' is not supported")
# conflicting options
key = get_key(opt)
if key in unique_keys
conflicting = filter(opt->get_key(opt) == key, toks)
matrixerror("Conflicting options: $conflicting")
else
push!(unique_keys, key)
end
end
return toks
end
struct MatrixCommand
spec::CommandSpec
arguments::Vector{String}
end
Option(v::String) = Option(v, nothing)
# this the entry point for the majority of input checks
function MatrixCommand(statement::Statement)::MatrixCommand
meta_opts = enforce_meta_options(statement.meta_options,
meta_option_specs)
# return MatrixCommand(statement.command)
return MatrixCommand(statement.command, statement.arguments)
end
#############
# Execution #
#############
function do_cmd!(command::MatrixCommand, repl)
global user
if command.spec.kind == CMD_MSG
send(user, command.arguments[1], join(command.arguments[2:end], " "))
elseif command.spec.kind == CMD_HISTORY
if length(command.arguments) == 0
return
elseif length(command.arguments) == 1
history_size = 10
else
history_size = Base.parse(Int,command.arguments[2])
end
history(user, command.arguments[1], limit = history_size)
elseif command.spec.kind in [ CMD_DIALOG_LIST ]
global roomlist
map(println,roomlist["joined_rooms"])
elseif command.spec.kind in [ CMD_LOAD_GROUP_PHOTO ]
end
end
function do_cmd(repl::REPL.AbstractREPL, input::String; do_rethrow=false)
try
statements = parse(input)
commands = map(MatrixCommand, statements)
for cmd in commands
do_cmd!(cmd, repl)
end
catch err
if do_rethrow
rethrow(err)
end
Base.display_error(repl.t.err_stream, err, Base.catch_backtrace())
end
end
function CommandSpec(command_name::String)::Union{Nothing,CommandSpec}
# maybe a "matrix" command
spec = get(super_specs["matrix"], command_name, nothing)
if spec !== nothing
return spec
end
# maybe a "compound command"
m = match(r"(\w+)-(\w+)", command_name)
m !== nothing || (return nothing)
super = get(super_specs, m.captures[1], nothing)
super !== nothing || (return nothing)
return get(super, m.captures[2], nothing)
end
######################
# REPL mode creation #
######################
# Provide a string macro matrix"cmd" that can be used in the same way
# as the REPLMode `matrix> cmd`. Useful for testing and in environments
# where we do not have a REPL, e.g. IJulia.
struct MiniREPL <: REPL.AbstractREPL
display::TextDisplay
t::REPL.Terminals.TTYTerminal
end
function MiniREPL()
MiniREPL(TextDisplay(stdout), REPL.Terminals.TTYTerminal(get(ENV, "TERM", Sys.iswindows() ? "" : "dumb"), stdin, stdout, stderr))
end
REPL.REPLDisplay(repl::MiniREPL) = repl.display
const minirepl = Ref{MiniREPL}()
__init__() = minirepl[] = MiniREPL()
macro matrix_str(str::String)
:($(do_cmd)(minirepl[], $str; do_rethrow=true))
end
matrixstr(str::String) = do_cmd(minirepl[], str; do_rethrow=true)
# handle completions
mutable struct CompletionCache
commands::Vector{String}
canonical_names::Vector{String}
meta_options::Vector{String}
options::Dict{CommandKind, Vector{String}}
subcommands::Dict{String, Vector{String}}
CompletionCache() = new([],[],[],Dict(),Dict())
end
completion_cache = CompletionCache()
struct MatrixCompletionProvider <: LineEdit.CompletionProvider end
function LineEdit.complete_line(c::MatrixCompletionProvider, s)
partial = REPL.beforecursor(s.input_buffer)
full = LineEdit.input_string(s)
ret, range, should_complete = completions(full, lastindex(partial))
return ret, partial[range], should_complete
end
function complete_local_path(s, i1, i2)
cmp = REPL.REPLCompletions.complete_path(s, i2)
[REPL.REPLCompletions.completion_text(p) for p in cmp[1]], cmp[2], !isempty(cmp[1])
end
function complete_installed_package(s, i1, i2, project_opt)
matrixs = project_opt ? API.__installed(PKGMODE_PROJECT) : API.__installed()
matrixs = sort!(collect(keys(filter((p) -> p[2] != nothing, matrixs))))
cmp = filter(cmd -> startswith(cmd, s), matrixs)
return cmp, i1:i2, !isempty(cmp)
end
function complete_remote_package(s, i1, i2)
cmp = String[]
julia_version = VERSION
for reg in Types.registries(;clone_default=false)
data = Types.read_registry(joinpath(reg, "Registry.toml"))
for (uuid, matrixinfo) in data["packages"]
name = matrixinfo["name"]
if startswith(name, s)
compat_data = Operations.load_package_data_raw(
VersionSpec, joinpath(reg, matrixinfo["path"], "Compat.toml"))
supported_julia_versions = VersionSpec(VersionRange[])
for (ver_range, compats) in compat_data
for (compat, v) in compats
if compat == "julia"
union!(supported_julia_versions, VersionSpec(v))
end
end
end
if VERSION in supported_julia_versions
push!(cmp, name)
end
end
end
end
return cmp, i1:i2, !isempty(cmp)
end
function complete_argument(to_complete, i1, i2, lastcommand, project_opt
)::Tuple{Vector{String},UnitRange{Int},Bool}
# to_complete -> new letters typed
# lastcommand -> CMD_TYPE
# project_opt -> true
# i1 -> end command index
# i2 -> current index
if lastcommand in [ CMD_MSG, CMD_HISTORY ]
return complete_user_list(to_complete, i1, i2, project_opt)
elseif lastcommand in [ ] # file completition
end
return String[], 0:-1, false
end
function complete_user_list(to_complete, i1, i2, _)
global roomlist
l = filter(x -> startswith(x, to_complete), roomlist["joined_rooms"])
return l, i1:i2, true
end
function completions(full, index)::Tuple{Vector{String},UnitRange{Int},Bool}
# Full holds the whole line, while index the current cursor position!
pre = full[1:index]
# Returns all commands if first position
if isempty(pre)
return completion_cache.commands, 0:-1, false
end
x = parse(pre; for_completions=true)
if x === nothing # failed parse (invalid command name)
return String[], 0:-1, false
end
(key::Symbol, to_complete::String, spec, proj::Bool) = x
last = split(pre, ' ', keepempty=true)[end]
offset = isempty(last) ? index+1 : last.offset+1
if last != to_complete # require a space before completing next field
return String[], 0:-1, false
end
if key == :arg
return complete_argument(to_complete, offset, index, spec.kind, proj)
end
possible::Vector{String} =
key == :meta ? completion_cache.meta_options :
key == :cmd ? completion_cache.commands :
key == :sub ? completion_cache.subcommands[spec] :
key == :opt ? completion_cache.options[spec.kind] :
String[]
completions = filter(x->startswith(x,to_complete), possible)
return completions, offset:index, !isempty(completions)
end
prev_project_file = nothing
prev_project_timestamp = nothing
prev_prefix = ""
function promptf()
global user
# FIXME: If chatthing with peer, use the peer's name, else nothing
return string(user.userid, "> ")
end
# Set up the repl Pkg REPLMode
function create_mode(repl, main)
matrix_mode = LineEdit.Prompt(promptf;
prompt_prefix = Base.text_colors[:magenta],
prompt_suffix = "",
complete = MatrixCompletionProvider(),
sticky = true)
matrix_mode.repl = repl
hp = main.hist
hp.mode_mapping[:matrix] = matrix_mode
matrix_mode.hist = hp
search_prompt, skeymap = LineEdit.setup_search_keymap(hp)
prefix_prompt, prefix_keymap = LineEdit.setup_prefix_keymap(hp, matrix_mode)
matrix_mode.on_done = (s, buf, ok) -> begin
ok || return REPL.transition(s, :abort)
input = String(take!(buf))
REPL.reset(repl)
do_cmd(repl, input)
REPL.prepare_next(repl)
REPL.reset_state(s)
s.current_mode.sticky || REPL.transition(s, main)
end
mk = REPL.mode_keymap(main)
repl_keymap = Dict()
b = Dict{Any,Any}[
skeymap, repl_keymap, mk, prefix_keymap, LineEdit.history_keymap,
LineEdit.default_keymap, LineEdit.escape_defaults
]
matrix_mode.keymap_dict = LineEdit.keymap(b)
return matrix_mode
end
function repl_init(repl)
main_mode = repl.interface.modes[1]
matrix_mode = create_mode(repl, main_mode)
push!(repl.interface.modes, matrix_mode)
keymap = Dict{Any,Any}(
'/' => function (s,args...)
if isempty(s) || position(LineEdit.buffer(s)) == 0
buf = copy(LineEdit.buffer(s))
LineEdit.transition(s, matrix_mode) do
LineEdit.state(s, matrix_mode).input_buffer = buf
end
else
LineEdit.edit_insert(s, '/')
end
end
)
main_mode.keymap_dict = LineEdit.keymap_merge(main_mode.keymap_dict, keymap)
return
end
function load_config_file(; configfile::String = expanduser("~/matrix/config.jl"))
if isfile(configfile)
config = try
include(configfile)
catch e
@error "Malformed config file!"
throw(e)
end
else
# First run, create a new user/login and create the config file
@error "First run not implemented!"
end
config
end
function load_secret_or_login(config; secretfile = expanduser("~/matrix/secret.json"))
username = config["user"]["name"]
server = MatrixServer(config["user"]["server"])
if !isfile(secretfile)
# login
@info "Logging in..."
# FIXME: Don't know how to use a SecretBuffer
password = Base.getpass("@$(username):$(URI(server.instance).host)")
response = MatrixChat.login(server, username,
join(map(x -> read(seek(password, x), Char),
(1:password.size).-1),""))
write(secretfile, response.body |> String)
end
sec = JSON.parse(open(secretfile, "r"))
@info "Session loaded from secret"
token = sec["access_token"]
deviceid = sec["device_id"]
(server, username, token, deviceid)
end
"""Initialize the REPL mode, read config files and start the TUI client
"""
function repl()
global user, rooms_last_batch, syncstatus
userinfo = load_secret_or_login(load_config_file())
repl_init(Base.active_repl)
user = MatrixUser(userinfo...)
MatrixChat.updateroomlist(user)
syncstatus = MatrixSync(user)
rooms_last_batch = Dict()
updateroomlastbatch(sync!(syncstatus))
user
end
########
# SPEC #
########
command_declarations = [
["matrix"] => CommandDeclaration[
( CMD_MSG,
[ "msg", "send_message" ],
nothing,
(0=>Inf, identity, []),
[],
"send message to peer",
nothing
),
( CMD_HISTORY,
[ "history" ],
nothing,
(0=>Inf, identity, []),
[],
"show peer history",
nothing
),
( CMD_DIALOG_LIST,
[ "dialog_list" ],
nothing,
(0=>Inf, identity, []),
[],
"show available dialogs",
nothing
),
( CMD_LOAD_GROUP_PHOTO,
[ "load_chat_photo", "load_group_photo" ],
nothing,
(0=>Inf, identity, []),
[],
"Download a group's profile picture",
nothing
)
], #matrix
] #command_declarations
super_specs = SuperSpecs(command_declarations)
# cache things you need for completions
completion_cache.meta_options = sort(map(wrap_option, collect(keys(meta_option_specs))))
completion_cache.commands = sort(append!(collect(keys(super_specs)),
collect(keys(super_specs["matrix"]))))
let names = String[]
for (super, specs) in pairs(super_specs)
super == "matrix" && continue # skip "matrix"
for spec in unique(values(specs))
push!(names, join([super, spec.canonical_name], "-"))
end
end
for spec in unique(values(super_specs["matrix"]))
push!(names, spec.canonical_name)
end
completion_cache.canonical_names = names
sort!(completion_cache.canonical_names)
end
for (k, v) in pairs(super_specs)
completion_cache.subcommands[k] = sort(collect(keys(v)))
for spec in values(v)
completion_cache.options[spec.kind] =
sort(map(wrap_option, collect(keys(spec.option_specs))))
end
end
const help = md"""
**Welcome to the Matrix REPL-mode**. To return to the `julia>` prompt, either press
backspace when the input line is empty or press Ctrl+C.
**Synopsis**
matrix> cmd [opts] [args]
Multiple commands can be given on the same line by interleaving a `;` between the commands.
**Commands**
"""
#=
Get alias in the event list
curl 'https://chat.nixo.xyz/_matrix/client/r0/rooms/!maXyvEjllUwxzXPTua:nixo.xyz/state?access_token='
=#