# -*- coding: utf-8 -*-
from pygame import *
import pydub
import sys
import time
import threading
draw_lock = threading.RLock()
class Action:
action_types = [
'command',
'pause',
'play',
'stop',
'stop_all_actions',
'volume',
'wait',
]
def __init__(self, action, key, **kwargs):
if action in self.action_types:
self.action = action
else:
raise Exception("Unknown action {}".format(action))
self.key = key
self.arguments = kwargs
def ready(self):
if 'music' in self.arguments:
return self.arguments['music'].loaded
else:
return True
def run(self):
print(getattr(self, self.action + "_print")(**self.arguments))
return getattr(self, self.action)(**self.arguments)
def command(self, command = "", **kwargs):
pass
def pause(self, music = None, **kwargs):
if music is not None:
music.pause()
else:
mixer.pause()
def play(self, music = None, fade_in = 0, start_at = 0,
restart_if_running = False, volume = 100, **kwargs):
if music is not None:
music.play()
else:
mixer.unpause()
def stop(self, music = None, fade_out = 0, **kwargs):
if music is not None:
music.stop()
else:
mixer.stop()
def stop_all_actions(self, **kwargs):
self.key.mapping.stop_all_running()
def volume(self, music = None, value = 100, **kwargs):
pass
def wait(self, duration = 0, **kwargs):
time.sleep(duration)
def command_print(self, command = "", **kwargs):
return "running command {}".format(command)
def pause_print(self, music = None, **kwargs):
if music is not None:
return "pausing {}".format(music.filename)
else:
return "pausing all musics"
def play_print(self, music = None, fade_in = 0, start_at = 0,
restart_if_running = False, volume = 100, **kwargs):
message = "starting "
if music is not None:
message += music.filename
else:
message += "music"
if start_at != 0:
message += " at {}s".format(start_at)
if fade_in != 0:
message += " with {}s fade_in".format(fade_in)
message += " at volume {}%".format(volume)
if restart_if_running:
message += " (restarting if already running)"
return message
def stop_print(self, music = None, fade_out = 0, **kwargs):
if music is not None:
if fade_out == 0:
return "stopping music {}".format(music.filename)
else:
return "stopping music {} with {}s fadeout".format(music.filename, fade_out)
else:
if fade_out == 0:
return "stopping all musics"
else:
return "stopping all musics with {}s fadeout".format(fade_out)
def stop_all_actions_print(self):
return "stopping all actions"
def volume_print(self, music = None, value = 100, *kwargs):
if music is not None:
return "setting volume of {} to {}%".format(music.filename, value)
else:
return "setting volume to {}%".format(value)
def wait_print(self, duration, **kwargs):
return "waiting {}s".format(duration)
class Key:
row_positions = {
'first': 0,
'second': 50,
'third': 100,
'fourth': 150,
'fifth': 200,
'sixth': 250,
}
default_outer_color = (120, 120, 120)
lighter_outer_color = (200, 200, 200)
default_inner_color = (255, 255, 255)
mapped_inner_color = ( 0, 255, 0)
mapped_unready_inner_color = (255, 165, 0)
def __init__(self, mapping, key_name, key_sym, top, left, width = 48, height = 48, disabled = False):
self.mapping = mapping
self.key_name = key_name
self.key_sym = key_sym
if isinstance(top, str):
self.top = self.row_positions[top]
else:
self.top = top
self.left = left
self.width = width
self.height = height
self.bottom = self.top + self.height
self.right = self.left + self.width
self.rect = (self.left, self.top, self.right, self.bottom)
self.position = (self.left, self.top)
if disabled:
self.outer_color = self.lighter_outer_color
self.linewidth = 1
else:
self.outer_color = self.default_outer_color
self.linewidth = 3
self.inner_color = self.default_inner_color
self.actions = []
def square(self, all_actions_ready):
if self.has_actions():
if all_actions_ready:
self.inner_color = self.mapped_inner_color
else:
self.inner_color = self.mapped_unready_inner_color
return RoundedRect((0, 0, self.width, self.height),
self.outer_color, self.inner_color, self.linewidth)
def collidepoint(self, position):
return self.surface.get_rect().collidepoint(
position[0] - self.position[0],
position[1] - self.position[1]
)
def draw(self, background_surface):
draw_lock.acquire()
all_actions_ready = self.all_actions_ready()
self.surface = self.square(all_actions_ready).surface()
if getattr(sys, 'frozen', False):
police = font.Font(sys._MEIPASS + "/Ubuntu-Regular.ttf", 14)
else:
police = font.Font("Ubuntu-Regular.ttf", 14)
text = police.render(self.key_sym, True, (0,0,0))
self.surface.blit(text, (5,5))
background_surface.blit(self.surface, self.position)
draw_lock.release()
return not all_actions_ready
def poll_redraw(self, background):
while True:
time.sleep(1)
if self.all_actions_ready():
self.draw(background)
self.mapping.blit()
break
def has_actions(self):
return len(self.actions) > 0
def all_actions_ready(self):
return all(action.ready() for action in self.actions)
def add_action(self, action_name, **arguments):
self.actions.append(Action(action_name, self, **arguments))
def do_actions(self):
print("running actions for {}".format(self.key_sym))
start_time = time.time()
self.mapping.start_running(self, start_time)
for action in self.actions:
if self.mapping.keep_running(self, start_time):
action.run()
self.mapping.finished_running(self, start_time)
def list_actions(self, surface):
# FIXME: Todo
print("bouh", self.key_sym)
surface.fill((255, 0, 0))
class Mapping:
WIDTH = 903
HEIGHT = 298
SIZE = WIDTH, HEIGHT
KEYS = [
(K_ESCAPE, 'ESC', 'first', 0, {}),
(K_F1, 'F1', 'first', 100, {}),
(K_F2, 'F2', 'first', 150, {}),
(K_F3, 'F3', 'first', 200, {}),
(K_F4, 'F4', 'first', 250, {}),
(K_F5, 'F5', 'first', 325, {}),
(K_F6, 'F6', 'first', 375, {}),
(K_F7, 'F7', 'first', 425, {}),
(K_F8, 'F8', 'first', 475, {}),
(K_F9, 'F9', 'first', 550, {}),
(K_F10, 'F10', 'first', 600, {}),
(K_F11, 'F11', 'first', 650, {}),
(K_F12, 'F12', 'first', 700, {}),
(178, '²', 'second', 0, {}),
(K_AMPERSAND, '&', 'second', 50, {}),
(233, 'é', 'second', 100, {}),
(K_QUOTEDBL, '"', 'second', 150, {}),
(K_QUOTE, "'", 'second', 200, {}),
(K_LEFTPAREN, '(', 'second', 250, {}),
(K_MINUS, '-', 'second', 300, {}),
(232, 'è', 'second', 350, {}),
(K_UNDERSCORE, '_', 'second', 400, {}),
(231, 'ç', 'second', 450, {}),
(224, 'à', 'second', 500, {}),
(K_RIGHTPAREN, ')', 'second', 550, {}),
(K_EQUALS, '=', 'second', 600, {}),
(K_BACKSPACE, '<-', 'second', 650, { 'width': 98 }),
(K_TAB, 'tab', 'third', 0, { 'width' : 73 }),
(K_a, 'a', 'third', 75, {}),
(K_z, 'z', 'third', 125, {}),
(K_e, 'e', 'third', 175, {}),
(K_r, 'r', 'third', 225, {}),
(K_t, 't', 'third', 275, {}),
(K_y, 'y', 'third', 325, {}),
(K_u, 'u', 'third', 375, {}),
(K_i, 'i', 'third', 425, {}),
(K_o, 'o', 'third', 475, {}),
(K_p, 'p', 'third', 525, {}),
(K_CARET, '^', 'third', 575, {}),
(K_DOLLAR, '$', 'third', 625, {}),
(K_RETURN, 'Enter', 'third', 692, { 'width': 56, 'height': 98 }),
(K_CAPSLOCK, 'CAPS', 'fourth', 0, { 'width': 88, 'disabled': True }),
(K_q, 'q', 'fourth', 90, {}),
(K_s, 's', 'fourth', 140, {}),
(K_d, 'd', 'fourth', 190, {}),
(K_f, 'f', 'fourth', 240, {}),
(K_g, 'g', 'fourth', 290, {}),
(K_h, 'h', 'fourth', 340, {}),
(K_j, 'j', 'fourth', 390, {}),
(K_k, 'k', 'fourth', 440, {}),
(K_l, 'l', 'fourth', 490, {}),
(K_m, 'm', 'fourth', 540, {}),
(249, 'ù', 'fourth', 590, {}),
(K_ASTERISK, '*', 'fourth', 640, {}),
(K_LSHIFT, 'LShift', 'fifth', 0, { 'width': 63, 'disabled': True }),
(K_LESS, '<', 'fifth', 65, {}),
(K_w, 'w', 'fifth', 115, {}),
(K_x, 'x', 'fifth', 165, {}),
(K_c, 'c', 'fifth', 215, {}),
(K_v, 'v', 'fifth', 265, {}),
(K_b, 'b', 'fifth', 315, {}),
(K_n, 'n', 'fifth', 365, {}),
(K_COMMA, ',', 'fifth', 415, {}),
(K_SEMICOLON, ';', 'fifth', 465, {}),
(K_COLON, ':', 'fifth', 515, {}),
(K_EXCLAIM, '!', 'fifth', 565, {}),
(K_RSHIFT, 'RShift', 'fifth', 615, { 'width': 133, 'disabled': True }),
(K_LCTRL, 'LCtrl', 'sixth', 0, { 'width': 63, 'disabled': True }),
(K_LSUPER, 'LSuper', 'sixth', 115, { 'disabled': True }),
(K_LALT, 'LAlt', 'sixth', 165, { 'disabled': True }),
(K_SPACE, 'Espace', 'sixth', 215, { 'width': 248 }),
(K_MODE, 'AltGr', 'sixth', 465, { 'disabled': True }),
(314, 'Compose', 'sixth', 515, { 'disabled': True }),
(K_RCTRL, 'RCtrl', 'sixth', 565, { 'width': 63, 'disabled': True }),
(K_INSERT, 'ins', 'second', 755, {}),
(K_HOME, 'home', 'second', 805, {}),
(K_PAGEUP, 'pg_u', 'second', 855, {}),
(K_DELETE, 'del', 'third', 755, {}),
(K_END, 'end', 'third', 805, {}),
(K_PAGEDOWN, 'pg_d', 'third', 855, {}),
(K_UP, 'up', 'fifth', 805, {}),
(K_DOWN, 'down', 'sixth', 805, {}),
(K_LEFT, 'left', 'sixth', 755, {}),
(K_RIGHT, 'right', 'sixth', 855, {}),
]
def __init__(self, screen):
self.screen = screen
self.background = Surface(self.SIZE).convert()
self.background.fill((250, 250, 250))
self.keys = {}
self.running = []
for key in self.KEYS:
self.keys[key[0]] = Key(self, *key[0:4], **key[4])
def draw(self):
for key_name in self.keys:
key = self.keys[key_name]
should_redraw_key = key.draw(self.background)
if should_redraw_key:
threading.Thread(target = key.poll_redraw, args = [self.background]).start()
self.blit()
def blit(self):
draw_lock.acquire()
self.screen.blit(self.background, (5, 5))
display.flip()
draw_lock.release()
def find_by_key_num(self, key_num):
if key_num in self.keys:
return self.keys[key_num]
return None
def find_by_collidepoint(self, position):
for key in self.keys:
if self.keys[key].collidepoint(position):
return self.keys[key]
return None
def find_by_unicode(self, key_sym):
for key in self.keys:
if self.keys[key].key_sym == key_sym:
return self.keys[key]
return None
def stop_all_running(self):
self.running = []
def start_running(self, key, start_time):
self.running.append((key, start_time))
def keep_running(self, key, start_time):
return (key, start_time) in self.running
def finished_running(self, key, start_time):
if (key, start_time) in self.running:
self.running.remove((key, start_time))
class MusicFile:
def __init__(self, filename, lock):
self.filename = filename
self.channel = None
self.raw_data = None
self.sound = None
self.loaded = False
threading.Thread(target = self.load_sound, args = [lock]).start()
def load_sound(self, lock):
lock.acquire()
print("Loading {}".format(self.filename))
self.raw_data = pydub.AudioSegment.from_file(self.filename).raw_data
self.sound = mixer.Sound(self.raw_data)
print("Loaded {}".format(self.filename))
self.loaded = True
lock.release()
def play(self):
self.channel = self.sound.play()
def pause(self):
if self.channel is not None:
self.channel.pause()
def stop(self):
self.channel = None
self.sound.stop()
class RoundedRect:
def __init__(self, rect, outer_color, inner_color, linewidth = 2, radius = 0.4):
self.rect = Rect(rect)
self.outer_color = Color(*outer_color)
self.inner_color = Color(*inner_color)
self.linewidth = linewidth
self.radius = radius
def surface(self):
rectangle = self.filledRoundedRect(self.rect, self.outer_color, self.radius)
inner_rect = Rect((
self.rect.left + 2 * self.linewidth,
self.rect.top + 2 * self.linewidth,
self.rect.right - 2 * self.linewidth,
self.rect.bottom - 2 * self.linewidth
))
inner_rectangle = self.filledRoundedRect(inner_rect, self.inner_color, self.radius)
rectangle.blit(inner_rectangle, (self.linewidth, self.linewidth))
return rectangle
def filledRoundedRect(self, rect, color, radius=0.4):
"""
filledRoundedRect(rect,color,radius=0.4)
rect : rectangle
color : rgb or rgba
radius : 0 <= radius <= 1
"""
alpha = color.a
color.a = 0
pos = rect.topleft
rect.topleft = 0,0
rectangle = Surface(rect.size,SRCALPHA)
circle = Surface([min(rect.size)*3]*2,SRCALPHA)
draw.ellipse(circle,(0,0,0),circle.get_rect(),0)
circle = transform.smoothscale(circle,[int(min(rect.size)*radius)]*2)
radius = rectangle.blit(circle,(0,0))
radius.bottomright = rect.bottomright
rectangle.blit(circle,radius)
radius.topright = rect.topright
rectangle.blit(circle,radius)
radius.bottomleft = rect.bottomleft
rectangle.blit(circle,radius)
rectangle.fill((0,0,0),rect.inflate(-radius.w,0))
rectangle.fill((0,0,0),rect.inflate(0,-radius.h))
rectangle.fill(color,special_flags=BLEND_RGBA_MAX)
rectangle.fill((255,255,255,alpha),special_flags=BLEND_RGBA_MIN)
return rectangle
def parse_config(mapping):
import yaml
stream = open("config.yml", "r")
config = yaml.load(stream)
stream.close()
aliases = config['aliases']
seen_files = {}
file_lock = threading.RLock()
for mapped_key in config['keys']:
key = mapping.find_by_unicode(mapped_key)
if key is None:
continue
for action in config['keys'][mapped_key]:
action_name = list(action)[0]
action_args = {}
if action[action_name] is None:
action[action_name] = []
if 'include' in action[action_name]:
included = action[action_name]['include']
del(action[action_name]['include'])
if isinstance(included, str):
action[action_name].update(aliases[included], **action[action_name])
else:
for included_ in included:
action[action_name].update(aliases[included_], **action[action_name])
for argument in action[action_name]:
if argument == 'file':
filename = action[action_name]['file']
if filename not in seen_files:
seen_files[filename] = MusicFile(filename, file_lock)
action_args['music'] = seen_files[filename]
else:
action_args[argument] = action[action_name][argument]
key.add_action(action_name, **action_args)