ektoplayer 0.1.12 → 0.1.16

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/lib/ektoplayer/application.rb +49 -19
  3. data/lib/ektoplayer/bindings.rb +91 -87
  4. data/lib/ektoplayer/browsepage.rb +14 -27
  5. data/lib/ektoplayer/common.rb +12 -67
  6. data/lib/ektoplayer/compat.rb +5 -11
  7. data/lib/ektoplayer/config.rb +52 -17
  8. data/lib/ektoplayer/controllers/browser.rb +10 -5
  9. data/lib/ektoplayer/database.rb +7 -20
  10. data/lib/ektoplayer/download/externaldownload.rb +65 -0
  11. data/lib/ektoplayer/download/rubydownload.rb +69 -0
  12. data/lib/ektoplayer/icurses/curses.rb +18 -2
  13. data/lib/ektoplayer/icurses/{ffi_ncurses.rb → ffi-ncurses.rb} +1 -0
  14. data/lib/ektoplayer/icurses/ncurses.rb +10 -0
  15. data/lib/ektoplayer/icurses.rb +13 -5
  16. data/lib/ektoplayer/models/browser.rb +11 -11
  17. data/lib/ektoplayer/models/player.rb +4 -5
  18. data/lib/ektoplayer/operations/browser.rb +9 -1
  19. data/lib/ektoplayer/operations/playlist.rb +1 -1
  20. data/lib/ektoplayer/players/mpg_wrapper_player.rb +98 -40
  21. data/lib/ektoplayer/theme.rb +78 -63
  22. data/lib/ektoplayer/trackloader.rb +25 -74
  23. data/lib/ektoplayer/ui/colors.rb +33 -5
  24. data/lib/ektoplayer/ui/widgets/container.rb +1 -1
  25. data/lib/ektoplayer/ui/widgets/listwidget.rb +35 -34
  26. data/lib/ektoplayer/ui/widgets.rb +19 -0
  27. data/lib/ektoplayer/ui.rb +22 -23
  28. data/lib/ektoplayer/updater.rb +3 -4
  29. data/lib/ektoplayer/views/browser.rb +7 -2
  30. data/lib/ektoplayer/views/help.rb +5 -2
  31. data/lib/ektoplayer/views/info.rb +22 -27
  32. data/lib/ektoplayer/views/playinginfo.rb +20 -19
  33. data/lib/ektoplayer/views/playlist.rb +8 -3
  34. data/lib/ektoplayer/views/progressbar.rb +26 -33
  35. data/lib/ektoplayer/views/splash.rb +14 -22
  36. data/lib/ektoplayer/views/trackrenderer.rb +14 -10
  37. metadata +7 -5
@@ -1,15 +1,20 @@
1
1
  require 'thread'
2
2
  require 'open3'
3
3
  require 'scanf'
4
+ require 'time'
4
5
 
5
6
  require_relative '../events'
6
7
 
7
- fail 'MpgWrapperPlayer: mpg123 not found' unless system('which mpg123 >/dev/null')
8
+ unless system('mpg123 --version >/dev/null 2>/dev/null')
9
+ fail LoadError, 'MpgWrapperPlayer: /bin/mpg123 not found'
10
+ end
8
11
 
9
12
  class MpgWrapperPlayer
10
13
  attr_reader :events, :file
11
14
 
12
15
  STATE_STOPPED, STATE_PAUSED, STATE_PLAYING = 0, 1, 2
16
+ CMD_FORMAT = 'FORMAT'.freeze
17
+ CMD_SAMPLE = 'SAMPLE'.freeze
13
18
 
14
19
  def initialize
15
20
  @events = Events.new(:play, :pause, :stop, :position_change)
@@ -18,21 +23,34 @@ class MpgWrapperPlayer
18
23
  @state = 0
19
24
  @file = ''
20
25
 
21
- @frames_played = @frames_remaining =
22
- @seconds_played = @seconds_remaining = 0
26
+ @polling_interval = 0.9
27
+
28
+ @seconds_played = @seconds_total = 0
29
+ @track_completed = nil
23
30
  end
24
31
 
32
+ def can_http?; true; end
33
+
25
34
  def play(file=nil)
26
35
  start_mpg123_thread
36
+ @track_completed = :track_completed
27
37
  @file = file if file
28
38
  write("L #{@file}")
39
+ Thread.new { sleep 3; write(CMD_FORMAT) }
29
40
  end
30
41
 
31
42
  def pause; write(?P) if @state == STATE_PLAYING end
32
- def stop; write(?S) if @state != STATE_STOPPED end
33
- def toggle; write(?P) end
43
+ def toggle; write(?P) end
44
+
45
+ def stop
46
+ stop_polling_thread
47
+ @track_completed = nil
48
+ @seconds_played = @seconds_total = 0
49
+ @events.trigger(:position_change)
50
+ @events.trigger(:stop)
51
+ write(?Q) if @state != STATE_STOPPED
52
+ end
34
53
 
35
- def level; 30 end
36
54
  def paused?; @state == STATE_PAUSED end
37
55
  def stopped?; @state == STATE_STOPPED end
38
56
  def playing?; @state == STATE_PLAYING end
@@ -43,21 +61,70 @@ class MpgWrapperPlayer
43
61
  return :stopped if stopped?
44
62
  end
45
63
 
46
- def position; @seconds_played end
47
- def length; @seconds_played + @seconds_remaining end
48
- def position_percent; Float(@seconds_played) / length end
64
+ def position; @seconds_played end
65
+ def length; @seconds_total end
49
66
 
50
- def seek(seconds) write("J #{seconds}s") end
67
+ def position_percent
68
+ @seconds_played.to_f / length rescue 0.0
69
+ end
70
+
71
+ def seek(seconds) write("J #{seconds}s") end
51
72
  def rewind(seconds = 2) write("J -#{seconds}s") end
52
- def forward(seconds = 2) write("J +#{seconds}s") end
73
+ def forward(seconds = 2) write("J +#{seconds}s") end
53
74
  alias :backward :rewind
54
75
 
55
76
  private def write(string)
56
- @lock.synchronize do
57
- @mpg123_in.write(string + ?\n)
58
- end
77
+ @lock.synchronize { @mpg123_in.write(string + ?\n) }
59
78
  rescue
60
- Ektoplayer::Application.log(self, $!)
79
+ nil
80
+ end
81
+
82
+ def use_polling(interval)
83
+ @polling_interval = interval
84
+ start_polling_thread
85
+ end
86
+
87
+ private def start_polling_thread
88
+ write('SILENCE')
89
+ @polling_thread ||= Thread.new do
90
+ loop do
91
+ sleep @polling_interval
92
+ write(CMD_SAMPLE)
93
+ end
94
+ end
95
+ end
96
+
97
+ private def stop_polling_thread
98
+ @polling_thread.kill if @polling_thread
99
+ @polling_thread = nil
100
+ end
101
+
102
+ define_method '@FORMAT' do |line|
103
+ @sample_rate, channels = line.scanf('%d %d')
104
+ end
105
+
106
+ define_method '@SAMPLE' do |line|
107
+ @sample_rate ||= 44100
108
+ samples_played, samples_total = line.scanf('%f %f')
109
+ @seconds_played = samples_played / @sample_rate rescue 0.0
110
+ @seconds_total = samples_total / @sample_rate rescue 0.0
111
+ @events.trigger(:position_change)
112
+ end
113
+
114
+ define_method '@F' do |line|
115
+ _, _, @seconds_played, seconds_remaining = line.scanf('%d %d %f %f')
116
+ @seconds_total = @seconds_played + seconds_remaining
117
+ @events.trigger(:position_change)
118
+ end
119
+
120
+ define_method '@P' do |line|
121
+ if (@state = line.to_i) == STATE_STOPPED
122
+ @events.trigger(:stop, @track_completed)
123
+ elsif @state == STATE_PAUSED
124
+ @events.trigger(:pause)
125
+ elsif @state == STATE_PLAYING
126
+ @events.trigger(:play)
127
+ end
61
128
  end
62
129
 
63
130
  private def start_mpg123_thread
@@ -65,43 +132,34 @@ class MpgWrapperPlayer
65
132
  unless @mpg123_thread
66
133
  Thread.new do
67
134
  begin
68
- @mpg123_in, @mpg123_out, _, @mpg123_thread =
69
- Open3.popen3('mpg123', '-o', 'jack,pulse,alsa,oss', '--fuzzy', '-R')
135
+ @mpg123_in, @mpg123_out, mpg123_err, @mpg123_thread =
136
+ Open3.popen3('mpg123', '-o', 'jack,pulse,alsa,oss', '--fuzzy', '-b', '1024', '-R')
70
137
 
71
138
  while (line = @mpg123_out.readline)
72
- if line[1] == ?F
73
- @frames_played, @frames_remaining,
74
- @seconds_played, @seconds_remaining =
75
- line.scanf('@F %d %d %f %f')
76
- @events.trigger(:position_change)
77
- elsif line[1] == ?P
78
- if (@state = line[3].to_i) == STATE_STOPPED
79
- if @seconds_remaining < 3
80
- @events.trigger(:stop, :track_completed)
81
- else
82
- @events.trigger(:stop)
83
- end
84
- elsif @state == STATE_PAUSED
85
- @events.trigger(:pause)
86
- elsif @state == STATE_PLAYING
87
- @events.trigger(:play)
88
- end
89
- end
139
+ cmd, line = line.split(' ', 2)
140
+ send(cmd, line) rescue nil
90
141
  end
91
142
  rescue
92
143
  Ektoplayer::Application.log(self, $!)
93
144
  ensure
94
- # shouldn't reach here
95
- Ektoplayer::Application.log(self, 'player closed')
96
- @mpg123_thread.kill
145
+ begin msg = mpg123_err.read
146
+ rescue
147
+ msg = ''
148
+ end
149
+
150
+ Ektoplayer::Application.log(self, 'player closed:', msg)
151
+ @mpg123_thread.kill if @mpg123_thread
152
+ (@mpg123_in.close rescue nil) if @mpg123_in
153
+ (@mpg123_out.close rescue nil) if @mpg123_out
97
154
  @mpg123_thread = nil
98
- @mpg123_in.close
99
- @mpg123_out.close
155
+ stop_polling_thread
100
156
  end
101
157
  end
102
158
 
103
159
  sleep 0.1 while not @mpg123_thread
104
160
  end
105
161
  end
162
+
163
+ start_polling_thread if @polling_interval > 0
106
164
  end
107
165
  end
@@ -5,64 +5,74 @@ module Ektoplayer
5
5
  attr_reader :current, :theme
6
6
 
7
7
  def initialize
8
- @current = 0
9
- @theme = {
10
- 0 => { default: [-1, -1].freeze,
11
- :'url' => [-1, -1, :underline ].freeze,
12
- :'tabbar.selected' => [-1, -1, :bold ].freeze},
13
- 8 => { default: [-1, -1].freeze,
14
- :'url' => [:magenta, -1, :underline].freeze,
15
-
16
- :'info.head' => [:blue, -1, :bold ].freeze,
17
- :'info.tag' => [:blue ].freeze,
18
- :'info.value' => [:magenta ].freeze,
19
- :'info.description' => [:blue ].freeze,
20
- :'info.download.file' => [:blue ].freeze,
21
- :'info.download.percent' => [:magenta, -1 ].freeze,
22
- :'info.download.error' => [:red ].freeze,
23
-
24
- :'progressbar.progress' => [:blue ].freeze,
25
- :'progressbar.rest' => [:black ].freeze,
26
-
27
- :'tabbar.selected' => [:blue ].freeze,
28
- :'tabbar.unselected' => [:none ].freeze,
29
-
30
- :'list.item_even' => [:blue ].freeze,
31
- :'list.item_odd' => [:blue ].freeze,
32
- :'list.item_selection' => [:magenta ].freeze,
33
-
34
- :'playinginfo.position' => [:magenta ].freeze,
35
- :'playinginfo.state' => [:cyan ].freeze,
36
-
37
- :'help.widget_name' => [:blue, -1, :bold ].freeze,
38
- :'help.key_name' => [:blue ].freeze,
39
- :'help.command_name' => [:magenta ].freeze,
40
- :'help.command_desc' => [:yellow ].freeze},
41
- 256 => { default: [-1, -1].freeze,
42
- :'url' => [97, -1, :underline ].freeze,
43
-
44
- :'info.head' => [32, -1, :bold ].freeze,
45
- :'info.tag' => [74 ].freeze,
46
- :'info.value' => [67 ].freeze,
47
- :'info.description' => [67 ].freeze,
48
- :'info.download.file' => [75 ].freeze,
49
- :'info.download.percent' => [68 ].freeze,
50
- :'info.download.error' => [:red ].freeze,
51
-
52
- :'progressbar.progress' => [23 ].freeze,
53
- :'progressbar.rest' => [236 ].freeze,
54
-
55
- :'tabbar.selected' => [75 ].freeze,
56
- :'tabbar.unselected' => [250 ].freeze,
57
-
58
- :'list.item_even' => [:blue ].freeze,
59
- :'list.item_odd' => [25 ].freeze,
60
-
61
- :'help.widget_name' => [33 ].freeze,
62
- :'help.key_name' => [75 ].freeze,
63
- :'help.command_name' => [68 ].freeze,
64
- :'help.command_desc' => [29 ].freeze}
65
- }.freeze
8
+ @theme, @current = {}, 0
9
+
10
+ @theme[0] = {
11
+ :default => [-1, -1 ].freeze,
12
+ :'url' => [:default, :default, :underline].freeze,
13
+ :'tabbar.selected' => [:default, :default, :bold ].freeze
14
+ }
15
+ @theme[8] = {
16
+ :default => [-1, -1 ].freeze,
17
+ :'url' => [:magenta, :default, :underline].freeze,
18
+
19
+ :'info.head' => [:blue, :default, :bold ].freeze,
20
+ :'info.tag' => [:blue ].freeze,
21
+ :'info.value' => [:magenta ].freeze,
22
+ :'info.description' => [:blue ].freeze,
23
+ :'info.download.file' => [:blue ].freeze,
24
+ :'info.download.percent' => [:magenta ].freeze,
25
+ :'info.download.error' => [:red ].freeze,
26
+
27
+ :'progressbar.progress' => [:blue ].freeze,
28
+ :'progressbar.rest' => [:black ].freeze,
29
+
30
+ :'tabbar.selected' => [:blue ].freeze,
31
+ :'tabbar.unselected' => [:white ].freeze,
32
+
33
+ :'list.item_even' => [:blue ].freeze,
34
+ :'list.item_odd' => [:blue ].freeze,
35
+ :'list.item_selection' => [:magenta ].freeze,
36
+
37
+ :'playinginfo.position' => [:magenta ].freeze,
38
+ :'playinginfo.state' => [:cyan ].freeze,
39
+
40
+ :'help.widget_name' => [:blue, :default, :bold ].freeze,
41
+ :'help.key_name' => [:blue ].freeze,
42
+ :'help.command_name' => [:magenta ].freeze,
43
+ :'help.command_desc' => [:yellow ].freeze
44
+ }
45
+ @theme[256] = {
46
+ :'default' => [:white, 233 ].freeze,
47
+ :'url' => [97, :default, :underline ].freeze,
48
+
49
+ :'info.head' => [32, :default, :bold ].freeze,
50
+ :'info.tag' => [74 ].freeze,
51
+ :'info.value' => [67 ].freeze,
52
+ :'info.description' => [67 ].freeze,
53
+ :'info.download.file' => [75 ].freeze,
54
+ :'info.download.percent' => [68 ].freeze,
55
+ :'info.download.error' => [:red ].freeze,
56
+
57
+ :'progressbar.progress' => [23 ].freeze,
58
+ :'progressbar.rest' => [:black ].freeze,
59
+
60
+ :'tabbar.selected' => [75 ].freeze,
61
+ :'tabbar.unselected' => [250 ].freeze,
62
+
63
+ :'list.item_even' => [26 ].freeze,
64
+ :'list.item_odd' => [25 ].freeze,
65
+ :'list.item_selection' => [97 ].freeze,
66
+
67
+ :'playinginfo.position' => [97 ].freeze,
68
+ :'playinginfo.state' => [37 ].freeze,
69
+
70
+ :'help.widget_name' => [33 ].freeze,
71
+ :'help.key_name' => [75 ].freeze,
72
+ :'help.command_name' => [68 ].freeze,
73
+ :'help.command_desc' => [29 ].freeze
74
+ }
75
+ @theme.freeze
66
76
  end
67
77
 
68
78
  def color(name, *defs, theme: 8)
@@ -81,16 +91,21 @@ module Ektoplayer
81
91
  @current = colors
82
92
 
83
93
  UI::Colors.reset
94
+ UI::Colors.default_fg(@theme[@current][:default][0])
95
+ UI::Colors.default_bg(@theme[@current][:default][1])
96
+
84
97
  @theme.values.map(&:keys).flatten.each do |name|
98
+ next if name == :default
99
+
85
100
  defs ||= @theme[256][name] if @current == 256
86
101
  defs ||= @theme[8][name] if @current >= 8
87
102
  defs ||= @theme[0][name]
88
103
 
89
- unless defs
90
- defs ||= @theme[256][:default] if @current == 256
91
- defs ||= @theme[8][:default] if @current >= 8
92
- defs ||= @theme[0][:default]
93
- end
104
+ #unless defs
105
+ # defs ||= @theme[256][:default] if @current == 256
106
+ # defs ||= @theme[8][:default] if @current >= 8
107
+ # defs ||= @theme[0][:default]
108
+ #end
94
109
 
95
110
  UI::Colors.set(name, *defs)
96
111
  end
@@ -1,10 +1,16 @@
1
1
  require 'fileutils'
2
- require 'net/https'
3
2
  require 'uri'
4
3
 
5
- require_relative 'events'
6
4
  require_relative 'common'
7
5
 
6
+ begin
7
+ require_relative 'download/externaldownload'
8
+ DownloadThread = ExternalDownload
9
+ rescue LoadError
10
+ require_relative 'download/rubydownload'
11
+ DownloadThread = RubyDownload
12
+ end
13
+
8
14
  module Ektoplayer
9
15
  class Trackloader
10
16
  attr_reader :downloads
@@ -21,7 +27,7 @@ module Ektoplayer
21
27
  @database.get_archives(url).select {|_|_['archive_type'] == 'MP3'}[0]
22
28
  )
23
29
 
24
- r['archive_filename'] = URI.unescape(File.basename(URI.parse(r['archive_url']).path))
30
+ r['archive_filename'] = URI.unescape(r['archive_url'])
25
31
  r['archive_basename'] = File.basename(r['archive_filename'], '.zip')
26
32
  r['album_path'] = File.join(Config[:archive_dir], r['archive_basename'])
27
33
  r
@@ -34,7 +40,9 @@ module Ektoplayer
34
40
  archive_file = File.join(Config[:download_dir], track_info['archive_filename'])
35
41
  return if File.exists? archive_file
36
42
 
37
- dl = DownloadThread.new(track_info['archive_url'], archive_file)
43
+ archive_url = Application.archive_url(track_info['archive_url'])
44
+ Application.log(self, 'starting download:', archive_url)
45
+ dl = DownloadThread.new(archive_url, archive_file)
38
46
 
39
47
  if Config[:auto_extract_to_archive_dir]
40
48
  dl.events.on(:completed) do
@@ -50,8 +58,8 @@ module Ektoplayer
50
58
  end
51
59
 
52
60
  dl.events.on(:failed) do |reason|
53
- Application.log(self, dl.file, dl.url, reason)
54
- FileUtils::rm(dl.file) rescue nil
61
+ Application.log(self, dl.filename, dl.url, reason)
62
+ FileUtils::rm(dl.filename) rescue nil
55
63
  end
56
64
 
57
65
  @downloads << dl.start!
@@ -59,7 +67,7 @@ module Ektoplayer
59
67
  Application.log(self, $!)
60
68
  end
61
69
 
62
- def get_track_file(url, reload: false)
70
+ def get_track_file(url, reload: false, http_okay: false)
63
71
  begin
64
72
  track_info = get_track_infos(url)
65
73
  album_files = Dir.glob(File.join(track_info['album_path'], '*.mp3'))
@@ -69,31 +77,33 @@ module Ektoplayer
69
77
  Application.log(self, 'could not load track from archive_dir:', $!)
70
78
  end
71
79
 
72
- url_obj = URI.parse(url)
80
+ real_url = Application.track_url(url)
81
+ url_obj = URI.parse(real_url)
73
82
  basename = File.basename(url_obj.path)
74
83
  cache_file = File.join(Config[:cache_dir], basename)
75
- temp_file = File.join(Config[:temp_dir], basename)
84
+ temp_file = File.join(Config[:temp_dir], '~ekto-' + basename)
76
85
 
77
86
  (File.delete(cache_file) rescue nil) if reload
78
87
  (File.delete(temp_file) rescue nil) if reload
79
88
 
80
89
  return cache_file if File.file?(cache_file)
81
90
  return temp_file if File.file?(temp_file)
91
+ return real_url if http_okay
82
92
 
83
- dl = DownloadThread.new(url, temp_file)
93
+ Application.log(self, 'starting download:', real_url)
94
+ dl = DownloadThread.new(real_url, temp_file)
84
95
 
85
96
  if Config[:use_cache]
86
97
  dl.events.on(:completed) do
87
- begin FileUtils::mv(temp_file, cache_file)
88
- rescue
98
+ FileUtils::mv(temp_file, cache_file) rescue (
89
99
  Application.log(self, 'mv failed', temp_file, cache_file, $!)
90
- end
100
+ )
91
101
  end
92
102
  end
93
103
 
94
104
  dl.events.on(:failed) do |reason|
95
- Application.log(self, dl.file, dl.url, reason)
96
- FileUtils::rm(dl.file) rescue nil
105
+ Application.log(self, dl.filename, dl.url, reason)
106
+ FileUtils::rm(dl.filename) rescue nil
97
107
  end
98
108
 
99
109
  @downloads << dl.start!
@@ -103,63 +113,4 @@ module Ektoplayer
103
113
  Application.log(self, $!)
104
114
  end
105
115
  end
106
-
107
- class DownloadThread
108
- attr_reader :events, :url, :progress, :total, :file, :filename, :error
109
-
110
- def initialize(url, filename)
111
- @events = Events.new(:completed, :failed, :progress)
112
- @url = URI.parse(url)
113
- @filename = filename
114
- @file = File.open(filename, ?w)
115
- @progress = 0
116
- @error = nil
117
- @tries = 3
118
- end
119
-
120
- def start!
121
- Application.log(self, 'starting download:', @url)
122
-
123
- Thread.new do
124
- begin
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', $!)
153
- end
154
- end
155
- ensure
156
- @file.close
157
- end
158
- end
159
-
160
- sleep 0.1 while @total.nil?
161
- sleep 0.2
162
- self
163
- end
164
- end
165
116
  end
@@ -11,6 +11,7 @@ module UI
11
11
 
12
12
  def ColorFader._fade(colors, size)
13
13
  return [] if size < 1
14
+ return [colors[0]] * size if colors.size == 1
14
15
 
15
16
  part_len = (size / colors.size)
16
17
  diff = size - part_len * colors.size
@@ -22,13 +23,13 @@ module UI
22
23
 
23
24
  def ColorFader._fade2(colors, size)
24
25
  half = size / 2
25
- ColorFader._fade(colors, half) + ColorFader._fade(colors, size - half).reverse
26
+ ColorFader._fade(colors, half) + ColorFader._fade(colors.reverse, size - half)
26
27
  end
27
28
  end
28
29
 
29
30
  class Colors
30
31
  COLORS = {
31
- none: -1, default: -1, nil => -1,
32
+ none: -1,
32
33
  white: ICurses::COLOR_WHITE,
33
34
  black: ICurses::COLOR_BLACK,
34
35
  red: ICurses::COLOR_RED,
@@ -48,7 +49,10 @@ module UI
48
49
  bold: ICurses::A_BOLD, standout: ICurses::A_STANDOUT,
49
50
  blink: ICurses::A_BLINK, underline: ICurses::A_UNDERLINE
50
51
  }
51
- ATTRIBUTES.default_proc = proc { |h,k| k }
52
+ ATTRIBUTES.default_proc = proc do |h,key|
53
+ fail "Unknown attribute #{key}" unless key.is_a?Integer
54
+ key
55
+ end
52
56
  ATTRIBUTES.freeze
53
57
 
54
58
  def self.start
@@ -57,11 +61,35 @@ module UI
57
61
  @@volatile ||= {}
58
62
  @@volatile_ids ||= {}
59
63
  @@cached ||= Hash.new { |h,k| h[k] = {} }
64
+ @@default_fg = @@default_bg = -1
60
65
  end
61
66
  def self.reset; self.start end
62
67
 
68
+ def self.default_fg(color)
69
+ @@default_fg = COLORS[color]
70
+ end
71
+
72
+ def self.default_bg(color)
73
+ @@default_bg = COLORS[color]
74
+ end
75
+
76
+ def self.default_colors(fg = -1, bg = -1)
77
+ self.default_fg(fg)
78
+ self.default_bg(bg)
79
+ end
80
+
63
81
  def self.init_pair_cached(fg, bg)
64
- fg, bg = COLORS[fg], COLORS[bg]
82
+ if !fg or fg == :default
83
+ fg = @@default_fg
84
+ else
85
+ fg = COLORS[fg]
86
+ end
87
+
88
+ if !bg or bg == :default
89
+ bg = @@default_bg
90
+ else
91
+ bg = COLORS[bg]
92
+ end
65
93
 
66
94
  unless id = @@cached[fg][bg]
67
95
  id = @@cached[fg][bg] = @@id
@@ -81,7 +109,7 @@ module UI
81
109
  def self.[](name) @@aliases[name] || 0 end
82
110
  def self.get(name) @@aliases[name] || 0 end
83
111
 
84
- def self.set(name, fg, bg = -1, *attrs)
112
+ def self.set(name, fg, bg = nil, *attrs)
85
113
  @@aliases[name] = self.init_pair_cached(fg, bg)
86
114
  attrs.each { |attr| @@aliases[name] |= ATTRIBUTES[attr] }
87
115
  @@aliases[name]
@@ -73,7 +73,7 @@ module UI
73
73
 
74
74
  def draw; visible_widgets.each(&:draw) end
75
75
  def refresh; visible_widgets.each(&:refresh) end
76
- def layout; @widgets.each(&:layout) end
76
+ def layout; @widgets.each(&:layout) end
77
77
 
78
78
  def on_key_press(key)
79
79
  @selected.key_press(key) if @selected