ektoplayer 0.1.0

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 (51) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +49 -0
  3. data/bin/ektoplayer +7 -0
  4. data/lib/ektoplayer.rb +10 -0
  5. data/lib/ektoplayer/application.rb +148 -0
  6. data/lib/ektoplayer/bindings.rb +230 -0
  7. data/lib/ektoplayer/browsepage.rb +138 -0
  8. data/lib/ektoplayer/client.rb +18 -0
  9. data/lib/ektoplayer/common.rb +91 -0
  10. data/lib/ektoplayer/config.rb +247 -0
  11. data/lib/ektoplayer/controllers/browser.rb +47 -0
  12. data/lib/ektoplayer/controllers/controller.rb +9 -0
  13. data/lib/ektoplayer/controllers/help.rb +21 -0
  14. data/lib/ektoplayer/controllers/info.rb +22 -0
  15. data/lib/ektoplayer/controllers/mainwindow.rb +40 -0
  16. data/lib/ektoplayer/controllers/playlist.rb +60 -0
  17. data/lib/ektoplayer/database.rb +199 -0
  18. data/lib/ektoplayer/events.rb +56 -0
  19. data/lib/ektoplayer/models/browser.rb +127 -0
  20. data/lib/ektoplayer/models/database.rb +49 -0
  21. data/lib/ektoplayer/models/model.rb +15 -0
  22. data/lib/ektoplayer/models/player.rb +28 -0
  23. data/lib/ektoplayer/models/playlist.rb +72 -0
  24. data/lib/ektoplayer/models/search.rb +42 -0
  25. data/lib/ektoplayer/models/trackloader.rb +17 -0
  26. data/lib/ektoplayer/mp3player.rb +151 -0
  27. data/lib/ektoplayer/operations/browser.rb +19 -0
  28. data/lib/ektoplayer/operations/operations.rb +26 -0
  29. data/lib/ektoplayer/operations/player.rb +11 -0
  30. data/lib/ektoplayer/operations/playlist.rb +67 -0
  31. data/lib/ektoplayer/theme.rb +102 -0
  32. data/lib/ektoplayer/trackloader.rb +146 -0
  33. data/lib/ektoplayer/ui.rb +404 -0
  34. data/lib/ektoplayer/ui/colors.rb +105 -0
  35. data/lib/ektoplayer/ui/widgets.rb +195 -0
  36. data/lib/ektoplayer/ui/widgets/container.rb +125 -0
  37. data/lib/ektoplayer/ui/widgets/labelwidget.rb +43 -0
  38. data/lib/ektoplayer/ui/widgets/listwidget.rb +332 -0
  39. data/lib/ektoplayer/ui/widgets/tabbedcontainer.rb +110 -0
  40. data/lib/ektoplayer/updater.rb +77 -0
  41. data/lib/ektoplayer/views/browser.rb +25 -0
  42. data/lib/ektoplayer/views/help.rb +46 -0
  43. data/lib/ektoplayer/views/info.rb +208 -0
  44. data/lib/ektoplayer/views/mainwindow.rb +64 -0
  45. data/lib/ektoplayer/views/playinginfo.rb +135 -0
  46. data/lib/ektoplayer/views/playlist.rb +39 -0
  47. data/lib/ektoplayer/views/progressbar.rb +51 -0
  48. data/lib/ektoplayer/views/splash.rb +99 -0
  49. data/lib/ektoplayer/views/trackrenderer.rb +137 -0
  50. data/lib/ektoplayer/views/volumemeter.rb +74 -0
  51. metadata +164 -0
@@ -0,0 +1,105 @@
1
+ require 'curses'
2
+
3
+ module UI
4
+ class ColorFader
5
+ def initialize(colors)
6
+ @colors = colors.map { |attrs| UI::Colors.set(nil, *attrs) }
7
+ end
8
+
9
+ def fade(size) ColorFader._fade(@colors, size) end
10
+ def fade2(size) ColorFader._fade2(@colors, size) end
11
+
12
+ def ColorFader._fade(colors, size)
13
+ return [] if size < 1
14
+
15
+ part_len = (size / colors.size)
16
+ diff = size - part_len * colors.size
17
+
18
+ (colors.size - 1).times.map do |color_i|
19
+ [colors[color_i]] * part_len
20
+ end.flatten.concat( [colors[-1]] * (part_len + diff) )
21
+ end
22
+
23
+ def ColorFader._fade2(colors, size)
24
+ half = size / 2
25
+ ColorFader._fade(colors, half) + ColorFader._fade(colors, size - half).reverse
26
+ end
27
+ end
28
+
29
+ class Colors
30
+ COLORS = {
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
40
+ }
41
+ COLORS.default_proc = proc do |h, key|
42
+ fail "Unknown color #{key}" unless key.is_a?Integer
43
+ key
44
+ end
45
+ COLORS.freeze
46
+
47
+ ATTRIBUTES = {
48
+ bold: Curses::A_BOLD, standout: Curses::A_STANDOUT,
49
+ blink: Curses::A_BLINK, underline: Curses::A_UNDERLINE
50
+ }
51
+ ATTRIBUTES.default_proc = proc { |h,k| k }
52
+ ATTRIBUTES.freeze
53
+
54
+ def self.start
55
+ @@id ||= 1
56
+ @@aliases ||= {}
57
+ @@volatile ||= {}
58
+ @@volatile_ids ||= {}
59
+ @@cached ||= Hash.new { |h,k| h[k] = {} }
60
+ end
61
+ def self.reset; self.start end
62
+
63
+ def self.init_pair_cached(fg, bg)
64
+ fg, bg = COLORS[fg], COLORS[bg]
65
+
66
+ unless id = @@cached[fg][bg]
67
+ id = @@cached[fg][bg] = @@id
68
+ @@id += 1
69
+ end
70
+
71
+ Curses.init_pair(id, fg, bg) or fail
72
+ Curses.color_pair(id)
73
+ end
74
+
75
+ def self.add_attributes(*attrs)
76
+ flags = 0
77
+ attrs.each { |attr| flags |= ATTRIBUTES[attr] }
78
+ flags
79
+ end
80
+
81
+ def self.[](name) @@aliases[name] || 0 end
82
+ def self.get(name) @@aliases[name] || 0 end
83
+
84
+ def self.set(name, fg, bg = -1, *attrs)
85
+ @@aliases[name] = self.init_pair_cached(fg, bg)
86
+ attrs.each { |attr| @@aliases[name] |= ATTRIBUTES[attr] }
87
+ @@aliases[name]
88
+ end
89
+
90
+ def self.get_volatile(name)
91
+ @@volatile[name] || 0
92
+ end
93
+
94
+ def self.set_volatile(name, fg, bg)
95
+ fg, bg = COLORS[fg], COLORS[bg]
96
+
97
+ unless id = @@volatile_ids[name]
98
+ id = @@volatile_ids[name] = @@id
99
+ @@id += 1
100
+ end
101
+
102
+ @@volatile[name] = Curses.init_pair(id, fg, bg)
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,195 @@
1
+ module UI
2
+ class Widget
3
+ WANT_REFRESH, WANT_REDRAW, WANT_LAYOUT = 1, 2, 4
4
+
5
+ attr_reader :pos, :size
6
+ def keys; @keys ||= Events.new end
7
+ def events; @events ||= Events.new end
8
+ def mouse; @mouse ||= MouseEvents.new end
9
+ def mouse_section; @mouse_section ||= MouseSectionEvents.new end
10
+
11
+ def initialize(parent: nil, size: nil, pos: nil, visible: true)
12
+ if !parent and (!size or !pos)
13
+ fail ArgumentError, "must provide 'size:' and 'pos:' if 'parent:' is nil"
14
+ end
15
+
16
+ @parent, @visible, = parent, visible
17
+ @size = (size or @parent.size.dup)
18
+ @pos = (pos or @parent.pos.dup)
19
+ @want, @lock = WANT_LAYOUT, Monitor.new
20
+ end
21
+
22
+ # Proxy method for creating a new widget object with the current
23
+ # object as parent.
24
+ def sub(class_type, **opts)
25
+ class_type.new(parent: self, **opts)
26
+ end
27
+
28
+ # This method should be used each time a widget may modify
29
+ # its window contents.
30
+ #
31
+ # It ensures that operations that modify the window (such
32
+ # as draw, layout and refresh) are executed once and only once at the
33
+ # end of this function.
34
+ def with_lock
35
+ held_locks = lock; yield
36
+ ensure
37
+ unlock
38
+ end
39
+
40
+ def lock
41
+ @lock.enter
42
+ end
43
+
44
+ def unlock
45
+ return unless (@lock.exit rescue nil) or not visible?
46
+
47
+ layout if @want >= WANT_LAYOUT
48
+ draw if @want >= WANT_REDRAW
49
+ if @want >= WANT_REFRESH
50
+ Canvas.update_screen
51
+ end
52
+
53
+ @want = 0
54
+ end
55
+
56
+ def display(force_refresh=false, force_redraw=false)
57
+ return if not visible?
58
+ layout if @want >= WANT_LAYOUT
59
+ draw if @want >= WANT_REDRAW or force_redraw
60
+ refresh if @want >= WANT_REFRESH or force_refresh
61
+ end
62
+
63
+ def want_redraw; @want |= WANT_REDRAW end
64
+ def want_layout; @want |= WANT_LAYOUT end
65
+ def want_refresh; @want |= WANT_REFRESH end
66
+
67
+ def invisible?; !visible? end
68
+ def visible!; self.visible=(true) end
69
+ def invisible!; self.visible=(false) end
70
+ def visible?; @visible and (!@parent or @parent.visible?) end
71
+
72
+ def visible=(new)
73
+ return if @visible == new
74
+ with_lock { @visible = new; want_refresh }
75
+ end
76
+
77
+ def size=(size)
78
+ return if @size == size
79
+ with_lock { @size = size; want_layout }
80
+ end
81
+
82
+ def pos=(pos)
83
+ return if @pos == pos
84
+ with_lock { @pos = pos; want_layout }
85
+ end
86
+
87
+ def mouse_event_transform(mevent)
88
+ if mevent.y >= @pos.y and mevent.x >= @pos.x and
89
+ mevent.y < (@pos.y + @size.height) and
90
+ mevent.x < (@pos.x + @size.width)
91
+ new_mouse = mevent.to_fake
92
+ new_mouse.update!(y: mevent.y - @pos.y, x: mevent.x - @pos.x)
93
+ new_mouse
94
+ end
95
+ end
96
+
97
+ def key_press(key) on_key_press(key) end
98
+ def raise_widget(widget) on_widget_raise(widget) end
99
+ def on_key_press(key) trigger(@keys, key) end
100
+
101
+ def mouse_click(mevent)
102
+ if new_event = mouse_event_transform(mevent)
103
+ trigger(@mouse, new_event)
104
+ trigger(@mouse_event, new_event)
105
+ end
106
+ end
107
+
108
+ def draw; fail NotImplementedError end
109
+ def refresh; fail NotImplementedError end
110
+ def layout; fail NotImplementedError end
111
+
112
+ protected def trigger(event_obj, event_name, *event_args)
113
+ event_obj.trigger(event_name, *event_args) if event_obj
114
+ end
115
+
116
+ def on_widget_raise(widget)
117
+ fail 'unhandled widget raise' unless @parent
118
+ @parent.raise_widget(widget)
119
+ end
120
+ end
121
+
122
+ class Window < Widget
123
+ attr_reader :win
124
+
125
+ def initialize(**opts)
126
+ super(**opts)
127
+ @win = Curses::Window.new(@size.height, @size.width, @pos.y, @pos.x)
128
+ @win.keypad=(true)
129
+ end
130
+
131
+ def layout
132
+ fail WidgetSizeError if @size.height < 1 or @size.width < 1
133
+ @win.size=(@size)
134
+ @win.pos=(@pos)
135
+ end
136
+
137
+ def refresh
138
+ @win.noutrefresh
139
+ end
140
+ end
141
+
142
+ class Pad < Widget
143
+ attr_reader :win
144
+
145
+ def initialize(**opts)
146
+ super(**opts)
147
+ @win = Curses::Pad.new(@size.height, @size.width)
148
+ @pad_minrow = @pad_mincol = 0
149
+ end
150
+
151
+ def pad_minrow=(n)
152
+ return if @pad_minrow == n
153
+ with_lock { @pad_minrow = n; want_refresh }
154
+ end
155
+
156
+ def pad_mincol=(n)
157
+ return if @pad_mincol == n
158
+ with_lock { @pad_mincol = n; want_refresh }
159
+ end
160
+
161
+ def pad_size=(s)
162
+ @win.size=(s)
163
+ end
164
+
165
+ def layout
166
+ @win.pos=(@pos)
167
+ end
168
+
169
+ def top; self.pad_minrow=(0) end
170
+ def page_up; self.up(@size.height / 2) end
171
+ def page_down; self.down(@size.height / 2) end
172
+
173
+ def bottom
174
+ self.pad_minrow=(@win.height - @size.height)
175
+ end
176
+
177
+ def up(n=1)
178
+ new_minrow = (@pad_minrow - n).clamp(0, @win.height)
179
+ self.pad_minrow=(new_minrow)
180
+ end
181
+
182
+ def down(n=1)
183
+ new_minrow = (@pad_minrow + n).clamp(0, (@win.height - @size.height)) rescue 0
184
+ self.pad_minrow=(new_minrow)
185
+ end
186
+
187
+ def refresh
188
+ @win.noutrefresh(
189
+ @pad_minrow, @pad_mincol,
190
+ @pos.y, @pos.x,
191
+ @pos.y + @size.height - 1, @pos.x + @size.width - 1
192
+ )
193
+ end
194
+ end
195
+ end
@@ -0,0 +1,125 @@
1
+ require_relative '../widgets'
2
+
3
+ module UI
4
+ class GenericContainer < Widget
5
+ attr_reader :selected, :selected_index, :widgets
6
+
7
+ def initialize(widgets: [], **opts)
8
+ super(**opts)
9
+ @selected, @selected_index, @widgets = nil, nil, widgets
10
+ end
11
+
12
+ def visible_widgets
13
+ @widgets.select(&:visible?)
14
+ end
15
+
16
+ def selected_index=(index)
17
+ return if @selected_index == index
18
+
19
+ with_lock do
20
+ if index
21
+ unless @selected = @widgets[index]
22
+ fail KeyError, "#{self.class}: #{@widgets.size} #{index}"
23
+ end
24
+ end
25
+
26
+ @selected_index = index
27
+ want_layout
28
+ end
29
+ end
30
+
31
+ def selected=(widget)
32
+ return if @selected.equal?(widget)
33
+
34
+ with_lock do
35
+ if widget
36
+ unless @selected_index = @widgets.index(widget)
37
+ fail KeyError
38
+ end
39
+ end
40
+
41
+ @selected = widget
42
+ want_layout
43
+ end
44
+ end
45
+
46
+ def win
47
+ (@selected or UI::Canvas).win
48
+ end
49
+
50
+ def add(widget)
51
+ with_lock do
52
+ @widgets << widget
53
+ self.selected=(widget) unless @selected
54
+ want_layout # important: layout, not redraw
55
+ end
56
+ end
57
+
58
+ def remove(widget)
59
+ with_lock do
60
+ self.selected=(nil) if @selected.equal?(widget)
61
+ @widgets.delete widget
62
+ want_layout # important: layout, not redraw
63
+ end
64
+ end
65
+
66
+ def mouse_click(mevent)
67
+ visible_widgets.each { |w| w.mouse_click(mevent) }
68
+ super(mevent)
69
+ end
70
+
71
+ def draw; visible_widgets.each(&:draw) end
72
+ def refresh; visible_widgets.each(&:refresh) end
73
+ def layout; visible_widgets.each(&:layout) end
74
+
75
+ def on_key_press(key)
76
+ @selected.key_press(key) if @selected
77
+ super(key)
78
+ end
79
+
80
+ def next
81
+ return unless @selected
82
+ self.selected_index=((@selected_index + 1) % @widgets.size)
83
+ end
84
+
85
+ def prev
86
+ return unless @selected
87
+ return self.selected_index=(@widgets.size - 1) if @selected_index == 0
88
+ self.selected_index=(@selected_index - 1)
89
+ end
90
+ end
91
+
92
+ class HorizontalContainer < GenericContainer
93
+ def layout
94
+ xoff = 0
95
+ visible_widgets.each do |widget|
96
+ widget.with_lock do
97
+ fail WidgetSizeError if xoff + widget.size.width > @size.width
98
+ widget.size=(widget.size.update(height: @size.height))
99
+ widget.pos=(@pos.calc(x: xoff))
100
+ fail WidgetSizeError if widget.size.height > @size.height
101
+ xoff += (widget.size.width + (@pad or 0))
102
+ end
103
+ end
104
+
105
+ super
106
+ end
107
+ end
108
+
109
+ class VerticalContainer < GenericContainer
110
+ def layout
111
+ yoff = 0
112
+ visible_widgets.each do |widget|
113
+ widget.with_lock do
114
+ widget.size=(widget.size.update(width: @size.width))
115
+ widget.pos=(@pos.calc(y: yoff))
116
+ fail WidgetSizeError if widget.size.width > @size.width
117
+ fail WidgetSizeError if yoff + widget.size.height > @size.height
118
+ yoff += widget.size.height
119
+ end
120
+ end
121
+
122
+ super
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,43 @@
1
+ require_relative '../widgets'
2
+
3
+ module UI
4
+ class LabelWidget < Window
5
+ attr_reader :text, :pad, :attributes
6
+
7
+ def initialize(text: '', attributes: 0, pad: {}, **opts)
8
+ super(**opts)
9
+ @text, @attributes, @pad = text.to_s, attributes, Hash.new(0)
10
+ @pad.update pad
11
+ end
12
+
13
+ def attributes=(new)
14
+ return if @attributes == new
15
+ with_lock { @attributes = new; want_redraw }
16
+ end
17
+
18
+ def text=(new)
19
+ return if @text == new
20
+ with_lock { @text = new.to_s; want_redraw }
21
+ end
22
+
23
+ def pad=(new)
24
+ return if @pad == new
25
+ with_lock { @pad.update!(new); want_redraw }
26
+ end
27
+
28
+ def draw
29
+ @win.erase
30
+ @text.split(?\n).each_with_index do |l, i|
31
+ @win.setpos(@pad[:top] + i, @pad[:left])
32
+ @win.attron(@attributes) { @win << l }
33
+ end
34
+ end
35
+
36
+ def fit
37
+ self.size=(Size.new(
38
+ height: @pad[:top] + @pad[:bottom] + 1 + @text.count(?\n),
39
+ width: @pad[:left] + @pad[:right] + @text.split(?\n).max.size
40
+ ))
41
+ end
42
+ end
43
+ end