├── lib ├── rutui │ ├── radio.rb │ ├── selectbox.rb │ ├── pixel.rb │ ├── theme.rb │ ├── ansi.rb │ ├── sprites.rb │ ├── axx.rb │ ├── screenmanager.rb │ ├── checkbox.rb │ ├── textfield.rb │ ├── screen.rb │ ├── figlet.rb │ ├── input.rb │ ├── table.rb │ └── objects.rb └── rutui.rb ├── .gitignore ├── Gemfile ├── examples ├── checkbox-radio.rb ├── colors.rb ├── circle.rb ├── res │ ├── space-invader.axx │ ├── space-invader_sprite.axx │ └── colossal.flf ├── logo.rb ├── sprites.rb ├── textfield.rb ├── hello-input.rb ├── hello-world.rb ├── sortable-table.rb └── tic-tac-toe.rb ├── rutui.gemspec ├── test └── objects │ └── line_test.rb └── README.md /lib/rutui/radio.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | -------------------------------------------------------------------------------- /lib/rutui/selectbox.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in rutui.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /examples/checkbox-radio.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | lib_dir = File.dirname(__FILE__) + '/../lib' 4 | $LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir) 5 | 6 | require 'rutui' 7 | 8 | -------------------------------------------------------------------------------- /examples/colors.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | lib_dir = File.dirname(__FILE__) + '/../lib' 4 | $LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir) 5 | 6 | require 'rutui' 7 | 8 | 255.times do |i| 9 | print "#{RuTui::Ansi.fg(i)}#{i}\t" 10 | end 11 | 12 | print RuTui::Ansi.clear_color 13 | -------------------------------------------------------------------------------- /examples/circle.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | lib_dir = File.dirname(__FILE__) + '/../lib' 4 | $LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir) 5 | 6 | require 'rutui' 7 | 8 | radius = ARGV[0].to_i 9 | radius = 6 if radius.nil? 10 | 11 | screen = RuTui::Screen.new RuTui::Pixel.new(nil,nil," ") 12 | screen.add RuTui::Circle.new({ :x => 2, :y => 2, :radius => radius, :pixel => RuTui::Pixel.new(42,230,"%") }) 13 | screen.draw 14 | -------------------------------------------------------------------------------- /lib/rutui.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # 3 | # https://github.com/b1nary/rutui 4 | # 5 | # Author: Roman Pramberger (roman@pramberger.ch) 6 | # License: MIT 7 | # 8 | require 'rutui/ansi' 9 | require 'rutui/pixel' 10 | require 'rutui/theme' 11 | require 'rutui/objects' 12 | require 'rutui/input' 13 | require 'rutui/screen' 14 | require 'rutui/screenmanager' 15 | require 'rutui/figlet' 16 | require 'rutui/axx' 17 | require 'rutui/sprites' 18 | require 'rutui/table' 19 | require 'rutui/textfield' 20 | require 'rutui/checkbox' 21 | -------------------------------------------------------------------------------- /rutui.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = 'rutui' 3 | s.version = '0.7.3' 4 | s.summary = "RUby Textbased User Interface" 5 | s.description = "Create Pure Ruby textbased interfaces of all kinds (Unix only)" 6 | s.license = "MIT" 7 | s.authors = ["Roman Pramberger"] 8 | s.email = 'roman@pramberger.ch' 9 | s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 10 | s.homepage = 'http://rubygems.org/gems/rutui' 11 | s.required_ruby_version = '>= 2.0.0' 12 | end 13 | -------------------------------------------------------------------------------- /examples/res/space-invader.axx: -------------------------------------------------------------------------------- 1 | # Space Invader ~ Example image format 2 | nil|nil| :0:10|nil|nil|nil|nil|nil| :0:10|nil|nil 3 | nil|nil|nil| :0:10|nil|nil|nil| :0:10|nil|nil|nil 4 | nil|nil| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10|nil|nil 5 | nil| :0:10| :0:10|nil| :0:10| :0:10| :0:10|nil| :0:10| :0:10|nil 6 | :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10 7 | :0:10|nil| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10|nil| :0:10 8 | :0:10|nil| :0:10|nil|nil|nil|nil|nil| :0:10|nil| :0:10 9 | nil|nil|nil| :0:10| :0:10|nil| :0:10| :0:10|nil|nil|nil 10 | 11 | -------------------------------------------------------------------------------- /examples/logo.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | lib_dir = File.dirname(__FILE__) + '/../lib' 4 | $LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir) 5 | 6 | require 'rutui' 7 | 8 | res_dir = File.dirname(__FILE__) + "/res/" 9 | RuTui::Figlet.add :test_font, res_dir + "colossal.flf" 10 | 11 | screen = RuTui::Screen.new 12 | screen.add_static RuTui::Axx.new({ :x => 3, :y => 2, :file => res_dir + 'space-invader.axx' }) 13 | screen.add_static RuTui::Figlet.new({ :y => 2, :x => 18, :font => :test_font, :text => 'RuTui' }) 14 | screen.draw 15 | 16 | print RuTui::Ansi.clear_color + RuTui::Ansi.clear 17 | -------------------------------------------------------------------------------- /lib/rutui/pixel.rb: -------------------------------------------------------------------------------- 1 | module RuTui 2 | ## Pixel Class 3 | # Each pixel consinsts of foreground, background and 4 | # a symbol. What it is, is self explainaring 5 | # 6 | # Colors can be numbers between 0 and 255 7 | # (dunno how windows or OSX handles colors over 16) 8 | # 9 | # The Symbol may eats every in Ruby working Ascii Symbol 10 | # 11 | class Pixel 12 | attr_accessor :fg, :bg, :symbol 13 | 14 | def initialize foreground = 15, background = nil, symbol = " " 15 | @fg = foreground 16 | @bg = background 17 | @symbol = symbol 18 | end 19 | 20 | def self.random sym = "#" 21 | Pixel.new(rand(255),rand(255),sym) 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /examples/sprites.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | lib_dir = File.dirname(__FILE__) + '/../lib' 4 | $LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir) 5 | 6 | require 'rutui' 7 | 8 | screen = RuTui::Screen.new 9 | 10 | size = RuTui::Screen.size 11 | 12 | screen.add_static RuTui::Text.new({ :x => size[1]/2-30, :y => 2, :text => "use the cursor keys to choose the sprite or q/CTRL+C to quit" }) 13 | 14 | res_dir = File.dirname(__FILE__) + '/res/' 15 | sprites = RuTui::Sprite.new({ :x => size[1]/2-6, :y => 5, :file => res_dir + 'space-invader_sprite.axx' }) 16 | screen.add sprites 17 | 18 | Thread.new { 19 | while true 20 | sprites.update 21 | RuTui::ScreenManager.draw 22 | sleep 0.8 23 | end 24 | } 25 | 26 | RuTui::ScreenManager.loop({ :autofit => true, :autodraw => false }) do |key| 27 | 28 | break if key == :ctrl_c or key == "q" 29 | 30 | # We can translate the key symbol to a string to set the sprite 31 | if [:up, :down, :left, :right].include? key 32 | sprites.set_current key.to_s 33 | end 34 | end 35 | 36 | print RuTui::Ansi.clear_color + RuTui::Ansi.clear 37 | -------------------------------------------------------------------------------- /examples/textfield.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | lib_dir = File.dirname(__FILE__) + '/../lib' 4 | $LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir) 5 | 6 | require 'rutui' 7 | 8 | screen = RuTui::Screen.new 9 | @tf1 = RuTui::Textfield.new({ :x => 1, :y => 1, :pixel => RuTui::Pixel.new(12,44,"-"), :focus_pixel => RuTui::Pixel.new(15,64,"-") }) 10 | @tf2 = RuTui::Textfield.new({ :x => 1, :y => 4, :password => true }) 11 | 12 | screen.add @tf1 13 | screen.add @tf2 14 | screen.add_static RuTui::Text.new( :x => 1, :y => 7, :text => "Start writing, change focus with [tab]" ) 15 | 16 | @focus = 0 17 | @tf1.set_focus 18 | 19 | RuTui::ScreenManager.add :default, screen 20 | RuTui::ScreenManager.loop({ :autodraw => true }) do |key| 21 | break if key == "q" or key == :ctrl_c # CTRL+C 22 | 23 | if @focus == 1 and key == :tab 24 | @tf2.take_focus 25 | @focus = 0 26 | @tf1.set_focus 27 | elsif @focus == 0 and key == :tab 28 | @tf1.take_focus 29 | @focus = 1 30 | @tf2.set_focus 31 | end 32 | @tf1.write key if @tf1.focus 33 | @tf2.write key if @tf2.focus 34 | end 35 | 36 | print RuTui::Ansi.clear_color + RuTui::Ansi.clear 37 | -------------------------------------------------------------------------------- /lib/rutui/theme.rb: -------------------------------------------------------------------------------- 1 | module RuTui 2 | # Simple themeing engine 3 | class Theme 4 | @@themes = {} 5 | @@use = :default 6 | 7 | # init themes 8 | def self.init 9 | @@themes[:default] = { 10 | :background => Pixel.new(236,234,":"), 11 | :border => Pixel.new(144,234,"-"), 12 | :textcolor => 11, 13 | :rainbow => [1,3,11,2,4,5] 14 | } 15 | @@themes[:light] = { 16 | :background => Pixel.new(251,253,":"), 17 | :border => Pixel.new(237,253,"-"), 18 | :textcolor => 0, 19 | :rainbow => [1,3,11,2,4,5] 20 | } 21 | @@themes[:basic] = { 22 | :background => Pixel.new(0,14," "), 23 | :border => Pixel.new(8,14,"-"), 24 | :textcolor => 0, 25 | :rainbow => [1,2,3,4,5,6] 26 | } 27 | end 28 | 29 | # Create new theme 30 | def self.create name, theme 31 | @@themes[name] = theme 32 | end 33 | 34 | # Delete theme 35 | def self.delete name 36 | @@themes.delete(name) 37 | end 38 | 39 | # Set value from current theme 40 | def self.set key, val 41 | @@themes[@@use][key] = val 42 | end 43 | 44 | # Get value from current theme 45 | def self.get val 46 | @@themes[@@use][val] 47 | end 48 | 49 | # set current theme 50 | def self.use name 51 | @@use = name if !@@themes[name].nil? 52 | end 53 | end 54 | end 55 | 56 | RuTui::Theme.init 57 | -------------------------------------------------------------------------------- /test/objects/line_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | 3 | class LineTest < Minitest::Test 4 | 5 | def test_create_new_line_without_options_raises 6 | assert_raises RuntimeError do 7 | dot = RuTui::Line.new() 8 | end 9 | end 10 | 11 | def test_create_new_line_with_options 12 | line = RuTui::Line.new(length: 12, orientation: :vertical) 13 | assert_equal 12, line.length 14 | assert_equal :vertical, line.orientation 15 | end 16 | 17 | def test_create_new_line_defaults_to_horizontal 18 | line = RuTui::Line.new(length: 12) 19 | assert_equal :horizontal, line.orientation 20 | end 21 | 22 | def test_creates_right_line_object 23 | assert_equal([ 24 | { 25 | background: nil, 26 | foreground: 11, 27 | symbol: " " 28 | }, 29 | { 30 | background: nil, 31 | foreground: 11, 32 | symbol: " " 33 | } 34 | ], create_line(length: 2)) 35 | end 36 | 37 | def test_creates_bordered_horizontal_line_object 38 | assert_equal([ 39 | { 40 | background: nil, 41 | foreground: 11, 42 | symbol: "─" 43 | } 44 | ], create_line(length: 1, border: true)) 45 | end 46 | 47 | def test_creates_bordered_vertical_line_object 48 | assert_equal([ 49 | { 50 | background: nil, 51 | foreground: 11, 52 | symbol: "│" 53 | } 54 | ], create_line(length: 1, border: true, orientation: :vertical)) 55 | end 56 | 57 | def create_line(*args) 58 | RuTui::Line.new(*args).object.map{|l| l.map(&:to_h) }.flatten 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/rutui/ansi.rb: -------------------------------------------------------------------------------- 1 | module RuTui 2 | ## Ansi Class 3 | # This Class generates ansi strings for the 4 | # Terminal based output. 5 | # 6 | # Its only tested on Linux, should work atleast 7 | # on other unix based systems (Use a proper ansi, 8 | # shell/simulator on windows, and you'll be fine) 9 | # 10 | class Ansi 11 | # Calculate color from RGB values 12 | def self.rgb(red, green, blue); 16 + (red * 36) + (green * 6) + blue; end 13 | # Set background color 14 | def self.bg color; "\e[48;5;#{color}m"; end 15 | # Set text color 16 | def self.fg color; "\e[38;5;#{color}m"; end 17 | # "Shortcut" to background color 18 | def self.background color; self.bg color; end 19 | # "Shortcut" to foreground/text color 20 | def self.foreground color; self.fg color; end 21 | # Set bold 22 | def self.bold text; "\e[1m#{text}"; end 23 | # Set thin 24 | def self.thin text; "\e[2m#{text}"; end 25 | # Set italic 26 | def self.italic text; "\e[3m#{text}"; end 27 | # Set underline 28 | def self.underline text; "\e[4m#{text}"; end 29 | # Set blink 30 | def self.blink text; "\e[5m#{text}"; end 31 | # Clear all color 32 | def self.clear_color; "\e[0m"; end 33 | # Clear Screen/Terminal 34 | def self.clear; "\e[2J"; end 35 | # set start 36 | def self.set_start; "\e[s"; end 37 | # goto start 38 | def self.goto_start; "\e[u"; end 39 | # Go home 40 | def self.go_home; "\e[H"; end 41 | # Goto position 42 | def self.position x,y; "\e[#{y};#{x}f"; end 43 | # Hide cursor 44 | def self.hide_cursor; "\e[?25l"; end 45 | # Show cursor 46 | def self.show_cursor; "\e[?25h"; end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/rutui/sprites.rb: -------------------------------------------------------------------------------- 1 | module RuTui 2 | class Sprite < BaseObject 3 | attr_accessor :tick, :current, :sprites, :ticks, :object 4 | @@spr = [] 5 | 6 | def initialize options 7 | @x = options[:x] 8 | @y = options[:y] 9 | @x = 0 if @x.nil? 10 | @y = 0 if @y.nil? 11 | 12 | @tick = 0 13 | @object = options[:obj] 14 | @file = options[:file] 15 | @current = nil 16 | 17 | @@spr << self 18 | 19 | @sprites = {} 20 | 21 | create_from_file if !@file.nil? 22 | end 23 | 24 | def create_from_file 25 | if File.exists? @file 26 | data = File.open(@file).read 27 | # AXX Format 28 | if data.include? '---' and data.include? '| :' 29 | out = [] 30 | data = data.split("---") 31 | while data.size > 0 32 | while data[0].match(/^#/) 33 | data.shift 34 | end 35 | name = data.shift 36 | content = data.shift 37 | @sprites[name] = [] if @sprites[name].nil? 38 | @sprites[name] << Axx.parse(content) 39 | @current = name if @current.nil? # first sprite gets default 40 | @obj = @sprites[@current][0] 41 | end 42 | end 43 | end 44 | end 45 | 46 | # set current animation 47 | def set_current name 48 | @current = name 49 | @obj = @sprites[@current][0] 50 | end 51 | 52 | # Add sprite 53 | def add name, images 54 | @current = name if @current.nil? # first sprite gets default 55 | @sprites[name] = images 56 | end 57 | 58 | # Update ticker 59 | def update 60 | @tick += 1 61 | @tick = 0 if @tick >= ( @sprites[@current].size ) 62 | @obj = @sprites[@current][@tick] 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /examples/hello-input.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # Hello world deeper example for RuTui 4 | # 5 | 6 | lib_dir = File.dirname(__FILE__) + '/../lib' 7 | $LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir) 8 | 9 | require 'rutui' 10 | 11 | # Switch theme 12 | RuTui::Theme.use :light 13 | 14 | # Create Screen 15 | # You should remember this from the first hallo world 16 | screen = RuTui::Screen.new 17 | 18 | # Lets get the shells size 19 | size = RuTui::Screen.size 20 | 21 | # Lets create a box again, this time stick it to the middle 22 | box = RuTui::Box.new({ :x => size[1]/2-13, :y => 4, :width => 26, :height => 5 }) 23 | 24 | # add it static 25 | screen.add_static box 26 | 27 | # Now lets create an text element into the box! 28 | text = RuTui::Text.new( :x => size[1]/2-11, :y => 6, :text => "You pressed: nil (nil)" ) 29 | last = RuTui::Text.new( :x => size[1]/2-2, :y => 12, :text => "Last: nil (nil)") 30 | lost = RuTui::Text.new( :x => size[1]/2-2, :y => 13, :text => "Last: nil (nil)") 31 | 32 | # add the text as dynamic object 33 | screen.add text 34 | screen.add last 35 | screen.add lost 36 | 37 | # Add some info, may somebody would'nt find out else 38 | info = RuTui::Text.new( :x => 1, :y => 1, :text => "Press any key. Use q or CTRL+C to close" ) 39 | screen.add_static info 40 | 41 | # Create loop 42 | # it will automatically redraw after each key input 43 | RuTui::ScreenManager.loop({ :autodraw => true }) do |key| 44 | break if key == :ctrl_c or key == "q" # Exit on STRG+C or "q" 45 | lost.set_text last.get_text 46 | last.set_text text.get_text 47 | key_desc = key.class == Symbol ? key.to_s.sub('_', '+').capitalize : key 48 | text.set_text "You pressed: #{key_desc} (#{key.ord unless key.class == Symbol})" 49 | end 50 | 51 | # this was the a little deeper hello world example ;) 52 | print RuTui::Ansi.clear_color + RuTui::Ansi.clear 53 | -------------------------------------------------------------------------------- /examples/hello-world.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # Hello world example for RuTui 4 | # 5 | 6 | lib_dir = File.dirname(__FILE__) + '/../lib' 7 | $LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir) 8 | 9 | require 'rutui' 10 | 11 | # Create Screen 12 | # the first screen gets automatically the tag :default 13 | # (which is also the default tag) and gets added to the ScreenManager 14 | # anyway, thats not even important for now 15 | screen = RuTui::Screen.new 16 | 17 | # Lets create a box! 18 | box = RuTui::Box.new({ :x => 2, :y => 2, :width => 16, :height => 3 }) 19 | 20 | # This box should be a static object 21 | # We dont change it while using this screen 22 | # so we can write it directly to the map 23 | screen.add_static box 24 | 25 | # Now lets create an text element into the box! 26 | text = RuTui::Text.new( :x => 3, :y => 3, :text => "Hello world!", :foreground => 11 ) 27 | 28 | # Now lets add the text to the screen map 29 | # this time its not static, so we can modify it later 30 | screen.add text 31 | 32 | # Lets draw the screen first time 33 | screen.draw 34 | 35 | # Lets wait for a key 36 | a = RuTui::Input.getc 37 | 38 | # and change the text of our text box 39 | text.set_text "Yay!" 40 | 41 | # and redraw 42 | screen.draw 43 | 44 | # Lets wait for a key again 45 | a = RuTui::Input.getc 46 | 47 | # Lets do a little more! 48 | # Each Object which is dynamic, is moveable 49 | text.move(-1,10) 50 | box.move(-1,10) # This wont work because its static 51 | # you can also text.set_position(x,y) 52 | 53 | # we can even modify single pixel by default 54 | text.set_pixel(0,0,RuTui::Pixel.new(2,4,"#")) 55 | text.set_pixel(0,1,RuTui::Pixel.new(5,6,"@")) 56 | 57 | # Or whats about the colors? 58 | text.bg = 14 59 | text.fg = 12 60 | 61 | # and redraw 62 | screen.draw 63 | 64 | # this was the very basic hello world example ;) 65 | 66 | print RuTui::Ansi.clear_color + RuTui::Ansi.clear 67 | -------------------------------------------------------------------------------- /examples/sortable-table.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | lib_dir = File.dirname(__FILE__) + '/../lib' 4 | $LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir) 5 | 6 | require 'rutui' 7 | 8 | screen = RuTui::Screen.new 9 | @table = RuTui::Table.new({ 10 | :x => 1, 11 | :y => 1, 12 | :highlight_direction => :horizontal, # default 13 | :table => [ 14 | [1,"Some", "random","example"], 15 | [2,"text", "presented", "in"], 16 | [3,"an", "table","plus some random way to long text"] 17 | ], 18 | :cols => [ 19 | { :title => "ID", :color => RuTui::Pixel.random.fg, :title_color => RuTui::Pixel.random.fg, :length => 3 }, 20 | { :title => "Col 1", :color => RuTui::Pixel.random.fg, :title_color => RuTui::Pixel.random.fg }, 21 | { :title => "Col 2", :color => RuTui::Pixel.random.fg, :title_color => RuTui::Pixel.random.fg }, 22 | { :title => "Col 3", :color => RuTui::Pixel.random.fg, :title_color => RuTui::Pixel.random.fg, :max_length => 10 } 23 | ], 24 | :header => true, 25 | :hover => 32, 26 | :background => 30 27 | }) 28 | screen.add @table 29 | screen.add_static RuTui::Text.new( :x => 1, :y => 9, :text => "Selection: cursor keys, Sort by col: x, Exit: q or CTRL+C" ) 30 | 31 | highlight = 0 32 | highlight2 = 0 33 | 34 | RuTui::ScreenManager.add :default, screen 35 | RuTui::ScreenManager.loop({ :autodraw => true }) do |key| 36 | break if key == "q" or key == :ctrl_c # CTRL+C 37 | 38 | (highlight += 1; @table.highlight_direction = :horizontal; @table.highlight highlight) if key == :down and highlight < @table.height-1 39 | (highlight -= 1; @table.highlight_direction = :horizontal; @table.highlight highlight) if key == :up and highlight >= 1 40 | (highlight2 += 1; @table.highlight_direction = :vertical; @table.highlight highlight2) if key == :right and highlight2 < @table.width-1 41 | (highlight2 -= 1; @table.highlight_direction = :vertical; @table.highlight highlight2) if key == :left and highlight2 >= 1 42 | 43 | if key == "x" 44 | @table.sort highlight2 45 | end 46 | 47 | end 48 | 49 | print RuTui::Ansi.clear_color + RuTui::Ansi.clear 50 | -------------------------------------------------------------------------------- /lib/rutui/axx.rb: -------------------------------------------------------------------------------- 1 | module RuTui 2 | ## Basic Ascii Image format Axx 3 | # First implemented: here :3 4 | # 5 | # Example: 6 | # # Comments ... 7 | # nil|A:1:0|A|A:2 8 | # 9 | # nil empty pixel 10 | # A:1:0 Char is A, 1 is foreground, 0 is background 11 | # A Char only (colors default) 12 | # A:2 Char A, Foreground 2, Background default 13 | # 14 | class Axx < BaseObject 15 | def initialize options 16 | @file = options[:file] 17 | @x = options[:x] 18 | @y = options[:y] 19 | 20 | return if @file.nil? 21 | return if !File.exists? @file 22 | 23 | @img = File.open(@file).read 24 | 25 | parse 26 | end 27 | 28 | def parse 29 | out = [] 30 | @img.split("\n").each_with_index do |line,li| 31 | if !line.match(/^#/) 32 | out[li] = [] 33 | line.split("|").each_with_index do |elem,ei| 34 | ele = elem.split(":") 35 | if ele.size == 3 36 | out[li][ei] = Pixel.new(ele[1].to_i,ele[2].to_i,ele[0]) 37 | elsif ele.size == 2 38 | out[li][ei] = Pixel.new(ele[1].to_i,nil,ele[0]) 39 | else 40 | if ele[0] == "nil" 41 | out[li][ei] = nil 42 | else 43 | out[li][ei] = Pixel.new(nil,nil,ele[0]) 44 | end 45 | end 46 | end 47 | end 48 | end 49 | 50 | out.each do |o| 51 | out.delete(o) if o.nil? 52 | end 53 | @obj = out 54 | @height = out.size 55 | @width = out.size 56 | end 57 | 58 | def self.parse data 59 | out = [] 60 | data.split("\n").each_with_index do |line,li| 61 | if !line.match(/^#/) 62 | out[li] = [] 63 | line.split("|").each_with_index do |elem,ei| 64 | ele = elem.split(":") 65 | if ele.size == 3 66 | out[li][ei] = Pixel.new(ele[1].to_i,ele[2].to_i,ele[0]) 67 | elsif ele.size == 2 68 | out[li][ei] = Pixel.new(ele[1].to_i,nil,ele[0]) 69 | else 70 | if ele[0] == "nil" 71 | out[li][ei] = nil 72 | else 73 | out[li][ei] = Pixel.new(nil,nil,ele[0]) 74 | end 75 | end 76 | end 77 | end 78 | end 79 | 80 | out.each do |o| 81 | out.delete(o) if o.nil? or o == [] 82 | end 83 | return out 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /lib/rutui/screenmanager.rb: -------------------------------------------------------------------------------- 1 | # This file contains the ScreenManager 2 | module RuTui 3 | ## Screen Manager Class 4 | # Static class to handle screens 5 | # 6 | class ScreenManager 7 | @@screens = {} 8 | @@current = :default 9 | @@blocked = false 10 | 11 | # Set a screen 12 | # Ex.: ScreenManager.set :default, Screen.new 13 | def self.add name, screen 14 | @@screens[name] = screen 15 | end 16 | 17 | # Get count of existing screens 18 | def self.size 19 | @@screens.size 20 | end 21 | 22 | # Delete screen by name 23 | def self.delete name 24 | @@screens.delete(name) if !@@screens[name].nil? 25 | end 26 | 27 | # Set current screen 28 | def self.set_current name 29 | @@current = name 30 | end 31 | 32 | # Get current screen 33 | def self.get_current 34 | # Fix size and others of screen here 35 | @@current 36 | end 37 | 38 | # Get the complete screen by name 39 | def self.get_screen name 40 | @@screens[name] 41 | end 42 | 43 | # Refit screen size 44 | def self.refit 45 | size = Screen.size 46 | if @autofit and size != @lastsize 47 | @@screens[@@current].rescreen 48 | @lastsize = size 49 | end 50 | end 51 | 52 | # draw current screen 53 | def self.draw 54 | print RuTui::Ansi.go_home 55 | @@screens[@@current].draw 56 | end 57 | 58 | # Raw Game Loop 59 | # Ex.: ScreenManager.loop({ :autodraw => true, :autofit => true }){ |key| p key } 60 | def self.loop options 61 | autodraw = options[:autodraw] 62 | @autofit = options[:autofit] 63 | @autofit = true if @autofit.nil? 64 | @timeout = options[:timeout] 65 | @lastsize = nil 66 | @lastaction = Time.now.to_f 67 | print RuTui::Ansi.clear 68 | print RuTui::Ansi.set_start 69 | Screen.hide_cursor 70 | 71 | ScreenManager.draw 72 | 73 | while true 74 | if !@@blocked 75 | @@blocked = true 76 | key = Input.getc 77 | yield key 78 | if (@timeout.nil? or (@lastaction < Time.now.to_f-@timeout)) 79 | @lastaction = Time.now.to_f 80 | ScreenManager.refit if @autofit 81 | ScreenManager.draw if autodraw 82 | end 83 | @@blocked = false 84 | end 85 | end 86 | 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /lib/rutui/checkbox.rb: -------------------------------------------------------------------------------- 1 | module RuTui 2 | ## Checkbox Object Class 3 | # Creates an Checkbox element 4 | # 5 | # Methods: 6 | # :set_focus Set focus to this textfield 7 | # :take_focus Unset focus 8 | # :toggle Toggle checkbox 9 | # :check Check 10 | # :uncheck Uncheck 11 | # 12 | # Attributes (all accessible): 13 | # :x MUST 14 | # :y MUST 15 | # :pixel - pixel from which colors get inherted 16 | # :focus_pixel - pixel used when on focus (defaults to pixel) 17 | # :focus - has focus? 18 | # :checked - is checked? 19 | # :symbol_checked - symbol if checked 20 | # :symbol_unchecked - symbol if unchecked 21 | # 22 | class Checkbox < BaseObject 23 | attr_accessor :focus, :checked 24 | 25 | def initialize options 26 | @pixel = options[:pixel] 27 | @pixel = Theme.get(:border) if @pixel.nil? 28 | 29 | @focus_pixel = options[:focus_pixel] 30 | @focus_pixel = @pixel if @focus_pixel.nil? 31 | 32 | @symbol_checked = options[:symbol_checked] 33 | @symbol_checked = "☑" if @symbol_checked.nil? 34 | 35 | @symbol_unchecked = options[:symbol_unchecked] 36 | @symbol_unchecked = "☐" if @symbol_unchecked.nil? 37 | 38 | @x = options[:x] 39 | @x = 1 if @x.nil? 40 | @y = options[:y] 41 | @y = 1 if @y.nil? 42 | 43 | @focus = options[:focus] 44 | @focus = false if @focus.nil? 45 | 46 | @checked = options[:checked] 47 | @checked = false if @checked.nil? 48 | 49 | create 50 | end 51 | 52 | def set_focus 53 | @focus = true 54 | create 55 | end 56 | 57 | def take_focus 58 | @focus = false 59 | create 60 | end 61 | 62 | def toggle 63 | if @checked 64 | self.uncheck 65 | else 66 | self.check 67 | end 68 | end 69 | 70 | def check 71 | @checked = true 72 | create 73 | end 74 | 75 | def uncheck 76 | @checked = false 77 | create 78 | end 79 | 80 | private 81 | 82 | def create 83 | if @focus 84 | @obj = [[@focus_pixel]] 85 | else 86 | @obj = [[@pixel]] 87 | end 88 | 89 | if @checked 90 | @obj[0][0].symbol = @symbol_checked 91 | else 92 | @obj[0][0].symbol = @symbol_unchecked 93 | end 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /lib/rutui/textfield.rb: -------------------------------------------------------------------------------- 1 | module RuTui 2 | ## Textfield Object Class 3 | # Creates an Textfield element 4 | # 5 | # Methods: 6 | # :set_focus Set focus to this textfield 7 | # :take_focus Unset focus 8 | # :write CHAR Write char to textfield (if has focus) 9 | # :set_text TEXT Set text 10 | # :get_text Returns text 11 | # :count Text count 12 | # 13 | # Attributes (all accessible): 14 | # :x MUST 15 | # :y MUST 16 | # :pixel - pixel from which colors get inherted 17 | # :focus_pixel - pixel used when on focus (defaults to pixel) 18 | # :allow - allowed chars 19 | # :password - boolean: is password field? 20 | # :focus - has focus? 21 | # 22 | class Textfield < BaseObject 23 | attr_accessor :focus, :password, :allow 24 | 25 | def initialize options 26 | @width = 14 27 | @width = options[:width] if !options[:width].nil? 28 | 29 | @pixel = options[:pixel] 30 | @pixel = Theme.get(:border) if @pixel.nil? 31 | 32 | @focus_pixel = options[:focus_pixel] 33 | @focus_pixel = @pixel if @focus_pixel.nil? 34 | 35 | @x = options[:x] 36 | @x = 1 if @x.nil? 37 | @y = options[:y] 38 | @y = 1 if @y.nil? 39 | 40 | @allow = ('0'..'9').to_a + ('a'..'z').to_a + ('A'..'Z').to_a + [" ", ":", "<", ">", "|", "#", 41 | "@", "*", ",", "!", "?", ".", "-", "_", "=", "[", "]", "(", ")", "{", "}", ";"] 42 | @allow = options[:allow] if !options[:allow].nil? 43 | 44 | @height = 1 45 | 46 | @focus = false 47 | @password = false 48 | @password = true if options[:password] == true 49 | 50 | @text = "" 51 | 52 | create 53 | end 54 | 55 | ## 56 | # set focus 57 | def set_focus 58 | @focus = true 59 | RuTui::Ansi.position @x, @y 60 | create 61 | end 62 | 63 | ## 64 | # take focus 65 | def take_focus 66 | @focus = false 67 | create 68 | end 69 | 70 | ## 71 | # count 72 | def count 73 | @text.size 74 | end 75 | 76 | ## 77 | # write to textfield 78 | def write char 79 | if @focus 80 | @text += char if @allow.include? char 81 | @text = @text[0..-2] if char == :backspace or char == :ctrl_h # its 8 on windows 82 | end 83 | create 84 | end 85 | 86 | ## 87 | # set text 88 | def set_text text 89 | @text = text 90 | create 91 | end 92 | 93 | ## 94 | # get text 95 | def get_text 96 | @text 97 | end 98 | 99 | ## 100 | # create or recreate the table object 101 | def create 102 | obj = [] 103 | _obj = [] 104 | if (@text.size+2) < @width 105 | text = @text 106 | else 107 | text = @text.split(//).last(@width).join("").to_s 108 | end 109 | 110 | if password 111 | _text = text 112 | text = "" 113 | _text.size.times do |i| 114 | text += "*" 115 | end 116 | end 117 | 118 | text.to_s.split("").each do |t| 119 | if @focus && @focus_pixel != @pixel 120 | _obj << Pixel.new(@focus_pixel.fg, @focus_pixel.bg, t) 121 | else 122 | _obj << Pixel.new(@pixel.fg, @pixel.bg, t) 123 | end 124 | end 125 | 126 | if @focus && @focus_pixel != @pixel 127 | _obj << Pixel.new(@focus_pixel.fg, @focus_pixel.bg, "_") 128 | else 129 | _obj << Pixel.new(@pixel.fg, @pixel.bg, "_") 130 | end 131 | obj << _obj 132 | @obj = obj 133 | end 134 | end 135 | end 136 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![RuTui Logo](http://i.imgur.com/cu0yNM3.png "RuTui Logo") 2 | 3 | # RuTui 4 | 5 | RuTui is a lightweight, pure ruby alternative to known command line interfaces like nCurses. Most CLI/TUI Frameworks sit on an C like approach, are very limited or are just not beautiful enough to use. This lib aims to be different, to be easy, modular and just Ruby like to handle. There is built-in color support and input control which sits on top of Unix standards. 6 | 7 | [**Take a look at the wiki**](https://github.com/b1nary/rutui/wiki) 8 | 9 | ## Features 10 | 11 | - Fully text based interfaces 12 | - No dependencies (except ruby) 13 | - 255 Colors (at least in Unix) 14 | - FIGlet font support 15 | - Own dynamic image format 16 | - Sprites (with animation) 17 | - Basic theming 18 | - Prebuild Elements to draw 19 | - Tables 20 | - ... 21 | 22 | You can use this in many different application purposes. You can create simple CLIs for your scripts, you can make awesome ASCII based data visualizations. Its also basically a small game engine. 23 | 24 | ## Installation 25 | 26 | ``` bash 27 | sudo gem install rutui 28 | ``` 29 | 30 | If you are on windows please take a look at [Ansicon](https://github.com/adoxa/ansicon). 31 | 32 | ## Todo 33 | 34 | * Test on windows/mac 35 | * "Radio" object_ 36 | * "Selectbox" object_ 37 | 38 | ## Changelog 39 | 40 | * 0.7.2 41 | * Push version number because rubygems already took 7.1 42 | * 0.7.1 43 | * Merge [pull request](https://github.com/b1nary/rutui/pull/6) by @lmc (improve draw performance by a lot!) 44 | * 0.7 45 | * Merge [pull request](https://github.com/b1nary/rutui/pull/1) from @jaydg 46 | * 0.6 47 | * Fixing transparency on multiple objects 48 | * Removing a flicker from slower devices 49 | * Fixing figlet problems 50 | * Added text style options (bold/italic/...) 51 | * Added multiline text support 52 | * Add focus pixel to textfield 53 | * Added "Checkbox" object 54 | * 0.5 55 | * Fixed refactoring bugs 56 | * Added "Dot" object 57 | * Add color inheritance (trough -1) to objects 58 | * Added Timeout for Screenmanager for a minimal time per operation 59 | * 0.4 60 | * Refactor structure 61 | * better printing (much less flicker) 62 | * New objects: [Tables](https://github.com/b1nary/rutui/wiki/Tables), [Textfields](https://github.com/b1nary/rutui/wiki/Textfields) 63 | * Adding cmd.exe support (colors only trough [Ansicon](https://github.com/adoxa/ansicon)) 64 | * 0.3 65 | * Fixes, fixes, fixes 66 | * 0.2 (beta) 67 | * Rolling updates 68 | * 0.1 (alpha) 69 | * Rolling updates 70 | 71 | ## License 72 | This Project is licensed under the [MIT License](http://de.wikipedia.org/wiki/MIT-Lizenz) 73 | 74 | > Copyright (c) 2012 Roman Pramberger 75 | > Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 76 | 77 | > The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 78 | 79 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 80 | -------------------------------------------------------------------------------- /examples/tic-tac-toe.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # Tic-Tac-Toe 4 | # 5 | # This is the first example done in this library 6 | # Its more a proof of concept than an read worthy example 7 | # 8 | 9 | lib_dir = File.dirname(__FILE__) + '/../lib' 10 | $LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir) 11 | 12 | require 'rutui' 13 | 14 | RuTui::Theme.use :basic 15 | screen = RuTui::Screen.new 16 | 17 | RuTui::ScreenManager.add :default, screen 18 | RuTui::ScreenManager.set_current :default 19 | 20 | size = RuTui::Screen.size 21 | map = [[0,0,0],[0,0,0],[0,0,0]] 22 | current = "X" 23 | pos = [0,0] 24 | 25 | screen.add_static RuTui::Text.new({ 26 | :x => (size[1]/2)-3, 27 | :y => 2, 28 | :text => "Tic-Tac-Toe", 29 | :foreground => 15 }) 30 | 31 | screen.add_static RuTui::Text.new({ 32 | :x => (size[1]/2)-2, 33 | :y => 3, 34 | :text => "by b1nary", 35 | :foreground => 244 }) 36 | 37 | screen.add info = RuTui::Text.new({ 38 | :x => (size[1]/2)-3, 39 | :y => 12, 40 | :text => "Current: #{current}", 41 | :foreground => 250 }) 42 | 43 | hor = RuTui::Pixel.new(RuTui::Theme.get(:border).fg, RuTui::Theme.get(:background).bg, "-") 44 | ver = RuTui::Pixel.new(RuTui::Theme.get(:border).fg, RuTui::Theme.get(:background).bg, "|") 45 | cor = RuTui::Pixel.new(RuTui::Theme.get(:border).fg, RuTui::Theme.get(:background).bg, "*") 46 | 47 | xhor = RuTui::Pixel.new(RuTui::Theme.get(:textcolor), RuTui::Theme.get(:background).bg, "-") 48 | xver = RuTui::Pixel.new(RuTui::Theme.get(:textcolor), RuTui::Theme.get(:background).bg, "|") 49 | xcor = RuTui::Pixel.new(RuTui::Theme.get(:textcolor), RuTui::Theme.get(:background).bg, "#") 50 | 51 | 3.times do |ir| 52 | 3.times do |ic| 53 | screen.add_static RuTui::Box.new({ 54 | :x => (size[1]/2)+(ic*4-4), 55 | :y => 5+(ir*2), 56 | :width => 5, 57 | :height => 3, 58 | :horizontal => hor, 59 | :vertical => ver, 60 | :corner => cor }) 61 | end 62 | end 63 | 64 | screen.add box0 = RuTui::Box.new({ 65 | :x => (size[1]/2)-4, 66 | :y => 5, 67 | :width => 5, 68 | :height => 3, 69 | :horizontal => xhor, 70 | :vertical => xver, 71 | :corner => xcor }) 72 | 73 | 74 | RuTui::ScreenManager.loop({ :autodraw => true }) do |key| 75 | 76 | if key == "q" or key == :ctrl_c # CTRL+C 77 | break 78 | 79 | elsif key == " " # space bar 80 | if map[pos[0]][pos[1]] == 0 81 | color = RuTui::Theme.get(:rainbow)[1] if current == "X" 82 | color = RuTui::Theme.get(:rainbow)[0] if current == "O" 83 | 84 | screen.add RuTui::Text.new({ 85 | :x => (size[1]/2)+(pos[0]*4-4)+2, 86 | :y => 5+(pos[1]*2)+1, 87 | :text => current, 88 | :foreground => color }) 89 | 90 | map[pos[0]][pos[1]] = current.dup 91 | 92 | # Simple all possibilities, not that much so .... 93 | (info.set_text "#{current} WON! yay!"; RuTui::ScreenManager.draw; break) if (map[0][0] == map[0][1] and map[0][0] == map[0][2] and map[0][2]!=0) or (map[1][0] == map[1][1] and map[1][0] == map[1][2] and map[1][2]!=0) or (map[2][0] == map[2][1] and map[2][0] == map[2][2] and map[2][2]!=0) or (map[0][0] == map[1][0] and map[1][0] == map[2][0] and map[2][0]!=0) or (map[0][1] == map[1][1] and map[1][1] == map[2][1] and map[2][1]!=0) or (map[0][2] == map[1][2] and map[1][2] == map[2][2] and map[1][2]!=0) or (map[0][0] == map[1][1] and map[1][1] == map[2][2] and map[1][1]!=0) or (map[2][0] == map[1][1] and map[2][0] == map[0][2] and map[2][0]!=0) 94 | 95 | if current == "X" 96 | current = "O" 97 | else 98 | current = "X" 99 | end 100 | 101 | info.set_text "Current: #{current}" 102 | end 103 | 104 | elsif key == 'w' or key == :up # up 105 | (box0.move(0,-2); pos[1] -= 1) if pos[1] > 0 106 | elsif key == 's' or key == :down # down 107 | (box0.move(0,2); pos[1] += 1) if pos[1] < 2 108 | elsif key == 'd' or key == :right # right 109 | (box0.move(4,0); pos[0] += 1) if pos[0] < 2 110 | elsif key == 'a' or key == :left # left 111 | (box0.move(-4,0); pos[0] -= 1) if pos[0] > 0 112 | end 113 | end 114 | 115 | print RuTui::Ansi.clear_color + RuTui::Ansi.clear 116 | -------------------------------------------------------------------------------- /examples/res/space-invader_sprite.axx: -------------------------------------------------------------------------------- 1 | # Space Invader SPRITE ~ Example image format 2 | ---down--- 3 | nil|nil| :0:10|nil|nil|nil|nil|nil| :0:10|nil|nil 4 | nil|nil|nil| :0:10|nil|nil|nil| :0:10|nil|nil|nil 5 | nil|nil| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10|nil|nil 6 | nil| :0:10| :0:10| :0:12| :0:10| :0:10| :0:10| :0:12| :0:10| :0:10|nil 7 | :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10 8 | :0:10|nil| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10|nil| :0:10 9 | :0:10|nil| :0:10|nil|nil|nil|nil|nil| :0:10|nil| :0:10 10 | nil|nil|nil| :0:10| :0:10|nil| :0:10| :0:10|nil|nil|nil 11 | ---down--- 12 | nil|nil| :0:10|nil|nil|nil|nil|nil| :0:10|nil|nil 13 | nil|nil|nil| :0:10|nil|nil|nil| :0:10|nil|nil|nil 14 | :0:10|nil| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10|nil| :0:10 15 | :0:10| :0:10| :0:10| :0:12| :0:10| :0:10| :0:10| :0:12| :0:10| :0:10| :0:10 16 | :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10 17 | nil|nil| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10|nil|nil 18 | nil|nil| :0:10|nil|nil|nil|nil|nil| :0:10|nil|nil 19 | nil| :0:10|nil|nil|nil|nil|nil|nil|nil| :0:10|nil 20 | ---up--- 21 | nil|nil| :0:10|nil|nil|nil|nil|nil| :0:10|nil|nil 22 | nil|nil|nil| :0:10|nil|nil|nil| :0:10|nil|nil|nil 23 | nil|nil| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10|nil|nil 24 | nil| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10|nil 25 | :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10 26 | :0:10|nil| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10|nil| :0:10 27 | :0:10|nil| :0:10|nil|nil|nil|nil|nil| :0:10|nil| :0:10 28 | nil|nil|nil| :0:10| :0:10|nil| :0:10| :0:10|nil|nil|nil 29 | ---up--- 30 | nil|nil| :0:10|nil|nil|nil|nil|nil| :0:10|nil|nil 31 | nil|nil|nil| :0:10|nil|nil|nil| :0:10|nil|nil|nil 32 | :0:10|nil| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10|nil| :0:10 33 | :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10 34 | :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10 35 | nil|nil| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10|nil|nil 36 | nil|nil| :0:10|nil|nil|nil|nil|nil| :0:10|nil|nil 37 | nil| :0:10|nil|nil|nil|nil|nil|nil|nil| :0:10|nil 38 | ---right--- 39 | nil|nil| :0:10|nil|nil|nil|nil|nil| :0:10|nil|nil 40 | nil|nil|nil| :0:10|nil|nil|nil| :0:10|nil|nil|nil 41 | nil|nil| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10|nil|nil 42 | nil| :0:10| :0:10| :0:10| :0:12| :0:10| :0:10| :0:10| :0:12| :0:10|nil 43 | :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10 44 | :0:10|nil| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10|nil| :0:10 45 | :0:10|nil| :0:10|nil|nil|nil|nil|nil| :0:10|nil| :0:10 46 | nil|nil|nil| :0:10| :0:10|nil| :0:10| :0:10|nil|nil|nil 47 | ---right--- 48 | nil|nil| :0:10|nil|nil|nil|nil|nil| :0:10|nil|nil 49 | nil|nil|nil| :0:10|nil|nil|nil| :0:10|nil|nil|nil 50 | :0:10|nil| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10|nil| :0:10 51 | :0:10| :0:10| :0:10| :0:10| :0:12| :0:10| :0:10| :0:10| :0:12| :0:10| :0:10 52 | :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10 53 | nil|nil| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10|nil|nil 54 | nil|nil| :0:10|nil|nil|nil|nil|nil| :0:10|nil|nil 55 | nil| :0:10|nil|nil|nil|nil|nil|nil|nil| :0:10|nil 56 | ---left--- 57 | nil|nil| :0:10|nil|nil|nil|nil|nil| :0:10|nil|nil 58 | nil|nil|nil| :0:10|nil|nil|nil| :0:10|nil|nil|nil 59 | nil|nil| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10|nil|nil 60 | nil| :0:10| :0:12| :0:10| :0:10| :0:10| :0:12| :0:10| :0:10| :0:10|nil 61 | :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10 62 | :0:10|nil| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10|nil| :0:10 63 | :0:10|nil| :0:10|nil|nil|nil|nil|nil| :0:10|nil| :0:10 64 | nil|nil|nil| :0:10| :0:10|nil| :0:10| :0:10|nil|nil|nil 65 | ---left--- 66 | nil|nil| :0:10|nil|nil|nil|nil|nil| :0:10|nil|nil 67 | nil|nil|nil| :0:10|nil|nil|nil| :0:10|nil|nil|nil 68 | :0:10|nil| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10|nil| :0:10 69 | :0:10| :0:10| :0:12| :0:10| :0:10| :0:10| :0:12| :0:10| :0:10| :0:10| :0:10 70 | :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10 71 | nil|nil| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10| :0:10|nil|nil 72 | nil|nil| :0:10|nil|nil|nil|nil|nil| :0:10|nil|nil 73 | nil| :0:10|nil|nil|nil|nil|nil|nil|nil| :0:10|nil 74 | -------------------------------------------------------------------------------- /lib/rutui/screen.rb: -------------------------------------------------------------------------------- 1 | # This File contains the Screen Class 2 | 3 | require 'io/console' 4 | 5 | module RuTui 6 | ## Screen Class 7 | # The Screen is the root element of your app. 8 | # Its basicly a map containing you screens pixels 9 | # 10 | class Screen 11 | # Initialize me with a default pixel, if you want 12 | def initialize default_pixel = Theme.get(:background) 13 | size = Screen.size 14 | @smap = Array.new(size[0]){ Array.new(size[1]) } 15 | @map = @smap.dup 16 | @default = default_pixel 17 | @objects = [] 18 | @statics = [] 19 | # Set as default if first screen 20 | ScreenManager.add :default, self if ScreenManager.size == 0 21 | end 22 | 23 | # regen screen (size change?) 24 | def rescreen 25 | size = Screen.size 26 | @smap = Array.new(size[0]){ Array.new(size[1]) } 27 | 28 | @statics.each do |s| 29 | self.add_static s 30 | end 31 | 32 | @map = @smap.dup 33 | end 34 | 35 | # Set default/background pixel 36 | # Ex.: screen.set_default Pixel.new(244,1,";") 37 | def set_default pixel 38 | @default = pixel 39 | end 40 | 41 | ## 42 | # Get default/background pixel 43 | def get_default 44 | @default 45 | end 46 | 47 | ## 48 | # add object that doesnt change over time 49 | def add_static object 50 | @statics << object if !@statics.include? object 51 | object.each do |ri,ci,pixel| 52 | if !pixel.nil? and object.y+ri >= 0 and object.y+ci >= 0 53 | if @smap[object.y+ri][object.x+ci].nil? 54 | if pixel.bg == -1 55 | pixel.bg = @map[object.y + ri][object.x + ci].bg if !@map[object.y + ri][object.x + ci].nil? 56 | pixel.bg = Theme.get(:background).bg if pixel.bg == -1 57 | end 58 | if pixel.fg == -1 59 | pixel.fg = @map[object.y + ri][object.x + ci].fg if !@map[object.y + ri][object.x + ci].nil? 60 | pixel.fg = Theme.get(:background).fg if pixel.fg == -1 61 | end 62 | 63 | @smap[object.y+ri][object.x+ci] = pixel 64 | else 65 | @smap[object.y+ri][object.x+ci] = @smap[object.y+ri][object.x+ci].dup 66 | @smap[object.y+ri][object.x+ci].fg = pixel.fg if pixel.fg != -1 67 | @smap[object.y+ri][object.x+ci].bg = pixel.bg if pixel.bg != -1 68 | @smap[object.y+ri][object.x+ci].symbol = pixel.symbol 69 | end 70 | end 71 | end 72 | end 73 | ## 74 | 75 | # add dynamic object 76 | def add object 77 | @objects << object 78 | end 79 | 80 | ## 81 | # remove object 82 | def delete object 83 | @objects.delete(object) 84 | end 85 | 86 | ## 87 | # draw the pixel-screen map 88 | def draw 89 | lastpixel = Pixel.new(rand(255), rand(255), ".") 90 | @map = Marshal.load( Marshal.dump( @smap )) # Deep copy 91 | 92 | # get all the objects 93 | @objects.each do |o| 94 | next if o.x.nil? or o.y.nil? 95 | o.each do |ri,ci,pixel| 96 | if !pixel.nil? and o.y+ri >= 0 and o.x+ci >= 0 and o.y+ri < @map.size and o.x+ci < @map[0].size 97 | # -1 enables a "transparent" effect 98 | if pixel.bg == -1 99 | pixel.bg = @map[o.y + ri][o.x + ci].bg if !@map[o.y + ri][o.x + ci].nil? 100 | pixel.bg = Theme.get(:background).bg if pixel.bg == -1 101 | end 102 | if pixel.fg == -1 103 | pixel.fg = @map[o.y + ri][o.x + ci].fg if !@map[o.y + ri][o.x + ci].nil? 104 | pixel.fg = Theme.get(:background).fg if pixel.fg == -1 105 | end 106 | 107 | @map[o.y + ri][o.x + ci] = pixel 108 | end 109 | end 110 | end 111 | 112 | out = "" # Color.go_home 113 | # and DRAW! 114 | @map.each do |line| 115 | line.each do |pixel| 116 | if lastpixel != pixel 117 | out << RuTui::Ansi.clear_color if lastpixel != 0 118 | if pixel.nil? 119 | out << "#{RuTui::Ansi.bg(@default.bg)}#{RuTui::Ansi.fg(@default.fg)}#{@default.symbol}" 120 | else 121 | out << "#{RuTui::Ansi.bg(pixel.bg)}#{RuTui::Ansi.fg(pixel.fg)}#{pixel.symbol}" 122 | end 123 | lastpixel = pixel 124 | else 125 | if pixel.nil? 126 | out << @default.symbol 127 | else 128 | out << pixel.symbol 129 | end 130 | end 131 | end 132 | end 133 | 134 | # draw out 135 | print out.chomp 136 | $stdout.flush 137 | end 138 | 139 | # Hides the cursor 140 | def self.hide_cursor 141 | print RuTui::Ansi.hide_cursor 142 | end 143 | 144 | def self.size 145 | IO.console.winsize 146 | end 147 | 148 | end 149 | end 150 | -------------------------------------------------------------------------------- /lib/rutui/figlet.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | module RuTui 3 | ## Figlet Class 4 | # parse Figlet Font files 5 | # 6 | # Attributes (All accessable): 7 | # :x MUST 8 | # :y MUST 9 | # :text Your text 10 | # :font Use a loaded font file 11 | # :rainbow true|any 12 | # :colors Array of example pixels used for colors 13 | # 14 | # 15 | # Example: 16 | # Figlet.add :test, "path/to/font.flf" 17 | # Figlet.new({ :text => "Meh", :font => :test, :rainbow => true }) 18 | # 19 | # Config content (just to remember what the figlet slug means) 20 | # 0 - name 21 | # 1 - height of a character 22 | # 2 - height of a character, not including descenders 23 | # 3 - max line length (excluding comment lines) + a fudge factor 24 | # 4 - default smushmode for this font (like "-m 0" on command line) 25 | # 5 - number of comment lines 26 | # 6 - rtol 27 | # 28 | class Figlet < BaseObject 29 | attr_accessor :text, :rainbow, :font, :colors 30 | # Figlet font: '!" #$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz[|]~ÄÖÜäöüß' 31 | @@chars = '!!"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz[|]~ÄÖÜäöüß'.split("") 32 | @@rainbow = nil 33 | @@fonts = {} 34 | 35 | # Initialize (see above) 36 | def initialize options 37 | @x = options[:x] 38 | @y = options[:y] 39 | 40 | @@rainbow = Theme.get(:rainbow) if @@rainbow.nil? 41 | 42 | return if @x.nil? or @y.nil? 43 | 44 | @font = options[:font] 45 | @text = options[:text] 46 | @rainbow = options[:rainbow] 47 | @space = options[:space] 48 | @space = 0 if @space.nil? 49 | @colors = options[:colors] 50 | @colors = [Pixel.new(Theme.get(:textcolor), -1, Theme.get(:background).symbol)] if @colors.nil? 51 | 52 | create 53 | end 54 | 55 | # Create Figlet text object 56 | # Recall it any time when attributes have changed 57 | def create 58 | obj = [] 59 | @@fonts[@font]['n'].size.times do 60 | obj << [] 61 | end 62 | current = 0 63 | 64 | count = 0 65 | @text.each_byte do |c| 66 | if @@chars.include? c.chr 67 | @@fonts[@font][c.chr].each_with_index do |fr,ri| 68 | fr.each do |fc| 69 | if @rainbow 70 | obj[ri] << Pixel.new(@@rainbow[current], @colors[0].bg,fc) 71 | else 72 | obj[ri] << Pixel.new(@colors[current].fg,@colors[current].bg,fc) 73 | end 74 | 75 | if @rainbow != true 76 | current += 1 if @colors.size > 1 77 | current = 0 if current > @colors.size 78 | end 79 | end 80 | end 81 | 82 | if @rainbow 83 | current += 1 84 | current = 0 if current >= @@rainbow.size 85 | end 86 | 87 | if count < @text.size-1 88 | @@fonts[@font]['n'].size.times do |ri| 89 | @space.times do 90 | if @rainbow 91 | obj[ri] << Pixel.new(@@rainbow[current], @colors[0].bg,Theme.get(:background).symbol) 92 | else 93 | obj[ri] << Pixel.new(@colors[current].fg,@colors[current].bg,Theme.get(:background).symbol) 94 | end 95 | end 96 | end 97 | end 98 | # SPace 99 | else 100 | @@fonts[@font]['n'].size.times do |ri| 101 | 3.times do 102 | if @rainbow 103 | obj[ri] << Pixel.new(@@rainbow[current], @colors[0].bg, Theme.get(:background).symbol) 104 | else 105 | obj[ri] << Pixel.new(@colors[current].fg,@colors[current].bg, Theme.get(:background).symbol) 106 | end 107 | end 108 | end 109 | end 110 | count += 1 111 | end 112 | @obj = obj 113 | end 114 | 115 | # Static methods to add/load new Fonts 116 | def self.add name, file 117 | if File.exists?(file) 118 | 119 | data = File.new(file, "r").read.split("\n") 120 | config = data[0].split(" ") 121 | 122 | # Remove Comments (Size is in info line) 123 | config[5].to_i.times do 124 | data.delete_at(0) 125 | end 126 | data.delete_at(0) 127 | 128 | # Remove empty line if exist 129 | data.delete_at(0) if data[0].strip() == "" 130 | 131 | height = config[1].to_i 132 | rest = height - config[2].to_i 133 | 134 | @@fonts[name] = {} 135 | 136 | @@chars.each do |i| 137 | out = [] 138 | 139 | config[2].to_i.times do |x| 140 | xdata = data[0].gsub("$"," ").gsub("@","").gsub("\r","").split("") 141 | out << xdata if xdata.size > 0 142 | data.delete_at(0) 143 | end 144 | 145 | rest.times do 146 | data.delete_at(0) 147 | end 148 | 149 | @@fonts[name][i] = out 150 | end 151 | return true 152 | else 153 | return false 154 | end 155 | end 156 | end 157 | end 158 | -------------------------------------------------------------------------------- /lib/rutui/input.rb: -------------------------------------------------------------------------------- 1 | module RuTui 2 | ## Input class 3 | # read and interpret keyboard input 4 | class Input 5 | def self.init 6 | # Look for the current terminal in the hash of terminal scancodes and 7 | # contruct a scancode -> key symbol lookup hash. This hash will we used 8 | # by self.getc to translate the received key presses to symbols. 9 | @@scancodes.keys.each { |term| 10 | # Match the terminal name as regexp to get variants, 11 | # e.g. xterm matches xterm-256color, too. 12 | if /#{term}/.match(ENV["TERM"]) 13 | @@keys = Hash[@@scancodes[term].zip @@key_symbols] 14 | break 15 | end 16 | } 17 | 18 | # Special keys are currently not supported on Windows 19 | # That needs some investigation 20 | if RUBY_PLATFORM =~ /(win32|w32)/ 21 | @@keys = {} 22 | end 23 | 24 | # Exit with failure if the terminal was not found 25 | unless defined? @@keys 26 | STDERR.puts "Unsupported terminal '#{ENV['TERM']}'." 27 | exit 1 28 | end 29 | 30 | # Add the common control key definitions 31 | @@keys.merge!(@@control_keys) 32 | end 33 | 34 | # Get input char without enter 35 | def self.getc 36 | begin 37 | IO.console.raw! 38 | key = IO.console.getch 39 | 40 | if key == "\e" 41 | begin 42 | char = IO.console.getch(min: 0, time: 0) 43 | key += char unless char.nil? 44 | end until char.nil? 45 | end 46 | 47 | ensure 48 | IO.console.cooked! 49 | end 50 | 51 | # look up special keys 52 | if @@keys.has_key? key 53 | return @@keys[key] 54 | else 55 | return key 56 | end 57 | end 58 | 59 | private 60 | @@key_symbols = [ 61 | :f1, 62 | :f2, 63 | :f3, 64 | :f4, 65 | :f5, 66 | :f6, 67 | :f7, 68 | :f8, 69 | :f9, 70 | :f10, 71 | :f11, 72 | :f12, 73 | :insert, 74 | :delete, 75 | :home, 76 | :end, 77 | :pgup, 78 | :pgdn, 79 | :up, 80 | :down, 81 | :left, 82 | :right, 83 | ] 84 | 85 | # Keyboard scancodes for some terminals. 86 | # The order of keys must match the key symbols above. 87 | @@scancodes = { 88 | "linux" => [ 89 | "\e[[A", # F1 90 | "\e[[B", # F2 91 | "\e[[C", # F3 92 | "\e[[D", # F4 93 | "\e[[E", # F5 94 | "\e[17~", # F6 95 | "\e[18~", # F7 96 | "\e[19~", # F8 97 | "\e[20~", # F9 98 | "\e[21~", # F10 99 | "\e[23~", # F11 100 | "\e[24~", # F12 101 | "\e[2~", # ins 102 | "\e[3~", # del 103 | "\e[1~", # home 104 | "\e[4~", # end 105 | "\e[5~", # page up 106 | "\e[6~", # page down 107 | "\e[A", # up 108 | "\e[B", # down 109 | "\e[D", # left 110 | "\e[C", # right 111 | ], 112 | "Eterm" => [ 113 | "\e[11~", # F1 114 | "\e[12~", # F2 115 | "\e[13~", # F3 116 | "\e[14~", # F4 117 | "\e[15~", # F5 118 | "\e[17~", # F6 119 | "\e[18~", # F7 120 | "\e[19~", # F8 121 | "\e[20~", # F9 122 | "\e[21~", # F10 123 | "\e[23~", # F11 124 | "\e[24~", # F12 125 | "\e[2~", # ins 126 | "\e[3~", # del 127 | "\e[7~", # home 128 | "\e[8~", # end 129 | "\e[5~", # page up 130 | "\e[6~", # page down 131 | "\e[A", # arrow up 132 | "\e[B", # arrow down 133 | "\e[D", # arrow left 134 | "\e[C", # arrow right 135 | ], 136 | "rxvt" => [ 137 | "\e[11~", # F1 138 | "\e[12~", # F2 139 | "\e[13~", # F3 140 | "\e[14~", # F4 141 | "\e[15~", # F5 142 | "\e[17~", # F6 143 | "\e[18~", # F7 144 | "\e[19~", # F8 145 | "\e[20~", # F9 146 | "\e[21~", # F10 147 | "\e[23~", # F11 148 | "\e[24~", # F12 149 | "\e[2~", # ins 150 | "\e[3~", # del 151 | "\e[7~", # home 152 | "\e[8~", # end 153 | "\e[5~", # page up 154 | "\e[6~", # page down 155 | "\e[A", # up 156 | "\e[B", # down 157 | "\e[D", # left 158 | "\e[C", # right 159 | ], 160 | "xterm" => [ 161 | "\eOP", # F1 162 | "\eOQ", # F2 163 | "\eOR", # F3 164 | "\eOS", # F4 165 | "\e[15~", # F5 166 | "\e[17~", # F6 167 | "\e[18~", # F7 168 | "\e[19~", # F8 169 | "\e[20~", # F9 170 | "\e[21~", # F10 171 | "\e[23~", # F11 172 | "\e[24~", # F12 173 | "\e[2~", # ins 174 | "\e[3~", # del 175 | "\e[H", # home 176 | "\e[F", # end 177 | "\e[5~", # page up 178 | "\e[6~", # page down 179 | "\e[A", # up 180 | "\e[B", # down 181 | "\e[D", # left 182 | "\e[C", # right 183 | ] 184 | } 185 | 186 | @@control_keys = { 187 | "\x01" => :ctrl_a, 188 | "\x02" => :ctrl_b, 189 | "\x03" => :ctrl_c, 190 | "\x04" => :ctrl_d, 191 | "\x05" => :ctrl_e, 192 | "\x06" => :ctrl_f, 193 | "\a" => :ctrl_g, 194 | "\b" => :ctrl_h, 195 | "\t" => :tab, # aka CTRL+I 196 | "\n" => :ctrl_n, 197 | "\v" => :ctrl_k, 198 | "\f" => :ctrl_l, 199 | "\r" => :enter, # aka CTRL+M 200 | "\x0e" => :ctrl_n, 201 | "\x0f" => :ctrl_o, 202 | "\x10" => :ctrl_p, 203 | "\x11" => :ctrl_q, 204 | "\x12" => :ctrl_r, 205 | "\x13" => :ctrl_s, 206 | "\x14" => :ctrl_t, 207 | "\x15" => :ctrl_u, 208 | "\x16" => :ctrl_v, 209 | "\x17" => :ctrl_w, 210 | "\x18" => :ctrl_x, 211 | "\x19" => :ctrl_y, 212 | "\x1a" => :ctrl_z, 213 | "\e" => :esc, 214 | "\x7f" => :backspace 215 | } 216 | end 217 | end 218 | 219 | RuTui::Input.init 220 | -------------------------------------------------------------------------------- /lib/rutui/table.rb: -------------------------------------------------------------------------------- 1 | module RuTui 2 | ## Table Object Class 3 | # Creates an Table element 4 | # 5 | # Attributes (all accessible): 6 | # :x MUST 7 | # :y MUST 8 | # :table - the tabular data in an array (like: [[1,"A"], [2, "B"]]) 9 | # :background - Background color (0-255) 10 | # :foreground - Foreground color (0-255) 11 | # :hover - Hover color (0-255) 12 | # :ascii - Draw ASCII decoration 13 | # :highlight - To highlight line 14 | # :highlight_direction - Highlight direction 15 | # :cols - Header and meta definitions (see example) 16 | # :pixel - Default pixel (colors) for border 17 | # 18 | class Table < BaseObject 19 | attr_accessor :table, :ascii, :highlight, :highlight_direction, :cols 20 | 21 | def initialize options 22 | @bg = options[:background] 23 | @hover = options[:hover] 24 | @fg = options[:foreground] 25 | @bg = -1 if @bg.nil? 26 | @fg = Theme.get(:textcolor) if @fg.nil? 27 | @bg2 = @bg if @bg2.nil? 28 | @ascii = options[:ascii] 29 | @ascii = true if @ascii.nil? 30 | 31 | @pixel = options[:pixel] 32 | @pixel = Theme.get(:border) if @pixel.nil? 33 | 34 | @table = options[:table] 35 | @table = [] if @table.nil? 36 | 37 | @cols = options[:cols] 38 | @cols = [] if @cols.nil? 39 | 40 | @highlight = options[:highlight] 41 | @highlight = 0 if @highlight.nil? 42 | 43 | @highlight_direction = options[:highlight_direction] 44 | @highlight_direction = :horizontal if @highlight_direction.nil? 45 | 46 | @x = options[:x] 47 | @x = 1 if @x.nil? 48 | @y = options[:y] 49 | @y = 1 if @y.nil? 50 | 51 | @reverse = false 52 | @header = false 53 | @header = true if options[:header] == true 54 | 55 | @meta = { :height => @table.size, :cols => 0 } 56 | index_col_widths 57 | 58 | create 59 | 60 | @height = @table.size 61 | #@height += 2 if @ascii 62 | @width = @table[0].size 63 | 64 | 65 | end 66 | 67 | ## 68 | # Set data 69 | def set_table table 70 | @table = table 71 | index_col_widths 72 | create 73 | end 74 | 75 | ## 76 | # Add line to table 77 | def add line 78 | @table << line 79 | @height += 1 80 | index_col_widths 81 | create 82 | end 83 | 84 | ## 85 | # Delete line from table by index 86 | def delete line_id 87 | if !@table[line_id].nil? 88 | @table.delete_at(line_id) 89 | @height -= 1 90 | index_col_widths 91 | create 92 | end 93 | end 94 | 95 | ## 96 | # highlight line 97 | def highlight line_id 98 | return highlight if line_id.nil? 99 | @highlight = line_id 100 | @reverse = false 101 | create 102 | end 103 | 104 | ## 105 | # create or recreate the table object 106 | def create 107 | obj = [] 108 | if @header 109 | obj << ascii_table_line if @ascii 110 | _obj = [] 111 | _obj << Pixel.new(@pixel.fg,@bg,"|") if @ascii 112 | _obj << nil 113 | @cols.each_with_index do |col, index| 114 | fg = @pixel.fg 115 | fg = @cols[index][:title_color] if !@cols[index].nil? and !@cols[index][:title_color].nil? 116 | chars = "".to_s.split("") 117 | chars = "#{ col[:title] }".to_s.split("") if !col.nil? and !col[:title].nil? 118 | chars.each_with_index do |e, char_count| 119 | _obj << Pixel.new(fg,@bg,e) 120 | end 121 | (@meta[:max_widths][index]-chars.size+2).times do |i| 122 | _obj << nil 123 | end 124 | _obj << Pixel.new(@pixel.fg,@bg,"|") if @ascii 125 | end 126 | obj << _obj 127 | end 128 | obj << ascii_table_line if @ascii 129 | @table.each_with_index do |line, lindex| 130 | bg = @bg 131 | bg = @hover if lindex == @highlight and @highlight_direction == :horizontal 132 | _obj = [] 133 | _obj << Pixel.new(@pixel.fg,bg,"|") if @ascii 134 | line.each_with_index do |col, index| 135 | fg = @fg 136 | fg = @cols[index][:color] if !@cols[index].nil? and !@cols[index][:color].nil? 137 | 138 | if @highlight_direction == :vertical 139 | if index == @highlight 140 | bg = @hover 141 | else 142 | bg = @bg 143 | end 144 | end 145 | 146 | 147 | chars = col.to_s.split("") 148 | _obj << nil 149 | max_chars = nil 150 | max_chars = @cols[index][:max_length]+1 if !@cols[index].nil? and !@cols[index][:max_length].nil? 151 | max_chars = @cols[index][:length]+1 if !@cols[index].nil? and !@cols[index][:length].nil? 152 | chars.each_with_index do |e, char_count| 153 | break if !max_chars.nil? and char_count >= max_chars 154 | _obj << Pixel.new(fg,bg,e) 155 | end 156 | (@meta[:max_widths][index]-chars.size+1).times do |i| 157 | _obj << nil 158 | end 159 | 160 | bg = @bg if @highlight_direction == :vertical 161 | _obj << Pixel.new(@pixel.fg,bg,"|") if @ascii 162 | end 163 | obj << _obj 164 | end 165 | obj << ascii_table_line if @ascii 166 | @obj = obj 167 | end 168 | 169 | ## 170 | # sort by column 171 | def sort col 172 | @table.sort! { |a,b| a[col] <=> b[col] } 173 | @table.reverse! if @reverse 174 | if @reverse == false 175 | @reverse = true 176 | else 177 | @reverse = false 178 | end 179 | create 180 | end 181 | 182 | private 183 | def ascii_table_line 184 | _obj = [] 185 | _obj << Pixel.new(@pixel.fg,@bg,"+") 186 | @meta[:max_widths].each do |index,mw| 187 | (mw+2).times do |i| 188 | _obj << Pixel.new(@pixel.fg,@bg,"-") 189 | end 190 | _obj << Pixel.new(@pixel.fg,@bg,"+") 191 | end 192 | _obj 193 | end 194 | 195 | def index_col_widths 196 | @meta[:cols] = 0 197 | @meta[:max_widths] = {} 198 | @table.each do |line| 199 | cols = 0 200 | if !line.nil? 201 | line.each_with_index do |col, index| 202 | @meta[:max_widths][index] = col.size if @meta[:max_widths][index].nil? 203 | @meta[:max_widths][index] = col.size if @meta[:max_widths][index] < col.size 204 | @meta[:max_widths][index] = @cols[index][:max_length] if !@cols.nil? and !@cols[index].nil? and !@cols[index][:max_length].nil? 205 | @meta[:max_widths][index] = @cols[index][:length] if !@cols.nil? and !@cols[index].nil? and !@cols[index][:length].nil? 206 | end 207 | @meta[:cols] = cols if cols > @meta[:cols] 208 | end 209 | end 210 | end 211 | end 212 | end 213 | -------------------------------------------------------------------------------- /lib/rutui/objects.rb: -------------------------------------------------------------------------------- 1 | # This class contains different Objects 2 | 3 | module RuTui 4 | ## Base Object Class 5 | # Dynamic Objects that get drawn on the Screen 6 | # This class is used as basic class for other objects 7 | # 8 | class BaseObject 9 | attr_accessor :x, :y, :width, :height, :obj 10 | # Ex.: BaseObject.new({ :x => 1, :y => 20, :obj => [[Pixel.new(1),Pixel.new(2,4,"#")]] }) 11 | def initialize options 12 | @obj = options[:obj] 13 | @obj = Array.new(height){ Array.new(width) } if @obj.nil? 14 | 15 | @x = options[:x] 16 | @y = options[:y] 17 | @x = 1 if @x.nil? 18 | @y = 1 if @y.nil? 19 | 20 | @height = @obj.size 21 | @width = @obj[0].size 22 | end 23 | 24 | # Move object X and Y (modifiers like: -2, 2) 25 | def move x, y; @x += x; @y += y; end 26 | # Set object position X Y 27 | def set_position x, y; @x = x; @y = y; end 28 | # Set pixel on position on object 29 | def set_pixel x, y, pixel 30 | @obj[x][y] = pixel if !@obj[x].nil? and !@obj[x][y].nil? 31 | end 32 | # Get pixel by position 33 | def get_pixel x, y 34 | @obj[x][y] if !@obj[x].nil? and !@obj[x][y].nil? 35 | end 36 | 37 | # "special" each methods for objects 38 | def each 39 | return if @obj.nil? 40 | @obj.each_with_index do |row,row_count| 41 | row.each_with_index do |pixel,col_count| 42 | yield row_count, col_count, pixel 43 | end 44 | end 45 | end 46 | end 47 | 48 | ## Box Class 49 | # draw a box with borders 50 | # 51 | # Attributes (all accessible): 52 | # :width * Box width 53 | # :height * Box height 54 | # :x * Box X MUST 55 | # :y * Box Y MUST 56 | # :horizontal Horizontal Pixel 57 | # :vertical Vertical Pixel 58 | # :corner Corner Pixel 59 | # :fill Between the border lines (Color) 60 | # 61 | # Example: 62 | # Box.new({ :x => 1, :y => 42, :width => 10, :height => 5, :corner => Pixel.new(1,2,"#"), :fill => Pixel.new(4,6,":") }) 63 | # 64 | class Box < BaseObject 65 | attr_accessor :fill, :width, :height, :vertical, :horizontal, :corner 66 | 67 | # initialize object (see above) 68 | def initialize options 69 | @x = options[:x] 70 | @y = options[:y] 71 | @width = options[:width] 72 | @height = options[:height] 73 | 74 | return if @x.nil? or @y.nil? or @width.nil? or @height.nil? 75 | 76 | @fill = options[:fill] 77 | 78 | @vertical = options[:vertical] 79 | @horizontal = options[:horizontal] 80 | @corner = options[:corner] 81 | ref = Theme.get(:border) 82 | @horizontal = Pixel.new(ref.fg,ref.bg,"-") if options[:horizontal].nil? 83 | @vertical = Pixel.new(ref.fg,ref.bg,"|") if options[:vertical].nil? 84 | @corner = Pixel.new(ref.fg,ref.bg,"*") if options[:corner].nil? 85 | 86 | @width = 3 if @width < 3 87 | @height = 3 if @height < 3 88 | 89 | create 90 | end 91 | 92 | # Create Box 93 | # can be recalled any time on attribute changes 94 | def create 95 | if !@fill.nil? 96 | backpixel = Pixel.new(@fill.fg,@fill.bg,@fill.symbol) 97 | obj = Array.new(@height){ Array.new(@width) { backpixel } } 98 | else 99 | obj = Array.new(@height){ Array.new(@width) } 100 | end 101 | 102 | (@width-2).times do |i| 103 | obj[0][i+1] = @horizontal 104 | obj[@height-1][i+1] = @horizontal 105 | end 106 | 107 | (@height-2).times do |i| 108 | obj[i+1][0] = @vertical 109 | obj[i+1][@width-1] = @vertical 110 | end 111 | 112 | obj[0][0] = @corner 113 | obj[0][@width-1] = @corner 114 | obj[@height-1][0] = @corner 115 | obj[@height-1][@width-1] = @corner 116 | 117 | @height = obj.size 118 | @width = obj[0].size 119 | @obj = obj 120 | end 121 | end 122 | 123 | 124 | ## Line Class 125 | # draw a straight line 126 | # 127 | # Attributes (all accessible) 128 | # :x MUST 129 | # :y MUST 130 | # :direction :vertical|:horizontal (Default: :horizontal) 131 | # :length 132 | # :pixel 133 | # :endpixel 134 | # 135 | class Line < BaseObject 136 | attr_accessor :length, :pixel, :endpixel, :direction 137 | 138 | def initialize options 139 | @x = options[:x] 140 | @y = options[:y] 141 | @length = options[:length] 142 | @direction = options[:direction] 143 | @direction = :horizontal if @direction.nil? 144 | 145 | return if @x.nil? or @y.nil? or @length.nil? 146 | @pixel = options[:pixel] 147 | @endpixel = options[:endpixel] 148 | 149 | @pixel = Theme.get(:border) if @pixel.nil? 150 | 151 | create 152 | end 153 | 154 | # Create Line 155 | # can be recalled any time on attribute changes 156 | def create 157 | @obj = [] 158 | if @direction == :horizontal 159 | @obj = [Array.new(@length){ @pixel }] 160 | else 161 | @obj = Array.new(@length){ [@pixel] } 162 | end 163 | if !@endpixel.nil? 164 | @obj[0][0] = @endpixel 165 | @obj[0][@length-1] = @endpixel 166 | end 167 | end 168 | end 169 | 170 | ## Circle Class 171 | # draw a circle 172 | # 173 | # thx to: http://rubyquiz.strd6.com/quizzes/166-circle-drawing 174 | # 175 | # Attributes (all accessible): 176 | # :x MUST 177 | # :y MUST 178 | # :radius - Circle radius 179 | # :pixel - Circle Pixel 180 | # :fill - Actually just fills the box not the circle... 181 | # 182 | class Circle < BaseObject 183 | attr_accessor :radius, :pixel, :fill 184 | # Initialize circle (see above) 185 | def initialize options 186 | @x = options[:x] 187 | @y = options[:y] 188 | @radius = options[:radius] 189 | @pixel = options[:pixel] 190 | @fill = options[:fill_pixel] 191 | 192 | @pixel = Theme.get(:border) if @pixel.nil? 193 | 194 | return if @x.nil? or @y.nil? or @radius.nil? 195 | 196 | @width = options[:radius]*2 # needed? 197 | @height = @width # needed? 198 | 199 | create 200 | end 201 | 202 | # Create Circle 203 | # can be recalled any time on attribute changes 204 | def create 205 | obj = [] 206 | (0..(@radius*2)).each do |x| 207 | (0..(@radius*2)).each do |y| 208 | obj[y] = [] if obj[y].nil? 209 | obj[y][x] = distance_from_center(x,y).round == @radius ? @pixel : @fill 210 | end 211 | end 212 | @obj = obj 213 | end 214 | 215 | private 216 | def distance_from_center(x,y) 217 | a = calc_side(x) 218 | b = calc_side(y) 219 | return Math.sqrt(a**2 + b**2) 220 | end 221 | 222 | def calc_side(z) 223 | z < @radius ? (@radius - z) : (z - @radius) 224 | end 225 | 226 | end 227 | 228 | ## Text Object Class 229 | # Creates an Text element 230 | # 231 | # Attributes (all accessible): 232 | # :x MUST 233 | # :y MUST 234 | # :text - the text to draw 235 | # :background - Background color (0-255) 236 | # :foreground - Foreground color (0-255) 237 | # :max_width - with max_width defined text breaks to next line automatically 238 | # :rainbow - true|false|none Rainbow text 239 | # :bold - true|false|nil Bold text 240 | # :italic - true|false|nil Italic text (Not supported everywhere) 241 | # :thin - true|false|nil Thin text (Not supported everywhere) 242 | # :underline - true|false|nil Underline text (Not supported everywhere) 243 | # :blink - true|false|nil Blink text (Not supported everywhere) 244 | # 245 | class Text < BaseObject 246 | attr_accessor :bg, :fg, :text, :do_rainbow, :bold, 247 | :thin, :italic, :underline, :blink, :max_width, :pixel 248 | @@rainbow = nil 249 | 250 | def initialize options 251 | @do_rainbow = options[:rainbow] 252 | if @do_rainbow 253 | @@rainbow = Theme.get(:rainbow) if @@rainbow.nil? 254 | end 255 | @text = options[:text] 256 | @x = options[:x] 257 | @y = options[:y] 258 | @pixel = options[:pixel] 259 | @bg = options[:background] 260 | @fg = options[:foreground] 261 | if !@pixel.nil? 262 | @bg = @pixel.bg 263 | @fg = @pixel.fg 264 | end 265 | @bg = -1 if @bg.nil? 266 | @fg = Theme.get(:textcolor) if @fg.nil? 267 | @bold = options[:bold] || false 268 | @thin = options[:thin] || false 269 | @italic = options[:italic] || false 270 | @underline = options[:underline] || false 271 | @blink = options[:blink] || false 272 | @max_width = options[:max_width] 273 | return if @x.nil? or @y.nil? 274 | 275 | @height = 1 276 | create 277 | end 278 | 279 | # Create Text 280 | # can be recalled any time on attribute changes 281 | def create 282 | if @do_rainbow 283 | rainbow = 0 284 | end 285 | @width = @text.size 286 | @obj = [] 287 | 288 | texts = [@text] 289 | if !@max_width.nil? 290 | texts = @text.chars.each_slice(@max_width).map(&:join) 291 | end 292 | 293 | texts.each do |l| 294 | tmp = [] 295 | l.split("").each do |t| 296 | t = RuTui::Ansi.bold(t) if @bold 297 | t = RuTui::Ansi.thin(t) if @thin 298 | t = RuTui::Ansi.italic(t) if @italic 299 | t = RuTui::Ansi.underline(t) if @underline 300 | t = RuTui::Ansi.blink(t) if @blink 301 | if @do_rainbow 302 | tmp << Pixel.new(@@rainbow[rainbow],@bg,t) 303 | rainbow += 1 304 | rainbow = 0 if rainbow >= @@rainbow.size 305 | else 306 | tmp << Pixel.new(@fg,@bg,t) 307 | end 308 | end 309 | @obj << tmp 310 | end 311 | end 312 | 313 | def set_text text 314 | @text = text 315 | create 316 | end 317 | 318 | def get_text 319 | @text 320 | end 321 | end 322 | 323 | ## Dot Object Class 324 | # Single pixel element 325 | # 326 | # Attributes (all accessible): 327 | # :x MUST 328 | # :y MUST 329 | # :pixel MUST - Pixel 330 | # 331 | class Dot < BaseObject 332 | def initialize options 333 | @x = options[:x] 334 | @y = options[:y] 335 | @pixel = options[:pixel] 336 | return if @x.nil? or @y.nil? or @pixel.nil? 337 | create 338 | end 339 | 340 | def create 341 | @obj = [[ @pixel ]] 342 | end 343 | end 344 | end 345 | -------------------------------------------------------------------------------- /examples/res/colossal.flf: -------------------------------------------------------------------------------- 1 | flf2a$ 11 8 20 32 13 2 | Colossal.flf (Jonathon - jon@mq.edu.au) 3 | 8 June 1994 4 | 5 | Explanation of first line: 6 | flf2 - "magic number" for file identification 7 | a - should always be `a', for now 8 | $ - the "hardblank" -- prints as a blank, but can't be smushed 9 | 11 - height of a character 10 | 8 - height of a character, not including descenders 11 | 20 - max line length (excluding comment lines) + a fudge factor 12 | 32 - default smushmode for this font 13 | 13 - number of comment lines 14 | 15 | $ $@ 16 | $ $@ 17 | $ $@ 18 | $ $@ 19 | $ $@ 20 | $ $@ 21 | $ $@ 22 | $ $@ 23 | $ $@ 24 | $ $@ 25 | $ $@@ 26 | 888$@ 27 | 888$@ 28 | 888$@ 29 | 888$@ 30 | 888$@ 31 | Y8P$@ 32 | " $@ 33 | 888$@ 34 | @ 35 | @ 36 | @@ 37 | 88 88$@ 38 | 8P 8P$@ 39 | " " $@ 40 | $ $ @ 41 | $ $ @ 42 | $ $ @ 43 | $ $ @ 44 | $ $ @ 45 | $ $ @ 46 | $ $ @ 47 | $ $ @@ 48 | 888 888 $@ 49 | 888 888 $@ 50 | 888888888888$@ 51 | 888 888 $@ 52 | 888 888 $@ 53 | 888888888888$@ 54 | 888 888 $@ 55 | 888 888 $@ 56 | $@ 57 | $@ 58 | $@@ 59 | 88 $@ 60 | .d88888b. $@ 61 | d88P 88"88b$@ 62 | Y88b.88 $ @ 63 | "Y88888b.$ @ 64 | 88"88b$@ 65 | Y88b 88.88P$@ 66 | "Y88888P"$ @ 67 | 88 @ 68 | @ 69 | @@ 70 | d88b d88P$@ 71 | Y88P d88P $@ 72 | d88P $ @ 73 | d88P $ @ 74 | $ d88P $ @ 75 | $ d88P $ @ 76 | $d88P d88b$@ 77 | d88P Y88P$@ 78 | @ 79 | @ 80 | @@ 81 | .d8888b. $ @ 82 | d88P "88b $ @ 83 | Y88b. d88P $ @ 84 | "Y8888P" $ @ 85 | .d88P88K.d88P$@ 86 | 888" Y888P" $@ 87 | Y88b .d8888b $@ 88 | "Y8888P" Y88b@ 89 | @ 90 | @ 91 | @@ 92 | d8b$@ 93 | 88P$@ 94 | 8P $@ 95 | " $ @ 96 | $ @ 97 | $ @ 98 | $ @ 99 | $ @ 100 | $ @ 101 | $ @ 102 | $ @@ 103 | $.d88$@ 104 | $d88P"$@ 105 | d88P $ @ 106 | 888 $ @ 107 | 888 $ @ 108 | Y88b $ @ 109 | $Y88b.$@ 110 | $"Y88$@ 111 | @ 112 | @ 113 | @@ 114 | 88b.$ @ 115 | "Y88b$ @ 116 | Y88b$@ 117 | 888$@ 118 | 888$@ 119 | d88P$@ 120 | .d88P$ @ 121 | 88P"$ @ 122 | @ 123 | @ 124 | @@ 125 | @ 126 | o $ @ 127 | d8b $ @ 128 | d888b $ @ 129 | "Y888888888P"@ 130 | "Y88888P"$ @ 131 | d88P"Y88b $@ 132 | dP" "Yb$@ 133 | @ 134 | @ 135 | @@ 136 | $ @ 137 | $ @ 138 | $ @ 139 | 888 $@ 140 | 8888888$@ 141 | 888 $@ 142 | $ @ 143 | $ @ 144 | $ @ 145 | $ @ 146 | $ @@ 147 | $ @ 148 | $ @ 149 | $ @ 150 | $ @ 151 | $ @ 152 | $ @ 153 | d8b$@ 154 | 88P$@ 155 | 8P @ 156 | " @ 157 | @@ 158 | $ $ @ 159 | $ $ @ 160 | $ $ @ 161 | $ $ @ 162 | $ $ @ 163 | 888888$@ 164 | $ $ @ 165 | $ $ @ 166 | $ $ @ 167 | $ $ @ 168 | $ $ @@ 169 | $ @ 170 | $ @ 171 | $ @ 172 | $ @ 173 | $ @ 174 | $ @ 175 | d8b$@ 176 | Y8P$@ 177 | @ 178 | @ 179 | @@ 180 | $ d88P$@ 181 | $ d88P $@ 182 | $ d88P $ @ 183 | $ d88P $ @ 184 | $ d88P $ @ 185 | $ d88P $ @ 186 | $d88P $ @ 187 | d88P $ @ 188 | @ 189 | @ 190 | @@ 191 | $.d8888b.$ @ 192 | d88P Y88b$@ 193 | 888 888$@ 194 | 888 888$@ 195 | 888 888$@ 196 | 888 888$@ 197 | Y88b d88P$@ 198 | $"Y8888P"$ @ 199 | @ 200 | @ 201 | @@ 202 | d888 $ @ 203 | d8888 $ @ 204 | 888 $ @ 205 | 888 $ @ 206 | 888 $ @ 207 | 888 $ @ 208 | 888 $ @ 209 | 8888888$@ 210 | @ 211 | @ 212 | @@ 213 | .d8888b.$ @ 214 | d88P Y88b$@ 215 | $ 888$@ 216 | $ .d88P$@ 217 | .od888P" $@ 218 | d88P" $@ 219 | 888" $@ 220 | 888888888 $@ 221 | @ 222 | @ 223 | @@ 224 | .d8888b.$ @ 225 | d88P Y88b$@ 226 | $ .d88P$@ 227 | $ 8888" $@ 228 | $ "Y8b.$@ 229 | 888 888$@ 230 | Y88b d88P$@ 231 | "Y8888P" $@ 232 | @ 233 | @ 234 | @@ 235 | d8888 $@ 236 | d8P888 $@ 237 | d8P 888 $@ 238 | d8P 888 $@ 239 | d88 888 $@ 240 | 8888888888$@ 241 | 888 $@ 242 | 888 $@ 243 | @ 244 | @ 245 | @@ 246 | 888888888$ @ 247 | 888 $ @ 248 | 888 $ @ 249 | 8888888b.$ @ 250 | $ "Y88b$@ 251 | $ 888$@ 252 | Y88b d88P$@ 253 | "Y8888P"$ @ 254 | @ 255 | @ 256 | @@ 257 | $.d8888b.$ @ 258 | d88P Y88b$@ 259 | 888 $ @ 260 | 888d888b.$ @ 261 | 888P "Y88b$@ 262 | 888 888$@ 263 | Y88b d88P$@ 264 | $"Y8888P"$ @ 265 | @ 266 | @ 267 | @@ 268 | 8888888888$@ 269 | $ d88P$@ 270 | $ d88P $@ 271 | $ d88P $ @ 272 | $88888888$ @ 273 | $ d88P $ @ 274 | $d88P $ @ 275 | d88P $ @ 276 | @ 277 | @ 278 | @@ 279 | .d8888b.$ @ 280 | d88P Y88b$@ 281 | Y88b. d88P$@ 282 | "Y88888" $@ 283 | .d8P""Y8b.$@ 284 | 888 888$@ 285 | Y88b d88P$@ 286 | "Y8888P" $@ 287 | @ 288 | @ 289 | @@ 290 | $.d8888b.$ @ 291 | d88P Y88b$@ 292 | 888 888$@ 293 | Y88b. d888$@ 294 | $"Y888P888$@ 295 | $ 888$@ 296 | Y88b d88P$@ 297 | "Y8888P"$ @ 298 | @ 299 | @ 300 | @@ 301 | $ @ 302 | $ @ 303 | $ @ 304 | d8b$@ 305 | Y8P$@ 306 | $ @ 307 | d8b$@ 308 | Y8P$@ 309 | @ 310 | @ 311 | @@ 312 | $ @ 313 | $ @ 314 | $ @ 315 | d8b @ 316 | Y8P @ 317 | $ @ 318 | d8b$@ 319 | 88P$@ 320 | 8P @ 321 | " @ 322 | @@ 323 | $ d88P$@ 324 | $ d88P $@ 325 | d88P $ @ 326 | d88P $ @ 327 | Y88b $ @ 328 | Y88b $ @ 329 | $ Y88b $@ 330 | $ Y88b$@ 331 | @ 332 | @ 333 | @@ 334 | $ $ @ 335 | $ $ @ 336 | $ $ @ 337 | 888888$@ 338 | $ $ @ 339 | 888888$@ 340 | $ $ @ 341 | $ $ @ 342 | $ $ @ 343 | $ $ @ 344 | $ $ @@ 345 | Y88b $ @ 346 | Y88b $ @ 347 | Y88b $@ 348 | Y88b$@ 349 | d88P$@ 350 | d88P $@ 351 | d88P $ @ 352 | d88P $ @ 353 | @ 354 | @ 355 | @@ 356 | $.d8888b.$ @ 357 | d88P Y88b$@ 358 | $ .d88P$@ 359 | $ .d88P"$ @ 360 | $ 888" $ @ 361 | $ 888 $ @ 362 | $ $ @ 363 | $ 888 $ @ 364 | @ 365 | @ 366 | @@ 367 | $.d8888888b.$ @ 368 | d88P" "Y88b$@ 369 | 888 d8b 888$@ 370 | 888 888 888$@ 371 | 888 888bd88P$@ 372 | 888 Y8888P" $@ 373 | Y88b. .d8$@ 374 | $"Y88888888P"$@ 375 | @ 376 | @ 377 | @@ 378 | $d8888$@ 379 | $d88888$@ 380 | $d88P888$@ 381 | $d88P 888$@ 382 | $d88P 888$@ 383 | $d88P 888$@ 384 | $d8888888888$@ 385 | d88P 888$@ 386 | @ 387 | @ 388 | @@ 389 | 888888b.$ @ 390 | 888 "88b$ @ 391 | 888 .88P$ @ 392 | 8888888K.$ @ 393 | 888 "Y88b$@ 394 | 888 888$@ 395 | 888 d88P$@ 396 | 8888888P"$ @ 397 | @ 398 | @ 399 | @@ 400 | $.d8888b.$ @ 401 | d88P Y88b$@ 402 | 888 888$@ 403 | 888 $ @ 404 | 888 $ @ 405 | 888 888$@ 406 | Y88b d88P$@ 407 | $"Y8888P"$ @ 408 | @ 409 | @ 410 | @@ 411 | 8888888b.$ @ 412 | 888 "Y88b$@ 413 | 888 888$@ 414 | 888 888$@ 415 | 888 888$@ 416 | 888 888$@ 417 | 888 .d88P$@ 418 | 8888888P"$ @ 419 | @ 420 | @ 421 | @@ 422 | 8888888888$@ 423 | 888 $ @ 424 | 888 $ @ 425 | 8888888$ @ 426 | 888 $ @ 427 | 888 $ @ 428 | 888 $ @ 429 | 8888888888$@ 430 | @ 431 | @ 432 | @@ 433 | 8888888888$@ 434 | 888 $ @ 435 | 888 $ @ 436 | 8888888$ @ 437 | 888 $ @ 438 | 888 $ @ 439 | 888 $ @ 440 | 888 $ @ 441 | @ 442 | @ 443 | @@ 444 | $.d8888b.$ @ 445 | d88P Y88b$@ 446 | 888 888$@ 447 | 888 $@ 448 | 888 88888$@ 449 | 888 888$@ 450 | Y88b d88P$@ 451 | $"Y8888P88$@ 452 | @ 453 | @ 454 | @@ 455 | 888 888$@ 456 | 888 888$@ 457 | 888 888$@ 458 | 8888888888$@ 459 | 888 888$@ 460 | 888 888$@ 461 | 888 888$@ 462 | 888 888$@ 463 | @ 464 | @ 465 | @@ 466 | 8888888$@ 467 | 888 $ @ 468 | 888 $ @ 469 | 888 $ @ 470 | 888 $ @ 471 | 888 $ @ 472 | 888 $ @ 473 | 8888888$@ 474 | @ 475 | @ 476 | @@ 477 | 888888$@ 478 | "88b$@ 479 | 888$@ 480 | 888$@ 481 | 888$@ 482 | 888$@ 483 | 88P$@ 484 | 888$@ 485 | .d88P$@ 486 | .d88P"$ @ 487 | 888P" $ @@ 488 | 888 d8P$ @ 489 | 888 d8P $ @ 490 | 888 d8P $ @ 491 | 888d88K $ @ 492 | 8888888b $ @ 493 | 888 Y88b $ @ 494 | 888 Y88b $@ 495 | 888 Y88b$@ 496 | @ 497 | @ 498 | @@ 499 | 888 $ @ 500 | 888 $ @ 501 | 888 $ @ 502 | 888 $ @ 503 | 888 $ @ 504 | 888 $ @ 505 | 888 $ @ 506 | 88888888$@ 507 | @ 508 | @ 509 | @@ 510 | 888b d888$@ 511 | 8888b d8888$@ 512 | 88888b.d88888$@ 513 | 888Y88888P888$@ 514 | 888 Y888P 888$@ 515 | 888 Y8P 888$@ 516 | 888 " 888$@ 517 | 888 888$@ 518 | @ 519 | @ 520 | @@ 521 | 888b 888$@ 522 | 8888b 888$@ 523 | 88888b 888$@ 524 | 888Y88b 888$@ 525 | 888 Y88b888$@ 526 | 888 Y88888$@ 527 | 888 Y8888$@ 528 | 888 Y888$@ 529 | @ 530 | @ 531 | @@ 532 | $.d88888b.$ @ 533 | d88P" "Y88b$@ 534 | 888 888$@ 535 | 888 888$@ 536 | 888 888$@ 537 | 888 888$@ 538 | Y88b. .d88P$@ 539 | $"Y88888P"$ @ 540 | @ 541 | @ 542 | @@ 543 | 8888888b.$ @ 544 | 888 Y88b$@ 545 | 888 888$@ 546 | 888 d88P$@ 547 | 8888888P"$ @ 548 | 888 $ @ 549 | 888 $ @ 550 | 888 $ @ 551 | @ 552 | @ 553 | @@ 554 | $.d88888b.$ @ 555 | d88P" "Y88b$@ 556 | 888 888$@ 557 | 888 888$@ 558 | 888 888$@ 559 | 888 Y8b 888$@ 560 | Y88b.Y8b88P$@ 561 | $"Y888888" $@ 562 | Y8b $@ 563 | @ 564 | @@ 565 | 8888888b.$ @ 566 | 888 Y88b$@ 567 | 888 888$@ 568 | 888 d88P$@ 569 | 8888888P"$ @ 570 | 888 T88b $ @ 571 | 888 T88b$ @ 572 | 888 T88b$@ 573 | @ 574 | @ 575 | @@ 576 | $.d8888b.$ @ 577 | d88P Y88b$@ 578 | Y88b. $ @ 579 | $"Y888b. $ @ 580 | $ "Y88b.$@ 581 | $ "888$@ 582 | Y88b d88P$@ 583 | "Y8888P"$ @ 584 | @ 585 | @ 586 | @@ 587 | 88888888888$@ 588 | 888 $ @ 589 | 888 $ @ 590 | 888 $ @ 591 | 888 $ @ 592 | 888 $ @ 593 | 888 $ @ 594 | 888 $ @ 595 | @ 596 | @ 597 | @@ 598 | 888 888$@ 599 | 888 888$@ 600 | 888 888$@ 601 | 888 888$@ 602 | 888 888$@ 603 | 888 888$@ 604 | Y88b. .d88P$@ 605 | $"Y88888P"$ @ 606 | @ 607 | @ 608 | @@ 609 | 888 888$@ 610 | 888 888$@ 611 | 888 888$@ 612 | Y88b d88P$@ 613 | Y88b d88P $@ 614 | Y88o88P $ @ 615 | Y888P $ @ 616 | Y8P $ @ 617 | @ 618 | @ 619 | @@ 620 | 888 888$@ 621 | 888 o 888$@ 622 | 888 d8b 888$@ 623 | 888 d888b 888$@ 624 | 888d88888b888$@ 625 | 88888P Y88888$@ 626 | 8888P Y8888$@ 627 | 888P Y888$@ 628 | @ 629 | @ 630 | @@ 631 | Y88b d88P$@ 632 | Y88b d88P $@ 633 | Y88o88P $ @ 634 | Y888P $ @ 635 | d888b $ @ 636 | d88888b $ @ 637 | d88P Y88b $@ 638 | d88P Y88b$@ 639 | @ 640 | @ 641 | @@ 642 | Y88b d88P$@ 643 | Y88b d88P $@ 644 | Y88o88P $ @ 645 | Y888P $ @ 646 | 888 $ @ 647 | 888 $ @ 648 | 888 $ @ 649 | 888 $ @ 650 | @ 651 | @ 652 | @@ 653 | 8888888888P$@ 654 | $ d88P $@ 655 | $ d88P $ @ 656 | $ d88P $ @ 657 | $ d88P $ @ 658 | $ d88P $ @ 659 | $d88P $ @ 660 | d8888888888$@ 661 | @ 662 | @ 663 | @@ 664 | 8888888$@ 665 | 888 $ @ 666 | 888 $ @ 667 | 888 $ @ 668 | 888 $ @ 669 | 888 $ @ 670 | 888 $ @ 671 | 8888888$@ 672 | @ 673 | @ 674 | @@ 675 | Y88b $ @ 676 | $Y88b $ @ 677 | $ Y88b $ @ 678 | $ Y88b $ @ 679 | $ Y88b $ @ 680 | $ Y88b $ @ 681 | $ Y88b $@ 682 | $ Y88b$@ 683 | @ 684 | @ 685 | @@ 686 | 8888888$@ 687 | $ 888$@ 688 | $ 888$@ 689 | $ 888$@ 690 | $ 888$@ 691 | $ 888$@ 692 | $ 888$@ 693 | 8888888$@ 694 | @ 695 | @ 696 | @@ 697 | o$ @ 698 | d8b$ @ 699 | d888b$ @ 700 | d8P"Y8b$@ 701 | $ $ @ 702 | $ $ @ 703 | $ $ @ 704 | $ $ @ 705 | $ $ @ 706 | $ $ @ 707 | $ $ @@ 708 | $ $ @ 709 | $ $ @ 710 | $ $ @ 711 | $ $ @ 712 | $ $ @ 713 | $ $ @ 714 | $ $ @ 715 | 88888888$@ 716 | @ 717 | @ 718 | @@ 719 | d8b$@ 720 | Y88$@ 721 | Y8$@ 722 | Y$@ 723 | $ @ 724 | $ @ 725 | $ @ 726 | $ @ 727 | $ @ 728 | $ @ 729 | $ @@ 730 | @ 731 | @ 732 | @ 733 | $8888b. $@ 734 | $ "88b$@ 735 | .d888888$@ 736 | 888 888$@ 737 | "Y888888$@ 738 | @ 739 | @ 740 | @@ 741 | 888 $ @ 742 | 888 $ @ 743 | 888 $ @ 744 | 88888b.$ @ 745 | 888 "88b$@ 746 | 888 888$@ 747 | 888 d88P$@ 748 | 88888P"$ @ 749 | @ 750 | @ 751 | @@ 752 | @ 753 | @ 754 | @ 755 | $.d8888b$@ 756 | d88P" $ @ 757 | 888 $ @ 758 | Y88b. $ @ 759 | $"Y8888P$@ 760 | @ 761 | @ 762 | @@ 763 | 888$@ 764 | 888$@ 765 | 888$@ 766 | $.d88888$@ 767 | d88" 888$@ 768 | 888 888$@ 769 | Y88b 888$@ 770 | $"Y88888$@ 771 | @ 772 | @ 773 | @@ 774 | @ 775 | @ 776 | @ 777 | $.d88b.$ @ 778 | d8P Y8b$@ 779 | 88888888$@ 780 | Y8b.$ @ 781 | $"Y8888$ @ 782 | @ 783 | @ 784 | @@ 785 | $.d888$@ 786 | d88P"$ @ 787 | 888 $ @ 788 | 888888$@ 789 | 888 $ @ 790 | 888 $ @ 791 | 888 $ @ 792 | 888 $ @ 793 | @ 794 | @ 795 | @@ 796 | @ 797 | @ 798 | @ 799 | $.d88b.$ @ 800 | d88P"88b$@ 801 | 888 888$@ 802 | Y88b 888$@ 803 | $"Y88888$@ 804 | $ 888$@ 805 | Y8b d88P$@ 806 | "Y88P"$ @@ 807 | 888 $ @ 808 | 888 $ @ 809 | 888 $ @ 810 | 88888b.$ @ 811 | 888 "88b$@ 812 | 888 888$@ 813 | 888 888$@ 814 | 888 888$@ 815 | @ 816 | @ 817 | @@ 818 | d8b$@ 819 | Y8P$@ 820 | $ $@ 821 | 888$@ 822 | 888$@ 823 | 888$@ 824 | 888$@ 825 | 888$@ 826 | @ 827 | @ 828 | @@ 829 | $d8b$@ 830 | $Y8P$@ 831 | $ $@ 832 | $8888$@ 833 | $"888$@ 834 | $ 888$@ 835 | $ 888$@ 836 | $ 888$@ 837 | $ 888$@ 838 | $ d88P$@ 839 | 888P"$ @@ 840 | 888 $ @ 841 | 888 $ @ 842 | 888 $ @ 843 | 888 888$@ 844 | 888 .88P$@ 845 | 888888K$ @ 846 | 888 "88b$@ 847 | 888 888$@ 848 | @ 849 | @ 850 | @@ 851 | 888$@ 852 | 888$@ 853 | 888$@ 854 | 888$@ 855 | 888$@ 856 | 888$@ 857 | 888$@ 858 | 888$@ 859 | @ 860 | @ 861 | @@ 862 | @ 863 | @ 864 | @ 865 | 88888b.d88b.$ @ 866 | 888 "888 "88b$@ 867 | 888 888 888$@ 868 | 888 888 888$@ 869 | 888 888 888$@ 870 | @ 871 | @ 872 | @@ 873 | @ 874 | @ 875 | @ 876 | 88888b.$ @ 877 | 888 "88b$@ 878 | 888 888$@ 879 | 888 888$@ 880 | 888 888$@ 881 | @ 882 | @ 883 | @@ 884 | @ 885 | @ 886 | @ 887 | $.d88b.$ @ 888 | d88""88b$@ 889 | 888 888$@ 890 | Y88..88P$@ 891 | $"Y88P"$ @ 892 | @ 893 | @ 894 | @@ 895 | @ 896 | @ 897 | @ 898 | 88888b.$ @ 899 | 888 "88b$@ 900 | 888 888$@ 901 | 888 d88P$@ 902 | 88888P"$ @ 903 | 888 $ @ 904 | 888 $ @ 905 | 888 $ @@ 906 | @ 907 | @ 908 | @ 909 | $.d88888$@ 910 | d88" 888$@ 911 | 888 888$@ 912 | Y88b 888$@ 913 | $"Y88888$@ 914 | $ 888$@ 915 | $ 888$@ 916 | $ 888$@@ 917 | @ 918 | @ 919 | @ 920 | 888d888$@ 921 | 888P"$ @ 922 | 888 $ @ 923 | 888 $ @ 924 | 888 $ @ 925 | @ 926 | @ 927 | @@ 928 | @ 929 | @ 930 | @ 931 | .d8888b$ @ 932 | 88K $ @ 933 | "Y8888b.$@ 934 | $ X88$@ 935 | $88888P'$@ 936 | @ 937 | @ 938 | @@ 939 | 888 $ @ 940 | 888 $ @ 941 | 888 $ @ 942 | 888888$@ 943 | 888 $ @ 944 | 888 $ @ 945 | Y88b.$ @ 946 | "Y888$@ 947 | @ 948 | @ 949 | @@ 950 | @ 951 | @ 952 | @ 953 | 888 888$@ 954 | 888 888$@ 955 | 888 888$@ 956 | Y88b 888$@ 957 | $"Y88888$@ 958 | @ 959 | @ 960 | @@ 961 | @ 962 | @ 963 | @ 964 | 888 888$@ 965 | 888 888$@ 966 | Y88 88P$@ 967 | $Y8bd8P$ @ 968 | $ Y88P $ @ 969 | @ 970 | @ 971 | @@ 972 | @ 973 | @ 974 | @ 975 | 888 888 888$@ 976 | 888 888 888$@ 977 | 888 888 888$@ 978 | Y88b 888 d88P$@ 979 | $"Y8888888P"$ @ 980 | @ 981 | @ 982 | @@ 983 | @ 984 | @ 985 | @ 986 | 888 888$@ 987 | `Y8bd8P'$@ 988 | $ X88K $ @ 989 | .d8""8b.$@ 990 | 888 888$@ 991 | @ 992 | @ 993 | @@ 994 | @ 995 | @ 996 | @ 997 | 888 888$@ 998 | 888 888$@ 999 | 888 888$@ 1000 | Y88b 888$@ 1001 | $"Y88888$@ 1002 | $ 888$@ 1003 | Y8b d88P$@ 1004 | $"Y88P"$ @@ 1005 | @ 1006 | @ 1007 | @ 1008 | 88888888$@ 1009 | $ d88P $@ 1010 | $ d88P $ @ 1011 | $d88P $ @ 1012 | 88888888$@ 1013 | @ 1014 | @ 1015 | @@ 1016 | $.d888$@ 1017 | $d88P"$ @ 1018 | $888 $ @ 1019 | .888 $ @ 1020 | 888( $ @ 1021 | "888 $ @ 1022 | $888 $ @ 1023 | $Y88b.$ @ 1024 | $"Y888$@ 1025 | @ 1026 | @@ 1027 | $ 888 $@ 1028 | $ 888 $@ 1029 | $ 888 $@ 1030 | $ 888 $@ 1031 | $ $@ 1032 | $ 888 $@ 1033 | $ 888 $@ 1034 | $ 888 $@ 1035 | $ 888 $@ 1036 | @ 1037 | @@ 1038 | 888b. $ @ 1039 | $"Y88b $@ 1040 | $ 888 $@ 1041 | $ 888.$@ 1042 | $ )888$@ 1043 | $ 888"$@ 1044 | $ 888 $@ 1045 | $.d88P $@ 1046 | 888P" $ @ 1047 | @ 1048 | @@ 1049 | @ 1050 | @ 1051 | $d888b d88$@ 1052 | d888888888P$@ 1053 | 88P Y888P$ @ 1054 | @ 1055 | @ 1056 | @ 1057 | @ 1058 | @ 1059 | @@ 1060 | d8b d8b@ 1061 | Y8P Y8P@ 1062 | $d88888$@ 1063 | $d88P888$@ 1064 | $d88P 888$@ 1065 | $d88P 888$@ 1066 | $d888888888$@ 1067 | $d88P 888$@ 1068 | @ 1069 | @ 1070 | @@ 1071 | d8b d8b @ 1072 | Y8P Y8P @ 1073 | $.d88888b.$ @ 1074 | d88P" "Y88b$@ 1075 | 888 888$@ 1076 | 888 888$@ 1077 | Y88b. .d88P$@ 1078 | $"Y88888P"$ @ 1079 | @ 1080 | @ 1081 | @@ 1082 | d8b d8b @ 1083 | Y8P Y8P @ 1084 | 888 888$@ 1085 | 888 888$@ 1086 | 888 888$@ 1087 | 888 888$@ 1088 | Y88b. .d88P$@ 1089 | $"Y88888P"$ @ 1090 | @ 1091 | @ 1092 | @@ 1093 | d8b d8b @ 1094 | Y8P Y8P @ 1095 | @ 1096 | $8888b. $@ 1097 | $ "88b$@ 1098 | .d888888$@ 1099 | 888 888$@ 1100 | "Y888888$@ 1101 | @ 1102 | @ 1103 | @@ 1104 | d8b d8b @ 1105 | Y8P Y8P @ 1106 | @ 1107 | $.d88b.$ @ 1108 | d88""88b$@ 1109 | 888 888$@ 1110 | Y88..88P$@ 1111 | $"Y88P"$ @ 1112 | @ 1113 | @ 1114 | @@ 1115 | d8b d8b @ 1116 | Y8P Y8P @ 1117 | @ 1118 | 888 888$@ 1119 | 888 888$@ 1120 | 888 888$@ 1121 | Y88b 888$@ 1122 | $"Y88888$@ 1123 | @ 1124 | @ 1125 | @@ 1126 | .d888b.$ @ 1127 | d88" "88b$ @ 1128 | 888 .88P$ @ 1129 | 888 888K.$ @ 1130 | 888 "Y88b$@ 1131 | 888 888$@ 1132 | 888 d88P$@ 1133 | 888 888P"$ @ 1134 | 888 @ 1135 | 888 @ 1136 | @@ 1137 | --------------------------------------------------------------------------------