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.
- checksums.yaml +4 -4
- data/.circleci/config.yml +62 -0
- data/.gitignore +1 -0
- data/Makefile +18 -0
- data/README.md +30 -4
- data/bin/twterm +1 -3
- data/default.nix +21 -0
- data/gemset.nix +5130 -0
- data/lib/twterm/app.rb +0 -18
- data/lib/twterm/client.rb +1 -6
- data/lib/twterm/color_manager.rb +8 -9
- data/lib/twterm/image.rb +26 -0
- data/lib/twterm/image/underlined.rb +31 -0
- data/lib/twterm/image_builder/user_name_image_builder.rb +1 -0
- data/lib/twterm/key_mapper.rb +6 -8
- data/lib/twterm/key_mapper/abstract_key_mapper.rb +2 -2
- data/lib/twterm/list.rb +36 -1
- data/lib/twterm/message_window.rb +4 -5
- data/lib/twterm/persistable_configuration_proxy.rb +6 -6
- data/lib/twterm/preferences.rb +8 -1
- data/lib/twterm/rest_client.rb +0 -33
- data/lib/twterm/screen.rb +54 -14
- data/lib/twterm/search_query_window.rb +4 -5
- data/lib/twterm/status.rb +10 -0
- data/lib/twterm/tab/abstract_tab.rb +33 -8
- data/lib/twterm/tab/new/index.rb +0 -10
- data/lib/twterm/tab/new/list.rb +3 -3
- data/lib/twterm/tab/new/search.rb +2 -2
- data/lib/twterm/tab/new/user.rb +2 -2
- data/lib/twterm/tab/preferences/control.rb +77 -0
- data/lib/twterm/tab/preferences/index.rb +6 -0
- data/lib/twterm/tab/status_tab.rb +10 -0
- data/lib/twterm/tab/statuses/abstract_statuses_tab.rb +4 -4
- data/lib/twterm/tab/statuses/home.rb +0 -3
- data/lib/twterm/tab/statuses/mentions.rb +1 -0
- data/lib/twterm/tab/user_tab.rb +0 -9
- data/lib/twterm/tab_manager.rb +77 -5
- data/lib/twterm/tweetbox.rb +2 -3
- data/lib/twterm/user.rb +1 -0
- data/lib/twterm/version.rb +1 -1
- data/nix/Gemfile +3 -0
- data/nix/Gemfile.lock +70 -0
- data/nix/gemset.nix +283 -0
- data/shell.nix +40 -0
- data/twterm.gemspec +13 -13
- metadata +42 -44
- data/.travis.yml +0 -12
- data/lib/twterm/direct_message.rb +0 -60
- data/lib/twterm/direct_message_composer.rb +0 -80
- data/lib/twterm/direct_message_manager.rb +0 -51
- data/lib/twterm/event/direct_message/fetched.rb +0 -10
- data/lib/twterm/event/notification/direct_message.rb +0 -30
- data/lib/twterm/event/status/timeline.rb +0 -10
- data/lib/twterm/repository/direct_message_repository.rb +0 -14
- data/lib/twterm/streaming_client.rb +0 -146
- data/lib/twterm/tab/direct_message/conversation.rb +0 -104
- 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
|
-
|
78
|
-
|
79
|
-
|
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
|
-
|
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 -
|
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.
|
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
|
-
|
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 -
|
111
|
-
window.move(
|
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
|
data/lib/twterm/tab/new/index.rb
CHANGED
@@ -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
|
data/lib/twterm/tab/new/list.rb
CHANGED
@@ -33,10 +33,10 @@ module Twterm
|
|
33
33
|
@@lists || []
|
34
34
|
end
|
35
35
|
|
36
|
-
def matches?(
|
36
|
+
def matches?(list, query)
|
37
37
|
[
|
38
|
-
|
39
|
-
|
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
|
data/lib/twterm/tab/new/user.rb
CHANGED
@@ -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
|