termular 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/termular +2 -0
- data/lib/termular/bootstrap.rb +33 -0
- data/lib/termular/console.rb +46 -0
- data/lib/termular/context.rb +54 -0
- data/lib/termular/graph.rb +152 -0
- data/lib/termular/parser.rb +218 -0
- data/lib/termular/ui.rb +166 -0
- metadata +76 -0
data/bin/termular
ADDED
@@ -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
|
data/lib/termular/ui.rb
ADDED
@@ -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: []
|