twterm 2.6.0 → 2.10.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +62 -0
  3. data/.gitignore +1 -0
  4. data/Gemfile +7 -0
  5. data/Makefile +21 -0
  6. data/README.md +52 -6
  7. data/bin/twterm +1 -3
  8. data/default.nix +21 -0
  9. data/gemset.nix +5130 -0
  10. data/lib/twterm/app.rb +13 -24
  11. data/lib/twterm/client.rb +1 -4
  12. data/lib/twterm/color_manager.rb +8 -9
  13. data/lib/twterm/image.rb +31 -0
  14. data/lib/twterm/image/attr.rb +42 -0
  15. data/lib/twterm/image/bold.rb +7 -21
  16. data/lib/twterm/image/color.rb +9 -15
  17. data/lib/twterm/image/dim.rb +13 -0
  18. data/lib/twterm/image/underlined.rb +17 -0
  19. data/lib/twterm/image_builder/user_name_image_builder.rb +1 -0
  20. data/lib/twterm/key_mapper.rb +6 -8
  21. data/lib/twterm/key_mapper/abstract_key_mapper.rb +2 -2
  22. data/lib/twterm/list.rb +36 -1
  23. data/lib/twterm/message_window.rb +4 -13
  24. data/lib/twterm/persistable_configuration_proxy.rb +6 -6
  25. data/lib/twterm/preferences.rb +8 -1
  26. data/lib/twterm/rest_client.rb +0 -33
  27. data/lib/twterm/screen.rb +103 -25
  28. data/lib/twterm/search_query_window.rb +5 -13
  29. data/lib/twterm/status.rb +10 -0
  30. data/lib/twterm/tab/abstract_tab.rb +30 -16
  31. data/lib/twterm/tab/new/index.rb +0 -10
  32. data/lib/twterm/tab/new/search.rb +2 -2
  33. data/lib/twterm/tab/new/user.rb +2 -2
  34. data/lib/twterm/tab/preferences/control.rb +77 -0
  35. data/lib/twterm/tab/preferences/index.rb +6 -0
  36. data/lib/twterm/tab/scrollable.rb +1 -1
  37. data/lib/twterm/tab/searchable.rb +6 -4
  38. data/lib/twterm/tab/status_tab.rb +10 -0
  39. data/lib/twterm/tab/statuses/abstract_statuses_tab.rb +11 -6
  40. data/lib/twterm/tab/user_tab.rb +0 -9
  41. data/lib/twterm/tab_manager.rb +77 -10
  42. data/lib/twterm/tweetbox.rb +2 -3
  43. data/lib/twterm/user.rb +1 -0
  44. data/lib/twterm/version.rb +1 -1
  45. data/nix/Gemfile +3 -0
  46. data/nix/Gemfile.lock +77 -0
  47. data/nix/gemset.nix +325 -0
  48. data/shell.nix +40 -0
  49. data/spec/twterm/image/bold_spec.rb +30 -0
  50. data/spec/twterm/image/color_spec.rb +16 -0
  51. data/spec/twterm/image/dim_spec.rb +30 -0
  52. data/twterm.gemspec +7 -14
  53. metadata +31 -113
  54. data/.travis.yml +0 -12
  55. data/lib/twterm/direct_message.rb +0 -60
  56. data/lib/twterm/direct_message_composer.rb +0 -80
  57. data/lib/twterm/direct_message_manager.rb +0 -51
  58. data/lib/twterm/event/direct_message/fetched.rb +0 -10
  59. data/lib/twterm/event/notification/direct_message.rb +0 -30
  60. data/lib/twterm/event/screen/resize.rb +0 -13
  61. data/lib/twterm/repository/direct_message_repository.rb +0 -14
  62. data/lib/twterm/tab/direct_message/conversation.rb +0 -104
  63. data/lib/twterm/tab/direct_message/conversation_list.rb +0 -100
  64. data/spec/twterm/event/screen/resize_spec.rb +0 -11
@@ -1,23 +1,19 @@
1
1
  require 'twterm/subscriber'
2
2
  require 'twterm/event/message/abstract_message'
3
- require 'twterm/event/screen/resize'
4
3
 
5
4
  module Twterm
6
5
  class MessageWindow
7
- include Singleton
8
- include Curses
9
6
  include Subscriber
10
7
 
11
- def initialize
12
- @window = stdscr.subwin(1, stdscr.maxx, stdscr.maxy - 2, 0)
8
+ # @param window [Curses::Window]
9
+ def initialize(window)
10
+ @window = window
13
11
  @queue = Queue.new
14
12
 
15
13
  subscribe(Event::Message::AbstractMessage) do |e|
16
14
  queue(e)
17
15
  end
18
16
 
19
- subscribe(Event::Screen::Resize, :resize)
20
-
21
17
  Thread.new do
22
18
  while message = @queue.pop # rubocop:disable Lint/AssignmentInCondition:
23
19
  show(message)
@@ -34,7 +30,7 @@ module Twterm
34
30
 
35
31
  def show(message = nil)
36
32
  loop do
37
- break unless closed?
33
+ break unless Curses.closed?
38
34
  sleep 0.5
39
35
  end
40
36
 
@@ -72,10 +68,5 @@ module Twterm
72
68
  @queue.push(message)
73
69
  self
74
70
  end
75
-
76
- def resize(_event)
77
- @window.resize(1, stdscr.maxx)
78
- @window.move(stdscr.maxy - 2, 0)
79
- end
80
71
  end
81
72
  end
@@ -1,4 +1,4 @@
1
- require 'toml'
1
+ require 'toml-rb'
2
2
 
3
3
  module Twterm
4
4
  class PersistableConfigurationProxy
@@ -32,16 +32,16 @@ module Twterm
32
32
  # @param [String] filepath File path to load configuration from
33
33
  # @return [Twterm::PersistableConfigurationProxy] a configuration proxy
34
34
  def self.load_from_file!(klass, filepath)
35
- config = TOML.load_file(filepath, symbolize_keys: true)
35
+ config = TomlRB.load_file(filepath, symbolize_keys: true)
36
36
  new(klass.new(config), filepath).migrate!
37
37
  rescue Errno::ENOENT
38
38
  new(klass.default, filepath)
39
- rescue TOML::ParseError, TOML::ValueOverwriteError => e
39
+ rescue TomlRB::ParseError, TomlRB::ValueOverwriteError => e
40
40
  msg =
41
41
  case e
42
- when TOML::ParseError
42
+ when TomlRB::ParseError
43
43
  "Your configuration file could not be parsed"
44
- when TOML::ValueOverwriteError
44
+ when TomlRB::ValueOverwriteError
45
45
  "`#{e.key}` is declared more than once"
46
46
  end
47
47
 
@@ -75,7 +75,7 @@ Press any key to continue
75
75
  attr_reader :filepath, :instance
76
76
 
77
77
  def persist!
78
- hash = TOML.dump(instance.to_h).gsub("\n[", "\n\n[")
78
+ hash = TomlRB.dump(instance.to_h).gsub("\n[", "\n\n[")
79
79
  File.open(filepath, 'w', 0644) { |f| f.write(hash) }
80
80
  end
81
81
  end
@@ -1,4 +1,4 @@
1
- require 'toml'
1
+ require 'toml-rb'
2
2
 
3
3
  require 'twterm/abstract_persistable_configuration'
4
4
 
@@ -27,6 +27,9 @@ module Twterm
27
27
  # @return [Twterm::Preferences] an instance having the default value
28
28
  def self.default
29
29
  new({
30
+ control: {
31
+ scroll_direction: 'traditional',
32
+ },
30
33
  photo_viewer_backend: {
31
34
  browser: true,
32
35
  imgcat: false,
@@ -48,8 +51,12 @@ module Twterm
48
51
  # @return [Hash]
49
52
  def self.structure
50
53
  bool = -> x { x == true || x == false }
54
+ scroll_direction = -> x { x == 'natural' || x == 'traditional' }
51
55
 
52
56
  {
57
+ control: {
58
+ scroll_direction: scroll_direction
59
+ },
53
60
  photo_viewer_backend: {
54
61
  browser: bool,
55
62
  imgcat: bool,
@@ -1,6 +1,4 @@
1
1
  require 'concurrent'
2
- require 'twterm/direct_message'
3
- require 'twterm/direct_message_manager'
4
2
  require 'twterm/publisher'
5
3
  require 'twterm/event/message/success'
6
4
 
@@ -25,33 +23,6 @@ module Twterm
25
23
  end
26
24
  end
27
25
 
28
- def create_direct_message(recipient, text)
29
- send_request do
30
- rest_client.create_direct_message(recipient.id, text)
31
- end.then do |message|
32
- msg = direct_message_repository.create(message)
33
- direct_message_manager.add(recipient.id, msg)
34
- publish(Event::DirectMessage::Fetched.new)
35
- publish(Event::Message::Success.new('Your message to @%s has been sent' % recipient.screen_name))
36
- end
37
- end
38
-
39
- def direct_message_conversations
40
- direct_message_manager.conversations
41
- end
42
-
43
- def direct_messages_received
44
- send_request do
45
- rest_client.direct_messages(count: 200).map { |dm| direct_message_repository.create(dm) }
46
- end
47
- end
48
-
49
- def direct_messages_sent
50
- send_request do
51
- rest_client.direct_messages_sent(count: 200).map { |dm| direct_message_repository.create(dm) }
52
- end
53
- end
54
-
55
26
  def destroy_status(status)
56
27
  send_request_without_catch do
57
28
  rest_client.destroy_status(status.id)
@@ -424,10 +395,6 @@ module Twterm
424
395
 
425
396
  private
426
397
 
427
- def direct_message_manager
428
- @direct_message_manager ||= DirectMessageManager.new(self)
429
- end
430
-
431
398
  def show_error
432
399
  proc do |e|
433
400
  case e
data/lib/twterm/screen.rb CHANGED
@@ -1,26 +1,74 @@
1
1
  require 'twterm/event/screen/refresh'
2
- require 'twterm/event/screen/resize'
3
2
  require 'twterm/key_mapper'
4
3
  require 'twterm/subscriber'
5
4
 
6
5
  module Twterm
7
6
  class Screen
8
7
  include Subscriber
9
- include Curses
8
+
9
+ # @todo Make private
10
+ # @return [Curses::Window]
11
+ attr_reader :tab_manager_window
12
+
13
+ # @todo Make private
14
+ # @return [Curses::Window]
15
+ attr_reader :tab_window
16
+
17
+ # @todo Make private
18
+ # @return [Curses::Window]
19
+ attr_reader :message_window_window
20
+
21
+ # @todo Make private
22
+ # @return [Curses::Window]
23
+ attr_reader :search_query_window_window
10
24
 
11
25
  def initialize(app, client)
12
26
  @app, @client = app, client
13
27
 
14
- @screen = init_screen
15
- noecho
16
- raw
17
- curs_set(0)
18
- stdscr.keypad(true)
19
- start_color
20
- use_default_colors
28
+ @stdscr = Curses.init_screen
29
+
30
+ width = @stdscr.maxx
31
+ height = @stdscr.maxy
32
+
33
+ @tab_manager_window = @stdscr.subwin(1, width, 0, 0)
34
+ @tab_window = @stdscr.subwin(height - 3, width, 2, 0)
35
+ @message_window_window = @stdscr.subwin(1, width, height - 1, 0)
36
+ @search_query_window_window = @stdscr.subwin(1, width, height - 1, 0)
37
+
38
+ Curses.noecho
39
+ Curses.raw
40
+ Curses.curs_set(0)
41
+ Curses.stdscr.keypad(true)
42
+ Curses.start_color
43
+ Curses.use_default_colors
44
+ Curses.mousemask(Curses::BUTTON1_CLICKED | 65536 | 2097152)
21
45
 
22
46
  subscribe(Event::Screen::Refresh) { refresh }
23
- subscribe(Event::Screen::Resize, :resize)
47
+ end
48
+
49
+ def resize(lines, cols)
50
+ return if Curses.closed?
51
+
52
+ Curses.resizeterm(lines, cols)
53
+ @stdscr.resize(lines, cols)
54
+
55
+ tab_manager_window.move(0, 0)
56
+ tab_manager_window.resize(1, cols)
57
+ tab_manager_window.refresh
58
+
59
+ tab_window.move(2, 0)
60
+ tab_window.resize(lines - 3, cols)
61
+ tab_window.refresh
62
+
63
+ message_window_window.move(cols - 1, 0)
64
+ message_window_window.resize(1, cols)
65
+ message_window_window.refresh
66
+
67
+ search_query_window_window.move(cols - 1, 0)
68
+ search_query_window_window.resize(1, cols)
69
+ search_query_window_window.refresh
70
+
71
+ refresh
24
72
  end
25
73
 
26
74
  def respond_to_key(key)
@@ -53,30 +101,60 @@ module Twterm
53
101
 
54
102
  attr_reader :app, :client
55
103
 
56
- def refresh
57
- app.tab_manager.refresh_window
58
- app.tab_manager.current_tab.render
59
- MessageWindow.instance.show
104
+ # @param [Integer, String] key
105
+ def handle_keyboard_event(key)
106
+ return if app.tab_manager.current_tab.respond_to_key(key)
107
+ return if app.tab_manager.respond_to_key(key)
108
+ respond_to_key(key)
60
109
  end
61
110
 
62
- def resize(event)
63
- return if closed?
64
-
65
- lines, cols = event.lines, event.cols
66
- resizeterm(lines, cols)
67
- @screen.resize(lines, cols)
111
+ # @param [Curses::MouseEvent] e
112
+ def handle_mouse_event(e)
113
+ x = e.x
114
+ y = e.y
115
+
116
+ case e.bstate
117
+ when Curses::BUTTON1_CLICKED
118
+ return app.tab_manager.handle_left_click(x, y) if app.tab_manager.enclose?(x, y)
119
+ when 65536
120
+ scroll_direction = app.preferences[:control, :scroll_direction]
121
+
122
+ case scroll_direction
123
+ when 'natural'
124
+ return app.tab_manager.handle_scroll_up(x, y) if app.tab_manager.enclose?(x, y)
125
+ when 'traditional'
126
+ return app.tab_manager.handle_scroll_down(x, y) if app.tab_manager.enclose?(x, y)
127
+ end
128
+ when 2097152
129
+ scroll_direction = app.preferences[:control, :scroll_direction]
130
+
131
+ case scroll_direction
132
+ when 'natural'
133
+ return app.tab_manager.handle_scroll_down(x, y) if app.tab_manager.enclose?(x, y)
134
+ when 'traditional'
135
+ return app.tab_manager.handle_scroll_up(x, y) if app.tab_manager.enclose?(x, y)
136
+ end
137
+ end
138
+ end
68
139
 
69
- refresh
140
+ def refresh
141
+ app.tab_manager.refresh_window
142
+ app.tab_manager.current_tab.render
143
+ app.message_window.show
70
144
  end
71
145
 
72
146
  def scan
73
147
  app.reset_interruption_handler
74
148
 
75
- key = getch
149
+ key = Curses.getch
76
150
 
77
- return if app.tab_manager.current_tab.respond_to_key(key)
78
- return if app.tab_manager.respond_to_key(key)
79
- respond_to_key(key)
151
+ if key == Curses::Key::MOUSE
152
+ e = Curses.getmouse
153
+
154
+ handle_mouse_event(e) unless e.nil?
155
+ else
156
+ handle_keyboard_event(key)
157
+ end
80
158
  end
81
159
  end
82
160
  end
@@ -1,23 +1,19 @@
1
- require 'twterm/event/screen/resize'
2
1
  require 'twterm/subscriber'
3
2
 
4
3
  module Twterm
5
4
  class SearchQueryWindow
6
- include Curses
7
- include Singleton
8
5
  include Subscriber
9
6
 
10
7
  class CancelInput < StandardError; end
11
8
 
12
9
  attr_reader :last_query
13
10
 
14
- def initialize
15
- @window = stdscr.subwin(1, stdscr.maxx, stdscr.maxy - 1, 0)
11
+ # @param window [Curses::Window]
12
+ def initialize(window)
13
+ @window = window
16
14
  @searching_down = true
17
15
  @str = ''
18
16
  @last_query = ''
19
-
20
- subscribe(Event::Screen::Resize, :resize)
21
17
  end
22
18
 
23
19
  def input
@@ -29,7 +25,7 @@ module Twterm
29
25
  chars = []
30
26
 
31
27
  loop do
32
- char = getch
28
+ char = Curses.getch
33
29
 
34
30
  if char.nil?
35
31
  case chars.first
@@ -114,13 +110,9 @@ module Twterm
114
110
 
115
111
  private
116
112
 
113
+ # @return [Curses::Window]
117
114
  attr_reader :window
118
115
 
119
- def resize(_event)
120
- window.resize(1, stdscr.maxx)
121
- window.move(stdscr.maxy - 1, 0)
122
- end
123
-
124
116
  def render(str)
125
117
  window.clear
126
118
  window.setpos(0, 0)
data/lib/twterm/status.rb CHANGED
@@ -12,6 +12,7 @@ class Twitter::Tweet
12
12
  end
13
13
 
14
14
  module Twterm
15
+ # A tweet
15
16
  class Status
16
17
  attr_reader :created_at, :favorite_count, :favorited, :hashtags, :id,
17
18
  :in_reply_to_status_id, :media, :retweet_count, :retweeted,
@@ -23,11 +24,13 @@ module Twterm
23
24
  other.is_a?(self.class) && id == other.id
24
25
  end
25
26
 
27
+ # @todo This should be done in a presenter
26
28
  def date
27
29
  format = Time.now - @created_at < 86_400 ? '%H:%M:%S' : '%Y-%m-%d %H:%M:%S'
28
30
  @created_at.strftime(format)
29
31
  end
30
32
 
33
+ # @todo This can be marked as private
31
34
  def expand_url!
32
35
  sub = -> (x) { @text.sub!(x.url, x.display_url) }
33
36
  (@media + @urls).each(&sub)
@@ -69,10 +72,16 @@ module Twterm
69
72
  expand_url!
70
73
  end
71
74
 
75
+ # Is this status a quote?
76
+ #
77
+ # @return [Boolean]
72
78
  def quote?
73
79
  !quoted_status_id.nil?
74
80
  end
75
81
 
82
+ # Is this status a retweet?
83
+ #
84
+ # @return [Boolean]
76
85
  def retweet?
77
86
  !retweeted_status_id.nil?
78
87
  end
@@ -93,6 +102,7 @@ module Twterm
93
102
  @retweeted = false
94
103
  end
95
104
 
105
+ # @return [self]
96
106
  def update!(tweet, is_retweeted_status = false)
97
107
  @retweet_count = tweet.retweet_count
98
108
  @favorite_count = tweet.favorite_count
@@ -1,26 +1,31 @@
1
1
  require 'concurrent'
2
2
 
3
- require 'twterm/event/screen/resize'
4
3
  require 'twterm/image'
5
4
  require 'twterm/subscriber'
6
5
 
7
6
  module Twterm
8
7
  module Tab
9
8
  class AbstractTab
10
- include Curses
11
9
  include Subscriber
12
10
 
13
- attr_reader :window, :title
11
+ # @return [String]
12
+ attr_reader :title
14
13
 
14
+ # @param other [Twterm::Tab::AbstractTab]
15
+ #
16
+ # @return [Boolean]
15
17
  def ==(other)
16
18
  self.equal?(other)
17
19
  end
18
20
 
21
+ # @return [void]
19
22
  def close
20
23
  unsubscribe
21
- window.close
22
24
  end
23
25
 
26
+ # A utility method to find a status by its ID
27
+ #
28
+ # @return [Concurrent::Promise<Twterm::Status>]
24
29
  def find_or_fetch_status(id)
25
30
  status = app.status_repository.find(id)
26
31
 
@@ -31,6 +36,9 @@ module Twterm
31
36
  end
32
37
  end
33
38
 
39
+ # A utility method to find a list by their ID
40
+ #
41
+ # @return [Concurrent::Promise<Twterm::List>]
34
42
  def find_or_fetch_list(id)
35
43
  list = app.list_repository.find(id)
36
44
 
@@ -41,6 +49,9 @@ module Twterm
41
49
  end
42
50
  end
43
51
 
52
+ # A utility method to find a user by their id
53
+ #
54
+ # @return [Concurrent::Promise<Twterm::User>]
44
55
  def find_or_fetch_user(id)
45
56
  user = app.user_repository.find(id)
46
57
 
@@ -53,10 +64,6 @@ module Twterm
53
64
 
54
65
  def initialize(app, client)
55
66
  @app, @client = app, client
56
-
57
- @window = stdscr.subwin(stdscr.maxy - 5, stdscr.maxx, 3, 0)
58
-
59
- subscribe(Event::Screen::Resize, :resize)
60
67
  end
61
68
 
62
69
  def render
@@ -72,7 +79,7 @@ module Twterm
72
79
  end
73
80
  end
74
81
 
75
- view.at(1, 2).render
82
+ view.render
76
83
  end if refreshable?
77
84
  end
78
85
  end
@@ -88,8 +95,13 @@ module Twterm
88
95
 
89
96
  private
90
97
 
91
- attr_reader :app, :client
98
+ # @return [Twterm::App]
99
+ attr_reader :app
92
100
 
101
+ # @return [Twterm::Client]
102
+ attr_reader :client
103
+
104
+ # @return [Twterm::Image]
93
105
  def image
94
106
  Image.string('view method is not implemented')
95
107
  end
@@ -98,22 +110,24 @@ module Twterm
98
110
  @refresh_mutex ||= Mutex.new
99
111
  end
100
112
 
113
+ # @return [Boolean]
101
114
  def refreshable?
102
115
  !(
103
116
  refresh_mutex.locked? ||
104
- closed? ||
117
+ Curses.closed? ||
105
118
  app.tab_manager.current_tab.object_id != object_id
106
119
  )
107
120
  end
108
121
 
109
- def resize(_event)
110
- window.resize(stdscr.maxy - 5, stdscr.maxx)
111
- window.move(3, 0)
112
- end
113
-
122
+ # @return [Twterm::View]
114
123
  def view
115
124
  View.new(window, image)
116
125
  end
126
+
127
+ # @todo This method is for transition. `window` should explicitly be obtained on initialization.
128
+ def window
129
+ app.screen.tab_window
130
+ end
117
131
  end
118
132
  end
119
133
  end