def __patch_svg_data__(data): display = Display() screen = display.screen() atom = display.get_atom('_GNOME_BACKGROUND_REPRESENTATIVE_COLORS') result = screen.root.get_property(atom, Xatom.STRING, 0, 100) if result: color = result.value.strip('\x00') rgba = Gdk.RGBA() rgba.parse(color) color = rgba.to_color().to_string() data = data.replace('#000000', color) return data
class ProgramIdentifier(object): def __init__(self): self.display = Display() self.pid_atom = self.display.get_atom("_NET_WM_PID") def get_active_program(self): pid = self._get_top_window_pid() realpath = self._get_pid_realpath(pid) return self._get_program_name_from_path(realpath) def _get_pid_realpath(self, pid): return os.path.realpath('/proc/' + str(pid) + '/exe') def _get_program_name_from_path(self, path): return path.split("/")[-1] def _get_top_window_pid(self): top_window = self._get_top_window() if top_window is not None: pid = self._get_window_pid(top_window) if pid is None: raise CantGetPIDOfWindowError("") return pid else: raise NoTopWindowFoundError("") def _get_top_window(self): focused_window = self.display.get_input_focus().focus if focused_window is not None and focused_window != X.NONE and focused_window != X.PointerRoot: parent = focused_window while (True): if self.pid_atom in parent.list_properties(): break query_result = parent.query_tree() root = query_result.root parent = query_result.parent if root.id == parent.id: break return parent return None def _get_window_pid(self, window): if self.pid_atom in window.list_properties(): value = window.get_full_property(self.pid_atom, X.AnyPropertyType).value if value is not None and len(value) == 1: return value[0] return None
class XlibHelper: """Utility class for some X11-specific logic""" def __init__(self): self.display = Display() self.use_xres = self._try_init_xres() def _try_init_xres(self) -> bool: if XRes is None or self.display.query_extension(XRes.extname) is None: LOG.warning( "X-Resource extension is not supported. " "Process identification for X11 applications will be less reliable." ) return False ver = self.display.res_query_version() LOG.info( "X-Resource version %d.%d", ver.server_major, ver.server_minor, ) return (ver.server_major, ver.server_minor) >= (1, 2) def get_net_wm_pid(self, wid: int) -> int: """Get PID from _NET_WM_PID property of X11 window""" window = self.display.create_resource_object("window", wid) net_wm_pid = self.display.get_atom("_NET_WM_PID") pid = window.get_full_property(net_wm_pid, X.AnyPropertyType) if pid is None: raise Exception("Failed to get PID from _NET_WM_PID") return int(pid.value.tolist()[0]) def get_xres_client_id(self, wid: int) -> int: """Get PID from X server via X-Resource extension""" res = self.display.res_query_client_ids([{ "client": wid, "mask": XRes.LocalClientPIDMask }]) for cid in res.ids: if cid.spec.client > 0 and cid.spec.mask == XRes.LocalClientPIDMask: for value in cid.value: return value raise Exception("Failed to get PID via X-Resource extension") def get_window_pid(self, wid: int) -> Optional[int]: """Get PID of X11 window""" if self.use_xres: return self.get_xres_client_id(wid) return self.get_net_wm_pid(wid)
def get_chameleonic_pixbuf_from_svg(filename): # Note: Experimental!!!! # TODO: Fix or Remove completely data = open(get_media_file(filename, '/'), 'r').read() display = Display() screen = display.screen() atom = display.get_atom('_GNOME_BACKGROUND_REPRESENTATIVE_COLORS') result = screen.root.get_property(atom, Xatom.STRING, 0, 100) if result: print result.value color = result.value.strip('\x00') data = data.replace('#000000', color) h = Rsvg.Handle.new_from_data(data) return h.get_pixbuf()
def main(argv): if len(sys.argv) != 2: sys.exit( 'usage: {0} SELECTION\n\n' 'SELECTION is typically PRIMARY, SECONDARY or CLIPBOARD.\n'.format( sys.argv[0])) display = Display() sel_name = sys.argv[1] sel_atom = display.get_atom(sel_name) if not display.has_extension('XFIXES'): if display.query_extension('XFIXES') is None: print('XFIXES extension not supported', file=sys.stderr) return 1 xfixes_version = display.xfixes_query_version() print('Found XFIXES version %s.%s' % ( xfixes_version.major_version, xfixes_version.minor_version, ), file=sys.stderr) screen = display.screen() mask = xfixes.XFixesSetSelectionOwnerNotifyMask | \ xfixes.XFixesSelectionWindowDestroyNotifyMask | \ xfixes.XFixesSelectionClientCloseNotifyMask display.xfixes_select_selection_input(screen.root, sel_atom, mask) while True: e = display.next_event() print(e) if (e.type, e.sub_code) == display.extension_event.SetSelectionOwnerNotify: print('SetSelectionOwner: owner=0x{0:08x}'.format(e.owner.id)) elif (e.type, e.sub_code ) == display.extension_event.SelectionWindowDestroyNotify: print('SelectionWindowDestroy: owner=0x{0:08x}'.format(e.owner.id)) elif (e.type, e.sub_code ) == display.extension_event.SelectionClientCloseNotify: print('SelectionClientClose: owner=0x{0:08x}'.format(e.owner.id))
def _find_chrome_window(): from Xlib.display import Display from Xlib import X, Xatom from Xlib.protocol import event display = Display() name = display.get_atom('WM_NAME', 1) root = display.screen().root class FoundItException(Exception): def __init__(self, msg, window): self.window = window def searcher(parent): children = parent.query_tree().children for child in children: prop = child.get_property(name, Xatom.STRING, 0, 1024) if prop and 0 < len(prop.value): if prop.value == 'cafesys terminal': # FIXME: make dynamic raise FoundItException('found', child) searcher(child) try: searcher(root) except FoundItException, e: return e.window
class Environment(): def __init__(self): self.display = Display() self.screen = self.display.screen() self.root = self.screen.root self.region = Region(x=0, y=0, width=self.screen.width_in_pixels, height=self.screen.height_in_pixels) self.desktops = [] self.windows = self.get_window_set() self.window_desktop_map = {} self.hidden_windows = set() self.visible_windows = set() for i in range(self.number_of_desktops()): LOGGER.debug("\n\n\nCreating Desktop %d" % (i)) d = Desktop(i, self) self.desktops.append(d) #d.print_windows() d.arrange() self.update_desktop_map() self.setup_listeners() #self.print_hierarchy(self.root, " - ") def setup_listeners(self): self.root.change_attributes(event_mask=X.SubstructureNotifyMask | X.PropertyChangeMask) anchor = self.root.create_window(0, 0, 1, 1, 1, self.screen.root_depth) anchor.xrandr_select_input(randr.RRScreenChangeNotifyMask | randr.RRCrtcChangeNotifyMask | randr.RROutputChangeNotifyMask | randr.RROutputPropertyNotifyMask) def update_all_the_things(self): self.display = Display() self.screen = self.display.screen() self.root = self.screen.root self.region = Region(x=0, y=0, width=self.screen.width_in_pixels, height=self.screen.height_in_pixels - 32) LOGGER.debug("NEW REGION: %s" % (self.region)) for d in self.desktops: d.resize() self.setup_listeners() def print_hierarchy(self, window, indent): children = window.query_tree().children for w in children: LOGGER.debug(indent, window.get_wm_class()) self.print_hierarchy(w, indent + '-') def interesting_properties(self): #_NET_WM_DESKTOP # root: _NET_CURRENT_DESKTOP LOGGER.debug("Current desktop") LOGGER.debug( self.root.get_full_property( self.display.intern_atom('_NET_CURRENT_DESKTOP'), Xatom.CARDINAL).value[0]) def current_desktop(self): return self.root.get_full_property( self.display.intern_atom('_NET_CURRENT_DESKTOP'), Xatom.CARDINAL).value[0] def number_of_desktops(self): return self.root.get_full_property( self.display.intern_atom('_NET_NUMBER_OF_DESKTOPS'), Xatom.CARDINAL).value[0] def update_window_states(self): window_ids = self.get_window_set(include_hidden=True) for window_id in window_ids: if self.is_window_hidden(window_id): self.hidden_windows.add(window_id) else: self.visible_windows.add(window_id) def update_desktop_map(self): self.window_desktop_map = {} for d in self.desktops: for window_id in d.get_window_set(include_hidden=True): self.window_desktop_map[window_id] = d def get_window_desktop(self, window): if type(window) is long: w = self.display.create_resource_object('window', window) else: w = window try: value = w.get_full_property( self.display.intern_atom('_NET_WM_DESKTOP'), Xatom.CARDINAL).value[0] if value > self.number_of_desktops(): return None else: return value except AttributeError: return None except Xlib.error.BadWindow: return None def get_window_states(self, window_id): w = self.display.create_resource_object('window', window_id) #return w.get_full_property(self.display.intern_atom('_NET_WM_STATE'), Xatom.ATOM).value try: states = w.get_full_property( self.display.get_atom('_NET_WM_STATE'), Xatom.WINDOW) except Xlib.error.BadWindow: LOGGER.warn("Bad window fetching states...") states = None if states == None: return [] else: res = w.get_full_property(self.display.get_atom('_NET_WM_STATE'), 0).value.tolist() return res def is_window_hidden(self, window_id): hidden_state_atom = self.display.get_atom("_NET_WM_STATE_HIDDEN") states = self.get_window_states(window_id) return hidden_state_atom in states def is_window_visible(self, window_id): return not self.is_window_hidden(window_id) def listen_for_events(self): LOGGER.debug("Listening for change events!") while True: ev = self.display.next_event() self.handle_event(ev) def handle_event(self, event): old_hidden = set(self.hidden_windows) old_visible = set(self.visible_windows) self.update_window_states() changed_ids = set() changed_ids.update(old_hidden.symmetric_difference( self.hidden_windows)) changed_ids.update( old_visible.symmetric_difference(self.visible_windows)) if len(changed_ids) > 0: LOGGER.debug("Changed IDs: %s" % (changed_ids)) needs_update = False wm_active_window = self.display.get_atom('_NET_ACTIVE_WINDOW') wm_move_window = self.display.get_atom('_NET_MOVERESIZE_WINDOW') wm_hidden_window = self.display.get_atom('_NET_WM_STATE_HIDDEN') wm_state = self.display.get_atom('_NET_WM_STATE') try: window_props = event.window.get_full_property(event.atom, 0) window_id = int(window_props.value.tolist()[0]) LOGGER.debug("Window ID: %s" % (window_id)) except AttributeError: pass #LOGGER.debug("Not a window-level event") for window_id in changed_ids: desktop = self.window_desktop_map.get(window_id, None) LOGGER.debug("Window changed: %s on desktop: %s" % (window_id, desktop)) if desktop is not None: desktop.arrange() needs_update = True if event.type == X.PropertyNotify: LOGGER.debug("Property changed...") if event.atom == wm_active_window: LOGGER.debug("Property changed on an active window....") elif event.type == X.CreateNotify or event.type == X.DestroyNotify: needs_update = True window_set = self.get_window_set() if event.type == X.CreateNotify: LOGGER.debug("Handling creation!") new_windows = window_set.difference(self.windows) for window in new_windows: window_resource = self.display.create_resource_object( 'window', window) window_desktop = self.get_window_desktop(window_resource) if window_desktop is not None: self.desktops[window_desktop].layout.add_window( window_resource) if event.type == X.DestroyNotify: LOGGER.debug("Handling destruction!") missing_windows = self.windows.difference(window_set) for window in missing_windows: window_resource = self.display.create_resource_object( 'window', window) # TODO: optimize lookup by keeping old map? for desktop in self.desktops: LOGGER.debug("Trying to remove from desktop %s" % (desktop)) desktop.layout.remove_window(window_resource) self.windows = window_set for desktop in self.desktops: desktop.layout.redraw() elif event.__class__.__name__ == randr.ScreenChangeNotify.__name__: LOGGER.debug('Screen change') self.update_all_the_things() else: #LOGGER.debug("Unhandled event: %d" % (event.type)) pass if needs_update: self.update_desktop_map() def get_window_set(self, include_hidden=False, desktop_number=None): windows = set([ x for x in self.root.get_full_property( self.display.intern_atom('_NET_CLIENT_LIST'), Xatom.WINDOW).value ]) if desktop_number is not None: #LOGGER.debug("Filtering windows not on: %d" % (desktop_number)) windows = filter( lambda w: self.get_window_desktop(w) == desktop_number, windows) if include_hidden is False: #LOGGER.debug("Filtering hidden windows...") windows = filter(lambda w: self.is_window_visible(w) == True, windows) return set(windows)
class KnoX: Geometry = namedtuple("Geometry", "x y width height") FrameExtents = namedtuple("FrameExtents", "left right top bottom") def __init__(self): #self.display = Display(os.environ.get("DISPLAY", ":0.0")) self.display = Display() print("Connected to X DISPLAY %r" % self.display.get_display_name()) self.display.set_error_handler(self.knox_error_handler) self.screen = self.display.screen() self.root = self.screen.root self.atoms = dict() self.atom_names = dict() self.keysyms = Keysyms() self.modifiers = Modifiers(self) self._supported_properties = None self._acceptable_error_sequence = 0 self._acceptable_errors = dict() self._silenced_errors = set() def fileno(self): """This function is here to make select work with this object""" return self.display.fileno() @contextmanager def silenced_error(self, error): silencer = self.silence_error(error) try: yield silencer finally: self.remove_silencer(silencer) def silence_error(self, error): k = self._acceptable_error_sequence self._acceptable_errors[k] = error self._acceptable_error_sequence += 1 self._silenced_errors = set(self._acceptable_errors.values()) return k def remove_silencer(self, key): if key in self._acceptable_errors: del self._acceptable_errors[key] self._silenced_errors = set(self._acceptable_errors.values()) def knox_error_handler(self, err, *args): if type(err) not in self._silenced_errors: print("X protocol error: %s" % err) traceback.print_stack() # def wait_for_event(self, timeout_seconds): # """ Wait up to `timeout_seconds` seconds for an event to be queued. # Return True, if a xevent is available. # Return False, if the timeout was reached. # from https://gist.github.com/fphammerle/d81ca3ff0a169f062a9f28e57b18f04d""" # rlist = select.select( # [self.display], # rlist # [], # wlist # [], # xlist # timeout_seconds, # timeout [seconds] # )[0] # return len(rlist) > 0 def next_event(self, wait=True): if (wait or self.display.pending_events()): return self.display.next_event() else: return None # def next_event(self, event_loop): # event_loop.register_reader(self.display, def atom(self, name, only_if_exists=False): if isinstance(name, int): a = name elif name not in self.atoms: a = self.display.get_atom(name, only_if_exists=only_if_exists) self.atoms[name] = a else: a = self.atoms[name] return a def atom_name(self, atom): if atom in self.atom_names: return self.atom_names[atom] name = self.display.get_atom_name(atom) if name: self.atom_names[atom] = name if name not in self.atoms: self.atoms[name] = atom return name def get_prop(self, window, name): prop_name = self.atom(name, only_if_exists=True) if not prop_name: return None if isinstance(window, int): window = self.get_window(window) p = window.get_full_property(prop_name, X.AnyPropertyType) if p: return p.value def get_text_prop(self, window, name): prop_name = self.atom(name, only_if_exists=True) if not prop_name: return None s = window.get_full_text_property(prop_name, Xatom.STRING) if not s: t = self.atom("UTF8_STRING", only_if_exists=True) if t: s = window.get_full_text_property(prop_name, t) return s def onerror(self, *args, **kwargs): print("ERROR: something bad happened about %r and %r" % (args, kwargs)) raise Exception("Error is bad...") def set_prop(self, window, name, type_name, value): if isinstance(window, int): window = self.get_window(window) if isinstance(type_name, int): prop_type_name = type_name #type_name = self.atom_name(prop_type_name) else: prop_type_name = self.atom(type_name, only_if_exists=False) prop_name = self.atom(name, only_if_exists=False) if value is None: window.delete_property(prop_name) else: window.change_property(prop_name, prop_type_name, 32, value, mode=X.PropModeReplace, onerror=self.onerror) def send_prop_change_event( self, property_name, data, target=None, window=None, ): if target is None: target = self.root if window is None: window = target ev = protocol.event.ClientMessage(window=window, client_type=self.atom(property_name), data=data) target.send_event(ev, event_mask=X.SubstructureNotifyMask | X.SubstructureRedirectMask, propagate=False, onerror=self.onerror) def current_desktop(self, desktop=None, wait=True): prop_name = "_NET_CURRENT_DESKTOP" if desktop is None: pv = self.get_prop(self.root, prop_name) if pv: return pv[0] else: v = array('I', [desktop]) #self.set_prop(self.root, prop_name, Xatom.CARDINAL, v) self.send_prop_change_event( prop_name, (32, [desktop, X.CurrentTime, 0, 0, 0])) self.flush() w = Waiter(wait) while w.wait(): print("DESKTOPCHECK", hex(desktop)) if self.current_desktop() == desktop: print("DESKTOP OK") break def get_wm_pid(self, window): pid_prop = self.get_prop(window, "_NET_WM_PID") if pid_prop: return pid_prop[0] return None def get_wm_name(self, window): if isinstance(window, int): window = self.get_window(window) # window.get_wm_name gets only STRING property and returns nothing # if it's UTF8_STRING return self.get_text_prop(window, Xatom.WM_NAME) def active_window(self, window=None, wait=3, id_only=False): prop_name = "_NET_ACTIVE_WINDOW" if window is None: pv = self.get_prop(self.root, prop_name) if pv and pv[0]: window = self.get_window(pv[0]) if window and window.get_wm_name() != 'Desktop': if id_only: return window.id else: return window else: if isinstance(window, int): window = self.get_window(window) desktop = self.get_desktop_for_window(window) self.current_desktop(desktop) #v = array('I', [ window.id, 0 ]) #self.set_prop(self.root, prop_name, Xatom.WINDOW, v) # data[0]: source indication # 1: when the request comes from an application # 2: from a pager # 0: no spec. self.send_prop_change_event(prop_name, (32, [2, X.CurrentTime, 0, 0, 0]), window=window) self.flush() #self.raise_window(window) # it won't become active until it's focused focused = self.set_focused_window(window, wait=1) w = Waiter(wait) while w.wait(): a = self.active_window() self.flush() if not focused: focused = self.set_focused_window(window, wait=1) self.flush() if a and a.id == window.id: print("Activated %r!" % window.id) return True self.send_prop_change_event(prop_name, (32, [2, X.CurrentTime, 0, 0, 0]), window=window) self.flush() print("Can't activate %d" % window.id) return False def get_focused_window(self, toplevel=True): f = self.display.get_input_focus() #f = protocol.request.GetInputFocus(display=self.display.display) if f.focus in [X.NONE, X.PointerRoot]: return None if toplevel: w = self.get_client_window(f.focus) if w is not None: return w.id return f.focus.id def raise_window(self, window): if isinstance(window, int): window = self.get_window(window) elif window is None: return window.raise_window() def focus_error(self, *args, **kwargs): print("Cannot set_input_focus: %r %r" % (args, kwargs)) def set_focused_window(self, window, wait=3): if window is None: self.display.set_input_focus(X.NONE, X.RevertToParent, X.CurrentTime, onerror=self.focus_error) return True elif not wait: self.display.set_input_focus(window, X.RevertToParent, X.CurrentTime) return True else: with self.silenced_error(error.BadMatch): if isinstance(window, int): window = self.get_window(window) self.display.set_input_focus(window, X.RevertToParent, X.CurrentTime) self.flush() w = Waiter(wait) while w.wait(): if w.timeout: if w.progressed: print("WAITING %.3f seconds more for focus on %r" % (w.remaining, window.id)) else: print( "READY TO WAIT %.3f seconds for focus on %r" % (w.remaining, window.id)) focused_win_id = self.get_focused_window() if focused_win_id == window.id: print("FOCUSED %r" % window.id) return True # many times it's needed to repeat the command, esp. when mouse is # not inside the target window self.display.set_input_focus(window, X.RevertToParent, X.CurrentTime) self.flush() #self.display.set_input_focus(window, X.RevertToParent, X.CurrentTime) #self.display.flush() return False def get_desktop_for_window(self, window): pv = self.get_prop(window, "_NET_WM_DESKTOP") if pv: return pv[0] def set_desktop_for_window(self, window, desktop): if desktop is None: return name = self.atom("_NET_WM_DESKTOP", only_if_exists=True) if name in self.supported_properties: pv = self.set_prop(window, name, Xatom.CARDINAL, array('I', [desktop])) def save_state(self): state = { "Current Desktop": self.current_desktop(), "Active Window": self.active_window(id_only=True), "Focused Window": self.get_focused_window() } return state def restore_state(self, state): a = self.supported_properties self.current_desktop(state["Current Desktop"]) self.flush() try: self.set_focused_window(state["Focused Window"]) except error.BadWindow: print("Sorry, the old focused window went away...") # self.active_window(state["Active Window"]) def keysym_to_string(self, keysym, friendly=False, very_friendly=False): if keysym not in self.keysyms.keysyms: return chr(keysym) if very_friendly: return self.keysyms.friendly_name(keysym, simplest=True) if friendly: return self.keysyms.friendly_name(keysym, simplest=False) else: return self.keysyms[keysym] def keycode_to_keysym(self, keycode, idx=None): if idx is None: syms = set() for i in range(4): keysym = self.display.keycode_to_keysym(keycode, i) if keysym: syms.add(keysym) return syms else: return self.display.keycode_to_keysym(event.detail, i) def keysym_to_keycode(self, keysym): return self.display.keysym_to_keycode(keysym) def string_to_keysym(self, s): k = self.keysyms[s] if not k: k = self.keysyms["XK_" + s] if k: return k k = XK.string_to_keysym(s) return k # allow simpler names, like AudioRaiseVolume? # if s.startswith("XF86_"): # s = "XF86" + s[5:] # return XK.string_to_keysym(s) def error_handler(self, fn, *args, **kwargs): return functools.partial(fn, *args, **kwargs) def toggle_frame(self, window, frame=None, wait=1): """Set window frame. Value should be True or False for on and off, or None for toggle.""" # flags - set bit for every iteresting value # 0 functions => integer bits # 1 decorations => integer bits # 2 input_mode => enum string or integer # 3 status => integer bits # # functions: # bit actions offered # --- --------------- # 1 all functions # 2 resize window # 4 move window # 8 minimize, to iconify # 16 maximize, to full-screen (with a frame still) # 32 close window # # decorations: # bit decorations displayed # --- --------------------- # 1 all decorations # 2 border around the window # 4 resizeh, handles to resize by dragging # 8 title bar, showing WM_NAME # 16 menu, drop-down menu of the "functions" above # 32 minimize button, to iconify # 64 maximize button, to full-screen # # input mode: # string integer # "modeless" 0 not modal (the default) # "primary_application_modal" 1 modal to its "transient for" # "system_modal" 2 modal to the whole display # "full_application_modal" 3 modal to the current client # # status: # # bit # 1 tearoff menu window name = self.atom("_MOTIF_WM_HINTS", only_if_exists=True) # If does not exist, probably not supported, though should check # root for _NET_SUPPORTED list return assert prop != 0 pv = pv = self.get_prop(window, name) fe = self.get_frame_extents(window) if pv and len(pv) == 5: hints = array(pv.typecode, pv) if frame is None: hints[2] = 0 if hints[2] else 1 elif frame: hints[2] = 1 else: hints[2] = 0 else: # reasonable default hints = array('I', [2, 0, 0, 0, 0]) self.set_prop(window, name, name, hints) w = Waiter(wait) while w.wait(): pv = self.get_prop(window, name) if pv and array(pv.typecode, pv) == hints: new_fe = self.get_frame_extents(window) # make sure frame extents changed # this seems to take a while once the hints change if new_fe != fe: break def set_opacity(self, window, value): """value is a number between 0 and 1""" v = int(((1 << 32) - 1) * value) self.set_prop(window, "_NET_WM_WINDOW_OPACITY", Xatom.CARDINAL, array('I', [v])) def get_opacity(self, window): pv = self.get_prop(window, "_NET_WM_WINDOW_OPACITY") if pv: value = int(pv[0] / ((1 << 32) - 1)) return value return 1 @property def supported_properties(self): if self._supported_properties is None: self._supported_properties = self.get_prop(self.root, "_NET_SUPPORTED") or [] return self._supported_properties def get_window(self, win_id): if isinstance(win_id, int): return self.display.create_resource_object('window', win_id) else: return win_id def get_client_window(self, window): win_id = window.id for tlw in self.toplevel_windows(): for (_, parent, _) in self.window_tree( tlw, filter=lambda w, parent, level: w.id == win_id): return tlw return None def toplevel_windows(self, id_only=False): name = self.atom("_NET_CLIENT_LIST", only_if_exists=True) if name in self.supported_properties: lst = self.get_prop(self.root, name) if id_only: return lst else: return list(map(lambda win_id: self.get_window(win_id), lst)) else: print("BELGENGOC") if id_only: return list( map(lambda w: w.id, self.root.query_tree().children)) else: return list(self.root.query_tree().children) def window_tree(self, parent=None, level=1, filter=None): if parent is None: parent = self.root if filter is None or filter(parent, None, 0): yield (parent, None, 0) for w in parent.query_tree().children: if filter is None or filter(w, parent, level): yield (w, parent, level) yield from self.window_tree(parent=w, level=level + 1, filter=filter) def close_window(self, window): self.send_prop_change_event("_NET_CLOSE_WINDOW", (32, [0, 0, 0, 0, 0]), window=self.get_window(window)) # https://specifications.freedesktop.org/wm-spec/wm-spec-1.3.html # window = the respective client window # message_type = _NET_WM_STATE # format = 32 # data.l[0] = the action, as listed below # data.l[1] = first property to alter # data.l[2] = second property to alter # data.l[3] = source indication # other data.l[] elements = 0 # This message allows two prop # _NET_WM_STATE_REMOVE = 0 # remove/unset property _NET_WM_STATE_ADD = 1 #add/set property _NET_WM_STATE_TOGGLE = 2 # toggle property def set_wm_states(self, window, names, action=None): if action is None: action = self._NET_WM_STATE_TOGGLE elif action is True: action = self._NET_WM_STATE_ADD elif action is False: action = self._NET_WM_STATE_REMOVE window = self.get_window(window) values = list() for name in names: value = self.atom("_NET_WM_STATE_%s" % name.upper()) values.append(value) data = [action, *values] while len(data) < 5: data.append(0) self.send_prop_change_event("_NET_WM_STATE", (32, data), window=self.get_window(window)) def set_wm_state(self, window, name, action=None): if action is None: action = self._NET_WM_STATE_TOGGLE elif action is True: action = self._NET_WM_STATE_ADD elif action is False: action = self._NET_WM_STATE_REMOVE window = self.get_window(window) value = self.atom("_NET_WM_STATE_%s" % name.upper()) self.send_prop_change_event("_NET_WM_STATE", (32, [action, value, 0, 0, 0]), window=self.get_window(window)) def below_window(self, window, action=None): self.set_wm_state(window, name="below", action=action) def fullscreen_window(self, window, action=None): self.set_wm_state(window, name="fullscreen", action=action) def above_window(self, window, action=None): self.set_wm_state(window, name="above", action=action) def sticky_window(self, window, action=None): self.set_wm_state(window, name="sticky", action=action) def skip_pager(self, window, action=None): self.set_wm_state(window, name="skip_pager", action=action) def skip_taskbar(self, window, action=None): self.set_wm_state(window, name="skip_taskbar", action=action) def maximize_window(self, window, horizontal=True, vertical=True, action=None): if horizontal: self.set_wm_state(window, name="maximized_horz", action=action) if vertical: self.set_wm_state(window, name="maximized_vert", action=action) def minimize_window(self, window): if isinstance(window, int): window = self.get_window(window) self.send_prop_change_event("WM_CHANGE_STATE", (32, [Xutil.IconicState, 0, 0, 0, 0]), window=self.get_window(window)) def get_attributes(self, window): if isinstance(window, int): window = self.get_window(window) return window.get_attributes() def get_window_type(self, window): e = self.get_prop(window, "_NET_WM_WINDOW_TYPE") if e is None: return None type_details = set() prefix = "_NET_WM_WINDOW_TYPE_" for t in e: if not t: continue s = self.atom_name(t) if s.startswith(prefix): s = s[len(prefix):] type_details.add(s) return type_details def get_frame_extents(self, window): # x, y, width, height if isinstance(window, int): window = self.get_window(window) e = self.get_prop(window, "_NET_FRAME_EXTENTS") if e: return self.FrameExtents(*e) else: return self.FrameExtents(0, 0, 0, 0) def get_geometry(self, window): # x, y, width, height if isinstance(window, int): window = self.get_window(window) return window.get_geometry() def set_geometry(self, window, **data): # x, y, width, height if isinstance(window, int): window = self.get_window(window) if any(map(lambda v: v < 0, data.values())): gw = self.get_geometry(window) f = self.get_frame_extents(window) wa = self.usable_workarea() if 'x' in data and data['x'] < 0: data['x'] = wa.width - gw.width - (f.left + f.right) + data['x'] + 1 else: data['x'] += wa.x if 'y' in data and data['y'] < 0: data['y'] = wa.height - gw.height - (f.top + f.bottom) + data['y'] + 1 else: data['y'] += wa.y window.configure(**data) def usable_workarea(self): a = self.get_prop(self.root, "_NET_WORKAREA") if a: p = self.current_desktop() * 4 #return (x, y, width, height) return self.Geometry(*a[p:p + 4]) else: r = self.get_geometry(self.root) return self.Geometry(0, 0, r.width, r.height) def send_key(self, window, keysym, modifiers): if isinstance(window, int): window = self.get_window(window) keycode = self.display.keysym_to_keycode(keysym) event = protocol.event.KeyPress(time=X.CurrentTime, root=self.root, window=window, child=X.NONE, same_screen=True, root_x=0, root_y=0, event_x=0, event_y=0, state=modifiers.bitmap, detail=keycode) window.send_event(event, propagate=False) event = protocol.event.KeyRelease( time=X.CurrentTime, root=self.root, window=window, child=X.NONE, same_screen=True, # same screen as the root window root_x=0, root_y=0, event_x=0, event_y=0, state=modifiers.bitmap, detail=keycode) window.send_event(event, propagate=False) def show_desktop(self, action=None): prop_name = self.atom("_NET_SHOWING_DESKTOP") if action is True: self.send_prop_change_event(prop_name, (32, [1, X.CurrentTime, 0, 0, 0])) elif action is False: self.send_prop_change_event(prop_name, (32, [0, X.CurrentTime, 0, 0, 0])) else: pv = self.get_prop(self.root, prop_name) new_val = 0 if pv and pv[0] else 1 self.send_prop_change_event( prop_name, (32, [new_val, X.CurrentTime, 0, 0, 0])) def flush(self): # send all pending events self.display.flush() def sync(self): # flush and make sure everything is handled and processed or rejected by the server self.display.sync() @property def display_count(self): res = randr.get_screen_resources(self.root) n = 0 for i in res.outputs: o = randr.get_output_info(self.root, i, config_timestamp=0) if o.modes: # has modes, empty if there's no monitor connected here n += 1 return n
xdiff = ev.root_x - start.root_x ydiff = ev.root_y - start.root_y start.child.configure( x=attr.x + (start.detail == 1 and xdiff or 0), y=attr.y + (start.detail == 1 and ydiff or 0), width=max(1, attr.width + (start.detail == 3 and xdiff or 0)), height=max(1, attr.height + (start.detail == 3 and ydiff or 0))) elif ev.type == X.ButtonRelease: start = None wm_name = 'spwm' dpy = Display() wids = [] root = dpy.screen().root fw = dpy.screen().width_in_pixels // 2 fh = dpy.screen().height_in_pixels // 2 net_active_window = dpy.get_atom('_NET_ACTIVE_WINDOW') net_client_list = dpy.get_atom('_NET_CLIENT_LIST') net_supported = dpy.get_atom('_NET_SUPPORTED') net_supporting_wm = dpy.get_atom('_NET_SUPPORTING_WM_CHECK') net_wm_name = dpy.get_atom('_NET_WM_NAME') args = sys.argv[1:] if len(sys.argv) == 6 else ['4', 'u', 'i', 'j', 'k'] mods = [X.Mod1Mask, X.Mod2Mask, X.Mod3Mask, X.Mod4Mask, X.Mod5Mask] mod = mods[int(args[0]) - 1] btn_mask = X.ButtonPressMask | X.ButtonReleaseMask | X.PointerMotionMask root.grab_button(1, mod, 1, btn_mask, X.GrabModeAsync, X.GrabModeAsync, X.NONE, X.NONE) root.grab_button(3, mod, 1, btn_mask, X.GrabModeAsync, X.GrabModeAsync, X.NONE, X.NONE) root.change_attributes(event_mask=X.SubstructureNotifyMask) root.change_property( net_supported,
class GlobalKeyBinding(GObject.GObject, threading.Thread): __gsignals__ = { 'activate': (GObject.SignalFlags.RUN_LAST, None, ()), } def __init__(self): GObject.GObject.__init__(self) threading.Thread.__init__(self) self.setDaemon(True) self.keymap = Gdk.Keymap().get_default() self.display = Display() self.screen = self.display.screen() self.window = self.screen.root self.ignored_masks = self.get_mask_combinations(X.LockMask | X.Mod2Mask | X.Mod5Mask) self.map_modifiers() self.raw_keyval = None self.keytext = "" def map_modifiers(self): gdk_modifiers = (Gdk.ModifierType.CONTROL_MASK, Gdk.ModifierType.SHIFT_MASK, Gdk.ModifierType.MOD1_MASK, Gdk.ModifierType.MOD2_MASK, Gdk.ModifierType.MOD3_MASK, Gdk.ModifierType.MOD4_MASK, Gdk.ModifierType.MOD5_MASK, Gdk.ModifierType.SUPER_MASK, Gdk.ModifierType.HYPER_MASK) self.known_modifiers_mask = 0 for modifier in gdk_modifiers: if "Mod" not in Gtk.accelerator_name( 0, modifier) or "Mod4" in Gtk.accelerator_name( 0, modifier): self.known_modifiers_mask |= modifier def grab(self, key): accelerator = key accelerator = accelerator.replace("<Super>", "<Mod4>") keyval, modifiers = Gtk.accelerator_parse(accelerator) if not accelerator or (not keyval and not modifiers): self.keycode = None self.modifiers = None return False self.keytext = key try: self.keycode = self.keymap.get_entries_for_keyval( keyval).keys[0].keycode except AttributeError: # In older Gtk3 the get_entries_for_keyval() returns an unnamed tuple... self.keycode = self.keymap.get_entries_for_keyval( keyval)[1][0].keycode self.modifiers = int(modifiers) # Request to receive key press/release reports from other windows that may not be using modifiers catch = error.CatchError(error.BadWindow) if self.modifiers: self.window.change_attributes(onerror=catch, event_mask=X.KeyPressMask | X.KeyReleaseMask) else: self.window.change_attributes(onerror=catch, event_mask=X.NoEventMask) if catch.get_error(): return False catch = error.CatchError(error.BadAccess) for ignored_mask in self.ignored_masks: mod = modifiers | ignored_mask result = self.window.grab_key(self.keycode, mod, True, X.GrabModeAsync, X.GrabModeAsync, onerror=catch) self.display.flush() if catch.get_error(): return False catch = error.CatchError(error.BadCursor) if not self.modifiers: # We grab Super+click so that we can forward it to the window manager and allow Super+click bindings (window move, resize, etc.) self.window.grab_button(X.AnyButton, X.Mod4Mask, True, X.ButtonPressMask, X.GrabModeSync, X.GrabModeAsync, X.NONE, X.NONE) self.display.flush() if catch.get_error(): return False return True def ungrab(self): if self.keycode: self.window.ungrab_key(self.keycode, X.AnyModifier, self.window) def rebind(self, key): self.ungrab() if key != "": self.grab(key) else: self.keytext = "" def set_focus_window(self, window=None): self.ungrab() if window is None: self.window = self.screen.root else: self.window = self.display.create_resource_object( "window", window.get_xid()) self.grab(self.keytext) def get_mask_combinations(self, mask): return [x for x in range(mask + 1) if not (x & ~mask)] def idle(self): self.emit("activate") return False def activate(self): GLib.idle_add(self.run) # Get which window manager we're currently using (Marco, Compiz, Metacity, etc...) def get_wm(self): name = '' wm_check = self.display.get_atom('_NET_SUPPORTING_WM_CHECK') win_id = self.window.get_full_property(wm_check, X.AnyPropertyType) if win_id: w = self.display.create_resource_object("window", win_id.value[0]) wm_name = self.display.get_atom('_NET_WM_NAME') prop = w.get_full_property(wm_name, X.AnyPropertyType) if prop: name = prop.value return name.lower() def run(self): self.running = True wait_for_release = False while self.running: event = self.display.next_event() if self.modifiers: # Use simpler logic when using traditional combined keybindings modifiers = event.state & self.known_modifiers_mask if event.type == X.KeyPress and event.detail == self.keycode and modifiers == self.modifiers: GLib.idle_add(self.idle) else: try: # KeyPress if event.type == X.KeyPress and event.detail == self.keycode and not wait_for_release: modifiers = event.state & self.known_modifiers_mask if modifiers == self.modifiers: wait_for_release = True # KeyRelease elif event.type == X.KeyRelease and event.detail == self.keycode and wait_for_release: GLib.idle_add(self.idle) wait_for_release = False # Modifiers are often used with mouse events - don't let the system swallow those elif event.type == X.ButtonPress: self.display.allow_events(X.ReplayPointer, X.CurrentTime) # Compiz would rather not have the event sent to it and just read it from the replayed queue wm = self.get_wm() if wm != "compiz": self.display.ungrab_keyboard(X.CurrentTime) self.display.ungrab_pointer(X.CurrentTime) query_pointer = self.window.query_pointer() self.display.send_event(query_pointer.child, event, X.ButtonPressMask, True) wait_for_release = False # If the user presses another key in between the KeyPress and the KeyRelease, they # meant to use a different shortcut else: self.display.ungrab_keyboard(X.CurrentTime) # Send the event up in case another window is listening to it self.display.send_event( event.window, event, X.KeyPressMask | X.KeyReleaseMask, True) wait_for_release = False except AttributeError: continue def stop(self): self.running = False self.ungrab() self.display.close()