ektoplayer 0.1.6 → 0.1.11

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