termular 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []