class KV::Screen
Constants
- ANIMATION
- LINE_ATTR
- RenderStatus
Attributes
x[R]
Public Class Methods
new(input, lines: [], name: nil, search: nil, first_line: 0, following_mode: false, line_mode: false, separation_mode: false, time_stamp: nil, ext_input: nil, fifo_file: nil)
click to toggle source
# File lib/kv.rb, line 32 def initialize input, lines: [], name: nil, search: nil, first_line: 0, following_mode: false, line_mode: false, separation_mode: false, time_stamp: nil, ext_input: nil, fifo_file: nil @rs = RenderStatus.new @last_rs = nil @rs.y = first_line @rs.goto = first_line if first_line > 0 @rs.x = 0 @rs.last_lineno = 0 @rs.line_mode = line_mode @rs.search = search @rs.wrapping = false @rs.ts_mode = false @rs.separation_mode = separation_mode @name = name @filename = @name if @name && File.exist?(@name) @time_stamp = time_stamp @ext_input = ext_input @fifo_file = fifo_file @lines = lines @mode = :screen @following = following_mode @apos = 0 @mouse = false @search_ignore_case = false @search_regexp = true @loading = false @buffer_lines = 10_000 @yq = Queue.new if @filename @load_unlimited = true else @load_unlimited = false end @prev_render = {} @meta = input.respond_to?(:meta) ? input.meta : nil read_async input if input end
Public Instance Methods
cattr(attr) { || ... }
click to toggle source
# File lib/kv.rb, line 193 def cattr attr Curses.attron attr begin yield ensure Curses.attroff attr end end
check_update()
click to toggle source
# File lib/kv.rb, line 363 def check_update if @loading == false if @filename && File.exist?(@filename) && File.mtime(@filename) > @file_mtime input = open(@filename) if input.size < @file_lastpos screen_status "#{@filename} is truncated. Rewinded." pause @lineno = 0 else input.seek @file_lastpos end read_async input end end end
control()
click to toggle source
# File lib/kv.rb, line 720 def control case @mode when :screen control_screen when :terminal control_terminal else raise end end
control_screen()
click to toggle source
# File lib/kv.rb, line 501 def control_screen ev = render_screen case ev when 'q' raise PopScreen when Curses::KEY_UP, 'k' self.y -= 1 when Curses::KEY_DOWN, 'j' self.y += 1 when Curses::KEY_LEFT, 'h' self.x -= 1 when Curses::KEY_RIGHT, 'l' self.x += 1 when 'g' self.y = 0 self.x = 0 when 'G' self.y = self.y_max self.x = 0 when ' ', Curses::KEY_NPAGE, Curses::KEY_CTRL_D self.y += Curses.lines-1 when Curses::KEY_PPAGE, Curses::KEY_CTRL_U self.y -= Curses.lines-1 when /[0-9]/ screen_status "Goto:", ev ystr = input_str(/\d/, ev) if ystr && !ystr.empty? @rs.goto = ystr.to_i - 1 self.y = @rs.goto end when 'F' @following = true set_load_unlimited true when 'L' set_load_unlimited !@load_unlimited when '/' search_str = ''.dup update_search_status = -> do regexp = @search_regexp ? 'regexp' : 'string' ignore = @search_ignore_case ? '/ignore' : '' screen_status "Search[#{regexp}#{ignore}]:", search_str end update_search_status[] input_str(/./, search_str, other_actions: { Curses::KEY_CTRL_I => -> ev do @search_ignore_case = !@search_ignore_case update_search_status[] end, Curses::KEY_CTRL_R => -> ev do @search_regexp = !@search_regexp update_search_status[] end, }) if search_str && !search_str.empty? ic = @search_ignore_case ? [Regexp::IGNORECASE] : [] if @search_regexp begin @rs.search = Regexp.compile(search_str, *ic) rescue RegexpError => e @rs.search = nil screen_status "regexp compile error: #{e.message}" pause end else @rs.search = Regexp.compile(Regexp.escape(search_str), *ic) end else @rs.search = nil end if @rs.search @rs.search.instance_variable_set(:@search_str, search_str) search_next self.y end when 'n' search_next self.y+1 if @rs.search when 'p' search_prev self.y-1 if @rs.search when 'f' if @rs.search filter_mode_title = "*filter mode [#{self.search_str}]*" if @name != filter_mode_title lines = @lines.grep(@rs.search) fscr = Screen.new nil, lines: lines, search: @rs.search, name: filter_mode_title raise PushScreen.new(fscr) end end when 's' screen_status "Save file:" file = input_str /./ begin if file && !file.empty? if File.exist? file screen_status "#{file.dump} exists. Override? [y/n] " yn = input_str(/[yn]/) if yn == 'y' File.write(file, @lines.join("\n")) else # do nothing end else File.write(file, @lines.join("\n")) end end rescue SystemCallError # TODO: status line end when 'v' if @filename syste m("vi #{@filename} +#{self.y + 1}") @last_rs = nil end when 'P' begin if v = `gist -v` and /^gist v\d/ =~ v screen_status "gist-ing..." url = IO.popen('gist -p', 'a+'){|rw| @lines.each{|line| rw.puts line} rw.close_write rw.read } msg = "gist URL: #{url}" at_exit{ puts msg } screen_status msg pause else raise v.inspect end rescue Errno::ENOENT screen_status 'gist command is not found' pause end when 'm' @mouse = !@mouse Curses.close_screen init_screen when Curses::KEY_MOUSE m = Curses.getmouse log m, "mouse ->" # log [m.bstate, m.x, m.y, m.z, m.eid] log @lines[self.y + m.y] when 'N' @rs.line_mode = !@rs.line_mode when 'T' @rs.ts_mode = !@rs.ts_mode if @time_stamp when 'S' @rs.separation_mode = !@rs.separation_mode when 't' Curses.close_screen @mode = :terminal when 'x' while @ext_input && !@ext_input.closed? update_ext_status = -> str do screen_status "input for ext:", str end update_ext_status[''] actions = { update: -> str do self.y = self.y_max render_data update_ext_status[str] end } str = input_str(/./, other_actions: actions) if str && !str.empty? @ext_input.puts str unless @ext_input.closed? else break end end when 'H' if @meta lines = @meta.map{|k, v| "#{k}: #{v}"} raise PushScreen.new(Screen.new nil, lines: lines, name: "Response header [#{@name}]") end when Curses::KEY_CTRL_G # do nothing when '?' raise PushScreen.new(Screen.new help_io) when nil # ignore when Curses::KEY_RESIZE # ignore else screen_status "unknown: #{key_name(ev)}" pause end end
control_terminal()
click to toggle source
# File lib/kv.rb, line 712 def control_terminal @rs.instance_eval('binding').irb @mode = :screen init_screen @last_rs = nil end
ctimeout(ms) { || ... }
click to toggle source
# File lib/kv.rb, line 202 def ctimeout ms Curses.timeout = ms begin yield ensure Curses.timeout = -1 end end
init_screen()
click to toggle source
# File lib/kv.rb, line 168 def init_screen Curses.init_screen Curses.stdscr.keypad(true) if @mouse Curses.mousemask(Curses::BUTTON1_CLICKED | Curses::BUTTON2_CLICKED | Curses::BUTTON3_CLICKED | Curses::BUTTON4_CLICKED) else Curses.mousemask(0) end if @loading && self.y_max < @rs.y log [:going, self.y_max, @rs.y] @following = :going end self.y = @rs.y end
input_str(pattern, str = ''.dup, other_actions: {})
click to toggle source
# File lib/kv.rb, line 468 def input_str pattern, str = ''.dup, other_actions: {} update_action = other_actions[:update] ctimeout update_action ? 200 : -1 do loop do ev = Curses.getch case ev when 10 return str when Curses::KEY_BACKSPACE str.chop! when pattern str << ev when nil # timeout update_action[str] else if action = other_actions[ev] action.call(ev) else log "failure: #{key_name ev}" return nil end end end end end
key_name(ev)
click to toggle source
# File lib/kv.rb, line 461 def key_name ev Curses.constants.grep(/KEY/){|c| return c if Curses.const_get(c) == ev } ev end
pause()
click to toggle source
# File lib/kv.rb, line 496 def pause ev = Curses.getch Curses.ungetch ev if ev end
read_async(input)
click to toggle source
# File lib/kv.rb, line 86 def read_async input @loading = true begin data = input.read_nonblock(800_000) rescue IO::EAGAINWaitReadable, EOFError data = '' end last_line = nil data.each_line{|line| if line[-1] != "\n" last_line = line break end @lines << setup_line(line) } Thread.abort_on_exception = true @reader_thread = Thread.new do while line = input.gets if last_line line = last_line + line last_line = nil end @lines << setup_line(line) while !@load_unlimited && @lines.size > self.y + @buffer_lines @yq.pop; @yq.clear end @yq.clear end ensure if @filename @file_mtime = File.mtime(@filename) @file_lastpos = input.tell elsif @fifo_file input = open(@fifo_file) log(input) redo end input.close @loading = false end end
redraw!()
click to toggle source
# File lib/kv.rb, line 731 def redraw! @last_rs = nil end
render_data()
click to toggle source
# File lib/kv.rb, line 213 def render_data # check update c_lines = @rs.c_lines = Curses.lines c_cols = @rs.c_cols = Curses.cols if @rs != @last_rs @last_rs = @rs.dup else return end Curses.clear if @rs.separation_mode && (lines = @lines[self.y ... (self.y + c_lines - 1)]) max_cols = [] lines.each.with_index{|line, ln| line.split("\t").each_with_index{|w, i| max_cols[i] = max_cols[i] ? [max_cols[i], w.size].max : w.size } } end (c_lines-1).times{|i| lno = i + self.y line = @lines[lno] cols = c_cols unless line if lno == @lines.size Curses.setpos i, 0 cattr LINE_ATTR do Curses.addstr '(END)' end end break end Curses.setpos i, 0 if @rs.line_mode cattr LINE_ATTR do lineno = line.instance_variable_get(:@lineno) ln_str = '%5d |' % lineno if @rs.goto == lineno - 1 || (@rs.search && (@rs.search === line)) standout do Curses.addstr(ln_str) end else Curses.addstr(ln_str) end cols -= ln_str.size end end if @rs.ts_mode && ts = line.instance_variable_get(:@time_stamp) cattr LINE_ATTR do ts = line.instance_variable_get(:@time_stamp) Curses.addstr("#{ts} |") end end line = line[self.x, cols] || '' if @rs.separation_mode line = line.split(/\t/).tap{|e| if (max = max_cols.size) > 0 # fill empty columns e[max - 1] ||= nil end }.map.with_index{|w, i| "%-#{max_cols[i]}s" % w }.join(' | ') end if !@rs.search || !(Regexp === @rs.search) Curses.addstr line else partition(line, @rs.search).each{|(matched, str)| if matched == :match standout{ Curses.addstr str } else Curses.addstr str end } end } end
render_screen()
click to toggle source
# File lib/kv.rb, line 380 def render_screen ev = nil ms = @following ? 100 : 500 ctimeout ms do while ev == nil render_data render_status ev = Curses.getch check_update y_max = self.y_max if @following case @following when :searching break if search_next_move when :going if @rs.goto <= y_max self.y = @rs.goto break end when true # ok else raise "unknown following mode: #{@following}" end self.y = y_max end end @following = false set_load_unlimited false return ev end end
render_status()
click to toggle source
# File lib/kv.rb, line 316 def render_status name = @name ? "<#{@name}>" : '' mouse = @mouse ? ' [MOUSE]' : '' search = @rs.search ? " search[#{search_str}]" : '' loading = @loading ? " (loading...#{@load_unlimited ? '!' : nil}#{@following ? ' following' : ''}) " : '' x = self.x > 0 ? " x:#{self.x}" : '' screen_status "#{name} lines:#{self.y+1}/#{@lines.size}#{x}#{loading}#{search}#{mouse}" end
screen_status(status, post = nil)
click to toggle source
# File lib/kv.rb, line 342 def screen_status status, post = nil cols = Curses.cols line = Curses.lines-1 Curses.setpos line, 0 Curses.addstr ' '.ljust(cols) len = status.size len += post.size if post standout{ Curses.setpos Curses.lines-1, 0 Curses.addstr status } Curses.addstr post if post if !post && len < cols - ANIMATION.first.size Curses.setpos line, cols - ANIMATION.first.size - 1 @apos = (@apos + 1) % ANIMATION.size Curses.addstr ANIMATION[@apos] end end
search_next(start)
click to toggle source
# File lib/kv.rb, line 434 def search_next start @searching = start if search_next_move # OK. self.y is updated. else if @loading set_load_unlimited true @following = :searching else screen_status "not found: [#{self.search_str}]" pause @searching = false end end end
search_next_move()
click to toggle source
# File lib/kv.rb, line 419 def search_next_move last_line = @lines.size # log (@searching..last_line) (@searching...last_line).each{|i| if @rs.search === @lines[i] self.y = i @searching = false return true end } @searching = last_line return false end
search_prev(start)
click to toggle source
# File lib/kv.rb, line 450 def search_prev start start.downto(0){|i| if @rs.search === @lines[i] self.y = i return true end } screen_status "not found: [#{self.search_str}]" pause end
search_str()
click to toggle source
# File lib/kv.rb, line 304 def search_str if @rs.search if str = @rs.search.instance_variable_get(:@search_str) str else @rs.search.inspect end else nil end end
set_load_unlimited(b)
click to toggle source
# File lib/kv.rb, line 163 def set_load_unlimited b @load_unlimited = b @yq << true end
setup_line(line)
click to toggle source
# File lib/kv.rb, line 79 def setup_line line line = line.chomp line.instance_variable_set(:@time_stamp, Time.now.strftime('%H:%M:%S')) if @time_stamp line.instance_variable_set(:@lineno, @rs.last_lineno += 1) line end
standout() { || ... }
click to toggle source
# File lib/kv.rb, line 187 def standout Curses.standout yield Curses.standend end
x=(x)
click to toggle source
# File lib/kv.rb, line 154 def x=(x) @rs.x = x @rs.x = 0 if @rs.x < 0 end
y()
click to toggle source
# File lib/kv.rb, line 137 def y @rs.y end
y=(y)
click to toggle source
# File lib/kv.rb, line 141 def y=(y) if y > (ym = self.y_max) @rs.y = ym else @rs.y = y end @rs.y = 0 if @rs.y < 0 @yq << nil if @loading end
y_max()
click to toggle source
# File lib/kv.rb, line 132 def y_max max = @lines.size - Curses.lines + 2 max < 0 ? 0 : max end