267 lines
8.0 KiB
Ruby
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
|
|
|