Merge pull request #20 from and3rson/configurable-keybinds

Configurable keybinds
This commit is contained in:
Andrew Dunai 2018-03-23 11:59:03 +02:00 committed by GitHub
commit 98c96bd462
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 503 additions and 188 deletions

4
.gitignore vendored
View file

@ -104,3 +104,7 @@ ENV/
# mypy # mypy
.mypy_cache/ .mypy_cache/
# Editor
#*
*~

View file

@ -4,6 +4,7 @@ max-args = 8
[messages control] [messages control]
disable = disable =
duplicate-code,
too-few-public-methods, too-few-public-methods,
too-many-public-methods, too-many-public-methods,
too-many-instance-attributes, too-many-instance-attributes,

View file

@ -24,59 +24,7 @@ from clay.pages.settings import SettingsPage
from clay.settings import settings from clay.settings import settings
from clay.notifications import notification_area from clay.notifications import notification_area
from clay.gp import gp from clay.gp import gp
from clay.hotkeys import hotkey_manager
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'),
]
class AppWidget(urwid.Frame): class AppWidget(urwid.Frame):
""" """
@ -84,20 +32,6 @@ class AppWidget(urwid.Frame):
Handles tab switches, global keypresses etc. 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): class Tab(urwid.Text):
""" """
Represents a single tab in header tabbar. Represents a single tab in header tabbar.
@ -138,12 +72,7 @@ class AppWidget(urwid.Frame):
SearchPage(self), SearchPage(self),
SettingsPage(self) SettingsPage(self)
] ]
self.tabs = [ self.tabs = [AppWidget.Tab(page) for page in self.pages]
AppWidget.Tab(page)
for page
in self.pages
]
self.current_page = None self.current_page = None
self.loop = None self.loop = None
@ -176,11 +105,8 @@ class AppWidget(urwid.Frame):
Request user authorization. Request user authorization.
""" """
username, password, device_id, authtoken = [ authtoken, device_id, _, password, username = settings.get_section("play_settings").values()
settings.get(x)
for x
in ('username', 'password', 'device_id', 'authtoken')
]
if self._login_notification: if self._login_notification:
self._login_notification.close() self._login_notification.close()
if use_token and authtoken: if use_token and authtoken:
@ -238,7 +164,7 @@ class AppWidget(urwid.Frame):
return return
with settings.edit() as config: with settings.edit() as config:
config['authtoken'] = gp.get_authtoken() config['play_settings']['authtoken'] = gp.get_authtoken()
self._login_notification.close() self._login_notification.close()
@ -297,11 +223,7 @@ class AppWidget(urwid.Frame):
self.set_page(tab.page.__class__.__name__) self.set_page(tab.page.__class__.__name__)
return return
method_name = AppWidget.KEYBINDS.get(key) hotkey_manager.keypress("global", self, super(AppWidget, self), size, key)
if method_name:
getattr(self, method_name)()
else:
super(AppWidget, self).keypress(size, key)
@staticmethod @staticmethod
def seek_start(): def seek_start():
@ -324,6 +246,13 @@ class AppWidget(urwid.Frame):
""" """
player.next(True) player.next(True)
@staticmethod
def prev_song():
"""
Play the previous song.
"""
player.prev(True)
@staticmethod @staticmethod
def seek_backward(): def seek_backward():
""" """
@ -352,14 +281,14 @@ class AppWidget(urwid.Frame):
""" """
player.set_repeat_one(not player.get_is_repeat_one()) player.set_repeat_one(not player.get_is_repeat_one())
@staticmethod def quit(self):
def quit():
""" """
Quit app. Quit app.
""" """
self.loop = None
sys.exit(0) sys.exit(0)
def handle_escape_action(self): def handle_escape(self):
""" """
Run escape actions. If none are pending, close newest notification. 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 An argparser action for multiple lines so we can display the copyright notice
Based on: https://stackoverflow.com/a/41147122 Based on: https://stackoverflow.com/a/41147122
""" """
version = "0.6.2"
author = "Andrew Dunai"
def __init__(self, option_strings, dest, nargs=None, **kwargs): def __init__(self, option_strings, dest, nargs=None, **kwargs):
if nargs is not None: if nargs is not None:
raise ValueError("nargs not allowed") raise ValueError("nargs not allowed")
@ -404,15 +330,17 @@ def main():
parser.add_argument("-v", "--version", action=MultilineVersionAction) parser.add_argument("-v", "--version", action=MultilineVersionAction)
parser.add_argument( keybinds_group = parser.add_mutually_exclusive_group()
keybinds_group.add_argument(
"--with-x-keybinds", "--with-x-keybinds",
help="define global X keybinds (requires Keybinder and PyGObject)", help="define global X keybinds (requires Keybinder and PyGObject)",
action='store_true' action='store_true'
) )
parser.add_argument( keybinds_group.add_argument(
"--transparent", "--without-x-keybinds",
help="use transparent background", help="Don't define global keybinds (overrides configuration file)",
action='store_true' action='store_true'
) )
@ -421,12 +349,17 @@ def main():
if args.version: if args.version:
exit(0) 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() 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 # Run the actual program
app_widget = AppWidget() app_widget = AppWidget()
loop = urwid.MainLoop(app_widget, create_palette(args.transparent)) loop = urwid.MainLoop(app_widget, palette)
app_widget.set_loop(loop) app_widget.set_loop(loop)
loop.screen.set_terminal_properties(256) loop.screen.set_terminal_properties(256)
loop.run() loop.run()

95
clay/colours.yaml Normal file
View 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
View 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:

View file

@ -12,7 +12,6 @@ from gmusicapi.clients import Mobileclient
from clay.eventhook import EventHook from clay.eventhook import EventHook
from clay.log import logger from clay.log import logger
def asynchronous(func): def asynchronous(func):
""" """
Decorates a function to become asynchronous. Decorates a function to become asynchronous.
@ -45,6 +44,7 @@ def asynchronous(func):
callback(result, None, **extra) callback(result, None, **extra)
Thread(target=process).start() Thread(target=process).start()
return wrapper return wrapper

View file

@ -44,14 +44,9 @@ class _HotkeyManager(object):
Manages configs. Manages configs.
Runs Gtk main loop in a thread. Runs Gtk main loop in a thread.
""" """
DEFAULT_HOTKEYS = {
'play_pause': 'XF86AudioPlay',
'next': 'XF86AudioNext',
'prev': 'XF86AudioPrev'
}
def __init__(self): def __init__(self):
self.hotkeys = {} self._x_hotkeys = {}
self._hotkeys = self._parse_hotkeys()
self.config = None self.config = None
self.play_pause = EventHook() self.play_pause = EventHook()
@ -61,7 +56,6 @@ class _HotkeyManager(object):
if IS_INIT: if IS_INIT:
Keybinder.init() Keybinder.init()
self.initialize() self.initialize()
threading.Thread(target=Gtk.main).start() threading.Thread(target=Gtk.main).start()
else: else:
logger.debug("Not loading the global shortcuts.") logger.debug("Not loading the global shortcuts.")
@ -72,24 +66,92 @@ class _HotkeyManager(object):
) )
@staticmethod @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', {}) key = key.strip()
for operation, default_key in _HotkeyManager.DEFAULT_HOTKEYS.items():
if operation not in hotkeys or not hotkeys[operation]: if key == "meta":
hotkeys[operation] = default_key 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 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): def initialize(self):
""" """
Unbind previous hotkeys, re-read config & bind new hotkeys. 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) 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) Keybinder.bind(key, self.fire_hook, operation)
def fire_hook(self, key, operation): def fire_hook(self, key, operation):

View file

@ -7,6 +7,7 @@ from clay.pages.page import AbstractPage
from clay.log import logger from clay.log import logger
from clay.clipboard import copy from clay.clipboard import copy
from clay.gp import gp from clay.gp import gp
from clay.hotkeys import hotkey_manager
class DebugItem(urwid.AttrMap): class DebugItem(urwid.AttrMap):
@ -36,10 +37,12 @@ class DebugItem(urwid.AttrMap):
""" """
Handle heypress. Handle heypress.
""" """
if key == 'enter': return hotkey_manager.keypress("debug_page", self, None, None, key)
copy(self.log_record.formatted_message)
return None def copy_message(self):
return key """Copy the selected error message to the clipboard"""
copy(self.log_record.formatted_message)
return None
class DebugPage(urwid.Pile, AbstractPage): class DebugPage(urwid.Pile, AbstractPage):

View file

@ -7,6 +7,7 @@ from clay.gp import gp
from clay.songlist import SongListBox from clay.songlist import SongListBox
from clay.notifications import notification_area from clay.notifications import notification_area
from clay.pages.page import AbstractPage from clay.pages.page import AbstractPage
from clay.hotkeys import hotkey_manager
class MyPlaylistListItem(urwid.Columns): class MyPlaylistListItem(urwid.Columns):
@ -33,10 +34,15 @@ class MyPlaylistListItem(urwid.Columns):
""" """
Handle keypress. Handle keypress.
""" """
if key == 'enter': return hotkey_manager.keypress("playlist_page", self, super(MyPlaylistListItem, self),
urwid.emit_signal(self, 'activate', self) size, key)
return None
return super(MyPlaylistListItem, self).keypress(size, key) def start_playlist(self):
"""
Start playing the selected playlist
"""
urwid.emit_signal(self, 'activate', self)
return None
def get_tracks(self): def get_tracks(self):
""" """

View file

@ -6,6 +6,7 @@ import urwid
from clay.gp import gp from clay.gp import gp
from clay.songlist import SongListBox from clay.songlist import SongListBox
from clay.notifications import notification_area from clay.notifications import notification_area
from clay.hotkeys import hotkey_manager
from clay.pages.page import AbstractPage from clay.pages.page import AbstractPage
@ -35,10 +36,14 @@ class SearchBox(urwid.Columns):
""" """
Handle keypress. Handle keypress.
""" """
if key == 'enter': return hotkey_manager.keypress("search_page", self, super(SearchBox, self), size, key)
urwid.emit_signal(self, 'search-requested', self.query.edit_text)
return None def send_query(self):
return super(SearchBox, self).keypress(size, key) """
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): class SearchPage(urwid.Pile, AbstractPage):

View file

@ -6,6 +6,7 @@ import urwid
from clay.pages.page import AbstractPage from clay.pages.page import AbstractPage
from clay.settings import settings from clay.settings import settings
from clay.player import player from clay.player import player
from clay.hotkeys import hotkey_manager
class Slider(urwid.Widget): class Slider(urwid.Widget):
@ -80,18 +81,27 @@ class Slider(urwid.Widget):
""" """
Handle equalizer band modification. Handle equalizer band modification.
""" """
if key == '+': return hotkey_manager.keypress("settings_page", self, None, None, key)
if self.value < self.max_value:
self.value += 1 def equalizer_up(self):
self.update() """
return None Turn the equalizer band up
elif key == '-': """
if self.value > -self.max_value: if self.value < self.max_value:
self.value -= 1 self.value += 1
self.update() self.update()
return None
else: return None
return key
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): def update(self):
""" """
@ -131,17 +141,17 @@ class SettingsPage(urwid.Columns, AbstractPage):
def __init__(self, app): def __init__(self, app):
self.app = app self.app = app
self.username = urwid.Edit( self.username = urwid.Edit(
edit_text=settings.get('username', '') edit_text=settings.get('username', 'play_settings')
) )
self.password = urwid.Edit( self.password = urwid.Edit(
mask='*', edit_text=settings.get('password', '') mask='*', edit_text=settings.get('password', 'play_settings')
) )
self.device_id = urwid.Edit( self.device_id = urwid.Edit(
edit_text=settings.get('device_id', '') edit_text=settings.get('device_id', 'play_settings')
) )
self.download_tracks = urwid.CheckBox( self.download_tracks = urwid.CheckBox(
'Download tracks before playback', 'Download tracks before playback',
state=settings.get('download_tracks', False) state=settings.get('download_tracks', 'play_settings')
) )
self.equalizer = Equalizer() self.equalizer = Equalizer()
super(SettingsPage, self).__init__([urwid.ListBox(urwid.SimpleListWalker([ super(SettingsPage, self).__init__([urwid.ListBox(urwid.SimpleListWalker([
@ -170,10 +180,10 @@ class SettingsPage(urwid.Columns, AbstractPage):
Called when "Save" button is pressed. Called when "Save" button is pressed.
""" """
with settings.edit() as config: with settings.edit() as config:
config['username'] = self.username.edit_text config['play_settings']['username'] = self.username.edit_text
config['password'] = self.password.edit_text config['play_settings']['password'] = self.password.edit_text
config['device_id'] = self.device_id.edit_text config['play_settings']['device_id'] = self.device_id.edit_text
config['download_tracks'] = self.download_tracks.state config['play_settings']['download_tracks'] = self.download_tracks.state
self.app.set_page('MyLibraryPage') self.app.set_page('MyLibraryPage')
self.app.log_in() self.app.log_in()

View file

@ -4,6 +4,7 @@ Media player built using libVLC.
# pylint: disable=too-many-instance-attributes # pylint: disable=too-many-instance-attributes
# pylint: disable=too-many-public-methods # pylint: disable=too-many-public-methods
from random import randint from random import randint
from ctypes import CFUNCTYPE, c_void_p, c_int, c_char_p
import json import json
import os import os
@ -35,6 +36,7 @@ class _Queue(object):
self.repeat_one = False self.repeat_one = False
self.tracks = [] self.tracks = []
self._played_tracks = []
self.current_track_index = None self.current_track_index = None
def load(self, tracks, current_track_index=None): def load(self, tracks, current_track_index=None):
@ -91,6 +93,8 @@ class _Queue(object):
if not self.tracks: if not self.tracks:
return None return None
self.current_track_index = self.tracks[0] self.current_track_index = self.tracks[0]
else:
self._played_tracks.append(self.current_track_index)
if self.repeat_one and not force: if self.repeat_one and not force:
return self.get_current_track() return self.get_current_track()
@ -105,12 +109,39 @@ class _Queue(object):
return self.get_current_track() 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): def get_tracks(self):
""" """
Return current queue, i.e. a list of :class:`Track` instances. Return current queue, i.e. a list of :class:`Track` instances.
""" """
return self.tracks 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): class _Player(object):
""" """
@ -129,6 +160,15 @@ class _Player(object):
def __init__(self): def __init__(self):
self.instance = vlc.Instance() 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( self.instance.set_user_agent(
meta.APP_NAME, meta.APP_NAME,
meta.USER_AGENT meta.USER_AGENT
@ -154,9 +194,7 @@ class _Player(object):
) )
self.equalizer = vlc.libvlc_audio_equalizer_new() self.equalizer = vlc.libvlc_audio_equalizer_new()
self.media_player.set_equalizer(self.equalizer) self.media_player.set_equalizer(self.equalizer)
self._create_station_notification = None self._create_station_notification = None
self._is_loading = False self._is_loading = False
self.queue = _Queue() self.queue = _Queue()
@ -331,8 +369,10 @@ class _Player(object):
self.broadcast_state() self.broadcast_state()
self.track_changed.fire(track) 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) path = settings.get_cached_file_path(track.filename)
if path is None: if path is None:
logger.debug('Track %s not in cache, downloading...', track.store_id) logger.debug('Track %s not in cache, downloading...', track.store_id)
track.get_url(callback=self._download_track) track.get_url(callback=self._download_track)
@ -425,6 +465,14 @@ class _Player(object):
self.queue.next(force) self.queue.next(force)
self._play() 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): def get_current_track(self):
""" """
Return currently played track. Return currently played track.

View file

@ -6,8 +6,8 @@ import os
import copy import copy
import errno import errno
import yaml import yaml
import appdirs import appdirs
import pkg_resources
class _SettingsEditor(dict): class _SettingsEditor(dict):
@ -42,6 +42,7 @@ class _Settings(object):
""" """
def __init__(self): def __init__(self):
self._config = {} self._config = {}
self._default_config = {}
self._cached_files = set() self._cached_files = set()
self._config_dir = None self._config_dir = None
@ -58,6 +59,7 @@ class _Settings(object):
""" """
self._config_dir = appdirs.user_config_dir('clay', 'Clay') self._config_dir = appdirs.user_config_dir('clay', 'Clay')
self._config_file_path = os.path.join(self._config_dir, 'config.yaml') 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: try:
os.makedirs(self._config_dir) os.makedirs(self._config_dir)
@ -83,6 +85,17 @@ class _Settings(object):
with open(self._config_file_path, 'r') as settings_file: with open(self._config_file_path, 'r') as settings_file:
self._config = yaml.load(settings_file.read()) 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): def _load_cache(self):
""" """
Load cached files. Load cached files.
@ -100,11 +113,45 @@ class _Settings(object):
with open(self._config_file_path, 'w') as settings_file: with open(self._config_file_path, 'w') as settings_file:
settings_file.write(yaml.dump(self._config, default_flow_style=False)) 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): def edit(self):
""" """

View file

@ -17,6 +17,7 @@ from clay.player import player
from clay.gp import gp from clay.gp import gp
from clay.clipboard import copy from clay.clipboard import copy
from clay.settings import settings from clay.settings import settings
from clay.hotkeys import hotkey_manager
class SongListItem(urwid.Pile): class SongListItem(urwid.Pile):
@ -143,19 +144,7 @@ class SongListItem(urwid.Pile):
""" """
Handle keypress. Handle keypress.
""" """
if key == 'enter': return hotkey_manager.keypress("library_item", self, super(SongListItem, self), size, key)
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)
def mouse_event(self, size, event, button, col, row, focus): def mouse_event(self, size, event, button, col, row, focus):
""" """
@ -166,6 +155,40 @@ class SongListItem(urwid.Pile):
return None return None
return super(SongListItem, self).mouse_event(size, event, button, col, row, focus) 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 @property
def is_currently_played(self): def is_currently_played(self):
""" """
@ -504,7 +527,7 @@ class SongListBox(urwid.Frame):
""" """
Hide context menu. 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.contents['body'] = (self.content, None)
self.app.unregister_cancel_action(self.popup.close) self.app.unregister_cancel_action(self.popup.close)
self.popup = None self.popup = None
@ -582,33 +605,51 @@ class SongListBox(urwid.Frame):
songlistitem.set_index(i) songlistitem.set_index(i)
def keypress(self, size, key): def keypress(self, size, key):
if self._is_filtering and key in ('up', 'down', 'home', 'end'): if key in ascii_letters + digits + ' _-.,?!()[]/':
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 + ' _-.,?!()[]/':
self.perform_filtering(key) self.perform_filtering(key)
elif key == 'backspace': elif key == 'backspace':
self.perform_filtering(key) 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): return None
"""
Handle up/down/home/end keypress while in fitering mode. def _get_filtered(self):
""" """Get filtered list of items"""
matches = self.get_filtered_items() matches = self.get_filtered_items()
if not matches: if not matches:
return False return False
_, index = self.walker.get_focus() _, index = self.walker.get_focus()
if key == 'home':
self.list_box.set_focus(matches[0].index, 'below') return (matches, index)
elif key == 'end':
self.list_box.set_focus(matches[-1].index, 'above') def move_to_beginning(self):
elif key == 'up': """Move to the focus to beginning of the songlist"""
self.list_box.set_focus(*self.get_prev_item(matches, index)) matches, _ = self._get_filtered()
else: self.list_box.set_focus(matches[0].index, 'below')
self.list_box.set_focus(*self.get_next_item(matches, index)) 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 return False
@staticmethod @staticmethod

View file

@ -21,6 +21,8 @@ setup(
'console_scripts': [ 'console_scripts': [
'clay=clay.app:main' 'clay=clay.app:main'
] ]
} },
package_data={
'clay': ['config.yaml', 'colours.yaml'],
},
) )