twterm 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +88 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +10 -0
- data/Rakefile +1 -0
- data/bin/twterm +5 -0
- data/lib/extentions.rb +51 -0
- data/lib/twterm/app.rb +45 -0
- data/lib/twterm/auth.rb +23 -0
- data/lib/twterm/client.rb +214 -0
- data/lib/twterm/color_manager.rb +50 -0
- data/lib/twterm/config.rb +37 -0
- data/lib/twterm/list.rb +20 -0
- data/lib/twterm/notification/base.rb +24 -0
- data/lib/twterm/notification/error.rb +19 -0
- data/lib/twterm/notification/message.rb +19 -0
- data/lib/twterm/notifier.rb +68 -0
- data/lib/twterm/screen.rb +53 -0
- data/lib/twterm/status.rb +102 -0
- data/lib/twterm/tab/auto_reloadable.rb +14 -0
- data/lib/twterm/tab/base.rb +41 -0
- data/lib/twterm/tab/conversation_tab.rb +31 -0
- data/lib/twterm/tab/exceptions.rb +6 -0
- data/lib/twterm/tab/favorites.rb +7 -0
- data/lib/twterm/tab/list_tab.rb +33 -0
- data/lib/twterm/tab/mentions_tab.rb +36 -0
- data/lib/twterm/tab/new/list.rb +88 -0
- data/lib/twterm/tab/new/search.rb +52 -0
- data/lib/twterm/tab/new/start.rb +67 -0
- data/lib/twterm/tab/scrollable.rb +130 -0
- data/lib/twterm/tab/search_tab.rb +29 -0
- data/lib/twterm/tab/statuses_tab.rb +235 -0
- data/lib/twterm/tab/timeline_tab.rb +33 -0
- data/lib/twterm/tab/user_tab.rb +29 -0
- data/lib/twterm/tab_manager.rb +111 -0
- data/lib/twterm/tweetbox.rb +53 -0
- data/lib/twterm/user.rb +40 -0
- data/lib/twterm/user_window.rb +71 -0
- data/lib/twterm/version.rb +3 -0
- data/lib/twterm.rb +50 -0
- data/twterm.gemspec +28 -0
- metadata +184 -0
@@ -0,0 +1,68 @@
|
|
1
|
+
module Twterm
|
2
|
+
class Notifier
|
3
|
+
include Singleton
|
4
|
+
include Curses
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@window = stdscr.subwin(2, stdscr.maxx, stdscr.maxy - 2, 0)
|
8
|
+
@queue = Queue.new
|
9
|
+
@help = ''
|
10
|
+
|
11
|
+
Thread.new do
|
12
|
+
while notification = @queue.pop
|
13
|
+
show(notification)
|
14
|
+
sleep 3
|
15
|
+
show
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def show_message(message)
|
21
|
+
notification = Notification::Message.new(message)
|
22
|
+
@queue.push(notification)
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
def show_error(message)
|
27
|
+
notification = Notification::Error.new(message)
|
28
|
+
@queue.push(notification)
|
29
|
+
self
|
30
|
+
end
|
31
|
+
|
32
|
+
def show_help(message)
|
33
|
+
return if @help == message
|
34
|
+
|
35
|
+
@help = message
|
36
|
+
show
|
37
|
+
end
|
38
|
+
|
39
|
+
def show(notification = nil)
|
40
|
+
loop do
|
41
|
+
break unless closed?
|
42
|
+
sleep 0.5
|
43
|
+
end
|
44
|
+
|
45
|
+
@window.clear
|
46
|
+
|
47
|
+
if notification.is_a? Notification::Base
|
48
|
+
@window.with_color(notification.fg_color, notification.bg_color) do
|
49
|
+
@window.setpos(1, 0)
|
50
|
+
@window.addstr(' ' * @window.maxx)
|
51
|
+
@window.setpos(1, 1)
|
52
|
+
time = notification.time.strftime('[%H:%M:%S]')
|
53
|
+
message = notification.show_with_width(@window.maxx)
|
54
|
+
@window.addstr("#{time} #{message}")
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
@window.with_color(:black, :green) do
|
59
|
+
@window.setpos(0, 0)
|
60
|
+
@window.addstr(' ' * @window.maxx)
|
61
|
+
@window.setpos(0, 1)
|
62
|
+
@window.addstr(@help)
|
63
|
+
end
|
64
|
+
|
65
|
+
@window.refresh
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Twterm
|
2
|
+
class Screen
|
3
|
+
include Singleton
|
4
|
+
include Curses
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@screen = init_screen
|
8
|
+
noecho
|
9
|
+
cbreak
|
10
|
+
curs_set(0)
|
11
|
+
stdscr.keypad(true)
|
12
|
+
start_color
|
13
|
+
end
|
14
|
+
|
15
|
+
def wait
|
16
|
+
@thread = Thread.new do
|
17
|
+
loop do
|
18
|
+
scan
|
19
|
+
end
|
20
|
+
end
|
21
|
+
@thread.join
|
22
|
+
end
|
23
|
+
|
24
|
+
def refresh
|
25
|
+
TabManager.instance.refresh_window
|
26
|
+
TabManager.instance.current_tab.refresh
|
27
|
+
UserWindow.instance.refresh_window
|
28
|
+
Notifier.instance.show
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def scan
|
34
|
+
App.instance.reset_interruption_handler
|
35
|
+
|
36
|
+
key = getch
|
37
|
+
|
38
|
+
return if TabManager.instance.current_tab.respond_to_key(key)
|
39
|
+
return if TabManager.instance.respond_to_key(key)
|
40
|
+
|
41
|
+
case key
|
42
|
+
when 'n'
|
43
|
+
Tweetbox.instance.compose
|
44
|
+
return
|
45
|
+
when 'Q'
|
46
|
+
exit
|
47
|
+
when '/'
|
48
|
+
# filter
|
49
|
+
else
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module Twterm
|
2
|
+
class Status
|
3
|
+
attr_reader :id, :text, :created_at, :created_at_for_sort, :retweet_count, :favorite_count, :in_reply_to_status_id, :favorited, :retweeted, :user, :retweeted_by, :urls, :media
|
4
|
+
alias_method :favorited?, :favorited
|
5
|
+
alias_method :retweeted?, :retweeted
|
6
|
+
|
7
|
+
@@instances = []
|
8
|
+
|
9
|
+
def self.new(tweet)
|
10
|
+
detector = -> (instance) { instance.id == tweet.id }
|
11
|
+
instance = @@instances.find(&detector)
|
12
|
+
instance.nil? ? super : instance.update!(tweet)
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(tweet)
|
16
|
+
unless tweet.retweeted_status.is_a? Twitter::NullObject
|
17
|
+
@retweeted_by = User.new(tweet.user)
|
18
|
+
retweeted_at = Status.parse_time(tweet.created_at)
|
19
|
+
tweet = tweet.retweeted_status
|
20
|
+
end
|
21
|
+
|
22
|
+
@id = tweet.id
|
23
|
+
@text = CGI.unescapeHTML(tweet.full_text.dup)
|
24
|
+
@created_at = Status.parse_time(tweet.created_at)
|
25
|
+
@created_at_for_sort = retweeted_at || @created_at
|
26
|
+
@retweet_count = tweet.retweet_count
|
27
|
+
@favorite_count = tweet.favorite_count
|
28
|
+
@in_reply_to_status_id = tweet.in_reply_to_status_id
|
29
|
+
|
30
|
+
@retweeted = tweet.retweeted?
|
31
|
+
@favorited = tweet.favorited?
|
32
|
+
|
33
|
+
@media = tweet.media
|
34
|
+
@urls = tweet.urls
|
35
|
+
|
36
|
+
@user = User.new(tweet.user)
|
37
|
+
|
38
|
+
@splitted_text = {}
|
39
|
+
|
40
|
+
expand_url!
|
41
|
+
|
42
|
+
@@instances << self
|
43
|
+
end
|
44
|
+
|
45
|
+
def update!(tweet)
|
46
|
+
@retweet_count = tweet.retweet_count
|
47
|
+
@favorite_count = tweet.favorite_count
|
48
|
+
@retweeted = tweet.retweeted?
|
49
|
+
@favorited = tweet.favorited?
|
50
|
+
self
|
51
|
+
end
|
52
|
+
|
53
|
+
def date
|
54
|
+
format = Time.now - @created_at < 86_400 ? '%H:%M:%S' : '%Y-%m-%d %H:%M:%S'
|
55
|
+
@created_at.strftime(format)
|
56
|
+
end
|
57
|
+
|
58
|
+
def expand_url!
|
59
|
+
sub = -> (x) { @text.sub!(x.url, x.display_url) }
|
60
|
+
(@media + @urls).each(&sub)
|
61
|
+
end
|
62
|
+
|
63
|
+
def favorite!
|
64
|
+
@favorited = true
|
65
|
+
end
|
66
|
+
|
67
|
+
def unfavorite!
|
68
|
+
@favorited = false
|
69
|
+
end
|
70
|
+
|
71
|
+
def retweet!
|
72
|
+
@retweeted = true
|
73
|
+
end
|
74
|
+
|
75
|
+
def split(width)
|
76
|
+
@splitted_text[:width] ||= @text.split_by_width(width)
|
77
|
+
end
|
78
|
+
|
79
|
+
def in_reply_to_status(&block)
|
80
|
+
block.call(nil) if @in_reply_to_status_id.nil?
|
81
|
+
|
82
|
+
status = Status.find_by_in_reply_to_status_id(@in_reply_to_status_id)
|
83
|
+
block.call(status) unless status.nil?
|
84
|
+
|
85
|
+
Client.current.show_status(@in_reply_to_status_id, &block)
|
86
|
+
end
|
87
|
+
|
88
|
+
def ==(other)
|
89
|
+
other.is_a?(self.class) && id == other.id
|
90
|
+
end
|
91
|
+
|
92
|
+
class << self
|
93
|
+
def find_by_in_reply_to_status_id(in_reply_to_status_id)
|
94
|
+
@@instances.find { |status| status.id == in_reply_to_status_id }
|
95
|
+
end
|
96
|
+
|
97
|
+
def parse_time(time)
|
98
|
+
(time.is_a?(String) ? Time.parse(time) : time.dup).localtime
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Twterm
|
2
|
+
module Tab
|
3
|
+
module Base
|
4
|
+
include Curses
|
5
|
+
|
6
|
+
attr_reader :title
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@window = stdscr.subwin(stdscr.maxy - 5, stdscr.maxx - 30, 3, 0)
|
10
|
+
end
|
11
|
+
|
12
|
+
def refresh
|
13
|
+
return if @refreshing || closed? || TabManager.instance.current_tab.object_id != object_id
|
14
|
+
|
15
|
+
@refreshing = true
|
16
|
+
Thread.new do
|
17
|
+
update
|
18
|
+
@refreshing = false
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def close
|
23
|
+
@window.close
|
24
|
+
end
|
25
|
+
|
26
|
+
def respond_to_key(_)
|
27
|
+
fail NotImplementedError, 'respond_to_key method must be implemented'
|
28
|
+
end
|
29
|
+
|
30
|
+
def ==(other)
|
31
|
+
self.equal?(other)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def update
|
37
|
+
fail NotImplementedError, 'update method must be implemented'
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Twterm
|
2
|
+
module Tab
|
3
|
+
class ConversationTab
|
4
|
+
include StatusesTab
|
5
|
+
|
6
|
+
attr_reader :status
|
7
|
+
|
8
|
+
def initialize(status)
|
9
|
+
fail ArgumentError, 'argument must be an instance of Status class' unless status.is_a? Status
|
10
|
+
|
11
|
+
@title = 'Conversation'
|
12
|
+
|
13
|
+
super()
|
14
|
+
prepend(status)
|
15
|
+
Thread.new { fetch_reply(status) }
|
16
|
+
end
|
17
|
+
|
18
|
+
def fetch_reply(status)
|
19
|
+
status.in_reply_to_status do |reply|
|
20
|
+
return if reply.nil?
|
21
|
+
append(reply)
|
22
|
+
fetch_reply(reply)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def ==(other)
|
27
|
+
other.is_a?(self.class) && status == other.status
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Twterm
|
2
|
+
module Tab
|
3
|
+
class ListTab
|
4
|
+
include StatusesTab
|
5
|
+
|
6
|
+
attr_reader :list
|
7
|
+
|
8
|
+
def initialize(list)
|
9
|
+
fail ArgumentError, 'argument must be an instance of List class' unless list.is_a? List
|
10
|
+
|
11
|
+
super()
|
12
|
+
|
13
|
+
@list = list
|
14
|
+
@title = @list.full_name
|
15
|
+
fetch { move_to_top }
|
16
|
+
auto_reload(300) { fetch }
|
17
|
+
end
|
18
|
+
|
19
|
+
def fetch
|
20
|
+
client = Client.current
|
21
|
+
client.list(@list) do |statuses|
|
22
|
+
statuses.reverse.each(&method(:prepend))
|
23
|
+
sort
|
24
|
+
yield if block_given?
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def ==(other)
|
29
|
+
other.is_a?(self.class) && list == other.list
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Twterm
|
2
|
+
module Tab
|
3
|
+
class MentionsTab
|
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
|
+
|
11
|
+
@client = client
|
12
|
+
@client.on_mention do |status|
|
13
|
+
prepend(status)
|
14
|
+
Notifier.instance.show_message "Mentioned by @#{status.user.screen_name}: #{status.text}"
|
15
|
+
end
|
16
|
+
|
17
|
+
@title = 'Mentions'
|
18
|
+
|
19
|
+
fetch { move_to_top }
|
20
|
+
auto_reload(300) { fetch }
|
21
|
+
end
|
22
|
+
|
23
|
+
def fetch
|
24
|
+
@client.mentions do |statuses|
|
25
|
+
statuses.reverse.each(&method(:prepend))
|
26
|
+
sort
|
27
|
+
yield if block_given?
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def close
|
32
|
+
fail NotClosableError
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module Twterm
|
2
|
+
module Tab
|
3
|
+
module New
|
4
|
+
class List
|
5
|
+
include Base
|
6
|
+
include Scrollable
|
7
|
+
|
8
|
+
@@lists = nil
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
super
|
12
|
+
|
13
|
+
@title = 'New tab'
|
14
|
+
end
|
15
|
+
|
16
|
+
def respond_to_key(key)
|
17
|
+
return true if super
|
18
|
+
|
19
|
+
case key
|
20
|
+
when 10
|
21
|
+
return true if current_list.nil?
|
22
|
+
list_tab = Tab::ListTab.new(current_list)
|
23
|
+
TabManager.instance.switch(list_tab)
|
24
|
+
else
|
25
|
+
return false
|
26
|
+
end
|
27
|
+
true
|
28
|
+
end
|
29
|
+
|
30
|
+
def ==(other)
|
31
|
+
other.is_a?(self.class)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def current_list
|
37
|
+
@@lists.nil? ? nil : @@lists[index]
|
38
|
+
end
|
39
|
+
|
40
|
+
def update
|
41
|
+
@window.clear
|
42
|
+
|
43
|
+
@window.bold do
|
44
|
+
@window.setpos(2, 3)
|
45
|
+
@window.addstr('Open list tab')
|
46
|
+
end
|
47
|
+
|
48
|
+
Thread.new do
|
49
|
+
Notifier.instance.show_message('Loading lists ...')
|
50
|
+
Client.current.lists do |lists|
|
51
|
+
@@lists = lists
|
52
|
+
show_lists
|
53
|
+
update_scrollbar_length
|
54
|
+
@window.refresh
|
55
|
+
end
|
56
|
+
end if @@lists.nil?
|
57
|
+
|
58
|
+
show_lists
|
59
|
+
draw_scroll_bar
|
60
|
+
|
61
|
+
@window.refresh
|
62
|
+
end
|
63
|
+
|
64
|
+
def show_lists
|
65
|
+
return if @@lists.nil?
|
66
|
+
|
67
|
+
@@lists.each.with_index(0) do |list, i|
|
68
|
+
@window.with_color(:black, :magenta) do
|
69
|
+
@window.setpos(i * 3 + 5, 4)
|
70
|
+
@window.addstr(' ')
|
71
|
+
@window.setpos(i * 3 + 6, 4)
|
72
|
+
@window.addstr(' ')
|
73
|
+
end if i == index
|
74
|
+
|
75
|
+
@window.setpos(i * 3 + 5, 6)
|
76
|
+
@window.addstr("#{list.full_name} (#{list.member_count} members / #{list.subscriber_count} subscribers)")
|
77
|
+
@window.setpos(i * 3 + 6, 8)
|
78
|
+
@window.addstr(list.description)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def count
|
83
|
+
@@lists.nil? ? 0 : @@lists.count
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Twterm
|
2
|
+
module Tab
|
3
|
+
module New
|
4
|
+
class Search
|
5
|
+
include Base
|
6
|
+
include Readline
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
super
|
10
|
+
|
11
|
+
@title = 'New tab'
|
12
|
+
@window.refresh
|
13
|
+
end
|
14
|
+
|
15
|
+
def respond_to_key(_)
|
16
|
+
false
|
17
|
+
end
|
18
|
+
|
19
|
+
def invoke_input
|
20
|
+
resetter = proc do
|
21
|
+
reset_prog_mode
|
22
|
+
sleep 0.1
|
23
|
+
Screen.instance.refresh
|
24
|
+
end
|
25
|
+
|
26
|
+
input_thread = Thread.new do
|
27
|
+
close_screen
|
28
|
+
puts "\ninput search query"
|
29
|
+
query = readline('input query > ').strip
|
30
|
+
resetter.call
|
31
|
+
|
32
|
+
tab = query.nil? || query.empty? ? Tab::New::Start.new : Tab::SearchTab.new(query)
|
33
|
+
TabManager.instance.switch(tab)
|
34
|
+
end
|
35
|
+
|
36
|
+
App.instance.register_interruption_handler do
|
37
|
+
input_thread.kill
|
38
|
+
resetter.call
|
39
|
+
tab = Tab::New::Start.new
|
40
|
+
TabManager.instance.switch(tab)
|
41
|
+
end
|
42
|
+
|
43
|
+
input_thread.join
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def update; end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module Twterm
|
2
|
+
module Tab
|
3
|
+
module New
|
4
|
+
class Start
|
5
|
+
include Base
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
super
|
9
|
+
@title = 'New tab'
|
10
|
+
refresh
|
11
|
+
end
|
12
|
+
|
13
|
+
def respond_to_key(key)
|
14
|
+
case key
|
15
|
+
when 'L'
|
16
|
+
tab = Tab::New::List.new
|
17
|
+
TabManager.instance.switch(tab)
|
18
|
+
when 'S'
|
19
|
+
tab = Tab::New::Search.new
|
20
|
+
TabManager.instance.switch(tab)
|
21
|
+
tab.invoke_input
|
22
|
+
else
|
23
|
+
return false
|
24
|
+
end
|
25
|
+
true
|
26
|
+
end
|
27
|
+
|
28
|
+
def ==(other)
|
29
|
+
other.is_a?(self.class)
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def update
|
35
|
+
@window.clear
|
36
|
+
|
37
|
+
@window.bold do
|
38
|
+
@window.setpos(2, 3)
|
39
|
+
@window.addstr("You've opened a new tab")
|
40
|
+
end
|
41
|
+
|
42
|
+
@window.setpos(4, 5)
|
43
|
+
@window.addstr('- [L] Open list tab')
|
44
|
+
@window.bold do
|
45
|
+
@window.setpos(4, 7)
|
46
|
+
@window.addstr('[L]')
|
47
|
+
end
|
48
|
+
|
49
|
+
@window.setpos(6, 5)
|
50
|
+
@window.addstr('- [S] Open search tab')
|
51
|
+
@window.bold do
|
52
|
+
@window.setpos(6, 7)
|
53
|
+
@window.addstr('[S]')
|
54
|
+
end
|
55
|
+
|
56
|
+
@window.setpos(9, 3)
|
57
|
+
@window.addstr('To cancel opening a new tab, just press [w] to close this tab.')
|
58
|
+
@window.bold do
|
59
|
+
@window.setpos(9, 43)
|
60
|
+
@window.addstr('[w]')
|
61
|
+
end
|
62
|
+
@window.refresh
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|