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.
- 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: []
|