#! /usr/bin/env python # # Skippy 0.2.0 -- Seducing Kids Into Perversion # Copyright (C) 2004 Hyriand # # 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 2 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. # # GNU General Public License is available online at: # http://www.gnu.org/licenses/gpl.html # # Config file example (~/.skippyrc): """ [general] keysym = F11 [border] width = 5 distance = 50 color = black highlight = white """ from Xlib import display, Xatom, X, Xutil, rdb from Xlib.xobject.drawable import Window, Pixmap import Xlib.protocol.event import Xlib.protocol.rq import Xlib.XK import imlib2 import math, time, sys, os import ConfigParser # Default config: DISTANCE = 50 BORDERCOLOR = "black" HILITECOLOR = "white" BORDERWIDTH = 5 GRAB_KEY = "F11" cfg = ConfigParser.ConfigParser() cfg.read(os.path.expanduser("~/.skippyrc")) if cfg.has_section("border"): if cfg.has_option("border", "distance"): DISTANCE = cfg.getint("border", "distance") if cfg.has_option("border", "color"): BORDERCOLOR = cfg.get("border", "color") if cfg.has_option("border", "highlight"): HILITECOLOR = cfg.get("border", "highlight") if cfg.has_option("border", "width"): BORDERWIDTH = cfg.getint("border", "width") if cfg.has_section("general"): if cfg.has_option("general", "keysym"): GRAB_KEY = cfg.get("general", "keysym") def lhl(l, sortfunc = cmp): l.sort(lambda a,b:sortfunc(b,a)) a, b = l[:1], l[1:] switch = False for e in b: if switch: a.append(e) else: a.insert(0, e) switch = not switch return a class Client: def __init__(self, skippy, win): self.skippy = skippy self.client = win self.x = self.y = 0 self.pressed = False self.pixmap = None self.img = None self.mapped = False self.want_focus = False self.DOWN = self.skippy.display.keysym_to_keycode(Xlib.XK.XK_Down) self.UP = self.skippy.display.keysym_to_keycode(Xlib.XK.XK_Up) self.LEFT = self.skippy.display.keysym_to_keycode(Xlib.XK.XK_Left) self.RIGHT = self.skippy.display.keysym_to_keycode(Xlib.XK.XK_Right) self.ACTIVATE = [self.skippy.display.keysym_to_keycode(Xlib.XK.XK_Return), self.skippy.display.keysym_to_keycode(Xlib.XK.XK_space), self.skippy.display.keysym_to_keycode(getattr(Xlib.XK, "XK_" + GRAB_KEY))] self.window = self.skippy.window.create_window( 0, 0, 1, 1, BORDERWIDTH, self.skippy.depth, X.InputOutput, event_mask = ( X.ButtonPressMask | X.ButtonReleaseMask | X.KeyReleaseMask | X.EnterWindowMask | X.FocusChangeMask ), background_pixel = self.skippy.screen.black_pixel, border_pixel = self.skippy.border_pixel ) def destroy(self): self.window.destroy() if self.pixmap: self.pixmap.free() def geometry(self): geom = self.client.get_geometry() self.width = geom.width self.height = geom.height return geom.width, geom.height def configure(self, factor): w = self.s_width = self.width * factor h = self.s_height = self.height * factor x = self.s_x = self.skippy.left + self.x * factor y = self.s_y = self.skippy.top + self.y * factor if self.pixmap: self.pixmap.free() self.pixmap = self.window.create_pixmap(w, h, self.skippy.depth) self.skippy.display.flush() self.skippy.imlibx.render_image_on_drawable(self.img, self.pixmap.id, dest_rect=[0,0, w, h]) self.window.configure(x = x, y = y, width = w, height = h) self.window.change_attributes(background_pixmap = self.pixmap) self.mapped = True self.window.map() def unmap(self): self.mapped = False self.window.unmap() def snap(self, resnap = False): self.geometry() if resnap or not self.img or self.img.get_width() != self.width or self.img.get_height() != self.height: self.client.configure(stack_mode = X.Above) self.skippy.display.sync() time.sleep(0.3) self.skippy.display.sync() self.img = self.skippy.imlibx.image_from_drawable(self.client.id, 0, 0, 0, self.width, self.height) def focus(self): self.want_focus = False self.window.set_input_focus(X.RevertToParent, X.CurrentTime) def activate(self): self.skippy.stop() self.client.configure(stack_mode = X.Above) self.skippy.display.sync() time.sleep(0.1) self.client.set_input_focus(X.RevertToNone, X.CurrentTime) self.skippy.display.sync() def handle(self, e, stack): id = e.window.id if e.type == X.ButtonPress and e.detail == 1 and id == self.window.id: self.pressed = True elif e.type == X.ButtonRelease and e.detail == 1: if id == self.window.id and self.pressed: self.activate() self.pressed = False elif e.type == X.FocusIn: if id == self.window.id: self.window.change_attributes(border_pixel = self.skippy.hilite_pixel) else: self.window.change_attributes(border_pixel = self.skippy.border_pixel) elif e.type == X.FocusOut and id == self.window.id: self.window.change_attributes(border_pixel = self.skippy.border_pixel) elif e.type == X.EnterNotify and id == self.window.id: self.focus() elif e.type == X.KeyRelease and id == self.window.id: if e.detail == self.DOWN: self.focus_down(stack) elif e.detail == self.UP: self.focus_up(stack) elif e.detail == self.LEFT: self.focus_left(stack) elif e.detail == self.RIGHT: self.focus_right(stack) elif e.detail in self.ACTIVATE: self.activate() def dir_focus(self, stack, cond_f, dist_f): candidates = [win for win in stack if cond_f(self, win)] if not candidates: return candidate = diff = None for win in candidates: distance = dist_f(self, win) if candidate is None or distance < diff: candidate = win diff = distance candidate.focus() def focus_down(self, stack): half_f = lambda a: a.s_x + a.s_width / 2 self.dir_focus(stack, lambda a, b: b.s_y > a.s_y + a.s_height, lambda a, b: math.sqrt((half_f(a) - half_f(b))**2 + (a.s_y+a.s_height - b.s_y)**2) ) def focus_right(self, stack): half_f = lambda a: a.s_y + a.s_height / 2 self.dir_focus(stack, lambda a, b: b.s_x > a.s_x + a.s_width, lambda a, b: math.sqrt((half_f(a) - half_f(b))**2 + (a.s_x+a.s_width - b.s_x)**2) ) def focus_up(self, stack): half_f = lambda a: a.s_x + a.s_width / 2 self.dir_focus(stack, lambda a, b: b.s_y + b.s_height < a.s_y, lambda a, b: math.sqrt((half_f(a) - half_f(b))**2 + (a.s_y - (b.s_y + b.s_height))**2) ) def focus_left(self, stack): half_f = lambda a: a.s_y + a.s_height / 2 self.dir_focus(stack, lambda a, b: b.s_x + b.s_width < a.s_x, lambda a, b: math.sqrt((half_f(a) - half_f(b))**2 + (a.s_x - (b.s_x + b.s_width))**2) ) class Skippy: def __init__(self): self.die = False self.running = False self.display = display.Display() self.screen = self.display.screen() self.root = self.screen.root self.depth = self.screen.root_depth colormap = self.root.list_installed_colormaps()[0] color = colormap.alloc_named_color(BORDERCOLOR) self.border_pixel = color.pixel color = colormap.alloc_named_color(HILITECOLOR) self.hilite_pixel = color.pixel self.context = imlib2.Context() self.context.push() self.context.set_blend(1) self.imlibx = imlib2.ImlibX() self.stack = {} self.root_pmap = None self.root.change_attributes(event_mask = X.PropertyChangeMask | X.KeyPressMask) self.get_atoms() if not self.wm_check(): raise RuntimeError, "WM does not support NET properties or no WM running" if not self.check_atoms(): raise RuntimeError, "Required properties not supported" geom = self.root.get_geometry() self.width = geom.width self.height = geom.height self.get_root_pmap() self.make_window() try: keysym = getattr(Xlib.XK, "XK_" + GRAB_KEY) except: print "invalid keysym" sys.exit() keycode = self.display.keysym_to_keycode(keysym) self.root.grab_key(keycode, X.AnyModifier, True, X.GrabModeAsync, X.GrabModeAsync, onerror = self.grab_error) while not self.die: e = self.display.next_event() if e.type == X.KeyRelease and e.detail == keycode: self.run((e.state & X.ControlMask) == X.ControlMask) def grab_error(self, *args): print "Couldn't grab the key.. sorry.." self.die = True def run(self, resnap = False): self.running = True focus = self.display.get_input_focus().focus cur_desktop = self.get_current_desktop() cur_stack = self.get_stacking_list() stack = [win for win in cur_stack if self.validate_window(win, cur_desktop)] if not stack: return windows = self.stack.keys() for win in stack: if not win in windows: self.stack[win] = Client(self, win) for win in [win for win in self.stack.keys() if win not in cur_stack]: self.stack[win].destroy() del self.stack[win] for win in [win for win in self.stack.keys() if win not in stack]: self.stack[win].unmap() winstack = [self.stack[win] for win in stack] for win in winstack: win.snap(resnap) winstack = lhl(winstack, lambda a,b: cmp(a.height * a.width**2, b.height * b.width**2)) total_width, total_height = self.layout(winstack) factor = float(self.width - 2*DISTANCE) / float(total_width) if float(total_height) * factor > self.height - 2*DISTANCE: factor = float(self.height - 2*DISTANCE) / float(total_height) self.left = (self.width - total_width*factor) / 2 self.top = (self.height - total_height*factor) / 2 for win in [self.stack[win] for win in self.stack if win in stack]: win.configure(factor) if focus in stack: self.stack[focus].want_focus = True else: winstack[0].want_focus = True self.map() while self.running: e = self.display.next_event() if e.window == self.toplevel and e.type == X.VisibilityNotify and e.state == 1: for win in winstack: if win.want_focus: win.focus() for win in winstack: win.handle(e, winstack) def stop(self): self.toplevel.unmap() self.display.sync() self.running = False def get_atoms(self): self._NET_SUPPORTED = self.display.intern_atom("_NET_SUPPORTED") self._NET_SUPPORTING_WM_CHECK = self.display.intern_atom("_NET_SUPPORTING_WM_CHECK") self._NET_CLIENT_LIST = self.display.intern_atom("_NET_CLIENT_LIST") self._NET_CLIENT_LIST_STACKING = self.display.intern_atom("_NET_CLIENT_LIST_STACKING") self._NET_NUMBER_OF_DESKTOPS = self.display.intern_atom("_NET_NUMBER_OF_DESKTOPS") self._NET_DESKTOP_GEOMETRY = self.display.intern_atom("_NET_DESKTOP_GEOMETRY") self._NET_CURRENT_DESKTOP = self.display.intern_atom("_NET_CURRENT_DESKTOP") self._NET_WM_DESKTOP = self.display.intern_atom("_NET_WM_DESKTOP") self._WM_STATE = self.display.intern_atom("WM_STATE") self._NET_ACTIVE_WINDOW = self.display.intern_atom("_NET_ACTIVE_WINDOW") self._NET_WM_STATE = self.display.intern_atom("_NET_WM_STATE") self._NET_WM_STATE_HIDDEN = self.display.intern_atom("_NET_WM_STATE_HIDDEN") self._NET_WM_STATE_SHADED = self.display.intern_atom("_NET_WM_STATE_SHADED") self._NET_WM_STATE_SKIP_TASKBAR = self.display.intern_atom("_NET_WM_STATE_SKIP_TASKBAR") self._NET_WM_STATE_FULLSCREEN = self.display.intern_atom("_NET_WM_STATE_FULLSCREEN") self._NET_WM_ICON = self.display.intern_atom("_NET_WM_ICON") self._XROOTPMAP_ID = self.display.intern_atom("_XROOTPMAP_ID") self.ESETROOT_PMAP_ID = self.display.intern_atom("ESETROOT_PMAP_ID") def get_current_desktop(self): desktop = self.root.get_full_property(self._NET_CURRENT_DESKTOP, Xatom.CARDINAL) return desktop.value[0] def wm_check(self): wm_check = self.root.get_full_property(self._NET_SUPPORTING_WM_CHECK, Xatom.WINDOW) if not wm_check: return False win = Window(self.display.display, wm_check.value[0]) wm_check = win.get_full_property(self._NET_SUPPORTING_WM_CHECK, Xatom.WINDOW) if win.id == wm_check.value[0]: return True return False def check_atoms(self): supported = self.root.get_full_property(self._NET_SUPPORTED, Xatom.ATOM) if self._NET_NUMBER_OF_DESKTOPS not in supported.value: return False return True def get_stacking_list(self): stack = self.root.get_full_property(self._NET_CLIENT_LIST_STACKING, Xatom.WINDOW) if stack is None: return [] winlist = [] for w_id in stack.value: win = Window(self.display.display, w_id) winlist.append(win) return winlist def validate_window(self, win, desktop): try: state = win.get_full_property(self._NET_WM_STATE, 0) except Exception, e: print e return False if not state: return False if self._NET_WM_STATE_HIDDEN in state.value: return False if self._NET_WM_STATE_SKIP_TASKBAR in state.value: return False if self._NET_WM_STATE_SHADED in state.value: return False mask = win.get_full_property(self._NET_WM_DESKTOP, Xatom.CARDINAL) if not mask: return False if mask.value[0] not in (4294967295, desktop): return False return True def new_layout(self, windows): items_per_line = int(round(math.sqrt(len(windows)))) half = items_per_line / 2.0 col_w = [0] * items_per_line n = 0 for win in windows: col_w[n] = max(col_w[n], win.width) n = (n + 1) % items_per_line total_width = reduce(lambda x,y: x+y+DISTANCE, col_w, -DISTANCE) row_h = n = cx = cy = 0 for win in windows: row_h = max(win.height, row_h) win.x = cx win.y = cy cx += col_w[n] + DISTANCE n += 1 if n == items_per_line: cy += row_h + DISTANCE cx = row_h = n = 0 if row_h: cy += row_h else: cy -= DISTANCE return total_width, cy # Pretty literal conversion from Expocity def layout(self, windows): items_per_line = int(math.ceil(math.sqrt(len(windows)) + 0.5)) slots = [[0] * 4] * len(windows) slots_number = 0 line_max = [0] * int(math.ceil(math.sqrt(len(windows)))) total_width = total_height = cx = cy = line_length = current_line = 0 for win in windows: have_place = False for i in range(0, slots_number): j = slots[i][3] if win.width < slots[i][2] and slots[i][1] + win.height <= line_max[j]: win.x = slots[i][0] win.y = slots[i][1] slots[i][1] += win.height + DISTANCE have_place = True break if not have_place and line_length < items_per_line: win.x = cx win.y = cy line_length += 1 slots[slots_number][0] = win.x slots[slots_number][1] = win.y + win.height + DISTANCE slots[slots_number][2] = win.width slots[slots_number][3] = current_line slots_number += 1 cx += win.width + DISTANCE line_max[current_line] = max(line_max[current_line], cy + win.height) elif not have_place: current_line += 1 line_length = 1 cx = 0 cy = line_max[current_line - 1] + DISTANCE line_max[current_line] = cy + win.height win.x = cx win.y = cy slots[slots_number][0] = win.x slots[slots_number][1] = win.y + win.height + DISTANCE slots[slots_number][2] = win.width slots[slots_number][3] = current_line slots_number += 1 cx += win.width + DISTANCE total_width = max(total_width, win.x + win.width) total_height = max(total_height, win.y + win.height) return total_width, total_height def get_root_pmap(self): pmap = self.root.get_full_property(self._XROOTPMAP_ID, Xatom.PIXMAP) if not pmap: pmap = self.root.get_full_property(self.ESETROOT_PMAP_ID, Xatom.PIXMAP) if not pmap: self.root_pmap = None pix = Pixmap(self.display.display, pmap.value[0]) geom = pix.get_geometry() img = self.imlibx.image_from_drawable(pmap.value[0], 0, 0, 0, geom.width, geom.height) img = img.scale(self.width, self.height) self.root_pmap = img def make_window(self): top = self.root.create_window( 0, 0, self.width, self.height, 0, self.depth, event_mask = X.VisibilityChangeMask ) w = top.create_window( 0, 0, self.width, self.height, 0, self.depth, X.InputOutput, event_mask = ( X.ButtonPressMask | X.ButtonReleaseMask ), background_pixel = self.screen.black_pixel ) if not self.root_pmap is None: self.pixmap = w.create_pixmap(self.width, self.height, self.depth) self.display.sync() self.imlibx.render_image_on_drawable(self.root_pmap, self.pixmap.id, dest_rect=[0,0]) w.change_attributes(background_pixmap = self.pixmap) top.set_wm_name("Skippy") top.set_wm_icon_name("Skippy") top.set_wm_class("skippy", "Skippy") w.map() self.window = w self.toplevel = top return w def map(self): props = [self._NET_WM_STATE_FULLSCREEN, 0] self.toplevel.change_property(self._NET_WM_STATE, Xatom.ATOM, 32, props) self.toplevel.map() Skippy()