Class: Agentilda::Keyboard

Inherits:
Object
  • Object
show all
Defined in:
lib/agentilda/keyboard.rb

Overview

Listens for single keypresses while a run is in flight, and turns them into Control broadcasts.

The listener is a thread reading STDIN in raw mode, which only makes sense when STDIN is a terminal: piped input must never be eaten one byte at a time, and a run under cron has nobody at the keys. Keyboard.listen returns nil in both cases and every caller treats nil as "no keyboard".

Raw mode swallows Ctrl-C along with everything else, so ETX is forwarded to the main thread as the Interrupt it would have been — the listener must never make a run harder to kill than it was without one.

Constant Summary collapse

BINDINGS =

Key → what it does, rendered by #help and dispatched by #handle.

[
  ["h  ?", "this help"],
  ["w", "ask every running agent to wrap up as fast as possible"],
  ["n", "ask agents to write out what they have and stop; the loop continues"],
  ["q", "write out, stop everything, and quit after a #{Control::GRACE}s grace"],
  ["ctrl-c", "interrupt the run, as ever"]
].freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(input: $stdin) ⇒ Keyboard

Returns a new instance of Keyboard.

Parameters:

  • input (IO) (defaults to: $stdin)


36
37
38
# File 'lib/agentilda/keyboard.rb', line 36

def initialize(input: $stdin)
  @input = input
end

Class Method Details

.listen(input: $stdin) ⇒ Agentilda::Keyboard?

Returns a running listener, or nil when STDIN is not a terminal.

Returns:



29
30
31
32
33
# File 'lib/agentilda/keyboard.rb', line 29

def self.listen(input: $stdin)
  return nil unless input.tty?

  new(input:).start
end

Instance Method Details

#handle(key) ⇒ void

This method returns an undefined value.

Parameters:

  • key (String, nil)


59
60
61
62
63
64
65
66
67
# File 'lib/agentilda/keyboard.rb', line 59

def handle(key)
  case key
  when "h", "?" then UI.popup("Keys", help)
  when "w" then acted("w — agents asked to wrap up") { Control.wrap_up! }
  when "n" then acted("n — agents asked to write out and stop") { Control.stop! }
  when "q" then acted("q — quitting; #{Control::GRACE}s grace to write out") { Control.quit! }
  when "\u0003" then Thread.main.raise(Interrupt)
  end
end

#helpString

Returns the bindings, one per line, widest key first.

Returns:

  • (String)

    the bindings, one per line, widest key first



70
71
72
73
# File 'lib/agentilda/keyboard.rb', line 70

def help
  width = BINDINGS.map { |key, _| key.length }.max
  BINDINGS.map { |key, does| "#{key.ljust(width)}   #{does}" }.join("\n")
end

#startself

Returns:

  • (self)


41
42
43
44
45
46
47
48
49
# File 'lib/agentilda/keyboard.rb', line 41

def start
  @thread = Thread.new do
    Thread.current.report_on_exception = false
    loop { handle(@input.getch) }
  rescue IOError, Errno::EIO
    # STDIN went away; a keyboard with no keys just stops listening.
  end
  self
end

#stopvoid

This method returns an undefined value.



52
53
54
55
# File 'lib/agentilda/keyboard.rb', line 52

def stop
  @thread&.kill
  @thread = nil
end