ektoplayer 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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