twterm 2.7.0 → 2.10.2
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/.gitattributes +4 -0
- data/.gitignore +1 -0
- data/Gemfile +7 -0
- data/Makefile +21 -0
- data/README.md +52 -6
- data/bin/twterm +1 -3
- data/default.nix +21 -0
- data/gemset.nix +5130 -0
- data/lib/twterm/app.rb +13 -8
- data/lib/twterm/color_manager.rb +8 -9
- data/lib/twterm/image.rb +31 -0
- data/lib/twterm/image/attr.rb +42 -0
- data/lib/twterm/image/bold.rb +7 -21
- data/lib/twterm/image/color.rb +9 -15
- data/lib/twterm/image/dim.rb +13 -0
- data/lib/twterm/image/underlined.rb +17 -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 -13
- data/lib/twterm/persistable_configuration_proxy.rb +6 -6
- data/lib/twterm/preferences.rb +8 -1
- data/lib/twterm/screen.rb +103 -25
- data/lib/twterm/search_query_window.rb +5 -13
- data/lib/twterm/status.rb +10 -0
- data/lib/twterm/tab/abstract_tab.rb +30 -16
- 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/scrollable.rb +1 -1
- data/lib/twterm/tab/searchable.rb +6 -4
- data/lib/twterm/tab/status_tab.rb +10 -0
- data/lib/twterm/tab/statuses/abstract_statuses_tab.rb +11 -6
- data/lib/twterm/tab_manager.rb +77 -10
- 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 +77 -0
- data/nix/gemset.nix +325 -0
- data/shell.nix +40 -0
- data/spec/twterm/image/bold_spec.rb +30 -0
- data/spec/twterm/image/color_spec.rb +16 -0
- data/spec/twterm/image/dim_spec.rb +30 -0
- data/twterm.gemspec +7 -14
- metadata +35 -108
- data/.travis.yml +0 -12
- data/lib/twterm/event/screen/resize.rb +0 -13
- data/spec/twterm/event/screen/resize_spec.rb +0 -11
@@ -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 =
|
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
|
39
|
+
rescue TomlRB::ParseError, TomlRB::ValueOverwriteError => e
|
40
40
|
msg =
|
41
41
|
case e
|
42
|
-
when
|
42
|
+
when TomlRB::ParseError
|
43
43
|
"Your configuration file could not be parsed"
|
44
|
-
when
|
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 =
|
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
|
data/lib/twterm/preferences.rb
CHANGED
@@ -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,
|
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
|
-
|
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
|
-
@
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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 - 2, 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
|
-
|
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
|
-
|
57
|
-
|
58
|
-
app.tab_manager.current_tab.
|
59
|
-
|
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
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
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
|
-
|
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
|
-
|
78
|
-
|
79
|
-
|
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
|
-
|
15
|
-
|
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
|
-
|
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.
|
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
|
-
|
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
|
-
|
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
|
@@ -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
|