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.
- checksums.yaml +7 -0
- data/README.md +49 -0
- data/bin/ektoplayer +7 -0
- data/lib/ektoplayer.rb +10 -0
- data/lib/ektoplayer/application.rb +148 -0
- data/lib/ektoplayer/bindings.rb +230 -0
- data/lib/ektoplayer/browsepage.rb +138 -0
- data/lib/ektoplayer/client.rb +18 -0
- data/lib/ektoplayer/common.rb +91 -0
- data/lib/ektoplayer/config.rb +247 -0
- data/lib/ektoplayer/controllers/browser.rb +47 -0
- data/lib/ektoplayer/controllers/controller.rb +9 -0
- data/lib/ektoplayer/controllers/help.rb +21 -0
- data/lib/ektoplayer/controllers/info.rb +22 -0
- data/lib/ektoplayer/controllers/mainwindow.rb +40 -0
- data/lib/ektoplayer/controllers/playlist.rb +60 -0
- data/lib/ektoplayer/database.rb +199 -0
- data/lib/ektoplayer/events.rb +56 -0
- data/lib/ektoplayer/models/browser.rb +127 -0
- data/lib/ektoplayer/models/database.rb +49 -0
- data/lib/ektoplayer/models/model.rb +15 -0
- data/lib/ektoplayer/models/player.rb +28 -0
- data/lib/ektoplayer/models/playlist.rb +72 -0
- data/lib/ektoplayer/models/search.rb +42 -0
- data/lib/ektoplayer/models/trackloader.rb +17 -0
- data/lib/ektoplayer/mp3player.rb +151 -0
- data/lib/ektoplayer/operations/browser.rb +19 -0
- data/lib/ektoplayer/operations/operations.rb +26 -0
- data/lib/ektoplayer/operations/player.rb +11 -0
- data/lib/ektoplayer/operations/playlist.rb +67 -0
- data/lib/ektoplayer/theme.rb +102 -0
- data/lib/ektoplayer/trackloader.rb +146 -0
- data/lib/ektoplayer/ui.rb +404 -0
- data/lib/ektoplayer/ui/colors.rb +105 -0
- data/lib/ektoplayer/ui/widgets.rb +195 -0
- data/lib/ektoplayer/ui/widgets/container.rb +125 -0
- data/lib/ektoplayer/ui/widgets/labelwidget.rb +43 -0
- data/lib/ektoplayer/ui/widgets/listwidget.rb +332 -0
- data/lib/ektoplayer/ui/widgets/tabbedcontainer.rb +110 -0
- data/lib/ektoplayer/updater.rb +77 -0
- data/lib/ektoplayer/views/browser.rb +25 -0
- data/lib/ektoplayer/views/help.rb +46 -0
- data/lib/ektoplayer/views/info.rb +208 -0
- data/lib/ektoplayer/views/mainwindow.rb +64 -0
- data/lib/ektoplayer/views/playinginfo.rb +135 -0
- data/lib/ektoplayer/views/playlist.rb +39 -0
- data/lib/ektoplayer/views/progressbar.rb +51 -0
- data/lib/ektoplayer/views/splash.rb +99 -0
- data/lib/ektoplayer/views/trackrenderer.rb +137 -0
- data/lib/ektoplayer/views/volumemeter.rb +74 -0
- metadata +164 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 6f7e3bffc50021abf9f3c8384db4c81952abb036
|
4
|
+
data.tar.gz: a7d7612d2175422b4e1f59d01e6c3992915bbb6c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 788048c6464ae80c0d1d1ef2d0a85a307787ab8d8b7116cb4f2bf934223087238d5eb2af9ce48973899a248daee415aca40a23cf3a63c80d2554ab5a8fd7c8a7
|
7
|
+
data.tar.gz: 16bb67c65cd1b0b6c530d4f6e74f36b32d3a727e44125c8ba29c1b5e1d467362ec36d5b81ceb7a6ce5aa223d4cedd6accd8ea354b2f871d765fd581c1b82cd1a
|
data/README.md
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# Ektoplayer
|
2
|
+
|
3
|
+
Ektoplayer is a console audio player for [ektplazm.com](http://www.ektoplazm.com).
|
4
|
+
|
5
|
+

|
6
|
+

|
7
|
+
|
8
|
+
## Requirements
|
9
|
+
|
10
|
+
* Ruby (1.9)
|
11
|
+
* Portaudio (19)
|
12
|
+
* Mpg123 (1.14)
|
13
|
+
* Sqlite3
|
14
|
+
|
15
|
+
## Installation
|
16
|
+
|
17
|
+
Assuming you have Ruby/Rubygems installed, you need portaudio and mpg123 as
|
18
|
+
library to compile the native extensions.
|
19
|
+
|
20
|
+
### Arch Linux
|
21
|
+
|
22
|
+
pacman -S portaudio mpg123 ncurses ruby sqlite
|
23
|
+
gem install ektoplayer
|
24
|
+
|
25
|
+
### Debian / Ubuntu (not yet tested)
|
26
|
+
|
27
|
+
apt-get install portaudio19-dev libmpg123-dev libncurses-dev ruby1.9.1-dev sqlite-??
|
28
|
+
gem install ektoplayer
|
29
|
+
|
30
|
+
## Features
|
31
|
+
|
32
|
+
* Listen to ektoplazm tracks
|
33
|
+
* Download whole albums
|
34
|
+
* Browse database by tags
|
35
|
+
* Vi keybindings (`hjkl`, `^d`, `^u`, `/`, `?`, `n`, `N`, `:`, ...)
|
36
|
+
* Mouse is supported
|
37
|
+
* Supports 256/16/mono colors
|
38
|
+
* Local sound file cache and download archive
|
39
|
+
* Highly configurable
|
40
|
+
|
41
|
+
## Authors
|
42
|
+
|
43
|
+
* [Benjamin Abendroth](https://github.com/braph)
|
44
|
+
|
45
|
+
## See also
|
46
|
+
|
47
|
+
* Ektoplayer was inspired by [Soundcloud2000](https://github.com/grobie/soundcloud2000) and [ncmpcpp](https://github.com/arybczak/ncmpcpp)
|
48
|
+
* It uses [Audite](https://github.com/georgi/audite) as playback engine and [Nokogiri](http://www.nokogiri.org/) for parsing HTML
|
49
|
+
|
data/bin/ektoplayer
ADDED
data/lib/ektoplayer.rb
ADDED
@@ -0,0 +1,148 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'date'
|
3
|
+
|
4
|
+
{ '.' => %w(config theme bindings client common ui),
|
5
|
+
'views' => %w(mainwindow),
|
6
|
+
'models' => %w(player browser playlist database trackloader),
|
7
|
+
'operations' => %w(operations player browser playlist),
|
8
|
+
'controllers' => %w(mainwindow browser playlist info help)
|
9
|
+
}.each { |d,files| files.each { |f| require_relative "#{d}/#{f}" } }
|
10
|
+
|
11
|
+
module Ektoplayer
|
12
|
+
class Application
|
13
|
+
VERSION = '0.1'.freeze
|
14
|
+
GITHUB_URL = 'https://github.com/braph/ektoplayer'.freeze
|
15
|
+
EKTOPLAZM_URL = 'http://www.ektoplazm.com'.freeze
|
16
|
+
|
17
|
+
CONFIG_DIR = File.join(Dir.home, '.config', 'ektoplayer').freeze
|
18
|
+
CONFIG_FILE = File.join(CONFIG_DIR, 'ektoplayer.rc').freeze
|
19
|
+
|
20
|
+
def self.log(from, *msgs)
|
21
|
+
func = caller[0][/`.*'/][1..-2]
|
22
|
+
from = from.class unless from.is_a?String
|
23
|
+
$stderr.puts("#{DateTime.now.rfc3339} #{from}.#{func}: " + msgs.join(' '))
|
24
|
+
|
25
|
+
if e = msgs.select { |m| m.kind_of?Exception }[0]
|
26
|
+
$stderr.puts "#{e.backtrace.first}: #{e.message} (#{e.class})", e.backtrace.drop(1).map{|s| "\t#{s}"}
|
27
|
+
end
|
28
|
+
$stderr.flush
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.open_log(file)
|
32
|
+
$stderr.reopen(file, ?a)
|
33
|
+
$stderr.sync=(true)
|
34
|
+
end
|
35
|
+
|
36
|
+
def run
|
37
|
+
#Thread.abort_on_exception=(true)
|
38
|
+
|
39
|
+
# make each configuration object globally accessible as a singleton
|
40
|
+
[Config, Bindings, Theme].each { |c| Common::mksingleton(c) }
|
41
|
+
|
42
|
+
if File.file? Config::CONFIG_FILE
|
43
|
+
Config.parse(Config::CONFIG_FILE, Bindings, Theme) rescue (
|
44
|
+
fail "Config: #{$!}"
|
45
|
+
)
|
46
|
+
end
|
47
|
+
|
48
|
+
FileUtils::mkdir_p Config::CONFIG_DIR rescue (
|
49
|
+
fail "Could not create config dir: #{$!}"
|
50
|
+
)
|
51
|
+
|
52
|
+
Application.open_log(Config[:log_file])
|
53
|
+
|
54
|
+
if Config[:use_cache]
|
55
|
+
unless File.directory? Config[:cache_dir]
|
56
|
+
FileUtils::mkdir Config[:cache_dir] rescue (
|
57
|
+
fail "Could not create cache dir: #{$!}"
|
58
|
+
)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
[:temp_dir, :download_dir, :archive_dir].each do |key|
|
63
|
+
unless File.directory? Config[key]
|
64
|
+
FileUtils::mkdir Config[key] rescue (
|
65
|
+
fail "Could not create #{key}: #{$!}"
|
66
|
+
)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
UI::Canvas.run do
|
71
|
+
if Config[:use_colors] == :auto
|
72
|
+
Theme.use_colors(ENV['TERM'] =~ /256/ ? 256 : 8)
|
73
|
+
else
|
74
|
+
Theme.use_colors(Config[:use_colors])
|
75
|
+
end
|
76
|
+
|
77
|
+
client = Client.new
|
78
|
+
|
79
|
+
# ... models ...
|
80
|
+
player = Models::Player.new(client)
|
81
|
+
browser = Models::Browser.new(client)
|
82
|
+
playlist = Models::Playlist.new
|
83
|
+
database = Models::Database.new(client)
|
84
|
+
trackloader = Models::Trackloader.new(client)
|
85
|
+
|
86
|
+
# ... operations ...
|
87
|
+
operations = Operations::Operations.new
|
88
|
+
operations.register(:quit, &method(:exit))
|
89
|
+
operations.register(:reload, &browser.method(:reload))
|
90
|
+
operations.register(:update, &database.method(:update))
|
91
|
+
operations.register(:refresh) { UI::Canvas.on_winch; UI::Canvas.update_screen(true) }
|
92
|
+
Operations::Player.new(operations, player)
|
93
|
+
Operations::Browser.new(operations, browser, playlist)
|
94
|
+
Operations::Playlist.new(operations, playlist, player, trackloader)
|
95
|
+
|
96
|
+
# ... views ...
|
97
|
+
main_w = UI::Canvas.sub(Views::MainWindow)
|
98
|
+
|
99
|
+
# next operations may take some time, espacially the ones
|
100
|
+
# using the database (browser), so we put this inside a thread
|
101
|
+
Thread.new do
|
102
|
+
# ... controllers ...
|
103
|
+
view_ops = Operations::Operations.new
|
104
|
+
Controllers::MainWindow.new(main_w, view_ops)
|
105
|
+
Controllers::Browser.new(main_w.browser, browser, view_ops, operations)
|
106
|
+
Controllers::Playlist.new(main_w.playlist, playlist, view_ops, operations)
|
107
|
+
Controllers::Help.new(main_w.help, view_ops)
|
108
|
+
Controllers::Info.new(main_w.info, playlist, trackloader, database, view_ops)
|
109
|
+
main_w.progressbar.attach(player)
|
110
|
+
main_w.volumemeter.attach(player)
|
111
|
+
main_w.playinginfo.attach(playlist, player)
|
112
|
+
|
113
|
+
# ... events ...
|
114
|
+
database.events.on(:update_finished, &browser.method(:reload ))
|
115
|
+
player.events.on(:stop) do |reason|
|
116
|
+
operations.send(:'playlist.play_next') if reason == :track_completed
|
117
|
+
end
|
118
|
+
|
119
|
+
# ... bindings ...
|
120
|
+
Bindings.bind_view(:global, main_w, view_ops, operations)
|
121
|
+
%w(splash playlist browser info help).each do |w|
|
122
|
+
Bindings.bind_view(w, main_w.send(w), view_ops, operations)
|
123
|
+
end
|
124
|
+
|
125
|
+
player.stop
|
126
|
+
|
127
|
+
# If database is empty, start an initial update
|
128
|
+
if browser.tracks(0).size < 1
|
129
|
+
operations.send(:update)
|
130
|
+
elsif (c = Config[:small_update_pages]) > 0
|
131
|
+
operations.send(:update, pages: c)
|
132
|
+
end
|
133
|
+
|
134
|
+
if (n = Config[:playlist_load_newest]) > 0
|
135
|
+
r = client.database.select(order_by: 'date', limit: n)
|
136
|
+
playlist.add(*r)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
UI::Canvas.update_screen
|
141
|
+
UI::Input.start_loop
|
142
|
+
end
|
143
|
+
rescue
|
144
|
+
puts "Error: #{e}"
|
145
|
+
Application.log(self, $!)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,230 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'curses'
|
3
|
+
|
4
|
+
module Ektoplayer
|
5
|
+
# Keybinding storage.
|
6
|
+
#
|
7
|
+
# Provides access for changing keybinds (with collision detection):
|
8
|
+
# bind(), unbind()
|
9
|
+
#
|
10
|
+
# Provides binding these keys to widgets
|
11
|
+
# bind_section_to_widget(), bind_to_widget()
|
12
|
+
#
|
13
|
+
class Bindings
|
14
|
+
attr_reader :bindings, :commands
|
15
|
+
|
16
|
+
def register(command, description)
|
17
|
+
@commands[command.to_sym] = description.freeze
|
18
|
+
end
|
19
|
+
alias :reg :register
|
20
|
+
|
21
|
+
def initialize
|
22
|
+
@commands = {}
|
23
|
+
|
24
|
+
reg 'quit', 'Quit the program'
|
25
|
+
reg 'refresh', 'Refresh the screen'
|
26
|
+
reg 'reload', 'Apply database changes to browser'
|
27
|
+
reg 'update', 'Start a database update'
|
28
|
+
|
29
|
+
reg 'player.stop', 'Stop playing'
|
30
|
+
reg 'player.toggle', 'Toggle play/pause'
|
31
|
+
reg 'player.forward', 'Seek forward'
|
32
|
+
reg 'player.backward', 'Seek backward'
|
33
|
+
|
34
|
+
reg 'tabs.next', 'Select next tab'
|
35
|
+
reg 'tabs.prev', 'Select previous tab'
|
36
|
+
|
37
|
+
reg 'browser.enter', 'Enter selected directory'
|
38
|
+
reg 'browser.add_to_playlist', 'Add tracks under cursor to playlist'
|
39
|
+
reg 'browser.back', 'Change to parent directory'
|
40
|
+
|
41
|
+
reg 'playinginfo.toggle', 'Toggle playinginfo visibility'
|
42
|
+
reg 'progressbar.toggle', 'Toggle progressbar visibility'
|
43
|
+
reg 'volumemeter.toggle', 'Toggle volumemeter visibility'
|
44
|
+
|
45
|
+
reg 'playlist.goto_current', 'Go to current playing track'
|
46
|
+
reg 'playlist.clear', 'Delete all items in playlist'
|
47
|
+
reg 'playlist.delete', 'Delete selected track from playlist'
|
48
|
+
reg 'playlist.download_album', 'Download album archive for selected track'
|
49
|
+
reg 'playlist.play', 'Play selected track'
|
50
|
+
reg 'playlist.reload', 'Force redownload of selected track'
|
51
|
+
reg 'playlist.play_next', 'Play next track in playlist'
|
52
|
+
reg 'playlist.play_prev', 'Play previous track in playlist'
|
53
|
+
|
54
|
+
%w(info help browser playlist splash).each do |w|
|
55
|
+
reg("#{w}.show", "Show #{w}")
|
56
|
+
end
|
57
|
+
|
58
|
+
{ up: 'Move cursor up', down: 'Move cursor down',
|
59
|
+
page_up: 'Scroll page up', page_down: 'Scroll page down',
|
60
|
+
top: 'Move to top', bottom: 'Move to bottom'
|
61
|
+
}.each do |cmd, desc|
|
62
|
+
%w(browser info help playlist).each { |w| reg("#{w}.#{cmd}", desc) }
|
63
|
+
end
|
64
|
+
|
65
|
+
{ search_up: 'Start search upwards', search_next: 'Goto next search result',
|
66
|
+
search_down: 'Start search downwards', search_prev: 'Goto previous search result'
|
67
|
+
}.each do |cmd, desc|
|
68
|
+
%w(browser playlist).each { |w| reg("#{w}.#{cmd}", desc) }
|
69
|
+
end
|
70
|
+
|
71
|
+
@bindings = {
|
72
|
+
global: {
|
73
|
+
'splash.show': [?`, ?^ ],
|
74
|
+
'playlist.show': [?1 ],
|
75
|
+
'browser.show': [?2 ],
|
76
|
+
'info.show': [?3 ],
|
77
|
+
'help.show': [?4 ],
|
78
|
+
|
79
|
+
'playinginfo.toggle': [ Curses::KEY_F2 ],
|
80
|
+
'progressbar.toggle': [ Curses::KEY_F3 ],
|
81
|
+
'volumemeter.toggle': [ Curses::KEY_F4 ],
|
82
|
+
|
83
|
+
'player.forward': [?f, Curses::KEY_RIGHT ],
|
84
|
+
'player.backward': [?b, Curses::KEY_LEFT ],
|
85
|
+
'player.stop': [?s ],
|
86
|
+
'player.toggle': [?p ],
|
87
|
+
|
88
|
+
'playlist.play_next': [?> ],
|
89
|
+
'playlist.play_prev': [?< ],
|
90
|
+
|
91
|
+
'tabs.next': [?l ],
|
92
|
+
'tabs.prev': [?h ],
|
93
|
+
|
94
|
+
quit: [?q ],
|
95
|
+
refresh: ['^l' ],
|
96
|
+
reload: ['^r' ],
|
97
|
+
update: [?U ]},
|
98
|
+
playlist: {
|
99
|
+
# movement
|
100
|
+
'playlist.top': [?g, Curses::KEY_HOME ],
|
101
|
+
'playlist.bottom': [?G, Curses::KEY_END ],
|
102
|
+
'playlist.up': [?k, Curses::KEY_UP ],
|
103
|
+
'playlist.down': [?j, Curses::KEY_DOWN ],
|
104
|
+
'playlist.page_down': ['^d', Curses::KEY_NPAGE ],
|
105
|
+
'playlist.page_up': ['^u', Curses::KEY_PPAGE ],
|
106
|
+
# search
|
107
|
+
'playlist.search_next': [?n ],
|
108
|
+
'playlist.search_prev': [?N ],
|
109
|
+
'playlist.search_up': [?? ],
|
110
|
+
'playlist.search_down': [?/ ],
|
111
|
+
# playlist
|
112
|
+
'playlist.play': [ Curses::KEY_ENTER ],
|
113
|
+
'playlist.download_album': [?$ ],
|
114
|
+
'playlist.reload': [?r ],
|
115
|
+
'playlist.goto_current': [?o ],
|
116
|
+
'playlist.clear': [?c ],
|
117
|
+
'playlist.delete': [?d ],
|
118
|
+
# other
|
119
|
+
'player.toggle': [' ' ]},
|
120
|
+
browser: {
|
121
|
+
# movement
|
122
|
+
'browser.top': [?g, Curses::KEY_HOME ],
|
123
|
+
'browser.bottom': [?G, Curses::KEY_END ],
|
124
|
+
'browser.up': [?k, Curses::KEY_UP ],
|
125
|
+
'browser.down': [?j, Curses::KEY_DOWN ],
|
126
|
+
'browser.page_up': ['^u', Curses::KEY_PPAGE ],
|
127
|
+
'browser.page_down': ['^d', Curses::KEY_NPAGE ],
|
128
|
+
# search
|
129
|
+
'browser.search_next': [?n ],
|
130
|
+
'browser.search_prev': [?N ],
|
131
|
+
'browser.search_up': [?? ],
|
132
|
+
'browser.search_down': [?/ ],
|
133
|
+
# browser
|
134
|
+
'browser.add_to_playlist': [' ', ?a ],
|
135
|
+
'browser.enter': [?E, Curses::KEY_ENTER ],
|
136
|
+
'browser.back': [?b, Curses::KEY_BACKSPACE ]},
|
137
|
+
help: {
|
138
|
+
'help.top': [?g, Curses::KEY_HOME ],
|
139
|
+
'help.bottom': [?G, Curses::KEY_END ],
|
140
|
+
'help.up': [?k, Curses::KEY_UP ],
|
141
|
+
'help.down': [?j, Curses::KEY_DOWN ],
|
142
|
+
'help.page_up': ['^u', Curses::KEY_PPAGE ],
|
143
|
+
'help.page_down': ['^d', Curses::KEY_NPAGE ]},
|
144
|
+
info: {
|
145
|
+
'info.top': [?g, Curses::KEY_HOME ],
|
146
|
+
'info.bottom': [?G, Curses::KEY_END ],
|
147
|
+
'info.up': [?k, Curses::KEY_UP ],
|
148
|
+
'info.down': [?j, Curses::KEY_DOWN ],
|
149
|
+
'info.page_up': ['^u', Curses::KEY_PPAGE ],
|
150
|
+
'info.page_down': ['^d', Curses::KEY_NPAGE ]},
|
151
|
+
splash: {}
|
152
|
+
}
|
153
|
+
|
154
|
+
@bindings.default_proc = proc { |h,k| fail "Unknown widget #{k}" }
|
155
|
+
@bindings.each do |widget, hash|
|
156
|
+
hash.default_proc = proc { |h,k| h[k] = {} }
|
157
|
+
hash.values.each do |keys|
|
158
|
+
keys.map! { |key| parse_key(key) }
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def keyname(key)
|
164
|
+
return 'SPACE' if key.to_s == ' '
|
165
|
+
return key.to_s if key.is_a? Symbol
|
166
|
+
|
167
|
+
name = Curses.keyname(key)
|
168
|
+
if name.start_with? 'KEY_'
|
169
|
+
name.sub('KEY_', '').sub(/\((\d+)\)/, '\1')
|
170
|
+
else
|
171
|
+
name
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def parse_key(key)
|
176
|
+
if key.is_a? Integer
|
177
|
+
key
|
178
|
+
elsif key.size == 1
|
179
|
+
key.to_sym
|
180
|
+
elsif key.size == 2 and key.start_with?(?^)
|
181
|
+
Curses.const_get("KEY_CTRL_#{key[1].upcase}")
|
182
|
+
else
|
183
|
+
key = key.upcase.tr(?-, ?_)
|
184
|
+
key = "KEY_#{key}" unless key.start_with?('KEY_')
|
185
|
+
Curses.const_get(key)
|
186
|
+
end
|
187
|
+
rescue NameError
|
188
|
+
fail "Unknown key: #{key}"
|
189
|
+
end
|
190
|
+
|
191
|
+
def bind(widget, key, command)
|
192
|
+
widget, command = widget.to_sym, command.to_sym
|
193
|
+
fail "Unknown command #{command}" unless COMMANDS.include? command
|
194
|
+
|
195
|
+
@bindings[widget][command].delete parse_key(key)
|
196
|
+
@bindings[widget][command] << parse_key(key)
|
197
|
+
check_collisions
|
198
|
+
end
|
199
|
+
|
200
|
+
def unbind(widget, key)
|
201
|
+
@bindings[widget.to_sym].each do |command, keys|
|
202
|
+
keys.delete( (parsed_key ||= parse_key(k)) )
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def bind_view(section, view, view_operations, operations)
|
207
|
+
@bindings[section.to_sym].each do |command, keys|
|
208
|
+
keys.each do |key|
|
209
|
+
meth = view_operations.method(command) rescue operations.method(command)
|
210
|
+
view.keys.on(key, &meth)
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
private def check_collisions
|
216
|
+
global_keys = @bindings[:global].values.flatten
|
217
|
+
global_keys.each do |k|
|
218
|
+
fail "Double binding in 'global': #{k}" if global_keys.count(k) > 1
|
219
|
+
end
|
220
|
+
|
221
|
+
@bindings.each_pair do |widget, commands|
|
222
|
+
widget_keys = commands.values.flatten
|
223
|
+
widget_keys.each do |k|
|
224
|
+
fail "Double binding in '#{widget}': #{k}" if widget_keys.count(k) > 1
|
225
|
+
fail "Double binding in 'global/#{widget}': #{k}" if global_keys.include? k
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|