termular 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env ruby
2
+ require "termular/bootstrap"
@@ -0,0 +1,33 @@
1
+ require "termular/parser"
2
+ require "termular/context"
3
+ require "termular/console"
4
+ require "termular/graph"
5
+ require "termular/ui"
6
+ require "ncurses"
7
+ require "paint"
8
+
9
+ Termular::Console.init
10
+ ui = Termular::UI.new
11
+ ui.render
12
+
13
+ loop do
14
+ c = nil
15
+ if ui.needs_tick?
16
+ hack = Thread.new { c = STDIN.getc }
17
+ hack.join 0.1
18
+ hack.kill
19
+ if c.nil?
20
+ ui.tick
21
+ next
22
+ end
23
+ else
24
+ c = STDIN.getc
25
+ end
26
+ if c.ord == 27
27
+ hack = Thread.new { c << STDIN.getc; c << STDIN.getc; c << STDIN.getc }
28
+ hack.join 0.001
29
+ hack.kill
30
+ end
31
+ ui.dispatch_keypress c
32
+ ui.render
33
+ end
@@ -0,0 +1,46 @@
1
+ module Termular
2
+ module Console
3
+ def self.init
4
+ Ncurses.initscr
5
+ Ncurses.raw
6
+ Ncurses.noecho
7
+ Ncurses.keypad Ncurses.stdscr, true
8
+ Ncurses.refresh
9
+ at_exit { Ncurses.endwin }
10
+ end
11
+
12
+ def self.move(x, y)
13
+ "\e[#{y.to_i};#{x.to_i}H"
14
+ end
15
+
16
+ def self.clear
17
+ "\e[H\e[2J"
18
+ end
19
+
20
+ def self.clear_line
21
+ "\e[0G\e[2K"
22
+ end
23
+
24
+ def self.color(*col)
25
+ Paint["|", *col].split("|").first
26
+ end
27
+
28
+ def self.reset
29
+ "\e[m"
30
+ end
31
+
32
+ def self.cols
33
+ Ncurses.getmaxx Ncurses.stdscr
34
+ end
35
+
36
+ def self.rows
37
+ Ncurses.getmaxy Ncurses.stdscr
38
+ end
39
+
40
+ def self.buffered_print
41
+ buff = ""
42
+ yield buff
43
+ print buff
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,54 @@
1
+ module Termular
2
+ class Context
3
+ def self.global
4
+ @@global ||= begin
5
+ ctx = Context.new
6
+ %w( acos acosh asin asinh atan atan2 atanh cbrt cos cosh erf erfc exp
7
+ log log10 log2 sin sinh sqrt tan tanh ).each do |m|
8
+ ctx[m] = Math.method m
9
+ end
10
+ %w( PI E ).each do |c|
11
+ ctx[c.downcase] = Math.const_get c
12
+ end
13
+ ctx["ln"] = ctx["log"]
14
+ ctx["abs"] = ->x { x.abs }
15
+ ctx["arg"] = ->x { x.arg }
16
+ ctx["ceil"] = ->x { x.ceil }
17
+ ctx["floor"] = ->x { x.floor }
18
+ ctx["int"] = ->x { x.to_i }
19
+ ctx["mod"] = ->a,b { a % b }
20
+ ctx
21
+ end
22
+ end
23
+
24
+ attr_accessor :vars, :parent
25
+
26
+ def initialize(parent = nil)
27
+ @vars, @parent = {}, parent
28
+ end
29
+
30
+ def [](var)
31
+ if vars.key? var
32
+ vars[var]
33
+ elsif parent
34
+ parent[var]
35
+ else
36
+ raise "undefined variable '#{var}'"
37
+ end
38
+ end
39
+
40
+ def []=(var, val)
41
+ if vars.key? var
42
+ vars[var] = val
43
+ elsif parent and parent.has? val
44
+ parent[var] = val
45
+ else
46
+ vars[var] = val
47
+ end
48
+ end
49
+
50
+ def has?(var)
51
+ vars.key?(var) or parent && parent.has?(var)
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,152 @@
1
+ module Termular
2
+ class Graph
3
+ attr_accessor :expression, :max_x, :max_y, :min_x, :min_y, :needs_redraw, :start_time, :options
4
+
5
+ def initialize(expression)
6
+ @expression = expression
7
+ @max_x = 10
8
+ @min_x = -10
9
+ factor = (Console.rows * 2.4 / Console.cols.to_f)
10
+ @max_y = factor * @max_x
11
+ @min_y = factor * @min_x
12
+ @start_time = Time.now.to_f
13
+ @options = {}
14
+ invalidate
15
+ end
16
+
17
+ def invalidate
18
+ @needs_redraw = true
19
+ end
20
+
21
+ def pan(fx, fy)
22
+ x = (max_x - min_x) * fx
23
+ y = (max_y - min_y) * fy
24
+ @min_x += x
25
+ @max_x += x
26
+ @min_y += y
27
+ @max_y += y
28
+ invalidate
29
+ end
30
+
31
+ def center
32
+ w = max_x - min_x
33
+ h = max_y - min_y
34
+
35
+ @max_x = w/2.0
36
+ @min_x = -@max_x
37
+ @max_y = h/2.0
38
+ @min_y = -@max_y
39
+ invalidate
40
+ end
41
+
42
+ def zoom(factor)
43
+ w = max_x - min_x
44
+ h = max_y - min_y
45
+
46
+ cx = min_x + w/2.0
47
+ cy = min_y + h/2.0
48
+
49
+ w /= factor.to_f
50
+ h /= factor.to_f
51
+
52
+ @min_x = cx - w/2.0
53
+ @max_x = cx + w/2.0
54
+ @min_y = cy - h/2.0
55
+ @max_y = cy + h/2.0
56
+ invalidate
57
+ end
58
+
59
+ def point_to_screen(x, y)
60
+ sw = Console.cols
61
+ sh = Console.rows
62
+ pw = max_x - min_x
63
+ ph = max_y - min_y
64
+ sx = (((x - min_x) / pw.to_f) * sw).round
65
+ sy = sh - (((y - min_y) / ph.to_f) * sh).round
66
+ [sx, sy]
67
+ end
68
+
69
+ def screen_to_point(x, y)
70
+ sw = Console.cols
71
+ sh = Console.rows
72
+ pw = max_x - min_x
73
+ ph = max_y - min_y
74
+ sx = ((x / sw.to_f) * pw) + min_x
75
+ sy = ((y / sh.to_f) * ph) + min_y
76
+ [sx, sy]
77
+ end
78
+
79
+ def render_axes
80
+ buff = Console.clear
81
+ scr_origin = point_to_screen 0, 0
82
+ buff << Console.color(:white)
83
+ if scr_origin[0] >= 0 and scr_origin[0] <= Console.cols
84
+ 0.upto(Console.rows).each do |y|
85
+ buff << Console.move(scr_origin[0], y)
86
+ buff << "|"
87
+ end
88
+ end
89
+ if scr_origin[1] >= 0 and scr_origin[1] <= Console.rows
90
+ buff << Console.move(0, scr_origin[1])
91
+ buff << "-" * Console.cols
92
+ if scr_origin[0] >= 0 and scr_origin[0] <= Console.cols
93
+ buff << Console.move(scr_origin[0], scr_origin[1])
94
+ buff << "+"
95
+ end
96
+ end
97
+ buff
98
+ end
99
+
100
+ class Cartesian < Graph
101
+ def render
102
+ @needs_redraw = false
103
+ Console.buffered_print do |buff|
104
+ buff << render_axes
105
+ # render actual graph
106
+ ctx = Context.new Context.global
107
+ buff << Console.color(:green)
108
+ ctx["time"] = Time.now.to_f - @start_time
109
+ old_y = nil
110
+ 0.upto(Console.cols).map { |x| screen_to_point(x, 0)[0] }.each do |x|
111
+ ctx["x"] = x
112
+ py = expression.eval ctx
113
+ scr_pt = point_to_screen(x, py)
114
+ new_y = scr_pt[1]
115
+ (old_y ? (old_y > new_y ? (old_y - 1).downto(new_y) : (old_y < new_y ? (old_y + 1).upto(new_y) : [new_y])) : [new_y]).each do |y|
116
+ if scr_pt[0] >= 0 and scr_pt[0] <= Console.cols and y >= 0 and y <= Console.rows
117
+ buff << Console.move(scr_pt[0], y)
118
+ buff << "+"
119
+ end
120
+ end
121
+ old_y = new_y
122
+ end
123
+ buff << Console.reset
124
+ end
125
+ end
126
+ end
127
+
128
+ class Polar < Graph
129
+ def render
130
+ @needs_redraw = false
131
+ Console.buffered_print do |buff|
132
+ buff << render_axes
133
+ # render actual graph
134
+ ctx = Context.new Context.global
135
+ buff << Console.color(:green)
136
+ ctx["time"] = Time.now.to_f - @start_time
137
+ old_y = nil
138
+ (options["tmin"] || 0).upto(((options["tmax"] || 2 * Math::PI) * 100).round).map { |t| t / 100.0 }.each do |theta|
139
+ ctx["t"] = theta
140
+ radius = expression.eval ctx
141
+ scr_pt = point_to_screen(radius*Math.cos(theta), radius*Math.sin(theta))
142
+ if scr_pt[0] >= 0 and scr_pt[0] <= Console.cols and scr_pt[1] >= 0 and scr_pt[1] <= Console.rows
143
+ buff << Console.move(scr_pt[0], scr_pt[1])
144
+ buff << "+"
145
+ end
146
+ end
147
+ buff << Console.reset
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,218 @@
1
+ module Termular
2
+ class SyntaxError < StandardError; end
3
+
4
+ module AST
5
+ class Base
6
+ def initialize(hash = {})
7
+ hash.each { |k,v| send "#{k}=", v }
8
+ end
9
+ end
10
+
11
+ class Binary < Base
12
+ attr_accessor :left, :right
13
+ end
14
+
15
+ class Add < Binary; def eval(ctx) left.eval(ctx) + right.eval(ctx) end end
16
+ class Subtract < Binary; def eval(ctx) left.eval(ctx) - right.eval(ctx) end end
17
+ class Multiply < Binary; def eval(ctx) left.eval(ctx) * right.eval(ctx) end end
18
+ class Divide < Binary; def eval(ctx) left.eval(ctx) / right.eval(ctx) end end
19
+ class Power < Binary; def eval(ctx) left.eval(ctx) ** right.eval(ctx) end end
20
+
21
+ class Negate < Base
22
+ attr_accessor :expression
23
+ def eval(ctx)
24
+ -expression.eval(ctx)
25
+ end
26
+ end
27
+
28
+ class Variable < Base
29
+ attr_accessor :name
30
+ def eval(ctx)
31
+ ctx[name]
32
+ end
33
+ end
34
+
35
+ class Number < Base
36
+ attr_accessor :number
37
+ def eval(ctx)
38
+ number
39
+ end
40
+ end
41
+
42
+ class Call < Base
43
+ attr_accessor :callee
44
+ attr_accessor :args
45
+ def eval(ctx)
46
+ callee.eval(ctx).call *args.map { |a| a.eval(ctx) }
47
+ end
48
+ end
49
+
50
+ class CartesianCommand < Base
51
+ attr_accessor :expression
52
+ end
53
+
54
+ class PolarCommand < Base
55
+ attr_accessor :expression
56
+ end
57
+
58
+ class OptionCommand < Base
59
+ attr_accessor :option, :expression
60
+ end
61
+
62
+ class QuitCommand < Base; end
63
+ end
64
+
65
+ class Lexer
66
+ TOKENS = [
67
+ [ :WHITESPACE, /\s+/ ],
68
+ [ :NUMBER, /\d+(\.\d+)?/, ->m { m[0].to_f } ],
69
+ [ :BAREWORD, /[a-z][a-z0-9]*/, ->m { m[0] } ],
70
+ [ :PLUS, /\+/ ],
71
+ [ :MINUS, /-/ ],
72
+ [ :TIMES, /\*/ ],
73
+ [ :DOT, /\./ ],
74
+ [ :DIVIDE, /\// ],
75
+ [ :POWER, /\^/ ],
76
+ [ :OPEN_PAREN, /\(/ ],
77
+ [ :CLOSE_PAREN, /\)/ ],
78
+ [ :OPEN_BRACKET, /\[/ ],
79
+ [ :CLOSE_BRACKET, /\]/ ],
80
+ [ :COMMA, /\,/ ],
81
+ [ :COMMAND, /:([a-z][a-z0-9]*)/, ->m { m[1] } ]
82
+ ].map { |a| [a[0], Regexp.new("\\A#{a[1].source}", Regexp::MULTILINE), a[2]] }
83
+
84
+ def self.lex!(original_src)
85
+ tokens = []
86
+ offset = 0
87
+ src = original_src
88
+ until src.empty?
89
+ match = nil
90
+ tok, re, conv = TOKENS.find { |tok, re, conv| match = re.match(src, offset) }
91
+ raise SyntaxError, "illegal character at position #{original_src.length - src.length}" unless match
92
+ tokens << [tok, conv && conv[match]] unless tok == :WHITESPACE
93
+ src = match.post_match
94
+ end
95
+ tokens << [:END]
96
+ end
97
+ end
98
+
99
+ class Parser
100
+ def self.parse!(src)
101
+ Parser.new(src).parse
102
+ end
103
+
104
+ def initialize(src)
105
+ @src = src
106
+ @tokens = Lexer.lex! src
107
+ @i = -1
108
+ end
109
+
110
+ def token
111
+ @tokens[@i]
112
+ end
113
+
114
+ def peek_token
115
+ @tokens[@i + 1]
116
+ end
117
+
118
+ def next_token
119
+ @tokens[@i += 1]
120
+ end
121
+
122
+ def assert_type(token, *types)
123
+ unless types.include? token[0]
124
+ raise SyntaxError, "Expected one of #{types.join ", "}, found #{token[0]}"
125
+ end
126
+ end
127
+
128
+ def parse
129
+ cmd = if peek_token[0] == :COMMAND
130
+ command
131
+ else
132
+ AST::CartesianCommand.new expression: expression
133
+ end
134
+ assert_type next_token, :END
135
+ cmd
136
+ end
137
+
138
+ def command
139
+ assert_type next_token, :COMMAND
140
+ case token[1]
141
+ when "q"; AST::QuitCommand.new
142
+ when "c"; AST::CartesianCommand.new expression: expression
143
+ when "p"; AST::PolarCommand.new expression: expression
144
+ when "opt"; option_command
145
+ end
146
+ end
147
+
148
+ def option_command
149
+ assert_type next_token, :BAREWORD
150
+ opt = token[1]
151
+ AST::OptionCommand.new option: opt, expression: expression
152
+ end
153
+
154
+ def expression
155
+ additive_expression
156
+ end
157
+
158
+ def additive_expression
159
+ expr = multiplicative_expression
160
+ while [:PLUS, :MINUS].include? peek_token[0]
161
+ case next_token[0]
162
+ when :PLUS; expr = AST::Add.new left: expr, right: multiplicative_expression
163
+ when :MINUS; expr = AST::Subtract.new left: expr, right: multiplicative_expression
164
+ end
165
+ end
166
+ expr
167
+ end
168
+
169
+ def multiplicative_expression
170
+ expr = power_expression
171
+ while [:TIMES, :DIVIDE].include? peek_token[0]
172
+ case next_token[0]
173
+ when :TIMES; expr = AST::Multiply.new left: expr, right: power_expression
174
+ when :DIVIDE; expr = AST::Divide.new left: expr, right: power_expression
175
+ end
176
+ end
177
+ expr
178
+ end
179
+
180
+ def power_expression
181
+ expr = call_expression
182
+ if peek_token[0] == :POWER
183
+ next_token
184
+ expr = AST::Power.new left: expr, right: power_expression
185
+ end
186
+ expr
187
+ end
188
+
189
+ def call_expression
190
+ expr = unary_expression
191
+ while peek_token[0] == :OPEN_BRACKET
192
+ next_token
193
+ args = []
194
+ args << expression
195
+ while peek_token[0] == :COMMA
196
+ next_token
197
+ args << expression
198
+ end
199
+ expr = AST::Call.new callee: expr, args: args
200
+ assert_type next_token, :CLOSE_BRACKET
201
+ end
202
+ expr
203
+ end
204
+
205
+ def unary_expression
206
+ case next_token[0]
207
+ when :MINUS; AST::Negate.new expression: unary_expression
208
+ when :BAREWORD; AST::Variable.new name: token[1]
209
+ when :NUMBER; AST::Number.new number: token[1]
210
+ when :OPEN_PAREN; expr = expression
211
+ assert_type next_token, :CLOSE_PAREN
212
+ expr
213
+ else
214
+ raise SyntaxError, "Unexpected #{token[0]}"
215
+ end
216
+ end
217
+ end
218
+ end
@@ -0,0 +1,166 @@
1
+ module Termular
2
+ class UI
3
+ attr_accessor :current_graph, :current_command, :insert_mode, :expression, :cursor_offset, :invalidate_next
4
+
5
+ def draw_status_line
6
+ Console.buffered_print do |s|
7
+ s << Console.move(0, Console.rows) <<
8
+ Console.clear_line <<
9
+ Console.color(:black, :white) <<
10
+ (" " * Console.cols) <<
11
+ Console.move(0, Console.rows) <<
12
+ expression
13
+ if insert_mode
14
+ msg = "--INSERT--"
15
+ s << Console.move(Console.cols - msg.length + 1, Console.rows) <<
16
+ Console.color(:black, :white) << msg <<
17
+ Console.move(cursor_offset + 1, Console.rows)
18
+ else
19
+ s << Console.move(Console.cols, Console.rows)
20
+ end
21
+ s << Console.reset
22
+ end
23
+ end
24
+
25
+ def dispatch_keypress(k)
26
+ if invalidate_next
27
+ current_graph and current_graph.invalidate
28
+ @invalidate_next = false
29
+ end
30
+ return dispatch_insert_keypress k if insert_mode
31
+ case k[0]
32
+ when "h" # move left
33
+ current_graph.pan -0.2, 0
34
+ when "H"
35
+ current_graph.pan -0.05, 0
36
+ when "l" # move right
37
+ current_graph.pan 0.2, 0
38
+ when "L"
39
+ current_graph.pan 0.05, 0
40
+ when "j" # move down
41
+ current_graph.pan 0, -0.2
42
+ when "J"
43
+ current_graph.pan 0, -0.05
44
+ when "k" # move up
45
+ current_graph.pan 0, 0.2
46
+ when "K"
47
+ current_graph.pan 0, 0.05
48
+ when " " # center
49
+ current_graph.center
50
+ when "]" # zoom in
51
+ current_graph.zoom 2
52
+ when "[" # zoom out
53
+ current_graph.zoom 0.5
54
+ when "i"
55
+ @insert_mode = true
56
+ when ":"
57
+ @insert_mode = true
58
+ @expression = ":"
59
+ @cursor_offset = 1
60
+ when "o"
61
+ @insert_mode = true
62
+ @expression = ""
63
+ @cursor_offset = 0
64
+ else
65
+ print Console.move 0, 0
66
+ print k.ord
67
+ end
68
+
69
+ #print Termular::Console.move 0, Termular::Console.rows
70
+ #print c.ord
71
+ exit if k[0] == "\3"
72
+ end
73
+
74
+ def dispatch_insert_keypress(k)
75
+ case k[0].ord
76
+ when 127 # backspace
77
+ unless cursor_offset.zero?
78
+ @cursor_offset -= 1
79
+ expression[cursor_offset..cursor_offset] = ""
80
+ end
81
+ when 10 # enter
82
+ begin
83
+ @current_command = Parser.parse! expression
84
+ if current_command.is_a? AST::CartesianCommand
85
+ @current_graph = Graph::Cartesian.new current_command.expression
86
+ @needs_tick = expression =~ /time/
87
+ elsif current_command.is_a? AST::PolarCommand
88
+ @current_graph = Graph::Polar.new current_command.expression
89
+ @needs_tick = expression =~ /time/
90
+ elsif current_command.is_a? AST::OptionCommand
91
+ if current_graph
92
+ current_graph.options[current_command.option] = current_command.expression.eval(Context.global)
93
+ current_graph.invalidate
94
+ end
95
+ elsif current_command.is_a? AST::QuitCommand
96
+ exit
97
+ end
98
+ rescue => e
99
+ show_exception "#{e.class.name} #{e.message}"
100
+ end
101
+ when 27 # special key
102
+ if k == "\e"
103
+ # legit escape
104
+ @insert_mode = false
105
+ return
106
+ end
107
+ case k[1..-1]
108
+ when "OH" # home
109
+ @cursor_offset = 0
110
+ when "OF" # end
111
+ @cursor_offset = expression.length
112
+ when "OD" # arrow left
113
+ @cursor_offset -= 1 unless cursor_offset.zero?
114
+ when "OC" # arrow right
115
+ @cursor_offset += 1 unless cursor_offset == expression.length
116
+ when "[3~"
117
+ expression[cursor_offset..cursor_offset] = ""
118
+ else
119
+ # print k.each_byte.to_a.join ","
120
+ # raise k[1..-1]#k.each_byte.to_a.join ","
121
+ end
122
+ when 0x20..0x7E
123
+ # expression << k.ord.to_s << " "
124
+ expression[cursor_offset, 0] = k
125
+ @cursor_offset += 1
126
+ end
127
+ end
128
+
129
+ def show_exception(msg)
130
+ Console.buffered_print do |s|
131
+ s << Console.move(0, Console.rows - 1) <<
132
+ Console.color(:bright, :white, :red) <<
133
+ msg <<
134
+ Console.reset
135
+ end
136
+ # this writes over where the graph goes, so invalidate it
137
+ # however if we invalidate it now, it gets immediately dismissed, so set
138
+ # a special flag
139
+ @invalidate_next = true
140
+ end
141
+
142
+ def render
143
+ Console.clear
144
+ begin
145
+ current_graph.render if current_graph and current_graph.needs_redraw
146
+ rescue => e
147
+ show_exception e.message
148
+ end
149
+ draw_status_line
150
+ end
151
+
152
+ def tick
153
+ current_graph and current_graph.invalidate
154
+ render
155
+ end
156
+
157
+ def needs_tick?
158
+ @needs_tick
159
+ end
160
+
161
+ def initialize
162
+ @cursor_offset = 0
163
+ @expression = ""
164
+ end
165
+ end
166
+ end
metadata ADDED
@@ -0,0 +1,76 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: termular
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Charlie Somerville
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-01-07 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: ncurses-ruby
16
+ requirement: &2152096840 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *2152096840
25
+ - !ruby/object:Gem::Dependency
26
+ name: paint
27
+ requirement: &2152095940 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *2152095940
36
+ description: Termular Grapher is a simple graphing program that's capable of cartesian
37
+ and polar graphs
38
+ email:
39
+ - charlie@charliesomerville.com
40
+ executables:
41
+ - termular
42
+ extensions: []
43
+ extra_rdoc_files: []
44
+ files:
45
+ - bin/termular
46
+ - lib/termular/bootstrap.rb
47
+ - lib/termular/console.rb
48
+ - lib/termular/context.rb
49
+ - lib/termular/graph.rb
50
+ - lib/termular/parser.rb
51
+ - lib/termular/ui.rb
52
+ homepage: http://github.com/charliesome/termular
53
+ licenses: []
54
+ post_install_message:
55
+ rdoc_options: []
56
+ require_paths:
57
+ - lib
58
+ required_ruby_version: !ruby/object:Gem::Requirement
59
+ none: false
60
+ requirements:
61
+ - - ! '>='
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ required_rubygems_version: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ requirements: []
71
+ rubyforge_project:
72
+ rubygems_version: 1.8.10
73
+ signing_key:
74
+ specification_version: 3
75
+ summary: Simple terminal grapher with vim-like keybindings
76
+ test_files: []