Commit a591e9e6 authored by Daniel Schulte's avatar Daniel Schulte Committed by Christopher Spinrath

Added screenkey as an alternative to key-mon, etc

parent 7e5e2d8c
Copyright (c) 2010 Pablo Seminario <pabluk@gmail.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
include screenkey README LICENSE
recursive-include data *
Screenkey v0.2
http://launchpad.net/screenkey
About
=====
Screencast your keys.
A screencast tool to display your keys inspired by Screenflick
and initially based on the key-mon project code.
Usage
=====
Download the latest version from https://launchpad.net/screenkey/+download
and you need install python-xlib.
To run without installing (change x.x by current version number)
tar xvfz screenkey-x.x.tar.gz
cd screenkey-x.x/
sudo ./screenkey
To install
tar xvfz screenkey-x.x.tar.gz
cd screenkey-x.x/
sudo ./setup.py install
Or you can use dpkg (Debian and Ubuntu)
sudo dpkg -i screenkey_x.x-y_all.deb
Author
======
Pablo Seminario <pabluk@gmail.com>
Thanks to
=========
Jacob Gardner
farrer
Ivan Makfinsky
Muneeb Shaikh
License
=======
Copyright (c) 2010 Pablo Seminario <pabluk@gmail.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
APP_NAME = "Screenkey"
APP_DESC = _("Screencast your keys")
APP_URL = 'http://launchpad.net/screenkey'
VERSION = '0.2'
AUTHOR = 'Pablo Seminario'
# Copyright (c) 2010 Pablo Seminario <pabluk@gmail.com>
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import threading
import time
import sys
import subprocess
import modmap
import gtk
from Xlib import X, XK, display
from Xlib.ext import record
from Xlib.protocol import rq
MODE_RAW = 0
MODE_NORMAL = 1
REPLACE_KEYS = {
'XK_Escape':_('Esc '),
'XK_Tab':u'\u21B9 ',
'XK_Return':u'\u23CE ',
'XK_Space':u' ',
'XK_Caps_Lock':_('Caps '),
'XK_F1':u'F1 ',
'XK_F2':u'F2 ',
'XK_F3':u'F3 ',
'XK_F4':u'F4 ',
'XK_F5':u'F5 ',
'XK_F6':u'F6 ',
'XK_F7':u'F7 ',
'XK_F8':u'F8 ',
'XK_F9':u'F9 ',
'XK_F10':u'F10 ',
'XK_F11':u'F11 ',
'XK_F12':u'F12 ',
'XK_Home':_('Home '),
'XK_Up':u'\u2191',
'XK_Page_Up':_('PgUp '),
'XK_Left':u'\u2190',
'XK_Right':u'\u2192',
'XK_End':_('End '),
'XK_Down':u'\u2193',
'XK_Next':_('PgDn '),
'XK_Insert':_('Ins '),
'XK_Delete':_('Del '),
'XK_KP_Home':u'(7)',
'XK_KP_Up':u'(8)',
'XK_KP_Prior':u'(9)',
'XK_KP_Left':u'(4)',
'XK_KP_Right':u'(6)',
'XK_KP_End':u'(1)',
'XK_KP_Down':u'(2)',
'XK_KP_Page_Down':u'(3)',
'XK_KP_Begin':u'(5)',
'XK_KP_Insert':u'(0)',
'XK_KP_Delete':u'(.)',
'XK_KP_Add':u'(+)',
'XK_KP_Subtract':u'(-)',
'XK_KP_Multiply':u'(*)',
'XK_KP_Divide':u'(/)',
'XK_Num_Lock':u'NumLock ',
'XK_KP_Enter':u'\u23CE ',
}
class ListenKbd(threading.Thread):
def __init__(self, label, logger, mode):
threading.Thread.__init__(self)
self.mode = mode
self.logger = logger
self.label = label
self.text = ""
self.command = None
self.shift = None
self.cmd_keys = {
'shift': False,
'ctrl': False,
'alt': False,
'capslock': False,
'meta': False,
'super':False
}
self.logger.debug("Thread created")
self.keymap = modmap.get_keymap_table()
self.modifiers = modmap.get_modifier_map()
self.local_dpy = display.Display()
self.record_dpy = display.Display()
if not self.record_dpy.has_extension("RECORD"):
self.logger.error("RECORD extension not found.")
print "RECORD extension not found"
sys.exit(1)
self.ctx = self.record_dpy.record_create_context(
0,
[record.AllClients],
[{
'core_requests': (0, 0),
'core_replies': (0, 0),
'ext_requests': (0, 0, 0, 0),
'ext_replies': (0, 0, 0, 0),
'delivered_events': (0, 0),
'device_events': (X.KeyPress, X.KeyRelease),
'errors': (0, 0),
'client_started': False,
'client_died': False,
}])
def run(self):
self.logger.debug("Thread started.")
self.record_dpy.record_enable_context(self.ctx, self.key_press)
def lookup_keysym(self, keysym):
for name in dir(XK):
if name[:3] == "XK_" and getattr(XK, name) == keysym:
return name[3:]
return ""
def replace_key(self, key, keysym):
for name in dir(XK):
if name[:3] == "XK_" and getattr(XK, name) == keysym:
if name in REPLACE_KEYS:
return REPLACE_KEYS[name]
def update_text(self, string=None):
gtk.gdk.threads_enter()
if not string is None:
self.text = "%s%s" % (self.label.get_text(), string)
self.label.set_text(self.text)
else:
self.label.set_text("")
gtk.gdk.threads_leave()
self.label.emit("text-changed")
def key_press(self, reply):
# FIXME:
# This is not the most efficient way to detect the
# use of sudo/gksudo but it works.
sudo_is_running = subprocess.call(['ps', '-C', 'sudo'],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
if not sudo_is_running:
return
if reply.category != record.FromServer:
return
if reply.client_swapped:
self.logger.warning(
"* received swapped protocol data, cowardly ignored"
)
return
if not len(reply.data) or ord(reply.data[0]) < 2:
# not an event
return
data = reply.data
key = None
while len(data):
event, data = rq.EventField(None).parse_binary_value(data,
self.record_dpy.display, None, None)
if event.type in [X.KeyPress, X.KeyRelease]:
if self.mode == MODE_NORMAL:
key = self.key_normal_mode(event)
if self.mode == MODE_RAW:
key = self.key_raw_mode(event)
if not key:
return
self.update_text(key)
def key_normal_mode(self, event):
key = ''
mod = ''
keysym = self.local_dpy.keycode_to_keysym(event.detail, 0)
if event.detail in self.keymap:
key_normal, key_shift, key_dead, key_deadshift = \
self.keymap[event.detail]
self.logger.debug("Key %s(keycode) %s. Symbols %s" %
(event.detail,
event.type == X.KeyPress and "pressed" or "released",
self.keymap[event.detail])
)
else:
self.logger.debug('No mapping for scan_code %d' % event.detail)
return
# Alt key
if event.detail in self.modifiers['mod1']:
if event.type == X.KeyPress:
self.cmd_keys['alt'] = True
else:
self.cmd_keys['alt'] = False
return
# Meta key
# Fixme: it must use self.modifiers['mod5']
# but doesn't work
if event.detail == 108:
if event.type == X.KeyPress:
self.cmd_keys['meta'] = True
else:
self.cmd_keys['meta'] = False
return
# Super key
if event.detail in self.modifiers['mod4']:
if event.type == X.KeyPress:
self.cmd_keys['super'] = True
else:
self.cmd_keys['super'] = False
return
# Ctrl keys
elif event.detail in self.modifiers['control']:
if event.type == X.KeyPress:
self.cmd_keys['ctrl'] = True
else:
self.cmd_keys['ctrl'] = False
return
# Shift keys
elif event.detail in self.modifiers['shift']:
if event.type == X.KeyPress:
self.cmd_keys['shift'] = True
else:
self.cmd_keys['shift'] = False
return
# Capslock key
elif event.detail in self.modifiers['lock']:
if event.type == X.KeyPress:
if self.cmd_keys['capslock']:
self.cmd_keys['capslock'] = False
else:
self.cmd_keys['capslock'] = True
return
# Backspace key
elif event.detail == 22 and event.type == X.KeyPress:
gtk.gdk.threads_enter()
if len(self.label.get_text()) > 0:
self.label.set_text(
unicode(self.label.get_text(), 'utf-8')[:-1]
)
key = ""
gtk.gdk.threads_leave()
else:
gtk.gdk.threads_leave()
return
else:
if event.type == X.KeyPress:
key = key_normal
if self.cmd_keys['ctrl']:
mod = mod + _("Ctrl+")
if self.cmd_keys['alt']:
mod = mod + _("Alt+")
if self.cmd_keys['super']:
mod = mod + _("Super+")
if self.cmd_keys['shift']:
key = key_shift
if self.cmd_keys['capslock'] \
and ord(key_normal) in range(97,123):
key = key_shift
if self.cmd_keys['meta']:
key = key_dead
if self.cmd_keys['shift'] and self.cmd_keys['meta']:
key = key_deadshift
string = self.replace_key(key, keysym)
if string:
key = string
if mod != '':
key = "%s%s " % (mod, key)
else:
key = "%s%s" % (mod, key)
else:
return
return key
def key_raw_mode(self, event):
key = ''
if event.type == X.KeyPress:
keysym = self.local_dpy.keycode_to_keysym(event.detail, 0)
key = self.lookup_keysym(keysym)
else:
return
return key
def stop(self):
self.local_dpy.record_disable_context(self.ctx)
self.local_dpy.flush()
self.record_dpy.record_free_context(self.ctx)
self.logger.debug("Thread stopped.")
#!/usr/bin/env python
# Copyright (c) 2010 Pablo Seminario <pabluk@gmail.com>
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import re
import subprocess
def cmd_keymap_table():
return subprocess.Popen(
['xmodmap','-pk'], stdout=subprocess.PIPE).communicate()[0]
def cmd_modifier_map():
return subprocess.Popen(
['xmodmap','-pm'], stdout=subprocess.PIPE).communicate()[0]
def get_keymap_table():
keymap = {}
keymap_table = cmd_keymap_table()
re_line = re.compile(r'0x\w+')
for line in keymap_table.split('\n')[1:]:
if len(line) > 0:
keycode = re.search(r'\s+(\d+).*', line)
if keycode:
new_keysyms = []
keycode = int(keycode.group(1))
keysyms = re_line.findall(line)
# When you press only one key
unicode_char = ''
try:
unicode_char = unichr(int(keysyms[0], 16))
except:
unicode_char = ''
if unicode_char == u'\x00':
unicode_char = ''
new_keysyms.append(unicode_char)
# When you press a key plus Shift key
unicode_char = ''
try:
unicode_char = unichr(int(keysyms[1], 16))
except:
unicode_char = ''
if unicode_char == u'\x00':
unicode_char = ''
new_keysyms.append(unicode_char)
# When you press a key plus meta (dead keys)
unicode_char = ''
try:
unicode_char = unichr(int(keysyms[4], 16))
except:
unicode_char = ''
if unicode_char == u'\x00':
unicode_char = ''
new_keysyms.append(unicode_char)
# When you press a key plus meta plus Shift key
unicode_char = ''
try:
unicode_char = unichr(int(keysyms[5], 16))
except:
unicode_char = ''
if unicode_char == u'\x00':
unicode_char = ''
new_keysyms.append(unicode_char)
#keymap[keycode-8] = new_keysyms
keymap[keycode] = new_keysyms
return keymap
def get_modifier_map():
modifiers = {}
modifier_map = cmd_modifier_map()
re_line = re.compile(r'(0x\w+)')
for line in modifier_map.split('\n')[1:]:
if len(line) > 0:
mod_name = re.match(r'(\w+).*', line)
if mod_name:
mod_name = mod_name.group(1)
keycodes = re_line.findall(line)
# Convert key codes from hex to dec for use them
# with the keymap table
keycodes =[ int(kc, 16) for kc in keycodes]
modifiers[mod_name] = keycodes
return modifiers
# Copyright (c) 2010 Pablo Seminario <pabluk@gmail.com>
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import pygtk
pygtk.require('2.0')
import gtk
import gobject
import glib
import cairo
import pango
import pickle
from threading import Timer
from Screenkey import APP_NAME, APP_DESC, APP_URL, VERSION, AUTHOR
from listenkbd import ListenKbd
POS_TOP = 0
POS_CENTER = 1
POS_BOTTOM = 2
SIZE_LARGE = 0
SIZE_MEDIUM = 1
SIZE_SMALL = 2
MODE_RAW = 0
MODE_NORMAL = 1
class Screenkey(gtk.Window):
POSITIONS = {
POS_TOP:_('Top'),
POS_CENTER:_('Center'),
POS_BOTTOM:_('Bottom'),
}
SIZES = {
SIZE_LARGE:_('Large'),
SIZE_MEDIUM:_('Medium'),
SIZE_SMALL:_('Small'),
}
MODES = {
MODE_RAW:_('Raw'),
MODE_NORMAL:_('Normal'),
}
STATE_FILE = os.path.join(glib.get_user_cache_dir(),
'screenkey.dat')
def __init__(self, logger, nodetach):
gtk.Window.__init__(self)
self.timer = None
self.logger = logger
default_options = {
'timeout': 2.5,
'position': POS_BOTTOM,
'size': SIZE_MEDIUM,
'opacity': 0.7,
'mode': MODE_NORMAL,
}
self.options = self.load_state()
for key, value in default_options.items():
if key not in self.options:
self.options[key] = value
if not nodetach:
self.logger.debug("Detach from the parent.")
self.drop_tty()
self.set_skip_taskbar_hint(True)
self.set_skip_pager_hint(True)
self.set_keep_above(True)
self.set_decorated(False)
self.stick()
self.set_property('accept-focus', False)
self.set_property('focus-on-map', False)
self.set_position(gtk.WIN_POS_CENTER)
self.bgcolor = gtk.gdk.color_parse("black")
self.modify_bg(gtk.STATE_NORMAL, self.bgcolor)
gobject.signal_new("text-changed", gtk.Label,
gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ())
self.label = gtk.Label()
self.label.set_justify(gtk.JUSTIFY_RIGHT)
self.label.set_ellipsize(pango.ELLIPSIZE_START)
self.label.connect("text-changed", self.on_label_change)
self.label.show()
self.add(self.label)
self.compositing = self.is_composited()
if self.compositing:
# Use cairo and rgba palette
self.set_app_paintable(True)
self.connect("screen_changed", self.on_screen_changed)
self.connect("expose_event", self.on_expose)
self.set_screen_colormap()
# XShape extension can be used otherwise, not implemented yet
self.screen_width = gtk.gdk.screen_width()
self.screen_height = gtk.gdk.screen_height()
self.set_window_size(self.options['size'])
self.set_gravity(gtk.gdk.GRAVITY_CENTER)
self.set_xy_position(self.options['position'])
self.listenkbd = ListenKbd(self.label, logger=self.logger,
mode=self.options['mode'])
self.listenkbd.start()
menu = gtk.Menu()
show_item = gtk.CheckMenuItem(_("Show keys"))
show_item.set_active(True)
show_item.connect("toggled", self.on_show_keys)
show_item.show()
menu.append(show_item)
preferences_item = gtk.ImageMenuItem(gtk.STOCK_PREFERENCES)
preferences_item.connect("activate", self.on_preferences_dialog)
preferences_item.show()
menu.append(preferences_item)
about_item = gtk.ImageMenuItem(gtk.STOCK_ABOUT)
about_item.connect("activate", self.on_about_dialog)
about_item.show()
menu.append(about_item)
separator_item = gtk.SeparatorMenuItem()
separator_item.show()
menu.append(separator_item)
image = gtk.ImageMenuItem(gtk.STOCK_QUIT)
image.connect("activate", self.quit)
image.show()
menu.append(image)
menu.show()
try:
import appindicator
self.systray = appindicator.Indicator(APP_NAME,
'indicator-messages',
appindicator.CATEGORY_APPLICATION_STATUS)
self.systray.set_status(appindicator.STATUS_ACTIVE)
self.systray.set_attention_icon("indicator-messages-new")
self.systray.set_icon(
"preferences-desktop-keyboard-shortcuts")
self.systray.set_menu(menu)
self.logger.debug("Using AppIndicator.")
except(ImportError):
self.systray = gtk.StatusIcon()
self.systray.set_from_icon_name(
"preferences-desktop-keyboard-shortcuts")
self.systray.connect("popup-menu",
self.on_statusicon_popup, menu)
self.logger.debug("Using StatusIcon.")
self.connect("delete-event", self.quit)
def quit(self, widget, data=None):
self.listenkbd.stop()
gtk.main_quit()
def load_state(self):
"""Load stored options"""
options = {}
try:
f = open(self.STATE_FILE, 'r')
try:
options = pickle.load(f)
self.logger.debug("Options loaded.")
except:
f.close()
except IOError:
self.logger.debug("file %s does not exists." %
self.STATE_FILE)
return options
def store_state(self, options):
"""Store options"""
try:
f = open(self.STATE_FILE, 'w')
try:
pickle.dump(options, f)
self.logger.debug("Options saved.")
except:
f.close()
except IOError:
self.logger.debug("Cannot open %s." % self.STATE_FILE)
def set_screen_colormap(self):
screen = self.get_screen()
rgba = screen.get_rgba_colormap()
if rgba is None:
self.set_colormap(screen.get_rgb_colormap())
self.compositing = False
else:
self.set_colormap(rgba)
self.compositing = True
def set_opacity(self, setting, region=None):