mirror of
https://github.com/vale981/clay
synced 2025-03-04 17:11:41 -05:00
Merge pull request #20 from and3rson/configurable-keybinds
Configurable keybinds
This commit is contained in:
commit
98c96bd462
15 changed files with 503 additions and 188 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -104,3 +104,7 @@ ENV/
|
|||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
|
||||
# Editor
|
||||
#*
|
||||
*~
|
||||
|
|
|
@ -4,6 +4,7 @@ max-args = 8
|
|||
|
||||
[messages control]
|
||||
disable =
|
||||
duplicate-code,
|
||||
too-few-public-methods,
|
||||
too-many-public-methods,
|
||||
too-many-instance-attributes,
|
||||
|
|
125
clay/app.py
125
clay/app.py
|
@ -24,59 +24,7 @@ from clay.pages.settings import SettingsPage
|
|||
from clay.settings import settings
|
||||
from clay.notifications import notification_area
|
||||
from clay.gp import gp
|
||||
|
||||
|
||||
def create_palette(transparent=False):
|
||||
"""
|
||||
Return urwid palette.
|
||||
"""
|
||||
if transparent:
|
||||
bgcolor = ''
|
||||
else:
|
||||
bgcolor = '#222'
|
||||
|
||||
return [
|
||||
(None, '', '', '', '#FFF', bgcolor),
|
||||
('default', '', '', '', '#FFF', bgcolor),
|
||||
('logo', '', '', '', '#F54', bgcolor),
|
||||
|
||||
('bg', '', '', '', '#FFF', '#222'),
|
||||
('primary', '', '', '', '#F54', '#FFF'),
|
||||
('secondary', '', '', '', '#17F', '#FFF'),
|
||||
('selected', '', '', '', '#FFF', '#444'),
|
||||
('primary_inv', '', '', '', '#FFF', '#17F'),
|
||||
('secondary_inv', '', '', '', '#FFF', '#F17'),
|
||||
('progress', '', '', '', '#FFF', '#F54'),
|
||||
('progress_remaining', '', '', '', '#FFF', '#444'),
|
||||
|
||||
('progressbar_done', '', '', '', '#F54', bgcolor),
|
||||
('progressbar_done_paused', '', '', '', '', bgcolor),
|
||||
('progressbar_remaining', '', '', '', '#222', bgcolor),
|
||||
|
||||
('title-idle', '', '', '', '', bgcolor),
|
||||
('title-playing', '', '', '', '#F54', bgcolor),
|
||||
|
||||
('panel', '', '', '', '#FFF', '#222'),
|
||||
('panel_focus', '', '', '', '#FFF', '#F54'),
|
||||
('panel_divider', '', '', '', '#444', '#222'),
|
||||
('panel_divider_focus', '', '', '', '#444', '#F54'),
|
||||
|
||||
('line1', '', '', '', '#FFF', bgcolor),
|
||||
('line1_focus', '', '', '', '#FFF', '#333'),
|
||||
('line1_active', '', '', '', '#F54', bgcolor),
|
||||
('line1_active_focus', '', '', '', '#F54', '#333'),
|
||||
('line2', '', '', '', '#AAA', bgcolor),
|
||||
('line2_focus', '', '', '', '#AAA', '#333'),
|
||||
|
||||
('input', '', '', '', '#FFF', '#444'),
|
||||
('input_focus', '', '', '', '#FFF', '#F54'),
|
||||
|
||||
('flag', '', '', '', '#AAA', bgcolor),
|
||||
('flag-active', '', '', '', '#F54', bgcolor),
|
||||
|
||||
('notification', '', '', '', '#F54', '#222'),
|
||||
]
|
||||
|
||||
from clay.hotkeys import hotkey_manager
|
||||
|
||||
class AppWidget(urwid.Frame):
|
||||
"""
|
||||
|
@ -84,20 +32,6 @@ class AppWidget(urwid.Frame):
|
|||
|
||||
Handles tab switches, global keypresses etc.
|
||||
"""
|
||||
|
||||
KEYBINDS = {
|
||||
'ctrl q': 'seek_start',
|
||||
'ctrl w': 'play_pause',
|
||||
'ctrl e': 'next_song',
|
||||
'shift left': 'seek_backward',
|
||||
'shift right': 'seek_forward',
|
||||
'ctrl s': 'toggle_shuffle',
|
||||
'ctrl r': 'toggle_repeat_one',
|
||||
'ctrl x': 'quit',
|
||||
'esc': 'handle_escape_action',
|
||||
'ctrl _': 'handle_escape_action'
|
||||
}
|
||||
|
||||
class Tab(urwid.Text):
|
||||
"""
|
||||
Represents a single tab in header tabbar.
|
||||
|
@ -138,12 +72,7 @@ class AppWidget(urwid.Frame):
|
|||
SearchPage(self),
|
||||
SettingsPage(self)
|
||||
]
|
||||
self.tabs = [
|
||||
AppWidget.Tab(page)
|
||||
for page
|
||||
in self.pages
|
||||
]
|
||||
|
||||
self.tabs = [AppWidget.Tab(page) for page in self.pages]
|
||||
self.current_page = None
|
||||
self.loop = None
|
||||
|
||||
|
@ -176,11 +105,8 @@ class AppWidget(urwid.Frame):
|
|||
|
||||
Request user authorization.
|
||||
"""
|
||||
username, password, device_id, authtoken = [
|
||||
settings.get(x)
|
||||
for x
|
||||
in ('username', 'password', 'device_id', 'authtoken')
|
||||
]
|
||||
authtoken, device_id, _, password, username = settings.get_section("play_settings").values()
|
||||
|
||||
if self._login_notification:
|
||||
self._login_notification.close()
|
||||
if use_token and authtoken:
|
||||
|
@ -238,7 +164,7 @@ class AppWidget(urwid.Frame):
|
|||
return
|
||||
|
||||
with settings.edit() as config:
|
||||
config['authtoken'] = gp.get_authtoken()
|
||||
config['play_settings']['authtoken'] = gp.get_authtoken()
|
||||
|
||||
self._login_notification.close()
|
||||
|
||||
|
@ -297,11 +223,7 @@ class AppWidget(urwid.Frame):
|
|||
self.set_page(tab.page.__class__.__name__)
|
||||
return
|
||||
|
||||
method_name = AppWidget.KEYBINDS.get(key)
|
||||
if method_name:
|
||||
getattr(self, method_name)()
|
||||
else:
|
||||
super(AppWidget, self).keypress(size, key)
|
||||
hotkey_manager.keypress("global", self, super(AppWidget, self), size, key)
|
||||
|
||||
@staticmethod
|
||||
def seek_start():
|
||||
|
@ -324,6 +246,13 @@ class AppWidget(urwid.Frame):
|
|||
"""
|
||||
player.next(True)
|
||||
|
||||
@staticmethod
|
||||
def prev_song():
|
||||
"""
|
||||
Play the previous song.
|
||||
"""
|
||||
player.prev(True)
|
||||
|
||||
@staticmethod
|
||||
def seek_backward():
|
||||
"""
|
||||
|
@ -352,14 +281,14 @@ class AppWidget(urwid.Frame):
|
|||
"""
|
||||
player.set_repeat_one(not player.get_is_repeat_one())
|
||||
|
||||
@staticmethod
|
||||
def quit():
|
||||
def quit(self):
|
||||
"""
|
||||
Quit app.
|
||||
"""
|
||||
self.loop = None
|
||||
sys.exit(0)
|
||||
|
||||
def handle_escape_action(self):
|
||||
def handle_escape(self):
|
||||
"""
|
||||
Run escape actions. If none are pending, close newest notification.
|
||||
"""
|
||||
|
@ -376,9 +305,6 @@ class MultilineVersionAction(argparse.Action):
|
|||
An argparser action for multiple lines so we can display the copyright notice
|
||||
Based on: https://stackoverflow.com/a/41147122
|
||||
"""
|
||||
version = "0.6.2"
|
||||
author = "Andrew Dunai"
|
||||
|
||||
def __init__(self, option_strings, dest, nargs=None, **kwargs):
|
||||
if nargs is not None:
|
||||
raise ValueError("nargs not allowed")
|
||||
|
@ -404,15 +330,17 @@ def main():
|
|||
|
||||
parser.add_argument("-v", "--version", action=MultilineVersionAction)
|
||||
|
||||
parser.add_argument(
|
||||
keybinds_group = parser.add_mutually_exclusive_group()
|
||||
|
||||
keybinds_group.add_argument(
|
||||
"--with-x-keybinds",
|
||||
help="define global X keybinds (requires Keybinder and PyGObject)",
|
||||
action='store_true'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--transparent",
|
||||
help="use transparent background",
|
||||
keybinds_group.add_argument(
|
||||
"--without-x-keybinds",
|
||||
help="Don't define global keybinds (overrides configuration file)",
|
||||
action='store_true'
|
||||
)
|
||||
|
||||
|
@ -421,12 +349,17 @@ def main():
|
|||
if args.version:
|
||||
exit(0)
|
||||
|
||||
if args.with_x_keybinds:
|
||||
if (args.with_x_keybinds or settings.get('x_keybinds', 'clay_settings')) \
|
||||
and not args.without_x_keybinds:
|
||||
player.enable_xorg_bindings()
|
||||
|
||||
# Create a 256 colour palette.
|
||||
palette = [(name, '', '', '', res['foreground'], res['background'])
|
||||
for name, res in settings.colours_config.items()]
|
||||
|
||||
# Run the actual program
|
||||
app_widget = AppWidget()
|
||||
loop = urwid.MainLoop(app_widget, create_palette(args.transparent))
|
||||
loop = urwid.MainLoop(app_widget, palette)
|
||||
app_widget.set_loop(loop)
|
||||
loop.screen.set_terminal_properties(256)
|
||||
loop.run()
|
||||
|
|
95
clay/colours.yaml
Normal file
95
clay/colours.yaml
Normal file
|
@ -0,0 +1,95 @@
|
|||
default: &default
|
||||
foreground: "#FFF"
|
||||
background: "#222"
|
||||
|
||||
primary: &primary
|
||||
foreground: "#F54"
|
||||
background: "#FFF"
|
||||
|
||||
primary_inv: &primary_inv
|
||||
foreground: "#FFF"
|
||||
background: "#F54"
|
||||
|
||||
secondary: &secondary
|
||||
foreground: "#17F"
|
||||
background: "#FFF"
|
||||
|
||||
secondary_inv: &secondary_inv
|
||||
foreground: "#FFF"
|
||||
background: "#17F"
|
||||
|
||||
background: *default
|
||||
None: *default
|
||||
'': *default
|
||||
|
||||
logo: *primary
|
||||
progress: *primary
|
||||
progressbar_done: *primary
|
||||
selected: *primary_inv
|
||||
|
||||
progress_remaining:
|
||||
foreground: "#FFF"
|
||||
background: "#444"
|
||||
|
||||
progressbar_done_paused:
|
||||
<<: *default
|
||||
foreground: null
|
||||
|
||||
progressbar_remaining:
|
||||
<<: *default
|
||||
foreground: "#222"
|
||||
|
||||
title-idle:
|
||||
<<: *default
|
||||
foreground: null
|
||||
|
||||
title-playing: *primary
|
||||
|
||||
panel:
|
||||
foreground: "#FFF"
|
||||
background: "#222"
|
||||
|
||||
panel_focus: *primary_inv
|
||||
|
||||
panel_divider:
|
||||
foreground: "#444"
|
||||
background: "#222"
|
||||
|
||||
panel_divider_focus:
|
||||
foreground: "#444"
|
||||
background: '#F54'
|
||||
|
||||
line1: *default
|
||||
line1_focus:
|
||||
foreground: "#FFF"
|
||||
background: "#333"
|
||||
|
||||
line1_active: *primary
|
||||
|
||||
line1_active_focus:
|
||||
foreground: "#F54"
|
||||
background: "#333"
|
||||
|
||||
line2:
|
||||
<<: *default
|
||||
foreground: "#AAA"
|
||||
|
||||
line2_focus:
|
||||
foreground: "#AAA"
|
||||
background: "#333"
|
||||
|
||||
input:
|
||||
foreground: "#FFF"
|
||||
background: "#444"
|
||||
|
||||
input_focus: *primary_inv
|
||||
|
||||
flag:
|
||||
<<: *default
|
||||
foreground: "#AAA"
|
||||
|
||||
flag-active: *primary
|
||||
|
||||
notification:
|
||||
foreground: "#F54"
|
||||
background: "#222"
|
58
clay/config.yaml
Normal file
58
clay/config.yaml
Normal file
|
@ -0,0 +1,58 @@
|
|||
#: pylint:skip-file
|
||||
hotkeys:
|
||||
mod_key: ctrl
|
||||
|
||||
x_hotkeys:
|
||||
play_pause: XF86AudioPlay
|
||||
next: XF86AudioNext
|
||||
prev: XF86AudioPrev
|
||||
|
||||
clay_hotkeys:
|
||||
global:
|
||||
seek_start: mod + q
|
||||
play_pause: mod + w
|
||||
seek_backward: shift + left
|
||||
seek_forward: shift + right
|
||||
quit: mod + x
|
||||
toggle_shuffle: mod + s
|
||||
next_song: mod + d
|
||||
prev_song: mod + a
|
||||
toggle_repeat_one: mod + r
|
||||
handle_escape: esc, mod + _
|
||||
|
||||
library_item:
|
||||
play: enter
|
||||
append: mod + a
|
||||
unappend: mod + u
|
||||
request_station: mod + p
|
||||
show_context_menu: meta + p
|
||||
|
||||
library_view:
|
||||
move_to_beginning: home
|
||||
move_to_end: end
|
||||
move_up: up
|
||||
move_down: down
|
||||
hide_context_menu: meta + p
|
||||
|
||||
playlist_page:
|
||||
start_playlist: enter
|
||||
|
||||
debug_page:
|
||||
copy_message: enter
|
||||
|
||||
search_page:
|
||||
send_query: enter
|
||||
|
||||
settings_page:
|
||||
equalizer_up: "+"
|
||||
equalizer_down: "-"
|
||||
|
||||
clay_settings:
|
||||
x_keybinds: false
|
||||
|
||||
play_settings:
|
||||
authtoken:
|
||||
device_id:
|
||||
download_tracks: false
|
||||
password:
|
||||
username:
|
|
@ -12,7 +12,6 @@ from gmusicapi.clients import Mobileclient
|
|||
from clay.eventhook import EventHook
|
||||
from clay.log import logger
|
||||
|
||||
|
||||
def asynchronous(func):
|
||||
"""
|
||||
Decorates a function to become asynchronous.
|
||||
|
@ -45,6 +44,7 @@ def asynchronous(func):
|
|||
callback(result, None, **extra)
|
||||
|
||||
Thread(target=process).start()
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
|
|
|
@ -44,14 +44,9 @@ class _HotkeyManager(object):
|
|||
Manages configs.
|
||||
Runs Gtk main loop in a thread.
|
||||
"""
|
||||
DEFAULT_HOTKEYS = {
|
||||
'play_pause': 'XF86AudioPlay',
|
||||
'next': 'XF86AudioNext',
|
||||
'prev': 'XF86AudioPrev'
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.hotkeys = {}
|
||||
self._x_hotkeys = {}
|
||||
self._hotkeys = self._parse_hotkeys()
|
||||
self.config = None
|
||||
|
||||
self.play_pause = EventHook()
|
||||
|
@ -61,7 +56,6 @@ class _HotkeyManager(object):
|
|||
if IS_INIT:
|
||||
Keybinder.init()
|
||||
self.initialize()
|
||||
|
||||
threading.Thread(target=Gtk.main).start()
|
||||
else:
|
||||
logger.debug("Not loading the global shortcuts.")
|
||||
|
@ -72,24 +66,92 @@ class _HotkeyManager(object):
|
|||
)
|
||||
|
||||
@staticmethod
|
||||
def load_keys():
|
||||
def _to_gtk_modifier(key):
|
||||
"""
|
||||
Load hotkey config from settings.
|
||||
Translates the modifies to the way that GTK likes them.
|
||||
"""
|
||||
hotkeys = settings.get('hotkeys', {})
|
||||
for operation, default_key in _HotkeyManager.DEFAULT_HOTKEYS.items():
|
||||
if operation not in hotkeys or not hotkeys[operation]:
|
||||
hotkeys[operation] = default_key
|
||||
key = key.strip()
|
||||
|
||||
if key == "meta":
|
||||
key = "<alt>"
|
||||
elif key in ("ctrl", "alt", "shift"):
|
||||
key = "<" + key + ">"
|
||||
else:
|
||||
key = key
|
||||
|
||||
return key
|
||||
|
||||
def _parse_x_hotkeys(self):
|
||||
"""
|
||||
Reads out them configuration file and parses them into hotkeys readable by GTK.
|
||||
"""
|
||||
hotkey_default_config = settings.get_default_config_section('hotkeys', 'x_hotkeys')
|
||||
mod_key = settings.get('mod_key', 'hotkeys')
|
||||
hotkeys = {}
|
||||
|
||||
for action in hotkey_default_config:
|
||||
key_seq = settings.get(action, 'hotkeys', 'x_hotkeys')
|
||||
|
||||
for key in key_seq.split(', '):
|
||||
hotkey = key.split(' + ')
|
||||
|
||||
if hotkey[0].strip() == 'mod':
|
||||
hotkey[0] = mod_key
|
||||
|
||||
hotkey = [self._to_gtk_modifier(key) for key in hotkey]
|
||||
|
||||
hotkeys[action] = ''.join(hotkey)
|
||||
|
||||
return hotkeys
|
||||
|
||||
def _parse_hotkeys(self):
|
||||
"""
|
||||
Reads out the configuration file and parse them into a hotkeys for urwid.
|
||||
"""
|
||||
hotkey_config = settings.get_default_config_section('hotkeys', 'clay_hotkeys')
|
||||
mod_key = settings.get('mod_key', 'hotkeys')
|
||||
hotkeys = {}
|
||||
|
||||
for hotkey_name, hotkey_dict in hotkey_config.items():
|
||||
hotkeys[hotkey_name] = {}
|
||||
for action in hotkey_dict.keys():
|
||||
key_seq = settings.get(action, 'hotkeys', 'clay_hotkeys', hotkey_name)
|
||||
|
||||
for key in key_seq.split(', '):
|
||||
hotkey = key.split(' + ')
|
||||
|
||||
if hotkey[0].strip() == 'mod':
|
||||
hotkey[0] = mod_key
|
||||
|
||||
hotkeys[hotkey_name][' '.join(hotkey)] = action
|
||||
return hotkeys
|
||||
|
||||
def keypress(self, name, caller, super_, size, key):
|
||||
"""
|
||||
Process the pressed key by looking it up in the configuration file
|
||||
|
||||
"""
|
||||
method_name = self._hotkeys[name].get(key)
|
||||
|
||||
if method_name:
|
||||
ret = getattr(caller, method_name)()
|
||||
elif super_ is not None:
|
||||
ret = super_.keypress(size, key)
|
||||
else:
|
||||
ret = key
|
||||
|
||||
return ret
|
||||
|
||||
def initialize(self):
|
||||
"""
|
||||
Unbind previous hotkeys, re-read config & bind new hotkeys.
|
||||
"""
|
||||
for operation, key in self.hotkeys.items():
|
||||
for operation, key in self._x_hotkeys.items():
|
||||
Keybinder.unbind(key)
|
||||
self.hotkeys = self.load_keys()
|
||||
for operation, key in self.hotkeys.items():
|
||||
|
||||
self._x_hotkeys = self._parse_x_hotkeys()
|
||||
|
||||
for operation, key in self._x_hotkeys.items():
|
||||
Keybinder.bind(key, self.fire_hook, operation)
|
||||
|
||||
def fire_hook(self, key, operation):
|
||||
|
|
|
@ -7,6 +7,7 @@ from clay.pages.page import AbstractPage
|
|||
from clay.log import logger
|
||||
from clay.clipboard import copy
|
||||
from clay.gp import gp
|
||||
from clay.hotkeys import hotkey_manager
|
||||
|
||||
|
||||
class DebugItem(urwid.AttrMap):
|
||||
|
@ -36,10 +37,12 @@ class DebugItem(urwid.AttrMap):
|
|||
"""
|
||||
Handle heypress.
|
||||
"""
|
||||
if key == 'enter':
|
||||
copy(self.log_record.formatted_message)
|
||||
return None
|
||||
return key
|
||||
return hotkey_manager.keypress("debug_page", self, None, None, key)
|
||||
|
||||
def copy_message(self):
|
||||
"""Copy the selected error message to the clipboard"""
|
||||
copy(self.log_record.formatted_message)
|
||||
return None
|
||||
|
||||
|
||||
class DebugPage(urwid.Pile, AbstractPage):
|
||||
|
|
|
@ -7,6 +7,7 @@ from clay.gp import gp
|
|||
from clay.songlist import SongListBox
|
||||
from clay.notifications import notification_area
|
||||
from clay.pages.page import AbstractPage
|
||||
from clay.hotkeys import hotkey_manager
|
||||
|
||||
|
||||
class MyPlaylistListItem(urwid.Columns):
|
||||
|
@ -33,10 +34,15 @@ class MyPlaylistListItem(urwid.Columns):
|
|||
"""
|
||||
Handle keypress.
|
||||
"""
|
||||
if key == 'enter':
|
||||
urwid.emit_signal(self, 'activate', self)
|
||||
return None
|
||||
return super(MyPlaylistListItem, self).keypress(size, key)
|
||||
return hotkey_manager.keypress("playlist_page", self, super(MyPlaylistListItem, self),
|
||||
size, key)
|
||||
|
||||
def start_playlist(self):
|
||||
"""
|
||||
Start playing the selected playlist
|
||||
"""
|
||||
urwid.emit_signal(self, 'activate', self)
|
||||
return None
|
||||
|
||||
def get_tracks(self):
|
||||
"""
|
||||
|
|
|
@ -6,6 +6,7 @@ import urwid
|
|||
from clay.gp import gp
|
||||
from clay.songlist import SongListBox
|
||||
from clay.notifications import notification_area
|
||||
from clay.hotkeys import hotkey_manager
|
||||
from clay.pages.page import AbstractPage
|
||||
|
||||
|
||||
|
@ -35,10 +36,14 @@ class SearchBox(urwid.Columns):
|
|||
"""
|
||||
Handle keypress.
|
||||
"""
|
||||
if key == 'enter':
|
||||
urwid.emit_signal(self, 'search-requested', self.query.edit_text)
|
||||
return None
|
||||
return super(SearchBox, self).keypress(size, key)
|
||||
return hotkey_manager.keypress("search_page", self, super(SearchBox, self), size, key)
|
||||
|
||||
def send_query(self):
|
||||
"""
|
||||
Send a message to urwid to search the filled in search query
|
||||
"""
|
||||
urwid.emit_signal(self, 'search-requested', self.query.edit_text)
|
||||
return None
|
||||
|
||||
|
||||
class SearchPage(urwid.Pile, AbstractPage):
|
||||
|
|
|
@ -6,6 +6,7 @@ import urwid
|
|||
from clay.pages.page import AbstractPage
|
||||
from clay.settings import settings
|
||||
from clay.player import player
|
||||
from clay.hotkeys import hotkey_manager
|
||||
|
||||
|
||||
class Slider(urwid.Widget):
|
||||
|
@ -80,18 +81,27 @@ class Slider(urwid.Widget):
|
|||
"""
|
||||
Handle equalizer band modification.
|
||||
"""
|
||||
if key == '+':
|
||||
if self.value < self.max_value:
|
||||
self.value += 1
|
||||
self.update()
|
||||
return None
|
||||
elif key == '-':
|
||||
if self.value > -self.max_value:
|
||||
self.value -= 1
|
||||
self.update()
|
||||
return None
|
||||
else:
|
||||
return key
|
||||
return hotkey_manager.keypress("settings_page", self, None, None, key)
|
||||
|
||||
def equalizer_up(self):
|
||||
"""
|
||||
Turn the equalizer band up
|
||||
"""
|
||||
if self.value < self.max_value:
|
||||
self.value += 1
|
||||
self.update()
|
||||
|
||||
return None
|
||||
|
||||
def equalizer_down(self):
|
||||
"""
|
||||
Turn the equalizer band down
|
||||
"""
|
||||
if self.value > -self.max_value:
|
||||
self.value -= 1
|
||||
self.update()
|
||||
|
||||
return None
|
||||
|
||||
def update(self):
|
||||
"""
|
||||
|
@ -131,17 +141,17 @@ class SettingsPage(urwid.Columns, AbstractPage):
|
|||
def __init__(self, app):
|
||||
self.app = app
|
||||
self.username = urwid.Edit(
|
||||
edit_text=settings.get('username', '')
|
||||
edit_text=settings.get('username', 'play_settings')
|
||||
)
|
||||
self.password = urwid.Edit(
|
||||
mask='*', edit_text=settings.get('password', '')
|
||||
mask='*', edit_text=settings.get('password', 'play_settings')
|
||||
)
|
||||
self.device_id = urwid.Edit(
|
||||
edit_text=settings.get('device_id', '')
|
||||
edit_text=settings.get('device_id', 'play_settings')
|
||||
)
|
||||
self.download_tracks = urwid.CheckBox(
|
||||
'Download tracks before playback',
|
||||
state=settings.get('download_tracks', False)
|
||||
state=settings.get('download_tracks', 'play_settings')
|
||||
)
|
||||
self.equalizer = Equalizer()
|
||||
super(SettingsPage, self).__init__([urwid.ListBox(urwid.SimpleListWalker([
|
||||
|
@ -170,10 +180,10 @@ class SettingsPage(urwid.Columns, AbstractPage):
|
|||
Called when "Save" button is pressed.
|
||||
"""
|
||||
with settings.edit() as config:
|
||||
config['username'] = self.username.edit_text
|
||||
config['password'] = self.password.edit_text
|
||||
config['device_id'] = self.device_id.edit_text
|
||||
config['download_tracks'] = self.download_tracks.state
|
||||
config['play_settings']['username'] = self.username.edit_text
|
||||
config['play_settings']['password'] = self.password.edit_text
|
||||
config['play_settings']['device_id'] = self.device_id.edit_text
|
||||
config['play_settings']['download_tracks'] = self.download_tracks.state
|
||||
|
||||
self.app.set_page('MyLibraryPage')
|
||||
self.app.log_in()
|
||||
|
|
|
@ -4,6 +4,7 @@ Media player built using libVLC.
|
|||
# pylint: disable=too-many-instance-attributes
|
||||
# pylint: disable=too-many-public-methods
|
||||
from random import randint
|
||||
from ctypes import CFUNCTYPE, c_void_p, c_int, c_char_p
|
||||
import json
|
||||
import os
|
||||
|
||||
|
@ -35,6 +36,7 @@ class _Queue(object):
|
|||
self.repeat_one = False
|
||||
|
||||
self.tracks = []
|
||||
self._played_tracks = []
|
||||
self.current_track_index = None
|
||||
|
||||
def load(self, tracks, current_track_index=None):
|
||||
|
@ -91,6 +93,8 @@ class _Queue(object):
|
|||
if not self.tracks:
|
||||
return None
|
||||
self.current_track_index = self.tracks[0]
|
||||
else:
|
||||
self._played_tracks.append(self.current_track_index)
|
||||
|
||||
if self.repeat_one and not force:
|
||||
return self.get_current_track()
|
||||
|
@ -105,12 +109,39 @@ class _Queue(object):
|
|||
|
||||
return self.get_current_track()
|
||||
|
||||
def prev(self, force=False):
|
||||
"""
|
||||
Revert to their last song and return it.
|
||||
|
||||
If *force* is ``True`` then tracks will be changed event if
|
||||
tracks repition is enabled. Otherwise current tracks may be
|
||||
yielded again.
|
||||
|
||||
Manual tracks switching calls this method with ``force=True``.
|
||||
"""
|
||||
if self._played_tracks == []:
|
||||
return None
|
||||
|
||||
if self.repeat_one and not force:
|
||||
return self.get_current_track()
|
||||
|
||||
self.current_track_index = self._played_tracks.pop()
|
||||
return self.get_current_track()
|
||||
|
||||
def get_tracks(self):
|
||||
"""
|
||||
Return current queue, i.e. a list of :class:`Track` instances.
|
||||
"""
|
||||
return self.tracks
|
||||
|
||||
#+pylint: disable=unused-argument
|
||||
def _dummy_log(data, level, ctx, fmt, args):
|
||||
"""
|
||||
A dummy callback function for VLC so it doesn't write to stdout.
|
||||
Should probably do something in the future
|
||||
"""
|
||||
pass
|
||||
#+pylint: disable=unused-argument
|
||||
|
||||
class _Player(object):
|
||||
"""
|
||||
|
@ -129,6 +160,15 @@ class _Player(object):
|
|||
|
||||
def __init__(self):
|
||||
self.instance = vlc.Instance()
|
||||
print_func = CFUNCTYPE(c_void_p,
|
||||
c_void_p, # data
|
||||
c_int, # level
|
||||
c_void_p, # context
|
||||
c_char_p, # fmt
|
||||
c_void_p) #args
|
||||
|
||||
self.instance.log_set(print_func(_dummy_log), None)
|
||||
|
||||
self.instance.set_user_agent(
|
||||
meta.APP_NAME,
|
||||
meta.USER_AGENT
|
||||
|
@ -154,9 +194,7 @@ class _Player(object):
|
|||
)
|
||||
|
||||
self.equalizer = vlc.libvlc_audio_equalizer_new()
|
||||
|
||||
self.media_player.set_equalizer(self.equalizer)
|
||||
|
||||
self._create_station_notification = None
|
||||
self._is_loading = False
|
||||
self.queue = _Queue()
|
||||
|
@ -331,8 +369,10 @@ class _Player(object):
|
|||
self.broadcast_state()
|
||||
self.track_changed.fire(track)
|
||||
|
||||
if settings.get('download_tracks', False) or settings.get_is_file_cached(track.filename):
|
||||
if settings.get('download_tracks', 'play_settings') or \
|
||||
settings.get_is_file_cached(track.filename):
|
||||
path = settings.get_cached_file_path(track.filename)
|
||||
|
||||
if path is None:
|
||||
logger.debug('Track %s not in cache, downloading...', track.store_id)
|
||||
track.get_url(callback=self._download_track)
|
||||
|
@ -425,6 +465,14 @@ class _Player(object):
|
|||
self.queue.next(force)
|
||||
self._play()
|
||||
|
||||
def prev(self, force=False):
|
||||
"""
|
||||
Advance to their previous track in their queue
|
||||
seek :meth:`._Queue.prev`
|
||||
"""
|
||||
self.queue.prev(force)
|
||||
self._play()
|
||||
|
||||
def get_current_track(self):
|
||||
"""
|
||||
Return currently played track.
|
||||
|
|
|
@ -6,8 +6,8 @@ import os
|
|||
import copy
|
||||
import errno
|
||||
import yaml
|
||||
|
||||
import appdirs
|
||||
import pkg_resources
|
||||
|
||||
|
||||
class _SettingsEditor(dict):
|
||||
|
@ -42,6 +42,7 @@ class _Settings(object):
|
|||
"""
|
||||
def __init__(self):
|
||||
self._config = {}
|
||||
self._default_config = {}
|
||||
self._cached_files = set()
|
||||
|
||||
self._config_dir = None
|
||||
|
@ -58,6 +59,7 @@ class _Settings(object):
|
|||
"""
|
||||
self._config_dir = appdirs.user_config_dir('clay', 'Clay')
|
||||
self._config_file_path = os.path.join(self._config_dir, 'config.yaml')
|
||||
self._colours_file_path = os.path.join(self._config_dir, 'colours.yaml')
|
||||
|
||||
try:
|
||||
os.makedirs(self._config_dir)
|
||||
|
@ -83,6 +85,17 @@ class _Settings(object):
|
|||
with open(self._config_file_path, 'r') as settings_file:
|
||||
self._config = yaml.load(settings_file.read())
|
||||
|
||||
# Load the configuration from Setuptools' ResourceManager API
|
||||
self._default_config = yaml.load(pkg_resources.resource_string(__name__, "config.yaml"))
|
||||
|
||||
# We only either the user colour or the default colours to ease parsing logic.
|
||||
if os.path.exists(self._colours_file_path):
|
||||
with open(self._colours_file_path, 'r') as colours_file:
|
||||
self.colours_config = yaml.load(colours_file.read())
|
||||
else:
|
||||
self.colours_config = yaml.load(pkg_resources.resource_string(__name__, "colours.yaml"))
|
||||
|
||||
|
||||
def _load_cache(self):
|
||||
"""
|
||||
Load cached files.
|
||||
|
@ -100,11 +113,45 @@ class _Settings(object):
|
|||
with open(self._config_file_path, 'w') as settings_file:
|
||||
settings_file.write(yaml.dump(self._config, default_flow_style=False))
|
||||
|
||||
def get(self, key, default=None):
|
||||
def get(self, key, *sections):
|
||||
"""
|
||||
Return config value.
|
||||
Return their configuration key in a specified section
|
||||
By default it looks in play_settings.
|
||||
"""
|
||||
return self._config.get(key, default)
|
||||
section = self.get_section(*sections)
|
||||
|
||||
try:
|
||||
return section[key]
|
||||
except (KeyError, TypeError):
|
||||
section = self.get_default_config_section(*sections)
|
||||
return section[key]
|
||||
|
||||
def _get_section(self, config, *sections):
|
||||
config = config.copy()
|
||||
|
||||
for section in sections:
|
||||
config = config[section]
|
||||
|
||||
return config
|
||||
|
||||
def get_section(self, *sections):
|
||||
"""
|
||||
Get a section from the user configuration file if it can find it,
|
||||
else load it from the system config
|
||||
"""
|
||||
try:
|
||||
return self._get_section(self._config, *sections)
|
||||
except (KeyError, TypeError):
|
||||
return self._get_section(self._default_config, *sections)
|
||||
|
||||
|
||||
def get_default_config_section(self, *sections):
|
||||
"""
|
||||
Always get a section from the default/system configuration. You would use this whenever
|
||||
you need to loop through all the values in a section. In the user config they might be
|
||||
incomplete.
|
||||
"""
|
||||
return self._get_section(self._default_config, *sections)
|
||||
|
||||
def edit(self):
|
||||
"""
|
||||
|
|
107
clay/songlist.py
107
clay/songlist.py
|
@ -17,6 +17,7 @@ from clay.player import player
|
|||
from clay.gp import gp
|
||||
from clay.clipboard import copy
|
||||
from clay.settings import settings
|
||||
from clay.hotkeys import hotkey_manager
|
||||
|
||||
|
||||
class SongListItem(urwid.Pile):
|
||||
|
@ -143,19 +144,7 @@ class SongListItem(urwid.Pile):
|
|||
"""
|
||||
Handle keypress.
|
||||
"""
|
||||
if key == 'enter':
|
||||
urwid.emit_signal(self, 'activate', self)
|
||||
return None
|
||||
elif key == 'ctrl a':
|
||||
urwid.emit_signal(self, 'append-requested', self)
|
||||
elif key == 'ctrl u':
|
||||
if not self.is_currently_played:
|
||||
urwid.emit_signal(self, 'unappend-requested', self)
|
||||
elif key == 'ctrl p':
|
||||
urwid.emit_signal(self, 'station-requested', self)
|
||||
elif key == 'meta m':
|
||||
urwid.emit_signal(self, 'context-menu-requested', self)
|
||||
return super(SongListItem, self).keypress(size, key)
|
||||
return hotkey_manager.keypress("library_item", self, super(SongListItem, self), size, key)
|
||||
|
||||
def mouse_event(self, size, event, button, col, row, focus):
|
||||
"""
|
||||
|
@ -166,6 +155,40 @@ class SongListItem(urwid.Pile):
|
|||
return None
|
||||
return super(SongListItem, self).mouse_event(size, event, button, col, row, focus)
|
||||
|
||||
def _send_signal(self, signal):
|
||||
urwid.emit_signal(self, signal, self)
|
||||
|
||||
def play(self):
|
||||
"""
|
||||
Play this song.
|
||||
"""
|
||||
self._send_signal("activate")
|
||||
|
||||
def append(self):
|
||||
"""
|
||||
Add this song to the queue.
|
||||
"""
|
||||
self._send_signal("append-requested")
|
||||
|
||||
def unappend(self):
|
||||
"""
|
||||
Remove this song from the queue.
|
||||
"""
|
||||
if not self.is_currently_played:
|
||||
self._send_signal("unappend-requested")
|
||||
|
||||
def request_station(self):
|
||||
"""
|
||||
Create a Google Play Music radio for this song.
|
||||
"""
|
||||
self._send_signal("station-requested")
|
||||
|
||||
def show_context_menu(self):
|
||||
"""
|
||||
Display the context menu for this song.
|
||||
"""
|
||||
self._send_signal("context-menu-requested")
|
||||
|
||||
@property
|
||||
def is_currently_played(self):
|
||||
"""
|
||||
|
@ -504,7 +527,7 @@ class SongListBox(urwid.Frame):
|
|||
"""
|
||||
Hide context menu.
|
||||
"""
|
||||
if self.popup is not None:
|
||||
if self.popup is not None and self.is_context_menu_visible:
|
||||
self.contents['body'] = (self.content, None)
|
||||
self.app.unregister_cancel_action(self.popup.close)
|
||||
self.popup = None
|
||||
|
@ -582,33 +605,51 @@ class SongListBox(urwid.Frame):
|
|||
songlistitem.set_index(i)
|
||||
|
||||
def keypress(self, size, key):
|
||||
if self._is_filtering and key in ('up', 'down', 'home', 'end'):
|
||||
return self.handle_filtered_keypress(key)
|
||||
elif key == 'meta m' and self.is_context_menu_visible:
|
||||
self.hide_context_menu()
|
||||
return None
|
||||
elif key in ascii_letters + digits + ' _-.,?!()[]/':
|
||||
if key in ascii_letters + digits + ' _-.,?!()[]/':
|
||||
self.perform_filtering(key)
|
||||
elif key == 'backspace':
|
||||
self.perform_filtering(key)
|
||||
return super(SongListBox, self).keypress(size, key)
|
||||
elif self._is_filtering:
|
||||
return hotkey_manager.keypress("library_view", self, super(SongListBox, self),
|
||||
size, key)
|
||||
else:
|
||||
return super(SongListBox, self).keypress(size, key)
|
||||
|
||||
def handle_filtered_keypress(self, key):
|
||||
"""
|
||||
Handle up/down/home/end keypress while in fitering mode.
|
||||
"""
|
||||
return None
|
||||
|
||||
def _get_filtered(self):
|
||||
"""Get filtered list of items"""
|
||||
matches = self.get_filtered_items()
|
||||
|
||||
if not matches:
|
||||
return False
|
||||
|
||||
_, index = self.walker.get_focus()
|
||||
if key == 'home':
|
||||
self.list_box.set_focus(matches[0].index, 'below')
|
||||
elif key == 'end':
|
||||
self.list_box.set_focus(matches[-1].index, 'above')
|
||||
elif key == 'up':
|
||||
self.list_box.set_focus(*self.get_prev_item(matches, index))
|
||||
else:
|
||||
self.list_box.set_focus(*self.get_next_item(matches, index))
|
||||
|
||||
return (matches, index)
|
||||
|
||||
def move_to_beginning(self):
|
||||
"""Move to the focus to beginning of the songlist"""
|
||||
matches, _ = self._get_filtered()
|
||||
self.list_box.set_focus(matches[0].index, 'below')
|
||||
return False
|
||||
|
||||
def move_to_end(self):
|
||||
"""Move to the focus to end of the songlist"""
|
||||
matches, _ = self._get_filtered()
|
||||
self.list_box.set_focus(matches[-1].index, 'above')
|
||||
return False
|
||||
|
||||
def move_up(self):
|
||||
"""Move the focus an item up in the playlist"""
|
||||
matches, index = self._get_filtered()
|
||||
self.list_box.set_focus(*self.get_prev_item(matches, index))
|
||||
return False
|
||||
|
||||
def move_down(self):
|
||||
"""Move the focus an item down in the playlist """
|
||||
matches, index = self._get_filtered()
|
||||
self.list_box.set_focus(*self.get_next_item(matches, index))
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
|
|
6
setup.py
6
setup.py
|
@ -21,6 +21,8 @@ setup(
|
|||
'console_scripts': [
|
||||
'clay=clay.app:main'
|
||||
]
|
||||
}
|
||||
},
|
||||
package_data={
|
||||
'clay': ['config.yaml', 'colours.yaml'],
|
||||
},
|
||||
)
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue