twterm 2.6.0 → 2.10.1

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