882 lines
28 KiB
Julia
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='
|
|
=#
|