ektoplayer 0.1.3 → 0.1.5
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/README.md +5 -0
- data/lib/ektoplayer/application.rb +9 -5
- data/lib/ektoplayer/bindings.rb +21 -7
- data/lib/ektoplayer/common.rb +39 -0
- data/lib/ektoplayer/compat.rb +1 -1
- data/lib/ektoplayer/config.rb +26 -24
- data/lib/ektoplayer/operations/playlist.rb +5 -3
- data/lib/ektoplayer/theme.rb +3 -2
- data/lib/ektoplayer/trackloader.rb +30 -17
- data/lib/ektoplayer/ui/widgets.rb +32 -13
- data/lib/ektoplayer/ui.rb +26 -38
- data/lib/ektoplayer/views/help.rb +1 -1
- data/lib/ektoplayer/views/info.rb +3 -3
- data/lib/ektoplayer/views/mainwindow.rb +6 -6
- data/lib/ektoplayer/views/playinginfo.rb +23 -33
- data/lib/ektoplayer/views/progressbar.rb +3 -4
- data/lib/ektoplayer/views/volumemeter.rb +5 -3
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6d45cf3f25e3f52e0d259b0396903d2bee9ae811
|
4
|
+
data.tar.gz: cee36a784279f4b0698adadfa8fb99df16b979be
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 82df89a2a491c4171a7f91f0d46d42e630d1ab1f942785a02ae570d13ecd9ac6f993eddfe720555c0b8bb256810c4d43a96bd2a6da5e24721b5123209e454bab
|
7
|
+
data.tar.gz: a553baadb74b75f7053a7dca15bb2305887482ff9596d04fb70acc68b4b22415655480d70a2554da70995c4caeafb0ed0cde08f19acc58093c54af627f7d933c
|
data/README.md
CHANGED
@@ -14,6 +14,7 @@ It allows you to
|
|
14
14
|
* Vi-like keybindings (`hjkl`, `^d`, `^u`, `/`, `?`, `n`, `N`, ...)
|
15
15
|
* Up to 256 colors are supported
|
16
16
|
* Local sound file cache
|
17
|
+
* Song prefetching
|
17
18
|
|
18
19
|
## Screenshots
|
19
20
|
|
@@ -44,6 +45,10 @@ library to compile the native extensions.
|
|
44
45
|
apt-get install ruby ruby-dev portaudio19-dev libmpg123-dev sqlite3 libsqlite3-dev libncurses-dev libz1g-dev build-essential
|
45
46
|
gem install ektoplayer
|
46
47
|
|
48
|
+
## Configuration
|
49
|
+
|
50
|
+
Have a look at the default [ektoplayer.rc](https://github.com/braph/ektoplayer/blob/master/doc/ektoplayer.rc).
|
51
|
+
|
47
52
|
## Authors
|
48
53
|
|
49
54
|
* [Benjamin Abendroth](https://github.com/braph)
|
@@ -10,7 +10,7 @@ require 'date'
|
|
10
10
|
|
11
11
|
module Ektoplayer
|
12
12
|
class Application
|
13
|
-
VERSION = '0.1.
|
13
|
+
VERSION = '0.1.5'.freeze
|
14
14
|
GITHUB_URL = 'https://github.com/braph/ektoplayer'.freeze
|
15
15
|
EKTOPLAZM_URL = 'http://www.ektoplazm.com'.freeze
|
16
16
|
|
@@ -35,7 +35,7 @@ module Ektoplayer
|
|
35
35
|
|
36
36
|
def run
|
37
37
|
#Thread.abort_on_exception=(true)
|
38
|
-
Thread.report_on_exception=(true)
|
38
|
+
Thread.report_on_exception=(true) if Thread.public_method_defined? :report_on_exception
|
39
39
|
|
40
40
|
# make each configuration object globally accessible as a singleton
|
41
41
|
[Config, Bindings, Theme].each { |c| Common::mksingleton(c) }
|
@@ -121,14 +121,18 @@ module Ektoplayer
|
|
121
121
|
|
122
122
|
if Config[:prefetch]
|
123
123
|
trackloader_mutex = Mutex.new
|
124
|
+
@prefetch_thread = nil
|
124
125
|
player.events.on(:position_change) do
|
125
|
-
Thread.new do
|
126
|
-
if player.length > 30 and player.position_percent > 0.
|
126
|
+
@prefetch_thread ||= Thread.new do
|
127
|
+
if player.length > 30 and player.position_percent > 0.7
|
127
128
|
trackloader_mutex.synchronize do
|
128
129
|
trackloader.get_track_file(playlist[playlist.get_next_pos]['url'])
|
129
|
-
sleep
|
130
|
+
sleep 30
|
130
131
|
end
|
131
132
|
end
|
133
|
+
|
134
|
+
sleep 10
|
135
|
+
@prefetch_thread = nil
|
132
136
|
end
|
133
137
|
end
|
134
138
|
end
|
data/lib/ektoplayer/bindings.rb
CHANGED
@@ -135,7 +135,7 @@ module Ektoplayer
|
|
135
135
|
# browser
|
136
136
|
:'browser.add_to_playlist' => [' ', ?a ],
|
137
137
|
:'browser.enter' => [?E, Curses::KEY_ENTER ],
|
138
|
-
:'browser.back' => [?
|
138
|
+
:'browser.back' => [?B, Curses::KEY_BACKSPACE ]},
|
139
139
|
help: {
|
140
140
|
:'help.top' => [?g, Curses::KEY_HOME ],
|
141
141
|
:'help.bottom' => [?G, Curses::KEY_END ],
|
@@ -155,7 +155,7 @@ module Ektoplayer
|
|
155
155
|
|
156
156
|
@bindings.default_proc = proc { |h,k| fail "Unknown widget #{k}" }
|
157
157
|
@bindings.each do |widget, hash|
|
158
|
-
hash.default_proc = proc { |h,k| h[k] =
|
158
|
+
hash.default_proc = proc { |h,k| h[k] = [] }
|
159
159
|
hash.values.each do |keys|
|
160
160
|
keys.map! { |key| parse_key(key) }
|
161
161
|
end
|
@@ -181,6 +181,8 @@ module Ektoplayer
|
|
181
181
|
key.to_sym
|
182
182
|
elsif key.size == 2 and key.start_with?(?^)
|
183
183
|
Curses.const_get("KEY_CTRL_#{key[1].upcase}")
|
184
|
+
elsif key =~ /^(key_)?space$/i
|
185
|
+
:' '
|
184
186
|
else
|
185
187
|
key = key.upcase.tr(?-, ?_)
|
186
188
|
key = "KEY_#{key}" unless key.start_with?('KEY_')
|
@@ -192,9 +194,9 @@ module Ektoplayer
|
|
192
194
|
|
193
195
|
def bind(widget, key, command)
|
194
196
|
widget, command = widget.to_sym, command.to_sym
|
195
|
-
fail "Unknown command #{command}" unless
|
197
|
+
fail "Unknown command #{command}" unless @commands.include? command
|
196
198
|
|
197
|
-
@bindings[widget][command].delete parse_key(key)
|
199
|
+
@bindings[widget][command].delete parse_key(key) rescue nil
|
198
200
|
@bindings[widget][command] << parse_key(key)
|
199
201
|
check_collisions
|
200
202
|
end
|
@@ -205,6 +207,12 @@ module Ektoplayer
|
|
205
207
|
end
|
206
208
|
end
|
207
209
|
|
210
|
+
def unbind_all
|
211
|
+
@bindings.each do |widget, commands|
|
212
|
+
commands.clear
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
208
216
|
def bind_view(section, view, view_operations, operations)
|
209
217
|
@bindings[section.to_sym].each do |command, keys|
|
210
218
|
keys.each do |key|
|
@@ -217,14 +225,20 @@ module Ektoplayer
|
|
217
225
|
private def check_collisions
|
218
226
|
global_keys = @bindings[:global].values.flatten
|
219
227
|
global_keys.each do |k|
|
220
|
-
fail "Double binding in 'global'
|
228
|
+
fail "Double binding in 'global', key #{keyname(k)}" if global_keys.count(k) > 1
|
221
229
|
end
|
222
230
|
|
223
231
|
@bindings.each_pair do |widget, commands|
|
232
|
+
next if widget == :global
|
224
233
|
widget_keys = commands.values.flatten
|
225
234
|
widget_keys.each do |k|
|
226
|
-
|
227
|
-
|
235
|
+
if widget_keys.count(k) > 1
|
236
|
+
fail "Double binding in '#{widget}', key `#{keyname(k)}`"
|
237
|
+
end
|
238
|
+
|
239
|
+
if global_keys.include? k
|
240
|
+
fail "Double binding in 'global <> #{widget}', key #{keyname(k)}"
|
241
|
+
end
|
228
242
|
end
|
229
243
|
end
|
230
244
|
end
|
data/lib/ektoplayer/common.rb
CHANGED
@@ -1,4 +1,43 @@
|
|
1
1
|
require 'zip'
|
2
|
+
require 'thread'
|
3
|
+
|
4
|
+
class ConditionSignals
|
5
|
+
def initialize
|
6
|
+
@mutex, @cond = Mutex.new, ConditionVariable.new
|
7
|
+
@curr_signal = nil
|
8
|
+
@signal_hooks = {}
|
9
|
+
|
10
|
+
Thread.new do
|
11
|
+
@mutex.synchronize do
|
12
|
+
loop do
|
13
|
+
@cond.wait(@mutex)
|
14
|
+
|
15
|
+
if @signal_hooks.key? @curr_signal
|
16
|
+
@signal_hooks[@curr_signal].()
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def wait(name, timeout=nil)
|
24
|
+
@mutex.synchronize do
|
25
|
+
loop do
|
26
|
+
@cond.wait(@mutex, timeout)
|
27
|
+
return if @curr_signal == name
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def signal(name)
|
33
|
+
@curr_signal = name
|
34
|
+
@cond.broadcast
|
35
|
+
end
|
36
|
+
|
37
|
+
def on(name, &block)
|
38
|
+
@signal_hooks[name] = block
|
39
|
+
end
|
40
|
+
end
|
2
41
|
|
3
42
|
class Object
|
4
43
|
alias :frz :freeze
|
data/lib/ektoplayer/compat.rb
CHANGED
data/lib/ektoplayer/config.rb
CHANGED
@@ -52,20 +52,20 @@ module Ektoplayer
|
|
52
52
|
|
53
53
|
DEFAULT_PLAYLIST_FORMAT = (
|
54
54
|
'<number size="3" fg="magenta" />' +
|
55
|
-
'<artist rel="
|
56
|
-
'<album rel="
|
55
|
+
'<artist rel="25" fg="blue" />' +
|
56
|
+
'<album rel="30" fg="red" />' +
|
57
57
|
'<title rel="33" fg="yellow" />' +
|
58
58
|
'<styles rel="20" fg="cyan" />' +
|
59
59
|
'<bpm size="4" fg="green" justify="right" />').freeze
|
60
60
|
|
61
61
|
DEFAULT_PLAYINGINFO_FORMAT1 =
|
62
|
-
'<text fg="black"
|
62
|
+
'<text fg="black"><< </text><title bold="on" fg="yellow" /><text fg="black"> >></text>'.freeze
|
63
63
|
|
64
64
|
DEFAULT_PLAYINGINFO_FORMAT2 =
|
65
65
|
'<artist bold="on" fg="blue" /><text> - </text><album bold="on" fg="red" /><text> (</text><date fg="cyan" /><text>)</text>'.freeze
|
66
66
|
|
67
67
|
def register(key, description, default, method=nil)
|
68
|
-
|
68
|
+
# parameter `description` is used by tools/mkconfig.rb, but not here
|
69
69
|
|
70
70
|
if method
|
71
71
|
@cast[key.to_sym] = method if method
|
@@ -78,23 +78,24 @@ module Ektoplayer
|
|
78
78
|
|
79
79
|
def initialize
|
80
80
|
@options = Hash.new { |h,k| fail "Unknown option #{k}" }
|
81
|
-
@
|
81
|
+
@cast = {}
|
82
82
|
|
83
|
-
reg :database_file, '
|
83
|
+
reg :database_file, 'Database file for storing ektoplazm metadata',
|
84
84
|
File.join(CONFIG_DIR, 'meta.db'),
|
85
85
|
File.method(:expand_path)
|
86
86
|
|
87
|
-
reg :log_file, '
|
87
|
+
reg :log_file, 'File used for logging',
|
88
88
|
File.join(CONFIG_DIR, 'ektoplayer.log'),
|
89
89
|
File.method(:expand_path)
|
90
90
|
|
91
|
-
reg :temp_dir, %{
|
92
|
-
|
91
|
+
reg :temp_dir, %{Temporary dir for downloading mp3 files. They will be moved to `cache_dir`
|
92
|
+
after the download completed and was successful.
|
93
|
+
Directory will be created if it does not exist, parent directories will not be created.},
|
93
94
|
'/tmp/.ektoplazm',
|
94
95
|
File.method(:expand_path)
|
95
96
|
|
96
97
|
reg :cache_dir,
|
97
|
-
'Directory for storing mp3 files',
|
98
|
+
'Directory for storing cached mp3 files',
|
98
99
|
File.join(Dir.home, '.cache', 'ektoplayer'),
|
99
100
|
File.method(:expand_path)
|
100
101
|
|
@@ -109,18 +110,21 @@ module Ektoplayer
|
|
109
110
|
|
110
111
|
reg :auto_extract_to_archive_dir,
|
111
112
|
%{Enable/disable automatic extraction of downloaded MP3
|
112
|
-
archives from download_dir to archive_dir}, true
|
113
|
+
archives from `download_dir' to `archive_dir'}, true
|
113
114
|
|
114
115
|
reg :delete_after_extraction,
|
115
|
-
%{In combination with auto_extract_to_archive_dir:
|
116
|
+
%{In combination `with auto_extract_to_archive_dir':
|
116
117
|
Delete zip archive after successful extraction}, true
|
117
118
|
|
118
119
|
reg :playlist_load_newest,
|
119
120
|
%{How many tracks from database should be added to
|
120
|
-
the playlist on application start}, 100
|
121
|
+
the playlist on application start.}, 100
|
121
122
|
|
122
123
|
reg :use_cache,
|
123
|
-
|
124
|
+
%{Enable/disable local mp3 cache.
|
125
|
+
If this option is disabled, the downloaded mp3 files won't be moved
|
126
|
+
from `cache_dir`. Instead they will reside in `temp_dir` and will
|
127
|
+
be deleted on application exit.}, true
|
124
128
|
|
125
129
|
reg :prefetch,
|
126
130
|
'Enable prefetching next track do be played', true
|
@@ -149,24 +153,21 @@ module Ektoplayer
|
|
149
153
|
reg 'progressbar.display',
|
150
154
|
'Enable/disable progressbar', true
|
151
155
|
|
152
|
-
reg 'progressbar.download_char',
|
153
|
-
'Character used for displaying download progress', ?-
|
154
|
-
|
155
156
|
reg 'progressbar.progress_char',
|
156
|
-
'Character used for displaying playing progress',
|
157
|
+
'Character used for displaying playing progress', ?~
|
157
158
|
|
158
159
|
reg 'progressbar.rest_char',
|
159
|
-
'Character used for the rest of the line',
|
160
|
+
'Character used for the rest of the line', ?~
|
160
161
|
|
161
162
|
# - Volumemeter
|
162
163
|
reg 'volumemeter.display',
|
163
164
|
'Enable/disable volumemeter', true
|
164
165
|
|
165
166
|
reg 'volumemeter.level_char',
|
166
|
-
'Character used for displaying volume level',
|
167
|
+
'Character used for displaying volume level', ?~
|
167
168
|
|
168
169
|
reg 'volumemeter.rest_char',
|
169
|
-
'Character used for the rest of the line',
|
170
|
+
'Character used for the rest of the line', ?~
|
170
171
|
|
171
172
|
# - Playinginfo
|
172
173
|
reg 'playinginfo.display',
|
@@ -225,11 +226,12 @@ module Ektoplayer
|
|
225
226
|
set: self.method(:set),
|
226
227
|
bind: bindings.method(:bind),
|
227
228
|
unbind: bindings.method(:unbind),
|
229
|
+
unbind_all: bindings.method(:unbind_all),
|
228
230
|
color: theme.method(:color),
|
229
231
|
color_256: theme.method(:color_256),
|
230
232
|
color_mono: theme.method(:color_mono)
|
231
233
|
}
|
232
|
-
callbacks.default_proc = proc {
|
234
|
+
callbacks.default_proc = proc { fail 'unknown command' }
|
233
235
|
callbacks.freeze
|
234
236
|
|
235
237
|
open(file, ?r).readlines.each do |line|
|
@@ -239,10 +241,10 @@ module Ektoplayer
|
|
239
241
|
|
240
242
|
begin
|
241
243
|
cb = callbacks[command.to_sym]
|
242
|
-
fail "missing arguments for #{command}" if args.size != cb.arity
|
243
244
|
cb.call(*args)
|
245
|
+
#fail "Command '#{command}' given args: #{args.size}, wanted #{cb.arity}" if args.size != cb.arity
|
244
246
|
rescue
|
245
|
-
fail "#{file}:#{$.}: #{$!}"
|
247
|
+
fail "#{file}:#{$.}: #{command}: #{$!}"
|
246
248
|
end
|
247
249
|
end
|
248
250
|
end
|
@@ -19,7 +19,7 @@ module Ektoplayer
|
|
19
19
|
@trackloader.download_album(track['url']) rescue (
|
20
20
|
Application.log(self, $!)
|
21
21
|
)
|
22
|
-
end
|
22
|
+
end.join(0.3) # prevent too many hits
|
23
23
|
end
|
24
24
|
|
25
25
|
def reload(index)
|
@@ -28,13 +28,15 @@ module Ektoplayer
|
|
28
28
|
@trackloader.get_track_file(track['url'], reload: true) rescue (
|
29
29
|
Application.log(self, $!)
|
30
30
|
)
|
31
|
-
end
|
31
|
+
end.join(0.3) # prevent too many hits
|
32
32
|
end
|
33
33
|
|
34
34
|
def play(index)
|
35
35
|
return unless track = @playlist[index]
|
36
36
|
@playlist.current_playing=(index)
|
37
|
-
|
37
|
+
Thread.new do
|
38
|
+
@player.play(@trackloader.get_track_file(track['url']))
|
39
|
+
end.join(0.3) # prevent too many hits
|
38
40
|
end
|
39
41
|
|
40
42
|
def play_next
|
data/lib/ektoplayer/theme.rb
CHANGED
@@ -2,7 +2,7 @@ require_relative 'ui/colors'
|
|
2
2
|
|
3
3
|
module Ektoplayer
|
4
4
|
class Theme
|
5
|
-
attr_reader :current
|
5
|
+
attr_reader :current, :theme
|
6
6
|
|
7
7
|
def initialize
|
8
8
|
@current = 0
|
@@ -71,6 +71,7 @@ module Ektoplayer
|
|
71
71
|
end
|
72
72
|
|
73
73
|
def color(name, *defs, theme: 8)
|
74
|
+
defs.map! { |d| Integer(d) rescue d.to_sym }
|
74
75
|
@theme[theme][name.to_sym] = defs.freeze
|
75
76
|
end
|
76
77
|
|
@@ -81,7 +82,7 @@ module Ektoplayer
|
|
81
82
|
def [](theme_def) UI::Colors.get(theme_def) end
|
82
83
|
|
83
84
|
def use_colors(colors)
|
84
|
-
fail
|
85
|
+
fail 'unknown theme' unless @theme[colors]
|
85
86
|
@current = colors
|
86
87
|
|
87
88
|
UI::Colors.reset
|
@@ -114,31 +114,44 @@ module Ektoplayer
|
|
114
114
|
@file = File.open(filename, ?w)
|
115
115
|
@progress = 0
|
116
116
|
@error = nil
|
117
|
+
@tries = 3
|
117
118
|
end
|
118
119
|
|
119
120
|
def start!
|
120
|
-
Application.log(self, 'starting download:
|
121
|
+
Application.log(self, 'starting download:', @url)
|
121
122
|
|
122
123
|
Thread.new do
|
123
124
|
begin
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
125
|
+
loop do
|
126
|
+
begin
|
127
|
+
http = Net::HTTP.new(@url.host, @url.port)
|
128
|
+
@file.rewind
|
129
|
+
@progress, @total = 0, nil
|
130
|
+
|
131
|
+
http.request(Net::HTTP::Get.new(@url.request_uri)) do |res|
|
132
|
+
fail res.body unless res.code == '200'
|
133
|
+
|
134
|
+
@total = res.header['Content-Length'].to_i
|
135
|
+
|
136
|
+
res.read_body do |chunk|
|
137
|
+
@progress += chunk.size
|
138
|
+
@events.trigger(:progress, @progress)
|
139
|
+
@file << chunk
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
fail 'filesize mismatch' if @progress != @total
|
144
|
+
@file.flush
|
145
|
+
@events.trigger(:completed)
|
146
|
+
break
|
147
|
+
rescue
|
148
|
+
if (@tries -= 1) < 1
|
149
|
+
@events.trigger(:failed, (@error = $!))
|
150
|
+
break
|
151
|
+
end
|
152
|
+
Application.log(self, 'retrying failed DL', $!)
|
135
153
|
end
|
136
154
|
end
|
137
|
-
|
138
|
-
fail 'filesize mismatch' if @progress != @total
|
139
|
-
@events.trigger(:completed)
|
140
|
-
rescue
|
141
|
-
@events.trigger(:failed, (@error = $!))
|
142
155
|
ensure
|
143
156
|
@file.close
|
144
157
|
end
|
@@ -32,7 +32,7 @@ module UI
|
|
32
32
|
# as draw, layout and refresh) are executed once and only once at the
|
33
33
|
# end of this function.
|
34
34
|
def with_lock
|
35
|
-
|
35
|
+
lock; yield
|
36
36
|
ensure
|
37
37
|
unlock
|
38
38
|
end
|
@@ -42,26 +42,45 @@ module UI
|
|
42
42
|
end
|
43
43
|
|
44
44
|
def unlock
|
45
|
-
return unless (@lock.exit rescue nil)
|
45
|
+
return unless (@lock.exit rescue nil)
|
46
46
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
47
|
+
if @want & WANT_LAYOUT > 0
|
48
|
+
layout;
|
49
|
+
@want ^= WANT_LAYOUT
|
50
|
+
end
|
51
|
+
return if not visible?
|
52
|
+
|
53
|
+
if @want & WANT_REDRAW > 0
|
54
|
+
draw
|
55
|
+
@want ^= WANT_REDRAW
|
51
56
|
end
|
52
57
|
|
53
|
-
@want
|
58
|
+
if @want > 0 #& WANT_REFRESH > 0
|
59
|
+
Canvas.update_screen
|
60
|
+
@want ^= WANT_REFRESH
|
61
|
+
end
|
54
62
|
end
|
55
63
|
|
56
|
-
def display(force_refresh=false, force_redraw=false)
|
64
|
+
def display(force_refresh=false, force_redraw=false, force_layout=false)
|
65
|
+
if @want & WANT_LAYOUT > 0 or force_layout
|
66
|
+
layout;
|
67
|
+
@want ^= WANT_LAYOUT
|
68
|
+
end
|
57
69
|
return if not visible?
|
58
|
-
|
59
|
-
|
60
|
-
|
70
|
+
|
71
|
+
if @want & WANT_REDRAW > 0 or force_redraw
|
72
|
+
draw
|
73
|
+
@want ^= WANT_REDRAW
|
74
|
+
end
|
75
|
+
|
76
|
+
if @want > 0 or force_refresh #WANT_REFRESH > 0 or force_refresh
|
77
|
+
refresh
|
78
|
+
@want ^= WANT_REFRESH
|
79
|
+
end
|
61
80
|
end
|
62
81
|
|
63
|
-
def want_redraw; @want |=
|
64
|
-
def want_layout; @want
|
82
|
+
def want_redraw; @want |= 3 end
|
83
|
+
def want_layout; @want = 7 end
|
65
84
|
def want_refresh; @want |= WANT_REFRESH end
|
66
85
|
|
67
86
|
def invisible?; !visible? end
|
data/lib/ektoplayer/ui.rb
CHANGED
@@ -6,6 +6,8 @@ require_relative 'ui/colors'
|
|
6
6
|
require_relative 'events'
|
7
7
|
|
8
8
|
module UI
|
9
|
+
CONDITION_SIGNALS = ConditionSignals.new
|
10
|
+
|
9
11
|
class WidgetSizeError < Exception; end
|
10
12
|
|
11
13
|
class Canvas
|
@@ -36,19 +38,7 @@ module UI
|
|
36
38
|
end
|
37
39
|
|
38
40
|
def self.enable_resize_detection
|
39
|
-
@@
|
40
|
-
@@winch_cond ||= ConditionVariable.new
|
41
|
-
|
42
|
-
Signal.trap('WINCH') { @@winch_cond.signal }
|
43
|
-
|
44
|
-
@@winch_thread ||= Thread.new do
|
45
|
-
loop do
|
46
|
-
@@winch_mutex.synchronize do
|
47
|
-
@@winch_cond.wait(@@winch_mutex)
|
48
|
-
self.on_winch
|
49
|
-
end
|
50
|
-
end
|
51
|
-
end
|
41
|
+
Signal.trap('WINCH') { @@want_resize = true }
|
52
42
|
end
|
53
43
|
|
54
44
|
def self.widget; @@widget end
|
@@ -57,19 +47,6 @@ module UI
|
|
57
47
|
def self.visible?; true end
|
58
48
|
def self.inivsibile?; false end
|
59
49
|
|
60
|
-
def self.on_winch
|
61
|
-
h, w = IO.console.winsize()
|
62
|
-
Curses.resizeterm(h, w)
|
63
|
-
Curses.refresh
|
64
|
-
@@widget.size=(Size.new(height: h, width: w)) if @@widget
|
65
|
-
rescue UI::WidgetSizeError
|
66
|
-
Curses.clear
|
67
|
-
Curses.addstr('terminal too small!')
|
68
|
-
Curses.refresh
|
69
|
-
rescue
|
70
|
-
nil
|
71
|
-
end
|
72
|
-
|
73
50
|
def self.sub(cls, **opts)
|
74
51
|
@@widget ||= (widget = cls.new(parent: self, **opts))
|
75
52
|
widget
|
@@ -82,20 +59,26 @@ module UI
|
|
82
59
|
|
83
60
|
def self.update_screen(force_redraw=false)
|
84
61
|
@@updating ||= Mutex.new
|
62
|
+
@@want_resize ||= false
|
85
63
|
|
86
64
|
if @@updating.try_lock
|
87
|
-
if force_redraw
|
88
|
-
Curses.clear
|
89
|
-
Curses.refresh
|
90
|
-
end
|
91
|
-
|
92
65
|
begin
|
93
|
-
|
66
|
+
if @@want_resize
|
67
|
+
@@want_resize = false
|
68
|
+
h, w = IO.console.winsize()
|
69
|
+
Curses.resizeterm(h, w)
|
70
|
+
Curses.clear
|
71
|
+
Curses.refresh
|
72
|
+
@@widget.size=(Size.new(height: h, width: w)) if @@widget
|
73
|
+
@@widget.display(true, true, true) if @@widget
|
74
|
+
else
|
75
|
+
@@widget.display(true, force_redraw) if @@widget
|
76
|
+
end
|
94
77
|
rescue UI::WidgetSizeError
|
95
78
|
Curses.clear
|
96
79
|
Curses.addstr('terminal too small!')
|
97
80
|
rescue
|
98
|
-
Application.log(self, $!)
|
81
|
+
Ektoplayer::Application.log(self, $!)
|
99
82
|
end
|
100
83
|
|
101
84
|
Curses.doupdate
|
@@ -166,13 +149,17 @@ module UI
|
|
166
149
|
end
|
167
150
|
end
|
168
151
|
end
|
152
|
+
rescue
|
153
|
+
# getch() returned something weird that could not be chr()d
|
169
154
|
end while @@readline_obj.active?
|
170
155
|
end
|
171
156
|
end
|
172
157
|
end
|
173
158
|
|
174
159
|
def self.readline(*args, **opts, &block)
|
175
|
-
(@@readline_obj ||= ReadlineWindow.new).readline(*args, **opts
|
160
|
+
(@@readline_obj ||= ReadlineWindow.new).readline(*args, **opts) do
|
161
|
+
Canvas.class_variable_get('@@updating').synchronize { yield }
|
162
|
+
end
|
176
163
|
end
|
177
164
|
end
|
178
165
|
|
@@ -190,17 +177,18 @@ module UI
|
|
190
177
|
@thread ||= Thread.new do
|
191
178
|
window = Curses::Window.new(size.height, size.width, pos.y, pos.x)
|
192
179
|
|
180
|
+
rlt = Thread.new { Readline.readline(prompt, add_hist) }
|
193
181
|
Readline.set_screen_size(size.height, size.width)
|
194
182
|
Readline.delete_text
|
195
|
-
|
183
|
+
@readline_in_write.read_nonblock(100) rescue nil
|
196
184
|
|
197
185
|
while rlt.alive?
|
198
186
|
window.erase
|
199
|
-
buffer =
|
187
|
+
buffer = prompt + Readline.line_buffer.to_s
|
200
188
|
window << buffer[(buffer.size - size.width).clamp(0, buffer.size)..-1]
|
201
189
|
window.cursor=(Point.new(x: Readline.point + prompt.size, y: 0))
|
202
190
|
window.refresh
|
203
|
-
|
191
|
+
CONDITION_SIGNALS.wait(:readline, 0.2)
|
204
192
|
end
|
205
193
|
|
206
194
|
window.clear
|
@@ -212,7 +200,7 @@ module UI
|
|
212
200
|
|
213
201
|
def feed(c)
|
214
202
|
@readline_in_write.write(c)
|
215
|
-
|
203
|
+
CONDITION_SIGNALS.signal(:readline)
|
216
204
|
@thread = nil if c == ?\n
|
217
205
|
end
|
218
206
|
end
|
@@ -37,7 +37,7 @@ module Ektoplayer
|
|
37
37
|
|
38
38
|
@win.from_left(START_TAG_VALUE)
|
39
39
|
@win.with_attr(Theme[:'info.value']) do
|
40
|
-
@win.addstr(
|
40
|
+
@win.addstr(value.to_s)
|
41
41
|
end
|
42
42
|
end
|
43
43
|
|
@@ -48,7 +48,7 @@ module Ektoplayer
|
|
48
48
|
|
49
49
|
@win.from_left(START_INFO_VALUE)
|
50
50
|
@win.with_attr(Theme[:'info.value']) do
|
51
|
-
@win.addstr(
|
51
|
+
@win.addstr(value.to_s)
|
52
52
|
end
|
53
53
|
end
|
54
54
|
|
@@ -161,7 +161,7 @@ module Ektoplayer
|
|
161
161
|
@trackloader.downloads.each do |dl|
|
162
162
|
name = File.basename(dl.filename)
|
163
163
|
percent = Float(dl.progress) / dl.total * 100
|
164
|
-
percent =
|
164
|
+
percent = '%0.2f' % percent
|
165
165
|
draw_download(name, ?( + percent + '%)', dl.error)
|
166
166
|
end
|
167
167
|
@win.next_line
|
@@ -11,11 +11,11 @@ module Ektoplayer
|
|
11
11
|
def initialize(**opts)
|
12
12
|
super(**opts)
|
13
13
|
|
14
|
-
@playinginfo = sub(PlayingInfo)
|
15
|
-
@progressbar = sub(ProgressBar)
|
16
|
-
@volumemeter = sub(VolumeMeter)
|
17
|
-
@tabbar = sub(TabBar)
|
18
|
-
@windows = sub(UI::SwitchContainer)
|
14
|
+
@playinginfo = sub(PlayingInfo, size: @size.update(height: 2))
|
15
|
+
@progressbar = sub(ProgressBar, size: @size.update(height: 1))
|
16
|
+
@volumemeter = sub(VolumeMeter, size: @size.update(height: 1))
|
17
|
+
@tabbar = sub(TabBar, size: @size.update(height: 1))
|
18
|
+
@windows = sub(UI::SwitchContainer, size: @size.calc(height: -4))
|
19
19
|
@help = @windows.sub(Help, visible: false)
|
20
20
|
@info = @windows.sub(Info, visible: false)
|
21
21
|
@splash = @windows.sub(Splash, visible: false)
|
@@ -29,8 +29,8 @@ module Ektoplayer
|
|
29
29
|
|
30
30
|
Config[:'main.widgets'].each { |w| add(send(w)) }
|
31
31
|
|
32
|
-
@windows.selected=(@splash)
|
33
32
|
self.selected=(@windows)
|
33
|
+
@windows.selected=(@splash)
|
34
34
|
end
|
35
35
|
|
36
36
|
def layout
|
@@ -30,51 +30,42 @@ module Ektoplayer
|
|
30
30
|
|
31
31
|
def length=(l)
|
32
32
|
return if @length == l.to_i
|
33
|
-
|
34
|
-
|
35
|
-
@length = l.to_i
|
36
|
-
draw_position_and_length
|
37
|
-
end
|
33
|
+
@length = l.to_i
|
34
|
+
draw_position_and_length
|
38
35
|
end
|
39
36
|
|
40
37
|
def position=(p)
|
41
38
|
return if @position == p.to_i
|
42
|
-
|
43
|
-
|
44
|
-
@position = p.to_i
|
45
|
-
draw_position_and_length
|
46
|
-
end
|
39
|
+
@position = p.to_i
|
40
|
+
draw_position_and_length
|
47
41
|
end
|
48
42
|
|
49
43
|
def draw_position_and_length
|
44
|
+
return unless visible?
|
50
45
|
@win.setpos(0,0)
|
51
46
|
@win.with_attr(Theme[:'playinginfo.position']) do
|
52
47
|
@win << "[#{Common::to_time(@position)}/#{Common::to_time(@length)}]"
|
53
48
|
end
|
54
|
-
|
49
|
+
@win.refresh
|
55
50
|
end
|
56
51
|
|
57
52
|
def attach(playlist, player)
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
player.events.on(:play) { self.playing! }
|
62
|
-
|
63
|
-
player.events.on(:position_change) do
|
64
|
-
with_lock do
|
65
|
-
self.position=(player.position)
|
66
|
-
self.length=(player.length)
|
67
|
-
end
|
68
|
-
end
|
53
|
+
player.events.on(:pause) { self.paused! }
|
54
|
+
player.events.on(:stop) { self.stopped! }
|
55
|
+
player.events.on(:play) { self.playing! }
|
69
56
|
|
70
|
-
|
71
|
-
|
72
|
-
|
57
|
+
player.events.on(:position_change) do
|
58
|
+
self.position=(player.position)
|
59
|
+
self.length=(player.length)
|
60
|
+
end
|
73
61
|
|
74
|
-
|
75
|
-
self.
|
76
|
-
|
77
|
-
|
62
|
+
playlist.events.on(:current_changed) {
|
63
|
+
self.track=(playlist[playlist.current_playing])
|
64
|
+
}
|
65
|
+
|
66
|
+
# TODO: move mouse?
|
67
|
+
self.mouse.on(Curses::BUTTON1_CLICKED) do |mevent|
|
68
|
+
player.toggle
|
78
69
|
end
|
79
70
|
end
|
80
71
|
|
@@ -98,10 +89,9 @@ module Ektoplayer
|
|
98
89
|
|
99
90
|
def draw
|
100
91
|
@win.erase
|
92
|
+
draw_position_and_length
|
101
93
|
|
102
94
|
if @track
|
103
|
-
draw_position_and_length
|
104
|
-
|
105
95
|
fill(Config[:'playinginfo.format1']).each_with_index do |fmt,i|
|
106
96
|
@win.center(fmt[:sum]) if i == 0
|
107
97
|
@win.with_attr(UI::Colors.set(nil, *fmt[:curses_attrs])) do
|
@@ -110,7 +100,7 @@ module Ektoplayer
|
|
110
100
|
end
|
111
101
|
|
112
102
|
@win.with_attr(Theme[:'playinginfo.state']) do
|
113
|
-
@win.from_right(
|
103
|
+
@win.from_right(@state.to_s.size + 2) << "[#{@state}]"
|
114
104
|
end
|
115
105
|
|
116
106
|
@win.next_line
|
@@ -124,7 +114,7 @@ module Ektoplayer
|
|
124
114
|
else
|
125
115
|
@win.center_string(STOPPED_HEADING)
|
126
116
|
@win.with_attr(Theme[:'playinginfo.state']) do
|
127
|
-
@win.from_right(
|
117
|
+
@win.from_right(9) << '[stopped]'
|
128
118
|
end
|
129
119
|
end
|
130
120
|
|
@@ -32,10 +32,9 @@ module Ektoplayer
|
|
32
32
|
|
33
33
|
def draw
|
34
34
|
@win.setpos(0,0)
|
35
|
-
|
36
|
-
@
|
37
|
-
@
|
38
|
-
@rest_char ||= Config[:'progressbar.rest_char']
|
35
|
+
@progress_width ||= 0
|
36
|
+
@progress_char ||= Config[:'progressbar.progress_char']
|
37
|
+
@rest_char ||= Config[:'progressbar.rest_char']
|
39
38
|
|
40
39
|
@win.with_attr(Theme[:'progressbar.progress']) do
|
41
40
|
repeat = (@progress_width - @progress_char.size)
|
@@ -28,18 +28,20 @@ module Ektoplayer
|
|
28
28
|
end
|
29
29
|
|
30
30
|
def level=(level)
|
31
|
+
return unless visible?
|
32
|
+
|
31
33
|
new_level_width = (1.4 * level * @size.width).to_i.clamp(0, @size.width - 1) rescue 0
|
32
34
|
return if @level_width == new_level_width
|
33
35
|
load_colors
|
34
36
|
|
35
37
|
if new_level_width > @level_width
|
36
|
-
@win.setpos(
|
38
|
+
@win.setpos(0, @level_width)
|
37
39
|
(@level_width).upto(new_level_width).each do |i|
|
38
40
|
@win.attron(@fade[i])
|
39
41
|
@win << @level_char
|
40
42
|
end
|
41
43
|
else
|
42
|
-
@win.setpos(
|
44
|
+
@win.setpos(0, new_level_width)
|
43
45
|
end
|
44
46
|
|
45
47
|
if (repeat = @size.width - @win.curx - 1) > 0
|
@@ -48,7 +50,7 @@ module Ektoplayer
|
|
48
50
|
end
|
49
51
|
|
50
52
|
@level_width = new_level_width
|
51
|
-
@win.refresh
|
53
|
+
@win.refresh
|
52
54
|
end
|
53
55
|
|
54
56
|
def attach(player)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ektoplayer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Benjamin Abendroth
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-03-
|
11
|
+
date: 2017-03-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: audite
|