public
Description: Tiny, experimental, and anecdotic twitter interface with shoes !
Homepage: http://cyprio.net/wiki/TwitterUI
Clone URL: git://github.com/oz/twitterui.git
twitterui / twitterui.rb
100644 431 lines (377 sloc) 12.306 kb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
#!/usr/bin/ruby
 
Shoes.setup do
  gem 'htmlentities'
  gem 'twitter'
end
 
require 'fileutils'
require 'htmlentities'
require 'twitter'
require 'yaml'
 
# ----------------------------------------------------------------- [ twitter ]
 
class TwitterApp
  attr :login
  attr :password
  attr :twitter
  attr :cache_dir
 
  def initialize
    @config_file = ENV['HOME'] + '/' + '.twitteruirc'
    config = load_config
    connect(config)
  end
 
  def load_config
    config = nil
    if File.exists? @config_file
      File.open(@config_file) do |fd|
        config = YAML::load(fd)
      end
    end
    config
  end
 
  def save_config(opts)
    opts[:cache_dir] = ENV['HOME'] + '/.twitterui/cache' unless opts[:cache_dir]
 
    File.open(@config_file, File::WRONLY|File::TRUNC|File::CREAT, 0600) do |fd|
      fd.puts YAML::dump(opts)
    end
    connect(opts)
  end
 
  def connect(config = nil)
    unless config.nil?
      @login = config[:user]
      @password = config[:password]
      @cache_dir = config[:cache_dir] || ENV['HOME'] + '/.twitterui/cache'
      @twitter = Twitter::Base.new @login, @password
      @coder = HTMLEntities.new
    end
  end
 
  # get friends timeline & decode XML entities out of twitter.
  def tweets
    @twitter_timeline = @twitter.timeline :friends
    return [] if @twitter_timeline.nil?
 
    @twitter_timeline.each do |s|
      s.text = @coder.decode(s.text)
    end
  end
 
  # update your twitter status
  def post(msg)
    @twitter.post msg
  end
  alias update post
end
 
# ------------------------------------------------------------------- [ shoes ]
 
class TwitterUI < Shoes
  url '/', :index
  url '/config', :config
  url '/first_config', :first_config
 
  # Keep the app's context & status under there.
  @@context = {
    :twitter => nil,
    :tweets_flow => nil,
    :sleeptime => 180, # Check timeline every 3 minutes
    :timeout => 30 # Don't wait for Twitter more than 30 seconds
  }
 
  # First time config page: ask for login & password
  def first_config
    background gray(0.1)
    stack :width => 1.0 do
      banner "Welcome...\n", :stroke => "#bfd34a", :size => 14
      para "to this tiny Shoes app!\n\n",
          "It seems you've never launched me, so we need to know each other better",
          " so I can get you to twitter.",
          :font => "Verdana", :size => 8, :stroke => white
    end
 
    stack :margin_top => 5 do
      para "Login: \n", :font => "Verdana", :size => 8, :stroke => white, :margin_left => 20
      @login = edit_line :margin_left => 20, :width => '200px'
    end
    stack :margin_top => 5 do
      para "Password: \n", :font => "Verdana", :size => 8, :stroke => white, :margin_left => 20
      @password = edit_line :margin_left => 20, :secret => true, :width => '200px'
    end
 
    button "No, thanks.", :margin_left => 5 do
      quit
    end
    button "Ok, connect !", :margin_left => 20 do
      if @login.text != "" && @password.text != ""
        @@context[:twitter].save_config(:user => @login.text, :password => @password.text)
        visit('/')
      elsif 1 == show_welcome
        alert "*cough* I really need a login and password please. ^^"
      end
    end
  end
 
  # "Main" config page.
  def config
    # Kill twitter + watcher thread
    %w{twitter_thread check_thread}.each do |t|
      @@context[t.to_sym] = kill_thread @@context[t.to_sym]
    end
    @@context[:tweets_flow] = nil
    @status_msg = nil
 
    background gray(0.1)
    login = @@context[:twitter].login ? @@context[:twitter].login : 'username?'
    stack :width => 1.0 do
      banner "Configure...\n", :stroke => "#bfd34a", :size => 14
      para "Configure TwitterUI: you can only change your twitter credentials, for now.",
          :font => "Verdana", :size => 8, :stroke => white
    end
    stack :margin_bottom => 15 do
      para "Login:", :font => "Verdana", :size => 8, :stroke => white, :margin_left => 20
      @login = edit_line login, :margin_left => 20, :width => '200px'
    end
    stack :margin_bottom => 25 do
      para "Password:", :font => "Verdana", :size => 8, :stroke => white, :margin_left => 20
      @password = edit_line '', :margin_left => 20, :secret => true, :width => '200px'
    end
 
    button "Cancel", :margin_left => 5 do
      visit('/')
    end
    button "Save", :margin_left => 20 do
      if @login.text != "" && @password.text != ""
        @@context[:twitter].save_config(:user => @login.text, :password => @password.text)
        visit('/')
      end
    end
  end
 
  # Go Shoes ! \o/
  def index
    background black
    display_control_box
    @@context[:twitter] = TwitterApp.new
    visit '/first_config' if @@context[:twitter].login.nil?
    load_tweets 'Loading...'
    wait_for_tweets
 
    # Check that we're not waiting for Twitter too long.
    Thread.new do
      @@context[:check_thread] = Thread.current
      while true do
        unless @@context[:twitter_check].nil?
          timeouted = (Time.now - @@context[:twitter_check]) > @@context[:timeout]
          if timeouted
            @@context[:twitter_thread] = kill_thread @@context[:twitter_thread]
            load_tweets 'Loading...'
            wait_for_tweets
          end
        end
        sleep 5
      end
    end
 
    # Keyboard shortcuts.
    keypress do |key|
      case key.to_s
        when "f5" then load_tweets
        when "\022" then load_tweets
        when "\e":
          @status_flow.hide
        when "\016":
          @status_flow.show
        when "\021" then quit
      end
    end
  end
 
  # Bye Shoes !
  def quit
    current = Thread.current
    main = Thread.main
    Thread.list.each { |t| t.kill unless t == current || t == main }
    exit
  end
 
  protected
 
  # format twitter status message into eval-able code
  def format_status(status, bg)
    user = status.user.screen_name
    "[ em(
link(\"#{user}\", :click => \"http://twitter.com/#{user}\",
:underline => false,
:stroke => \"#bfd34a\",
:fill => \"#{bg}\") ,
:size => 7 ),
em(\": \", :size => 7, :stroke => white),
" + format_text(status.text, bg) + ",
em(\" (" + rel_time(status.created_at) + ") \", :size => 7, :stroke => white)
]"
  end
 
  # format text and links out of a twitter status message
  def format_text(text, bg)
    text.gsub!(/\\/, "\\\\\\")
    text.gsub!(/"/, '\"')
    text.gsub!(/&lt;/, '<')
    text.gsub!(/&gt;/, '>')
    text.gsub!(/#/, "\\#")
    return '"'+text+'"' unless text.include?('http') || text.include?('@')
 
    # Clickable links.
    text = text.split.collect do |tok|
      if tok =~ /http:\/\//
        tok.gsub(/(.*)(http:\/\/.*)/, '"\1", link("\2", :click => "\2",' +
                                ':stroke => orange, :fill => "'+bg+'"), " "') + ' '
      else
        "\"#{tok} \""
      end
    end.join(', ')
 
    if text.include? '@'
      text.gsub!(/(.*)@(\w+)\b(.*)/, '\1@", link("\2", :click => "http://twitter.com/\2",'+
        ' :underline => false, :stroke => "#bfd34a", :fill => "'+bg+'"), "\3')
    end
    text
  end
 
  # shows update edit-box & refresh links...
  def display_control_box
    char_left = 140
    char_text = "%d character%s left."
 
    flow :width => -20, :margin => 10 do
      background gray(0.1), :curve => 10
 
      # Edit-box toggle & refresh links
      image "media/new_post.png", :margin => 2 do
        @status_flow.toggle
      end
      image "media/refresh.png", :margin => 2 do
        load_tweets 'Refreshing...'
      end
      image "media/config.png", :margin => 2 do
        visit '/config'
      end
 
      # Twitter logo
      image "media/twitter_logo.png", :left => "83%", :margin => 2,
            :click => "http://twitter.com"
 
      # Status' edit-box
      @status_flow = flow :width => 1.0, :margin => 8, :hidden => true do
 
        # Edit-box input
        @up_text = edit_box "What are you doing?", :width => 1.0, :height => 50 do
          char_left = 140 - @up_text.text.size
          text = char_text % [char_left, (char_left != 1) ? 's' : '']
          @char_count.replace text
        end
 
        stack :width => -20 do
          # Characters left para
          text = char_text % [char_left, (char_left != 1) ? 's' : '']
          @char_count = para text, :stroke => white, :font => "Verdana", :size => 8
        end
 
        # Save button
        image "media/save.png", :margin => 2 do
          update_status
        end
      end
    end
  end
 
  # Show twitter satuses
  def display_tweets(tweets)
    @@context[:tweets_flow].clear unless @@context[:tweets_flow].nil?
    @@context[:tweets_flow] = flow :margin => 0, :width => 1.0 do
      tweets.each do |status|
        stack :width => -20, :margin => 5 do
          bg_color = ( status.user.screen_name != @@context[:twitter].login ) ? "#191919" : "#39414A"
          background bg_color, :curve => 10
          flow :width => -5 do
            stack :width => 48, :margin => 5 do
              image cached_image(status.user)
            end
            stack :width => -48, :margin => 10 do
              eval "para " + format_status(status, bg_color) +
                    ", :font => \"Verdana\", :size => 8, :stroke => white"
            end
          end
        end
      end
    end
  end
 
  # Display a loading message, and reset the loading timer
  def load_tweets(msg = "Refreshing...")
    @status_msg = status_msg(msg)
    @@context[:seconds_to_reload] = 0
  end
 
  # Load and displays tweets in their own thread.
  def wait_for_tweets
    tweets = nil
    last_tweets = nil
 
    Thread.new do
      @@context[:twitter_thread] = Thread.current
      while true do
 
        # Ask Twitter.
        @@context[:twitter_check] = Time.now
        tweets = @@context[:twitter].tweets
        @@context[:twitter_check] = nil
 
        # Display timeline if needed, then wait until it's time to reload
        # again...
        @@context[:seconds_to_reload] = @@context[:sleeptime]
        if last_tweets.nil? ||
           tweets.zip(last_tweets).any? {|t,l|t.created_at!=l.created_at}
          display_tweets(tweets)
        end
        last_tweets = tweets
        @status_msg.replace ""
        sleep 1 until 0 >= (@@context[:seconds_to_reload] -= 1)
      end
    end
  end
 
  # Displays a text status para on top of the app
  def status_msg(msg)
    if @status_msg
      @status_msg.replace msg
      return @status_msg
    end
 
    if @@context[:tweets_flow].nil?
      @status_msg = para msg, :stroke => white, :font => "Verdana", :size => 8
    else
      @@context[:tweets_flow].before do
        @status_msg = para msg, :stroke => white, :font => "Verdana", :size => 8
      end
    end
    @status_msg
  end
 
  # Update status on Twitter
  def update_status
    @status_msg = status_msg('Sending...')
 
    Thread.new do
      @@context[:twitter].post @up_text.text
      @status_msg.replace ""
      load_tweets
    end
    @up_text.text = ""
  end
 
  # Relative date/time
  def rel_time(dt)
    dt = Time.now - Time.parse(dt)
    case dt
      when 1..60
        "#{dt.to_i} secs ago"
      when 60..120
        "#{(dt/60).to_i} min ago"
      when 120..3600
        "#{(dt/60).to_i} mins ago"
      when 3600..7200
        "#{(dt/60/60).to_i} hour ago"
      when 7200..86400
        "#{(dt/60/60).to_i} hours ago"
      when 86400..172800
        "#{(dt/60/60).to_i} day ago"
      else
        "#{(dt/60/60/60).to_i} days ago"
    end
  end
 
  # Thread terminator
  def kill_thread(thread)
    thread.terminate
    nil
  end
 
  # Cache profile images
  def cached_image(user)
    cache_dir = @@context[:twitter].cache_dir
    FileUtils.mkdir_p(cache_dir) unless File.exists? cache_dir
 
    # Cache file with friend's name, keeping the extension
    # to not confuse Shoes about the file type...
    cache = cache_dir + '/' + user.screen_name + '.' + user.profile_image_url.gsub(/.*\./, '')
    unless File.exists? cache
      begin
        open(user.profile_image_url) do |fd|
          open(cache, 'w') do |cache_fd|
            cache_fd.write fd.read
          end
        end
      rescue => exc
        puts "Error: can't fetch profile image: #{exc}"
        cache = "media/profile_default.png"
      end
    end
    cache
  end
end
 
Shoes.app :title => 'Twitter UI', :width => 300, :height => 350