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.
- 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
|