Files
meow/lib/meow/session.rb
T

267 lines
8.0 KiB
Ruby

module MeowClient
class Session
MAX_LINES = 5_000
MAX_BYTES = 1_048_576
MAX_HISTORY = 100
MAX_SPEECH = 200
SPEECH_CHUNK = 600
PASSWORD_PATTERN = /\b(?:password|passphrase|secret|token|pin)\b.*[:?>]\s*\z/i
def initialize(program, profile, transport)
@program = program
@profile = profile
@transport = transport
@pipeline = TextPipeline.new(profile.encoding)
@lines = []
@line_bytes = 0
@history = []
@history_index = nil
@speech_queue = []
@speech_paused = profile.auto_read == false
@speech_overflow = 0
@sensitive_next = false
@closed = false
build_form
end
def run
@form.wait
ensure
@pipeline.close
@transport.close
@program.release(@transport, :close => false)
end
private
def build_form
@status = Static.new(_("Connected to %{name}.") % {:name => @profile.name})
@output = EditBox.new(
_("MUD output"),
:type => EditBox::Flags::ReadOnly | EditBox::Flags::MultiLine,
:text => "",
:detect_text_links => false
)
@command = EditBox.new(_("Command"), :text => "")
@send = Button.new(_("Send"))
@speech_button = Button.new(@speech_paused ? _("Resume automatic reading") : _("Pause automatic reading"))
@stop_speech = Button.new(_("Stop current speech"))
@disconnect = Button.new(_("Disconnect"))
@form = Form.new(
[@status, @output, @command, @send, @speech_button, @stop_speech, @disconnect],
:quiet => true
)
@form.accept_button = @send
@form.cancel_button = @disconnect
@form.add_timer(FormTimer.new(0.05, :repeat => true) { tick })
@send.on(:press) { send_command }
@speech_button.on(:press) { toggle_speech }
@stop_speech.on(:press) { speech_stop }
@disconnect.on(:press) { request_disconnect }
@output.add_tip(_("Use the context menu to jump to the latest output."))
@output.bind_context do |menu|
menu.option(_("Jump to latest output")) { jump_to_latest }
end
@command.add_tip(_("Use the context menu to recall commands or mark the next command as sensitive."))
@command.bind_context do |menu|
menu.option(_("Previous command")) { recall(-1) }
menu.option(_("Next command")) { recall(1) }
menu.option(_("Sensitive command")) { set_sensitive(true) }
menu.option(_("Resume reading from latest output")) { resume_from_latest }
end
add_system_line(_("Connected to %{host} on port %{port}.") % {
:host => @profile.host,
:port => @profile.port
})
end
def tick
drain_events
if @pipeline.prompt_due?
record = @pipeline.flush_prompt
receive_record(record) if record != nil
end
dispatch_speech
rescue Telnet::CompressionError
add_system_line(_("The compressed data stream could not be processed safely. The connection was closed."))
close_form
rescue Exception => error
Log.error("Meow session update failed: #{error.class}: #{error.message}") if defined?(Log)
add_system_line(_("Session update error: %{message}") % {:message => error.message})
close_form
end
def drain_events
loop do
event = @transport.events.pop(true)
case event.type
when :bytes
result = @pipeline.feed(event.payload)
result.replies.each { |reply| @transport.send_bytes(reply) }
result.records.each { |record| receive_record(record) }
when :disconnected, :error
@pipeline.finish.each { |record| receive_record(record) }
add_system_line(event.payload.to_s)
close_form
end
end
rescue ThreadError
end
def receive_record(record)
text = record.to_s
append_line(text)
@sensitive_next = true if text.strip.match?(PASSWORD_PATTERN)
apply_sensitive_flag
enqueue_speech(text)
end
def append_line(text)
value = text.to_s
@lines << value
@line_bytes += value.bytesize + 1
while @lines.size > MAX_LINES || @line_bytes > MAX_BYTES
removed = @lines.shift.to_s
@line_bytes -= removed.bytesize + 1
end
@output.set_text(@lines.join("\n"), false)
end
def add_system_line(text)
append_line("[#{text}]")
end
def send_command
text = @command.text.to_s
return if text == ""
begin
encoded = text.encode(@profile.encoding)
rescue Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError
alert(_("This command contains characters that cannot be represented by %{encoding}.") % {
:encoding => @profile.encoding
})
return
end
unless @transport.send_bytes(encoded.b + "\r\n".b)
alert(_("The connection is no longer available."))
close_form
return
end
sensitive = @sensitive_next
unless sensitive
add_history(text)
append_line("> #{text}") unless @pipeline.remote_echo?
end
@command.set_text("")
@history_index = nil
set_sensitive(false, false)
end
def add_history(text)
@history.delete(text)
@history << text
@history.shift while @history.size > MAX_HISTORY
end
def recall(direction)
return alert(_("No command history is available."), false) if @history.empty?
if @history_index == nil
@history_index = direction < 0 ? @history.size - 1 : 0
else
@history_index = [[@history_index + direction, 0].max, @history.size - 1].min
end
@command.set_text(@history[@history_index])
@command.index = @command.text.length
@command.check = @command.index
set_sensitive(false, false)
@form.index = @command
@command.focus
end
def set_sensitive(value, announce = true)
@sensitive_next = value == true
apply_sensitive_flag
if announce
speak(@sensitive_next ? _("Sensitive input enabled.") : _("Sensitive input disabled."))
end
end
def apply_sensitive_flag
if @sensitive_next
@command.flags |= EditBox::Flags::Password
else
@command.flags &= ~EditBox::Flags::Password
end
end
def enqueue_speech(text)
return if text.strip == ""
chunks = split_speech(text)
chunks.each do |chunk|
if @speech_queue.size >= MAX_SPEECH
@speech_queue.shift
@speech_overflow += 1
end
@speech_queue << chunk
end
end
def split_speech(text)
value = text.to_s.strip
return [] if value == ""
value.scan(/.{1,#{SPEECH_CHUNK}}/m).map(&:strip).reject(&:empty?)
end
def dispatch_speech
return if @speech_paused || speech_actived
if @speech_overflow > 0
count = @speech_overflow
@speech_overflow = 0
speak(n_("%{count} older line was skipped.", "%{count} older lines were skipped.", count) % {:count => count})
return
end
text = @speech_queue.shift
speak(text, :stop => false) if text != nil
end
def toggle_speech
@speech_paused = !@speech_paused
@speech_button.label = @speech_paused ? _("Resume automatic reading") : _("Pause automatic reading")
speak(@speech_paused ? _("Automatic reading paused.") : _("Automatic reading resumed."))
end
def resume_from_latest
@speech_queue.clear
@speech_overflow = 0
@speech_paused = false
@speech_button.label = _("Pause automatic reading")
speak(_("Automatic reading resumed from the latest output."))
end
def jump_to_latest
position = @output.text.length
@output.index = position
@output.check = position
@form.index = @output
@output.focus
end
def request_disconnect
if @closed || confirm(_("Disconnect from %{name}?") % {:name => @profile.name})
add_system_line(_("Disconnected."))
close_form
end
end
def close_form
return if @closed
@closed = true
@transport.close
@form.resume
end
end
end