예제 #1
0
파일: libsmon.py 프로젝트: inish777/smon
class Scheduler(Thread):
  """ Schedules and executes tasks using threaded workers
  """
  def __init__(self, workers=5):
    super().__init__(daemon=True)
    self.lock = Lock()       # lock queue manipulations
    self.inq  = PriorityQueue()
    self.outq = Queue()      # fired checks to be executed
    self.timer = MyTimer(0)  # timer placeholder for .schedule() so it can call self.timer.cancel() during first time
    self.log = Log("scheduler")
    for i in range(workers):
      worker = Worker(self)
      worker.start()

  def schedule(self, checker):
    time = checker.get_next_check()
    with self.lock:
      self.inq.put((time, checker))
      self.timer.cancel()  # trigger recalculate because this task may go before pending

  def run(self):
    while True:
      t, c = self.inq.get()
      with self.lock:
        now = time.time()
        self.timer = MyTimer(t-now)
      try:
        self.timer.start()
        self.log.debug("sleeping for %s" % self.timer.interval)
        self.timer.join()
      except TimerCanceled:
        self.log.notice("timer aborted, recalculating timeouts")
        with self.lock:
          print(t,c)
          self.inq.put((t,c))
        continue
      self.outq.put(c)
예제 #2
0
class WM:
    """
        Provides basic building blocks to make a window manager.
        It hides many dirty details about XCB. It was intended
        to provide minimum functionality, the rest supposed to
        be implemented by user in configuration file.
    """
    root = None   # type: Window
    atoms = None  # type: AtomVault

    def __init__(self, display=None, desktops=None, loop=None):
        self.log = Log("WM")
        # INIT SOME BASIC STUFF
        self.hook = Hook()
        self.windows = {}  # mapping between window id and Window
        self.win2desk = {}

        if not display:
            display = os.environ.get("DISPLAY")

        try:
            self._conn = xcffib.connect(display=display)
        except xcffib.ConnectionException:
            sys.exit("cannot connect to %s" % display)

        self.atoms = AtomVault(self._conn)
        self.desktops = desktops or [Desktop()]
        self.cur_desktop = self.desktops[0]
        self.cur_desktop.show()

        # CREATE ROOT WINDOW
        xcb_setup = self._conn.get_setup()
        xcb_screens = [i for i in xcb_setup.roots]
        self.xcb_default_screen = xcb_screens[self._conn.pref_screen]
        root_wid = self.xcb_default_screen.root
        self.root = Window(self, wid=root_wid, atoms=self.atoms, mapped=True)
        self.windows[root_wid] = self.root
#        for desktop in self.desktops:
#            desktop.windows.append(self.root)

        self.root.set_attr(
            eventmask=(
                  EventMask.StructureNotify
                | EventMask.SubstructureNotify
                | EventMask.FocusChange
                # | EventMask.SubstructureRedirect
                | EventMask.EnterWindow
                # | EventMask.LeaveWindow
                # | EventMask.PropertyChange
                | EventMask.OwnerGrabButton
            )
        )

        # INFORM X WHICH FEATURES WE SUPPORT
        self.root.props[self.atoms._NET_SUPPORTED] = [self.atoms[a] for a in SUPPORTED_ATOMS]

        # PRETEND TO BE A WINDOW MANAGER
        supporting_wm_check_window = self.create_window(-1, -1, 1, 1)
        supporting_wm_check_window.props['_NET_WM_NAME'] = "SWM"
        self.root.props['_NET_SUPPORTING_WM_CHECK'] = supporting_wm_check_window.wid
        self.root.props['_NET_NUMBER_OF_DESKTOPS'] = len(self.desktops)
        self.root.props['_NET_CURRENT_DESKTOP'] = 0

        # TODO: set cursor

        # EVENTS THAT HAVE LITTLE USE FOR US...
        self.ignoreEvents = {
            "KeyRelease",
            "ReparentNotify",
            # "CreateNotify",
            # DWM handles this to help "broken focusing windows".
            # "MapNotify",
            "ConfigureNotify",
            "LeaveNotify",
            "FocusOut",
            "FocusIn",
            "NoExposure",
        }
        # KEYBOARD
        self.kbd = Keyboard(xcb_setup, self._conn)
        self.mouse = Mouse(conn=self._conn, root=self.root)

        # FLUSH XCB BUFFER
        self.xsync()    # apply settings
        # the event loop is not yet there, but we might have some pending
        # events...
        self._xpoll()
        # TODO: self.grabMouse

        # NOW IT'S TIME TO GET PHYSICAL SCREEN CONFIGURATION
        self.xrandr = Xrandr(root=self.root, conn=self._conn)

        # TODO: self.update_net_desktops()

        # SETUP EVENT LOOP
        if not loop:
            loop = asyncio.new_event_loop()
        self._eventloop = loop
        self._eventloop.add_signal_handler(signal.SIGINT, self.stop)
        self._eventloop.add_signal_handler(signal.SIGTERM, self.stop)
        self._eventloop.add_signal_handler(signal.SIGCHLD, self.on_sigchld)
        self._eventloop.set_exception_handler(
            lambda loop, ctx: self.log.error(
                "Got an exception in {}: {}".format(loop, ctx))
        )
        fd = self._conn.get_file_descriptor()
        self._eventloop.add_reader(fd, self._xpoll)

        # HANDLE STANDARD EVENTS
        self.hook.register("MapRequest", self.on_map_request)
        self.hook.register("MapNotify", self.on_map_notify)
        self.hook.register("UnmapNotify", self.on_window_unmap)
        self.hook.register("KeyPress", self.on_key_press)
        # self.hook.register("KeyRelease", self.on_key_release)
        # self.hook.register("CreateNotify", self.on_window_create)
        self.hook.register("PropertyNotify", self.on_property_notify)
        self.hook.register("ClientMessage", self.on_client_message)
        self.hook.register("DestroyNotify", self.on_window_destroy)
        self.hook.register("EnterNotify", self.on_window_enter)
        self.hook.register("ConfigureRequest", self.on_configure_window)
        self.hook.register("MotionNotify", self.on_mouse_event)
        self.hook.register("ButtonPress", self.on_mouse_event)
        self.hook.register("ButtonRelease", self.on_mouse_event)

    def on_property_notify(self, evname, xcb_event):
        # TODO: messy ugly code
        wid = xcb_event.window
        atom = self.atoms.get_name(xcb_event.atom)
        # window = self.windows.get(wid, Window(wm=self, wid=wid, mapped=True))
        self.log.error("PropertyNotify: %s" % atom)
        run_("xprop -id %s %s" % (wid, atom))

    # TODO: dirty code, relocate to config
    def on_client_message(self, evname, xcb_event):
        self.log.error(dir(xcb_event))
        data = xcb_event.data
        resp_type = self.atoms.get_name(xcb_event.response_type)
        type = self.atoms.get_name(xcb_event.type)
        wid = xcb_event.window
        self.log.error("client message: resp_type={resp_type} window={wid} type={type} data={data}"  \
                       .format(**locals()))
        # 'bufsize', 'data', 'format', 'pack', 'response_type', 'sequence', 'synthetic', 'type', 'window']

        if type == '_NET_ACTIVE_WINDOW':
            window = self.windows[wid]
            window.rise()
            self.focus_on(window)

    def on_sigchld(self):
        """ Rip orphans. """
        while True:
            try:
                pid, status = os.waitpid(-1, os.WNOHANG)
                if (pid, status) == (0, 0):
                    # no child to rip
                    break
                self.log.notice("ripped child PID=%s" % pid)
            except ChildProcessError:
                break

    def on_map_request(self, evname, xcb_event):
        """ Map request is a request to draw the window on screen. """
        wid = xcb_event.window
        if wid not in self.windows:
            window = self.on_new_window(wid)
        else:
            window = self.windows[wid]
            self.log.on_map_request.debug("map request for %s" % window)
        window.show()
        if window.above_all:
            window.rise()
        if window.can_focus:
            window.focus()

    def on_new_window(self, wid):
        """ Registers new window. """
        window = Window(wm=self, wid=wid, atoms=self.atoms, mapped=True)
        # call configuration hood first
        # to setup attributes like 'sticky'
        self.hook.fire("new_window", window)
        self.log.on_new_window.debug(
            "new window is ready: %s" % window)
        self.windows[wid] = window
        self.win2desk[window] = self.cur_desktop
        if window.sticky:
            for desktop in self.desktops:
                desktop.add(window)
        else:
            self.cur_desktop.windows.append(window)
        return window

    def on_map_notify(self, evname, xcb_event):
        wid = xcb_event.window
        if wid not in self.windows:
            window = self.on_new_window(wid)
        else:
            window = self.windows[wid]
        window.mapped = True

        if window.above_all:
            window.rise()
        # if window.can_focus:
        #     window.focus()

        self.log.on_map_notify.debug("map notify for %s" % window)

    def on_window_unmap(self, evname, xcb_event):
        wid = xcb_event.window
        if wid not in self.windows:
            return
        window = self.windows[wid]
        window.mapped = False
        self.hook.fire("window_unmap", window)

    def on_window_destroy(self, evname, xcb_event):
        wid = xcb_event.window
        if wid not in self.windows:
            return

        window = self.windows[wid]
        assert isinstance(window, Window), "it's not a window: %s (%s)" % (
            window, type(window))

        for desktop in self.desktops:
            try:
                desktop.windows.remove(window)
                self.log.debug("%s removed from %s" % (self, desktop))
            except ValueError:
                pass
        del self.windows[wid]
        if window in self.win2desk:
            del self.win2desk[window]

    def on_window_enter(self, evname, xcb_event):
        wid = xcb_event.event
        if wid not in self.windows:
            # self.log.on_window_enter.error("no window with wid=%s" % wid)
            self.hook.fire("unknown_window", wid)
            return
        window = self.windows[wid]
        # self.log.on_window_enter("window_enter: %s %s" % (wid, window))
        self.hook.fire("window_enter", window)

    def grab_key(self, modifiers, key, owner_events=False, window=None):
        """ Intercept this key when it is pressed. If owner_events=False then
            the window in focus will not receive it. This is useful from WM hotkeys.
        """
        # TODO: check if key already grabbed?
        # Here is how X works with keys:
        # key => keysym => keycode
        # where `key' is something like 'a', 'b' or 'Enter',
        # `keysum' is what should be written on they key cap (physical keyboard)
        # and `keycode' is a number reported by the keyboard when the key is pressed.
        # Modifiers are keys like Shift, Alt, Win and some other buttons.
        self.log.grab_key.debug("intercept keys: %s %s" % (modifiers, key))

        if window is None:
            window = self.root

        keycode = self.kbd.key_to_code(key)
        modmask = get_modmask(modifiers)  # TODO: move to Keyboard
        event = ("on_key_press", modmask, keycode)
        pointer_mode = xproto.GrabMode.Async
        keyboard_mode = xproto.GrabMode.Async
        self._conn.core.GrabKey(
            owner_events,
            window.wid,
            modmask,
            keycode,
            pointer_mode,
            keyboard_mode
        )
        self.flush()  # TODO: do we need this?
        return event

    def on_key_press(self, evname, xcb_event):
        # TODO: ignore capslock, scrolllock and other modifiers?
        modmap = xcb_event.state
        keycode = xcb_event.detail
        event = ("on_key_press", modmap, keycode)
        self.hook.fire(event)

    def on_key_release(self, evname, xcb_event):
        modmap = xcb_event.state
        keycode = xcb_event.detail
        event = ("on_key_release", modmap, keycode)
        self.hook.fire(event)

    def grab_mouse(self, modifiers, button, owner_events=False, window=None):
        # http://www.x.org/archive/X11R7.7/doc/man/man3/xcb_grab_button.3.xhtml
        wid = (window or self.root).wid
        event_mask = xcffib.xproto.EventMask.ButtonPress |    \
            xcffib.xproto.EventMask.ButtonRelease |  \
            xcffib.xproto.EventMask.Button1Motion
        modmask = get_modmask(modifiers)
        pointer_mode = xproto.GrabMode.Async      # I don't know what it is
        keyboard_mode = xproto.GrabMode.Async     # do not block other keyboard events
        confine_to = xcffib.xproto.Atom._None     # do not restrict cursor movements
        cursor = xcffib.xproto.Atom._None         # do not change cursor
        event = ("on_mouse", modmask, button)     # event to be used in hooks

        self._conn.core.GrabButton(
            owner_events,
            wid,
            event_mask,
            pointer_mode,
            keyboard_mode,
            confine_to,
            cursor,
            button,
            modmask,
        )
        self.flush()  # TODO: do we need this?
        return event

    def hotkey(self, keys, cmd):
        """ Setup hook to launch a command on specific hotkeys. """
        @self.hook(self.grab_key(*keys))
        def cb(event):
            run_(cmd)

    def focus_on(self, window, warp=False):
        """ Focuses on given window. """
        self.cur_desktop.focus_on(window, warp)
        self.root.set_prop('_NET_ACTIVE_WINDOW', window.wid)

    def switch_to(self, desktop: Desktop):
        """ Switches to another desktop. """
        if isinstance(desktop, int):
            desktop = self.desktops[desktop]
        if self.cur_desktop == desktop:
            self.log.notice("attempt to switch to the same desktop")
            return
        self.log.debug("switching from {} to {}".format(
            self.cur_desktop, desktop))
        self.cur_desktop.hide()
        self.cur_desktop = desktop
        self.cur_desktop.show()
        # TODO: move this code to Desktop.show()
        self.root.props[self.atom._NET_CURRENT_DESKTOP] = desktop.id

    def relocate_to(self, window: Window, to_desktop: Desktop):
        """ Relocates window to a specific desktop. """
        if window.sticky:
            self.log.debug(
                "%s is meant to be on all desktops, cannot relocate to specific one" % window)
            return

        from_desktop = self.cur_desktop

        if from_desktop == to_desktop:
            self.log.debug(
                "no need to relocate %s because remains on the same desktop" % window)
            return

        from_desktop.remove(window)
        to_desktop.add(window)

    def on_mouse_event(self, evname, xcb_event):
        """evname is one of ButtonPress, ButtonRelease or MotionNotify."""
        # l = [(attr, getattr(xcb_event, attr)) for attr in sorted(dir(xcb_event)) if not attr.startswith('_')]
        # print(evname)
        # print(l)
        modmask = xcb_event.state & 0xff  # TODO: is the mask correct?
        if evname == 'MotionNotify':
            button = 1  # TODO
        else:
            button = xcb_event.detail

        event = ('on_mouse', modmask, button)
        # print(event)
        self.hook.fire(event, evname, xcb_event)

    def on_configure_window(self, _, event):
        # This code is so trivial that I just took it from fpwm as is :)
        values = []
        if event.value_mask & ConfigWindow.X:
            values.append(event.x)
        if event.value_mask & ConfigWindow.Y:
            values.append(event.y)
        if event.value_mask & ConfigWindow.Width:
            values.append(event.width)
        if event.value_mask & ConfigWindow.Height:
            values.append(event.height)
        if event.value_mask & ConfigWindow.BorderWidth:
            values.append(event.border_width)
        if event.value_mask & ConfigWindow.Sibling:
            values.append(event.sibling)
        if event.value_mask & ConfigWindow.StackMode:
            values.append(event.stack_mode)
        self._conn.core.ConfigureWindow(event.window, event.value_mask, values)

    def create_window(self, x, y, width, height):
        """ Create a window. Right now only used for initialization, see __init__. """
        wid = self._conn.generate_id()
        self._conn.core.CreateWindow(
            self.xcb_default_screen.root_depth,
            wid,
            self.xcb_default_screen.root,
            x, y, width, height, 0,
            WindowClass.InputOutput,
            self.xcb_default_screen.root_visual,
            CW.BackPixel | CW.EventMask,
            [
                self.xcb_default_screen.black_pixel,
                EventMask.StructureNotify | EventMask.Exposure
            ]
        )
        return Window(self, wid=wid, atoms=self.atoms)

    def scan(self, focus=True):
        """ Gets all windows in the system. """
        self.log.debug("performing scan of all mapped windows")
        q = self._conn.core.QueryTree(self.root.wid).reply()
        for wid in q.children:
            # attrs=self._conn.core.GetWindowAttributes(wid).reply()
            # print(attrs, type(attrs))
            # if attrs.map_state == xproto.MapState.Unmapped:
            #    self.log.scan.debug(
            #        "window %s is not mapped, skipping" % wid)  # TODO
            #    continue
            if wid not in self.windows:
                self.on_new_window(wid)
        self.log.scan.info("the following windows are active: %s" %
                           sorted(self.windows.values()))

        if focus:
            windows = sorted(self.windows.values())
            windows = list(filter(lambda w: w != self.root and not w.skip, windows))
            if windows:
                # on empty desktop there is nothing to focus on
                self.cur_desktop.focus_on(windows[-1], warp=True)

    def finalize(self):
        """ This code is run when event loop is terminated. """
        pass  # currently nothing to do here

    def flush(self):
        """ Force pending X request to be sent.
            By default XCB aggressevly buffers for performance reasons. """
        return self._conn.flush()

    def xsync(self):
        """ Flush XCB queue and wait till it is processed by X server. """
        # The idea here is that pushing an innocuous request through the queue
        # and waiting for a response "syncs" the connection, since requests are
        # serviced in order.
        self._conn.core.GetInputFocus().reply()

    def stop(self, xserver_dead=False):
        """ Stop WM to quit. """
        self.hook.fire("on_exit")
        # display all hidden windows
        try:
            if not xserver_dead:
                for window in self.windows.values():
                    window.show()
                self.xsync()
        except Exception as err:
            self.log.stop.error("error on stop: %s" % err)
        self.log.stop.debug("stopping event loop")
        self._eventloop.stop()

    def replace(self, execv_args):
        self.log.notice("replacing current process with %s" % (execv_args,))
        self.stop()
        import os
        os.execv(*execv_args)

    def loop(self):
        """ DITTO """
        self.scan()
        try:
            self._eventloop.run_forever()
        finally:
            self.finalize()

    def _xpoll(self):
        """ Fetch incomming events (if any) and call hooks. """

        # OK, kids, today I'll teach you how to write reliable enterprise
        # software! You just catch all the exceptions in the top-level loop
        # and ignore them. No, I'm kidding, these exceptions are no use
        # for us because we don't care if a window cannot be drawn or something.
        # We actually only need to handle just a few events and ignore the rest.
        # Exceptions happen because of the async nature of X.

        while True:
            try:
                xcb_event = self._conn.poll_for_event()
                if not xcb_event:
                    break
                evname = xcb_event.__class__.__name__
                if evname.endswith("Event"):
                    evname = evname[:-5]
                if evname in self.ignoreEvents:
                    self.log._xpoll.info("ignoring %s" % xcb_event)
                    continue
                self.log._xpoll.critical("got %s %s" % (evname, xcb_event))
                self.hook.fire(evname, xcb_event)
                self.flush()  # xcb doesn't flush implicitly
            except (WindowError, AccessError, DrawableError):
                self.log.debug("(minor exception)")
            except Exception as e:
                self.log._xpoll.error(traceback.format_exc())
                error_code = self._conn.has_error()
                if error_code:
                    error_string = XCB_CONN_ERRORS[error_code]
                    self.log.critical("Shutting down due to X connection error %s (%s)" %
                                      (error_string, error_code))
                    self.stop(xserver_dead=True)
                    break
예제 #3
0
파일: window.py 프로젝트: kopchik/swm
class Window:
    sticky = False
    can_focus = True
    above_all = False
    mapped = False
    skip = False  # do not handle show() and hide() for this event
    type = "normal"
    name = "<no name>"

    def __init__(self, wm, wid, atoms, mapped=True):
        from wm import WM  # TODO: dirtyhack to avoid circular imports
        assert isinstance(wm, WM), "wm must be an instance of WM"
        assert isinstance(wid, int), "wid must be int"
        self.wid = wid
        self.wm = wm
        self._conn = self.wm._conn
        self.prev_geometry = None
        self.props = Props(conn=self._conn, window=self, atoms=atoms)
        self.update_name()  # TODO: this is not updated
        self.update_window_type()
        # do it after self.name is set (so repr works)
        self.log = Log(self)
        self.mapped = mapped
        self.hints = {}
        self.update_wm_hints()
        # subscribe for notifications
        self._conn.core.ChangeWindowAttributesChecked(wid, CW.EventMask,
                                                      [EventMask.EnterWindow])

    def show(self):
        assert not self.skip
        self.log.show.debug("showing")
        self._conn.core.MapWindow(self.wid)
        self.wm.flush()
        self.mapped = True

    def hide(self):
        assert not self.skip
        self._conn.core.UnmapWindow(self.wid)
        self.wm.flush()
        self.mapped = False

    def rise(self):
        """ Put window on top of others. TODO: what about focus? """
        return self.stackmode(xproto.StackMode.Above)

    def lower(self):
        """ Put window on top of others. TODO: what about focus? """
        return self.stackmode(xproto.StackMode.Below)

    def raiseorlower(self):
        """ Put window on top of others. TODO: what about focus? """
        return self.stackmode(xproto.StackMode.Opposite)

    def stackmode(self, mode):
        self._conn.core.ConfigureWindowChecked(self.wid,
                                               xproto.ConfigWindow.StackMode,
                                               [mode]).check()
        self.wm.flush()

    def focus(self):
        """ Let window receive mouse and keyboard events.
            X expects window to be mapped.
        """
        if not self.mapped:
            self.show()
        #self.wm.cur_desktop.cur_focus = self
        # TODO: self.wm.root.set_property("_NET_ACTIVE_WINDOW", self.wid)
        self._conn.core.SetInputFocus(xproto.InputFocus.PointerRoot, self.wid,
                                      xproto.Time.CurrentTime)
        self.wm.flush()  # it is here mandatory :(
        return self

    def kill(self):
        """ This is what happens to windows when Alt-F4 or Ctrl-w is pressed. """
        self._conn.core.KillClient(self.wid)

    def move(self, x=None, y=None, dx=0, dy=0):
        """ Like set_geometry, but with sanity check. """
        if dx or dy:
            x, y, width, height = self.geometry
            x += dx
            y += dy
        x = max(x, 0)
        y = max(y, 0)
        self.set_geometry(x=x, y=y)
        return self

    def resize(self, x=None, y=None, dx=0, dy=0):
        """ Like set_geometry, but with sanity check. """
        assert not ((x and y) and (dx or dy)), "wrong arguments"
        if x and y:
            width = x
            height = y
        else:
            x, y, width, height = self.geometry
            width += dx
            height += dy
        width = max(5, width)
        height = max(5, height)
        self.set_geometry(width=width, height=height)
        return self

    def toggle_maximize(self):
        if self.prev_geometry:
            self.set_geometry(*self.prev_geometry)
            self.prev_geometry = None
        else:
            self.prev_geometry = self.geometry
            screen = self.wm.xrandr.screen
            self.set_geometry(x=0,
                              y=0,
                              width=screen.width - 1,
                              height=screen.height - 1 -
                              18)  # TODO: 18 dirtyhack to place bottom panel
            self.rise()

    @property
    def geometry(self):
        geom = self._conn.core.GetGeometry(self.wid).reply()
        return [geom.x, geom.y, geom.width, geom.height]

    def set_geometry(self, x=None, y=None, width=None, height=None):
        mask = 0
        values = []
        if x is not None:
            mask |= xproto.ConfigWindow.X
            values.append(x)
        if y is not None:
            mask |= xproto.ConfigWindow.Y
            values.append(y)
        if width is not None:
            mask |= xproto.ConfigWindow.Width
            values.append(width)
        if height is not None:
            mask |= xproto.ConfigWindow.Height
            values.append(height)
        # TODO: what the hell is *Checked and check?

        # filter negative values
        values = [max(value, 0) for value in values]
        self._conn.core.ConfigureWindowChecked(self.wid, mask, values).check()

    def update_name(self):
        name = "(no name)"
        for prop in ["_NET_WM_VISIBLE_NAME", "_NET_WM_NAME"]:
            new_name = self.props[prop]
            if new_name:
                name = new_name
                break
        self.name = name
        return name

    def warp(self):
        """ Warps pointer to the middle of the window. Does not work under Xephyr :( """
        x, y, width, height = self.geometry
        self._conn.core.WarpPointer(
            0,
            self.wid,  # src_window, dst_window
            0,
            0,  # src_x, src_y
            0,
            0,  # src_width, src_height
            width // 2,
            height // 2  # dest_x, dest_y
        )
        self.wm.flush()
        return self

    def get_attributes(self):
        """ Returns https://tronche.com/gui/x/xlib/window-information/XGetWindowAttributes.html . """
        return self._conn.core.GetWindowAttributes(self.wid).reply()

    def set_attr(self, **kwargs):
        mask, values = AttributeMasks(**kwargs)
        self.wm._conn.core.ChangeWindowAttributesChecked(
            self.wid, mask, values)

    def get_prop(self, prop, typ=None, unpack=None):
        """
            Return the contents of a property as a GetPropertyReply. If unpack
            is specified, a tuple of values is returned.  The type to unpack,
            either `str` or `int` must be specified.
        """
        if typ is None:
            if prop not in PROPERTYMAP:
                raise ValueError("Must specify type for unknown property.")
            else:
                typ, _ = PROPERTYMAP[prop]

        prop = self.wm.atoms[prop] if isinstance(prop, str) else prop
        typ = self.wm.atoms[typ] if isinstance(typ, str) else typ

        r = self._conn.core.GetProperty(
            False,  # delete
            self.wid,  # window id
            prop,
            typ,
            0,  # long_offset,
            (2**32) - 1  # long_length
        ).reply()

        if not r.value_len:
            if unpack:
                return []
            return None
        elif unpack:
            # Should we allow more options for unpacking?
            if unpack is int:
                return r.value.to_atoms()
            elif unpack is str:
                return r.value.to_string()
        else:
            return r

    # TODO: move this code to WM
    def set_prop(self, name, value, type=None, format=None):
        """
            name: String Atom name
            type: String Atom name
            format: 8, 16, 32
        """
        if name in PROPERTYMAP:
            if type or format:
                raise ValueError(
                    "Over-riding default type or format for property.")
            type, format = PROPERTYMAP[name]
        else:
            if None in (type, format):
                raise ValueError(
                    "Must specify type and format for unknown property.")

        if isinstance(value, str):
            # xcffib will pack the bytes, but we should encode them properly
            value = value.encode()
        elif isinstance(value, int):
            value = [value]

        self.wm._conn.core.ChangePropertyChecked(
            xproto.PropMode.Replace,
            self.wid,
            self.wm.atoms[name],
            self.wm.atoms[type],
            format,  # Format - 8, 16, 32
            len(value),
            value).check()

    def list_props(self):
        reply = self.wm._conn.core.ListProperties(self.wid).reply()
        atoms = reply.atoms.list
        return [self.wm.atoms.get_name(atom) for atom in atoms]

    def update_window_type(self):
        window_types = self.props['_NET_WM_WINDOW_TYPE']
        if not types:
            self.skip = True

        for atom in window_types:
            if atom.name in WINDOW_TYPES:
                self.type = WINDOW_TYPES[raw_type.name]
                break
        else:
            self.type = "normal"

        return self.type

    def update_wm_hints(self):
        # TODO: dirty code borowwed from qtile
        l = self.get_prop(Atom.WM_HINTS,
                          typ=xproto.GetPropertyType.Any,
                          unpack=int)
        # self.log.error("WM_HINTS: {}".format(l))
        if not l:
            return

        flags = set(k for k, v in HintsFlags.items() if l[0] & v)
        hints = dict(flags=flags,
                     input=l[1],
                     initial_state=l[2],
                     icon_pixmap=l[3],
                     icon_window=l[4],
                     icon_x=l[5],
                     icon_y=l[6],
                     icon_mask=l[7],
                     window_group=l[8])

        self.log.error("parsed hints: {}".format(hints))
        if "InputHint" not in hints['flags']:
            self.log.notice("input hint off")
            self.can_focus = False
        self.hints = hints
        self.flags = flags

    def __lt__(self, other):  # used for sorting and comparison
        return True

    def __repr__(self):
        name = self.name
        if len(name) > 20:
            name = name[:17] + '...'
        return "Window(%s, \"%s\")" % (self.wid, name)
예제 #4
0
파일: window.py 프로젝트: kopchik/swm
class Window:
    sticky = False
    can_focus = True
    above_all = False
    mapped = False
    skip = False  # do not handle show() and hide() for this event
    type = "normal"
    name = "<no name>"

    def __init__(self, wm, wid, atoms, mapped=True):
        from wm import WM  # TODO: dirtyhack to avoid circular imports
        assert isinstance(wm, WM), "wm must be an instance of WM"
        assert isinstance(wid, int), "wid must be int"
        self.wid = wid
        self.wm = wm
        self._conn = self.wm._conn
        self.prev_geometry = None
        self.props = Props(conn=self._conn, window=self, atoms=atoms)
        self.update_name()  # TODO: this is not updated
        self.update_window_type()
        # do it after self.name is set (so repr works)
        self.log = Log(self)
        self.mapped = mapped
        self.hints = {}
        self.update_wm_hints()
        # subscribe for notifications
        self._conn.core.ChangeWindowAttributesChecked(
            wid, CW.EventMask, [EventMask.EnterWindow])

    def show(self):
        assert not self.skip
        self.log.show.debug("showing")
        self._conn.core.MapWindow(self.wid)
        self.wm.flush()
        self.mapped = True

    def hide(self):
        assert not self.skip
        self._conn.core.UnmapWindow(self.wid)
        self.wm.flush()
        self.mapped = False

    def rise(self):
        """ Put window on top of others. TODO: what about focus? """
        return self.stackmode(xproto.StackMode.Above)

    def lower(self):
        """ Put window on top of others. TODO: what about focus? """
        return self.stackmode(xproto.StackMode.Below)

    def raiseorlower(self):
        """ Put window on top of others. TODO: what about focus? """
        return self.stackmode(xproto.StackMode.Opposite)

    def stackmode(self, mode):
        self._conn.core.ConfigureWindowChecked(self.wid,
                                               xproto.ConfigWindow.StackMode,
                                               [mode]).check()
        self.wm.flush()

    def focus(self):
        """ Let window receive mouse and keyboard events.
            X expects window to be mapped.
        """
        if not self.mapped:
            self.show()
        #self.wm.cur_desktop.cur_focus = self
        # TODO: self.wm.root.set_property("_NET_ACTIVE_WINDOW", self.wid)
        self._conn.core.SetInputFocus(xproto.InputFocus.PointerRoot,
                                      self.wid, xproto.Time.CurrentTime)
        self.wm.flush()  # it is here mandatory :(
        return self

    def kill(self):
        """ This is what happens to windows when Alt-F4 or Ctrl-w is pressed. """
        self._conn.core.KillClient(self.wid)

    def move(self, x=None, y=None, dx=0, dy=0):
        """ Like set_geometry, but with sanity check. """
        if dx or dy:
            x, y, width, height = self.geometry
            x += dx
            y += dy
        x = max(x, 0)
        y = max(y, 0)
        self.set_geometry(x=x, y=y)
        return self

    def resize(self, x=None, y=None, dx=0, dy=0):
        """ Like set_geometry, but with sanity check. """
        assert not ((x and y) and (dx or dy)), "wrong arguments"
        if x and y:
            width = x
            height = y
        else:
            x, y, width, height = self.geometry
            width += dx
            height += dy
        width = max(5, width)
        height = max(5, height)
        self.set_geometry(width=width, height=height)
        return self

    def toggle_maximize(self):
        if self.prev_geometry:
            self.set_geometry(*self.prev_geometry)
            self.prev_geometry = None
        else:
            self.prev_geometry = self.geometry
            screen = self.wm.xrandr.screen
            self.set_geometry(
                x=0,
                y=0,
                width=screen.width -
                1,
                height=screen.height -
                1 -
                18)  # TODO: 18 dirtyhack to place bottom panel
            self.rise()

    @property
    def geometry(self):
        geom = self._conn.core.GetGeometry(self.wid).reply()
        return [geom.x, geom.y, geom.width, geom.height]

    def set_geometry(self, x=None, y=None, width=None, height=None):
        mask = 0
        values = []
        if x is not None:
            mask |= xproto.ConfigWindow.X
            values.append(x)
        if y is not None:
            mask |= xproto.ConfigWindow.Y
            values.append(y)
        if width is not None:
            mask |= xproto.ConfigWindow.Width
            values.append(width)
        if height is not None:
            mask |= xproto.ConfigWindow.Height
            values.append(height)
        # TODO: what the hell is *Checked and check?

        # filter negative values
        values = [max(value, 0) for value in values]
        self._conn.core.ConfigureWindowChecked(self.wid, mask, values).check()

    def update_name(self):
        name = "(no name)"
        for prop in ["_NET_WM_VISIBLE_NAME", "_NET_WM_NAME"]:
            new_name = self.props[prop]
            if new_name:
                name = new_name
                break
        self.name = name
        return name

    def warp(self):
        """ Warps pointer to the middle of the window. Does not work under Xephyr :( """
        x, y, width, height = self.geometry
        self._conn.core.WarpPointer(
            0, self.wid,                    # src_window, dst_window
            0, 0,                           # src_x, src_y
            0, 0,                           # src_width, src_height
            width // 2, height // 2         # dest_x, dest_y
        )
        self.wm.flush()
        return self

    def get_attributes(self):
        """ Returns https://tronche.com/gui/x/xlib/window-information/XGetWindowAttributes.html . """
        return self._conn.core.GetWindowAttributes(self.wid).reply()

    def set_attr(self, **kwargs):
        mask, values = AttributeMasks(**kwargs)
        self.wm._conn.core.ChangeWindowAttributesChecked(
            self.wid, mask, values
        )

    def get_prop(self, prop, typ=None, unpack=None):
        """
            Return the contents of a property as a GetPropertyReply. If unpack
            is specified, a tuple of values is returned.  The type to unpack,
            either `str` or `int` must be specified.
        """
        if typ is None:
            if prop not in PROPERTYMAP:
                raise ValueError(
                    "Must specify type for unknown property."
                )
            else:
                typ, _ = PROPERTYMAP[prop]

        prop = self.wm.atoms[prop] if isinstance(prop, str) else prop
        typ = self.wm.atoms[typ] if isinstance(typ, str) else typ

        r = self._conn.core.GetProperty(
            False,         # delete
            self.wid,      # window id
            prop,
            typ,
            0,             # long_offset,
            (2 ** 32) - 1  # long_length
        ).reply()

        if not r.value_len:
            if unpack:
                return []
            return None
        elif unpack:
            # Should we allow more options for unpacking?
            if unpack is int:
                return r.value.to_atoms()
            elif unpack is str:
                return r.value.to_string()
        else:
            return r

    # TODO: move this code to WM
    def set_prop(self, name, value, type=None, format=None):
        """
            name: String Atom name
            type: String Atom name
            format: 8, 16, 32
        """
        if name in PROPERTYMAP:
            if type or format:
                raise ValueError(
                    "Over-riding default type or format for property."
                )
            type, format = PROPERTYMAP[name]
        else:
            if None in (type, format):
                raise ValueError(
                    "Must specify type and format for unknown property."
                )

        if isinstance(value, str):
            # xcffib will pack the bytes, but we should encode them properly
            value = value.encode()
        elif isinstance(value, int):
            value = [value]

        self.wm._conn.core.ChangePropertyChecked(
            xproto.PropMode.Replace,
            self.wid,
            self.wm.atoms[name],
            self.wm.atoms[type],
            format,  # Format - 8, 16, 32
            len(value),
            value
        ).check()

    def list_props(self):
        reply = self.wm._conn.core.ListProperties(self.wid).reply()
        atoms = reply.atoms.list
        return [self.wm.atoms.get_name(atom) for atom in atoms]

    def update_window_type(self):
        window_types = self.props['_NET_WM_WINDOW_TYPE']
        if not types:
            self.skip = True

        for atom in window_types:
            if atom.name in WINDOW_TYPES:
                self.type = WINDOW_TYPES[raw_type.name]
                break
        else:
            self.type = "normal"

        return self.type

    def update_wm_hints(self):
        # TODO: dirty code borowwed from qtile
        l = self.get_prop(
            Atom.WM_HINTS,
            typ=xproto.GetPropertyType.Any,
            unpack=int)
        # self.log.error("WM_HINTS: {}".format(l))
        if not l:
            return

        flags = set(k for k, v in HintsFlags.items() if l[0] & v)
        hints = dict(
            flags=flags,
            input=l[1],
            initial_state=l[2],
            icon_pixmap=l[3],
            icon_window=l[4],
            icon_x=l[5],
            icon_y=l[6],
            icon_mask=l[7],
            window_group=l[8]
        )

        self.log.error("parsed hints: {}".format(hints))
        if "InputHint" not in hints['flags']:
            self.log.notice("input hint off")
            self.can_focus = False
        self.hints = hints
        self.flags = flags

    def __lt__(self, other):  # used for sorting and comparison
        return True

    def __repr__(self):
        name = self.name
        if len(name) > 20:
            name = name[:17] + '...'
        return "Window(%s, \"%s\")" % (self.wid, name)
예제 #5
0
파일: hook.py 프로젝트: kopchik/swm
class Hook:
    """ Simple callback dispatcher. """
    def __init__(self):
        self.cb_map = defaultdict(list)
        self.log = Log("hook")
        self.suppressed = set()

    def decor(self, event):
        def wrap(cb):
            self.register(event, cb)
            return cb

        return wrap

    __call__ = decor

    def register(self, event, cb):
        self.cb_map[event].append(cb)

    def has_hook(self, event):
        return event in self.cb_map

    def suppress(self, event):
        hook = self

        class Context:
            def __enter__(self):
                hook.log.debug("suppressing %s" % event)
                hook.suppressed.add(event)

            def __exit__(self, *args):
                hook.log.debug("un-suppressing %s" % event)
                if event in hook.suppressed:
                    hook.suppressed.remove(event)
                else:
                    hook.log.notice("uhm, event is not suppressed: %s" % event)

        return Context()

    def fire(self, event, *args, **kwargs):
        self.log.debug("{} {} {}".format(event, args, kwargs))
        if event not in self.cb_map:
            self.log.notice("no handler for {}".format(event))
            return

        if event in self.suppressed:
            self.log.debug("event suppressed: {} {} {}".format(
                event, args, kwargs))
            return

        handlers = self.cb_map[event]
        for handler in handlers:
            try:
                handler(event, *args, **kwargs)
            # except SupressEvent:
            # break
            except Exception as err:
                # msg = "error on event {ev}: {err} ({typ}) (in {hdl})" \
                #     .format(err=err, typ=type(err), ev=event, hdl=handler)
                msg = traceback.format_exc()
                self.log.error(msg)
예제 #6
0
파일: hook.py 프로젝트: kopchik/swm
class Hook:
    """ Simple callback dispatcher. """

    def __init__(self):
        self.cb_map = defaultdict(list)
        self.log = Log("hook")
        self.suppressed = set()

    def decor(self, event):
        def wrap(cb):
            self.register(event, cb)
            return cb

        return wrap

    __call__ = decor

    def register(self, event, cb):
        self.cb_map[event].append(cb)

    def has_hook(self, event):
        return event in self.cb_map

    def suppress(self, event):
        hook = self

        class Context:
            def __enter__(self):
                hook.log.debug("suppressing %s" % event)
                hook.suppressed.add(event)

            def __exit__(self, *args):
                hook.log.debug("un-suppressing %s" % event)
                if event in hook.suppressed:
                    hook.suppressed.remove(event)
                else:
                    hook.log.notice("uhm, event is not suppressed: %s" % event)

        return Context()

    def fire(self, event, *args, **kwargs):
        self.log.debug("{} {} {}".format(event, args, kwargs))
        if event not in self.cb_map:
            self.log.notice("no handler for {}".format(event))
            return

        if event in self.suppressed:
            self.log.debug("event suppressed: {} {} {}".format(event, args, kwargs))
            return

        handlers = self.cb_map[event]
        for handler in handlers:
            try:
                handler(event, *args, **kwargs)
            # except SupressEvent:
            # break
            except Exception as err:
                # msg = "error on event {ev}: {err} ({typ}) (in {hdl})" \
                #     .format(err=err, typ=type(err), ev=event, hdl=handler)
                msg = traceback.format_exc()
                self.log.error(msg)