twterm 1.0.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 (43) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +88 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +10 -0
  6. data/Rakefile +1 -0
  7. data/bin/twterm +5 -0
  8. data/lib/extentions.rb +51 -0
  9. data/lib/twterm/app.rb +45 -0
  10. data/lib/twterm/auth.rb +23 -0
  11. data/lib/twterm/client.rb +214 -0
  12. data/lib/twterm/color_manager.rb +50 -0
  13. data/lib/twterm/config.rb +37 -0
  14. data/lib/twterm/list.rb +20 -0
  15. data/lib/twterm/notification/base.rb +24 -0
  16. data/lib/twterm/notification/error.rb +19 -0
  17. data/lib/twterm/notification/message.rb +19 -0
  18. data/lib/twterm/notifier.rb +68 -0
  19. data/lib/twterm/screen.rb +53 -0
  20. data/lib/twterm/status.rb +102 -0
  21. data/lib/twterm/tab/auto_reloadable.rb +14 -0
  22. data/lib/twterm/tab/base.rb +41 -0
  23. data/lib/twterm/tab/conversation_tab.rb +31 -0
  24. data/lib/twterm/tab/exceptions.rb +6 -0
  25. data/lib/twterm/tab/favorites.rb +7 -0
  26. data/lib/twterm/tab/list_tab.rb +33 -0
  27. data/lib/twterm/tab/mentions_tab.rb +36 -0
  28. data/lib/twterm/tab/new/list.rb +88 -0
  29. data/lib/twterm/tab/new/search.rb +52 -0
  30. data/lib/twterm/tab/new/start.rb +67 -0
  31. data/lib/twterm/tab/scrollable.rb +130 -0
  32. data/lib/twterm/tab/search_tab.rb +29 -0
  33. data/lib/twterm/tab/statuses_tab.rb +235 -0
  34. data/lib/twterm/tab/timeline_tab.rb +33 -0
  35. data/lib/twterm/tab/user_tab.rb +29 -0
  36. data/lib/twterm/tab_manager.rb +111 -0
  37. data/lib/twterm/tweetbox.rb +53 -0
  38. data/lib/twterm/user.rb +40 -0
  39. data/lib/twterm/user_window.rb +71 -0
  40. data/lib/twterm/version.rb +3 -0
  41. data/lib/twterm.rb +50 -0
  42. data/twterm.gemspec +28 -0
  43. metadata +184 -0
@@ -0,0 +1,130 @@
1
+ module Twterm
2
+ module Tab
3
+ module Scrollable
4
+ include Base
5
+
6
+ def initialize
7
+ super
8
+
9
+ @scrollable_index = 0
10
+ @scrollable_count = 0
11
+ @scrollable_offset = 0
12
+ @scrollable_last = 0
13
+ @scrollable_scrollbar_length = 0
14
+ end
15
+
16
+ def respond_to_key(key)
17
+ case key
18
+ when 'g'
19
+ move_to_top
20
+ when 'G'
21
+ move_to_bottom
22
+ when 'j', 14, Key::DOWN
23
+ move_down
24
+ when 'k', 16, Key::UP
25
+ move_up
26
+ when 'd', 4
27
+ 10.times { move_down }
28
+ when 'u', 21
29
+ 10.times { move_up }
30
+ else
31
+ return false
32
+ end
33
+ true
34
+ end
35
+
36
+ def index
37
+ @scrollable_index
38
+ end
39
+
40
+ def count
41
+ @scrollable_count
42
+ end
43
+
44
+ def offset
45
+ @scrollable_offset
46
+ end
47
+
48
+ def last
49
+ @scrollable_last
50
+ end
51
+
52
+ def item_prepended
53
+ @scrollable_index += 1
54
+ @scrollable_offset += 1
55
+ update_scrollbar_length
56
+ end
57
+
58
+ def item_appended
59
+ @scrollable_index -= 1
60
+ @scrollable_offset -= 1 if @scrollable_offset > 0
61
+ update_scrollbar_length
62
+ end
63
+
64
+ def move_up
65
+ return if count == 0 || index == 0
66
+
67
+ @scrollable_index = [index - 1, 0].max
68
+ @scrollable_offset = [offset - 1, 0].max if index - 4 < offset
69
+ refresh
70
+ end
71
+
72
+ def move_down
73
+ return if count == 0 || index == count - 1
74
+
75
+ @scrollable_index = [index + 1, count - 1].min
76
+ @scrollable_offset = [
77
+ offset + 1,
78
+ count - 1,
79
+ count - offset_from_bottom
80
+ ].min if index > last - 4
81
+
82
+ refresh
83
+ end
84
+
85
+ def move_to_top
86
+ return if count == 0 || index == 0
87
+
88
+ @scrollable_index = 0
89
+ @scrollable_offset = 0
90
+ refresh
91
+ end
92
+
93
+ def move_to_bottom
94
+ return if count == 0 || index == count - 1
95
+
96
+ @scrollable_index = count - 1
97
+ @scrollable_offset = count - 1 - offset_from_bottom
98
+ refresh
99
+ end
100
+
101
+ def offset_from_bottom
102
+ 0
103
+ end
104
+
105
+ def update_scrollbar_length
106
+ @scrollable_scrollbar_length =
107
+ if count == 0
108
+ 0
109
+ else
110
+ height = @window.maxy
111
+ [height * (last - index + 1) / count, 1].max
112
+ end
113
+ end
114
+
115
+ def draw_scroll_bar
116
+ return if count == 0
117
+
118
+ height = @window.maxy
119
+ top = height * index / count
120
+
121
+ @window.with_color(:black, :white) do
122
+ @scrollable_scrollbar_length.times do |i|
123
+ @window.setpos(top + i, @window.maxx - 1)
124
+ @window.addch(' ')
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,29 @@
1
+ module Twterm
2
+ module Tab
3
+ class SearchTab
4
+ include StatusesTab
5
+
6
+ attr_reader :query
7
+
8
+ def initialize(query)
9
+ super()
10
+
11
+ @query = query
12
+ @title = "\"#{@query}\""
13
+
14
+ fetch { move_to_top }
15
+ end
16
+
17
+ def fetch
18
+ Client.current.search(@query) do |statuses|
19
+ statuses.reverse.each { |status| prepend(status) }
20
+ yield if block_given?
21
+ end
22
+ end
23
+
24
+ def ==(other)
25
+ other.is_a?(self.class) && query == other.query
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,235 @@
1
+ module Twterm
2
+ module Tab
3
+ module StatusesTab
4
+ include Base
5
+ include Scrollable
6
+ include AutoReloadable
7
+
8
+ def initialize
9
+ super
10
+
11
+ @statuses = []
12
+ end
13
+
14
+ def prepend(status)
15
+ fail unless status.is_a? Status
16
+
17
+ return if @statuses.any? { |s| s.id == status.id }
18
+
19
+ @statuses << status
20
+ status.split(@window.maxx - 4)
21
+ item_prepended
22
+ refresh
23
+ end
24
+
25
+ def append(status)
26
+ fail ArgumentError, 'argument must be an instance of Status class' unless status.is_a? Status
27
+
28
+ return if @statuses.any? { |s| s == status }
29
+
30
+ @statuses.unshift(status)
31
+ status.split(@window.maxx - 4)
32
+ item_appended
33
+ refresh
34
+ end
35
+
36
+ def reply
37
+ return if highlighted_status.nil?
38
+ Tweetbox.instance.compose(highlighted_status)
39
+ end
40
+
41
+ def favorite
42
+ return if highlighted_status.nil?
43
+ if highlighted_status.favorited?
44
+ Client.current.unfavorite(highlighted_status) do
45
+ refresh
46
+ end
47
+ else
48
+ Client.current.favorite(highlighted_status) do
49
+ refresh
50
+ end
51
+ end
52
+ end
53
+
54
+ def retweet
55
+ return if highlighted_status.nil?
56
+ Client.current.retweet(highlighted_status) do
57
+ refresh
58
+ end
59
+ end
60
+
61
+ def delete_status(status_id)
62
+ detector = -> (status) { status.id == status_id }
63
+ @statuses.delete_if(&detector)
64
+ refresh
65
+ end
66
+
67
+ def show_user
68
+ return if highlighted_status.nil?
69
+ user = highlighted_status.user
70
+ user_tab = Tab::UserTab.new(user)
71
+ TabManager.instance.add_and_show(user_tab)
72
+ end
73
+
74
+ def open_link
75
+ return if highlighted_status.nil?
76
+ status = highlighted_status
77
+ urls = status.urls.map(&:expanded_url) + status.media.map(&:expanded_url)
78
+ urls.each(&Launchy.method(:open))
79
+ end
80
+
81
+ def show_conversation
82
+ return if highlighted_status.nil?
83
+ tab = Tab::ConversationTab.new(highlighted_status)
84
+ TabManager.instance.add_and_show(tab)
85
+ end
86
+
87
+ def fetch
88
+ fail NotImplementedError, 'fetch method must be implemented'
89
+ end
90
+
91
+ def update
92
+ current_line = 0
93
+
94
+ @window.clear
95
+
96
+ @statuses.reverse.drop(offset).each.with_index(offset) do |status, i|
97
+ formatted_lines = status.split(@window.maxx - 4).count
98
+ if current_line + formatted_lines + 2 > @window.maxy
99
+ @scrollable_last = i
100
+ break
101
+ end
102
+
103
+ posy = current_line
104
+
105
+ if index == i
106
+ @window.with_color(:black, :magenta) do
107
+ (formatted_lines + 1).times do |j|
108
+ @window.setpos(posy + j, 0)
109
+ @window.addch(' ')
110
+ end
111
+ end
112
+ end
113
+
114
+ @window.setpos(current_line, 2)
115
+
116
+ @window.bold do
117
+ @window.with_color(status.user.color) do
118
+ @window.addstr(status.user.name)
119
+ end
120
+ end
121
+
122
+ @window.addstr(" (@#{status.user.screen_name}) [#{status.date}] ")
123
+
124
+ unless status.retweeted_by.nil?
125
+ @window.addstr('(retweeted by ')
126
+ @window.bold do
127
+ @window.addstr("@#{status.retweeted_by.screen_name}")
128
+ end
129
+ @window.addstr(') ')
130
+ end
131
+
132
+ if status.favorited?
133
+ @window.with_color(:black, :yellow) do
134
+ @window.addch(' ')
135
+ end
136
+
137
+ @window.addch(' ')
138
+ end
139
+
140
+ if status.retweeted?
141
+ @window.with_color(:black, :green) do
142
+ @window.addch(' ')
143
+ end
144
+ @window.addch(' ')
145
+ end
146
+
147
+ if status.favorite_count > 0
148
+ @window.with_color(:yellow) do
149
+ @window.addstr("#{status.favorite_count}fav#{status.favorite_count > 1 ? 's' : ''}")
150
+ end
151
+ @window.addch(' ')
152
+ end
153
+
154
+ if status.retweet_count > 0
155
+ @window.with_color(:green) do
156
+ @window.addstr("#{status.retweet_count}RT#{status.retweet_count > 1 ? 's' : ''}")
157
+ end
158
+ @window.addch(' ')
159
+ end
160
+
161
+ status.split(@window.maxx - 4).each do |line|
162
+ current_line += 1
163
+ @window.setpos(current_line, 2)
164
+ @window.addstr(line)
165
+ end
166
+
167
+ current_line += 2
168
+ end
169
+
170
+ draw_scroll_bar
171
+
172
+ @window.refresh
173
+
174
+ UserWindow.instance.update(highlighted_status.user) unless highlighted_status.nil?
175
+ show_help
176
+ end
177
+
178
+ def respond_to_key(key)
179
+ return true if super
180
+
181
+ case key
182
+ when 'c'
183
+ show_conversation
184
+ when 'F'
185
+ favorite
186
+ when 'o'
187
+ open_link
188
+ when 'r'
189
+ reply
190
+ when 'R'
191
+ retweet
192
+ when 18
193
+ fetch
194
+ when 'U'
195
+ show_user
196
+ else
197
+ return false
198
+ end
199
+ true
200
+ end
201
+
202
+ private
203
+
204
+ def highlighted_status
205
+ @statuses[count - index - 1]
206
+ end
207
+
208
+ def count
209
+ @statuses.count
210
+ end
211
+
212
+ def offset_from_bottom
213
+ return @offset_from_bottom unless @offset_from_bottom.nil?
214
+
215
+ height = 0
216
+ @statuses.each.with_index(-1) do |status, i|
217
+ height += status.split(@window.maxx - 4).count + 2
218
+ if height >= @window.maxy
219
+ @offset_from_bottom = i
220
+ return i
221
+ end
222
+ end
223
+ count
224
+ end
225
+
226
+ def sort
227
+ @statuses.sort_by!(&:created_at_for_sort)
228
+ end
229
+
230
+ def show_help
231
+ Notifier.instance.show_help '[n] Compose [r] Reply [F] Favorite [R] Retweet [U] Show user [w] Close tab [Q] Quit'
232
+ end
233
+ end
234
+ end
235
+ end
@@ -0,0 +1,33 @@
1
+ module Twterm
2
+ module Tab
3
+ class TimelineTab
4
+ include StatusesTab
5
+
6
+ def initialize(client)
7
+ fail ArgumentError, 'argument must be an instance of Client class' unless client.is_a? Client
8
+
9
+ super()
10
+ @client = client
11
+ @client.on_timeline_status(&method(:prepend))
12
+ @title = 'Timeline'
13
+
14
+ fetch { move_to_top }
15
+ auto_reload(180) { fetch }
16
+ end
17
+
18
+ def fetch
19
+ Thread.new do
20
+ @client.home_timeline do |statuses|
21
+ statuses.each(&method(:prepend))
22
+ sort
23
+ yield if block_given?
24
+ end
25
+ end
26
+ end
27
+
28
+ def close
29
+ fail NotClosableError
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,29 @@
1
+ module Twterm
2
+ module Tab
3
+ class UserTab
4
+ include StatusesTab
5
+
6
+ attr_reader :user
7
+
8
+ def initialize(user)
9
+ fail ArgumentError, 'argument must be an instance of User class' unless user.is_a? User
10
+
11
+ super()
12
+
13
+ @user = user
14
+ @title = "@#{user.screen_name}"
15
+
16
+ fetch { move_to_top }
17
+ auto_reload(120) { fetch }
18
+ end
19
+
20
+ def fetch
21
+ Client.current.user_timeline(@user.id) do |statuses|
22
+ statuses.reverse.each(&method(:prepend))
23
+ sort
24
+ yield if block_given?
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,111 @@
1
+ module Twterm
2
+ class TabManager
3
+ include Singleton
4
+ include Curses
5
+
6
+ def initialize
7
+ @tabs = []
8
+ @index = 0
9
+ @history = []
10
+
11
+ @window = stdscr.subwin(3, stdscr.maxx - 30, 0, 0)
12
+ end
13
+
14
+ def add(tab_to_add)
15
+ fail ArgumentError, 'argument must be an instance of Tab::Base' unless tab_to_add.is_a? Tab::Base
16
+ @tabs.each.with_index do |tab, i|
17
+ next unless tab == tab_to_add
18
+ @index = i
19
+ refresh_window
20
+ return false
21
+ end
22
+ @tabs << tab_to_add
23
+ @history.push(@tabs.count)
24
+ refresh_window
25
+ true
26
+ end
27
+
28
+ def add_and_show(tab)
29
+ result = add(tab)
30
+ @index = @tabs.count - 1 if result
31
+ current_tab.refresh
32
+ refresh_window
33
+ result
34
+ end
35
+
36
+ def current_tab
37
+ @history.unshift(@index).uniq!
38
+ @tabs[@index]
39
+ end
40
+
41
+ def show_next
42
+ @index = (@index + 1) % @tabs.count
43
+ current_tab.refresh
44
+ refresh_window
45
+ end
46
+
47
+ def show_previous
48
+ @index = (@index - 1) % @tabs.count
49
+ current_tab.refresh
50
+ refresh_window
51
+ end
52
+
53
+ def open_new
54
+ tab = Tab::New::Start.new
55
+ add_and_show(tab)
56
+ end
57
+
58
+ def close
59
+ current_tab.close
60
+ @tabs.delete_at(@index)
61
+ @history.delete_if { |n| n == @index }
62
+ @history = @history.map { |i| i > @index ? i - 1 : i }
63
+ @index = @history.first
64
+ current_tab.refresh
65
+ refresh_window
66
+ rescue Tab::NotClosableError
67
+ Notifier.instance.show_error 'This tab cannot be closed'
68
+ end
69
+
70
+ def switch(tab)
71
+ close
72
+ add_and_show(tab)
73
+ end
74
+
75
+ def refresh_window
76
+ @window.clear
77
+ current_tab_id = current_tab.object_id
78
+
79
+ @window.setpos(1, 1)
80
+ @window.addstr('| ')
81
+ @tabs.each do |tab|
82
+ if tab.object_id == current_tab_id
83
+ @window.bold do
84
+ @window.addstr(tab.title)
85
+ end
86
+ else
87
+ @window.addstr(tab.title)
88
+ end
89
+ @window.addstr(' | ')
90
+ end
91
+
92
+ @window.refresh
93
+ end
94
+
95
+ def respond_to_key(key)
96
+ case key
97
+ when 'h', 2, Key::LEFT
98
+ show_previous
99
+ when 'l', 6, Key::RIGHT
100
+ show_next
101
+ when 'N'
102
+ open_new
103
+ when 'w'
104
+ close
105
+ else
106
+ return false
107
+ end
108
+ true
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,53 @@
1
+ module Twterm
2
+ class Tweetbox
3
+ include Singleton
4
+ include Readline
5
+ include Curses
6
+
7
+ def initialize
8
+ @status = ''
9
+ end
10
+
11
+ def compose(in_reply_to = nil)
12
+ if in_reply_to.is_a? Status
13
+ @in_reply_to = in_reply_to
14
+ else
15
+ @in_reply_to = nil
16
+ end
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
+ puts "\ncompose new tweet:"
27
+ @status = readline(@in_reply_to.nil? ? '> ' : " @#{in_reply_to.user.screen_name} ", true)
28
+ resetter.call
29
+ post
30
+ end
31
+
32
+ App.instance.register_interruption_handler do
33
+ thread.kill
34
+ clear
35
+ puts "\ncanceled"
36
+ resetter.call
37
+ end
38
+
39
+ thread.join
40
+ end
41
+
42
+ def post
43
+ return if @status.nil?
44
+
45
+ Client.current.post(@status, @in_reply_to)
46
+ clear
47
+ end
48
+
49
+ def clear
50
+ @status = ''
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,40 @@
1
+ module Twterm
2
+ class User
3
+ attr_reader :id, :name, :screen_name, :description, :location, :website, :following, :protected, :statuses_count, :friends_count, :followers_count
4
+ attr_reader :color
5
+ alias_method :following?, :following
6
+ alias_method :protected?, :protected
7
+
8
+ COLORS = [:red, :blue, :green, :cyan, :yellow, :magenta]
9
+
10
+ @@instances = []
11
+
12
+ def self.new(user)
13
+ detector = -> (instance) { instance.id == user.id }
14
+ instance = @@instances.find(&detector)
15
+ instance.nil? ? super : instance.update!(user)
16
+ end
17
+
18
+ def initialize(user)
19
+ @id = user.id
20
+ update!(user)
21
+ @color = COLORS[@id % 6]
22
+
23
+ @@instances << self
24
+ end
25
+
26
+ def update!(user)
27
+ @name = user.name
28
+ @screen_name = user.screen_name
29
+ @description = user.description || ''
30
+ @location = user.location.is_a?(Twitter::NullObject) ? '' : user.location
31
+ @website = user.website
32
+ @following = user.following?
33
+ @protected = user.protected?
34
+ @statuses_count = user.statuses_count
35
+ @friends_count = user.friends_count
36
+ @followers_count = user.followers_count
37
+ self
38
+ end
39
+ end
40
+ end