twterm 1.0.9 → 1.0.10

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.
data/lib/twterm/list.rb CHANGED
@@ -4,6 +4,10 @@ module Twterm
4
4
 
5
5
  @@instances = {}
6
6
 
7
+ def ==(other)
8
+ other.is_a?(self.class) && id == other.id
9
+ end
10
+
7
11
  def initialize(list)
8
12
  @id = list.id
9
13
  update!(list)
@@ -24,13 +28,8 @@ module Twterm
24
28
  self
25
29
  end
26
30
 
27
- def ==(other)
28
- other.is_a?(self.class) && id == other.id
29
- end
30
-
31
- def self.new(list)
32
- instance = find(list.id)
33
- instance.nil? ? super : instance.update!(list)
31
+ def self.all
32
+ @@instances.values
34
33
  end
35
34
 
36
35
  def self.find(id)
@@ -44,8 +43,9 @@ module Twterm
44
43
  Client.current.list(id) { |list| yield list }
45
44
  end
46
45
 
47
- def self.all
48
- @@instances.values
46
+ def self.new(list)
47
+ instance = find(list.id)
48
+ instance.nil? ? super : instance.update!(list)
49
49
  end
50
50
  end
51
51
  end
@@ -4,9 +4,8 @@ module Twterm
4
4
  include Curses
5
5
 
6
6
  def initialize
7
- @window = stdscr.subwin(2, stdscr.maxx, stdscr.maxy - 2, 0)
7
+ @window = stdscr.subwin(1, stdscr.maxx, stdscr.maxy - 2, 0)
8
8
  @queue = Queue.new
9
- @help = ''
10
9
 
11
10
  Thread.new do
12
11
  while notification = @queue.pop
@@ -17,23 +16,16 @@ module Twterm
17
16
  end
18
17
  end
19
18
 
20
- def show_message(message)
21
- notification = Notification::Message.new(message)
22
- @queue.push(notification)
23
- self
24
- end
25
-
26
19
  def show_error(message)
27
20
  notification = Notification::Error.new(message)
28
21
  @queue.push(notification)
29
22
  self
30
23
  end
31
24
 
32
- def show_help(message)
33
- return if @help == message
34
-
35
- @help = message
36
- show
25
+ def show_message(message)
26
+ notification = Notification::Message.new(message)
27
+ @queue.push(notification)
28
+ self
37
29
  end
38
30
 
39
31
  def show(notification = nil)
@@ -46,22 +38,15 @@ module Twterm
46
38
 
47
39
  if notification.is_a? Notification::Base
48
40
  @window.with_color(notification.fg_color, notification.bg_color) do
49
- @window.setpos(1, 0)
41
+ @window.setpos(0, 0)
50
42
  @window.addstr(' ' * @window.maxx)
51
- @window.setpos(1, 1)
43
+ @window.setpos(0, 1)
52
44
  time = notification.time.strftime('[%H:%M:%S]')
53
45
  message = notification.show_with_width(@window.maxx)
54
46
  @window.addstr("#{time} #{message}")
55
47
  end
56
48
  end
57
49
 
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
50
  @window.refresh
66
51
  end
67
52
  end
@@ -7,10 +7,7 @@ class Scheduler
7
7
  @paused = false
8
8
 
9
9
  @thread = Thread.new do
10
- loop do
11
- sleep @interval
12
- run
13
- end
10
+ loop { sleep(@interval) && run }
14
11
  end
15
12
  end
16
13
 
@@ -22,11 +19,11 @@ class Scheduler
22
19
  @paused = true
23
20
  end
24
21
 
25
- def run
26
- @block.call unless @paused
22
+ def resume
23
+ @paused = false
27
24
  end
28
25
 
29
- def unpause
30
- @paused = false
26
+ def run
27
+ @block.call unless @paused
31
28
  end
32
29
  end
data/lib/twterm/screen.rb CHANGED
@@ -13,15 +13,6 @@ module Twterm
13
13
  use_default_colors
14
14
  end
15
15
 
16
- def wait
17
- @thread = Thread.new do
18
- loop do
19
- scan
20
- end
21
- end
22
- @thread.join
23
- end
24
-
25
16
  def refresh
26
17
  TabManager.instance.refresh_window
27
18
  TabManager.instance.current_tab.refresh
@@ -29,6 +20,30 @@ module Twterm
29
20
  Notifier.instance.show
30
21
  end
31
22
 
23
+ def respond_to_key(key)
24
+ case key
25
+ when ?n
26
+ Tweetbox.instance.compose
27
+ return
28
+ when ?Q
29
+ App.instance.quit
30
+ when ??
31
+ tab = Tab::KeyAssignmentsCheatsheet.new
32
+ TabManager.instance.add_and_show tab
33
+ else
34
+ return false
35
+ end
36
+
37
+ true
38
+ end
39
+
40
+ def wait
41
+ @thread = Thread.new do
42
+ loop { scan }
43
+ end
44
+ @thread.join
45
+ end
46
+
32
47
  private
33
48
 
34
49
  def scan
@@ -38,17 +53,7 @@ module Twterm
38
53
 
39
54
  return if TabManager.instance.current_tab.respond_to_key(key)
40
55
  return if TabManager.instance.respond_to_key(key)
41
-
42
- case key
43
- when 'n'
44
- Tweetbox.instance.compose
45
- return
46
- when 'Q'
47
- App.instance.quit
48
- when '/'
49
- # filter
50
- else
51
- end
56
+ respond_to_key(key)
52
57
  end
53
58
  end
54
59
  end
data/lib/twterm/status.rb CHANGED
@@ -2,21 +2,57 @@ module Twterm
2
2
  class Status
3
3
  MAX_CACHED_TIME = 3600
4
4
 
5
- attr_reader :id, :text, :created_at, :created_at_for_sort, :retweet_count, :favorite_count, :in_reply_to_status_id, :favorited, :retweeted, :user_id, :retweeted_by_user_id, :urls, :media, :touched_at
5
+ attr_reader :appeared_at, :created_at, :favorite_count, :favorited, :id,
6
+ :in_reply_to_status_id, :media, :retweet_count, :retweeted,
7
+ :retweeted_by_user_id, :text, :touched_at, :urls, :user_id
6
8
  alias_method :favorited?, :favorited
7
9
  alias_method :retweeted?, :retweeted
8
10
 
9
11
  @@instances = {}
10
12
 
11
- def self.new(tweet)
12
- instance = find(tweet.id)
13
- instance.nil? ? super : instance.update!(tweet)
13
+ def ==(other)
14
+ other.is_a?(self.class) && id == other.id
15
+ end
16
+
17
+ def date
18
+ format = Time.now - @created_at < 86_400 ? '%H:%M:%S' : '%Y-%m-%d %H:%M:%S'
19
+ @created_at.strftime(format)
20
+ end
21
+
22
+ def expand_url!
23
+ sub = -> (x) { @text.sub!(x.url, x.display_url) }
24
+ (@media + @urls).each(&sub)
25
+ end
26
+
27
+ def favorite!
28
+ @favorite_count += 1
29
+ @favorited = true
30
+ end
31
+
32
+ def grepped_with?(query)
33
+ [text, user.screen_name, user.name]
34
+ .any? { |x| x.downcase.include?(query.downcase) }
35
+ end
36
+
37
+ def in_reply_to_status(&block)
38
+ if @in_reply_to_status_id.nil?
39
+ block.call(nil)
40
+ return
41
+ end
42
+
43
+ status = Status.find(@in_reply_to_status_id)
44
+ unless status.nil?
45
+ block.call(status)
46
+ return
47
+ end
48
+
49
+ Client.current.show_status(@in_reply_to_status_id, &block)
14
50
  end
15
51
 
16
52
  def initialize(tweet)
17
53
  unless tweet.retweeted_status.is_a? Twitter::NullObject
18
54
  @retweeted_by_user_id = tweet.user.id
19
- User.create(tweet.user)
55
+ User.new(tweet.user)
20
56
  retweeted_at = Status.parse_time(tweet.created_at)
21
57
  tweet = tweet.retweeted_status
22
58
  end
@@ -24,7 +60,7 @@ module Twterm
24
60
  @id = tweet.id
25
61
  @text = CGI.unescapeHTML(tweet.full_text.dup)
26
62
  @created_at = Status.parse_time(tweet.created_at)
27
- @created_at_for_sort = retweeted_at || @created_at
63
+ @appeared_at = retweeted_at || @created_at
28
64
  @retweet_count = tweet.retweet_count
29
65
  @favorite_count = tweet.favorite_count
30
66
  @in_reply_to_status_id = tweet.in_reply_to_status_id
@@ -36,7 +72,7 @@ module Twterm
36
72
  @urls = tweet.urls
37
73
 
38
74
  @user_id = tweet.user.id
39
- User.create(tweet.user)
75
+ User.new(tweet.user)
40
76
 
41
77
  @splitted_text = {}
42
78
 
@@ -51,32 +87,8 @@ module Twterm
51
87
  @@instances[id] = self
52
88
  end
53
89
 
54
- def update!(tweet)
55
- @retweet_count = tweet.retweet_count
56
- @favorite_count = tweet.favorite_count
57
- @retweeted = tweet.retweeted?
58
- @favorited = tweet.favorited?
59
- self
60
- end
61
-
62
- def date
63
- format = Time.now - @created_at < 86_400 ? '%H:%M:%S' : '%Y-%m-%d %H:%M:%S'
64
- @created_at.strftime(format)
65
- end
66
-
67
- def expand_url!
68
- sub = -> (x) { @text.sub!(x.url, x.display_url) }
69
- (@media + @urls).each(&sub)
70
- end
71
-
72
- def favorite!
73
- @favorite_count += 1
74
- @favorited = true
75
- end
76
-
77
- def unfavorite!
78
- @favorite_count -= 1
79
- @favorited = false
90
+ def replies
91
+ Status.all.select { |s| s.in_reply_to_status_id == id }
80
92
  end
81
93
 
82
94
  def retweet!
@@ -84,74 +96,67 @@ module Twterm
84
96
  @retweeted = true
85
97
  end
86
98
 
87
- def split(width)
88
- @splitted_text[:width] ||= @text.split_by_width(width)
99
+ def retweeted_by
100
+ User.find(@retweeted_by_user_id)
89
101
  end
90
102
 
91
- def in_reply_to_status(&block)
92
- if @in_reply_to_status_id.nil?
93
- block.call(nil)
94
- return
95
- end
96
-
97
- status = Status.find(@in_reply_to_status_id)
98
- unless status.nil?
99
- block.call(status)
100
- return
101
- end
102
-
103
- Client.current.show_status(@in_reply_to_status_id, &block)
103
+ def split(width)
104
+ @splitted_text[:width] ||= @text.split_by_width(width)
104
105
  end
105
106
 
106
- def replies
107
- Status.all.select { |s| s.in_reply_to_status_id == id }
107
+ def touch!
108
+ @touched_at = Time.now
108
109
  end
109
110
 
110
- def retweeted_by
111
- User.find(@retweeted_by_user_id)
111
+ def unfavorite!
112
+ @favorite_count -= 1
113
+ @favorited = false
112
114
  end
113
115
 
114
- def touch!
115
- @touched_at = Time.now
116
+ def update!(tweet)
117
+ @retweet_count = tweet.retweet_count
118
+ @favorite_count = tweet.favorite_count
119
+ @retweeted = tweet.retweeted?
120
+ @favorited = tweet.favorited?
121
+ self
116
122
  end
117
123
 
118
124
  def user
119
125
  User.find(user_id)
120
126
  end
121
127
 
122
- def ==(other)
123
- other.is_a?(self.class) && id == other.id
128
+ def self.all
129
+ @@instances.values
124
130
  end
125
131
 
126
- class << self
127
- def all
128
- @@instances.values
132
+ def self.cleanup
133
+ TabManager.instance.each_tab do |tab|
134
+ tab.touch_statuses if tab.is_a?(Tab::StatusesTab)
129
135
  end
136
+ cond = -> (status) { status.touched_at > Time.now - MAX_CACHED_TIME }
137
+ statuses = all.select(&cond)
138
+ status_ids = statuses.map(&:id)
139
+ @@instances = status_ids.zip(statuses).to_h
140
+ end
130
141
 
131
- def find(id)
132
- @@instances[id]
133
- end
142
+ def self.find(id)
143
+ @@instances[id]
144
+ end
134
145
 
135
- def find_or_fetch(id)
136
- instance = find(id)
137
- (yield(instance) && return) if instance
146
+ def self.find_or_fetch(id)
147
+ instance = find(id)
148
+ (yield(instance) && return) if instance
138
149
 
139
- Client.current.show_status(id) { |status| yield status }
140
- end
150
+ Client.current.show_status(id) { |status| yield status }
151
+ end
141
152
 
142
- def parse_time(time)
143
- (time.is_a?(String) ? Time.parse(time) : time.dup).localtime
144
- end
153
+ def self.new(tweet)
154
+ instance = find(tweet.id)
155
+ instance.nil? ? super : instance.update!(tweet)
156
+ end
145
157
 
146
- def cleanup
147
- TabManager.instance.each_tab do |tab|
148
- tab.touch_statuses if tab.is_a?(Tab::StatusesTab)
149
- end
150
- cond = -> (status) { status.touched_at > Time.now - MAX_CACHED_TIME }
151
- statuses = all.select(&cond)
152
- status_ids = statuses.map(&:id)
153
- @@instances = status_ids.zip(statuses).to_h
154
- end
158
+ def self.parse_time(time)
159
+ (time.is_a?(String) ? Time.parse(time) : time.dup).localtime
155
160
  end
156
161
  end
157
162
  end
@@ -3,35 +3,50 @@ module Twterm
3
3
  module Base
4
4
  include Curses
5
5
 
6
+ attr_reader :window
6
7
  attr_accessor :title
7
8
 
9
+ def ==(other)
10
+ self.equal?(other)
11
+ end
12
+
13
+ def close
14
+ window.close
15
+ end
16
+
8
17
  def initialize
9
18
  @window = stdscr.subwin(stdscr.maxy - 5, stdscr.maxx - 30, 3, 0)
10
19
  end
11
20
 
12
21
  def refresh
13
- return if @refreshing || closed? || TabManager.instance.current_tab.object_id != object_id
22
+ return unless refreshable?
14
23
 
15
- @refreshing = true
16
24
  Thread.new do
17
- update
18
- @refreshing = false
25
+ refresh_mutex.synchronize do
26
+ window.clear
27
+ update
28
+ window.refresh
29
+ end
19
30
  end
20
31
  end
21
32
 
22
- def close
23
- @window.close
24
- end
25
-
26
33
  def respond_to_key(_)
27
34
  fail NotImplementedError, 'respond_to_key method must be implemented'
28
35
  end
29
36
 
30
- def ==(other)
31
- self.equal?(other)
37
+ private
38
+
39
+ def refresh_mutex
40
+ @refresh_mutex ||= Mutex.new
32
41
  end
33
42
 
34
- private
43
+ def refreshable?
44
+ !(
45
+ refresh_mutex.locked? ||
46
+ closed? ||
47
+ TabManager.instance.current_tab.object_id != object_id
48
+ )
49
+ end
35
50
 
36
51
  def update
37
52
  fail NotImplementedError, 'update method must be implemented'
@@ -6,18 +6,8 @@ module Twterm
6
6
 
7
7
  attr_reader :status
8
8
 
9
- def initialize(status_id)
10
- @title = 'Conversation'
11
- super()
12
-
13
- Status.find_or_fetch(status_id) do |status|
14
- @status = status
15
-
16
- append(status)
17
- move_to_top
18
- Thread.new { fetch_in_reply_to_status(status) }
19
- Thread.new { fetch_replies(status) }
20
- end
9
+ def ==(other)
10
+ other.is_a?(self.class) && status == other.status
21
11
  end
22
12
 
23
13
  def fetch_in_reply_to_status(status)
@@ -37,13 +27,23 @@ module Twterm
37
27
  end
38
28
  end
39
29
 
40
- def ==(other)
41
- other.is_a?(self.class) && status == other.status
42
- end
43
-
44
30
  def dump
45
31
  @status.id
46
32
  end
33
+
34
+ def initialize(status_id)
35
+ @title = 'Conversation'
36
+ super()
37
+
38
+ Status.find_or_fetch(status_id) do |status|
39
+ @status = status
40
+
41
+ append(status)
42
+ scroll_manager.move_to_top
43
+ Thread.new { fetch_in_reply_to_status(status) }
44
+ Thread.new { fetch_replies(status) }
45
+ end
46
+ end
47
47
  end
48
48
  end
49
49
  end
@@ -0,0 +1,113 @@
1
+ module Twterm
2
+ module Tab
3
+ class KeyAssignmentsCheatsheet
4
+ include Base
5
+
6
+ def ==(other)
7
+ other.is_a?(self.class)
8
+ end
9
+
10
+ SHORTCUTS = {
11
+ 'General' => {
12
+ '[d] [C-d]' => 'Scroll down',
13
+ '[g]' => 'Move to top',
14
+ '[G]' => 'Move to bottom',
15
+ '[j] [C-p] [DOWN]' => 'Move down',
16
+ '[k] [C-n] [UP]' => 'Move up',
17
+ '[u] [C-u]' => 'Scroll up',
18
+ '[Q]' => 'Quit twterm',
19
+ '[?]' => 'Open key assignments cheatsheet'
20
+ },
21
+ 'Tabs' => {
22
+ '[h] [C-b] [LEFT]' => 'Show previous tab',
23
+ '[l] [C-f] [RIGHT]' => 'Show next tab',
24
+ '[N]' => 'Open new tab',
25
+ '[C-R]' => 'Reload',
26
+ '[w]' => 'Close tab',
27
+ '[q]' => 'Quit filtering mode',
28
+ '[/]' => 'Filter items in tab'
29
+ },
30
+ 'Tweets' => {
31
+ '[D]' => 'Delete tweet',
32
+ '[F]' => 'Add to favorite',
33
+ '[n]' => 'Compose new tweet',
34
+ '[o]' => 'Open URLs in tweet',
35
+ '[r]' => 'Reply',
36
+ '[R]' => 'Retweet',
37
+ '[U]' => 'Show user'
38
+ }
39
+ }
40
+
41
+ def respond_to_key(key)
42
+ case key
43
+ when ?d, 4
44
+ 10.times { scroll_manager.move_down }
45
+ when ?g
46
+ scroll_manager.move_to_top
47
+ when ?G
48
+ scroll_manager.move_to_bottom
49
+ when ?j, 14, Curses::Key::DOWN
50
+ scroll_manager.move_down
51
+ when ?k, 16, Curses::Key::UP
52
+ scroll_manager.move_up
53
+ when ?u, 21
54
+ 10.times { scroll_manager.move_up }
55
+ else
56
+ return false
57
+ end
58
+
59
+ true
60
+ end
61
+
62
+ def title
63
+ 'Key assignments'.freeze
64
+ end
65
+
66
+ def update
67
+ top = 2 # begin drawing from line 2
68
+ draw_cond = -> line { top <= line && line <= window.maxy - top }
69
+
70
+ current_line = top - scroll_manager.offset
71
+
72
+ window.setpos(current_line, 3)
73
+ window.bold { window.addstr('Key assignments') } if draw_cond[current_line]
74
+
75
+ SHORTCUTS.each do |category, shortcuts|
76
+ current_line += 3
77
+ window.setpos(current_line, 5)
78
+ window.bold { window.addstr("<#{category}>") } if draw_cond[current_line]
79
+ current_line += 1
80
+
81
+ shortcuts.each do |key, description|
82
+ current_line += 1
83
+ next unless draw_cond[current_line]
84
+
85
+ window.setpos(current_line, 7)
86
+ window.bold { window.addstr(key.rjust(17)) }
87
+ window.setpos(current_line, 25)
88
+ window.addstr(": #{description}")
89
+ end
90
+ end
91
+ end
92
+
93
+ private
94
+
95
+ def count
96
+ @count ||= SHORTCUTS.count * 4 + SHORTCUTS.map(&:count).reduce(0, :+) + 1
97
+ end
98
+
99
+ def offset_from_bottom
100
+ 0
101
+ end
102
+
103
+ def scroll_manager
104
+ return @scroll_manager unless @scroll_manager.nil?
105
+
106
+ @scroll_manager = ScrollManager.new
107
+ @scroll_manager.delegate = self
108
+ @scroll_manager.after_move { refresh }
109
+ @scroll_manager
110
+ end
111
+ end
112
+ end
113
+ end
@@ -13,7 +13,7 @@ module Twterm
13
13
  @list = list
14
14
  @title = @list.full_name
15
15
  TabManager.instance.refresh_window
16
- fetch { move_to_top }
16
+ fetch { scroll_manager.move_to_top }
17
17
  @auto_reloader = Scheduler.new(300) { fetch }
18
18
  end
19
19
  end
@@ -3,6 +3,18 @@ module Twterm
3
3
  class MentionsTab
4
4
  include StatusesTab
5
5
 
6
+ def close
7
+ fail NotClosableError
8
+ end
9
+
10
+ def fetch
11
+ @client.mentions do |statuses|
12
+ statuses.reverse.each(&method(:prepend))
13
+ sort
14
+ yield if block_given?
15
+ end
16
+ end
17
+
6
18
  def initialize(client)
7
19
  fail ArgumentError, 'argument must be an instance of Client class' unless client.is_a? Client
8
20
 
@@ -16,21 +28,9 @@ module Twterm
16
28
 
17
29
  @title = 'Mentions'
18
30
 
19
- fetch { move_to_top }
31
+ fetch { scroll_manager.move_to_top }
20
32
  @auto_reloader = Scheduler.new(300) { fetch }
21
33
  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
34
  end
35
35
  end
36
36
  end