├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.rst ├── aur ├── orcsome-git │ └── PKGBUILD └── orcsome │ └── PKGBUILD ├── bin └── orcsome ├── docs ├── Makefile ├── _templates │ └── layout.html ├── actions.rst ├── conf.py ├── core.rst ├── faq.rst ├── guide.rst └── index.rst ├── orcsome ├── __init__.py ├── __main__.py ├── actions.py ├── aliases.py ├── ev.py ├── ev_build.py ├── notify.py ├── run.py ├── testwm.py ├── utils.py ├── wm.py ├── wrappers.py ├── xlib.py └── xlib_build.py ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.so 3 | *.c 4 | *.o 5 | __pycache__ 6 | /build 7 | /dist 8 | /*.egg-info 9 | /.eggs 10 | /docs/_build 11 | /aur 12 | 13 | /.snaked_project 14 | /dev-run 15 | /test.py 16 | /testrc.py 17 | /testev.py 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2010 Anton Bobrov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE 3 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Orcsome is a scripting extension for NETWM compliant window managers. It can 2 | help a lot to customize your work environment. 3 | 4 | 5 | Motivation 6 | ---------- 7 | 8 | I'm old `awesome`_ user with two year experience. I like it not for tiling but 9 | for lua and ability to tune its behavior. But for a very long time some problems 10 | stay unsolved: 11 | 12 | * Grey swing windows. I was hoping it will be fixed in java7 but no luck. 13 | * Input focus for swing windows. Awesome treats such windows as inputless. 14 | * Random focus problems. For example sometimes evince or opera save dialog are 15 | not take focus. 16 | 17 | Simply put, awesome sucks as window manager. 18 | 19 | I need a robust wm with long devel history, small, fast, candy and 20 | **scriptable** on normal language (hello fvwm). But there are a plenty of 21 | robust, small, fast and candy only wm's. There is no any scriptable. 22 | 23 | So I decide to write tiny wm helper application which will be compatible with 24 | many window managers and allow to configure flexible workflows. 25 | 26 | .. _awesome: http://awesome.naquadah.org/ 27 | 28 | Features 29 | -------- 30 | 31 | * Written on python. It means very hackable. 32 | 33 | * Optimization, cpu and memory efficiency are top goals (cffi is used for xlib 34 | bindings). 35 | 36 | * Extensive use of python syntax to provide easy and expressive eDSL in 37 | configuration script. 38 | 39 | * Supports NETWM standards. 40 | 41 | * Very thin wrapper around X. You can use existing xlib background. 42 | 43 | 44 | Installation 45 | ------------ 46 | 47 | From PyPI 48 | ''''''''' 49 | 50 | I'm regularly upload packages of new versions. So you can install orcsome with 51 | ``easy_install``:: 52 | 53 | sudo easy_install orcsome 54 | 55 | or `pip`_:: 56 | 57 | sudo pip install orcsome 58 | 59 | 60 | From source 61 | ''''''''''' 62 | 63 | :: 64 | 65 | git clone --depth=1 git://github.com/baverman/orcsome.git 66 | cd orcsome 67 | python setup.py build 68 | sudo python setup.py install 69 | 70 | If you often pull changes from master brunch I recommend you following recipe: 71 | 72 | * First install orcsome in develop mode (remove any orcsome dirs in site-packages 73 | before that):: 74 | 75 | sudo python setup.py develop 76 | 77 | * Then, if you want use latest version from master branch simply do:: 78 | 79 | cd cloned/orcsome/dir 80 | git pull 81 | 82 | 83 | ArchLinux 84 | ''''''''' 85 | 86 | There is orcsome package in AUR. 87 | 88 | .. _pip: http://pip.openplans.org/ 89 | 90 | 91 | `Documentation `_ 92 | --------------------------------------------------- 93 | 94 | Quick start 95 | ''''''''''' 96 | 97 | The most common functionality needed is to bind hot keys to spawn or raise 98 | applications. 99 | 100 | Edit ``~/.config/orcsome/rc.py``:: 101 | 102 | from orcsome import get_wm 103 | from orcsome.actions import * 104 | 105 | wm = get_wm() 106 | 107 | wm.on_key('Shift+Mod+r')( 108 | restart) 109 | 110 | wm.on_key('Ctrl+Alt+p')( 111 | spawn_or_raise('urxvtc -name ncmpcpp -e ncmpcpp', name='ncmpcpp')) 112 | 113 | wm.on_key('Mod+n')( 114 | spawn_or_raise('urxvtc -name mutt -e mutt', name='mutt')) 115 | 116 | wm.on_key('Mod+k')( 117 | spawn_or_raise('urxvtc -name rtorrent -e rtorrent-screen', name='rtorrent')) 118 | 119 | And start orcsome. That's all. 120 | 121 | 122 | TODO 123 | ---- 124 | 125 | * Tests 126 | * Python3 port 127 | * API to configure window geometry 128 | * Layouts (tiling) 129 | * Multiple screens 130 | 131 | 132 | Contacts 133 | -------- 134 | 135 | You can create issues on `github `_. 136 | 137 | Or mail directly to bobrov at vl dot ru. 138 | -------------------------------------------------------------------------------- /aur/orcsome-git/PKGBUILD: -------------------------------------------------------------------------------- 1 | #Maintainer: Anton Bobrov 2 | 3 | pkgname=orcsome-git 4 | pkgver=20110816 5 | pkgrel=1 6 | pkgdesc="A scripting extension for NETWM compliant window managers" 7 | arch=(any) 8 | url="https://github.com/baverman/orcsome" 9 | license=('MIT') 10 | groups=() 11 | depends=('python2' 'python-xlib') 12 | makedepends=('python2-distribute') 13 | 14 | _gitroot="git://github.com/baverman/orcsome.git" 15 | _gitname="orcsome" 16 | 17 | build() { 18 | cd $srcdir 19 | 20 | msg "Connecting to GIT server..." 21 | if [[ -d ${_gitname} ]]; then 22 | (cd ${_gitname} && git pull origin) 23 | else 24 | git clone --depth=1 ${_gitroot} ${_gitname} 25 | fi 26 | 27 | msg "GIT checkout done or server timeout" 28 | msg "Starting make..." 29 | 30 | rm -rf ${_gitname}-build 31 | cp -r ${_gitname} ${_gitname}-build 32 | 33 | cd ${srcdir}/${_gitname}-build 34 | 35 | python2 setup.py build 36 | } 37 | 38 | package() { 39 | cd ${srcdir}/${_gitname}-build 40 | 41 | python2 setup.py install --root=$pkgdir/ --optimize=1 42 | } 43 | -------------------------------------------------------------------------------- /aur/orcsome/PKGBUILD: -------------------------------------------------------------------------------- 1 | #Maintainer: Anton Bobrov 2 | 3 | pkgname=orcsome 4 | pkgver=0.6 5 | pkgrel=1 6 | pkgdesc="A scripting extension for NETWM compliant window managers" 7 | arch=(any) 8 | url="https://github.com/baverman/orcsome" 9 | license=('MIT') 10 | groups=() 11 | depends=('python2') 12 | makedepends=('python2-setuptools' 'python2-cffi' 'libev' 'libx11' 'libxss' 'libxext' 'kbproto') 13 | source=(https://github.com/baverman/orcsome/archive/$pkgver.tar.gz) 14 | md5sums=('7f36cd3aad2f41691c689aee08ccb775') 15 | 16 | build() { 17 | cd "$srcdir/$pkgname-$pkgver" 18 | python2 setup.py build 19 | } 20 | 21 | package() { 22 | cd "$srcdir/$pkgname-$pkgver" 23 | 24 | python2 setup.py install --root=$pkgdir/ --optimize=1 25 | } 26 | -------------------------------------------------------------------------------- /bin/orcsome: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from orcsome.run import run 4 | run() 5 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " singlehtml to make a single large HTML file" 22 | @echo " pickle to make pickle files" 23 | @echo " json to make JSON files" 24 | @echo " htmlhelp to make HTML files and a HTML help project" 25 | @echo " qthelp to make HTML files and a qthelp project" 26 | @echo " devhelp to make HTML files and a Devhelp project" 27 | @echo " epub to make an epub" 28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 29 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 30 | @echo " text to make text files" 31 | @echo " man to make manual pages" 32 | @echo " changes to make an overview of all changed/added/deprecated items" 33 | @echo " linkcheck to check all external links for integrity" 34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 35 | 36 | clean: 37 | -rm -rf $(BUILDDIR)/* 38 | 39 | html: 40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 41 | @echo 42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 43 | 44 | dirhtml: 45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 48 | 49 | singlehtml: 50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 51 | @echo 52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 53 | 54 | pickle: 55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 56 | @echo 57 | @echo "Build finished; now you can process the pickle files." 58 | 59 | json: 60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 61 | @echo 62 | @echo "Build finished; now you can process the JSON files." 63 | 64 | htmlhelp: 65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 66 | @echo 67 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 68 | ".hhp project file in $(BUILDDIR)/htmlhelp." 69 | 70 | qthelp: 71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 72 | @echo 73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Orcsome.qhcp" 76 | @echo "To view the help file:" 77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Orcsome.qhc" 78 | 79 | devhelp: 80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 81 | @echo 82 | @echo "Build finished." 83 | @echo "To view the help file:" 84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Orcsome" 85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Orcsome" 86 | @echo "# devhelp" 87 | 88 | epub: 89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 90 | @echo 91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 92 | 93 | latex: 94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 95 | @echo 96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 98 | "(use \`make latexpdf' here to do that automatically)." 99 | 100 | latexpdf: 101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 102 | @echo "Running LaTeX files through pdflatex..." 103 | make -C $(BUILDDIR)/latex all-pdf 104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 105 | 106 | text: 107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 108 | @echo 109 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 110 | 111 | man: 112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 113 | @echo 114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 115 | 116 | changes: 117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 118 | @echo 119 | @echo "The overview file is in $(BUILDDIR)/changes." 120 | 121 | linkcheck: 122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 123 | @echo 124 | @echo "Link check complete; look for any errors in the above output " \ 125 | "or in $(BUILDDIR)/linkcheck/output.txt." 126 | 127 | doctest: 128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 129 | @echo "Testing of doctests in the sources finished, look at the " \ 130 | "results in $(BUILDDIR)/doctest/output.txt." 131 | -------------------------------------------------------------------------------- /docs/_templates/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "!layout.html" %} 2 | {% block extrahead %} 3 | {{ super }} 4 | 15 | {% endblock %} -------------------------------------------------------------------------------- /docs/actions.rst: -------------------------------------------------------------------------------- 1 | Actions 2 | ======= 3 | 4 | .. automodule:: orcsome.actions 5 | :members: -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Orcsome documentation build configuration file, created by 4 | # sphinx-quickstart on Sun Aug 7 21:43:08 2011. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | sys.path.insert(0, os.path.abspath('..')) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # If your documentation needs a minimal Sphinx version, state it here. 24 | #needs_sphinx = '1.0' 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be extensions 27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | extensions = ['sphinx.ext.autodoc'] 29 | 30 | # Add any paths that contain templates here, relative to this directory. 31 | templates_path = ['_templates'] 32 | 33 | # The suffix of source filenames. 34 | source_suffix = '.rst' 35 | 36 | # The encoding of source files. 37 | #source_encoding = 'utf-8-sig' 38 | 39 | # The master toctree document. 40 | master_doc = 'index' 41 | 42 | # General information about the project. 43 | project = u'Orcsome' 44 | copyright = u'2011, Anton Bobrov' 45 | 46 | # The version info for the project you're documenting, acts as replacement for 47 | # |version| and |release|, also used in various other places throughout the 48 | # built documents. 49 | # 50 | # The short X.Y version. 51 | import orcsome 52 | version = orcsome.VERSION.replace('dev', '') 53 | # The full version, including alpha/beta/rc tags. 54 | release = orcsome.VERSION 55 | 56 | # The language for content autogenerated by Sphinx. Refer to documentation 57 | # for a list of supported languages. 58 | #language = None 59 | 60 | # There are two options for replacing |today|: either, you set today to some 61 | # non-false value, then it is used: 62 | #today = '' 63 | # Else, today_fmt is used as the format for a strftime call. 64 | #today_fmt = '%B %d, %Y' 65 | 66 | # List of patterns, relative to source directory, that match files and 67 | # directories to ignore when looking for source files. 68 | exclude_patterns = ['_build'] 69 | 70 | # The reST default role (used for this markup: `text`) to use for all documents. 71 | #default_role = None 72 | 73 | # If true, '()' will be appended to :func: etc. cross-reference text. 74 | #add_function_parentheses = True 75 | 76 | # If true, the current module name will be prepended to all description 77 | # unit titles (such as .. function::). 78 | #add_module_names = True 79 | 80 | # If true, sectionauthor and moduleauthor directives will be shown in the 81 | # output. They are ignored by default. 82 | #show_authors = False 83 | 84 | # The name of the Pygments (syntax highlighting) style to use. 85 | pygments_style = 'sphinx' 86 | 87 | # A list of ignored prefixes for module index sorting. 88 | #modindex_common_prefix = [] 89 | 90 | 91 | # -- Options for HTML output --------------------------------------------------- 92 | 93 | # The theme to use for HTML and HTML Help pages. See the documentation for 94 | # a list of builtin themes. 95 | html_theme = 'default' 96 | 97 | # Theme options are theme-specific and customize the look and feel of a theme 98 | # further. For a list of options available for each theme, see the 99 | # documentation. 100 | #html_theme_options = {} 101 | 102 | # Add any paths that contain custom themes here, relative to this directory. 103 | #html_theme_path = [] 104 | 105 | # The name for this set of Sphinx documents. If None, it defaults to 106 | # " v documentation". 107 | #html_title = None 108 | 109 | # A shorter title for the navigation bar. Default is the same as html_title. 110 | #html_short_title = None 111 | 112 | # The name of an image file (relative to this directory) to place at the top 113 | # of the sidebar. 114 | #html_logo = None 115 | 116 | # The name of an image file (within the static path) to use as favicon of the 117 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 118 | # pixels large. 119 | #html_favicon = None 120 | 121 | # Add any paths that contain custom static files (such as style sheets) here, 122 | # relative to this directory. They are copied after the builtin static files, 123 | # so a file named "default.css" will overwrite the builtin "default.css". 124 | html_static_path = ['_static'] 125 | 126 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 127 | # using the given strftime format. 128 | #html_last_updated_fmt = '%b %d, %Y' 129 | 130 | # If true, SmartyPants will be used to convert quotes and dashes to 131 | # typographically correct entities. 132 | #html_use_smartypants = True 133 | 134 | # Custom sidebar templates, maps document names to template names. 135 | #html_sidebars = {} 136 | 137 | # Additional templates that should be rendered to pages, maps page names to 138 | # template names. 139 | #html_additional_pages = {} 140 | 141 | # If false, no module index is generated. 142 | #html_domain_indices = True 143 | 144 | # If false, no index is generated. 145 | #html_use_index = True 146 | 147 | # If true, the index is split into individual pages for each letter. 148 | #html_split_index = False 149 | 150 | # If true, links to the reST sources are added to the pages. 151 | #html_show_sourcelink = True 152 | 153 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 154 | #html_show_sphinx = True 155 | 156 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 157 | #html_show_copyright = True 158 | 159 | # If true, an OpenSearch description file will be output, and all pages will 160 | # contain a tag referring to it. The value of this option must be the 161 | # base URL from which the finished HTML is served. 162 | #html_use_opensearch = '' 163 | 164 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 165 | #html_file_suffix = None 166 | 167 | # Output file base name for HTML help builder. 168 | htmlhelp_basename = 'Orcsomedoc' 169 | 170 | 171 | # -- Options for LaTeX output -------------------------------------------------- 172 | 173 | # The paper size ('letter' or 'a4'). 174 | #latex_paper_size = 'letter' 175 | 176 | # The font size ('10pt', '11pt' or '12pt'). 177 | #latex_font_size = '10pt' 178 | 179 | # Grouping the document tree into LaTeX files. List of tuples 180 | # (source start file, target name, title, author, documentclass [howto/manual]). 181 | latex_documents = [ 182 | ('index', 'Orcsome.tex', u'Orcsome Documentation', 183 | u'Anton Bobrov', 'manual'), 184 | ] 185 | 186 | # The name of an image file (relative to this directory) to place at the top of 187 | # the title page. 188 | #latex_logo = None 189 | 190 | # For "manual" documents, if this is true, then toplevel headings are parts, 191 | # not chapters. 192 | #latex_use_parts = False 193 | 194 | # If true, show page references after internal links. 195 | #latex_show_pagerefs = False 196 | 197 | # If true, show URL addresses after external links. 198 | #latex_show_urls = False 199 | 200 | # Additional stuff for the LaTeX preamble. 201 | #latex_preamble = '' 202 | 203 | # Documents to append as an appendix to all manuals. 204 | #latex_appendices = [] 205 | 206 | # If false, no module index is generated. 207 | #latex_domain_indices = True 208 | 209 | 210 | # -- Options for manual page output -------------------------------------------- 211 | 212 | # One entry per manual page. List of tuples 213 | # (source start file, name, description, authors, manual section). 214 | man_pages = [ 215 | ('index', 'orcsome', u'Orcsome Documentation', 216 | [u'Anton Bobrov'], 1) 217 | ] 218 | -------------------------------------------------------------------------------- /docs/core.rst: -------------------------------------------------------------------------------- 1 | Orcsome instance api 2 | ==================== 3 | 4 | .. automodule:: orcsome.core 5 | :members: -------------------------------------------------------------------------------- /docs/faq.rst: -------------------------------------------------------------------------------- 1 | FAQ 2 | === 3 | 4 | 1. Arrrrr! Where are simple text configs? I don't want to learn python 5 | ---------------------------------------------------------------------- 6 | 7 | Please accept my condolences. This is **scripting** extension 8 | for NETWM window managers. You'd better pass. 9 | 10 | 11 | 2. Where I can find some python tutor? 12 | -------------------------------------- 13 | 14 | Python is a very simple language. You can find introduction material in following 15 | places: 16 | 17 | * `Official tutorial `_ You should 18 | reed sections 3, 4 and 5. 19 | 20 | * `Learn Python The Hard Way `_ is 21 | a must-read thing for beginners. 22 | 23 | The only part you'll find hard to understand are decorators. Yeah, these 24 | mysterious ``@`` signs. I couldn't find basic short explanation, only 25 | `this post `_. 26 | 27 | 28 | 3. Is Orcsome fast? I hear python is a slow interpreted language 29 | ---------------------------------------------------------------- 30 | 31 | Orcsome handles not so many X events and python performance is acceptable for 32 | this task. For example Orcsome with my huge and complex config takes same amount 33 | of processor time as bmpanel2 (small and light panel app written in "C"). 34 | 35 | 36 | 4. Hey, I need functionality XXX. Where it is? 37 | ---------------------------------------------- 38 | 39 | Current Orcsome API is made for my configuration script needs. 40 | If you need XXX — just :ref:`ask `. -------------------------------------------------------------------------------- /docs/guide.rst: -------------------------------------------------------------------------------- 1 | Guide 2 | ===== 3 | 4 | This guide will explain orcsome configure concepts. 5 | 6 | 7 | Configuration script 8 | -------------------- 9 | 10 | Orcsome is only tiny wrapper around xlib. You should provide needed 11 | functionality by configuration script ``$HOME/.config/orcsome/rc.py`` 12 | 13 | The basic template is:: 14 | 15 | from orcsome import get_wm 16 | from orcsome.actions import * 17 | 18 | wm = get_wm() 19 | 20 | # Your commands here 21 | 22 | For example (reference) you can look at my 23 | `config `_. 24 | 25 | As you see it is a simple python file without any implicit magic. 26 | 27 | 28 | Signal decorators 29 | ----------------- 30 | 31 | The whole orcsome idea is built around signal decorators. What it is? 32 | Signal decorators represent X events which will be handled by your wrapped 33 | functions. 34 | 35 | Some examples:: 36 | 37 | # Define global hotkey 38 | @wm.on_key('Alt+Return') 39 | def start_terminal(): 40 | print 'run terminal' 41 | 42 | # Handle window creation 43 | @wm.on_create 44 | def window_created(): 45 | print 'window created', wm.event_window.get_wm_class() 46 | 47 | # Decorators can be nested to handle specific windows events 48 | @wm.on_key('Alt+1') 49 | def bind_additional_keys(): 50 | 51 | @wm.on_key(wm.event_window, 'Alt+2') 52 | def custom_key(): 53 | print 'This is a special Alt-tered window' 54 | 55 | Here is the list of available signal decorators: 56 | 57 | * :meth:`~orcsome.core.WM.on_create` 58 | * :meth:`~orcsome.core.WM.on_destroy` 59 | * :meth:`~orcsome.core.WM.on_key` 60 | * :meth:`~orcsome.core.WM.on_property_change` 61 | 62 | Signal handlers removing 63 | ************************ 64 | 65 | Sometimes you may need to remove signal handler. Just call ``remove()`` on 66 | wrapped function:: 67 | 68 | @wm.on_create 69 | def switch_to_desktop(): 70 | # We are interested in freshly created windows only 71 | if not wm.startup: 72 | if wm.activate_window_desktop(wm.event_window) is None: 73 | 74 | # Subscribe to _NET_WM_DESKTOP property change 75 | @wm.on_property_change(wm.event_window, '_NET_WM_DESKTOP') 76 | def property_was_set(): 77 | wm.activate_window_desktop(wm.event_window) 78 | 79 | # Ok. Handler was called and can be removed 80 | property_was_set.remove() 81 | 82 | 83 | Actions 84 | ------- 85 | 86 | Actions are signal handler generators which provide shorthand way to do some 87 | tasks:: 88 | 89 | # Start xterm 90 | wm.on_key('Mod+Return')( 91 | spawn('xterm')) 92 | 93 | Is equivalent to:: 94 | 95 | @wm.on_key('Mod+Return'): 96 | def start_xterm(): 97 | # Take note: Parentheses after ``spawn`` call is 98 | # important. Action will not be executed without it. 99 | spawn('xterm')() 100 | 101 | :doc:`Here ` is the list of builtin actions. -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Orcsome 2 | ======= 3 | 4 | .. raw:: html 5 | 6 | 12 | 13 | Orcsome is a scripting extension for NETWM compliant window managers. It can 14 | help a lot to customize your work environment. 15 | 16 | .. raw:: html 17 | 18 |
 
19 | 20 | Motivation 21 | ---------- 22 | 23 | I'm old `awesome`_ user with two year experience. I like it not for tiling but 24 | for lua and ability to tune its behavior. But for a very long time some problems 25 | stay unsolved: 26 | 27 | * Grey swing windows. I was hoping it will be fixed in java7 but no luck. 28 | * Input focus for swing windows. Awesome treats such windows as inputless. 29 | * Random focus problems. For example sometimes evince or opera save dialog are 30 | not take focus. 31 | 32 | Simply put, awesome sucks as window manager. 33 | 34 | I need a robust wm with long devel history, small, fast, candy and 35 | **scriptable** on normal language (hello fvwm). But there are a plenty of 36 | robust, small, fast and candy only wm's. There is no any scriptable. 37 | 38 | So I decide to write tiny wm helper application which will be compatible with 39 | many window managers and allow to configure flexible workflows. 40 | 41 | .. _awesome: http://awesome.naquadah.org/ 42 | 43 | Features 44 | -------- 45 | 46 | * Written in python. It means very hackable. 47 | 48 | * Optimization, cpu and memory efficiency are top goals. 49 | 50 | * Extensive use of python syntax to provide easy and expressive eDSL in 51 | configuration script. 52 | 53 | * Supports NETWM standards. 54 | 55 | * Very thin wrapper around X. You can use existing xlib background. 56 | 57 | 58 | Installation 59 | ------------ 60 | 61 | Orcsome needs only ``python-xlib`` as dependency. Install it from your 62 | distributive repository. 63 | 64 | From PyPI 65 | ''''''''' 66 | 67 | I'm regularly upload packages of new versions. So you can install orcsome with 68 | ``easy_install``:: 69 | 70 | sudo easy_install orcsome 71 | 72 | or `pip`_:: 73 | 74 | sudo pip install orcsome 75 | 76 | 77 | From source 78 | ''''''''''' 79 | 80 | :: 81 | 82 | git clone --depth=1 git://github.com/baverman/orcsome.git 83 | cd orcsome 84 | python setup.py build 85 | sudo python setup.py install 86 | 87 | If you often pull changes from master brunch I recommend you following recipe: 88 | 89 | * First install orcsome in develop mode (remove any orcsome dirs in site-packages 90 | before that):: 91 | 92 | sudo python setup.py develop 93 | 94 | * Then, if you want use latest version from master branch simply do:: 95 | 96 | cd cloned/orcsome/dir 97 | git pull 98 | 99 | 100 | ArchLinux 101 | ''''''''' 102 | 103 | There is orcsome package in AUR. 104 | 105 | 106 | .. _pip: http://pip.openplans.org/ 107 | 108 | 109 | Quick start 110 | ----------- 111 | 112 | The most common functionality needed is to bind hot keys to spawn or raise 113 | applications. 114 | 115 | Edit ``~/.config/orcsome/rc.py``:: 116 | 117 | from orcsome import get_wm 118 | from orcsome.actions import * 119 | 120 | wm = get_wm() 121 | 122 | wm.on_key('Shift+Mod+r')( 123 | restart) 124 | 125 | wm.on_key('Ctrl+Alt+p')( 126 | spawn_or_raise('urxvtc -name ncmpcpp -e ncmpcpp', name='ncmpcpp')) 127 | 128 | wm.on_key('Mod+n')( 129 | spawn_or_raise('urxvtc -name mutt -e mutt', name='mutt')) 130 | 131 | wm.on_key('Mod+k')( 132 | spawn_or_raise('urxvtc -name rtorrent -e rtorrent-screen', name='rtorrent')) 133 | 134 | And start orcsome. That's all. 135 | 136 | 137 | Documentation 138 | ------------- 139 | 140 | .. toctree:: 141 | 142 | guide 143 | actions 144 | core 145 | faq 146 | 147 | 148 | .. _contacts: 149 | 150 | Contacts 151 | -------- 152 | 153 | You can create issues on `github `_. 154 | 155 | Or mail directly to bobrov at vl dot ru. 156 | -------------------------------------------------------------------------------- /orcsome/__init__.py: -------------------------------------------------------------------------------- 1 | VERSION = '0.6' 2 | 3 | _wm = None 4 | 5 | def get_wm(immediate=False): 6 | if immediate: 7 | from .wm import ImmediateWM 8 | return ImmediateWM() 9 | else: 10 | return _wm 11 | -------------------------------------------------------------------------------- /orcsome/__main__.py: -------------------------------------------------------------------------------- 1 | from orcsome.run import run 2 | run() 3 | -------------------------------------------------------------------------------- /orcsome/actions.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from . import utils 4 | from .wm import RestartException 5 | 6 | class Actions(object): 7 | def __init__(self): 8 | self.spawn_queue = [] 9 | 10 | def create_spawn_hook(self): 11 | if not self.spawn_queue: 12 | return 13 | 14 | t = time.time() - 100 # 100 seconds must be enough to start any heavy app 15 | for r in self.spawn_queue[:]: 16 | st, handler, cw, cd, matchers = r 17 | if st < t: 18 | self.spawn_queue.remove(r) 19 | elif self.event_window.matches(**matchers): 20 | self.spawn_queue.remove(r) 21 | handler(cd, self.window(cw)) 22 | 23 | def spawn(self, cmd, switch_to_desktop=None): 24 | """Run specified cmd 25 | 26 | :param cmd: shell command. Can include pipes, redirection and so on. 27 | :param switch_to_desktop: integer. Desktop number to activate after 28 | command start. Starts from zero. 29 | """ 30 | utils.spawn(cmd) 31 | if switch_to_desktop is not None: 32 | self.activate_desktop(switch_to_desktop) 33 | 34 | def spawn_or_raise(self, cmd, switch_to_desktop=None, bring_to_current=False, 35 | on_create=None, **matchers): 36 | """Activate window or run command 37 | 38 | Activation means to give input focus for existing window matched 39 | by provided rules. 40 | 41 | ``switch_to_desktop`` controls appear of spawned windows and 42 | ``bring_to_current`` change matched windows behavior. 43 | 44 | ``on_create`` is a function with the following signature:: 45 | 46 | def on_spawned_window_create(wm, desktop, window) 47 | 48 | Where ``wm`` is :class:`orcsome instance `, 49 | ``desktop`` and ``window`` are active desktop and focused 50 | window before spawn_or_raise call. 51 | 52 | :param cmd: same as in :func:`spawn`. 53 | :param switch_to_desktop: same as in :func:`spawn`. 54 | :param bring_to_current: if True, move matched window to current desktop 55 | :param on_create: on create handler, called after command spawn 56 | :param \*\*matchers: see :meth:`~orcsome.core.WM.is_match` 57 | """ 58 | client = self.find_client(self.get_clients(), **matchers) 59 | if client: 60 | if bring_to_current and self.current_desktop != client.desktop: 61 | @self.on_property_change(client, '_NET_WM_DESKTOP') 62 | def focus_and_raise_cb(): 63 | focus_and_raise_cb.remove() 64 | self.focus_and_raise(self.event_window) 65 | 66 | self.change_window_desktop(client, self.current_desktop) 67 | else: 68 | self.focus_and_raise(client) 69 | else: 70 | if on_create: 71 | if not self.create_spawn_hook in self.create_handlers: 72 | self.on_create(self.create_spawn_hook) 73 | 74 | self.spawn_queue.append((time.time(), on_create, 75 | int(self.current_window), self.current_desktop, matchers)) 76 | 77 | self.spawn(cmd, switch_to_desktop) 78 | 79 | def _focus(self, window, direction): 80 | clients = self.find_clients(self.get_clients(), desktop=window.desktop) 81 | idx = clients.index(window) 82 | newc = clients[(idx + direction) % len(clients)] 83 | self.focus_and_raise(newc) 84 | 85 | def focus_next(self, window=None): 86 | """Focus next client on current desktop. 87 | 88 | next/prev are defined by client creation time 89 | """ 90 | self._focus(window or self.current_window, 1) 91 | 92 | def focus_prev(self, window=None): 93 | """Focus previous client on current desktop. 94 | 95 | next/prev are defined by client creation time 96 | """ 97 | self._focus(window or self.current_window, -1) 98 | 99 | def restart(self): 100 | """Restart orcsome""" 101 | raise RestartException() 102 | 103 | def do(self, callable, *args, **kwargs): 104 | callable(*args, **kwargs) 105 | 106 | def activate_window_desktop(self, window): 107 | """Activate window desktop 108 | 109 | Return: 110 | 111 | * True if window is placed on different from current desktop 112 | * False if window desktop is the same 113 | * None if window does not have desktop property 114 | """ 115 | wd = window.desktop 116 | if wd is not None: 117 | if self.current_desktop != wd: 118 | self.activate_desktop(wd) 119 | return True 120 | else: 121 | return False 122 | else: 123 | return None 124 | 125 | -------------------------------------------------------------------------------- /orcsome/aliases.py: -------------------------------------------------------------------------------- 1 | KEYS = { 2 | '`': 'grave', 3 | '-': 'minus', 4 | '=': 'equal', 5 | '[': 'bracketleft', 6 | ']': 'bracketright', 7 | '\\': 'backslash', 8 | ';': 'semicolon', 9 | "'": 'apostrophe', 10 | ',': 'comma', 11 | '.': 'period', 12 | '/': 'slash', 13 | 'bs': 'BackSpace', 14 | 'BS': 'BackSpace', 15 | 'Space': 'space', 16 | 'left': 'Left', 17 | 'right': 'Right', 18 | 'up': 'Up', 19 | 'down': 'Down', 20 | 'tab': 'Tab', 21 | 'f1': 'F1', 22 | 'f2': 'F2', 23 | 'f3': 'F3', 24 | 'f4': 'F4', 25 | 'f5': 'F5', 26 | 'f6': 'F6', 27 | 'f7': 'F7', 28 | 'f8': 'F8', 29 | 'f9': 'F9', 30 | 'f10': 'F10', 31 | 'f11': 'F11', 32 | 'f12': 'F12', 33 | 'insert': 'Insert', 34 | 'ins': 'Insert', 35 | 'delete': 'Delete', 36 | 'del': 'Delete', 37 | 'home': 'Home', 38 | 'end': 'End', 39 | 'pgup': 'Prior', 40 | 'PgUp': 'Prior', 41 | 'PageUp': 'Prior', 42 | 'pgdown': 'Next', 43 | 'PgDown': 'Next', 44 | 'PageDown': 'Next', 45 | 'menu': 'Menu', 46 | 'enter': 'Return', 47 | 'Enter': 'Return', 48 | 'return': 'Return', 49 | } 50 | -------------------------------------------------------------------------------- /orcsome/ev.py: -------------------------------------------------------------------------------- 1 | from time import time 2 | from ._ev import ffi, lib 3 | 4 | NULL = ffi.NULL 5 | globals().update(lib.__dict__) 6 | 7 | 8 | class Loop(object): 9 | def __init__(self): 10 | self._loop = ev_loop_new(EVBACKEND_SELECT) 11 | 12 | def destroy(self): 13 | ev_loop_destroy(self._loop) 14 | 15 | def run(self, flags=0): 16 | ev_run(self._loop, flags) 17 | 18 | def break_(self, flags=EVBREAK_ALL): 19 | ev_break(self._loop, flags) 20 | 21 | 22 | class IOWatcher(object): 23 | def __init__(self, cb, fd, flags): 24 | self._watcher = ffi.new('ev_io*') 25 | self._cb = ffi.callback('io_cb', cb) 26 | ev_io_init(self._watcher, self._cb, fd, flags) 27 | 28 | def start(self, loop): 29 | ev_io_start(loop._loop, self._watcher) 30 | 31 | def stop(self, loop): 32 | ev_io_stop(loop._loop, self._watcher) 33 | 34 | 35 | class SignalWatcher(object): 36 | def __init__(self, cb, signal): 37 | self._watcher = ffi.new('ev_signal*') 38 | self._cb = ffi.callback('signal_cb', cb) 39 | ev_signal_init(self._watcher, self._cb, signal) 40 | 41 | def start(self, loop): 42 | ev_signal_start(loop._loop, self._watcher) 43 | 44 | def stop(self, loop): 45 | ev_signal_stop(loop._loop, self._watcher) 46 | 47 | 48 | class TimerWatcher(object): 49 | def __init__(self, cb, after, repeat=0.0): 50 | self._after = after 51 | self._repeat = repeat 52 | self._watcher = ffi.new('ev_timer*') 53 | self._cb = ffi.callback('timer_cb', cb) 54 | ev_timer_init(self._watcher, self._cb, after, repeat) 55 | 56 | def start(self, loop, after=None, repeat=None): 57 | if after or repeat: 58 | self._after = after or self._after 59 | self._repeat = repeat or self._repeat 60 | ev_timer_set(self._watcher, self._after, self._repeat) 61 | 62 | self.next_stop = time() + self._after 63 | ev_timer_start(loop._loop, self._watcher) 64 | 65 | def stop(self, loop): 66 | ev_timer_stop(loop._loop, self._watcher) 67 | 68 | def again(self, loop): 69 | self.next_stop = time() + self._repeat 70 | ev_timer_again(loop._loop, self._watcher) 71 | 72 | def remaining(self, loop): 73 | return ev_timer_remaining(loop._loop, self._watcher) 74 | 75 | def update_next_stop(self): 76 | self.next_stop = time() + self._repeat 77 | 78 | def overdue(self, timeout): 79 | return time() > self.next_stop + timeout 80 | -------------------------------------------------------------------------------- /orcsome/ev_build.py: -------------------------------------------------------------------------------- 1 | import cffi 2 | 3 | ffi = cffi.FFI() 4 | 5 | ffi.set_source('orcsome._ev', "#include ", libraries=['ev']) 6 | 7 | ffi.cdef(""" 8 | #define EVBACKEND_SELECT ... 9 | #define EV_READ ... 10 | #define EV_WRITE ... 11 | #define EVBREAK_ALL ... 12 | 13 | typedef ... ev_loop; 14 | 15 | struct ev_loop *ev_loop_new (unsigned int flags); 16 | void ev_loop_destroy (struct ev_loop*); 17 | void ev_break (struct ev_loop*, int); 18 | int ev_run (struct ev_loop*, int); 19 | 20 | typedef struct { ...; } ev_io; 21 | typedef void (*io_cb) (struct ev_loop*, ev_io*, int); 22 | void ev_io_init(ev_io*, io_cb, int, int); 23 | void ev_io_start(struct ev_loop*, ev_io*); 24 | void ev_io_stop(struct ev_loop*, ev_io*); 25 | 26 | typedef struct { ...; } ev_signal; 27 | typedef void (*signal_cb) (struct ev_loop*, ev_signal*, int); 28 | void ev_signal_init(ev_signal*, signal_cb, int); 29 | void ev_signal_start(struct ev_loop*, ev_signal*); 30 | void ev_signal_stop(struct ev_loop*, ev_signal*); 31 | 32 | typedef double ev_tstamp; 33 | typedef struct { ...; } ev_timer; 34 | typedef void (*timer_cb) (struct ev_loop*, ev_timer*, int); 35 | void ev_timer_init(ev_timer*, timer_cb, ev_tstamp, ev_tstamp); 36 | void ev_timer_set(ev_timer*, ev_tstamp, ev_tstamp); 37 | void ev_timer_start(struct ev_loop*, ev_timer*); 38 | void ev_timer_again(struct ev_loop*, ev_timer*); 39 | void ev_timer_stop(struct ev_loop*, ev_timer*); 40 | ev_tstamp ev_timer_remaining(struct ev_loop*, ev_timer*); 41 | """) 42 | 43 | if __name__ == "__main__": 44 | ffi.compile(verbose=True) 45 | -------------------------------------------------------------------------------- /orcsome/notify.py: -------------------------------------------------------------------------------- 1 | from subprocess import Popen, PIPE 2 | 3 | default_appname = 'orcsome' 4 | 5 | 6 | def notify(summary, body, timeout=-1, urgency=1, appname=None): 7 | n = Notification(summary, body, timeout, urgency, appname or default_appname) 8 | n.show() 9 | return n 10 | 11 | 12 | class Notification(object): 13 | def __init__(self, summary, body, timeout, urgency, appname): 14 | self.summary = summary.lstrip('-') 15 | self.body = body 16 | self.timeout = timeout 17 | self.urgency = urgency 18 | self.appname = appname 19 | self.replace_id = 0 20 | 21 | def show(self): 22 | timeout = int(self.timeout * 1000) 23 | if timeout < 0: timeout = -1 24 | 25 | urgency = '{}' 26 | if self.urgency != 1: 27 | urgency = "{'urgency': }" % self.urgency 28 | 29 | self.lastcmd = cmd = [ 30 | 'gdbus', 31 | 'call', 32 | '--session', 33 | '--dest=org.freedesktop.Notifications', 34 | '--object-path=/org/freedesktop/Notifications', 35 | '--method=org.freedesktop.Notifications.Notify', 36 | self.appname, 37 | '{}'.format(self.replace_id), 38 | '', 39 | self.summary, 40 | self.body, 41 | '[]', 42 | urgency, 43 | '{}'.format(timeout), 44 | ] 45 | 46 | out, err = Popen(cmd, stdout=PIPE, stderr=PIPE).communicate() 47 | if err: 48 | raise Exception(err) 49 | 50 | self.replace_id = int(out.strip().split()[1].rstrip(b',)')) 51 | 52 | def update(self, summary=None, body=None, timeout=None, urgency=None): 53 | if summary is not None: 54 | self.summary = summary 55 | 56 | if body is not None: 57 | self.body = body 58 | 59 | if timeout is not None: 60 | self.timeout = timeout 61 | 62 | if urgency is not None: 63 | self.urgency = urgency 64 | 65 | self.show() 66 | 67 | def close(self): 68 | cmd = [ 69 | 'gdbus', 70 | 'call', 71 | '--session', 72 | '--dest=org.freedesktop.Notifications', 73 | '--object-path=/org/freedesktop/Notifications', 74 | '--method=org.freedesktop.Notifications.CloseNotification', 75 | '{}'.format(self.replace_id), 76 | ] 77 | 78 | _, err = Popen(cmd, stdout=PIPE, stderr=PIPE).communicate() 79 | if err: 80 | raise Exception(err) 81 | -------------------------------------------------------------------------------- /orcsome/run.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import signal 3 | import os.path 4 | import logging 5 | import argparse 6 | import runpy 7 | 8 | from . import VERSION, ev 9 | from .wm import WM 10 | from .actions import Actions 11 | from .testwm import TestWM 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | def load_config(wm, config): 17 | import orcsome 18 | orcsome._wm = wm 19 | 20 | sys.path.insert(0, os.path.dirname(config)) 21 | try: 22 | runpy.run_path(config) 23 | except: 24 | logger.exception('Error on loading %s' % config) 25 | sys.exit(1) 26 | finally: 27 | sys.path.pop(0) 28 | 29 | 30 | def check_config(config): 31 | wm = TestWM() 32 | wm.mix(Actions) 33 | 34 | import orcsome 35 | orcsome._wm = wm 36 | 37 | env = {} 38 | sys.path.insert(0, os.path.dirname(config)) 39 | try: 40 | runpy.run_path(config, env) 41 | except: 42 | logger.exception('Config file check failed %s' % config) 43 | return False 44 | finally: 45 | sys.path.pop(0) 46 | 47 | return True 48 | 49 | 50 | def run(): 51 | parser = argparse.ArgumentParser() 52 | parser.add_argument('--version', action='version', version='%(prog)s ' + VERSION) 53 | parser.add_argument('-l', '--log', dest='log', metavar='FILE', 54 | help='Path to log file (log to stdout by default)') 55 | parser.add_argument('--log-level', metavar='LOGLEVEL', default='INFO', 56 | help='log level, default is INFO') 57 | 58 | config_dir = os.getenv('XDG_CONFIG_HOME', os.path.expanduser('~/.config')) 59 | default_rcfile = os.path.join(config_dir, 'orcsome', 'rc.py') 60 | parser.add_argument('-c', '--config', dest='config', metavar='FILE', 61 | default=default_rcfile, help='Path to config file (%(default)s)') 62 | 63 | args = parser.parse_args() 64 | 65 | if args.log: 66 | handler = logging.FileHandler(args.log) 67 | else: 68 | handler = logging.StreamHandler() 69 | 70 | root_logger = logging.getLogger() 71 | root_logger.setLevel(args.log_level) 72 | handler.setFormatter(logging.Formatter( 73 | "%(asctime)s %(name)s %(levelname)s: %(message)s")) 74 | root_logger.addHandler(handler) 75 | 76 | loop = ev.Loop() 77 | wm = WM(loop) 78 | wm.mix(Actions) 79 | 80 | def stop(l, w, e): 81 | wm.stop(True) 82 | loop.break_() 83 | 84 | sigint = ev.SignalWatcher(stop, signal.SIGINT) 85 | sigint.start(loop) 86 | 87 | def on_restart(): 88 | if check_config(args.config): 89 | wm.stop() 90 | logger.info('Restarting...') 91 | load_config(wm, args.config) 92 | wm.init() 93 | 94 | wm.restart_handler = on_restart 95 | 96 | load_config(wm, args.config) 97 | wm.init() 98 | loop.run() 99 | -------------------------------------------------------------------------------- /orcsome/testwm.py: -------------------------------------------------------------------------------- 1 | from .utils import ActionCaller, Mixable 2 | 3 | idfunc = lambda func: func 4 | 5 | 6 | class TestWM(Mixable): 7 | def on_key(self, key): 8 | assert isinstance(key, basestring), 'First argument to on_key must be string' 9 | return ActionCaller(self, idfunc) 10 | 11 | def on_timer(self, period, start=True): 12 | return ActionCaller(self, idfunc) 13 | 14 | def on_create(self, *args, **matchers): 15 | assert matchers or args 16 | 17 | if args: 18 | assert len(args) == 1 19 | return args[0] 20 | 21 | if matchers: 22 | possible_args = set(('cls', 'role', 'name', 'title', 'desktop')) 23 | assert possible_args.union(matchers) == possible_args, \ 24 | 'Invalid matcher, must be one of %s' % possible_args 25 | 26 | return ActionCaller(self, idfunc) 27 | 28 | def on_manage(self, *args, **matchers): 29 | assert matchers or args 30 | 31 | if args: 32 | assert len(args) == 1 33 | return args[0] 34 | 35 | if matchers: 36 | possible_args = set(('cls', 'role', 'name', 'title', 'desktop')) 37 | assert possible_args.union(matchers) == possible_args, \ 38 | 'Invalid matcher, must be one of %s' % possible_args 39 | 40 | return ActionCaller(self, idfunc) 41 | 42 | def on_property_change(self, *args): 43 | assert all(isinstance(r, basestring) for r in args) 44 | return ActionCaller(self, idfunc) 45 | 46 | def on_destroy(self, window): 47 | return ActionCaller(self, idfunc) 48 | 49 | def on_init(self, func): 50 | return func 51 | 52 | def on_deinit(self, func): 53 | return func 54 | 55 | def close_window(self, window=None): 56 | pass 57 | -------------------------------------------------------------------------------- /orcsome/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | 4 | btype = type(b'') 5 | ntype = type('') 6 | utype = type(u'') 7 | 8 | 9 | def bstr(s, encoding='latin-1'): 10 | if type(s) is not btype: 11 | return s.encode(encoding) 12 | return s 13 | 14 | 15 | def nstr(s, encoding='latin-1'): 16 | t = type(s) 17 | if t is not ntype: 18 | if t is btype: 19 | return s.decode(encoding) 20 | elif t is utype: 21 | return s.encode(encoding) 22 | return s 23 | 24 | 25 | class cached_property(object): 26 | def __init__(self, func): 27 | self.func = func 28 | self.__name__ = func.__name__ 29 | self.__doc__ = func.__doc__ 30 | 31 | def __get__(self, obj, cls): 32 | if obj is None: 33 | return self 34 | val = self.func(obj) 35 | obj.__dict__[self.__name__] = val 36 | return val 37 | 38 | 39 | re_cache = {} 40 | def match_string(pattern, data): 41 | if data is None: 42 | return False 43 | 44 | try: 45 | r = re_cache[pattern] 46 | except KeyError: 47 | r = re_cache[pattern] = re.compile(pattern) 48 | 49 | return r.match(data) 50 | 51 | 52 | def spawn(cmd): 53 | pid = os.fork() 54 | if pid != 0: 55 | os.waitpid(pid, 0) 56 | return 57 | 58 | os.setsid() 59 | 60 | pid = os.fork() 61 | if pid != 0: 62 | os._exit(0) 63 | 64 | try: 65 | os.execv(os.environ.get('SHELL', '/bin/sh'), ['shell', '-c', cmd]) 66 | except Exception: 67 | os._exit(255) 68 | 69 | 70 | class Mixable(object): 71 | def mix(self, mixin): 72 | for name, value in mixin.__dict__.items(): 73 | if name == '__init__': 74 | value(self) 75 | elif name.startswith('__'): 76 | continue 77 | else: 78 | if name in self.__class__.__dict__: 79 | raise Exception("Can't override base class method {}".format(name)) 80 | setattr(self, name, value.__get__(self)) 81 | 82 | 83 | class ActionCaller(object): 84 | def __init__(self, obj, decorator): 85 | self.obj = obj 86 | self.decorator = decorator 87 | 88 | def __getattr__(self, name): 89 | func = getattr(self.obj, name) 90 | def result(*args, **kwargs): 91 | return self.decorator(lambda: func(*args, **kwargs)) 92 | 93 | return result 94 | 95 | def __call__(self, func): 96 | return self.decorator(func) 97 | -------------------------------------------------------------------------------- /orcsome/wm.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | 4 | from . import xlib as X, ev 5 | from .wrappers import Window 6 | from .aliases import KEYS as KEY_ALIASES 7 | from .utils import Mixable, ActionCaller, bstr, nstr 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | MODIFICATORS = { 12 | 'Alt': X.Mod1Mask, 13 | 'Control': X.ControlMask, 14 | 'Ctrl': X.ControlMask, 15 | 'Shift': X.ShiftMask, 16 | 'Win': X.Mod4Mask, 17 | 'Mod': X.Mod4Mask, 18 | 'Hyper': X.Mod4Mask, 19 | 'Super': X.Mod4Mask, 20 | } 21 | 22 | IGNORED_MOD_MASKS = (0, X.LockMask, X.Mod2Mask, X.LockMask | X.Mod2Mask) 23 | 24 | 25 | class RestartException(Exception): pass 26 | 27 | 28 | class WM(Mixable): 29 | """Core orcsome instance 30 | 31 | Can be get in any time as:: 32 | 33 | import orcsome 34 | wm = orcsome.get_wm() 35 | """ 36 | 37 | def __init__(self, loop): 38 | self.handlers = { 39 | X.KeyPress: self.handle_keypress, 40 | X.KeyRelease: self.handle_keyrelease, 41 | X.CreateNotify: self.handle_create, 42 | X.DestroyNotify: self.handle_destroy, 43 | X.FocusIn: self.handle_focus, 44 | X.FocusOut: self.handle_focus, 45 | X.PropertyNotify: self.handle_property, 46 | } 47 | self._event = X.ffi.new('XEvent *') 48 | 49 | self.key_handlers = {} 50 | self.property_handlers = {} 51 | self.create_handlers = [] 52 | self.destroy_handlers = {} 53 | self.init_handlers = [] 54 | self.deinit_handlers = [] 55 | self.timer_handlers = [] 56 | 57 | self.grab_keyboard_handler = None 58 | self.grab_pointer_handler = None 59 | 60 | self.focus_history = [] 61 | 62 | self.dpy = X.XOpenDisplay(X.NULL) 63 | if self.dpy == X.NULL: 64 | raise Exception("Can't open display") 65 | 66 | self.fd = X.ConnectionNumber(self.dpy) 67 | self.root = X.DefaultRootWindow(self.dpy) 68 | self.atom = X.AtomCache(self.dpy) 69 | 70 | self.undecorated_atom_name = '_OB_WM_STATE_UNDECORATED' 71 | self.track_kbd_layout = False 72 | 73 | self.loop = loop 74 | self.xevent_watcher = ev.IOWatcher(self._xevent_cb, self.fd, ev.EV_READ) 75 | self.xevent_watcher.start(self.loop) 76 | 77 | self.restart_handler = None 78 | 79 | def window(self, window_id): 80 | window = Window(window_id) 81 | window.wm = self 82 | return window 83 | 84 | def emit(self, signal): 85 | os.write(self.wfifo, signal + '\n') 86 | 87 | def keycode(self, key): 88 | sym = X.XStringToKeysym(bstr(KEY_ALIASES.get(key, key))) 89 | if sym is X.NoSymbol: 90 | return None 91 | 92 | return X.XKeysymToKeycode(self.dpy, sym) 93 | 94 | def parse_keydef(self, keydef): 95 | keys = [r.strip() for r in keydef.split()] 96 | result = [] 97 | for k in keys: 98 | parts = k.split('+') 99 | mod, key = parts[:-1], parts[-1] 100 | modmask = 0 101 | for m in mod: 102 | try: 103 | modmask |= MODIFICATORS[m] 104 | except KeyError: 105 | return None 106 | 107 | code = self.keycode(key) 108 | if not code: 109 | return None 110 | 111 | result.append((code, modmask)) 112 | 113 | return result 114 | 115 | def bind_key(self, window, keydef): 116 | code_mmask_list = self.parse_keydef(keydef) 117 | if not code_mmask_list: 118 | logger.error('Invalid key definition [%s]' % keydef) 119 | return ActionCaller(self, lambda func: func) 120 | 121 | if len(code_mmask_list) == 1: 122 | code, modmask = code_mmask_list[0] 123 | 124 | def inner(func): 125 | keys = [] 126 | for imask in IGNORED_MOD_MASKS: 127 | mask = modmask | imask 128 | X.XGrabKey(self.dpy, code, mask, window, True, X.GrabModeAsync, X.GrabModeAsync) 129 | self.key_handlers.setdefault(window, {})[(mask, code)] = func 130 | keys.append((mask, code)) 131 | 132 | def remove(): 133 | for k in keys: 134 | del self.key_handlers[window][k] 135 | 136 | func.remove = remove 137 | return func 138 | 139 | return ActionCaller(self, inner) 140 | else: 141 | return ActionCaller(self, lambda func: func) 142 | 143 | def on_key(self, *args): 144 | """Signal decorator to define hotkey 145 | 146 | You can define global key:: 147 | 148 | wm.on_key('Alt+Return')( 149 | spawn('xterm')) 150 | 151 | Or key binded to specific window:: 152 | 153 | @wm.on_create(cls='URxvt') 154 | def bind_urxvt_keys(): 155 | # Custom key to close only urxvt windows 156 | wm.on_key(wm.event_window, 'Ctrl+d')( 157 | close) 158 | 159 | Key defenition is a string in format ``[mod + ... +]keysym`` where ``mod`` is 160 | one of modificators [Alt, Shift, Control(Ctrl), Mod(Win)] and 161 | ``keysym`` is a key name. 162 | """ 163 | 164 | if isinstance(args[0], Window): 165 | window = args[0] 166 | key = args[1] 167 | else: 168 | window = self.root 169 | key = args[0] 170 | 171 | return self.bind_key(window, key) 172 | 173 | def _on_create_manage(self, ignore_startup, *args, **matchers): 174 | """Signal decorator to handle window creation 175 | 176 | Can be used in two forms. Listen to any window creation:: 177 | 178 | @wm.on_create 179 | def debug(wm): 180 | print wm.event_window.get_wm_class() 181 | 182 | Or specific window:: 183 | 184 | @wm.on_create(cls='Opera') 185 | def use_firefox_luke(wm): 186 | wm.close_window(wm.event_window) 187 | spawn('firefox')() 188 | 189 | Also, orcsome calls on_create handlers on its startup. 190 | You can check ``wm.startup`` attribute to denote such event. 191 | 192 | See :meth:`is_match` for ``**matchers`` argument description. 193 | """ 194 | def inner(func): 195 | if matchers: 196 | ofunc = func 197 | func = lambda: self.event_window.matches(**matchers) and ofunc() 198 | 199 | if ignore_startup: 200 | oofunc = func 201 | func = lambda: self.startup or oofunc() 202 | 203 | self.create_handlers.append(func) 204 | 205 | def remove(): 206 | self.create_handlers.remove(func) 207 | 208 | func.remove = remove 209 | return func 210 | 211 | if args: 212 | return inner(args[0]) 213 | else: 214 | return ActionCaller(self, inner) 215 | 216 | def on_create(self, *args, **matchers): 217 | return self._on_create_manage(True, *args, **matchers) 218 | 219 | def on_manage(self, *args, **matchers): 220 | return self._on_create_manage(False, *args, **matchers) 221 | 222 | def on_destroy(self, window): 223 | """Signal decorator to handle window destroy""" 224 | 225 | def inner(func): 226 | self.destroy_handlers.setdefault(window, []).append(func) 227 | return func 228 | 229 | return ActionCaller(self, inner) 230 | 231 | def on_property_change(self, *args): 232 | """Signal decorator to handle window property change 233 | 234 | One can handle any window property change:: 235 | 236 | @wm.on_property_change('_NET_WM_STATE') 237 | def window_maximized_state_change(): 238 | state = wm.get_window_state(wm.event_window) 239 | if state.maximized_vert and state.maximized_horz: 240 | print 'Look, ma! Window is maximized now!' 241 | 242 | And specific window:: 243 | 244 | @wm.on_create 245 | def switch_to_desktop(): 246 | if not wm.startup: 247 | if wm.activate_window_desktop(wm.event_window) is None: 248 | # Created window has no any attached desktop so wait for it 249 | @wm.on_property_change(wm.event_window, '_NET_WM_DESKTOP') 250 | def property_was_set(): 251 | wm.activate_window_desktop(wm.event_window) 252 | property_was_set.remove() 253 | 254 | """ 255 | def inner(func): 256 | if isinstance(args[0], Window): 257 | window = args[0] 258 | props = args[1:] 259 | else: 260 | window = None 261 | props = args 262 | 263 | for p in props: 264 | atom = self.atom[p] 265 | self.property_handlers.setdefault( 266 | atom, {}).setdefault(window, []).append(func) 267 | 268 | def remove(): 269 | for p in props: 270 | atom = self.atom[p] 271 | self.property_handlers[atom][window].remove(func) 272 | 273 | func.remove = remove 274 | return func 275 | 276 | return ActionCaller(self, inner) 277 | 278 | def on_timer(self, timeout, start=True, first_timeout=None): 279 | def inner(func): 280 | def cb(l, w, e): 281 | if func(): 282 | timer.stop(self.loop) 283 | else: 284 | timer.update_next_stop() 285 | 286 | self.timer_handlers.append(func) 287 | 288 | timer = ev.TimerWatcher(cb, first_timeout or timeout, timeout) 289 | func.start = lambda after=None, repeat=None: timer.start(self.loop, after, repeat) 290 | func.stop = lambda: timer.stop(self.loop) 291 | func.again = lambda: timer.again(self.loop) 292 | func.remaining = lambda: timer.remaining(self.loop) 293 | func.overdue = lambda timeout: timer.overdue(timeout) 294 | 295 | if start: 296 | func.start() 297 | 298 | return func 299 | 300 | return ActionCaller(self, inner) 301 | 302 | def get_clients(self, ids=False): 303 | """Return wm client list""" 304 | result = X.get_window_property(self.dpy, self.root, 305 | self.atom['_NET_CLIENT_LIST'], self.atom['WINDOW']) or [] 306 | 307 | if not ids: 308 | result = [self.window(r) for r in result] 309 | 310 | return result 311 | 312 | def get_stacked_clients(self): 313 | """Return client list in stacked order. 314 | 315 | Most top window will be last in list. Can be useful to determine window visibility. 316 | """ 317 | result = X.get_window_property(self.dpy, self.root, 318 | self.atom['_NET_CLIENT_LIST_STACKING'], self.atom['WINDOW']) or [] 319 | return [self.window(r) for r in result] 320 | 321 | @property 322 | def current_window(self): 323 | """Return currently active (with input focus) window""" 324 | result = X.get_window_property(self.dpy, self.root, 325 | self.atom['_NET_ACTIVE_WINDOW'], self.atom['WINDOW']) 326 | 327 | if result: 328 | return self.window(result[0]) 329 | 330 | @property 331 | def current_desktop(self): 332 | """Return current desktop number 333 | 334 | Counts from zero. 335 | """ 336 | return X.get_window_property(self.dpy, self.root, 337 | self.atom['_NET_CURRENT_DESKTOP'])[0] 338 | 339 | def activate_desktop(self, num): 340 | """Activate desktop ``num``""" 341 | if num < 0: 342 | return 343 | 344 | self._send_event(self.root, self.atom['_NET_CURRENT_DESKTOP'], [num]) 345 | self._flush() 346 | 347 | def _send_event(self, window, mtype, data): 348 | data = (data + ([0] * (5 - len(data))))[:5] 349 | ev = X.ffi.new('XClientMessageEvent *', { 350 | 'type': X.ClientMessage, 351 | 'window': window, 352 | 'message_type': mtype, 353 | 'format': 32, 354 | 'data': {'l': data}, 355 | }) 356 | X.XSendEvent(self.dpy, self.root, False, X.SubstructureRedirectMask, 357 | X.ffi.cast('XEvent *', ev)) 358 | 359 | def _flush(self): 360 | X.XFlush(self.dpy) 361 | 362 | def find_clients(self, clients, **matchers): 363 | """Return matching clients list 364 | 365 | :param clients: window list returned by :meth:`get_clients` or :meth:`get_stacked_clients`. 366 | :param \*\*matchers: keyword arguments defined in :meth:`is_match` 367 | """ 368 | return [r for r in clients if r.matches(**matchers)] 369 | 370 | def find_client(self, clients, **matchers): 371 | """Return first matching client 372 | 373 | :param clients: window list returned by :meth:`get_clients` or :meth:`get_stacked_clients`. 374 | :param \*\*matchers: keyword arguments defined in :meth:`is_match` 375 | """ 376 | result = self.find_clients(clients, **matchers) 377 | try: 378 | return result[0] 379 | except IndexError: 380 | return None 381 | 382 | def process_create_window(self, window): 383 | X.XSelectInput(self.dpy, window, X.StructureNotifyMask | 384 | X.PropertyChangeMask | X.FocusChangeMask) 385 | 386 | self.event_window = window 387 | for handler in self.create_handlers: 388 | handler() 389 | 390 | def init(self): 391 | X.XSelectInput(self.dpy, self.root, X.SubstructureNotifyMask) 392 | 393 | for h in self.init_handlers: 394 | h() 395 | 396 | self.startup = True 397 | for c in self.get_clients(): 398 | self.process_create_window(c) 399 | 400 | X.XSync(self.dpy, False) 401 | 402 | X.XSetErrorHandler(error_handler) 403 | 404 | def handle_keypress(self, event): 405 | event = event.xkey 406 | logger.debug('Keypress {} {}'.format(event.state, event.keycode)) 407 | if self.grab_keyboard_handler: 408 | self.grab_keyboard_handler(True, event.state, event.keycode) 409 | else: 410 | try: 411 | handler = self.key_handlers[event.window][(event.state, event.keycode)] 412 | except KeyError: 413 | pass 414 | else: 415 | self.event = event 416 | self.event_window = self.window(event.window) 417 | handler() 418 | 419 | def handle_keyrelease(self, event): 420 | event = event.xkey 421 | if self.grab_keyboard_handler: 422 | self.grab_keyboard_handler(False, event.state, event.keycode) 423 | 424 | def handle_create(self, event): 425 | event = event.xcreatewindow 426 | self.event = event 427 | self.startup = False 428 | self.process_create_window(self.window(event.window)) 429 | 430 | def handle_destroy(self, event): 431 | event = event.xdestroywindow 432 | try: 433 | handlers = self.destroy_handlers[event.window] 434 | except KeyError: 435 | pass 436 | else: 437 | self.event = event 438 | self.event_window = self.window(event.window) 439 | for h in handlers: 440 | h() 441 | finally: 442 | self._clean_window_data(event.window) 443 | 444 | def handle_property(self, event): 445 | event = event.xproperty 446 | atom = event.atom 447 | if event.state == 0 and atom in self.property_handlers: 448 | wphandlers = self.property_handlers[atom] 449 | self.event_window = self.window(event.window) 450 | self.event = event 451 | if event.window in wphandlers: 452 | for h in wphandlers[event.window]: 453 | h() 454 | 455 | if None in wphandlers: 456 | for h in wphandlers[None]: 457 | h() 458 | 459 | def handle_focus(self, event): 460 | event = event.xfocus 461 | if event.type == X.FocusIn: 462 | try: 463 | self.focus_history.remove(event.window) 464 | except ValueError: 465 | pass 466 | 467 | self.focus_history.append(event.window) 468 | if event.mode in (0, 3) and self.track_kbd_layout: 469 | prop = X.get_window_property(self.dpy, event.window, self.atom['_ORCSOME_KBD_GROUP']) 470 | if prop: 471 | X.set_kbd_group(self.dpy, prop[0]) 472 | else: 473 | X.set_kbd_group(self.dpy, 0) 474 | else: 475 | if event.mode in (0, 3) and self.track_kbd_layout: 476 | X.set_window_property(self.dpy, event.window, self.atom['_ORCSOME_KBD_GROUP'], 477 | self.atom['CARDINAL'], 32, [X.get_kbd_group(self.dpy)]) 478 | 479 | def _xevent_cb(self, loop, watcher, events): 480 | event = self._event 481 | while True: 482 | i = X.XPending(self.dpy) 483 | if not i: break 484 | 485 | while i > 0: 486 | X.XNextEvent(self.dpy, event) 487 | i -= 1 488 | 489 | try: 490 | h = self.handlers[event.type] 491 | except KeyError: 492 | continue 493 | 494 | try: 495 | h(event) 496 | except RestartException: 497 | if self.restart_handler: 498 | self.restart_handler() 499 | return 500 | except: 501 | logger.exception('Boo') 502 | 503 | def _clean_window_data(self, window): 504 | if window in self.key_handlers: 505 | del self.key_handlers[window] 506 | 507 | if window in self.destroy_handlers: 508 | self.destroy_handlers[window] 509 | 510 | try: 511 | self.focus_history.remove(window) 512 | except ValueError: 513 | pass 514 | 515 | for atom, whandlers in list(self.property_handlers.items()): 516 | if window in whandlers: 517 | del whandlers[window] 518 | 519 | if not self.property_handlers[atom]: 520 | del self.property_handlers[atom] 521 | 522 | def focus_window(self, window): 523 | """Activate window""" 524 | self._send_event(window, self.atom['_NET_ACTIVE_WINDOW'], [2, X.CurrentTime]) 525 | self._flush() 526 | 527 | def focus_and_raise(self, window): 528 | """Activate window desktop, set input focus and raise it""" 529 | self.activate_window_desktop(window) 530 | X.XConfigureWindow(self.dpy, window, X.CWStackMode, 531 | X.ffi.new('XWindowChanges *', {'stack_mode': X.Above})) 532 | self.focus_window(window) 533 | 534 | def place_window_above(self, window): 535 | """Float up window in wm stack""" 536 | X.XConfigureWindow(self.dpy, window, X.CWStackMode, 537 | X.ffi.new('XWindowChanges *', {'stack_mode': X.Above})) 538 | self._flush() 539 | 540 | def place_window_below(self, window): 541 | """Float down window in wm stack""" 542 | X.XConfigureWindow(self.dpy, window, X.CWStackMode, 543 | X.ffi.new('XWindowChanges *', {'stack_mode': X.Below})) 544 | self._flush() 545 | 546 | def _change_window_hidden_state(self, window, p): 547 | """Minize window""" 548 | params = p, self.atom['_NET_WM_STATE_HIDDEN'] 549 | self._send_event(window, self.atom['_NET_WM_STATE'], list(params)) 550 | self._flush() 551 | 552 | def minimize_window(self, window): 553 | """Minize window""" 554 | self._change_window_hidden_state(window, 1) 555 | 556 | def restore_window(self, window): 557 | """Restore window""" 558 | self._change_window_hidden_state(window, 0) 559 | 560 | def set_window_state(self, window, taskbar=None, pager=None, 561 | decorate=None, otaskbar=None, vmax=None, hmax=None): 562 | """Set window state""" 563 | state_atom = self.atom['_NET_WM_STATE'] 564 | 565 | if decorate is not None: 566 | params = not decorate, self.atom[self.undecorated_atom_name] 567 | self._send_event(window, state_atom, list(params)) 568 | 569 | if taskbar is not None: 570 | params = not taskbar, self.atom['_NET_WM_STATE_SKIP_TASKBAR'] 571 | self._send_event(window, state_atom, list(params)) 572 | 573 | if vmax is not None and vmax == hmax: 574 | params = vmax, self.atom['_NET_WM_STATE_MAXIMIZED_VERT'], \ 575 | self.atom['_NET_WM_STATE_MAXIMIZED_HORZ'] 576 | self._send_event(window, state_atom, list(params)) 577 | 578 | if otaskbar is not None: 579 | params = [] if otaskbar else [self.atom['_ORCSOME_SKIP_TASKBAR']] 580 | X.set_window_property(self.dpy, window, self.atom['_ORCSOME_STATE'], 581 | self.atom['ATOM'], 32, params) 582 | 583 | if pager is not None: 584 | params = not pager, self.atom['_NET_WM_STATE_SKIP_PAGER'] 585 | self._send_event(window, state_atom, list(params)) 586 | 587 | self._flush() 588 | 589 | 590 | def get_window_geometry(self, window): 591 | """Get window geometry 592 | 593 | Returns window geometry without decorations""" 594 | root_ret = X.ffi.new('Window *') 595 | x = X.ffi.new('int *') 596 | y = X.ffi.new('int *') 597 | w = X.ffi.new('unsigned int *') 598 | h = X.ffi.new('unsigned int *') 599 | border_width = X.ffi.new('unsigned int *') 600 | depth = X.ffi.new('unsigned int *') 601 | X.XGetGeometry(self.dpy, window, root_ret, x, y, w, h, border_width, depth) 602 | return x[0], y[0], w[0], h[0] 603 | 604 | def get_screen_size(self): 605 | """Get size of screen (root window)""" 606 | return self.get_window_geometry(self.root)[2:] 607 | 608 | def get_workarea(self, desktop=None): 609 | """Get workarea geometery 610 | 611 | :param desktop: Desktop for working area receiving. If None then current_desktop is using""" 612 | result = X.get_window_property(self.dpy, self.root, 613 | self.atom['_NET_WORKAREA'], self.atom['CARDINAL']) 614 | if desktop is None: 615 | desktop = self.current_desktop 616 | return result[4*desktop:4*desktop+4] 617 | 618 | def moveresize_window(self, window, x=None, y=None, w=None, h=None): 619 | """Change window geometry""" 620 | flags = 0 621 | flags |= 2 << 12 622 | if x is not None: 623 | flags |= 1 << 8 624 | if y is not None: 625 | flags |= 1 << 9 626 | if w is not None: 627 | flags |= 1 << 10 628 | if h is not None: 629 | flags |= 1 << 11 630 | # Workarea offsets 631 | o_x, o_y, _, _ = tuple(self.get_workarea()) 632 | params = flags, x+o_x, y+o_y, max(1, w), max(1, h) 633 | self._send_event(window, self.atom['_NET_MOVERESIZE_WINDOW'], list(params)) 634 | self._flush() 635 | 636 | def moveresize_window2(self, window, left, top, right, bottom): 637 | """Change window geometry""" 638 | flags = 0x2f00 639 | # Workarea offsets 640 | dl, dt, dw, dh = tuple(self.get_workarea(window.desktop)) 641 | params = flags, left + dl, top + dt, max(1, dw - right - left), max(1, dh - bottom - top) 642 | self._send_event(window, self.atom['_NET_MOVERESIZE_WINDOW'], list(params)) 643 | self._flush() 644 | 645 | def close_window(self, window=None): 646 | """Send request to wm to close window""" 647 | window = window or self.current_window 648 | if not window: return 649 | self._send_event(window, self.atom['_NET_CLOSE_WINDOW'], [X.CurrentTime]) 650 | self._flush() 651 | 652 | def change_window_desktop(self, window, desktop): 653 | """Move window to ``desktop``""" 654 | if desktop < 0: 655 | return 656 | 657 | self._send_event(window, self.atom['_NET_WM_DESKTOP'], [desktop]) 658 | self._flush() 659 | 660 | def stop(self, is_exit=False): 661 | self.key_handlers.clear() 662 | self.property_handlers.clear() 663 | self.create_handlers[:] = [] 664 | self.destroy_handlers.clear() 665 | self.focus_history[:] = [] 666 | 667 | if not is_exit: 668 | X.XUngrabKey(self.dpy, X.AnyKey, X.AnyModifier, self.root) 669 | for window in self.get_clients(): 670 | X.XUngrabKey(self.dpy, X.AnyKey, X.AnyModifier, window) 671 | 672 | for h in self.timer_handlers: 673 | h.stop() 674 | self.timer_handlers[:] = [] 675 | 676 | for h in self.deinit_handlers: 677 | try: 678 | h() 679 | except: 680 | logger.exception('Shutdown error') 681 | 682 | self.init_handlers[:] = [] 683 | self.deinit_handlers[:] = [] 684 | 685 | def grab_keyboard(self, func): 686 | logger.debug('Grab a keyboard with %r', func) 687 | if self.grab_keyboard_handler: 688 | return False 689 | 690 | result = X.XGrabKeyboard(self.dpy, self.root, False, X.GrabModeAsync, 691 | X.GrabModeAsync, X.CurrentTime) 692 | 693 | if result == 0: 694 | self.grab_keyboard_handler = func 695 | return True 696 | 697 | return False 698 | 699 | def ungrab_keyboard(self): 700 | logger.debug('Ungrab a keyboard with %r', self.grab_keyboard_handler) 701 | self.grab_keyboard_handler = None 702 | return X.XUngrabKeyboard(self.dpy, X.CurrentTime) 703 | 704 | def grab_pointer(self, func): 705 | if self.grab_pointer_handler: 706 | return False 707 | 708 | result = X.XGrabPointer(self.dpy, self.root, False, 0, 709 | X.GrabModeAsync, X.GrabModeAsync, X.NONE, X.NONE, X.CurrentTime) 710 | 711 | if result == 0: 712 | self.grab_pointer_handler = func 713 | return True 714 | 715 | return False 716 | 717 | def ungrab_pointer(self): 718 | self.grab_pointer_handler = None 719 | return X.XUngrabPointer(self.dpy, X.CurrentTime) 720 | 721 | def on_init(self, func): 722 | self.init_handlers.append(func) 723 | return func 724 | 725 | def on_deinit(self, func): 726 | self.deinit_handlers.append(func) 727 | return func 728 | 729 | def get_screen_saver_info(self): 730 | result = X.ffi.new('XScreenSaverInfo *') 731 | X.XScreenSaverQueryInfo(self.dpy, self.root, result) 732 | return result 733 | 734 | def reset_dpms(self): 735 | power = X.ffi.new('unsigned short *') 736 | state = X.ffi.new('unsigned char *') 737 | if X.DPMSInfo(self.dpy, power, state): 738 | if state[0]: 739 | X.DPMSDisable(self.dpy) 740 | X.DPMSEnable(self.dpy) 741 | 742 | 743 | class ImmediateWM(WM): 744 | def __init__(self): 745 | self.dpy = X.XOpenDisplay(X.NULL) 746 | if self.dpy == X.NULL: 747 | raise Exception("Can't open display") 748 | 749 | self.root = X.DefaultRootWindow(self.dpy) 750 | self.atom = X.AtomCache(self.dpy) 751 | self.undecorated_atom_name = '_OB_WM_STATE_UNDECORATED' 752 | 753 | 754 | @X.ffi.callback('XErrorHandler') 755 | def error_handler(display, error): 756 | msg = X.ffi.new('char[100]') 757 | X.XGetErrorText(display, error.error_code, msg, 100) 758 | logger.error('{} ({}:{})'.format(nstr(X.ffi.string(msg)), 759 | error.request_code, error.minor_code)) 760 | return 0 761 | -------------------------------------------------------------------------------- /orcsome/wrappers.py: -------------------------------------------------------------------------------- 1 | from . utils import cached_property, match_string, nstr 2 | from . import xlib as X 3 | 4 | 5 | class Window(int): 6 | @cached_property 7 | def desktop(self): 8 | """Return window desktop. 9 | 10 | Result is: 11 | 12 | * number from 0 to desktop_count - 1 13 | * -1 if window placed on all desktops 14 | * None if window does not have desktop property 15 | 16 | """ 17 | d = self.get_property('_NET_WM_DESKTOP') 18 | if d: 19 | d = d[0] 20 | if d == 0xffffffff: 21 | return -1 22 | else: 23 | return d 24 | 25 | @cached_property 26 | def role(self): 27 | """Return WM_WINDOW_ROLE property""" 28 | return nstr(self.get_property('WM_WINDOW_ROLE', 'STRING'), 'utf-8') 29 | 30 | def get_name_and_class(self): 31 | """Return WM_CLASS property""" 32 | result = self.get_property('WM_CLASS', 'STRING', split=True) 33 | if not result: 34 | return None, None 35 | 36 | return nstr(result[0], 'utf-8'), nstr(result[1], 'utf-8') 37 | 38 | @cached_property 39 | def cls(self): 40 | """Return second part from WM_CLASS property""" 41 | self.name, cls = self.get_name_and_class() 42 | return cls 43 | 44 | @cached_property 45 | def name(self): 46 | """Return first part from WM_CLASS property""" 47 | name, self.cls = self.get_name_and_class() 48 | return name 49 | 50 | @cached_property 51 | def title(self): 52 | """Return _NET_WM_NAME property""" 53 | return nstr(self.get_property('_NET_WM_NAME', 'UTF8_STRING'), 'utf-8') 54 | 55 | def matches(self, name=None, cls=None, role=None, desktop=None, title=None): 56 | """Check if window suits given matchers. 57 | 58 | Matchers keyword arguments are used in :meth:`on_create`, 59 | :func:`~orcsome.actions.spawn_or_raise`. :meth:`find_clients` and 60 | :meth:`find_client`. 61 | 62 | name 63 | window name (also referenced as `instance`). 64 | The first part of ``WM_CLASS`` property. 65 | 66 | cls 67 | window class. The second part of ``WM_CLASS`` property. 68 | 69 | role 70 | window role. Value of ``WM_WINDOW_ROLE`` property. 71 | 72 | desktop 73 | matches windows placed on specific desktop. Must be int. 74 | 75 | title 76 | window title. 77 | 78 | `name`, `cls`, `title` and `role` can be regular expressions. 79 | 80 | """ 81 | if name and not match_string(name, self.name): return False 82 | if cls and not match_string(cls, self.cls): return False 83 | if role and not match_string(role, self.role): return False 84 | if title and not match_string(title, self.title): return False 85 | if desktop is not None and desktop != self.desktop: return False 86 | 87 | return True 88 | 89 | @cached_property 90 | def state(self): 91 | """Return _NET_WM_STATE""" 92 | return self.get_property('_NET_WM_STATE', 'ATOM') or [] 93 | 94 | def get_property(self, property, type=None, **kwargs): 95 | atom = self.wm.atom 96 | return X.get_window_property(self.wm.dpy, self, atom[property], 97 | atom[type] if type else 0, **kwargs) 98 | 99 | @cached_property 100 | def maximized_vert(self): 101 | return self.wm.atom['_NET_WM_STATE_MAXIMIZED_VERT'] in self.state 102 | 103 | @cached_property 104 | def maximized_horz(self): 105 | return self.wm.atom['_NET_WM_STATE_MAXIMIZED_HORZ'] in self.state 106 | 107 | @cached_property 108 | def decorated(self): 109 | return self.wm.atom[self.wm.undecorated_atom_name] not in self.state 110 | 111 | @cached_property 112 | def urgent(self): 113 | return self.wm.atom['_NET_WM_STATE_DEMANDS_ATTENTION'] in self.state 114 | 115 | @cached_property 116 | def fullscreen(self): 117 | return self.wm.atom['_NET_WM_STATE_FULLSCREEN'] in self.state 118 | -------------------------------------------------------------------------------- /orcsome/xlib.py: -------------------------------------------------------------------------------- 1 | from array import array 2 | from ._xlib import ffi, lib 3 | from .utils import bstr, btype 4 | 5 | NULL = ffi.NULL 6 | globals().update(lib.__dict__) 7 | 8 | NONE = 0 9 | 10 | 11 | class AtomCache(object): 12 | def __init__(self, dpy): 13 | self.dpy = dpy 14 | self._cache = {} 15 | 16 | def __getitem__(self, name): 17 | try: 18 | return self._cache[name] 19 | except KeyError: 20 | pass 21 | 22 | atom = self._cache[name] = XInternAtom(self.dpy, bstr(name), False) 23 | return atom 24 | 25 | 26 | ITEM_SIZE = array('L').itemsize 27 | 28 | def get_window_property(display, window, property, type=0, split=False, size=50): 29 | type_return = ffi.new('Atom *') 30 | fmt_return = ffi.new('int *') 31 | nitems_return = ffi.new('unsigned long *') 32 | bytes_after = ffi.new('unsigned long *') 33 | data = ffi.new('unsigned char **') 34 | XGetWindowProperty(display, window, property, 0, size, False, type, 35 | type_return, fmt_return, nitems_return, bytes_after, data) 36 | 37 | fmt = fmt_return[0] 38 | bafter = bytes_after[0] 39 | result = b'' 40 | if fmt == 32: 41 | result += btype(ffi.buffer(data[0], nitems_return[0]*ITEM_SIZE)) 42 | elif fmt == 8: 43 | result += btype(ffi.buffer(data[0], nitems_return[0])) 44 | elif not fmt: 45 | return None 46 | else: 47 | raise Exception('Unknown format {}'.format(fmt)) 48 | 49 | if bafter: 50 | XFree(data[0]) 51 | XGetWindowProperty(display, window, property, size, bafter / 4 + 1, 52 | False, type, type_return, fmt_return, nitems_return, bytes_after, data) 53 | fmt = fmt_return[0] 54 | if fmt == 32: 55 | result += btype(ffi.buffer(data[0], nitems_return[0]*ITEM_SIZE)) 56 | elif fmt == 8: 57 | result += btype(ffi.buffer(data[0], nitems_return[0])) 58 | else: 59 | raise Exception('Unknown format {}'.format(fmt)) 60 | 61 | if fmt_return[0] == 32: 62 | result = array('L', result) 63 | elif fmt_return[0] == 8: 64 | result = result.rstrip(b'\x00') 65 | if split: 66 | result = result.split(b'\x00') 67 | else: 68 | raise Exception('Unknown format {}'.format(fmt_return[0])) 69 | 70 | XFree(data[0]) 71 | return result 72 | 73 | 74 | def set_window_property(display, window, property, type, fmt, values): 75 | if fmt == 32: 76 | if values: 77 | data = ffi.cast('unsigned char *', ffi.new('XID[]', values)) 78 | else: 79 | data = b'' 80 | elif fmt == 8: 81 | data = values 82 | else: 83 | raise Exception('Unknown format {}'.format(fmt)) 84 | 85 | XChangeProperty(display, window, property, type, fmt, PropModeReplace, 86 | data, len(values)) 87 | 88 | 89 | def get_kbd_group(display): 90 | state = ffi.new('XkbStateRec *') 91 | XkbGetState(display, XkbUseCoreKbd, state) 92 | return state[0].group 93 | 94 | 95 | def set_kbd_group(display, group): 96 | XkbLockGroup(display, XkbUseCoreKbd, group) 97 | XFlush(display) 98 | -------------------------------------------------------------------------------- /orcsome/xlib_build.py: -------------------------------------------------------------------------------- 1 | import cffi 2 | 3 | ffi = cffi.FFI() 4 | 5 | ffi.set_source('orcsome._xlib', """ 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | """, libraries=['X11', 'Xss', 'Xext']) 13 | 14 | ffi.cdef(""" 15 | static const long StructureNotifyMask; 16 | static const long SubstructureNotifyMask; 17 | static const long SubstructureRedirectMask; 18 | static const long PropertyChangeMask; 19 | static const long FocusChangeMask; 20 | 21 | static const long CurrentTime; 22 | 23 | static const int KeyPress; 24 | static const int KeyRelease; 25 | static const int CreateNotify; 26 | static const int DestroyNotify; 27 | static const int FocusIn; 28 | static const int FocusOut; 29 | static const int PropertyNotify; 30 | static const int ClientMessage; 31 | 32 | static const int CWX; 33 | static const int CWY; 34 | static const int CWWidth; 35 | static const int CWHeight; 36 | static const int CWBorderWidth; 37 | static const int CWSibling; 38 | static const int CWStackMode; 39 | 40 | static const int Above; 41 | static const int Below; 42 | 43 | static const int ShiftMask; 44 | static const int LockMask; 45 | static const int ControlMask; 46 | static const int Mod1Mask; 47 | static const int Mod2Mask; 48 | static const int Mod3Mask; 49 | static const int Mod4Mask; 50 | static const int Mod5Mask; 51 | static const int AnyKey; 52 | static const int AnyModifier; 53 | 54 | static const long NoSymbol; 55 | 56 | static const int GrabModeSync; 57 | static const int GrabModeAsync; 58 | 59 | static const int PropModeReplace; 60 | static const int PropModePrepend; 61 | static const int PropModeAppend; 62 | 63 | static const int XkbUseCoreKbd; 64 | 65 | 66 | typedef int Bool; 67 | typedef int Status; 68 | typedef unsigned long XID; 69 | typedef unsigned long Time; 70 | typedef unsigned long Atom; 71 | typedef XID Window; 72 | typedef XID Drawable; 73 | typedef XID KeySym; 74 | typedef XID Cursor; 75 | typedef unsigned char KeyCode; 76 | typedef ... Display; 77 | 78 | typedef struct { 79 | int type; 80 | unsigned long serial; /* # of last request processed by server */ 81 | Bool send_event; /* true if this came from a SendEvent request */ 82 | Display *display;/* Display the event was read from */ 83 | Window window; /* window on which event was requested in event mask */ 84 | } XAnyEvent; 85 | 86 | typedef struct { 87 | int type; 88 | unsigned long serial; /* # of last request processed by server */ 89 | Bool send_event; /* true if this came from a SendEvent request */ 90 | Display *display; /* Display the event was read from */ 91 | Window window; 92 | Atom message_type; 93 | int format; 94 | union { 95 | char b[20]; 96 | short s[10]; 97 | long l[5]; 98 | } data; 99 | } XClientMessageEvent; 100 | 101 | typedef struct { 102 | int type; /* of event */ 103 | unsigned long serial; /* # of last request processed by server */ 104 | Bool send_event; /* true if this came from a SendEvent request */ 105 | Display *display; /* Display the event was read from */ 106 | Window window; /* "event" window it is reported relative to */ 107 | Window root; /* root window that the event occurred on */ 108 | Window subwindow; /* child window */ 109 | Time time; /* milliseconds */ 110 | int x, y; /* pointer x, y coordinates in event window */ 111 | int x_root, y_root; /* coordinates relative to root */ 112 | unsigned int state; /* key or button mask */ 113 | unsigned int keycode; /* detail */ 114 | Bool same_screen; /* same screen flag */ 115 | } XKeyEvent; 116 | 117 | typedef struct { 118 | int type; 119 | unsigned long serial; /* # of last request processed by server */ 120 | Bool send_event; /* true if this came from a SendEvent request */ 121 | Display *display; /* Display the event was read from */ 122 | Window parent; /* parent of the window */ 123 | Window window; /* window id of window created */ 124 | int x, y; /* window location */ 125 | int width, height; /* size of window */ 126 | int border_width; /* border width */ 127 | Bool override_redirect; /* creation should be overridden */ 128 | } XCreateWindowEvent; 129 | 130 | typedef struct { 131 | int type; 132 | unsigned long serial; /* # of last request processed by server */ 133 | Bool send_event; /* true if this came from a SendEvent request */ 134 | Display *display; /* Display the event was read from */ 135 | Window event; 136 | Window window; 137 | } XDestroyWindowEvent; 138 | 139 | typedef struct { 140 | int type; /* FocusIn or FocusOut */ 141 | unsigned long serial; /* # of last request processed by server */ 142 | Bool send_event; /* true if this came from a SendEvent request */ 143 | Display *display; /* Display the event was read from */ 144 | Window window; /* window of event */ 145 | int mode; /* NotifyNormal, NotifyWhileGrabbed, 146 | NotifyGrab, NotifyUngrab */ 147 | int detail; 148 | /* 149 | * NotifyAncestor, NotifyVirtual, NotifyInferior, 150 | * NotifyNonlinear,NotifyNonlinearVirtual, NotifyPointer, 151 | * NotifyPointerRoot, NotifyDetailNone 152 | */ 153 | } XFocusChangeEvent; 154 | 155 | typedef struct { 156 | int type; 157 | unsigned long serial; /* # of last request processed by server */ 158 | Bool send_event; /* true if this came from a SendEvent request */ 159 | Display *display; /* Display the event was read from */ 160 | Window window; 161 | Atom atom; 162 | Time time; 163 | int state; /* NewValue, Deleted */ 164 | } XPropertyEvent; 165 | 166 | typedef union { 167 | int type; 168 | XAnyEvent xany; 169 | XKeyEvent xkey; 170 | XCreateWindowEvent xcreatewindow; 171 | XDestroyWindowEvent xdestroywindow; 172 | XFocusChangeEvent xfocus; 173 | XPropertyEvent xproperty; 174 | ...; 175 | } XEvent; 176 | 177 | typedef struct { 178 | int x, y; 179 | int width, height; 180 | int border_width; 181 | Window sibling; 182 | int stack_mode; 183 | } XWindowChanges; 184 | 185 | typedef struct { 186 | Window window; 187 | int state; 188 | int kind; 189 | unsigned long til_or_since; 190 | unsigned long idle; 191 | unsigned long eventMask; 192 | } XScreenSaverInfo; 193 | 194 | typedef struct { 195 | int type; 196 | Display *display; /* Display the event was read from */ 197 | XID resourceid; /* resource id */ 198 | unsigned long serial; /* serial number of failed request */ 199 | unsigned char error_code; /* error code of failed request */ 200 | unsigned char request_code; /* Major op-code of failed request */ 201 | unsigned char minor_code; /* Minor op-code of failed request */ 202 | } XErrorEvent; 203 | typedef int (*XErrorHandler) ( Display* display, XErrorEvent* event); 204 | int XGetErrorText(Display *display, int code, char *buffer_return, int length); 205 | 206 | 207 | XErrorHandler XSetErrorHandler (XErrorHandler handler); 208 | 209 | Display* XOpenDisplay(char *display_name); 210 | int XCloseDisplay(Display *display); 211 | int XFree(void *data); 212 | Atom XInternAtom(Display *display, char *atom_name, Bool only_if_exists); 213 | 214 | int XPending(Display *display); 215 | int XNextEvent(Display *display, XEvent *event_return); 216 | int XSelectInput(Display *display, Window w, long event_mask); 217 | int XFlush(Display *display); 218 | int XSync(Display *display, Bool discard); 219 | Status XSendEvent(Display *display, Window w, Bool propagate, long event_mask, XEvent *event_send); 220 | 221 | KeySym XStringToKeysym(char *string); 222 | KeyCode XKeysymToKeycode(Display *display, KeySym keysym); 223 | 224 | int XGrabKey(Display *display, int keycode, unsigned int modifiers, 225 | Window grab_window, Bool owner_events, int pointer_mode, int keyboard_mode); 226 | int XUngrabKey(Display *display, int keycode, unsigned int modifiers, Window grab_window); 227 | int XGrabKeyboard(Display *display, Window grab_window, Bool owner_events, 228 | int pointer_mode, int keyboard_mode, Time time); 229 | int XUngrabKeyboard(Display *display, Time time); 230 | int XGrabPointer(Display *display, Window grab_window, Bool owner_events, 231 | unsigned int event_mask, int pointer_mode, int keyboard_mode, 232 | Window confine_to, Cursor cursor, Time time); 233 | int XUngrabPointer(Display *display, Time time); 234 | 235 | int XGetWindowProperty(Display *display, Window w, Atom property, 236 | long long_offset, long long_length, Bool delete, Atom req_type, 237 | Atom *actual_type_return, int *actual_format_return, 238 | unsigned long *nitems_return, unsigned long *bytes_after_return, 239 | unsigned char **prop_return); 240 | int XChangeProperty(Display *display, Window w, Atom property, Atom type, 241 | int format, int mode, unsigned char *data, int nelements); 242 | int XDeleteProperty(Display *display, Window w, Atom property); 243 | int XConfigureWindow(Display *display, Window w, unsigned int value_mask, 244 | XWindowChanges *changes); 245 | 246 | Status XGetGeometry(Display *display, Drawable d, Window *root_return, 247 | int *x_return, int *y_return, unsigned int *width_return, 248 | unsigned int *height_return, unsigned int *border_width_return, unsigned int *depth_return); 249 | 250 | Status XScreenSaverQueryInfo(Display *dpy, Drawable drawable, XScreenSaverInfo *saver_info); 251 | 252 | Status DPMSInfo (Display *display, unsigned short *power_level, unsigned char *state); 253 | Status DPMSEnable (Display *display); 254 | Status DPMSDisable (Display *display); 255 | 256 | Window DefaultRootWindow(Display *display); 257 | int ConnectionNumber(Display *display); 258 | 259 | typedef struct { 260 | unsigned char group; 261 | unsigned char locked_group; 262 | unsigned short base_group; 263 | unsigned short latched_group; 264 | unsigned char mods; 265 | unsigned char base_mods; 266 | unsigned char latched_mods; 267 | unsigned char locked_mods; 268 | unsigned char compat_state; 269 | unsigned char grab_mods; 270 | unsigned char compat_grab_mods; 271 | unsigned char lookup_mods; 272 | unsigned char compat_lookup_mods; 273 | unsigned short ptr_buttons; 274 | } XkbStateRec; 275 | 276 | Status XkbGetState (Display *display, unsigned int device_spec, XkbStateRec *state_return); 277 | Bool XkbLockGroup (Display *display, unsigned int device_spec, unsigned int group); 278 | """) 279 | 280 | if __name__ == "__main__": 281 | ffi.compile(verbose=True) 282 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [upload_docs] 2 | upload_dir=docs/_build/html 3 | 4 | [install] 5 | single_version_externally_managed=1 6 | record=/dev/null 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | import orcsome 4 | 5 | setup( 6 | name = 'orcsome', 7 | version = orcsome.VERSION, 8 | author = 'Anton Bobrov', 9 | author_email = 'baverman+dev@gmail.com', 10 | description = 'Scripting extension for NETWM compliant window managers', 11 | long_description = open('README.rst').read(), 12 | zip_safe = False, 13 | packages = find_packages(exclude=('tests', )), 14 | cffi_modules=["orcsome/ev_build.py:ffi", "orcsome/xlib_build.py:ffi"], 15 | setup_requires=["cffi>=1.0.0"], 16 | install_requires = ['cffi>=1.0.0'], 17 | include_package_data = True, 18 | scripts = ['bin/orcsome'], 19 | url = 'https://github.com/baverman/orcsome', 20 | classifiers = [ 21 | "Programming Language :: Python", 22 | "Programming Language :: Python :: 2.7", 23 | "License :: OSI Approved :: MIT License", 24 | "Development Status :: 4 - Beta", 25 | "Environment :: X11 Applications", 26 | "Topic :: Desktop Environment :: Window Managers", 27 | "Intended Audience :: Developers", 28 | "Intended Audience :: End Users/Desktop", 29 | "Natural Language :: English", 30 | ], 31 | ) 32 | --------------------------------------------------------------------------------