Skip to content

Add a curses-based console #45

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions lib/kaitai/console_ansi.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,21 @@ def clear
print @seq_clear
end

def refresh
end

def print(*args)
Kernel.print(*args)
end

def puts(*args)
Kernel.puts(*args)
end

def readline
Readline.readline('', false)
end

# Put the cursor up to screen position (x, y). First line is 0, first column is 0.
def goto(x, y)
# print `tput cup #{y} #{x}`
Expand Down
148 changes: 148 additions & 0 deletions lib/kaitai/console_curses.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
require 'curses'

module Kaitai
class ConsoleCurses
def initialize
Curses.init_screen
Curses.start_color
Curses.use_default_colors
Curses.stdscr.keypad = true
ObjectSpace.define_finalizer(self, proc { Curses.close_screen })

@on_resize = nil
@color_pairs = {}
@fg_color = COLORS[:default]
@bg_color = COLORS[:default]
update_color
end

attr_writer :on_resize

def rows
Curses.lines
end

def cols
Curses.cols
end

private def update_color
color = @color_pairs[[@fg_color, @bg_color]]
if color.nil? then
color = @color_pairs.size
@color_pairs[[@fg_color, @bg_color]] = color
Curses.init_pair(color, @fg_color, @bg_color)
end
Curses.attrset(Curses.color_pair(color))
end

def fg_color=(col)
code = COLORS[col]
raise "Invalid color: #{col}" unless code
@fg_color = code
update_color
end

def bg_color=(col)
code = COLORS[col]
raise "Invalid color: #{col}" unless code
@bg_color = code
update_color
end

def reset_colors
@fg_color = COLORS[:default]
@bg_color = COLORS[:default]
update_color
end

def read_char
loop do
char = Curses.getch
if char == Curses::Key::RESIZE then
@on_resize&.call(true)
else
return char
end
end
end

def read_char_mapped
c = read_char
KEY_MAP[c] || c
end

def goto(x, y)
Curses.setpos(y, x)
end

def clear
Curses.clear
end

def refresh
Curses.refresh
end

def readline
Curses.getstr
end

def print(*args)
args.each do |arg|
Curses.addstr(arg)
end
end

def puts(*args)
Curses.addstr("\n") if args.empty?
args.each do |arg|
Curses.addstr(arg)
Curses.addstr("\n")
end
end

COLORS = {
default: -1,
black: 0,
gray: 7,
gray0: 232,
gray1: 233,
gray2: 234,
gray3: 235,
gray4: 236,
gray5: 237,
gray6: 238,
gray7: 239,
gray8: 240,
gray9: 241,
gray10: 242,
gray11: 243,
gray12: 244,
gray13: 245,
gray14: 246,
gray15: 247,
gray16: 248,
gray17: 249,
gray18: 250,
gray19: 251,
gray20: 252,
gray21: 253,
gray22: 254,
gray23: 255,
}.freeze

KEY_MAP = {
9 => :tab,
10 => :enter,
Curses::Key::UP => :up_arrow,
Curses::Key::DOWN => :down_arrow,
Curses::Key::LEFT => :left_arrow,
Curses::Key::RIGHT => :right_arrow,
Curses::Key::PPAGE => :pg_up,
Curses::Key::NPAGE => :pg_dn,
Curses::Key::HOME => :home,
Curses::Key::END => :end,
}.freeze
end
end
15 changes: 13 additions & 2 deletions lib/kaitai/console_windows.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,24 @@ def load_term_size

attr_writer :on_resize

def puts(s)
Kernel.puts s
def refresh
end

def print(*args)
Kernel.print(*args)
end

def puts(*args)
Kernel.puts(*args)
# num_written = 'XXXX'
# reserved = 'XXXX'
# WRITE_CONSOLE.call(@stdout_handle, s, s.length, num_written, reserved)
end

def readline
Readline.readline('', false)
end

def clear
con_size = @buf_cols * @buf_rows
num_written = 'XXXX'.dup
Expand Down
14 changes: 9 additions & 5 deletions lib/kaitai/struct/visualizer/hex_viewer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,18 @@ def initialize(ui, buf, tree = nil)
@tree = tree

@embedded = !tree.nil?
@max_scr_ln = @ui.rows - 3
recalc_sizes

@addr = 0
@scroll_y = 0
reset_cur
raise if @cur_x.nil?
end

def recalc_sizes
@max_scr_ln = @ui.rows - 3
end

attr_writer :buf

attr_reader :addr
Expand Down Expand Up @@ -58,7 +62,7 @@ def run
c = nil
loop do
@ui.goto(0, @max_scr_ln + 1)
printf '%08x (%d, %d)', @addr, @cur_x, @cur_y
@ui.printf '%08x (%d, %d)', @addr, @cur_x, @cur_y

@ui.goto(col_to_col_char(@cur_x), row_to_scr(@cur_y))
c = @ui.read_char_mapped
Expand Down Expand Up @@ -181,7 +185,7 @@ def redraw
hex = line.bytes.map { |x| format('%02x', x) }.join(' ')
char = line.bytes.map { |x| byte_to_display_char(x) }.join

printf FMT, i, hex, char
@ui.printf FMT, i, hex, char
i += PER_LINE
row += 1
end
Expand Down Expand Up @@ -237,7 +241,7 @@ def highlight_draw_hex(r, c, i, p2)
v = byte_at(i)
return if v.nil?

printf('%02x ', v)
@ui.printf('%02x ', v)
c += 1
if c >= PER_LINE
c = 0
Expand All @@ -257,7 +261,7 @@ def highlight_draw_char(r, c, i, p2)
v = byte_at(i)
return if v.nil?

print byte_to_display_char(v)
@ui.print byte_to_display_char(v)
c += 1
if c >= PER_LINE
c = 0
Expand Down
22 changes: 11 additions & 11 deletions lib/kaitai/struct/visualizer/node.rb
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ def close
end

def draw(_ui)
print ' ' * level
print(if @value.nil?
_ui.print ' ' * level
_ui.print(if @value.nil?
'[?]'
elsif open?
'[-]'
Expand All @@ -86,17 +86,17 @@ def draw(_ui)
else
'[.]'
end)
print " #{@id}"
_ui.print " #{@id}"

pos = 2 * level + 4 + @id.length

if open? || !openable?
if @value.is_a?(Float) || @value.is_a?(Integer)
print " = #{@value}"
_ui.print " = #{@value}"
elsif @value.is_a?(Symbol)
print " = #{@value}"
_ui.print " = #{@value}"
elsif @value.is_a?(String)
print ' = '
_ui.print ' = '
pos += 3
@str_mode ||= detect_str_mode
max_len = @tree.tree_width - pos
Expand All @@ -119,17 +119,17 @@ def draw(_ui)
s = s[0, max_len - 1] || ''
s += '…'
end
print s
_ui.print s
elsif (@value == true) || (@value == false)
print " = #{@value}"
_ui.print " = #{@value}"
elsif @value.nil?
print ' = null'
_ui.print ' = null'
elsif @value.is_a?(Array)
printf ' (%d = 0x%x entries)', @value.size, @value.size
_ui.printf ' (%d = 0x%x entries)', @value.size, @value.size
end
end

puts
_ui.puts
end

def first_n_bytes_dump(s, n)
Expand Down
6 changes: 4 additions & 2 deletions lib/kaitai/struct/visualizer/tree.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ def initialize(ui, st)

@ui.on_resize = proc { |redraw_needed|
recalc_sizes
@hv.recalc_sizes
redraw if redraw_needed
@hv.redraw if redraw_needed
}
Expand Down Expand Up @@ -68,9 +69,10 @@ def run
end

@ui.goto(0, @max_scr_ln + 1)
printf 'all: %d, tree: %d, tree_draw: %d, hexview: %d, ln: %d, ', (t + thv) * 1e6, t * 1e6, @draw_time * 1e6, thv * 1e6, @ln
puts "highlight = #{@cur_node.pos1}..#{@cur_node.pos2}"
@ui.printf 'all: %d, tree: %d, tree_draw: %d, hexview: %d, ln: %d, ', (t + thv) * 1e6, t * 1e6, @draw_time * 1e6, thv * 1e6, @ln
@ui.puts "highlight = #{@cur_node.pos1}..#{@cur_node.pos2}"
# puts "keypress: #{c.inspect}"
@ui.refresh

begin
process_keypress
Expand Down
17 changes: 13 additions & 4 deletions lib/kaitai/tui.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
module Kaitai
class TUI
extend Forwardable
def_delegators :@console, :rows, :cols, :goto, :clear, :fg_color=, :bg_color=, :reset_colors, :read_char_mapped
def_delegators :@console, :rows, :cols, :goto, :clear, :fg_color=, :bg_color=, :reset_colors, :read_char_mapped, :refresh, :print, :puts

attr_reader :highlight_colors

Expand All @@ -15,8 +15,13 @@ def initialize
@console = ConsoleWindows.new
@highlight_colors = %i[white aqua blue green white]
else
require 'kaitai/console_ansi'
@console = ConsoleANSI.new
begin
require 'kaitai/console_curses'
@console = ConsoleCurses.new
rescue LoadError
require 'kaitai/console_ansi'
@console = ConsoleANSI.new
end
@highlight_colors = %i[gray14 gray11 gray8 gray5 gray2]
end
end
Expand Down Expand Up @@ -61,7 +66,7 @@ def input_str(header, _msg)
print ' ', header, ' '

goto(11, top_y + 1)
Readline.readline('', false)
@console.readline
end

def draw_rectangle(x, y, w, h, charset = DOUBLE_CHARSET)
Expand All @@ -88,6 +93,10 @@ def draw_button(x, y, _w, caption)
puts "[ #{caption} ]"
end

def printf(*args)
@console.print sprintf(*args)
end

# Regexp borrowed from
# http://stackoverflow.com/questions/170956/how-can-i-find-which-operating-system-my-ruby-program-is-running-on
@@is_windows = (RUBY_PLATFORM =~ /cygwin|mswin|mingw|bccwin|wince|emx/) ? true : false
Expand Down