ektoplayer 0.1.12 → 0.1.16

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 (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