howl.command

local cmd, readline

before_each ->
  readline = Spy as_null_object: true
  app.window = :readline
  cmd = name: 'foo', description: 'desc', handler: spy.new -> true

after_each ->
  command.unregister name for name in *command.names!
  app.window = nil

.names() returns a list of all command names

command.register cmd
assert.includes command.names!, 'foo'

.<name> allows direct indexing of commands

command.register cmd
assert.equal command.foo.handler, cmd.handler

.get(name) returns the command with the specified name

command.register cmd
assert.equal command.get('foo').handler, cmd.handler

allows a registered command to be invoked directly

cmd.handler = (num) -> num * 2
command.register cmd
assert.equal command.foo(2), 4

.unregister(command) removes the command and any aliases

command.register cmd
command.alias 'foo', 'bar'
command.unregister 'foo'

assert.is_nil command.foo
assert.is_nil command.bar
assert.same command.names!, {}

.register(command)

raises an error if any of the mandatory fields are missing

assert.raises 'name', -> command.register {}
assert.raises 'description', -> command.register name: 'foo'
assert.raises 'handler', -> command.register name: 'foo', description: 'do'

.alias(target, name)

raises an error if target does not exist

assert.raises 'exist', -> command.alias 'nothing', 'something'

allows for multiple names for the same command

command.register cmd
command.alias 'foo', 'bar'
assert.equal 'foo', command.bar
assert.includes command.names!, 'bar'

(when command name is a non-lua identifier)

before_each -> cmd.name = 'foo-cmd:bar'

register() adds accessible aliases for the direct indexing

command.register cmd
assert.equal command['foo-cmd:bar'].handler, cmd.handler
assert.equal command.foo_cmd_bar.handler, cmd.handler

the accessible alias is not part of names()

command.register cmd
assert.same command.names!, { 'foo-cmd:bar' }

unregister() removes the accessible name as well

command.register cmd
command.unregister 'foo-cmd:bar'
assert.is_nil command.foo_cmd_bar

.run(cmd_string)

local first_input, second_input

before_each ->
  inputs.register 'test_first', ->
    first_input = {
      complete: -> 'completions'
      should_complete: -> 'perhaps'
      close_on_cancel: -> true
      value_for: -> 123
      on_completed: Spy!
      go_back: Spy!
      on_cancelled: Spy!
    }
    first_input

  inputs.register 'test_second', ->
    second_input = {
      complete: -> 'other completions'
      should_complete: -> 'oh yes'
      close_on_cancel: -> false
      on_completed: Spy!
      go_back: Spy!
      on_cancelled: Spy!
    }
    second_input

  inputs.register 'dummy', -> {}

after_each ->
  inputs.unregister 'test_first'
  inputs.unregister 'test_second'
  inputs.unregister 'dummy'

(when <cmd_string> is empty or missing)

invokes howl.app.window.readline with a ":" prompt

command.run!
assert.is_true readline.read.called
_, prompt = unpack readline.read.called_with
assert.equal prompt, ':'

(when <cmd_string> is given)

(.. and it matches a simple command without parameters)

that command is invoked direcly

cmd.handler = Spy!
command.register cmd
command.run cmd.name
assert.is_true cmd.handler.called

(.. when it specifies a command with all required parameters)

that command is invoked directly with converted values

cmd.handler = Spy!
cmd.inputs = { 'test_first' }
command.register cmd
command.run cmd.name .. ' foo'
assert.same cmd.handler.called_with, { first_input.value_for! }

(.. when it specifies a command without all required parameters)

invokes howl.app.window.readline with the prompt set to the given string

cmd.inputs = { 'test_first', 'test_second' }
command.register cmd
command.run cmd.name .. ' arg'
assert.is_true readline.read.called
_, prompt = unpack readline.read.called_with
assert.equal prompt, ':' .. cmd.name .. ' arg '

(.. when it specifies a unknown command)

invokes readline.read with the text set to the given string

command.run 'what-the-heck now'
assert.is_true readline.read.called
assert.equals 'what-the-heck now', readline.read.called_with[4].text

(parameter parsing)

separates arguments on whitespace

cmd.inputs = { 'dummy', 'dummy' }
command.register cmd
command.run "#{cmd.name} first second"
assert.spy(cmd.handler).was.called_with('first', 'second')
command.run "#{cmd.name} tab1\ttab2"
assert.spy(cmd.handler).was.called_with('tab1', 'tab2')

treats the input as a wildcard input if it is prefixed with "*"

cmd.inputs = { 'dummy', '*dummy' }
command.register cmd
command.run "#{cmd.name} first second / third"
assert.spy(cmd.handler).was.called_with('first',  'second / third')

cmd.inputs = { '*dummy' }
command.register cmd
command.run "#{cmd.name} first second / third"
assert.spy(cmd.handler).was.called_with('first second / third')

accepts function values as inputs"

input = value_for: -> 'yay'
cmd.inputs = { -> input }
command.register cmd
command.run "#{cmd.name} arg"
assert.spy(cmd.handler).was.called_with('yay')

cmd.inputs = { '*dummy' }
command.register cmd
command.run "#{cmd.name} first second / third"
assert.spy(cmd.handler).was.called_with('first second / third')

only allows the last input to be a wildcard

cmd.inputs = { '*dummy', 'dummy' }
assert.raises 'Wildcard', -> command.register cmd

(interacting with readline)

local input, readline, handler, co, return_value

run = (...) ->
  f = coroutine.wrap (...) -> command.run ...
  return_value = f ...

fake_return = (...) ->
  coroutine.resume co, ...

before_each ->
  readline = read: (prompt, i) =>
    co = coroutine.running!
    input = i
    @prompt = prompt
    @text = ''
    coroutine.yield!

  app.window = :readline

  handler = spy.new -> nil

  command.register {
    name: 'p_cmd',
    description: 'desc',
    :handler
    inputs: { 'test_first', 'test_second' }
  }

after_each -> command.unregister name for name in *command.names!

(.. when entering a command)

should_complete(..) returns false

run!
assert.is_false input\should_complete '', readline

complete(..) returns a list of command names, key bindings, and descriptions

keymap.ctrl_shift_p = 'p_cmd'
run!
completions = input\complete '', readline
assert.same completions, { { 'p_cmd', 'ctrl_shift_p', 'desc' } }

(.. when entering command arguments)

delegates all input methods to the current corresponding input

run!
input\update 'p_cmd first', readline
assert.not_nil first_input
assert.equal input\complete(readline.text, readline), first_input.complete!
assert.equal input\should_complete(readline.text, readline), first_input.should_complete!
assert.equal input\close_on_cancel(readline.text, readline), first_input.close_on_cancel!

input\on_completed(readline.text, readline)
assert.is_true first_input.on_completed.called

input\go_back(readline)
assert.is_true first_input.go_back.called

readline.text ..= ' second'
input\update readline.text, readline
assert.not_nil second_input
assert.equal input\complete(readline.text, readline), second_input.complete!
assert.equal input\should_complete(readline.text, readline), second_input.should_complete!
assert.equal input\close_on_cancel(readline.text, readline), second_input.close_on_cancel!

input\on_completed(readline.text, readline)
assert.is_true second_input.on_completed.called

input\go_back(readline)
assert.is_true second_input.go_back.called

calls on_cancelled on all instantiated inputs when the user cancels

run!
input\update 'p_cmd first', readline
fake_return nil
assert.is_true first_input.on_cancelled.called
assert.is_false second_input.on_cancelled.called

updates the readline prompt to include arguments when they are finished

run!
input\update 'p_cmd first', readline
assert.match readline.prompt, 'p_cmd $'
readline.text ..= ' second'
input\update readline.text, readline
assert.match readline.prompt, 'p_cmd first $'

runs the command when the user submits

run!
readline.text = 'p_cmd first final'
input\update readline.text, readline
readline.text = 'final'
fake_return 'final'
assert.spy(handler).was_called_with first_input\value_for!, 'final'

(.. with a wildcard input)

before_each ->
  cmd.inputs = { '*dummy' }
  command.register cmd
  run!

does not update the readline prompt to include partial arguments

input\update "#{cmd.name} first second third", readline
assert.match readline.prompt, "#{cmd.name} $"

handles multiple updates when typing

readline.text = "#{cmd.name} first second third"
input\update readline.text, readline
input\update readline.text, readline
assert.match readline.prompt, "#{cmd.name} $"
fake_return readline.text
assert.spy(cmd.handler).was_called_with readline.text

(.. when the user submits an unknown command)

the on_submit callback returns false to keep readline open

run!
assert.is_false input\on_submit 'unknowncommand', readline

(.. when the user submits a known command but with too few arguments)

the on_submit callback returns false to keep readline open

run!
assert.is_false input\on_submit 'p_cmd', readline

the callback adds command to the readline prompt

run!
readline.text = 'p_cmd'
input\update readline.text, readline
input\on_submit 'p_cmd', readline
assert.match readline.prompt, 'p_cmd $'