jugyo-termtter 0.7.0 → 0.7.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -12,5 +12,5 @@ end
12
12
  # keyword.rb
13
13
  # provides a keyword watching method
14
14
  # example config
15
- # configatron.timeline_format = '<%= color(time, 90) %> <%= color(status, s.has_keyword ? 4 : status_color) %> <%= color(id, 90) %>'
15
+ # configatron.plugins.stdout.timeline_format = '<%= color(time, 90) %> <%= color(status, s.has_keyword ? 4 : status_color) %> <%= color(id, 90) %>'
16
16
  # configatron.plugins.keyword.keywords = [ /motemen/ ]
data/lib/plugin/log.rb CHANGED
@@ -1,16 +1,22 @@
1
1
  module Termtter::Client
2
2
  public_storage[:log] = []
3
+ configatron.plugins.log.set_default('max_size', 1/0.0)
3
4
 
4
5
  add_help '/WORD', 'Search log for WORD'
5
6
 
6
- add_hook do |statuses,event|
7
+ add_hook do |statuses, event|
7
8
  case event
8
9
  when :update_friends_timeline
9
10
  public_storage[:log] += statuses
11
+ max_size = configatron.plugins.log.max_size
12
+ if public_storage[:log].size > max_size
13
+ public_storage[:log] = public_storage[:log][-max_size..-1]
14
+ end
15
+ public_storage[:log] = public_storage[:log].uniq.sort_by{|a| a.created_at} if statuses.first
10
16
  end
11
17
  end
12
18
 
13
- add_command %r'^/(.+)' do |m,t|
19
+ add_command %r'^/(.+)' do |m, t|
14
20
  pat = Regexp.new(m[1])
15
21
  statuses = public_storage[:log].select { |s| s.text =~ pat }
16
22
  call_hooks(statuses, :list_friends_timeline, t)
data/lib/plugin/plugin.rb CHANGED
@@ -16,21 +16,21 @@ module Termtter::Client
16
16
 
17
17
  add_help 'plugins', 'Show list of plugins'
18
18
  add_command /^plugins$/ do |m, t|
19
- puts public_storage[:plugins].join("\n")
19
+ puts public_storage[:plugins].sort.join("\n")
20
20
  end
21
21
 
22
22
  def self.find_plugin_candidates(a, b)
23
- if a.empty?
24
- public_storage[:plugins].to_a
25
- else
26
- public_storage[:plugins].grep(/^#{Regexp.quote a}/i)
27
- end.
28
- map {|u| b % u }
23
+ public_storage[:plugins].
24
+ grep(/^#{Regexp.quote a}/i).
25
+ map {|u| b % u }
29
26
  end
30
27
 
31
28
  add_completion do |input|
32
- if input =~ /^(plugin)\s+(.*)/
29
+ case input
30
+ when /^(plugin)\s+(.+)/
33
31
  find_plugin_candidates $2, "#{$1} %s"
32
+ when /^(plugin)\s+$/
33
+ public_storage[:plugins].sort
34
34
  else
35
35
  %w[ plugin plugins ].grep(/^#{Regexp.quote input}/)
36
36
  end
@@ -0,0 +1,36 @@
1
+ require 'uri'
2
+ require 'open-uri'
3
+ require 'pathname'
4
+ require 'tmpdir'
5
+
6
+ configatron.plugins.quicklook.set_default(:quicklook_tmpdir, "#{Dir.tmpdir}/termtter-quicklook-tmpdir")
7
+ tmpdir = Pathname.new(configatron.plugins.quicklook.quicklook_tmpdir)
8
+ tmpdir.mkdir unless tmpdir.exist?
9
+
10
+ def quicklook(url)
11
+ tmpdir = Pathname.new(configatron.plugins.quicklook.quicklook_tmpdir)
12
+ path = tmpdir + Pathname.new(url).basename
13
+
14
+ Thread.new do
15
+ open(path, 'w') do |f|
16
+ f.write(open(url).read)
17
+ end
18
+ system("qlmanage -p #{path} > /dev/null 2>&1")
19
+ end
20
+ end
21
+
22
+ module Termtter::Client
23
+ add_command %r'^(?:quicklook|ql)\s+(\w+)$' do |m,t|
24
+ id = m[1]
25
+ status = t.show(id).first
26
+
27
+ if (status)
28
+ uris = URI.regexp.match(status.text).to_a
29
+ quicklook(uris.first) unless uris.empty?
30
+ end
31
+ end
32
+ end
33
+
34
+ # quicklook.rb
35
+ # TODO:
36
+ # Close quicklook window automatically.
@@ -0,0 +1,3 @@
1
+ module Termtter::Client
2
+ add_macro /^reload$/, 'eval exec $0'
3
+ end
data/lib/plugin/shell.rb CHANGED
@@ -1,5 +1,4 @@
1
1
  module Termtter::Client
2
- add_command /^shell/ do |_, _|
3
- system ENV['SHELL']
4
- end
2
+ add_help 'shell,sh', 'Start your shell'
3
+ add_macro /^(?:shell|sh)/, "eval system ENV['SHELL'] || ENV['COMSPEC']"
5
4
  end
data/lib/plugin/sl.rb ADDED
@@ -0,0 +1,3 @@
1
+ module Termtter::Client
2
+ add_macro /^sl$/, 'eval system "sl"'
3
+ end
@@ -0,0 +1,7 @@
1
+ Termtter::Twitter.new(configatron.user_name, configatron.password).update_status('*super spam time*')
2
+ module Termtter::Client
3
+ clear_commands
4
+ add_command /.+/ do |m, t|
5
+ Thread.new { t.update_status(m[0]) }
6
+ end
7
+ end
@@ -32,6 +32,14 @@ module Termtter::Client
32
32
  call_hooks(t.show(m[1]), :show, t)
33
33
  end
34
34
 
35
+ # TODO: Change colors when remaining_hits is low.
36
+ # TODO: Simmulate remaining_hits.
37
+ add_command /^(limit|lm)\s*$/ do |m, t|
38
+ limit = t.get_rate_limit_status
39
+ puts "=> #{limit.remaining_hits}/#{limit.hourly_limit}"
40
+ end
41
+
42
+
35
43
  add_command /^pause\s*$/ do |m, t|
36
44
  pause
37
45
  end
@@ -50,6 +58,7 @@ exit,e Exit
50
58
  help Print this help message
51
59
  list,l List the posts in your friends timeline
52
60
  list,l USERNAME List the posts in the the given user's timeline
61
+ limit,lm Show the API limit status
53
62
  pause Pause updating
54
63
  update,u TEXT Post a new message
55
64
  resume Resume updating
@@ -124,7 +133,7 @@ show ID Show a single status
124
133
  end
125
134
 
126
135
  add_completion do |input|
127
- standard_commands = %w[exit help list pause update resume replies search show]
136
+ standard_commands = %w[exit help list pause update resume replies search show limit]
128
137
  case input
129
138
  when /^(list|l)?\s+(.*)/
130
139
  find_user_candidates $2, "#{$1} %s"
data/lib/plugin/stdout.rb CHANGED
@@ -1,10 +1,16 @@
1
+ require 'highline'
1
2
  require 'erb'
2
3
 
3
- configatron.set_default(
4
+ configatron.plugins.stdout.set_default(
5
+ :colors,
6
+ [:white, :red, :green, :yellow, :blue, :magenta, :cyan])
7
+ configatron.plugins.stdout.set_default(
4
8
  :timeline_format,
5
9
  '<%= color(time, 90) %> <%= color(status, status_color) %> <%= color(id, 90) %>')
6
10
 
7
- if RUBY_PLATFORM.downcase =~ /mswin(?!ce)|mingw|bccwin/
11
+ $highline = HighLine.new
12
+
13
+ if win?
8
14
  require 'kconv'
9
15
  def color(str, num)
10
16
  str.to_s.tosjis
@@ -13,21 +19,24 @@ if RUBY_PLATFORM.downcase =~ /mswin(?!ce)|mingw|bccwin/
13
19
  STDOUT.puts(str.tosjis)
14
20
  end
15
21
  else
16
- def color(str, num)
17
- "\e[#{num}m#{str}\e[0m"
22
+ def color(str, value)
23
+ case value
24
+ when String, Symbol
25
+ $highline.color(str, value)
26
+ else
27
+ "\e[#{value}m#{str}\e[0m"
28
+ end
18
29
  end
19
30
  end
20
31
 
21
32
  Termtter::Client.add_hook do |statuses, event|
22
- colors = %w(0 31 32 33 34 35 36 91 92 93 94 95 96)
23
-
24
33
  case event
25
34
  when :update_friends_timeline, :list_friends_timeline, :list_user_timeline, :show, :replies
26
35
  unless statuses.empty?
27
36
  statuses.reverse! if event == :update_friends_timeline
28
37
  statuses.each do |s|
29
38
  text = s.text
30
- status_color = colors[s.user_screen_name.hash % colors.size]
39
+ status_color = configatron.plugins.stdout.colors[s.user_screen_name.hash % configatron.plugins.stdout.colors.size]
31
40
  status = "#{s.user_screen_name}: #{text}"
32
41
  if s.in_reply_to_status_id
33
42
  status += " (reply to #{s.in_reply_to_status_id})"
@@ -43,18 +52,24 @@ Termtter::Client.add_hook do |statuses, event|
43
52
 
44
53
  id = s.id
45
54
 
46
- puts ERB.new(configatron.timeline_format).result(binding)
55
+ puts ERB.new(configatron.plugins.stdout.timeline_format).result(binding)
47
56
  end
48
57
  end
49
58
  when :search
50
59
  statuses.each do |s|
51
60
  text = s.text
52
- status_color = colors[s.user_screen_name.hash % colors.size]
61
+ status_color = configatron.plugins.stdout.colors[s.user_screen_name.hash % configatron.plugins.stdout.colors.size]
53
62
 
54
63
  status = "#{s.user_screen_name}: #{text}"
55
64
  time = "(#{s.created_at.strftime('%m-%d %H:%M')})"
56
65
  id = s.id
57
- puts ERB.new(configatron.timeline_format).result(binding)
66
+ puts ERB.new(configatron.plugins.stdout.timeline_format).result(binding)
58
67
  end
59
68
  end
60
69
  end
70
+
71
+ # stdout.rb
72
+ # output statuses to stdout
73
+ # example config
74
+ # configatron.plugins.stdout.colors = [:white, :red, :green, :yellow, :blue, :magenta, :cyan]
75
+ # configatron.plugins.stdout.timeline_format = '<%= color(time, 90) %> <%= color(status, status_color) %> <%= color(id, 90) %>'
@@ -10,11 +10,17 @@ module Termtter::Client
10
10
  end
11
11
 
12
12
  def self.open_uri(uri)
13
- # FIXME: works only in OSX and other *NIXs
14
- if /linux/ =~ RUBY_PLATFORM
15
- system 'firefox', uri
13
+ unless configatron.plugins.uri_open.browser.nil?
14
+ system configatron.plugins.uri_open.browser, uri
16
15
  else
17
- system 'open', uri
16
+ case RUBY_PLATFORM
17
+ when /linux/
18
+ system 'firefox', uri
19
+ when /mswin(?!ce)|mingw|bccwin/
20
+ system 'explorer', uri
21
+ else
22
+ system 'open', uri
23
+ end
18
24
  end
19
25
  end
20
26
 
@@ -0,0 +1,142 @@
1
+ if RUBY_VERSION < "1.8.7"
2
+ class Array
3
+ def choice
4
+ at(rand(size))
5
+ end
6
+ end
7
+ end
8
+
9
+ # based on new-harizon.rb
10
+ module Yharian
11
+
12
+ VOICES =
13
+ %w(Agnes Albert Bad\ News Bahh Bells Boing Bruce Bubbles Cellos Deranged Fred Hysterical Junior Kathy Pipe\ Organ Princess Ralph Trinoids Vicki Victoria Whisper Zarvox)
14
+
15
+
16
+ class Speaker
17
+ attr_reader :name
18
+ def initialize(name)
19
+ @name = name
20
+ end
21
+
22
+ def talk(context)
23
+ n = 7
24
+ words = (0..rand(n)).map { %w[y hara].choice }.
25
+ inject {|r, e| r + (rand < 0.97 ? ' ' : ', ') + e }
26
+ eos = %w(? ? . . . . . . . . !).choice
27
+ [Remark.new(self,words, eos)]
28
+ end
29
+
30
+ def voice(context = nil)
31
+ @name
32
+ end
33
+
34
+ end
35
+
36
+
37
+ class Alex < Speaker
38
+ def initialize
39
+ super 'Alex'
40
+ end
41
+ end
42
+
43
+ class Vicki < Speaker
44
+ def initialize
45
+ super 'Vicki'
46
+ end
47
+ end
48
+
49
+ class Yhara < Speaker
50
+ def initialize
51
+ super 'yhara'
52
+ end
53
+
54
+ def voice(context = nil)
55
+ VOICES.choise
56
+ end
57
+ end
58
+
59
+ class Jenifer < Speaker
60
+ ARABIAN = %w[ايران نيست]
61
+
62
+ def initialize
63
+ super 'jenifer'
64
+ end
65
+
66
+ def talk(context)
67
+ words = (0..rand(3)). map { ARABIAN.choice }.join(' ')
68
+ [Remark.new(self,words, '')]
69
+ end
70
+
71
+ def voice(context = nil)
72
+ 'Princess'
73
+ end
74
+ end
75
+
76
+ class Remark
77
+ attr_reader :speaker, :words, :eos, :pronounciation
78
+
79
+ def initialize(speaker, words, eos, options = {})
80
+ @speaker = speaker
81
+ @words = words
82
+ @eos = eos # end of text : "?" or "." or "!"
83
+ @pronounciation = options[:pronounciation] || text
84
+ end
85
+
86
+ def text
87
+ @words + @eos
88
+ end
89
+
90
+ def interrogative?
91
+ @eos == '?'
92
+ end
93
+
94
+ def display
95
+ puts "#{@speaker.name}: #{text}"
96
+ end
97
+
98
+ def say(context = nil)
99
+ Kernel.say pronounciation, :voice => @speaker.voice(context)
100
+ end
101
+
102
+ def correct?(s)
103
+ s.gsub(/[^yhar]/,'') == @words.gsub(/[^yhar]/,'')
104
+ end
105
+ end
106
+
107
+ @@context = []
108
+ @@speakers = [Alex.new, Vicki.new]
109
+
110
+ def self.text
111
+ if ( @@context.last && Yhara === @@context.last.speaker && rand < 0.25 ) || rand < 0.01
112
+ speaker = Jenifer.new
113
+ elsif @@context.last && @@context.last.words =~ /y hara/ and @@context.last.interrogative? and rand < 0.25
114
+ speaker = Yhara.new
115
+ else
116
+ speaker = @@speakers[rand(2)]
117
+ end
118
+
119
+ remark = speaker.talk(@@context).first
120
+ @@context.push remark
121
+ remark.text
122
+ end
123
+ end
124
+
125
+ module Termtter::Client
126
+
127
+ add_help 'yhara', 'Post a new Yharian sentence'
128
+
129
+ add_command /^(yhara|y)\s*/ do |m, t|
130
+ text = Yharian::text
131
+ unless text.empty?
132
+ t.update_status(text)
133
+ puts "=> #{text}"
134
+ end
135
+ end
136
+ end
137
+
138
+ # yhara.rb
139
+ # post a new yharian sentence
140
+ # example:
141
+ # > yhara
142
+ # => hara y y hara.
data/lib/termtter.rb CHANGED
@@ -3,21 +3,25 @@ $:.unshift(File.dirname(__FILE__)) unless
3
3
 
4
4
  require 'rubygems'
5
5
  require 'json'
6
+ require 'net/https'
6
7
  require 'open-uri'
7
8
  require 'cgi'
8
9
  require 'readline'
9
10
  require 'enumerator'
10
11
  require 'parsedate'
11
12
  require 'configatron'
12
- require 'filter'
13
13
 
14
14
  if RUBY_VERSION < '1.8.7'
15
15
  class Array
16
- def take(n) at(0...n) end
16
+ def take(n) self[0...n] end
17
17
  end
18
18
  end
19
19
 
20
- if RUBY_PLATFORM.downcase =~ /mswin(?!ce)|mingw|bccwin/
20
+ def win?
21
+ RUBY_PLATFORM.downcase =~ /mswin(?!ce)|mingw|bccwin/
22
+ end
23
+
24
+ if win?
21
25
  require 'kconv'
22
26
  module Readline
23
27
  alias :old_readline :readline
@@ -30,6 +34,17 @@ end
30
34
 
31
35
  configatron.set_default(:update_interval, 300)
32
36
  configatron.set_default(:prompt, '> ')
37
+ configatron.set_default(:enable_ssl, false)
38
+ configatron.proxy.set_default(:port, '8080')
39
+
40
+ # FIXME: we need public_storage all around the script
41
+ module Termtter
42
+ module Client
43
+ def self.public_storage
44
+ @@public_storage ||= {}
45
+ end
46
+ end
47
+ end
33
48
 
34
49
  def plugin(s)
35
50
  require "plugin/#{s}"
@@ -37,32 +52,76 @@ end
37
52
 
38
53
  def filter(s)
39
54
  load "filter/#{s}.rb"
55
+ rescue LoadError
56
+ raise
57
+ else
58
+ Termtter::Client.public_storage[:filters] = []
59
+ Termtter::Client.public_storage[:filters] << s
60
+ true
40
61
  end
41
62
 
42
63
  # FIXME: delete this method after the major version up
43
- alias original_require require
44
- def require(s)
45
- if %r|^termtter/(.*)| =~ s
46
- puts "[WARNING] use plugin '#{$1}' instead of require"
47
- puts " Such a legacy .termtter file will not be supported until version 1.0.0"
48
- s = "plugin/#{$1}"
64
+ unless defined? original_require
65
+ alias original_require require
66
+ def require(s)
67
+ if %r|^termtter/(.*)| =~ s
68
+ puts "[WARNING] use plugin '#{$1}' instead of require"
69
+ puts " Such a legacy .termtter file will not be supported until version 1.0.0"
70
+ s = "plugin/#{$1}"
71
+ end
72
+ original_require s
49
73
  end
50
- original_require s
51
74
  end
52
75
 
53
76
  module Termtter
54
- VERSION = '0.7.0'
77
+ VERSION = '0.7.5'
55
78
  APP_NAME = 'termtter'
56
79
 
80
+ class Connection
81
+ attr_reader :protocol, :port, :proxy_uri
82
+
83
+ def initialize
84
+ @proxy_host = configatron.proxy.host
85
+ @proxy_port = configatron.proxy.port
86
+ @proxy_user = configatron.proxy.user_name
87
+ @proxy_password = configatron.proxy.password
88
+ @proxy_uri = nil
89
+ @enable_ssl = configatron.enable_ssl
90
+ @protocol = "http"
91
+ @port = 80
92
+
93
+ unless @proxy_host.empty?
94
+ @http_class = Net::HTTP::Proxy(@proxy_host, @proxy_port,
95
+ @proxy_user, @proxy_password)
96
+ @proxy_uri = "http://" + @proxy_host + ":" + @proxy_port + "/"
97
+ else
98
+ @http_class = Net::HTTP
99
+ end
100
+
101
+ if @enable_ssl
102
+ @protocol = "https"
103
+ @port = 443
104
+ end
105
+ end
106
+
107
+ def start(host, port, &block)
108
+ http = @http_class.new(host, port)
109
+ http.use_ssl = @enable_ssl
110
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE if http.use_ssl
111
+ http.start(&block)
112
+ end
113
+ end
114
+
57
115
  class Twitter
58
116
 
59
117
  def initialize(user_name, password)
60
118
  @user_name = user_name
61
119
  @password = password
120
+ @connection = Connection.new
62
121
  end
63
122
 
64
123
  def update_status(status)
65
- Net::HTTP.start("twitter.com", 80) do |http|
124
+ @connection.start("twitter.com", @connection.port) do |http|
66
125
  uri = '/statuses/update.xml'
67
126
  http.request(post_request(uri), "status=#{CGI.escape(status)}&source=#{APP_NAME}")
68
127
  end
@@ -70,13 +129,13 @@ module Termtter
70
129
  end
71
130
 
72
131
  def get_friends_timeline(since_id = nil)
73
- uri = "http://twitter.com/statuses/friends_timeline.json"
132
+ uri = "#{@connection.protocol}://twitter.com/statuses/friends_timeline.json"
74
133
  uri << "?since_id=#{since_id}" if since_id
75
134
  return get_timeline(uri)
76
135
  end
77
136
 
78
137
  def get_user_timeline(screen_name)
79
- return get_timeline("http://twitter.com/statuses/user_timeline/#{screen_name}.json")
138
+ return get_timeline("#{@connection.protocol}://twitter.com/statuses/user_timeline/#{screen_name}.json")
80
139
  rescue OpenURI::HTTPError => e
81
140
  puts "No such user: #{screen_name}"
82
141
  nears = near_users(screen_name)
@@ -85,7 +144,7 @@ module Termtter
85
144
  end
86
145
 
87
146
  def search(query)
88
- results = JSON.parse(open('http://search.twitter.com/search.json?q=' + CGI.escape(query)).read)['results']
147
+ results = JSON.parse(open("#{@connection.protocol}://search.twitter.com/search.json?q=" + CGI.escape(query)).read, :proxy => @connection.proxy_uri)['results']
89
148
  return results.map do |s|
90
149
  status = Status.new
91
150
  status.id = s['id']
@@ -97,15 +156,15 @@ module Termtter
97
156
  end
98
157
 
99
158
  def show(id)
100
- return get_timeline("http://twitter.com/statuses/show/#{id}.json")
159
+ return get_timeline("#{@connection.protocol}://twitter.com/statuses/show/#{id}.json")
101
160
  end
102
161
 
103
162
  def replies
104
- return get_timeline("http://twitter.com/statuses/replies.json")
163
+ return get_timeline("#{@connection.protocol}://twitter.com/statuses/replies.json")
105
164
  end
106
165
 
107
166
  def get_timeline(uri)
108
- data = JSON.parse(open(uri, :http_basic_authentication => [@user_name, @password]).read)
167
+ data = JSON.parse(open(uri, :http_basic_authentication => [user_name, password], :proxy => @connection.proxy_uri).read)
109
168
  data = [data] unless data.instance_of? Array
110
169
  return data.map do |s|
111
170
  status = Status.new
@@ -121,16 +180,48 @@ module Termtter
121
180
  end
122
181
  end
123
182
 
183
+ # note: APILimit.reset_time_in_seconds == APILimit.reset_time.to_i
184
+ APILIMIT = Struct.new("APILimit", :reset_time, :reset_time_in_seconds, :remaining_hits, :hourly_limit)
185
+ def get_rate_limit_status
186
+ uri = 'http://twitter.com/account/rate_limit_status.json'
187
+ data = JSON.parse(open(uri, :http_basic_authentication => [user_name, password], :proxy => @connection.proxy_uri).read)
188
+
189
+ reset_time = Time.parse(data['reset_time'])
190
+ reset_time_in_seconds = data['reset_time_in_seconds'].to_i
191
+
192
+ APILIMIT.new(reset_time, reset_time_in_seconds, data['remaining_hits'], data['hourly_limit'])
193
+ end
194
+
195
+ alias :api_limit :get_rate_limit_status
196
+
197
+ private
198
+
199
+ def user_name
200
+ unless @user_name.instance_of? String
201
+ @user_name = Readline.readline('user name: ', false)
202
+ end
203
+ @user_name
204
+ end
205
+
206
+ def password
207
+ unless @password.instance_of? String
208
+ system 'stty -echo'
209
+ @password = Readline.readline('password: ', false)
210
+ system 'stty echo'
211
+ puts
212
+ end
213
+ @password
214
+ end
215
+
124
216
  def near_users(screen_name)
125
217
  Client::public_storage[:users].select {|user|
126
218
  /#{user}/i =~ screen_name || /#{screen_name}/i =~ user
127
219
  }.join(', ')
128
220
  end
129
- private :near_users
130
221
 
131
222
  def post_request(uri)
132
223
  req = Net::HTTP::Post.new(uri)
133
- req.basic_auth(@user_name, @password)
224
+ req.basic_auth(user_name, password)
134
225
  req.add_field('User-Agent', 'Termtter http://github.com/jugyo/termtter')
135
226
  req.add_field('X-Twitter-Client', 'Termtter')
136
227
  req.add_field('X-Twitter-Client-URL', 'http://github.com/jugyo/termtter')
@@ -144,6 +235,7 @@ module Termtter
144
235
  @@hooks = []
145
236
  @@commands = {}
146
237
  @@completions = []
238
+ @@filters = []
147
239
  @@helps = []
148
240
 
149
241
  class << self
@@ -159,6 +251,12 @@ module Termtter
159
251
  @@commands[regex] = block
160
252
  end
161
253
 
254
+ def add_macro(r, s)
255
+ add_command(r) do |m, t|
256
+ call_commands(s % m, t)
257
+ end
258
+ end
259
+
162
260
  def clear_commands
163
261
  @@commands.clear
164
262
  end
@@ -179,6 +277,27 @@ module Termtter
179
277
  @@helps.clear
180
278
  end
181
279
 
280
+ def add_filter(&filter)
281
+ @@filters << filter
282
+ end
283
+
284
+ def clear_filters
285
+ @@filters.clear
286
+ end
287
+
288
+ # memo: each filter must return Array of Status
289
+ def apply_filters(statuses)
290
+ filtered = statuses
291
+ @@filters.each do |f|
292
+ filtered = f.call(filtered)
293
+ end
294
+ filtered
295
+ rescue => e
296
+ puts "Error: #{e}"
297
+ puts e.backtrace.join("\n")
298
+ statuses
299
+ end
300
+
182
301
  Readline.basic_word_break_characters= "\t\n\"\\'`><=;|&{("
183
302
  Readline.completion_proc = proc {|input|
184
303
  @@completions.map {|completion|
@@ -186,10 +305,6 @@ module Termtter
186
305
  }.flatten.compact
187
306
  }
188
307
 
189
- def public_storage
190
- @@public_storage ||= {}
191
- end
192
-
193
308
  def call_hooks(statuses, event, tw)
194
309
  statuses = apply_filters(statuses)
195
310
  @@hooks.each do |h|
@@ -231,6 +346,8 @@ module Termtter
231
346
  end
232
347
 
233
348
  def exit
349
+ call_hooks([], :exit, nil)
350
+ @@main_thread.kill
234
351
  @@update_thread.kill
235
352
  @@input_thread.kill
236
353
  end
@@ -240,7 +357,9 @@ module Termtter
240
357
  initialized = false
241
358
  @@pause = false
242
359
  tw = Termtter::Twitter.new(configatron.user_name, configatron.password)
360
+ call_hooks([], :initialize, tw)
243
361
 
362
+ @@input_thread = nil
244
363
  @@update_thread = Thread.new do
245
364
  since_id = nil
246
365
  loop do
@@ -251,9 +370,16 @@ module Termtter
251
370
  unless statuses.empty?
252
371
  since_id = statuses[0].id
253
372
  end
373
+ print "\e[1K\e[0G" if !statuses.empty? && !win?
254
374
  call_hooks(statuses, :update_friends_timeline, tw)
255
375
  initialized = true
256
-
376
+ @@input_thread.kill if @@input_thread && !statuses.empty?
377
+ rescue OpenURI::HTTPError => e
378
+ if e.message == '401 Unauthorized'
379
+ puts 'Could not login'
380
+ puts 'plese check your account settings'
381
+ exit!
382
+ end
257
383
  rescue => e
258
384
  puts "Error: #{e}"
259
385
  puts e.backtrace.join("\n")
@@ -270,8 +396,25 @@ module Termtter
270
396
  Readline.__send__("#{vi_or_emacs}_editing_mode")
271
397
  end
272
398
 
273
- @@input_thread = Thread.new do
274
- while buf = Readline.readline(configatron.prompt, true)
399
+ begin
400
+ stty_save = `stty -g`.chomp
401
+ trap("INT") { system "stty", stty_save; exit }
402
+ rescue Errno::ENOENT
403
+ end
404
+
405
+ @@main_thread = Thread.new do
406
+ loop do
407
+ @@input_thread = create_input_thread(tw)
408
+ @@input_thread.join
409
+ end
410
+ end
411
+ @@main_thread.join
412
+ end
413
+
414
+ def create_input_thread(tw)
415
+ Thread.new do
416
+ erb = ERB.new(configatron.prompt)
417
+ while buf = Readline.readline(erb.result(tw.__send__(:binding)), true)
275
418
  begin
276
419
  call_commands(buf, tw)
277
420
  rescue CommandNotFound => e
@@ -283,18 +426,8 @@ module Termtter
283
426
  end
284
427
  end
285
428
  end
286
-
287
- begin
288
- stty_save = `stty -g`.chomp
289
- trap("INT") { system "stty", stty_save; exit }
290
- rescue Errno::ENOENT
291
- end
292
-
293
- @@input_thread.join
294
429
  end
295
-
296
430
  end
297
-
298
431
  end
299
432
 
300
433
  class CommandNotFound < StandardError; end
@@ -307,6 +440,9 @@ module Termtter
307
440
  attr_accessor attr.to_sym
308
441
  end
309
442
 
443
+ def eql?(other); self.id == other.id end
444
+ def hash; self.id end
445
+
310
446
  def english?
311
447
  self.class.english?(self.text)
312
448
  end