tkar 0.63
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/.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
|