Repository: BurntSushi/pytyle3
Branch: master
Commit: 8a1ca4189659
Files: 17
Total size: 35.6 KB
Directory structure:
gitextract__fk6cinx/
├── COPYING
├── INSTALL
├── README
├── config.py
├── keybind.py
├── pt3/
│ ├── __init__.py
│ ├── client.py
│ ├── config.py
│ ├── debug.py
│ ├── keybind.py
│ ├── layouts/
│ │ ├── __init__.py
│ │ ├── layout_vert_horz.py
│ │ └── store.py
│ ├── state.py
│ └── tile.py
├── pytyle3
└── setup.py
================================================
FILE CONTENTS
================================================
================================================
FILE: COPYING
================================================
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.
================================================
FILE: INSTALL
================================================
It should be as straight-forward as
sudo python2 setup.py install
Two configuration files, config.py and keybind.py will be copied to
/etc/xdg/pytyle3.
To configure pytyle3 on a user basis, create ~/.config/pytyle3 and copy
/etc/xdg/pytyle3/*py to that directory.
pytyle3 will require a restart if the configuration has changed.
================================================
FILE: README
================================================
An updated (and much faster) version of pytyle that uses xpybutil and is
compatible with Openbox Multihead.
Due to using xpybutil much of the lower level XCB/xpyb stuff has been factored
out of pytyle3. Also, because it relies on Openbox Multihead when there is more
than one monitor active, a lot less state needs to be saved.
As a result, pytyle3 is ~1000 lines while pytyle2 is ~7000 lines. Additionally,
because of a simpler design, pytyle3's memory footprint is much smaller and is
actually quite snappier in moving windows on the screen.
================================================
FILE: config.py
================================================
# A list of windows to ignore...
# will search both the class and role of the WM_CLASS property
# case-insensitive
ignore = ['gmrun', 'qjackctl', 'viewnior', 'gnome-screenshot', 'mplayer', 'file-roller']
# If this list is non-empty, only windows in the list will be tiled.
# The matching algorithm is precisely the same as for 'ignore'.
tile_only = []
# Whether to enable tiling on startup
tile_on_startup = False
# Whether tiled windows are below others
tiles_below = True
# Whether new windows should tile or float by default (default is False)
floats_default = True
# Whether tiled windows should have their decorations removed
remove_decorations = False
# How much to increment the master area proportion size
proportion_change = 0.05
# If you have panels that don't set struts (*ahem* JWM's panel), then
# setting a margin is the only way to force pytyle not to cover your panels.
# IMPORTANT NOTE: If you set *any* margin, pytyle3 will automatically skip
# all strut auto-detection. So your margins should account for all panels, even
# if the others set struts.
# The format here is to have one set of margins for each active physical
# head. They should be in the following order: Left to right, top to bottom.
# Make sure to set 'use_margins' to True!
use_margins = False
margins = [ {'top': 0, 'bottom': 1, 'left': 0, 'right': 0} ]
# Leave some empty space between windows
gap = 0
# Whether to send any debug information to stdout
debug = False
================================================
FILE: keybind.py
================================================
# This is a python script. Pay attention to the syntax and indentation
import state
import tile
bindings = {
# You can use Control and Shift. Alt is Mod1, Super is Mod4.
#Available commands :
# tile: start tiling
# untile: stop tiling and move the windows back to their original position
# cycle: switch between horizontal and vertical tiling
# increase_master: increase the space allocated to master windows
# decrease_master: increase the space allocated to slave windows
# add_master: send a window from the slave group to the master group
# remove_master: send a window from the master group to the slave group
# prev_client: Focus the previous window
# next_client: Focus the next window
# focus_master: Focus the master window
# switch_prev_client: switch active window with previous
# switch_next_client: switch active window with next
# rotate: shift all windows' positions (clockwise)
# make_master: send active window to the master position
'Control-Mod1-v': tile.cmd('tile'),
'Control-Mod1-BackSpace': tile.cmd('untile'),
'Control-Mod1-s': tile.cmd('decrease_master'),
'Control-Mod1-r': tile.cmd('increase_master'),
'Control-Mod1-g': tile.cmd('remove_master'),
'Control-Mod1-d': tile.cmd('add_master'),
'Control-Mod1-c': tile.cmd('rotate'),
'Control-Mod1-h': tile.cmd('cycle'),
'Control-Mod1-f': tile.cmd('toggle_float'),
# quit pytyle
'Control-Mod1-q': state.quit,
}
================================================
FILE: pt3/__init__.py
================================================
================================================
FILE: pt3/client.py
================================================
import time
import xcb.xproto
import xpybutil
import xpybutil.event as event
import xpybutil.ewmh as ewmh
import xpybutil.motif as motif
import xpybutil.icccm as icccm
import xpybutil.rect as rect
import xpybutil.util as util
import xpybutil.window as window
from debug import debug
import config
import state
import tile
clients = {}
ignore = [] # Some clients are never gunna make it...
class Client(object):
def __init__(self, wid):
self.wid = wid
self.name = ewmh.get_wm_name(self.wid).reply() or 'N/A'
debug('Connecting to %s' % self)
window.listen(self.wid, 'PropertyChange', 'FocusChange')
event.connect('PropertyNotify', self.wid, self.cb_property_notify)
event.connect('FocusIn', self.wid, self.cb_focus_in)
event.connect('FocusOut', self.wid, self.cb_focus_out)
# This connects to the parent window (decorations)
# We get all resize AND move events... might be too much
self.parentid = window.get_parent_window(self.wid)
window.listen(self.parentid, 'StructureNotify')
event.connect('ConfigureNotify', self.parentid,
self.cb_configure_notify)
# A window should only be floating if that is default
self.floating = getattr(config, 'floats_default', False)
# Not currently in a "moving" state
self.moving = False
# Load some data
self.desk = ewmh.get_wm_desktop(self.wid).reply()
# Add it to this desktop's tilers
tile.update_client_add(self)
# First cut at saving client geometry
self.save()
def remove(self):
tile.update_client_removal(self)
debug('Disconnecting from %s' % self)
event.disconnect('ConfigureNotify', self.parentid)
event.disconnect('PropertyNotify', self.wid)
event.disconnect('FocusIn', self.wid)
event.disconnect('FocusOut', self.wid)
def activate(self):
ewmh.request_active_window_checked(self.wid, source=1).check()
def unmaximize(self):
vatom = util.get_atom('_NET_WM_STATE_MAXIMIZED_VERT')
hatom = util.get_atom('_NET_WM_STATE_MAXIMIZED_HORZ')
ewmh.request_wm_state_checked(self.wid, 0, vatom, hatom).check()
def save(self):
self.saved_geom = window.get_geometry(self.wid)
self.saved_state = ewmh.get_wm_state(self.wid).reply()
def restore(self):
debug('Restoring %s' % self)
if getattr(config, 'remove_decorations', False):
motif.set_hints_checked(self.wid,2,decoration=1).check()
if getattr(config, 'tiles_below', False):
ewmh.request_wm_state_checked(self.wid,0,util.get_atom('_NET_WM_STATE_BELOW')).check()
if self.saved_state:
fullymaxed = False
vatom = util.get_atom('_NET_WM_STATE_MAXIMIZED_VERT')
hatom = util.get_atom('_NET_WM_STATE_MAXIMIZED_HORZ')
if vatom in self.saved_state and hatom in self.saved_state:
fullymaxed = True
ewmh.request_wm_state_checked(self.wid, 1, vatom, hatom).check()
elif vatom in self.saved_state:
ewmh.request_wm_state_checked(self.wid, 1, vatom).check()
elif hatom in self.saved_state:
ewmh.request_wm_state_checked(self.wid, 1, hatom).check()
# No need to continue if we've fully maximized the window
if fullymaxed:
return
mnow = rect.get_monitor_area(window.get_geometry(self.wid),
state.monitors)
mold = rect.get_monitor_area(self.saved_geom, state.monitors)
x, y, w, h = self.saved_geom
# What if the client is on a monitor different than what it was before?
# Use the same algorithm in Openbox to convert one monitor's
# coordinates to another.
if mnow != mold:
nowx, nowy, noww, nowh = mnow
oldx, oldy, oldw, oldh = mold
xrat, yrat = float(noww) / float(oldw), float(nowh) / float(oldh)
x = nowx + (x - oldx) * xrat
y = nowy + (y - oldy) * yrat
w *= xrat
h *= yrat
window.moveresize(self.wid, x, y, w, h)
def moveresize(self, x=None, y=None, w=None, h=None):
# Ignore this if the user is moving the window...
if self.moving:
print 'Sorry but %s is moving...' % self
return
try:
window.moveresize(self.wid, x, y, w, h)
except:
pass
def is_button_pressed(self):
try:
pointer = xpybutil.conn.core.QueryPointer(self.wid).reply()
if pointer is None:
return False
if (xcb.xproto.KeyButMask.Button1 & pointer.mask or
xcb.xproto.KeyButMask.Button3 & pointer.mask):
return True
except xcb.xproto.BadWindow:
pass
return False
def cb_focus_in(self, e):
if self.moving and e.mode == xcb.xproto.NotifyMode.Ungrab:
state.GRAB = None
self.moving = False
tile.update_client_moved(self)
def cb_focus_out(self, e):
if e.mode == xcb.xproto.NotifyMode.Grab:
state.GRAB = self
def cb_configure_notify(self, e):
if state.GRAB is self and self.is_button_pressed():
self.moving = True
def cb_property_notify(self, e):
aname = util.get_atom_name(e.atom)
try:
if aname == '_NET_WM_DESKTOP':
if should_ignore(self.wid):
untrack_client(self.wid)
return
olddesk = self.desk
self.desk = ewmh.get_wm_desktop(self.wid).reply()
if self.desk is not None and self.desk != olddesk:
tile.update_client_desktop(self, olddesk)
else:
self.desk = olddesk
elif aname == '_NET_WM_STATE':
if should_ignore(self.wid):
untrack_client(self.wid)
return
except xcb.xproto.BadWindow:
pass # S'ok...
def __str__(self):
return '{%s (%d)}' % (self.name[0:30], self.wid)
def update_clients():
client_list = ewmh.get_client_list_stacking().reply()
client_list = list(reversed(client_list))
for c in client_list:
if c not in clients:
track_client(c)
for c in clients.keys():
if c not in client_list:
untrack_client(c)
def track_client(client):
assert client not in clients
try:
if not should_ignore(client):
if state.PYTYLE_STATE == 'running':
# This is truly unfortunate and only seems to be necessary when
# a client comes back from an iconified state. This causes a
# slight lag when a new window is mapped, though.
time.sleep(0.2)
clients[client] = Client(client)
except xcb.xproto.BadWindow:
debug('Window %s was destroyed before we could finish inspecting it. '
'Untracking it...' % client)
untrack_client(client)
def untrack_client(client):
if client not in clients:
return
c = clients[client]
del clients[client]
c.remove()
def should_ignore(client):
# Don't waste time on clients we'll never possibly tile
if client in ignore:
return True
nm = ewmh.get_wm_name(client).reply()
wm_class = icccm.get_wm_class(client).reply()
if wm_class is not None:
try:
inst, cls = wm_class
matchNames = set([inst.lower(), cls.lower()])
if matchNames.intersection(config.ignore):
debug('Ignoring %s because it is in the ignore list' % nm)
return True
if hasattr(config, 'tile_only') and config.tile_only:
if not matchNames.intersection(config.tile_only):
debug('Ignoring %s because it is not in the tile_only '
'list' % nm)
return True
except ValueError:
pass
if icccm.get_wm_transient_for(client).reply() is not None:
debug('Ignoring %s because it is transient' % nm)
ignore.append(client)
return True
wtype = ewmh.get_wm_window_type(client).reply()
if wtype:
for atom in wtype:
aname = util.get_atom_name(atom)
if aname in ('_NET_WM_WINDOW_TYPE_DESKTOP',
'_NET_WM_WINDOW_TYPE_DOCK',
'_NET_WM_WINDOW_TYPE_TOOLBAR',
'_NET_WM_WINDOW_TYPE_MENU',
'_NET_WM_WINDOW_TYPE_UTILITY',
'_NET_WM_WINDOW_TYPE_SPLASH',
'_NET_WM_WINDOW_TYPE_DIALOG',
'_NET_WM_WINDOW_TYPE_DROPDOWN_MENU',
'_NET_WM_WINDOW_TYPE_POPUP_MENU',
'_NET_WM_WINDOW_TYPE_TOOLTIP',
'_NET_WM_WINDOW_TYPE_NOTIFICATION',
'_NET_WM_WINDOW_TYPE_COMBO',
'_NET_WM_WINDOW_TYPE_DND'):
debug('Ignoring %s because it has type %s' % (nm, aname))
ignore.append(client)
return True
wstate = ewmh.get_wm_state(client).reply()
if wstate is None:
debug('Ignoring %s because it does not have a state' % nm)
return True
for atom in wstate:
aname = util.get_atom_name(atom)
# For now, while I decide how to handle these guys
if aname == '_NET_WM_STATE_STICKY':
debug('Ignoring %s because it is sticky and they are weird' % nm)
return True
if aname in ('_NET_WM_STATE_SHADED', '_NET_WM_STATE_HIDDEN',
'_NET_WM_STATE_FULLSCREEN', '_NET_WM_STATE_MODAL'):
debug('Ignoring %s because it has state %s' % (nm, aname))
return True
d = ewmh.get_wm_desktop(client).reply()
if d == 0xffffffff:
debug('Ignoring %s because it\'s on all desktops' \
'(not implemented)' % nm)
return True
return False
def cb_property_notify(e):
aname = util.get_atom_name(e.atom)
if aname == '_NET_CLIENT_LIST_STACKING':
update_clients()
event.connect('PropertyNotify', xpybutil.root, cb_property_notify)
================================================
FILE: pt3/config.py
================================================
import os
import os.path
import sys
xdg = os.getenv('XDG_CONFIG_HOME') or os.path.join(os.getenv('HOME'), '.config')
conffile = os.path.join(xdg, 'pytyle3', 'config.py')
if not os.access(conffile, os.R_OK):
conffile = os.path.join('/', 'etc', 'xdg', 'pytyle3', 'config.py')
if not os.access(conffile, os.R_OK):
print >> sys.stderr, 'UNRECOVERABLE ERROR: ' \
'No configuration file found at %s' % conffile
sys.exit(1)
execfile(conffile)
================================================
FILE: pt3/debug.py
================================================
import sys
import config
def debug(s):
if not config.debug:
return
print s
sys.stdout.flush()
================================================
FILE: pt3/keybind.py
================================================
import os
import os.path
import sys
# from xpybutil import conn, root
# import xpybutil.event as event
import xpybutil.keybind as keybind
bindings = None
#####################
# Get key bindings
xdg = os.getenv('XDG_CONFIG_HOME') or os.path.join(os.getenv('HOME'), '.config')
conffile = os.path.join(xdg, 'pytyle3', 'keybind.py')
if not os.access(conffile, os.R_OK):
conffile = os.path.join('/', 'etc', 'xdg', 'pytyle3', 'keybind.py')
if not os.access(conffile, os.R_OK):
print >> sys.stderr, 'UNRECOVERABLE ERROR: ' \
'No configuration file found at %s' % conffile
sys.exit(1)
execfile(conffile)
#####################
assert bindings is not None
for key_string, fun in bindings.iteritems():
if not keybind.bind_global_key('KeyPress', key_string, fun):
print >> sys.stderr, 'Could not bind %s' % key_string
================================================
FILE: pt3/layouts/__init__.py
================================================
import abc
import pt3.state as state
class Layout(object):
__metaclass__ = abc.ABCMeta
@abc.abstractmethod
def __init__(self, desk):
self.desk = desk # Should never change
self.active = False
self.tiling = False
@abc.abstractmethod
def add(self, c): pass
@abc.abstractmethod
def remove(self, c): pass
@abc.abstractmethod
def tile(self, save=True):
if not self.active or self.desk not in state.visibles:
return False
if not self.tiling and save:
for c in self.clients():
c.save()
# c.unmaximize()
for c in self.clients(): #unmaximize all windows while tiling
c.unmaximize()
self.tiling = True
return True
@abc.abstractmethod
def untile(self): pass
@abc.abstractmethod
def next_client(self): pass
@abc.abstractmethod
def switch_next_client(self): pass
@abc.abstractmethod
def prev_client(self): pass
@abc.abstractmethod
def switch_prev_client(self): pass
@abc.abstractmethod
def clients(self): pass
def get_workarea(self):
if self.desk not in state.visibles:
return None
mon = state.workarea[state.visibles.index(self.desk)]
return mon
def __str__(self):
wa = self.get_workarea()
if wa is None:
wastr = 'which isn\'t visible'
else:
wx, wy, ww, wh = wa
wastr = '%dx%d+%d+%d' % (ww, wh, wx, wy)
istiling = '- TILING' if self.tiling else ''
return '%s (desk %d) %s%s' % (
self.__class__.__name__, self.desk, wastr, istiling)
from layout_vert_horz import VerticalLayout, HorizontalLayout
layouts = [VerticalLayout, HorizontalLayout]
================================================
FILE: pt3/layouts/layout_vert_horz.py
================================================
import xpybutil
from pt3.debug import debug
import pt3.config as config
import pt3.client as client
import pt3.state as state
from pt3.layouts import Layout
import store
class OrientLayout(Layout):
# Start implementing abstract methods
def __init__(self, desk):
super(OrientLayout, self).__init__(desk)
self.store = store.Store()
self.proportion = 0.5
def add(self, c):
debug('%s being added to %s' % (c, self))
self.store.add(c)
if self.tiling:
self.tile()
def remove(self, c):
debug('%s being removed from %s' % (c, self))
self.store.remove(c)
if self.tiling:
self.tile()
def untile(self):
debug('Untiling %s' % (self))
for c in self.store.masters + self.store.slaves:
c.restore()
self.tiling = False
xpybutil.conn.flush()
def next_client(self):
nxt = self._get_next()
if nxt:
nxt.activate()
def switch_next_client(self):
assert self.tiling
awin = self._get_focused()
nxt = self._get_next()
if None not in (awin, nxt):
self.store.switch(awin, nxt)
self.tile()
def prev_client(self):
prv = self._get_prev()
if prv:
prv.activate()
def switch_prev_client(self):
assert self.tiling
awin = self._get_focused()
prv = self._get_prev()
if None not in (awin, prv):
self.store.switch(awin, prv)
self.tile()
def rotate(self):
assert self.tiling
self.store.slaves.insert(0,self.store.masters.pop(0)) # move the first master to slave
self.store.masters.append(self.store.slaves.pop(-1)) # move the last slave to master
self.tile()
def clients(self):
return self.store.masters + self.store.slaves
# End abstract methods; begin OrientLayout specific methods
def decrease_master(self):
self.proportion = max(0.0, self.proportion - config.proportion_change)
self.tile()
def increase_master(self):
self.proportion = min(1.0, self.proportion + config.proportion_change)
self.tile()
def add_master(self):
assert self.tiling
self.store.inc_masters(self._get_focused())
self.tile()
def remove_master(self):
assert self.tiling
self.store.dec_masters(self._get_focused())
self.tile()
def make_master(self):
assert self.tiling
if not self.store.masters: # no masters right now, so don't add any!
return
awin = self._get_focused()
if awin is None:
return
self.store.switch(self.store.masters[0], awin)
self.tile()
def focus_master(self):
assert self.tiling
if not self.store.masters:
return
self.store.masters[0].activate()
def toggle_float(self):
assert self.tiling
self.store.toggle_float(self._get_focused())
self.tile()
# Begin private methods that should not be called by the user directly
def _get_focused(self):
if type(state.activewin) == list:
state.activewin = state.activewin[0]
if state.activewin not in client.clients:
return None
awin = client.clients[state.activewin]
if awin not in self.store.masters + self.store.slaves + self.store.floats:
return None
return awin
def _get_next(self):
ms, ss = self.store.masters, self.store.slaves
awin = self._get_focused()
if awin is None:
return None
nxt = None
try:
i = ms.index(awin)
if i == 0:
nxt = ss[0] if ss else ms[-1]
else:
nxt = ms[i - 1]
except ValueError:
i = ss.index(awin)
if i == len(ss) - 1:
nxt = ms[-1] if ms else ss[0]
else:
nxt = ss[i + 1]
return nxt
def _get_prev(self):
ms, ss = self.store.masters, self.store.slaves
awin = self._get_focused()
if awin is None:
return None
prv = None
try:
i = ms.index(awin)
if i == len(ms) - 1:
prv = ss[-1] if ss else ms[0]
else:
prv = ms[i + 1]
except ValueError:
i = ss.index(awin)
if i == 0:
prv = ms[0] if ms else ss[-1]
else:
prv = ss[i - 1]
return prv
class VerticalLayout(OrientLayout):
def tile(self, save=True):
if not super(VerticalLayout, self).tile(save):
return
wx, wy, ww, wh = self.get_workarea()
msize = len(self.store.masters)
ssize = len(self.store.slaves)
if not msize and not ssize:
return
mx = wx # left limit
mw = int(ww * self.proportion) # width of the master
sx = mx + mw
sw = ww - mw
g = config.gap # Gap between windows
if mw <= 0 or mw > ww or sw <= 0 or sw > ww:
return
#Masters
if msize:
mh = (wh - (msize + 1) * g) / msize # Height of each window
mw = ww if not ssize else mw
for i, c in enumerate(self.store.masters): # i is the number of the window in the list
c.moveresize(x=mx + g, y= g + wy + i * (mh + g), w=mw - 2 * g, h=mh)
# Slaves
if ssize:
sh = (wh - (ssize + 1) * g)/ ssize # Height of each window
if not msize:
sx, sw = wx, ww
for i, c in enumerate(self.store.slaves):
c.moveresize(x=sx, y= g + wy + i * (sh + g), w=sw - g, h=sh)
xpybutil.conn.flush()
class HorizontalLayout(OrientLayout):
def tile(self, save=True):
if not super(HorizontalLayout, self).tile(save):
return
wx, wy, ww, wh = self.get_workarea()
msize = len(self.store.masters)
ssize = len(self.store.slaves)
if not msize and not ssize:
return
my = wy
mh = int(wh * self.proportion)
sy = my + mh
sh = wh - mh
g = config.gap # Gap between windows
if mh <= 0 or mh > wh or sh <= 0 or sh > wh:
return
# Masters
if msize:
#mw = ww / msize
mw = (ww - (msize + 1) * g) / msize # Height of each window
mh = wh if not ssize else mh
for i, c in enumerate(self.store.masters):
c.moveresize(x=g + wx + i * (g + mw), y=my + g, w=mw, h=mh - 2 * g)
#Slaves
if ssize:
#sw = ww / ssize
sw = (ww - (ssize + 1) * g) / ssize # Height of each window
if not msize:
sy, sh = wy, wh
for i, c in enumerate(self.store.slaves):
c.moveresize(x= g + wx + i * (g + sw), y=sy, w=sw, h=sh - g)
xpybutil.conn.flush()
================================================
FILE: pt3/layouts/store.py
================================================
import pt3.config as config
import xpybutil.ewmh as ewmh
import xpybutil.util as util
import xpybutil.motif as motif
class Store(object):
def __init__(self):
self.masters, self.slaves, self.floats = [], [], []
self.mcnt = 1 # Number of masters allowed
def add(self, c, above=None):
if c.floating:
if getattr(config, 'remove_decorations', False):
motif.set_hints_checked(c.wid,2,decoration=1).check() # add decorations
if getattr(config, 'tiles_below', False):
ewmh.request_wm_state_checked(c.wid,0,util.get_atom('_NET_WM_STATE_BELOW')).check()
self.floats.append(c)
else:
#restore window if maximized
#ewmh.request_wm_state_checked(c.wid,0,util.get_atom('_NET_WM_STATE_MAXIMIZED_VERT')).check()
#ewmh.request_wm_state_checked(c.wid,0,util.get_atom('_NET_WM_STATE_MAXIMIZED_HORZ')).check()
if getattr(config, 'remove_decorations', False):
motif.set_hints_checked(c.wid,2,decoration=2).check() #remove decorations
if getattr(config, 'tiles_below', False):
ewmh.request_wm_state_checked(c.wid,1,util.get_atom('_NET_WM_STATE_BELOW')).check()
if len(self.masters) < self.mcnt:
if c in self.slaves:
self.slaves.remove(c)
self.masters.append(c)
elif c not in self.slaves:
self.slaves.append(c)
def remove(self, c):
if c in self.floats:
self.floats.remove(c)
else:
if c in self.masters:
self.masters.remove(c)
if len(self.masters) < self.mcnt and self.slaves:
self.masters.append(self.slaves.pop(0))
elif c in self.slaves:
self.slaves.remove(c)
def reset(self):
self.__init__()
def inc_masters(self, current=None):
self.mcnt = min(self.mcnt + 1, len(self))
if len(self.masters) < self.mcnt and self.slaves:
try:
newmast = self.slaves.index(current)
except ValueError:
newmast = 0
self.masters.append(self.slaves.pop(newmast))
def dec_masters(self, current=None):
self.mcnt = max(self.mcnt - 1, 0)
if len(self.masters) > self.mcnt:
try:
newslav = self.masters.index(current)
except ValueError:
newslav = -1
self.slaves.append(self.masters.pop(newslav))
def switch(self, c1, c2):
ms, ss = self.masters, self.slaves # alias
if c1 in ms and c2 in ms:
i1, i2 = ms.index(c1), ms.index(c2)
ms[i1], ms[i2] = ms[i2], ms[i1]
elif c1 in self.slaves and c2 in self.slaves:
i1, i2 = ss.index(c1), ss.index(c2)
ss[i1], ss[i2] = ss[i2], ss[i1]
elif c1 in ms: # and c2 in self.slaves
i1, i2 = ms.index(c1), ss.index(c2)
ms[i1], ss[i2] = ss[i2], ms[i1]
else: # c1 in ss and c2 in ms
i1, i2 = ss.index(c1), ms.index(c2)
ss[i1], ms[i2] = ms[i2], ss[i1]
def toggle_float(self, c):
self.remove(c)
c.floating = not c.floating
self.add(c)
def __len__(self):
return len(self.masters) + len(self.slaves)
def __str__(self):
s = ['Masters: %s' % [str(c) for c in self.masters],
'Slaves: %s' % [str(c) for c in self.slaves]]
return '\n'.join(s)
================================================
FILE: pt3/state.py
================================================
import sys
import time
import xpybutil
import xpybutil.event as event
import xpybutil.ewmh as ewmh
import xpybutil.rect as rect
import xpybutil.util as util
import xpybutil.window as window
import xpybutil.xinerama as xinerama
import config
PYTYLE_STATE = 'startup'
GRAB = None
_wmrunning = False
wm = 'N/A'
utilwm = window.WindowManagers.Unknown
while not _wmrunning:
w = ewmh.get_supporting_wm_check(xpybutil.root).reply()
if w:
childw = ewmh.get_supporting_wm_check(w).reply()
if childw == w:
_wmrunning = True
wm = ewmh.get_wm_name(childw).reply()
if wm.lower() == 'openbox':
utilwm = window.WindowManagers.Openbox
elif wm.lower() == 'kwin':
utilwm = window.WindowManagers.KWin
print '%s window manager is running...' % wm
sys.stdout.flush()
if not _wmrunning:
time.sleep(1)
root_geom = ewmh.get_desktop_geometry().reply()
monitors = xinerama.get_monitors()
phys_monitors = xinerama.get_physical_mapping(monitors)
desk_num = ewmh.get_number_of_desktops().reply()
activewin = ewmh.get_active_window().reply()
desktop = ewmh.get_current_desktop().reply()
visibles = ewmh.get_visible_desktops().reply() or [desktop]
stacking = ewmh.get_client_list_stacking().reply()
workarea = []
def quit():
print 'Exiting...'
import tile
for tiler in tile.tilers:
tile.get_active_tiler(tiler)[0].untile()
sys.exit(0)
def update_workarea():
'''
We update the current workarea either by autodetecting struts, or by
using margins specified in the config file. Never both, though.
'''
global workarea
if hasattr(config, 'use_margins') and config.use_margins:
workarea = monitors[:]
for physm, margins in enumerate(config.margins):
if physm == len(phys_monitors):
break
i = phys_monitors[physm]
mx, my, mw, mh = workarea[i]
workarea[i] = (mx + margins['left'], my + margins['top'],
mw - (margins['left'] + margins['right']),
mh - (margins['top'] + margins['bottom']))
else:
workarea = rect.monitor_rects(monitors)
def cb_property_notify(e):
global activewin, desk_num, desktop, monitors, phys_monitors, root_geom, \
stacking, visibles, workarea
aname = util.get_atom_name(e.atom)
if aname == '_NET_DESKTOP_GEOMETRY':
root_geom = ewmh.get_desktop_geometry().reply()
monitors = xinerama.get_monitors()
phys_monitors = xinerama.get_physical_mapping(monitors)
elif aname == '_NET_ACTIVE_WINDOW':
activewin = ewmh.get_active_window().reply()
elif aname == '_NET_CURRENT_DESKTOP':
desktop = ewmh.get_current_desktop().reply()
if visibles is None or len(visibles) == 1:
visibles = [desktop]
elif aname == '_NET_VISIBLE_DESKTOPS':
visibles = ewmh.get_visible_desktops().reply()
elif aname == '_NET_NUMBER_OF_DESKTOPS':
desk_num = ewmh.get_number_of_desktops().reply()
elif aname == '_NET_CLIENT_LIST_STACKING':
stacking = ewmh.get_client_list_stacking().reply()
elif aname == '_NET_WORKAREA':
update_workarea()
window.listen(xpybutil.root, 'PropertyChange')
event.connect('PropertyNotify', xpybutil.root, cb_property_notify)
update_workarea()
================================================
FILE: pt3/tile.py
================================================
import xpybutil
import xpybutil.event as event
import xpybutil.util as util
from debug import debug
import state
from layouts import layouts
try:
from config import tile_on_startup
except ImportError:
tile_on_startup = False
tilers = {}
def debug_state():
debug('-' * 45)
for d in tilers:
if d not in state.visibles:
continue
tiler, _ = get_active_tiler(d)
debug(tiler)
debug(tiler.store)
debug('-' * 45)
def cmd(action):
def _cmd():
if state.desktop not in tilers:
return
tiler, _ = get_active_tiler(state.desktop)
if action == 'tile':
tiler.tile()
elif tiler.tiling:
if action == 'cycle':
cycle_current_tiler()
else:
getattr(tiler, action)()
return _cmd
def cycle_current_tiler():
assert state.desktop in tilers
tiler, i = get_active_tiler(state.desktop)
newtiler = tilers[state.desktop][(i + 1) % len(tilers[state.desktop])]
tiler.active = False
tiler.tiling = False
newtiler.active = True
debug('Switching tiler from %s to %s on desktop %d' % (
tiler.__class__.__name__, newtiler.__class__.__name__,
state.desktop))
newtiler.tile(save=False)
def get_active_tiler(desk):
assert desk in tilers
for i, tiler in enumerate(tilers[desk]):
if tiler.active:
return tiler, i
def update_client_moved(c):
assert c.desk in tilers
tiler, _ = get_active_tiler(c.desk)
if tiler.tiling:
tiler.tile()
def update_client_desktop(c, olddesk):
assert c.desk in tilers
if olddesk in tilers:
for tiler in tilers[olddesk]:
tiler.remove(c)
for tiler in tilers[c.desk]:
tiler.add(c)
def update_client_add(c):
assert c.desk in tilers
for tiler in tilers[c.desk]:
tiler.add(c)
def update_client_removal(c):
assert c.desk in tilers
for tiler in tilers[c.desk]:
tiler.remove(c)
def update_tilers():
for d in xrange(state.desk_num):
if d not in tilers:
debug('Adding tilers to desktop %d' % d)
tilers[d] = []
for lay in layouts:
t = lay(d)
tilers[d].append(t)
tilers[d][0].active = True
if tile_on_startup:
tilers[d][0].tiling = True
tilers[d][0].tile()
for d in tilers.keys():
if d >= state.desk_num:
debug('Removing tilers from desktop %d' % d)
del tilers[d]
def cb_property_notify(e):
aname = util.get_atom_name(e.atom)
if aname == '_NET_NUMBER_OF_DESKTOPS':
update_tilers()
elif aname == '_NET_CURRENT_DESKTOP':
if len(state.visibles) == 1:
tiler, _ = get_active_tiler(state.desktop)
if tiler.tiling:
tiler.tile()
elif aname == '_NET_VISIBLE_DESKTOPS':
for d in state.visibles:
tiler, _ = get_active_tiler(d)
if tiler.tiling:
tiler.tile()
event.connect('PropertyNotify', xpybutil.root, cb_property_notify)
================================================
FILE: pytyle3
================================================
#!/usr/bin/env python2
import sys
import xpybutil.event as event
import pt3.config as config
# Perhaps we want to debug...
if '--debug' in sys.argv:
config.debug = True
import pt3.keybind
import pt3.state as state
import pt3.tile as tile
import pt3.client as client
tile.update_tilers()
client.update_clients()
# Get those juices flowing...
state.PYTYLE_STATE = 'running'
# Start the event loop
event.main()
================================================
FILE: setup.py
================================================
import os
import os.path
import sys
from distutils import sysconfig
from distutils.core import setup
try:
import xpybutil
except:
print ''
print 'pytyle3 requires xpybutil'
print 'See: https://github.com/BurntSushi/xpybutil'
sys.exit(1)
setup(
name = 'pytyle3',
author = 'Andrew Gallant',
author_email = 'andrew@pytyle.com',
version = '3.0.0',
license = 'WTFPL',
description = 'A new and much more lightweight pytyle that supports Openbox Multihead',
long_description = 'See README',
url = 'https://github.com/BurntSushi/pytyle3',
platforms = 'POSIX',
packages = ['pt3', 'pt3/layouts'],
data_files = [('share/doc/pytyle3', ['README', 'COPYING', 'INSTALL']),
('/etc/xdg/pytyle3',
['config.py', 'keybind.py'])],
scripts = ['pytyle3']
)
gitextract__fk6cinx/ ├── COPYING ├── INSTALL ├── README ├── config.py ├── keybind.py ├── pt3/ │ ├── __init__.py │ ├── client.py │ ├── config.py │ ├── debug.py │ ├── keybind.py │ ├── layouts/ │ │ ├── __init__.py │ │ ├── layout_vert_horz.py │ │ └── store.py │ ├── state.py │ └── tile.py ├── pytyle3 └── setup.py
SYMBOL INDEX (82 symbols across 7 files)
FILE: pt3/client.py
class Client (line 23) | class Client(object):
method __init__ (line 24) | def __init__(self, wid):
method remove (line 57) | def remove(self):
method activate (line 65) | def activate(self):
method unmaximize (line 68) | def unmaximize(self):
method save (line 73) | def save(self):
method restore (line 77) | def restore(self):
method moveresize (line 122) | def moveresize(self, x=None, y=None, w=None, h=None):
method is_button_pressed (line 133) | def is_button_pressed(self):
method cb_focus_in (line 147) | def cb_focus_in(self, e):
method cb_focus_out (line 153) | def cb_focus_out(self, e):
method cb_configure_notify (line 157) | def cb_configure_notify(self, e):
method cb_property_notify (line 161) | def cb_property_notify(self, e):
method __str__ (line 184) | def __str__(self):
function update_clients (line 187) | def update_clients():
function track_client (line 197) | def track_client(client):
function untrack_client (line 214) | def untrack_client(client):
function should_ignore (line 222) | def should_ignore(client):
function cb_property_notify (line 299) | def cb_property_notify(e):
FILE: pt3/debug.py
function debug (line 5) | def debug(s):
FILE: pt3/layouts/__init__.py
class Layout (line 5) | class Layout(object):
method __init__ (line 9) | def __init__(self, desk):
method add (line 15) | def add(self, c): pass
method remove (line 18) | def remove(self, c): pass
method tile (line 21) | def tile(self, save=True):
method untile (line 37) | def untile(self): pass
method next_client (line 40) | def next_client(self): pass
method switch_next_client (line 43) | def switch_next_client(self): pass
method prev_client (line 46) | def prev_client(self): pass
method switch_prev_client (line 49) | def switch_prev_client(self): pass
method clients (line 52) | def clients(self): pass
method get_workarea (line 54) | def get_workarea(self):
method __str__ (line 62) | def __str__(self):
FILE: pt3/layouts/layout_vert_horz.py
class OrientLayout (line 12) | class OrientLayout(Layout):
method __init__ (line 14) | def __init__(self, desk):
method add (line 19) | def add(self, c):
method remove (line 26) | def remove(self, c):
method untile (line 33) | def untile(self):
method next_client (line 41) | def next_client(self):
method switch_next_client (line 46) | def switch_next_client(self):
method prev_client (line 55) | def prev_client(self):
method switch_prev_client (line 60) | def switch_prev_client(self):
method rotate (line 69) | def rotate(self):
method clients (line 76) | def clients(self):
method decrease_master (line 81) | def decrease_master(self):
method increase_master (line 85) | def increase_master(self):
method add_master (line 89) | def add_master(self):
method remove_master (line 95) | def remove_master(self):
method make_master (line 101) | def make_master(self):
method focus_master (line 114) | def focus_master(self):
method toggle_float (line 122) | def toggle_float(self):
method _get_focused (line 130) | def _get_focused(self):
method _get_next (line 144) | def _get_next(self):
method _get_prev (line 166) | def _get_prev(self):
class VerticalLayout (line 188) | class VerticalLayout(OrientLayout):
method tile (line 189) | def tile(self, save=True):
class HorizontalLayout (line 226) | class HorizontalLayout(OrientLayout):
method tile (line 227) | def tile(self, save=True):
FILE: pt3/layouts/store.py
class Store (line 6) | class Store(object):
method __init__ (line 7) | def __init__(self):
method add (line 11) | def add(self, c, above=None):
method remove (line 33) | def remove(self, c):
method reset (line 44) | def reset(self):
method inc_masters (line 47) | def inc_masters(self, current=None):
method dec_masters (line 56) | def dec_masters(self, current=None):
method switch (line 65) | def switch(self, c1, c2):
method toggle_float (line 80) | def toggle_float(self, c):
method __len__ (line 85) | def __len__(self):
method __str__ (line 88) | def __str__(self):
FILE: pt3/state.py
function quit (line 49) | def quit():
function update_workarea (line 56) | def update_workarea():
function cb_property_notify (line 76) | def cb_property_notify(e):
FILE: pt3/tile.py
function debug_state (line 17) | def debug_state():
function cmd (line 27) | def cmd(action):
function cycle_current_tiler (line 44) | def cycle_current_tiler():
function get_active_tiler (line 60) | def get_active_tiler(desk):
function update_client_moved (line 67) | def update_client_moved(c):
function update_client_desktop (line 74) | def update_client_desktop(c, olddesk):
function update_client_add (line 84) | def update_client_add(c):
function update_client_removal (line 90) | def update_client_removal(c):
function update_tilers (line 96) | def update_tilers():
function cb_property_notify (line 113) | def cb_property_notify(e):
Condensed preview — 17 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (38K chars).
[
{
"path": "COPYING",
"chars": 483,
"preview": " DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE\n Version 2, December 2004\n\n Copyright (C) 200"
},
{
"path": "INSTALL",
"chars": 339,
"preview": "It should be as straight-forward as\n\n sudo python2 setup.py install\n\nTwo configuration files, config.py and keybind.py "
},
{
"path": "README",
"chars": 551,
"preview": "An updated (and much faster) version of pytyle that uses xpybutil and is\ncompatible with Openbox Multihead.\n\nDue to usin"
},
{
"path": "config.py",
"chars": 1466,
"preview": "# A list of windows to ignore... \n# will search both the class and role of the WM_CLASS property\n# case-insensitive\nigno"
},
{
"path": "keybind.py",
"chars": 1421,
"preview": "# This is a python script. Pay attention to the syntax and indentation\nimport state\nimport tile\n\nbindings = {\n# You can "
},
{
"path": "pt3/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "pt3/client.py",
"chars": 10453,
"preview": "import time\n\nimport xcb.xproto\n\nimport xpybutil\nimport xpybutil.event as event\nimport xpybutil.ewmh as ewmh\nimport xpybu"
},
{
"path": "pt3/config.py",
"chars": 493,
"preview": "import os\nimport os.path\nimport sys\n\nxdg = os.getenv('XDG_CONFIG_HOME') or os.path.join(os.getenv('HOME'), '.config')\nco"
},
{
"path": "pt3/debug.py",
"chars": 117,
"preview": "import sys\n\nimport config\n\ndef debug(s):\n if not config.debug:\n return\n print s\n sys.stdout.flush()\n\n"
},
{
"path": "pt3/keybind.py",
"chars": 882,
"preview": "import os\nimport os.path\nimport sys\n\n# from xpybutil import conn, root \n# import xpybutil.event as event \nimport xpybuti"
},
{
"path": "pt3/layouts/__init__.py",
"chars": 1806,
"preview": "import abc\n\nimport pt3.state as state\n\nclass Layout(object):\n __metaclass__ = abc.ABCMeta\n\n @abc.abstractmethod\n "
},
{
"path": "pt3/layouts/layout_vert_horz.py",
"chars": 7052,
"preview": "import xpybutil\n\nfrom pt3.debug import debug\n\nimport pt3.config as config\nimport pt3.client as client\nimport pt3.state a"
},
{
"path": "pt3/layouts/store.py",
"chars": 3519,
"preview": "import pt3.config as config\nimport xpybutil.ewmh as ewmh\nimport xpybutil.util as util\nimport xpybutil.motif as motif\n\ncl"
},
{
"path": "pt3/state.py",
"chars": 3406,
"preview": "import sys\nimport time\n\nimport xpybutil\nimport xpybutil.event as event\nimport xpybutil.ewmh as ewmh\nimport xpybutil.rect"
},
{
"path": "pt3/tile.py",
"chars": 3172,
"preview": "import xpybutil\nimport xpybutil.event as event\nimport xpybutil.util as util\n\nfrom debug import debug\n\nimport state\nfrom "
},
{
"path": "pytyle3",
"chars": 421,
"preview": "#!/usr/bin/env python2\n\nimport sys\n\nimport xpybutil.event as event\n\nimport pt3.config as config\n\n# Perhaps we want to de"
},
{
"path": "setup.py",
"chars": 844,
"preview": "import os\nimport os.path\nimport sys\n\nfrom distutils import sysconfig\nfrom distutils.core import setup\n\ntry:\n import x"
}
]
About this extraction
This page contains the full source code of the BurntSushi/pytyle3 GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 17 files (35.6 KB), approximately 9.6k tokens, and a symbol index with 82 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.