twterm 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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