twterm 1.3.0 → 2.0.0.beta1

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 (121) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +14 -18
  3. data/bin/twterm +1 -1
  4. data/lib/twterm/app.rb +120 -30
  5. data/lib/twterm/client.rb +10 -13
  6. data/lib/twterm/completion_mamanger.rb +11 -6
  7. data/lib/twterm/direct_message.rb +6 -28
  8. data/lib/twterm/direct_message_composer.rb +10 -5
  9. data/lib/twterm/direct_message_manager.rb +5 -6
  10. data/lib/twterm/event/notification/abstract_notification.rb +27 -0
  11. data/lib/twterm/event/notification/error.rb +13 -0
  12. data/lib/twterm/event/notification/info.rb +13 -0
  13. data/lib/twterm/event/notification/success.rb +13 -0
  14. data/lib/twterm/event/notification/warning.rb +13 -0
  15. data/lib/twterm/event_dispatcher.rb +1 -1
  16. data/lib/twterm/extensions/array.rb +5 -0
  17. data/lib/twterm/extensions/enumerator/lazy.rb +3 -0
  18. data/lib/twterm/extensions/string.rb +0 -4
  19. data/lib/twterm/friendship.rb +1 -85
  20. data/lib/twterm/image/between.rb +31 -0
  21. data/lib/twterm/image/blank_line.rb +21 -0
  22. data/lib/twterm/image/bold.rb +31 -0
  23. data/lib/twterm/image/brackets.rb +21 -0
  24. data/lib/twterm/image/color.rb +45 -0
  25. data/lib/twterm/image/empty.rb +21 -0
  26. data/lib/twterm/image/horizontal_sequential_image.rb +48 -0
  27. data/lib/twterm/image/parens.rb +21 -0
  28. data/lib/twterm/image/string_image.rb +38 -0
  29. data/lib/twterm/image/vertical_sequential_image.rb +43 -0
  30. data/lib/twterm/image.rb +107 -0
  31. data/lib/twterm/key_mapper/abstract_key_mapper.rb +51 -0
  32. data/lib/twterm/key_mapper/app_key_mapper.rb +13 -0
  33. data/lib/twterm/key_mapper/cursor_key_mapper.rb +13 -0
  34. data/lib/twterm/key_mapper/general_key_mapper.rb +18 -0
  35. data/lib/twterm/key_mapper/no_such_command.rb +20 -0
  36. data/lib/twterm/key_mapper/no_such_key.rb +16 -0
  37. data/lib/twterm/key_mapper/status_key_mapper.rb +18 -0
  38. data/lib/twterm/key_mapper/tab_key_mapper.rb +31 -0
  39. data/lib/twterm/key_mapper.rb +127 -0
  40. data/lib/twterm/list.rb +0 -31
  41. data/lib/twterm/notifier.rb +7 -7
  42. data/lib/twterm/repository/abstract_entity_repository.rb +41 -0
  43. data/lib/twterm/repository/abstract_expirable_entity_repository.rb +35 -0
  44. data/lib/twterm/repository/abstract_repository.rb +64 -0
  45. data/lib/twterm/repository/direct_message_repository.rb +14 -0
  46. data/lib/twterm/repository/friendship_repository.rb +108 -0
  47. data/lib/twterm/repository/hashtag_repository.rb +39 -0
  48. data/lib/twterm/repository/list_repository.rb +14 -0
  49. data/lib/twterm/repository/status_repository.rb +36 -0
  50. data/lib/twterm/repository/user_repository.rb +22 -0
  51. data/lib/twterm/rest_client.rb +107 -63
  52. data/lib/twterm/screen.rb +21 -15
  53. data/lib/twterm/search_query_window.rb +139 -0
  54. data/lib/twterm/status.rb +14 -108
  55. data/lib/twterm/streaming_client.rb +13 -12
  56. data/lib/twterm/tab/base.rb +48 -8
  57. data/lib/twterm/tab/direct_message/conversation.rb +53 -52
  58. data/lib/twterm/tab/direct_message/conversation_list.rb +46 -45
  59. data/lib/twterm/tab/dumpable.rb +3 -3
  60. data/lib/twterm/tab/key_assignments_cheatsheet.rb +58 -57
  61. data/lib/twterm/tab/loadable.rb +20 -0
  62. data/lib/twterm/tab/new/list.rb +32 -43
  63. data/lib/twterm/tab/new/search.rb +31 -44
  64. data/lib/twterm/tab/new/start.rb +44 -55
  65. data/lib/twterm/tab/new/user.rb +15 -12
  66. data/lib/twterm/tab/rate_limit_status.rb +84 -0
  67. data/lib/twterm/tab/scrollable.rb +39 -19
  68. data/lib/twterm/tab/searchable.rb +133 -0
  69. data/lib/twterm/tab/statuses/base.rb +139 -129
  70. data/lib/twterm/tab/statuses/conversation.rb +26 -15
  71. data/lib/twterm/tab/statuses/favorites.rb +10 -8
  72. data/lib/twterm/tab/statuses/home.rb +10 -9
  73. data/lib/twterm/tab/statuses/list_timeline.rb +12 -8
  74. data/lib/twterm/tab/statuses/mentions.rb +17 -11
  75. data/lib/twterm/tab/statuses/search.rb +8 -5
  76. data/lib/twterm/tab/statuses/user_timeline.rb +11 -8
  77. data/lib/twterm/tab/user_list_management.rb +109 -0
  78. data/lib/twterm/tab/user_tab.rb +125 -126
  79. data/lib/twterm/tab/users/base.rb +39 -41
  80. data/lib/twterm/tab/users/followers.rb +9 -6
  81. data/lib/twterm/tab/users/friends.rb +9 -6
  82. data/lib/twterm/tab_manager.rb +64 -40
  83. data/lib/twterm/tweetbox.rb +18 -13
  84. data/lib/twterm/uri_opener.rb +2 -1
  85. data/lib/twterm/user.rb +2 -110
  86. data/lib/twterm/version.rb +1 -1
  87. data/lib/twterm/view.rb +30 -0
  88. data/lib/twterm.rb +3 -9
  89. data/spec/fixtures/status.json +107 -0
  90. data/spec/fixtures/user.json +102 -0
  91. data/spec/spec_helper.rb +7 -0
  92. data/spec/supports/shared_examples/abstract_key_mapper.rb +17 -0
  93. data/spec/twterm/extension/enumerator/lazy_spec.rb +11 -0
  94. data/spec/twterm/friendship_spec.rb +0 -102
  95. data/spec/twterm/image/blank_line_spec.rb +11 -0
  96. data/spec/twterm/image/brackets_spec.rb +12 -0
  97. data/spec/twterm/image/color_spec.rb +22 -0
  98. data/spec/twterm/image/empry_spec.rb +11 -0
  99. data/spec/twterm/image/horizontal_sequential_image_spec.rb +15 -0
  100. data/spec/twterm/image/parens_spec.rb +12 -0
  101. data/spec/twterm/image/string_image_spec.rb +12 -0
  102. data/spec/twterm/image/vertical_sequential_image_spec.rb +14 -0
  103. data/spec/twterm/image_spec.rb +65 -0
  104. data/spec/twterm/key_mapper/abstract_key_mapper_spec.rb +21 -0
  105. data/spec/twterm/key_mapper/app_key_mapper_spec.rb +7 -0
  106. data/spec/twterm/key_mapper/status_key_mapper_spec.rb +7 -0
  107. data/spec/twterm/key_mapper/tab_key_mapper_spec.rb +7 -0
  108. data/spec/twterm/repository/friendship_repository_spec.rb +108 -0
  109. data/spec/twterm/status_spec.rb +58 -0
  110. data/spec/twterm/user_spec.rb +94 -0
  111. data/twterm.gemspec +13 -10
  112. metadata +129 -35
  113. data/lib/twterm/event/notification.rb +0 -33
  114. data/lib/twterm/extensions/integer.rb +0 -5
  115. data/lib/twterm/filter_query_window.rb +0 -91
  116. data/lib/twterm/filterable_list.rb +0 -41
  117. data/lib/twterm/history/base.rb +0 -21
  118. data/lib/twterm/history/hashtag.rb +0 -13
  119. data/lib/twterm/history/savable.rb +0 -37
  120. data/lib/twterm/history/screen_name.rb +0 -11
  121. data/lib/twterm/promise.rb +0 -143
@@ -0,0 +1,84 @@
1
+ require 'twterm/tab/loadable'
2
+
3
+ module Twterm
4
+ module Tab
5
+ class RateLimitStatus < Tab::Base
6
+ include Loadable
7
+ include Scrollable
8
+
9
+ @@mutex = Mutex.new
10
+ @@status = nil
11
+
12
+ def initialize(app, client)
13
+ super(app, client)
14
+
15
+ scroller.set_no_cursor_mode!
16
+
17
+ @ticker = Scheduler.new(1) { render }
18
+
19
+ @scheduler = Scheduler.new(10) { fetch }
20
+
21
+ fetch.then { initially_loaded! }
22
+ end
23
+
24
+ def close
25
+ @scheduler.kill
26
+ @ticker.kill
27
+ super
28
+ end
29
+
30
+ def drawable_item_count
31
+ window.maxy - 2
32
+ end
33
+
34
+ def image
35
+ return Image.string('Loading...') unless initially_loaded?
36
+
37
+ items
38
+ .drop(scroller.offset)
39
+ .take(drawable_item_count)
40
+ .reduce(Image.empty, :|)
41
+ end
42
+
43
+ def items
44
+ return [] unless initially_loaded?
45
+
46
+ @@status[:resources].flat_map do |category, limits|
47
+ [
48
+ !Image.string(category.to_s.gsub('_', ' ')).color(:green),
49
+ Image.blank_line,
50
+ *limits.flat_map do |endpoint, data|
51
+ limit, remaining = data[:limit], data[:remaining]
52
+ t = Time.at(data[:reset])
53
+ diff = (t - Time.now).round
54
+
55
+ [
56
+ Image.string(" #{endpoint}").color(:cyan) - (
57
+ limit == remaining \
58
+ ? Image.empty
59
+ : Image.string(" (Resets #{diff.positive? ? "in #{diff} seconds" : 'soon...'})")),
60
+ Image.string(' ') - Image.remaining_resource(remaining, limit, 50) - Image.string(" #{remaining}/#{limit}"),
61
+ Image.blank_line,
62
+ ]
63
+ end,
64
+ Image.blank_line,
65
+ ]
66
+ end
67
+ end
68
+
69
+ def respond_to_key(key)
70
+ scroller.respond_to_key(key)
71
+ end
72
+
73
+ def title
74
+ 'Rate Limit Status'.freeze
75
+ end
76
+
77
+ private
78
+
79
+ def fetch
80
+ client.rate_limit_status.then { |status| @@mutex.synchronize { @@status = status } }
81
+ end
82
+ end
83
+ end
84
+ end
@@ -9,9 +9,9 @@ module Twterm
9
9
  def scroller
10
10
  return @scroller unless @scroller.nil?
11
11
 
12
- @scroller = Scroller.new
12
+ @scroller = self.class::Scroller.new(self)
13
13
  @scroller.delegate = self
14
- @scroller.after_move { refresh }
14
+ @scroller.after_move { render }
15
15
  @scroller
16
16
  end
17
17
 
@@ -21,10 +21,9 @@ module Twterm
21
21
  items.count
22
22
  end
23
23
 
24
- private
25
-
26
24
  class Scroller
27
25
  extend Forwardable
26
+ include Publisher
28
27
 
29
28
  attr_reader :index, :offset
30
29
 
@@ -35,19 +34,21 @@ module Twterm
35
34
  add_hook(:after_move, &block)
36
35
  end
37
36
 
38
- def current_item
39
- items[index]
37
+ def current_index?(i)
38
+ index == offset + i
40
39
  end
41
40
 
42
- def current_item?(i)
43
- index == offset + i
41
+ def current_item
42
+ items[index]
44
43
  end
45
44
 
46
45
  def no_cursor_mode?
47
46
  !!@no_cursor_mode
48
47
  end
49
48
 
50
- def initialize
49
+ def initialize(tab)
50
+ @tab = tab
51
+
51
52
  @index = 0
52
53
  @offset = 0
53
54
  @no_cursor_mode = false
@@ -58,7 +59,6 @@ module Twterm
58
59
  end
59
60
 
60
61
  def item_appended!
61
- @index -= 1
62
62
  @offset -= 1 if @offset > 0
63
63
  end
64
64
 
@@ -69,7 +69,7 @@ module Twterm
69
69
  end
70
70
 
71
71
  def move_down
72
- return if count == 0 || index == count - 1
72
+ return if count == 0 || index == count - 1 || (no_cursor_mode? && offset + drawable_item_count >= count)
73
73
  # return when there are no items or cursor is at the bottom
74
74
 
75
75
  @index += 1
@@ -81,7 +81,7 @@ module Twterm
81
81
  def move_to_bottom
82
82
  return if count == 0 || index == count - 1
83
83
 
84
- @index = count - 1
84
+ @index = no_cursor_mode? ? count - drawable_item_count : count - 1
85
85
  @offset = [count - drawable_item_count + 1, 0].max
86
86
 
87
87
  @offset += 1 until last_item_shown?
@@ -89,6 +89,16 @@ module Twterm
89
89
  hook :after_move
90
90
  end
91
91
 
92
+ def move_to(n)
93
+ if nth_item_drawable?(n)
94
+ @index = n
95
+ else
96
+ @index = @offset = n
97
+ end
98
+
99
+ hook :after_move
100
+ end
101
+
92
102
  def move_to_top
93
103
  return if count.zero? || index.zero?
94
104
 
@@ -108,23 +118,31 @@ module Twterm
108
118
  end
109
119
 
110
120
  def nth_item_drawable?(n)
111
- n.between?(offset, offset + drawable_item_count)
121
+ n.between?(offset, offset + drawable_item_count - 1)
112
122
  end
113
123
 
114
124
  def respond_to_key(key)
125
+ k = KeyMapper.instance
126
+
115
127
  case key
116
- when ?d, 4
128
+ when k[:general, :page_down]
117
129
  10.times { move_down }
118
- when ?g
130
+ when k[:general, :top]
119
131
  move_to_top
120
- when ?G
132
+ when k[:general, :bottom]
121
133
  move_to_bottom
122
- when ?j, 14, Curses::Key::DOWN
134
+ when k[:general, :down], Curses::Key::DOWN
123
135
  move_down
124
- when ?k, 16, Curses::Key::UP
136
+ when k[:general, :up], Curses::Key::UP
125
137
  move_up
126
- when ?u, 21
138
+ when k[:general, :page_up]
127
139
  10.times { move_up }
140
+ when k[:cursor, :top_of_window]
141
+ move_to(offset)
142
+ when k[:cursor, :middle_of_window]
143
+ move_to((2 * offset + [drawable_item_count, total_item_count - offset].min - 1) / 2)
144
+ when k[:cursor, :bottom_of_window]
145
+ move_to(offset + [drawable_item_count, total_item_count - offset].min - 1)
128
146
  else
129
147
  return false
130
148
  end
@@ -138,6 +156,8 @@ module Twterm
138
156
 
139
157
  private
140
158
 
159
+ attr_reader :tab
160
+
141
161
  def add_hook(name, &block)
142
162
  @hooks ||= {}
143
163
  @hooks[name] = block
@@ -0,0 +1,133 @@
1
+ require 'twterm/event/notification/info'
2
+ require 'twterm/event/notification/warning'
3
+ require 'twterm/search_query_window'
4
+
5
+ module Twterm
6
+ module Tab
7
+ module Searchable
8
+ include Scrollable
9
+ extend Forwardable
10
+
11
+ def_delegators :scroller, :search_query
12
+
13
+ def matches?(item, query)
14
+ raise NotImplementedError, '`matches?` method must be implemented'
15
+ end
16
+
17
+ class Scroller < Scrollable::Scroller
18
+ extend Forwardable
19
+ include Publisher
20
+
21
+ attr_reader :search_query
22
+
23
+ def_delegators :search_query_window,
24
+ :searching_backward!, :searching_backward?,
25
+ :searching_down!, :searching_down?,
26
+ :searching_forward!, :searching_forward?,
27
+ :searching_up!, :searching_up?
28
+
29
+ def initialize(*)
30
+ super
31
+ @search_query = ''
32
+ end
33
+
34
+ def find_next
35
+ searching_forward!
36
+
37
+ search_query_window.render_last_query
38
+ search
39
+ end
40
+
41
+ def find_previous
42
+ searching_backward!
43
+
44
+ search_query_window.render_last_query
45
+ search
46
+ end
47
+
48
+ def respond_to_key(key)
49
+ k = KeyMapper.instance
50
+
51
+ return if super
52
+
53
+ case key
54
+ when k[:tab, :find_next]
55
+ find_next
56
+ when k[:tab, :find_previous]
57
+ find_previous
58
+ when k[:tab, :search_down]
59
+ search_down
60
+ when k[:tab, :search_up]
61
+ search_up
62
+ else
63
+ return false
64
+ end
65
+
66
+ true
67
+ end
68
+
69
+ def search_up
70
+ searching_up!
71
+ searching_forward!
72
+ ask
73
+
74
+ return if search_query.empty?
75
+
76
+ find_next
77
+ end
78
+
79
+ def search_down
80
+ searching_down!
81
+ searching_forward!
82
+ ask
83
+
84
+ return if search_query.empty?
85
+
86
+ find_next
87
+ end
88
+
89
+ private
90
+
91
+ def ask
92
+ @search_query = search_query_window.input || ''
93
+ end
94
+
95
+ def search
96
+ @search_query = search_query_window.last_query if search_query.empty?
97
+
98
+ if @search_query.empty?
99
+ event = Event::Notification::Info.new("search query is empty. Press '/' or '?' to start searching.")
100
+ publish(event)
101
+ return
102
+ end
103
+
104
+ f = searching_down? ^ searching_forward? ? -> xs { xs.reverse } : -> xs { xs }
105
+ previous_index = index
106
+
107
+ xs = [
108
+ *f.(items.each_with_index.drop(index.succ) + items.each_with_index.take(index)),
109
+ [current_item, index]
110
+ ]
111
+ _, index = xs.find { |x, _| tab.matches?(x, search_query) }
112
+
113
+ if index.nil?
114
+ publish(Event::Notification::Warning.new("No matches found: \"#{search_query}\""))
115
+ else
116
+ hit_bottom = Event::Notification::Info.new('search hit BOTTOM, continuing at TOP')
117
+ hit_top = Event::Notification::Info.new('search hit TOP, continuing at BOTTOM')
118
+
119
+ publish(hit_bottom) if (searching_down? ^ searching_backward?) && index <= previous_index
120
+ publish(hit_top) if (searching_up? ^ searching_backward?) && index >= previous_index
121
+ move_to(index)
122
+ end
123
+ end
124
+
125
+ def search_query_window
126
+ SearchQueryWindow.instance
127
+ end
128
+
129
+ alias_method :count, :total_item_count
130
+ end
131
+ end
132
+ end
133
+ end