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