ektoplayer 0.1.6 → 0.1.11

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/README.md +26 -12
  3. data/lib/ektoplayer/application.rb +6 -5
  4. data/lib/ektoplayer/bindings.rb +61 -55
  5. data/lib/ektoplayer/common.rb +19 -6
  6. data/lib/ektoplayer/compat.rb +3 -3
  7. data/lib/ektoplayer/config.rb +1 -11
  8. data/lib/ektoplayer/controllers/browser.rb +7 -5
  9. data/lib/ektoplayer/controllers/help.rb +1 -1
  10. data/lib/ektoplayer/controllers/info.rb +1 -1
  11. data/lib/ektoplayer/controllers/playlist.rb +24 -12
  12. data/lib/ektoplayer/icurses.rb +21 -0
  13. data/lib/ektoplayer/icurses/curses.rb +53 -0
  14. data/lib/ektoplayer/icurses/ffi_ncurses.rb +69 -0
  15. data/lib/ektoplayer/icurses/ncurses.rb +79 -0
  16. data/lib/ektoplayer/icurses/ncursesw.rb +1 -0
  17. data/lib/ektoplayer/icurses/sugar.rb +65 -0
  18. data/lib/ektoplayer/icurses/test.rb +99 -0
  19. data/lib/ektoplayer/models/player.rb +2 -2
  20. data/lib/ektoplayer/{mp3player.rb → players/mpg_portaudio_player.rb} +3 -3
  21. data/lib/ektoplayer/players/mpg_wrapper_player.rb +107 -0
  22. data/lib/ektoplayer/theme.rb +1 -6
  23. data/lib/ektoplayer/ui.rb +100 -129
  24. data/lib/ektoplayer/ui/colors.rb +14 -14
  25. data/lib/ektoplayer/ui/widgets.rb +4 -4
  26. data/lib/ektoplayer/ui/widgets/labelwidget.rb +1 -1
  27. data/lib/ektoplayer/ui/widgets/listwidget.rb +115 -46
  28. data/lib/ektoplayer/views/help.rb +7 -10
  29. data/lib/ektoplayer/views/info.rb +29 -38
  30. data/lib/ektoplayer/views/mainwindow.rb +2 -5
  31. data/lib/ektoplayer/views/playinginfo.rb +15 -20
  32. data/lib/ektoplayer/views/progressbar.rb +30 -10
  33. data/lib/ektoplayer/views/splash.rb +24 -25
  34. data/lib/ektoplayer/views/tabbar.rb +6 -5
  35. data/lib/ektoplayer/views/trackrenderer.rb +20 -14
  36. metadata +15 -47
  37. data/lib/ektoplayer/views/volumemeter.rb +0 -76
@@ -1,4 +1,4 @@
1
- require 'curses'
1
+ require_relative '../icurses'
2
2
 
3
3
  module UI
4
4
  class ColorFader
@@ -29,14 +29,14 @@ module UI
29
29
  class Colors
30
30
  COLORS = {
31
31
  none: -1, default: -1, nil => -1,
32
- white: Curses::COLOR_WHITE,
33
- black: Curses::COLOR_BLACK,
34
- red: Curses::COLOR_RED,
35
- blue: Curses::COLOR_BLUE,
36
- cyan: Curses::COLOR_CYAN,
37
- green: Curses::COLOR_GREEN,
38
- yellow: Curses::COLOR_YELLOW,
39
- magenta: Curses::COLOR_MAGENTA
32
+ white: ICurses::COLOR_WHITE,
33
+ black: ICurses::COLOR_BLACK,
34
+ red: ICurses::COLOR_RED,
35
+ blue: ICurses::COLOR_BLUE,
36
+ cyan: ICurses::COLOR_CYAN,
37
+ green: ICurses::COLOR_GREEN,
38
+ yellow: ICurses::COLOR_YELLOW,
39
+ magenta: ICurses::COLOR_MAGENTA
40
40
  }
41
41
  COLORS.default_proc = proc do |h, key|
42
42
  fail "Unknown color #{key}" unless key.is_a?Integer
@@ -45,8 +45,8 @@ module UI
45
45
  COLORS.freeze
46
46
 
47
47
  ATTRIBUTES = {
48
- bold: Curses::A_BOLD, standout: Curses::A_STANDOUT,
49
- blink: Curses::A_BLINK, underline: Curses::A_UNDERLINE
48
+ bold: ICurses::A_BOLD, standout: ICurses::A_STANDOUT,
49
+ blink: ICurses::A_BLINK, underline: ICurses::A_UNDERLINE
50
50
  }
51
51
  ATTRIBUTES.default_proc = proc { |h,k| k }
52
52
  ATTRIBUTES.freeze
@@ -68,8 +68,8 @@ module UI
68
68
  @@id += 1
69
69
  end
70
70
 
71
- Curses.init_pair(id, fg, bg) #or fail
72
- Curses.color_pair(id)
71
+ ICurses.init_pair(id, fg, bg) #or fail
72
+ ICurses.color_pair(id)
73
73
  end
74
74
 
75
75
  def self.add_attributes(*attrs)
@@ -99,7 +99,7 @@ module UI
99
99
  @@id += 1
100
100
  end
101
101
 
102
- @@volatile[name] = Curses.init_pair(id, fg, bg)
102
+ @@volatile[name] = ICurses.init_pair(id, fg, bg)
103
103
  end
104
104
  end
105
105
  end
@@ -143,8 +143,8 @@ module UI
143
143
 
144
144
  def initialize(**opts)
145
145
  super(**opts)
146
- @win = Curses::Window.new(@size.height, @size.width, @pos.y, @pos.x)
147
- @win.keypad=(true)
146
+ @win = ICurses.newwin(@size.height, @size.width, @pos.y, @pos.x)
147
+ @win.keypad(true)
148
148
  end
149
149
 
150
150
  def layout
@@ -163,7 +163,7 @@ module UI
163
163
 
164
164
  def initialize(**opts)
165
165
  super(**opts)
166
- @win = Curses::Pad.new(@size.height, @size.width)
166
+ @win = ICurses.newpad(@size.height, @size.width)
167
167
  @pad_minrow = @pad_mincol = 0
168
168
  end
169
169
 
@@ -230,7 +230,7 @@ module UI
230
230
  end
231
231
 
232
232
  def refresh
233
- @win.noutrefresh(
233
+ @win.pnoutrefresh(
234
234
  @pad_minrow, @pad_mincol,
235
235
  @pos.y, @pos.x,
236
236
  @pos.y + @size.height - 1, @pos.x + @size.width - 1
@@ -28,7 +28,7 @@ module UI
28
28
  def draw
29
29
  @win.erase
30
30
  @text.split(?\n).each_with_index do |l, i|
31
- @win.setpos(@pad[:top] + i, @pad[:left])
31
+ @win.move(@pad[:top] + i, @pad[:left])
32
32
  @win.attron(@attributes) { @win << l }
33
33
  end
34
34
  end
@@ -12,12 +12,28 @@ module UI
12
12
 
13
13
  def layout; end
14
14
 
15
- def render(scr, item, selected: false, marked: false)
15
+ def render(scr, item, selected: false, marked: false, selection: false)
16
16
  scr << (selected ? ?> : ' ')
17
17
  scr << item_to.s
18
18
  end
19
19
  end
20
20
 
21
+ class ListSelector
22
+ attr_reader :start_pos
23
+ def start(pos)
24
+ @start_pos = pos
25
+ end
26
+
27
+ def started?; @start_pos end
28
+
29
+ def stop(pos)
30
+ return [] unless @start_pos
31
+ r = [pos, @start_pos].min.upto([pos, @start_pos].max).to_a
32
+ @start_pos = nil
33
+ r
34
+ end
35
+ end
36
+
21
37
  class ListSearch
22
38
  attr_accessor :direction, :source
23
39
 
@@ -28,7 +44,7 @@ module UI
28
44
  end
29
45
 
30
46
  def comp(item, search)
31
- if item.is_a?String
47
+ if item.is_a?String or item.is_a?Symbol
32
48
  return item.downcase =~ Regexp.new(search.downcase)
33
49
  elsif item.is_a?Hash
34
50
  %w(title artist album).each do |key|
@@ -46,24 +62,29 @@ module UI
46
62
  @result = @source.size.times.select {|i| self.comp(@source[i], search) }
47
63
  end
48
64
 
49
- def current; @result[@current] or 0 end
50
- def next; @direction == :up ? search_up : search_down end
51
- def prev; @direction == :up ? search_down : search_up end
65
+ def current; @current or 0 end #or 0 end
66
+ def next(p) @direction == :up ? search_up(p): search_down(p) end
67
+ def prev(p) @direction == :up ? search_down(p) : search_up(p) end
68
+
69
+ def search_up(p)
70
+ @current = (
71
+ @result.reverse.select { |v| v < p }[0] or @result[-1]
72
+ )
52
73
 
53
- def search_up
54
- @current -= 1
55
- @current = @result.size - 1 if @current < 0
56
74
  self
57
75
  end
58
76
 
59
- def search_down
60
- @current = 0 if (@current += 1) >= @result.size
77
+ def search_down(p)
78
+ @current = (
79
+ @result.select { |v| v > p }[0] or @result[0]
80
+ )
81
+
61
82
  self
62
83
  end
63
84
  end
64
85
 
65
86
  class ListWidget < Window
66
- attr_reader :list, :selected, :cursor
87
+ attr_reader :list, :selected, :cursor, :selection
67
88
  attr_accessor :item_renderer
68
89
 
69
90
  def initialize(list: [], item_renderer: nil, **opts)
@@ -72,13 +93,14 @@ module UI
72
93
  @item_renderer = (item_renderer or ListItemRenderer.new)
73
94
  @cursor = @selected = 0
74
95
  @search = ListSearch.new
96
+ @selection = ListSelector.new
75
97
  end
76
98
 
77
- def search_next; self.selected=(@search.next.current) end
78
- def search_prev; self.selected=(@search.prev.current) end
99
+ def search_next; self.selected=(@search.next(@selected).current) end
100
+ def search_prev; self.selected=(@search.prev(@selected).current) end
79
101
  def search_up; self.search_start(:up) end
80
102
  def search_down; self.search_start(:down) end
81
- def search_start(direction=:down)
103
+ def search_start(direction)
82
104
  UI::Input.readline(@pos, @size.update(height: 1), prompt: '> ', add_hist: true) do |result|
83
105
  if result
84
106
  @search.source=(@list)
@@ -89,9 +111,39 @@ module UI
89
111
  end
90
112
  end
91
113
 
114
+ def toggle_selection
115
+ with_lock do
116
+ if @selection.started?
117
+ @selection.stop(@selected)
118
+ want_redraw
119
+ else
120
+ @selection.start(@selected)
121
+ end
122
+ end
123
+ end
124
+
125
+ def get_selection
126
+ self.lock
127
+ r = @selection.stop(@selected)
128
+ r << @selected if r.empty?
129
+ r
130
+ ensure
131
+ want_redraw
132
+ self.unlock
133
+ end
134
+
92
135
  def render(index, **opts)
93
136
  return unless @item_renderer
94
- return unless @list[index] # TODO...?
137
+ return Ektoplayer::Application.log(self, 'render todo') unless @list[index]
138
+
139
+ opts[:selection] = (@selection.started? and (
140
+ opts[:selected] or index.between?(
141
+ [@selection.start_pos, @selected].min,
142
+ [@selection.start_pos, @selected].max
143
+ )
144
+ )
145
+ )
146
+
95
147
  @item_renderer.render(@win, @list[index], index, **opts)
96
148
  end
97
149
 
@@ -119,56 +171,64 @@ module UI
119
171
 
120
172
  def selected=(new_index)
121
173
  fail ArgumentError unless new_index
122
- new_index = new_index.clamp(0, index_last)
123
- return if @selected == new_index or @list.empty?
174
+ old_index_bottom = index_bottom
175
+ old_index_top = index_top
176
+ old_selected, @selected = @selected, new_index.clamp(0, index_last)
177
+ return if old_selected == @selected or @list.empty?
124
178
 
125
179
  self.lock
126
180
 
127
- new_cursor = @cursor + new_index - @selected
181
+ old_cursor, new_cursor = @cursor, @cursor + @selected - old_selected
182
+
128
183
  if new_cursor.between?(0, @size.height - 1)
129
184
  # new selected item resides in current screen,
130
185
  # just want_redraw the old line and the newly selected one
131
- write_at(@cursor); render(@selected)
186
+ @cursor = new_cursor
187
+
188
+ # redraw whole screen in selection mode!
189
+ return want_redraw if @selection.started?
190
+
191
+ write_at(old_cursor); render(old_selected)
132
192
  write_at(new_cursor); render(new_index, selected: true)
133
- @selected, @cursor = new_index, new_cursor
134
193
  _check
135
194
  want_refresh
136
195
  elsif (new_cursor.between?(-(@size.height - 1), (2 * @size.height - 1)))
137
196
  # new selected item is max a half screen size away
138
- if new_index < @selected
139
- if lines_after_cursor > (@selected - new_index)
140
- write_at(@cursor); render(@selected)
197
+ if @selected < old_selected
198
+ if lines_after_cursor > (old_selected - @selected)
199
+ write_at(old_cursor); render(old_selected)
141
200
  end
142
201
 
143
- (index_top - 1).downto(new_index + 1).each do |index|
202
+ (old_index_top - 1).downto(@selected + 1).each do |index|
144
203
  @win.insert_top; render(index)
145
204
  end
146
205
 
147
- @win.insert_top; render(new_index, selected: true)
148
- @selected, @cursor = new_index, 0
206
+ @win.insert_top; render(@selected, selected: true)
207
+ @cursor = 0
149
208
  _check
150
209
  else
151
- if lines_before_cursor > (new_index - @selected)
152
- write_at(@cursor); render(@selected)
210
+ if lines_before_cursor > (@selected - old_selected)
211
+ write_at(old_cursor); render(old_selected)
153
212
  end
154
213
 
155
- (index_bottom + 1).upto(new_index - 1).each do |index|
214
+ (old_index_bottom + 1).upto(@selected - 1).each do |index|
156
215
  @win.append_bottom; render(index)
157
216
  end
158
217
 
159
- @win.append_bottom; render(new_index, selected: true)
160
- @selected, @cursor = new_index, cursor_max
218
+ @win.append_bottom; render(@selected, selected: true)
219
+ @cursor = cursor_max
161
220
  _check
162
221
  end
163
222
 
164
223
  want_refresh
165
224
  else
166
- @selected = new_index
167
- @cursor = new_index.clamp(0, cursor_max)
225
+ #@selected = new_index
226
+ @cursor = new_index.clamp(0, cursor_max) # todo new_index<>new_cursor? ne muess scho pasn
168
227
  _check
169
228
  want_redraw
170
229
  end
171
230
 
231
+ ensure
172
232
  self.unlock
173
233
  end
174
234
 
@@ -179,12 +239,17 @@ module UI
179
239
  return if (new_cursor == @cursor) or @list.empty?
180
240
 
181
241
  with_lock do
182
- new_index = (@selected - (@cursor - new_cursor)).clamp(0, index_last)
183
- write_at(@cursor); render(@selected)
184
- write_at(new_cursor); render(new_index, selected: true)
185
- @selected, @cursor = new_index, new_cursor
242
+ old_cursor, @cursor = @cursor, new_cursor
243
+ old_selected, @selected = @selected, (@selected - (old_cursor - @cursor)).clamp(0, index_last)
186
244
  _check
187
- want_refresh
245
+
246
+ if @selection.started?
247
+ want_redraw
248
+ else
249
+ write_at(old_cursor); render(old_selected)
250
+ write_at(new_cursor); render(@selected, selected: true)
251
+ want_refresh
252
+ end
188
253
  end
189
254
  end
190
255
 
@@ -211,23 +276,25 @@ module UI
211
276
  # list is already on top
212
277
  select_from_cursorpos((@cursor - n).clamp(0, cursor_max))
213
278
  elsif n < @size.height
279
+ old_index_top = index_top
280
+ old_selected, @selected = @selected, @selected - n
281
+
214
282
  if lines_after_cursor > n
215
- write_at(@cursor); render(@selected)
283
+ write_at(@cursor); render(old_selected)
216
284
  end
217
285
 
218
- (index_top - 1).downto(index_top - n).each do |index|
286
+ (old_index_top - 1).downto(old_index_top - n).each do |index|
219
287
  @win.insert_top; render(index)
220
288
  end
221
289
 
222
- @selected -= n
223
290
  write_at(@cursor); render(@selected, selected: true)
224
291
 
225
292
  _check
226
293
  want_refresh
227
294
  else
228
- @selected -= n
295
+ @selected -= n # TODO: move up?
229
296
  force_cursorpos(@cursor)
230
- _check
297
+ _check # todo: move up
231
298
  want_redraw
232
299
  end
233
300
 
@@ -244,15 +311,17 @@ module UI
244
311
  select_from_cursorpos((@cursor + n).clamp(0, cursor_max))
245
312
  _check
246
313
  elsif n < @size.height
314
+ old_index_bottom = index_bottom
315
+ old_selected, @selected = @selected, @selected + n
316
+
247
317
  if lines_before_cursor > n
248
- write_at(@cursor); render(@selected)
318
+ write_at(@cursor); render(old_selected)
249
319
  end
250
320
 
251
- (index_bottom + 1).upto(index_bottom + n).each do |index|
321
+ (old_index_bottom + 1).upto(old_index_bottom + n).each do |index|
252
322
  @win.append_bottom; render(index)
253
323
  end
254
324
 
255
- @selected += n
256
325
  write_at(@cursor); render(@selected, selected: true)
257
326
 
258
327
  _check
@@ -15,9 +15,8 @@ module Ektoplayer
15
15
  @win.erase
16
16
 
17
17
  Bindings.bindings.each do |widget, commands|
18
- @win.with_attr(Theme[:'help.widget_name']) do
19
- @win << "\n#{widget}\n"
20
- end
18
+ @win.attrset(Theme[:'help.widget_name'])
19
+ @win.addstr("\n#{widget}\n")
21
20
 
22
21
  commands.each do |name, keys|
23
22
  next if keys.empty?
@@ -29,15 +28,13 @@ module Ektoplayer
29
28
  @win.with_attr(Theme[:'help.key_name']) { @win << key }
30
29
  end
31
30
 
32
- @win.with_attr(Theme[:'help.command_name']) do
33
- @win.on_column(18).addstr(name.to_s)
34
- end
31
+ @win.attrset(Theme[:'help.command_name'])
32
+ @win.mvaddstr(@win.cury, 18, name.to_s)
35
33
 
36
- @win.with_attr(Theme[:'help.command_desc']) do
37
- @win.on_column(43).addstr(Bindings.commands[name.to_sym])
38
- end
34
+ @win.attrset(Theme[:'help.command_desc'])
35
+ @win.mvaddstr(@win.cury, 43, Bindings.commands[name.to_sym])
39
36
 
40
- @win.addch(?\n)
37
+ @win.next_line
41
38
  end
42
39
  end
43
40
  end
@@ -25,55 +25,47 @@ module Ektoplayer
25
25
  end
26
26
 
27
27
  def draw_heading(heading)
28
- @win.with_attr(Theme[:'info.head']) do
29
- @win.next_line.from_left(START_HEADING).addstr(heading)
30
- end
28
+ @win.attrset(Theme[:'info.head'])
29
+ @win.mvaddstr(@win.cury + 1, START_HEADING, heading)
31
30
  end
32
31
 
33
32
  def draw_tag(tag, value=nil)
34
- @win.with_attr(Theme[:'info.tag']) do
35
- @win.next_line.from_left(START_TAG).addstr(tag)
36
- end
33
+ @win.attrset(Theme[:'info.tag'])
34
+ @win.mvaddstr(@win.cury + 1, START_TAG, tag)
37
35
 
38
- @win.from_left(START_TAG_VALUE)
39
- @win.with_attr(Theme[:'info.value']) do
40
- @win.addstr(value.to_s)
41
- end
36
+ @win.attrset(Theme[:'info.value'])
37
+ @win.mvaddstr(@win.cury, START_TAG_VALUE, value.to_s)
42
38
  end
43
39
 
44
40
  def draw_info(string, value=nil)
45
- @win.with_attr(Theme[:'info.tag']) do
46
- @win.next_line.from_left(START_INFO).addstr(string)
47
- end
41
+ @win.attrset(Theme[:'info.tag'])
42
+ @win.mvaddstr(@win.cury + 1, START_INFO, string)
48
43
 
49
- @win.from_left(START_INFO_VALUE)
50
- @win.with_attr(Theme[:'info.value']) do
51
- @win.addstr(value.to_s)
52
- end
44
+ @win.attrset(Theme[:'info.value'])
45
+ @win.mvaddstr(@win.cury, START_INFO_VALUE, value.to_s)
53
46
  end
54
47
 
55
48
  def draw_url(url, title=nil)
56
49
  title ||= url
57
50
  mevent = with_mouse_section_event do
58
- @win.with_attr(Theme[:url]) { @win << title }
51
+ @win.attrset(Theme[:url])
52
+ @win << title
59
53
  end
60
- mevent.on(Curses::BUTTON1_CLICKED) do
54
+ mevent.on(ICurses::BUTTON1_CLICKED) do
61
55
  Common::open_url_extern(url)
62
56
  end
63
57
  end
64
58
 
65
59
  def draw_download(file, percent, error)
66
- @win.with_attr(Theme[:'info.download.file']) do
67
- @win.next_line.from_left(START_TAG).addstr(file)
68
- end
69
- @win.with_attr(Theme[:'info.download.percent']) do
70
- @win.addstr(" #{percent}")
71
- end
60
+ @win.attrset(Theme[:'info.download.file'])
61
+ @win.mvaddstr(@win.cury + 1, START_TAG, file)
62
+
63
+ @win.attrset(Theme[:'info.download.percent'])
64
+ @win.addstr(" #{percent}")
72
65
 
73
66
  if error
74
- @win.with_attr(Theme[:'info.download.error']) do
75
- @win.addstr(" #{error}")
76
- end
67
+ @win.attrset(Theme[:'info.download.error'])
68
+ @win.addstr(" #{error}")
77
69
  end
78
70
  end
79
71
 
@@ -87,7 +79,7 @@ module Ektoplayer
87
79
 
88
80
  mouse_section.clear
89
81
  @win.erase
90
- @win.setpos(0,0)
82
+ @win.move(0,0)
91
83
 
92
84
  if @track = (@playlist[@playlist.current_playing] rescue nil)
93
85
  draw_heading('Current track')
@@ -123,30 +115,29 @@ module Ektoplayer
123
115
  draw_heading('Description')
124
116
  line_length = START_TAG
125
117
  wrap_length = @size.width.clamp(1, LINE_WRAP)
126
- @win.next_line.from_left(START_TAG)
118
+ @win.move(@win.cury + 1, START_TAG)
127
119
 
128
120
  Nokogiri::HTML("<p>#{@track['description']}</p>").css(?p).each do |p|
129
121
  p.children.each do |element|
130
122
  if element[:href]
131
123
  if (line_length += element.text.size) > wrap_length
132
- @win.next_line.from_left(START_TAG)
124
+ @win.move(@win.cury + 1, START_TAG)
133
125
  line_length = START_TAG
134
126
  end
135
127
 
136
128
  draw_url(element[:href], element.text.strip)
137
- @win.addch(' ')
129
+ @win.addch(32) # ' '
138
130
  else
139
131
  element.text.split(' ').each do |text|
140
132
  if (line_length += text.size) > wrap_length
141
- @win.next_line.from_left(START_TAG)
133
+ @win.move(@win.cury + 1, START_TAG)
142
134
  line_length = START_TAG
143
135
  end
144
136
 
145
- @win.with_attr(Theme[:'info.description']) do
146
- @win.mv_left(1) if text =~ /^[\.,:;]$/
147
- @win << text
148
- @win << ' '
149
- end
137
+ @win.attrset(Theme[:'info.description'])
138
+ @win.mv_left(1) if text =~ /^[\.,:;]$/
139
+ @win << text
140
+ @win.addch(32) # ' '
150
141
  end
151
142
  end
152
143
  end