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,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,14 @@
1
+ module Twterm
2
+ module Tab
3
+ module AutoReloadable
4
+ def auto_reload(period, &block)
5
+ Thread.new do
6
+ loop do
7
+ sleep period
8
+ block.call
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
14
+ 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,6 @@
1
+ module Twterm
2
+ module Tab
3
+ class NotClosableError < StandardError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,7 @@
1
+ module Twterm
2
+ module Tab
3
+ class Favorites < Base
4
+
5
+ end
6
+ end
7
+ 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