tkar 0.63
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/FAQ.rdoc +39 -0
- data/History.txt +175 -0
- data/README.rdoc +153 -0
- data/TODO +67 -0
- data/bin/tkar +104 -0
- data/examples/dial.rb +172 -0
- data/examples/help.gif +0 -0
- data/examples/home.gif +0 -0
- data/examples/mkgrid.rb +58 -0
- data/examples/ps.rb +47 -0
- data/examples/rotate +26 -0
- data/examples/s +3 -0
- data/examples/sample +14 -0
- data/examples/sample.rb +98 -0
- data/examples/sample2 +48 -0
- data/examples/sample3 +57 -0
- data/examples/server.rb +45 -0
- data/examples/tavis.rb +90 -0
- data/install.rb +1015 -0
- data/lib/tkar.rb +109 -0
- data/lib/tkar/argos.rb +214 -0
- data/lib/tkar/canvas.rb +370 -0
- data/lib/tkar/help-window.rb +168 -0
- data/lib/tkar/primitives.rb +376 -0
- data/lib/tkar/stream.rb +284 -0
- data/lib/tkar/timer.rb +174 -0
- data/lib/tkar/tkaroid.rb +95 -0
- data/lib/tkar/version.rb +5 -0
- data/lib/tkar/window.rb +383 -0
- data/protocol.rdoc +539 -0
- data/rakefile +56 -0
- data/tasks/ann.rake +80 -0
- data/tasks/bones.rake +20 -0
- data/tasks/gem.rake +201 -0
- data/tasks/git.rake +40 -0
- data/tasks/notes.rake +27 -0
- data/tasks/post_load.rake +34 -0
- data/tasks/rdoc.rake +51 -0
- data/tasks/rubyforge.rake +55 -0
- data/tasks/setup.rb +292 -0
- data/tasks/spec.rake +54 -0
- data/tasks/svn.rake +47 -0
- data/tasks/test.rake +40 -0
- data/tasks/zentest.rake +36 -0
- metadata +116 -0
data/lib/tkar/stream.rb
ADDED
@@ -0,0 +1,284 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'thread'
|
3
|
+
|
4
|
+
module Tkar
|
5
|
+
module MessageStream
|
6
|
+
# Get a bidirectional stream for sending and receiving Tkar
|
7
|
+
# protocol messages in either binary or ascii format.
|
8
|
+
def self.for(argv, opts = {})
|
9
|
+
binary, client = opts["b"], opts["c"]
|
10
|
+
(binary ? Binary : Ascii).new(opts, *get_fds(argv, client))
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.get_fds argv, client
|
14
|
+
case argv.size
|
15
|
+
when 0
|
16
|
+
[$stdin, $stdout]
|
17
|
+
when 1
|
18
|
+
case argv[0]
|
19
|
+
when /^\d+$/
|
20
|
+
if client
|
21
|
+
[TCPSocket.new("127.0.0.1", Integer(argv[0]))]
|
22
|
+
else
|
23
|
+
server = TCPServer.new("127.0.0.1", Integer(argv[0]))
|
24
|
+
flag = Socket.do_not_reverse_lookup
|
25
|
+
Socket.do_not_reverse_lookup = false
|
26
|
+
port = server.addr[1]
|
27
|
+
Socket.do_not_reverse_lookup = flag
|
28
|
+
puts "listening on port #{port}"
|
29
|
+
[server.accept]
|
30
|
+
end
|
31
|
+
else
|
32
|
+
if client
|
33
|
+
[UNIXSocket.new(argv[0])]
|
34
|
+
else
|
35
|
+
[UNIXServer.new(argv[0]).accept]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
when 2
|
39
|
+
case argv[1]
|
40
|
+
when /^\d+$/
|
41
|
+
if client
|
42
|
+
[TCPSocket.new(argv[0], Integer(argv[1]))]
|
43
|
+
else
|
44
|
+
[TCPServer.new(argv[0], Integer(argv[1])).accept]
|
45
|
+
end
|
46
|
+
else
|
47
|
+
raise "Bad arguments--second arg must be port: #{argv.inspect}"
|
48
|
+
end
|
49
|
+
else
|
50
|
+
raise "Too many arguments: #{argv.inspect}"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class StreamClosed < IOError; end
|
55
|
+
|
56
|
+
class Base
|
57
|
+
def initialize(opts, fd_in, fd_out = fd_in)
|
58
|
+
@flip = opts["flip"]
|
59
|
+
@radians = opts["radians"]
|
60
|
+
@verbose = opts["v"]
|
61
|
+
|
62
|
+
@fd_in, @fd_out = fd_in, fd_out
|
63
|
+
@fd_in_stack = []
|
64
|
+
@fd_out_mutex = Mutex.new
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Translate ascii command text to ruby method calls.
|
69
|
+
class Ascii < Base
|
70
|
+
NORMALIZE = {}
|
71
|
+
|
72
|
+
%w{ a>dd d>el|delete m>ove>to r>ot|rotate p>ar>am s>h>ape u>p>date title
|
73
|
+
background|bg height width zoom_to|zoom view_at|view view_id wait
|
74
|
+
follow done bound>s load exit delete_all scale>_obj echo window_xy
|
75
|
+
}.each do |s|
|
76
|
+
alts = s.split("|")
|
77
|
+
cmd = alts.first.delete(">")
|
78
|
+
alts.each do |alt|
|
79
|
+
parts = alt.split(">")
|
80
|
+
parts.inject("") do |prefix, part|
|
81
|
+
prefix << part
|
82
|
+
NORMALIZE[prefix] = cmd
|
83
|
+
prefix
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def conv_param(s)
|
89
|
+
s.slice!(/\.0+$/) # so that floats can be used for colors
|
90
|
+
(Integer(s) rescue Float(s)) rescue s
|
91
|
+
end
|
92
|
+
|
93
|
+
def flip_Float(s)
|
94
|
+
@flip ? -Float(s) : Float(s)
|
95
|
+
end
|
96
|
+
|
97
|
+
def conv_angle(s)
|
98
|
+
r = flip_Float(s)
|
99
|
+
@radians ? r : r * DEGREES_TO_RADIANS
|
100
|
+
end
|
101
|
+
|
102
|
+
ARG_CONVERSION = {
|
103
|
+
"add" => [nil, :Integer, nil, :Integer,
|
104
|
+
:Float, :flip_Float, :conv_angle, :conv_param],
|
105
|
+
"del" => [:Integer],
|
106
|
+
"moveto" => [:Integer, :Float, :flip_Float],
|
107
|
+
"rot" => [:Integer, :conv_angle],
|
108
|
+
"param" => [:Integer, :Integer, :conv_param], ## see below
|
109
|
+
"shape" => [nil, nil],
|
110
|
+
"update" => [],
|
111
|
+
"title" => [], ## see below
|
112
|
+
"background" => [:conv_param],
|
113
|
+
"height" => [:Float],
|
114
|
+
"width" => [:Float],
|
115
|
+
"zoom_to" => [:Float],
|
116
|
+
"view_at" => [:Float, :flip_Float],
|
117
|
+
"view_id" => [:Integer],
|
118
|
+
"wait" => [:Float],
|
119
|
+
"follow" => [:Integer],
|
120
|
+
"done" => [],
|
121
|
+
"bounds" => [:Float, :flip_Float, :Float, :flip_Float],
|
122
|
+
"load" => [], ## see below
|
123
|
+
"exit" => [],
|
124
|
+
"delete_all" => [],
|
125
|
+
"scale_obj" => [:Integer, :Float, :Float],
|
126
|
+
"echo" => [], ## see below
|
127
|
+
"window_xy" => [:Integer, :Integer],
|
128
|
+
}
|
129
|
+
|
130
|
+
def get_line
|
131
|
+
line = @fd_in.gets
|
132
|
+
while line == nil and not @fd_in_stack.empty?
|
133
|
+
@fd_in = @fd_in_stack.pop
|
134
|
+
line = @fd_in.gets
|
135
|
+
end
|
136
|
+
if line
|
137
|
+
while line =~ /(.*)\\\r?$/ # to allow continuation of long lines
|
138
|
+
next_line = @fd_in.gets
|
139
|
+
break unless next_line
|
140
|
+
line = $1 + next_line
|
141
|
+
end
|
142
|
+
end
|
143
|
+
line
|
144
|
+
end
|
145
|
+
|
146
|
+
class TryAgain < StandardError; end
|
147
|
+
|
148
|
+
# Returns next command in pipe, in form <tt>[:meth, arg, arg, ...]</tt>.
|
149
|
+
def get_cmd
|
150
|
+
begin
|
151
|
+
cmdline = get_line
|
152
|
+
raise StreamClosed, "Session ended" unless cmdline ## ?
|
153
|
+
end while cmdline =~ /^\s*(#|$)/
|
154
|
+
parse_cmd(cmdline)
|
155
|
+
rescue TryAgain
|
156
|
+
retry
|
157
|
+
rescue SystemCallError => ex
|
158
|
+
$stderr.puts ex.class, ex.message
|
159
|
+
raise StreamClosed, "Session ended"
|
160
|
+
end
|
161
|
+
|
162
|
+
def parse_cmd cmdline
|
163
|
+
$stderr.puts cmdline if @verbose
|
164
|
+
cmd, *args = cmdline.split
|
165
|
+
cmd = NORMALIZE[cmd] || (raise ArgumentError, "Bad command: #{cmd}")
|
166
|
+
|
167
|
+
case cmd
|
168
|
+
when "param"
|
169
|
+
return [cmd, Integer(args.shift), Integer(args.shift),
|
170
|
+
conv_param(args.join(" "))]
|
171
|
+
## hacky, and loses multiple spaces
|
172
|
+
when "title"
|
173
|
+
return [cmd, args.join(" ")]
|
174
|
+
when "echo"
|
175
|
+
str = args.join(" ")
|
176
|
+
## hacky, and loses multiple spaces
|
177
|
+
put_msg(str)
|
178
|
+
raise TryAgain # hm...
|
179
|
+
when "load"
|
180
|
+
filename = args.join(" ")
|
181
|
+
## hacky, and loses multiple spaces
|
182
|
+
begin
|
183
|
+
new_fd_in = File.open(filename)
|
184
|
+
rescue Errno::ENOENT # if not absolute, try local
|
185
|
+
raise unless @fd_in_dir
|
186
|
+
new_fd_in = File.open(File.join(@fd_in_dir, filename))
|
187
|
+
end
|
188
|
+
@fd_in_dir ||= File.dirname(new_fd_in.path)
|
189
|
+
@fd_in_stack.push(@fd_in)
|
190
|
+
@fd_in = new_fd_in
|
191
|
+
raise TryAgain # hm...
|
192
|
+
end
|
193
|
+
|
194
|
+
conv = ARG_CONVERSION[cmd]
|
195
|
+
unless conv
|
196
|
+
raise "No argument conversion for command #{cmd.inspect}"
|
197
|
+
end
|
198
|
+
i = -1; last_i = conv.length - 1
|
199
|
+
args.map! do |arg|
|
200
|
+
i += 1 unless i == last_i # keep using the last conversion thereafter
|
201
|
+
(c = conv[i]) ? send(c, arg) : arg
|
202
|
+
end
|
203
|
+
[cmd, *args]
|
204
|
+
end
|
205
|
+
|
206
|
+
def put_msg(msg)
|
207
|
+
@fd_out_mutex.synchronize do ## why necessary?
|
208
|
+
@fd_out.puts msg
|
209
|
+
end
|
210
|
+
@fd_out.flush
|
211
|
+
rescue Errno::ECONNABORTED
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
# Translate binary command data to ruby method calls.
|
216
|
+
class Binary < Base
|
217
|
+
def get_cmd
|
218
|
+
lendata = @fd_in.recv(4)
|
219
|
+
raise "Session ended" if lendata.empty?
|
220
|
+
|
221
|
+
len = lendata.unpack("N")
|
222
|
+
if len < 4
|
223
|
+
raise ArgumentError, "Input too short: #{len}"
|
224
|
+
end
|
225
|
+
if len > 10000
|
226
|
+
raise ArgumentError, "Input too long: #{len}"
|
227
|
+
end
|
228
|
+
|
229
|
+
msg = ""
|
230
|
+
part = nil
|
231
|
+
while (delta = len - msg.length) > 0 and (part = @fd_in.recv(delta))
|
232
|
+
if part.length == 0
|
233
|
+
raise \
|
234
|
+
"Peer closed socket before finishing message --" +
|
235
|
+
" received #{msg.length} of #{len} bytes:\n" +
|
236
|
+
msg[0..99].unpack("H*")[0] + "..."
|
237
|
+
end
|
238
|
+
msg << part
|
239
|
+
end
|
240
|
+
|
241
|
+
raise StreamClosed, "Session ended" if msg.empty?
|
242
|
+
parse_cmd(msg)
|
243
|
+
end
|
244
|
+
|
245
|
+
CMD_DATA = {
|
246
|
+
1 => ["add", "Z* N Z* N g3 N*"],
|
247
|
+
2 => ["del", "N"],
|
248
|
+
3 => ["moveto", "N g2"],
|
249
|
+
4 => ["rot", "N g"],
|
250
|
+
5 => ["param", "N n N"], ## fix to allow strings
|
251
|
+
6 => ["shape", "Z* Z*"],
|
252
|
+
7 => ["update", ""],
|
253
|
+
8 => ["title", "Z*"],
|
254
|
+
9 => ["background", "N"],
|
255
|
+
10 => ["height", "g"],
|
256
|
+
11 => ["width", "g"],
|
257
|
+
12 => ["zoom_to", "g"],
|
258
|
+
13 => ["view_at", "g2"],
|
259
|
+
14 => ["view_id", "N"],
|
260
|
+
15 => ["wait", "g"],
|
261
|
+
16 => ["follow", "N"],
|
262
|
+
17 => ["done", ""],
|
263
|
+
18 => ["bounds", "NNNN"],
|
264
|
+
19 => ["load", "Z*"],
|
265
|
+
20 => ["exit", ""],
|
266
|
+
21 => ["delete_all", ""],
|
267
|
+
22 => ["scale_obj", "N g2"],
|
268
|
+
23 => ["echo", "Z*"],
|
269
|
+
24 => ["window_xy", "NN"],
|
270
|
+
}
|
271
|
+
|
272
|
+
def parse_cmd msg
|
273
|
+
cmd = msg[0..1].unpack("n")
|
274
|
+
cmd, fmt = CMD_DATA[cmd]
|
275
|
+
[cmd, *msg[2..-1].unpack(fmt)]
|
276
|
+
end
|
277
|
+
|
278
|
+
def put_msg(msg)
|
279
|
+
@fd_out.puts msg ## assume output ascii for now
|
280
|
+
@fd_out.flush
|
281
|
+
end
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
data/lib/tkar/timer.rb
ADDED
@@ -0,0 +1,174 @@
|
|
1
|
+
# Ruby license. Copyright (C)2004-2009 Joel VanderWerf.
|
2
|
+
# Contact mailto:vjoel@users.sourceforge.net.
|
3
|
+
#
|
4
|
+
# A lightweight, non-drifting, self-correcting timer. Average error is bounded
|
5
|
+
# as long as, on average, there is enough time to complete the work done, and
|
6
|
+
# the timer is checked often enough. It is lightweight in the sense that no
|
7
|
+
# threads are created. Can be used either as an internal iterator (Timer.every)
|
8
|
+
# or as an external iterator (Timer.new). Obviously, the GC can cause a
|
9
|
+
# temporary slippage.
|
10
|
+
#
|
11
|
+
# Simple usage:
|
12
|
+
#
|
13
|
+
# require 'timer'
|
14
|
+
#
|
15
|
+
# Timer.every(0.1, 0.5) { |elapsed| puts elapsed }
|
16
|
+
#
|
17
|
+
# timer = Timer.new(0.1)
|
18
|
+
# 5.times do
|
19
|
+
# puts timer.elapsed
|
20
|
+
# timer.wait
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
class Timer
|
24
|
+
# Yields to the supplied block every +period+ seconds. The value yielded is
|
25
|
+
# the total elapsed time (an instance of +Time+). If +expire+ is given, then
|
26
|
+
# #every returns after that amount of elapsed time.
|
27
|
+
def Timer.every(period, expire = nil)
|
28
|
+
target = time_start = Time.now
|
29
|
+
loop do
|
30
|
+
elapsed = Time.now - time_start
|
31
|
+
break if expire and elapsed > expire
|
32
|
+
yield elapsed
|
33
|
+
target += period
|
34
|
+
error = target - Time.now
|
35
|
+
sleep error if error > 0
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Make a Timer that can be checked when needed, using #wait or #if_ready. The
|
40
|
+
# advantage over Timer.every is that the timer can be checked on separate
|
41
|
+
# passes through a loop.
|
42
|
+
def initialize(period = 1)
|
43
|
+
@period = period
|
44
|
+
restart
|
45
|
+
end
|
46
|
+
|
47
|
+
attr_accessor :period
|
48
|
+
|
49
|
+
# Call this to restart the timer after a period of inactivity (e.g., the user
|
50
|
+
# hits the pause button, and then hits the go button).
|
51
|
+
def restart
|
52
|
+
@target = @time_start = Time.now
|
53
|
+
end
|
54
|
+
|
55
|
+
# Time on timer since instantiation or last #restart.
|
56
|
+
def elapsed
|
57
|
+
Time.now - @time_start
|
58
|
+
end
|
59
|
+
|
60
|
+
# Wait for the next cycle, if time remains in the current cycle. Otherwise,
|
61
|
+
# return immediately to caller.
|
62
|
+
def wait(per = nil)
|
63
|
+
@target += per || @period
|
64
|
+
error = @target - Time.now
|
65
|
+
sleep error if error > 0
|
66
|
+
true
|
67
|
+
end
|
68
|
+
|
69
|
+
# Yield to the block if no time remains in cycle. Otherwise, return
|
70
|
+
# immediately to caller
|
71
|
+
def if_ready
|
72
|
+
error = @target + @period - Time.now
|
73
|
+
if error <= 0
|
74
|
+
@target += @period
|
75
|
+
elapsed = Time.now - @time_start
|
76
|
+
yield elapsed
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
if __FILE__ == $0
|
82
|
+
|
83
|
+
require 'test/unit'
|
84
|
+
|
85
|
+
# These tests may not work on a slow machine or heavily loaded system; try
|
86
|
+
# adjusting FUDGE.
|
87
|
+
class Test_Timer < Test::Unit::TestCase # :nodoc:
|
88
|
+
|
89
|
+
STEPS = 100
|
90
|
+
PERIOD = 0.01
|
91
|
+
FUDGE = 0.01 # a constant independent of period.
|
92
|
+
|
93
|
+
def generic_test(steps = STEPS, period = PERIOD, fudge = FUDGE)
|
94
|
+
max_sleep = period/2
|
95
|
+
|
96
|
+
start_time = Time.now
|
97
|
+
yield steps, period, max_sleep
|
98
|
+
finish_time = Time.now
|
99
|
+
|
100
|
+
assert_in_delta(
|
101
|
+
start_time.to_f + steps*period,
|
102
|
+
finish_time.to_f,
|
103
|
+
period + fudge,
|
104
|
+
"delta = #{finish_time.to_f - (start_time.to_f + steps*period)}"
|
105
|
+
)
|
106
|
+
end
|
107
|
+
|
108
|
+
def test_every
|
109
|
+
generic_test do |steps, period, max_sleep|
|
110
|
+
Timer.every period do |elapsed|
|
111
|
+
s = rand()*max_sleep
|
112
|
+
#puts "#{elapsed} elapsed; sleeping #{s}"
|
113
|
+
sleep(s)
|
114
|
+
steps -= 1
|
115
|
+
break if steps == 0
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def test_every_with_expire
|
121
|
+
generic_test do |steps, period, max_sleep|
|
122
|
+
Timer.every period, period*steps do
|
123
|
+
s = rand()*max_sleep
|
124
|
+
#puts "#{elapsed} elapsed; sleeping #{s}"
|
125
|
+
sleep(s)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def test_wait
|
131
|
+
generic_test do |steps, period, max_sleep|
|
132
|
+
timer = Timer.new(period)
|
133
|
+
steps.times do
|
134
|
+
s = rand()*max_sleep
|
135
|
+
#puts "#{timer.elapsed} elapsed; sleeping #{s}"
|
136
|
+
sleep(s)
|
137
|
+
timer.wait
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def test_wait_per
|
143
|
+
offset = PERIOD / 2
|
144
|
+
generic_test(STEPS, PERIOD, FUDGE + offset) do |steps, period, max_sleep|
|
145
|
+
timer = Timer.new(period)
|
146
|
+
steps.times do |i|
|
147
|
+
s = rand()*max_sleep
|
148
|
+
#puts "#{timer.elapsed} elapsed; sleeping #{s}"
|
149
|
+
sleep(s)
|
150
|
+
timer.wait(i%2==0 ? period + offset : period - offset)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def test_if_ready
|
156
|
+
generic_test do |steps, period, max_sleep|
|
157
|
+
timer = Timer.new(period)
|
158
|
+
catch :done do
|
159
|
+
loop do
|
160
|
+
timer.if_ready do |elapsed|
|
161
|
+
s = rand()*max_sleep
|
162
|
+
#puts "#{elapsed} elapsed; sleeping #{s}"
|
163
|
+
sleep(s)
|
164
|
+
steps -= 1
|
165
|
+
throw :done if steps == 0
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
end
|
173
|
+
|
174
|
+
end
|