twterm 2.5.0 → 2.9.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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +62 -0
  3. data/.gitignore +1 -0
  4. data/Makefile +18 -0
  5. data/README.md +30 -4
  6. data/bin/twterm +1 -3
  7. data/default.nix +21 -0
  8. data/gemset.nix +5130 -0
  9. data/lib/twterm/app.rb +0 -18
  10. data/lib/twterm/client.rb +1 -6
  11. data/lib/twterm/color_manager.rb +8 -9
  12. data/lib/twterm/image.rb +26 -0
  13. data/lib/twterm/image/underlined.rb +31 -0
  14. data/lib/twterm/image_builder/user_name_image_builder.rb +1 -0
  15. data/lib/twterm/key_mapper.rb +6 -8
  16. data/lib/twterm/key_mapper/abstract_key_mapper.rb +2 -2
  17. data/lib/twterm/list.rb +36 -1
  18. data/lib/twterm/message_window.rb +4 -5
  19. data/lib/twterm/persistable_configuration_proxy.rb +6 -6
  20. data/lib/twterm/preferences.rb +8 -1
  21. data/lib/twterm/rest_client.rb +0 -33
  22. data/lib/twterm/screen.rb +54 -14
  23. data/lib/twterm/search_query_window.rb +4 -5
  24. data/lib/twterm/status.rb +10 -0
  25. data/lib/twterm/tab/abstract_tab.rb +33 -8
  26. data/lib/twterm/tab/new/index.rb +0 -10
  27. data/lib/twterm/tab/new/list.rb +3 -3
  28. data/lib/twterm/tab/new/search.rb +2 -2
  29. data/lib/twterm/tab/new/user.rb +2 -2
  30. data/lib/twterm/tab/preferences/control.rb +77 -0
  31. data/lib/twterm/tab/preferences/index.rb +6 -0
  32. data/lib/twterm/tab/status_tab.rb +10 -0
  33. data/lib/twterm/tab/statuses/abstract_statuses_tab.rb +4 -4
  34. data/lib/twterm/tab/statuses/home.rb +0 -3
  35. data/lib/twterm/tab/statuses/mentions.rb +1 -0
  36. data/lib/twterm/tab/user_tab.rb +0 -9
  37. data/lib/twterm/tab_manager.rb +77 -5
  38. data/lib/twterm/tweetbox.rb +2 -3
  39. data/lib/twterm/user.rb +1 -0
  40. data/lib/twterm/version.rb +1 -1
  41. data/nix/Gemfile +3 -0
  42. data/nix/Gemfile.lock +70 -0
  43. data/nix/gemset.nix +283 -0
  44. data/shell.nix +40 -0
  45. data/twterm.gemspec +13 -13
  46. metadata +42 -44
  47. data/.travis.yml +0 -12
  48. data/lib/twterm/direct_message.rb +0 -60
  49. data/lib/twterm/direct_message_composer.rb +0 -80
  50. data/lib/twterm/direct_message_manager.rb +0 -51
  51. data/lib/twterm/event/direct_message/fetched.rb +0 -10
  52. data/lib/twterm/event/notification/direct_message.rb +0 -30
  53. data/lib/twterm/event/status/timeline.rb +0 -10
  54. data/lib/twterm/repository/direct_message_repository.rb +0 -14
  55. data/lib/twterm/streaming_client.rb +0 -146
  56. data/lib/twterm/tab/direct_message/conversation.rb +0 -104
  57. data/lib/twterm/tab/direct_message/conversation_list.rb +0 -100
data/lib/twterm/screen.rb CHANGED
@@ -6,18 +6,18 @@ require 'twterm/subscriber'
6
6
  module Twterm
7
7
  class Screen
8
8
  include Subscriber
9
- include Curses
10
9
 
11
10
  def initialize(app, client)
12
11
  @app, @client = app, client
13
12
 
14
- @screen = init_screen
15
- noecho
16
- raw
17
- curs_set(0)
18
- stdscr.keypad(true)
19
- start_color
20
- use_default_colors
13
+ @screen = Curses.init_screen
14
+ Curses.noecho
15
+ Curses.raw
16
+ Curses.curs_set(0)
17
+ Curses.stdscr.keypad(true)
18
+ Curses.start_color
19
+ Curses.use_default_colors
20
+ Curses.mousemask(Curses::BUTTON1_CLICKED | 65536 | 2097152)
21
21
 
22
22
  subscribe(Event::Screen::Refresh) { refresh }
23
23
  subscribe(Event::Screen::Resize, :resize)
@@ -53,6 +53,42 @@ module Twterm
53
53
 
54
54
  attr_reader :app, :client
55
55
 
56
+ # @param [Integer, String] key
57
+ def handle_keyboard_event(key)
58
+ return if app.tab_manager.current_tab.respond_to_key(key)
59
+ return if app.tab_manager.respond_to_key(key)
60
+ respond_to_key(key)
61
+ end
62
+
63
+ # @param [Curses::MouseEvent] e
64
+ def handle_mouse_event(e)
65
+ x = e.x
66
+ y = e.y
67
+
68
+ case e.bstate
69
+ when Curses::BUTTON1_CLICKED
70
+ return app.tab_manager.handle_left_click(x, y) if app.tab_manager.enclose?(x, y)
71
+ when 65536
72
+ scroll_direction = app.preferences[:control, :scroll_direction]
73
+
74
+ case scroll_direction
75
+ when 'natural'
76
+ return app.tab_manager.handle_scroll_up(x, y) if app.tab_manager.enclose?(x, y)
77
+ when 'traditional'
78
+ return app.tab_manager.handle_scroll_down(x, y) if app.tab_manager.enclose?(x, y)
79
+ end
80
+ when 2097152
81
+ scroll_direction = app.preferences[:control, :scroll_direction]
82
+
83
+ case scroll_direction
84
+ when 'natural'
85
+ return app.tab_manager.handle_scroll_down(x, y) if app.tab_manager.enclose?(x, y)
86
+ when 'traditional'
87
+ return app.tab_manager.handle_scroll_up(x, y) if app.tab_manager.enclose?(x, y)
88
+ end
89
+ end
90
+ end
91
+
56
92
  def refresh
57
93
  app.tab_manager.refresh_window
58
94
  app.tab_manager.current_tab.render
@@ -60,10 +96,10 @@ module Twterm
60
96
  end
61
97
 
62
98
  def resize(event)
63
- return if closed?
99
+ return if Curses.closed?
64
100
 
65
101
  lines, cols = event.lines, event.cols
66
- resizeterm(lines, cols)
102
+ Curses.resizeterm(lines, cols)
67
103
  @screen.resize(lines, cols)
68
104
 
69
105
  refresh
@@ -72,11 +108,15 @@ module Twterm
72
108
  def scan
73
109
  app.reset_interruption_handler
74
110
 
75
- key = getch
111
+ key = Curses.getch
76
112
 
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)
113
+ if key == Curses::Key::MOUSE
114
+ e = Curses.getmouse
115
+
116
+ handle_mouse_event(e) unless e.nil?
117
+ else
118
+ handle_keyboard_event(key)
119
+ end
80
120
  end
81
121
  end
82
122
  end
@@ -3,7 +3,6 @@ require 'twterm/subscriber'
3
3
 
4
4
  module Twterm
5
5
  class SearchQueryWindow
6
- include Curses
7
6
  include Singleton
8
7
  include Subscriber
9
8
 
@@ -12,7 +11,7 @@ module Twterm
12
11
  attr_reader :last_query
13
12
 
14
13
  def initialize
15
- @window = stdscr.subwin(1, stdscr.maxx, stdscr.maxy - 1, 0)
14
+ @window = Curses.stdscr.subwin(1, Curses.stdscr.maxx, Curses.stdscr.maxy - 1, 0)
16
15
  @searching_down = true
17
16
  @str = ''
18
17
  @last_query = ''
@@ -29,7 +28,7 @@ module Twterm
29
28
  chars = []
30
29
 
31
30
  loop do
32
- char = getch
31
+ char = Curses.getch
33
32
 
34
33
  if char.nil?
35
34
  case chars.first
@@ -117,8 +116,8 @@ module Twterm
117
116
  attr_reader :window
118
117
 
119
118
  def resize(_event)
120
- window.resize(1, stdscr.maxx)
121
- window.move(stdscr.maxy - 1, 0)
119
+ window.resize(1, Curses.stdscr.maxx)
120
+ window.move(Curses.stdscr.maxy - 1, 0)
122
121
  end
123
122
 
124
123
  def render(str)
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
@@ -7,20 +7,31 @@ require 'twterm/subscriber'
7
7
  module Twterm
8
8
  module Tab
9
9
  class AbstractTab
10
- include Curses
11
10
  include Subscriber
12
11
 
13
- attr_reader :window, :title
12
+ # @return [String]
13
+ attr_reader :title
14
14
 
15
+ # @return [Curses::Window]
16
+ # @todo This can be (and should be) private
17
+ attr_reader :window
18
+
19
+ # @param other [Twterm::Tab::AbstractTab]
20
+ #
21
+ # @return [Boolean]
15
22
  def ==(other)
16
23
  self.equal?(other)
17
24
  end
18
25
 
26
+ # @return [void]
19
27
  def close
20
28
  unsubscribe
21
29
  window.close
22
30
  end
23
31
 
32
+ # A utility method to find a status by its ID
33
+ #
34
+ # @return [Concurrent::Promise<Twterm::Status>]
24
35
  def find_or_fetch_status(id)
25
36
  status = app.status_repository.find(id)
26
37
 
@@ -31,6 +42,9 @@ module Twterm
31
42
  end
32
43
  end
33
44
 
45
+ # A utility method to find a list by their ID
46
+ #
47
+ # @return [Concurrent::Promise<Twterm::List>]
34
48
  def find_or_fetch_list(id)
35
49
  list = app.list_repository.find(id)
36
50
 
@@ -41,6 +55,9 @@ module Twterm
41
55
  end
42
56
  end
43
57
 
58
+ # A utility method to find a user by their id
59
+ #
60
+ # @return [Concurrent::Promise<Twterm::User>]
44
61
  def find_or_fetch_user(id)
45
62
  user = app.user_repository.find(id)
46
63
 
@@ -54,7 +71,7 @@ module Twterm
54
71
  def initialize(app, client)
55
72
  @app, @client = app, client
56
73
 
57
- @window = stdscr.subwin(stdscr.maxy - 5, stdscr.maxx, 3, 0)
74
+ @window = Curses.stdscr.subwin(Curses.stdscr.maxy - 3, Curses.stdscr.maxx, 2, 0)
58
75
 
59
76
  subscribe(Event::Screen::Resize, :resize)
60
77
  end
@@ -72,7 +89,7 @@ module Twterm
72
89
  end
73
90
  end
74
91
 
75
- view.at(1, 2).render
92
+ view.render
76
93
  end if refreshable?
77
94
  end
78
95
  end
@@ -88,8 +105,13 @@ module Twterm
88
105
 
89
106
  private
90
107
 
91
- attr_reader :app, :client
108
+ # @return [Twterm::App]
109
+ attr_reader :app
110
+
111
+ # @return [Twterm::Client]
112
+ attr_reader :client
92
113
 
114
+ # @return [Twterm::Image]
93
115
  def image
94
116
  Image.string('view method is not implemented')
95
117
  end
@@ -98,19 +120,22 @@ module Twterm
98
120
  @refresh_mutex ||= Mutex.new
99
121
  end
100
122
 
123
+ # @return [Boolean]
101
124
  def refreshable?
102
125
  !(
103
126
  refresh_mutex.locked? ||
104
- closed? ||
127
+ Curses.closed? ||
105
128
  app.tab_manager.current_tab.object_id != object_id
106
129
  )
107
130
  end
108
131
 
132
+ # @return [void]
109
133
  def resize(_event)
110
- window.resize(stdscr.maxy - 5, stdscr.maxx)
111
- window.move(3, 0)
134
+ window.resize(Curses.stdscr.maxy - 3, Curses.stdscr.maxx)
135
+ window.move(2, 0)
112
136
  end
113
137
 
138
+ # @return [Twterm::View]
114
139
  def view
115
140
  View.new(window, image)
116
141
  end
@@ -1,5 +1,4 @@
1
1
  require 'twterm/tab/abstract_tab'
2
- require 'twterm/tab/direct_message/conversation_list'
3
2
  require 'twterm/tab/rate_limit_status'
4
3
  require 'twterm/tab/preferences/index'
5
4
 
@@ -19,7 +18,6 @@ module Twterm
19
18
 
20
19
  def items
21
20
  %i(
22
- direct_messages
23
21
  list_tab
24
22
  search_tab
25
23
  user_tab
@@ -60,8 +58,6 @@ module Twterm
60
58
 
61
59
  desc =
62
60
  case item
63
- when :direct_messages
64
- 'Direct messages'
65
61
  when :list_tab
66
62
  'List tab'
67
63
  when :search_tab
@@ -82,10 +78,6 @@ module Twterm
82
78
  .reduce(Image.empty, :|)
83
79
  end
84
80
 
85
- def open_direct_messages
86
- switch(Tab::DirectMessage::ConversationList.new(app, client))
87
- end
88
-
89
81
  def open_list_tab
90
82
  switch(Tab::New::List.new(app, client))
91
83
  end
@@ -114,8 +106,6 @@ module Twterm
114
106
 
115
107
  def perform_selected_action
116
108
  case current_item
117
- when :direct_messages
118
- open_direct_messages
119
109
  when :list_tab
120
110
  open_list_tab
121
111
  when :search_tab
@@ -33,10 +33,10 @@ module Twterm
33
33
  @@lists || []
34
34
  end
35
35
 
36
- def matches?(_list, query)
36
+ def matches?(list, query)
37
37
  [
38
- other.description,
39
- other.full_name,
38
+ list.description,
39
+ list.full_name,
40
40
  ].any? { |x| x.downcase.include?(query.downcase) }
41
41
  end
42
42
 
@@ -32,13 +32,13 @@ module Twterm
32
32
 
33
33
  def invoke_input
34
34
  resetter = proc do
35
- reset_prog_mode
35
+ Curses.reset_prog_mode
36
36
  sleep 0.1
37
37
  publish(Event::Screen::Refresh.new)
38
38
  end
39
39
 
40
40
  input_thread = Thread.new do
41
- close_screen
41
+ Curses.close_screen
42
42
  app.completion_manager.set_search_mode!
43
43
  puts "\ninput search query"
44
44
  query = (readline('> ', true) || '').strip
@@ -16,7 +16,7 @@ module Twterm
16
16
 
17
17
  def invoke_input
18
18
  resetter = proc do
19
- reset_prog_mode
19
+ Curses.reset_prog_mode
20
20
  sleep 0.1
21
21
  publish(Event::Screen::Refresh.new)
22
22
  end
@@ -24,7 +24,7 @@ module Twterm
24
24
  app.completion_manager.set_screen_name_mode!
25
25
 
26
26
  input_thread = Thread.new do
27
- close_screen
27
+ Curses.close_screen
28
28
  puts "\nSearch user"
29
29
  screen_name = (readline('> @', true) || '').strip
30
30
  resetter.call
@@ -0,0 +1,77 @@
1
+ require 'twterm/image'
2
+ require 'twterm/preferences'
3
+ require 'twterm/publisher'
4
+ require 'twterm/tab/abstract_tab'
5
+ require 'twterm/tab/scrollable'
6
+
7
+ module Twterm
8
+ module Tab
9
+ module Preferences
10
+ class Control < AbstractTab
11
+ include Scrollable
12
+ include Publisher
13
+
14
+ def drawable_item_count
15
+ 1
16
+ end
17
+
18
+ def image
19
+ drawable_items.map.with_index do |item, i|
20
+ curr = scroller.current_index?(i)
21
+ cursor = Image.cursor(2, curr)
22
+ options = Image.toggle_switch(['traditional', 'natural'], app.preferences[:control, item])
23
+ desc =
24
+ case item
25
+ when :scroll_direction
26
+ header = Image.string('Scroll direction')
27
+ body = Image.string(' ') - options
28
+ cursor - Image.whitespace - (header | body)
29
+ end
30
+ end
31
+ .intersperse(Image.blank_line)
32
+ .reduce(Image.empty) { |acc, x| acc | x }
33
+ end
34
+
35
+ def items
36
+ [
37
+ :scroll_direction,
38
+ ]
39
+ end
40
+
41
+ def respond_to_key(key)
42
+ return true if scroller.respond_to_key(key)
43
+
44
+ case key
45
+ when 10
46
+ perform_selected_action
47
+ end
48
+
49
+ false
50
+ end
51
+
52
+ def title
53
+ 'Control preferences'
54
+ end
55
+
56
+ private
57
+
58
+ def perform_selected_action
59
+ item = scroller.current_item
60
+
61
+ case item
62
+ when :scroll_direction
63
+ app.preferences[:control, :scroll_direction] =
64
+ case app.preferences[:control, :scroll_direction]
65
+ when 'natural'
66
+ 'traditional'
67
+ when 'traditional'
68
+ 'natural'
69
+ end
70
+ end
71
+
72
+ render
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end