termtter 0.8.3

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.
Files changed (93) hide show
  1. data/History.txt +4 -0
  2. data/README.rdoc +97 -0
  3. data/Rakefile +46 -0
  4. data/bin/kill_termtter +22 -0
  5. data/bin/termtter +7 -0
  6. data/lib/filter/en2ja.rb +11 -0
  7. data/lib/filter/english.rb +8 -0
  8. data/lib/filter/expand-tinyurl.rb +24 -0
  9. data/lib/filter/fib.rb +15 -0
  10. data/lib/filter/ignore.rb +19 -0
  11. data/lib/filter/reply.rb +8 -0
  12. data/lib/filter/reverse.rb +13 -0
  13. data/lib/filter/url_addspace.rb +16 -0
  14. data/lib/filter/yhara.rb +20 -0
  15. data/lib/plugin/april_fool.rb +15 -0
  16. data/lib/plugin/bomb.rb +29 -0
  17. data/lib/plugin/clear.rb +14 -0
  18. data/lib/plugin/confirm.rb +9 -0
  19. data/lib/plugin/cool.rb +10 -0
  20. data/lib/plugin/devel.rb +13 -0
  21. data/lib/plugin/english.rb +59 -0
  22. data/lib/plugin/erb.rb +17 -0
  23. data/lib/plugin/favorite.rb +75 -0
  24. data/lib/plugin/fib.rb +8 -0
  25. data/lib/plugin/filter.rb +69 -0
  26. data/lib/plugin/follow.rb +56 -0
  27. data/lib/plugin/graduatter.rb +9 -0
  28. data/lib/plugin/grass.rb +27 -0
  29. data/lib/plugin/group.rb +60 -0
  30. data/lib/plugin/growl.rb +62 -0
  31. data/lib/plugin/hatebu.rb +59 -0
  32. data/lib/plugin/history.rb +82 -0
  33. data/lib/plugin/keyword.rb +18 -0
  34. data/lib/plugin/log.rb +63 -0
  35. data/lib/plugin/modify_arg_hook_sample.rb +7 -0
  36. data/lib/plugin/msagent.rb +26 -0
  37. data/lib/plugin/multi_reply.rb +36 -0
  38. data/lib/plugin/notify-send.rb +17 -0
  39. data/lib/plugin/otsune.rb +21 -0
  40. data/lib/plugin/outputz.rb +35 -0
  41. data/lib/plugin/pause.rb +3 -0
  42. data/lib/plugin/plugin.rb +53 -0
  43. data/lib/plugin/post_exec_hook_sample.rb +9 -0
  44. data/lib/plugin/pre_exec_hook_sample.rb +9 -0
  45. data/lib/plugin/primes.rb +23 -0
  46. data/lib/plugin/quicklook.rb +38 -0
  47. data/lib/plugin/reblog.rb +40 -0
  48. data/lib/plugin/reload.rb +3 -0
  49. data/lib/plugin/say.rb +24 -0
  50. data/lib/plugin/scrape.rb +41 -0
  51. data/lib/plugin/screen.rb +24 -0
  52. data/lib/plugin/shell.rb +14 -0
  53. data/lib/plugin/sl.rb +48 -0
  54. data/lib/plugin/spam.rb +9 -0
  55. data/lib/plugin/standard_plugins.rb +269 -0
  56. data/lib/plugin/stdout.rb +62 -0
  57. data/lib/plugin/system_status.rb +33 -0
  58. data/lib/plugin/translation.rb +28 -0
  59. data/lib/plugin/update_editor.rb +53 -0
  60. data/lib/plugin/uri-open.rb +69 -0
  61. data/lib/plugin/wassr_post.rb +22 -0
  62. data/lib/plugin/yhara.rb +148 -0
  63. data/lib/plugin/yonda.rb +20 -0
  64. data/lib/termtter/api.rb +14 -0
  65. data/lib/termtter/client.rb +399 -0
  66. data/lib/termtter/command.rb +77 -0
  67. data/lib/termtter/connection.rb +39 -0
  68. data/lib/termtter/hook.rb +18 -0
  69. data/lib/termtter/status.rb +26 -0
  70. data/lib/termtter/task.rb +16 -0
  71. data/lib/termtter/task_manager.rb +116 -0
  72. data/lib/termtter/twitter.rb +173 -0
  73. data/lib/termtter/user.rb +13 -0
  74. data/lib/termtter/version.rb +3 -0
  75. data/lib/termtter.rb +157 -0
  76. data/spec/plugin/cool_spec.rb +10 -0
  77. data/spec/plugin/fib_spec.rb +16 -0
  78. data/spec/plugin/filter_spec.rb +18 -0
  79. data/spec/plugin/plugin_spec.rb +25 -0
  80. data/spec/plugin/shell_spec.rb +10 -0
  81. data/spec/plugin/spam_spec.rb +17 -0
  82. data/spec/plugin/standard_plugins_spec.rb +31 -0
  83. data/spec/spec_helper.rb +4 -0
  84. data/spec/termtter/client_spec.rb +175 -0
  85. data/spec/termtter/command_spec.rb +161 -0
  86. data/spec/termtter/task_manager_spec.rb +78 -0
  87. data/spec/termtter/task_spec.rb +22 -0
  88. data/spec/termtter/user_spec.rb +27 -0
  89. data/spec/termtter_spec.rb +43 -0
  90. data/test/friends_timeline.json +5 -0
  91. data/test/search.json +8 -0
  92. data/test/test_termtter.rb +86 -0
  93. metadata +177 -0
@@ -0,0 +1,399 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ module Termtter
4
+ class CommandNotFound < StandardError; end
5
+
6
+ module Client
7
+
8
+ class << self
9
+
10
+ def init
11
+ @@hooks = []
12
+ @@new_hooks = {}
13
+ @@commands = {}
14
+ @@new_commands = {}
15
+ @@completions = []
16
+ @@filters = []
17
+ @@helps = []
18
+ @@since_id = nil
19
+ @@main_thread = nil
20
+ @@input_thread = nil
21
+ @@task_manager = Termtter::TaskManager.new
22
+ end
23
+
24
+ def public_storage
25
+ @@public_storage ||= {}
26
+ end
27
+
28
+ %w[hook completion filter].each do |n|
29
+ eval <<-EOF
30
+ def add_#{n}(&b)
31
+ @@#{n}s << b
32
+ end
33
+ EOF
34
+ end
35
+
36
+ # Deprecated
37
+ # FIXME: delete when become unnecessary
38
+ def add_command(regex, &block)
39
+ warn "Termtter:Client.add_command method will be removed. Use Termtter::Client.register_command() instead. (#{caller.first})"
40
+ @@commands[regex] = block
41
+ end
42
+
43
+ def register_hook(arg)
44
+ hook = case arg
45
+ when Hook
46
+ arg
47
+ when Hash
48
+ Hook.new(arg)
49
+ else
50
+ raise ArgumentError, 'must be given Termtter::Hook or Hash'
51
+ end
52
+ @@new_hooks[hook.name] = hook
53
+ end
54
+
55
+ def get_hook(name)
56
+ @@new_hooks[name]
57
+ end
58
+
59
+ def get_hooks(point)
60
+ @@new_hooks.values.select do |hook|
61
+ hook.match?(point)
62
+ end
63
+ end
64
+
65
+ def register_command(arg)
66
+ command = case arg
67
+ when Command
68
+ arg
69
+ when Hash
70
+ Command.new(arg)
71
+ else
72
+ raise ArgumentError, 'must be given Termtter::Command or Hash'
73
+ end
74
+ @@new_commands[command.name] = command
75
+ end
76
+
77
+ def get_command(name)
78
+ @@new_commands[name]
79
+ end
80
+
81
+ def register_macro(name, macro, options = {})
82
+ command = {
83
+ :name => name.to_sym,
84
+ :exec_proc => lambda {|arg| call_commands(macro % arg)}
85
+ }.merge(options)
86
+ register_command(command)
87
+ end
88
+
89
+ def add_help(name, desc)
90
+ @@helps << [name, desc]
91
+ end
92
+
93
+ %w[hooks commands completions helps filters].each do |n|
94
+ eval <<-EOF
95
+ def clear_#{n}
96
+ @@#{n}.clear
97
+ end
98
+ EOF
99
+ end
100
+
101
+ # memo: each filter must return Array of Status
102
+ def apply_filters(statuses)
103
+ filtered = statuses.map{|s| s.dup }
104
+ @@filters.each do |f|
105
+ filtered = f.call(filtered)
106
+ end
107
+ filtered
108
+ rescue => e
109
+ handle_error(e)
110
+ statuses
111
+ end
112
+
113
+ def do_hooks(statuses, event)
114
+ @@hooks.each do |h|
115
+ begin
116
+ h.call(statuses.dup, event, Termtter::API.twitter)
117
+ rescue => e
118
+ handle_error(e)
119
+ end
120
+ end
121
+ end
122
+
123
+ # return last hook return value
124
+ def call_new_hooks(point, *args)
125
+ result = nil
126
+ get_hooks(point).each {|hook|
127
+ break if result == false # interrupt if hook return false
128
+ result = hook.exec_proc.call(*args)
129
+ }
130
+ return result
131
+ rescue => e
132
+ if point.to_sym == :on_error
133
+ raise
134
+ else
135
+ handle_error(e)
136
+ end
137
+ end
138
+
139
+ # TODO: delete argument "tw" when unnecessary
140
+ def call_hooks(statuses, event, tw = nil)
141
+ do_hooks(statuses, :pre_filter)
142
+ do_hooks(apply_filters(statuses), event)
143
+ end
144
+
145
+ def call_commands(text, tw = nil)
146
+ return if text.empty?
147
+
148
+ command_found = false
149
+ @@commands.each do |key, command|
150
+ if key =~ text
151
+ command_found = true
152
+ @@task_manager.invoke_and_wait do
153
+ command.call($~, Termtter::API.twitter)
154
+ end
155
+ end
156
+ end
157
+
158
+ @@new_commands.each do |key, command|
159
+ command_info = command.match?(text)
160
+ if command_info
161
+ command_found = true
162
+ input_command, arg = *command_info
163
+
164
+ modified_arg = call_new_hooks("modify_arg_for_#{command.name.to_s}", input_command, arg) || arg || ''
165
+
166
+ @@task_manager.invoke_and_wait do
167
+ pre_exec_hook_result = call_new_hooks("pre_exec_#{command.name.to_s}", input_command, modified_arg)
168
+ next if pre_exec_hook_result == false
169
+
170
+ # exec command
171
+ result = command.execute(modified_arg)
172
+ if result
173
+ call_new_hooks("post_exec_#{command.name.to_s}", input_command, modified_arg, result)
174
+ end
175
+ end
176
+ end
177
+ end
178
+
179
+ raise CommandNotFound unless command_found
180
+ end
181
+
182
+ def pause
183
+ @@task_manager.pause
184
+ end
185
+
186
+ def resume
187
+ @@task_manager.resume
188
+ end
189
+
190
+ def add_task(*arg, &block)
191
+ @@task_manager.add_task(*arg, &block)
192
+ end
193
+
194
+ def exit
195
+ puts 'finalizing...'
196
+
197
+ call_hooks([], :exit)
198
+ call_new_hooks(:exit)
199
+ @@task_manager.kill
200
+ @@main_thread.kill if @@main_thread
201
+ @@input_thread.kill if @@input_thread
202
+ end
203
+
204
+ def load_default_plugins
205
+ plugin 'standard_plugins'
206
+ plugin 'stdout'
207
+ configatron.set_default(:devel, false)
208
+ if configatron.devel
209
+ plugin 'devel'
210
+ end
211
+ end
212
+
213
+ def load_config
214
+ conf_file = File.expand_path('~/.termtter')
215
+ if File.exist? conf_file
216
+ wrap_require do
217
+ load conf_file
218
+ end
219
+ else
220
+ ui = create_highline
221
+ username = ui.ask('your twitter username: ')
222
+ password = ui.ask('your twitter password: ') { |q| q.echo = false }
223
+
224
+ File.open(File.expand_path('~/.termtter'), 'w') {|io|
225
+ plugins = Dir.glob(File.dirname(__FILE__) + "/../lib/plugin/*.rb").map {|f|
226
+ f.match(%r|lib/plugin/(.*?).rb$|)[1]
227
+ }
228
+ plugins -= %w[stdout standard_plugins]
229
+ plugins.each do |p|
230
+ io.puts "#plugin '#{p}'"
231
+ end
232
+
233
+ io.puts
234
+ io.puts "configatron.user_name = '#{username}'"
235
+ io.puts "configatron.password = '#{password}'"
236
+ io.puts "#configatron.update_interval = 120"
237
+ io.puts "#configatron.proxy.host = 'proxy host'"
238
+ io.puts "#configatron.proxy.port = '8080'"
239
+ io.puts "#configatron.proxy.user_name = 'proxy user'"
240
+ io.puts "#configatron.proxy.password = 'proxy password'"
241
+ io.puts
242
+ io.puts "# vim: set filetype=ruby"
243
+ }
244
+ puts "generated: ~/.termtter"
245
+ puts "enjoy!"
246
+ wrap_require do
247
+ load conf_file
248
+ end
249
+ end
250
+ end
251
+
252
+ def setup_readline
253
+ Readline.basic_word_break_characters= "\t\n\"\\'`><=;|&{("
254
+ Readline.completion_proc = lambda {|input|
255
+ begin
256
+ # FIXME: when migrate to Termtter::Command
257
+ completions = @@completions.map {|completion|
258
+ completion.call(input)
259
+ }
260
+ completions += @@new_commands.map {|name, command|
261
+ command.complement(input)
262
+ }
263
+ completions.flatten.compact
264
+ rescue => e
265
+ handle_error(e)
266
+ end
267
+ }
268
+ vi_or_emacs = configatron.editing_mode
269
+ unless vi_or_emacs.empty?
270
+ Readline.__send__("#{vi_or_emacs}_editing_mode")
271
+ end
272
+ end
273
+
274
+ def setup_update_timeline_task()
275
+ register_command(
276
+ :name => :_update_timeline,
277
+ :exec_proc => lambda {|arg|
278
+ begin
279
+ statuses = Termtter::API.twitter.get_friends_timeline(@@since_id)
280
+ unless statuses.empty?
281
+ @@since_id = statuses[0].id
282
+ end
283
+ print "\e[1K\e[0G" if !statuses.empty? && !win?
284
+ call_hooks(statuses, :update_friends_timeline)
285
+ @@input_thread.kill if @@input_thread && !statuses.empty?
286
+ rescue OpenURI::HTTPError => e
287
+ if e.message == '401 Unauthorized'
288
+ puts 'Could not login'
289
+ puts 'plese check your account settings'
290
+ exit!
291
+ end
292
+ end
293
+ }
294
+ )
295
+
296
+ add_task(:name => :update_timeline, :interval => configatron.update_interval) do
297
+ call_commands('_update_timeline')
298
+ end
299
+ end
300
+
301
+ def trap_setting()
302
+ begin
303
+ stty_save = `stty -g`.chomp
304
+ trap("INT") do
305
+ begin
306
+ system "stty", stty_save
307
+ ensure
308
+ exit
309
+ end
310
+ end
311
+ rescue Errno::ENOENT
312
+ end
313
+ end
314
+
315
+ def start_input_thread
316
+ setup_readline()
317
+ trap_setting()
318
+ @@main_thread = Thread.new do
319
+ loop do
320
+ @@input_thread = create_input_thread()
321
+ @@input_thread.join
322
+ end
323
+ end
324
+ @@main_thread.join
325
+ end
326
+
327
+ def run
328
+ puts 'initializing...'
329
+
330
+ load_default_plugins()
331
+ load_config()
332
+ Termtter::API.setup()
333
+
334
+ call_hooks([], :initialize)
335
+ call_new_hooks(:initialize)
336
+
337
+ setup_update_timeline_task()
338
+ call_commands('_update_timeline')
339
+
340
+ @@task_manager.run()
341
+ start_input_thread()
342
+ end
343
+
344
+ def create_input_thread()
345
+ Thread.new do
346
+ while buf = Readline.readline(ERB.new(configatron.prompt).result(API.twitter.__send__(:binding)), true)
347
+ Readline::HISTORY.pop if /^(u|update)\s+(.+)$/ =~ buf
348
+ begin
349
+ call_commands(buf)
350
+ rescue CommandNotFound => e
351
+ puts "Unknown command \"#{buf}\""
352
+ puts 'Enter "help" for instructions'
353
+ end
354
+ end
355
+ exit # exit when press Control-D
356
+ end
357
+ end
358
+
359
+ def create_highline
360
+ HighLine.track_eof = false
361
+ if $stdin.respond_to?(:getbyte) # for ruby1.9
362
+ require 'delegate'
363
+ stdin_for_highline = SimpleDelegator.new($stdin)
364
+ def stdin_for_highline.getc
365
+ getbyte
366
+ end
367
+ else
368
+ stdin_for_highline = $stdin
369
+ end
370
+ return HighLine.new(stdin_for_highline)
371
+ end
372
+
373
+ def handle_error(e)
374
+ call_new_hooks("on_error", e)
375
+ rescue => e
376
+ puts "Error: #{e}"
377
+ puts e.backtrace.join("\n")
378
+ end
379
+
380
+ def wrap_require
381
+ # FIXME: delete this method after the major version up
382
+ alias original_require require
383
+ def require(s)
384
+ if %r|^termtter/(.*)| =~ s
385
+ puts "[WARNING] use plugin '#{$1}' instead of require"
386
+ puts " Such a legacy .termtter file will not be supported until version 1.0.0"
387
+ s = "plugin/#{$1}"
388
+ end
389
+ original_require s
390
+ end
391
+ yield
392
+ alias require original_require
393
+ end
394
+ private :wrap_require
395
+ end
396
+ end
397
+ end
398
+
399
+ Termtter::Client.init
@@ -0,0 +1,77 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ module Termtter
4
+ class Command
5
+ attr_accessor :name, :aliases, :exec_proc, :completion_proc, :help
6
+
7
+ # args
8
+ # name: Symbol as command name
9
+ # aliases: Array of command alias (ex. ['u', 'up'])
10
+ # exec_proc: Proc for procedure of the command. If need the proc must return object for hook.
11
+ # completion_proc: Proc for input completion. The proc must return Array of candidates (Optional)
12
+ # help: help text for the command (Optional)
13
+ def initialize(args)
14
+ raise ArgumentError, ":name is not given." unless args.has_key?(:name)
15
+ @name = args[:name].to_sym
16
+ @aliases = args[:aliases] ? args[:aliases].map {|i| i.to_sym } : []
17
+ @exec_proc = args[:exec_proc] || lambda {|arg|}
18
+ @completion_proc = args[:completion_proc] || lambda {|command, arg| [] }
19
+ @help = args[:help]
20
+ end
21
+
22
+ def complement(input)
23
+ command_info = match?(input)
24
+ if command_info
25
+ [completion_proc.call(command_info[0], command_info[1] || '')].flatten.compact
26
+ else
27
+ [name.to_s].grep(/^#{Regexp.quote(input)}/)
28
+ end
29
+ end
30
+
31
+ # MEMO: Termtter:Client からはこのメソッドを呼び出すことになると思う。
32
+ def exec_if_match(input)
33
+ command_info = match?(input)
34
+ if command_info
35
+ result = execute(command_info[1])
36
+ unless result.nil?
37
+ return result
38
+ else
39
+ return true
40
+ end
41
+ else
42
+ return nil
43
+ end
44
+ end
45
+
46
+ # return array like [command, arg]
47
+ def match?(input)
48
+ if pattern =~ input
49
+ [$2 || $3, $4] # $2 or $3 => command, $4 => argument
50
+ else
51
+ nil
52
+ end
53
+ end
54
+
55
+ def execute(arg)
56
+ arg = case arg
57
+ when nil
58
+ ''
59
+ when String
60
+ arg
61
+ else
62
+ raise ArgumentError, 'arg should be String or nil'
63
+ end
64
+ exec_proc.call(arg)
65
+ end
66
+
67
+ def pattern
68
+ commands_regex = commands.map {|i| Regexp.quote(i.to_s) }.join('|')
69
+ /^((#{commands_regex})|(#{commands_regex})\s+(.*?))\s*$/
70
+ end
71
+
72
+ def commands
73
+ aliases.unshift(name)
74
+ end
75
+ end
76
+ end
77
+
@@ -0,0 +1,39 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ module Termtter
4
+ class Connection
5
+ attr_reader :protocol, :port, :proxy_uri
6
+
7
+ def initialize
8
+ @proxy_host = configatron.proxy.host
9
+ @proxy_port = configatron.proxy.port
10
+ @proxy_user = configatron.proxy.user_name
11
+ @proxy_password = configatron.proxy.password
12
+ @proxy_uri = nil
13
+ @enable_ssl = configatron.enable_ssl
14
+ @protocol = "http"
15
+ @port = 80
16
+
17
+ unless @proxy_host.empty?
18
+ @http_class = Net::HTTP::Proxy(@proxy_host, @proxy_port,
19
+ @proxy_user, @proxy_password)
20
+ @proxy_uri = "http://" + @proxy_host + ":" + @proxy_port + "/"
21
+ else
22
+ @http_class = Net::HTTP
23
+ end
24
+
25
+ if @enable_ssl
26
+ @protocol = "https"
27
+ @port = 443
28
+ end
29
+ end
30
+
31
+ def start(host, port, &block)
32
+ http = @http_class.new(host, port)
33
+ http.use_ssl = @enable_ssl
34
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE if http.use_ssl?
35
+ http.start(&block)
36
+ end
37
+ end
38
+ end
39
+
@@ -0,0 +1,18 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ module Termtter
4
+ class Hook
5
+ attr_accessor :name, :points, :exec_proc
6
+
7
+ def initialize(args)
8
+ raise ArgumentError, ":name is not given." unless args.has_key?(:name)
9
+ @name = args[:name].to_sym
10
+ @points = args[:points] ? args[:points].map {|i| i.to_sym } : []
11
+ @exec_proc = args[:exec_proc] || lambda {}
12
+ end
13
+
14
+ def match?(point)
15
+ points.include?(point.to_sym)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,26 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ module Termtter
4
+ class Status
5
+ %w(
6
+ id text created_at truncated
7
+ in_reply_to_status_id in_reply_to_user_id in_reply_to_screen_name
8
+ user_id user_name user_screen_name user_url user_profile_image_url
9
+ ).each do |attr|
10
+ attr_accessor attr.to_sym
11
+ end
12
+
13
+ def eql?(other); self.id == other.id end
14
+ def hash; self.id end
15
+
16
+ def english?
17
+ self.class.english?(self.text)
18
+ end
19
+
20
+ # english? :: String -> Boolean
21
+ def self.english?(message)
22
+ /[一-龠]+|[ぁ-ん]+|[ァ-ヴー]+|[a-zA-Z0-9]+/ !~ message
23
+ end
24
+ end
25
+ end
26
+
@@ -0,0 +1,16 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ module Termtter
4
+ class Task
5
+ attr_accessor :name, :exec_at, :exec_proc, :interval
6
+ def initialize(args = {}, &block)
7
+ @name = args[:name]
8
+ @exec_at = Time.now + (args[:after] || 0)
9
+ @interval = args[:interval]
10
+ @exec_proc = block || lambda {}
11
+ end
12
+ def execute
13
+ exec_proc.call
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,116 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ module Termtter
4
+ class TaskManager
5
+
6
+ INTERVAL = 1
7
+
8
+ def initialize()
9
+ @tasks = {}
10
+ @work = true
11
+ @mutex = Mutex.new
12
+ @pause = false
13
+ end
14
+
15
+ def pause
16
+ @pause = true
17
+ end
18
+
19
+ def resume
20
+ @pause = false
21
+ end
22
+
23
+ def kill
24
+ @work = false
25
+ end
26
+
27
+ def run
28
+ Thread.new do
29
+ while @work
30
+ step unless @pause
31
+ sleep INTERVAL
32
+ end
33
+ end
34
+ end
35
+
36
+ def step
37
+ pull_due_tasks().each do |task|
38
+ invoke_and_wait do
39
+ task.execute
40
+ end
41
+ end
42
+ end
43
+
44
+ def invoke_later
45
+ Thread.new do
46
+ invoke_and_wait do
47
+ yield
48
+ end
49
+ end
50
+ end
51
+
52
+ def invoke_and_wait(&block)
53
+ synchronize do
54
+ begin
55
+ yield
56
+ rescue => e
57
+ Termtter::Client.handle_error(e)
58
+ end
59
+ end
60
+ end
61
+
62
+ def add_task(args = {}, &block)
63
+ synchronize do
64
+ task = Task.new(args, &block)
65
+ @tasks[task.name || task.object_id] = task
66
+ end
67
+ end
68
+
69
+ def get_task(key)
70
+ synchronize do
71
+ @tasks[key]
72
+ end
73
+ end
74
+
75
+ def delete_task(key)
76
+ synchronize do
77
+ @tasks.delete(key)
78
+ end
79
+ end
80
+
81
+ private
82
+
83
+ def synchronize
84
+ unless Thread.current == @thread_in_sync
85
+ @mutex.synchronize do
86
+ @thread_in_sync = Thread.current
87
+ yield
88
+ end
89
+ else
90
+ yield
91
+ end
92
+ end
93
+
94
+ def pull_due_tasks()
95
+ synchronize do
96
+ time_now = Time.now
97
+ due_tasks = []
98
+ @tasks.delete_if do |key, task|
99
+ if task.exec_at <= time_now
100
+ due_tasks << task
101
+ if task.interval
102
+ task.exec_at = time_now + task.interval
103
+ false
104
+ else
105
+ true
106
+ end
107
+ else
108
+ false
109
+ end
110
+ end
111
+ return due_tasks
112
+ end
113
+ end
114
+
115
+ end
116
+ end