├── .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 |
7 |
8 |
11 |
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 |
--------------------------------------------------------------------------------