twterm 1.1.3 → 1.2.0

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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/bin/twterm +4 -4
  3. data/lib/twterm/app.rb +19 -4
  4. data/lib/twterm/client.rb +8 -470
  5. data/lib/twterm/direct_message.rb +82 -0
  6. data/lib/twterm/direct_message_composer.rb +74 -0
  7. data/lib/twterm/direct_message_manager.rb +52 -0
  8. data/lib/twterm/event/base.rb +22 -0
  9. data/lib/twterm/event/direct_message/fetched.rb +10 -0
  10. data/lib/twterm/event/favorite.rb +18 -0
  11. data/lib/twterm/event/follow.rb +17 -0
  12. data/lib/twterm/event/notification.rb +33 -0
  13. data/lib/twterm/event/open_uri.rb +11 -0
  14. data/lib/twterm/event/screen/resize.rb +13 -0
  15. data/lib/twterm/event/status/base.rb +14 -0
  16. data/lib/twterm/event/status/delete.rb +13 -0
  17. data/lib/twterm/event/status/mention.rb +10 -0
  18. data/lib/twterm/event/status/timeline.rb +10 -0
  19. data/lib/twterm/event_dispatcher.rb +59 -0
  20. data/lib/twterm/filter_query_window.rb +11 -5
  21. data/lib/twterm/filterable_list.rb +6 -1
  22. data/lib/twterm/notifier.rb +39 -15
  23. data/lib/twterm/promise.rb +2 -2
  24. data/lib/twterm/publisher.rb +16 -0
  25. data/lib/twterm/rest_client.rb +401 -0
  26. data/lib/twterm/screen.rb +16 -13
  27. data/lib/twterm/status.rb +12 -1
  28. data/lib/twterm/streaming_client.rb +103 -0
  29. data/lib/twterm/subscriber.rb +33 -0
  30. data/lib/twterm/tab/base.rb +13 -6
  31. data/lib/twterm/tab/direct_message/conversation.rb +103 -0
  32. data/lib/twterm/tab/direct_message/conversation_list.rb +99 -0
  33. data/lib/twterm/tab/key_assignments_cheatsheet.rb +3 -2
  34. data/lib/twterm/tab/new/list.rb +5 -3
  35. data/lib/twterm/tab/new/search.rb +3 -2
  36. data/lib/twterm/tab/new/start.rb +17 -2
  37. data/lib/twterm/tab/new/user.rb +6 -3
  38. data/lib/twterm/tab/statuses/base.rb +18 -11
  39. data/lib/twterm/tab/statuses/conversation.rb +3 -2
  40. data/lib/twterm/tab/statuses/favorites.rb +3 -2
  41. data/lib/twterm/tab/statuses/home.rb +10 -4
  42. data/lib/twterm/tab/statuses/list_timeline.rb +3 -2
  43. data/lib/twterm/tab/statuses/mentions.rb +6 -6
  44. data/lib/twterm/tab/statuses/search.rb +4 -3
  45. data/lib/twterm/tab/statuses/user_timeline.rb +3 -2
  46. data/lib/twterm/tab/user_tab.rb +26 -16
  47. data/lib/twterm/tab/users/base.rb +3 -2
  48. data/lib/twterm/tab/users/followers.rb +3 -2
  49. data/lib/twterm/tab/users/friends.rb +3 -2
  50. data/lib/twterm/tab_manager.rb +20 -8
  51. data/lib/twterm/tweetbox.rb +5 -2
  52. data/lib/twterm/uri_opener.rb +25 -0
  53. data/lib/twterm/user.rb +11 -1
  54. data/lib/twterm/utils.rb +13 -0
  55. data/lib/twterm/version.rb +1 -1
  56. data/lib/twterm.rb +0 -3
  57. data/spec/twterm/event/screen/resize_spec.rb +11 -0
  58. data/spec/twterm/event_dispatcher_spec.rb +19 -0
  59. data/twterm.gemspec +1 -1
  60. metadata +29 -7
  61. data/lib/twterm/notification/base.rb +0 -24
  62. data/lib/twterm/notification/error.rb +0 -19
  63. data/lib/twterm/notification/message.rb +0 -19
@@ -0,0 +1,74 @@
1
+ require 'twterm/publisher'
2
+ require 'twterm/user'
3
+ require 'twterm/utils'
4
+
5
+ module Twterm
6
+ class DirectMessageComposer
7
+ include Singleton
8
+ include Readline
9
+ include Curses
10
+ include Publisher
11
+ include Utils
12
+
13
+ def compose(recipient)
14
+ check_type User, recipient
15
+
16
+ clear
17
+
18
+ resetter = proc do
19
+ reset_prog_mode
20
+ sleep 0.1
21
+ Screen.instance.refresh
22
+ end
23
+
24
+ thread = Thread.new do
25
+ close_screen
26
+
27
+ puts "\nCompose new message to @%s:" % recipient.screen_name
28
+
29
+ CompletionManager.instance.set_default_mode!
30
+
31
+ loop do
32
+ line = (readline('> ', true) || '').strip
33
+ break if line.empty?
34
+
35
+ if line.end_with?('\\')
36
+ @text << line.chop.rstrip + "\n"
37
+ else
38
+ @text << line
39
+ break
40
+ end
41
+ end
42
+
43
+ puts "\n"
44
+
45
+ resetter.call
46
+ send(recipient) unless text.empty?
47
+ end
48
+
49
+ App.instance.register_interruption_handler do
50
+ thread.kill
51
+ clear
52
+ puts "\nCanceled"
53
+ resetter.call
54
+ end
55
+
56
+ thread.join
57
+ end
58
+
59
+ private
60
+
61
+ def clear
62
+ @text = ''
63
+ end
64
+
65
+ def send(recipient)
66
+ Client.current.create_direct_message(recipient, text)
67
+ clear
68
+ end
69
+
70
+ def text
71
+ @text || ''
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,52 @@
1
+ require 'twterm/client'
2
+ require 'twterm/direct_message'
3
+ require 'twterm/event/direct_message/fetched'
4
+ require 'twterm/publisher'
5
+ require 'twterm/scheduler'
6
+ require 'twterm/user'
7
+ require 'twterm/utils'
8
+
9
+ module Twterm
10
+ class DirectMessageManager
11
+ include Publisher, Utils
12
+
13
+ def initialize(client)
14
+ check_type Client, client
15
+
16
+ @client = client
17
+ @conversations = {}
18
+
19
+ fetch
20
+
21
+ Scheduler.new(300) { fetch }
22
+ end
23
+
24
+ def add(collocutor, message)
25
+ check_type User, collocutor
26
+ check_type DirectMessage, message
27
+
28
+ @conversations[collocutor.id] ||= DirectMessage::Conversation.new(collocutor)
29
+ @conversations[collocutor.id] << message
30
+ end
31
+
32
+ def fetch
33
+ client.direct_messages_received.then do |messages|
34
+ messages.each { |m| add(m.sender, m) }
35
+ publish(Event::DirectMessage::Fetched.new)
36
+ end
37
+
38
+ client.direct_messages_sent.then do |messages|
39
+ messages.each { |m| add(m.recipient, m) }
40
+ publish(Event::DirectMessage::Fetched.new)
41
+ end
42
+ end
43
+
44
+ def conversations
45
+ @conversations.values
46
+ end
47
+
48
+ private
49
+
50
+ attr_reader :client
51
+ end
52
+ end
@@ -0,0 +1,22 @@
1
+ require 'twterm/utils'
2
+
3
+ module Twterm
4
+ module Event
5
+ class Base
6
+ include Utils
7
+
8
+ def initialize(*args)
9
+ fields.zip(args).map(&:flatten).each do |name, type, value|
10
+ check_type type, value
11
+
12
+ instance_variable_set('@%s' % name, value)
13
+ self.class.send(:attr_reader, name)
14
+ end
15
+ end
16
+
17
+ def fields
18
+ []
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,10 @@
1
+ require 'twterm/event/base'
2
+
3
+ module Twterm
4
+ module Event
5
+ module DirectMessage
6
+ class Fetched < Base
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,18 @@
1
+ require 'twterm/client'
2
+ require 'twterm/event/base'
3
+ require 'twterm/user'
4
+ require 'twterm/status'
5
+
6
+ module Twterm
7
+ module Event
8
+ class Favorite < Base
9
+ def fields
10
+ {
11
+ source: ::Twterm::User,
12
+ target: ::Twterm::Status,
13
+ authenticating_user: ::Twterm::Client
14
+ }
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,17 @@
1
+ require 'twterm/client'
2
+ require 'twterm/event/base'
3
+ require 'twterm/user'
4
+
5
+ module Twterm
6
+ module Event
7
+ class Follow < Base
8
+ def fields
9
+ {
10
+ source: ::Twterm::User,
11
+ target: ::Twterm::User,
12
+ authenticating_user: ::Twterm::Client
13
+ }
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,33 @@
1
+ require 'twterm/event/base'
2
+
3
+ module Twterm
4
+ module Event
5
+ class Notification < Base
6
+ attr_reader :time
7
+
8
+ def initialize(type, message)
9
+ super(type, CGI.unescapeHTML(message))
10
+
11
+ @time = Time.now
12
+ end
13
+
14
+ def fields
15
+ {
16
+ type: Symbol,
17
+ message: String
18
+ }
19
+ end
20
+
21
+ def color
22
+ case type
23
+ when :error
24
+ [:white, :red]
25
+ when :message
26
+ [:white, :blue]
27
+ else
28
+ [:white, :black]
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,11 @@
1
+ require 'twterm/event/base'
2
+
3
+ module Twterm
4
+ module Event
5
+ class OpenURI < Base
6
+ def fields
7
+ { uri: Addressable::URI }
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ require 'twterm/event/base'
2
+
3
+ module Twterm
4
+ module Event
5
+ module Screen
6
+ class Resize < Base
7
+ def fields
8
+ { lines: Integer, cols: Integer }
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,14 @@
1
+ require 'twterm/status'
2
+ require 'twterm/event/base'
3
+
4
+ module Twterm
5
+ module Event
6
+ module Status
7
+ class Base < ::Twterm::Event::Base
8
+ def fields
9
+ { status: ::Twterm::Status }
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ require 'twterm/event/base'
2
+
3
+ module Twterm
4
+ module Event
5
+ module Status
6
+ class Delete < Base
7
+ def fields
8
+ { status_id: Integer }
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,10 @@
1
+ require 'twterm/event/status/base'
2
+
3
+ module Twterm
4
+ module Event
5
+ module Status
6
+ class Mention < Base
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ require 'twterm/event/status/base'
2
+
3
+ module Twterm
4
+ module Event
5
+ module Status
6
+ class Timeline < Base
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,59 @@
1
+ require 'singleton'
2
+ require 'twterm/utils'
3
+
4
+ module Twterm
5
+ class EventDispatcher
6
+ include Singleton
7
+ include Utils
8
+
9
+ def initialize
10
+ @subscriptions = []
11
+ end
12
+
13
+ def dispatch(event)
14
+ @subscriptions
15
+ .select { |s| s.event == event.class }
16
+ .map(&:callback)
17
+ .each { |cb| cb.call(event) }
18
+
19
+ self
20
+ end
21
+
22
+ def register_subscription(subscriber_id, event, callback)
23
+ check_type Class, event
24
+ unless event <= Event::Base
25
+ raise TypeError, 'the second argument must be a subclass of Twterm::Event::Base'
26
+ end
27
+
28
+ @subscriptions << Subscription.new(subscriber_id, event, callback)
29
+
30
+ self
31
+ end
32
+
33
+ def unregister_subscription(subscriber_id, event)
34
+ cond = if event.nil? # remove all subscriptions from the subscriber
35
+ -> s { s.subscriber_id == subscriber_id }
36
+ else # remove only specified event
37
+ -> s { s.subscriber_id == subscriber_id && s.event == event }
38
+ end
39
+
40
+ @subscriptions.reject!(&cond)
41
+
42
+ self
43
+ end
44
+
45
+ class Subscription
46
+ attr_reader :subscriber_id, :event, :callback
47
+
48
+ def initialize(subscriber_id, event, block)
49
+ @subscriber_id, @event, @callback = subscriber_id, event, block
50
+ end
51
+
52
+ def ==(other)
53
+ self.class == other.class &&
54
+ subscriber_id == other.subscriber_id &&
55
+ event == other.event
56
+ end
57
+ end
58
+ end
59
+ end
@@ -1,10 +1,16 @@
1
+ require 'twterm/event/screen/resize'
2
+ require 'twterm/subscriber'
3
+
1
4
  module Twterm
2
5
  class FilterQueryWindow
3
6
  include Curses
4
7
  include Singleton
8
+ include Subscriber
5
9
 
6
10
  def initialize
7
11
  @window = stdscr.subwin(1, stdscr.maxx, stdscr.maxy - 1, 0)
12
+
13
+ subscribe(Event::Screen::Resize, :resize)
8
14
  end
9
15
 
10
16
  def input
@@ -73,13 +79,13 @@ module Twterm
73
79
  stdscr.addstr(' ' * window.maxx)
74
80
  end
75
81
 
76
- def resize
77
- @window.resize(1, stdscr.maxx)
78
- @window.move(stdscr.maxy - 1, 0)
79
- end
80
-
81
82
  private
82
83
 
83
84
  attr_reader :window
85
+
86
+ def resize(event)
87
+ @window.resize(1, stdscr.maxx)
88
+ @window.move(stdscr.maxy - 1, 0)
89
+ end
84
90
  end
85
91
  end
@@ -1,7 +1,12 @@
1
+ require 'twterm/event/notification'
2
+ require 'twterm/publisher'
3
+
1
4
  module Twterm
2
5
  module FilterableList
3
6
  extend Forwardable
4
7
 
8
+ include Publisher
9
+
5
10
  def filter
6
11
  @filter_query = FilterQueryWindow.instance.input
7
12
 
@@ -10,7 +15,7 @@ module Twterm
10
15
  elsif items.count == 0
11
16
  query = filter_query
12
17
  reset_filter
13
- Notifier.instance.show_error "No matches found: \"#{query}\""
18
+ publish(Event::Notification.new(:error, "No matches found: \"#{query}\""))
14
19
  else
15
20
  Notifier.instance.show_message "#{total_item_count} items found: \"#{filter_query}\""
16
21
  scroller.move_to_top
@@ -1,12 +1,33 @@
1
+ require 'twterm/subscriber'
2
+ require 'twterm/event/favorite'
3
+ require 'twterm/event/notification'
4
+ require 'twterm/event/screen/resize'
5
+
1
6
  module Twterm
2
7
  class Notifier
3
8
  include Singleton
4
9
  include Curses
10
+ include Subscriber
5
11
 
6
12
  def initialize
7
13
  @window = stdscr.subwin(1, stdscr.maxx, stdscr.maxy - 2, 0)
8
14
  @queue = Queue.new
9
15
 
16
+ subscribe(Event::Favorite) do |e|
17
+ break if e.source.id == e.authenticating_user.user_id
18
+
19
+ msg = '@%s has favorited your tweet: %s' % [
20
+ e.source.screen_name, e.target.text
21
+ ]
22
+ show_message(msg)
23
+ end
24
+
25
+ subscribe(Event::Notification) do |e|
26
+ queue(e)
27
+ end
28
+
29
+ subscribe(Event::Screen::Resize, :resize)
30
+
10
31
  Thread.new do
11
32
  while notification = @queue.pop
12
33
  show(notification)
@@ -16,19 +37,8 @@ module Twterm
16
37
  end
17
38
  end
18
39
 
19
- def resize
20
- @window.resize(1, stdscr.maxx)
21
- @window.move(stdscr.maxy - 2, 0)
22
- end
23
-
24
- def show_error(message)
25
- notification = Notification::Error.new(message)
26
- @queue.push(notification)
27
- self
28
- end
29
-
30
40
  def show_message(message)
31
- notification = Notification::Message.new(message)
41
+ notification = Event::Notification.new(:message, message)
32
42
  @queue.push(notification)
33
43
  self
34
44
  end
@@ -41,18 +51,32 @@ module Twterm
41
51
 
42
52
  @window.clear
43
53
 
44
- if notification.is_a? Notification::Base
45
- @window.with_color(notification.fg_color, notification.bg_color) do
54
+ if notification.is_a?(Event::Notification)
55
+ fg_color, bg_color = notification.color
56
+
57
+ @window.with_color(fg_color, bg_color) do
46
58
  @window.setpos(0, 0)
47
59
  @window.addstr(' ' * @window.maxx)
48
60
  @window.setpos(0, 1)
49
61
  time = notification.time.strftime('[%H:%M:%S]')
50
- message = notification.show_with_width(@window.maxx)
62
+ message = notification.message.gsub("\n", ' ')
51
63
  @window.addstr("#{time} #{message}")
52
64
  end
53
65
  end
54
66
 
55
67
  @window.refresh
56
68
  end
69
+
70
+ private
71
+
72
+ def queue(notification)
73
+ @queue.push(notification)
74
+ self
75
+ end
76
+
77
+ def resize(event)
78
+ @window.resize(1, stdscr.maxx)
79
+ @window.move(stdscr.maxy - 2, 0)
80
+ end
57
81
  end
58
82
  end
@@ -11,7 +11,7 @@ module Twterm
11
11
 
12
12
  def catch(on_rejected = nil, &block)
13
13
  if on_rejected.is_a?(Proc) && block_given?
14
- fail ArgumentError, 'both proc and block cannot be passed at the same time'
14
+ fail SyntaxError, 'both block arg and actual block given'
15
15
  end
16
16
 
17
17
  on_rejected ||= block
@@ -48,7 +48,7 @@ module Twterm
48
48
 
49
49
  def then(on_fulfilled = nil, on_rejected = nil, &block)
50
50
  if (on_fulfilled.is_a?(Proc) || on_rejected.is_a?(Proc)) && block_given?
51
- fail ArgumentError, 'both procs and block cannot be passed at the same time'
51
+ fail SyntaxError, 'both block arg and actual block given'
52
52
  end
53
53
 
54
54
  on_fulfilled ||= block
@@ -0,0 +1,16 @@
1
+ require 'twterm/event_dispatcher'
2
+ require 'twterm/event/base'
3
+ require 'twterm/utils'
4
+
5
+ module Twterm
6
+ module Publisher
7
+ include Utils
8
+
9
+ def publish(event)
10
+ check_type Event::Base, event
11
+
12
+ EventDispatcher.instance.dispatch(event)
13
+ event
14
+ end
15
+ end
16
+ end