qtile-0.31.0/0000775000175000017500000000000014762660347012636 5ustar epsilonepsilonqtile-0.31.0/MANIFEST.in0000664000175000017500000000151214762660347014373 0ustar epsilonepsiloninclude CHANGELOG include LICENSE include README.rst include MANIFEST.in include CONTRIBUTING.md include .pre-commit-config.yaml include builder.py exclude .coveragerc exclude .pylintrc exclude flake.nix exclude flake.lock exclude nix/* exclude tox.ini exclude requirements.txt exclude requirements-dev.txt exclude libqtile/_ffi*.py exclude libqtile/backend/x11/_ffi*.py exclude libqtile/backend/wayland/_ffi*.* exclude libqtile/backend/wayland/_libinput*.* exclude Makefile exclude dev.sh exclude logo.png exclude .readthedocs.yaml exclude .git-blame-ignore-revs graft libqtile/resources graft libqtile/widget/helpers graft resources graft test recursive-include libqtile *.typed prune bin prune docs prune scripts prune rpm include scripts/ffibuild recursive-exclude * __pycache__ recursive-exclude * *.py[co] recursive-exclude stubs * qtile-0.31.0/setup.cfg0000664000175000017500000000213014762660347014453 0ustar epsilonepsilon[options] packages = find_namespace: python_requires >= 3.10 install_requires = cffi >= 1.1.0 cairocffi[xcb] >= 1.6.0 xcffib >= 1.4.0 tests_require = ruff psutil coverage [options.extras_require] doc = sphinx sphinx_rtd_theme numpydoc lint = ruff coverage test = pytest >= 6.2.1 libcst >= 1.0.0 dbus-fast PyGObject ipython = ipykernel jupyter_console wayland = pywayland>=0.4.17 xkbcommon>=0.3 # NOTE: When updating the major or minor version of pywlroots here, also update: # tox.ini, docs/manual/install/index.rst, docs/manual/wayland.rst, builder.py pywlroots==0.17.0 widgets = dbus-fast imaplib2 iwlib keyring mailbox psutil pulsectl pulsectl_asyncio python-mpd2 pytz xdg xmltodict all = # All user dependencies i.e. excluding dev deps. %(ipython)s %(wayland)s %(widgets)s libcst setproctitle [options.package_data] libqtile.resources = battery-icons/*.png layout-icons/*.png [options.packages.find] exclude = test* [build_sphinx] source-dir = docs build-dir = docs/_build [check-manifest] ignore = stubs** qtile-0.31.0/requirements-dev.txt0000664000175000017500000000011314762660347016671 0ustar epsilonepsilonlibcst dbus-fast psutil coverage pytest-cov tox pre-commit PyGObject isort qtile-0.31.0/README.rst0000664000175000017500000000717514762660347014337 0ustar epsilonepsilon|logo| **A full-featured, hackable tiling window manager written and configured in Python** |website| |pypi| |ci| |rtd| |license| |ruff| |coverage| Features ======== * Simple, small and extensible. It's easy to write your own layouts, widgets and commands. * Configured in Python. * Runs as an X11 WM or a Wayland compositor. * Command shell that allows all aspects of Qtile to be managed and inspected. * Complete remote scriptability - write scripts to set up workspaces, manipulate windows, update status bar widgets and more. * Qtile's remote scriptability makes it one of the most thoroughly unit-tested window managers around. Community ========= Qtile is supported by a dedicated group of users. If you need any help, please don't hesitate to fire off an email to our mailing list or join us on IRC. You can also ask questions on the discussions board. :Mailing List: https://groups.google.com/group/qtile-dev :Q&A: https://github.com/qtile/qtile/discussions/categories/q-a :IRC: irc://irc.oftc.net:6667/qtile :Discord: https://discord.gg/ehh233wCrC (Bridged with IRC) Example code ============ Check out the `qtile-examples`_ repo which contains examples of users' configurations, scripts and other useful links. .. _`qtile-examples`: https://github.com/qtile/qtile-examples Contributing ============ Please report any suggestions, feature requests, bug reports, or annoyances to the GitHub `issue tracker`_. There are also a few `tips & tricks`_, and `guidelines`_ for contributing in the documentation. Please also consider submitting useful scripts etc. to the qtile-examples repo (see above). .. _`issue tracker`: https://github.com/qtile/qtile/issues .. _`tips & tricks`: https://docs.qtile.org/en/latest/manual/hacking.html .. _`guidelines`: https://docs.qtile.org/en/latest/manual/contributing.html .. |logo| image:: https://raw.githubusercontent.com/qtile/qtile/master/logo.png :alt: Logo :target: https://qtile.org .. |website| image:: https://img.shields.io/badge/website-qtile.org-blue.svg :alt: Website :target: https://qtile.org .. |pypi| image:: https://img.shields.io/pypi/v/qtile.svg :alt: PyPI :target: https://pypi.org/project/qtile/ .. |ci| image:: https://github.com/qtile/qtile/workflows/ci/badge.svg?branch=master :alt: CI status :target: https://github.com/qtile/qtile/actions .. |rtd| image:: https://readthedocs.org/projects/qtile/badge/?version=latest :alt: Read the Docs :target: https://docs.qtile.org/en/latest/ .. |license| image:: https://img.shields.io/github/license/qtile/qtile.svg :alt: License :target: https://github.com/qtile/qtile/blob/master/LICENSE .. |ruff| image:: https://img.shields.io/badge/code%20style-ruff-000000.svg :alt: Codestyle :target: https://github.com/astral-sh/ruff .. |coverage| image:: https://coveralls.io/repos/github/qtile/qtile/badge.svg :alt: Coverage :target: https://coveralls.io/github/qtile/qtile Maintainers =========== | `@tych0`_ GPG: ``3CCA B226 289D E016 0C61 BDB4 18D1 8F1B C464 DCA3`` | `@ramnes`_ GPG: ``99CC A84E 2C8C 74F3 2E12 AD53 8C17 0207 0803 487A`` | `@m-col`_ GPG: ``35D9 2E7C C735 7A81 173E A1C9 74F9 FDD2 0984 FBEC`` | `@flacjacket`_ GPG: ``58B5 F350 8339 BFE5 CA93 AC9F 439D 9701 E7EA C588`` | `@elParaguayo`_ GPG: ``A6BA A1E1 7D26 64AD B97B 2C6F 58A9 AA7C 8672 7DF7`` | `@jwijenbergh`_ GPG: ``B1C8 1CF3 063B 5836 4946 3687 4827 061B D417 C233`` .. _`@tych0`: https://github.com/tych0 .. _`@ramnes`: https://github.com/ramnes .. _`@m-col`: https://github.com/m-col .. _`@flacjacket`: https://github.com/flacjacket .. _`@elParaguayo`: https://github.com/elparaguayo .. _`@jwijenbergh`: https://github.com/jwijenbergh qtile-0.31.0/.git-blame-ignore-revs0000664000175000017500000000055114762660347016737 0ustar epsilonepsilon# formatted (#4985) 923eb6a209e13d65ed2af1c44d886823b43b8c66 # enabled and fixed pyuprade rules (#4985) 4ca8fd2fcac3bb1f076dc4ace5615b75e9e3cd92 # enabled and fixed flaek8-pie rules (#4985) 65a3d20e2a2423a650bbaa9d5e21dc44fd9366a7 # initial black -> ruff 13bee8befc81c1d5738c795b6b707e7ac26c616c # initial black run 5fed8371bdd32a185b8e7f1abd37e01a511f05b9 qtile-0.31.0/flake.lock0000664000175000017500000000106714762660347014576 0ustar epsilonepsilon{ "nodes": { "nixpkgs": { "locked": { "lastModified": 1734649271, "narHash": "sha256-4EVBRhOjMDuGtMaofAIqzJbg4Ql7Ai0PSeuVZTHjyKQ=", "owner": "nixos", "repo": "nixpkgs", "rev": "d70bd19e0a38ad4790d3913bf08fcbfc9eeca507", "type": "github" }, "original": { "owner": "nixos", "ref": "nixos-unstable", "repo": "nixpkgs", "type": "github" } }, "root": { "inputs": { "nixpkgs": "nixpkgs" } } }, "root": "root", "version": 7 } qtile-0.31.0/nix/0000775000175000017500000000000014762660347013434 5ustar epsilonepsilonqtile-0.31.0/nix/overlays.nix0000664000175000017500000000245514762660347016026 0ustar epsilonepsilonself: final: prev: { pythonPackagesOverlays = (prev.pythonPackagesOverlays or [ ]) ++ [ (_: pprev: { qtile = (pprev.qtile.overrideAttrs ( old: let flakever = self.shortRev or "dev"; releases = ( builtins.filter (x: !builtins.isList x && prev.lib.strings.hasPrefix "Qtile" x) ( builtins.split "\n" (builtins.readFile ../CHANGELOG) ) ); # the 0th element is the template current-release-title = builtins.elemAt releases 1; symver = builtins.head ( builtins.match "Qtile ([0-9.]+), released ([0-9-]+):" current-release-title ); in { version = "${symver}+${flakever}.flake"; # use the source of the git repo src = ./..; # for qtile migrate, not in nixpkgs yet propagatedBuildInputs = (old.propagatedBuildInputs or [ ]) ++ [ pprev.libcst ]; } )).override { wlroots = prev.wlroots_0_17; }; }) ]; python3 = let self = prev.python3.override { inherit self; packageOverrides = prev.lib.composeManyExtensions final.pythonPackagesOverlays; }; in self; python3Packages = final.python3.pkgs; } qtile-0.31.0/requirements.txt0000664000175000017500000000010314762660347016114 0ustar epsilonepsiloncairocffi >= 1.6.0 cffi >= 1.1.0 xcffib >= 1.4.0 pycairo >= 1.25.1 qtile-0.31.0/setup.py0000664000175000017500000000427214762660347014355 0ustar epsilonepsilon#!/usr/bin/env python3 # Copyright (c) 2008 Aldo Cortesi # Copyright (c) 2011 Mounier Florian # Copyright (c) 2012 dmpayton # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 roger # Copyright (c) 2014 Pedro Algarvio # Copyright (c) 2014-2015 Tycho Andersen # Copyright (c) 2023 Matt Colligan # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import importlib import sys from pathlib import Path from setuptools import setup ROOT = Path(__file__).parent.resolve() sys.path.insert(0, ROOT.as_posix()) def can_import(module): try: importlib.import_module(module) except Exception: return False return True def get_cffi_modules(): # Check we have cffi around. If not, none of these will get built. if not can_import("cffi.pkgconfig"): print("CFFI package is missing") return cffi_modules = [] # Wayland backend dependencies if can_import("wlroots.ffi_build"): cffi_modules.append("libqtile/backend/wayland/cffi/build.py:ffi") else: print("Failed to find pywlroots. Wayland backend dependencies not built.") return cffi_modules setup( use_scm_version=True, cffi_modules=get_cffi_modules(), include_package_data=True, ) qtile-0.31.0/logo.png0000664000175000017500000005100414762660347014304 0ustar epsilonepsilonPNG  IHDRXzTXtRaw profile type exifxڭgv[ǖ(zNT\͠߾%Jl {+CA~rOeR^?H!DsnF.D{Μ <:dsX. 90 y^aO<ǸbzkדGEL%ގy3 w,p/՛6н* 8PHHᾥȠ=anlpbZ^[s:Wk"6%$ %c 863XyL9N2 iQssO ϵmDRI Dr6F K֬(bZQRkmRZi' z{IC\1ƌ3] N q˪ij=u튪soe?6漻[ gw_coqqsߍ J>J>rv%9RRܥj|:4%^ϖvm1B/ʃmy'N\[0gw/Rl+L9~[6X 6`<ܤ)–TrnOi{vkf ;=Ʋ^{:Q).ϳzºi'ٓg w籶4vYw Ȭ{Mimth ~vJދ$kZdU14|R4;+g8HvrY%b 85K#zS{${bq}'g>LଫK]Z=Li\ jAγnkyN ~\ycKlgS[:%'wv$u]9^n렾L۬>xJynf6U*{aEpaRIb^:+E!; 74PC.Kj TX ԧ(JxVÍiPՙō9}VKw& J0,7c$TpWG ʓw00KXT_Ajlil5=ՂB8G%[H*U,zEȺH=NP:KG?PULJ.K,K7@CEG\vU pp0ݸ$[hej$lcvFܫzHhDثЦ|KNbZ$ARToPuPʵIA-e=YY[?ކT,5)TxuSuh+;l4y? Y' {}z0 d/UOY֓Hvbp*BG19(hd: 5]6)5$~6cD'Ejpb76}&.D+ij,{07XQlDG?9FICx~Tw Ӓљ4N YK0yG0=pvS֠BYʮPN7B/ILh~3i/05-m8%AN \OK^H4/dU >;/䍸 `=xCH>HV,lb+= @ zxfϢ";4liTt/_[QS$ג@_NtQW!"> zѫBC,aU9B\ȵ!Ϧ)Гu{G_ݠb@ȫ P$+7.Y9M4A)怂R8FLs:z `Ө)Ne}Zˉ^ -J+P `Tl՝6HJxE(:}LE5bjERx ";ekOs-%u4W(.jvhp:)zH,2N :OYGwSV`d}z@>0YM}]X+V}PׄXLZPsv/+vJ`ǦHj@nB q^uXO39  ĒrD"sXnvCG'2` "/lT $2o{8𠳓m _zmC71ԤjЀlױ1Ou$p+ދx-a:+X;D!T -:}{t*H!Է ^7?0 r@r^U^T$J&v;=РiSqUf9{Dw7<t5ٱS!,'0ԹP_9JajWi ߍr1ʑJccSh03DV4 YZO9S}Mc A]6̎^{_: HiY.lNY'\$gQ?H.N<0$"AWV=Xy>qF}2Gư`Єjvƅ^4F  Ȇ8 H>b9ǿQt:E{.]\E[]A E}f\Aȱ"ϑr"0]GȀA{Q`f)M܀׊&BPY:^B}[O@+z_xM :7># \Za J hӈM=  |>LKW琨F ТȋHYoi&u>-qS ѓ%&Y:Mw[9`@**[j2uYնH@4i*k'@ c[ 1= 7M3P co3,9WE)lP?i0Ynz_ǜ"{hXL HD:L$MqQamK( uHNe0+2XSFk\Toӑ,|FM,UL>'ꥸЩ3!΄` 3 N8Kgpv(aSj+6Bb\C.b1">:,g?> DW"$&J$T" ߍr E!A$k- oqA$XdO1,up^Fs,E!"ч?x 6-x=}`>b!X$,MOOL&Ad<ϗYVWJaEQG #XG<crKGSAZ[q\J*dV3@~m{;W Uoa /{FY< ±ciJr$q{$_!G eب ԕwDYF fP*%Z]\=Pv@u;IOa0oADT$T*Aoٺqf@BK ޡWdHAсLRX,nd?2 {TV}x׶Rg.qJh+ F"z0Ɣe2TPADL"ɦfffNg)!# qjPAD< z}W,x(${b cL::A㸒ԩ>Q?p~up!F#7}j *jh= x"STZ}Xmu$i,IL#0:AqB #B !BQ*S] /JR9UPh$X@)zrN8:Aq:.Ï@u}),ߞzeL<} 2N71J "10Ru:ki!<䌎tr|2hADB"'<t'$XLQgpG!H@8_{߯@łE!**WAI`RU*Uf?GX?<0^_cdcZ:tA MTgD HoADR8D.gB@b*aq8˦cFpq 'FSb#]x2DP(0rH >jFFSSAh $'lmHM*j$cz:do RSShV!qn. Bgg'\.}hsrǬW*9s&ZBRy8NXVر:jjjH$Ƙ1ND9 VDM,6r(Jddd`=z4ӡT* NV B!"n7`6 ݎٳ;w%T*3wP(rЀ7bϞ=:r .rmDzz:0eK|IDD"@S޹D<(^EVTT&LɓB\,("\.N'78t M~t1}t\}՘1c4 r9cG?v؁W_}[lAdqUWA~ pm _|DQ5{fP~΃X(_TBH@բ_|1O\dfffr92 (//Icc#PUUFl6:(8^x~[1|31{l^o'O+2<6l~MMMtK:R0Y/0AШ ,ZSNŴiӠj#0"E.%%%P*hkkáC0W+PXXcF#>#QE@ jqUWAsrrp-@RaŊ Ju].7hXe<8vXL_xq 7@R8ݎ{~-T dDqW\h0n NZܯu:nf?sni+QՅVXVѣ8qF#L&xGJJ Q\\bB#33j:.馛҂uRRRW^ye9 'Oիcbij>*,ZP*yw͖}a͚54 d0{lr-HMMxV۷o6oތF޽ !Gaȑ7n0k,@ĴWNN~ԩSػwo^`0`ҥ~<222q\\hN'vڅ]vA"-- j6 FF[$ĉq]>;Htb߾}صk֬YK CСC8 q饗bҤI9rdLjs1P^^ vN>HKKߎE Ž;sNsŋQ\\֨P%RYYKGBU۰hѢ %<㦛nٳ9rpA;XzuT%9~8???яpW"///Wr .7|?0*ϙe˖/&yDr@$PYYK.$국G_DKKKBFNL& XnnF̛7/#RRRp7Ҥ;]tр)OC=zr-ɉsngu>_#݋kQKk?񈣜,[ \pqdp\4ߕOIIAfffhAJ p8dB[[F#_B@c+1qĨ5nUUU&UBݎ^{ {=܃cF%e<.]_}U؏]w݅yIZFeee (FGG:::"N+*JL0 ,t:q[{2Cxe2RSSQVV3f`ܸqtHIIN^R=DQBWWf3:;;aXpalٲ{AGG .bPN| zꩰ־w4sNwC-GEŤztM kTqw{cĉ׿[,ر~)֮]jXd Ν ^g:,\~ϱbŊXI$jg}6>luY>|8d2d2YЋ2 ӑSbhii7|[bƍ0`;Pl||dTn7>cOPL&oߎq!77WrO&!##~i}ݘ5kVB<cP(//bwJ{g7r8999MN$@8ԩSq*JYYY8s0qD8N%*` $3g"'Dgg,CEfԨQ~JKKqwK?_RRRAo8sCxy{=IMM~3<䓘;w.t:]\0ƠRpYg#-[s D~/Z(*W]]ƴ l#<'NH~.ZYf,3rHs=8snUNN̙ ~5z=f̘4|曑EcHMMŏc<裘2eʐX܍d\z͕<xcZ*^ܹVDI*0~3~_^^?Iۘ* L25x$ßc2eʀ9f'MH$B k֬X5Xx1ƌ#EvB}}=8Çn:EEE;v,P^^ 9e%_@^t:=&M$9:;;vA ꫯ?2 #Fpp:xW_Z*//~{D+Dl6L&>$i7 .'uT*E<ĉxgpb7_>:1aʔ)tI ƑVȒ!),y*xČ5 EEEXr%ms=֌ L6{$ijIwEQj2[{c 98y)//:(ƚ5k$ āoG4Ě87.)*@0)))PT?^[1b>anKCZZMJRxGEEEDn1]\E|!/㣲`AKH2,ȑ#1qDzt:XVf߿{z' F#6l؀ &=R`I]F2N)CxAdJ{6lnF̜9yyyP(`AE\.:u /FF3J% $I),,he(ؾ};N>F &Mg}Fy.egS3f nv}g EfA.c+0n8<ؾ}{T%"5r"A@FF:4p2YD0(++q1!X[!IDATNବ,q8sLqa;vЁ#2jԨһ2 f矟 c fPWЖG%'qHMM~3pa˗/xO#*-EO )Jz8 $2l6ԞofX`Aء;c %%%x1m4I%!r\JVN RԤYfE<O:+Y& =B~ ˅?V/_rn{SO=E0$tuuFsz̞=;uuu#GD]"ɓ'_5~i47. _~%P$tJJ툢;)6ODс{".?ԠA!\ןfx;>}:rssqɸ_<뮻PZZ~?r(wESSn;Ͼn455aݺuxRtwwCňSGM2nOFmm-v܉#GH"M!uKMURRcŊqoQaDJJJC{ ؼy3v܉;(..Fjj*N>ǏcݨaSNp@TZ,[j@Gɓ'q,q饗? O lܸaٲeQcw ݎu~ Ça#HAA%/yL$ AJ>rH\qq%"?)&}"tV83֝N'v;cr5JR52ؽ{ J%=lܸcֱC,Ck꫑F$ dhpQڵKrÛ_Wh4 y6lDJJJ#De="q:غukD!FGV$H Y~}3=##˖-9wp$"d;DPaNN,Y҄HѣIѱ(zF\21"w}?~Y1kw܁;z+.BZ6BhonUU/^1cH2WVV{}݇Ƅ 61 #F<{vJH"vzj̚5+_%RRR5k֠5R0j(\|I/kkk6l  ؙ|6g{# y^k0236m+& PXXm۶b$Jɓ;v,Ң*J8pMMM1{""f͚@NKK%\Ԉ_=}+(( "с+W܈FTTTPDբ&MFAcc#Gĝr袋?q9`0Ye1J\L6 V5hv[88{6س@bE]]*++F#Gľ}Vhhhq.K$++ ]vYDRgp( 磴yyyPTX,!o@N' MѤ> yyy>}:.\deeK82 iii(..FEEfΜǸ[1o<=z>w1Nr۷/%@(x뭷0j(Hy3g΄^wlٲ%!Kinذc,\0*ep5rp8p}$YQ.DJJ2'']wvA,H1g,X * -q-HJeT)L@<ĪUV… O7 222>JhÆ x'c",_{khp-h@Id2^"H q}E<(+]FI&aĉlaڊ455Nw|{FF1lذ9 {McǎO<3EQ,V /0*E_|.bTTTĽjmmx{jxRdwwt 裏ՅKl@ 0X,Xf A#?/[oÆ kz̫jߕ8qb0~x,Z~ -WaÆޕ %0fب}&xpT侸\.۷ob [ 0L>|x R|[0}Ex뭷D'g{fĸqzx \.6mpر_WW=a ov>zG^SSW^ygtt$&MLyÁ7xaGQV6*+Պ>HؼysoFT)NlݺO>$ 63=LH Al6ƌqxK`0`صkW+ա cƌDz6{ĨQzgJ---xg pwCQw^@'qMM 6oތ,deeAP$]Ȯ]nݺzTWW^+>Q{KxWi~w{$DW@6l@mm-򐖖4}# _C_}vލv0z+n߾/"VXM6 aiX0X,|gغu+*̟?eeeW퉢IrZb믿ƦM""Ĥ]%{]tR&iz"*6 ;wW_}@'D$N~!}ݨKx J-G$V',#ēR"HWw_|Gp -- r<v~z< [S 88%!10DP<1 cj:fcq!TUUO>NP(h;Qtw:Xv-׿l&2A 3Ђv]^qHlLÁ'OgRDqq1q9砲)))jjͬv\0hkkûヒիWmH)Aq}ST!@D(E AEAEP_-QqA_1deea(..^ZFff& t:T*UTf3:;; G-[`6 1(EQH_RN"#݂ d):u<# KRPgj<_T*G2 $ qh4V%O}},$X_twww<\T0@: 1L/Bkkk-L[yٍd`шTE(q%~Ry>4ė "6Y>:%.E $D|ws_lJ2d"Ƙ (l7m/>M {U4O 4ބvfcl\.q#a  Ndzy<u eGшZDEһ6(njM2,1 i5 ".AjVuvvE%}#~)~7d^gSPP2Ɣ:.`0LUT8;A`&IͶh4nj)lyqG4D8{6DcJVk0 J41RR)AOvیFcb1z# $NyGH""D#De"P Fզr\XcLK2!b ICE( 49]fbY7p@K>$/F7HyE"h[$xW R.˳d2pc1}H.BD (ݢ(EQlw.lfsl.qx V$1@Bo43xƘ81cϊ'MQܻ(g nQ](w%WsEQ(o#`"'ݿ[JLxAo\ˏ$|8b.X68/O&|ix?6`GAq`HD#`%]Syĺ1 vR"@m( x $X$ Yb,l`Jq1qpn'y(\$DBL"XڊBDE!G## jU|[*- Д@=0.17.0,<0.18.0"] def wants_wayland(config_settings): if config_settings: for key in ["Backend", "backend"]: if config_settings.get(key, "").lower() == "wayland": return True return False def get_requires_for_build_wheel(config_settings=None): """Inject pywlroots into build dependencies if wayland requested.""" reqs = _orig.get_requires_for_build_wheel(config_settings) if wants_wayland(config_settings): reqs += WAYLAND_DEPENDENCIES return reqs def build_wheel(wheel_directory, config_settings=None, metadata_directory=None): """Stop building if wayland requested but pywlroots is not installed.""" if wants_wayland(config_settings): try: import wlroots # noqa: F401 except ImportError: sys.exit("Wayland backend requested but pywlroots is not installed.") return _orig.build_wheel(wheel_directory, config_settings, metadata_directory) qtile-0.31.0/pyproject.toml0000664000175000017500000000627114762660347015560 0ustar epsilonepsilon[build-system] requires = [ "cffi>=1.1.0", "cairocffi[xcb]>=1.6.0", "setuptools>=60", "setuptools-scm>=7.0", "wheel", ] backend-path=["."] build-backend = "builder" [project] name = "qtile" description = "A pure-Python tiling window manager." dynamic = ["version", "readme", "dependencies", "optional-dependencies"] license = {text = "MIT"} classifiers = [ "Intended Audience :: End Users/Desktop", "License :: OSI Approved :: MIT License", "Operating System :: POSIX :: BSD :: FreeBSD", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Desktop Environment :: Window Managers", ] [project.urls] "Homepage" = "https://qtile.org" "Documentation" = "https://docs.qtile.org/" "Code" = "https://github.com/qtile/qtile/" "Issue tracker" = "https://github.com/qtile/qtile/issues" "Contributing" = "https://docs.qtile.org/en/latest/manual/contributing.html" [project.scripts] qtile = "libqtile.scripts.main:main" [tool.setuptools.packages.find] include = ["libqtile*"] [tool.setuptools_scm] [tool.setuptools.dynamic] readme = {file = "README.rst"} [tool.ruff] line-length = 98 exclude = ["libqtile/_ffi.*.py", "libqtile/backend/x11/_ffi.*.py", "test/configs/syntaxerr.py"] target-version = "py310" [tool.ruff.lint] select = [ "F", # pyflakes "E", # pycodestyle errors "W", # pycodestyle warnings "I", # isort "N", # pep8-naming "G", # flake8-logging-format "PIE", # flake8-pie "UP", # pyupgrade ] ignore = [ "E501", # ignore due to conflict with formatter "N818", # exceptions don't need the Error suffix "E741", # allow ambiguous variable names ] fixable = ["ALL"] [tool.ruff.lint.per-file-ignores] "stubs/*" = [ "N", # naming conventions don't matter in stubs "F403", # star imports are okay in stubs "F405", # star imports are okay in stubs ] [tool.ruff.format] quote-style = "double" indent-style = "space" skip-magic-trailing-comma = false line-ending = "auto" [tool.ruff.lint.isort] known-first-party = ["libqtile", "test"] default-section = "third-party" [tool.mypy] mypy_path = "stubs" python_version = "3.13" warn_unused_configs = true warn_unused_ignores = true warn_unreachable = true no_implicit_optional = true disallow_untyped_defs = false disable_error_code = ["method-assign", "annotation-unchecked"] [[tool.mypy.overrides]] module = [ "libqtile.backend.wayland.*", "libqtile.backend", "libqtile.bar", "libqtile.core.*", "libqtile.config", "libqtile.layout.base", "libqtile.log_utils", "libqtile.utils", ] disallow_untyped_defs = true [tool.vulture] paths = ["libqtile"] exclude = ["test/configs/syntaxerr.py"] min_confidence = 100 [tool.pytest.ini_options] python_files = "test_*.py" testpaths = ["test"] filterwarnings = [ "error:::libqtile", "error:::test", "ignore:::dbus_next", ] qtile-0.31.0/bin/0000775000175000017500000000000014762660347013406 5ustar epsilonepsilonqtile-0.31.0/bin/qtile0000775000175000017500000000277614762660347014466 0ustar epsilonepsilon#!/usr/bin/env python3 # # Copyright (c) 2008, Aldo Cortesi. All rights reserved. # Copyright (c) 2011, Florian Mounier # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import os import sys this_dir = os.path.dirname(__file__) base_dir = os.path.abspath(os.path.join(this_dir, "..")) sys.path.insert(0, base_dir) # import lifecycle early so no atexit handlers are lost on restart import libqtile.core.lifecycle # noqa: F401, E402 from libqtile.scripts.main import main # noqa: E402 if __name__ == "__main__": main() qtile-0.31.0/libqtile/0000775000175000017500000000000014762660347014443 5ustar epsilonepsilonqtile-0.31.0/libqtile/scratchpad.py0000664000175000017500000004011314762660347017130 0ustar epsilonepsilon# Copyright (c) 2017, Dirk Hartmann # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations from typing import TYPE_CHECKING from libqtile import config, group, hook from libqtile.backend.base import FloatStates from libqtile.command.base import expose_command from libqtile.config import Match, _Match if TYPE_CHECKING: from libqtile.backend.base import Window class WindowVisibilityToggler: """ WindowVisibilityToggler is a wrapper for a window, used in ScratchPad group to toggle visibility of a window by toggling the group it belongs to. The window is either sent to the named ScratchPad, which is by default invisble, or the current group on the current screen. With this functionality the window can be shown and hidden by a single keystroke (bound to command of ScratchPad group). By default, the window is also hidden if it loses focus. """ def __init__(self, scratchpad_name, window: Window, on_focus_lost_hide, warp_pointer): """ Initiliaze the WindowVisibilityToggler. Parameters: =========== scratchpad_name: string The name (not label) of the ScratchPad group used to hide the window window: window The window to toggle on_focus_lost_hide: bool if True the associated window is hidden if it loses focus warp_pointer: bool if True the mouse pointer is warped to center of associated window if shown. Only used if on_focus_lost_hide is True """ self.scratchpad_name = scratchpad_name self.window = window self.on_focus_lost_hide = on_focus_lost_hide self.warp_pointer = warp_pointer # determine current status based on visibility self.shown = False self.show() def info(self): return dict( window=self.window.info(), scratchpad_name=self.scratchpad_name, visible=self.visible, on_focus_lost_hide=self.on_focus_lost_hide, warp_pointer=self.warp_pointer, ) @property def visible(self): """ Determine if associated window is currently visible. That is the window is on a group different from the scratchpad and that group is the current visible group. """ if self.window.group is None: return False return ( self.window.group.name != self.scratchpad_name and self.window.group is self.window.qtile.current_group ) def toggle(self): """ Toggle the visibility of associated window. Either show() or hide(). """ if not self.visible or not self.shown: self.show() else: self.hide() def show(self): """ Show the associated window on top of current screen. The window is moved to the current group as floating window. If 'warp_pointer' is True the mouse pointer is warped to center of the window if 'on_focus_lost_hide' is True. Otherwise, if pointer is moved manually to window by the user the window might be hidden again before actually reaching it. """ if (not self.visible) or (not self.shown): win = self.window # always set the floating state before changing group # to avoid disturbance of tiling layout win._float_state = FloatStates.TOP # add to group and bring it to front. win.togroup() win.bring_to_front() # toggle internal flag of visibility self.shown = True # add hooks to determine if focus get lost if self.on_focus_lost_hide: if self.warp_pointer: win.focus(warp=True) hook.subscribe.client_focus(self.on_focus_change) hook.subscribe.setgroup(self.on_focus_change) def hide(self): """ Hide the associated window. That is, send it to the scratchpad group. """ if self.visible or self.shown: # unsubscribe the hook methods, since the window is not shown if self.on_focus_lost_hide: hook.unsubscribe.client_focus(self.on_focus_change) hook.unsubscribe.setgroup(self.on_focus_change) self.window.togroup(self.scratchpad_name) self.shown = False def unsubscribe(self): """unsubscribe all hooks""" if self.on_focus_lost_hide and (self.visible or self.shown): hook.unsubscribe.client_focus(self.on_focus_change) hook.unsubscribe.setgroup(self.on_focus_change) def on_focus_change(self, *args, **kwargs): """ hook method which is called on window focus change and group change. Depending on 'on_focus_lost_xxx' arguments, the associated window may get hidden (by call to hide) or even killed. """ if self.shown: current_group = self.window.qtile.current_group if ( self.window.group is not current_group or self.window is not current_group.current_window ): if self.on_focus_lost_hide: self.hide() class DropDownToggler(WindowVisibilityToggler): """ Specialized WindowVisibilityToggler which places the associatd window each time it is shown at desired location. For example this can be used to create a quake-like terminal. """ def __init__(self, window, scratchpad_name, ddconfig): self.name = ddconfig.name self.x = ddconfig.x self.y = ddconfig.y self.width = ddconfig.width self.height = ddconfig.height self.border_width = window.qtile.config.floating_layout.border_width # add "SKIP_TASKBAR" to _NET_WM_STATE atom (for X11) if window.qtile.core.name == "x11": net_wm_state = list(window.window.get_property("_NET_WM_STATE", "ATOM", unpack=int)) skip_taskbar = window.qtile.core.conn.atoms["_NET_WM_STATE_SKIP_TASKBAR"] if net_wm_state: if skip_taskbar not in net_wm_state: net_wm_state.append(skip_taskbar) else: net_wm_state = [skip_taskbar] window.window.set_property("_NET_WM_STATE", net_wm_state) # Let's add the window to the scratchpad group. window.togroup(scratchpad_name) window.opacity = ddconfig.opacity WindowVisibilityToggler.__init__( self, scratchpad_name, window, ddconfig.on_focus_lost_hide, ddconfig.warp_pointer ) def info(self): info = WindowVisibilityToggler.info(self) info.update( dict(name=self.name, x=self.x, y=self.y, width=self.width, height=self.height) ) return info def show(self): """ Like WindowVisibilityToggler.show, but before showing the window, its floating x, y, width and height is set. """ if (not self.visible) or (not self.shown): # SET GEOMETRY win = self.window screen = win.qtile.current_screen # calculate windows floating position and width/height # these may differ for screens, and thus always recalculated. x = int(screen.dx + self.x * screen.dwidth) y = int(screen.dy + self.y * screen.dheight) win.float_x = x win.float_y = y width = int(screen.dwidth * self.width) - 2 * self.border_width height = int(screen.dheight * self.height) - 2 * self.border_width win.place(x, y, width, height, self.border_width, win.bordercolor, respect_hints=True) # Toggle the dropdown WindowVisibilityToggler.show(self) class ScratchPad(group._Group): """ Specialized group which is by default invisible and can be configured, to spawn windows and toggle its visibility (in the current group) by command. The ScratchPad group acts as a container for windows which are currently not visible but associated to a DropDownToggler and can toggle their group by command (of ScratchPad group). The ScratchPad, by default, has no label and thus is not shown in GroupBox widget. """ def __init__( self, name="scratchpad", dropdowns: list[config.DropDown] | None = None, label="", single=False, ): group._Group.__init__(self, name, label=label) self._dropdownconfig = {dd.name: dd for dd in dropdowns} if dropdowns is not None else {} self.dropdowns: dict[str, DropDownToggler] = {} self._spawned: dict[str, _Match] = {} self._to_hide: list[str] = [] self._single = single def _check_unsubscribe(self): if not self.dropdowns: hook.unsubscribe.client_killed(self.on_client_killed) hook.unsubscribe.float_change(self.on_float_change) def _spawn(self, ddconfig): """ Spawn a process by defined command. Method is only called if no window is associated. This is either on the first call to show or if the window was killed. The process id of spawned process is saved and compared to new windows. In case of a match the window gets associated to this DropDown object. """ name = ddconfig.name if name not in self._spawned: if not self._spawned: hook.subscribe.client_new(self.on_client_new) pid = self.qtile.spawn(ddconfig.command) self._spawned[name] = ddconfig.match or Match(net_wm_pid=pid) def on_client_new(self, client, *args, **kwargs): """ hook method which is called on new windows. This method is subscribed if the given command is spawned and unsubscribed immediately if the associated window is detected. """ name = None for n, match in self._spawned.items(): if match.compare(client): name = n break if name is not None: self._spawned.pop(name) if not self._spawned: hook.unsubscribe.client_new(self.on_client_new) self.dropdowns[name] = DropDownToggler(client, self.name, self._dropdownconfig[name]) if self._single: for n, d in self.dropdowns.items(): if n != name: d.hide() if name in self._to_hide: self.dropdowns[name].hide() self._to_hide.remove(name) if len(self.dropdowns) == 1: hook.subscribe.client_killed(self.on_client_killed) hook.subscribe.float_change(self.on_float_change) def on_client_killed(self, client, *args, **kwargs): """ hook method which is called if a client is killed. If the associated window is killed, reset internal state. """ name = None for name, dd in self.dropdowns.items(): if dd.window is client: del self.dropdowns[name] break self._check_unsubscribe() def on_float_change(self, *args, **kwargs): """ hook method which is called if window float state is changed. If the current associated window is not floated (any more) the window and process is detached from DropDown, thus the next call to Show will spawn a new process. """ name = None for name, dd in self.dropdowns.items(): if not dd.window.floating: if dd.window.group is not self: dd.unsubscribe() del self.dropdowns[name] break self._check_unsubscribe() @expose_command() def dropdown_toggle(self, name): """ Toggle visibility of named DropDown. """ if self._single: for n, d in self.dropdowns.items(): if n != name: d.hide() if name in self.dropdowns: self.dropdowns[name].toggle() else: if name in self._dropdownconfig: self._spawn(self._dropdownconfig[name]) @expose_command() def hide_all(self): """ Hide all scratchpads. """ for d in self.dropdowns.values(): d.hide() @expose_command() def dropdown_reconfigure(self, name, **kwargs): """ reconfigure the named DropDown configuration. Note that changed attributes only have an effect on spawning the window. """ if name not in self._dropdownconfig: return dd = self._dropdownconfig[name] for attr, value in kwargs.items(): if hasattr(dd, attr): setattr(dd, attr, value) @expose_command() def dropdown_info(self, name=None): """ Get information on configured or currently active DropDowns. If name is None, a list of all dropdown names is returned. """ if name is None: return {"dropdowns": [ddname for ddname in self._dropdownconfig]} elif name in self.dropdowns: return self.dropdowns[name].info() elif name in self._dropdownconfig: return self._dropdownconfig[name].info() else: raise ValueError(f'No DropDown named "{name}".') def get_state(self): """ Get the state of existing dropdown windows. Used for restoring state across Qtile restarts (`restart` == True) or config reloads (`restart` == False). """ state = [] for name, dd in self.dropdowns.items(): client_wid = dd.window.wid state.append((name, client_wid, dd.visible)) return state def restore_state(self, state, restart: bool) -> list[int]: """ Restore the state of existing dropdown windows. Used for restoring state across Qtile restarts (`restart` == True) or config reloads (`restart` == False). """ orphans = [] for name, wid, visible in state: if name in self._dropdownconfig: if restart: self._spawned[name] = Match(wid=wid) if not visible: self._to_hide.append(name) else: # We are reloading the config; manage the clients now self.dropdowns[name] = DropDownToggler( self.qtile.windows_map[wid], self.name, self._dropdownconfig[name], ) if not visible: self.dropdowns[name].hide() else: orphans.append(wid) if self._spawned: # Handle re-managed clients after restarting assert restart hook.subscribe.client_new(self.on_client_new) if not restart and self.dropdowns: # We're only reloading so don't have these hooked via self.on_client_new hook.subscribe.client_killed(self.on_client_killed) hook.subscribe.float_change(self.on_float_change) return orphans qtile-0.31.0/libqtile/configurable.py0000664000175000017500000000621614762660347017462 0ustar epsilonepsilon# Copyright (c) 2012, Tycho Andersen. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import copy class Configurable: global_defaults = {} # type: dict def __init__(self, **config): self._variable_defaults = {} self._user_config = config def add_defaults(self, defaults): """Add defaults to this object, overwriting any which already exist""" # Since we can't check for immutability reliably, shallow copy the # value. If a mutable value were set and it were changed in one place # it would affect all other instances, since this is typically called # on __init__ self._variable_defaults.update((d[0], copy.copy(d[1])) for d in defaults) def __getattr__(self, name): if name == "_variable_defaults": raise AttributeError found, value = self._find_default(name) if found: setattr(self, name, value) return value else: cname = self.__class__.__name__ raise AttributeError(f"{cname} has no attribute: {name}") def _find_default(self, name): """Returns a tuple (found, value)""" defaults = self._variable_defaults.copy() defaults.update(self.global_defaults) defaults.update(self._user_config) if name in defaults: return (True, defaults[name]) else: return (False, None) class ExtraFallback: """Adds another layer of fallback to attributes Used to look up a different attribute name """ def __init__(self, name, fallback): self.name = name self.hidden_attribute = "_" + name self.fallback = fallback def __get__(self, instance, owner=None): retval = getattr(instance, self.hidden_attribute, None) if retval is None: _found, retval = Configurable._find_default(instance, self.name) if retval is None: retval = getattr(instance, self.fallback, None) return retval def __set__(self, instance, value): """Set own value to a hidden attribute of the object""" setattr(instance, self.hidden_attribute, value) qtile-0.31.0/libqtile/images.py0000664000175000017500000002462214762660347016270 0ustar epsilonepsilon# Copyright (c) 2020, Matt Colligan. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import os from collections import namedtuple import cairocffi import cairocffi.pixbuf from libqtile.utils import scan_files class LoadingError(Exception): pass _SurfaceInfo = namedtuple("_SurfaceInfo", ("surface", "file_type")) def get_cairo_surface(bytes_img, width=None, height=None): try: surf, fmt = cairocffi.pixbuf.decode_to_image_surface(bytes_img, width, height) return _SurfaceInfo(surf, fmt) except TypeError: from libqtile.log_utils import logger logger.exception( "Couldn't load cairo image at specified width and height. " "Falling back to image scaling using cairo. " "Need cairocffi > v0.8.0" ) surf, fmt = cairocffi.pixbuf.decode_to_image_surface(bytes_img) return _SurfaceInfo(surf, fmt) def get_cairo_pattern(surface, width=None, height=None, theta=0.0): """Return a SurfacePattern from an ImageSurface. if width and height are not None scale the pattern to be size width and height. theta is in degrees ccw """ pattern = cairocffi.SurfacePattern(surface) pattern.set_filter(cairocffi.FILTER_BEST) matrix = cairocffi.Matrix() tr_width, tr_height = 1.0, 1.0 surf_width, surf_height = surface.get_width(), surface.get_height() if (width is not None) and (width != surf_width): tr_width = surf_width / width if (height is not None) and (height != surf_height): tr_height = surf_height / height matrix.scale(tr_width, tr_height) epsilon = 1.0e-6 pi = 3.141592653589793 if abs(theta) > epsilon: theta_rad = pi / 180.0 * theta mat_rot = cairocffi.Matrix() # https://cairographics.org/cookbook/transform_about_point/ xt = surf_width * tr_width * 0.5 yt = surf_height * tr_height * 0.5 mat_rot.translate(xt, yt) mat_rot.rotate(theta_rad) mat_rot.translate(-xt, -yt) matrix = mat_rot.multiply(matrix) pattern.set_matrix(matrix) return pattern class _Descriptor: def __init__(self, name=None, default=None, **opts): self.name = name self.under_name = "_" + name self.default = default for key, value in opts.items(): setattr(self, key, value) def __get__(self, obj, cls): if obj is None: return self _getattr = getattr try: return _getattr(obj, self.under_name) except AttributeError: return self.get_default(obj) def get_default(self, obj): return self.default def __set__(self, obj, value): setattr(obj, self.under_name, value) def __delete__(self, obj): delattr(obj, self.under_name) class _Resetter(_Descriptor): def __set__(self, obj, value): super().__set__(obj, value) obj._reset() class _PixelSize(_Resetter): def __set__(self, obj, value): value = max(round(value), 1) super().__set__(obj, value) def get_default(self, obj): size = obj.default_size return getattr(size, self.name) class _Rotation(_Resetter): def __set__(self, obj, value): value = float(value) super().__set__(obj, value) _ImgSize = namedtuple("_ImgSize", ("width", "height")) class Img: """Img is a class which creates & manipulates cairo SurfacePatterns from an image There are two constructors Img(...) and Img.from_path(...) The cairo surface pattern is at img.pattern. Changing any of the attributes width, height, or theta will update the pattern. - width :: pattern width in pixels - height :: pattern height in pixels - theta :: rotation of pattern counter clockwise in degrees Pattern is first stretched, then rotated. """ def __init__(self, bytes_img, name="", path=""): self.bytes_img = bytes_img self.name = name self.path = path def _reset(self): attrs = ("surface", "pattern") for attr in attrs: try: delattr(self, attr) except AttributeError: pass @classmethod def from_path(cls, image_path): "Create an Img instance from image_path" with open(image_path, "rb") as fobj: bytes_img = fobj.read() name = os.path.basename(image_path) name, file_type = os.path.splitext(name) return cls(bytes_img, name=name, path=image_path) @property def default_surface(self): try: return self._default_surface except AttributeError: surf, fmt = get_cairo_surface(self.bytes_img) self._default_surface = surf return surf @property def default_size(self): try: return self._default_size except AttributeError: surf = self.default_surface size = _ImgSize(surf.get_width(), surf.get_height()) self._default_size = size return size theta = _Rotation("theta", default=0.0) width = _PixelSize("width") height = _PixelSize("height") def resize(self, width=None, height=None): width0, height0 = self.default_size width_factor, height_factor = None, None if width is not None: width_factor = width / width0 if height is not None: height_factor = height / height0 if width and height: return self.scale(width_factor, height_factor, lock_aspect_ratio=False) if width or height: return self.scale(width_factor, height_factor, lock_aspect_ratio=True) raise ValueError("You must supply either width or height!") def scale(self, width_factor=None, height_factor=None, lock_aspect_ratio=False): if not (width_factor or height_factor): raise ValueError("You must supply width_factor or height_factor") if lock_aspect_ratio: res = self._scale_lock(width_factor, height_factor, self.default_size) else: res = self._scale_free(width_factor, height_factor, self.default_size) self.width, self.height = res @staticmethod def _scale_lock(width_factor, height_factor, initial_size): if width_factor and height_factor: raise ValueError( "Can't rescale with locked aspect ratio " "and give width_factor and height_factor." f" {width_factor}, {height_factor}" ) width0, height0 = initial_size if width_factor: width = width0 * width_factor height = height0 / width0 * width else: height = height0 * height_factor width = width0 / height0 * height return _ImgSize(width, height) @staticmethod def _scale_free(width_factor, height_factor, initial_size): width_factor = 1 if width_factor is None else width_factor height_factor = 1 if height_factor is None else height_factor width0, height0 = initial_size return _ImgSize(width0 * width_factor, height0 * height_factor) @property def surface(self): try: return self._surface except AttributeError: surf, fmt = get_cairo_surface(self.bytes_img, self.width, self.height) self._surface = surf return surf @surface.deleter def surface(self): try: del self._surface except AttributeError: pass @property def pattern(self): try: return self._pattern except AttributeError: pat = get_cairo_pattern(self.surface, self.width, self.height, self.theta) self._pattern = pat return pat @pattern.deleter def pattern(self): try: del self._pattern except AttributeError: pass def __repr__(self): return f"<{self.__class__.__name__}: {self.name!r}, {self.width}x{self.height}@{self.theta:.1f}deg, {self.path!r}>" def __eq__(self, other): if not isinstance(other, self.__class__): return False s0 = (self.bytes_img, self.theta, self.width, self.height) s1 = (other.bytes_img, other.theta, other.width, other.height) return s0 == s1 class Loader: """Loader - create Img() instances from image names load icons with Loader e.g., >>> ldr = Loader('/usr/share/icons/Adwaita/24x24', '/usr/share/icons/Adwaita') >>> d_loaded_images = ldr('audio-volume-muted', 'audio-volume-low') """ def __init__(self, *directories, **kwargs): for k, v in kwargs.items(): setattr(self, k, v) self.directories = list(directories) def __call__(self, *names): d = {} seen = set() set_names = set() for n in names: root, ext = os.path.splitext(n) if ext: set_names.add(n) else: set_names.add(n + ".*") for directory in self.directories: d_matches = scan_files(directory, *(set_names - seen)) for name, paths in d_matches.items(): if paths: d[name if name in names else name[:-2]] = Img.from_path(paths[0]) seen.add(name) if seen != set_names: msg = "Wasn't able to find images corresponding to the names: {}" raise LoadingError(msg.format(set_names - seen)) return d qtile-0.31.0/libqtile/command/0000775000175000017500000000000014762660347016061 5ustar epsilonepsilonqtile-0.31.0/libqtile/command/__init__.py0000664000175000017500000000000014762660347020160 0ustar epsilonepsilonqtile-0.31.0/libqtile/command/base.py0000664000175000017500000003101714762660347017347 0ustar epsilonepsilon# Copyright (c) 2019 Sean Vig # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the \"Software\"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """ The objects in the command graph and command resolution on the objects """ from __future__ import annotations import abc import asyncio import inspect import sys import traceback from functools import partial from typing import TYPE_CHECKING from libqtile.configurable import Configurable from libqtile.log_utils import logger from libqtile.utils import create_task if TYPE_CHECKING: from collections.abc import Callable from libqtile.command.graph import SelectorType ItemT = tuple[bool, list[str | int]] | None def expose_command(name: Callable | str | list[str] | None = None) -> Callable: """ Decorator to expose methods to the command interface. The exposed command will have the name of the defined method. Methods can also be exposed via multiple names by passing the names to this decorator. e.g. if a layout wants "up" and "previous" to call the same method: @expose_command("previous") def up(self): ... `up` will be exposed as `up` and `previous`. Multiple names can be passed as a list. """ def wrapper(func: Callable): setattr(func, "_cmd", True) if name is not None: if not hasattr(func, "_mapping"): setattr(func, "_mapping", list()) if isinstance(name, list): func._mapping += name # type:ignore elif isinstance(name, str): func._mapping.append(name) # type:ignore else: logger.error("Unexpected value received in command decorator: %s", name) return func # If the decorator is added with no parentheses then we should treat it # as if it had been i.e. expose the decorated method if callable(name): func = name name = None return wrapper(func) return wrapper class SelectError(Exception): """Error raised in resolving a command graph object""" def __init__(self, err_string: str, name: str, selectors: list[SelectorType]): super().__init__(f"{err_string}, name: {name}, selectors: {selectors}") self.name = name self.selectors = selectors class CommandError(Exception): """Error raised in resolving a command""" class CommandException(Exception): """Error raised while executing a command""" class CommandObject(metaclass=abc.ABCMeta): """Base class for objects that expose commands Any command to be exposed should be decorated with `@expose_command()` (classes that are not explicitly inheriting from CommandObject will need to import the module) A CommandObject should also implement `._items()` and `._select()` methods (c.f. docstring for `.items()` and `.select()`). """ def __new__(cls, *args, **kwargs): # Check which level of command object has been parsed # This test ensures inherited classes don't stop additional # methods from being exposed. # For example, if widget.TextBox has already been parsed, a subsequent # call to initialise a new TextBox will return here. However, if a user # subclasses TextBox for a new widget then that new widget will still # be parsed here to check for new commands. if getattr(cls, "_command_object", "") == cls.__name__: super().__new__(cls) commands = {} cmd_s = set() # We need to iterate over the class's inherited classes in reverse order # We reverse the order so the exposed command will always be the latest # definition of the method. for c in reversed(list(cls.__mro__)): for method_name in list(c.__dict__.keys()): method = getattr(c, method_name, None) if method is None: continue # If the command has been exposed, add it to our dictionary # If the method name is already in our dictionary then bind the # latest definition to that command if hasattr(method, "_cmd") or method_name in commands: commands[method_name] = method # For now, we'll accept the old format `cmd_` naming scheme for # exposing commands. # NOTE: This will be deprecated in the future elif method_name.startswith("cmd_"): cmd_s.add(method_name) commands[method_name[4:]] = method # Expose additional names for mapping in getattr(method, "_mapping", list()): setattr(cls, mapping, method) commands[mapping] = method if cmd_s: names = ", ".join(cmd_s) msg = ( f"The use of the 'cmd_' prefix to expose commands via IPC " f"is deprecated. Methods should use the " f"@expose_command() decorator instead. " f"Please update: {names}" ) logger.warning("Deprecation Warning: %s", msg) # Record the object as being parsed. cls._command_object = cls.__name__ # Store list of exposed commands cls._commands = commands return super().__new__(cls) def select(self, selectors: list[SelectorType]) -> CommandObject: """Return a selected object Recursively finds an object specified by a list of `(name, selector)` items. Raises SelectError if the object does not exist. """ obj: CommandObject = self for name, selector in selectors: root, items = obj.items(name) # if non-root object and no selector given if root is False and selector is None: raise SelectError("", name, selectors) # if no items in container, but selector is given if items is None and selector is not None: raise SelectError("", name, selectors) # if selector is not in the list of contained items if items is not None and selector and selector not in items: raise SelectError("", name, selectors) maybe_obj = obj._select(name, selector) if maybe_obj is None: raise SelectError("", name, selectors) obj = maybe_obj return obj @expose_command() def items(self, name: str) -> tuple[bool, list[str | int] | None]: """ Build a list of contained items for the given item class. Exposing this allows __qsh__ to navigate the command graph. Returns a tuple `(root, items)` for the specified item class, where: root: True if this class accepts a "naked" specification without an item seletion (e.g. "layout" defaults to current layout), and False if it does not (e.g. no default "widget"). items: a list of contained items """ ret = self._items(name) if ret is None: # Not finding information for a particular item class is OK here; # we don't expect layouts to have a window, etc. return False, None return ret @abc.abstractmethod def _items(self, name) -> ItemT: """Generate the items for a given Same return as `.items()`. Return `None` if name is not a valid item class. """ @abc.abstractmethod def _select(self, name: str, sel: str | int | None) -> CommandObject | None: """Select the given item of the given item class This method is called with the following guarantees: - `name` is a valid selector class for this item - `sel` is a valid selector for this item - the `(name, sel)` tuple is not an "impossible" combination (e.g. a selector is specified when `name` is not a containment object). Return None if no such object exists """ def command(self, name: str) -> Callable | None: """Return the command with the given name Parameters ---------- name: str The name of the command to fetch. """ return self._commands.get(name) def __getattr__(self, name): # We can use __getattr_ to handle deprecated calls to # cmd_ but we need to stop this overriding Configurable's # use of this method if isinstance(self, Configurable): try: return Configurable.__getattr__(self, name) except AttributeError: pass # It's not a Configurable attribute so let's check if it's # a command call if name.startswith("cmd_"): cmd = name[4:] if cmd in self.commands(): logger.warning( "Deprecation Warning: commands exposed via IPC no " "longer use the 'cmd_' prefix. " "Please replace '%s' with '%s' in your code.", name, cmd, ) # This is not a bound method so we need to pass 'self' return partial(self.command(cmd), self) raise AttributeError(f"{self.__class__} has no attribute {name}") @expose_command() def commands(self) -> list[str]: """ Returns a list of possible commands for this object Used by __qsh__ for command completion and online help """ return sorted([cmd for cmd in self._commands]) @expose_command() def doc(self, name) -> str: """Returns the documentation for a specified command name Used by __qsh__ to provide online help. """ if name in self.commands(): command = self.command(name) assert command signature = self._get_command_signature(command) spec = name + signature htext = inspect.getdoc(command) or "" return spec + "\n" + htext raise CommandError(f"No such command: {name}") def _get_command_signature(self, command: Callable) -> str: signature = inspect.signature(command) args = list(signature.parameters) if args and args[0] == "self": args = args[1:] parameters = [signature.parameters[arg] for arg in args] signature = signature.replace(parameters=parameters) return str(signature) @expose_command() def eval(self, code: str) -> tuple[bool, str | None]: """Evaluates code in the same context as this function Return value is tuple `(success, result)`, success being a boolean and result being a string representing the return value of eval, or None if exec was used instead. """ try: globals_ = vars(sys.modules[self.__module__]) try: return True, str(eval(code, globals_, locals())) except SyntaxError: exec(code, globals_, locals()) return True, None except Exception: error = traceback.format_exc().strip().split("\n")[-1] return False, error @expose_command() def function(self, function, *args, **kwargs) -> asyncio.Task | None: """Call a function with current object as argument""" try: if asyncio.iscoroutinefunction(function): return create_task(function(self, *args, **kwargs)) else: return function(self, *args, **kwargs) except Exception: error = traceback.format_exc() logger.error('Exception calling "%s":\n%s', function, error) return None qtile-0.31.0/libqtile/command/client.py0000664000175000017500000002743514762660347017724 0ustar epsilonepsilon# Copyright (c) 2019 Sean Vig # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the \"Software\"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """ The clients that expose the command graph of a given command interface The clients give the ability to navigate the command graph while providing name resolution with the given command graph interface. When writing functionality that interacts with qtile objects, it should favor using the command graph clients to do this interaction. """ from __future__ import annotations from typing import TYPE_CHECKING from libqtile.command.base import SelectError from libqtile.command.graph import ( CommandGraphCall, CommandGraphNode, CommandGraphObject, CommandGraphRoot, ) from libqtile.command.interface import CommandInterface, IPCCommandInterface from libqtile.ipc import Client, find_sockfile if TYPE_CHECKING: from typing import Any from libqtile.command.graph import GraphType from libqtile.command.interface import SelectorType class CommandClient: """The object that resolves the commands""" def __init__( self, command: CommandInterface | None = None, *, current_node: CommandGraphNode | None = None, ) -> None: """A client that resolves calls through the command object interface Exposes a similar API to the command graph, but performs resolution of objects. Any navigation done on the command graph is resolved at the point it is invoked. This command resolution is done via the command interface. Parameters ---------- command: CommandInterface The object that is used to resolve command graph calls, as well as navigate the command graph. current_node: CommandGraphNode The current node that is pointed to in the command graph. If not specified, the command graph root is used. """ if command is None: command = IPCCommandInterface(Client(find_sockfile())) self._command = command self._current_node = current_node if current_node is not None else CommandGraphRoot() def navigate(self, name: str, selector: str | None) -> CommandClient: """Resolve the given object in the command graph Parameters ---------- name: str The name of the command graph object to resolve. selector: str | None If given, the selector to use to select the next object, and if None, then selects the default object. Returns ------- CommandClient The client with the given command graph object resolved. """ if name not in self.children: raise SelectError("Not valid child", name, self._current_node.selectors) normalized_selector = _normalize_item(name, selector) if selector is not None else None if normalized_selector is not None: if not self._command.has_item(self._current_node, name, normalized_selector): raise SelectError( "Item not available in object", name, self._current_node.selectors ) next_node = self._current_node.navigate(name, normalized_selector) return self.__class__(self._command, current_node=next_node) def call(self, name: str, *args, lifted=True, **kwargs) -> Any: """Resolve and invoke the call into the command graph Parameters ---------- name: str The name of the command to resolve in the command graph. args: The arguments to pass into the call invocation. kwargs: The keyword arguments to pass into the call invocation. Returns ------- The output returned from the function call. """ if name not in self.commands: raise SelectError("Not valid child or command", name, self._current_node.selectors) call = self._current_node.call(name, lifted=lifted) return self._command.execute(call, args, kwargs) @property def children(self) -> list[str]: """Get the children of the current location in the command graph""" return self._current_node.children @property def selectors(self) -> list[SelectorType]: return self._current_node.selectors @property def commands(self) -> list[str]: """Get the commands available on the current object""" command_call = self._current_node.call("commands") return self._command.execute(command_call, (), {}) def items(self, name: str) -> tuple[bool, list[str | int]]: """Get the available items""" items_call = self._current_node.call("items") return self._command.execute(items_call, (name,), {}) @property def root(self) -> CommandClient: """Get the root of the command graph""" return self.__class__(self._command) @property def parent(self) -> CommandClient: """Get the parent of the current client""" if self._current_node.parent is None: raise SelectError("", "", self._current_node.selectors) return self.__class__(self._command, current_node=self._current_node.parent) class InteractiveCommandClient: """ A command graph client that can be used to easily resolve elements interactively """ def __init__( self, command: CommandInterface | None = None, *, current_node: GraphType | None = None ) -> None: """An interactive client that resolves calls through the gives client Exposes the command graph API in such a way that it can be traversed directly on this object. The command resolution for this object is done via the command interface. Parameters ---------- command: CommandInterface The object that is used to resolve command graph calls, as well as navigate the command graph. current_node: CommandGraphNode The current node that is pointed to in the command graph. If not specified, the command graph root is used. """ if command is None: command = IPCCommandInterface(Client(find_sockfile())) self._command = command self._current_node = current_node if current_node is not None else CommandGraphRoot() def __call__(self, *args, **kwargs) -> Any: """When the client has navigated to a command, execute it""" if not isinstance(self._current_node, CommandGraphCall): raise SelectError("Invalid call", "", self._current_node.selectors) return self._command.execute(self._current_node, args, kwargs) def __getattr__(self, name: str) -> InteractiveCommandClient: """Get the child element of the currently selected object Resolve the element specified by the given name, either the child object, or the command on the current object. Parameters ---------- name: str The name of the element to resolve Return ------ InteractiveCommandClient The client navigated to the specified name. Will respresent either a command graph node (if the name is a valid child) or a command graph call (if the name is a valid command). """ # Python's help() command will try to look up __name__ and __origin__ so we # need to handle these explicitly otherwise they'll result in a SelectError # which help() does not expect. if name in ["__name__", "__origin__"]: raise AttributeError if isinstance(self._current_node, CommandGraphCall): raise SelectError( "Cannot select children of call", name, self._current_node.selectors ) # we do not know if the name is a command to be executed, or an object # to navigate to if name not in self._current_node.children: # we are going to resolve a command, check that the command is valid if not self._command.has_command(self._current_node, name): raise SelectError( "Not valid child or command", name, self._current_node.selectors ) call_object = self._current_node.call(name) return self.__class__(self._command, current_node=call_object) next_node = self._current_node.navigate(name, None) return self.__class__(self._command, current_node=next_node) def __getitem__(self, name: str | int) -> InteractiveCommandClient: """Get the selected element of the currently selected object From the current command graph object, select the instance with the given name. Parameters ---------- name: str The name, or index if it's of int type, of the item to resolve Return ------ InteractiveCommandClient The current client, navigated to the specified command graph object. """ if isinstance(self._current_node, CommandGraphRoot): raise KeyError("Root node has no available items", name, self._current_node.selectors) if not isinstance(self._current_node, CommandGraphObject): raise SelectError( "Unable to make selection on current node", str(name), self._current_node.selectors, ) if self._current_node.selector is not None: raise SelectError("Selection already made", str(name), self._current_node.selectors) # check the selection is valid in the server-side qtile manager if not self._command.has_item( self._current_node.parent, self._current_node.object_type, name ): raise SelectError( "Item not available in object", str(name), self._current_node.selectors ) next_node = self._current_node.parent.navigate(self._current_node.object_type, name) return self.__class__(self._command, current_node=next_node) def normalize_item(self, item: str) -> str | int: "Normalize the item according to Qtile._items()." object_type = ( self._current_node.object_type if isinstance(self._current_node, CommandGraphObject) else None ) return _normalize_item(object_type, item) def _normalize_item(object_type: str | None, item: str) -> str | int: if object_type in ["group", "widget", "bar"]: return str(item) elif object_type in ["layout", "window", "screen"]: try: return int(item) except ValueError: # A value error could arise because the next selector has been passed raise SelectError( f"Unexpected index {item}. Is this an object_type?", str(object_type), [(str(object_type), str(item))], ) else: return item qtile-0.31.0/libqtile/command/graph.py0000664000175000017500000001554414762660347017545 0ustar epsilonepsilon# Copyright (c) 2008, Aldo Cortesi. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """ The objects defining the nodes in the command graph and the navigation of the abstract command graph """ from __future__ import annotations import abc from typing import TYPE_CHECKING if TYPE_CHECKING: SelectorType = tuple[str, str | int | None] class CommandGraphNode(metaclass=abc.ABCMeta): """A container node in the command graph structure A command graph node which can contain other elements that it can link to. May also have commands that can be executed on itself. """ @property @abc.abstractmethod def selector(self) -> str | int | None: """The selector for the current node""" @property @abc.abstractmethod def selectors(self) -> list[SelectorType]: """The selectors resolving the location of the node in the command graph""" @property @abc.abstractmethod def parent(self) -> CommandGraphNode | None: """The parent of the current node""" @property @abc.abstractmethod def children(self) -> list[str]: """The child objects that are contained within this object""" def navigate(self, name: str, selector: str | int | None) -> CommandGraphNode: """Navigate from the current node to the specified child""" if name in self.children: return _COMMAND_GRAPH_MAP[name](selector, self) raise KeyError(f"Given node is not an object: {name}") def call(self, name: str, lifted: bool = False) -> CommandGraphCall: """Execute the given call on the selected object""" return CommandGraphCall(name, self, lifted=lifted) class CommandGraphCall: """A call performed on a particular object in the command graph""" def __init__(self, name: str, parent: CommandGraphNode, lifted: bool = False) -> None: """A command to be executed on the selected object A terminal node in the command graph, specifying an actual command to execute on the selected graph element. Parameters ---------- name: The name of the command to execute parent: The command graph node on which to execute the given command. """ self._name = name self._parent = parent self.lifted = lifted @property def name(self) -> str: """The name of the call to make""" return self._name @property def selectors(self) -> list[SelectorType]: """The selectors resolving the location of the node in the command graph""" return self.parent.selectors @property def parent(self) -> CommandGraphNode: """The parent of the current node""" return self._parent class CommandGraphRoot(CommandGraphNode): """The root node of the command graph Contains all of the elements connected to the root of the command graph. """ @property def selector(self) -> None: """The selector for the current node""" return None @property def selectors(self) -> list[SelectorType]: """The selectors resolving the location of the node in the command graph""" return [] @property def parent(self) -> None: """The parent of the current node""" return None @property def children(self) -> list[str]: """All of the child elements in the root of the command graph""" return ["bar", "group", "layout", "screen", "widget", "window", "core"] class CommandGraphObject(CommandGraphNode, metaclass=abc.ABCMeta): """An object in the command graph that contains a collection of objects""" def __init__(self, selector: str | int | None, parent: CommandGraphNode) -> None: """A container object in the command graph Parameters ---------- selector: str | None The name of the selected element within the command graph. If not given, corresponds to the default selection of this type of object. parent: CommandGraphNode The container object that this object is the child of. """ self._selector = selector self._parent = parent @property def selector(self) -> str | int | None: """The selector for the current node""" return self._selector @property def selectors(self) -> list[SelectorType]: """The selectors resolving the location of the node in the command graph""" selectors = self.parent.selectors + [(self.object_type, self.selector)] return selectors @property def parent(self) -> CommandGraphNode: """The parent of the current node""" return self._parent @property @abc.abstractmethod def object_type(self) -> str: """The type of the current container object""" class _BarGraphNode(CommandGraphObject): object_type = "bar" children = ["screen", "widget"] class _GroupGraphNode(CommandGraphObject): object_type = "group" children = ["layout", "window", "screen"] class _LayoutGraphNode(CommandGraphObject): object_type = "layout" children = ["group", "window", "screen"] class _ScreenGraphNode(CommandGraphObject): object_type = "screen" children = ["layout", "window", "bar", "widget", "group"] class _WidgetGraphNode(CommandGraphObject): object_type = "widget" children = ["bar", "screen"] class _WindowGraphNode(CommandGraphObject): object_type = "window" children = ["group", "screen", "layout"] class _CoreGraphNode(CommandGraphObject): object_type = "core" children: list[str] = [] _COMMAND_GRAPH_MAP: dict[str, type[CommandGraphObject]] = { "bar": _BarGraphNode, "group": _GroupGraphNode, "layout": _LayoutGraphNode, "widget": _WidgetGraphNode, "window": _WindowGraphNode, "screen": _ScreenGraphNode, "core": _CoreGraphNode, } GraphType = CommandGraphNode | CommandGraphCall qtile-0.31.0/libqtile/command/interface.py0000664000175000017500000003446714762660347020411 0ustar epsilonepsilon# Copyright (c) 2019 Sean Vig # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """ The interface to execute commands on the command graph """ from __future__ import annotations import traceback import types import typing from abc import ABCMeta, abstractmethod from typing import TYPE_CHECKING, Any, Literal, Union, get_args, get_origin from libqtile import ipc from libqtile.command.base import CommandError, CommandException, CommandObject, SelectError from libqtile.command.graph import CommandGraphCall, CommandGraphNode from libqtile.log_utils import logger from libqtile.utils import ColorsType, ColorType # noqa: F401 if TYPE_CHECKING: from libqtile.command.graph import SelectorType SUCCESS = 0 ERROR = 1 EXCEPTION = 2 # these two mask their aliases from elsewhere in the tree (i.e. # libqtile.extension.base._Extension, and libqtile.layout.base.Layout # # since neither of these have constructors from a single string, we can't # really lift them to a type. probably nobody actually passes us layouts here # so it doesn't matter, but in the event that it does, we can probably fix it # up with some eval() hackery whackery. class _Extension: pass class Layout: pass def format_selectors(selectors: list[SelectorType]) -> str: """Build the path to the selected command graph node""" path_elements = [] for name, selector in selectors: if selector is not None: path_elements.append(f"{name}[{selector}]") else: path_elements.append(name) return ".".join(path_elements) class CommandInterface(metaclass=ABCMeta): """Defines an interface which can be used to evaluate a given call on a command graph. The implementations of this may use, for example, an IPC call to access the running qtile instance remotely or directly access the qtile instance from within the same process, or it may return lazily evaluated results. """ @abstractmethod def execute(self, call: CommandGraphCall, args: tuple, kwargs: dict) -> Any: """Execute the given call, returning the result of the execution Perform the given command graph call, calling the function with the given arguments and keyword arguments. Parameters ---------- call: CommandGraphCall The call on the command graph that is to be performed. args: The arguments to pass into the command graph call. kwargs: The keyword arguments to pass into the command graph call. """ @abstractmethod def has_command(self, node: CommandGraphNode, command: str) -> bool: """Check if the given command exists Parameters ---------- node: CommandGraphNode The node to check for commands command: str The name of the command to check for Returns ------- bool True if the command is resolved on the given node """ @abstractmethod def has_item(self, node: CommandGraphNode, object_type: str, item: str | int) -> bool: """Check if the given item exists Parameters ---------- node: CommandGraphNode The node to check for items object_type: str The type of object to check for items. command: str The name of the item to check for Returns ------- bool True if the item is resolved on the given node """ class QtileCommandInterface(CommandInterface): """Execute the commands via the in process running qtile instance""" def __init__(self, command_object: CommandObject): """A command object that directly resolves commands Parameters ---------- command_object: CommandObject The command object to use for resolving the commands and items against. """ self._command_object = command_object def execute(self, call: CommandGraphCall, args: tuple, kwargs: dict) -> Any: """Execute the given call, returning the result of the execution Perform the given command graph call, calling the function with the given arguments and keyword arguments. Parameters ---------- call: CommandGraphCall The call on the command graph that is to be performed. args: The arguments to pass into the command graph call. kwargs: The keyword arguments to pass into the command graph call. """ obj = self._command_object.select(call.selectors) cmd = None try: cmd = obj.command(call.name) except SelectError: pass if cmd is None: return "No such command." logger.debug("Command: %s(%s, %s)", call.name, args, kwargs) return cmd(self._command_object, *args, **kwargs) def has_command(self, node: CommandGraphNode, command: str) -> bool: """Check if the given command exists Parameters ---------- node: CommandGraphNode The node to check for commands command: str The name of the command to check for Returns ------- bool True if the command is resolved on the given node """ obj = self._command_object.select(node.selectors) cmd = obj.command(command) return cmd is not None def has_item(self, node: CommandGraphNode, object_type: str, item: str | int) -> bool: """Check if the given item exists Parameters ---------- node: CommandGraphNode The node to check for items object_type: str The type of object to check for items. item: str The name or index of the item to check for Returns ------- bool True if the item is resolved on the given node """ try: self._command_object.select(node.selectors + [(object_type, item)]) except SelectError: return False return True class IPCCommandInterface(CommandInterface): """Execute the resolved commands using the IPC connection to a running qtile instance""" def __init__(self, ipc_client: ipc.Client): """Build a command object which resolves commands through IPC calls Parameters ---------- ipc_client: ipc.Client The client that is to be used to resolve the calls. """ self._client = ipc_client def execute(self, call: CommandGraphCall, args: tuple, kwargs: dict) -> Any: """Execute the given call, returning the result of the execution Executes the given command over the given IPC client. Returns the result of the execution. Parameters ---------- call: CommandGraphCall The call on the command graph that is to be performed. args: The arguments to pass into the command graph call. kwargs: The keyword arguments to pass into the command graph call. """ status, result = self._client.send( (call.parent.selectors, call.name, args, kwargs, call.lifted) ) if status == SUCCESS: return result if status == ERROR: raise CommandError(result) raise CommandException(result) def has_command(self, node: CommandGraphNode, command: str) -> bool: """Check if the given command exists Resolves the allowed commands over the IPC interface, and returns a boolean indicating of the given command is valid. Parameters ---------- node: CommandGraphNode The node to check for commands command: str The name of the command to check for Returns ------- bool True if the command is resolved on the given node """ cmd_call = node.call("commands") commands = self.execute(cmd_call, (), {}) return command in commands def has_item(self, node: CommandGraphNode, object_type: str, item: str | int) -> bool: """Check if the given item exists Resolves the available commands for the given command node of the given command type. Performs the resolution of the items through the given IPC client. Parameters ---------- node: CommandGraphNode The node to check for items object_type: str The type of object to check for items. command: str The name of the item to check for Returns ------- bool True if the item is resolved on the given node """ items_call = node.call("items") _, items = self.execute(items_call, (object_type,), {}) return items is not None and item in items def lift_args(cmd, args, kwargs): """ Lift args lifts the arguments to the type annotations on cmd's parameters. """ def lift_arg(typ, arg): # for stuff like int | None, allow either if get_origin(typ) in [types.UnionType, Union]: for t in get_args(typ): if t == types.NoneType: # special case None? I don't know what this looks like # coming over IPC if arg == "": return None if arg is None: return None continue try: return lift_arg(t, arg) except TypeError: pass # uh oh, we couldn't lift it to anything raise TypeError(f"{arg} is not a {typ}") # for literals, check that it is one of the valid strings if get_origin(typ) is Literal: if arg not in get_args(typ): raise TypeError(f"{arg} is not one of {get_origin(typ)}") return arg if typ is bool: # >>> bool("False") is True # True # ... but we want it to be false :) if arg == "True" or arg is True: return True if arg == "False" or arg is False: return False raise TypeError(f"{arg} is not a bool") if typ is Any: # can't do any lifting if we don't know the type return arg if typ in [_Extension, Layout]: # these are "complex" objects that can't be created with a # single string argument. we generally don't expect people to # be passing these over the command line, so let's ignore then. return arg if get_origin(typ) in [list, dict]: # again, we do not want to be in the business of parsing # lists/dicts of types out of strings; just pass on whatever we # got return arg return typ(arg) converted_args = [] converted_kwargs = dict() params = typing.get_type_hints(cmd, globalns=globals()) non_return_annotated_args = filter(lambda k: k != "return", params.keys()) for param, arg in zip(non_return_annotated_args, args): converted_args.append(lift_arg(params[param], arg)) # if not all args were annotated, we need to keep them anyway. note # that mixing some annotated and not annotated args will not work well: # we will reorder args here and cause problems. this is solveable but # somewhat ugly, and we can avoid it by always annotating all # parameters. # # if we really want to fix this, we # inspect.signature(foo).parameters.keys() gives us the ordered # parameters to reason about. if len(converted_args) < len(args): converted_args.extend(args[len(converted_args) :]) for k, v in kwargs.items(): # if this kwarg has a type annotation, use it if k in params: converted_kwargs[k] = lift_arg(params[k], v) else: converted_kwargs[k] = v return tuple(converted_args), converted_kwargs class IPCCommandServer: """Execute the object commands for the calls that are sent to it""" def __init__(self, qtile) -> None: """Wrapper around the ipc server for communitacing with the IPCCommandInterface sets up the IPC server such that it will receive and send messages to and from the IPCCommandInterface. """ self.qtile = qtile def call( self, data: tuple[list[SelectorType], str, tuple, dict, bool], ) -> tuple[int, Any]: """Receive and parse the given data""" selectors, name, args, kwargs, lifted = data try: obj = self.qtile.select(selectors) cmd = obj.command(name) except SelectError as err: sel_string = format_selectors(selectors) return ERROR, f"No object {err.name} in path '{sel_string}'" if not cmd: return ERROR, "No such command" logger.debug("Command: %s(%s, %s)", name, args, kwargs) if lifted: args, kwargs = lift_args(cmd, args, kwargs) # Check if method is bound, if itis, insert magic self if not hasattr(cmd, "__self__"): args = (obj,) + args try: return SUCCESS, cmd(*args, **kwargs) except CommandError as err: return ERROR, err.args[0] except Exception: return EXCEPTION, traceback.format_exc() qtile-0.31.0/libqtile/pango_ffi.py0000664000175000017500000001137114762660347016750 0ustar epsilonepsilon# Copyright (c) 2014-2015 Sean Vig # Copyright (c) 2014 roger # Copyright (c) 2014 Tycho Andersen # Copyright (c) 2015 Craig Barnes # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from cairocffi.ffi import ffi as cairocffi_ffi from cffi import FFI pango_ffi = FFI() pango_ffi.include(cairocffi_ffi) pango_ffi.cdef( """ typedef ... PangoContext; typedef ... PangoLayout; typedef ... PangoFontDescription; typedef ... PangoAttrList; typedef enum { PANGO_ALIGN_LEFT, PANGO_ALIGN_CENTER, PANGO_ALIGN_RIGHT } PangoAlignment; typedef enum { PANGO_ELLIPSEIZE_NONE, PANGO_ELLIPSIZE_START, PANGO_ELLIPSIZE_MIDDLE, PANGO_ELLIPSIZE_END } PangoEllipsizeMode; int pango_units_from_double (double d); typedef void* gpointer; typedef int gboolean; typedef unsigned int guint32; typedef guint32 gunichar; typedef char gchar; typedef signed long gssize; typedef ... GError; typedef int gint; void pango_cairo_show_layout (cairo_t *cr, PangoLayout *layout); gboolean pango_parse_markup (const char *markup_text, int length, gunichar accel_marker, PangoAttrList **attr_list, char **text, gunichar *accel_char, GError **error); // https://developer.gnome.org/pango/stable/pango-Layout-Objects.html PangoLayout *pango_cairo_create_layout (cairo_t *cr); void g_object_unref(gpointer object); void pango_layout_set_font_description (PangoLayout *layout, const PangoFontDescription *desc); const PangoFontDescription * pango_layout_get_font_description (PangoLayout *layout); void pango_layout_set_alignment (PangoLayout *layout, PangoAlignment alignment); void pango_layout_set_attributes (PangoLayout *layout, PangoAttrList *attrs); void pango_layout_set_text (PangoLayout *layout, const char *text, int length); const char * pango_layout_get_text (PangoLayout *layout); void pango_layout_get_pixel_size (PangoLayout *layout, int *width, int *height); void pango_layout_set_width (PangoLayout *layout, int width); void pango_layout_set_ellipsize (PangoLayout *layout, PangoEllipsizeMode ellipsize); PangoEllipsizeMode pango_layout_get_ellipsize (PangoLayout *layout); // https://developer.gnome.org/pango/stable/pango-Fonts.html#PangoFontDescription PangoFontDescription *pango_font_description_new (void); void pango_font_description_free (PangoFontDescription *desc); PangoFontDescription * pango_font_description_from_string (const char *str); void pango_font_description_set_family (PangoFontDescription *desc, const char *family); const char * pango_font_description_get_family (const PangoFontDescription *desc); void pango_font_description_set_absolute_size (PangoFontDescription *desc, double size); void pango_font_description_set_size (PangoFontDescription *desc, gint size); gint pango_font_description_get_size (const PangoFontDescription *desc); // https://developer.gnome.org/glib/stable/glib-Simple-XML-Subset-Parser.html gchar * g_markup_escape_text(const gchar *text, gssize length); """ ) qtile-0.31.0/libqtile/group.py0000664000175000017500000005001414762660347016151 0ustar epsilonepsilon# Copyright (c) 2012-2014 Tycho Andersen # Copyright (c) 2013 xarvh # Copyright (c) 2013 roger # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014 ramnes # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 dequis # Copyright (c) 2015 Dario Giovannetti # Copyright (c) 2015 Alexander Lozovskoy # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations from typing import TYPE_CHECKING from libqtile import hook, utils from libqtile.command.base import CommandObject, expose_command from libqtile.log_utils import logger if TYPE_CHECKING: from libqtile.command.base import ItemT class _Group(CommandObject): """A container for a bunch of windows Analogous to workspaces in other window managers. Each client window managed by the window manager belongs to exactly one group. A group is identified by its name but displayed in GroupBox widget by its label. """ def __init__(self, name, layout=None, label=None, screen_affinity=None, persist=False): self.screen_affinity = screen_affinity self.name = name self.label = name if label is None else label self.custom_layout = layout # will be set on _configure self.windows = [] self.tiled_windows = set() self.qtile = None self.layouts = [] self.floating_layout = None # self.focus_history lists the group's windows in the order they # received focus, from the oldest (first item) to the currently # focused window (last item); NB the list does *not* contain any # windows that never received focus; refer to self.windows for the # complete set self.focus_history = [] self.screen = None self.current_layout = None self.last_focused = None self.persist = persist def _configure(self, layouts, floating_layout, qtile): self.screen = None self.current_layout = 0 self.focus_history = [] self.windows = [] self.qtile = qtile self.layouts = [i.clone(self) for i in layouts] self.floating_layout = floating_layout if self.custom_layout is not None: self.layout = self.custom_layout self.custom_layout = None @property def current_window(self): try: return self.focus_history[-1] except IndexError: # no window has focus return None @current_window.setter def current_window(self, win): try: self.focus_history.remove(win) except ValueError: # win has never received focus before pass self.focus_history.append(win) def _remove_from_focus_history(self, win): try: index = self.focus_history.index(win) except ValueError: # win has never received focus return False else: del self.focus_history[index] # return True if win was the last item (i.e. it was current_window) return index == len(self.focus_history) @property def layout(self): return self.layouts[self.current_layout] @layout.setter def layout(self, layout): """ Parameters ========== layout : a string with matching the name of a Layout object. """ for index, obj in enumerate(self.layouts): if obj.name == layout: self.use_layout(index) return logger.error("No such layout: %s", layout) def use_layout(self, index: int): assert -len(self.layouts) <= index < len(self.layouts), "layout index out of bounds" self.layout.hide() self.current_layout = index % len(self.layouts) hook.fire("layout_change", self.layouts[self.current_layout], self) self.layout_all() if self.screen is not None: screen_rect = self.screen.get_rect() self.layout.show(screen_rect) def use_next_layout(self): self.use_layout((self.current_layout + 1) % (len(self.layouts))) def use_previous_layout(self): self.use_layout((self.current_layout - 1) % (len(self.layouts))) def layout_all(self, warp=False, focus=True): """Layout the floating layer, then the current layout. Parameters ========== focus : If we have have a current_window give it focus, optionally moving warp to it. """ if self.screen and self.windows: with self.qtile.core.masked(): normal = [x for x in self.windows if not x.floating] floating = [x for x in self.windows if x.floating and not x.minimized] screen_rect = self.screen.get_rect() if normal: try: self.layout.layout(normal, screen_rect) except Exception: logger.exception("Exception in layout %s", self.layout.name) if floating: self.floating_layout.layout(floating, screen_rect) if focus: if self.current_window and self.screen == self.qtile.current_screen: self.current_window.focus(warp) else: # Screen has lost focus so we reset record of focused window so # focus will warp when screen is focused again self.last_focused = None def set_screen(self, screen, warp=True): """Set this group's screen to screen""" if screen == self.screen: return self.screen = screen if self.screen: # move all floating guys offset to new screen self.floating_layout.to_screen(self, self.screen) self.layout_all(warp=warp and self.qtile.config.cursor_warp) screen_rect = self.screen.get_rect() self.floating_layout.show(screen_rect) self.layout.show(screen_rect) else: self.hide() def hide(self): self.screen = None with self.qtile.core.masked(): for i in self.windows: i.hide() self.layout.hide() def focus(self, win, warp=True, force=False): """Focus the given window If win is in the group, blur any windows and call ``focus`` on the layout (in case it wants to track anything), fire focus_change hook and invoke layout_all. Parameters ========== win : Window to focus warp : Warp pointer to win. This should basically always be True, unless the focus event is coming from something like EnterNotify, where the user is actively using the mouse, or on full screen layouts where only one window is "maximized" at a time, and it doesn't make sense for the mouse to automatically move. """ if self.qtile._drag and not force: # don't change focus while dragging windows (unless forced) return if win: if win not in self.windows: return # ignore focus events if window is the current window if win is self.last_focused: warp = False self.current_window = win self.last_focused = self.current_window if win.floating: for layout in self.layouts: layout.blur() self.floating_layout.focus(win) else: self.floating_layout.blur() for layout in self.layouts: layout.focus(win) hook.fire("focus_change") self.layout_all(warp) @expose_command() def info(self): """Returns a dictionary of info for this group""" return dict( name=self.name, label=self.label, focus=self.current_window.name if self.current_window else None, tiled_windows={i.name for i in self.tiled_windows}, windows=[i.name for i in self.windows], focus_history=[i.name for i in self.focus_history], layout=self.layout.name, layouts=[i.name for i in self.layouts], floating_info=self.floating_layout.info(), screen=self.screen.index if self.screen else None, ) def add(self, win, focus=True, force=False): hook.fire("group_window_add", self, win) if win not in self.windows: self.windows.append(win) win.group = self if self.qtile.config.auto_fullscreen and win.wants_to_fullscreen: win.fullscreen = True elif self.floating_layout.match(win) and not win.fullscreen: win.floating = True if win.floating and not win.fullscreen: self.floating_layout.add_client(win) else: self.tiled_windows.add(win) for i in self.layouts: i.add_client(win) if focus: self.focus(win, warp=True, force=force) else: self.layout_all(focus=False) def remove(self, win, force=False): hook.fire("group_window_remove", self, win) self.windows.remove(win) hadfocus = self._remove_from_focus_history(win) win.group = None if win.floating: nextfocus = self.floating_layout.remove(win) nextfocus = ( nextfocus or self.current_window or self.layout.focus_first() or self.floating_layout.focus_first(group=self) ) # Remove from the tiled layouts if it was not floating or fullscreen if not win.floating or win.fullscreen: for i in self.layouts: if i is self.layout: nextfocus = i.remove(win) else: i.remove(win) nextfocus = ( nextfocus or self.floating_layout.focus_first(group=self) or self.current_window or self.layout.focus_first() ) if win in self.tiled_windows: self.tiled_windows.remove(win) # a notification may not have focus if hadfocus: self.focus(nextfocus, warp=True, force=force) # no next focus window means focus changed to nothing if not nextfocus: hook.fire("focus_change") elif self.screen: self.layout_all() def mark_floating(self, win, floating): if floating: if win in self.floating_layout.find_clients(self): # already floating pass else: # Remove from the tiled windows list if the window is not fullscreen if not win.fullscreen: self.tiled_windows.remove(win) # Remove the window from the layout if it is not fullscreen for i in self.layouts: i.remove(win) if win is self.current_window: i.blur() self.floating_layout.add_client(win) if win is self.current_window: self.floating_layout.focus(win) else: self.floating_layout.remove(win) self.floating_layout.blur() # A window that was fullscreen should only be added if it was not a tiled window if win not in self.tiled_windows: for i in self.layouts: i.add_client(win) self.tiled_windows.add(win) if win is self.current_window: for i in self.layouts: i.focus(win) self.layout_all() def _items(self, name) -> ItemT: if name == "layout": return True, list(range(len(self.layouts))) if name == "screen" and self.screen is not None: return True, [] if name == "window": return self.current_window is not None, [i.wid for i in self.windows] return None def _select(self, name, sel): if name == "layout": if sel is None: return self.layout return utils.lget(self.layouts, sel) if name == "screen": return self.screen if name == "window": if sel is None: return self.current_window for i in self.windows: if i.wid == sel: return i raise RuntimeError(f"Invalid selection: {name}") @expose_command() def setlayout(self, layout): self.layout = layout @expose_command() def toscreen(self, screen=None, toggle=False): """Pull a group to a specified screen. Parameters ========== screen : Screen offset. If not specified, we assume the current screen. toggle : If this group is already on the screen, then the group is toggled with last used Examples ======== Pull group to the current screen:: toscreen() Pull group to screen 0:: toscreen(0) """ if screen is None: screen = self.qtile.current_screen else: screen = self.qtile.screens[screen] if screen.group == self: if toggle: screen.toggle_group(self) else: screen.set_group(self) def _get_group(self, direction, skip_empty=False, skip_managed=False): """Find a group walking the groups list in the specified direction Parameters ========== skip_empty : skips the empty groups skip_managed : skips the groups that have a screen """ def match(group): from libqtile import scratchpad if group is self: return True if skip_empty and not group.windows: return False if skip_managed and group.screen: return False if isinstance(group, scratchpad.ScratchPad): return False return True try: groups = [group for group in self.qtile.groups if match(group)] index = (groups.index(self) + direction) % len(groups) return groups[index] except ValueError: # group is not managed return None def get_previous_group(self, skip_empty=False, skip_managed=False): return self._get_group(-1, skip_empty, skip_managed) def get_next_group(self, skip_empty=False, skip_managed=False): return self._get_group(1, skip_empty, skip_managed) @expose_command() def unminimize_all(self): """Unminimise all windows in this group""" for win in self.windows: win.minimized = False self.layout_all() @expose_command() def next_window(self): """ Focus the next window in group. Method cycles _all_ windows in group regardless if tiled in current layout or floating. Cycling of tiled and floating windows is not mixed. The cycling order depends on the current Layout. """ if not self.windows: return if self.current_window.floating: nxt = ( self.floating_layout.focus_next(self.current_window) or self.layout.focus_first() or self.floating_layout.focus_first(group=self) ) else: nxt = ( self.layout.focus_next(self.current_window) or self.floating_layout.focus_first(group=self) or self.layout.focus_first() ) self.focus(nxt, True) @expose_command() def prev_window(self): """ Focus the previous window in group. Method cycles _all_ windows in group regardless if tiled in current layout or floating. Cycling of tiled and floating windows is not mixed. The cycling order depends on the current Layout. """ if not self.windows: return if self.current_window.floating: nxt = ( self.floating_layout.focus_previous(self.current_window) or self.layout.focus_last() or self.floating_layout.focus_last(group=self) ) else: nxt = ( self.layout.focus_previous(self.current_window) or self.floating_layout.focus_last(group=self) or self.layout.focus_last() ) self.focus(nxt, True) @expose_command() def focus_back(self): """ Focus the window that had focus before the current one got it. Repeated calls to this function would basically continuously switch between the last two focused windows. Do nothing if less than 2 windows ever received focus. """ try: win = self.focus_history[-2] except IndexError: pass else: self.focus(win) @expose_command() def focus_by_name(self, name): """ Focus the first window with the given name. Do nothing if the name is not found. """ for win in self.windows: if win.name == name: self.focus(win) break @expose_command() def info_by_name(self, name): """ Get the info for the first window with the given name without giving it focus. Do nothing if the name is not found. """ for win in self.windows: if win.name == name: return win.info() @expose_command() def focus_by_index(self, index: int) -> None: """ Change to the window at the specified index in the current group. """ windows = self.windows if index < 0 or index > len(windows) - 1: return self.focus(windows[index]) @expose_command() def swap_window_order(self, new_location: int) -> None: """ Change the order of the current window within the current group. """ if new_location < 0 or new_location > len(self.windows) - 1: return windows = self.windows current_window_index = windows.index(self.current_window) windows[current_window_index], windows[new_location] = ( windows[new_location], windows[current_window_index], ) @expose_command() def switch_groups(self, name): """Switch position of current group with name""" self.qtile.switch_groups(self.name, name) @expose_command() def set_label(self, label): """ Set the display name of current group to be used in GroupBox widget. If label is None, the name of the group is used as display name. If label is the empty string, the group is invisible in GroupBox. """ self.label = label if label is not None else self.name hook.fire("changegroup") def __repr__(self): return f"" qtile-0.31.0/libqtile/log_utils.py0000664000175000017500000001062014762660347017015 0ustar epsilonepsilon# Copyright (c) 2012 Florian Mounier # Copyright (c) 2013-2014 Tao Sauvage # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 roger # Copyright (c) 2022 Matt Colligan # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations import os import sys import typing import warnings from logging import WARNING, Formatter, StreamHandler, captureWarnings, getLogger from logging.handlers import RotatingFileHandler from pathlib import Path if typing.TYPE_CHECKING: from logging import Logger, LogRecord logger = getLogger(__package__) class ColorFormatter(Formatter): """Logging formatter adding console colors to the output.""" black, red, green, yellow, blue, magenta, cyan, white = range(8) colors = { "WARNING": yellow, "INFO": green, "DEBUG": blue, "CRITICAL": yellow, "ERROR": red, "RED": red, "GREEN": green, "YELLOW": yellow, "BLUE": blue, "MAGENTA": magenta, "CYAN": cyan, "WHITE": white, } reset_seq = "\033[0m" color_seq = "\033[%dm" bold_seq = "\033[1m" def format(self, record: LogRecord) -> str: """Format the record with colors.""" color = self.color_seq % (30 + self.colors[record.levelname]) message = Formatter.format(self, record) message = ( message.replace("$RESET", self.reset_seq) .replace("$BOLD", self.bold_seq) .replace("$COLOR", color) ) for color, value in self.colors.items(): message = ( message.replace("$" + color, self.color_seq % (value + 30)) .replace("$BG" + color, self.color_seq % (value + 40)) .replace("$BG-" + color, self.color_seq % (value + 40)) ) return message + self.reset_seq def get_default_log() -> Path: data_directory = os.path.expandvars("$XDG_DATA_HOME") if data_directory == "$XDG_DATA_HOME": # if variable wasn't set data_directory = os.path.expanduser("~/.local/share") qtile_directory = Path(data_directory) / "qtile" if not qtile_directory.exists(): qtile_directory.mkdir(parents=True) return qtile_directory / "qtile.log" def init_log( log_level: int = WARNING, log_path: Path | None = None, log_size: int = 10000000, log_numbackups: int = 1, logger: Logger = logger, ) -> None: for handler in logger.handlers: logger.removeHandler(handler) if log_path is None or os.getenv("QTILE_XEPHYR"): # During tests or interactive xephyr development, log to stdout. handler = StreamHandler(sys.stdout) formatter: Formatter = ColorFormatter( "$RESET$COLOR%(asctime)s $BOLD$COLOR%(name)s " "%(filename)s:%(funcName)s():L%(lineno)d $RESET %(message)s" ) else: # Otherwise during normal usage, log to file. handler = RotatingFileHandler( log_path, maxBytes=log_size, backupCount=log_numbackups, ) formatter = Formatter( "%(asctime)s %(levelname)s %(name)s " "%(filename)s:%(funcName)s():L%(lineno)d %(message)s" ) handler.setFormatter(formatter) logger.addHandler(handler) logger.setLevel(log_level) # Capture everything from the warnings module. captureWarnings(True) warnings.simplefilter("always") logger.debug("Starting logging for Qtile") qtile-0.31.0/libqtile/notify.py0000664000175000017500000002001514762660347016323 0ustar epsilonepsilon# Copyright (c) 2010 dequis # Copyright (c) 2011 Florian Mounier # Copyright (c) 2011 Mounier Florian # Copyright (c) 2013 Mickael FALCK # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2020 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from typing import Any try: from dbus_fast.aio import MessageBus from dbus_fast.constants import NameFlag, RequestNameReply from dbus_fast.service import ServiceInterface, method, signal has_dbus = True except ImportError: has_dbus = False from libqtile.log_utils import logger BUS_NAME = "org.freedesktop.Notifications" SERVICE_PATH = "/org/freedesktop/Notifications" notifier: Any = None class ClosedReason: expired = 1 dismissed = 2 method = 3 # CloseNotification method if has_dbus: class NotificationService(ServiceInterface): def __init__(self, manager): super().__init__(BUS_NAME) self.manager = manager self._capabilities = {"body"} @method() def GetCapabilities(self) -> "as": # type:ignore # noqa: N802, F722 return list(self._capabilities) def register_capabilities(self, capabilities): if isinstance(capabilities, str): self._capabilities.add(capabilities) elif isinstance(capabilities, tuple | list | set): self._capabilities.update(set(capabilities)) @method() def Notify( # noqa: N802, F722 self, app_name: "s", # type:ignore # noqa: F821 replaces_id: "u", # type:ignore # noqa: F821 app_icon: "s", # type:ignore # noqa: F821 summary: "s", # type:ignore # noqa: F821 body: "s", # type:ignore # noqa: F821 actions: "as", # type:ignore # noqa: F722, F821 hints: "a{sv}", # type:ignore # noqa: F722, F821 timeout: "i", # type:ignore # noqa: F821 ) -> "u": # type:ignore # noqa: F821 notif = Notification( summary, body, timeout, hints, app_name, replaces_id, app_icon, actions ) return self.manager.add(notif) @method() def CloseNotification(self, nid: "u"): # type:ignore # noqa: F821, N802 self.manager.close(nid) @signal() def NotificationClosed( # noqa: N802 self, nid: "u", # type: ignore # noqa: F821 reason: "u", # type: ignore # noqa: F821 ) -> "uu": # type: ignore # noqa: F821 return [nid, reason] @signal() def ActionInvoked( # noqa: N802 self, nid: "u", # type: ignore # noqa: F821 action_key: "s", # type: ignore # noqa: F821 ) -> "us": # type:ignore # noqa: N802, F821 return [nid, action_key] @method() def GetServerInformation(self) -> "ssss": # type: ignore # noqa: N802, F821 return ["qtile-notify-daemon", "qtile", "1.0", "1"] class Notification: def __init__( self, summary, body="", timeout=-1, hints=None, app_name="", replaces_id=None, app_icon=None, actions=None, ): self.summary = summary self.body = body self.timeout = timeout self.hints = hints or {} self.app_name = app_name self.replaces_id = replaces_id self.app_icon = app_icon self.actions = actions class NotificationManager: def __init__(self): self.notifications = [] self.callbacks = [] self.close_callbacks = [] self._service = None self.bus = None async def service(self): if not self.callbacks: if self.bus is None: try: self.bus = await MessageBus().connect() except Exception: logger.exception("Dbus connection failed") self._service = None return self._service self._service = NotificationService(self) self.bus.export(SERVICE_PATH, self._service) reply = await self.bus.request_name( BUS_NAME, flags=NameFlag.ALLOW_REPLACEMENT | NameFlag.REPLACE_EXISTING | NameFlag.DO_NOT_QUEUE, ) # Check the reply to see if another server is already running. # Other replies could be RequestNameReply.PRIMARY_OWNER or # RequestNameReply.ALREADY_ONWER. Both would indicate qtile server # is running successfully so we don't need to check for them. # RequestNameReply.IN_QUEUE should not be possible as we pass # NameFlag.DO_NOT_QUEUE when requesting the name. if reply == RequestNameReply.EXISTS: logger.warning( "Cannot start notification server as another server is already running." ) self._service = None self.bus.disconnect() self.bus = None return self._service async def register(self, callback, capabilities=None, on_close=None): service = await self.service() if not service: logger.warning( "Registering %s without any dbus connection existing", callback.__name__, ) self.callbacks.append(callback) if capabilities: self._service.register_capabilities(capabilities) if on_close: self.close_callbacks.append(on_close) def unregister(self, callback, on_close=None): try: self.callbacks.remove(callback) except ValueError: logger.error("Unable to remove notify callback. Unknown callback.") if on_close: try: self.close_callbacks.remove(on_close) except ValueError: logger.error("Unable to remove notify on_close callback. Unknown callback.") def add(self, notif): self.notifications.append(notif) notif.id = len(self.notifications) for callback in self.callbacks: try: callback(notif) except Exception: logger.exception("Exception in notifier callback") return len(self.notifications) def show(self, *args, **kwargs): notif = Notification(*args, **kwargs) return (notif, self.add(notif)) def close(self, nid): for callback in self.close_callbacks: try: callback(nid) except Exception: logger.exception("Exception in notifier close callback") notifier = NotificationManager() qtile-0.31.0/libqtile/__init__.py0000664000175000017500000000241214762660347016553 0ustar epsilonepsilon# Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Tycho Andersen # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. class _UndefinedCore: name = None class _UndefinedQtile: core = _UndefinedCore() qtile = _UndefinedQtile() def init(q): global qtile qtile = q qtile-0.31.0/libqtile/py.typed0000664000175000017500000000000014762660347016130 0ustar epsilonepsilonqtile-0.31.0/libqtile/popup.py0000664000175000017500000001300714762660347016161 0ustar epsilonepsilon# Copyright (c) 2020-21, Matt Colligan. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations from typing import TYPE_CHECKING from libqtile import configurable, pangocffi if TYPE_CHECKING: from typing import Any from cairocffi import ImageSurface from libqtile.backend.base import Drawer from libqtile.core.manager import Qtile from libqtile.utils import ColorType class Popup(configurable.Configurable): """ This class can be used to create popup windows that display images and/or text. """ defaults = [ ("opacity", 1.0, "Opacity of notifications."), ("foreground", "#ffffff", "Colour of text."), ("background", "#111111", "Background colour."), ("border", "#111111", "Border colour."), ("border_width", 0, "Line width of drawn borders."), ("font", "sans", "Font used in notifications."), ("font_size", 14, "Size of font."), ("fontshadow", None, "Colour for text shadows, or None for no shadows."), ("horizontal_padding", 0, "Padding at sides of text."), ("vertical_padding", 0, "Padding at top and bottom of text."), ("text_alignment", "left", "Text alignment: left, center or right."), ("wrap", True, "Whether to wrap text."), ] def __init__( self, qtile: Qtile, x: int = 50, y: int = 50, width: int = 256, height: int = 64, **config, ): configurable.Configurable.__init__(self, **config) self.add_defaults(Popup.defaults) self.qtile = qtile self.win: Any = qtile.core.create_internal( x, y, width, height ) # TODO: better annotate Internal self.win.opacity = self.opacity self.win.process_button_click = self.process_button_click self.win.process_window_expose = self.draw self.drawer: Drawer = self.win.create_drawer(width, height) self.clear() self.layout = self.drawer.textlayout( text="", colour=self.foreground, font_family=self.font, font_size=self.font_size, font_shadow=self.fontshadow, wrap=self.wrap, markup=True, ) self.layout.layout.set_alignment(pangocffi.ALIGNMENTS[self.text_alignment]) if self.border_width and self.border: self.win.paint_borders(self.border, self.border_width) self.x = self.win.x self.y = self.win.y def process_button_click(self, x, y, button) -> None: if button == 1: self.hide() @property def width(self) -> int: return self.win.width @width.setter def width(self, value: int) -> None: self.win.width = value self.drawer.width = value @property def height(self) -> int: return self.win.height @height.setter def height(self, value: int) -> None: self.win.height = value self.drawer.height = value @property def text(self) -> str: return self.layout.text @text.setter def text(self, value: str) -> None: self.layout.text = value @property def foreground(self) -> ColorType: return self._foreground @foreground.setter def foreground(self, value: ColorType) -> None: self._foreground = value if hasattr(self, "layout"): self.layout.colour = value def set_border(self, color: ColorType) -> None: self.win.paint_borders(color, self.border_width) def clear(self) -> None: self.drawer.clear(self.background) def draw_text(self, x: int | None = None, y: int | None = None) -> None: self.layout.draw( x or self.horizontal_padding, y or self.vertical_padding, ) def draw(self) -> None: self.drawer.draw() def place(self) -> None: self.win.place( self.x, self.y, self.width, self.height, self.border_width, self.border, above=True ) def unhide(self) -> None: self.win.unhide() def draw_image(self, image: ImageSurface, x: int, y: int) -> None: """ Paint an image onto the window at point x, y. The image should be a surface e.g. loaded from libqtile.images.Img.from_path. """ self.drawer.ctx.set_source_surface(image, x, y) self.drawer.ctx.paint() def hide(self) -> None: self.win.hide() def kill(self) -> None: self.win.kill() self.layout.finalize() self.drawer.finalize() qtile-0.31.0/libqtile/extension/0000775000175000017500000000000014762660347016457 5ustar epsilonepsilonqtile-0.31.0/libqtile/extension/dmenu.py0000664000175000017500000001543714762660347020153 0ustar epsilonepsilon# Copyright (C) 2016, zordsdavini # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import shlex from libqtile.extension import base class Dmenu(base.RunCommand): """ Python wrapper for dmenu http://tools.suckless.org/dmenu/ """ defaults = [ ("dmenu_font", None, "override the default 'font' and 'fontsize' options for dmenu"), # NOTE: Do not use a list as a default value, since it would be shared # among all the objects inheriting this class, and if one of them # modified it, all the other objects would see the modified list; # use a string or a tuple instead, which are immutable ("dmenu_command", "dmenu", "the dmenu command to be launched"), ("dmenu_bottom", False, "dmenu appears at the bottom of the screen"), ("dmenu_ignorecase", False, "dmenu matches menu items case insensitively"), ("dmenu_lines", None, "dmenu lists items vertically, with the given number of lines"), ( "dmenu_prompt", None, "defines the prompt to be displayed to the left of the input field", ), ("dmenu_height", None, "defines the height (only supported by some dmenu forks)"), ] def __init__(self, **config): base.RunCommand.__init__(self, **config) self.add_defaults(Dmenu.defaults) def _configure(self, qtile): base.RunCommand._configure(self, qtile) dmenu_command = self.dmenu_command or self.command if isinstance(dmenu_command, str): self.configured_command = shlex.split(dmenu_command) else: # Create a clone of dmenu_command, don't use it directly since # it's shared among all the instances of this class self.configured_command = list(dmenu_command) if self.dmenu_bottom: self.configured_command.append("-b") if self.dmenu_ignorecase: self.configured_command.append("-i") if self.dmenu_lines: self.configured_command.extend(("-l", str(self.dmenu_lines))) if self.dmenu_prompt: self.configured_command.extend(("-p", self.dmenu_prompt)) if self.dmenu_font: font = self.dmenu_font elif self.font: if self.fontsize: font = f"{self.font}-{self.fontsize}" else: font = self.font self.configured_command.extend(("-fn", font)) if self.background: self.configured_command.extend(("-nb", self.background)) if self.foreground: self.configured_command.extend(("-nf", self.foreground)) if self.selected_background: self.configured_command.extend(("-sb", self.selected_background)) if self.selected_foreground: self.configured_command.extend(("-sf", self.selected_foreground)) # NOTE: The original dmenu doesn't support the '-h' option if self.dmenu_height: self.configured_command.extend(("-h", str(self.dmenu_height))) def run(self, items=None): if items and self.dmenu_lines: lines = min(len(items), int(self.dmenu_lines)) self.configured_command.extend(("-l", str(lines))) proc = super().run() if items: input_str = "\n".join([i for i in items]) + "\n" return proc.communicate(str.encode(input_str))[0].decode("utf-8") return proc class DmenuRun(Dmenu): """ Special case to run applications. config.py should have something like: .. code-block:: python from libqtile import extension keys = [ Key(['mod4'], 'r', lazy.run_extension(extension.DmenuRun( dmenu_prompt=">", dmenu_font="Andika-8", background="#15181a", foreground="#00ff00", selected_background="#079822", selected_foreground="#fff", dmenu_height=24, # Only supported by some dmenu forks ))), ] """ defaults = [ ("dmenu_command", "dmenu_run", "the dmenu command to be launched"), ] def __init__(self, **config): Dmenu.__init__(self, **config) self.add_defaults(DmenuRun.defaults) class J4DmenuDesktop(Dmenu): """ Python wrapper for j4-dmenu-desktop https://github.com/enkore/j4-dmenu-desktop """ defaults = [ ("j4dmenu_command", "j4-dmenu-desktop", "the dmenu command to be launched"), ( "j4dmenu_use_xdg_de", False, "read $XDG_CURRENT_DESKTOP to determine the desktop environment", ), ("j4dmenu_display_binary", False, "display binary name after each entry"), ("j4dmenu_generic", True, "include the generic name of desktop entries"), ("j4dmenu_terminal", None, "terminal emulator used to start terminal apps"), ("j4dmenu_usage_log", None, "file used to sort items by usage frequency"), ] def __init__(self, **config): Dmenu.__init__(self, **config) self.add_defaults(J4DmenuDesktop.defaults) def _configure(self, qtile): Dmenu._configure(self, qtile) self.configured_command = [ self.j4dmenu_command, "--dmenu", " ".join(shlex.quote(arg) for arg in self.configured_command), ] if self.j4dmenu_use_xdg_de: self.configured_command.append("--use-xdg-de") if self.j4dmenu_display_binary: self.configured_command.append("--display-binary") if not self.j4dmenu_generic: self.configured_command.append("--no-generic") if self.j4dmenu_terminal: self.configured_command.extend(("--term", self.j4dmenu_terminal)) if self.j4dmenu_usage_log: self.configured_command.extend(("--usage-log", self.j4dmenu_usage_log)) qtile-0.31.0/libqtile/extension/__init__.py0000664000175000017500000000256214762660347020575 0ustar epsilonepsilon# Copyright (c) 2016 zordsdavini # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from libqtile.utils import lazify_imports extensions = { "CommandSet": "command_set", "Dmenu": "dmenu", "DmenuRun": "dmenu", "J4DmenuDesktop": "dmenu", "RunCommand": "base", "WindowList": "window_list", } __all__, __dir__, __getattr__ = lazify_imports(extensions, __package__) qtile-0.31.0/libqtile/extension/window_list.py0000664000175000017500000000573414762660347021404 0ustar epsilonepsilon# Copyright (C) 2016, zordsdavini # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from libqtile.backend import base from libqtile.extension.dmenu import Dmenu from libqtile.scratchpad import ScratchPad class WindowList(Dmenu): """ Give vertical list of all open windows in dmenu. Switch to selected. """ defaults = [ ("item_format", "{group}.{id}: {window}", "the format for the menu items"), ( "all_groups", True, "If True, list windows from all groups; otherwise only from the current group", ), ("dmenu_lines", "80", "Give lines vertically. Set to None get inline"), ] def __init__(self, **config): Dmenu.__init__(self, **config) self.add_defaults(WindowList.defaults) def list_windows(self): id = 0 self.item_to_win = {} if self.all_groups: windows = [w for w in self.qtile.windows_map.values() if isinstance(w, base.Window)] else: windows = self.qtile.current_group.windows for win in windows: if win.group and not isinstance(win.group, ScratchPad): item = self.item_format.format( group=win.group.label or win.group.name, id=id, window=win.name ) self.item_to_win[item] = win id += 1 def run(self): self.list_windows() out = super().run(items=self.item_to_win.keys()) try: sout = out.rstrip("\n") except AttributeError: # out is not a string (for example it's a Popen object returned # by super(WindowList, self).run() when there are no menu items to # list return try: win = self.item_to_win[sout] except KeyError: # The selected window got closed while the menu was open? return screen = self.qtile.current_screen screen.set_group(win.group) win.group.focus(win) qtile-0.31.0/libqtile/extension/command_set.py0000664000175000017500000000630314762660347021324 0ustar epsilonepsilon# Copyright (C) 2018, zordsdavini # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from libqtile.extension.dmenu import Dmenu class CommandSet(Dmenu): """ Give list of commands to be executed in dmenu style. ex. manage mocp deamon: .. code-block:: python Key([mod], 'm', lazy.run_extension(extension.CommandSet( commands={ 'play/pause': '[ $(mocp -i | wc -l) -lt 2 ] && mocp -p || mocp -G', 'next': 'mocp -f', 'previous': 'mocp -r', 'quit': 'mocp -x', 'open': 'urxvt -e mocp', 'shuffle': 'mocp -t shuffle', 'repeat': 'mocp -t repeat', }, pre_commands=['[ $(mocp -i | wc -l) -lt 1 ] && mocp -S'], **Theme.dmenu))), ex. CommandSet inside another CommandSet .. code-block:: python CommandSet( commands={ "Hello": CommandSet( commands={ "World": "echo 'Hello, World!'" }, **Theme.dmenu ) }, **Theme.dmenu ) """ defaults = [ ("commands", None, "dictionary of commands where key is runable command"), ("pre_commands", None, "list of commands to be executed before getting dmenu answer"), ] def __init__(self, **config): Dmenu.__init__(self, **config) self.add_defaults(CommandSet.defaults) def run(self): if not self.commands: return if self.pre_commands: for cmd in self.pre_commands: self.qtile.spawn(cmd) out = super().run(items=self.commands.keys()) try: sout = out.rstrip("\n") except AttributeError: # out is not a string (for example it's a Popen object returned # by super(WindowList, self).run() when there are no menu items to # list return if sout not in self.commands: return command = self.commands[sout] if isinstance(command, str): self.qtile.spawn(command) elif isinstance(command, CommandSet): command.run() qtile-0.31.0/libqtile/extension/base.py0000664000175000017500000001200014762660347017734 0ustar epsilonepsilon# Copyright (c) 2017 Dario Giovannetti # Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import re import shlex from subprocess import PIPE, Popen from typing import Any from libqtile import configurable from libqtile.log_utils import logger RGB = re.compile(r"^#?([a-fA-F0-9]{3}|[a-fA-F0-9]{6})$") class _Extension(configurable.Configurable): """Base Extension class""" installed_extensions = [] # type: list defaults = [ ("font", "sans", "defines the font name to be used"), ("fontsize", None, "defines the font size to be used"), ("background", None, "defines the normal background color (#RGB or #RRGGBB)"), ("foreground", None, "defines the normal foreground color (#RGB or #RRGGBB)"), ("selected_background", None, "defines the selected background color (#RGB or #RRGGBB)"), ("selected_foreground", None, "defines the selected foreground color (#RGB or #RRGGBB)"), ] def __init__(self, **config): configurable.Configurable.__init__(self, **config) self.add_defaults(_Extension.defaults) _Extension.installed_extensions.append(self) def _check_colors(self): """ dmenu needs colours to be in #rgb or #rrggbb format. Checks colour value, removes invalid values and adds # if missing. NB This should not be called in _Extension.__init__ as _Extension.global_defaults may not have been set at this point. """ for c in ["background", "foreground", "selected_background", "selected_foreground"]: col = getattr(self, c, None) if col is None: continue if not isinstance(col, str) or not RGB.match(col): logger.warning( "Invalid extension '%s' color: %s. Must be #RGB or #RRGGBB string.", c, col ) setattr(self, c, None) continue if not col.startswith("#"): col = f"#{col}" setattr(self, c, col) def _configure(self, qtile): self.qtile = qtile self._check_colors() def run(self): """ This method must be implemented by the subclasses. """ raise NotImplementedError() class RunCommand(_Extension): """ Run an arbitrary command. Mostly useful as a superclass for more specific extensions that need to interact with the qtile object. Also consider simply using lazy.spawn() or writing a `client `_. """ defaults: list[tuple[str, Any, str]] = [ # NOTE: Do not use a list as a default value, since it would be shared # among all the objects inheriting this class, and if one of them # modified it, all the other objects would see the modified list; # use a string or a tuple instead, which are immutable ("command", None, "the command to be launched (string or list with arguments)"), ] def __init__(self, **config): _Extension.__init__(self, **config) self.add_defaults(RunCommand.defaults) self.configured_command = None def run(self): """ An extension can inherit this class, define configured_command and use the process object by overriding this method and using super(): .. code-block:: python def _configure(self, qtile): Superclass._configure(self, qtile) self.configured_command = "foo --bar" def run(self): process = super(Subclass, self).run() """ if self.configured_command: if isinstance(self.configured_command, str): self.configured_command = shlex.split(self.configured_command) # Else assume that self.configured_command is already a sequence else: self.configured_command = self.command return Popen(self.configured_command, stdout=PIPE, stdin=PIPE) qtile-0.31.0/libqtile/dgroups.py0000664000175000017500000002164114762660347016504 0ustar epsilonepsilon# Copyright (c) 2011-2012 Florian Mounier # Copyright (c) 2012-2014 roger # Copyright (c) 2012 Craig Barnes # Copyright (c) 2012-2014 Tycho Andersen # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014 ramnes # Copyright (c) 2014 Sebastian Kricner # Copyright (c) 2014 Sean Vig # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import collections import libqtile.hook from libqtile.backend.base import Static from libqtile.config import Group, Key, Match, Rule from libqtile.lazy import lazy from libqtile.log_utils import logger def simple_key_binder(mod, keynames=None): """Bind keys to mod+group position or to the keys specified as second argument""" def func(dgroup): # unbind all for key in dgroup.keys[:]: dgroup.qtile.ungrab_key(key) dgroup.qtile.config.keys.remove(key) dgroup.keys.remove(key) if keynames: keys = keynames else: # keys 1 to 9 and 0 keys = list(map(str, list(range(1, 10)) + [0])) # bind all keys for keyname, group in zip(keys, dgroup.qtile.groups): name = group.name key = Key([mod], keyname, lazy.group[name].toscreen()) key_s = Key([mod, "shift"], keyname, lazy.window.togroup(name)) key_c = Key([mod, "control"], keyname, lazy.group.switch_groups(name)) dgroup.keys.extend([key, key_s, key_c]) dgroup.qtile.config.keys.extend([key, key_s, key_c]) dgroup.qtile.grab_key(key) dgroup.qtile.grab_key(key_s) dgroup.qtile.grab_key(key_c) return func class DGroups: """Dynamic Groups""" def __init__(self, qtile, dgroups, key_binder=None, delay=1): self.qtile = qtile self.groups = dgroups self.groups_map = {} self.rules = [] self.rules_map = {} self.last_rule_id = 0 for rule in getattr(qtile.config, "dgroups_app_rules", []): self.add_rule(rule) self.keys = [] self.key_binder = key_binder self._setup_hooks() self._setup_groups() self.delay = delay self.timeout = {} def add_rule(self, rule, last=True): rule_id = self.last_rule_id self.rules_map[rule_id] = rule if last: self.rules.append(rule) else: self.rules.insert(0, rule) self.last_rule_id += 1 return rule_id def remove_rule(self, rule_id): rule = self.rules_map.get(rule_id) if rule: self.rules.remove(rule) del self.rules_map[rule_id] else: logger.warning('Rule "%s" not found', rule_id) def add_dgroup(self, group, start=False): self.groups_map[group.name] = group rule = Rule(group.matches, group=group.name) self.rules.append(rule) if start: self.qtile.add_group( group.name, group.layout, group.layouts, group.label, screen_affinity=group.screen_affinity, ) def _setup_groups(self): for group in self.groups: self.add_dgroup(group, group.init) if group.spawn and not self.qtile.no_spawn: if isinstance(group.spawn, str): spawns = [group.spawn] else: spawns = group.spawn for spawn in spawns: pid = self.qtile.spawn(spawn) self.add_rule(Rule(Match(net_wm_pid=pid), group.name)) def _setup_hooks(self): libqtile.hook.subscribe.addgroup(self._addgroup) libqtile.hook.subscribe.client_new(self._add) libqtile.hook.subscribe.client_killed(self._del) if self.key_binder: libqtile.hook.subscribe.setgroup(lambda: self.key_binder(self)) libqtile.hook.subscribe.changegroup(lambda: self.key_binder(self)) def _addgroup(self, group_name): if group_name not in self.groups_map: self.add_dgroup(Group(group_name, persist=False)) def _add(self, client): if client in self.timeout: logger.debug("Remove dgroup source") self.timeout.pop(client).cancel() # ignore static windows if isinstance(client, Static): return # ignore windows whose groups is already set (e.g. from another hook or # when it was set on state restore) if client.group is not None: return group_set = False intrusive = False for rule in self.rules: # Matching Rules if rule.matches(client): if rule.group: if rule.group in self.groups_map: layout = self.groups_map[rule.group].layout layouts = self.groups_map[rule.group].layouts label = self.groups_map[rule.group].label else: layout = None layouts = None label = None group_added = self.qtile.add_group(rule.group, layout, layouts, label) client.togroup(rule.group) group_set = True group_obj = self.qtile.groups_map[rule.group] group = self.groups_map.get(rule.group) if group and group_added: for k, v in list(group.layout_opts.items()): if isinstance(v, collections.abc.Callable): v(group_obj.layout) else: setattr(group_obj.layout, k, v) affinity = group.screen_affinity if affinity and len(self.qtile.screens) > affinity: self.qtile.screens[affinity].set_group(group_obj) if rule.float: client.enable_floating() if rule.intrusive: intrusive = rule.intrusive if rule.break_on_match: break # If app doesn't have a group if not group_set: current_group = self.qtile.current_group.name if ( current_group in self.groups_map and self.groups_map[current_group].exclusive and not intrusive ): wm_class = client.get_wm_class() if wm_class: if len(wm_class) > 1: wm_class = wm_class[1] else: wm_class = wm_class[0] group_name = wm_class else: group_name = client.name or "Unnamed" self.add_dgroup(Group(group_name, persist=False), start=True) client.togroup(group_name) self.sort_groups() def sort_groups(self): grps = self.qtile.groups sorted_grps = sorted(grps, key=lambda g: self.groups_map[g.name].position) if grps != sorted_grps: self.qtile.groups = sorted_grps libqtile.hook.fire("changegroup") def _del(self, client): # ignore static windows if isinstance(client, Static): return group = client.group def delete_client(): # Delete group if empty and don't persist if ( group and group.name in self.groups_map and not self.groups_map[group.name].persist and len(group.windows) <= 0 ): self.qtile.delete_group(group.name) self.sort_groups() del self.timeout[client] if group is not None and group.persist: return logger.debug("Deleting %s in %ss", group, self.delay) if client not in self.timeout: self.timeout[client] = self.qtile.call_later(self.delay, delete_client) qtile-0.31.0/libqtile/backend/0000775000175000017500000000000014762660347016032 5ustar epsilonepsilonqtile-0.31.0/libqtile/backend/wayland/0000775000175000017500000000000014762660347017471 5ustar epsilonepsilonqtile-0.31.0/libqtile/backend/wayland/__init__.py0000664000175000017500000000272114762660347021604 0ustar epsilonepsilon# Copyright (c) 2022-3 Matt Colligan # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. try: # Shorten the import for this because it will be used in configs from libqtile.backend.wayland.inputs import InputConfig # noqa: F401 except ModuleNotFoundError: print("InputConfig couldn't be imported from libqtile.backend.wayland") print("If this happened during setup.py installation, ignore this message.") print("Otherwise, make sure to run ./scripts/ffibuild.") qtile-0.31.0/libqtile/backend/wayland/drawer.py0000664000175000017500000000630314762660347021331 0ustar epsilonepsilon# Copyright (c) 2022 m-col # Copyright (c) 2023 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations from typing import TYPE_CHECKING import cairocffi from wlroots.util.region import PixmanRegion32 from libqtile.backend.base import drawer if TYPE_CHECKING: from libqtile.backend.wayland.window import Internal from libqtile.core.manager import Qtile class Drawer(drawer.Drawer): """ A helper class for drawing and text layout. 1. We stage drawing operations locally in memory using a cairo RecordingSurface. 2. Then apply these operations to the windows's underlying ImageSurface. """ def __init__(self, qtile: Qtile, win: Internal, width: int, height: int): drawer.Drawer.__init__(self, qtile, win, width, height) def _draw( self, offsetx: int = 0, offsety: int = 0, width: int | None = None, height: int | None = None, src_x: int = 0, src_y: int = 0, ) -> None: if offsetx > self._win.width: return # Make sure geometry doesn't extend beyond texture if width is None: width = self.width if width > self._win.width - offsetx: width = self._win.width - offsetx if height is None: height = self.height if height > self._win.height - offsety: height = self._win.height - offsety # Paint recorded operations to our window's underlying ImageSurface with cairocffi.Context(self._win.surface) as context: context.set_operator(cairocffi.OPERATOR_SOURCE) # Adjust the source surface position by src_x and src_y e.g. if we want # to render part of the surface in a different position context.set_source_surface(self.surface, offsetx - src_x, offsety - src_y) context.rectangle(offsetx, offsety, width, height) context.fill() damage = PixmanRegion32() damage.init_rect(offsetx, offsety, width, height) # TODO: do we really need to `set_buffer` here? would be good to just set damage self._win._scene_buffer.set_buffer_with_damage(self._win.wlr_buffer, damage) damage.fini() qtile-0.31.0/libqtile/backend/wayland/wlrq.py0000664000175000017500000002541214762660347021034 0ustar epsilonepsilon# Copyright (c) 2021 Matt Colligan # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations import functools import operator from dataclasses import dataclass from typing import TYPE_CHECKING, cast import cairocffi from pywayland.server import Listener from wlroots import ffi as wlr_ffi from wlroots import lib as wlr_lib from wlroots.wlr_types import Buffer, SceneBuffer, SceneTree, data_device_manager from wlroots.wlr_types.keyboard import KeyboardModifier from wlroots.wlr_types.scene import SceneRect from libqtile.log_utils import logger from libqtile.utils import QtileError, rgb try: # Continue if ffi not built, so that docs can be built without wayland deps. from libqtile.backend.wayland._ffi import ffi, lib except ModuleNotFoundError: pass if TYPE_CHECKING: from collections.abc import Callable from typing import Any from pywayland.server import Signal from wlroots import xwayland from wlroots.wlr_types import Surface from libqtile.backend.wayland.core import Core from libqtile.config import Screen from libqtile.utils import ColorType class WlrQError(QtileError): pass ModMasks = { "shift": KeyboardModifier.SHIFT, "lock": KeyboardModifier.CAPS, "control": KeyboardModifier.CTRL, "mod1": KeyboardModifier.ALT, "mod2": KeyboardModifier.MOD2, "mod3": KeyboardModifier.MOD3, "mod4": KeyboardModifier.LOGO, "mod5": KeyboardModifier.MOD5, } # from linux/input-event-codes.h _KEY_MAX = 0x2FF # These are mouse buttons 1-9 BTN_LEFT = 0x110 BTN_MIDDLE = 0x112 BTN_RIGHT = 0x111 SCROLL_UP = _KEY_MAX + 1 SCROLL_DOWN = _KEY_MAX + 2 SCROLL_LEFT = _KEY_MAX + 3 SCROLL_RIGHT = _KEY_MAX + 4 BTN_SIDE = 0x113 BTN_EXTRA = 0x114 buttons = [ BTN_LEFT, BTN_MIDDLE, BTN_RIGHT, SCROLL_UP, SCROLL_DOWN, SCROLL_LEFT, SCROLL_RIGHT, BTN_SIDE, BTN_EXTRA, ] # from drm_fourcc.h DRM_FORMAT_ARGB8888 = 875713089 def translate_masks(modifiers: list[str]) -> int: """ Translate a modifier mask specified as a list of strings into an or-ed bit representation. """ masks = [] for i in modifiers: try: masks.append(ModMasks[i.lower()]) except KeyError as e: raise WlrQError(f"Unknown modifier: {i}") from e if masks: return functools.reduce(operator.or_, masks) else: return 0 class Painter: def __init__(self, core: Core): self.core = core def _clear_previous_background(self, screen: Screen) -> None: # Drop references to existing wallpaper if there is one if screen in self.core.wallpapers: old_scene_buffer, old_surface = self.core.wallpapers.pop(screen) old_scene_buffer.node.destroy() if old_surface is not None: old_surface.finish() def fill(self, screen: Screen, background: ColorType) -> None: self._clear_previous_background(screen) rect = SceneRect(self.core.wallpaper_tree, screen.width, screen.height, rgb(background)) self.core.wallpapers[screen] = (rect, None) def paint(self, screen: Screen, image_path: str, mode: str | None = None) -> None: try: with open(image_path, "rb") as f: image, _ = cairocffi.pixbuf.decode_to_image_surface(f.read()) except OSError: logger.exception("Could not load wallpaper:") return image_w = image.get_width() image_h = image.get_height() # the dimensions of cairo are the full screen if the mode is not specified # otherwise they are the image dimensions for fill and stretch mode # the actual scaling of the image is then done with wlroots functions # so that the GPU is used cairo_w = image_w if mode in ["fill", "stretch"] else screen.width cairo_h = image_h if mode in ["fill", "stretch"] else screen.height surface = cairocffi.ImageSurface(cairocffi.FORMAT_ARGB32, cairo_w, cairo_h) with cairocffi.Context(surface) as context: context.save() context.set_operator(cairocffi.OPERATOR_SOURCE) context.set_source_rgb(0, 0, 0) context.rectangle(0, 0, cairo_w, cairo_h) context.fill() context.restore() context.set_source_surface(image) context.paint() surface.flush() stride = surface.get_stride() data = cairocffi.cairo.cairo_image_surface_get_data(surface._pointer) wlr_buffer = lib.cairo_buffer_create(cairo_w, cairo_h, stride, data) if wlr_buffer == ffi.NULL: raise RuntimeError("Couldn't allocate cairo buffer.") self._clear_previous_background(screen) # We need to keep a reference to the surface so its data persists if scene_buffer := SceneBuffer.create(self.core.wallpaper_tree, Buffer(wlr_buffer)): scene_buffer.node.set_position(screen.x, screen.y) self.core.wallpapers[screen] = (scene_buffer, surface) else: logger.warning("Failed to create wlr_scene_buffer.") return # Handle fill mode if mode == "fill": if image_w / image_h > screen.width / screen.height: # image is wider than screen; clip left and right new_w = image_h * screen.width / screen.height side = (image_w - new_w) // 2 fbox = wlr_ffi.new("struct wlr_fbox *") fbox.x = side fbox.y = 0 fbox.width = image_w - 2 * side fbox.height = image_h wlr_lib.wlr_scene_buffer_set_source_box(scene_buffer._ptr, fbox) elif image_w / image_h < screen.width / screen.height: # image is taller than screen; clip top and bottom new_h = image_w * screen.height / screen.width side = (image_h - new_h) // 2 fbox = wlr_ffi.new("struct wlr_fbox *") fbox.x = 0 fbox.y = side fbox.width = image_w fbox.height = image_h - 2 * side wlr_lib.wlr_scene_buffer_set_source_box(scene_buffer._ptr, fbox) wlr_lib.wlr_scene_buffer_set_dest_size(scene_buffer._ptr, screen.width, screen.height) elif mode == "stretch": wlr_lib.wlr_scene_buffer_set_dest_size(scene_buffer._ptr, screen.width, screen.height) # Otherwise (mode is None), the image takes up its native size in # layout coordinate pixels (which doesn't account for output scaling) class HasListeners: """ Classes can subclass this to get some convenience handlers around `pywayland.server.Listener`. This guarantees that all listeners that set up and then removed in reverse order. """ def add_listener(self, event: Signal, callback: Callable) -> None: if not hasattr(self, "_listeners"): self._listeners = [] listener = Listener(callback) event.add(listener) self._listeners.append(listener) def finalize_listeners(self) -> None: for listener in reversed(self._listeners): listener.remove() self._listeners.clear() def finalize_listener(self, event: Signal) -> None: for listener in self._listeners.copy(): if listener._signal._ptr == event._ptr: listener.remove() return logger.warning("Failed to remove listener for event: %s", event) class Dnd(HasListeners): """A helper for drag and drop functionality.""" def __init__(self, core: Core, wlr_drag: data_device_manager.Drag): self.core = core self.icon = cast(data_device_manager.DragIcon, wlr_drag.icon) self.add_listener(self.icon.destroy_event, self._on_destroy) self.node = SceneTree.drag_icon_create(core.drag_icon_tree, self.icon).node # The data handle at .data is used for finding what's under the cursor when it's # moved. self.data_handle = ffi.new_handle(self) self.node.data = self.data_handle def finalize(self) -> None: self.finalize_listeners() self.core.live_dnd = None self.node.destroy() self.node.data = None self.data_handle = None def _on_destroy(self, _listener: Listener, _event: Any) -> None: logger.debug("Signal: wlr_drag destroy") self.finalize() def position(self, cx: float, cy: float) -> None: self.node.set_position(int(cx), int(cy)) def get_xwayland_atoms(xwayland: xwayland.XWayland) -> dict[int, str]: """ These can be used when matching on XWayland clients with wm_type. http://standards.freedesktop.org/wm-spec/latest/ar01s05.html#idm139870830002400 """ xwayland_wm_types = { "_NET_WM_WINDOW_TYPE_DESKTOP": "desktop", "_NET_WM_WINDOW_TYPE_DOCK": "dock", "_NET_WM_WINDOW_TYPE_TOOLBAR": "toolbar", "_NET_WM_WINDOW_TYPE_MENU": "menu", "_NET_WM_WINDOW_TYPE_UTILITY": "utility", "_NET_WM_WINDOW_TYPE_SPLASH": "splash", "_NET_WM_WINDOW_TYPE_DIALOG": "dialog", "_NET_WM_WINDOW_TYPE_DROPDOWN_MENU": "dropdown", "_NET_WM_WINDOW_TYPE_POPUP_MENU": "menu", "_NET_WM_WINDOW_TYPE_TOOLTIP": "tooltip", "_NET_WM_WINDOW_TYPE_NOTIFICATION": "notification", "_NET_WM_WINDOW_TYPE_COMBO": "combo", "_NET_WM_WINDOW_TYPE_DND": "dnd", "_NET_WM_WINDOW_TYPE_NORMAL": "normal", } atoms = {} for atom, name in xwayland_wm_types.items(): atoms[xwayland.get_atom(atom)] = name return atoms @dataclass() class CursorState: """ The surface and hotspot state of the cursor. This is tracked directly by the core so that the cursor can be hidden and later restored to this state at will. """ surface: Surface | None = None hotspot: tuple[int, int] = (0, 0) hidden: bool = False qtile-0.31.0/libqtile/backend/wayland/window.py0000664000175000017500000011640614762660347021362 0ustar epsilonepsilon# Copyright (c) 2021 Matt Colligan # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations import abc import functools import typing import cairocffi import wlroots.wlr_types.foreign_toplevel_management_v1 as ftm from pywayland.server import Client, Listener from wlroots import PtrHasData from wlroots.util.box import Box from wlroots.wlr_types import Buffer from wlroots.wlr_types.idle_inhibit_v1 import IdleInhibitorV1 from wlroots.wlr_types.pointer_constraints_v1 import ( PointerConstraintV1, PointerConstraintV1StateField, ) from wlroots.wlr_types.scene import SceneBuffer, SceneRect, SceneTree from libqtile import config, hook, utils from libqtile.backend import base from libqtile.backend.base import FloatStates from libqtile.backend.wayland.drawer import Drawer from libqtile.backend.wayland.wlrq import HasListeners from libqtile.command.base import CommandError, expose_command from libqtile.log_utils import logger try: # Continue if ffi not built, so that docs can be built without wayland deps. from libqtile.backend.wayland._ffi import ffi, lib except ModuleNotFoundError: pass if typing.TYPE_CHECKING: from typing import Any from wlroots.wlr_types import Surface from libqtile.backend.wayland.core import Core from libqtile.command.base import CommandObject, ItemT from libqtile.core.manager import Qtile from libqtile.group import _Group from libqtile.utils import ColorsType, ColorType S = typing.TypeVar("S", bound=PtrHasData) @functools.lru_cache def _rgb(color: ColorType) -> ffi.CData: """Helper to create and cache float[4] arrays for border painting""" if isinstance(color, ffi.CData): return color return ffi.new("float[4]", utils.rgb(color)) class _Base: _wid: int @property def wid(self) -> int: return self._wid @property def width(self) -> int: return self._width @width.setter def width(self, width: int) -> None: self._width = width @property def height(self) -> int: return self._height @height.setter def height(self, height: int) -> None: self._height = height class Window(typing.Generic[S], _Base, base.Window, HasListeners): """ This is a generic window class for "regular" windows. The type variable `S` denotes which type of surface the window manages, and by extension which shell the window belongs to. While this does implement some of `base.Window`'s abstract methods, the concrete classes are responsible for implementing a few others. """ def __init__(self, core: Core, qtile: Qtile, surface: S): base.Window.__init__(self) self.core = core self.qtile = qtile self.surface = surface self._group: _Group | None = None self.x = 0 self.y = 0 self._opacity: float = 1.0 self._wm_class: str | None = None self._idle_inhibitors_count: int = 0 self._urgent = False # Create a scene-graph tree for this window and its borders self.data_handle: ffi.CData = ffi.new_handle(self) self.container = SceneTree.create(core.mid_window_tree) self.container.node.set_enabled(enabled=False) self.container.node.data = self.data_handle # The borders are wlr_scene_rects. # Inner list: N, E, S, W edges # Outer list: outside-in borders i.e. multiple for multiple borders self._borders: list[list[SceneRect]] = [] self.bordercolor: ColorsType = "000000" # This is a placeholder to be set properly when the window maps for the first # time (and therefore exposed to the user). We need the attribute to exist so # that __repr__ doesn't AttributeError. self._wid: int = -1 self._width: int = 0 self._height: int = 0 self.float_x: int | None = None self.float_y: int | None = None self._float_width: int = 0 self._float_height: int = 0 self._float_state = FloatStates.NOT_FLOATING # Each regular window gets a foreign toplevel handle: all instances of Window # (i.e. toplevel XDG windows and regular X11 windows) have one. If a user uses # the static() command to convert one of these into a Static, that Static will # keep the same handle. However, Static windows can also be layer shell windows # or non-regular X11 clients (e.g. X11 bars or popups), and so might not have a # handle. Because we pass ownership of the handle to a Static during static(), # and the old Window would destroy the handle during finalize(), we make this # attribute optional to avoid the destroy(). self.ftm_handle: ftm.ForeignToplevelHandleV1 | None = None def finalize(self) -> None: self.finalize_listeners() self.surface.data = None # Remove the scene graph container. Any borders will die with it. self.container.node.data = None self.container.node.destroy() del self.data_handle if self.ftm_handle: self.ftm_handle.destroy() self.ftm_handle = None self.core.remove_pointer_constraints(self) def _on_map(self, _listener: Listener, _data: Any) -> None: logger.debug("Signal: window map") self.unhide() def _on_unmap(self, _listener: Listener, _data: Any) -> None: logger.debug("Signal: window unmap") self.hide() @property def wid(self) -> int: return self._wid @property def group(self) -> _Group | None: return self._group @group.setter def group(self, group: _Group | None) -> None: self._group = group @property def urgent(self) -> bool: return self._urgent def _on_destroy(self, _listener: Listener, _data: Any) -> None: logger.debug("Signal: window destroy") self.hide() if self in self.core.pending_windows: self.core.pending_windows.remove(self) else: self.qtile.unmanage(self.wid) self.finalize() def _on_foreign_request_maximize( self, _listener: Listener, event: ftm.ForeignToplevelHandleV1MaximizedEvent ) -> None: logger.debug("Signal: foreign_toplevel_management request_maximize") self.maximized = event.maximized def _on_foreign_request_minimize( self, _listener: Listener, event: ftm.ForeignToplevelHandleV1MinimizedEvent ) -> None: logger.debug("Signal: foreign_toplevel_management request_minimize") self.minimized = event.minimized def _on_foreign_request_fullscreen( self, _listener: Listener, event: ftm.ForeignToplevelHandleV1FullscreenEvent ) -> None: logger.debug("Signal: foreign_toplevel_management request_fullscreen") self.fullscreen = event.fullscreen def _on_foreign_request_activate( self, _listener: Listener, event: ftm.ForeignToplevelHandleV1ActivatedEvent ) -> None: logger.debug("Signal: foreign_toplevel_management request_activate") if self.group: self.qtile.current_screen.set_group(self.group) self.group.focus(self) def _on_foreign_request_close(self, _listener: Listener, _data: Any) -> None: logger.debug("Signal: foreign_toplevel_management request_close") self.kill() def _on_inhibitor_destroy(self, listener: Listener, surface: Surface) -> None: # We don't have reference to the inhibitor, but it doesn't really # matter we only need to keep count of how many inhibitors there are self._idle_inhibitors_count -= 1 listener.remove() if self._idle_inhibitors_count == 0: self.core.check_idle_inhibitor() # TODO: do we also need to check idle inhibitors when unmapping? def hide(self) -> None: self.container.node.set_enabled(enabled=False) seat = self.core.seat if not seat.destroyed: if self.surface.surface == seat.keyboard_state.focused_surface: seat.keyboard_clear_focus() def get_wm_class(self) -> list | None: if self._wm_class: return [self._wm_class] return None def belongs_to_client(self, other: Client) -> bool: return other == Client.from_resource(self.surface.surface._ptr.resource) def focus(self, warp: bool = True) -> None: self._urgent = False self.core.focus_window(self) if warp and self.qtile.config.cursor_warp: self.core.warp_pointer( self.x + self._width / 2, self.y + self._height / 2, ) if self.group and self.group.current_window is not self: self.group.focus(self) hook.fire("client_focus", self) def togroup( self, group_name: str | None = None, *, switch_group: bool = False, toggle: bool = False ) -> None: """ Move window to a specified group Also switch to that group if switch_group is True. If `toggle` is True and and the specified group is already on the screen, use the last used group as target instead. """ if group_name is None: group = self.qtile.current_group else: if group_name not in self.qtile.groups_map: raise CommandError(f"No such group: {group_name}") group = self.qtile.groups_map[group_name] if self.group is group: if toggle and self.group.screen.previous_group: group = self.group.screen.previous_group else: return self.hide() if self.group: if self.group.screen: # for floats remove window offset self.x -= self.group.screen.x group_ref = self.group self.group.remove(self) # delete groups with `persist=False` if ( not self.qtile.dgroups.groups_map[group_ref.name].persist and len(group_ref.windows) <= 1 ): # set back original group so _del() can grab it self.group = group_ref self.qtile.dgroups._del(self) self.group = None if group.screen and self.x < group.screen.x: self.x += group.screen.x group.add(self) if switch_group: group.toscreen(toggle=toggle) def paint_borders(self, colors: ColorsType | None, width: int) -> None: if not colors: colors = [] width = 0 if not isinstance(colors, list): colors = [colors] if self.tree: self.tree.node.set_position(width, width) self.bordercolor = colors self.borderwidth = width if width == 0: for rects in self._borders: for rect in rects: rect.node.destroy() self._borders.clear() return if len(colors) > width: colors = colors[:width] num = len(colors) old_borders = self._borders new_borders = [] widths = [width // num] * num for i in range(width % num): widths[i] += 1 outer_w = self.width + width * 2 outer_h = self.height + width * 2 coord = 0 for i, color in enumerate(colors): color_ = _rgb(color) bw = widths[i] # [x, y, width, height] for N, E, S, W geometries = ( (coord, coord, outer_w - coord * 2, bw), (outer_w - bw - coord, bw + coord, bw, outer_h - bw * 2 - coord * 2), (coord, outer_h - bw - coord, outer_w - coord * 2, bw), (coord, bw + coord, bw, outer_h - bw * 2 - coord * 2), ) if old_borders: rects = old_borders.pop(0) for (x, y, w, h), rect in zip(geometries, rects): rect.set_color(color_) rect.set_size(w, h) rect.node.set_position(x, y) else: rects = [] for x, y, w, h in geometries: rect = SceneRect(self.container, w, h, color_) rect.node.set_position(x, y) rects.append(rect) new_borders.append(rects) coord += bw for rects in old_borders: for rect in rects: rect.node.destroy() # Ensure the window contents and any nested surfaces are drawn above the # borders. if self.tree: self.tree.node.raise_to_top() self._borders = new_borders @property def opacity(self) -> float: return self._opacity @opacity.setter def opacity(self, opacity: float) -> None: self._opacity = opacity self.core.configure_node_opacity(self.container.node) @property def floating(self) -> bool: return self._float_state != FloatStates.NOT_FLOATING @floating.setter def floating(self, do_float: bool) -> None: if do_float and self._float_state == FloatStates.NOT_FLOATING: if self.is_placed(): screen = self.group.screen # type: ignore[union-attr] # see is_placed() if not self._float_width: # These might start as 0 self._float_width = self._width self._float_height = self._height self._reconfigure_floating( screen.x + self.float_x, screen.y + self.float_y, self._float_width, self._float_height, ) else: # if we are setting floating early, e.g. from a hook, we don't have a screen yet self._float_state = FloatStates.FLOATING elif (not do_float) and self._float_state != FloatStates.NOT_FLOATING: self._update_fullscreen(False) if self._float_state == FloatStates.FLOATING: # store last size self._float_width = self._width self._float_height = self._height self._float_state = FloatStates.NOT_FLOATING if self.group: self.group.mark_floating(self, False) hook.fire("float_change") @property def fullscreen(self) -> bool: return self._float_state == FloatStates.FULLSCREEN @fullscreen.setter def fullscreen(self, do_full: bool) -> None: if do_full and self._float_state != FloatStates.FULLSCREEN: screen = (self.group and self.group.screen) or self.qtile.find_closest_screen( self.x, self.y ) if self._float_state not in (FloatStates.MAXIMIZED, FloatStates.FULLSCREEN): self._save_geometry() bw = self.group.floating_layout.fullscreen_border_width if self.group else 0 self._reconfigure_floating( screen.x, screen.y, screen.width - 2 * bw, screen.height - 2 * bw, new_float_state=FloatStates.FULLSCREEN, ) elif self._float_state == FloatStates.FULLSCREEN: self._restore_geometry() self.floating = False @abc.abstractmethod def _update_fullscreen(self, do_full: bool) -> None: pass @property def maximized(self) -> bool: return self._float_state == FloatStates.MAXIMIZED @maximized.setter def maximized(self, do_maximize: bool) -> None: if do_maximize: screen = (self.group and self.group.screen) or self.qtile.find_closest_screen( self.x, self.y ) if self._float_state not in (FloatStates.MAXIMIZED, FloatStates.FULLSCREEN): self._save_geometry() bw = self.group.floating_layout.max_border_width if self.group else 0 self._reconfigure_floating( screen.dx, screen.dy, screen.dwidth - 2 * bw, screen.dheight - 2 * bw, new_float_state=FloatStates.MAXIMIZED, ) else: if self._float_state == FloatStates.MAXIMIZED: self._restore_geometry() self.floating = False if self.ftm_handle: self.ftm_handle.set_maximized(do_maximize) @property def minimized(self) -> bool: return self._float_state == FloatStates.MINIMIZED @minimized.setter def minimized(self, do_minimize: bool) -> None: if do_minimize: if self._float_state != FloatStates.MINIMIZED: self._reconfigure_floating(new_float_state=FloatStates.MINIMIZED) else: if self._float_state == FloatStates.MINIMIZED: self.floating = False if self.ftm_handle: self.ftm_handle.set_minimized(do_minimize) def _tweak_float( self, x: int | None = None, y: int | None = None, dx: int = 0, dy: int = 0, w: int | None = None, h: int | None = None, dw: int = 0, dh: int = 0, ) -> None: if x is None: x = self.x x += dx if y is None: y = self.y y += dy if w is None: w = self._width w += dw if h is None: h = self._height h += dh if h < 0: h = 0 if w < 0: w = 0 screen = self.qtile.find_closest_screen(x + w // 2, y + h // 2) if self.group and screen is not None and screen != self.group.screen: self.group.remove(self, force=True) screen.group.add(self, force=True) self.qtile.focus_screen(screen.index) self._reconfigure_floating(x, y, w, h) def _reconfigure_floating( self, x: int | None = None, y: int | None = None, w: int | None = None, h: int | None = None, new_float_state: FloatStates = FloatStates.FLOATING, ) -> None: self._update_fullscreen(new_float_state == FloatStates.FULLSCREEN) if new_float_state == FloatStates.MINIMIZED: self.hide() else: self.place( x, y, w, h, self.borderwidth, self.bordercolor, above=True, respect_hints=True ) if self._float_state != new_float_state: self._float_state = new_float_state if self.group: # may be not, if it's called from hook self.group.mark_floating(self, True) hook.fire("float_change") @expose_command() def info(self) -> dict: """Return a dictionary of info.""" float_info = { "x": self.float_x, "y": self.float_y, "width": self._float_width, "height": self._float_height, } return dict( name=self.name, x=self.x, y=self.y, width=self._width, height=self._height, group=self.group.name if self.group else None, id=self.wid, wm_class=self.get_wm_class(), shell="XDG" if self.__class__.__name__ == "XdgWindow" else "XWayland", float_info=float_info, floating=self._float_state != FloatStates.NOT_FLOATING, maximized=self._float_state == FloatStates.MAXIMIZED, minimized=self._float_state == FloatStates.MINIMIZED, fullscreen=self._float_state == FloatStates.FULLSCREEN, ) def match(self, match: config._Match) -> bool: return match.compare(self) def add_idle_inhibitor( self, surface: Surface, _x: int, _y: int, inhibitor: IdleInhibitorV1, ) -> None: if surface == inhibitor.surface: self._idle_inhibitors_count += 1 inhibitor.data = self self.add_listener(inhibitor.destroy_event, self._on_inhibitor_destroy) if self._idle_inhibitors_count == 1: self.core.check_idle_inhibitor() @property def is_idle_inhibited(self) -> bool: return self._idle_inhibitors_count > 0 def _items(self, name: str) -> ItemT: if name == "group": return True, [] if name == "layout": if self.group: return True, list(range(len(self.group.layouts))) return None if name == "screen": if self.group and self.group.screen: return True, [] return None def _select(self, name: str, sel: str | int | None) -> CommandObject | None: if name == "group": return self.group elif name == "layout": if sel is None: return self.group.layout if self.group else None else: return utils.lget(self.group.layouts, int(sel)) if self.group else None elif name == "screen": return self.group.screen if self.group else None return None @expose_command() def move_floating(self, dx: int, dy: int) -> None: self._tweak_float(dx=dx, dy=dy) @expose_command() def resize_floating(self, dw: int, dh: int) -> None: self._tweak_float(dw=dw, dh=dh) @expose_command() def set_position_floating(self, x: int, y: int) -> None: self._tweak_float(x=x, y=y) @expose_command() def set_position(self, x: int, y: int) -> None: if self.floating: self._tweak_float(x=x, y=y) return if self.group: cx = self.core.cursor.x cy = self.core.cursor.y for window in self.group.windows: if ( window is not self and not window.floating and window.x <= cx <= (window.x + window.width) and window.y <= cy <= (window.y + window.height) ): self.group.layout.swap(self, window) return @expose_command() def set_size_floating(self, w: int, h: int) -> None: self._tweak_float(w=w, h=h) @expose_command() def get_position(self) -> tuple[int, int]: return self.x, self.y @expose_command() def get_size(self) -> tuple[int, int]: return self._width, self._height @expose_command() def toggle_floating(self) -> None: self.floating = not self.floating @expose_command() def enable_floating(self) -> None: self.floating = True @expose_command() def disable_floating(self) -> None: self.floating = False @expose_command() def toggle_maximize(self) -> None: self.maximized = not self.maximized @expose_command() def toggle_minimize(self) -> None: self.minimized = not self.minimized @expose_command() def toggle_fullscreen(self) -> None: self.fullscreen = not self.fullscreen @expose_command() def enable_fullscreen(self) -> None: self.fullscreen = True @expose_command() def disable_fullscreen(self) -> None: self.fullscreen = False @expose_command() def bring_to_front(self) -> None: self.container.node.raise_to_top() @expose_command() def static( self, screen: int | None = None, x: int | None = None, y: int | None = None, width: int | None = None, height: int | None = None, ) -> None: # The concrete Window class must fire the client_managed hook after it's # completed any custom logic. self.defunct = True if self.group: self.group.remove(self) # Keep track of user-specified geometry to support X11. # Respect configure requests only if these are `None` here. conf_x = x conf_y = y conf_width = width conf_height = height if x is None: x = self.x + self.borderwidth if y is None: y = self.y + self.borderwidth if width is None: width = self._width if height is None: height = self._height self.finalize_listeners() # Destroy the borders. Currently static windows are always borderless. while self._borders: for rect in self._borders.pop(): rect.node.destroy() if self.tree: self.tree.node.set_position(0, 0) win = self._to_static(conf_x, conf_y, conf_width, conf_height) # Pass ownership of the foreign toplevel handle to the static window. if self.ftm_handle: win.ftm_handle = self.ftm_handle self.ftm_handle = None win.add_listener(win.ftm_handle.request_close_event, win._on_foreign_request_close) if screen is not None: win.screen = self.qtile.screens[screen] win.unhide() win.place(x, y, width, height, 0, None) self.qtile.windows_map[self.wid] = win @expose_command() def is_visible(self) -> bool: return self.container.node.enabled @abc.abstractmethod def _to_static( self, x: int | None, y: int | None, width: int | None, height: int | None ) -> Static: # This must return a new `Static` subclass instance pass class Static(typing.Generic[S], _Base, base.Static, HasListeners): def __init__( self, core: Core, qtile: Qtile, surface: S, wid: int, idle_inhibitor_count: int = 0, ): base.Static.__init__(self) self.core = core self.qtile = qtile self.surface = surface self.screen = qtile.current_screen self._wid = wid self.x = 0 self.y = 0 self._width = 0 self._height = 0 self.borderwidth: int = 0 self.bordercolor: list[ffi.CData] = [_rgb((0, 0, 0, 1))] self.opacity: float = 1.0 self._wm_class: str | None = None self._idle_inhibitors_count = idle_inhibitor_count self.ftm_handle: ftm.ForeignToplevelHandleV1 | None = None self.data_handle = ffi.new_handle(self) surface.data = self.data_handle self._urgent = False def finalize(self) -> None: self.finalize_listeners() self.surface.data = None self.core.remove_pointer_constraints(self) del self.data_handle @property def wid(self) -> int: return self._wid @property def urgent(self) -> bool: return self._urgent def focus(self, warp: bool = True) -> None: self._urgent = False self.core.focus_window(self) if warp and self.qtile.config.cursor_warp: self.core.warp_pointer( self.x + self._width / 2, self.y + self._height / 2, ) hook.fire("client_focus", self) def _on_map(self, _listener: Listener, _data: Any) -> None: logger.debug("Signal: static map") self.unhide() self.focus(True) self.bring_to_front() def _on_unmap(self, _listener: Listener, _data: Any) -> None: logger.debug("Signal: static unmap") self.hide() def hide(self) -> None: if self.surface.surface == self.core.seat.keyboard_state.focused_surface: group = self.qtile.current_screen.group if group.current_window: group.focus(group.current_window, warp=self.qtile.config.cursor_warp) else: self.core.seat.keyboard_clear_focus() def _on_destroy(self, _listener: Listener, _data: Any) -> None: logger.debug("Signal: static destroy") self.hide() if self in self.core.pending_windows: self.core.pending_windows.remove(self) else: self.qtile.unmanage(self.wid) self.finalize() def _on_foreign_request_close(self, _listener: Listener, _data: Any) -> None: logger.debug("Signal: foreign_toplevel_management static request_close") self.kill() def _on_inhibitor_destroy(self, listener: Listener, surface: Surface) -> None: # We don't have reference to the inhibitor, but it doesn't really # matter we only need to keep count of how many inhibitors there are self._idle_inhibitors_count -= 1 listener.remove() if self._idle_inhibitors_count == 0: self.core.check_idle_inhibitor() def add_idle_inhibitor( self, surface: Surface, _x: int, _y: int, inhibitor: IdleInhibitorV1, ) -> None: if surface == inhibitor.surface: self._idle_inhibitors_count += 1 inhibitor.data = self self.add_listener(inhibitor.destroy_event, self._on_inhibitor_destroy) if self._idle_inhibitors_count == 1: self.core.check_idle_inhibitor() @property def is_idle_inhibited(self) -> bool: return self._idle_inhibitors_count > 0 def belongs_to_client(self, other: Client) -> bool: return other == Client.from_resource(self.surface.surface._ptr.resource) @expose_command() def bring_to_front(self) -> None: self.container.node.raise_to_top() @expose_command() def info(self) -> dict: """Return a dictionary of info.""" info = base.Static.info(self) cls_name = self.__class__.__name__ if cls_name == "XdgStatic": info["shell"] = "XDG" elif cls_name == "XStatic": info["shell"] = "XWayland" else: info["shell"] = "layer" return info class Internal(_Base, base.Internal): """ Internal windows are simply textures controlled by the compositor. """ def __init__(self, core: Core, qtile: Qtile, x: int, y: int, width: int, height: int): self.core = core self.qtile = qtile self._wid: int = self.core.new_wid() self.x: int = x self.y: int = y self._width: int = width self._height: int = height self._opacity: float = 1.0 # Store this object on the scene node for finding the window under the pointer. self.wlr_buffer, self.surface = self._new_buffer(init=True) self.tree = SceneTree.create(core.mid_window_tree) self.data_handle = ffi.new_handle(self) self.tree.node.set_enabled(enabled=False) self.tree.node.data = self.data_handle self.tree.node.set_position(x, y) scene_buffer = SceneBuffer.create(self.tree, self.wlr_buffer) if scene_buffer is None: raise RuntimeError("Couldn't create scene buffer") self._scene_buffer = scene_buffer # The borders are wlr_scene_rects. # Inner list: N, E, S, W edges # Outer list: outside-in borders i.e. multiple for multiple borders self._borders: list[list[SceneRect]] = [] self.bordercolor: ColorsType = "000000" def finalize(self) -> None: self.hide() def _new_buffer(self, init: bool = False) -> tuple[Buffer, cairocffi.ImageSurface]: if not init: self.wlr_buffer.drop() surface = cairocffi.ImageSurface(cairocffi.FORMAT_ARGB32, self._width, self._height) stride = surface.get_stride() data = cairocffi.cairo.cairo_image_surface_get_data(surface._pointer) wlr_buffer = lib.cairo_buffer_create(self._width, self._height, stride, data) if wlr_buffer == ffi.NULL: raise RuntimeError("Couldn't allocate cairo buffer.") buffer = Buffer(wlr_buffer) if not init: self._scene_buffer.set_buffer_with_damage(buffer) return buffer, surface def create_drawer(self, width: int, height: int) -> Drawer: """Create a Drawer that draws to this window.""" return Drawer(self.qtile, self, width, height) def hide(self) -> None: self.tree.node.set_enabled(enabled=False) def unhide(self) -> None: self.tree.node.set_enabled(enabled=True) @expose_command() def focus(self, warp: bool = True) -> None: self.core.focus_window(self) @expose_command() def kill(self) -> None: self.hide() if self.wid in self.qtile.windows_map: # It will be present during config reloads; absent during shutdown as this # will follow graceful_shutdown del self.qtile.windows_map[self.wid] @expose_command() def place( self, x: int, y: int, width: int, height: int, borderwidth: int, bordercolor: ColorsType | None, above: bool = False, margin: int | list[int] | None = None, respect_hints: bool = False, ) -> None: if above: self.bring_to_front() self.x = x self.y = y self.tree.node.set_position(x, y) if width != self._width or height != self._height: # Changed size, we need to regenerate the buffer self._width = width self._height = height self.wlr_buffer, self.surface = self._new_buffer() def paint_borders(self, colors: ColorsType | None, width: int) -> None: if not colors: colors = [] width = 0 if not isinstance(colors, list): colors = [colors] if self._scene_buffer: self._scene_buffer.node.set_position(width, width) self.bordercolor = colors self.borderwidth = width if width == 0: for rects in self._borders: for rect in rects: rect.node.destroy() self._borders.clear() return if len(colors) > width: colors = colors[:width] num = len(colors) old_borders = self._borders new_borders = [] widths = [width // num] * num for i in range(width % num): widths[i] += 1 outer_w = self.width + width * 2 outer_h = self.height + width * 2 coord = 0 for i, color in enumerate(colors): color_ = _rgb(color) bw = widths[i] # [x, y, width, height] for N, E, S, W geometries = ( (coord, coord, outer_w - coord * 2, bw), (outer_w - bw - coord, bw + coord, bw, outer_h - bw * 2 - coord * 2), (coord, outer_h - bw - coord, outer_w - coord * 2, bw), (coord, bw + coord, bw, outer_h - bw * 2 - coord * 2), ) if old_borders: rects = old_borders.pop(0) for (x, y, w, h), rect in zip(geometries, rects): rect.set_color(color_) rect.set_size(w, h) rect.node.set_position(x, y) else: rects = [] for x, y, w, h in geometries: rect = SceneRect(self.tree, w, h, color_) rect.node.set_position(x, y) rects.append(rect) new_borders.append(rects) coord += bw for rects in old_borders: for rect in rects: rect.node.destroy() # Ensure the window contents and any nested surfaces are drawn above the # borders. if self._scene_buffer: self._scene_buffer.node.raise_to_top() self._borders = new_borders @expose_command() def info(self) -> dict: """Return a dictionary of info.""" return dict( x=self.x, y=self.y, width=self._width, height=self._height, id=self.wid, ) @expose_command() def bring_to_front(self) -> None: self.tree.node.raise_to_top() WindowType = Window | Static | Internal class PointerConstraint(HasListeners): """ A small object to listen to signals on `struct wlr_pointer_constraint_v1` instances. """ rect: Box def __init__(self, core: Core, wlr_constraint: PointerConstraintV1): self.core = core self.wlr_constraint = wlr_constraint self._warp_target: tuple[float, float] = (0, 0) self._needs_warp: bool = False assert core.qtile is not None owner = None for win in core.qtile.windows_map.values(): if isinstance(win, (Window | Static)): if win.surface.surface == self.wlr_constraint.surface: owner = win break if owner is None: logger.error("No window found for pointer constraints. Please report.") raise RuntimeError self.window: Window | Static = owner self.add_listener(wlr_constraint.set_region_event, self._on_set_region) self.add_listener(wlr_constraint.destroy_event, self._on_destroy) def finalize(self) -> None: if self.core.active_pointer_constraint is self: self.disable() self.finalize_listeners() self.core.pointer_constraints.remove(self) def _on_set_region(self, _listener: Listener, _data: Any) -> None: logger.debug("Signal: wlr_pointer_constraint_v1 set_region") self._get_region() def _on_destroy(self, _listener: Listener, wlr_constraint: PointerConstraintV1) -> None: logger.debug("Signal: wlr_pointer_constraint_v1 destroy") self.finalize() def _on_commit(self, _listener: Listener, _data: Any) -> None: if self._needs_warp: # Warp in case the pointer is not inside the rect if not self.rect.contains_point(self.core.cursor.x, self.core.cursor.y): self.core.warp_pointer(*self._warp_target) self._needs_warp = False def _get_region(self) -> None: rect = self.wlr_constraint.region.rectangles_as_boxes()[0] rect.x += self.window.x + self.window.borderwidth rect.y += self.window.y + self.window.borderwidth self._warp_target = (rect.x + rect.width / 2, rect.y + rect.height / 2) self.rect = rect self._needs_warp = True def enable(self) -> None: logger.debug("Enabling pointer constraints.") self.core.active_pointer_constraint = self self._get_region() self.add_listener(self.wlr_constraint.surface.commit_event, self._on_commit) self.wlr_constraint.send_activated() def disable(self) -> None: logger.debug("Disabling pointer constraints.") if self.wlr_constraint.current.committed & PointerConstraintV1StateField.CURSOR_HINT: x, y = self.wlr_constraint.current.cursor_hint self.core.warp_pointer(x + self.window.x, y + self.window.y) self.core.active_pointer_constraint = None self.wlr_constraint.send_deactivated() qtile-0.31.0/libqtile/backend/wayland/layer.py0000664000175000017500000001653314762660347021167 0ustar epsilonepsilon# Copyright (c) 2021 Matt Colligan # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations from typing import TYPE_CHECKING, cast from pywayland.server import Listener from wlroots.wlr_types import Output as WlrOutput from wlroots.wlr_types import SceneTree from wlroots.wlr_types.layer_shell_v1 import LayerShellV1Layer, LayerSurfaceV1 from libqtile import hook from libqtile.backend.wayland.output import Output from libqtile.backend.wayland.window import Static from libqtile.command.base import expose_command from libqtile.log_utils import logger try: # Continue if ffi not built, so that docs can be built without wayland deps. from libqtile.backend.wayland._ffi import ffi except ModuleNotFoundError: pass if TYPE_CHECKING: from typing import Any from wlroots.wlr_types.scene import SceneLayerSurfaceV1 from libqtile.backend.wayland.core import Core from libqtile.core.manager import Qtile from libqtile.utils import ColorsType class LayerStatic(Static[LayerSurfaceV1]): """A static window belonging to the layer shell.""" def __init__( self, core: Core, qtile: Qtile, surface: LayerSurfaceV1, wid: int, ): Static.__init__(self, core, qtile, surface, wid) self.desired_width = 0 self.desired_height = 0 self.data_handle = ffi.new_handle(self) surface.data = self.data_handle # Determine which output this window is to appear on if wlr_output := surface.output: logger.debug("Layer surface requested output: %s", wlr_output.name) else: wlr_output = cast( WlrOutput, core.output_layout.output_at(core.cursor.x, core.cursor.y) ) logger.debug("Layer surface given output: %s", wlr_output.name) surface.output = wlr_output output = cast(Output, wlr_output.data) self.output = output self.screen = output.screen # Add the window to the scene graph parent_tree = core.layer_trees[surface.pending.layer] self.scene_layer: SceneLayerSurfaceV1 = core.scene.layer_surface_v1_create( parent_tree, surface ) self.tree: SceneTree = self.scene_layer.tree self.tree.node.data = self.data_handle self.popup_tree = SceneTree.create(parent_tree) # Popups get their own tree self.popup_tree.node.data = self.data_handle # Set up listeners self.add_listener(surface.surface.map_event, self._on_map) self.add_listener(surface.surface.unmap_event, self._on_unmap) self.add_listener(surface.destroy_event, self._on_destroy) self.add_listener(surface.surface.commit_event, self._on_commit) # Temporarily set the layer's current state to pending so that we can easily # arrange it. TODO: how much of this is needed? self._layer = surface.pending.layer old_state = surface.current surface.current = surface.pending self.unhide() self.output.organise_layers() surface.current = old_state self._move_to_layer(old_state.layer) def _on_commit(self, _listener: Listener, _data: Any) -> None: if self.surface.output and self.surface.output.data: output = self.surface.output.data if output != self.output: # The window wants to move to a different output. if self.tree.node.enabled: self.output.layers[self._layer].remove(self) output.layers[self._layer].append(self) self.output = output pending = self.surface.pending if ( self._layer != pending.layer or self._width != pending.desired_width or self._height != pending.desired_height ): # The window has changed its desired layer or dimensions. self._move_to_layer(pending.layer) def _move_to_layer(self, layer: LayerShellV1Layer) -> None: new_parent = self.core.layer_trees[layer] self.tree.node.reparent(new_parent) self.popup_tree.node.reparent(new_parent) if self.tree.node.enabled: # If we're mapped, we also need to update the lists on the output. self.output.layers[self._layer].remove(self) self.output.layers[layer].append(self) self.output.organise_layers() self._layer = layer def finalize(self) -> None: super().finalize() self.popup_tree.node.destroy() def kill(self) -> None: self.surface.destroy() def hide(self) -> None: if self.core.exclusive_layer is self: self.core.exclusive_layer = None if self.reserved_space: self.qtile.free_reserved_space(self.reserved_space, self.screen) self.reserved_space = None if self.surface.surface == self.core.seat.keyboard_state.focused_surface: group = self.qtile.current_screen.group if group.current_window: group.focus(group.current_window, warp=self.qtile.config.cursor_warp) else: self.core.seat.keyboard_clear_focus() if self in self.output.layers[self._layer]: self.tree.node.set_enabled(enabled=False) # TODO also toggle popup_tree self.output.layers[self._layer].remove(self) self.output.organise_layers() def unhide(self) -> None: if self not in self.output.layers[self._layer]: self.tree.node.set_enabled(enabled=True) self.output.layers[self._layer].append(self) self.output.organise_layers() def focus(self, _warp: bool = True) -> None: self.core.focus_window(self) hook.fire("client_focus", self) def place( self, x: int, y: int, width: int, height: int, borderwidth: int, bordercolor: ColorsType | None, above: bool = False, margin: int | list[int] | None = None, respect_hints: bool = False, ) -> None: self.x = x self.y = y self.tree.node.set_position(x, y) self.popup_tree.node.set_position(x, y) # The actual resizing is done by `Output`. self._width = width self._height = height @expose_command() def bring_to_front(self) -> None: self.tree.node.raise_to_top() qtile-0.31.0/libqtile/backend/wayland/output.py0000664000175000017500000002040614762660347021405 0ustar epsilonepsilon# Copyright (c) 2021 Matt Colligan # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations import os from typing import TYPE_CHECKING from wlroots.util.box import Box from wlroots.util.clock import Timespec from wlroots.wlr_types import Output as wlrOutput from wlroots.wlr_types import OutputState, SceneOutput from wlroots.wlr_types.layer_shell_v1 import ( LayerShellV1Layer, LayerSurfaceV1KeyboardInteractivity, ) from wlroots.wlr_types.output import CustomMode from libqtile.backend.wayland.wlrq import HasListeners from libqtile.config import ScreenRect from libqtile.log_utils import logger if TYPE_CHECKING: from typing import Any from pywayland.server import Listener from wlroots.wlr_types.output import OutputEventRequestState from libqtile.backend.wayland.core import Core from libqtile.backend.wayland.layer import LayerStatic from libqtile.backend.wayland.window import WindowType from libqtile.config import Screen class Output(HasListeners): def __init__(self, core: Core, wlr_output: wlrOutput): self.core = core self.renderer = core.renderer self.wlr_output = wlr_output self._reserved_space = (0, 0, 0, 0) # These will get updated on the output layout's change event self.x = 0 self.y = 0 self.scene_output = SceneOutput.create(core.scene, wlr_output) wlr_output.init_render(core.allocator, core.renderer) # The output may be disabled, switch it on. state = OutputState() state.set_enabled(True) # Select the output's preferred mode. if mode := wlr_output.preferred_mode(): state.set_mode(mode) # During tests, we want to fix the geometry of the 1 or 2 outputs. if wlr_output.is_headless and "PYTEST_CURRENT_TEST" in os.environ: if not core.get_enabled_outputs(): # First test output state.set_custom_mode(CustomMode(width=800, height=600, refresh=0)) else: # Second test output state.set_custom_mode(CustomMode(width=640, height=480, refresh=0)) # Commit this initial state. wlr_output.commit(state) state.finish() wlr_output.data = self self.add_listener(wlr_output.destroy_event, self._on_destroy) self.add_listener(wlr_output.frame_event, self._on_frame) self.add_listener(wlr_output.request_state_event, self._on_request_state) # The layers enum indexes into this list to get a list of surfaces self.layers: list[list[LayerStatic]] = [[] for _ in range(len(LayerShellV1Layer))] def finalize(self) -> None: self.finalize_listeners() self.core.remove_output(self) self.scene_output.destroy() def __repr__(self) -> str: i = self.get_screen_info() return f"" @property def screen(self) -> Screen: assert self.core.qtile is not None for screen in self.core.qtile.screens: # Outputs alias if they have the same (x, y) and share the same Screen, so # we don't need to check the if the width and height match the Screen's. if screen.x == self.x and screen.y == self.y: return screen return self.core.qtile.current_screen def _on_destroy(self, _listener: Listener, _data: Any) -> None: logger.debug("Signal: output destroy") self.finalize() def _on_frame(self, _listener: Listener, _data: Any) -> None: self.core.configure_node_opacity(self.core.windows_tree.node) try: self.scene_output.commit() except RuntimeError: # Failed to commit scene output; skip rendering. pass # Inform clients of the frame self.scene_output.send_frame_done(Timespec.get_monotonic_time()) def _on_request_state(self, _listener: Listener, request: OutputEventRequestState) -> None: logger.debug("Signal: output request_state") self.wlr_output.commit(request.state) def get_screen_info(self) -> ScreenRect: width, height = self.wlr_output.effective_resolution() return ScreenRect(int(self.x), int(self.y), width, height) def organise_layers(self) -> None: """Organise the positioning of layer shell surfaces.""" logger.debug("Output: organising layers") ow, oh = self.wlr_output.effective_resolution() # These rects are in output layout coordinates full_area = Box(self.x, self.y, ow, oh) usable_area = Box(self.x, self.y, ow, oh) for layer in reversed(LayerShellV1Layer): # Arrange exclusive surface from top to bottom self._organise_layer(layer, full_area, usable_area, exclusive=True) # TODO: can this be a geometry? # The positions used for reserving space are screen-relative coordinates new_reserved_space = ( usable_area.x - self.x, # left self.x + ow - usable_area.x - usable_area.width, # right usable_area.y - self.y, # top self.y + oh - usable_area.y - usable_area.height, # bottom ) delta = tuple(new - old for new, old in zip(new_reserved_space, self._reserved_space)) if any(delta): self.core.qtile.reserve_space(delta, self.screen) # type: ignore self._reserved_space = new_reserved_space for layer in reversed(LayerShellV1Layer): # Arrange non-exclusive surface from top to bottom self._organise_layer(layer, full_area, usable_area, exclusive=False) # Find topmost keyboard interactive layer for layer in (LayerShellV1Layer.OVERLAY, LayerShellV1Layer.TOP): for win in self.layers[layer]: if ( win.surface.current.keyboard_interactive == LayerSurfaceV1KeyboardInteractivity.EXCLUSIVE ): self.core.exclusive_layer = win self.core.focus_window(win) return if self.core.exclusive_layer is win: # This window previously had exclusive focus, but no longer wants it. self.core.exclusive_layer = None def _organise_layer( self, layer: LayerShellV1Layer, full_area: Box, usable_area: Box, *, exclusive: bool, ) -> None: for win in self.layers[layer]: state = win.surface.current if exclusive != (0 < state.exclusive_zone): continue win.scene_layer.configure(full_area, usable_area) win.place( win.tree.node.x, win.tree.node.y, state.desired_width, state.desired_height, 0, None, ) def contains(self, rect: WindowType) -> bool: """Returns whether the given window is visible on this output.""" if rect.x + rect.width < self.x: return False if rect.y + rect.height < self.y: return False ow, oh = self.wlr_output.effective_resolution() if self.x + ow < rect.x: return False if self.y + oh < rect.y: return False return True qtile-0.31.0/libqtile/backend/wayland/inputs.py0000664000175000017500000004073514762660347021376 0ustar epsilonepsilon# Copyright (c) 2022-3 Matt Colligan # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations from abc import ABC, abstractmethod from functools import reduce from operator import or_ from typing import TYPE_CHECKING from pywayland.protocol.wayland import WlKeyboard from xkbcommon import xkb from libqtile import configurable from libqtile.backend.wayland.wlrq import HasListeners, buttons from libqtile.log_utils import logger try: # Continue if ffi not built, so that docs can be built without wayland deps. from libqtile.backend.wayland._ffi import ffi, lib _has_ffi = True except ModuleNotFoundError: _has_ffi = False if TYPE_CHECKING: from asyncio import TimerHandle from typing import Any from pywayland.server import Listener from wlroots.wlr_types import InputDevice from wlroots.wlr_types.keyboard import Keyboard as WlrKeyboard from wlroots.wlr_types.keyboard import KeyboardKeyEvent, KeyboardModifier from libqtile.backend.wayland.core import Core KEY_PRESSED = WlKeyboard.key_state.pressed KEY_RELEASED = WlKeyboard.key_state.released if _has_ffi: # Keep this around instead of creating it on every key xkb_keysym = ffi.new("const xkb_keysym_t **") ACCEL_PROFILES = { "adaptive": lib.LIBINPUT_CONFIG_ACCEL_PROFILE_ADAPTIVE, "flat": lib.LIBINPUT_CONFIG_ACCEL_PROFILE_FLAT, } CLICK_METHODS = { "none": lib.LIBINPUT_CONFIG_CLICK_METHOD_NONE, "button_areas": lib.LIBINPUT_CONFIG_CLICK_METHOD_BUTTON_AREAS, "clickfinger": lib.LIBINPUT_CONFIG_CLICK_METHOD_CLICKFINGER, } TAP_MAPS = { "lrm": lib.LIBINPUT_CONFIG_TAP_MAP_LRM, "lmr": lib.LIBINPUT_CONFIG_TAP_MAP_LMR, } SCROLL_METHODS = { "none": lib.LIBINPUT_CONFIG_SCROLL_NO_SCROLL, "two_finger": lib.LIBINPUT_CONFIG_SCROLL_2FG, "edge": lib.LIBINPUT_CONFIG_SCROLL_EDGE, "on_button_down": lib.LIBINPUT_CONFIG_SCROLL_ON_BUTTON_DOWN, } class InputConfig(configurable.Configurable): """ This is used to configure input devices. An instance of this class represents one set of settings that can be applied to an input device. To use this, define a dictionary called ``wl_input_rules`` in your config. The keys are used to match input devices, and the values are instances of this class with the desired settings. For example: .. code-block:: python from libqtile.backend.wayland import InputConfig wl_input_rules = { "1267:12377:ELAN1300:00 04F3:3059 Touchpad": InputConfig(left_handed=True), "*": InputConfig(left_handed=True, pointer_accel=True), "type:keyboard": InputConfig(kb_options="ctrl:nocaps,compose:ralt"), } When a input device is being configured, the most specific matching key in the dictionary is found and the corresponding settings are used to configure the device. Unique identifiers are chosen first, then ``"type:X"``, then ``"*"``. The command ``qtile cmd-obj -o core -f get_inputs`` can be used to get information about connected devices, including their identifiers. Options default to ``None``, leave a device's default settings intact. For information on what each option does, see the documenation for libinput: https://wayland.freedesktop.org/libinput/doc/latest/configuration.html. Note that devices often only support a subset of settings. This tries to mirror how Sway configures libinput devices. For more information check out sway-input(5): https://man.archlinux.org/man/sway-input.5#LIBINPUT_CONFIGURATION Keyboards, managed by `xkbcommon `_, are configured with the options prefixed by ``kb_``. X11's helpful `XKB guide `_ may be useful for figuring out the syntax for some of these settings. """ defaults = [ ("accel_profile", None, "``'adaptive'`` or ``'flat'``"), ("click_method", None, "``'none'``, ``'button_areas'`` or ``'clickfinger'``"), ("drag", None, "``True`` or ``False``"), ("drag_lock", None, "``True`` or ``False``"), ("dwt", None, "True or False"), ("left_handed", None, "``True`` or ``False``"), ("middle_emulation", None, "``True`` or ``False``"), ("natural_scroll", None, "``True`` or ``False``"), ("pointer_accel", None, "A ``float`` between -1 and 1."), ("scroll_button", None, "``'disable'``, 'Button[1-3,8,9]' or a keycode"), ( "scroll_method", None, "``'none'``, ``'two_finger'``, ``'edge'``, or ``'on_button_down'``", ), ("tap", None, "``True`` or ``False``"), ("tap_button_map", None, "``'lrm'`` or ``'lmr'``"), ("kb_layout", None, "Keyboard layout i.e. ``XKB_DEFAULT_LAYOUT``"), ("kb_options", None, "Keyboard options i.e. ``XKB_DEFAULT_OPTIONS``"), ("kb_variant", None, "Keyboard variant i.e. ``XKB_DEFAULT_VARIANT``"), ("kb_repeat_rate", 25, "Keyboard key repeats made per second"), ("kb_repeat_delay", 600, "Keyboard delay in milliseconds before repeating"), ] def __init__(self, **config: Any) -> None: configurable.Configurable.__init__(self, **config) self.add_defaults(InputConfig.defaults) class _Device(ABC, HasListeners): def __init__(self, core: Core, wlr_device: InputDevice): self.core = core self.wlr_device = wlr_device self.add_listener(wlr_device.destroy_event, self._on_destroy) def finalize(self) -> None: self.finalize_listeners() def _on_destroy(self, _listener: Listener, _data: Any) -> None: logger.debug("Signal: wlr_device destroy (%s)", self.__class__.__name__) self.finalize() def get_info(self) -> tuple[str, str]: """ Get the device type and identifier for this input device. These can be used be used to assign ``InputConfig`` options to devices or types of devices. """ device = self.wlr_device name = device.name if name == " " or not name.isprintable(): name = "_" type_key = "type:" + device.type.name.lower() identifier = "%d:%d:%s" % (device.vendor, device.product, name) if type_key == "type:pointer" and lib is not None: # This checks whether the pointer is a touchpad, so that we can target those # specifically. handle = device.libinput_get_device_handle() if handle and lib.libinput_device_config_tap_get_finger_count(handle) > 0: type_key = "type:touchpad" return type_key, identifier def _match_config(self, configs: dict[str, InputConfig]) -> InputConfig | None: """Finds a matching ``InputConfig`` rule.""" type_key, identifier = self.get_info() if identifier in configs: conf = configs[identifier] elif type_key in configs: conf = configs[type_key] elif "*" in configs: conf = configs["*"] else: return None return conf @abstractmethod def configure(self, configs: dict[str, InputConfig]) -> None: """Applies ``InputConfig`` rules to this input device.""" class Keyboard(_Device): def __init__(self, core: Core, wlr_device: InputDevice, keyboard: WlrKeyboard): super().__init__(core, wlr_device) self.seat = core.seat self.keyboard = keyboard self.grabbed_keys = core.grabbed_keys self.repeat_delay_future: TimerHandle | None = None self.repeat_rate_future: TimerHandle | None = None self.keyboard.set_repeat_info(25, 600) self.xkb_context = xkb.Context() self._keymaps: dict[tuple[str | None, str | None, str | None], xkb.Keymap] = {} self.set_keymap(None, None, None) self.add_listener(self.keyboard.modifiers_event, self._on_modifier) self.add_listener(self.keyboard.key_event, self._on_key) def finalize(self) -> None: super().finalize() self.core.keyboards.remove(self) if self.core.seat.get_keyboard() == self.keyboard: # If this is the active keyboard and we have other keyboards enabled, set # the previous keyboard as the new active keyboard. if self.core.keyboards: self.seat.set_keyboard(self.core.keyboards[-1].keyboard) def set_keymap(self, layout: str | None, options: str | None, variant: str | None) -> None: """ Set the keymap for this keyboard. """ if (layout, options, variant) in self._keymaps: keymap = self._keymaps[(layout, options, variant)] else: keymap = self.xkb_context.keymap_new_from_names( layout=layout, options=options, variant=variant ) self._keymaps[(layout, options, variant)] = keymap self._state = keymap.state_new() self.keyboard.set_keymap(keymap) def _on_modifier(self, _listener: Listener, _data: Any) -> None: self.seat.set_keyboard(self.keyboard) self.seat.keyboard_notify_modifiers(self.keyboard.modifiers) def _on_repeat_key(self, keysyms: list[int], mods: KeyboardModifier) -> None: repeat_rate = self.keyboard._ptr.repeat_info.rate if self.repeat_delay_future is None or repeat_rate <= 0: return # when the repeat rate is set to 25 it means that we need to repeat the key 25 times per second # so divide 1 by the repeat rate self.repeat_rate_future = self.qtile.call_later( 1 / repeat_rate, self._on_repeat_key, keysyms, mods ) for keysym in keysyms: if (keysym, mods) in self.grabbed_keys: if self.qtile.process_key_event(keysym, mods)[1]: return def _on_key(self, _listener: Listener, event: KeyboardKeyEvent) -> None: self.qtile = self.core.qtile self.core.idle.notify_activity(self.seat) if not self.core.exclusive_client: # translate libinput keycode -> xkbcommon keycode = event.keycode + 8 layout_index = lib.xkb_state_key_get_layout(self.keyboard._ptr.xkb_state, keycode) nsyms = lib.xkb_keymap_key_get_syms_by_level( self.keyboard._ptr.keymap, keycode, layout_index, 0, xkb_keysym, ) keysyms = [xkb_keysym[0][i] for i in range(nsyms)] mods = reduce(or_, [k.keyboard.modifier for k in self.core.keyboards]) handled = False should_repeat = False if event.state == KEY_PRESSED: for keysym in keysyms: if (keysym, mods) in self.grabbed_keys: should_repeat = True if self.qtile.process_key_event(keysym, mods)[1]: handled = True break repeat_delay = self.keyboard._ptr.repeat_info.delay if should_repeat and repeat_delay > 0: # repeat delay is the delay in ms, whereas call_later expects the delay in seconds self.repeat_delay_future = self.qtile.call_later( repeat_delay / 1000, self._on_repeat_key, keysyms, mods ) else: if self.repeat_delay_future is not None: self.repeat_delay_future.cancel() self.repeat_delay_future = None if self.repeat_rate_future is not None: self.repeat_rate_future.cancel() self.repeat_rate_future = None if handled: return if self.core.focused_internal: self.core.focused_internal.process_key_press(keysym) return self.seat.keyboard_notify_key(event) def configure(self, configs: dict[str, InputConfig]) -> None: """Applies ``InputConfig`` rules to this keyboard device.""" config = self._match_config(configs) if config: self.keyboard.set_repeat_info(config.kb_repeat_rate, config.kb_repeat_delay) self.set_keymap(config.kb_layout, config.kb_options, config.kb_variant) class Pointer(_Device): def finalize(self) -> None: super().finalize() self.core._pointers.remove(self) def configure(self, configs: dict[str, InputConfig]) -> None: """Applies ``InputConfig`` rules to this pointer device.""" config = self._match_config(configs) if config is None: return handle = self.wlr_device.libinput_get_device_handle() if handle is None: logger.debug("Device not handled by libinput: %s", self.wlr_device.name) return if lib.libinput_device_config_accel_is_available(handle): if ACCEL_PROFILES.get(config.accel_profile): lib.libinput_device_config_accel_set_profile( handle, ACCEL_PROFILES.get(config.accel_profile) ) if config.pointer_accel is not None: lib.libinput_device_config_accel_set_speed(handle, config.pointer_accel) if CLICK_METHODS.get(config.click_method): lib.libinput_device_config_click_set_method( handle, CLICK_METHODS.get(config.click_method) ) if config.drag is not None: lib.libinput_device_config_tap_set_drag_enabled(handle, int(config.drag)) if config.drag_lock is not None: lib.libinput_device_config_tap_set_drag_lock_enabled(handle, int(config.drag_lock)) if config.dwt is not None: if lib.libinput_device_config_dwt_is_available(handle): lib.libinput_device_config_dwt_set_enabled(handle, int(config.dwt)) if config.left_handed is not None: if lib.libinput_device_config_left_handed_is_available(handle): lib.libinput_device_config_left_handed_set(handle, int(config.left_handed)) if config.middle_emulation is not None: lib.libinput_device_config_middle_emulation_set_enabled( handle, int(config.middle_emulation) ) if config.natural_scroll is not None: if lib.libinput_device_config_scroll_has_natural_scroll(handle): lib.libinput_device_config_scroll_set_natural_scroll_enabled( handle, int(config.natural_scroll) ) if SCROLL_METHODS.get(config.scroll_method): lib.libinput_device_config_scroll_set_method( handle, SCROLL_METHODS.get(config.scroll_method) ) if config.scroll_method == "on_button_down": if isinstance(config.scroll_button, str): if config.scroll_button == "disable": button = 0 else: # e.g. Button1 button = buttons[int(config.scroll_button[-1]) - 1] else: button = config.scroll_button lib.libinput_device_config_scroll_set_button(handle, button) if lib.libinput_device_config_tap_get_finger_count(handle) > 1: if config.tap is not None: lib.libinput_device_config_tap_set_enabled(handle, int(config.tap)) if config.tap_button_map is not None: if TAP_MAPS.get(config.tap_button_map): lib.libinput_device_config_tap_set_button_map( handle, TAP_MAPS.get(config.tap_button_map) ) qtile-0.31.0/libqtile/backend/wayland/xwindow.py0000664000175000017500000005363314762660347021554 0ustar epsilonepsilon# Copyright (c) 2021 Matt Colligan # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations import typing from wlroots import xwayland from wlroots.util.box import Box from wlroots.wlr_types import SceneTree from libqtile import hook from libqtile.backend import base from libqtile.backend.base import FloatStates from libqtile.backend.wayland.window import Static, Window from libqtile.command.base import expose_command from libqtile.log_utils import logger if typing.TYPE_CHECKING: from typing import Any import wlroots.wlr_types.foreign_toplevel_management_v1 as ftm from pywayland.server import Listener from wlroots.xwayland import SurfaceConfigureEvent from libqtile.backend.wayland.core import Core from libqtile.core.manager import Qtile from libqtile.utils import ColorsType class XWindow(Window[xwayland.Surface]): """An X11 client connecting via XWayland.""" def __init__(self, core: Core, qtile: Qtile, surface: xwayland.Surface): Window.__init__(self, core, qtile, surface) self._wm_class = self.surface.wm_class # Wait until we get a surface when mapping before making a tree self.tree: SceneTree | None = None # Update the name if the client has set one if title := surface.title: self.name = title # Add some listeners self.add_listener(surface.associate_event, self._on_associate) self.add_listener(surface.dissociate_event, self._on_dissociate) self.add_listener(surface.request_activate_event, self._on_request_activate) self.add_listener(surface.request_configure_event, self._on_request_configure) self.add_listener(surface.destroy_event, self._on_destroy) def _on_associate(self, _listener: Listener, _data: Any) -> None: logger.debug("Signal: xwindow associate") if wlr_surface := self.surface.surface: self.add_listener(wlr_surface.map_event, self._on_map) self.add_listener(wlr_surface.unmap_event, self._on_unmap) else: raise RuntimeError("XWayland surface unexpectedly has no wlr_surface") def _on_dissociate(self, _listener: Listener, _data: Any) -> None: logger.debug("Signal: xwindow dissociate") if wlr_surface := self.surface.surface: self.finalize_listener(wlr_surface.map_event) self.finalize_listener(wlr_surface.unmap_event) def _on_commit(self, _listener: Listener, _data: Any) -> None: if self.floating: if wlr_surface := self.surface.surface: state = wlr_surface.current if state.width != self._width or state.height != self._height: self.place( self.x, self.y, state.width, state.height, self.borderwidth, self.bordercolor, ) def _on_request_activate(self, _listener: Listener, event: SurfaceConfigureEvent) -> None: logger.debug("Signal: xwindow request_activate") self.surface.activate(True) def _on_request_configure(self, _listener: Listener, event: SurfaceConfigureEvent) -> None: logger.debug("Signal: xwindow request_configure") if self.floating: self.place( event.x, event.y, event.width, event.height, self.borderwidth, self.bordercolor ) else: # TODO: We shouldn't need this first configure event, but some clients (e.g. # Ardour) seem to freeze up if we pass the current state, which is what we # want, and do with `self.place`. self.surface.configure(event.x, event.y, event.width, event.height) self.place( self.x, self.y, self.width, self.height, self.borderwidth, self.bordercolor ) def _on_unmap(self, _listener: Listener, _data: Any) -> None: logger.debug("Signal: xwindow unmap") self.hide() # If X11 clients unmap themselves, we stop managing them as we normally do. See # The X core's handler for UnmapNotify. Here, we restore them to a pending # state. if self not in self.core.pending_windows: self.finalize_listeners() if self.group and self not in self.group.windows: self.group = None self.qtile.unmanage(self.wid) self.core.pending_windows.add(self) self._wid = -1 # Restore the listeners that we set up in __init__ self.add_listener(self.surface.request_configure_event, self._on_request_configure) self.add_listener(self.surface.destroy_event, self._on_destroy) if self.ftm_handle: self.ftm_handle.destroy() self.ftm_handle = None self.core.remove_pointer_constraints(self) def _on_request_fullscreen( self, _listener: Listener | None = None, _data: Any | None = None ) -> None: logger.debug("Signal: xwindow request_fullscreen") wlr_surface = self.surface.surface # check if there is a surface and if it is mapped if not wlr_surface or not wlr_surface._ptr.mapped: return # check if auto fullscreen is enabled in the config if not self.qtile.config.auto_fullscreen: return self.fullscreen = self.surface.fullscreen def _on_set_title(self, _listener: Listener, _data: Any) -> None: logger.debug("Signal: xwindow set_title") title = self.surface.title if title and title != self.name: self.name = title if self.ftm_handle: self.ftm_handle.set_title(title) hook.fire("client_name_updated", self) def _on_set_class(self, _listener: Listener, _data: Any) -> None: logger.debug("Signal: xwindow set_class") self._wm_class = self.surface.wm_class if self.ftm_handle: self.ftm_handle.set_app_id(self._wm_class or "") def hide(self) -> None: super().hide() if self.tree: self.tree.node.destroy() self.tree = None # We stop listening for commit events when unmapped, as the underlying # surface can get destroyed by the client. self.finalize_listener(self.surface.surface.commit_event) def unhide(self) -> None: if self not in self.core.pending_windows: if self.group and self.group.screen: # Only when mapping does the xwayland_surface have a wlr_surface that we can # listen for commits on and create a tree for. self.add_listener(self.surface.surface.commit_event, self._on_commit) if not self.tree: self.tree = SceneTree.subsurface_tree_create( self.container, self.surface.surface ) self.tree.node.set_position(self.borderwidth, self.borderwidth) self.container.node.set_enabled(enabled=True) # Hack: This is to fix pointer focus on xwayland dialogs # We previously did bring_to_front here but then that breaks fullscreening (xwayland windows will always be on top) # So we now only restack the surface # This means that if the dialog is behind the xwayland toplevel (and bring front click being false), focus might break # We need to fix this properly with z layering self.surface.restack(None, 0) # XCB_STACK_MODE_ABOVE return # This is the first time this window has mapped, so we need to do some initial # setup. self.core.pending_windows.remove(self) self._wid = self.core.new_wid() logger.debug("Managing new XWayland window with window ID: %s", self._wid) surface = self.surface # Now we have a surface, we can create the scene-graph node to contain it self.tree = SceneTree.subsurface_tree_create(self.container, surface.surface) # Make it static if it isn't a regular window (i.e. a window that the X11 # backend would consider un if surface.override_redirect: self.static(None, surface.x, surface.y, surface.width, surface.height) win = self.qtile.windows_map[self._wid] assert isinstance(win, XStatic) self.core.focus_window(win) win.bring_to_front() return # Save the CData handle that references this object on the XWayland surface. surface.data = self.data_handle # Now that the xwayland_surface has a wlr_surface we can add a commit # listener. And now that we have `self.tree`, we can accept fullscreen # requests. self.add_listener(surface.surface.commit_event, self._on_commit) self.add_listener(surface.request_fullscreen_event, self._on_request_fullscreen) # And it doesn't mean make sense to listen to these until we manage this # window self.add_listener(surface.set_title_event, self._on_set_title) self.add_listener(surface.set_class_event, self._on_set_class) # Save the client's desired geometry. xterm seems to have these set to 1, so # let's ignore 1 or below. The float sizes will be fetched when it is floated. if surface.width > 1: self._width = self._float_width = surface.width if surface.height > 1: self._height = self._float_height = surface.height # Set up the foreign toplevel handle handle = self.ftm_handle = self.core.foreign_toplevel_manager_v1.create_handle() self.add_listener(handle.request_maximize_event, self._on_foreign_request_maximize) self.add_listener(handle.request_minimize_event, self._on_foreign_request_minimize) self.add_listener(handle.request_activate_event, self._on_foreign_request_activate) self.add_listener(handle.request_fullscreen_event, self._on_foreign_request_fullscreen) self.add_listener(handle.request_close_event, self._on_foreign_request_close) # Get the client's name and class if title := surface.title: self.name = title handle.set_title(title) self._wm_class = surface.wm_class handle.set_app_id(self._wm_class or "") # check if the surface wanted to be fullscreened # some applications e.g. games want to fullscreen # before the window is mapped self._on_request_fullscreen() # Now the window is ready to be mapped, we can go ahead and manage it. Map # it first so that we end end up recursing into this signal handler again. self.qtile.manage(self) if self.group and self.group.screen: self.core.focus_window(self) @expose_command() def kill(self) -> None: self.surface.close() def has_fixed_size(self) -> bool: hints = self.surface.size_hints # TODO: Maybe consider these flags too: # "PMinSize" in self.hints["flags"] and "PMaxSize" in self.hints["flags"] return bool( hints and 0 < hints.min_width == hints.max_width and 0 < hints.min_height == hints.max_height ) def is_transient_for(self) -> base.WindowType | None: """What window is this window a transient window for?""" parent = self.surface.parent if parent: for win in self.qtile.windows_map.values(): if isinstance(win, XWindow) and win.surface == parent: return win return None def get_pid(self) -> int: return self.surface.pid def get_wm_type(self) -> str | None: for wm_type in self.surface.window_type: if wm_type in self.core.xwayland_atoms: return self.core.xwayland_atoms[wm_type] return None def get_wm_role(self) -> str | None: return self.surface.role def _update_fullscreen(self, do_full: bool) -> None: if do_full != (self._float_state == FloatStates.FULLSCREEN): self.surface.set_fullscreen(do_full) if self.ftm_handle: self.ftm_handle.set_fullscreen(do_full) def clip(self) -> None: if not self.tree: return if not self.tree.node.enabled: return if next(self.tree.children, None) is None: return self.tree.node.subsurface_tree_set_clip(Box(0, 0, self._width, self._height)) def place( self, x: int, y: int, width: int, height: int, borderwidth: int, bordercolor: ColorsType | None, above: bool = False, margin: int | list[int] | None = None, respect_hints: bool = False, ) -> None: # Adjust the placement to account for layout margins, if there are any. if margin is not None: if isinstance(margin, int): margin = [margin] * 4 x += margin[3] y += margin[0] width -= margin[1] + margin[3] height -= margin[0] + margin[2] if respect_hints: hints = self.surface.size_hints if hints: width = max(width, hints.min_width) height = max(height, hints.min_height) if hints.max_width > 0: width = min(width, hints.max_width) if hints.max_height > 0: height = min(height, hints.max_height) # save x and y float offset if self.group is not None and self.group.screen is not None: self.float_x = x - self.group.screen.x self.float_y = y - self.group.screen.y if width < 1: width = 1 if height < 1: height = 1 place_changed = any( [self.x != x, self.y != y, self._width != width, self._height != height] ) geom_changed = any( [ self.surface.x != x, self.surface.y != y, self.surface.width != width, self.surface.height != height, ] ) needs_repos = place_changed or geom_changed has_border_changed = any( [borderwidth != self.borderwidth, bordercolor != self.bordercolor] ) self.x = x self.y = y self._width = width self._height = height self.container.node.set_position(x, y) self.surface.configure(x, y, width, height) if needs_repos: self.clip() if needs_repos or has_border_changed: self.paint_borders(bordercolor, borderwidth) if above: self.bring_to_front() @expose_command() def bring_to_front(self) -> None: self.surface.restack(None, 0) # XCB_STACK_MODE_ABOVE self.container.node.raise_to_top() @expose_command() def static( self, screen: int | None = None, x: int | None = None, y: int | None = None, width: int | None = None, height: int | None = None, ) -> None: Window.static(self, screen, x, y, width, height) hook.fire("client_managed", self.qtile.windows_map[self._wid]) def _to_static( self, x: int | None, y: int | None, width: int | None, height: int | None ) -> XStatic: return XStatic( self.core, self.qtile, self, self._idle_inhibitors_count, x, y, width, height ) class ConfigWindow: """The XCB_CONFIG_WINDOW_* constants. Reproduced here to remove a dependency on xcffib. """ X = 1 Y = 2 Width = 4 Height = 8 class XStatic(Static[xwayland.Surface]): """A static window belonging to the XWayland shell.""" surface: xwayland.Surface def __init__( self, core: Core, qtile: Qtile, win: XWindow, idle_inhibitor_count: int, x: int | None, y: int | None, width: int | None, height: int | None, ): surface = win.surface Static.__init__( self, core, qtile, surface, win.wid, idle_inhibitor_count=idle_inhibitor_count ) self._wm_class = surface.wm_class self._conf_x = x self._conf_y = y self._conf_width = width self._conf_height = height self.add_listener(surface.surface.map_event, self._on_map) self.add_listener(surface.surface.unmap_event, self._on_unmap) self.add_listener(surface.destroy_event, self._on_destroy) self.add_listener(surface.request_configure_event, self._on_request_configure) self.add_listener(surface.set_title_event, self._on_set_title) self.add_listener(surface.set_class_event, self._on_set_class) # Checks to see if the user manually created the XStatic surface. # In which case override_redirect would be false. if surface.override_redirect: self.add_listener(surface.set_geometry_event, self._on_set_geometry) # While XWindows will always have a foreign toplevel handle, as they are always # regular windows, XStatic windows can be: 1) regular windows made static by the # user, which have a handle, or 2) XWayland popups (like OR windows), which # we won't give a handle. self.ftm_handle: ftm.ForeignToplevelHandleV1 | None = None # Take control of the scene node and tree self.container = win.container self.container.node.data = self.data_handle self.tree = win.tree def _on_unmap(self, _listener: Listener, _data: Any) -> None: logger.debug("Signal: xstatic unmap") # When an X static window unmaps, just finalize it completely, re-instantiate a # regular XWindow instance, and stick it into a pending state. This way, the # client can re-use the window with a new xwayland surface without issue. There # is certainly a nicer way to do this but that's a TODO. self._on_destroy(None, None) win = XWindow(self.core, self.qtile, self.surface) self.core.pending_windows.add(win) def _on_request_configure(self, _listener: Listener, event: SurfaceConfigureEvent) -> None: logger.debug("Signal: xstatic request_configure") cw = ConfigWindow if self._conf_x is None and event.mask & cw.X: self.x = event.x if self._conf_y is None and event.mask & cw.Y: self.y = event.y if self._conf_width is None and event.mask & cw.Width: self.width = event.width if self._conf_height is None and event.mask & cw.Height: self.height = event.height self.place(self.x, self.y, self.width, self.height, self.borderwidth, self.bordercolor) @expose_command() def kill(self) -> None: self.surface.close() def hide(self) -> None: super().hide() self.container.node.set_enabled(enabled=False) def unhide(self) -> None: if self not in self.core.pending_windows: # Only when mapping does the xwayland_surface have a wlr_surface that we can # create a tree for. if not self.tree: self.tree = SceneTree.subsurface_tree_create(self.container, self.surface.surface) self.tree.node.set_position(self.borderwidth, self.borderwidth) self.container.node.set_enabled(enabled=True) self.bring_to_front() return def place( self, x: int, y: int, width: int, height: int, borderwidth: int, bordercolor: ColorsType | None, above: bool = False, margin: int | list[int] | None = None, respect_hints: bool = False, ) -> None: self.x = x self.y = y self._width = width self._height = height self.surface.configure(x, y, self._width, self._height) self.container.node.set_position(x, y) def _on_set_title(self, _listener: Listener, _data: Any) -> None: logger.debug("Signal: xstatic set_title") title = self.surface.title if title and title != self.name: self.name = title if self.ftm_handle: self.ftm_handle.set_title(title) hook.fire("client_name_updated", self) def _on_set_class(self, _listener: Listener, _data: Any) -> None: logger.debug("Signal: xstatic set_class") self._wm_class = self.surface.wm_class if self.ftm_handle: self.ftm_handle.set_app_id(self._wm_class or "") def _on_set_geometry(self, _listener: Listener, _data: Any) -> None: logger.debug("Signal: xstatic set_geometry") # check if the surface has moved if self.surface.x != self.x or self.surface.y != self.y: self.place( self.surface.x, self.surface.y, self.surface.width, self.surface.height, 0, None ) @expose_command() def bring_to_front(self) -> None: self.surface.restack(None, 0) # XCB_STACK_MODE_ABOVE self.container.node.raise_to_top() qtile-0.31.0/libqtile/backend/wayland/core.py0000664000175000017500000021330614762660347021000 0ustar epsilonepsilon# Copyright (c) 2021-3 Matt Colligan # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations import asyncio import contextlib import os import signal import time from collections import defaultdict from typing import TYPE_CHECKING, cast import pywayland import pywayland.server import wlroots.helper as wlroots_helper import wlroots.wlr_types.virtual_keyboard_v1 as vkeyboard import wlroots.wlr_types.virtual_pointer_v1 as vpointer from pywayland.protocol.wayland import WlSeat from pywayland.utils import wl_container_of from wlroots import xwayland from wlroots.util import log as wlr_log from wlroots.util.box import Box from wlroots.wlr_types import ( DataControlManagerV1, DataDeviceManager, ExportDmabufManagerV1, ForeignToplevelManagerV1, FractionalScaleManagerV1, GammaControlManagerV1, InputInhibitManager, OutputLayout, OutputState, PointerGesturesV1, Presentation, PrimarySelectionV1DeviceManager, RelativePointerManagerV1, ScreencopyManagerV1, Surface, Viewporter, XCursorManager, XdgOutputManagerV1, input_device, pointer, seat, xdg_activation_v1, xdg_decoration_v1, ) from wlroots.wlr_types.cursor import Cursor, WarpMode from wlroots.wlr_types.idle_inhibit_v1 import IdleInhibitorManagerV1, IdleInhibitorV1 from wlroots.wlr_types.idle_notify_v1 import IdleNotifierV1 from wlroots.wlr_types.keyboard import Keyboard from wlroots.wlr_types.layer_shell_v1 import LayerShellV1, LayerSurfaceV1 from wlroots.wlr_types.output_management_v1 import ( OutputConfigurationHeadV1, OutputConfigurationV1, OutputManagerV1, ) from wlroots.wlr_types.output_power_management_v1 import ( OutputPowerManagementV1Mode, OutputPowerManagerV1, OutputPowerV1SetModeEvent, ) from wlroots.wlr_types.pointer_constraints_v1 import PointerConstraintsV1, PointerConstraintV1 from wlroots.wlr_types.scene import ( Scene, SceneBuffer, SceneNode, SceneNodeType, SceneRect, SceneSurface, SceneTree, ) from wlroots.wlr_types.server_decoration import ( ServerDecorationManager, ServerDecorationManagerMode, ) from wlroots.wlr_types.xdg_shell import XdgShell, XdgSurface, XdgSurfaceRole from xkbcommon import xkb from libqtile import hook, log_utils from libqtile.backend import base from libqtile.backend.wayland import inputs, layer, window, wlrq, xdgwindow, xwindow from libqtile.backend.wayland.output import Output from libqtile.command.base import expose_command from libqtile.config import ScreenRect from libqtile.log_utils import logger from libqtile.utils import QtileError, reap_zombies try: # Continue if ffi not built, so that docs can be built without wayland deps. from libqtile.backend.wayland._ffi import ffi, lib except ModuleNotFoundError: print("Warning: Wayland backend not built. Backend will not run.") if TYPE_CHECKING: from collections.abc import Generator from typing import Any from cairocffi import ImageSurface from pywayland.server import Listener from wlroots.wlr_types import Output as wlrOutput from wlroots.wlr_types.data_device_manager import Drag from libqtile import config class ImplicitGrab(wlrq.HasListeners): """Keep track of an implicit pointer grab. A Wayland client expects to receive pointer events from the moment a pointer button is pressed on its surface until the moment the button is released. The Wayland protocol leaves this behavior to the compositor. """ def __init__( self, core: Core, surface: Surface, start_x: int, start_y: int, start_sx: int, start_sy: int, ) -> None: self.core = core self.surface = surface self.start_dx = start_sx - start_x self.start_dy = start_sy - start_y self.add_listener(surface.destroy_event, self._on_destroy) def finalize(self) -> None: self.finalize_listeners() def _on_destroy(self, _listener: Listener, _data: Any) -> None: self.core._release_implicit_grab() class Core(base.Core, wlrq.HasListeners): supports_restarting: bool = False def __init__(self) -> None: """Setup the Wayland core backend""" # This is the window under the pointer self._hovered_window: window.WindowType | None = None # but this Internal receives keyboard input, e.g. via the Prompt widget. self.focused_internal: window.Internal | None = None # Log exceptions that are raised in Wayland callback functions. log_utils.init_log( logger.level, log_path=log_utils.get_default_log(), logger=pywayland.server.listener.logger, ) wlr_log.log_init(logger.level) log_utils.init_log( logger.level, log_path=log_utils.get_default_log(), logger=wlr_log.logger, ) self.fd: int | None = None self.display = pywayland.server.display.Display() self.event_loop = self.display.get_event_loop() ( self.compositor, self.allocator, self.renderer, self.backend, self._subcompositor, ) = wlroots_helper.build_compositor(self.display) self.socket = self.display.add_socket() os.environ["WAYLAND_DISPLAY"] = self.socket.decode() logger.info("Starting core with WAYLAND_DISPLAY=%s", self.socket.decode()) # These windows have not been mapped yet; they'll get managed when mapped self.pending_windows: set[window.WindowType] = set() # Set up inputs self.keyboards: list[inputs.Keyboard] = [] self._pointers: list[inputs.Pointer] = [] self.grabbed_keys: list[tuple[int, int]] = [] DataDeviceManager(self.display) self.live_dnd: wlrq.Dnd | None = None DataControlManagerV1(self.display) self.seat = seat.Seat(self.display, "seat0") self.add_listener(self.seat.request_set_selection_event, self._on_request_set_selection) self.add_listener( self.seat.request_set_primary_selection_event, self._on_request_set_primary_selection ) self.add_listener(self.seat.request_start_drag_event, self._on_request_start_drag) self.add_listener(self.seat.start_drag_event, self._on_start_drag) self.add_listener(self.backend.new_input_event, self._on_new_input) # Some devices are added early, so we need to remember to configure them self._pending_input_devices: list[inputs._Device] = [] hook.subscribe.startup_complete(self._configure_pending_inputs) self._input_inhibit_manager = InputInhibitManager(self.display) self.add_listener( self._input_inhibit_manager.activate_event, self._on_input_inhibitor_activate ) self.add_listener( self._input_inhibit_manager.deactivate_event, self._on_input_inhibitor_deactivate ) # exclusive_layer: this layer shell window holds keyboard focus when above other # (layer or non-layer) windows, per the layer shell protocol. self.exclusive_layer: layer.LayerStatic | None = None # exclusive_client: this client (any shell) absorbs keyboard AND pointer input, # per input inhibitor protocol. self.exclusive_client: pywayland.server.Client | None = None # Set up outputs self._outputs: list[Output] = [] self._current_output: Output | None = None self.add_listener(self.backend.new_output_event, self._on_new_output) self.output_layout = OutputLayout() self.add_listener(self.output_layout.change_event, self._on_output_layout_change) self.output_manager = OutputManagerV1(self.display) self.add_listener(self.output_manager.apply_event, self._on_output_manager_apply) self.add_listener(self.output_manager.test_event, self._on_output_manager_test) self._blanked_outputs: set[Output] = set() # Set up cursor self.cursor = Cursor(self.output_layout) # we later load a new cursor manager with the config settings self._cursor_manager = XCursorManager(None, 24) self._gestures = PointerGesturesV1(self.display) self._pressed_button_count = 0 self._implicit_grab: ImplicitGrab | None = None self.add_listener(self.seat.request_set_cursor_event, self._on_request_cursor) self.add_listener(self.cursor.axis_event, self._on_cursor_axis) self.add_listener(self.cursor.frame_event, self._on_cursor_frame) self.add_listener(self.cursor.button_event, self._on_cursor_button) self.add_listener(self.cursor.motion_event, self._on_cursor_motion) self.add_listener(self.cursor.motion_absolute_event, self._on_cursor_motion_absolute) self.add_listener(self.cursor.pinch_begin, self._on_cursor_pinch_begin) self.add_listener(self.cursor.pinch_update, self._on_cursor_pinch_update) self.add_listener(self.cursor.pinch_end, self._on_cursor_pinch_end) self.add_listener(self.cursor.swipe_begin, self._on_cursor_swipe_begin) self.add_listener(self.cursor.swipe_update, self._on_cursor_swipe_update) self.add_listener(self.cursor.swipe_end, self._on_cursor_swipe_end) self.add_listener(self.cursor.hold_begin, self._on_cursor_hold_begin) self.add_listener(self.cursor.hold_end, self._on_cursor_hold_end) self._cursor_state = wlrq.CursorState() # Set up shell self.xdg_shell = XdgShell(self.display) self.add_listener(self.xdg_shell.new_surface_event, self._on_new_xdg_surface) self.layer_shell = LayerShellV1(self.display, 4) self.add_listener(self.layer_shell.new_surface_event, self._on_new_layer_surface) # Set up scene-graph tree, which looks like this from bottom to top: # # root (self.scene) # │ # ├── self.wallpaper_tree # │ ├── SceneBuffer in self.wallpapers # │ └── ... (further outputs) # │ # ├── self.windows_tree # │ │ # │ ├── Background (layer shell) # │ │ ├── LayerStatic.tree # │ │ └── ... # │ │ # │ ├── Bottom (layer shell) # │ │ ├── LayerStatic.tree # │ │ └── ... # │ │ # │ ├── self.mid_window_tree # │ │ ├── XdgWindow.container # │ │ │ ├── XdgWindow.tree # │ │ │ └── XdgWindow._borders # │ │ ├── XWindow.container # │ │ │ ├── XWindow.tree # │ │ │ └── XWindow._borders # │ │ └── ... (further regular windows) # │ │ # │ ├── Top (same as Background) # │ │ ├── LayerStatic.tree # │ │ └── ... # │ │ # │ └── Overlay (same as Background) # │ ├── LayerStatic.tree # │ └── ... # │ # └── self.drag_icon_tree # ├── DragIcon # │ └── wlrq.Dnd # └── ... (usually only one) # self.scene = Scene() # Each tree is created above existing trees self.wallpaper_tree = SceneTree.create(self.scene.tree) self.windows_tree = SceneTree.create(self.scene.tree) self.drag_icon_tree = SceneTree.create(self.scene.tree) self.layer_trees = [ SceneTree.create(self.windows_tree), # Background SceneTree.create(self.windows_tree), # Bottom SceneTree.create(self.windows_tree), # Regular windows SceneTree.create(self.windows_tree), # Top SceneTree.create(self.windows_tree), # Overlay ] self.mid_window_tree = self.layer_trees.pop(2) self.wallpapers: dict[ config.Screen, tuple[SceneBuffer | SceneRect, ImageSurface | None] ] = {} # Add support for additional protocols ExportDmabufManagerV1(self.display) XdgOutputManagerV1(self.display, self.output_layout) ScreencopyManagerV1(self.display) GammaControlManagerV1(self.display) Viewporter(self.display) FractionalScaleManagerV1(self.display) self.scene.set_presentation(Presentation.create(self.display, self.backend)) output_power_manager = OutputPowerManagerV1(self.display) self.add_listener( output_power_manager.set_mode_event, self._on_output_power_manager_set_mode ) self.idle = IdleNotifierV1(self.display) idle_ihibitor_manager = IdleInhibitorManagerV1(self.display) self.add_listener(idle_ihibitor_manager.new_inhibitor_event, self._on_new_idle_inhibitor) PrimarySelectionV1DeviceManager(self.display) virtual_keyboard_manager_v1 = vkeyboard.VirtualKeyboardManagerV1(self.display) self.add_listener( virtual_keyboard_manager_v1.new_virtual_keyboard_event, self._on_new_virtual_keyboard, ) virtual_pointer_manager_v1 = vpointer.VirtualPointerManagerV1(self.display) self.add_listener( virtual_pointer_manager_v1.new_virtual_pointer_event, self._on_new_virtual_pointer, ) xdg_decoration_manager_v1 = xdg_decoration_v1.XdgDecorationManagerV1.create(self.display) self.add_listener( xdg_decoration_manager_v1.new_toplevel_decoration_event, self._on_new_toplevel_decoration, ) # wlr_server_decoration will be removed in a future version of wlroots server_decoration_manager = ServerDecorationManager.create(self.display) server_decoration_manager.set_default_mode(ServerDecorationManagerMode.SERVER) pointer_constraints_v1 = PointerConstraintsV1(self.display) self.add_listener( pointer_constraints_v1.new_constraint_event, self._on_new_pointer_constraint, ) self.pointer_constraints: set[window.PointerConstraint] = set() self.active_pointer_constraint: window.PointerConstraint | None = None self._relative_pointer_manager_v1 = RelativePointerManagerV1(self.display) self.foreign_toplevel_manager_v1 = ForeignToplevelManagerV1.create(self.display) self._xdg_activation_v1 = xdg_activation_v1.XdgActivationV1.create(self.display) self.add_listener( self._xdg_activation_v1.request_activate_event, self._on_xdg_activation_v1_request_activate, ) # Set up XWayland. wlroots wants to fork() and waitpid() for the # xwayland server: # https://gitlab.freedesktop.org/wlroots/wlroots/-/commit/871646d22522141c45db2c0bfa1528d595bb69df # so we need to delay installing our SIGCHLD handler so they can # actually waitpid(). we install it in _on_xwayland_ready() or the # exception handler, whichever is executed. This can be reverted if/when: # https://gitlab.freedesktop.org/wlroots/wlroots/-/merge_requests/4926 # is merged. self._xwayland: xwayland.XWayland | None = None try: self._xwayland = xwayland.XWayland(self.display, self.compositor, True) except RuntimeError: logger.info("Failed to set up XWayland. Continuing without.") asyncio.get_running_loop().add_signal_handler(signal.SIGCHLD, reap_zombies) else: os.environ["DISPLAY"] = self._xwayland.display_name or "" logger.info("Set up XWayland with DISPLAY=%s", os.environ["DISPLAY"]) self.add_listener(self._xwayland.ready_event, self._on_xwayland_ready) self.add_listener(self._xwayland.new_surface_event, self._on_xwayland_new_surface) # Start self.backend.start() def get_enabled_outputs(self) -> list[Output]: ret = [] for output in self._outputs: if not output.wlr_output: continue if not output.wlr_output.enabled: continue ret.append(output) return ret @property def name(self) -> str: return "wayland" @property def active_keyboard(self) -> inputs.Keyboard | None: keyboard = self.seat.get_keyboard() if keyboard is not None: for kb in self.keyboards: if kb.keyboard._ptr == keyboard._ptr: return kb return None def finalize(self) -> None: self.finalize_listeners() self._poll() for kb in self.keyboards.copy(): kb.finalize() for pt in self._pointers.copy(): pt.finalize() for out in self._outputs.copy(): out.finalize() if self._xwayland: self._xwayland.destroy() self._cursor_manager.destroy() self.cursor.destroy() self.output_layout.destroy() self.seat.destroy() self.backend.destroy() self.display.destroy() if hasattr(self, "qtile"): delattr(self, "qtile") def win_from_node(self, node: SceneNode) -> window.Window | window.Internal: tree = node.parent while tree and tree.node.data is None: tree = tree.node.parent assert tree and tree.node.data return tree.node.data # Low level iteration code # We cannot use for_each_buffer I think because that does not give the underlying nodes # We need to underlying nodes to get the opacity property def configure_node_opacity(self, node: SceneNode) -> None: if not node.enabled: return if node.type == SceneNodeType.BUFFER: scene_buffer = SceneBuffer.from_node(node) win = self.win_from_node(node) scene_buffer.set_opacity(win.opacity) elif node.type == SceneNodeType.TREE: tree_ptr = wl_container_of(node._ptr, "struct wlr_scene_tree *", "node", ffi=ffi) parent_tree = SceneTree(tree_ptr) for child in parent_tree.children: self.configure_node_opacity(child) @property def display_name(self) -> str: return self.socket.decode() def _on_request_set_selection( self, _listener: Listener, event: seat.RequestSetSelectionEvent ) -> None: self.seat.set_selection(event._ptr.source, event.serial) logger.debug("Signal: seat request_set_selection") def _on_request_set_primary_selection( self, _listener: Listener, event: seat.RequestSetPrimarySelectionEvent ) -> None: self.seat.set_primary_selection(event._ptr.source, event.serial) logger.debug("Signal: seat request_set_primary_selection") def _on_request_start_drag( self, _listener: Listener, event: seat.RequestStartDragEvent ) -> None: logger.debug("Signal: seat request_start_drag") if not self.live_dnd and self.seat.validate_pointer_grab_serial( event.origin, event.serial ): self.seat.start_pointer_drag(event.drag, event.serial) else: event.drag.source.destroy() def _on_start_drag(self, _listener: Listener, wlr_drag: Drag) -> None: logger.debug("Signal: seat start_drag") self._release_implicit_grab() if not wlr_drag.icon: return self.live_dnd = wlrq.Dnd(self, wlr_drag) def _on_new_input(self, _listener: Listener, wlr_device: input_device.InputDevice) -> None: logger.debug("Signal: backend new_input_event") device: inputs._Device if wlr_device.type == input_device.InputDeviceType.POINTER: device = self._add_new_pointer(wlr_device) elif wlr_device.type == input_device.InputDeviceType.KEYBOARD: device = self._add_new_keyboard(wlr_device) else: logger.info("New %s device", wlr_device.type.name) return capabilities = WlSeat.capability.pointer if self.keyboards: capabilities |= WlSeat.capability.keyboard self.seat.set_capabilities(capabilities) logger.info("New device: %s %s", *device.get_info()) if hasattr(self, "qtile"): if self.qtile.config.wl_input_rules: device.configure(self.qtile.config.wl_input_rules) else: self._pending_input_devices.append(device) def _on_new_output(self, _listener: Listener, wlr_output: wlrOutput) -> None: logger.debug("Signal: backend new_output_event") output = Output(self, wlr_output) self._outputs.append(output) # Let the output layout place it if not self.output_layout.add_auto(wlr_output): logger.warning("Failed to add output to layout.") return # Set the current output as we have none defined # Now that we have our first output we can warp the pointer there too # We also set the cursor image as we're initializing the cursor here anyways if not self._current_output: self._current_output = output self.cursor.set_xcursor(self._cursor_manager, "default") rect = output.get_screen_info() box = Box(rect.x, rect.y, rect.width, rect.height) x = box.x + box.width / 2 y = box.y + box.height / 2 self.warp_pointer(x, y) def _on_output_layout_change( self, _listener: Listener | None = None, _data: Any = None ) -> None: logger.debug("Signal: output_layout change_event") config = OutputConfigurationV1() # disable mons that are no longer enabled for output in self._outputs: if output.wlr_output.enabled: continue head = OutputConfigurationHeadV1.create(config, output.wlr_output) head.state.enabled = False self.output_layout.remove(output.wlr_output) if output is self._current_output: en_outputs = self.get_enabled_outputs() self._current_output = en_outputs[0] if en_outputs else None for output in self._outputs: if not output.wlr_output.enabled: continue box = self.output_layout.get_box(output.wlr_output) if lib.wlr_box_empty(box._ptr): self.output_layout.add_auto(output.wlr_output) # add monitors to layout that are now enabled for output in self._outputs: if not output.wlr_output.enabled: continue head = OutputConfigurationHeadV1.create(config, output.wlr_output) box = self.output_layout.get_box(output.wlr_output) head.state.x = output.x = box.x head.state.y = output.y = box.y head.state.enabled = not lib.wlr_box_empty(box._ptr) output.scene_output.set_position(output.x, output.y) self.output_manager.set_configuration(config) self._outputs.sort(key=lambda o: (o.x, o.y)) hook.fire("screen_change", None) def _on_output_manager_apply( self, _listener: Listener, config: OutputConfigurationV1 ) -> None: logger.debug("Signal: output_manager apply_event") self._output_manager_reconfigure(config, True) def _on_output_manager_test(self, _listener: Listener, config: OutputConfigurationV1) -> None: logger.debug("Signal: output_manager test_event") self._output_manager_reconfigure(config, False) def _on_request_cursor( self, _listener: Listener, event: seat.PointerRequestSetCursorEvent ) -> None: if event._ptr.seat_client != self.seat.pointer_state._ptr.focused_client: # The request came from a cheeky window that doesn't have the pointer return self._cursor_state.surface = event.surface self._cursor_state.hotspot = event.hotspot if not self._cursor_state.hidden: self.cursor.set_surface(event.surface, event.hotspot) def _on_new_xdg_surface(self, _listener: Listener, xdg_surface: XdgSurface) -> None: assert self.qtile is not None logger.debug("Signal: xdg_shell new_surface_event") win: xdgwindow.XdgWindow | layer.LayerStatic if xdg_surface.role == XdgSurfaceRole.TOPLEVEL: # The new surface is a regular top-level window. win = xdgwindow.XdgWindow(self, self.qtile, xdg_surface) self.pending_windows.add(win) return if xdg_surface.role == XdgSurfaceRole.POPUP: # The new surface is a popup window. if not self._current_output: raise RuntimeError("Can't place a popup without any outputs enabled.") parent_surface = xdg_surface.popup.parent if parent_xdg_surface := XdgSurface.try_from_surface(parent_surface): # An XDG shell window or popup created this popup if parent_xdg_surface.role == XdgSurfaceRole.TOPLEVEL: # If the immediate parent is a toplevel, we're a level 1 popup win = cast(xdgwindow.XdgWindow, parent_xdg_surface.data) tree = win.tree else: # otherwise, this is a nested popup tree = cast(SceneTree, parent_xdg_surface.data) while parent_xdg_surface.role == XdgSurfaceRole.POPUP: parent_xdg_surface = XdgSurface.try_from_surface( parent_xdg_surface.popup.parent ) win = cast(xdgwindow.XdgWindow, parent_xdg_surface.data) xdg_surface.data = self.scene.xdg_surface_create(tree, xdg_surface) elif parent := LayerSurfaceV1.try_from_wlr_surface(parent_surface): # A layer shell window created this popup win = cast(layer.LayerStatic, parent.data) self.scene.xdg_surface_create(win.popup_tree, xdg_surface) else: raise RuntimeError("Unknown surface as popup's parent.") # Position the popup box = xdg_surface.get_geometry() lx, ly = self.output_layout.closest_point(win.x + box.x, win.y + box.y) wlr_output = self.output_layout.output_at(lx, ly) screen_rect = wlr_output.data.get_screen_info() box = Box(screen_rect.x, screen_rect.y, screen_rect.width, screen_rect.height) box.x = round(box.x - lx) box.y = round(box.y - ly) xdg_surface.popup.unconstrain_from_box(box) return logger.warning("xdg_shell surface had no role set. Ignoring.") def _release_implicit_grab(self, time: int = 0) -> None: if self._implicit_grab is not None: logger.debug("Releasing implicit grab.") self._implicit_grab.finalize() self._implicit_grab = None # Pretend the cursor just appeared where it is. self._process_cursor_motion(time, self.cursor.x, self.cursor.y) def _create_implicit_grab(self, time: int, surface: Surface, sx: float, sy: float) -> None: self._release_implicit_grab(time) logger.debug("Creating implicit grab.") self._implicit_grab = ImplicitGrab( self, surface, self.cursor.x, self.cursor.y, int(sx), int(sy) ) def _on_cursor_axis(self, _listener: Listener, event: pointer.PointerAxisEvent) -> None: handled = False if event.delta != 0 and not self.exclusive_client and not self._implicit_grab: # If we have a client who exclusively gets input, button bindings are disallowed. if event.orientation == pointer.AxisOrientation.VERTICAL: button = 5 if 0 < event.delta else 4 else: button = 7 if 0 < event.delta else 6 handled = self._process_cursor_button(button, True) if not handled: self.seat.pointer_notify_axis( event.time_msec, event.orientation, event.delta, event.delta_discrete, event.source, ) def _on_cursor_frame(self, _listener: Listener, _data: Any) -> None: self.seat.pointer_notify_frame() def _on_cursor_button(self, _listener: Listener, event: pointer.PointerButtonEvent) -> None: assert self.qtile is not None self.idle.notify_activity(self.seat) found = None pressed = event.button_state == input_device.ButtonState.PRESSED if pressed: self._pressed_button_count += 1 if self._implicit_grab is None: found = self._focus_by_click() else: if self._pressed_button_count > 0: # sanity check self._pressed_button_count -= 1 if self._implicit_grab is not None: self.seat.pointer_notify_button(event.time_msec, event.button, event.button_state) if self._pressed_button_count == 0: self._release_implicit_grab(event.time_msec) return handled = False if not self.exclusive_client and event.button in wlrq.buttons: # If we have a client who exclusively gets input, button bindings are disallowed. button = wlrq.buttons.index(event.button) + 1 handled = self._process_cursor_button(button, pressed) if not handled: if self._pressed_button_count == 1 and found and not self.live_dnd: win, surface, sx, sy = found if surface: self._create_implicit_grab(event.time_msec, surface, sx, sy) self.seat.pointer_notify_button(event.time_msec, event.button, event.button_state) def _implicit_grab_motion(self, time: int) -> None: if self._implicit_grab: sx = self.cursor.x + self._implicit_grab.start_dx sy = self.cursor.y + self._implicit_grab.start_dy self.seat.pointer_notify_motion(time, sx, sy) def _on_cursor_motion(self, _listener: Listener, event: pointer.PointerMotionEvent) -> None: assert self.qtile is not None self.idle.notify_activity(self.seat) dx = event.delta_x dy = event.delta_y # Send relative pointer events to seat - used e.g. by games that have # constrained cursor movement but want movement events self._relative_pointer_manager_v1.send_relative_motion( self.seat, event.time_msec * 1000, dx, dy, event.unaccel_delta_x, event.unaccel_delta_y, ) if self.active_pointer_constraint: if not self.active_pointer_constraint.rect.contains_point( self.cursor.x + dx, self.cursor.y + dy ): return self.cursor.move(dx, dy) if self._implicit_grab is None: self._process_cursor_motion(event.time_msec, self.cursor.x, self.cursor.y) else: self._implicit_grab_motion(event.time_msec) def _on_cursor_motion_absolute( self, _listener: Listener, event: pointer.PointerMotionAbsoluteEvent ) -> None: assert self.qtile is not None self.idle.notify_activity(self.seat) x, y = self.cursor.absolute_to_layout_coords(event.pointer.base, event.x, event.y) self.cursor.move(x - self.cursor.x, y - self.cursor.y) if self._implicit_grab is None: self._process_cursor_motion(event.time_msec, self.cursor.x, self.cursor.y) else: self._implicit_grab_motion(event.time_msec) def _on_cursor_pinch_begin( self, _listener: Listener, event: pointer.PointerPinchBeginEvent, ) -> None: self.idle.notify_activity(self.seat) self._gestures.send_pinch_begin(self.seat, event.time_msec, event.fingers) def _on_cursor_pinch_update( self, _listener: Listener, event: pointer.PointerPinchUpdateEvent, ) -> None: self._gestures.send_pinch_update( self.seat, event.time_msec, event.dx, event.dy, event.scale, event.rotation ) def _on_cursor_pinch_end( self, _listener: Listener, event: pointer.PointerPinchEndEvent, ) -> None: self.idle.notify_activity(self.seat) self._gestures.send_pinch_end(self.seat, event.time_msec, event.cancelled) def _on_cursor_swipe_begin( self, _listener: Listener, event: pointer.PointerSwipeBeginEvent, ) -> None: self.idle.notify_activity(self.seat) self._gestures.send_swipe_begin(self.seat, event.time_msec, event.fingers) def _on_cursor_swipe_update( self, _listener: Listener, event: pointer.PointerSwipeUpdateEvent, ) -> None: self._gestures.send_swipe_update(self.seat, event.time_msec, event.dx, event.dy) def _on_cursor_swipe_end( self, _listener: Listener, event: pointer.PointerSwipeEndEvent, ) -> None: self.idle.notify_activity(self.seat) self._gestures.send_swipe_end(self.seat, event.time_msec, event.cancelled) def _on_cursor_hold_begin( self, _listener: Listener, event: pointer.PointerHoldBeginEvent, ) -> None: self.idle.notify_activity(self.seat) self._gestures.send_hold_begin(self.seat, event.time_msec, event.fingers) def _on_cursor_hold_end( self, _listener: Listener, event: pointer.PointerHoldEndEvent, ) -> None: self.idle.notify_activity(self.seat) self._gestures.send_hold_end(self.seat, event.time_msec, event.cancelled) def _on_new_pointer_constraint( self, _listener: Listener, wlr_constraint: PointerConstraintV1 ) -> None: logger.debug("Signal: pointer_constraints new_constraint") constraint = window.PointerConstraint(self, wlr_constraint) self.pointer_constraints.add(constraint) if self.seat.pointer_state.focused_surface == wlr_constraint.surface: if self.active_pointer_constraint: self.active_pointer_constraint.disable() constraint.enable() def _on_new_virtual_keyboard( self, _listener: Listener, virtual_keyboard: vkeyboard.VirtualKeyboardV1 ) -> None: self._add_new_keyboard(virtual_keyboard.keyboard.base) def _on_new_virtual_pointer( self, _listener: Listener, new_pointer_event: vpointer.VirtualPointerV1NewPointerEvent ) -> None: device = self._add_new_pointer(new_pointer_event.new_pointer.pointer.base) logger.info("New virtual pointer: %s %s", *device.get_info()) def _on_new_idle_inhibitor( self, _listener: Listener, idle_inhibitor: IdleInhibitorV1 ) -> None: logger.debug("Signal: idle_inhibitor new_inhibitor") for win in self.qtile.windows_map.values(): if isinstance(win, window.Window | window.Static): win.surface.for_each_surface(win.add_idle_inhibitor, idle_inhibitor) if idle_inhibitor.data: # We break if the .data attribute was set, because that tells us # that `win.add_idle_inhibitor` identified this inhibitor as # belonging to that window. break def _on_input_inhibitor_activate(self, _listener: Listener, _data: Any) -> None: logger.debug("Signal: input_inhibitor activate") assert self.qtile is not None self.exclusive_client = self._input_inhibit_manager.active_client # If another client has keyboard focus, unfocus it. if self.qtile.current_window and not self.qtile.current_window.belongs_to_client( self.exclusive_client ): self.focus_window(None) # If another client has pointer focus, unfocus that too. found = self._under_pointer() if found: win, _, _, _ = found # If we have a client who exclusively gets input, no other client's # surfaces are allowed to get pointer input. if isinstance(win, base.Internal) or not win.belongs_to_client(self.exclusive_client): self.cursor.set_xcursor(self._cursor_manager, "default") self.seat.pointer_notify_clear_focus() return def _on_input_inhibitor_deactivate(self, _listener: Listener, _data: Any) -> None: logger.debug("Signal: input_inhibitor deactivate") self.exclusive_client = None def _on_output_power_manager_set_mode( self, _listener: Listener, mode: OutputPowerV1SetModeEvent ) -> None: """ Blank/unblank outputs via the output power management protocol. `_blanked_outputs` keeps track of those that were blanked because we don't want to unblank outputs that were already disabled due to not being part of the user-configured layout. """ logger.debug("Signal: output_power_manager set_mode_event") wlr_output = mode.output output = cast(Output, wlr_output.data) if mode.mode == OutputPowerManagementV1Mode.ON: if output in self._blanked_outputs: wlr_output.enable(enable=True) try: wlr_output.commit() except RuntimeError: logger.warning("Couldn't enable output %s", wlr_output.name) return self._blanked_outputs.remove(output) else: if wlr_output.enabled: wlr_output.enable(enable=False) try: wlr_output.commit() except RuntimeError: logger.warning("Couldn't disable output %s", wlr_output.name) return self._blanked_outputs.add(output) def _on_new_layer_surface(self, _listener: Listener, layer_surface: LayerSurfaceV1) -> None: logger.debug("Signal: layer_shell new_surface_event") assert self.qtile is not None wid = self.new_wid() win = layer.LayerStatic(self, self.qtile, layer_surface, wid) logger.info("Managing new layer_shell window with window ID: %s", wid) self.qtile.manage(win) def _on_new_toplevel_decoration( self, _listener: Listener, decoration: xdg_decoration_v1.XdgToplevelDecorationV1 ) -> None: logger.debug("Signal: xdg_decoration new_top_level_decoration") decoration.set_mode(xdg_decoration_v1.XdgToplevelDecorationV1Mode.SERVER_SIDE) def _on_xwayland_ready(self, _listener: Listener, _data: Any) -> None: logger.debug("Signal: xwayland ready") asyncio.get_running_loop().add_signal_handler(signal.SIGCHLD, reap_zombies) assert self._xwayland is not None self._xwayland.set_seat(self.seat) self.xwayland_atoms: dict[int, str] = wlrq.get_xwayland_atoms(self._xwayland) # Set the default XWayland cursor if xcursor := self._cursor_manager.get_xcursor("default"): image = next(xcursor.images, None) if image: self._xwayland.set_cursor( image._ptr.buffer, image._ptr.width * 4, image._ptr.width, image._ptr.height, image._ptr.hotspot_x, image._ptr.hotspot_y, ) def _on_xwayland_new_surface(self, _listener: Listener, surface: xwayland.Surface) -> None: logger.debug("Signal: xwayland new_surface") assert self.qtile is not None win = xwindow.XWindow(self, self.qtile, surface) self.pending_windows.add(win) def _on_xdg_activation_v1_request_activate( self, _listener: Listener, event: xdg_activation_v1.XdgActivationV1RequestActivateEvent ) -> None: """Respond to window activate events via the XDG activation V1 protocol.""" logger.debug("Signal: xdg_activation_v1 request_activate") assert self.qtile is not None focus_on_window_activation = self.qtile.config.focus_on_window_activation if focus_on_window_activation == "never": logger.debug("Ignoring focus request (focus_on_window_activation='never')") return surface = event.surface if surface: xdg_surface = XdgSurface.try_from_surface(surface) if xdg_surface is not None: if win := xdg_surface.data: win.handle_activation_request(focus_on_window_activation) else: # Shouldn't happen. logger.debug("Failed to find window to activate. Ignoring request.") def _output_manager_reconfigure(self, config: OutputConfigurationV1, apply: bool) -> None: """ See if an output configuration would be accepted by the backend, and apply it if desired. """ ok = True for head in config.heads: head_state = head.state wlr_output = head_state.output state = OutputState() state.set_enabled(head_state.enabled) if head_state.enabled: if head_state.mode: state.set_mode(head_state.mode) else: state.set_custom_mode(head_state.custom_mode) state.set_transform(head_state.transform) state.set_scale(head_state.scale) state.set_adaptive_sync_enabled(head_state.adaptive_sync_enabled) # Rescale the cursor if necessary box = self.output_layout.get_box(wlr_output) if box.x != head_state.x or box.y != head_state.y: self.output_layout.add(wlr_output, head_state.x, head_state.y) # Ensure we have cursors loaded for the new scale factor. self._cursor_manager.load(head_state.scale) if not self.seat.pointer_state.focused_surface: self.cursor.set_xcursor(self._cursor_manager, "default") if apply: ok = ok and wlr_output.commit(state) else: ok = ok and wlr_output.test(state) state.finish() if ok: config.send_succeeded() else: config.send_failed() config.destroy() if apply: self._on_output_layout_change() def _process_cursor_motion(self, time_msec: int, cx: float, cy: float) -> None: assert self.qtile cx_int = int(cx) cy_int = int(cy) if not self.exclusive_client: # If we have a client who exclusively gets input, button bindings are # disallowed, so process_button_motion doesn't need to be updated. self.qtile.process_button_motion(cx_int, cy_int) if len(self.get_enabled_outputs()) > 1: current_wlr_output = self.output_layout.output_at(cx, cy) if current_wlr_output: current_output = current_wlr_output.data if self._current_output is not current_output: self._current_output = current_output if self.live_dnd: self.live_dnd.position(cx, cy) self._focus_pointer(cx_int, cy_int, motion=time_msec) def _focus_pointer(self, cx: int, cy: int, motion: int | None = None) -> None: assert self.qtile is not None found = self._under_pointer() if found: win, surface, sx, sy = found if self.exclusive_client: # If we have a client who exclusively gets input, no other client's # surfaces are allowed to get pointer input. if isinstance(win, base.Internal) or not win.belongs_to_client( self.exclusive_client ): # Moved to an internal or unrelated window if self._hovered_window is not win: logger.debug( "Pointer focus withheld from window not owned by exclusive client." ) self.cursor.set_xcursor(self._cursor_manager, "default") self.seat.pointer_notify_clear_focus() self._hovered_window = win return if isinstance(win, window.Internal): if self._hovered_window is win: # pointer remained within the same Internal window if motion is not None: win.process_pointer_motion( cx - self._hovered_window.x, cy - self._hovered_window.y, ) else: if self._hovered_window: if isinstance(self._hovered_window, window.Internal): if motion is not None: # moved from an Internal to a different Internal self._hovered_window.process_pointer_leave( cx - self._hovered_window.x, cy - self._hovered_window.y, ) elif self.seat.pointer_state.focused_surface: # moved from a Window or Static to an Internal self.cursor.set_xcursor(self._cursor_manager, "default") self.seat.pointer_notify_clear_focus() win.process_pointer_enter(cx, cy) self._hovered_window = win return if surface: # The pointer is in a client's surface self.seat.pointer_notify_enter(surface, sx, sy) if motion is not None: self.seat.pointer_notify_motion(motion, sx, sy) else: # The pointer is on the border of a client's window if self.seat.pointer_state.focused_surface: # We just moved out of a client's surface self.cursor.set_xcursor(self._cursor_manager, "default") self.seat.pointer_notify_clear_focus() if self._hovered_window is not win: # We only want to fire client_mouse_enter once, so check # self._hovered_window. hook.fire("client_mouse_enter", win) if win is not self.qtile.current_window: if motion is not None and self.qtile.config.follow_mouse_focus is True: if isinstance(win, window.Static): self.qtile.focus_screen(win.screen.index, False) else: if win.group and win.group.current_window != win: win.group.focus(win, False) if ( win.group and win.group.screen and self.qtile.current_screen != win.group.screen ): self.qtile.focus_screen(win.group.screen.index, False) self._hovered_window = win else: # There is no window under the pointer if self._hovered_window: if isinstance(self._hovered_window, window.Internal): # We just moved out of an Internal self._hovered_window.process_pointer_leave( cx - self._hovered_window.x, cy - self._hovered_window.y, ) else: # We just moved out of a Window or Static self.cursor.set_xcursor(self._cursor_manager, "default") self.seat.pointer_notify_clear_focus() self._hovered_window = None def _process_cursor_button(self, button: int, pressed: bool) -> bool: assert self.qtile is not None handled = False if pressed: if keyboard := self.seat.get_keyboard(): handled = self.qtile.process_button_click( button, keyboard.modifier, int(self.cursor.x), int(self.cursor.y) ) else: logger.warning("No active keyboard found, keybinding may be missed.") if isinstance(self._hovered_window, window.Internal): self._hovered_window.process_button_click( int(self.cursor.x - self._hovered_window.x), int(self.cursor.y - self._hovered_window.y), button, ) else: if keyboard := self.seat.get_keyboard(): handled = self.qtile.process_button_release(button, keyboard.modifier) else: logger.warning("No active keyboard found, keybinding may be missed.") if isinstance(self._hovered_window, window.Internal): self._hovered_window.process_button_release( int(self.cursor.x - self._hovered_window.x), int(self.cursor.y - self._hovered_window.y), button, ) return handled def _add_new_pointer(self, wlr_device: input_device.InputDevice) -> inputs.Pointer: device = inputs.Pointer(self, wlr_device) self._pointers.append(device) self.cursor.attach_input_device(wlr_device) self.cursor.set_xcursor(self._cursor_manager, "default") # Map input device to output if required. if output_name := pointer.Pointer.from_input_device(wlr_device).output_name: target_output = None for output in self.get_enabled_outputs(): if output_name == output.wlr_output.name: target_output = output.wlr_output break if target_output: logger.debug("Mapping pointer to output: %s", output_name) else: logger.warning("Failed to find output (%s) for mapping pointer.", output_name) self.cursor.map_input_to_output(wlr_device, target_output) return device def _add_new_keyboard(self, wlr_device: input_device.InputDevice) -> inputs.Keyboard: keyboard = Keyboard.from_input_device(wlr_device) device = inputs.Keyboard(self, wlr_device, keyboard) self.keyboards.append(device) self.seat.set_keyboard(keyboard) return device def _configure_pending_inputs(self) -> None: """Configure inputs that were detected before the config was loaded.""" assert self.qtile is not None if self.qtile.config.wl_input_rules: for device in self._pending_input_devices: device.configure(self.qtile.config.wl_input_rules) self._pending_input_devices.clear() def setup_listener(self) -> None: """Setup a listener for the given qtile instance""" logger.debug("Adding io watch") # destroy the old one and load a new one with config settings self._cursor_manager.destroy() self._cursor_manager = XCursorManager( self.qtile.config.wl_xcursor_theme, self.qtile.config.wl_xcursor_size ) self.cursor.set_xcursor(self._cursor_manager, "default") self.fd = lib.wl_event_loop_get_fd(self.event_loop._ptr) if self.fd: asyncio.get_running_loop().add_reader(self.fd, self._poll) else: raise RuntimeError("Failed to get Wayland event loop file descriptor.") def remove_listener(self) -> None: """Remove the listener from the given event loop""" if self.fd is not None: logger.debug("Removing io watch") loop = asyncio.get_running_loop() loop.remove_reader(self.fd) self.fd = None def _poll(self) -> None: if not self.display.destroyed: self.display.flush_clients() self.event_loop.dispatch(0) self.display.flush_clients() def on_config_load(self, initial: bool) -> None: if initial: # This backend does not support restarting return assert self.qtile is not None managed_wins = [ w for w in self.qtile.windows_map.values() if isinstance(w, window.Window) ] for win in managed_wins: group = None if win.group: if win.group.name in self.qtile.groups_map: # Put window on group with same name as its old group if one exists group = self.qtile.groups_map[win.group.name] else: # Otherwise place it on the group at the same index for i, old_group in self.qtile._state.groups: # type: ignore if i < len(self.qtile.groups): name = old_group[0] if win.group.name == name: group = self.qtile.groups[i] if win in win.group.windows: # Remove window from old group win.group.remove(win) if group is None: # Falling back to current group if none found group = self.qtile.current_group group.add(win) if group == self.qtile.current_group: win.unhide() else: win.hide() # Apply input device configuration if self.qtile.config.wl_input_rules: for device in [*self.keyboards, *self._pointers]: device.configure(self.qtile.config.wl_input_rules) def new_wid(self) -> int: """Get a new unique window ID""" assert self.qtile is not None return max(self.qtile.windows_map.keys(), default=0) + 1 def focus_window( self, win: window.WindowType | None, surface: Surface | None = None, enter: bool = True ) -> None: if self.seat.destroyed: return if self.exclusive_client: # If we have a client who exclusively gets input, no other client's surfaces # are allowed to get keyboard input. if not win: self.seat.keyboard_clear_focus() return if isinstance(win, base.Internal) or not win.belongs_to_client(self.exclusive_client): logger.debug("Keyboard focus withheld from window not owned by exclusive client.") # We can't focus surfaces belonging to other clients. return if self.exclusive_layer and win is not self.exclusive_layer: logger.debug("Keyboard focus withheld: focus is fixed to exclusive layer surface.") return if isinstance(win, base.Internal): self.focused_internal = win self.seat.keyboard_clear_focus() return if surface is None and win is not None: surface = win.surface.surface if self.focused_internal: self.focused_internal = None if isinstance(win, layer.LayerStatic): if not win.surface.current.keyboard_interactive: return if isinstance(win, xwindow.XStatic): if win.surface.override_redirect and not win.surface.or_surface_wants_focus(): return if win.surface.icccm_input_model() == xwayland.ICCCMInputModel.NONE: return previous_surface = self.seat.keyboard_state.focused_surface if previous_surface == surface: return if previous_surface is not None: # Deactivate the previously focused surface if previous_xdg_surface := XdgSurface.try_from_surface(previous_surface): if not win or win.surface != previous_xdg_surface: previous_xdg_surface.set_activated(False) if prev_win := previous_xdg_surface.data: if ftm_handle := prev_win.ftm_handle: ftm_handle.set_activated(False) else: prev_xwayland_surface = xwayland.Surface.try_from_wlr_surface(previous_surface) if prev_xwayland_surface is not None and ( not win or win.surface != prev_xwayland_surface ): prev_xwayland_surface.activate(False) if prev_win := prev_xwayland_surface.data: if ftm_handle := prev_win.ftm_handle: ftm_handle.set_activated(False) if not win or not surface: self.seat.keyboard_clear_focus() return logger.debug("Focusing new window") ftm_handle = None if isinstance(win.surface, XdgSurface): win.surface.set_activated(True) ftm_handle = win.ftm_handle elif isinstance(win.surface, xwayland.Surface): win.surface.activate(True) ftm_handle = win.ftm_handle if ftm_handle: ftm_handle.set_activated(True) if enter: if keyboard := self.seat.get_keyboard(): self.seat.keyboard_notify_enter(surface, keyboard) def _focus_by_click(self) -> tuple[window.WindowType, Surface | None, float, float] | None: assert self.qtile is not None found = self._under_pointer() if found: win, surface, sx, sy = found if self.exclusive_client: # If we have a client who exclusively gets input, no other client's # surfaces are allowed to get focus. if isinstance(win, base.Internal) or not win.belongs_to_client( self.exclusive_client ): logger.debug("Focus withheld from window not owned by exclusive client.") return None if self.qtile.config.bring_front_click is True: win.bring_to_front() elif self.qtile.config.bring_front_click == "floating_only": if isinstance(win, base.Window) and win.floating: win.bring_to_front() if isinstance(win, window.Static): if win.screen is not self.qtile.current_screen: self.qtile.focus_screen(win.screen.index, warp=False) win.focus(False) elif isinstance(win, window.Window): if win.group and win.group.screen is not self.qtile.current_screen: self.qtile.focus_screen(win.group.screen.index, warp=False) self.qtile.current_group.focus(win, False) else: screen = self.qtile.find_screen(int(self.cursor.x), int(self.cursor.y)) if screen: self.qtile.focus_screen(screen.index, warp=False) return found def _under_pointer(self) -> tuple[window.WindowType, Surface | None, float, float] | None: """ Find which window and surface is currently under the pointer, if any. """ # Warning: this method is a bit difficult to follow and has liberal use of # typing.cast. Make sure you're familiar with how the scene-graph tree is laid # out (see diagram in __init__ above). assert self.qtile is not None maybe_node = self.windows_tree.node.node_at(self.cursor.x, self.cursor.y) if maybe_node is None: # We didn't find any node, so there is no window under the pointer. return None node, sx, sy = maybe_node if node.type == SceneNodeType.BUFFER: # Buffer nodes can be any surface or subsurface (nested in subtrees) of a # client or Internal window. In all cases we will get a wlr_scene_buffer, # but only client surfaces will have a wlr_scene_surface. scene_buffer = cast(SceneBuffer, SceneBuffer.from_node(node)) if scene_surface := SceneSurface.from_buffer(scene_buffer): # We got a node that is part of a window, walk up the scene graph to # find the window object. It could also be an XDG popup, which can be # the child of either an XDG window or a layer shell window. win = self.win_from_node(node) return win, scene_surface.surface, sx, sy # We didn't get a wlr_scene_surface, so we're dealing with an internal window # Internal windows have a scenetree for borders. The parent's data we will use to cast to an internal window parent_tree = cast(SceneTree, node.parent) win = cast(window.Internal, parent_tree.node.data) return win, None, sx, sy if node.type == SceneNodeType.RECT: # Rect nodes are only used for window borders. Their immediate parent is the # window container, which gives us the window at .data. # We have to differentiate between internal windows and normal windows parent_tree = cast(SceneTree, node.parent) if isinstance(parent_tree.node.data, window.Internal): win = cast(window.Internal, parent_tree.node.data) else: win = cast(window.Window, parent_tree.node.data) return win, None, sx, sy logger.warning("Couldn't determine what was under the pointer. Please report.") return None def check_idle_inhibitor(self) -> None: """ Checks if any window that is currently mapped has idle inhibitor and if so inhibits idle """ assert self.qtile is not None for win in self.qtile.windows_map.values(): if not isinstance(win, window.Internal) and win.is_idle_inhibited: # TODO: do we also need to check that the window is mapped? self.idle.set_inhibited(True) return self.idle.set_inhibited(False) def get_screen_info(self) -> list[ScreenRect]: """Get the output information""" return [output.get_screen_info() for output in self.get_enabled_outputs()] def _get_sym_from_code(self, keycode: int) -> str: keyboard = self.active_keyboard if keyboard is None: raise QtileError("Unable to grab keycode. No active keyboard found.") return keyboard._state.key_get_one_sym(keycode) def grab_key(self, key: config.Key | config.KeyChord) -> tuple[int, int]: """Configure the backend to grab the key event""" if isinstance(key.key, str): keysym = xkb.keysym_from_name(key.key, case_insensitive=True) else: keysym = self._get_sym_from_code(key.key) mask_key = wlrq.translate_masks(key.modifiers) self.grabbed_keys.append((keysym, mask_key)) return keysym, mask_key def ungrab_key(self, key: config.Key | config.KeyChord) -> tuple[int, int]: """Release the given key event""" if isinstance(key.key, str): keysym = xkb.keysym_from_name(key.key, case_insensitive=True) else: keysym = self._get_sym_from_code(key.key) mask_key = wlrq.translate_masks(key.modifiers) self.grabbed_keys.remove((keysym, mask_key)) return keysym, mask_key def ungrab_keys(self) -> None: """Release the grabbed key events""" self.grabbed_keys.clear() def grab_button(self, mouse: config.Mouse) -> int: """Configure the backend to grab the mouse event""" return wlrq.translate_masks(mouse.modifiers) def warp_pointer(self, x: float, y: float) -> None: """Warp the pointer to the coordinates in relative to the output layout""" self.cursor.warp(WarpMode.LayoutClosest, x, y) @contextlib.contextmanager def masked(self) -> Generator: yield self._focus_pointer(int(self.cursor.x), int(self.cursor.y)) def flush(self) -> None: self._poll() def create_internal(self, x: int, y: int, width: int, height: int) -> base.Internal: assert self.qtile is not None internal = window.Internal(self, self.qtile, x, y, width, height) self.qtile.manage(internal) return internal def graceful_shutdown(self) -> None: """Try to close windows gracefully before exiting""" assert self.qtile is not None # Copy in case the dictionary changes during the loop for win in self.qtile.windows_map.copy().values(): win.kill() # give everyone a little time to exit and write their state. but don't # sleep forever (1s). end = time.time() + 1 while time.time() < end: self._poll() if not self.qtile.windows_map: break @property def painter(self) -> Any: return wlrq.Painter(self) def remove_output(self, output: Output) -> None: # already removed if output not in self._outputs: return self._outputs.remove(output) box = self.output_layout.get_box(output.wlr_output) if not lib.wlr_box_empty(box._ptr): self.output_layout.remove(output.wlr_output) if output is self._current_output: en_outputs = self.get_enabled_outputs() self._current_output = en_outputs[0] if en_outputs else None def remove_pointer_constraints(self, window: window.Window | window.Static) -> None: for pc in self.pointer_constraints.copy(): if pc.window is window: pc.finalize() def keysym_from_name(self, name: str) -> int: """Get the keysym for a key from its name""" return xkb.keysym_from_name(name, case_insensitive=True) def simulate_keypress(self, modifiers: list[str], key: str) -> None: """Simulates a keypress on the focused window.""" keysym = xkb.keysym_from_name(key, case_insensitive=True) mods = wlrq.translate_masks(modifiers) if (keysym, mods) in self.grabbed_keys: assert self.qtile is not None self.qtile.process_key_event(keysym, mods) return if self.focused_internal: self.focused_internal.process_key_press(keysym) @expose_command() def set_keymap( self, layout: str | None = None, options: str | None = None, variant: str | None = None, ) -> None: """ Set the keymap for the current keyboard. The options correspond to xkbcommon configuration environmental variables and if not specified are taken from the environment. Acceptable values are strings identical to those accepted by the env variables. """ if self.keyboards: for keyboard in self.keyboards: keyboard.set_keymap(layout, options, variant) else: logger.warning("Could not set keymap: no keyboards set up.") @expose_command() def change_vt(self, vt: int) -> bool: """Change virtual terminal to that specified""" success = self.backend.get_session().change_vt(vt) if not success: logger.warning("Could not change VT to: %s", vt) return success @expose_command() def hide_cursor(self) -> None: """Hide the cursor.""" if not self._cursor_state.hidden: self.cursor.set_surface(None, self._cursor_state.hotspot) self._cursor_state.hidden = True @expose_command() def unhide_cursor(self) -> None: """Unhide the cursor.""" if self._cursor_state.hidden: self.cursor.set_surface( self._cursor_state.surface, self._cursor_state.hotspot, ) self._cursor_state.hidden = False @expose_command() def get_inputs(self) -> dict[str, list[dict[str, str]]]: """Get information on all input devices.""" info: defaultdict[str, list[dict]] = defaultdict(list) devices: list[inputs._Device] = self.keyboards + self._pointers # type: ignore for dev in devices: type_key, identifier = dev.get_info() type_info = dict( name=dev.wlr_device.name, identifier=identifier, ) info[type_key].append(type_info) return dict(info) @expose_command() def query_tree(self) -> list[int]: """Get IDs of all mapped windows in ascending Z order.""" wids = [] def iterator(buffer: SceneBuffer, _sx: int, _sy: int, _data: None) -> None: # Walk back up tree until we find a window or run out of parents node = buffer.node while True: if win := node.data: if node.enabled: # TODO does this need to check the container node rather than # three node within it? wids.append(win.wid) return parent = node.parent if not parent: return node = parent.node self.scene.tree.node.for_each_buffer(iterator, None) return wids def get_mouse_position(self) -> tuple[int, int]: """Get mouse coordinates.""" return int(self.cursor.x), int(self.cursor.y) @property def hovered_window(self) -> base.WindowType | None: return self._hovered_window qtile-0.31.0/libqtile/backend/wayland/cffi/0000775000175000017500000000000014762660347020400 5ustar epsilonepsilonqtile-0.31.0/libqtile/backend/wayland/cffi/build.py0000664000175000017500000000130614762660347022051 0ustar epsilonepsilonimport os import wlroots.ffi_build as wlr from cffi import FFI from libqtile.backend.wayland.cffi import cairo_buffer, libinput SOURCE = "\n".join( [ wlr.SOURCE, libinput.SOURCE, cairo_buffer.SOURCE, ] ) ffi = FFI() ffi.set_source( "libqtile.backend.wayland._ffi", SOURCE, libraries=["wlroots", "input"], define_macros=[("WLR_USE_UNSTABLE", None)], include_dirs=[ os.getenv("QTILE_PIXMAN_PATH", "/usr/include/pixman-1"), os.getenv("QTILE_LIBDRM_PATH", "/usr/include/libdrm"), wlr.include_dir, ], ) ffi.include(wlr.ffi_builder) ffi.cdef(libinput.CDEF) ffi.cdef(cairo_buffer.CDEF) if __name__ == "__main__": ffi.compile() qtile-0.31.0/libqtile/backend/wayland/cffi/cairo_buffer.py0000664000175000017500000000305114762660347023377 0ustar epsilonepsilon""" CFFI definitions for implementing wlr_buffer with cairo surfaces """ SOURCE = """ #include #include struct cairo_buffer { struct wlr_buffer base; void *data; size_t stride; }; static void handle_destroy(struct wlr_buffer *wlr_buffer) { struct cairo_buffer *buffer = wl_container_of(wlr_buffer, buffer, base); free(buffer); } static bool handle_begin_data_ptr_access(struct wlr_buffer *wlr_buffer, uint32_t flags, void **data, uint32_t *format, size_t *stride) { struct cairo_buffer *buffer = wl_container_of(wlr_buffer, buffer, base); *data = buffer->data; *stride = buffer->stride; *format = DRM_FORMAT_ARGB8888; return true; } static void handle_end_data_ptr_access(struct wlr_buffer *wlr_buffer) { // This space is intentionally left blank } static const struct wlr_buffer_impl cairo_buffer_impl = { .destroy = handle_destroy, .begin_data_ptr_access = handle_begin_data_ptr_access, .end_data_ptr_access = handle_end_data_ptr_access, }; struct wlr_buffer *cairo_buffer_create(int width, int height, size_t stride, void *data) { struct cairo_buffer *cairo_buffer = calloc(1, sizeof(struct cairo_buffer)); if (cairo_buffer == NULL) { return NULL; } wlr_buffer_init(&cairo_buffer->base, &cairo_buffer_impl, width, height); cairo_buffer->data = data; cairo_buffer->stride = stride; return &cairo_buffer->base; } """ CDEF = """ struct wlr_buffer *cairo_buffer_create(int width, int height, size_t stride, void *data); """ qtile-0.31.0/libqtile/backend/wayland/cffi/libinput.py0000664000175000017500000001025614762660347022604 0ustar epsilonepsilon""" CFFI definitions for interfacing with libinput """ SOURCE = """ #include """ CDEF = """ enum libinput_config_status { LIBINPUT_CONFIG_STATUS_SUCCESS = 0, LIBINPUT_CONFIG_STATUS_UNSUPPORTED, LIBINPUT_CONFIG_STATUS_INVALID, }; int libinput_device_config_tap_get_finger_count(struct libinput_device *device); enum libinput_config_tap_state { LIBINPUT_CONFIG_TAP_DISABLED, LIBINPUT_CONFIG_TAP_ENABLED, }; enum libinput_config_status libinput_device_config_tap_set_enabled(struct libinput_device *device, enum libinput_config_tap_state enable); enum libinput_config_tap_button_map { LIBINPUT_CONFIG_TAP_MAP_LRM, LIBINPUT_CONFIG_TAP_MAP_LMR, }; enum libinput_config_status libinput_device_config_tap_set_button_map(struct libinput_device *device, enum libinput_config_tap_button_map map); enum libinput_config_drag_state { LIBINPUT_CONFIG_DRAG_DISABLED, LIBINPUT_CONFIG_DRAG_ENABLED, }; enum libinput_config_status libinput_device_config_tap_set_drag_enabled(struct libinput_device *device, enum libinput_config_drag_state enable); enum libinput_config_drag_lock_state { LIBINPUT_CONFIG_DRAG_LOCK_DISABLED, LIBINPUT_CONFIG_DRAG_LOCK_ENABLED, }; enum libinput_config_status libinput_device_config_tap_set_drag_lock_enabled(struct libinput_device *device, enum libinput_config_drag_lock_state enable); int libinput_device_config_accel_is_available(struct libinput_device *device); enum libinput_config_status libinput_device_config_accel_set_speed(struct libinput_device *device, double speed); enum libinput_config_accel_profile { LIBINPUT_CONFIG_ACCEL_PROFILE_NONE = 0, LIBINPUT_CONFIG_ACCEL_PROFILE_FLAT = (1 << 0), LIBINPUT_CONFIG_ACCEL_PROFILE_ADAPTIVE = (1 << 1), }; enum libinput_config_status libinput_device_config_accel_set_profile(struct libinput_device *device, enum libinput_config_accel_profile profile); int libinput_device_config_scroll_has_natural_scroll(struct libinput_device *device); enum libinput_config_status libinput_device_config_scroll_set_natural_scroll_enabled(struct libinput_device *device, int enable); int libinput_device_config_left_handed_is_available(struct libinput_device *device); enum libinput_config_status libinput_device_config_left_handed_set(struct libinput_device *device, int left_handed); enum libinput_config_click_method { LIBINPUT_CONFIG_CLICK_METHOD_NONE = 0, LIBINPUT_CONFIG_CLICK_METHOD_BUTTON_AREAS = (1 << 0), LIBINPUT_CONFIG_CLICK_METHOD_CLICKFINGER = (1 << 1), }; enum libinput_config_status libinput_device_config_click_set_method(struct libinput_device *device, enum libinput_config_click_method method); enum libinput_config_middle_emulation_state { LIBINPUT_CONFIG_MIDDLE_EMULATION_DISABLED, LIBINPUT_CONFIG_MIDDLE_EMULATION_ENABLED, }; enum libinput_config_status libinput_device_config_middle_emulation_set_enabled( struct libinput_device *device, enum libinput_config_middle_emulation_state enable); enum libinput_config_scroll_method { LIBINPUT_CONFIG_SCROLL_NO_SCROLL = 0, LIBINPUT_CONFIG_SCROLL_2FG = (1 << 0), LIBINPUT_CONFIG_SCROLL_EDGE = (1 << 1), LIBINPUT_CONFIG_SCROLL_ON_BUTTON_DOWN = (1 << 2), }; enum libinput_config_status libinput_device_config_scroll_set_method(struct libinput_device *device, enum libinput_config_scroll_method method); enum libinput_config_status libinput_device_config_scroll_set_button(struct libinput_device *device, uint32_t button); enum libinput_config_dwt_state { LIBINPUT_CONFIG_DWT_DISABLED, LIBINPUT_CONFIG_DWT_ENABLED, }; int libinput_device_config_dwt_is_available(struct libinput_device *device); enum libinput_config_status libinput_device_config_dwt_set_enabled(struct libinput_device *device, enum libinput_config_dwt_state enable); """ qtile-0.31.0/libqtile/backend/wayland/xdgwindow.py0000664000175000017500000003656714762660347022076 0ustar epsilonepsilon# Copyright (c) 2021 Matt Colligan # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations import typing from pywayland.server import Listener from wlroots.util.box import Box from wlroots.util.clock import Timespec from wlroots.util.edges import Edges from wlroots.wlr_types.xdg_shell import XdgSurface, XdgToplevelWMCapabilities from libqtile import hook from libqtile.backend import base from libqtile.backend.base import FloatStates from libqtile.backend.wayland.window import Static, Window from libqtile.command.base import expose_command from libqtile.log_utils import logger try: # Continue if ffi not built, so that docs can be built without wayland deps. from libqtile.backend.wayland._ffi import ffi, lib except ModuleNotFoundError: pass if typing.TYPE_CHECKING: from typing import Any from libqtile.backend.wayland.core import Core from libqtile.core.manager import Qtile from libqtile.utils import ColorsType EDGES_TILED = Edges.TOP | Edges.BOTTOM | Edges.LEFT | Edges.RIGHT WM_CAPABILITIES = ( XdgToplevelWMCapabilities.MAXIMIZE | XdgToplevelWMCapabilities.FULLSCREEN | XdgToplevelWMCapabilities.MINIMIZE ) class XdgWindow(Window[XdgSurface]): """An Wayland client connecting via the xdg shell.""" def __init__(self, core: Core, qtile: Qtile, surface: XdgSurface): Window.__init__(self, core, qtile, surface) self._wm_class = surface.toplevel.app_id self._geom = Box(0, 0, 0, 0) surface.set_wm_capabilities(WM_CAPABILITIES) surface.data = self.data_handle self.tree = core.scene.xdg_surface_create(self.container, surface) self.add_listener(surface.surface.map_event, self._on_map) self.add_listener(surface.surface.unmap_event, self._on_unmap) self.add_listener(surface.surface.commit_event, self._on_commit) self.add_listener(surface.destroy_event, self._on_destroy) self.add_listener(surface.toplevel.request_maximize_event, self._on_request_maximize) self.add_listener(surface.toplevel.request_fullscreen_event, self._on_request_fullscreen) self.ftm_handle = core.foreign_toplevel_manager_v1.create_handle() def _on_commit(self, _listener: Listener, _data: Any) -> None: if self not in self.core.pending_windows and self.container.node.enabled: self.place( self.x, self.y, self.width, self.height, self.borderwidth, self.bordercolor ) def _on_request_fullscreen(self, _listener: Listener, _data: Any) -> None: logger.debug("Signal: xdgwindow request_fullscreen") if self.qtile.config.auto_fullscreen: requested = self.surface.toplevel.requested.fullscreen if self.fullscreen == requested: self.surface.schedule_configure() else: self.fullscreen = requested else: # Per xdg-shell protocol we must send a configure in response to this # request. Since we're ignoring it, we must schedule a configure manually. self.surface.schedule_configure() def _on_request_maximize(self, _listener: Listener, _data: Any) -> None: logger.debug("Signal: xdgwindow request_maximize") self.maximized = self.surface.toplevel.requested.maximized def _on_set_title(self, _listener: Listener, _data: Any) -> None: logger.debug("Signal: xdgwindow set_title") title = self.surface.toplevel.title if title and title != self.name: self.name = title if self.ftm_handle: self.ftm_handle.set_title(self.name) hook.fire("client_name_updated", self) def _on_set_app_id(self, _listener: Listener, _data: Any) -> None: logger.debug("Signal: xdgwindow set_app_id") self._wm_class = self.surface.toplevel.app_id if self.ftm_handle: self.ftm_handle.set_app_id(self._wm_class or "") def unhide(self) -> None: self._wm_class = self.surface.toplevel.app_id if self not in self.core.pending_windows: # Regular usage if not self.container.node.enabled and self.group and self.group.screen: self.container.node.set_enabled(enabled=True) return # This is the first time this window has mapped, so we need to do some initial # setup. self.core.pending_windows.remove(self) self._wid = self.core.new_wid() logger.debug( "Managing new top-level window with window ID: %s, app_id: %s", self._wid, self._wm_class, ) # Save the client's desired geometry surface = self.surface geometry = surface.get_geometry() self._width = self._float_width = geometry.width self._height = self._float_height = geometry.height # Tell the client to render tiled edges surface.set_tiled(EDGES_TILED) handle = self.ftm_handle assert handle is not None # Get the client's name if surface.toplevel.title: self.name = surface.toplevel.title handle.set_title(self.name) if self._wm_class: handle.set_app_id(self._wm_class or "") # Add the toplevel's listeners self.add_listener(surface.toplevel.set_title_event, self._on_set_title) self.add_listener(surface.toplevel.set_app_id_event, self._on_set_app_id) self.add_listener(handle.request_maximize_event, self._on_foreign_request_maximize) self.add_listener(handle.request_minimize_event, self._on_foreign_request_minimize) self.add_listener(handle.request_activate_event, self._on_foreign_request_activate) self.add_listener(handle.request_fullscreen_event, self._on_foreign_request_fullscreen) self.add_listener(handle.request_close_event, self._on_foreign_request_close) self.qtile.manage(self) # Send a frame done event to provide an opportunity to redraw even if we aren't # going to map (i.e. because the window was opened on a hidden group) surface.surface.send_frame_done(Timespec.get_monotonic_time()) @expose_command() def kill(self) -> None: self.surface.send_close() def has_fixed_size(self) -> bool: state = self.surface.toplevel._ptr.current return 0 < state.min_width == state.max_width and 0 < state.min_height == state.max_height def is_transient_for(self) -> base.WindowType | None: """What window is this window a transient window for?""" if parent := self.surface.toplevel.parent: for win in self.qtile.windows_map.values(): if isinstance(win, XdgWindow) and win.surface.toplevel == parent: return win return None def get_pid(self) -> int: pid = ffi.new("pid_t *") lib.wl_client_get_credentials(self.surface._ptr.client.client, pid, ffi.NULL, ffi.NULL) return pid[0] def _update_fullscreen(self, do_full: bool) -> None: if do_full != (self._float_state == FloatStates.FULLSCREEN): self.surface.set_fullscreen(do_full) if self.ftm_handle: self.ftm_handle.set_fullscreen(do_full) def handle_activation_request(self, focus_on_window_activation: str) -> None: """Respond to XDG activation requests targeting this window.""" assert self.qtile is not None if self.group is None: # Likely still pending, ignore this request. return if focus_on_window_activation == "focus": logger.debug("Focusing window (focus_on_window_activation='focus')") self.qtile.current_screen.set_group(self.group) self.group.focus(self) elif focus_on_window_activation == "smart": if not self.group.screen: logger.debug("Ignoring focus request (focus_on_window_activation='smart')") elif self.group.screen == self.qtile.current_screen: logger.debug("Focusing window (focus_on_window_activation='smart')") self.qtile.current_screen.set_group(self.group) self.group.focus(self) else: self._urgent = True hook.fire("client_urgent_hint_changed", self) elif focus_on_window_activation == "urgent": self._urgent = True hook.fire("client_urgent_hint_changed", self) def clip(self) -> None: if not self.tree: return if not self.tree.node.enabled: return if next(self.tree.children, None) is None: return self.tree.node.subsurface_tree_set_clip( Box(self._geom.x, self._geom.y, self.width, self.height) ) def place( self, x: int, y: int, width: int, height: int, borderwidth: int, bordercolor: ColorsType | None, above: bool = False, margin: int | list[int] | None = None, respect_hints: bool = False, ) -> None: # Adjust the placement to account for layout margins, if there are any. if margin is not None: if isinstance(margin, int): margin = [margin] * 4 x += margin[3] y += margin[0] width -= margin[1] + margin[3] height -= margin[0] + margin[2] state = self.surface.toplevel._ptr.current if respect_hints: width = max(width, state.min_width) height = max(height, state.min_height) if state.max_width: width = min(width, state.max_width) if state.max_height: height = min(height, state.max_height) # save x and y float offset if self.group is not None and self.group.screen is not None: self.float_x = x - self.group.screen.x self.float_y = y - self.group.screen.y if width < 1: width = 1 if height < 1: height = 1 geom = self.surface.get_geometry() geom_changed = any( [ self._geom.x != geom.x, self._geom.y != geom.y, self._geom.width != geom.width, self._geom.height != geom.height, ] ) place_changed = any( [ self.x != x, self.y != y, self._width != width, self._height != height, state.width != width, state.height != height, ] ) needs_repos = place_changed or geom_changed has_border_changed = any( [borderwidth != self.borderwidth, bordercolor != self.bordercolor] ) self._geom = geom self.x = x self.y = y self._width = width self._height = height self.container.node.set_position(x, y) if needs_repos: self.surface.set_size(width, height) self.surface.set_bounds(width, height) self.clip() if needs_repos or has_border_changed: self.paint_borders(bordercolor, borderwidth) if above: self.bring_to_front() @expose_command() def static( self, screen: int | None = None, x: int | None = None, y: int | None = None, width: int | None = None, height: int | None = None, ) -> None: Window.static(self, screen, x, y, width, height) win = typing.cast(XdgStatic, self.qtile.windows_map[self._wid]) for pc in self.core.pointer_constraints.copy(): if pc.window is self: pc.window = win hook.fire("client_managed", win) def _to_static( self, x: int | None, y: int | None, width: int | None, height: int | None ) -> XdgStatic: return XdgStatic( self.core, self.qtile, self, self._idle_inhibitors_count, ) class XdgStatic(Static[XdgSurface]): """A static window belonging to the XDG shell.""" def __init__( self, core: Core, qtile: Qtile, win: XdgWindow, idle_inhibitor_count: int, ): surface = win.surface Static.__init__( self, core, qtile, surface, win.wid, idle_inhibitor_count=idle_inhibitor_count ) if surface.toplevel.title: self.name = surface.toplevel.title self._wm_class = surface.toplevel.app_id self.add_listener(surface.surface.map_event, self._on_map) self.add_listener(surface.surface.unmap_event, self._on_unmap) self.add_listener(surface.destroy_event, self._on_destroy) # self.add_listener(surface.surface.commit_event, self._on_commit) self.add_listener(surface.toplevel.set_title_event, self._on_set_title) self.add_listener(surface.toplevel.set_app_id_event, self._on_set_app_id) # Take control of the scene tree self.container = win.container self.container.node.data = self.data_handle self.tree = win.tree @expose_command() def kill(self) -> None: self.surface.send_close() def hide(self) -> None: super().hide() self.container.node.set_enabled(enabled=False) def unhide(self) -> None: self.container.node.set_enabled(enabled=True) def place( self, x: int, y: int, width: int, height: int, borderwidth: int, bordercolor: ColorsType | None, above: bool = False, margin: int | list[int] | None = None, respect_hints: bool = False, ) -> None: self.x = x self.y = y self._width = width self._height = height self.surface.set_size(width, height) self.surface.set_bounds(width, height) self.container.node.set_position(x, y) def _on_set_title(self, _listener: Listener, _data: Any) -> None: logger.debug("Signal: xdgstatic set_title") title = self.surface.toplevel.title if title and title != self.name: self.name = title if self.ftm_handle: self.ftm_handle.set_title(self.name) hook.fire("client_name_updated", self) def _on_set_app_id(self, _listener: Listener, _data: Any) -> None: logger.debug("Signal: xdgstatic set_app_id") self._wm_class = self.surface.toplevel.app_id if self.ftm_handle: self.ftm_handle.set_app_id(self._wm_class or "") qtile-0.31.0/libqtile/backend/__init__.py0000664000175000017500000000202014762660347020135 0ustar epsilonepsilonfrom __future__ import annotations import importlib import importlib.util from typing import TYPE_CHECKING from libqtile.utils import QtileError if TYPE_CHECKING: from typing import Any from libqtile.backend.base import Core CORES = { "wayland": ("wlroots",), "x11": ("xcffib",), } def has_deps(backend: str) -> list[str]: """ Check if the backend has all its dependencies installed. Args: backend: The backend to check. Returns: A list of missing dependencies. If this is empty, we can use this backend. """ if backend not in CORES: raise QtileError(f"Backend {backend} does not exist") not_found = [] for dep in CORES[backend]: if not importlib.util.find_spec(dep): not_found.append(dep) return not_found def get_core(backend: str, *args: Any) -> Core: if backend not in CORES: raise QtileError(f"Backend {backend} does not exist") return importlib.import_module(f"libqtile.backend.{backend}.core").Core(*args) qtile-0.31.0/libqtile/backend/base/0000775000175000017500000000000014762660347016744 5ustar epsilonepsilonqtile-0.31.0/libqtile/backend/base/__init__.py0000664000175000017500000000037214762660347021057 0ustar epsilonepsilonfrom libqtile.backend.base.core import Core # noqa: F401 from libqtile.backend.base.drawer import Drawer # noqa: F401 from libqtile.backend.base.window import ( # noqa: F401 FloatStates, Internal, Static, Window, WindowType, ) qtile-0.31.0/libqtile/backend/base/drawer.py0000664000175000017500000004254514762660347020614 0ustar epsilonepsilon# Copyright (c) 2010 Aldo Cortesi # Copyright (c) 2011 Florian Mounier # Copyright (c) 2011 oitel # Copyright (c) 2011 Kenji_Takahashi # Copyright (c) 2011 Paul Colomiets # Copyright (c) 2012, 2014 roger # Copyright (c) 2012 nullzion # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014-2015 Sean Vig # Copyright (c) 2014 Nathan Hoad # Copyright (c) 2014 dequis # Copyright (c) 2014 Tycho Andersen # Copyright (c) 2020, 2021 Robert Andrew Ditthardt # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations import collections import math import typing import cairocffi from libqtile import pangocffi, utils if typing.TYPE_CHECKING: from libqtile.backend.base import Internal from libqtile.core.manager import Qtile from libqtile.utils import ColorsType class Drawer: """A helper class for drawing to Internal windows. We stage drawing operations locally in memory using a cairo RecordingSurface before finally drawing all operations to a backend-specific target. """ def __init__(self, qtile: Qtile, win: Internal, width: int, height: int): self.qtile = qtile self._win = win self._width = width self._height = height self.surface: cairocffi.RecordingSurface self.last_surface: cairocffi.RecordingSurface self.ctx: cairocffi.Context self._reset_surface() self._has_mirrors = False self._enabled = True def finalize(self): """Destructor/Clean up resources""" if hasattr(self, "surface"): self.surface.finish() delattr(self, "surface") if hasattr(self, "last_surface"): self.last_surface.finish() delattr(self, "last_surface") self.ctx = None @property def has_mirrors(self): return self._has_mirrors @has_mirrors.setter def has_mirrors(self, value): if value and not self._has_mirrors: self._create_last_surface() self._has_mirrors = value @property def width(self) -> int: return self._width @width.setter def width(self, width: int): self._width = width @property def height(self) -> int: return self._height @height.setter def height(self, height: int): self._height = height def _reset_surface(self): """This creates a fresh surface and cairo context.""" if hasattr(self, "surface"): self.surface.finish() self.surface = cairocffi.RecordingSurface( cairocffi.CONTENT_COLOR_ALPHA, None, ) self.ctx = self.new_ctx() def _create_last_surface(self): """Creates a separate RecordingSurface for mirrors to access.""" if hasattr(self, "last_surface"): self.last_surface.finish() self.last_surface = cairocffi.RecordingSurface(cairocffi.CONTENT_COLOR_ALPHA, None) def paint_to(self, drawer: Drawer) -> None: drawer.ctx.set_source_surface(self.last_surface) drawer.ctx.paint() def _rounded_rect(self, x, y, width, height, linewidth): aspect = 1.0 corner_radius = height / 10.0 radius = corner_radius / aspect degrees = math.pi / 180.0 self.ctx.new_sub_path() delta = radius + linewidth / 2 self.ctx.arc(x + width - delta, y + delta, radius, -90 * degrees, 0 * degrees) self.ctx.arc(x + width - delta, y + height - delta, radius, 0 * degrees, 90 * degrees) self.ctx.arc(x + delta, y + height - delta, radius, 90 * degrees, 180 * degrees) self.ctx.arc(x + delta, y + delta, radius, 180 * degrees, 270 * degrees) self.ctx.close_path() def rounded_rectangle(self, x: int, y: int, width: int, height: int, linewidth: int): self._rounded_rect(x, y, width, height, linewidth) self.ctx.set_line_width(linewidth) self.ctx.stroke() def rounded_fillrect(self, x: int, y: int, width: int, height: int, linewidth: int): self._rounded_rect(x, y, width, height, linewidth) self.ctx.fill() def rectangle(self, x: int, y: int, width: int, height: int, linewidth: int = 2): self.ctx.set_line_width(linewidth) self.ctx.rectangle(x, y, width, height) self.ctx.stroke() def fillrect(self, x: int, y: int, width: int, height: int, linewidth: int = 2): self.ctx.set_line_width(linewidth) self.ctx.rectangle(x, y, width, height) self.ctx.fill() self.ctx.stroke() def enable(self): """Enable drawing of surface to Internal window.""" self._enabled = True def disable(self): """Disable drawing of surface to Internal window.""" self._enabled = False def draw( self, offsetx: int = 0, offsety: int = 0, width: int | None = None, height: int | None = None, src_x: int = 0, src_y: int = 0, ): """ A wrapper for the draw operation. This draws our cached operations to the Internal window. If Drawer has been disabled then the RecordingSurface will be cleared if no mirrors are waiting to copy its contents. Parameters ========== offsetx : the X offset to start drawing at. offsety : the Y offset to start drawing at. width : the X portion of the canvas to draw at the starting point. height : the Y portion of the canvas to draw at the starting point. src_x : the X position of the origin in the source surface src_y : the Y position of the origin in the source surface """ if self._enabled: self._draw( offsetx=offsetx, offsety=offsety, width=width, height=height, src_x=src_x, src_y=src_y, ) if self.has_mirrors: self._create_last_surface() ctx = cairocffi.Context(self.last_surface) ctx.set_source_surface(self.surface) ctx.paint() self._reset_surface() def _draw( self, offsetx: int = 0, offsety: int = 0, width: int | None = None, height: int | None = None, src_x: int = 0, src_y: int = 0, ): """ This draws our cached operations to the Internal window. Parameters ========== offsetx : the X offset to start drawing at. offsety : the Y offset to start drawing at. width : the X portion of the canvas to draw at the starting point. height : the Y portion of the canvas to draw at the starting point. src_x : the X position of the origin in the source surface src_y : the Y position of the origin in the source surface """ def new_ctx(self): return pangocffi.patch_cairo_context(cairocffi.Context(self.surface)) def set_source_rgb(self, colour: ColorsType, ctx: cairocffi.Context | None = None): # If an alternate context is not provided then we draw to the # drawer's default context if ctx is None: ctx = self.ctx if isinstance(colour, list): if len(colour) == 0: # defaults to black ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) elif len(colour) == 1: ctx.set_source_rgba(*utils.rgb(colour[0])) else: linear = cairocffi.LinearGradient(0.0, 0.0, 0.0, self.height) step_size = 1.0 / (len(colour) - 1) step = 0.0 for c in colour: linear.add_color_stop_rgba(step, *utils.rgb(c)) step += step_size ctx.set_source(linear) else: ctx.set_source_rgba(*utils.rgb(colour)) def clear_rect(self, x=0, y=0, width=0, height=0): """ Erases the background area specified by parameters. By default, the whole Drawer is cleared. The ability to clear a smaller area may be useful when you want to erase a smaller area of the drawer (e.g. drawing widget decorations). """ if width <= 0: width = self.width if height <= 0: height = self.height self.ctx.save() self.ctx.set_operator(cairocffi.OPERATOR_CLEAR) self.ctx.rectangle(x, y, width, height) self.ctx.fill() self.ctx.restore() def clear(self, colour): """Clears background of the Drawer and fills with specified colour.""" if self.ctx is None: self._reset_surface() self.ctx.save() # Erase the background self.clear_rect() # Fill drawer with new colour self.ctx.set_operator(cairocffi.OPERATOR_SOURCE) self.set_source_rgb(colour) self.ctx.rectangle(0, 0, self.width, self.height) self.ctx.fill() self.ctx.restore() def textlayout(self, text, colour, font_family, font_size, font_shadow, markup=False, **kw): """Get a text layout""" textlayout = TextLayout( self, text, colour, font_family, font_size, font_shadow, markup=markup, **kw ) return textlayout def max_layout_size(self, texts, font_family, font_size, markup=False): sizelayout = self.textlayout("", "ffffff", font_family, font_size, None, markup=markup) widths, heights = [], [] for i in texts: sizelayout.text = i widths.append(sizelayout.width) heights.append(sizelayout.height) return max(widths), max(heights) def text_extents(self, text): return self.ctx.text_extents(utils.scrub_to_utf8(text)) def font_extents(self): return self.ctx.font_extents() def fit_fontsize(self, heightlimit): """Try to find a maximum font size that fits any strings within the height""" self.ctx.set_font_size(heightlimit) asc, desc, height, _, _ = self.font_extents() self.ctx.set_font_size(int(heightlimit * heightlimit / height)) return self.font_extents() def fit_text(self, strings, heightlimit): """Try to find a maximum font size that fits all strings within the height""" self.ctx.set_font_size(heightlimit) _, _, _, maxheight, _, _ = self.ctx.text_extents("".join(strings)) if not maxheight: return 0, 0 self.ctx.set_font_size(int(heightlimit * heightlimit / maxheight)) maxwidth, maxheight = 0, 0 for i in strings: _, _, x, y, _, _ = self.ctx.text_extents(i) maxwidth = max(maxwidth, x) maxheight = max(maxheight, y) return maxwidth, maxheight def draw_vbar(self, color, x, y1, y2, linewidth=1): self.set_source_rgb(color) self.ctx.move_to(x, y1) self.ctx.line_to(x, y2) self.ctx.set_line_width(linewidth) self.ctx.stroke() def draw_hbar(self, color, x1, x2, y, linewidth=1): self.set_source_rgb(color) self.ctx.move_to(x1, y) self.ctx.line_to(x2, y) self.ctx.set_line_width(linewidth) self.ctx.stroke() class TextLayout: def __init__( self, drawer, text, colour, font_family, font_size, font_shadow, wrap=True, markup=False ): self.drawer, self.colour = drawer, colour layout = drawer.ctx.create_layout() layout.set_alignment(pangocffi.ALIGN_CENTER) if not wrap: # pango wraps by default layout.set_ellipsize(pangocffi.ELLIPSIZE_END) desc = pangocffi.FontDescription.from_string(font_family) desc.set_absolute_size(pangocffi.units_from_double(float(font_size))) layout.set_font_description(desc) self.font_shadow = font_shadow self.layout = layout self.markup = markup self.text = text self._width = None def finalize(self): self.layout.finalize() def finalized(self): self.layout.finalized() @property def text(self): return self.layout.get_text() @text.setter def text(self, value): if self.markup: # pangocffi doesn't like None here, so we use "". if value is None: value = "" attrlist, value, accel_char = pangocffi.parse_markup(value) self.layout.set_attributes(attrlist) self.layout.set_text(utils.scrub_to_utf8(value)) @property def width(self): if self._width is not None: return self._width else: return self.layout.get_pixel_size()[0] @width.setter def width(self, value): self._width = value self.layout.set_width(pangocffi.units_from_double(value)) @width.deleter def width(self): self._width = None self.layout.set_width(-1) @property def height(self): return self.layout.get_pixel_size()[1] def fontdescription(self): return self.layout.get_font_description() @property def font_family(self): d = self.fontdescription() return d.get_family() @font_family.setter def font_family(self, font): d = self.fontdescription() d.set_family(font) self.layout.set_font_description(d) @property def font_size(self): d = self.fontdescription() return d.get_size() @font_size.setter def font_size(self, size): d = self.fontdescription() d.set_size(size) d.set_absolute_size(pangocffi.units_from_double(size)) self.layout.set_font_description(d) def draw(self, x, y): if self.font_shadow is not None: self.drawer.set_source_rgb(self.font_shadow) self.drawer.ctx.move_to(x + 1, y + 1) self.drawer.ctx.show_layout(self.layout) self.drawer.set_source_rgb(self.colour) self.drawer.ctx.move_to(x, y) self.drawer.ctx.show_layout(self.layout) def framed(self, border_width, border_color, pad_x, pad_y, highlight_color=None): return TextFrame( self, border_width, border_color, pad_x, pad_y, highlight_color=highlight_color ) class TextFrame: def __init__(self, layout, border_width, border_color, pad_x, pad_y, highlight_color=None): self.layout = layout self.border_width = border_width self.border_color = border_color self.drawer = self.layout.drawer self.highlight_color = highlight_color if isinstance(pad_x, collections.abc.Iterable): self.pad_left = pad_x[0] self.pad_right = pad_x[1] else: self.pad_left = self.pad_right = pad_x if isinstance(pad_y, collections.abc.Iterable): self.pad_top = pad_y[0] self.pad_bottom = pad_y[1] else: self.pad_top = self.pad_bottom = pad_y def draw(self, x, y, rounded=True, fill=False, line=False, highlight=False): self.drawer.set_source_rgb(self.border_color) opts = [ x, y, self.layout.width + self.pad_left + self.pad_right, self.layout.height + self.pad_top + self.pad_bottom, self.border_width, ] if line: if highlight: self.drawer.set_source_rgb(self.highlight_color) self.drawer.fillrect(*opts) self.drawer.set_source_rgb(self.border_color) # change to only fill in bottom line opts[1] = self.height - self.border_width # y opts[3] = self.border_width # height self.drawer.fillrect(*opts) elif fill: if rounded: self.drawer.rounded_fillrect(*opts) else: self.drawer.fillrect(*opts) else: if rounded: self.drawer.rounded_rectangle(*opts) else: self.drawer.rectangle(*opts) self.drawer.ctx.stroke() self.layout.draw(x + self.pad_left, y + self.pad_top) def draw_fill(self, x, y, rounded=True): self.draw(x, y, rounded=rounded, fill=True) def draw_line(self, x, y, highlighted): self.draw(x, y, line=True, highlight=highlighted) @property def height(self): return self.layout.height + self.pad_top + self.pad_bottom @property def width(self): return self.layout.width + self.pad_left + self.pad_right qtile-0.31.0/libqtile/backend/base/window.py0000664000175000017500000004017214762660347020631 0ustar epsilonepsilonfrom __future__ import annotations import enum import typing from abc import ABCMeta, abstractmethod from libqtile.command.base import CommandError, CommandObject, expose_command if typing.TYPE_CHECKING: from typing import Any from libqtile import config from libqtile.backend.base import Drawer from libqtile.command.base import ItemT from libqtile.core.manager import Qtile from libqtile.group import _Group from libqtile.utils import ColorsType @enum.unique class FloatStates(enum.Enum): NOT_FLOATING = 1 FLOATING = 2 MAXIMIZED = 3 FULLSCREEN = 4 TOP = 5 MINIMIZED = 6 class _Window(CommandObject, metaclass=ABCMeta): def __init__(self): self.borderwidth: int = 0 self.name: str = "" self.reserved_space: tuple[int, int, int, int] | None = None # Window.static sets this in case it is hooked to client_new to stop the # Window object from being managed, now that a Static is being used instead self.defunct: bool = False self._can_steal_focus: bool = True self.base_x: int | None = None self.base_y: int | None = None self.base_width: int | None = None self.base_height: int | None = None @property @abstractmethod def wid(self) -> int: """The unique window ID""" @abstractmethod def hide(self) -> None: """Hide the window""" @abstractmethod def unhide(self) -> None: """Unhide the window""" @expose_command() def is_visible(self) -> bool: """Is this window visible (i.e. not hidden)?""" return False @abstractmethod @expose_command() def kill(self) -> None: """Kill the window""" def get_wm_class(self) -> list | None: """Return the class(es) of the window""" return None def get_wm_type(self) -> str | None: """Return the type of the window""" return None def get_wm_role(self) -> str | None: """Return the role of the window""" return None @property def can_steal_focus(self) -> bool: """Is it OK for this window to steal focus?""" return self._can_steal_focus @can_steal_focus.setter def can_steal_focus(self, can_steal_focus: bool) -> None: """Can_steal_focus setter.""" self._can_steal_focus = can_steal_focus def has_fixed_ratio(self) -> bool: """Does this window want a fixed aspect ratio?""" return False def has_fixed_size(self) -> bool: """Does this window want a fixed size?""" return False @property def urgent(self): """Whether this window urgently wants focus""" return False @property def opacity(self) -> float: """The opacity of this window from 0 (transparent) to 1 (opaque).""" return self._opacity @opacity.setter def opacity(self, opacity: float) -> None: """Opacity setter.""" self._opacity = opacity @abstractmethod @expose_command() def place( self, x, y, width, height, borderwidth, bordercolor, above=False, margin=None, respect_hints=False, ): """Place the window in the given position.""" def _items(self, name: str) -> ItemT: return None def _select(self, name, sel): return None def _save_geometry(self): """Save current window geometry.""" self.base_x = self.x self.base_y = self.y self.base_width = self.width self.base_height = self.height def _restore_geometry(self): """Restore previously saved window geometry.""" if self.base_x is not None: self.x = self.base_x if self.base_y is not None: self.y = self.base_y if self.base_width is not None: self.width = self.base_width if self.base_height is not None: self.height = self.base_height @abstractmethod @expose_command() def info(self) -> dict[str, Any]: """ Return information on this window. Mimimum required keys are: - name - x - y - width - height - group - id - wm_class """ return {} @expose_command() def keep_above(self, enable: bool | None = None): """Keep this window above all others""" @expose_command() def keep_below(self, enable: bool | None = None): """Keep this window below all others""" @expose_command() def move_up(self, force: bool = False) -> None: """ Move this window above the next window along the z axis. Will not raise a "normal" window (i.e. one that is not "kept_above/below") above a window that is marked as "kept_above". Will not raise a window where "keep_below" is True unless force is set to True. """ @expose_command() def move_down(self, force: bool = False) -> None: """ Move this window below the previous window along the z axis. Will not lower a "normal" window (i.e. one that is not "kept_above/below") below a window that is marked as "kept_below". Will not lower a window where "keep_above" is True unless force is set to True. """ @expose_command() def move_to_top(self) -> None: """ Move this window above all windows in the current layer e.g. if you have 3 windows all with "keep_above" set, calling this method will move the window to the top of those three windows. Calling this on a "normal" window will not raise it above a "kept_above" window. """ @expose_command() def move_to_bottom(self) -> None: """ Move this window below all windows in the current layer e.g. if you have 3 windows all with "keep_above" set, calling this method will move the window to the bottom of those three windows. Calling this on a "normal" window will not raise it below a "kept_below" window. """ @abstractmethod @expose_command() def bring_to_front(self) -> None: """ Bring the window to the front. In X11, `bring_to_front` ignores all other layering rules and brings the window to the very front. When that window loses focus, it will be stacked again according the appropriate rules. """ class Window(_Window, metaclass=ABCMeta): """ A regular Window belonging to a client. Abstract methods are required to be defined as part of a specific backend's implementation. Non-abstract methods have default implementations here to be shared across backends. """ qtile: Qtile # If float_x or float_y are None, the window has never been placed float_x: int | None float_y: int | None def __repr__(self): return "%s(name=%r, wid=%i)" % (self.__class__.__name__, self.name, self.wid) @property @abstractmethod def group(self) -> _Group | None: """The group to which this window belongs.""" @group.setter def group(self, group: _Group | None) -> None: """Set the group.""" @property def floating(self) -> bool: """Whether this window is floating.""" return False @floating.setter def floating(self, do_float: bool) -> None: raise NotImplementedError @property def maximized(self) -> bool: """Whether this window is maximized.""" return False @maximized.setter def maximized(self, do_maximize: bool) -> None: raise NotImplementedError @property def minimized(self) -> bool: """Whether this window is minimized.""" return False @minimized.setter def minimized(self, do_minimize: bool) -> None: raise NotImplementedError @property def fullscreen(self) -> bool: """Whether this window is fullscreened.""" return False @fullscreen.setter def fullscreen(self, do_full: bool) -> None: raise NotImplementedError @property def wants_to_fullscreen(self) -> bool: """Does this window want to be fullscreen?""" return False def match(self, match: config._Match) -> bool: """Compare this window against a Match instance.""" return match.compare(self) @abstractmethod @expose_command() def focus(self, warp: bool = True) -> None: """Focus this window and optional warp the pointer to it.""" @property def has_focus(self): return self == self.qtile.current_window def has_user_set_position(self) -> bool: """Whether this window has user-defined geometry""" return False def is_placed(self) -> bool: """Whether this window has been placed, i.e. both float offsets are not None.""" return ( self.group is not None and self.group.screen is not None and self.float_x is not None and self.float_y is not None ) def is_transient_for(self) -> WindowType | None: """What window is this window a transient window for?""" return None @abstractmethod def get_pid(self) -> int: """Return the PID that owns the window.""" def paint_borders(self, color: ColorsType, width: int) -> None: """Paint the window borders with the given color(s) and width""" @abstractmethod @expose_command() def get_position(self) -> tuple[int, int]: """Get the (x, y) of the window""" @abstractmethod @expose_command() def get_size(self) -> tuple[int, int]: """Get the (width, height) of the window""" @abstractmethod @expose_command() def move_floating(self, dx: int, dy: int) -> None: """Move window by dx and dy""" @abstractmethod @expose_command() def resize_floating(self, dw: int, dh: int) -> None: """Add dw and dh to size of window""" @abstractmethod @expose_command() def set_position_floating(self, x: int, y: int) -> None: """Move window to x and y""" @abstractmethod @expose_command() def set_position(self, x: int, y: int) -> None: """ Move floating window to x and y; swap tiling window with the window under the pointer. """ @abstractmethod @expose_command() def set_size_floating(self, w: int, h: int) -> None: """Set window dimensions to w and h""" @abstractmethod @expose_command() def toggle_floating(self) -> None: """Toggle the floating state of the window.""" @abstractmethod @expose_command() def enable_floating(self) -> None: """Float the window.""" @abstractmethod @expose_command() def disable_floating(self) -> None: """Tile the window.""" @abstractmethod @expose_command() def toggle_maximize(self) -> None: """Toggle the fullscreen state of the window.""" @abstractmethod @expose_command() def toggle_minimize(self) -> None: """Toggle the minimized state of the window.""" @abstractmethod @expose_command() def toggle_fullscreen(self) -> None: """Toggle the fullscreen state of the window.""" @abstractmethod @expose_command() def enable_fullscreen(self) -> None: """Fullscreen the window""" @abstractmethod @expose_command() def disable_fullscreen(self) -> None: """Un-fullscreen the window""" @abstractmethod @expose_command() def togroup( self, group_name: str | None = None, switch_group: bool = False, toggle: bool = False, ) -> None: """Move window to a specified group Also switch to that group if `switch_group` is True. If `toggle` is True and and the specified group is already on the screen, use the last used group as target instead. Examples ======== Move window to current group:: togroup() Move window to group "a":: togroup("a") Move window to group "a", and switch to group "a":: togroup("a", switch_group=True) """ self.togroup(group_name, switch_group=switch_group, toggle=toggle) @expose_command() def toscreen(self, index: int | None = None) -> None: """Move window to a specified screen. If index is not specified, we assume the current screen Examples ======== Move window to current screen:: toscreen() Move window to screen 0:: toscreen(0) """ if index is None: screen = self.qtile.current_screen else: try: screen = self.qtile.screens[index] except IndexError: raise CommandError("No such screen: %d" % index) self.togroup(screen.group.name) @expose_command() def set_opacity(self, opacity: float) -> None: """Set the window's opacity""" if opacity < 0.1: self.opacity = 0.1 elif opacity > 1: self.opacity = 1 else: self.opacity = opacity @expose_command() def down_opacity(self) -> None: """Decrease the window's opacity by 10%.""" self.set_opacity(self.opacity - 0.1) @expose_command() def up_opacity(self) -> None: """Increase the window's opacity by 10%.""" self.set_opacity(self.opacity + 0.1) @abstractmethod @expose_command() def static( self, screen: int | None = None, x: int | None = None, y: int | None = None, width: int | None = None, height: int | None = None, ) -> None: """Makes this window a static window, attached to a Screen. Values left unspecified are taken from the existing window state. """ self.defunct = True @expose_command() def center(self) -> None: """Centers a floating window on the screen.""" if not self.floating: return if not (self.group and self.group.screen): return screen = self.group.screen x = screen.x + (screen.width - self.width) // 2 y = screen.y + (screen.height - self.height) // 2 self.place( x, y, self.width, self.height, self.borderwidth, self.bordercolor, above=True, respect_hints=True, ) class Internal(_Window, metaclass=ABCMeta): """An Internal window belonging to Qtile.""" def __repr__(self): return f"Internal(wid={self.wid})" @abstractmethod def create_drawer(self, width: int, height: int) -> Drawer: """Create a Drawer that draws to this window.""" def process_window_expose(self) -> None: """Respond to the window being exposed. Required by X11 backend.""" def process_button_click(self, x: int, y: int, button: int) -> None: """Handle a pointer button click.""" def process_button_release(self, x: int, y: int, button: int) -> None: """Handle a pointer button release.""" def process_pointer_enter(self, x: int, y: int) -> None: """Handle the pointer entering the window.""" def process_pointer_leave(self, x: int, y: int) -> None: """Handle the pointer leaving the window.""" def process_pointer_motion(self, x: int, y: int) -> None: """Handle pointer motion within the window.""" def process_key_press(self, keycode: int) -> None: """Handle a key press.""" class Static(_Window, metaclass=ABCMeta): """A window bound to a screen rather than a group.""" screen: config.Screen x: Any y: Any width: Any height: Any def __repr__(self): return "%s(name=%r, wid=%i)" % (self.__class__.__name__, self.name, self.wid) @expose_command() def info(self) -> dict: """Return a dictionary of info.""" return dict( name=self.name, wm_class=self.get_wm_class(), x=self.x, y=self.y, width=self.width, height=self.height, id=self.wid, ) WindowType = Window | Internal | Static qtile-0.31.0/libqtile/backend/base/core.py0000664000175000017500000000720414762660347020251 0ustar epsilonepsilonfrom __future__ import annotations import contextlib import typing from abc import ABCMeta, abstractmethod from libqtile.command.base import CommandObject, expose_command from libqtile.config import ScreenRect if typing.TYPE_CHECKING: from typing import Any from libqtile import config from libqtile.backend.base import Internal from libqtile.command.base import ItemT from libqtile.core.manager import Qtile from libqtile.group import _Group class Core(CommandObject, metaclass=ABCMeta): painter: Any supports_restarting: bool = True qtile: Qtile @property @abstractmethod def name(self) -> str: """The name of the backend""" def _items(self, name: str) -> ItemT: return None def _select(self, name, sel): return None @abstractmethod def finalize(self): """Destructor/Clean up resources""" @property @abstractmethod def display_name(self) -> str: pass @abstractmethod def setup_listener(self) -> None: """Setup a listener for the given qtile instance""" @abstractmethod def remove_listener(self) -> None: """Setup a listener for the given qtile instance""" def update_desktops(self, groups: list[_Group], index: int) -> None: """Set the current desktops of the window manager""" @abstractmethod def get_screen_info(self) -> list[ScreenRect]: """Get the screen information""" @abstractmethod def grab_key(self, key: config.Key | config.KeyChord) -> tuple[int, int]: """Configure the backend to grab the key event""" @abstractmethod def ungrab_key(self, key: config.Key | config.KeyChord) -> tuple[int, int]: """Release the given key event""" @abstractmethod def ungrab_keys(self) -> None: """Release the grabbed key events""" @abstractmethod def grab_button(self, mouse: config.Mouse) -> int: """Configure the backend to grab the mouse event""" def ungrab_buttons(self) -> None: """Release the grabbed button events""" def grab_pointer(self) -> None: """Configure the backend to grab mouse events""" def ungrab_pointer(self) -> None: """Release grabbed pointer events""" def on_config_load(self, initial: bool) -> None: """ Respond to config loading. `initial` will be `True` if Qtile just started. """ def warp_pointer(self, x: int, y: int) -> None: """Warp the pointer to the given coordinates relative.""" @contextlib.contextmanager def masked(self): """A context manager to suppress window events while operating on many windows.""" yield def create_internal(self, x: int, y: int, width: int, height: int) -> Internal: """Create an internal window controlled by Qtile.""" raise NotImplementedError # Only error when called, not when instantiating class def flush(self) -> None: """If needed, flush the backend's event queue.""" def graceful_shutdown(self): """Try to close windows gracefully before exiting""" def simulate_keypress(self, modifiers: list[str], key: str) -> None: """Simulate a keypress with given modifiers""" def keysym_from_name(self, name: str) -> int: """Get the keysym for a key from its name""" raise NotImplementedError def get_mouse_position(self) -> tuple[int, int]: """Get mouse coordinates.""" raise NotImplementedError @expose_command() def info(self) -> dict[str, Any]: """Get basic information about the running backend.""" return {"backend": self.name, "display_name": self.display_name} qtile-0.31.0/libqtile/backend/x11/0000775000175000017500000000000014762660347016443 5ustar epsilonepsilonqtile-0.31.0/libqtile/backend/x11/xcursors_ffi.py0000664000175000017500000000333314762660347021533 0ustar epsilonepsilon# Copyright (c) 2014-2015 Sean Vig # Copyright (c) 2014 roger # Copyright (c) 2014 Tycho Andersen # Copyright (c) 2015 Craig Barnes # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from cffi import FFI from xcffib.ffi import ffi as xcffib_ffi xcursors_ffi = FFI() xcursors_ffi.include(xcffib_ffi) xcursors_ffi.cdef( """ typedef uint32_t xcb_cursor_t; typedef struct xcb_cursor_context_t xcb_cursor_context_t; int xcb_cursor_context_new( xcb_connection_t *conn, xcb_screen_t *screen, xcb_cursor_context_t **ctx ); xcb_cursor_t xcb_cursor_load_cursor( xcb_cursor_context_t *ctx, const char *name ); void xcb_cursor_context_free(xcb_cursor_context_t *ctx); """ ) qtile-0.31.0/libqtile/backend/x11/__init__.py0000664000175000017500000000000014762660347020542 0ustar epsilonepsilonqtile-0.31.0/libqtile/backend/x11/drawer.py0000664000175000017500000001731314762660347020306 0ustar epsilonepsilon# Copyright (c) 2010 Aldo Cortesi # Copyright (c) 2011 Florian Mounier # Copyright (c) 2011 oitel # Copyright (c) 2011 Kenji_Takahashi # Copyright (c) 2011 Paul Colomiets # Copyright (c) 2012, 2014 roger # Copyright (c) 2012 nullzion # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014-2015 Sean Vig # Copyright (c) 2014 Nathan Hoad # Copyright (c) 2014 dequis # Copyright (c) 2014 Tycho Andersen # Copyright (c) 2020, 2021 Robert Andrew Ditthardt # Copyright (c) 2023 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations import contextlib from typing import TYPE_CHECKING import cairocffi import xcffib.xproto from libqtile import utils from libqtile.backend.base import drawer if TYPE_CHECKING: from libqtile.backend.base import Internal from libqtile.core.manager import Qtile class Drawer(drawer.Drawer): """A helper class for drawing to Internal windows. The underlying surface here is an XCBSurface backed by a pixmap. We draw to the pixmap starting at offset 0, 0, and when the time comes to display to the window (on draw()), we copy the appropriate portion of the pixmap onto the window. In the event that our drawing area is resized, we invalidate the underlying surface and pixmap and recreate them when we need them again with the new geometry. """ def __init__(self, qtile: Qtile, win: Internal, width: int, height: int): drawer.Drawer.__init__(self, qtile, win, width, height) self._xcb_surface = None self._gc = None self._depth, self._visual = qtile.core.conn.default_screen._get_depth_and_visual( win._depth ) # Create an XCBSurface and pixmap self._check_xcb() def finalize(self): self._free_xcb_surface() self._free_pixmap() self._free_gc() drawer.Drawer.finalize(self) @property def width(self): return self._width @width.setter def width(self, width): if width > self._width: self._free_xcb_surface() self._free_pixmap() self._width = width @property def height(self): return self._height @height.setter def height(self, height): if height > self._height: self._free_xcb_surface() self._free_pixmap() self._height = height @property def pixmap(self): if self._pixmap is None: # draw here since the only use case of this function is in the # systray widget which expects a filled pixmap. self.draw() return self._pixmap def _create_gc(self): gc = self.qtile.core.conn.conn.generate_id() self.qtile.core.conn.conn.core.CreateGC( gc, self._win.wid, xcffib.xproto.GC.Foreground | xcffib.xproto.GC.Background, [ self.qtile.core.conn.default_screen.black_pixel, self.qtile.core.conn.default_screen.white_pixel, ], ) return gc def _free_gc(self): if self._gc is not None: with contextlib.suppress(xcffib.ConnectionException): self.qtile.core.conn.conn.core.FreeGC(self._gc) self._gc = None def _create_xcb_surface(self): surface = cairocffi.XCBSurface( self.qtile.core.conn.conn, self._pixmap, self._visual, self.width, self.height, ) return surface def _free_xcb_surface(self): if self._xcb_surface is not None: self._xcb_surface.finish() self._xcb_surface = None def _create_pixmap(self): pixmap = self.qtile.core.conn.conn.generate_id() self.qtile.core.conn.conn.core.CreatePixmap( self._depth, pixmap, self._win.wid, self.width, self.height, ) return pixmap def _free_pixmap(self): if self._pixmap is not None: with contextlib.suppress(xcffib.ConnectionException): self.qtile.core.conn.conn.core.FreePixmap(self._pixmap) self._pixmap = None def _check_xcb(self): # If the Drawer has been resized/invalidated we need to recreate these if self._xcb_surface is None: self._pixmap = self._create_pixmap() self._xcb_surface = self._create_xcb_surface() def _paint(self): # Paint RecordingSurface operations to the XCBSurface ctx = cairocffi.Context(self._xcb_surface) ctx.set_source_surface(self.surface, 0, 0) ctx.paint() def _draw( self, offsetx: int = 0, offsety: int = 0, width: int | None = None, height: int | None = None, src_x: int = 0, src_y: int = 0, ): # If this is our first draw, create the gc if self._gc is None: self._gc = self._create_gc() # Recreate an XCBSurface self._check_xcb() # paint stored operations(if any) to XCBSurface self._paint() # Finally, copy XCBSurface's underlying pixmap to the window. self.qtile.core.conn.conn.core.CopyArea( self._pixmap, self._win.wid, self._gc, src_x, src_y, # srcx, srcy offsetx, offsety, # dstx, dsty self.width if width is None else width, self.height if height is None else height, ) def _find_root_visual(self): for i in self.qtile.core.conn.default_screen.allowed_depths: for v in i.visuals: if v.visual_id == self.qtile.core.conn.default_screen.root_visual: return v def set_source_rgb(self, colour, ctx=None): # Remove transparency from non-32 bit windows if utils.has_transparency(colour) and self._depth != 32: colour = utils.remove_transparency(colour) drawer.Drawer.set_source_rgb(self, colour, ctx) def clear_rect(self, x=0, y=0, width=0, height=0): """ Erases the background area specified by parameters. By default, the whole Drawer is cleared. The ability to clear a smaller area may be useful when you want to erase a smaller area of the drawer (e.g. drawing widget decorations). """ if width <= 0: width = self.width if height <= 0: height = self.height self._check_xcb() # Using OPERATOR_CLEAR in a RecordingSurface does not clear the # XCBSurface so we clear the XCBSurface directly. with cairocffi.Context(self._xcb_surface) as ctx: ctx.set_operator(cairocffi.OPERATOR_CLEAR) ctx.rectangle(x, y, width, height) ctx.fill() qtile-0.31.0/libqtile/backend/x11/xcbq.py0000664000175000017500000006222014762660347017754 0ustar epsilonepsilon# Copyright (c) 2009-2010 Aldo Cortesi # Copyright (c) 2010 matt # Copyright (c) 2010, 2012, 2014 dequis # Copyright (c) 2010 Philip Kranz # Copyright (c) 2010-2011 Paul Colomiets # Copyright (c) 2011 osebelin # Copyright (c) 2011 Mounier Florian # Copyright (c) 2011 Kenji_Takahashi # Copyright (c) 2011 Tzbob # Copyright (c) 2012, 2014 roger # Copyright (c) 2012, 2014-2015 Tycho Andersen # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014-2015 Sean Vig # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """ A minimal EWMH-aware OO layer over xcffib. This is NOT intended to be complete - it only implements the subset of functionalty needed by qtile. """ from __future__ import annotations import contextlib import functools import operator from itertools import chain, repeat import cairocffi import cairocffi.pixbuf import cairocffi.xcb import xcffib import xcffib.randr import xcffib.xinerama import xcffib.xproto from xcffib.xfixes import SelectionEventMask from xcffib.xproto import CW, EventMask, WindowClass from libqtile.backend.x11 import window from libqtile.backend.x11.xcursors import Cursors from libqtile.backend.x11.xkeysyms import keysyms from libqtile.config import ScreenRect from libqtile.log_utils import logger from libqtile.utils import QtileError, hex, rgb class XCBQError(QtileError): pass # Keyboard modifiers bitmask values from X Protocol ModMasks = { "shift": 1 << 0, "lock": 1 << 1, "control": 1 << 2, "mod1": 1 << 3, "mod2": 1 << 4, "mod3": 1 << 5, "mod4": 1 << 6, "mod5": 1 << 7, } AllButtonsMask = 0b11111 << 8 ButtonMotionMask = 1 << 13 ButtonReleaseMask = 1 << 3 PointerMotionHintMask = 1 << 7 NormalHintsFlags = { "USPosition": 1, # User-specified x, y "USSize": 2, # User-specified width, height "PPosition": 4, # Program-specified position "PSize": 8, # Program-specified size "PMinSize": 16, # Program-specified minimum size "PMaxSize": 32, # Program-specified maximum size "PResizeInc": 64, # Program-specified resize increments "PAspect": 128, # Program-specified min and max aspect ratios "PBaseSize": 256, # Program-specified base size "PWinGravity": 512, # Program-specified window gravity } HintsFlags = { "InputHint": 1, # input "StateHint": 2, # initial_state "IconPixmapHint": 4, # icon_pixmap "IconWindowHint": 8, # icon_window "IconPositionHint": 16, # icon_x & icon_y "IconMaskHint": 32, # icon_mask "WindowGroupHint": 64, # window_group "MessageHint": 128, # (this bit is obsolete) "UrgencyHint": 256, # urgency } # http://standards.freedesktop.org/wm-spec/latest/ar01s05.html#idm139870830002400 WindowTypes = { "_NET_WM_WINDOW_TYPE_DESKTOP": "desktop", "_NET_WM_WINDOW_TYPE_DOCK": "dock", "_NET_WM_WINDOW_TYPE_TOOLBAR": "toolbar", "_NET_WM_WINDOW_TYPE_MENU": "menu", "_NET_WM_WINDOW_TYPE_UTILITY": "utility", "_NET_WM_WINDOW_TYPE_SPLASH": "splash", "_NET_WM_WINDOW_TYPE_DIALOG": "dialog", "_NET_WM_WINDOW_TYPE_DROPDOWN_MENU": "dropdown", "_NET_WM_WINDOW_TYPE_POPUP_MENU": "menu", "_NET_WM_WINDOW_TYPE_TOOLTIP": "tooltip", "_NET_WM_WINDOW_TYPE_NOTIFICATION": "notification", "_NET_WM_WINDOW_TYPE_COMBO": "combo", "_NET_WM_WINDOW_TYPE_DND": "dnd", "_NET_WM_WINDOW_TYPE_NORMAL": "normal", } # http://standards.freedesktop.org/wm-spec/latest/ar01s05.html#idm139870829988448 net_wm_states = ( "_NET_WM_STATE_MODAL", "_NET_WM_STATE_STICKY", "_NET_WM_STATE_MAXIMIZED_VERT", "_NET_WM_STATE_MAXIMIZED_HORZ", "_NET_WM_STATE_SHADED", "_NET_WM_STATE_SKIP_TASKBAR", "_NET_WM_STATE_SKIP_PAGER", "_NET_WM_STATE_HIDDEN", "_NET_WM_STATE_FULLSCREEN", "_NET_WM_STATE_ABOVE", "_NET_WM_STATE_BELOW", "_NET_WM_STATE_DEMANDS_ATTENTION", "_NET_WM_STATE_FOCUSED", ) WindowStates = { None: "normal", "_NET_WM_STATE_FULLSCREEN": "fullscreen", "_NET_WM_STATE_DEMANDS_ATTENTION": "urgent", } # Maps property names to types and formats. PropertyMap = { # ewmh properties "_NET_DESKTOP_GEOMETRY": ("CARDINAL", 32), "_NET_SUPPORTED": ("ATOM", 32), "_NET_SUPPORTING_WM_CHECK": ("WINDOW", 32), "_NET_WM_NAME": ("UTF8_STRING", 8), "_NET_WM_PID": ("CARDINAL", 32), "_NET_CLIENT_LIST": ("WINDOW", 32), "_NET_CLIENT_LIST_STACKING": ("WINDOW", 32), "_NET_NUMBER_OF_DESKTOPS": ("CARDINAL", 32), "_NET_CURRENT_DESKTOP": ("CARDINAL", 32), "_NET_DESKTOP_NAMES": ("UTF8_STRING", 8), "_NET_DESKTOP_VIEWPORT": ("CARDINAL", 32), "_NET_WORKAREA": ("CARDINAL", 32), "_NET_ACTIVE_WINDOW": ("WINDOW", 32), "_NET_WM_DESKTOP": ("CARDINAL", 32), "_NET_WM_STRUT": ("CARDINAL", 32), "_NET_WM_STRUT_PARTIAL": ("CARDINAL", 32), "_NET_WM_WINDOW_OPACITY": ("CARDINAL", 32), "_NET_WM_WINDOW_TYPE": ("ATOM", 32), "_NET_FRAME_EXTENTS": ("CARDINAL", 32), # Net State "_NET_WM_STATE": ("ATOM", 32), # Xembed "_XEMBED_INFO": ("_XEMBED_INFO", 32), # ICCCM "WM_STATE": ("WM_STATE", 32), # Qtile-specific properties "QTILE_INTERNAL": ("CARDINAL", 32), } for _name in net_wm_states: PropertyMap[_name] = ("ATOM", 32) # TODO add everything required here: # http://standards.freedesktop.org/wm-spec/latest/ar01s03.html SUPPORTED_ATOMS = [ # From http://standards.freedesktop.org/wm-spec/latest/ar01s03.html "_NET_SUPPORTED", "_NET_CLIENT_LIST", "_NET_CLIENT_LIST_STACKING", "_NET_CURRENT_DESKTOP", "_NET_DESKTOP_VIEWPORT", "_NET_ACTIVE_WINDOW", "_NET_SUPPORTING_WM_CHECK", # From http://standards.freedesktop.org/wm-spec/latest/ar01s05.html "_NET_WM_NAME", "_NET_WM_VISIBLE_NAME", "_NET_WM_ICON_NAME", "_NET_WM_DESKTOP", "_NET_WM_WINDOW_TYPE", "_NET_WM_STATE", "_NET_WM_STRUT_PARTIAL", "_NET_WM_PID", ] SUPPORTED_ATOMS.extend(WindowTypes.keys()) SUPPORTED_ATOMS.extend(net_wm_states) XCB_CONN_ERRORS = { 1: "XCB_CONN_ERROR", 2: "XCB_CONN_CLOSED_EXT_NOTSUPPORTED", 3: "XCB_CONN_CLOSED_MEM_INSUFFICIENT", 4: "XCB_CONN_CLOSED_REQ_LEN_EXCEED", 5: "XCB_CONN_CLOSED_PARSE_ERR", 6: "XCB_CONN_CLOSED_INVALID_SCREEN", 7: "XCB_CONN_CLOSED_FDPASSING_FAILED", } # Some opcodes from xproto.h, used for faking input. XCB_KEY_PRESS = 2 XCB_KEY_RELEASE = 3 XCB_BUTTON_PRESS = 4 XCB_BUTTON_RELEASE = 5 XCB_MOTION_NOTIFY = 6 class MaskMap: """ A general utility class that encapsulates the way the bitmask/listofvalue idiom works in X protocol. It understands a special attribute _maskvalue on objects, which will be used instead of the object value if present. This lets us pass in a Font object, rather than Font.fid, for example. """ def __init__(self, obj): self.mmap = [] for i in dir(obj): if not i.startswith("_"): self.mmap.append((getattr(obj, i), i.lower())) self.mmap.sort() def __call__(self, **kwargs): """ kwargs: keys should be in the mmap name set Returns a (mask, values) tuple. """ mask = 0 values = [] for m, s in self.mmap: if s in kwargs: val = kwargs.get(s) if val is not None: mask |= m values.append(getattr(val, "_maskvalue", val)) del kwargs[s] if kwargs: raise ValueError(f"Unknown mask names: {list(kwargs.keys())}") return mask, values ConfigureMasks = MaskMap(xcffib.xproto.ConfigWindow) AttributeMasks = MaskMap(CW) class AtomCache: def __init__(self, conn): self.conn = conn self.atoms = {} self.reverse = {} # We can change the pre-loads not to wait for a return for name in WindowTypes.keys(): self.insert(name=name) for i in dir(xcffib.xproto.Atom): if not i.startswith("_"): self.insert(name=i, atom=getattr(xcffib.xproto.Atom, i)) def insert(self, name=None, atom=None): assert name or atom if atom is None: c = self.conn.conn.core.InternAtom(False, len(name), name) atom = c.reply().atom if name is None: c = self.conn.conn.core.GetAtomName(atom) name = c.reply().name.to_string() self.atoms[name] = atom self.reverse[atom] = name def get_name(self, atom): if atom not in self.reverse: self.insert(atom=atom) return self.reverse[atom] def __getitem__(self, key): if key not in self.atoms: self.insert(name=key) return self.atoms[key] class _Wrapper: def __init__(self, wrapped): self.wrapped = wrapped def __getattr__(self, x): return getattr(self.wrapped, x) class Screen(_Wrapper): """ This represents an actual X screen. """ def __init__(self, conn, screen): _Wrapper.__init__(self, screen) self.default_colormap = Colormap(conn, screen.default_colormap) self.root = window.XWindow(conn, self.root) self._visuals = {} # Get visuals for 32 and 24 bit for d in [32, 24, self.root_depth]: if d not in self._visuals: visual = self.get_visual_for_depth(self, d) if visual: self._visuals[d] = visual def _get_depth_and_visual(self, desired_depth): """ Returns a tuple of (depth, visual) for the requested depth. Falls back to the root depth and visual if the requested depth is unavailable. """ if desired_depth in self._visuals: return desired_depth, self._visuals[desired_depth] logger.info( "%s bit colour depth not available. Falling back to root depth: %s.", desired_depth, self.root_depth, ) return self.root_depth, self._visuals[self.root_depth] @staticmethod def get_visual_for_depth(screen, depth): """ Returns the visual object of the screen @ some depth For an ARGB visual -> depth=32 For a RGB visual -> depth=24 """ allowed = screen.allowed_depths if depth not in [x.depth for x in allowed]: logger.warning("Unsupported colour depth: %s", depth) return for i in allowed: if i.depth == depth: if i.visuals: return i.visuals[0] class Colormap: def __init__(self, conn, cid): self.conn = conn self.cid = cid def alloc_color(self, color): """ Flexible color allocation. """ try: return self.conn.conn.core.AllocNamedColor(self.cid, len(color), color).reply() except xcffib.xproto.NameError: def x8to16(i): return 0xFFFF * (i & 0xFF) // 0xFF try: color = hex(color) except ValueError: logger.error("Colormap failed to allocate %s", color) color = "#ff0000" r = x8to16(int(color[-6] + color[-5], 16)) g = x8to16(int(color[-4] + color[-3], 16)) b = x8to16(int(color[-2] + color[-1], 16)) return self.conn.conn.core.AllocColor(self.cid, r, g, b).reply() class Xinerama: def __init__(self, conn): self.ext = conn.conn(xcffib.xinerama.key) def query_screens(self): r = self.ext.QueryScreens().reply() return r.screen_info class RandR: def __init__(self, conn): self.ext = conn.conn(xcffib.randr.key) self.ext.SelectInput(conn.default_screen.root.wid, xcffib.randr.NotifyMask.ScreenChange) def query_crtcs(self, root): infos = [] for crtc in self.ext.GetScreenResources(root).reply().crtcs: crtc_info = self.ext.GetCrtcInfo(crtc, xcffib.CurrentTime).reply() infos.append(ScreenRect(crtc_info.x, crtc_info.y, crtc_info.width, crtc_info.height)) return infos class XFixes: selection_mask = ( SelectionEventMask.SetSelectionOwner | SelectionEventMask.SelectionClientClose | SelectionEventMask.SelectionWindowDestroy ) def __init__(self, conn): self.conn = conn self.ext = conn.conn(xcffib.xfixes.key) self.ext.QueryVersion(xcffib.xfixes.MAJOR_VERSION, xcffib.xfixes.MINOR_VERSION) def select_selection_input(self, window, selection="PRIMARY"): _selection = self.conn.atoms[selection] self.conn.xfixes.ext.SelectSelectionInput(window.wid, _selection, self.selection_mask) class Connection: _extmap = { "xinerama": Xinerama, "randr": RandR, "xfixes": XFixes, } def __init__(self, display): self.conn = xcffib.connect(display=display) self._connected = True self.cursors = Cursors(self) self.setup = self.conn.get_setup() extensions = self.extensions() self.screens = [Screen(self, i) for i in self.setup.roots] self.default_screen = self.screens[self.conn.pref_screen] for i in extensions: if i in self._extmap: setattr(self, i, self._extmap[i](self)) self.atoms = AtomCache(self) self.code_to_syms = {} self.sym_to_codes = None self.refresh_keymap() self.modmap = None self.refresh_modmap() self._cmaps = {} def colormap(self, desired_depth): if desired_depth in self._cmaps: return self._cmaps[desired_depth] _, visual = self.default_screen._get_depth_and_visual(desired_depth) cmap = self.conn.generate_id() self.conn.core.CreateColormap( xcffib.xproto.ColormapAlloc._None, cmap, self.default_screen.root.wid, visual.visual_id, is_checked=True, ).check() self._cmaps[desired_depth] = cmap return cmap @property def pseudoscreens(self): if hasattr(self, "xinerama"): pseudoscreens = [] for i, s in enumerate(self.xinerama.query_screens()): scr = ScreenRect( s.x_org, s.y_org, s.width, s.height, ) pseudoscreens.append(scr) return pseudoscreens elif hasattr(self, "randr"): return self.randr.query_crtcs(self.screens[0].root.wid) def finalize(self): self.cursors.finalize() self.disconnect() def refresh_keymap(self, first=None, count=None): if first is None: first = self.setup.min_keycode count = self.setup.max_keycode - self.setup.min_keycode + 1 q = self.conn.core.GetKeyboardMapping(first, count).reply() assert len(q.keysyms) % q.keysyms_per_keycode == 0 for i in range(len(q.keysyms) // q.keysyms_per_keycode): self.code_to_syms[first + i] = q.keysyms[ i * q.keysyms_per_keycode : (i + 1) * q.keysyms_per_keycode ] sym_to_codes = {} for k, s in self.code_to_syms.items(): for sym in s: if sym == 0: continue if sym not in sym_to_codes: sym_to_codes[sym] = [k] elif k not in sym_to_codes[sym]: sym_to_codes[sym].append(k) self.sym_to_codes = sym_to_codes def refresh_modmap(self): reply = self.conn.core.GetModifierMapping().reply() modmap = {} names = (repeat(name, reply.keycodes_per_modifier) for name in ModMasks) for name, keycode in zip(chain.from_iterable(names), reply.keycodes): value = modmap.setdefault(name, []) value.append(keycode) self.modmap = modmap def get_modifier(self, keycode): """Return the modifier matching keycode""" for n, l in self.modmap.items(): if keycode in l: return n return None def keysym_to_keycode(self, keysym): return self.sym_to_codes.get(keysym, [0]) def keycode_to_keysym(self, keycode, modifier): if keycode >= len(self.code_to_syms) or modifier >= len(self.code_to_syms[keycode]): return 0 return self.code_to_syms[keycode][modifier] def create_window(self, x, y, width, height, desired_depth=32): depth, visual = self.default_screen._get_depth_and_visual(desired_depth) wid = self.conn.generate_id() value_mask = CW.BackPixmap | CW.BorderPixel | CW.EventMask | CW.Colormap values = [ xcffib.xproto.BackPixmap._None, 0, EventMask.StructureNotify | EventMask.Exposure, self.colormap(depth), ] self.conn.core.CreateWindow( depth, wid, self.default_screen.root.wid, x, y, width, height, 0, WindowClass.InputOutput, visual.visual_id, value_mask, values, ) return window.XWindow(self, wid) def disconnect(self): with contextlib.suppress(xcffib.ConnectionException): self.conn.disconnect() self._connected = False def flush(self): if self._connected: return self.conn.flush() def xsync(self): # The idea here is that pushing an innocuous request through the queue # and waiting for a response "syncs" the connection, since requests are # serviced in order. self.conn.core.GetInputFocus().reply() def get_setup(self): return self.conn.get_setup() def extensions(self): return set( i.name.to_string().lower() for i in self.conn.core.ListExtensions().reply().names ) def fixup_focus(self): """ If the X11 focus is set to None, all keypress events are discarded, which makes our hotkeys not work. This fixes up the focus so it is not None. """ window = self.conn.core.GetInputFocus().reply().focus if window == xcffib.xproto.InputFocus._None: self.conn.core.SetInputFocus( xcffib.xproto.InputFocus.PointerRoot, xcffib.xproto.InputFocus.PointerRoot, xcffib.xproto.Time.CurrentTime, ) @functools.lru_cache def color_pixel(self, name): pixel = self.screens[0].default_colormap.alloc_color(name).pixel return pixel | 0xFF << 24 class Painter: def __init__(self, display): self.conn = xcffib.connect(display=display) self.setup = self.conn.get_setup() self.screens = [Screen(self, i) for i in self.setup.roots] self.default_screen = self.screens[self.conn.pref_screen] self.conn.core.SetCloseDownMode(xcffib.xproto.CloseDown.RetainPermanent) self.atoms = AtomCache(self) self.width = -1 self.height = -1 self.root_pixmap_id = None def _get_root_pixmap_and_surface(self, screen): # Querying the screen dimensions via the xcffib connection does not # take account of any screen scaling. We can therefore work out the # necessary size of the root window by looking at the # pseudoscreens attribute and calculating the max x and y extents. root_windows = screen.qtile.core.conn.pseudoscreens width = max(win.x + win.width for win in root_windows) height = max(win.y + win.height for win in root_windows) try: root_pixmap = self.default_screen.root.get_property( "_XROOTPMAP_ID", xcffib.xproto.Atom.PIXMAP, int ) except xcffib.ConnectionException: root_pixmap = None if not root_pixmap: root_pixmap = self.default_screen.root.get_property( "ESETROOT_PMAP_ID", xcffib.xproto.Atom.PIXMAP, int ) if root_pixmap and (self.width == width and self.height == height): root_pixmap = root_pixmap[0] else: self.width = width self.height = height root_pixmap = self.conn.generate_id() self.conn.core.CreatePixmap( self.default_screen.root_depth, root_pixmap, self.default_screen.root.wid, self.width, self.height, ) for depth in self.default_screen.allowed_depths: for visual in depth.visuals: if visual.visual_id == self.default_screen.root_visual: root_visual = visual break surface = cairocffi.xcb.XCBSurface( self.conn, root_pixmap, root_visual, self.width, self.height ) return root_pixmap, surface def _update_root_pixmap(self, root_pixmap): self.conn.core.ChangeProperty( xcffib.xproto.PropMode.Replace, self.default_screen.root.wid, self.atoms["_XROOTPMAP_ID"], xcffib.xproto.Atom.PIXMAP, 32, 1, [root_pixmap], ) self.conn.core.ChangeProperty( xcffib.xproto.PropMode.Replace, self.default_screen.root.wid, self.atoms["ESETROOT_PMAP_ID"], xcffib.xproto.Atom.PIXMAP, 32, 1, [root_pixmap], ) self.conn.core.ChangeWindowAttributes( self.default_screen.root.wid, CW.BackPixmap, [root_pixmap] ) self.conn.core.ClearArea(0, self.default_screen.root.wid, 0, 0, self.width, self.height) self.conn.flush() # now that we have drawn the new pixmap, free the old one if self.root_pixmap_id is not None and self.root_pixmap_id != root_pixmap: self.conn.core.FreePixmap(self.root_pixmap_id) self.root_pixmap_id = root_pixmap def fill(self, screen, background): root_pixmap, surface = self._get_root_pixmap_and_surface(screen) with cairocffi.Context(surface) as ctx: ctx.translate(screen.x, screen.y) ctx.rectangle(0, 0, screen.width, screen.height) ctx.set_source_rgba(*rgb(background)) ctx.fill() surface.finish() self._update_root_pixmap(root_pixmap) def paint(self, screen, image_path, mode=None): try: with open(image_path, "rb") as f: image, _ = cairocffi.pixbuf.decode_to_image_surface(f.read()) except OSError: logger.exception("Could not load wallpaper:") return root_pixmap, surface = self._get_root_pixmap_and_surface(screen) context = cairocffi.Context(surface) with context: context.translate(screen.x, screen.y) if mode == "fill": context.rectangle(0, 0, screen.width, screen.height) context.clip() image_w = image.get_width() image_h = image.get_height() width_ratio = screen.width / image_w if width_ratio * image_h >= screen.height: context.scale(width_ratio) else: height_ratio = screen.height / image_h context.translate(-(image_w * height_ratio - screen.width) // 2, 0) context.scale(height_ratio) elif mode == "stretch": context.scale( sx=screen.width / image.get_width(), sy=screen.height / image.get_height(), ) context.set_source_surface(image) context.paint() surface.finish() self._update_root_pixmap(root_pixmap) def __del__(self): self.conn.disconnect() def get_keysym(key: str) -> int: keysym = keysyms.get(key.lower()) if not keysym: raise XCBQError(f"Unknown key: {key}") return keysym def translate_modifiers(mask: int) -> list[str]: r = [] for k, v in ModMasks.items(): if mask & v: r.append(k) return r def translate_masks(modifiers: list[str]) -> int: """ Translate a modifier mask specified as a list of strings into an or-ed bit representation. """ masks = [] for i in modifiers: try: masks.append(ModMasks[i.lower()]) except KeyError as e: raise XCBQError(f"Unknown modifier: {i}") from e if masks: return functools.reduce(operator.or_, masks) else: return 0 qtile-0.31.0/libqtile/backend/x11/window.py0000664000175000017500000024475714762660347020347 0ustar epsilonepsilonfrom __future__ import annotations import array import contextlib import inspect import traceback from itertools import islice from types import FunctionType from typing import TYPE_CHECKING import xcffib import xcffib.xproto from xcffib.wrappers import GContextID, PixmapID from xcffib.xproto import EventMask, SetMode from libqtile import bar, hook, utils from libqtile.backend import base from libqtile.backend.base import FloatStates from libqtile.backend.x11 import xcbq from libqtile.backend.x11.drawer import Drawer from libqtile.command.base import CommandError, expose_command from libqtile.log_utils import logger from libqtile.scratchpad import ScratchPad if TYPE_CHECKING: from libqtile.command.base import ItemT # ICCM Constants NoValue = 0x0000 XValue = 0x0001 YValue = 0x0002 WidthValue = 0x0004 HeightValue = 0x0008 AllValues = 0x000F XNegative = 0x0010 YNegative = 0x0020 InputHint = 1 << 0 StateHint = 1 << 1 IconPixmapHint = 1 << 2 IconWindowHint = 1 << 3 IconPositionHint = 1 << 4 IconMaskHint = 1 << 5 WindowGroupHint = 1 << 6 MessageHint = 1 << 7 UrgencyHint = 1 << 8 AllHints = ( InputHint | StateHint | IconPixmapHint | IconWindowHint | IconPositionHint | IconMaskHint | WindowGroupHint | MessageHint | UrgencyHint ) WithdrawnState = 0 DontCareState = 0 NormalState = 1 ZoomState = 2 IconicState = 3 InactiveState = 4 RectangleOut = 0 RectangleIn = 1 RectanglePart = 2 VisualNoMask = 0x0 VisualIDMask = 0x1 VisualScreenMask = 0x2 VisualDepthMask = 0x4 VisualClassMask = 0x8 VisualRedMaskMask = 0x10 VisualGreenMaskMask = 0x20 VisualBlueMaskMask = 0x40 VisualColormapSizeMask = 0x80 VisualBitsPerRGBMask = 0x100 VisualAllMask = 0x1FF ReleaseByFreeingColormap = 1 BitmapSuccess = 0 BitmapOpenFailed = 1 BitmapFileInvalid = 2 BitmapNoMemory = 3 XCSUCCESS = 0 XCNOMEM = 1 XCNOENT = 2 _NET_WM_STATE_REMOVE = 0 _NET_WM_STATE_ADD = 1 _NET_WM_STATE_TOGGLE = 2 def _geometry_getter(attr): def get_attr(self): if getattr(self, "_" + attr) is None: g = self.window.get_geometry() # trigger the geometry setter on all these self.x = g.x self.y = g.y self.width = g.width self.height = g.height self.depth = g.depth return getattr(self, "_" + attr) return get_attr def _geometry_setter(attr): def f(self, value): if not isinstance(value, int): frame = inspect.currentframe() stack_trace = traceback.format_stack(frame) logger.error("!!!! setting %s to a non-int %s; please report this!", attr, value) logger.error("".join(stack_trace[:-1])) value = int(value) setattr(self, "_" + attr, value) return f class XWindow: def __init__(self, conn, wid): self.conn = conn self.wid = wid def _property_string(self, r): """Extract a string from a window property reply message""" return r.value.to_string() def _property_utf8(self, r): try: return r.value.to_utf8() except UnicodeDecodeError: return r.value.to_string() def send_event(self, synthevent, mask=EventMask.NoEvent): self.conn.conn.core.SendEvent(False, self.wid, mask, synthevent.pack()) def kill_client(self): self.conn.conn.core.KillClient(self.wid) def set_input_focus(self): self.conn.conn.core.SetInputFocus( xcffib.xproto.InputFocus.PointerRoot, self.wid, xcffib.xproto.Time.CurrentTime ) def warp_pointer(self, x, y): """Warps the pointer to the location `x`, `y` on the window""" self.conn.conn.core.WarpPointer( 0, self.wid, # src_window, dst_window 0, 0, # src_x, src_y 0, 0, # src_width, src_height x, y, # dest_x, dest_y ) def get_name(self): """Tries to retrieve a canonical window name. We test the following properties in order of preference: - _NET_WM_VISIBLE_NAME - _NET_WM_NAME - WM_NAME. """ r = self.get_property("_NET_WM_VISIBLE_NAME", "UTF8_STRING") if r: return self._property_utf8(r) r = self.get_property("_NET_WM_NAME", "UTF8_STRING") if r: return self._property_utf8(r) r = self.get_property(xcffib.xproto.Atom.WM_NAME, "UTF8_STRING") if r: return self._property_utf8(r) r = self.get_property(xcffib.xproto.Atom.WM_NAME, xcffib.xproto.GetPropertyType.Any) if r: return self._property_string(r) def get_wm_hints(self): wm_hints = self.get_property("WM_HINTS", xcffib.xproto.GetPropertyType.Any) if wm_hints: atoms_list = wm_hints.value.to_atoms() flags = {k for k, v in xcbq.HintsFlags.items() if atoms_list[0] & v} return { "flags": flags, "input": atoms_list[1] if "InputHint" in flags else None, "initial_state": atoms_list[2] if "StateHing" in flags else None, "icon_pixmap": atoms_list[3] if "IconPixmapHint" in flags else None, "icon_window": atoms_list[4] if "IconWindowHint" in flags else None, "icon_x": atoms_list[5] if "IconPositionHint" in flags else None, "icon_y": atoms_list[6] if "IconPositionHint" in flags else None, "icon_mask": atoms_list[7] if "IconMaskHint" in flags else None, "window_group": atoms_list[8] if "WindowGroupHint" in flags else None, } def get_wm_normal_hints(self): wm_normal_hints = self.get_property("WM_NORMAL_HINTS", xcffib.xproto.GetPropertyType.Any) if wm_normal_hints: atom_list = wm_normal_hints.value.to_atoms() flags = {k for k, v in xcbq.NormalHintsFlags.items() if atom_list[0] & v} hints = { "flags": flags, "min_width": atom_list[5], "min_height": atom_list[6], "max_width": atom_list[7], "max_height": atom_list[8], "width_inc": atom_list[9], "height_inc": atom_list[10], "min_aspect": (atom_list[11], atom_list[12]), "max_aspect": (atom_list[13], atom_list[14]), } # WM_SIZE_HINTS is potentially extensible (append to the end only) iterator = islice(hints, 15, None) hints["base_width"] = next(iterator, hints["min_width"]) hints["base_height"] = next(iterator, hints["min_height"]) hints["win_gravity"] = next(iterator, 1) return hints def get_wm_protocols(self): wm_protocols = self.get_property("WM_PROTOCOLS", "ATOM", unpack=int) if wm_protocols is not None: return {self.conn.atoms.get_name(wm_protocol) for wm_protocol in wm_protocols} return set() def get_wm_state(self): return self.get_property("WM_STATE", xcffib.xproto.GetPropertyType.Any, unpack=int) def get_wm_class(self): """Return an (instance, class) tuple if WM_CLASS exists.""" r = self.get_property("WM_CLASS", "STRING") if r: s = self._property_string(r) return list(s.strip("\0").split("\0")) return [] def get_wm_window_role(self): r = self.get_property("WM_WINDOW_ROLE", "STRING") if r: return self._property_string(r) def get_wm_transient_for(self): """Returns the WID of the parent window""" r = self.get_property("WM_TRANSIENT_FOR", "WINDOW", unpack=int) if r: return r[0] def get_wm_icon_name(self): r = self.get_property("_NET_WM_ICON_NAME", "UTF8_STRING") if r: return self._property_utf8(r) r = self.get_property("WM_ICON_NAME", "STRING") if r: return self._property_utf8(r) def get_wm_client_machine(self): r = self.get_property("WM_CLIENT_MACHINE", "STRING") if r: return self._property_utf8(r) def get_geometry(self): q = self.conn.conn.core.GetGeometry(self.wid) return q.reply() def get_wm_desktop(self): r = self.get_property("_NET_WM_DESKTOP", "CARDINAL", unpack=int) if r: return r[0] def get_wm_type(self): """ http://standards.freedesktop.org/wm-spec/wm-spec-latest.html#id2551529 """ r = self.get_property("_NET_WM_WINDOW_TYPE", "ATOM", unpack=int) if r: first_name = None for i, a in enumerate(r): name = self.conn.atoms.get_name(a) if i == 0: first_name = name qtile_type = xcbq.WindowTypes.get(name, None) if qtile_type is not None: return qtile_type return first_name return None def get_net_wm_state(self): r = self.get_property("_NET_WM_STATE", "ATOM", unpack=int) if r: names = [self.conn.atoms.get_name(p) for p in r] return [xcbq.WindowStates.get(n, n) for n in names] return [] def get_net_wm_pid(self): r = self.get_property("_NET_WM_PID", unpack=int) if r: return r[0] def configure(self, **kwargs): """ Arguments can be: x, y, width, height, borderwidth, sibling, stackmode """ mask, values = xcbq.ConfigureMasks(**kwargs) # older versions of xcb pack everything into unsigned ints "=I" # since 1.12, uses switches to pack things sensibly if float(".".join(xcffib.__xcb_proto_version__.split(".")[0:2])) < 1.12: values = [i & 0xFFFFFFFF for i in values] return self.conn.conn.core.ConfigureWindow(self.wid, mask, values) def set_attribute(self, **kwargs): mask, values = xcbq.AttributeMasks(**kwargs) self.conn.conn.core.ChangeWindowAttributesChecked(self.wid, mask, values) def set_cursor(self, name): cursor_id = self.conn.cursors[name] mask, values = xcbq.AttributeMasks(cursor=cursor_id) self.conn.conn.core.ChangeWindowAttributesChecked(self.wid, mask, values) def set_property(self, name, value, type=None, format=None): """ Parameters ========== name: String Atom name type: String Atom name format: 8, 16, 32 """ if name in xcbq.PropertyMap: if type or format: raise ValueError("Over-riding default type or format for property.") type, format = xcbq.PropertyMap[name] else: if None in (type, format): raise ValueError("Must specify type and format for unknown property.") try: if isinstance(value, str): # xcffib will pack the bytes, but we should encode them properly value = value.encode() else: # if this runs without error, the value is already a list, don't wrap it next(iter(value)) except StopIteration: # The value was an iterable, just empty value = [] except TypeError: # the value wasn't an iterable and wasn't a string, so let's # wrap it. value = [value] try: self.conn.conn.core.ChangePropertyChecked( xcffib.xproto.PropMode.Replace, self.wid, self.conn.atoms[name], self.conn.atoms[type], format, # Format - 8, 16, 32 len(value), value, ).check() except xcffib.xproto.WindowError: logger.debug("X error in SetProperty (wid=%r, prop=%r), ignoring", self.wid, name) def get_property(self, prop, type=None, unpack=None): """Return the contents of a property as a GetPropertyReply If unpack is specified, a tuple of values is returned. The type to unpack, either `str` or `int` must be specified. """ if type is None: if prop not in xcbq.PropertyMap: raise ValueError("Must specify type for unknown property.") else: type, _ = xcbq.PropertyMap[prop] try: r = self.conn.conn.core.GetProperty( False, self.wid, self.conn.atoms[prop] if isinstance(prop, str) else prop, self.conn.atoms[type] if isinstance(type, str) else type, 0, (2**32) - 1, ).reply() except (xcffib.xproto.WindowError, xcffib.xproto.AccessError): logger.debug("X error in GetProperty (wid=%r, prop=%r), ignoring", self.wid, prop) if unpack: return [] return None if not r.value_len: if unpack: return [] return None elif unpack: # Should we allow more options for unpacking? if unpack is int: return r.value.to_atoms() elif unpack is str: return r.value.to_string() else: return r def list_properties(self): r = self.conn.conn.core.ListProperties(self.wid).reply() return [self.conn.atoms.get_name(i) for i in r.atoms] def map(self): self.conn.conn.core.MapWindow(self.wid) def unmap(self): self.conn.conn.core.UnmapWindowUnchecked(self.wid) def get_attributes(self): return self.conn.conn.core.GetWindowAttributes(self.wid).reply() def query_tree(self): return self.conn.conn.core.QueryTree(self.wid).reply().children def paint_borders(self, depth, colors, borderwidth, width, height): """ This method is used only by the managing Window class. """ self.set_property("_NET_FRAME_EXTENTS", [borderwidth] * 4) if not colors or not borderwidth: return if isinstance(colors, str): self.set_attribute(borderpixel=self.conn.color_pixel(colors)) return if len(colors) > borderwidth: colors = colors[:borderwidth] core = self.conn.conn.core outer_w = width + borderwidth * 2 outer_h = height + borderwidth * 2 with PixmapID(self.conn.conn) as pixmap: with GContextID(self.conn.conn) as gc: core.CreatePixmap(depth, pixmap, self.wid, outer_w, outer_h) core.CreateGC(gc, pixmap, 0, None) borders = len(colors) borderwidths = [borderwidth // borders] * borders for i in range(borderwidth % borders): borderwidths[i] += 1 coord = 0 for i in range(borders): core.ChangeGC( gc, xcffib.xproto.GC.Foreground, [self.conn.color_pixel(colors[i])] ) rect = xcffib.xproto.RECTANGLE.synthetic( coord, coord, outer_w - coord * 2, outer_h - coord * 2 ) core.PolyFillRectangle(pixmap, gc, 1, [rect]) coord += borderwidths[i] self._set_borderpixmap(depth, pixmap, gc, borderwidth, width, height) def _set_borderpixmap(self, depth, pixmap, gc, borderwidth, width, height): core = self.conn.conn.core outer_w = width + borderwidth * 2 outer_h = height + borderwidth * 2 with PixmapID(self.conn.conn) as border: core.CreatePixmap(depth, border, self.wid, outer_w, outer_h) most_w = outer_w - borderwidth most_h = outer_h - borderwidth core.CopyArea(pixmap, border, gc, borderwidth, borderwidth, 0, 0, most_w, most_h) core.CopyArea(pixmap, border, gc, 0, 0, most_w, most_h, borderwidth, borderwidth) core.CopyArea(pixmap, border, gc, borderwidth, 0, 0, most_h, most_w, borderwidth) core.CopyArea(pixmap, border, gc, 0, borderwidth, most_w, 0, borderwidth, most_h) core.ChangeWindowAttributes(self.wid, xcffib.xproto.CW.BorderPixmap, [border]) class _Window: _window_mask = 0 # override in child class def __init__(self, window, qtile): base.Window.__init__(self) self.window, self.qtile = window, qtile self.hidden = False self.icons = {} window.set_attribute(eventmask=self._window_mask) self._group = None try: g = self.window.get_geometry() self._x = g.x self._y = g.y self._width = g.width self._height = g.height self._depth = g.depth except xcffib.xproto.DrawableError: # Whoops, we were too early, so let's ignore it for now and get the # values on demand. self._x = None self._y = None self._width = None self._height = None self._depth = None self.float_x: int | None = None self.float_y: int | None = None self._float_width: int = self._width self._float_height: int = self._height # We use `previous_layer` to see if a window has moved up or down a "layer" # The layers are defined in the spec: # https://specifications.freedesktop.org/wm-spec/1.3/ar01s07.html#STACKINGORDER # We assume a window starts off in the layer for "normal" windows, i.e. ones that # don't match the requirements to be in any of the other layers. self.previous_layer = (False, False, True, False, False, False) self.bordercolor = None self.state = NormalState self._float_state = FloatStates.NOT_FLOATING self._demands_attention = False self.hints = { "input": True, "icon_pixmap": None, "icon_window": None, "icon_x": 0, "icon_y": 0, "icon_mask": 0, "window_group": None, "urgent": False, # normal or size hints "width_inc": None, "height_inc": None, "base_width": 0, "base_height": 0, } self.update_hints() x = property(fset=_geometry_setter("x"), fget=_geometry_getter("x")) y = property(fset=_geometry_setter("y"), fget=_geometry_getter("y")) width = property( fset=_geometry_setter("width"), fget=_geometry_getter("width"), ) height = property( fset=_geometry_setter("height"), fget=_geometry_getter("height"), ) depth = property( fset=_geometry_setter("depth"), fget=_geometry_getter("depth"), ) @property def wid(self): return self.window.wid @property def group(self): return self._group def has_fixed_ratio(self) -> bool: try: if ( "PAspect" in self.hints["flags"] and self.hints["min_aspect"] == self.hints["max_aspect"] ): return True except KeyError: pass return False def has_fixed_size(self) -> bool: try: if ( "PMinSize" in self.hints["flags"] and "PMaxSize" in self.hints["flags"] and 0 < self.hints["min_width"] == self.hints["max_width"] and 0 < self.hints["min_height"] == self.hints["max_height"] ): return True except KeyError: pass return False def has_user_set_position(self): try: if "USPosition" in self.hints["flags"] or "PPosition" in self.hints["flags"]: return True except KeyError: pass return False def update_name(self): try: self.name = self.window.get_name() except (xcffib.xproto.WindowError, xcffib.xproto.AccessError): return hook.fire("client_name_updated", self) def update_wm_class(self) -> None: self._wm_class = self.window.get_wm_class() def get_wm_class(self) -> list[str] | None: return self._wm_class def get_wm_type(self): return self.window.get_wm_type() def get_wm_role(self): return self.window.get_wm_window_role() def is_transient_for(self): """What window is this window a transient windor for?""" wid = self.window.get_wm_transient_for() return self.qtile.windows_map.get(wid) def update_hints(self): """Update the local copy of the window's WM_HINTS See http://tronche.com/gui/x/icccm/sec-4.html#WM_HINTS """ try: h = self.window.get_wm_hints() normh = self.window.get_wm_normal_hints() except (xcffib.xproto.WindowError, xcffib.xproto.AccessError): return width_inc = self.hints["width_inc"] height_inc = self.hints["height_inc"] if normh: self.hints.update(normh) if h and "UrgencyHint" in h["flags"]: if self.qtile.current_window != self: self.hints["urgent"] = True hook.fire("client_urgent_hint_changed", self) elif self.urgent: self.hints["urgent"] = False hook.fire("client_urgent_hint_changed", self) if h and "InputHint" in h["flags"]: self.hints["input"] = h["input"] if ( self.group and self.floating and width_inc != self.hints["width_inc"] and height_inc != self.hints["height_inc"] ): self.group.layout_all() return def update_state(self): triggered = ["urgent"] state = self.window.get_net_wm_state() if self.qtile.config.auto_fullscreen: triggered.append("fullscreen") # This might seem a bit weird but it's to workaround a bug in chromium based clients not properly redrawing # The bug is described in https://github.com/qtile/qtile/issues/4176 # This only happens when auto fullscreen is set to false because we then do not obey the disable fullscreen state # So here we simply re-place the window at the coordinates which will magically solve the issue # This only seems to be an issue with unfullscreening, thus we check if we're fullscreen and the window wants to unfullscreen elif self.fullscreen and "fullscreen" not in state: self._reconfigure_floating(new_float_state=FloatStates.FULLSCREEN) for s in triggered: attr = s val = s in state if getattr(self, attr) != val: setattr(self, attr, val) @property def urgent(self): return self.hints["urgent"] or self._demands_attention @urgent.setter def urgent(self, val): self._demands_attention = val # TODO unset window hint as well? if not val: self.hints["urgent"] = False @expose_command() def info(self): if self.group: group = self.group.name else: group = None float_info = { "x": self.float_x, "y": self.float_y, "width": self._float_width, "height": self._float_height, } return dict( name=self.name, x=self.x, y=self.y, width=self.width, height=self.height, group=group, id=self.window.wid, wm_class=self.get_wm_class(), floating=self._float_state != FloatStates.NOT_FLOATING, float_info=float_info, maximized=self._float_state == FloatStates.MAXIMIZED, minimized=self._float_state == FloatStates.MINIMIZED, fullscreen=self._float_state == FloatStates.FULLSCREEN, ) @property def state(self): return self.window.get_wm_state()[0] @state.setter def state(self, val): if val in (WithdrawnState, NormalState, IconicState): self.window.set_property("WM_STATE", [val, 0]) @property def opacity(self): assert hasattr(self, "window") opacity = self.window.get_property("_NET_WM_WINDOW_OPACITY", unpack=int) if not opacity: return 1.0 else: value = opacity[0] # 2 decimal places as_float = round(value / 0xFFFFFFFF, 2) return as_float @opacity.setter def opacity(self, opacity: float) -> None: if 0.0 <= opacity <= 1.0: real_opacity = int(opacity * 0xFFFFFFFF) assert hasattr(self, "window") self.window.set_property("_NET_WM_WINDOW_OPACITY", real_opacity) @expose_command() def kill(self): if "WM_DELETE_WINDOW" in self.window.get_wm_protocols(): data = [ self.qtile.core.conn.atoms["WM_DELETE_WINDOW"], xcffib.xproto.Time.CurrentTime, 0, 0, 0, ] u = xcffib.xproto.ClientMessageData.synthetic(data, "I" * 5) e = xcffib.xproto.ClientMessageEvent.synthetic( format=32, window=self.window.wid, type=self.qtile.core.conn.atoms["WM_PROTOCOLS"], data=u, ) self.window.send_event(e) else: self.window.kill_client() self.qtile.core.conn.flush() def hide(self): # We don't want to get the UnmapNotify for this unmap with self.disable_mask(EventMask.StructureNotify): with self.qtile.core.disable_unmap_events(): self.window.unmap() self.hidden = True def unhide(self): self.window.map() self.state = NormalState self.hidden = False @contextlib.contextmanager def disable_mask(self, mask): self._disable_mask(mask) yield self._reset_mask() def _disable_mask(self, mask): self.window.set_attribute(eventmask=self._window_mask & (~mask)) def _reset_mask(self): self.window.set_attribute(eventmask=self._window_mask) def _grab_click(self): # Grab buttons 1 - 3 to focus upon click when unfocussed for amask in self.qtile.core._auto_modmasks(): for i in range(1, 4): self.qtile.core.conn.conn.core.GrabButton( True, self.window.wid, EventMask.ButtonPress, xcffib.xproto.GrabMode.Sync, xcffib.xproto.GrabMode.Async, xcffib.xproto.Atom._None, xcffib.xproto.Atom._None, i, amask, ) def _ungrab_click(self): # Ungrab buttons 1 - 3 when focussed self.qtile.core.conn.conn.core.UngrabButton( xcffib.xproto.Atom.Any, self.window.wid, xcffib.xproto.ModMask.Any, ) def get_pid(self): return self.window.get_net_wm_pid() @expose_command() def place( self, x, y, width, height, borderwidth, bordercolor, above=False, margin=None, respect_hints=False, ): """ Places the window at the specified location with the given size. Parameters ========== x: int y: int width: int height: int borderwidth: int bordercolor: string above: bool, optional margin: int or list, optional space around window as int or list of ints [N E S W] above: bool, optional If True, the geometry will be adjusted to respect hints provided by the client. """ # TODO: self.x/y/height/width are updated BEFORE # place is called, so there's no way to know if only # the position is changed, so we are sending # the ConfigureNotify every time place is called # # # if position change and size don't # # send a configure notify. See ICCCM 4.2.3 # send_notify = False # if (self.x != x or self.y != y) and \ # (self.width == width and self.height == height): # send_notify = True # #for now, we just: send_notify = True # Adjust the placement to account for layout margins, if there are any. if margin is not None: if isinstance(margin, int): margin = [margin] * 4 x += margin[3] y += margin[0] width -= margin[1] + margin[3] height -= margin[0] + margin[2] # Optionally adjust geometry to respect client hints if respect_hints: flags = self.hints.get("flags", {}) if "PMinSize" in flags: width = max(width, self.hints.get("min_width", 0)) height = max(height, self.hints.get("min_height", 0)) if "PMaxSize" in flags: width = min(width, self.hints.get("max_width", 0)) or width height = min(height, self.hints.get("max_height", 0)) or height if "PAspect" in flags and self._float_state == FloatStates.FLOATING: min_aspect = self.hints["min_aspect"] max_aspect = self.hints["max_aspect"] if width / height < min_aspect[0] / min_aspect[1]: height = width * min_aspect[1] // min_aspect[0] elif width / height > max_aspect[0] / max_aspect[1]: height = width * max_aspect[1] // max_aspect[0] if self.hints["base_width"] and self.hints["width_inc"]: width_adjustment = (width - self.hints["base_width"]) % self.hints["width_inc"] width -= width_adjustment if self.fullscreen: x += int(width_adjustment / 2) if self.hints["base_height"] and self.hints["height_inc"]: height_adjustment = (height - self.hints["base_height"]) % self.hints[ "height_inc" ] height -= height_adjustment if self.fullscreen: y += int(height_adjustment / 2) # save x and y float offset if self.group is not None and self.group.screen is not None: self.float_x = x - self.group.screen.x self.float_y = y - self.group.screen.y self.x = x self.y = y self.width = width self.height = height self.window.configure(x=x, y=y, width=width, height=height) if above: self.change_layer(up=True) self.paint_borders(bordercolor, borderwidth) if send_notify: self.send_configure_notify(x, y, width, height) def get_layering_information(self) -> tuple[bool, bool, bool, bool, bool, bool]: """ Get layer-related EMWH-flags https://specifications.freedesktop.org/wm-spec/1.3/ar01s07.html#STACKINGORDER Copied here: To obtain good interoperability between different Desktop Environments, the following layered stacking order is recommended, from the bottom: - windows of type _NET_WM_TYPE_DESKTOP - windows having state _NET_WM_STATE_BELOW - windows not belonging in any other layer - windows of type _NET_WM_TYPE_DOCK (unless they have state _NET_WM_TYPE_BELOW) and windows having state _NET_WM_STATE_ABOVE - focused windows having state _NET_WM_STATE_FULLSCREEN Windows that are transient for another window should be kept above this window. The window manager may choose to put some windows in different stacking positions, for example to allow the user to bring currently a active window to the top and return it back when the window loses focus. To this end, qtile adds an additional layer so that scratchpad windows are placed above all others, always. """ state = self.window.get_net_wm_state() _type = self.window.get_wm_type() or "" # Check if this window is focused active_window = self.qtile.core._root.get_property( "_NET_ACTIVE_WINDOW", "WINDOW", unpack=int ) if active_window and active_window[0] == self.window.wid: focus = True else: focus = False desktop = _type == "desktop" below = "_NET_WM_STATE_BELOW" in state dock = _type == "dock" above = "_NET_WM_STATE_ABOVE" in state full = ( "fullscreen" in state ) # get_net_wm_state translates this state so we don't use _NET_WM name is_scratchpad = isinstance(self.qtile.groups_map.get(self.group), ScratchPad) # sort the flags from bottom to top, True meaning further below than False at each step states = [desktop, below, above or (dock and not below), full and focus, is_scratchpad] other = not any(states) states.insert(2, other) # If we're a desktop, this should always be the lowest layer... if desktop: # mypy can't work out that this gives us tuple[bool, bool, bool, bool, bool, bool]... # (True, False, False, False, False, False) return tuple(not i for i in range(6)) # type: ignore # ...otherwise, we set to the highest matching layer. # Look for the highest matching level and then set all other levels to False highest = max(i for i, state in enumerate(states) if state) # mypy can't work out that this gives us tuple[bool, bool, bool, bool, bool, bool]... return tuple(i == highest for i in range(6)) # type: ignore def change_layer(self, up=True, top_bottom=False): """Raise a window above its peers or move it below them, depending on 'up'. Raising a normal window will not lift it above pinned windows etc. There are a few important things to take note of when relaying windows: 1. If a window has a defined parent, it should not be moved underneath it. In case children are blocking, this could leave an application in an unusable state. 2. If a window has children, they should be moved along with it. 3. If a window has a defined parent, either move the parent or do nothing at all. 4. EMWH-flags follow strict layering rules: https://specifications.freedesktop.org/wm-spec/1.3/ar01s07.html#STACKINGORDER """ if len(self.qtile.windows_map) < 2: return if self.group is None and not isinstance(self, Static): return # Use the window's group or current group if this isn't set (e.g. Static windows) group = self.group or self.qtile.current_group parent = self.window.get_wm_transient_for() if parent is not None and not up: return layering = self.get_layering_information() # Comparison of layer states: -1 if window is now in a lower state group, # 0 if it's in the same group and 1 if it's in a higher group moved = (self.previous_layer > layering) - (layering > self.previous_layer) self.previous_layer = layering stack = list(self.qtile.core._root.query_tree()) if self.wid not in stack or len(stack) < 2: return # Get all windows for the group and add Static windows to ensure these are included # in the stacking group_windows = group.windows.copy() statics = [win for win in self.qtile.windows_map.values() if isinstance(win, Static)] group_windows.extend(statics) if group.screen is not None: group_bars = [gap for gap in group.screen.gaps if isinstance(gap, bar.Bar)] else: group_bars = [] # Get list of windows that are in the stack and managed by qtile # List of tuples (XWindow object, transient_for, layering_information) windows = list( map( lambda w: ( w.window, w.window.get_wm_transient_for(), w.get_layering_information(), ), group_windows, ) ) # Remove any windows that aren't in the server's stack windows = list(filter(lambda w: w[0].wid in stack, windows)) # Sort this list to match stacking order reported by server windows.sort(key=lambda w: stack.index(w[0].wid)) # Get lists of windows on lower, higher or same "layer" as window lower = [w[0].wid for w in windows if w[2] > layering] higher = [w[0].wid for w in windows if w[2] < layering] same = [w[0].wid for w in windows if w[2] == layering] # We now need to identify the new position in the stack # If the window has a parent, the window should just be put above it # If the parent isn't being managed by qtile then it may not be stacked correctly if parent and parent in self.qtile.windows_map: # If the window is modal then it should be placed above every other window that is in that window group # e.g. the parent of the dialog and any other window that is also transient for the same parent. if "_NET_WM_STATE_MODAL" in self.window.get_net_wm_state(): window_group = [parent] window_group.extend( k for k, v in self.qtile.windows_map.items() if v.window.get_wm_transient_for() == parent ) window_group.sort(key=stack.index) # Make sure we're above the last window in that group sibling = window_group[-1] else: sibling = parent above = True # Now we just check whether the window has changed layer. # If we're forcing to top or bottom of current layer... elif top_bottom: # If there are no other windows in the same layer then there's nothing to do if not same: return if up: sibling = same[-1] above = True else: sibling = same[0] above = False # There are no windows in the desired layer (should never happen) or # we've moved to a new layer and are the only window in that layer elif not same or (len(same) == 1 and moved != 0): # Try to put it above the last window in the lower layers if lower: sibling = lower[-1] above = True # Or below the first window in the higher layers elif higher: sibling = higher[0] above = False # Don't think we should end up here but, if we do... else: # Put the window above the highest window if we're raising it if up: sibling = stack[-1] above = True # or below the lowest window if we're lowering the window else: sibling = stack[0] above = False else: # Window has moved to a lower layer state if moved < 0: if self.kept_below: sibling = same[0] above = False else: sibling = same[-1] above = True # Window is in same layer state elif moved == 0: try: pos = same.index(self.wid) except ValueError: pos = len(same) if up else 0 if not up: pos = max(0, pos - 1) else: pos = min(pos + 1, len(same) - 1) sibling = same[pos] above = up # Window is in a higher layer else: if self.kept_above: sibling = same[-1] above = True else: sibling = same[0] above = False # If the sibling is the current window then we just check if any windows in lower/higher layers are # stacked incorrectly and, if so, restack them. However, we don't need to configure stacking for this # window if sibling == self.wid: index = stack.index(self.wid) # We need to make sure the bars are included so add them now if group_bars: for group_bar in group_bars: bar_layer = group_bar.window.get_layering_information() if bar_layer > layering: lower.append(group_bar.window.wid) elif bar_layer < layering: higher.append(group_bar.window.wid) # Sort the list to match the server's stacking order lower.sort(key=lambda wid: stack.index(wid)) higher.sort(key=lambda wid: stack.index(wid)) for wid in [w for w in lower if stack.index(w) > index]: self.qtile.windows_map[wid].window.configure( stackmode=xcffib.xproto.StackMode.Below, sibling=same[0] ) # We reverse higher as each window will be placed above the last item in the current layer # this means the last item we stack will be just above the current layer. for wid in [w for w in higher[::-1] if stack.index(w) < index]: self.qtile.windows_map[wid].window.configure( stackmode=xcffib.xproto.StackMode.Above, sibling=same[-1] ) return # Window needs new stacking info. We tell the server to stack the window # above or below a given "sibling" self.window.configure( stackmode=xcffib.xproto.StackMode.Above if above else xcffib.xproto.StackMode.Below, sibling=sibling, ) # Move window's children if we were moved upwards if above: self.raise_children(stack=stack) self.qtile.core.update_client_lists() def raise_children(self, stack=None): """Ensure any transient windows are moved up with the parent.""" children = [ k for k, v in self.qtile.windows_map.items() if v.window.get_wm_transient_for() == self.window.wid ] if children: if stack is None: stack = list(self.qtile.core._root.query_tree()) parent = self.window.wid children.sort(key=stack.index) for child in children: self.qtile.windows_map[child].window.configure( stackmode=xcffib.xproto.StackMode.Above, sibling=parent ) parent = child def paint_borders(self, color, width): self.borderwidth = width self.bordercolor = color self.window.configure(borderwidth=width) self.window.paint_borders(self.depth, color, width, self.width, self.height) def send_configure_notify(self, x, y, width, height): """Send a synthetic ConfigureNotify""" window = self.window.wid above_sibling = False override_redirect = False event = xcffib.xproto.ConfigureNotifyEvent.synthetic( event=window, window=window, above_sibling=above_sibling, x=x, y=y, width=width, height=height, border_width=self.borderwidth, override_redirect=override_redirect, ) self.window.send_event(event, mask=EventMask.StructureNotify) @property def can_steal_focus(self): return super().can_steal_focus and self.window.get_wm_type() != "notification" @can_steal_focus.setter def can_steal_focus(self, can_steal_focus: bool) -> None: self._can_steal_focus = can_steal_focus def _do_focus(self): """ Focus the window if we can, and return whether or not it was successful. """ # don't focus hidden windows, they should be mapped. this is generally # a bug somewhere in the qtile code, but some of the tests do it, so we # just have to let it slide for now. if self.hidden: return False # if the window can be focused, just focus it. if self.hints["input"]: self.window.set_input_focus() return True # does the window want us to ask it about focus? if "WM_TAKE_FOCUS" in self.window.get_wm_protocols(): data = [ self.qtile.core.conn.atoms["WM_TAKE_FOCUS"], # The timestamp here must be a valid timestamp, not CurrentTime. # # see https://tronche.com/gui/x/icccm/sec-4.html#s-4.1.7 # > Windows with the atom WM_TAKE_FOCUS in their WM_PROTOCOLS # > property may receive a ClientMessage event from the # > window manager (as described in section 4.2.8) with # > WM_TAKE_FOCUS in its data[0] field and a valid timestamp # > (i.e. not *CurrentTime* ) in its data[1] field. self.qtile.core.get_valid_timestamp(), 0, 0, 0, ] u = xcffib.xproto.ClientMessageData.synthetic(data, "I" * 5) e = xcffib.xproto.ClientMessageEvent.synthetic( format=32, window=self.window.wid, type=self.qtile.core.conn.atoms["WM_PROTOCOLS"], data=u, ) self.window.send_event(e) return True # we didn't focus this time. but now the window knows if it wants # focus, it should SetFocus() itself; we'll get another notification # about this. return False @expose_command() def focus(self, warp: bool = True) -> None: """Focuses the window.""" did_focus = self._do_focus() if not did_focus: return # now, do all the other WM stuff since the focus actually changed if warp and self.qtile.config.cursor_warp: self.window.warp_pointer(self.width // 2, self.height // 2) # update net wm state state = list(self.window.get_property("_NET_WM_STATE", "ATOM", unpack=int)) state_focused = self.qtile.core.conn.atoms["_NET_WM_STATE_FOCUSED"] state.append(state_focused) if self.urgent: self.urgent = False atom = self.qtile.core.conn.atoms["_NET_WM_STATE_DEMANDS_ATTENTION"] if atom in state: state.remove(atom) self.window.set_property("_NET_WM_STATE", state) # re-grab button events on the previously focussed window old = self.qtile.core._root.get_property("_NET_ACTIVE_WINDOW", "WINDOW", unpack=int) if old and old[0] in self.qtile.windows_map: old_win = self.qtile.windows_map[old[0]] if not isinstance(old_win, base.Internal): old_win._grab_click() state = list(old_win.window.get_property("_NET_WM_STATE", "ATOM", unpack=int)) if state_focused in state: state.remove(state_focused) old_win.window.set_property("_NET_WM_STATE", state) self.qtile.core._root.set_property("_NET_ACTIVE_WINDOW", self.window.wid) self._ungrab_click() # Check if we need to restack a previously focused fullscreen window self.qtile.core.check_stacking(self) if self.group and self.group.current_window is not self: self.group.focus(self) hook.fire("client_focus", self) @expose_command() def get_hints(self): """Returns the X11 hints (WM_HINTS and WM_SIZE_HINTS) for this window.""" return self.hints @expose_command() def inspect(self): """Tells you more than you ever wanted to know about a window""" a = self.window.get_attributes() attrs = { "backing_store": a.backing_store, "visual": a.visual, "class": a._class, "bit_gravity": a.bit_gravity, "win_gravity": a.win_gravity, "backing_planes": a.backing_planes, "backing_pixel": a.backing_pixel, "save_under": a.save_under, "map_is_installed": a.map_is_installed, "map_state": a.map_state, "override_redirect": a.override_redirect, # "colormap": a.colormap, "all_event_masks": a.all_event_masks, "your_event_mask": a.your_event_mask, "do_not_propagate_mask": a.do_not_propagate_mask, } props = self.window.list_properties() normalhints = self.window.get_wm_normal_hints() hints = self.window.get_wm_hints() protocols = [] for i in self.window.get_wm_protocols(): protocols.append(i) state = self.window.get_wm_state() float_info = { "x": self.float_x, "y": self.float_y, "width": self._float_width, "height": self._float_height, } return dict( attributes=attrs, properties=props, name=self.window.get_name(), wm_class=self.get_wm_class(), wm_window_role=self.window.get_wm_window_role(), wm_type=self.window.get_wm_type(), wm_transient_for=self.window.get_wm_transient_for(), protocols=protocols, wm_icon_name=self.window.get_wm_icon_name(), wm_client_machine=self.window.get_wm_client_machine(), normalhints=normalhints, hints=hints, state=state, float_info=float_info, ) @expose_command() def keep_above(self, enable: bool | None = None): if enable is None: self.kept_above = not self.kept_above else: self.kept_above = enable self.change_layer(top_bottom=True, up=True) @expose_command() def keep_below(self, enable: bool | None = None): if enable is None: self.kept_below = not self.kept_below else: self.kept_below = enable self.change_layer(top_bottom=True, up=False) @expose_command() def move_up(self, force=False): if self.kept_below and force: self.kept_below = False with self.qtile.core.masked(): # Disable masks so that moving windows along the Z axis doesn't trigger # focus change events (i.e. due to `follow_mouse_focus`) self.change_layer() @expose_command() def move_down(self, force=False): if self.kept_above and force: self.kept_above = False with self.qtile.core.masked(): self.change_layer(up=False) @expose_command() def move_to_top(self, force=False): if self.kept_below and force: self.kept_below = False with self.qtile.core.masked(): self.change_layer(top_bottom=True) @expose_command() def move_to_bottom(self, force=False): if self.kept_above and force: self.kept_above = False with self.qtile.core.masked(): self.change_layer(up=False, top_bottom=True) @property def kept_above(self): reply = list(self.window.get_property("_NET_WM_STATE", "ATOM", unpack=int)) atom = self.qtile.core.conn.atoms["_NET_WM_STATE_ABOVE"] return atom in reply @kept_above.setter def kept_above(self, value): reply = list(self.window.get_property("_NET_WM_STATE", "ATOM", unpack=int)) atom = self.qtile.core.conn.atoms["_NET_WM_STATE_ABOVE"] if value and atom not in reply: reply.append(atom) elif not value and atom in reply: reply.remove(atom) else: return atom = self.qtile.core.conn.atoms["_NET_WM_STATE_BELOW"] if atom in reply: reply.remove(atom) self.window.set_property("_NET_WM_STATE", reply) self.change_layer() @property def kept_below(self): reply = list(self.window.get_property("_NET_WM_STATE", "ATOM", unpack=int)) atom = self.qtile.core.conn.atoms["_NET_WM_STATE_BELOW"] return atom in reply @kept_below.setter def kept_below(self, value): reply = list(self.window.get_property("_NET_WM_STATE", "ATOM", unpack=int)) atom = self.qtile.core.conn.atoms["_NET_WM_STATE_BELOW"] if value and atom not in reply: reply.append(atom) elif not value and atom in reply: reply.remove(atom) else: return atom = self.qtile.core.conn.atoms["_NET_WM_STATE_ABOVE"] if atom in reply: reply.remove(atom) self.window.set_property("_NET_WM_STATE", reply) self.change_layer(up=False) @expose_command() def bring_to_front(self): if self.get_wm_type() != "desktop": self.window.configure(stackmode=xcffib.xproto.StackMode.Above) self.raise_children() self.qtile.core.update_client_lists() class Internal(_Window, base.Internal): """An internal window, that should not be managed by qtile""" _window_mask = ( EventMask.StructureNotify | EventMask.PropertyChange | EventMask.EnterWindow | EventMask.LeaveWindow | EventMask.PointerMotion | EventMask.FocusChange | EventMask.Exposure | EventMask.ButtonPress | EventMask.ButtonRelease | EventMask.KeyPress ) def __init__(self, win, qtile, desired_depth=32): _Window.__init__(self, win, qtile) win.set_property("QTILE_INTERNAL", 1) self._depth = desired_depth def create_drawer(self, width: int, height: int) -> Drawer: """Create a Drawer that draws to this window.""" return Drawer(self.qtile, self, width, height) @expose_command() def kill(self): if self.window.wid in self.qtile.windows_map: # It will be present during config reloads; absent during shutdown as this # will follow graceful_shutdown with contextlib.suppress(xcffib.ConnectionException): self.qtile.core.conn.conn.core.DestroyWindow(self.window.wid) def handle_Expose(self, e): # noqa: N802 self.process_window_expose() def handle_ButtonPress(self, e): # noqa: N802 self.process_button_click(e.event_x, e.event_y, e.detail) def handle_ButtonRelease(self, e): # noqa: N802 self.process_button_release(e.event_x, e.event_y, e.detail) # return True to ensure Core also processes the release return True def handle_EnterNotify(self, e): # noqa: N802 self.process_pointer_enter(e.event_x, e.event_y) def handle_LeaveNotify(self, e): # noqa: N802 self.process_pointer_leave(e.event_x, e.event_y) def handle_MotionNotify(self, e): # noqa: N802 self.process_pointer_motion(e.event_x, e.event_y) def handle_KeyPress(self, e): # noqa: N802 mask = xcbq.ModMasks["shift"] | xcbq.ModMasks["lock"] state = 1 if e.state & mask else 0 keysym = self.qtile.core.conn.code_to_syms[e.detail][state] self.process_key_press(keysym) def info(self): return dict( x=self.x, y=self.y, width=self.width, height=self.height, id=self.window.wid, ) @expose_command() def focus(self, warp: bool = True) -> None: """Focuses the window.""" self._do_focus() class Static(_Window, base.Static): """An static window, belonging to a screen rather than a group""" _window_mask = ( EventMask.StructureNotify | EventMask.PropertyChange | EventMask.EnterWindow | EventMask.FocusChange | EventMask.Exposure ) def __init__(self, win, qtile, screen, x=None, y=None, width=None, height=None): _Window.__init__(self, win, qtile) self._wm_class: list[str] | None = None self.update_wm_class() self.update_name() self.conf_x = x self.conf_y = y self.conf_width = width self.conf_height = height x = x or self.x y = y or self.y self.x = x + screen.x self.y = y + screen.y self.screen = screen self.place(self.x, self.y, width or self.width, height or self.height, 0, 0) self.unhide() self.update_strut() self._grab_click() def handle_ConfigureRequest(self, e): # noqa: N802 cw = xcffib.xproto.ConfigWindow if self.conf_x is None and e.value_mask & cw.X: self.x = e.x if self.conf_y is None and e.value_mask & cw.Y: self.y = e.y if self.conf_width is None and e.value_mask & cw.Width: self.width = e.width if self.conf_height is None and e.value_mask & cw.Height: self.height = e.height self.place( self.x, self.y, self.width, self.height, self.borderwidth, self.bordercolor, ) return False def update_strut(self): strut = self.window.get_property("_NET_WM_STRUT_PARTIAL", unpack=int) if strut: x_screen_dimensions = self.qtile.core._root.get_geometry() if strut[0]: # left x = strut[0] y = (strut[4] + strut[5]) / 2 or (strut[6] + strut[7]) / 2 elif strut[1]: # right x = x_screen_dimensions.width - strut[1] y = (strut[4] + strut[5]) / 2 or (strut[6] + strut[7]) / 2 elif strut[2]: # top x = (strut[8] + strut[9]) / 2 or (strut[10] + strut[11]) / 2 y = strut[2] else: # bottom x = (strut[8] + strut[9]) / 2 or (strut[10] + strut[11]) / 2 y = x_screen_dimensions.height - strut[3] self.screen = self.qtile.find_screen(x, y) if self.screen is None: logger.error("No screen at target") return elif None in [self.screen.x, self.screen.y, self.screen.height, self.screen.width]: logger.error("Missing screen information") return empty_space = [ self.screen.x, x_screen_dimensions.width - self.screen.x - self.screen.width, self.screen.y, x_screen_dimensions.height - self.screen.y - self.screen.height, ] self.reserved_space = tuple( strut[i] - empty if strut[i] else 0 for i, empty in enumerate(empty_space) ) self.qtile.reserve_space(self.reserved_space, self.screen) else: self.reserved_space = None def handle_PropertyNotify(self, e): # noqa: N802 name = self.qtile.core.conn.atoms.get_name(e.atom) if name == "_NET_WM_STRUT_PARTIAL": self.update_strut() class Window(_Window, base.Window): _window_mask = ( EventMask.StructureNotify | EventMask.PropertyChange | EventMask.EnterWindow | EventMask.FocusChange ) def __init__(self, window, qtile): _Window.__init__(self, window, qtile) self._wm_class: list[str] | None = None self.update_wm_class() self.update_name() self.set_group() # add window to the save-set, so it gets mapped when qtile dies qtile.core.conn.conn.core.ChangeSaveSet(SetMode.Insert, self.window.wid) self.update_wm_net_icon() self._grab_click() @property def group(self): return self._group @group.setter def group(self, group): if group: try: self.window.set_property("_NET_WM_DESKTOP", self.qtile.groups.index(group)) except xcffib.xproto.WindowError: logger.exception("whoops, got error setting _NET_WM_DESKTOP, too early?") self._group = group @property def edges(self): return (self.x, self.y, self.x + self.width, self.y + self.height) @property def floating(self): return self._float_state != FloatStates.NOT_FLOATING @floating.setter def floating(self, do_float): stack = self.qtile.core._root.query_tree() tiled = [win.window.wid for win in (self.group.tiled_windows if self.group else [])] tiled_stack = [wid for wid in stack if wid in tiled and wid != self.window.wid] if do_float and self._float_state == FloatStates.NOT_FLOATING: if self.is_placed(): screen = self.group.screen self._enablefloating( screen.x + self.float_x, screen.y + self.float_y, self._float_width, self._float_height, ) # Make sure floating window is placed above tiled windows if tiled_stack and (not self.kept_above or self.qtile.config.floats_kept_above): stack_list = list(stack) highest_tile = tiled_stack[-1] if stack_list.index(self.window.wid) < stack_list.index(highest_tile): self.window.configure( stackmode=xcffib.xproto.StackMode.Above, sibling=highest_tile ) else: # if we are setting floating early, e.g. from a hook, we don't have a screen yet self._float_state = FloatStates.FLOATING if not self.kept_above and self.qtile.config.floats_kept_above: self.keep_above(enable=True) elif (not do_float) and self._float_state != FloatStates.NOT_FLOATING: self.update_fullscreen_wm_state(False) if self._float_state == FloatStates.FLOATING: # store last size self._float_width = self.width self._float_height = self.height self._float_state = FloatStates.NOT_FLOATING self.group.mark_floating(self, False) if tiled_stack: self.window.configure( stackmode=xcffib.xproto.StackMode.Above, sibling=tiled_stack[-1] ) hook.fire("float_change") @property def wants_to_fullscreen(self): try: return "fullscreen" in self.window.get_net_wm_state() except (xcffib.xproto.WindowError, xcffib.xproto.AccessError): pass return False @expose_command() def toggle_floating(self): self.floating = not self.floating def set_wm_state(self, old_state, new_state): if new_state != old_state: self.window.set_property("_NET_WM_STATE", list(new_state)) def update_fullscreen_wm_state(self, do_full): # already done updating previously if do_full == self.fullscreen: return # update fullscreen _NET_WM_STATE atom = set([self.qtile.core.conn.atoms["_NET_WM_STATE_FULLSCREEN"]]) prev_state = set(self.window.get_property("_NET_WM_STATE", "ATOM", unpack=int)) if do_full: self.set_wm_state(prev_state, prev_state | atom) else: self.set_wm_state(prev_state, prev_state - atom) @property def fullscreen(self): return self._float_state == FloatStates.FULLSCREEN @fullscreen.setter def fullscreen(self, do_full): if do_full: needs_change = self._float_state != FloatStates.FULLSCREEN screen = self.group.screen or self.qtile.find_closest_screen(self.x, self.y) if self._float_state not in (FloatStates.MAXIMIZED, FloatStates.FULLSCREEN): self._save_geometry() bw = self.group.floating_layout.fullscreen_border_width self._enablefloating( screen.x, screen.y, screen.width - 2 * bw, screen.height - 2 * bw, new_float_state=FloatStates.FULLSCREEN, ) # Only restack layers if floating state has changed if needs_change: self.change_layer() return if self._float_state == FloatStates.FULLSCREEN: self._restore_geometry() self.floating = False self.change_layer() return @property def maximized(self): return self._float_state == FloatStates.MAXIMIZED @maximized.setter def maximized(self, do_maximize): if do_maximize: screen = self.group.screen or self.qtile.find_closest_screen(self.x, self.y) if self._float_state not in (FloatStates.MAXIMIZED, FloatStates.FULLSCREEN): self._save_geometry() bw = self.group.floating_layout.max_border_width self._enablefloating( screen.dx, screen.dy, screen.dwidth - 2 * bw, screen.dheight - 2 * bw, new_float_state=FloatStates.MAXIMIZED, ) else: if self._float_state == FloatStates.MAXIMIZED: self._restore_geometry() self.floating = False @property def minimized(self): return self._float_state == FloatStates.MINIMIZED @minimized.setter def minimized(self, do_minimize): if do_minimize: if self._float_state != FloatStates.MINIMIZED: self._enablefloating(new_float_state=FloatStates.MINIMIZED) else: if self._float_state == FloatStates.MINIMIZED: self.floating = False @expose_command() def toggle_minimize(self): self.minimized = not self.minimized @expose_command() def is_visible(self) -> bool: return not self.hidden and not self.minimized @expose_command() def static( self, screen: int | None = None, x: int | None = None, y: int | None = None, width: int | None = None, height: int | None = None, ) -> None: """Makes this window a static window, attached to a Screen If any of the arguments are left unspecified, the values given by the window itself are used instead. So, for a window that's aware of its appropriate size and location (like dzen), you don't have to specify anything. """ self.defunct = True if screen is None: screen = self.qtile.current_screen else: screen = self.qtile.screens[screen] if self.group: self.group.remove(self) s = Static(self.window, self.qtile, screen, x, y, width, height) self.qtile.windows_map[self.window.wid] = s self.qtile.core.update_client_lists() hook.fire("client_managed", s) def tweak_float(self, x=None, y=None, dx=0, dy=0, w=None, h=None, dw=0, dh=0): if x is not None: self.x = x self.x += dx if y is not None: self.y = y self.y += dy if w is not None: self.width = w self.width += dw if h is not None: self.height = h self.height += dh if self.height < 0: self.height = 0 if self.width < 0: self.width = 0 screen = self.qtile.find_closest_screen( self.x + self.width // 2, self.y + self.height // 2 ) if self.group and screen is not None and screen != self.group.screen: self.group.remove(self, force=True) screen.group.add(self, force=True) self.qtile.focus_screen(screen.index) self._reconfigure_floating() @expose_command() def get_size(self): return (self.width, self.height) @expose_command() def get_position(self): return (self.x, self.y) def _reconfigure_floating(self, new_float_state=FloatStates.FLOATING): self.update_fullscreen_wm_state(new_float_state == FloatStates.FULLSCREEN) if new_float_state == FloatStates.MINIMIZED: self.state = IconicState self.hide() else: self.place( self.x, self.y, self.width, self.height, self.borderwidth, self.bordercolor, above=False, respect_hints=True, ) if self._float_state != new_float_state: self._float_state = new_float_state if self.group: # may be not, if it's called from hook self.group.mark_floating(self, True) if new_float_state == FloatStates.FLOATING: if self.qtile.config.floats_kept_above: self.keep_above(enable=True) elif new_float_state == FloatStates.MAXIMIZED: self.move_to_top() hook.fire("float_change") def _enablefloating( self, x=None, y=None, w=None, h=None, new_float_state=FloatStates.FLOATING ): if new_float_state != FloatStates.MINIMIZED: self.x = x self.y = y self.width = w self.height = h self._reconfigure_floating(new_float_state=new_float_state) def set_group(self): # add to group by position according to _NET_WM_DESKTOP property group = None index = self.window.get_wm_desktop() if index is not None and index < len(self.qtile.groups): group = self.qtile.groups[index] elif index is None: transient_for = self.is_transient_for() if transient_for is not None: group = transient_for._group if group is not None: group.add(self) self._group = group if group != self.qtile.current_screen.group: self.hide() @expose_command() def togroup(self, group_name=None, *, switch_group=False, toggle=False): """Move window to a specified group Also switch to that group if switch_group is True. If `toggle` is True and and the specified group is already on the screen, use the last used group as target instead. """ if group_name is None: group = self.qtile.current_group else: group = self.qtile.groups_map.get(group_name) if group is None: raise CommandError(f"No such group: {group_name}") if self.group is group: if toggle and self.group.screen.previous_group: group = self.group.screen.previous_group else: return self.hide() if self.group: if self.group.screen: # for floats remove window offset self.x -= self.group.screen.x group_ref = self.group self.group.remove(self) if ( not self.qtile.dgroups.groups_map[group_ref.name].persist and len(group_ref.windows) <= 1 ): # set back original group so _del() can grab it self.group = group_ref self.qtile.dgroups._del(self) self.group = None if group.screen and self.x < group.screen.x: self.x += group.screen.x group.add(self) if switch_group: group.toscreen(toggle=toggle) @expose_command() def match(self, match): """Match window against given attributes. Parameters ========== match: a config.Match object """ try: return match.compare(self) except (xcffib.xproto.WindowError, xcffib.xproto.AccessError): return False def handle_EnterNotify(self, e): # noqa: N802 hook.fire("client_mouse_enter", self) if self.qtile.config.follow_mouse_focus is True: if self.group.current_window != self: self.group.focus(self, False) if self.group.screen and self.qtile.current_screen != self.group.screen: self.qtile.focus_screen(self.group.screen.index, False) return True def handle_ButtonPress(self, e): # noqa: N802 self.qtile.core.focus_by_click(e, window=self) self.qtile.core.conn.conn.core.AllowEvents(xcffib.xproto.Allow.ReplayPointer, e.time) def handle_ConfigureRequest(self, e): # noqa: N802 if self.qtile._drag and self.qtile.current_window == self: # ignore requests while user is dragging window return if getattr(self, "floating", False): # only obey resize for floating windows cw = xcffib.xproto.ConfigWindow width = e.width if e.value_mask & cw.Width else self.width height = e.height if e.value_mask & cw.Height else self.height x = e.x if e.value_mask & cw.X else self.x y = e.y if e.value_mask & cw.Y else self.y else: width, height, x, y = self.width, self.height, self.x, self.y if self.group and self.group.screen: self.place( x, y, width, height, self.borderwidth, self.bordercolor, ) self.update_state() return False def update_wm_net_icon(self): """Set a dict with the icons of the window""" icon = self.window.get_property("_NET_WM_ICON", "CARDINAL") if not icon: return icon = list(map(ord, icon.value)) icons = {} while True: if not icon: break size = icon[:8] if len(size) != 8 or not size[0] or not size[4]: break icon = icon[8:] width = size[0] height = size[4] next_pix = width * height * 4 data = icon[:next_pix] arr = array.array("B", data) for i in range(0, len(arr), 4): mult = arr[i + 3] / 255.0 arr[i + 0] = int(arr[i + 0] * mult) arr[i + 1] = int(arr[i + 1] * mult) arr[i + 2] = int(arr[i + 2] * mult) icon = icon[next_pix:] icons[f"{width}x{height}"] = arr self.icons = icons hook.fire("net_wm_icon_change", self) def handle_ClientMessage(self, event): # noqa: N802 atoms = self.qtile.core.conn.atoms opcode = event.type data = event.data if atoms["_NET_WM_STATE"] == opcode: prev_state = self.window.get_property("_NET_WM_STATE", "ATOM", unpack=int) current_state = set(prev_state) action = data.data32[0] for prop in (data.data32[1], data.data32[2]): if not prop: # skip 0 continue if action == _NET_WM_STATE_REMOVE: current_state.discard(prop) elif action == _NET_WM_STATE_ADD: current_state.add(prop) elif action == _NET_WM_STATE_TOGGLE: current_state ^= set([prop]) # toggle :D self.window.set_property("_NET_WM_STATE", list(current_state)) elif atoms["_NET_ACTIVE_WINDOW"] == opcode: source = data.data32[0] if source == 2: # XCB_EWMH_CLIENT_SOURCE_TYPE_NORMAL logger.debug("Focusing window by pager") self.qtile.current_screen.set_group(self.group) self.group.focus(self) self.bring_to_front() else: # XCB_EWMH_CLIENT_SOURCE_TYPE_OTHER focus_behavior = self.qtile.config.focus_on_window_activation if ( focus_behavior == "focus" or type(focus_behavior) is FunctionType and focus_behavior(self) ): logger.debug("Focusing window") # Windows belonging to a scratchpad need to be toggled properly if isinstance(self.group, ScratchPad): for dropdown in self.group.dropdowns.values(): if dropdown.window is self: dropdown.show() break else: self.qtile.current_screen.set_group(self.group) self.group.focus(self) elif focus_behavior == "smart": if not self.group.screen: logger.debug( "Ignoring focus request (focus_on_window_activation='smart')" ) return if self.group.screen == self.qtile.current_screen: logger.debug("Focusing window") # Windows belonging to a scratchpad need to be toggled properly if isinstance(self.group, ScratchPad): for dropdown in self.group.dropdowns.values(): if dropdown.window is self: dropdown.show() break else: self.qtile.current_screen.set_group(self.group) self.group.focus(self) else: # self.group.screen != self.qtile.current_screen: logger.debug("Setting urgent flag for window") self.urgent = True hook.fire("client_urgent_hint_changed", self) elif focus_behavior == "urgent": logger.debug("Setting urgent flag for window") self.urgent = True hook.fire("client_urgent_hint_changed", self) elif focus_behavior == "never": logger.debug("Ignoring focus request (focus_on_window_activation='never')") else: logger.debug( "Invalid value for focus_on_window_activation: %s", focus_behavior ) elif atoms["_NET_CLOSE_WINDOW"] == opcode: self.kill() elif atoms["WM_CHANGE_STATE"] == opcode: state = data.data32[0] if state == NormalState: self.minimized = False elif state == IconicState and self.qtile.config.auto_minimize: self.minimized = True elif atoms["_NET_WM_DESKTOP"] == opcode: group_index = data.data32[0] try: group = self.qtile.groups[group_index] self.togroup(group.name) except (IndexError, TypeError): logger.warning("Unexpected _NET_WM_DESKTOP value received: %s", group_index) else: logger.debug("Unhandled client message: %s", atoms.get_name(opcode)) def handle_PropertyNotify(self, e): # noqa: N802 name = self.qtile.core.conn.atoms.get_name(e.atom) if name == "WM_TRANSIENT_FOR": pass elif name == "WM_CLASS": self.update_wm_class() elif name == "WM_HINTS": self.update_hints() elif name == "WM_NORMAL_HINTS": self.update_hints() elif name == "WM_NAME": self.update_name() elif name == "_NET_WM_NAME": self.update_name() elif name == "_NET_WM_VISIBLE_NAME": self.update_name() elif name == "WM_ICON_NAME": pass elif name == "_NET_WM_ICON_NAME": pass elif name == "_NET_WM_ICON": self.update_wm_net_icon() elif name == "ZOOM": pass elif name == "_NET_WM_WINDOW_OPACITY": pass elif name == "WM_STATE": pass elif name == "_NET_WM_STATE": self.update_state() elif name == "WM_PROTOCOLS": pass elif name == "_NET_WM_DESKTOP": # Some windows set the state(fullscreen) when starts, # update_state is here because the group and the screen # are set when the property is emitted # self.update_state() self.update_state() else: logger.debug("Unknown window property: %s", name) return False def _items(self, name: str) -> ItemT: if name == "group": return True, [] if name == "layout": if self.group: return True, list(range(len(self.group.layouts))) return None if name == "screen": if self.group and self.group.screen: return True, [] return None def _select(self, name, sel): if name == "group": return self.group elif name == "layout": if sel is None: return self.group.layout else: return utils.lget(self.group.layouts, sel) elif name == "screen": return self.group.screen @expose_command() def move_floating(self, dx, dy): """Move window by dx and dy""" self.tweak_float(dx=dx, dy=dy) @expose_command() def resize_floating(self, dw, dh): """Add dw and dh to size of window""" self.tweak_float(dw=dw, dh=dh) @expose_command() def set_position_floating(self, x, y): """Move window to x and y""" self.tweak_float(x=x, y=y) @expose_command() def set_size_floating(self, w, h): """Set window dimensions to w and h""" self.tweak_float(w=w, h=h) @expose_command() def enable_floating(self): self.floating = True @expose_command() def disable_floating(self): self.floating = False @expose_command() def toggle_maximize(self): self.maximized = not self.maximized @expose_command() def toggle_fullscreen(self): self.fullscreen = not self.fullscreen @expose_command() def enable_fullscreen(self): self.fullscreen = True @expose_command() def disable_fullscreen(self): self.fullscreen = False def _is_in_window(self, x, y, window): return window.edges[0] <= x <= window.edges[2] and window.edges[1] <= y <= window.edges[3] @expose_command() def set_position(self, x, y): if self.floating: self.tweak_float(x, y) return curx, cury = self.qtile.core.get_mouse_position() for window in self.group.windows: if window == self or window.floating: continue if self._is_in_window(curx, cury, window): self.group.layout.swap(self, window) return @expose_command def focus(self, warp: bool = True) -> None: """Focus the window.""" _Window.focus(self, warp) # Focusing a fullscreen window puts it into a different layer # priority group. If it's not there already, we need to move it. if self.fullscreen and not self.previous_layer[4]: self.change_layer() qtile-0.31.0/libqtile/backend/x11/xcursors.py0000664000175000017500000001102614762660347020705 0ustar epsilonepsilonfrom libqtile.backend.x11.xcursors_ffi import xcursors_ffi as ffi from libqtile.log_utils import logger # Stolen from samurai-x # (Don't know where to put it, so I'll put it here) # XCB cursors doesn't want to be themed, libxcursor # would be better choice I think # and we (indirectly) depend on it anyway... class Cursors(dict): def __init__(self, conn): self.conn = conn cursors = ( (b"X_cursor", 0), (b"arrow", 2), (b"based_arrow_down", 4), (b"based_arrow_up", 6), (b"boat", 8), (b"bogosity", 10), (b"bottom_left_corner", 12), (b"bottom_right_corner", 14), (b"bottom_side", 16), (b"bottom_tee", 18), (b"box_spiral", 20), (b"center_ptr", 22), (b"circle", 24), (b"clock", 26), (b"coffee_mug", 28), (b"cross", 30), (b"cross_reverse", 32), (b"crosshair", 34), (b"diamond_cross", 36), (b"dot", 38), (b"dotbox", 40), (b"double_arrow", 42), (b"draft_large", 44), (b"draft_small", 46), (b"draped_box", 48), (b"exchange", 50), (b"fleur", 52), (b"gobbler", 54), (b"gumby", 56), (b"hand1", 58), (b"hand2", 60), (b"heart", 62), (b"icon", 64), (b"iron_cross", 66), (b"left_ptr", 68), (b"left_side", 70), (b"left_tee", 72), (b"leftbutton", 74), (b"ll_angle", 76), (b"lr_angle", 78), (b"man", 80), (b"middlebutton", 82), (b"mouse", 84), (b"pencil", 86), (b"pirate", 88), (b"plus", 90), (b"question_arrow", 92), (b"right_ptr", 94), (b"right_side", 96), (b"right_tee", 98), (b"rightbutton", 100), (b"rtl_logo", 102), (b"sailboat", 104), (b"sb_down_arrow", 106), (b"sb_h_double_arrow", 108), (b"sb_left_arrow", 110), (b"sb_right_arrow", 112), (b"sb_up_arrow", 114), (b"sb_v_double_arrow", 116), (b"shuttle", 118), (b"sizing", 120), (b"spider", 122), (b"spraycan", 124), (b"star", 126), (b"target", 128), (b"tcross", 130), (b"top_left_arrow", 132), (b"top_left_corner", 134), (b"top_right_corner", 136), (b"top_side", 138), (b"top_tee", 140), (b"trek", 142), (b"ul_angle", 144), (b"umbrella", 146), (b"ur_angle", 148), (b"watch", 150), (b"xterm", 152), ) self.xcursor = self._setup_xcursor_binding() for name, cursor_font in cursors: self._new(name, cursor_font) if self.xcursor: self.xcursor.xcb_cursor_context_free(self._cursor_ctx[0]) def finalize(self): self._cursor_ctx = None def _setup_xcursor_binding(self): try: xcursor = ffi.dlopen("libxcb-cursor.so.0") except Exception: logger.info("xcb-cursor not found, fallback to font pointer") return False conn = self.conn.conn screen_pointer = conn.get_screen_pointers()[0] self._cursor_ctx = ffi.new("xcb_cursor_context_t **") xcursor.xcb_cursor_context_new(conn._conn, screen_pointer, self._cursor_ctx) return xcursor def get_xcursor(self, name): """ Get the cursor using xcb-util-cursor, so we support themed cursors """ cursor = self.xcursor.xcb_cursor_load_cursor(self._cursor_ctx[0], name) return cursor def get_font_cursor(self, name, cursor_font): """ Get the cursor from the font, used as a fallback if xcb-util-cursor is not installed """ fid = self.conn.conn.generate_id() self.conn.conn.core.OpenFont(fid, len("cursor"), "cursor") cursor = self.conn.conn.generate_id() self.conn.conn.core.CreateGlyphCursor( cursor, fid, fid, cursor_font, cursor_font + 1, 0, 0, 0, 65535, 65535, 65535 ) return cursor def _new(self, name, cursor_font): if self.xcursor: cursor = self.get_xcursor(name) else: cursor = self.get_font_cursor(name, cursor_font) self[name.decode()] = cursor qtile-0.31.0/libqtile/backend/x11/xkeysyms.py0000664000175000017500000017413014762660347020717 0ustar epsilonepsilon# Copyright (c) 2010 Aldo Cortesi # Copyright (c) 2012 Julian Berman # Copyright (c) 2014 Björn Lässig # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. keysyms = { "XF86ModeLock": 0x1008FF01, "XF86MonBrightnessUp": 0x1008FF02, "XF86MonBrightnessDown": 0x1008FF03, "XF86KbdLightOnOff": 0x1008FF04, "XF86KbdBrightnessUp": 0x1008FF05, "XF86KbdBrightnessDown": 0x1008FF06, "XF86Standby": 0x1008FF10, "XF86AudioLowerVolume": 0x1008FF11, "XF86AudioMute": 0x1008FF12, "XF86AudioRaiseVolume": 0x1008FF13, "XF86AudioPlay": 0x1008FF14, "XF86AudioStop": 0x1008FF15, "XF86AudioPrev": 0x1008FF16, "XF86AudioNext": 0x1008FF17, "XF86HomePage": 0x1008FF18, "XF86Mail": 0x1008FF19, "XF86Start": 0x1008FF1A, "XF86Search": 0x1008FF1B, "XF86AudioRecord": 0x1008FF1C, "XF86Calculator": 0x1008FF1D, "XF86Memo": 0x1008FF1E, "XF86ToDoList": 0x1008FF1F, "XF86Calendar": 0x1008FF20, "XF86PowerDown": 0x1008FF21, "XF86ContrastAdjust": 0x1008FF22, "XF86RockerUp": 0x1008FF23, "XF86RockerDown": 0x1008FF24, "XF86RockerEnter": 0x1008FF25, "XF86Back": 0x1008FF26, "XF86Forward": 0x1008FF27, "XF86Stop": 0x1008FF28, "XF86Refresh": 0x1008FF29, "XF86PowerOff": 0x1008FF2A, "XF86WakeUp": 0x1008FF2B, "XF86Eject": 0x1008FF2C, "XF86ScreenSaver": 0x1008FF2D, "XF86WWW": 0x1008FF2E, "XF86Sleep": 0x1008FF2F, "XF86Favorites": 0x1008FF30, "XF86AudioPause": 0x1008FF31, "XF86AudioMedia": 0x1008FF32, "XF86MyComputer": 0x1008FF33, "XF86VendorHome": 0x1008FF34, "XF86LightBulb": 0x1008FF35, "XF86Shop": 0x1008FF36, "XF86History": 0x1008FF37, "XF86OpenURL": 0x1008FF38, "XF86AddFavorite": 0x1008FF39, "XF86HotLinks": 0x1008FF3A, "XF86BrightnessAdjust": 0x1008FF3B, "XF86Finance": 0x1008FF3C, "XF86Community": 0x1008FF3D, "XF86AudioRewind": 0x1008FF3E, "XF86BackForward": 0x1008FF3F, "XF86Launch0": 0x1008FF40, "XF86Launch1": 0x1008FF41, "XF86Launch2": 0x1008FF42, "XF86Launch3": 0x1008FF43, "XF86Launch4": 0x1008FF44, "XF86Launch5": 0x1008FF45, "XF86Launch6": 0x1008FF46, "XF86Launch7": 0x1008FF47, "XF86Launch8": 0x1008FF48, "XF86Launch9": 0x1008FF49, "XF86LaunchA": 0x1008FF4A, "XF86LaunchB": 0x1008FF4B, "XF86LaunchC": 0x1008FF4C, "XF86LaunchD": 0x1008FF4D, "XF86LaunchE": 0x1008FF4E, "XF86LaunchF": 0x1008FF4F, "XF86ApplicationLeft": 0x1008FF50, "XF86ApplicationRight": 0x1008FF51, "XF86Book": 0x1008FF52, "XF86CD": 0x1008FF53, "XF86Calculater": 0x1008FF54, "XF86Clear": 0x1008FF55, "XF86Close": 0x1008FF56, "XF86Copy": 0x1008FF57, "XF86Cut": 0x1008FF58, "XF86Display": 0x1008FF59, "XF86DOS": 0x1008FF5A, "XF86Documents": 0x1008FF5B, "XF86Excel": 0x1008FF5C, "XF86Explorer": 0x1008FF5D, "XF86Game": 0x1008FF5E, "XF86Go": 0x1008FF5F, "XF86iTouch": 0x1008FF60, "XF86LogOff": 0x1008FF61, "XF86Market": 0x1008FF62, "XF86Meeting": 0x1008FF63, "XF86MenuKB": 0x1008FF65, "XF86MenuPB": 0x1008FF66, "XF86MySites": 0x1008FF67, "XF86New": 0x1008FF68, "XF86News": 0x1008FF69, "XF86OfficeHome": 0x1008FF6A, "XF86Open": 0x1008FF6B, "XF86Option": 0x1008FF6C, "XF86Paste": 0x1008FF6D, "XF86Phone": 0x1008FF6E, "XF86Q": 0x1008FF70, "XF86Reply": 0x1008FF72, "XF86Reload": 0x1008FF73, "XF86RotateWindows": 0x1008FF74, "XF86RotationPB": 0x1008FF75, "XF86RotationKB": 0x1008FF76, "XF86Save": 0x1008FF77, "XF86ScrollUp": 0x1008FF78, "XF86ScrollDown": 0x1008FF79, "XF86ScrollClick": 0x1008FF7A, "XF86Send": 0x1008FF7B, "XF86Spell": 0x1008FF7C, "XF86SplitScreen": 0x1008FF7D, "XF86Support": 0x1008FF7E, "XF86TaskPane": 0x1008FF7F, "XF86Terminal": 0x1008FF80, "XF86Tools": 0x1008FF81, "XF86Travel": 0x1008FF82, "XF86UserPB": 0x1008FF84, "XF86User1KB": 0x1008FF85, "XF86User2KB": 0x1008FF86, "XF86Video": 0x1008FF87, "XF86WheelButton": 0x1008FF88, "XF86Word": 0x1008FF89, "XF86Xfer": 0x1008FF8A, "XF86ZoomIn": 0x1008FF8B, "XF86ZoomOut": 0x1008FF8C, "XF86Away": 0x1008FF8D, "XF86Messenger": 0x1008FF8E, "XF86WebCam": 0x1008FF8F, "XF86MailForward": 0x1008FF90, "XF86Pictures": 0x1008FF91, "XF86Music": 0x1008FF92, "XF86Battery": 0x1008FF93, "XF86Bluetooth": 0x1008FF94, "XF86WLAN": 0x1008FF95, "XF86UWB": 0x1008FF96, "XF86AudioForward": 0x1008FF97, "XF86AudioRepeat": 0x1008FF98, "XF86AudioRandomPlay": 0x1008FF99, "XF86Subtitle": 0x1008FF9A, "XF86AudioCycleTrack": 0x1008FF9B, "XF86CycleAngle": 0x1008FF9C, "XF86FrameBack": 0x1008FF9D, "XF86FrameForward": 0x1008FF9E, "XF86Time": 0x1008FF9F, "XF86Select": 0x1008FFA0, "XF86View": 0x1008FFA1, "XF86TopMenu": 0x1008FFA2, "XF86Red": 0x1008FFA3, "XF86Green": 0x1008FFA4, "XF86Yellow": 0x1008FFA5, "XF86Blue": 0x1008FFA6, "XF86Suspend": 0x1008FFA7, "XF86Hibernate": 0x1008FFA8, "XF86TouchpadToggle": 0x1008FFA9, "XF86TouchpadOn": 0x1008FFB0, "XF86TouchpadOff": 0x1008FFB1, "XF86AudioMicMute": 0x1008FFB2, "XF86Keyboard": 0x1008FFB3, "XF86Switch_VT_1": 0x1008FE01, "XF86Switch_VT_2": 0x1008FE02, "XF86Switch_VT_3": 0x1008FE03, "XF86Switch_VT_4": 0x1008FE04, "XF86Switch_VT_5": 0x1008FE05, "XF86Switch_VT_6": 0x1008FE06, "XF86Switch_VT_7": 0x1008FE07, "XF86Switch_VT_8": 0x1008FE08, "XF86Switch_VT_9": 0x1008FE09, "XF86Switch_VT_10": 0x1008FE0A, "XF86Switch_VT_11": 0x1008FE0B, "XF86Switch_VT_12": 0x1008FE0C, "XF86Ungrab": 0x1008FE20, "XF86ClearGrab": 0x1008FE21, "XF86Next_VMode": 0x1008FE22, "XF86Prev_VMode": 0x1008FE23, "XF86LogWindowTree": 0x1008FE24, "XF86LogGrabInfo": 0x1008FE25, "VoidSymbol": 0xFFFFFF, "BackSpace": 0xFF08, "Tab": 0xFF09, "Linefeed": 0xFF0A, "Clear": 0xFF0B, "Return": 0xFF0D, "Pause": 0xFF13, "Scroll_Lock": 0xFF14, "Sys_Req": 0xFF15, "Escape": 0xFF1B, "Delete": 0xFFFF, "Multi_key": 0xFF20, "Codeinput": 0xFF37, "SingleCandidate": 0xFF3C, "MultipleCandidate": 0xFF3D, "PreviousCandidate": 0xFF3E, "Kanji": 0xFF21, "Muhenkan": 0xFF22, "Henkan_Mode": 0xFF23, "Henkan": 0xFF23, "Romaji": 0xFF24, "Hiragana": 0xFF25, "Katakana": 0xFF26, "Hiragana_Katakana": 0xFF27, "Zenkaku": 0xFF28, "Hankaku": 0xFF29, "Zenkaku_Hankaku": 0xFF2A, "Touroku": 0xFF2B, "Massyo": 0xFF2C, "Kana_Lock": 0xFF2D, "Kana_Shift": 0xFF2E, "Eisu_Shift": 0xFF2F, "Eisu_toggle": 0xFF30, "Kanji_Bangou": 0xFF37, "Zen_Koho": 0xFF3D, "Mae_Koho": 0xFF3E, "Home": 0xFF50, "Left": 0xFF51, "Up": 0xFF52, "Right": 0xFF53, "Down": 0xFF54, "Prior": 0xFF55, "Page_Up": 0xFF55, "Next": 0xFF56, "Page_Down": 0xFF56, "End": 0xFF57, "Begin": 0xFF58, "Select": 0xFF60, "Print": 0xFF61, "Execute": 0xFF62, "Insert": 0xFF63, "Undo": 0xFF65, "Redo": 0xFF66, "Menu": 0xFF67, "Find": 0xFF68, "Cancel": 0xFF69, "Help": 0xFF6A, "Break": 0xFF6B, "Mode_switch": 0xFF7E, "script_switch": 0xFF7E, "Num_Lock": 0xFF7F, "KP_Space": 0xFF80, "KP_Tab": 0xFF89, "KP_Enter": 0xFF8D, "KP_F1": 0xFF91, "KP_F2": 0xFF92, "KP_F3": 0xFF93, "KP_F4": 0xFF94, "KP_Home": 0xFF95, "KP_Left": 0xFF96, "KP_Up": 0xFF97, "KP_Right": 0xFF98, "KP_Down": 0xFF99, "KP_Prior": 0xFF9A, "KP_Page_Up": 0xFF9A, "KP_Next": 0xFF9B, "KP_Page_Down": 0xFF9B, "KP_End": 0xFF9C, "KP_Begin": 0xFF9D, "KP_Insert": 0xFF9E, "KP_Delete": 0xFF9F, "KP_Equal": 0xFFBD, "KP_Multiply": 0xFFAA, "KP_Add": 0xFFAB, "KP_Separator": 0xFFAC, "KP_Subtract": 0xFFAD, "KP_Decimal": 0xFFAE, "KP_Divide": 0xFFAF, "KP_0": 0xFFB0, "KP_1": 0xFFB1, "KP_2": 0xFFB2, "KP_3": 0xFFB3, "KP_4": 0xFFB4, "KP_5": 0xFFB5, "KP_6": 0xFFB6, "KP_7": 0xFFB7, "KP_8": 0xFFB8, "KP_9": 0xFFB9, "F1": 0xFFBE, "F2": 0xFFBF, "F3": 0xFFC0, "F4": 0xFFC1, "F5": 0xFFC2, "F6": 0xFFC3, "F7": 0xFFC4, "F8": 0xFFC5, "F9": 0xFFC6, "F10": 0xFFC7, "F11": 0xFFC8, "L1": 0xFFC8, "F12": 0xFFC9, "L2": 0xFFC9, "F13": 0xFFCA, "L3": 0xFFCA, "F14": 0xFFCB, "L4": 0xFFCB, "F15": 0xFFCC, "L5": 0xFFCC, "F16": 0xFFCD, "L6": 0xFFCD, "F17": 0xFFCE, "L7": 0xFFCE, "F18": 0xFFCF, "L8": 0xFFCF, "F19": 0xFFD0, "L9": 0xFFD0, "F20": 0xFFD1, "L10": 0xFFD1, "F21": 0xFFD2, "R1": 0xFFD2, "F22": 0xFFD3, "R2": 0xFFD3, "F23": 0xFFD4, "R3": 0xFFD4, "F24": 0xFFD5, "R4": 0xFFD5, "F25": 0xFFD6, "R5": 0xFFD6, "F26": 0xFFD7, "R6": 0xFFD7, "F27": 0xFFD8, "R7": 0xFFD8, "F28": 0xFFD9, "R8": 0xFFD9, "F29": 0xFFDA, "R9": 0xFFDA, "F30": 0xFFDB, "R10": 0xFFDB, "F31": 0xFFDC, "R11": 0xFFDC, "F32": 0xFFDD, "R12": 0xFFDD, "F33": 0xFFDE, "R13": 0xFFDE, "F34": 0xFFDF, "R14": 0xFFDF, "F35": 0xFFE0, "R15": 0xFFE0, "Shift_L": 0xFFE1, "Shift_R": 0xFFE2, "Control_L": 0xFFE3, "Control_R": 0xFFE4, "Caps_Lock": 0xFFE5, "Shift_Lock": 0xFFE6, "Meta_L": 0xFFE7, "Meta_R": 0xFFE8, "Alt_L": 0xFFE9, "Alt_R": 0xFFEA, "Super_L": 0xFFEB, "Super_R": 0xFFEC, "Hyper_L": 0xFFED, "Hyper_R": 0xFFEE, "ISO_Lock": 0xFE01, "ISO_Level2_Latch": 0xFE02, "ISO_Level3_Shift": 0xFE03, "ISO_Level3_Latch": 0xFE04, "ISO_Level3_Lock": 0xFE05, "ISO_Level5_Shift": 0xFE11, "ISO_Level5_Latch": 0xFE12, "ISO_Level5_Lock": 0xFE13, "ISO_Group_Shift": 0xFF7E, "ISO_Group_Latch": 0xFE06, "ISO_Group_Lock": 0xFE07, "ISO_Next_Group": 0xFE08, "ISO_Next_Group_Lock": 0xFE09, "ISO_Prev_Group": 0xFE0A, "ISO_Prev_Group_Lock": 0xFE0B, "ISO_First_Group": 0xFE0C, "ISO_First_Group_Lock": 0xFE0D, "ISO_Last_Group": 0xFE0E, "ISO_Last_Group_Lock": 0xFE0F, "ISO_Left_Tab": 0xFE20, "ISO_Move_Line_Up": 0xFE21, "ISO_Move_Line_Down": 0xFE22, "ISO_Partial_Line_Up": 0xFE23, "ISO_Partial_Line_Down": 0xFE24, "ISO_Partial_Space_Left": 0xFE25, "ISO_Partial_Space_Right": 0xFE26, "ISO_Set_Margin_Left": 0xFE27, "ISO_Set_Margin_Right": 0xFE28, "ISO_Release_Margin_Left": 0xFE29, "ISO_Release_Margin_Right": 0xFE2A, "ISO_Release_Both_Margins": 0xFE2B, "ISO_Fast_Cursor_Left": 0xFE2C, "ISO_Fast_Cursor_Right": 0xFE2D, "ISO_Fast_Cursor_Up": 0xFE2E, "ISO_Fast_Cursor_Down": 0xFE2F, "ISO_Continuous_Underline": 0xFE30, "ISO_Discontinuous_Underline": 0xFE31, "ISO_Emphasize": 0xFE32, "ISO_Center_Object": 0xFE33, "ISO_Enter": 0xFE34, "dead_grave": 0xFE50, "dead_acute": 0xFE51, "dead_circumflex": 0xFE52, "dead_tilde": 0xFE53, "dead_perispomeni": 0xFE53, "dead_macron": 0xFE54, "dead_breve": 0xFE55, "dead_abovedot": 0xFE56, "dead_diaeresis": 0xFE57, "dead_abovering": 0xFE58, "dead_doubleacute": 0xFE59, "dead_caron": 0xFE5A, "dead_cedilla": 0xFE5B, "dead_ogonek": 0xFE5C, "dead_iota": 0xFE5D, "dead_voiced_sound": 0xFE5E, "dead_semivoiced_sound": 0xFE5F, "dead_belowdot": 0xFE60, "dead_hook": 0xFE61, "dead_horn": 0xFE62, "dead_stroke": 0xFE63, "dead_abovecomma": 0xFE64, "dead_psili": 0xFE64, "dead_abovereversedcomma": 0xFE65, "dead_dasia": 0xFE65, "dead_doublegrave": 0xFE66, "dead_belowring": 0xFE67, "dead_belowmacron": 0xFE68, "dead_belowcircumflex": 0xFE69, "dead_belowtilde": 0xFE6A, "dead_belowbreve": 0xFE6B, "dead_belowdiaeresis": 0xFE6C, "dead_invertedbreve": 0xFE6D, "dead_belowcomma": 0xFE6E, "dead_currency": 0xFE6F, "dead_a": 0xFE80, "dead_A": 0xFE81, "dead_e": 0xFE82, "dead_E": 0xFE83, "dead_i": 0xFE84, "dead_I": 0xFE85, "dead_o": 0xFE86, "dead_O": 0xFE87, "dead_u": 0xFE88, "dead_U": 0xFE89, "dead_small_schwa": 0xFE8A, "dead_capital_schwa": 0xFE8B, "First_Virtual_Screen": 0xFED0, "Prev_Virtual_Screen": 0xFED1, "Next_Virtual_Screen": 0xFED2, "Last_Virtual_Screen": 0xFED4, "Terminate_Server": 0xFED5, "AccessX_Enable": 0xFE70, "AccessX_Feedback_Enable": 0xFE71, "RepeatKeys_Enable": 0xFE72, "SlowKeys_Enable": 0xFE73, "BounceKeys_Enable": 0xFE74, "StickyKeys_Enable": 0xFE75, "MouseKeys_Enable": 0xFE76, "MouseKeys_Accel_Enable": 0xFE77, "Overlay1_Enable": 0xFE78, "Overlay2_Enable": 0xFE79, "AudibleBell_Enable": 0xFE7A, "Pointer_Left": 0xFEE0, "Pointer_Right": 0xFEE1, "Pointer_Up": 0xFEE2, "Pointer_Down": 0xFEE3, "Pointer_UpLeft": 0xFEE4, "Pointer_UpRight": 0xFEE5, "Pointer_DownLeft": 0xFEE6, "Pointer_DownRight": 0xFEE7, "Pointer_Button_Dflt": 0xFEE8, "Pointer_Button1": 0xFEE9, "Pointer_Button2": 0xFEEA, "Pointer_Button3": 0xFEEB, "Pointer_Button4": 0xFEEC, "Pointer_Button5": 0xFEED, "Pointer_DblClick_Dflt": 0xFEEE, "Pointer_DblClick1": 0xFEEF, "Pointer_DblClick2": 0xFEF0, "Pointer_DblClick3": 0xFEF1, "Pointer_DblClick4": 0xFEF2, "Pointer_DblClick5": 0xFEF3, "Pointer_Drag_Dflt": 0xFEF4, "Pointer_Drag1": 0xFEF5, "Pointer_Drag2": 0xFEF6, "Pointer_Drag3": 0xFEF7, "Pointer_Drag4": 0xFEF8, "Pointer_Drag5": 0xFEFD, "Pointer_EnableKeys": 0xFEF9, "Pointer_Accelerate": 0xFEFA, "Pointer_DfltBtnNext": 0xFEFB, "Pointer_DfltBtnPrev": 0xFEFC, "3270_Duplicate": 0xFD01, "3270_FieldMark": 0xFD02, "3270_Right2": 0xFD03, "3270_Left2": 0xFD04, "3270_BackTab": 0xFD05, "3270_EraseEOF": 0xFD06, "3270_EraseInput": 0xFD07, "3270_Reset": 0xFD08, "3270_Quit": 0xFD09, "3270_PA1": 0xFD0A, "3270_PA2": 0xFD0B, "3270_PA3": 0xFD0C, "3270_Test": 0xFD0D, "3270_Attn": 0xFD0E, "3270_CursorBlink": 0xFD0F, "3270_AltCursor": 0xFD10, "3270_KeyClick": 0xFD11, "3270_Jump": 0xFD12, "3270_Ident": 0xFD13, "3270_Rule": 0xFD14, "3270_Copy": 0xFD15, "3270_Play": 0xFD16, "3270_Setup": 0xFD17, "3270_Record": 0xFD18, "3270_ChangeScreen": 0xFD19, "3270_DeleteWord": 0xFD1A, "3270_ExSelect": 0xFD1B, "3270_CursorSelect": 0xFD1C, "3270_PrintScreen": 0xFD1D, "3270_Enter": 0xFD1E, "space": 0x0020, "exclam": 0x0021, "quotedbl": 0x0022, "numbersign": 0x0023, "dollar": 0x0024, "percent": 0x0025, "ampersand": 0x0026, "apostrophe": 0x0027, "quoteright": 0x0027, "parenleft": 0x0028, "parenright": 0x0029, "asterisk": 0x002A, "plus": 0x002B, "comma": 0x002C, "minus": 0x002D, "period": 0x002E, "slash": 0x002F, "0": 0x0030, "1": 0x0031, "2": 0x0032, "3": 0x0033, "4": 0x0034, "5": 0x0035, "6": 0x0036, "7": 0x0037, "8": 0x0038, "9": 0x0039, "colon": 0x003A, "semicolon": 0x003B, "less": 0x003C, "equal": 0x003D, "greater": 0x003E, "question": 0x003F, "at": 0x0040, "A": 0x0041, "B": 0x0042, "C": 0x0043, "D": 0x0044, "E": 0x0045, "F": 0x0046, "G": 0x0047, "H": 0x0048, "I": 0x0049, "J": 0x004A, "K": 0x004B, "L": 0x004C, "M": 0x004D, "N": 0x004E, "O": 0x004F, "P": 0x0050, "Q": 0x0051, "R": 0x0052, "S": 0x0053, "T": 0x0054, "U": 0x0055, "V": 0x0056, "W": 0x0057, "X": 0x0058, "Y": 0x0059, "Z": 0x005A, "bracketleft": 0x005B, "backslash": 0x005C, "bracketright": 0x005D, "asciicircum": 0x005E, "underscore": 0x005F, "grave": 0x0060, "quoteleft": 0x0060, "a": 0x0061, "b": 0x0062, "c": 0x0063, "d": 0x0064, "e": 0x0065, "f": 0x0066, "g": 0x0067, "h": 0x0068, "i": 0x0069, "j": 0x006A, "k": 0x006B, "l": 0x006C, "m": 0x006D, "n": 0x006E, "o": 0x006F, "p": 0x0070, "q": 0x0071, "r": 0x0072, "s": 0x0073, "t": 0x0074, "u": 0x0075, "v": 0x0076, "w": 0x0077, "x": 0x0078, "y": 0x0079, "z": 0x007A, "braceleft": 0x007B, "bar": 0x007C, "braceright": 0x007D, "asciitilde": 0x007E, "nobreakspace": 0x00A0, "exclamdown": 0x00A1, "cent": 0x00A2, "sterling": 0x00A3, "currency": 0x00A4, "yen": 0x00A5, "brokenbar": 0x00A6, "section": 0x00A7, "diaeresis": 0x00A8, "copyright": 0x00A9, "ordfeminine": 0x00AA, "guillemotleft": 0x00AB, "notsign": 0x00AC, "hyphen": 0x00AD, "registered": 0x00AE, "macron": 0x00AF, "degree": 0x00B0, "plusminus": 0x00B1, "twosuperior": 0x00B2, "threesuperior": 0x00B3, "acute": 0x00B4, "mu": 0x00B5, "paragraph": 0x00B6, "periodcentered": 0x00B7, "cedilla": 0x00B8, "onesuperior": 0x00B9, "masculine": 0x00BA, "guillemotright": 0x00BB, "onequarter": 0x00BC, "onehalf": 0x00BD, "threequarters": 0x00BE, "questiondown": 0x00BF, "Agrave": 0x00C0, "Aacute": 0x00C1, "Acircumflex": 0x00C2, "Atilde": 0x00C3, "Adiaeresis": 0x00C4, "Aring": 0x00C5, "AE": 0x00C6, "Ccedilla": 0x00C7, "Egrave": 0x00C8, "Eacute": 0x00C9, "Ecircumflex": 0x00CA, "Ediaeresis": 0x00CB, "Igrave": 0x00CC, "Iacute": 0x00CD, "Icircumflex": 0x00CE, "Idiaeresis": 0x00CF, "ETH": 0x00D0, "Eth": 0x00D0, "Ntilde": 0x00D1, "Ograve": 0x00D2, "Oacute": 0x00D3, "Ocircumflex": 0x00D4, "Otilde": 0x00D5, "Odiaeresis": 0x00D6, "multiply": 0x00D7, "Oslash": 0x00D8, "Ooblique": 0x00D8, "Ugrave": 0x00D9, "Uacute": 0x00DA, "Ucircumflex": 0x00DB, "Udiaeresis": 0x00DC, "Yacute": 0x00DD, "THORN": 0x00DE, "Thorn": 0x00DE, "ssharp": 0x00DF, "agrave": 0x00E0, "aacute": 0x00E1, "acircumflex": 0x00E2, "atilde": 0x00E3, "adiaeresis": 0x00E4, "aring": 0x00E5, "ae": 0x00E6, "ccedilla": 0x00E7, "egrave": 0x00E8, "eacute": 0x00E9, "ecircumflex": 0x00EA, "ediaeresis": 0x00EB, "igrave": 0x00EC, "iacute": 0x00ED, "icircumflex": 0x00EE, "idiaeresis": 0x00EF, "eth": 0x00F0, "ntilde": 0x00F1, "ograve": 0x00F2, "oacute": 0x00F3, "ocircumflex": 0x00F4, "otilde": 0x00F5, "odiaeresis": 0x00F6, "division": 0x00F7, "oslash": 0x00F8, "ooblique": 0x00F8, "ugrave": 0x00F9, "uacute": 0x00FA, "ucircumflex": 0x00FB, "udiaeresis": 0x00FC, "yacute": 0x00FD, "thorn": 0x00FE, "ydiaeresis": 0x00FF, "Aogonek": 0x01A1, "breve": 0x01A2, "Lstroke": 0x01A3, "Lcaron": 0x01A5, "Sacute": 0x01A6, "Scaron": 0x01A9, "Scedilla": 0x01AA, "Tcaron": 0x01AB, "Zacute": 0x01AC, "Zcaron": 0x01AE, "Zabovedot": 0x01AF, "aogonek": 0x01B1, "ogonek": 0x01B2, "lstroke": 0x01B3, "lcaron": 0x01B5, "sacute": 0x01B6, "caron": 0x01B7, "scaron": 0x01B9, "scedilla": 0x01BA, "tcaron": 0x01BB, "zacute": 0x01BC, "doubleacute": 0x01BD, "zcaron": 0x01BE, "zabovedot": 0x01BF, "Racute": 0x01C0, "Abreve": 0x01C3, "Lacute": 0x01C5, "Cacute": 0x01C6, "Ccaron": 0x01C8, "Eogonek": 0x01CA, "Ecaron": 0x01CC, "Dcaron": 0x01CF, "Dstroke": 0x01D0, "Nacute": 0x01D1, "Ncaron": 0x01D2, "Odoubleacute": 0x01D5, "Rcaron": 0x01D8, "Uring": 0x01D9, "Udoubleacute": 0x01DB, "Tcedilla": 0x01DE, "racute": 0x01E0, "abreve": 0x01E3, "lacute": 0x01E5, "cacute": 0x01E6, "ccaron": 0x01E8, "eogonek": 0x01EA, "ecaron": 0x01EC, "dcaron": 0x01EF, "dstroke": 0x01F0, "nacute": 0x01F1, "ncaron": 0x01F2, "odoubleacute": 0x01F5, "udoubleacute": 0x01FB, "rcaron": 0x01F8, "uring": 0x01F9, "tcedilla": 0x01FE, "abovedot": 0x01FF, "Hstroke": 0x02A1, "Hcircumflex": 0x02A6, "Iabovedot": 0x02A9, "Gbreve": 0x02AB, "Jcircumflex": 0x02AC, "hstroke": 0x02B1, "hcircumflex": 0x02B6, "idotless": 0x02B9, "gbreve": 0x02BB, "jcircumflex": 0x02BC, "Cabovedot": 0x02C5, "Ccircumflex": 0x02C6, "Gabovedot": 0x02D5, "Gcircumflex": 0x02D8, "Ubreve": 0x02DD, "Scircumflex": 0x02DE, "cabovedot": 0x02E5, "ccircumflex": 0x02E6, "gabovedot": 0x02F5, "gcircumflex": 0x02F8, "ubreve": 0x02FD, "scircumflex": 0x02FE, "kra": 0x03A2, "kappa": 0x03A2, "Rcedilla": 0x03A3, "Itilde": 0x03A5, "Lcedilla": 0x03A6, "Emacron": 0x03AA, "Gcedilla": 0x03AB, "Tslash": 0x03AC, "rcedilla": 0x03B3, "itilde": 0x03B5, "lcedilla": 0x03B6, "emacron": 0x03BA, "gcedilla": 0x03BB, "tslash": 0x03BC, "ENG": 0x03BD, "eng": 0x03BF, "Amacron": 0x03C0, "Iogonek": 0x03C7, "Eabovedot": 0x03CC, "Imacron": 0x03CF, "Ncedilla": 0x03D1, "Omacron": 0x03D2, "Kcedilla": 0x03D3, "Uogonek": 0x03D9, "Utilde": 0x03DD, "Umacron": 0x03DE, "amacron": 0x03E0, "iogonek": 0x03E7, "eabovedot": 0x03EC, "imacron": 0x03EF, "ncedilla": 0x03F1, "omacron": 0x03F2, "kcedilla": 0x03F3, "uogonek": 0x03F9, "utilde": 0x03FD, "umacron": 0x03FE, "Babovedot": 0x1001E02, "babovedot": 0x1001E03, "Dabovedot": 0x1001E0A, "Wgrave": 0x1001E80, "Wacute": 0x1001E82, "dabovedot": 0x1001E0B, "Ygrave": 0x1001EF2, "Fabovedot": 0x1001E1E, "fabovedot": 0x1001E1F, "Mabovedot": 0x1001E40, "mabovedot": 0x1001E41, "Pabovedot": 0x1001E56, "wgrave": 0x1001E81, "pabovedot": 0x1001E57, "wacute": 0x1001E83, "Sabovedot": 0x1001E60, "ygrave": 0x1001EF3, "Wdiaeresis": 0x1001E84, "wdiaeresis": 0x1001E85, "sabovedot": 0x1001E61, "Wcircumflex": 0x1000174, "Tabovedot": 0x1001E6A, "Ycircumflex": 0x1000176, "wcircumflex": 0x1000175, "tabovedot": 0x1001E6B, "ycircumflex": 0x1000177, "OE": 0x13BC, "oe": 0x13BD, "Ydiaeresis": 0x13BE, "overline": 0x047E, "kana_fullstop": 0x04A1, "kana_openingbracket": 0x04A2, "kana_closingbracket": 0x04A3, "kana_comma": 0x04A4, "kana_conjunctive": 0x04A5, "kana_middledot": 0x04A5, "kana_WO": 0x04A6, "kana_a": 0x04A7, "kana_i": 0x04A8, "kana_u": 0x04A9, "kana_e": 0x04AA, "kana_o": 0x04AB, "kana_ya": 0x04AC, "kana_yu": 0x04AD, "kana_yo": 0x04AE, "kana_tsu": 0x04AF, "kana_tu": 0x04AF, "prolongedsound": 0x04B0, "kana_A": 0x04B1, "kana_I": 0x04B2, "kana_U": 0x04B3, "kana_E": 0x04B4, "kana_O": 0x04B5, "kana_KA": 0x04B6, "kana_KI": 0x04B7, "kana_KU": 0x04B8, "kana_KE": 0x04B9, "kana_KO": 0x04BA, "kana_SA": 0x04BB, "kana_SHI": 0x04BC, "kana_SU": 0x04BD, "kana_SE": 0x04BE, "kana_SO": 0x04BF, "kana_TA": 0x04C0, "kana_CHI": 0x04C1, "kana_TI": 0x04C1, "kana_TSU": 0x04C2, "kana_TU": 0x04C2, "kana_TE": 0x04C3, "kana_TO": 0x04C4, "kana_NA": 0x04C5, "kana_NI": 0x04C6, "kana_NU": 0x04C7, "kana_NE": 0x04C8, "kana_NO": 0x04C9, "kana_HA": 0x04CA, "kana_HI": 0x04CB, "kana_FU": 0x04CC, "kana_HU": 0x04CC, "kana_HE": 0x04CD, "kana_HO": 0x04CE, "kana_MA": 0x04CF, "kana_MI": 0x04D0, "kana_MU": 0x04D1, "kana_ME": 0x04D2, "kana_MO": 0x04D3, "kana_YA": 0x04D4, "kana_YU": 0x04D5, "kana_YO": 0x04D6, "kana_RA": 0x04D7, "kana_RI": 0x04D8, "kana_RU": 0x04D9, "kana_RE": 0x04DA, "kana_RO": 0x04DB, "kana_WA": 0x04DC, "kana_N": 0x04DD, "voicedsound": 0x04DE, "semivoicedsound": 0x04DF, "kana_switch": 0xFF7E, "Farsi_0": 0x10006F0, "Farsi_1": 0x10006F1, "Farsi_2": 0x10006F2, "Farsi_3": 0x10006F3, "Farsi_4": 0x10006F4, "Farsi_5": 0x10006F5, "Farsi_6": 0x10006F6, "Farsi_7": 0x10006F7, "Farsi_8": 0x10006F8, "Farsi_9": 0x10006F9, "Arabic_percent": 0x100066A, "Arabic_superscript_alef": 0x1000670, "Arabic_tteh": 0x1000679, "Arabic_peh": 0x100067E, "Arabic_tcheh": 0x1000686, "Arabic_ddal": 0x1000688, "Arabic_rreh": 0x1000691, "Arabic_comma": 0x05AC, "Arabic_fullstop": 0x10006D4, "Arabic_0": 0x1000660, "Arabic_1": 0x1000661, "Arabic_2": 0x1000662, "Arabic_3": 0x1000663, "Arabic_4": 0x1000664, "Arabic_5": 0x1000665, "Arabic_6": 0x1000666, "Arabic_7": 0x1000667, "Arabic_8": 0x1000668, "Arabic_9": 0x1000669, "Arabic_semicolon": 0x05BB, "Arabic_question_mark": 0x05BF, "Arabic_hamza": 0x05C1, "Arabic_maddaonalef": 0x05C2, "Arabic_hamzaonalef": 0x05C3, "Arabic_hamzaonwaw": 0x05C4, "Arabic_hamzaunderalef": 0x05C5, "Arabic_hamzaonyeh": 0x05C6, "Arabic_alef": 0x05C7, "Arabic_beh": 0x05C8, "Arabic_tehmarbuta": 0x05C9, "Arabic_teh": 0x05CA, "Arabic_theh": 0x05CB, "Arabic_jeem": 0x05CC, "Arabic_hah": 0x05CD, "Arabic_khah": 0x05CE, "Arabic_dal": 0x05CF, "Arabic_thal": 0x05D0, "Arabic_ra": 0x05D1, "Arabic_zain": 0x05D2, "Arabic_seen": 0x05D3, "Arabic_sheen": 0x05D4, "Arabic_sad": 0x05D5, "Arabic_dad": 0x05D6, "Arabic_tah": 0x05D7, "Arabic_zah": 0x05D8, "Arabic_ain": 0x05D9, "Arabic_ghain": 0x05DA, "Arabic_tatweel": 0x05E0, "Arabic_feh": 0x05E1, "Arabic_qaf": 0x05E2, "Arabic_kaf": 0x05E3, "Arabic_lam": 0x05E4, "Arabic_meem": 0x05E5, "Arabic_noon": 0x05E6, "Arabic_ha": 0x05E7, "Arabic_heh": 0x05E7, "Arabic_waw": 0x05E8, "Arabic_alefmaksura": 0x05E9, "Arabic_yeh": 0x05EA, "Arabic_fathatan": 0x05EB, "Arabic_dammatan": 0x05EC, "Arabic_kasratan": 0x05ED, "Arabic_fatha": 0x05EE, "Arabic_damma": 0x05EF, "Arabic_kasra": 0x05F0, "Arabic_shadda": 0x05F1, "Arabic_sukun": 0x05F2, "Arabic_madda_above": 0x1000653, "Arabic_hamza_above": 0x1000654, "Arabic_hamza_below": 0x1000655, "Arabic_jeh": 0x1000698, "Arabic_veh": 0x10006A4, "Arabic_keheh": 0x10006A9, "Arabic_gaf": 0x10006AF, "Arabic_noon_ghunna": 0x10006BA, "Arabic_heh_doachashmee": 0x10006BE, "Farsi_yeh": 0x10006CC, "Arabic_farsi_yeh": 0x10006CC, "Arabic_yeh_baree": 0x10006D2, "Arabic_heh_goal": 0x10006C1, "Arabic_switch": 0xFF7E, "Cyrillic_GHE_bar": 0x1000492, "Cyrillic_ghe_bar": 0x1000493, "Cyrillic_ZHE_descender": 0x1000496, "Cyrillic_zhe_descender": 0x1000497, "Cyrillic_KA_descender": 0x100049A, "Cyrillic_ka_descender": 0x100049B, "Cyrillic_KA_vertstroke": 0x100049C, "Cyrillic_ka_vertstroke": 0x100049D, "Cyrillic_EN_descender": 0x10004A2, "Cyrillic_en_descender": 0x10004A3, "Cyrillic_U_straight": 0x10004AE, "Cyrillic_u_straight": 0x10004AF, "Cyrillic_U_straight_bar": 0x10004B0, "Cyrillic_u_straight_bar": 0x10004B1, "Cyrillic_HA_descender": 0x10004B2, "Cyrillic_ha_descender": 0x10004B3, "Cyrillic_CHE_descender": 0x10004B6, "Cyrillic_che_descender": 0x10004B7, "Cyrillic_CHE_vertstroke": 0x10004B8, "Cyrillic_che_vertstroke": 0x10004B9, "Cyrillic_SHHA": 0x10004BA, "Cyrillic_shha": 0x10004BB, "Cyrillic_SCHWA": 0x10004D8, "Cyrillic_schwa": 0x10004D9, "Cyrillic_I_macron": 0x10004E2, "Cyrillic_i_macron": 0x10004E3, "Cyrillic_O_bar": 0x10004E8, "Cyrillic_o_bar": 0x10004E9, "Cyrillic_U_macron": 0x10004EE, "Cyrillic_u_macron": 0x10004EF, "Serbian_dje": 0x06A1, "Macedonia_gje": 0x06A2, "Cyrillic_io": 0x06A3, "Ukrainian_ie": 0x06A4, "Ukranian_je": 0x06A4, "Macedonia_dse": 0x06A5, "Ukrainian_i": 0x06A6, "Ukranian_i": 0x06A6, "Ukrainian_yi": 0x06A7, "Ukranian_yi": 0x06A7, "Cyrillic_je": 0x06A8, "Serbian_je": 0x06A8, "Cyrillic_lje": 0x06A9, "Serbian_lje": 0x06A9, "Cyrillic_nje": 0x06AA, "Serbian_nje": 0x06AA, "Serbian_tshe": 0x06AB, "Macedonia_kje": 0x06AC, "Ukrainian_ghe_with_upturn": 0x06AD, "Byelorussian_shortu": 0x06AE, "Cyrillic_dzhe": 0x06AF, "Serbian_dze": 0x06AF, "numerosign": 0x06B0, "Serbian_DJE": 0x06B1, "Macedonia_GJE": 0x06B2, "Cyrillic_IO": 0x06B3, "Ukrainian_IE": 0x06B4, "Ukranian_JE": 0x06B4, "Macedonia_DSE": 0x06B5, "Ukrainian_I": 0x06B6, "Ukranian_I": 0x06B6, "Ukrainian_YI": 0x06B7, "Ukranian_YI": 0x06B7, "Cyrillic_JE": 0x06B8, "Serbian_JE": 0x06B8, "Cyrillic_LJE": 0x06B9, "Serbian_LJE": 0x06B9, "Cyrillic_NJE": 0x06BA, "Serbian_NJE": 0x06BA, "Serbian_TSHE": 0x06BB, "Macedonia_KJE": 0x06BC, "Ukrainian_GHE_WITH_UPTURN": 0x06BD, "Byelorussian_SHORTU": 0x06BE, "Cyrillic_DZHE": 0x06BF, "Serbian_DZE": 0x06BF, "Cyrillic_yu": 0x06C0, "Cyrillic_a": 0x06C1, "Cyrillic_be": 0x06C2, "Cyrillic_tse": 0x06C3, "Cyrillic_de": 0x06C4, "Cyrillic_ie": 0x06C5, "Cyrillic_ef": 0x06C6, "Cyrillic_ghe": 0x06C7, "Cyrillic_ha": 0x06C8, "Cyrillic_i": 0x06C9, "Cyrillic_shorti": 0x06CA, "Cyrillic_ka": 0x06CB, "Cyrillic_el": 0x06CC, "Cyrillic_em": 0x06CD, "Cyrillic_en": 0x06CE, "Cyrillic_o": 0x06CF, "Cyrillic_pe": 0x06D0, "Cyrillic_ya": 0x06D1, "Cyrillic_er": 0x06D2, "Cyrillic_es": 0x06D3, "Cyrillic_te": 0x06D4, "Cyrillic_u": 0x06D5, "Cyrillic_zhe": 0x06D6, "Cyrillic_ve": 0x06D7, "Cyrillic_softsign": 0x06D8, "Cyrillic_yeru": 0x06D9, "Cyrillic_ze": 0x06DA, "Cyrillic_sha": 0x06DB, "Cyrillic_e": 0x06DC, "Cyrillic_shcha": 0x06DD, "Cyrillic_che": 0x06DE, "Cyrillic_hardsign": 0x06DF, "Cyrillic_YU": 0x06E0, "Cyrillic_A": 0x06E1, "Cyrillic_BE": 0x06E2, "Cyrillic_TSE": 0x06E3, "Cyrillic_DE": 0x06E4, "Cyrillic_IE": 0x06E5, "Cyrillic_EF": 0x06E6, "Cyrillic_GHE": 0x06E7, "Cyrillic_HA": 0x06E8, "Cyrillic_I": 0x06E9, "Cyrillic_SHORTI": 0x06EA, "Cyrillic_KA": 0x06EB, "Cyrillic_EL": 0x06EC, "Cyrillic_EM": 0x06ED, "Cyrillic_EN": 0x06EE, "Cyrillic_O": 0x06EF, "Cyrillic_PE": 0x06F0, "Cyrillic_YA": 0x06F1, "Cyrillic_ER": 0x06F2, "Cyrillic_ES": 0x06F3, "Cyrillic_TE": 0x06F4, "Cyrillic_U": 0x06F5, "Cyrillic_ZHE": 0x06F6, "Cyrillic_VE": 0x06F7, "Cyrillic_SOFTSIGN": 0x06F8, "Cyrillic_YERU": 0x06F9, "Cyrillic_ZE": 0x06FA, "Cyrillic_SHA": 0x06FB, "Cyrillic_E": 0x06FC, "Cyrillic_SHCHA": 0x06FD, "Cyrillic_CHE": 0x06FE, "Cyrillic_HARDSIGN": 0x06FF, "Greek_ALPHAaccent": 0x07A1, "Greek_EPSILONaccent": 0x07A2, "Greek_ETAaccent": 0x07A3, "Greek_IOTAaccent": 0x07A4, "Greek_IOTAdieresis": 0x07A5, "Greek_IOTAdiaeresis": 0x07A5, "Greek_OMICRONaccent": 0x07A7, "Greek_UPSILONaccent": 0x07A8, "Greek_UPSILONdieresis": 0x07A9, "Greek_OMEGAaccent": 0x07AB, "Greek_accentdieresis": 0x07AE, "Greek_horizbar": 0x07AF, "Greek_alphaaccent": 0x07B1, "Greek_epsilonaccent": 0x07B2, "Greek_etaaccent": 0x07B3, "Greek_iotaaccent": 0x07B4, "Greek_iotadieresis": 0x07B5, "Greek_iotaaccentdieresis": 0x07B6, "Greek_omicronaccent": 0x07B7, "Greek_upsilonaccent": 0x07B8, "Greek_upsilondieresis": 0x07B9, "Greek_upsilonaccentdieresis": 0x07BA, "Greek_omegaaccent": 0x07BB, "Greek_ALPHA": 0x07C1, "Greek_BETA": 0x07C2, "Greek_GAMMA": 0x07C3, "Greek_DELTA": 0x07C4, "Greek_EPSILON": 0x07C5, "Greek_ZETA": 0x07C6, "Greek_ETA": 0x07C7, "Greek_THETA": 0x07C8, "Greek_IOTA": 0x07C9, "Greek_KAPPA": 0x07CA, "Greek_LAMDA": 0x07CB, "Greek_LAMBDA": 0x07CB, "Greek_MU": 0x07CC, "Greek_NU": 0x07CD, "Greek_XI": 0x07CE, "Greek_OMICRON": 0x07CF, "Greek_PI": 0x07D0, "Greek_RHO": 0x07D1, "Greek_SIGMA": 0x07D2, "Greek_TAU": 0x07D4, "Greek_UPSILON": 0x07D5, "Greek_PHI": 0x07D6, "Greek_CHI": 0x07D7, "Greek_PSI": 0x07D8, "Greek_OMEGA": 0x07D9, "Greek_alpha": 0x07E1, "Greek_beta": 0x07E2, "Greek_gamma": 0x07E3, "Greek_delta": 0x07E4, "Greek_epsilon": 0x07E5, "Greek_zeta": 0x07E6, "Greek_eta": 0x07E7, "Greek_theta": 0x07E8, "Greek_iota": 0x07E9, "Greek_kappa": 0x07EA, "Greek_lamda": 0x07EB, "Greek_lambda": 0x07EB, "Greek_mu": 0x07EC, "Greek_nu": 0x07ED, "Greek_xi": 0x07EE, "Greek_omicron": 0x07EF, "Greek_pi": 0x07F0, "Greek_rho": 0x07F1, "Greek_sigma": 0x07F2, "Greek_finalsmallsigma": 0x07F3, "Greek_tau": 0x07F4, "Greek_upsilon": 0x07F5, "Greek_phi": 0x07F6, "Greek_chi": 0x07F7, "Greek_psi": 0x07F8, "Greek_omega": 0x07F9, "Greek_switch": 0xFF7E, "leftradical": 0x08A1, "topleftradical": 0x08A2, "horizconnector": 0x08A3, "topintegral": 0x08A4, "botintegral": 0x08A5, "vertconnector": 0x08A6, "topleftsqbracket": 0x08A7, "botleftsqbracket": 0x08A8, "toprightsqbracket": 0x08A9, "botrightsqbracket": 0x08AA, "topleftparens": 0x08AB, "botleftparens": 0x08AC, "toprightparens": 0x08AD, "botrightparens": 0x08AE, "leftmiddlecurlybrace": 0x08AF, "rightmiddlecurlybrace": 0x08B0, "topleftsummation": 0x08B1, "botleftsummation": 0x08B2, "topvertsummationconnector": 0x08B3, "botvertsummationconnector": 0x08B4, "toprightsummation": 0x08B5, "botrightsummation": 0x08B6, "rightmiddlesummation": 0x08B7, "lessthanequal": 0x08BC, "notequal": 0x08BD, "greaterthanequal": 0x08BE, "integral": 0x08BF, "therefore": 0x08C0, "variation": 0x08C1, "infinity": 0x08C2, "nabla": 0x08C5, "approximate": 0x08C8, "similarequal": 0x08C9, "ifonlyif": 0x08CD, "implies": 0x08CE, "identical": 0x08CF, "radical": 0x08D6, "includedin": 0x08DA, "includes": 0x08DB, "intersection": 0x08DC, "union": 0x08DD, "logicaland": 0x08DE, "logicalor": 0x08DF, "partialderivative": 0x08EF, "function": 0x08F6, "leftarrow": 0x08FB, "uparrow": 0x08FC, "rightarrow": 0x08FD, "downarrow": 0x08FE, "blank": 0x09DF, "soliddiamond": 0x09E0, "checkerboard": 0x09E1, "ht": 0x09E2, "ff": 0x09E3, "cr": 0x09E4, "lf": 0x09E5, "nl": 0x09E8, "vt": 0x09E9, "lowrightcorner": 0x09EA, "uprightcorner": 0x09EB, "upleftcorner": 0x09EC, "lowleftcorner": 0x09ED, "crossinglines": 0x09EE, "horizlinescan1": 0x09EF, "horizlinescan3": 0x09F0, "horizlinescan5": 0x09F1, "horizlinescan7": 0x09F2, "horizlinescan9": 0x09F3, "leftt": 0x09F4, "rightt": 0x09F5, "bott": 0x09F6, "topt": 0x09F7, "vertbar": 0x09F8, "emspace": 0x0AA1, "enspace": 0x0AA2, "em3space": 0x0AA3, "em4space": 0x0AA4, "digitspace": 0x0AA5, "punctspace": 0x0AA6, "thinspace": 0x0AA7, "hairspace": 0x0AA8, "emdash": 0x0AA9, "endash": 0x0AAA, "signifblank": 0x0AAC, "ellipsis": 0x0AAE, "doubbaselinedot": 0x0AAF, "onethird": 0x0AB0, "twothirds": 0x0AB1, "onefifth": 0x0AB2, "twofifths": 0x0AB3, "threefifths": 0x0AB4, "fourfifths": 0x0AB5, "onesixth": 0x0AB6, "fivesixths": 0x0AB7, "careof": 0x0AB8, "figdash": 0x0ABB, "leftanglebracket": 0x0ABC, "decimalpoint": 0x0ABD, "rightanglebracket": 0x0ABE, "marker": 0x0ABF, "oneeighth": 0x0AC3, "threeeighths": 0x0AC4, "fiveeighths": 0x0AC5, "seveneighths": 0x0AC6, "trademark": 0x0AC9, "signaturemark": 0x0ACA, "trademarkincircle": 0x0ACB, "leftopentriangle": 0x0ACC, "rightopentriangle": 0x0ACD, "emopencircle": 0x0ACE, "emopenrectangle": 0x0ACF, "leftsinglequotemark": 0x0AD0, "rightsinglequotemark": 0x0AD1, "leftdoublequotemark": 0x0AD2, "rightdoublequotemark": 0x0AD3, "prescription": 0x0AD4, "minutes": 0x0AD6, "seconds": 0x0AD7, "latincross": 0x0AD9, "hexagram": 0x0ADA, "filledrectbullet": 0x0ADB, "filledlefttribullet": 0x0ADC, "filledrighttribullet": 0x0ADD, "emfilledcircle": 0x0ADE, "emfilledrect": 0x0ADF, "enopencircbullet": 0x0AE0, "enopensquarebullet": 0x0AE1, "openrectbullet": 0x0AE2, "opentribulletup": 0x0AE3, "opentribulletdown": 0x0AE4, "openstar": 0x0AE5, "enfilledcircbullet": 0x0AE6, "enfilledsqbullet": 0x0AE7, "filledtribulletup": 0x0AE8, "filledtribulletdown": 0x0AE9, "leftpointer": 0x0AEA, "rightpointer": 0x0AEB, "club": 0x0AEC, "diamond": 0x0AED, "heart": 0x0AEE, "maltesecross": 0x0AF0, "dagger": 0x0AF1, "doubledagger": 0x0AF2, "checkmark": 0x0AF3, "ballotcross": 0x0AF4, "musicalsharp": 0x0AF5, "musicalflat": 0x0AF6, "malesymbol": 0x0AF7, "femalesymbol": 0x0AF8, "telephone": 0x0AF9, "telephonerecorder": 0x0AFA, "phonographcopyright": 0x0AFB, "caret": 0x0AFC, "singlelowquotemark": 0x0AFD, "doublelowquotemark": 0x0AFE, "cursor": 0x0AFF, "leftcaret": 0x0BA3, "rightcaret": 0x0BA6, "downcaret": 0x0BA8, "upcaret": 0x0BA9, "overbar": 0x0BC0, "downtack": 0x0BC2, "upshoe": 0x0BC3, "downstile": 0x0BC4, "underbar": 0x0BC6, "jot": 0x0BCA, "quad": 0x0BCC, "uptack": 0x0BCE, "circle": 0x0BCF, "upstile": 0x0BD3, "downshoe": 0x0BD6, "rightshoe": 0x0BD8, "leftshoe": 0x0BDA, "lefttack": 0x0BDC, "righttack": 0x0BFC, "hebrew_doublelowline": 0x0CDF, "hebrew_aleph": 0x0CE0, "hebrew_bet": 0x0CE1, "hebrew_beth": 0x0CE1, "hebrew_gimel": 0x0CE2, "hebrew_gimmel": 0x0CE2, "hebrew_dalet": 0x0CE3, "hebrew_daleth": 0x0CE3, "hebrew_he": 0x0CE4, "hebrew_waw": 0x0CE5, "hebrew_zain": 0x0CE6, "hebrew_zayin": 0x0CE6, "hebrew_chet": 0x0CE7, "hebrew_het": 0x0CE7, "hebrew_tet": 0x0CE8, "hebrew_teth": 0x0CE8, "hebrew_yod": 0x0CE9, "hebrew_finalkaph": 0x0CEA, "hebrew_kaph": 0x0CEB, "hebrew_lamed": 0x0CEC, "hebrew_finalmem": 0x0CED, "hebrew_mem": 0x0CEE, "hebrew_finalnun": 0x0CEF, "hebrew_nun": 0x0CF0, "hebrew_samech": 0x0CF1, "hebrew_samekh": 0x0CF1, "hebrew_ayin": 0x0CF2, "hebrew_finalpe": 0x0CF3, "hebrew_pe": 0x0CF4, "hebrew_finalzade": 0x0CF5, "hebrew_finalzadi": 0x0CF5, "hebrew_zade": 0x0CF6, "hebrew_zadi": 0x0CF6, "hebrew_qoph": 0x0CF7, "hebrew_kuf": 0x0CF7, "hebrew_resh": 0x0CF8, "hebrew_shin": 0x0CF9, "hebrew_taw": 0x0CFA, "hebrew_taf": 0x0CFA, "Hebrew_switch": 0xFF7E, "Thai_kokai": 0x0DA1, "Thai_khokhai": 0x0DA2, "Thai_khokhuat": 0x0DA3, "Thai_khokhwai": 0x0DA4, "Thai_khokhon": 0x0DA5, "Thai_khorakhang": 0x0DA6, "Thai_ngongu": 0x0DA7, "Thai_chochan": 0x0DA8, "Thai_choching": 0x0DA9, "Thai_chochang": 0x0DAA, "Thai_soso": 0x0DAB, "Thai_chochoe": 0x0DAC, "Thai_yoying": 0x0DAD, "Thai_dochada": 0x0DAE, "Thai_topatak": 0x0DAF, "Thai_thothan": 0x0DB0, "Thai_thonangmontho": 0x0DB1, "Thai_thophuthao": 0x0DB2, "Thai_nonen": 0x0DB3, "Thai_dodek": 0x0DB4, "Thai_totao": 0x0DB5, "Thai_thothung": 0x0DB6, "Thai_thothahan": 0x0DB7, "Thai_thothong": 0x0DB8, "Thai_nonu": 0x0DB9, "Thai_bobaimai": 0x0DBA, "Thai_popla": 0x0DBB, "Thai_phophung": 0x0DBC, "Thai_fofa": 0x0DBD, "Thai_phophan": 0x0DBE, "Thai_fofan": 0x0DBF, "Thai_phosamphao": 0x0DC0, "Thai_moma": 0x0DC1, "Thai_yoyak": 0x0DC2, "Thai_rorua": 0x0DC3, "Thai_ru": 0x0DC4, "Thai_loling": 0x0DC5, "Thai_lu": 0x0DC6, "Thai_wowaen": 0x0DC7, "Thai_sosala": 0x0DC8, "Thai_sorusi": 0x0DC9, "Thai_sosua": 0x0DCA, "Thai_hohip": 0x0DCB, "Thai_lochula": 0x0DCC, "Thai_oang": 0x0DCD, "Thai_honokhuk": 0x0DCE, "Thai_paiyannoi": 0x0DCF, "Thai_saraa": 0x0DD0, "Thai_maihanakat": 0x0DD1, "Thai_saraaa": 0x0DD2, "Thai_saraam": 0x0DD3, "Thai_sarai": 0x0DD4, "Thai_saraii": 0x0DD5, "Thai_saraue": 0x0DD6, "Thai_sarauee": 0x0DD7, "Thai_sarau": 0x0DD8, "Thai_sarauu": 0x0DD9, "Thai_phinthu": 0x0DDA, "Thai_maihanakat_maitho": 0x0DDE, "Thai_baht": 0x0DDF, "Thai_sarae": 0x0DE0, "Thai_saraae": 0x0DE1, "Thai_sarao": 0x0DE2, "Thai_saraaimaimuan": 0x0DE3, "Thai_saraaimaimalai": 0x0DE4, "Thai_lakkhangyao": 0x0DE5, "Thai_maiyamok": 0x0DE6, "Thai_maitaikhu": 0x0DE7, "Thai_maiek": 0x0DE8, "Thai_maitho": 0x0DE9, "Thai_maitri": 0x0DEA, "Thai_maichattawa": 0x0DEB, "Thai_thanthakhat": 0x0DEC, "Thai_nikhahit": 0x0DED, "Thai_leksun": 0x0DF0, "Thai_leknung": 0x0DF1, "Thai_leksong": 0x0DF2, "Thai_leksam": 0x0DF3, "Thai_leksi": 0x0DF4, "Thai_lekha": 0x0DF5, "Thai_lekhok": 0x0DF6, "Thai_lekchet": 0x0DF7, "Thai_lekpaet": 0x0DF8, "Thai_lekkao": 0x0DF9, "Hangul": 0xFF31, "Hangul_Start": 0xFF32, "Hangul_End": 0xFF33, "Hangul_Hanja": 0xFF34, "Hangul_Jamo": 0xFF35, "Hangul_Romaja": 0xFF36, "Hangul_Codeinput": 0xFF37, "Hangul_Jeonja": 0xFF38, "Hangul_Banja": 0xFF39, "Hangul_PreHanja": 0xFF3A, "Hangul_PostHanja": 0xFF3B, "Hangul_SingleCandidate": 0xFF3C, "Hangul_MultipleCandidate": 0xFF3D, "Hangul_PreviousCandidate": 0xFF3E, "Hangul_Special": 0xFF3F, "Hangul_switch": 0xFF7E, "Hangul_Kiyeog": 0x0EA1, "Hangul_SsangKiyeog": 0x0EA2, "Hangul_KiyeogSios": 0x0EA3, "Hangul_Nieun": 0x0EA4, "Hangul_NieunJieuj": 0x0EA5, "Hangul_NieunHieuh": 0x0EA6, "Hangul_Dikeud": 0x0EA7, "Hangul_SsangDikeud": 0x0EA8, "Hangul_Rieul": 0x0EA9, "Hangul_RieulKiyeog": 0x0EAA, "Hangul_RieulMieum": 0x0EAB, "Hangul_RieulPieub": 0x0EAC, "Hangul_RieulSios": 0x0EAD, "Hangul_RieulTieut": 0x0EAE, "Hangul_RieulPhieuf": 0x0EAF, "Hangul_RieulHieuh": 0x0EB0, "Hangul_Mieum": 0x0EB1, "Hangul_Pieub": 0x0EB2, "Hangul_SsangPieub": 0x0EB3, "Hangul_PieubSios": 0x0EB4, "Hangul_Sios": 0x0EB5, "Hangul_SsangSios": 0x0EB6, "Hangul_Ieung": 0x0EB7, "Hangul_Jieuj": 0x0EB8, "Hangul_SsangJieuj": 0x0EB9, "Hangul_Cieuc": 0x0EBA, "Hangul_Khieuq": 0x0EBB, "Hangul_Tieut": 0x0EBC, "Hangul_Phieuf": 0x0EBD, "Hangul_Hieuh": 0x0EBE, "Hangul_A": 0x0EBF, "Hangul_AE": 0x0EC0, "Hangul_YA": 0x0EC1, "Hangul_YAE": 0x0EC2, "Hangul_EO": 0x0EC3, "Hangul_E": 0x0EC4, "Hangul_YEO": 0x0EC5, "Hangul_YE": 0x0EC6, "Hangul_O": 0x0EC7, "Hangul_WA": 0x0EC8, "Hangul_WAE": 0x0EC9, "Hangul_OE": 0x0ECA, "Hangul_YO": 0x0ECB, "Hangul_U": 0x0ECC, "Hangul_WEO": 0x0ECD, "Hangul_WE": 0x0ECE, "Hangul_WI": 0x0ECF, "Hangul_YU": 0x0ED0, "Hangul_EU": 0x0ED1, "Hangul_YI": 0x0ED2, "Hangul_I": 0x0ED3, "Hangul_J_Kiyeog": 0x0ED4, "Hangul_J_SsangKiyeog": 0x0ED5, "Hangul_J_KiyeogSios": 0x0ED6, "Hangul_J_Nieun": 0x0ED7, "Hangul_J_NieunJieuj": 0x0ED8, "Hangul_J_NieunHieuh": 0x0ED9, "Hangul_J_Dikeud": 0x0EDA, "Hangul_J_Rieul": 0x0EDB, "Hangul_J_RieulKiyeog": 0x0EDC, "Hangul_J_RieulMieum": 0x0EDD, "Hangul_J_RieulPieub": 0x0EDE, "Hangul_J_RieulSios": 0x0EDF, "Hangul_J_RieulTieut": 0x0EE0, "Hangul_J_RieulPhieuf": 0x0EE1, "Hangul_J_RieulHieuh": 0x0EE2, "Hangul_J_Mieum": 0x0EE3, "Hangul_J_Pieub": 0x0EE4, "Hangul_J_PieubSios": 0x0EE5, "Hangul_J_Sios": 0x0EE6, "Hangul_J_SsangSios": 0x0EE7, "Hangul_J_Ieung": 0x0EE8, "Hangul_J_Jieuj": 0x0EE9, "Hangul_J_Cieuc": 0x0EEA, "Hangul_J_Khieuq": 0x0EEB, "Hangul_J_Tieut": 0x0EEC, "Hangul_J_Phieuf": 0x0EED, "Hangul_J_Hieuh": 0x0EEE, "Hangul_RieulYeorinHieuh": 0x0EEF, "Hangul_SunkyeongeumMieum": 0x0EF0, "Hangul_SunkyeongeumPieub": 0x0EF1, "Hangul_PanSios": 0x0EF2, "Hangul_KkogjiDalrinIeung": 0x0EF3, "Hangul_SunkyeongeumPhieuf": 0x0EF4, "Hangul_YeorinHieuh": 0x0EF5, "Hangul_AraeA": 0x0EF6, "Hangul_AraeAE": 0x0EF7, "Hangul_J_PanSios": 0x0EF8, "Hangul_J_KkogjiDalrinIeung": 0x0EF9, "Hangul_J_YeorinHieuh": 0x0EFA, "Korean_Won": 0x0EFF, "Armenian_ligature_ew": 0x1000587, "Armenian_full_stop": 0x1000589, "Armenian_verjaket": 0x1000589, "Armenian_separation_mark": 0x100055D, "Armenian_but": 0x100055D, "Armenian_hyphen": 0x100058A, "Armenian_yentamna": 0x100058A, "Armenian_exclam": 0x100055C, "Armenian_amanak": 0x100055C, "Armenian_accent": 0x100055B, "Armenian_shesht": 0x100055B, "Armenian_question": 0x100055E, "Armenian_paruyk": 0x100055E, "Armenian_AYB": 0x1000531, "Armenian_ayb": 0x1000561, "Armenian_BEN": 0x1000532, "Armenian_ben": 0x1000562, "Armenian_GIM": 0x1000533, "Armenian_gim": 0x1000563, "Armenian_DA": 0x1000534, "Armenian_da": 0x1000564, "Armenian_YECH": 0x1000535, "Armenian_yech": 0x1000565, "Armenian_ZA": 0x1000536, "Armenian_za": 0x1000566, "Armenian_E": 0x1000537, "Armenian_e": 0x1000567, "Armenian_AT": 0x1000538, "Armenian_at": 0x1000568, "Armenian_TO": 0x1000539, "Armenian_to": 0x1000569, "Armenian_ZHE": 0x100053A, "Armenian_zhe": 0x100056A, "Armenian_INI": 0x100053B, "Armenian_ini": 0x100056B, "Armenian_LYUN": 0x100053C, "Armenian_lyun": 0x100056C, "Armenian_KHE": 0x100053D, "Armenian_khe": 0x100056D, "Armenian_TSA": 0x100053E, "Armenian_tsa": 0x100056E, "Armenian_KEN": 0x100053F, "Armenian_ken": 0x100056F, "Armenian_HO": 0x1000540, "Armenian_ho": 0x1000570, "Armenian_DZA": 0x1000541, "Armenian_dza": 0x1000571, "Armenian_GHAT": 0x1000542, "Armenian_ghat": 0x1000572, "Armenian_TCHE": 0x1000543, "Armenian_tche": 0x1000573, "Armenian_MEN": 0x1000544, "Armenian_men": 0x1000574, "Armenian_HI": 0x1000545, "Armenian_hi": 0x1000575, "Armenian_NU": 0x1000546, "Armenian_nu": 0x1000576, "Armenian_SHA": 0x1000547, "Armenian_sha": 0x1000577, "Armenian_VO": 0x1000548, "Armenian_vo": 0x1000578, "Armenian_CHA": 0x1000549, "Armenian_cha": 0x1000579, "Armenian_PE": 0x100054A, "Armenian_pe": 0x100057A, "Armenian_JE": 0x100054B, "Armenian_je": 0x100057B, "Armenian_RA": 0x100054C, "Armenian_ra": 0x100057C, "Armenian_SE": 0x100054D, "Armenian_se": 0x100057D, "Armenian_VEV": 0x100054E, "Armenian_vev": 0x100057E, "Armenian_TYUN": 0x100054F, "Armenian_tyun": 0x100057F, "Armenian_RE": 0x1000550, "Armenian_re": 0x1000580, "Armenian_TSO": 0x1000551, "Armenian_tso": 0x1000581, "Armenian_VYUN": 0x1000552, "Armenian_vyun": 0x1000582, "Armenian_PYUR": 0x1000553, "Armenian_pyur": 0x1000583, "Armenian_KE": 0x1000554, "Armenian_ke": 0x1000584, "Armenian_O": 0x1000555, "Armenian_o": 0x1000585, "Armenian_FE": 0x1000556, "Armenian_fe": 0x1000586, "Armenian_apostrophe": 0x100055A, "Georgian_an": 0x10010D0, "Georgian_ban": 0x10010D1, "Georgian_gan": 0x10010D2, "Georgian_don": 0x10010D3, "Georgian_en": 0x10010D4, "Georgian_vin": 0x10010D5, "Georgian_zen": 0x10010D6, "Georgian_tan": 0x10010D7, "Georgian_in": 0x10010D8, "Georgian_kan": 0x10010D9, "Georgian_las": 0x10010DA, "Georgian_man": 0x10010DB, "Georgian_nar": 0x10010DC, "Georgian_on": 0x10010DD, "Georgian_par": 0x10010DE, "Georgian_zhar": 0x10010DF, "Georgian_rae": 0x10010E0, "Georgian_san": 0x10010E1, "Georgian_tar": 0x10010E2, "Georgian_un": 0x10010E3, "Georgian_phar": 0x10010E4, "Georgian_khar": 0x10010E5, "Georgian_ghan": 0x10010E6, "Georgian_qar": 0x10010E7, "Georgian_shin": 0x10010E8, "Georgian_chin": 0x10010E9, "Georgian_can": 0x10010EA, "Georgian_jil": 0x10010EB, "Georgian_cil": 0x10010EC, "Georgian_char": 0x10010ED, "Georgian_xan": 0x10010EE, "Georgian_jhan": 0x10010EF, "Georgian_hae": 0x10010F0, "Georgian_he": 0x10010F1, "Georgian_hie": 0x10010F2, "Georgian_we": 0x10010F3, "Georgian_har": 0x10010F4, "Georgian_hoe": 0x10010F5, "Georgian_fi": 0x10010F6, "Xabovedot": 0x1001E8A, "Ibreve": 0x100012C, "Zstroke": 0x10001B5, "Gcaron": 0x10001E6, "Ocaron": 0x10001D1, "Obarred": 0x100019F, "xabovedot": 0x1001E8B, "ibreve": 0x100012D, "zstroke": 0x10001B6, "gcaron": 0x10001E7, "ocaron": 0x10001D2, "obarred": 0x1000275, "SCHWA": 0x100018F, "schwa": 0x1000259, "Lbelowdot": 0x1001E36, "lbelowdot": 0x1001E37, "Abelowdot": 0x1001EA0, "abelowdot": 0x1001EA1, "Ahook": 0x1001EA2, "ahook": 0x1001EA3, "Acircumflexacute": 0x1001EA4, "acircumflexacute": 0x1001EA5, "Acircumflexgrave": 0x1001EA6, "acircumflexgrave": 0x1001EA7, "Acircumflexhook": 0x1001EA8, "acircumflexhook": 0x1001EA9, "Acircumflextilde": 0x1001EAA, "acircumflextilde": 0x1001EAB, "Acircumflexbelowdot": 0x1001EAC, "acircumflexbelowdot": 0x1001EAD, "Abreveacute": 0x1001EAE, "abreveacute": 0x1001EAF, "Abrevegrave": 0x1001EB0, "abrevegrave": 0x1001EB1, "Abrevehook": 0x1001EB2, "abrevehook": 0x1001EB3, "Abrevetilde": 0x1001EB4, "abrevetilde": 0x1001EB5, "Abrevebelowdot": 0x1001EB6, "abrevebelowdot": 0x1001EB7, "Ebelowdot": 0x1001EB8, "ebelowdot": 0x1001EB9, "Ehook": 0x1001EBA, "ehook": 0x1001EBB, "Etilde": 0x1001EBC, "etilde": 0x1001EBD, "Ecircumflexacute": 0x1001EBE, "ecircumflexacute": 0x1001EBF, "Ecircumflexgrave": 0x1001EC0, "ecircumflexgrave": 0x1001EC1, "Ecircumflexhook": 0x1001EC2, "ecircumflexhook": 0x1001EC3, "Ecircumflextilde": 0x1001EC4, "ecircumflextilde": 0x1001EC5, "Ecircumflexbelowdot": 0x1001EC6, "ecircumflexbelowdot": 0x1001EC7, "Ihook": 0x1001EC8, "ihook": 0x1001EC9, "Ibelowdot": 0x1001ECA, "ibelowdot": 0x1001ECB, "Obelowdot": 0x1001ECC, "obelowdot": 0x1001ECD, "Ohook": 0x1001ECE, "ohook": 0x1001ECF, "Ocircumflexacute": 0x1001ED0, "ocircumflexacute": 0x1001ED1, "Ocircumflexgrave": 0x1001ED2, "ocircumflexgrave": 0x1001ED3, "Ocircumflexhook": 0x1001ED4, "ocircumflexhook": 0x1001ED5, "Ocircumflextilde": 0x1001ED6, "ocircumflextilde": 0x1001ED7, "Ocircumflexbelowdot": 0x1001ED8, "ocircumflexbelowdot": 0x1001ED9, "Ohornacute": 0x1001EDA, "ohornacute": 0x1001EDB, "Ohorngrave": 0x1001EDC, "ohorngrave": 0x1001EDD, "Ohornhook": 0x1001EDE, "ohornhook": 0x1001EDF, "Ohorntilde": 0x1001EE0, "ohorntilde": 0x1001EE1, "Ohornbelowdot": 0x1001EE2, "ohornbelowdot": 0x1001EE3, "Ubelowdot": 0x1001EE4, "ubelowdot": 0x1001EE5, "Uhook": 0x1001EE6, "uhook": 0x1001EE7, "Uhornacute": 0x1001EE8, "uhornacute": 0x1001EE9, "Uhorngrave": 0x1001EEA, "uhorngrave": 0x1001EEB, "Uhornhook": 0x1001EEC, "uhornhook": 0x1001EED, "Uhorntilde": 0x1001EEE, "uhorntilde": 0x1001EEF, "Uhornbelowdot": 0x1001EF0, "uhornbelowdot": 0x1001EF1, "Ybelowdot": 0x1001EF4, "ybelowdot": 0x1001EF5, "Yhook": 0x1001EF6, "yhook": 0x1001EF7, "Ytilde": 0x1001EF8, "ytilde": 0x1001EF9, "Ohorn": 0x10001A0, "ohorn": 0x10001A1, "Uhorn": 0x10001AF, "uhorn": 0x10001B0, "EcuSign": 0x10020A0, "ColonSign": 0x10020A1, "CruzeiroSign": 0x10020A2, "FFrancSign": 0x10020A3, "LiraSign": 0x10020A4, "MillSign": 0x10020A5, "NairaSign": 0x10020A6, "PesetaSign": 0x10020A7, "RupeeSign": 0x10020A8, "WonSign": 0x10020A9, "NewSheqelSign": 0x10020AA, "DongSign": 0x10020AB, "EuroSign": 0x20AC, "zerosuperior": 0x1002070, "foursuperior": 0x1002074, "fivesuperior": 0x1002075, "sixsuperior": 0x1002076, "sevensuperior": 0x1002077, "eightsuperior": 0x1002078, "ninesuperior": 0x1002079, "zerosubscript": 0x1002080, "onesubscript": 0x1002081, "twosubscript": 0x1002082, "threesubscript": 0x1002083, "foursubscript": 0x1002084, "fivesubscript": 0x1002085, "sixsubscript": 0x1002086, "sevensubscript": 0x1002087, "eightsubscript": 0x1002088, "ninesubscript": 0x1002089, "partdifferential": 0x1002202, "emptyset": 0x1002205, "elementof": 0x1002208, "notelementof": 0x1002209, "containsas": 0x100220B, "squareroot": 0x100221A, "cuberoot": 0x100221B, "fourthroot": 0x100221C, "dintegral": 0x100222C, "tintegral": 0x100222D, "because": 0x1002235, "approxeq": 0x1002248, "notapproxeq": 0x1002247, "notidentical": 0x1002262, "stricteq": 0x1002263, "braille_dot_1": 0xFFF1, "braille_dot_2": 0xFFF2, "braille_dot_3": 0xFFF3, "braille_dot_4": 0xFFF4, "braille_dot_5": 0xFFF5, "braille_dot_6": 0xFFF6, "braille_dot_7": 0xFFF7, "braille_dot_8": 0xFFF8, "braille_dot_9": 0xFFF9, "braille_dot_10": 0xFFFA, "braille_blank": 0x1002800, "braille_dots_1": 0x1002801, "braille_dots_2": 0x1002802, "braille_dots_12": 0x1002803, "braille_dots_3": 0x1002804, "braille_dots_13": 0x1002805, "braille_dots_23": 0x1002806, "braille_dots_123": 0x1002807, "braille_dots_4": 0x1002808, "braille_dots_14": 0x1002809, "braille_dots_24": 0x100280A, "braille_dots_124": 0x100280B, "braille_dots_34": 0x100280C, "braille_dots_134": 0x100280D, "braille_dots_234": 0x100280E, "braille_dots_1234": 0x100280F, "braille_dots_5": 0x1002810, "braille_dots_15": 0x1002811, "braille_dots_25": 0x1002812, "braille_dots_125": 0x1002813, "braille_dots_35": 0x1002814, "braille_dots_135": 0x1002815, "braille_dots_235": 0x1002816, "braille_dots_1235": 0x1002817, "braille_dots_45": 0x1002818, "braille_dots_145": 0x1002819, "braille_dots_245": 0x100281A, "braille_dots_1245": 0x100281B, "braille_dots_345": 0x100281C, "braille_dots_1345": 0x100281D, "braille_dots_2345": 0x100281E, "braille_dots_12345": 0x100281F, "braille_dots_6": 0x1002820, "braille_dots_16": 0x1002821, "braille_dots_26": 0x1002822, "braille_dots_126": 0x1002823, "braille_dots_36": 0x1002824, "braille_dots_136": 0x1002825, "braille_dots_236": 0x1002826, "braille_dots_1236": 0x1002827, "braille_dots_46": 0x1002828, "braille_dots_146": 0x1002829, "braille_dots_246": 0x100282A, "braille_dots_1246": 0x100282B, "braille_dots_346": 0x100282C, "braille_dots_1346": 0x100282D, "braille_dots_2346": 0x100282E, "braille_dots_12346": 0x100282F, "braille_dots_56": 0x1002830, "braille_dots_156": 0x1002831, "braille_dots_256": 0x1002832, "braille_dots_1256": 0x1002833, "braille_dots_356": 0x1002834, "braille_dots_1356": 0x1002835, "braille_dots_2356": 0x1002836, "braille_dots_12356": 0x1002837, "braille_dots_456": 0x1002838, "braille_dots_1456": 0x1002839, "braille_dots_2456": 0x100283A, "braille_dots_12456": 0x100283B, "braille_dots_3456": 0x100283C, "braille_dots_13456": 0x100283D, "braille_dots_23456": 0x100283E, "braille_dots_123456": 0x100283F, "braille_dots_7": 0x1002840, "braille_dots_17": 0x1002841, "braille_dots_27": 0x1002842, "braille_dots_127": 0x1002843, "braille_dots_37": 0x1002844, "braille_dots_137": 0x1002845, "braille_dots_237": 0x1002846, "braille_dots_1237": 0x1002847, "braille_dots_47": 0x1002848, "braille_dots_147": 0x1002849, "braille_dots_247": 0x100284A, "braille_dots_1247": 0x100284B, "braille_dots_347": 0x100284C, "braille_dots_1347": 0x100284D, "braille_dots_2347": 0x100284E, "braille_dots_12347": 0x100284F, "braille_dots_57": 0x1002850, "braille_dots_157": 0x1002851, "braille_dots_257": 0x1002852, "braille_dots_1257": 0x1002853, "braille_dots_357": 0x1002854, "braille_dots_1357": 0x1002855, "braille_dots_2357": 0x1002856, "braille_dots_12357": 0x1002857, "braille_dots_457": 0x1002858, "braille_dots_1457": 0x1002859, "braille_dots_2457": 0x100285A, "braille_dots_12457": 0x100285B, "braille_dots_3457": 0x100285C, "braille_dots_13457": 0x100285D, "braille_dots_23457": 0x100285E, "braille_dots_123457": 0x100285F, "braille_dots_67": 0x1002860, "braille_dots_167": 0x1002861, "braille_dots_267": 0x1002862, "braille_dots_1267": 0x1002863, "braille_dots_367": 0x1002864, "braille_dots_1367": 0x1002865, "braille_dots_2367": 0x1002866, "braille_dots_12367": 0x1002867, "braille_dots_467": 0x1002868, "braille_dots_1467": 0x1002869, "braille_dots_2467": 0x100286A, "braille_dots_12467": 0x100286B, "braille_dots_3467": 0x100286C, "braille_dots_13467": 0x100286D, "braille_dots_23467": 0x100286E, "braille_dots_123467": 0x100286F, "braille_dots_567": 0x1002870, "braille_dots_1567": 0x1002871, "braille_dots_2567": 0x1002872, "braille_dots_12567": 0x1002873, "braille_dots_3567": 0x1002874, "braille_dots_13567": 0x1002875, "braille_dots_23567": 0x1002876, "braille_dots_123567": 0x1002877, "braille_dots_4567": 0x1002878, "braille_dots_14567": 0x1002879, "braille_dots_24567": 0x100287A, "braille_dots_124567": 0x100287B, "braille_dots_34567": 0x100287C, "braille_dots_134567": 0x100287D, "braille_dots_234567": 0x100287E, "braille_dots_1234567": 0x100287F, "braille_dots_8": 0x1002880, "braille_dots_18": 0x1002881, "braille_dots_28": 0x1002882, "braille_dots_128": 0x1002883, "braille_dots_38": 0x1002884, "braille_dots_138": 0x1002885, "braille_dots_238": 0x1002886, "braille_dots_1238": 0x1002887, "braille_dots_48": 0x1002888, "braille_dots_148": 0x1002889, "braille_dots_248": 0x100288A, "braille_dots_1248": 0x100288B, "braille_dots_348": 0x100288C, "braille_dots_1348": 0x100288D, "braille_dots_2348": 0x100288E, "braille_dots_12348": 0x100288F, "braille_dots_58": 0x1002890, "braille_dots_158": 0x1002891, "braille_dots_258": 0x1002892, "braille_dots_1258": 0x1002893, "braille_dots_358": 0x1002894, "braille_dots_1358": 0x1002895, "braille_dots_2358": 0x1002896, "braille_dots_12358": 0x1002897, "braille_dots_458": 0x1002898, "braille_dots_1458": 0x1002899, "braille_dots_2458": 0x100289A, "braille_dots_12458": 0x100289B, "braille_dots_3458": 0x100289C, "braille_dots_13458": 0x100289D, "braille_dots_23458": 0x100289E, "braille_dots_123458": 0x100289F, "braille_dots_68": 0x10028A0, "braille_dots_168": 0x10028A1, "braille_dots_268": 0x10028A2, "braille_dots_1268": 0x10028A3, "braille_dots_368": 0x10028A4, "braille_dots_1368": 0x10028A5, "braille_dots_2368": 0x10028A6, "braille_dots_12368": 0x10028A7, "braille_dots_468": 0x10028A8, "braille_dots_1468": 0x10028A9, "braille_dots_2468": 0x10028AA, "braille_dots_12468": 0x10028AB, "braille_dots_3468": 0x10028AC, "braille_dots_13468": 0x10028AD, "braille_dots_23468": 0x10028AE, "braille_dots_123468": 0x10028AF, "braille_dots_568": 0x10028B0, "braille_dots_1568": 0x10028B1, "braille_dots_2568": 0x10028B2, "braille_dots_12568": 0x10028B3, "braille_dots_3568": 0x10028B4, "braille_dots_13568": 0x10028B5, "braille_dots_23568": 0x10028B6, "braille_dots_123568": 0x10028B7, "braille_dots_4568": 0x10028B8, "braille_dots_14568": 0x10028B9, "braille_dots_24568": 0x10028BA, "braille_dots_124568": 0x10028BB, "braille_dots_34568": 0x10028BC, "braille_dots_134568": 0x10028BD, "braille_dots_234568": 0x10028BE, "braille_dots_1234568": 0x10028BF, "braille_dots_78": 0x10028C0, "braille_dots_178": 0x10028C1, "braille_dots_278": 0x10028C2, "braille_dots_1278": 0x10028C3, "braille_dots_378": 0x10028C4, "braille_dots_1378": 0x10028C5, "braille_dots_2378": 0x10028C6, "braille_dots_12378": 0x10028C7, "braille_dots_478": 0x10028C8, "braille_dots_1478": 0x10028C9, "braille_dots_2478": 0x10028CA, "braille_dots_12478": 0x10028CB, "braille_dots_3478": 0x10028CC, "braille_dots_13478": 0x10028CD, "braille_dots_23478": 0x10028CE, "braille_dots_123478": 0x10028CF, "braille_dots_578": 0x10028D0, "braille_dots_1578": 0x10028D1, "braille_dots_2578": 0x10028D2, "braille_dots_12578": 0x10028D3, "braille_dots_3578": 0x10028D4, "braille_dots_13578": 0x10028D5, "braille_dots_23578": 0x10028D6, "braille_dots_123578": 0x10028D7, "braille_dots_4578": 0x10028D8, "braille_dots_14578": 0x10028D9, "braille_dots_24578": 0x10028DA, "braille_dots_124578": 0x10028DB, "braille_dots_34578": 0x10028DC, "braille_dots_134578": 0x10028DD, "braille_dots_234578": 0x10028DE, "braille_dots_1234578": 0x10028DF, "braille_dots_678": 0x10028E0, "braille_dots_1678": 0x10028E1, "braille_dots_2678": 0x10028E2, "braille_dots_12678": 0x10028E3, "braille_dots_3678": 0x10028E4, "braille_dots_13678": 0x10028E5, "braille_dots_23678": 0x10028E6, "braille_dots_123678": 0x10028E7, "braille_dots_4678": 0x10028E8, "braille_dots_14678": 0x10028E9, "braille_dots_24678": 0x10028EA, "braille_dots_124678": 0x10028EB, "braille_dots_34678": 0x10028EC, "braille_dots_134678": 0x10028ED, "braille_dots_234678": 0x10028EE, "braille_dots_1234678": 0x10028EF, "braille_dots_5678": 0x10028F0, "braille_dots_15678": 0x10028F1, "braille_dots_25678": 0x10028F2, "braille_dots_125678": 0x10028F3, "braille_dots_35678": 0x10028F4, "braille_dots_135678": 0x10028F5, "braille_dots_235678": 0x10028F6, "braille_dots_1235678": 0x10028F7, "braille_dots_45678": 0x10028F8, "braille_dots_145678": 0x10028F9, "braille_dots_245678": 0x10028FA, "braille_dots_1245678": 0x10028FB, "braille_dots_345678": 0x10028FC, "braille_dots_1345678": 0x10028FD, "braille_dots_2345678": 0x10028FE, "braille_dots_12345678": 0x10028FF, } keysyms = {k.lower(): v for k, v in keysyms.items()} qtile-0.31.0/libqtile/backend/x11/core.py0000664000175000017500000010545614762660347017760 0ustar epsilonepsilon# Copyright (c) 2019 Aldo Cortesi # Copyright (c) 2019 Sean Vig # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations import asyncio import contextlib import os import signal import time from typing import TYPE_CHECKING import xcffib import xcffib.render import xcffib.xproto import xcffib.xtest from xcffib.xproto import EventMask from libqtile import config, hook, utils from libqtile.backend import base from libqtile.backend.x11 import window, xcbq from libqtile.backend.x11.xkeysyms import keysyms from libqtile.config import ScreenRect from libqtile.log_utils import logger from libqtile.utils import QtileError if TYPE_CHECKING: from collections.abc import Callable, Iterator _IGNORED_EVENTS = { xcffib.xproto.CreateNotifyEvent, xcffib.xproto.FocusInEvent, xcffib.xproto.KeyReleaseEvent, # DWM handles this to help "broken focusing windows". xcffib.xproto.MapNotifyEvent, xcffib.xproto.NoExposureEvent, xcffib.xproto.ReparentNotifyEvent, } def get_keys() -> list[str]: return list(xcbq.keysyms.keys()) def get_modifiers() -> list[str]: return list(xcbq.ModMasks.keys()) class ExistingWMException(Exception): pass class Core(base.Core): def __init__(self, display_name: str | None = None) -> None: """Setup the X11 core backend :param display_name: The display name to setup the X11 connection to. Uses the DISPLAY environment variable if not given. """ if display_name is None: display_name = os.environ.get("DISPLAY") if not display_name: raise QtileError("No DISPLAY set") self.conn = xcbq.Connection(display_name) self._display_name = display_name # Because we only do Xinerama multi-screening, # we can assume that the first # screen's root is _the_ root. self._root = self.conn.default_screen.root supporting_wm_wid = self._root.get_property( "_NET_SUPPORTING_WM_CHECK", "WINDOW", unpack=int ) if len(supporting_wm_wid) > 0: supporting_wm_wid = supporting_wm_wid[0] supporting_wm = window.XWindow(self.conn, supporting_wm_wid) existing_wmname = supporting_wm.get_property( "_NET_WM_NAME", "UTF8_STRING", unpack=str ) if existing_wmname: logger.error("not starting; existing window manager %s", existing_wmname) raise ExistingWMException(existing_wmname) self.eventmask = ( EventMask.StructureNotify | EventMask.SubstructureNotify | EventMask.SubstructureRedirect | EventMask.EnterWindow | EventMask.LeaveWindow | EventMask.ButtonPress ) self._root.set_attribute(eventmask=self.eventmask) self._root.set_property( "_NET_SUPPORTED", [self.conn.atoms[x] for x in xcbq.SUPPORTED_ATOMS] ) self._wmname = "qtile" self._supporting_wm_check_window = self.conn.create_window(-1, -1, 1, 1) self._supporting_wm_check_window.set_property("_NET_WM_NAME", self._wmname) self._supporting_wm_check_window.set_property("_NET_WM_PID", os.getpid()) self._supporting_wm_check_window.set_property( "_NET_SUPPORTING_WM_CHECK", self._supporting_wm_check_window.wid ) self._root.set_property("_NET_SUPPORTING_WM_CHECK", self._supporting_wm_check_window.wid) self._selection = { "PRIMARY": {"owner": None, "selection": ""}, "CLIPBOARD": {"owner": None, "selection": ""}, } self._selection_window = self.conn.create_window(-1, -1, 1, 1) self._selection_window.set_attribute(eventmask=EventMask.PropertyChange) if hasattr(self.conn, "xfixes"): self.conn.xfixes.select_selection_input(self._selection_window, "PRIMARY") self.conn.xfixes.select_selection_input(self._selection_window, "CLIPBOARD") primary_atom = self.conn.atoms["PRIMARY"] reply = self.conn.conn.core.GetSelectionOwner(primary_atom).reply() self._selection["PRIMARY"]["owner"] = reply.owner clipboard_atom = self.conn.atoms["CLIPBOARD"] reply = self.conn.conn.core.GetSelectionOwner(primary_atom).reply() self._selection["CLIPBOARD"]["owner"] = reply.owner # ask for selection on start-up self.convert_selection(primary_atom) self.convert_selection(clipboard_atom) # setup the default cursor self._root.set_cursor("left_ptr") self._painter = None self._xtest = self.conn.conn(xcffib.xtest.key) numlock_code = self.conn.keysym_to_keycode(xcbq.keysyms["num_lock"])[0] self._numlock_mask = xcbq.ModMasks.get(self.conn.get_modifier(numlock_code), 0) self._valid_mask = ~( self._numlock_mask | xcbq.ModMasks["lock"] | xcbq.AllButtonsMask | xcbq.PointerMotionHintMask ) # The last motion notify event that we still need to handle self._motion_notify: xcffib.Event | None = None # The last time we were handling a MotionNotify event self._last_motion_time = 0 self.last_focused: base.Window | None = None @property def name(self): return "x11" def finalize(self) -> None: with contextlib.suppress(xcffib.ConnectionException): self.conn.conn.core.DeletePropertyChecked( self._root.wid, self.conn.atoms["_NET_SUPPORTING_WM_CHECK"], ).check() if hasattr(self, "qtile"): delattr(self, "qtile") self.conn.finalize() def get_screen_info(self) -> list[ScreenRect]: ps = self.conn.pseudoscreens if self.qtile: self._xpoll() return ps @property def wmname(self): return self._wmname @wmname.setter def wmname(self, wmname): self._wmname = wmname self._supporting_wm_check_window.set_property("_NET_WM_NAME", wmname) def setup_listener(self) -> None: """Setup a listener for the given qtile instance :param qtile: The qtile instance to dispatch events to. :param eventloop: The eventloop to use to listen to the file descriptor. """ logger.debug("Adding io watch") self.fd = self.conn.conn.get_file_descriptor() asyncio.get_running_loop().add_reader(self.fd, self._xpoll) def remove_listener(self) -> None: """Remove the listener from the given event loop""" if self.fd is not None: logger.debug("Removing io watch") loop = asyncio.get_running_loop() loop.remove_reader(self.fd) self.fd = None def on_config_load(self, initial) -> None: """Assign windows to groups""" assert self.qtile is not None # Ensure that properties are initialised at startup self.update_client_lists() if not initial: # We are just reloading config managed_wins = [ w for w in self.qtile.windows_map.values() if isinstance(w, window.Window) ] for managed_win in managed_wins: if managed_win.group and managed_win in managed_win.group.windows: # Remove window from old group managed_win.group.remove(managed_win) managed_win.set_group() return # Qtile just started - scan for clients for wid in self._root.query_tree(): item = window.XWindow(self.conn, wid) try: attrs = item.get_attributes() state = item.get_wm_state() internal = item.get_property("QTILE_INTERNAL") except (xcffib.xproto.WindowError, xcffib.xproto.AccessError): continue if ( attrs and attrs.map_state == xcffib.xproto.MapState.Unmapped or attrs.override_redirect ): continue if state and state[0] == window.WithdrawnState: item.unmap() continue if item.wid in self.qtile.windows_map: win = self.qtile.windows_map[item.wid] win.unhide() return if internal: win = window.Internal(item, self.qtile) else: win = window.Window(item, self.qtile) if item.get_wm_type() == "dock" or win.reserved_space: assert self.qtile.current_screen is not None win.static(self.qtile.current_screen.index) continue self.qtile.manage(win) self.update_client_lists() win.change_layer() def warp_pointer(self, x, y): self._root.warp_pointer(x, y) self._root.set_input_focus() self._root.set_property("_NET_ACTIVE_WINDOW", self._root.wid) def convert_selection(self, selection_atom, _type="UTF8_STRING") -> None: type_atom = self.conn.atoms[_type] self.conn.conn.core.ConvertSelection( self._selection_window.wid, selection_atom, type_atom, selection_atom, xcffib.CurrentTime, ) def handle_event(self, event): """Handle an X11 event by forwarding it to the right target""" event_type = event.__class__.__name__ if event_type.endswith("Event"): event_type = event_type[:-5] targets = self._get_target_chain(event_type, event) logger.debug("X11 event: %s (targets: %s)", event_type, targets) for target in targets: ret = target(event) if not ret: break def _xpoll(self) -> None: """Poll the connection and dispatch incoming events""" assert self.qtile is not None while True: try: event = self.conn.conn.poll_for_event() if not event: break if event.__class__ in _IGNORED_EVENTS: continue # Motion Notifies are handled later # Otherwise this is too CPU intensive if isinstance(event, xcffib.xproto.MotionNotifyEvent): self._motion_notify = event else: # These events need the motion notify event handled first handle_motion_first = type(event) in [ xcffib.xproto.EnterNotifyEvent, xcffib.xproto.LeaveNotifyEvent, xcffib.xproto.ButtonPressEvent, xcffib.xproto.ButtonReleaseEvent, ] # Handle events in the correct order if self._motion_notify and handle_motion_first: self.handle_event(self._motion_notify) self._motion_notify = None self.handle_event(event) # Catch some bad X exceptions. Since X is event based, race # conditions can occur almost anywhere in the code. For example, if # a window is created and then immediately destroyed (before the # event handler is evoked), when the event handler tries to examine # the window properties, it will throw a WindowError exception. We # can essentially ignore it, since the window is already dead and # we've got another event in the queue notifying us to clean it up. except ( xcffib.xproto.WindowError, xcffib.xproto.AccessError, xcffib.xproto.DrawableError, xcffib.xproto.GContextError, xcffib.xproto.PixmapError, xcffib.render.PictureError, ): pass except Exception: error_code = self.conn.conn.has_error() if error_code: logger.warning("Shutting down due to disconnection from X server") self.remove_listener() self.qtile.stop() return logger.exception("Got an exception in poll loop") # Handle any outstanding motion notify events if self._motion_notify: self.handle_event(self._motion_notify) self._motion_notify = None self.flush() def _get_target_chain(self, event_type: str, event) -> list[Callable]: """Returns a chain of targets that can handle this event Finds functions named `handle_X`, either on the window object itself or on the Qtile instance, where X is the event name (e.g. EnterNotify, ConfigureNotify, etc). The event will be passed to each target in turn for handling, until one of the handlers returns False or None, or the end of the chain is reached. """ assert self.qtile is not None handler = f"handle_{event_type}" # Certain events expose the affected window id as an "event" attribute. event_events = [ "EnterNotify", "LeaveNotify", "MotionNotify", "ButtonPress", "ButtonRelease", "KeyPress", ] if hasattr(event, "window"): window = self.qtile.windows_map.get(event.window) elif hasattr(event, "drawable"): window = self.qtile.windows_map.get(event.drawable) elif event_type in event_events: window = self.qtile.windows_map.get(event.event) else: window = None chain = [] if window is not None and hasattr(window, handler): chain.append(getattr(window, handler)) if hasattr(self, handler): chain.append(getattr(self, handler)) return chain def get_valid_timestamp(self): """Get a valid timestamp, i.e. not CurrentTime, for X server. It may be used in cases where CurrentTime is unacceptable for X server.""" # do a zero length append to get the time offset as suggested by ICCCM # https://tronche.com/gui/x/icccm/sec-2.html#s-2.1 # we do this on a separate connection since we can't receive events # without returning control to the event loop, which we can't do # because the event loop (via some window event) wants to know the # current time. conn = None try: conn = xcbq.Connection(self._display_name) conn.default_screen.root.set_attribute(eventmask=EventMask.PropertyChange) conn.conn.core.ChangePropertyChecked( xcffib.xproto.PropMode.Append, self._root.wid, self.conn.atoms["WM_CLASS"], self.conn.atoms["STRING"], 8, 0, "", ).check() while True: event = conn.conn.wait_for_event() if event.__class__ != xcffib.xproto.PropertyNotifyEvent: continue return event.time finally: if conn is not None: conn.finalize() @property def display_name(self) -> str: """The name of the connected display""" return self._display_name def update_client_lists(self) -> None: """Updates the _NET_CLIENT_LIST and _NET_CLIENT_LIST_STACKING properties This is needed for third party tasklists and drag and drop of tabs in chrome """ assert self.qtile # Regular top-level managed windows, i.e. excluding Static, Internal and Systray Icons wids = [wid for wid, c in self.qtile.windows_map.items() if isinstance(c, window.Window)] self._root.set_property("_NET_CLIENT_LIST", wids) # We rely on the stacking order from the X server stacked_wids = [] for wid in self._root.query_tree(): win = self.qtile.windows_map.get(wid) if not win: continue if isinstance(win, window.Window) and win.group: stacked_wids.append(wid) self._root.set_property("_NET_CLIENT_LIST_STACKING", stacked_wids) def update_desktops(self, groups, index: int) -> None: """Set the current desktops of the window manager The list of desktops is given by the list of groups, with the current desktop given by the index """ self._root.set_property("_NET_NUMBER_OF_DESKTOPS", len(groups)) self._root.set_property("_NET_DESKTOP_NAMES", "\0".join(i.name for i in groups)) self._root.set_property("_NET_CURRENT_DESKTOP", index) viewport = [] for group in groups: viewport += [group.screen.x, group.screen.y] if group.screen else [0, 0] self._root.set_property("_NET_DESKTOP_VIEWPORT", viewport) def lookup_key(self, key: config.Key | config.KeyChord) -> tuple[int, int]: """Find the keysym and the modifier mask for the given key""" if isinstance(key.key, str): keysym = xcbq.keysyms.get(key.key.lower()) if not keysym: raise utils.QtileError(f"Unknown keysym: {key.key}") else: keysym = self.conn.code_to_syms[key.key][0] if not keysym: raise utils.QtileError(f"Unknown keycode: {key.key}") modmask = xcbq.translate_masks(key.modifiers) return keysym, modmask def grab_key(self, key: config.Key | config.KeyChord) -> tuple[int, int]: """Map the key to receive events on it""" keysym, modmask = self.lookup_key(key) codes = self.conn.keysym_to_keycode(keysym) for code in codes: if code == 0: logger.warning("Can't grab %s (unknown keysym: %02x)", key, keysym) continue for amask in self._auto_modmasks(): self.conn.conn.core.GrabKey( True, self._root.wid, modmask | amask, code, xcffib.xproto.GrabMode.Async, xcffib.xproto.GrabMode.Async, ) return keysym, modmask & self._valid_mask def ungrab_key(self, key: config.Key | config.KeyChord) -> tuple[int, int]: """Ungrab the key corresponding to the given keysym and modifier mask""" keysym, modmask = self.lookup_key(key) codes = self.conn.keysym_to_keycode(keysym) for code in codes: for amask in self._auto_modmasks(): self.conn.conn.core.UngrabKey(code, self._root.wid, modmask | amask) return keysym, modmask & self._valid_mask def ungrab_keys(self) -> None: """Ungrab all of the key events""" self.conn.conn.core.UngrabKey( xcffib.xproto.Atom.Any, self._root.wid, xcffib.xproto.ModMask.Any ) def grab_pointer(self) -> None: """Get the focus for pointer events""" self.conn.conn.core.GrabPointer( True, self._root.wid, xcbq.ButtonMotionMask | xcbq.AllButtonsMask | xcbq.ButtonReleaseMask, xcffib.xproto.GrabMode.Async, xcffib.xproto.GrabMode.Async, xcffib.xproto.Atom._None, xcffib.xproto.Atom._None, xcffib.xproto.Atom._None, ) def ungrab_pointer(self) -> None: """Ungrab the focus for pointer events""" self.conn.conn.core.UngrabPointer(xcffib.xproto.Atom._None) def grab_button(self, mouse: config.Mouse) -> int: """Grab the given mouse button for events""" modmask = xcbq.translate_masks(mouse.modifiers) eventmask = EventMask.ButtonPress if isinstance(mouse, config.Drag): eventmask |= EventMask.ButtonRelease for amask in self._auto_modmasks(): self.conn.conn.core.GrabButton( True, self._root.wid, eventmask, xcffib.xproto.GrabMode.Async, xcffib.xproto.GrabMode.Async, xcffib.xproto.Atom._None, xcffib.xproto.Atom._None, mouse.button_code, modmask | amask, ) return modmask & self._valid_mask def ungrab_buttons(self) -> None: """Un-grab all mouse events""" self.conn.conn.core.UngrabButton( xcffib.xproto.Atom.Any, self._root.wid, xcffib.xproto.ModMask.Any ) def _auto_modmasks(self) -> Iterator[int]: """The modifier masks to add""" yield 0 yield xcbq.ModMasks["lock"] if self._numlock_mask: yield self._numlock_mask yield self._numlock_mask | xcbq.ModMasks["lock"] @contextlib.contextmanager def masked(self): for i in self.qtile.windows_map.values(): i._disable_mask(EventMask.EnterWindow | EventMask.FocusChange | EventMask.LeaveWindow) yield for i in self.qtile.windows_map.values(): i._reset_mask() def create_internal( self, x: int, y: int, width: int, height: int, desired_depth: int | None = 32 ) -> base.Internal: assert self.qtile is not None win = self.conn.create_window(x, y, width, height, desired_depth) internal = window.Internal(win, self.qtile, desired_depth) internal.place(x, y, width, height, 0, None) self.qtile.manage(internal) return internal def handle_FocusOut(self, event) -> None: # noqa: N802 if event.detail == xcffib.xproto.NotifyDetail._None: self.conn.fixup_focus() def handle_SelectionNotify(self, event) -> None: # noqa: N802 if not getattr(event, "owner", None): return name = self.conn.atoms.get_name(event.selection) self._selection[name]["owner"] = event.owner self._selection[name]["selection"] = "" self.convert_selection(event.selection) hook.fire("selection_notify", name, self._selection[name]) def handle_PropertyNotify(self, event) -> None: # noqa: N802 name = self.conn.atoms.get_name(event.atom) # it's the selection property if name in ("PRIMARY", "CLIPBOARD"): assert event.window == self._selection_window.wid prop = self._selection_window.get_property(event.atom, "UTF8_STRING") # If the selection property is None, it is unset, which means the # clipboard is empty. value = prop and prop.value.to_utf8() or "" self._selection[name]["selection"] = value hook.fire("selection_change", name, self._selection[name]) def handle_ClientMessage(self, event) -> None: # noqa: N802 assert self.qtile is not None atoms = self.conn.atoms opcode = event.type data = event.data # handle change of desktop if atoms["_NET_CURRENT_DESKTOP"] == opcode: index = data.data32[0] try: self.qtile.groups[index].toscreen() except IndexError: logger.debug("Invalid desktop index: %s", index) def handle_KeyPress(self, event, *, simulated=False) -> None: # noqa: N802 assert self.qtile is not None keysym = self.conn.code_to_syms[event.detail][0] key, handled = self.qtile.process_key_event(keysym, event.state & self._valid_mask) if simulated: # Even though simulated keybindings could use a proper X11 event, we don't want do any fake input # This is because it needs extra code for e.g. pressing/releasing the modifiers # This we don't want to handle and instead leave to external tools such as xdotool return # As we're grabbing async we can't just replay it, so... # We need to forward the event to the focused window if not handled and key: # We need to ungrab the key as otherwise we get an event loop self.ungrab_key(key) # Modifier is pressed, just repeat the event with xtest self._fake_KeyPress(event) # Grab the key again self.grab_key(key) def handle_ButtonPress(self, event) -> None: # noqa: N802 assert self.qtile is not None button_code = event.detail state = event.state & self._valid_mask if not event.child: # The client's handle_ButtonPress will focus it self.focus_by_click(event) self.qtile.process_button_click(button_code, state, event.event_x, event.event_y) self.conn.conn.core.AllowEvents(xcffib.xproto.Allow.ReplayPointer, event.time) def handle_ButtonRelease(self, event) -> None: # noqa: N802 assert self.qtile is not None button_code = event.detail state = event.state & self._valid_mask self.qtile.process_button_release(button_code, state) def handle_MotionNotify(self, event) -> None: # noqa: N802 assert self.qtile is not None # Limit the motion notify events from happening too frequently # As we already handle motion notify events "later", the default is None # So we also need to check if this has to be done in the first place resize_fps = self.qtile.current_screen.x11_drag_polling_rate if resize_fps is not None and (event.time - self._last_motion_time) <= ( 1000 / resize_fps ): return self._last_motion_time = event.time self.qtile.process_button_motion(event.event_x, event.event_y) def handle_ConfigureRequest(self, event): # noqa: N802 # It's not managed, or not mapped, so we just obey it. cw = xcffib.xproto.ConfigWindow args = {} if event.value_mask & cw.X: args["x"] = max(event.x, 0) if event.value_mask & cw.Y: args["y"] = max(event.y, 0) if event.value_mask & cw.Height: args["height"] = max(event.height, 0) if event.value_mask & cw.Width: args["width"] = max(event.width, 0) if event.value_mask & cw.BorderWidth: args["borderwidth"] = max(event.border_width, 0) w = window.XWindow(self.conn, event.window) w.configure(**args) def handle_MappingNotify(self, event): # noqa: N802 assert self.qtile is not None self.conn.refresh_keymap() if event.request == xcffib.xproto.Mapping.Keyboard: self.qtile.grab_keys() def handle_MapRequest(self, event) -> None: # noqa: N802 assert self.qtile is not None xwin = window.XWindow(self.conn, event.window) try: attrs = xwin.get_attributes() internal = xwin.get_property("QTILE_INTERNAL") except (xcffib.xproto.WindowError, xcffib.xproto.AccessError): return if attrs and attrs.override_redirect: return win = self.qtile.windows_map.get(xwin.wid) if win: if isinstance(win, window.Window) and win.group is self.qtile.current_group: win.unhide() return if internal: win = window.Internal(xwin, self.qtile) self.qtile.manage(win) win.unhide() else: win = window.Window(xwin, self.qtile) if xwin.get_wm_type() == "dock" or win.reserved_space: assert self.qtile.current_screen is not None win.static(self.qtile.current_screen.index) return self.qtile.manage(win) if not win.group or not win.group.screen: return win.unhide() self.update_client_lists() win.change_layer() def handle_DestroyNotify(self, event) -> None: # noqa: N802 assert self.qtile is not None self.qtile.unmanage(event.window) self.update_client_lists() if self.qtile.current_window is None: self.conn.fixup_focus() def handle_UnmapNotify(self, event) -> None: # noqa: N802 assert self.qtile is not None win = self.qtile.windows_map.get(event.window) if win and getattr(win, "group", None): try: win.hide() win.state = window.WithdrawnState # type: ignore except xcffib.xproto.WindowError: # This means that the window has probably been destroyed, # but we haven't yet seen the DestroyNotify (it is likely # next in the queue). So, we just let these errors pass # since the window is dead. pass # Clear these atoms as per spec win.window.conn.conn.core.DeleteProperty( win.wid, win.window.conn.atoms["_NET_WM_STATE"] ) win.window.conn.conn.core.DeleteProperty( win.wid, win.window.conn.atoms["_NET_WM_DESKTOP"] ) self.qtile.unmanage(event.window) self.update_client_lists() if self.qtile.current_window is None: self.conn.fixup_focus() def handle_ScreenChangeNotify(self, event) -> None: # noqa: N802 hook.fire("screen_change", event) def _fake_input(self, input_type, detail, x=0, y=0) -> None: self._xtest.FakeInput( input_type, detail, 0, # This is a delay, not timestamp, according to AwesomeWM xcffib.XCB_NONE, x, # x: Only used for motion events y, # y: Only used for motion events 0, ) self.flush() def _fake_KeyPress(self, event) -> None: # noqa: N802 # First release the key as it is possibly already pressed for input_type in ( xcbq.XCB_KEY_RELEASE, xcbq.XCB_KEY_PRESS, xcbq.XCB_KEY_RELEASE, ): self._fake_input(input_type, event.detail) @contextlib.contextmanager def disable_unmap_events(self): self._root.set_attribute(eventmask=self.eventmask & (~EventMask.SubstructureNotify)) yield self._root.set_attribute(eventmask=self.eventmask) @property def painter(self): if self._painter is None: self._painter = xcbq.Painter(self._display_name) return self._painter def simulate_keypress(self, modifiers, key): """Simulates a keypress on the focused window.""" modmasks = xcbq.translate_masks(modifiers) keysym = xcbq.keysyms.get(key.lower()) class DummyEv: pass d = DummyEv() d.detail = self.conn.keysym_to_keycode(keysym)[0] d.state = modmasks self.handle_KeyPress(d, simulated=True) def focus_by_click(self, e, window=None): """Bring a window to the front Parameters ========== e: xcb event Click event used to determine window to focus """ qtile = self.qtile assert qtile is not None if window: if qtile.config.bring_front_click and ( qtile.config.bring_front_click != "floating_only" or getattr(window, "floating", False) ): window.bring_to_front() try: if window.group.screen is not qtile.current_screen: qtile.focus_screen(window.group.screen.index, warp=False) qtile.current_group.focus(window, False) window.focus(False) except AttributeError: # probably clicked an internal window screen = qtile.find_screen(e.root_x, e.root_y) if screen: qtile.focus_screen(screen.index, warp=False) else: # clicked on root window screen = qtile.find_screen(e.root_x, e.root_y) if screen: if qtile.current_window: qtile.current_window._grab_click() qtile.focus_screen(screen.index, warp=False) def flush(self): self.conn.flush() def graceful_shutdown(self): """Try to close windows gracefully before exiting""" try: pids = [] for win in self.qtile.windows_map.values(): if not isinstance(win, base.Internal): if pid := win.get_pid(): pids.append(pid) except xcffib.ConnectionException: logger.warning("Server disconnected, couldn't close windows gracefully.") return # Give the windows a chance to shut down nicely. for pid in pids: try: os.kill(pid, signal.SIGTERM) except OSError: # might have died recently pass def still_alive(pid): # most pids will not be children, so we can't use wait() try: os.kill(pid, 0) return True except OSError: return False # give everyone a little time to exit and write their state. but don't # sleep forever (1s). for i in range(10): pids = list(filter(still_alive, pids)) if len(pids) == 0: break time.sleep(0.1) def get_mouse_position(self) -> tuple[int, int]: """ Get mouse coordinates. """ reply = self.conn.conn.core.QueryPointer(self._root.wid).reply() return reply.root_x, reply.root_y def keysym_from_name(self, name: str) -> int: """Get the keysym for a key from its name""" return keysyms[name.lower()] def check_stacking(self, win: base.Window) -> None: """Triggers restacking if a fullscreen window loses focus.""" if win is self.last_focused: return if self.last_focused and self.last_focused.fullscreen: self.last_focused.change_layer() self.last_focused = win @property def hovered_window(self) -> base.WindowType | None: _hovered_window = self.conn.conn.core.QueryPointer(self._root.wid).reply().child return self.qtile.windows_map.get(_hovered_window) qtile-0.31.0/libqtile/pangocffi.py0000664000175000017500000001510514762660347016753 0ustar epsilonepsilon# Copyright (c) 2014-2015 Sean Vig # Copyright (c) 2014 roger # Copyright (c) 2014 Tycho Andersen # Copyright (c) 2015 Craig Barnes # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # This module is kind of a hack; you've been warned :-). Some upstream work # needs to happen in order to avoid doing this, though. # # The problem is that we want to use pango to draw stuff. We need to create a # cairo surface, in particular an XCB surface. Since we're using xcffib as the # XCB binding and there is no portable way to go from cffi's PyObject* cdata # wrappers to the wrapped type [1], we can't add support to pycairo for XCB # surfaces via xcffib. # # A similar problem exists one layer of indirection down with cairocffi -- # python's pangocairo is almost all C, and only works by including pycairo's # headers and accessing members of structs only available in C, and not in # python. Since cairocffi is pure python and also cffi based, we cannot extract # the raw pointer to pass to the existing pangocairo bindings. # # The solution here is to implement a tiny pangocffi for the small set of pango # functions we call. We're doing it directly here because we can, but it would # not be difficult to use more upstream libraries (e.g. cairocffi and some # pangocairocffi when it exists). This also allows us to drop pygtk entirely, # since we are doing our own pango binding. # # [1]: https://groups.google.com/forum/#!topic/python-cffi/SPND0rRmazA # # This is not intended to be a complete cffi-based pango binding. from libqtile.pango_ffi import pango_ffi as ffi gobject = ffi.dlopen("libgobject-2.0.so.0") # type: ignore pango = ffi.dlopen("libpango-1.0.so.0") # type: ignore pangocairo = ffi.dlopen("libpangocairo-1.0.so.0") # type: ignore def patch_cairo_context(cairo_t): def create_layout(): return PangoLayout(cairo_t._pointer) cairo_t.create_layout = create_layout def show_layout(layout): pangocairo.pango_cairo_show_layout(cairo_t._pointer, layout._pointer) cairo_t.show_layout = show_layout return cairo_t ALIGN_CENTER = pango.PANGO_ALIGN_CENTER ELLIPSIZE_END = pango.PANGO_ELLIPSIZE_END units_from_double = pango.pango_units_from_double ALIGNMENTS = { "left": pango.PANGO_ALIGN_LEFT, "center": pango.PANGO_ALIGN_CENTER, "right": pango.PANGO_ALIGN_RIGHT, } class PangoLayout: def __init__(self, cairo_t): self._cairo_t = cairo_t self._pointer = pangocairo.pango_cairo_create_layout(cairo_t) def free(p): gobject.g_object_unref(p) self._pointer = ffi.gc(self._pointer, free) def finalize(self): self._desc = None self._pointer = None self._cairo_t = None def finalized(self): return self._pointer is None def set_font_description(self, desc): # save a pointer so it doesn't get GC'd out from under us self._desc = desc pango.pango_layout_set_font_description(self._pointer, desc._pointer) def get_font_description(self): descr = pango.pango_layout_get_font_description(self._pointer) return FontDescription(descr) def set_alignment(self, alignment): pango.pango_layout_set_alignment(self._pointer, alignment) def set_attributes(self, attrs): pango.pango_layout_set_attributes(self._pointer, attrs) def set_text(self, text): text = text.encode("utf-8") pango.pango_layout_set_text(self._pointer, text, -1) def get_text(self): ret = pango.pango_layout_get_text(self._pointer) return ffi.string(ret).decode() def set_ellipsize(self, ellipzize): pango.pango_layout_set_ellipsize(self._pointer, ellipzize) def get_ellipsize(self): return pango.pango_layout_get_ellipsize(self._pointer) def get_pixel_size(self): width = ffi.new("int[1]") height = ffi.new("int[1]") pango.pango_layout_get_pixel_size(self._pointer, width, height) return width[0], height[0] def set_width(self, width): pango.pango_layout_set_width(self._pointer, width) class FontDescription: def __init__(self, pointer=None): if pointer is None: self._pointer = pango.pango_font_description_new() self._pointer = ffi.gc(self._pointer, pango.pango_font_description_free) else: self._pointer = pointer @classmethod def from_string(cls, string): pointer = pango.pango_font_description_from_string(string.encode()) pointer = ffi.gc(pointer, pango.pango_font_description_free) return cls(pointer) def set_family(self, family): pango.pango_font_description_set_family(self._pointer, family.encode()) def get_family(self): ret = pango.pango_font_description_get_family(self._pointer) return ffi.string(ret).decode() def set_absolute_size(self, size): pango.pango_font_description_set_absolute_size(self._pointer, size) def set_size(self, size): pango.pango_font_description_set_size(self._pointer, size) def get_size(self): return pango.pango_font_description_get_size(self._pointer) def parse_markup(value, accel_marker=0): attr_list = ffi.new("PangoAttrList**") text = ffi.new("char**") error = ffi.new("GError**") value = value.encode() ret = pango.pango_parse_markup(value, -1, accel_marker, attr_list, text, ffi.NULL, error) if ret == 0: raise Exception(f"parse_markup() failed for {value}") return attr_list[0], ffi.string(text[0]), chr(accel_marker) def markup_escape_text(text): ret = gobject.g_markup_escape_text(text.encode("utf-8"), -1) return ffi.string(ret).decode() qtile-0.31.0/libqtile/config.py0000664000175000017500000011665114762660347016274 0ustar epsilonepsilon# Copyright (c) 2012-2015 Tycho Andersen # Copyright (c) 2013 xarvh # Copyright (c) 2013 horsik # Copyright (c) 2013-2014 roger # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014 ramnes # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Adi Sieker # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # from __future__ import annotations import os.path import re import sys from dataclasses import dataclass from typing import TYPE_CHECKING from libqtile import configurable, hook, utils from libqtile.bar import Bar from libqtile.command.base import CommandObject, expose_command from libqtile.log_utils import logger if TYPE_CHECKING: from collections.abc import Callable, Iterable from typing import Any from libqtile.backend import base from libqtile.bar import BarType from libqtile.command.base import ItemT from libqtile.core.manager import Qtile from libqtile.group import _Group from libqtile.layout.base import Layout from libqtile.lazy import LazyCall from libqtile.utils import ColorType class Key: """ Defines a keybinding. Parameters ========== modifiers: A list of modifier specifications. Modifier specifications are one of: ``"shift"``, ``"lock"``, ``"control"``, ``"mod1"``, ``"mod2"``, ``"mod3"``, ``"mod4"``, ``"mod5"``. key: A key specification, e.g. ``"a"``, ``"Tab"``, ``"Return"``, ``"space"``. Also accepts an integer value representing a keycode. commands: One or more :class:`LazyCall` objects to evaluate in sequence upon keypress. Multiple commands should be separated by commas. desc: Description to be added to the key binding. (Optional) swallow: Configures when we swallow the key binding. (Optional) Setting it to False will forward the key binding to the focused window after the commands have been executed. """ def __init__( self, modifiers: list[str], key: str | int, *commands: LazyCall, desc: str = "", swallow: bool = True, ) -> None: self.modifiers = modifiers self.key = key self.commands = commands self.desc = desc self.swallow = swallow def __repr__(self) -> str: return f"" class KeyChord: """ Define a key chord aka Vim-like mode. Parameters ========== modifiers: A list of modifier specifications. Modifier specifications are one of: ``"shift"``, ``"lock"``, ``"control"``, ``"mod1"``, ``"mod2"``, ``"mod3"``, ``"mod4"``, ``"mod5"``. key: A key specification, e.g. ``"a"``, ``"Tab"``, ``"Return"``, ``"space"``. Also accepts an integer value representing a keycode. submappings: A list of :class:`Key` or :class:`KeyChord` declarations to bind in this chord. mode: Boolean. Setting to ``True`` will result in the chord persisting until Escape is pressed. Setting to ``False`` (default) will exit the chord once the sequence has ended. name: A string to name the chord. The name will be displayed in the Chord widget. desc: A string to describe the chord. This attribute is not directly used by Qtile but users may want to access this when creating scripts to show configured keybindings. swallow: Configures when we swallow the key binding of the chord. (Optional) Setting it to False will forward the key binding to the focused window after the commands have been executed. """ def __init__( self, modifiers: list[str], key: str | int, submappings: list[Key | KeyChord], mode: bool | str = False, name: str = "", desc: str = "", swallow: bool = True, ): self.modifiers = modifiers self.key = key submappings.append(Key([], "Escape")) self.submappings = submappings self.mode = mode self.name = name self.desc = desc if isinstance(mode, str): logger.warning( "The use of `mode` to set the KeyChord name is deprecated. " "Please use `name='%s'` instead. " "'mode' should be a boolean value to set whether the chord is persistent (True) or not.", mode, ) self.name = mode self.mode = True self.swallow = swallow def __repr__(self) -> str: return f"" class Mouse: def __init__(self, modifiers: list[str], button: str, *commands: LazyCall) -> None: self.modifiers = modifiers self.button = button self.commands = commands self.button_code = int(self.button.replace("Button", "")) self.modmask: int = 0 class Drag(Mouse): """ Bind commands to a dragging action. On each motion event the bound commands are executed with two additional parameters specifying the x and y offset from the previous position. Parameters ========== modifiers: A list of modifier specifications. Modifier specifications are one of: ``"shift"``, ``"lock"``, ``"control"``, ``"mod1"``, ``"mod2"``, ``"mod3"``, ``"mod4"``, ``"mod5"``. button: The button used to start dragging e.g. ``"Button1"``. commands: A list :class:`LazyCall` objects to evaluate in sequence upon drag. start: A :class:`LazyCall` object to be evaluated when dragging begins. (Optional) warp_pointer: A :class:`bool` indicating if the pointer should be warped to the bottom right of the window at the start of dragging. (Default: `False`) """ def __init__( self, modifiers: list[str], button: str, *commands: LazyCall, start: LazyCall | None = None, warp_pointer: bool = False, ) -> None: super().__init__(modifiers, button, *commands) self.start = start self.warp_pointer = warp_pointer def __repr__(self) -> str: return f"" class Click(Mouse): """ Bind commands to a clicking action. Parameters ========== modifiers: A list of modifier specifications. Modifier specifications are one of: ``"shift"``, ``"lock"``, ``"control"``, ``"mod1"``, ``"mod2"``, ``"mod3"``, ``"mod4"``, ``"mod5"``. button: The button used to click e.g. ``"Button1"``. commands: A list :class:`LazyCall` objects to evaluate in sequence upon click. """ def __repr__(self) -> str: return f"" class EzConfig: """ Helper class for defining key and button bindings in an Emacs-like format. Inspired by Xmonad's XMonad.Util.EZConfig. Splits an emacs keydef into modifiers and keys. For example: "m-s-a" -> ['mod4', 'shift'], 'a' "a-" -> ['mod1'], 'minus' "C-" -> ['control'], 'Tab' """ modifier_keys = { "M": "mod4", "A": "mod1", "S": "shift", "C": "control", } def parse(self, spec: str) -> tuple[list[str], str]: mods = [] keys: list[str] = [] for key in spec.split("-"): if not key: break if key in self.modifier_keys: if keys: msg = "Modifiers must always come before key/btn: %s" raise utils.QtileError(msg % spec) mods.append(self.modifier_keys[key]) continue if len(key) == 1: keys.append(key) continue if len(key) > 3 and key[0] == "<" and key[-1] == ">": keys.append(key[1:-1]) continue if not keys: msg = "Invalid key/btn specifier: %s" raise utils.QtileError(msg % spec) if len(keys) > 1: msg = f"Key chains are not supported: {spec}" raise utils.QtileError(msg) return mods, keys[0] class EzKey(EzConfig, Key): """ Defines a keybinding using the Emacs-like format. Parameters ========== keydef: The Emacs-like key specification, e.g. ``"M-S-a"``. commands: A list :class:`LazyCall` objects to evaluate in sequence upon keypress. desc: Description to be added to the key binding. (Optional) """ def __init__(self, keydef: str, *commands: LazyCall, desc: str = "") -> None: modkeys, key = self.parse(keydef) super().__init__(modkeys, key, *commands, desc=desc) class EzKeyChord(EzConfig, KeyChord): """ Define a key chord using the Emacs-like format. Parameters ========== keydef: The Emacs-like key specification, e.g. ``"M-S-a"``. submappings: A list of :class:`Key` or :class:`KeyChord` declarations to bind in this chord. mode: Boolean. Setting to ``True`` will result in the chord persisting until Escape is pressed. Setting to ``False`` (default) will exit the chord once the sequence has ended. name: A string to name the chord. The name will be displayed in the Chord widget. desc: A string to describe the chord. This attribute is not directly used by Qtile but users may want to access this when creating scripts to show configured keybindings. """ def __init__( self, keydef: str, submappings: list[Key | KeyChord], mode: bool | str = False, name: str = "", desc: str = "", ): modkeys, key = self.parse(keydef) super().__init__(modkeys, key, submappings, mode, name, desc) class EzClick(EzConfig, Click): """ Bind commands to a clicking action using the Emacs-like format. Parameters ========== btndef: The Emacs-like button specification, e.g. ``"M-1"``. commands: A list :class:`LazyCall` objects to evaluate in sequence upon drag. """ def __init__(self, btndef: str, *commands: LazyCall) -> None: modkeys, button = self.parse(btndef) button = f"Button{button}" super().__init__(modkeys, button, *commands) class EzDrag(EzConfig, Drag): """ Bind commands to a dragging action using the Emacs-like format. Parameters ========== btndef: The Emacs-like button specification, e.g. ``"M-1"``. commands: A list :class:`LazyCall` objects to evaluate in sequence upon drag. start: A :class:`LazyCall` object to be evaluated when dragging begins. (Optional) """ def __init__(self, btndef: str, *commands: LazyCall, start: LazyCall | None = None) -> None: modkeys, button = self.parse(btndef) button = f"Button{button}" super().__init__(modkeys, button, *commands, start=start) @dataclass class ScreenRect: x: int y: int width: int height: int def hsplit(self, columnwidth: int) -> tuple[ScreenRect, ScreenRect]: assert 0 < columnwidth < self.width return ( self.__class__(self.x, self.y, columnwidth, self.height), self.__class__(self.x + columnwidth, self.y, self.width - columnwidth, self.height), ) def vsplit(self, rowheight: int) -> tuple[ScreenRect, ScreenRect]: assert 0 < rowheight < self.height return ( self.__class__(self.x, self.y, self.width, rowheight), self.__class__(self.x, self.y + rowheight, self.width, self.height - rowheight), ) class Screen(CommandObject): r""" A physical screen, and its associated paraphernalia. Define a screen with a given set of :class:`Bar`\s of a specific geometry. Also, ``x``, ``y``, ``width``, and ``height`` aren't specified usually unless you are using 'fake screens'. The ``background`` parameter, if given, should be a valid single colour. This will paint a solid background colour to the screen. Note, the setting is ignored if ``wallpaper`` is also set (see below). The ``wallpaper`` parameter, if given, should be a path to an image file. How this image is painted to the screen is specified by the ``wallpaper_mode`` parameter. By default, the image will be placed at the screens origin and retain its own dimensions. If the mode is ``"fill"``, the image will be centred on the screen and resized to fill it. If the mode is ``"stretch"``, the image is stretched to fit all of it into the screen. The ``x11_drag_polling_rate`` parameter specifies the rate for drag events in the X11 backend. By default this is set to None, indicating no limit. Because in the X11 backend we already handle motion notify events later, the performance should already be okay. However, to limit these events further you can use this variable and e.g. set it to your monitor refresh rate. 60 would mean that we handle a drag event 60 times per second. """ group: _Group index: int def __init__( self, top: BarType | None = None, bottom: BarType | None = None, left: BarType | None = None, right: BarType | None = None, background: ColorType | None = None, wallpaper: str | None = None, wallpaper_mode: str | None = None, x11_drag_polling_rate: int | None = None, x: int | None = None, y: int | None = None, width: int | None = None, height: int | None = None, ) -> None: self.top = top self.bottom = bottom self.left = left self.right = right self.background = background self.wallpaper = wallpaper self.wallpaper_mode = wallpaper_mode self.x11_drag_polling_rate = x11_drag_polling_rate self.qtile: Qtile | None = None # x position of upper left corner can be > 0 # if one screen is "right" of the other self.x = x if x is not None else 0 self.y = y if y is not None else 0 self.width = width if width is not None else 0 self.height = height if height is not None else 0 self.previous_group: _Group | None = None def _configure( self, qtile: Qtile, index: int, x: int, y: int, width: int, height: int, group: _Group, reconfigure_gaps: bool = False, ) -> None: self.qtile = qtile self.index = index self.x = x self.y = y self.width = width self.height = height for i in self.gaps: i._configure(qtile, self, reconfigure=reconfigure_gaps) self.set_group(group) if self.wallpaper: self.wallpaper = os.path.expanduser(self.wallpaper) self.paint(self.wallpaper, self.wallpaper_mode) elif self.background: self.qtile.fill_screen(self, self.background) def paint(self, path: str, mode: str | None = None) -> None: if self.qtile: self.qtile.paint_screen(self, path, mode) @property def gaps(self) -> Iterable[BarType]: return (i for i in [self.top, self.bottom, self.left, self.right] if i) def finalize_gaps(self) -> None: def remove(attr: str) -> None: gap = getattr(self, attr, None) if gap is not None: setattr(self, attr, None) gap.finalize() for attr in ["top", "bottom", "left", "right"]: remove(attr) @property def dx(self) -> int: if self.left and getattr(self.left, "reserve", True): return self.x + self.left.size return self.x @property def dy(self) -> int: if self.top and getattr(self.top, "reserve", True): return self.y + self.top.size return self.y @property def dwidth(self) -> int: val = self.width if self.left and getattr(self.left, "reserve", True): val -= self.left.size if self.right and getattr(self.right, "reserve", True): val -= self.right.size return val @property def dheight(self) -> int: val = self.height if self.top and getattr(self.top, "reserve", True): val -= self.top.size if self.bottom and getattr(self.bottom, "reserve", True): val -= self.bottom.size return val def get_rect(self) -> ScreenRect: return ScreenRect(self.dx, self.dy, self.dwidth, self.dheight) def set_group( self, new_group: _Group | None, save_prev: bool = True, warp: bool = True ) -> None: """Put group on this screen""" if new_group is None: return if new_group.screen == self: return if save_prev and new_group is not self.group: # new_group can be self.group only if the screen is getting configured for # the first time self.previous_group = self.group if new_group.screen: # g1 <-> s1 (self) # g2 (new_group) <-> s2 to # g1 <-> s2 # g2 <-> s1 g1 = self.group s1 = self g2 = new_group s2 = new_group.screen s2.group = g1 g1.set_screen(s2, warp) s1.group = g2 g2.set_screen(s1, warp) else: assert self.qtile is not None old_group = self.group self.group = new_group with self.qtile.core.masked(): # display clients of the new group and then hide from old group # to remove the screen flickering new_group.set_screen(self, warp) # Can be the same group only if the screen just got configured for the # first time - see `Qtile._process_screens`. if old_group is not new_group: old_group.set_screen(None, warp) hook.fire("setgroup") hook.fire("focus_change") hook.fire("layout_change", self.group.layouts[self.group.current_layout], self.group) def _toggle_group(self, group: _Group | None = None, warp: bool = True) -> None: """Switch to the selected group or to the previously active one""" if group in (self.group, None) and self.previous_group: group = self.previous_group self.set_group(group, warp=warp) def _items(self, name: str) -> ItemT: if name == "layout" and self.group is not None: return True, list(range(len(self.group.layouts))) elif name == "window" and self.group is not None: return True, [i.wid for i in self.group.windows] elif name == "bar": return False, [x.position for x in self.gaps if isinstance(x, Bar)] elif name == "widget": bars = (g for g in self.gaps if isinstance(g, Bar)) return False, [w.name for b in bars for w in b.widgets] elif name == "group": return True, [self.group.name] return None def _select(self, name: str, sel: str | int | None) -> CommandObject | None: if name == "layout": if sel is None: return self.group.layout else: assert isinstance(sel, int) return utils.lget(self.group.layouts, sel) elif name == "window": if sel is None: return self.group.current_window else: for i in self.group.windows: if i.wid == sel: return i elif name == "bar": assert isinstance(sel, str) bar = getattr(self, sel) if isinstance(bar, Bar): return bar elif name == "widget": for gap in self.gaps: if not isinstance(gap, Bar): continue for widget in gap.widgets: if widget.name == sel: return widget elif name == "group": if sel is None: return self.group else: return self.group if sel == self.group.name else None return None @expose_command def resize( self, x: int | None = None, y: int | None = None, w: int | None = None, h: int | None = None, ) -> None: assert self.qtile is not None if x is None: x = self.x if y is None: y = self.y if w is None: w = self.width if h is None: h = self.height self._configure(self.qtile, self.index, x, y, w, h, self.group) for bar in [self.top, self.bottom, self.left, self.right]: if bar: bar.draw() self.group.layout_all() @expose_command() def info(self) -> dict[str, int]: """Returns a dictionary of info for this screen.""" return dict(index=self.index, width=self.width, height=self.height, x=self.x, y=self.y) @expose_command() def next_group( self, skip_empty: bool = False, skip_managed: bool = False, warp: bool = True ) -> None: """Switch to the next group""" group = self.group.get_next_group(skip_empty, skip_managed) self.set_group(group, warp=warp) return group.name if group is not None else None @expose_command() def prev_group( self, skip_empty: bool = False, skip_managed: bool = False, warp: bool = True ) -> None: """Switch to the previous group""" group = self.group.get_previous_group(skip_empty, skip_managed) self.set_group(group, warp=warp) return group.name if group is not None else None @expose_command() def toggle_group(self, group_name: str | None = None, warp: bool = True) -> None: """Switch to the selected group or to the previously active one""" assert self.qtile is not None group = self.qtile.groups_map.get(group_name if group_name else "") self._toggle_group(group, warp=warp) @expose_command() def set_wallpaper(self, path: str, mode: str | None = None) -> None: """Set the wallpaper to the given file.""" self.paint(path, mode) class Group: """ Represents a "dynamic" group These groups can spawn apps, only allow certain Matched windows to be on them, hide when they're not in use, etc. Groups are identified by their name. Parameters ========== name: The name of this group. matches: List of :class:`Match` objects whose matched windows will be assigned to this group. exclusive: When other apps are started in this group, should we allow them here or not? spawn: This will be executed (via ``qtile.spawn()``) when the group is created. You can pass either a program name or a list of programs to ``exec()``. layout: The name of default layout for this group (e.g. ``"max"``). This is the name specified for a particular layout in ``config.py`` or if not defined it defaults in general to the class name in all lower case. layouts: The group layouts list overriding global layouts. Use this to define a separate list of layouts for this particular group. persist: Should this group stay alive when it has no member windows? init: Should this group be alive when Qtile starts? layout_opts: Options to pass to a layout. screen_affinity: Make a dynamic group prefer to start on a specific screen. position: The position of this group. label: The display name of the group. Use this to define a display name other than name of the group. If set to ``None``, the display name is set to the name. """ def __init__( self, name: str, matches: list[Match] | None = None, exclusive: bool = False, spawn: str | list[str] | None = None, layout: str | None = None, layouts: list[Layout] | None = None, persist: bool = True, init: bool = True, layout_opts: dict[str, Any] | None = None, screen_affinity: int | None = None, position: int = sys.maxsize, label: str | None = None, ) -> None: self.name = name self.label = label self.exclusive = exclusive self.spawn = spawn self.layout = layout self.layouts = layouts or [] self.persist = persist self.init = init self.matches = matches or [] self.layout_opts = layout_opts or {} self.screen_affinity = screen_affinity self.position = position def __repr__(self) -> str: attrs = utils.describe_attributes( self, [ "exclusive", "spawn", "layout", "layouts", "persist", "init", "matches", "layout_opts", "screen_affinity", ], ) return f"" class ScratchPad(Group): """ Represents a "ScratchPad" group ScratchPad adds a (by default) invisible group to Qtile. That group is used as a place for currently not visible windows spawned by a :class:`DropDown` configuration. Parameters ========== name: The name of this group. dropdowns: :class:`DropDown` s available on the scratchpad. position: The position of this group. label: The display name of the :class:`ScratchPad` group. Defaults to the empty string such that the group is hidden in :class:`~libqtile.widget.GroupBox` widget. single: If ``True``, only one of the dropdowns will be visible at a time. """ def __init__( self, name: str, dropdowns: list[DropDown] | None = None, position: int = sys.maxsize, label: str = "", single: bool = False, ) -> None: Group.__init__( self, name, layout="floating", init=False, position=position, label=label, ) self.dropdowns = dropdowns if dropdowns is not None else [] self.single = single def __repr__(self) -> str: return "".format( self.name, ", ".join(dd.name for dd in self.dropdowns), ) def convert_deprecated_list(vals: list[str], name: str) -> re.Pattern: # make a regex with OR on word boundaries regex_input = r"^({})$".format("|".join(map(re.escape, vals))) logger.warning( "Your Match with the %s property is using lists which are deprecated, replace Match(%s=%s) with Match(%s=re.compile(r\"%s\")) after importing the 're' module", name, name, vals, name, regex_input, ) return re.compile(regex_input) class _Match: """Base class to implement bitwise logic methods for Match objects.""" def compare(self, client: base.Window) -> bool: return True def __invert__(self) -> InvertMatch: return InvertMatch(self) def __and__(self, other: _Match) -> MatchAll: if not isinstance(other, _Match): raise TypeError return MatchAll(self, other) def __or__(self, other: _Match) -> MatchAny: if not isinstance(other, _Match): raise TypeError return MatchAny(self, other) def __xor__(self, other: _Match) -> MatchOnlyOne: if not isinstance(other, _Match): raise TypeError return MatchOnlyOne(self, other) class InvertMatch(_Match): """Wrapper to invert the result of the comparison.""" def __init__(self, match: _Match): self.match = match def compare(self, client: base.Window) -> bool: return not self.match.compare(client) def __repr__(self) -> str: return f"" class MatchAll(_Match): """Wrapper to check if all comparisons return True.""" def __init__(self, *matches: _Match): self.matches = matches def compare(self, client: base.Window) -> bool: return all(m.compare(client) for m in self.matches) def __repr__(self) -> str: return f"" class MatchAny(MatchAll): """Wrapper to check if at least one of the comparisons returns True.""" def compare(self, client: base.Window) -> bool: return any(m.compare(client) for m in self.matches) def __repr__(self) -> str: return f"" class MatchOnlyOne(_Match): """Wrapper to check if only one of the two comparisons returns True.""" def __init__(self, match1: _Match, match2: _Match): self.match1 = match1 self.match2 = match2 def compare(self, client: base.Window) -> bool: return self.match1.compare(client) != self.match2.compare(client) def __repr__(self) -> str: return f"" class Match(_Match): """ Window properties to compare (match) with a window. The properties will be compared to a :class:`~libqtile.base.Window` to determine if its properties *match*. It can match by title, wm_class, role, wm_type, wm_instance_class, net_wm_pid, or wid. Additionally, a function may be passed, which takes in the :class:`~libqtile.base.Window` to be compared against and returns a boolean. For some properties, :class:`Match` supports both regular expression objects (i.e. the result of ``re.compile()``) or strings (match as an exact string). If a window matches all specified values, it is considered a match. Parameters ========== title: Match against the WM_NAME atom (X11) or title (Wayland). wm_class: Match against any value in the whole WM_CLASS atom (X11) or app ID (Wayland). role: Match against the WM_ROLE atom (X11 only). wm_type: Match against the WM_TYPE atom (X11 only). wm_instance_class: Match against the first string in WM_CLASS atom (X11) or app ID (Wayland). net_wm_pid: Match against the _NET_WM_PID atom (X11) or PID (Wayland). func: Delegate the match to the given function, which receives the tested client as an argument and must return ``True`` if it matches, ``False`` otherwise. wid: Match against the window ID. This is a unique ID given to each window. """ def __init__( self, title: str | re.Pattern | None = None, wm_class: str | re.Pattern | None = None, role: str | re.Pattern | None = None, wm_type: str | re.Pattern | None = None, wm_instance_class: str | re.Pattern | None = None, net_wm_pid: int | None = None, func: Callable[[base.Window], bool] | None = None, wid: int | None = None, ) -> None: self._rules: dict[str, Any] = {} if title is not None: if isinstance(title, list): # type: ignore title = convert_deprecated_list(title, "title") # type: ignore self._rules["title"] = title if wm_class is not None: if isinstance(wm_class, list): # type: ignore wm_class = convert_deprecated_list(wm_class, "wm_class") # type: ignore self._rules["wm_class"] = wm_class if wm_instance_class is not None: if isinstance(wm_instance_class, list): # type: ignore wm_instance_class = convert_deprecated_list( # type: ignore wm_instance_class, "wm_instance_class" ) self._rules["wm_instance_class"] = wm_instance_class if wid is not None: self._rules["wid"] = wid if net_wm_pid is not None: try: self._rules["net_wm_pid"] = int(net_wm_pid) except ValueError: error = f'Invalid rule for net_wm_pid: "{str(net_wm_pid)}" only int allowed' raise utils.QtileError(error) if func is not None: self._rules["func"] = func if role is not None: if isinstance(role, list): # type: ignore role = convert_deprecated_list(role, "role") # type: ignore self._rules["role"] = role if wm_type is not None: if isinstance(wm_type, list): # type: ignore wm_type = convert_deprecated_list(wm_type, "wm_type") # type: ignore self._rules["wm_type"] = wm_type @staticmethod def _get_property_predicate(name: str, value: Any) -> Callable[..., bool]: if name == "net_wm_pid" or name == "wid": return lambda other: other == value elif name == "wm_class": def predicate(other) -> bool: # type: ignore match = getattr(other, "match", lambda v: v == other) return value and any(match(v) for v in value) return predicate else: def predicate(other) -> bool: # type: ignore match = getattr(other, "match", lambda v: v == other) return match(value) return predicate def compare(self, client: base.Window) -> bool: value: Any for property_name, rule_value in self._rules.items(): if property_name == "title": value = client.name elif "class" in property_name: wm_class = client.get_wm_class() if not wm_class: return False if property_name == "wm_instance_class": value = wm_class[0] else: value = wm_class elif property_name == "role": value = client.get_wm_role() elif property_name == "func": return rule_value(client) elif property_name == "net_wm_pid": value = client.get_pid() elif property_name == "wid": value = client.wid else: value = client.get_wm_type() # Some of the window.get_...() functions can return None if value is None: return False match = self._get_property_predicate(property_name, value) if not match(rule_value): return False if not self._rules: return False return True def map(self, callback: Callable[[base.Window], Any], clients: list[base.Window]) -> None: """Apply callback to each client that matches this Match""" for c in clients: if self.compare(c): callback(c) def __repr__(self) -> str: return f"" class Rule: """ How to act on a match. A :class:`Rule` contains a list of :class:`Match` objects, and a specification about what to do when any of them is matched. Parameters ========== match: :class:`Match` object or a list of such associated with this rule. float: Should we auto float this window? intrusive: Should we override the group's exclusive setting? break_on_match: Should we stop applying rules if this rule is matched? """ def __init__( self, match: _Match | list[_Match], group: _Group | None = None, float: bool = False, intrusive: bool = False, break_on_match: bool = True, ) -> None: if isinstance(match, _Match): self.matchlist = [match] else: self.matchlist = match self.group = group self.float = float self.intrusive = intrusive self.break_on_match = break_on_match def matches(self, w: base.Window) -> bool: return any(w.match(m) for m in self.matchlist) def __repr__(self) -> str: actions = utils.describe_attributes( self, ["group", "float", "intrusive", "break_on_match"] ) return f"" class DropDown(configurable.Configurable): """ Configure a specified command and its associated window for the :class:`ScratchPad`. That window can be shown and hidden using a configurable keystroke or any other scripted trigger. """ defaults = ( ( "x", 0.1, "X position of window as fraction of current screen width. " "0 is the left most position.", ), ( "y", 0.0, "Y position of window as fraction of current screen height. " "0 is the top most position. To show the window at bottom, " "you have to configure a value < 1 and an appropriate height.", ), ("width", 0.8, "Width of window as fraction of current screen width"), ("height", 0.35, "Height of window as fraction of current screen."), ("opacity", 0.9, "Opacity of window as fraction. One is opaque."), ( "on_focus_lost_hide", True, "Shall the window be hidden if focus is lost? If so, the :class:`DropDown` " "is hidden if window focus or the group is changed.", ), ( "warp_pointer", True, "Shall pointer warp to center of window on activation? " "This only has effect if any of the ``on_focus_lost_xxx`` options are " "``True``", ), ( "match", None, "Use a :class:`Match` to identify the spawned window and move it to the " "scratchpad, instead of relying on the window's PID. This works around " "some programs that may not be caught by the window's PID if it does " "not match the PID of the spawned process.", ), ) def __init__(self, name: str, cmd: str, **config: Any) -> None: """ Initialize :class:`DropDown` window wrapper. Define a command to spawn a process for the first time the class:`DropDown` is shown. Parameters ========== name: The name of the dropdown. cmd: Command to spawn a window to be captured by the dropdown. """ configurable.Configurable.__init__(self, **config) self.name = name self.command = cmd self.add_defaults(self.defaults) def info(self) -> dict[str, Any]: return dict( name=self.name, command=self.command, x=self.x, y=self.y, width=self.width, height=self.height, opacity=self.opacity, on_focus_lost_hide=self.on_focus_lost_hide, warp_pointer=self.warp_pointer, ) qtile-0.31.0/libqtile/scripts/0000775000175000017500000000000014762660347016132 5ustar epsilonepsilonqtile-0.31.0/libqtile/scripts/main.py0000664000175000017500000000514314762660347017433 0ustar epsilonepsilonimport argparse import logging import sys from pathlib import Path from libqtile.log_utils import get_default_log, init_log from libqtile.scripts import check, cmd_obj, migrate, run_cmd, shell, start, top, udev try: # Python>3.7 can get the version from importlib from importlib.metadata import PackageNotFoundError, distribution VERSION = distribution("qtile").version except PackageNotFoundError: VERSION = "dev" def check_folder(value): path = Path(value) if not path.parent.is_dir(): raise argparse.ArgumentTypeError("Log path destination folder does not exist.") # init_log expects a Path object so return `path` not `value` return path def main(): parent_parser = argparse.ArgumentParser(add_help=False) parent_parser.add_argument( "-l", "--log-level", default="WARNING", dest="log_level", type=str.upper, choices=("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"), help="Set qtile log level", ) parent_parser.add_argument( "-p", "--log-path", default=get_default_log(), dest="log_path", type=check_folder, help="Set alternative qtile log path", ) main_parser = argparse.ArgumentParser( prog="qtile", description="A full-featured tiling window manager for X11 and Wayland.", ) main_parser.add_argument( "-v", "--version", action="version", version=VERSION, ) subparsers = main_parser.add_subparsers() start.add_subcommand(subparsers, [parent_parser]) shell.add_subcommand(subparsers, [parent_parser]) top.add_subcommand(subparsers, [parent_parser]) run_cmd.add_subcommand(subparsers, [parent_parser]) cmd_obj.add_subcommand(subparsers, [parent_parser]) check.add_subcommand(subparsers, [parent_parser]) migrate.add_subcommand(subparsers, [parent_parser]) udev.add_subcommand(subparsers, [parent_parser]) # `qtile help` should print help def print_help(options): main_parser.print_help() help_ = subparsers.add_parser( "help", help="Print help message and exit.", parents=[parent_parser] ) help_.set_defaults(func=print_help) options = main_parser.parse_args() if func := getattr(options, "func", None): log_level = getattr(logging, options.log_level) init_log(log_level, log_path=options.log_path) func(options) else: main_parser.print_usage() print("") print("Did you mean:") print(" ".join(sys.argv + ["start"])) sys.exit(1) if __name__ == "__main__": main() qtile-0.31.0/libqtile/scripts/top.py0000664000175000017500000001566314762660347017321 0ustar epsilonepsilon# Copyright (c) 2015, Roger Duran # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """ Command-line top like for qtile """ import curses import linecache import os import sys import time from libqtile import ipc from libqtile.command import client, interface # These imports are here because they are not supported in pypy. # having them at the top of the file causes problems when running any # of the other scripts. try: import tracemalloc from tracemalloc import Snapshot ENABLED = True except ModuleNotFoundError: ENABLED = False class TraceNotStarted(Exception): pass class TraceCantStart(Exception): pass def get_trace(c, force_start): (started, path) = c.tracemalloc_dump() if force_start and not started: c.tracemalloc_toggle() (started, path) = c.tracemalloc_dump() if not started: raise TraceCantStart elif not started: raise TraceNotStarted return Snapshot.load(path) def filter_snapshot(snapshot): return snapshot.filter_traces( ( tracemalloc.Filter(False, ""), tracemalloc.Filter(False, ""), ) ) def get_stats(scr, c, group_by="lineno", limit=10, seconds=1.5, force_start=False): (max_y, max_x) = scr.getmaxyx() curses.init_pair(1, curses.COLOR_GREEN, curses.COLOR_BLACK) while True: scr.addstr(0, 0, f"Qtile - Top {limit} lines") scr.addstr( 1, 0, "{:<3s} {:<40s} {:<30s} {:<16s}".format("#", "Line", "Memory", " " * (max_x - 71)), curses.A_BOLD | curses.A_REVERSE, ) snapshot = get_trace(c, force_start) snapshot = filter_snapshot(snapshot) top_stats = snapshot.statistics(group_by) cnt = 1 for index, stat in enumerate(top_stats[:limit], 1): frame = stat.traceback[0] # replace "/path/to/module/file.py" with "module/file.py" filename = os.sep.join(frame.filename.split(os.sep)[-2:]) code = "" line = linecache.getline(frame.filename, frame.lineno).strip() if line: code = line mem = f"{stat.size / 1024.0:.1f} KiB" filename = f"{filename}:{frame.lineno}" scr.addstr(cnt + 1, 0, f"{index:<3} {filename:<40} {mem:<30}") scr.addstr(cnt + 2, 4, code, curses.color_pair(1)) cnt += 2 other = top_stats[limit:] cnt += 2 if other: size = sum(stat.size for stat in other) other_size = f"{len(other):d} other: {size / 1024.0:.1f} KiB" scr.addstr(cnt, 0, other_size, curses.A_BOLD) cnt += 1 total = sum(stat.size for stat in top_stats) total_size = f"Total allocated size: {total / 1024.0:.1f} KiB" scr.addstr(cnt, 0, total_size, curses.A_BOLD) scr.move(max_y - 2, max_y - 2) scr.refresh() time.sleep(seconds) scr.erase() def raw_stats(c, group_by="lineno", limit=10, force_start=False): snapshot = get_trace(c, force_start) snapshot = filter_snapshot(snapshot) top_stats = snapshot.statistics(group_by) print(f"Qtile - Top {limit} lines") for index, stat in enumerate(top_stats[:limit], 1): frame = stat.traceback[0] # replace "/path/to/module/file.py" with "module/file.py" filename = os.sep.join(frame.filename.split(os.sep)[-2:]) print(f"#{index}: {filename}:{frame.lineno}: {stat.size / 1024.0:.1f} KiB") line = linecache.getline(frame.filename, frame.lineno).strip() if line: print(f" {line}") other = top_stats[limit:] if other: size = sum(stat.size for stat in other) print(f"{len(other):d} other: {size / 1024.0:.1f} KiB") total = sum(stat.size for stat in top_stats) print(f"Total allocated size: {total / 1024.0:.1f} KiB") def top(opts): if not ENABLED: raise Exception("Could not import tracemalloc") lines = opts.lines seconds = opts.seconds force_start = opts.force_start if opts.socket is None: socket = ipc.find_sockfile() else: socket = opts.socket c = client.InteractiveCommandClient( interface.IPCCommandInterface( ipc.Client(socket), ), ) try: if not opts.raw: curses.wrapper(get_stats, c, limit=lines, seconds=seconds, force_start=force_start) else: raw_stats(c, limit=lines, force_start=force_start) except TraceNotStarted: print( "tracemalloc not started on qtile, start by setting " "PYTHONTRACEMALLOC=1 before starting qtile" ) print("or force start tracemalloc now, but you'll lose early traces") sys.exit(1) except TraceCantStart: print("Can't start tracemalloc on qtile, check the logs") except KeyboardInterrupt: sys.exit(1) except curses.error: print("Terminal too small for curses interface.") raw_stats(c, limit=lines, force_start=force_start) def add_subcommand(subparsers, parents): parser = subparsers.add_parser( "top", parents=parents, help="A top-like resource usage monitor." ) parser.add_argument( "-L", "--lines", type=int, dest="lines", default=10, help="Number of lines to show." ) parser.add_argument( "-r", "--raw", dest="raw", action="store_true", default=False, help="Output raw without curses.", ) parser.add_argument( "-t", "--time", type=float, dest="seconds", default=1.5, help="Refresh rate.", ) parser.add_argument( "--force-start", dest="force_start", action="store_true", default=False, help="Force start tracemalloc on Qtile.", ) parser.add_argument( "-s", "--socket", type=str, dest="socket", help="Use specified socket for IPC." ) parser.set_defaults(func=top) qtile-0.31.0/libqtile/scripts/udev.py0000664000175000017500000000223414762660347017450 0ustar epsilonepsilonimport glob import os import shutil import stat MODE = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IWGRP | stat.S_IROTH def set_file_perms(p, options): try: os.chmod(p, MODE) shutil.chown(p, user=None, group=options.group) except FileNotFoundError: pass def do_backlight_setup(options): set_file_perms(f"/sys/class/backlight/{options.device}/brightness", options) set_file_perms(f"/sys/class/leds/{options.device}/brightness", options) def do_battery_setup(options): files = glob.glob("/sys/class/power_supply/BAT*/charge_control_*_threshold") for file in files: set_file_perms(file, options) def udev(options): if options.kind == "backlight": do_backlight_setup(options) elif options.kind == "battery": do_battery_setup(options) else: raise f"Unknown udev option {options.kind}" def add_subcommand(subparsers, parents): parser = subparsers.add_parser("udev", parents=parents) parser.add_argument("kind", choices=["backlight", "battery"]) parser.add_argument("--device") parser.add_argument("--group", default="sudo") parser.set_defaults(func=udev) qtile-0.31.0/libqtile/scripts/migrations/0000775000175000017500000000000014762660347020306 5ustar epsilonepsilonqtile-0.31.0/libqtile/scripts/migrations/remove_cmd_prefix.py0000664000175000017500000001616414762660347024365 0ustar epsilonepsilon# Copyright (c) 2023, elParaguayo. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import libcst as cst import libcst.matchers as m from libcst import codemod from libcst.codemod.visitors import AddImportsVisitor from libqtile.scripts.migrations._base import ( Change, Check, MigrationTransformer, NoChange, _QtileMigrator, add_migration, ) # Three methods had their names changed beyond removing the prefix MIGRATION_MAP = { "cmd_hints": "get_hints", "cmd_groups": "get_groups", "cmd_screens": "get_screens", "cmd_opacity": "set_opacity", } def is_cmd(value): return value.startswith("cmd_") class CmdPrefixTransformer(MigrationTransformer): def __init__(self, *args, **kwargs): MigrationTransformer.__init__(self, *args, **kwargs) self.needs_import = False @m.call_if_inside(m.Call()) @m.leave(m.Name(m.MatchIfTrue(is_cmd))) def change_func_call_name(self, original_node, updated_node) -> cst.Name: """Removes cmd_prefix from a call where the command begins with the cmd_ prefix.""" name = original_node.value if name in MIGRATION_MAP: replacement = MIGRATION_MAP[name] else: replacement = name[4:] self.lint( original_node, f"Use of 'cmd_' prefix is deprecated. '{name}' should be replaced with '{replacement}'", ) return updated_node.with_changes(value=replacement) @m.call_if_inside(m.ClassDef()) @m.leave(m.FunctionDef(name=m.Name(m.MatchIfTrue(is_cmd)))) def change_func_def(self, original_node, updated_node) -> cst.FunctionDef: """ Renames method definitions using the cmd_ prefix and adds the @expose_command decorator. Also sets a flag to show that the script should add the relevant import if it's missing. """ decorator = cst.Decorator(cst.Name("expose_command")) name = original_node.name updated = name.with_changes(value=name.value[4:]) self.lint( original_node, "Use of 'cmd_' prefix is deprecated. Use '@expose_command' when defining methods.", ) self.needs_import = True return updated_node.with_changes(name=updated, decorators=[decorator]) class RemoveCmdPrefix(_QtileMigrator): ID = "RemoveCmdPrefix" SUMMARY = "Removes ``cmd_`` prefix from method calls and definitions." HELP = """ The ``cmd_`` prefix was used to identify methods that should be exposed to qtile's command API. This has been deprecated and so calls no longer require the prefix. For example: .. code:: python qtile.cmd_spawn("vlc") would be replaced with: .. code:: python qtile.spawn("vlc") Where users have created their own widgets with methods using this prefix, the syntax has also changed: For example: .. code:: python class MyWidget(libqtile.widget.base._Widget): def cmd_my_command(self): pass Should be updated as follows: .. code:: python from libqtile.command.base import expose_command class MyWidget(libqtile.widget.base._Widget): @expose_command def my_command(self): pass """ AFTER_VERSION = "0.22.1" TESTS = [ Change("""qtile.cmd_spawn("alacritty")""", """qtile.spawn("alacritty")"""), Change("""qtile.cmd_groups()""", """qtile.get_groups()"""), Change("""qtile.cmd_screens()""", """qtile.get_screens()"""), Change("""qtile.current_window.cmd_hints()""", """qtile.current_window.get_hints()"""), Change( """qtile.current_window.cmd_opacity(0.5)""", """qtile.current_window.set_opacity(0.5)""", ), Change( """ class MyWidget(widget.Clock): def cmd_my_command(self): pass """, """ from libqtile.command.base import expose_command class MyWidget(widget.Clock): @expose_command def my_command(self): pass """, ), NoChange( """ def cmd_some_other_func(): pass """ ), Check( """ from libqtile import qtile, widget class MyClock(widget.Clock): def cmd_my_exposed_command(self): pass def my_func(qtile): qtile.cmd_spawn("rickroll") hints = qtile.current_window.cmd_hints() groups = qtile.cmd_groups() screens = qtile.cmd_screens() qtile.current_window.cmd_opacity(0.5) def cmd_some_other_func(): pass """, """ from libqtile import qtile, widget from libqtile.command.base import expose_command class MyClock(widget.Clock): @expose_command def my_exposed_command(self): pass def my_func(qtile): qtile.spawn("rickroll") hints = qtile.current_window.get_hints() groups = qtile.get_groups() screens = qtile.get_screens() qtile.current_window.set_opacity(0.5) def cmd_some_other_func(): pass """, ), ] def run(self, original): # Run the base migrations transformer = CmdPrefixTransformer() updated = original.visit(transformer) self.update_lint(transformer) # Check if we need to add an import line if transformer.needs_import: # We use the built-in visitor to add the import context = codemod.CodemodContext() AddImportsVisitor.add_needed_import( context, "libqtile.command.base", "expose_command" ) visitor = AddImportsVisitor(context) # Run the visitor over the updated code updated = updated.visit(visitor) return original, updated add_migration(RemoveCmdPrefix) qtile-0.31.0/libqtile/scripts/migrations/module_renames.py0000664000175000017500000001411114762660347023655 0ustar epsilonepsilon# Copyright (c) 2023, elParaguayo. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import libcst as cst import libcst.matchers as m from libcst import codemod from libcst.codemod.visitors import AddImportsVisitor, RemoveImportsVisitor from libqtile.scripts.migrations._base import ( Change, MigrationTransformer, _QtileMigrator, add_migration, ) MODULE_MAP = { "window": cst.parse_expression("libqtile.backend.x11.window"), } IMPORT_MAP = { "window": ("libqtile.backend.x11", "window"), } class ModuleRenamesTransformer(MigrationTransformer): """ This transfore does a number of things: - Where possible, it replaces module names directly. It is able to do so where the module is shown in its dotted form e.g. libqtile.command_graph. - In addition, it identifies where modules are imported from libqtile and stores these values in a list where they can be accessed later. """ def __init__(self, *args, **kwargs): self.from_imports = [] MigrationTransformer.__init__(self, *args, **kwargs) def do_lint(self, original_node, module): if module == "window": self.lint( original_node, "The 'libqtile.window' has been moved to 'libqtile.backend.x11.window'.", ) @m.leave( m.ImportAlias( name=m.Attribute( value=m.Name("libqtile"), attr=m.Name(m.MatchIfTrue(lambda x: x in MODULE_MAP)) ) ) ) def update_import_module_names(self, original_node, updated_node) -> cst.ImportAlias: """Renames modules in 'import ...' statements.""" module = original_node.name.attr.value self.do_lint(original_node, module) new_module = MODULE_MAP[module] return updated_node.with_changes(name=new_module) @m.leave( m.ImportFrom( module=m.Attribute( value=m.Name("libqtile"), attr=m.Name(m.MatchIfTrue(lambda x: x in MODULE_MAP)) ) ) ) def update_import_from_module_names(self, original_node, updated_node) -> cst.ImportFrom: """Renames modules in 'from ... import ...' statements.""" module = original_node.module.attr.value self.do_lint(original_node, module) new_module = MODULE_MAP[module] return updated_node.with_changes(module=new_module) @m.leave( m.ImportFrom( module=m.Name("libqtile"), names=[ m.ZeroOrMore(), m.ImportAlias(name=m.Name(m.MatchIfTrue(lambda x: x in IMPORT_MAP))), m.ZeroOrMore(), ], ) ) def tag_from_imports(self, original_node, _) -> cst.ImportFrom: """Marks which modules are""" for name in original_node.names: if name.name.value in IMPORT_MAP: self.lint(original_node, f"From libqtile import {name.name.value} is deprecated.") self.from_imports.append(name.name.value) return original_node class ModuleRenames(_QtileMigrator): ID = "ModuleRenames" SUMMARY = "Updates certain deprecated ``libqtile.`` module names." HELP = """ ``libqtile.window`` module was moved to ``libqtile.backend.x11.window``. """ AFTER_VERSION = "0.18.1" TESTS = [] for mod, (new_mod, new_file) in IMPORT_MAP.items(): TESTS.append(Change(f"import libqtile.{mod}", f"import {new_mod}.{new_file}")) TESTS.append( Change(f"from libqtile.{mod} import foo", f"from {new_mod}.{new_file} import foo") ) TESTS.append(Change(f"from libqtile import {mod}", f"from {new_mod} import {new_file}")) def run(self, original_node): # Run the base transformer first... # This fixes simple replacements transformer = ModuleRenamesTransformer() updated = original_node.visit(transformer) # Update details of linting self.update_lint(transformer) # Check if we found any 'from libqtile import ...` lines which # need to be updated if transformer.from_imports: # Create a context to store details of changes context = codemod.CodemodContext() for name in transformer.from_imports: # For each import, get base name and module name from the map base, module = IMPORT_MAP[name] # Remove the old 'from libqtile import name' RemoveImportsVisitor.remove_unused_import(context, "libqtile", name) # Add correct import line AddImportsVisitor.add_needed_import(context, base, module) # Create the visitors. The advantage of using these is that they will # handle the cases where there are multiple imports in the same line. remove_visitor = RemoveImportsVisitor(context) add_visitor = AddImportsVisitor(context) # Rune the transformations updated = remove_visitor.transform_module(updated) updated = add_visitor.transform_module(updated) return original_node, updated add_migration(ModuleRenames) qtile-0.31.0/libqtile/scripts/migrations/rename_threaded_poll_text.py0000664000175000017500000000477714762660347026100 0ustar epsilonepsilon# Copyright (c) 2023, elParaguayo. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from libqtile.scripts.migrations._base import ( Check, RenamerTransformer, _QtileMigrator, add_migration, ) class RenameThreadedPollTextTransformer(RenamerTransformer): from_to = ("ThreadedPollText", "ThreadPoolText") class RenameThreadedPollText(_QtileMigrator): ID = "RenameThreadedPollText" SUMMARY = "Replaces ``ThreadedPollText`` with ``ThreadPoolText``." HELP = """ The ``ThreadedPollText`` class needs to replced with ``ThreadPoolText``. This is because the ``ThreadPoolText`` class can do everything that the ``ThreadedPollText`` does so the redundant code was removed. Example: .. code:: python from libqtile import widget class MyPollingWidget(widget.base.ThreadedPollText): ... Should be updated as follows: .. code:: python from libqtile import widget class MyPollingWidget(widget.base.ThreadPoolText): ... """ AFTER_VERSION = "0.16.1" TESTS = [ Check( """ from libqtile.widget.base import ThreadedPollText class MyWidget(ThreadedPollText): pass """, """ from libqtile.widget.base import ThreadPoolText class MyWidget(ThreadPoolText): pass """, ) ] visitor = RenameThreadedPollTextTransformer add_migration(RenameThreadedPollText) qtile-0.31.0/libqtile/scripts/migrations/_base.py0000664000175000017500000001733314762660347021740 0ustar epsilonepsilon# Copyright (c) 2023, elParaguayo. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations import difflib from textwrap import dedent from typing import TYPE_CHECKING import libcst import libcst.matchers as m from libqtile.scripts.migrations import MIGRATIONS if TYPE_CHECKING: from collections.abc import Collection from typing import ClassVar from libcst.metadata.base_provider import ProviderT # By default, libcst adds spaces around "=" so we should remove them EQUALS_NO_SPACE = libcst.AssignEqual( whitespace_before=libcst.SimpleWhitespace( value="", ), whitespace_after=libcst.SimpleWhitespace( value="", ), ) RED = "\033[38;2;255;0;0m" GREEN = "\033[38;2;0;255;0m" END = "\033[38;2;255;255;255m" def add_migration(migration): MIGRATIONS.append(migration) def add_colours(lines): coloured = [] for line in lines: if line.startswith("+"): coloured.append(f"{GREEN}{line}{END}") elif line.startswith("-"): coloured.append(f"{RED}{line}{END}") else: coloured.append(line) return coloured class LintLine: def __init__(self, pos, message: str = "", migrator_id: str = "") -> None: self.line = pos.line self.col = pos.column self.message = message self.id = migrator_id self._pos = (pos.line, pos.column) def set_id(self, migration_id: str = "") -> LintLine: self.id = migration_id return self def __lt__(self, other: LintLine) -> bool: if not isinstance(other, LintLine): raise ValueError return self._pos < other._pos def __str__(self) -> str: return "[Ln {line}, Col {col}]: {message} ({id})".format(**self.__dict__) class MigrationHelper: """ Migrations may need to access details about the position of the match (e.g. for linting) and the parent node. These are accessible via metadata providers so this class sets the relevant dependencies and provides some helper functions. """ METADATA_DEPENDENCIES: ClassVar[Collection[ProviderT]] = ( libcst.metadata.PositionProvider, libcst.metadata.ParentNodeProvider, ) def __init__(self) -> None: self._lint_lines: list[LintLine] = [] def lint(self, node, message) -> None: pos = self.get_metadata(libcst.metadata.PositionProvider, node).start # type: ignore[attr-defined] self._lint_lines.append(LintLine(pos, message)) def get_parent(self, node) -> libcst.CSTNode: return self.get_metadata(libcst.metadata.ParentNodeProvider, node) # type: ignore[attr-defined] class MigrationTransformer(MigrationHelper, m.MatcherDecoratableTransformer): def __init__(self, *args, **kwargs): m.MatcherDecoratableTransformer.__init__(self, *args, **kwargs) MigrationHelper.__init__(self) class MigrationVisitor(MigrationHelper, m.MatcherDecoratableVisitor): def __init__(self, *args, **kwargs): m.MatcherDecoratableVisitor.__init__(self, *args, **kwargs) MigrationHelper.__init__(self) class RenamerTransformer(MigrationTransformer): """ Very basic transformer. Will replace every instance of 'old_name' with 'new_name'. To use: subclass RenameTransformer and set the class attribute 'from_to' with ('old_name', 'new_name') """ from_to = ("old_name", "new_name") def leave_Name(self, original_node, updated_node): # noqa: N802 if original_node.value == self.from_to[0]: self.lint(original_node, "'{}' should be replaced with '{}'.".format(*self.from_to)) return updated_node.with_changes(value=self.from_to[1]) return original_node class Change: def __init__(self, input_code, output_code): self.input = input_code self.output = output_code self.check = False class NoChange(Change): def __init__(self, input_code): Change.__init__(self, input_code, input_code) class Check(Change): def __init__(self, input_code, output_code): Change.__init__(self, input_code, output_code) self.check = True class _QtileMigrator: ID: str = "UniqueMigrationName" SUMMARY: str = "Brief summary of migration" HELP: str = """ More detailed description of the migration including purpose and example changes. """ AFTER_VERSION: str = "" TESTS: list[Change] = [] # Set the name of the visitor/transformer here visitor: type[MigrationVisitor] | type[MigrationTransformer] | None = None def __init__(self) -> None: self._lint_lines: list[LintLine] = [] self.original: libcst.metadata.MetadataWrapper | None = None self.updated: libcst.Module | None = None def update_lint(self, visitor) -> None: self._lint_lines += [line.set_id(self.ID) for line in visitor._lint_lines] def show_lint(self) -> list[LintLine]: """ Return a list of LintLine objects storing details of deprecated code. """ return self._lint_lines @classmethod def show_help(cls) -> str: return dedent(cls.HELP) @classmethod def show_summary(cls) -> str: return cls.SUMMARY @classmethod def show_id(cls) -> str: return cls.ID @classmethod def get_version(cls) -> tuple[int, ...]: return tuple(int(x) for x in cls.AFTER_VERSION.split(".")) def show_diff(self, no_colour=False) -> str: """ Return a string showing the generated diff. """ if self.original is None or self.updated is None: return "" original = self.original.module updated = self.updated diff = difflib.unified_diff( original.code.splitlines(), updated.code.splitlines(), "original", "modified" ) if not no_colour: diff = add_colours(list(diff)) try: a, b, *lines = diff joined = "\n".join(lines) return f"{a}{b}{joined}" except ValueError: # No difference so we stop here. return "" def migrate(self, original) -> None: original, updated = self.run(original) self.original = original self.updated = updated def run( self, original ) -> tuple[libcst.Module | libcst.metadata.MetadataWrapper, libcst.Module]: """ Run the migration set in 'visitor'. Method can be overriden for more complex scenarios (e.g. multiple transformers). """ if self.visitor is None: raise AttributeError("Migration has not defined a 'visitor' attribute.") transformer = self.visitor() updated = original.visit(transformer) self.update_lint(transformer) return original, updated qtile-0.31.0/libqtile/scripts/migrations/update_togroup_args.py0000664000175000017500000000612714762660347024743 0ustar epsilonepsilon# Copyright (c) 2023, elParaguayo. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import libcst as cst import libcst.matchers as m from libqtile.scripts.migrations._base import ( Check, MigrationTransformer, _QtileMigrator, add_migration, ) class UpdateTogroupTransformer(MigrationTransformer): @m.call_if_inside( m.Call(func=m.Name(m.MatchIfTrue(lambda n: "togroup" in n))) | m.Call(func=m.Attribute(attr=m.Name(m.MatchIfTrue(lambda n: "togroup" in n)))) ) @m.leave(m.Arg(keyword=m.Name("groupName"))) def update_togroup_args(self, original_node, updated_node) -> cst.Arg: """Changes 'groupName' kwarg to 'group_name'.""" self.lint( original_node, "The 'groupName' keyword argument should be replaced with 'group_name." ) return updated_node.with_changes(keyword=cst.Name("group_name")) class UpdateTogroupArgs(_QtileMigrator): ID = "UpdateTogroupArgs" SUMMARY = "Updates ``groupName`` keyword argument to ``group_name``." HELP = """ To be consistent with codestyle, the ``groupName`` argument in the ``togroup`` command needs to be changed to ``group_name``. The following code: .. code:: python lazy.window.togroup(groupName="1") will result in a warning in your logfile: ``Window.togroup's groupName is deprecated; use group_name``. The code should be updated to: .. code:: python lazy.window.togroup(group_name="1") """ AFTER_VERSION = "0.18.1" TESTS = [ Check( """ from libqtile.config import Key from libqtile.lazy import lazy k = Key([], 's', lazy.window.togroup(groupName="g")) c = lambda win: win.togroup(groupName="g") """, """ from libqtile.config import Key from libqtile.lazy import lazy k = Key([], 's', lazy.window.togroup(group_name="g")) c = lambda win: win.togroup(group_name="g") """, ) ] visitor = UpdateTogroupTransformer add_migration(UpdateTogroupArgs) qtile-0.31.0/libqtile/scripts/migrations/change_stockticker_args.py0000664000175000017500000001015414762660347025527 0ustar epsilonepsilon# Copyright (c) 2023, elParaguayo. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import libcst as cst import libcst.matchers as m from libqtile.scripts.migrations._base import ( Change, Check, MigrationTransformer, NoChange, _QtileMigrator, add_migration, ) class StocktickerArgsTransformer(MigrationTransformer): @m.call_if_inside( m.Call(func=m.Name("StockTicker")) | m.Call(func=m.Attribute(attr=m.Name("StockTicker"))) ) @m.leave(m.Arg(keyword=m.Name("function"))) def update_stockticker_args(self, original_node, updated_node) -> cst.Arg: """Changes 'function' kwarg to 'mode' and 'func' kwargs.""" self.lint(original_node, "The 'function' keyword argument should be renamed 'func'.") return updated_node.with_changes(keyword=cst.Name("func")) class StocktickerArgs(_QtileMigrator): ID = "UpdateStocktickerArgs" SUMMARY = "Updates ``StockTicker`` argument signature." HELP = """ The ``StockTicker`` widget had a keyword argument called ``function``. This needs to be renamed to ``func`` to prevent clashes with the ``function()`` method of ``CommandObject``. For example: .. code:: python widget.StockTicker(function="TIME_SERIES_INTRADAY") should be changed to: .. code:: widget.StockTicker(func="TIME_SERIES_INTRADAY") """ AFTER_VERSION = "0.22.1" TESTS = [ Change( """StockTicker(function="TIME_SERIES_INTRADAY")""", """StockTicker(func="TIME_SERIES_INTRADAY")""", ), Change( """widget.StockTicker(function="TIME_SERIES_INTRADAY")""", """widget.StockTicker(func="TIME_SERIES_INTRADAY")""", ), Change( """libqtile.widget.StockTicker(function="TIME_SERIES_INTRADAY")""", """libqtile.widget.StockTicker(func="TIME_SERIES_INTRADAY")""", ), NoChange("""StockTicker(func="TIME_SERIES_INTRADAY")"""), NoChange("""widget.StockTicker(func="TIME_SERIES_INTRADAY")"""), NoChange("""libqtile.widget.StockTicker(func="TIME_SERIES_INTRADAY")"""), Check( """ import libqtile from libqtile import bar, widget from libqtile.widget import StockTicker bar.Bar( [ StockTicker(function="TIME_SERIES_INTRADAY"), widget.StockTicker(function="TIME_SERIES_INTRADAY"), libqtile.widget.StockTicker(function="TIME_SERIES_INTRADAY") ], 20 ) """, """ import libqtile from libqtile import bar, widget from libqtile.widget import StockTicker bar.Bar( [ StockTicker(func="TIME_SERIES_INTRADAY"), widget.StockTicker(func="TIME_SERIES_INTRADAY"), libqtile.widget.StockTicker(func="TIME_SERIES_INTRADAY") ], 20 ) """, ), ] visitor = StocktickerArgsTransformer add_migration(StocktickerArgs) qtile-0.31.0/libqtile/scripts/migrations/__init__.py0000664000175000017500000000320514762660347022417 0ustar epsilonepsilon# Copyright (c) 2023, elParaguayo. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations import importlib import pkgutil from pathlib import Path from typing import TYPE_CHECKING if TYPE_CHECKING: from libqtile.scripts.migrations._base import _QtileMigrator MIGRATIONS: list[type[_QtileMigrator]] = [] MIGRATION_FOLDER = Path(__file__).parent def load_migrations(): if MIGRATIONS: return for _, name, ispkg in pkgutil.iter_modules([MIGRATION_FOLDER.resolve().as_posix()]): if not name.startswith("_"): importlib.import_module(f"libqtile.scripts.migrations.{name}") qtile-0.31.0/libqtile/scripts/migrations/rename_check_updates_widget.py0000664000175000017500000000524214762660347026357 0ustar epsilonepsilon# Copyright (c) 2023, elParaguayo. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from libqtile.scripts.migrations._base import ( Check, RenamerTransformer, _QtileMigrator, add_migration, ) class RenameCheckUpdatesTransformer(RenamerTransformer): from_to = ("Pacman", "CheckUpdates") class RenamePacmanWidget(_QtileMigrator): ID = "RenamePacmanWidget" SUMMARY = "Changes deprecated ``Pacman`` widget name to ``CheckUpdates``." HELP = """ The ``Pacman`` widget has been renamed to ``CheckUpdates``. This is because the widget supports multiple package managers. Example: .. code:: python screens = [ Screen( top=Bar( [ ... widget.Pacman(), ... ] ) ) ] Should be updated as follows: .. code:: python screens = [ Screen( top=Bar( [ ... widget.CheckUpdates(), ... ] ) ) ] """ AFTER_VERSION = "0.16.1" TESTS = [ Check( """ from libqtile import bar, widget from libqtile.widget import Pacman bar.Bar([Pacman(), widget.Pacman()], 30) """, """ from libqtile import bar, widget from libqtile.widget import CheckUpdates bar.Bar([CheckUpdates(), widget.CheckUpdates()], 30) """, ) ] visitor = RenameCheckUpdatesTransformer add_migration(RenamePacmanWidget) qtile-0.31.0/libqtile/scripts/migrations/change_keychord_args.py0000664000175000017500000001323514762660347025015 0ustar epsilonepsilon# Copyright (c) 2023, elParaguayo. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import libcst as cst import libcst.matchers as m from libqtile.scripts.migrations._base import ( Check, MigrationTransformer, _QtileMigrator, add_migration, ) class KeychordTransformer(MigrationTransformer): @m.leave( m.Call( func=m.Name("KeyChord"), args=[m.ZeroOrMore(), m.Arg(keyword=m.Name("mode")), m.ZeroOrMore()], ) ) def update_keychord_args(self, original_node, updated_node) -> cst.Call: """Changes 'mode' kwarg to 'mode' and 'value' kwargs.""" args = original_node.args # (shouldn't be possible) if there are no args, stop here if not args: return original_node pos = 0 # Look for the "mode" kwarg and get its position and value for i, arg in enumerate(args): if kwarg := arg.keyword: if kwarg.value == "mode": # Ignore "mode" if it's already True or False if m.matches(arg.value, (m.Name("True") | m.Name("False"))): return original_node pos = i break # "mode" wasn't set so we can stop here else: return original_node self.lint( arg, # We can pass the argument here which ensures the position is reported correctly "The use of mode='mode name' for KeyChord is deprecated. Use mode=True and value='mode name'.", ) # Create two new kwargs # LibCST nodes are immutable so calling "with_changes" returns a new node name_arg = arg.with_changes(keyword=cst.Name("name")) mode_arg = arg.with_changes(value=cst.Name("True")) # Get the existing args and remove "mode" new_args = [a for i, a in enumerate(args) if i != pos] # Add "mode" and "value" kwargs new_args += [name_arg, mode_arg] # Return the updated node return updated_node.with_changes(args=new_args) class KeychordArgs(_QtileMigrator): ID = "UpdateKeychordArgs" SUMMARY = "Updates ``KeyChord`` argument signature." HELP = """ Previously, users could make a key chord persist by setting the `mode` to a string representing the name of the mode. For example: .. code:: python keys = [ KeyChord( [mod], "x", [ Key([], "Up", lazy.layout.grow()), Key([], "Down", lazy.layout.shrink()) ], mode="Resize layout", ) ] This will now result in the following warning message in the log file: .. code:: The use of `mode` to set the KeyChord name is deprecated. Please use `name='Resize Layout'` instead. 'mode' should be a boolean value to set whether the chord is persistent (True) or not." To remove the error, the config should be amended as follows: .. code:: python keys = [ KeyChord( [mod], "x", [ Key([], "Up", lazy.layout.grow()), Key([], "Down", lazy.layout.shrink()) ], name="Resize layout", mode=True, ) ] .. note:: The formatting of the inserted argument may not correctly match your own formatting. You may this to run a tool like ``black`` after applying this migration to tidy up your code. """ AFTER_VERSION = "0.21.0" TESTS = [ Check( """ from libqtile.config import Key, KeyChord from libqtile.lazy import lazy mod = "mod4" keys = [ KeyChord( [mod], "x", [ Key([], "Up", lazy.layout.grow()), Key([], "Down", lazy.layout.shrink()) ], mode="Resize layout", ) ] """, """ from libqtile.config import Key, KeyChord from libqtile.lazy import lazy mod = "mod4" keys = [ KeyChord( [mod], "x", [ Key([], "Up", lazy.layout.grow()), Key([], "Down", lazy.layout.shrink()) ], name="Resize layout", mode=True, ) ] """, ) ] visitor = KeychordTransformer add_migration(KeychordArgs) qtile-0.31.0/libqtile/scripts/migrations/update_monad_args.py0000664000175000017500000000652514762660347024344 0ustar epsilonepsilon# Copyright (c) 2023, elParaguayo. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import libcst as cst import libcst.matchers as m from libqtile.scripts.migrations._base import ( Check, MigrationTransformer, _QtileMigrator, add_migration, ) class UpdateMonadLayoutTransformer(MigrationTransformer): @m.call_if_inside( m.Call(func=m.Name(m.MatchIfTrue(lambda n: n.startswith("Monad")))) | m.Call(func=m.Attribute(attr=m.Name(m.MatchIfTrue(lambda n: n.startswith("Monad"))))) ) @m.leave(m.Arg(keyword=m.Name("new_at_current"))) def update_monad_args(self, original_node, updated_node) -> cst.Arg: """ Changes 'new_at_current' kwarg to 'new_client_position' and sets correct value ('before|after_current'). """ self.lint( original_node, "The 'new_at_current' keyword argument in 'Monad' layouts is invalid." ) new_value = cst.SimpleString( '"before_current"' if original_node.value.value == "True" else '"after_current"' ) return updated_node.with_changes(keyword=cst.Name("new_client_position"), value=new_value) class UpdateMonadArgs(_QtileMigrator): ID = "UpdateMonadArgs" SUMMARY = "Updates ``new_at_current`` keyword argument in Monad layouts." HELP = """ Replaces the ``new_at_current=True|False`` argument in ``Monad*`` layouts with ``new_client_position`` to be consistent with other layouts. ``new_at_current=True`` is replaced with ``new_client_position="before_current`` and ``new_at_current=False`` is replaced with ``new_client_position="after_current"``. """ AFTER_VERSION = "0.17.0" TESTS = [ Check( """ from libqtile import layout layouts = [ layout.MonadTall(border_focus="#ff0000", new_at_current=False), layout.MonadWide(new_at_current=True, border_focus="#ff0000"), ] """, """ from libqtile import layout layouts = [ layout.MonadTall(border_focus="#ff0000", new_client_position="after_current"), layout.MonadWide(new_client_position="before_current", border_focus="#ff0000"), ] """, ) ] visitor = UpdateMonadLayoutTransformer add_migration(UpdateMonadArgs) qtile-0.31.0/libqtile/scripts/migrations/change_bitcoin.py0000664000175000017500000000741514762660347023623 0ustar epsilonepsilon# Copyright (c) 2023, elParaguayo. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import libcst as cst import libcst.matchers as m from libqtile.scripts.migrations._base import ( Check, RenamerTransformer, _QtileMigrator, add_migration, ) def is_bitcoin(func): name = func.value if hasattr(func, "attr"): attr = func.attr.value else: attr = None return name == "BitcoinTicker" or attr == "BitcoinTicker" class BitcoinTransformer(RenamerTransformer): from_to = ("BitcoinTicker", "CryptoTicker") @m.leave( m.Call( func=m.MatchIfTrue(is_bitcoin), args=[m.ZeroOrMore(), m.Arg(keyword=m.Name("format")), m.ZeroOrMore()], ) ) def remove_format_kwarg(self, original_node, updated_node) -> cst.Call: """Removes the 'format' keyword argument from 'BitcoinTracker'.""" new_args = [a for a in original_node.args if a.keyword.value != "format"] new_args[-1] = new_args[-1].with_changes(comma=cst.MaybeSentinel.DEFAULT) return updated_node.with_changes(args=new_args) class BitcoinToCrypto(_QtileMigrator): ID = "UpdateBitcoin" SUMMARY = "Updates ``BitcoinTicker`` to ``CryptoTicker``." HELP = """ The ``BitcoinTicker`` widget has been renamed ``CryptoTicker``. In addition, the ``format`` keyword argument is removed during this migration as the available fields for the format have changed. The removal only happens on instances of ``BitcoinTracker``. i.e. running ``qtile migrate`` on the following code: .. code:: python BitcoinTicker(format="...") CryptoTicker(format="...") will return: .. code:: python CryptoTicker() CryptoTicker(format="...") """ AFTER_VERSION = "0.18.0" TESTS = [ Check( """ from libqtile import bar from libqtile.widget import BitcoinTicker bar.Bar( [ BitcoinTicker(crypto='BTC', format='BTC: {avg}'), BitcoinTicker(format='{crypto}: {avg}', font='sans'), BitcoinTicker(), BitcoinTicker(currency='EUR', format='{avg}', foreground='ffffff'), ], 30 ) """, """ from libqtile import bar from libqtile.widget import CryptoTicker bar.Bar( [ CryptoTicker(crypto='BTC'), CryptoTicker(font='sans'), CryptoTicker(), CryptoTicker(currency='EUR', foreground='ffffff'), ], 30 ) """, ) ] visitor = BitcoinTransformer add_migration(BitcoinToCrypto) qtile-0.31.0/libqtile/scripts/migrations/change_bluetooth_args.py0000664000175000017500000000632314762660347025212 0ustar epsilonepsilon# Copyright (c) 2023, elParaguayo. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import libcst as cst import libcst.matchers as m from libqtile.scripts.migrations._base import ( Check, MigrationTransformer, _QtileMigrator, add_migration, ) class BluetoothArgsTransformer(MigrationTransformer): @m.call_if_inside( m.Call(func=m.Name("Bluetooth")) | m.Call(func=m.Attribute(attr=m.Name("Bluetooth"))) ) @m.leave(m.Arg(keyword=m.Name("hci"))) def update_bluetooth_args(self, original_node, updated_node) -> cst.Arg: """Changes positional argumentto 'widgets' kwargs.""" self.lint( original_node, "The 'hci' argument is deprecated and should be replaced with 'device'.", ) return updated_node.with_changes(keyword=cst.Name("device")) class BluetoothArgs(_QtileMigrator): ID = "UpdateBluetoothArgs" SUMMARY = "Updates ``Bluetooth`` argument signature." HELP = """ The ``Bluetooth`` widget previously accepted a ``hci`` keyword argument. This has been deprecated following a major overhaul of the widget and should be replaced with a keyword argument named ``device``. For example: .. code:: python widget.Bluetooth(hci="/dev_XX_XX_XX_XX_XX_XX") should be changed to: .. code:: widget.Bluetooth(device="/dev_XX_XX_XX_XX_XX_XX") """ AFTER_VERSION = "0.23.0" TESTS = [ Check( """ from libqtile import bar, widget from libqtile.widget import Bluetooth bar.Bar( [ Bluetooth(hci="/dev_XX_XX_XX_XX_XX_XX"), widget.Bluetooth(hci="/dev_XX_XX_XX_XX_XX_XX"), ], 20, ) """, """ from libqtile import bar, widget from libqtile.widget import Bluetooth bar.Bar( [ Bluetooth(device="/dev_XX_XX_XX_XX_XX_XX"), widget.Bluetooth(device="/dev_XX_XX_XX_XX_XX_XX"), ], 20, ) """, ), ] visitor = BluetoothArgsTransformer add_migration(BluetoothArgs) qtile-0.31.0/libqtile/scripts/migrations/rename_unspecified.py0000664000175000017500000001012314762660347024502 0ustar epsilonepsilon# Copyright (c) 2024, Tycho Andersen. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from itertools import filterfalse import libcst as cst import libcst.matchers as m from libqtile.scripts.migrations._base import ( Check, MigrationTransformer, _QtileMigrator, add_migration, ) class RenameUnspecifiedTransformer(MigrationTransformer): @m.leave(m.Call(func=m.Attribute(attr=m.Name("set_font")))) def update_unspecified_arg(self, original_node, updated_node) -> cst.Call: args = original_node.args def is_interesting_arg(arg): return m.matches(arg.value, m.Name("UNSPECIFIED")) args = list(filterfalse(is_interesting_arg, original_node.args)) if len(args) == len(original_node.args): return original_node return updated_node.with_changes(args=args) def strip_unspecified_imports(self, original_node, updated_node) -> cst.Import: new_names = list(filter(lambda n: n.name.value != "UNSPECIFIED", original_node.names)) if len(new_names) == len(original_node.names): return original_node self.lint(original_node, "'UNSPECIFIED' can be dropped") if len(new_names) == 0: return cst.RemoveFromParent() return updated_node.with_changes(names=new_names) def leave_Import(self, original_node: cst.Import, updated_node: cst.Import): # noqa: N802 return self.strip_unspecified_imports(original_node, updated_node) def leave_ImportFrom( # noqa: N802 self, original_node: cst.ImportFrom, updated_node: cst.ImportFrom ): return self.strip_unspecified_imports(original_node, updated_node) class RenameUnspecified(_QtileMigrator): ID = "RenameUnspecified" SUMMARY = "Drops `UNSPECIFIED` argument" HELP = """ The UNSPECIFIED object was removed in favor of using the zero values (or None) to leave behavior unspecified. That is: font=UNSPECIFIED -> font=None fontsize=UNSPECIFIED -> fontsize=0 fontshadow=UNSPECIFIED -> fontshadow="" """ AFTER_VERSION = "0.25.0" TESTS = [ Check( """ from libqtile.widget.base import UNSPECIFIED, ORIENTATION_BOTH from libqtile.widget import TextBox from libqtile.layout import Tile tb = TextBox(text="hello") # just to use ORIENTATION_BOTH and force us to delete only the # right thing if False: tb.orientations = ORIENTATION_BOTH tb.set_font(font=UNSPECIFIED, fontsize=12) """, """ from libqtile.widget.base import ORIENTATION_BOTH from libqtile.widget import TextBox from libqtile.layout import Tile tb = TextBox(text="hello") # just to use ORIENTATION_BOTH and force us to delete only the # right thing if False: tb.orientations = ORIENTATION_BOTH tb.set_font(fontsize=12) """, ) ] visitor = RenameUnspecifiedTransformer add_migration(RenameUnspecified) qtile-0.31.0/libqtile/scripts/migrations/rename_tile_master.py0000664000175000017500000000451114762660347024520 0ustar epsilonepsilon# Copyright (c) 2023, elParaguayo. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from libqtile.scripts.migrations._base import ( Check, RenamerTransformer, _QtileMigrator, add_migration, ) class RenameTileMasterTransformer(RenamerTransformer): from_to = ("masterWindows", "master_length") class RenameTileMaster(_QtileMigrator): ID = "RenameTileMaster" SUMMARY = "Changes ``masterWindows`` argument to ``master_length``." HELP = """ To be consistent with other layouts, the ``masterWindows`` property of the ``Tile`` layout was renamed to ``master_length``. Configs using the ``masterWindows`` argument when configuring the layout should replace this. """ AFTER_VERSION = "0.16.1" TESTS = [ Check( """ from libqtile import layout from libqtile.layout import Tile layouts = [ Tile(masterWindows=2), layout.Tile(masterWindows=3) ] """, """ from libqtile import layout from libqtile.layout import Tile layouts = [ Tile(master_length=2), layout.Tile(master_length=3) ] """, ) ] visitor = RenameTileMasterTransformer add_migration(RenameTileMaster) qtile-0.31.0/libqtile/scripts/migrations/match_list_regex.py0000664000175000017500000001066514762660347024211 0ustar epsilonepsilon# Copyright (c) 2023, elParaguayo. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import libcst as cst import libcst.matchers as m from libcst import codemod from libcst.codemod.visitors import AddImportsVisitor from libqtile.scripts.migrations._base import ( Check, MigrationTransformer, _QtileMigrator, add_migration, ) try: import isort can_sort = True except ImportError: can_sort = False class MatchRegexTransformer(MigrationTransformer): def __init__(self, *args, **kwargs): self.needs_import = False MigrationTransformer.__init__(self, *args, **kwargs) @m.call_if_inside( m.Call(func=m.Name("Match")) | m.Call(func=m.Attribute(attr=m.Name("Match"))) ) @m.leave(m.Arg(value=m.List(), keyword=m.Name())) def update_match_args(self, original_node, updated_node) -> cst.Arg: """Changes positional argumentto 'widgets' kwargs.""" name = original_node.keyword.value if not (all(isinstance(e.value, cst.SimpleString) for e in original_node.value.elements)): self.lint( original_node, f"The {name} keyword uses a list but the migrate script can not fix this as " f"not all elements are strings.", ) return updated_node joined_text = "|".join(e.value.value.strip("'\"") for e in original_node.value.elements) regex = cst.parse_expression(f"""re.compile(r"^({joined_text})$")""") self.lint( original_node, f"The use of a list for the {name} argument is deprecated. " f"Please replace with 're.compile(r\"^({joined_text})$\")'.", ) self.needs_import = True return updated_node.with_changes(value=regex) class MatchListRegex(_QtileMigrator): ID = "MatchListRegex" SUMMARY = "Updates Match objects using lists" HELP = """ The use of lists in ``Match`` objects is deprecated and should be replaced with a regex. For example: .. code:: python Match(wm_class=["one", "two"]) should be changed to: .. code:: Match(wm_class=re.compile(r"^(one|two)$")) """ AFTER_VERSION = "0.23.0" TESTS = [ Check( """ from libqtile.config import Match Match(wm_class=["one", "two"]) """, """ import re from libqtile.config import Match Match(wm_class=re.compile(r"^(one|two)$")) """, ), ] def run(self, original_node): # Run the base transformer first... # This fixes simple replacements transformer = MatchRegexTransformer() updated = original_node.visit(transformer) # Update details of linting self.update_lint(transformer) # Check if we replaced any lists and now need to add re import if transformer.needs_import: # Create a context to store details of changes context = codemod.CodemodContext() AddImportsVisitor.add_needed_import(context, "re") add_visitor = AddImportsVisitor(context) # Run the transformations updated = add_visitor.transform_module(updated) if can_sort: updated = cst.parse_module(isort.code(updated.code)) else: print("Unable to sort import lines.") return original_node, updated add_migration(MatchListRegex) qtile-0.31.0/libqtile/scripts/migrations/change_widgetbox_args.py0000664000175000017500000001175714762660347025210 0ustar epsilonepsilon# Copyright (c) 2023, elParaguayo. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import libcst as cst import libcst.matchers as m from libqtile.scripts.migrations._base import ( EQUALS_NO_SPACE, Change, Check, MigrationTransformer, NoChange, _QtileMigrator, add_migration, ) class WidgetboxArgsTransformer(MigrationTransformer): @m.call_if_inside( m.Call(func=m.Name("WidgetBox")) | m.Call(func=m.Attribute(attr=m.Name("WidgetBox"))) ) @m.leave(m.Arg(keyword=None)) def update_widgetbox_args(self, original_node, updated_node) -> cst.Arg: """Changes positional argumentto 'widgets' kwargs.""" self.lint( original_node, "The positional argument should be replaced with a keyword argument named 'widgets'.", ) return updated_node.with_changes(keyword=cst.Name("widgets"), equal=EQUALS_NO_SPACE) class WidgetboxArgs(_QtileMigrator): ID = "UpdateWidgetboxArgs" SUMMARY = "Updates ``WidgetBox`` argument signature." HELP = """ The ``WidgetBox`` widget allowed a position argument to set the contents of the widget. This behaviour is deprecated and, instead, the contents should be specified with a keyword argument called ``widgets``. For example: .. code:: python widget.WidgetBox( [ widget.Systray(), widget.Volume(), ] ) should be changed to: .. code:: widget.WidgetBox( widgets=[ widget.Systray(), widget.Volume(), ] ) """ AFTER_VERSION = "0.20.0" TESTS = [ Change( """ widget.WidgetBox( [ widget.Systray(), widget.Volume(), ] ) """, """ widget.WidgetBox( widgets=[ widget.Systray(), widget.Volume(), ] ) """, ), Change( """ WidgetBox( [ widget.Systray(), widget.Volume(), ] ) """, """ WidgetBox( widgets=[ widget.Systray(), widget.Volume(), ] ) """, ), NoChange( """ widget.WidgetBox( widgets=[ widget.Systray(), widget.Volume(), ] ) """ ), Check( """ from libqtile import bar, widget from libqtile.widget import WidgetBox bar.Bar( [ WidgetBox( [ widget.Systray(), widget.Volume(), ] ), widget.WidgetBox( [ widget.Systray(), widget.Volume(), ] ) ], 20, ) """, """ from libqtile import bar, widget from libqtile.widget import WidgetBox bar.Bar( [ WidgetBox( widgets=[ widget.Systray(), widget.Volume(), ] ), widget.WidgetBox( widgets=[ widget.Systray(), widget.Volume(), ] ) ], 20, ) """, ), ] visitor = WidgetboxArgsTransformer add_migration(WidgetboxArgs) qtile-0.31.0/libqtile/scripts/migrations/rename_hook.py0000664000175000017500000000443714762660347023157 0ustar epsilonepsilon# Copyright (c) 2023, elParaguayo. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from libqtile.scripts.migrations._base import ( Check, RenamerTransformer, _QtileMigrator, add_migration, ) class RenameHookTransformer(RenamerTransformer): from_to = ("window_name_change", "client_name_updated") class RenameWindowNameHook(_QtileMigrator): ID = "RenameWindowNameHook" SUMMARY = "Changes ``window_name_changed`` hook name." HELP = """ The ``window_name_changed`` hook has been replaced with ``client_name_updated``. Example: .. code:: python @hook.subscribe.window_name_changed def my_func(window): ... Should be updated as follows: .. code:: python @hook.subscribe.client_name_updated def my_func(window): ... """ AFTER_VERSION = "0.16.1" TESTS = [ Check( """ from libqtile import hook @hook.subscribe.window_name_change def f(): pass """, """ from libqtile import hook @hook.subscribe.client_name_updated def f(): pass """, ) ] visitor = RenameHookTransformer add_migration(RenameWindowNameHook) qtile-0.31.0/libqtile/scripts/start.py0000664000175000017500000001140414762660347017641 0ustar epsilonepsilon# Copyright (c) 2008, Aldo Cortesi. All rights reserved. # Copyright (c) 2011, Florian Mounier # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # Set the locale before any widgets or anything are imported, so any widget # whose defaults depend on a reasonable locale sees something reasonable. from __future__ import annotations import locale from os import makedirs, path from sys import exit from typing import TYPE_CHECKING import libqtile.backend from libqtile import confreader, qtile from libqtile.log_utils import logger from libqtile.utils import get_config_file if TYPE_CHECKING: from libqtile.core.manager import Qtile def rename_process(): """ Try to rename the qtile process if py-setproctitle is installed: http://code.google.com/p/py-setproctitle/ Will fail silently if it's not installed. Setting the title lets you do stuff like "killall qtile". """ try: import setproctitle setproctitle.setproctitle("qtile") except ImportError: pass def make_qtile(options) -> Qtile | None: qtile.core.name = options.backend if missing_deps := libqtile.backend.has_deps(options.backend): print(f"Backend '{options.backend}' missing required Python dependencies:") for dep in missing_deps: print("\t", dep) return None kore = libqtile.backend.get_core(options.backend) if not path.isfile(options.configfile): try: makedirs(path.dirname(options.configfile), exist_ok=True) from shutil import copyfile default_config_path = path.join( path.dirname(__file__), "..", "resources", "default_config.py" ) copyfile(default_config_path, options.configfile) logger.info("Copied default_config.py to %s", options.configfile) except Exception: logger.exception("Failed to copy default_config.py to %s:", options.configfile) config = confreader.Config(options.configfile) # XXX: the import is here because we need to call init_log # before start importing stuff from libqtile.core.manager import Qtile return Qtile( kore, config, no_spawn=options.no_spawn, state=options.state, socket_path=options.socket, ) def start(options): try: locale.setlocale(locale.LC_ALL, "") except locale.Error: pass rename_process() q = make_qtile(options) if q is None: logger.warning("Backend is missing required Python dependencies. Exiting.") exit(1) try: q.loop() except Exception: logger.exception("Qtile crashed") exit(1) logger.info("Exiting...") def add_subcommand(subparsers, parents): parser = subparsers.add_parser("start", parents=parents, help="Start a Qtile session.") parser.add_argument( "-c", "--config", action="store", default=get_config_file(), dest="configfile", help="Use the specified configuration file.", ) parser.add_argument( "-s", "--socket", action="store", default=None, dest="socket", help="Use specified socket for IPC.", ) parser.add_argument( "-n", "--no-spawn", action="store_true", default=False, dest="no_spawn", help="Avoid spawning apps. (Used when restarting Qtile)", ) parser.add_argument( "--with-state", default=None, dest="state", help="Pickled QtileState object. (Used when restarting Qtile).", ) parser.add_argument( "-b", "--backend", default="x11", dest="backend", choices=libqtile.backend.CORES.keys(), help="Use specified backend.", ) parser.set_defaults(func=start) qtile-0.31.0/libqtile/scripts/cmd_obj.py0000664000175000017500000001665714762660347020120 0ustar epsilonepsilon#!/usr/bin/env python3 # # Copyright (c) 2017, Piotr Przymus # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """ Command-line tool to expose qtile.command functionality to shell. This can be used standalone or in other shell scripts. """ from __future__ import annotations import argparse import itertools import pprint import sys import textwrap from libqtile.command.base import CommandError, CommandException, SelectError from libqtile.command.client import CommandClient from libqtile.command.graph import CommandGraphRoot from libqtile.command.interface import IPCCommandInterface from libqtile.ipc import Client, find_sockfile def get_formated_info(obj: CommandClient, cmd: str, args=True, short=True) -> str: """Get documentation for command/function and format it. Returns: * args=True, short=True - '*' if arguments are present and a summary line. * args=True, short=False - (function args) and a summary line. * args=False - a summary line. If 'doc' function is not present in object or there is no doc string for given cmd it returns empty string. The arguments are extracted from doc[0] line, the summary is constructed from doc[1] line. """ doc = obj.call("doc", cmd).splitlines() tdoc = doc[0] doc_args = tdoc[tdoc.find("(") : tdoc.find(")") + 1].strip() short_description = doc[1] if len(doc) > 1 else "" if not args: doc_args = "" elif short: doc_args = " " if doc_args == "()" else "*" return (doc_args + " " + short_description).rstrip() def print_commands(prefix: str, obj: CommandClient) -> None: """Print available commands for given object.""" prefix += " -f " cmds = obj.call("commands") output = [] for cmd in cmds: doc_args = get_formated_info(obj, cmd) pcmd = prefix + cmd output.append([pcmd, doc_args]) max_cmd = max(len(pcmd) for pcmd, _ in output) # Print formatted output formatting = "{:<%d}\t{}" % (max_cmd + 1) for line in output: print(formatting.format(line[0], line[1])) def get_object(client: CommandClient, argv: list[str]) -> CommandClient: """ Constructs a path to object and returns given object (if it exists). """ if argv[0] in ("cmd", "root"): argv = argv[1:] # flag noting if we have consumed arg1 as the selector, eg screen[0] parsed_next = False for arg0, arg1 in itertools.zip_longest(argv, argv[1:]): # previous argument was an item, skip here if parsed_next: parsed_next = False continue # check if it is an item try: client = client.navigate(arg0, arg1) parsed_next = True continue except SelectError: pass # check if it is an attr try: client = client.navigate(arg0, None) continue except SelectError: pass print("Specified object does not exist: " + " ".join(argv)) sys.exit(1) return client def run_function(client: CommandClient, funcname: str, args: list[str]) -> str: "Run command with specified args on given object." try: ret = client.call(funcname, *args, lifted=True) except SelectError: print("error: Sorry no function ", funcname) sys.exit(1) except CommandError as e: print(f"error: Command '{funcname}' returned error: {str(e)}") sys.exit(1) except CommandException as e: print(f"error: Sorry cannot run function '{funcname}' with arguments {args}: {str(e)}") sys.exit(1) return ret def print_base_objects() -> None: """Prints access objects of Client, use cmd for commands.""" root = CommandGraphRoot() actions = ["-o cmd"] + [f"-o {key}" for key in root.children] print("Specify an object on which to execute command") print("\n".join(actions)) def cmd_obj(args) -> None: "Runs tool according to specified arguments." if args.obj_spec: sock_file = args.socket or find_sockfile() ipc_client = Client(sock_file) cmd_object = IPCCommandInterface(ipc_client) cmd_client = CommandClient(cmd_object) obj = get_object(cmd_client, args.obj_spec) if args.function == "help": try: print_commands("-o " + " ".join(args.obj_spec), obj) except CommandError: if len(args.obj_spec) == 1: print( f"{args.obj_spec} object needs a specified identifier e.g. '-o bar top'." ) sys.exit(1) else: raise elif args.info: print(args.function + get_formated_info(obj, args.function, args=True, short=False)) else: ret = run_function(obj, args.function, args.args) if ret is not None: pprint.pprint(ret) else: print_base_objects() sys.exit(1) def add_subcommand(subparsers, parents): epilog = textwrap.dedent( """\ Examples: qtile cmd-obj qtile cmd-obj -o root # same as above qtile cmd-obj -o root -f prev_layout -a 3 # prev_layout on group 3 qtile cmd-obj -o group 3 -f focus_back qtile cmd-obj -o root -f restart # restart qtile The graph traversal recurses: qtile cmd-obj -o screen 0 bar bottom screen group window -f info """ ) description = "Access the command interface from a shell." parser = subparsers.add_parser( "cmd-obj", help=description, parents=parents, epilog=epilog, formatter_class=argparse.RawDescriptionHelpFormatter, ) parser.add_argument( "--object", "-o", dest="obj_spec", nargs="+", default=["root"], help="Specify path to object (space separated). " "If no --function flag display available commands. " "The root node is selected by default or you can pass `root` explicitly.", ) parser.add_argument("--function", "-f", default="help", help="Select function to execute.") parser.add_argument( "--args", "-a", nargs="+", default=[], help="Set arguments supplied to function." ) parser.add_argument( "--info", "-i", action="store_true", help="With both --object and --function args prints documentation for function.", ) parser.add_argument("--socket", "-s", help="Use specified socket for IPC.") parser.set_defaults(func=cmd_obj) qtile-0.31.0/libqtile/scripts/__init__.py0000664000175000017500000000000014762660347020231 0ustar epsilonepsilonqtile-0.31.0/libqtile/scripts/check.py0000664000175000017500000001307214762660347017564 0ustar epsilonepsilon# Copyright (c) 2020, Tycho Andersen. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import shutil import subprocess import sys import tempfile import traceback from os import environ, path from libqtile import confreader from libqtile.utils import get_config_file class CheckError(Exception): pass def type_check_config_vars(tempdir, config_name): # write a .pyi file to tempdir: f = open(path.join(tempdir, config_name + ".pyi"), "w") f.write(confreader.config_pyi_header) for name, type_ in confreader.Config.__annotations__.items(): f.write(name) f.write(": ") f.write(type_) f.write("\n") f.close() # need to tell python to look in pwd for modules newenv = environ.copy() newenv["PYTHONPATH"] = newenv.get("PYTHONPATH", "") + ":" p = subprocess.Popen( ["stubtest", "--concise", config_name], stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=tempdir, text=True, env=newenv, ) stdout, stderr = p.communicate() missing_vars = [] for line in (stdout + stderr).split("\n"): # filter out stuff that users didn't specify; they'll be imported from # the default config if "is not present at runtime" in line: missing_vars.append(line.split()[0]) # write missing vars to a tempfile whitelist = open(path.join(tempdir, "stubtest_whitelist"), "w") for var in missing_vars: whitelist.write(var) whitelist.write("\n") whitelist.close() p = subprocess.Popen( [ "stubtest", # ignore variables that the user creates in their config that # aren't in our default config list "--ignore-missing-stub", # use our whitelist to ignore stuff users didn't specify "--whitelist", whitelist.name, config_name, ], cwd=tempdir, text=True, env=newenv, ) p.wait() if p.returncode != 0: raise CheckError() def type_check_config_args(config_file): try: subprocess.check_call(["mypy", config_file]) print("Config file type checking succeeded!") except subprocess.CalledProcessError as e: print(f"Config file type checking failed: {e}") raise CheckError() def check_deps() -> None: ok = True for dep in ["mypy", "stubtest"]: if shutil.which(dep) is None: print(f"{dep} was not found in PATH. Please install it, add to PATH and try again.") ok = False if not ok: raise CheckError() def check_config(args): print(f"Checking Qtile config at: {args.configfile}") print("Checking if config is valid python...") try: # can we load the config? config = confreader.Config(args.configfile) config.load() config.validate() except Exception: print(traceback.format_exc()) sys.exit("Errors found in config. Exiting check.") try: check_deps() except CheckError: print("Missing dependencies. Skipping type checking.") else: # need to do all the checking in a tempdir because we need to write stuff # for stubtest print("Type checking config file...") valid = True with tempfile.TemporaryDirectory() as tempdir: shutil.copytree(path.dirname(args.configfile), tempdir, dirs_exist_ok=True) tmp_path = path.join(tempdir, path.basename(args.configfile)) # are the top level config variables the right type? module_name = path.splitext(path.basename(args.configfile))[0] try: type_check_config_vars(tempdir, module_name) except CheckError: valid = False # are arguments passed to qtile APIs correct? try: type_check_config_args(tmp_path) except CheckError: valid = False if valid: print("Your config can be loaded by Qtile.") else: print( "Your config is valid python but has type checking errors. This may result in unexpected behaviour." ) def add_subcommand(subparsers, parents): parser = subparsers.add_parser( "check", parents=parents, help="Check a configuration file for errors." ) parser.add_argument( "-c", "--config", action="store", default=get_config_file(), dest="configfile", help="Use the specified configuration file.", ) parser.set_defaults(func=check_config) qtile-0.31.0/libqtile/scripts/shell.py0000664000175000017500000000440114762660347017612 0ustar epsilonepsilon# Copyright (c) 2008, Aldo Cortesi. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from libqtile import ipc, sh from libqtile.command import interface def qshell(args) -> None: if args.socket is None: socket = ipc.find_sockfile() else: socket = args.socket client = ipc.Client(socket, is_json=args.is_json) cmd_object = interface.IPCCommandInterface(client) qsh = sh.QSh(cmd_object) if args.command is not None: qsh.process_line(args.command) else: qsh.loop() def add_subcommand(subparsers, parents): parser = subparsers.add_parser( "shell", parents=parents, help="A shell-like interface to Qtile." ) parser.add_argument( "-s", "--socket", action="store", type=str, default=None, help="Use specified socket for IPC.", ) parser.add_argument( "-c", "--command", action="store", type=str, default=None, help="Run the specified qshell command and exit.", ) parser.add_argument( "-j", "--json", action="store_true", default=False, dest="is_json", help="Use JSON to communicate with Qtile.", ) parser.set_defaults(func=qshell) qtile-0.31.0/libqtile/scripts/migrate.py0000664000175000017500000002304414762660347020137 0ustar epsilonepsilon# Copyright (c) 2021, Tycho Andersen. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE from __future__ import annotations import argparse import os import os.path import shutil import sys from glob import glob from pathlib import Path from typing import TYPE_CHECKING from libqtile.scripts.migrations import MIGRATIONS, load_migrations from libqtile.utils import get_config_file if TYPE_CHECKING: from collections.abc import Iterator import libcst BACKUP_SUFFIX = ".migrate.bak" class AbortMigration(Exception): pass class SkipFile(Exception): pass def needs_libcst(func): def _wrapper(*args, **kwargs): if "libcst" not in sys.modules: try: global libcst libcst = __import__("libcst", globals(), locals()) except ImportError: print("libcst is needed for 'qtile migrate' commands.") print("Please install it and try again.") sys.exit(1) func(*args, **kwargs) return _wrapper def version_tuple(value: str) -> tuple[int, ...]: try: val = tuple(int(x) for x in value.split(".")) return val except TypeError: raise argparse.ArgumentTypeError("Cannot parse version string.") def file_and_backup(config_dir: str) -> Iterator[tuple[str, str]]: if os.path.isfile(config_dir): config_dir = os.path.dirname(config_dir) for py in glob(os.path.join(config_dir, "*.py")): backup = py + BACKUP_SUFFIX yield py, backup class QtileMigrate: """ May be overkill to use a class here but we can store state (i.e. args) without needing to pass them around all the time. """ @needs_libcst def __call__(self, args: argparse.Namespace) -> None: """ This is called by ArgParse when we run `qtile migrate`. The parsed options are passed as an argument. """ self.args = args self.filter_migrations() if self.args.list_migrations: self.list_migrations() return elif self.args.info: self.show_migration_info() return else: self.run_migrations() def filter_migrations(self) -> None: load_migrations() if self.args.run_migrations: self.migrations = [m for m in MIGRATIONS if m.ID in self.args.run_migrations] elif self.args.after_version: self.migrations = [m for m in MIGRATIONS if m.get_version() > self.args.after_version] else: self.migrations = MIGRATIONS if not self.migrations: sys.exit("No migrations found.") def list_migrations(self) -> None: width = max(len(m.ID) for m in self.migrations) + 4 ordered = sorted(self.migrations, key=lambda m: (m.get_version(), m.ID)) print(f"ID{' ' * (width - 2)}{'After Version':<15}Summary") for m in ordered: summary = m.show_summary().replace("``", "'") print(f"{m.ID:<{width}}{m.AFTER_VERSION:^15}{summary}") def show_migration_info(self) -> None: migration_id = self.args.info migration = [m for m in MIGRATIONS if m.ID == migration_id] if not migration: print(f"Unknown migration: {migration_id}") sys.exit(1) print(f"{migration_id}:") print(migration[0].show_help()) def get_source(self, path: str) -> libcst.metadata.MetadataWrapper: module = libcst.parse_module(Path(path).read_text()) return libcst.metadata.MetadataWrapper(module) def lint(self, path: str) -> None: print(f"{path}:") source = self.get_source(path) lint_lines = [] for m in self.migrations: migrator = m() migrator.migrate(source) lint_lines.extend(migrator.show_lint()) lint_lines.sort() print("\n".join(map(str, lint_lines))) def migrate(self, path: str) -> bool: source: libcst.metadata.MetadataWrapper | libcst.Module = self.get_source(path) changed = False for m in self.migrations: migrator = m() migrator.migrate(source) diff = migrator.show_diff(self.args.no_colour) if diff: if self.args.show_diff or not self.args.yes: print(f"{m.ID}: {m.show_summary()}\n") print(f"{diff}\n") if self.args.yes: assert migrator.updated is not None source = libcst.metadata.MetadataWrapper(migrator.updated) changed = True else: while ( a := input("Apply changes? (y)es, (n)o, (s)kip file, (q)uit. ").lower() ) not in ("y", "n", "s", "q"): print("Unexpected response. Try again.") if a == "y": assert migrator.updated is not None source = libcst.metadata.MetadataWrapper(migrator.updated) changed = True elif a == "n": assert migrator.original is not None source = migrator.original elif a == "s": raise SkipFile elif a == "q": raise AbortMigration if not changed: return False if not self.args.yes: while (save := input(f"Save all changes to {path}? (y)es, (n)o. ").lower()) not in ( "y", "n", ): print("Unexpected response. Try again.") do_save = save == "y" else: do_save = True if do_save: if isinstance(source, libcst.metadata.MetadataWrapper): source = source.module Path(f"{path}").write_text(source.code) print("Saved!") return True else: return False def run_migrations(self) -> None: backups = [] changed_files = [] aborted = False for py, backup in file_and_backup(self.args.config): if self.args.lint: self.lint(py) continue else: try: shutil.copyfile(py, backup) backups.append(backup) changed = self.migrate(py) if changed: changed_files.append(py) except SkipFile: backups.remove(backup) continue except AbortMigration: aborted = True break if aborted: print("Migration aborted. Reverting changes.") for f in changed_files: shutil.copyfile(f + BACKUP_SUFFIX, f) elif backups: print("Finished. Backup files have not been deleted.") def add_subcommand(subparsers, parents): parser = subparsers.add_parser( "migrate", parents=parents, help="Migrate a configuration file to the current API." ) parser.add_argument( "-c", "--config", action="store", default=get_config_file(), help="Use the specified configuration file (migrates every .py file in this directory).", ) parser.add_argument( "--yes", action="store_true", help="Automatically apply diffs with no confirmation.", ) parser.add_argument( "--show-diff", action="store_true", help="When used with --yes, will still output diff." ) parser.add_argument( "--lint", action="store_true", help="Providing linting output but don't update config." ) parser.add_argument( "--list-migrations", action="store_true", help="List available migrations." ) parser.add_argument( "--info", metavar="ID", help="Show detailed info for the migration with the given ID", ) parser.add_argument( "--after-version", metavar="VERSION", type=version_tuple, help="Run migrations introduced after VERSION.", ) parser.add_argument( "-r", "--run-migrations", type=lambda value: value.split(","), metavar="ID", help="Run named migration[s]. Comma separated list for multiple migrations", ) parser.add_argument( "--no-colour", action="store_true", help="Do not use colour in diff output." ) parser.add_argument("-v", "--verbose", action="store_true", help="Increase output verbosity") parser.set_defaults(func=QtileMigrate()) qtile-0.31.0/libqtile/scripts/run_cmd.py0000664000175000017500000000623414762660347020140 0ustar epsilonepsilon# Copyright (c) 2014, Roger Duran # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the \"Software\"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """ Command-line wrapper to run commands and add rules to new windows """ import argparse import atexit import subprocess from libqtile import ipc from libqtile.command import graph def run_cmd(opts) -> None: if opts.socket is None: socket = ipc.find_sockfile() else: socket = opts.socket client = ipc.Client(socket) root = graph.CommandGraphRoot() cmd = [opts.cmd] if opts.args: cmd.extend(opts.args) proc = subprocess.Popen(cmd) match_args = {"net_wm_pid": proc.pid} rule_args = { "float": opts.float, "intrusive": opts.intrusive, "group": opts.group, "break_on_match": not opts.dont_break, } graph_cmd = root.call("add_rule") # client.send(selectors, name, args, kwargs, lifted) _, rule_id = client.send((root.selectors, graph_cmd.name, (match_args, rule_args), {}, False)) def remove_rule() -> None: cmd = root.call("remove_rule") # client.send(selectors, name, args, kwargs, lifted) client.send((root.selectors, cmd.name, (rule_id,), {}, False)) atexit.register(remove_rule) proc.wait() def add_subcommand(subparsers, parents): parser = subparsers.add_parser( "run-cmd", parents=parents, help="A wrapper around the command graph." ) parser.add_argument("-s", "--socket", help="Use specified socket for IPC.") parser.add_argument( "-i", "--intrusive", action="store_true", help="If the new window should be intrusive." ) parser.add_argument( "-f", "--float", action="store_true", help="If the new window should be floating." ) parser.add_argument( "-b", "--dont-break", action="store_true", help="Do not break on match (keep applying rules).", ) parser.add_argument("-g", "--group", help="Set the window group.") parser.add_argument("cmd", help="Command to execute.") parser.add_argument( "args", nargs=argparse.REMAINDER, metavar="[args ...]", help="Optional arguments to pass to command.", ) parser.set_defaults(func=run_cmd) qtile-0.31.0/libqtile/widget/0000775000175000017500000000000014762660347015726 5ustar epsilonepsilonqtile-0.31.0/libqtile/widget/keyboardlayout.py0000664000175000017500000001647614762660347021354 0ustar epsilonepsilon# Copyright (c) 2013 Jacob Mourelos # Copyright (c) 2014 Shepilov Vladislav # Copyright (c) 2014-2015 Sean Vig # Copyright (c) 2014 Tycho Andersen # Copyright (c) 2019 zordsdavini # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations import re from abc import ABCMeta, abstractmethod from pathlib import Path from subprocess import CalledProcessError, check_output from typing import TYPE_CHECKING from libqtile.command.base import expose_command from libqtile.confreader import ConfigError from libqtile.log_utils import logger from libqtile.widget import base if TYPE_CHECKING: from libqtile.core.manager import Qtile class _BaseLayoutBackend(metaclass=ABCMeta): def __init__(self, qtile: Qtile): """ This handles getting and setter the keyboard layout with the appropriate backend. """ @abstractmethod def get_keyboard(self) -> str: """ Return the currently used keyboard layout as a string Examples: "us", "us dvorak". In case of error returns "unknown". """ def set_keyboard(self, layout: str, options: str | None) -> None: """ Set the keyboard layout with specified options. """ class _X11LayoutBackend(_BaseLayoutBackend): kb_layout_regex = re.compile(r"layout:\s+(?P[\w-]+)") kb_variant_regex = re.compile(r"variant:\s+(?P[\w-]+)") def get_keyboard(self) -> str: try: command = "setxkbmap -verbose 10 -query" setxkbmap_output = check_output(command.split(" ")).decode() except CalledProcessError: logger.exception("Can not get the keyboard layout:") return "unknown" except OSError: logger.exception("Please, check that xset is available:") return "unknown" match_layout = self.kb_layout_regex.search(setxkbmap_output) if match_layout is None: return "ERR" keyboard = match_layout.group("layout") match_variant = self.kb_variant_regex.search(setxkbmap_output) if match_variant: keyboard += " " + match_variant.group("variant") return keyboard def set_keyboard(self, layout: str, options: str | None) -> None: command = ["setxkbmap"] command.extend(layout.split(" ")) if options: command.extend(["-option", options]) try: check_output(command) except CalledProcessError: logger.error("Cannot change the keyboard layout.") except OSError: logger.error("Please, check that setxkbmap is available.") else: # Load Xmodmap if it's available if Path("~/.Xmodmap").expanduser().is_file(): try: check_output("xmodmap $HOME/.Xmodmap", shell=True) except CalledProcessError: logger.error("Could not load ~/.Xmodmap.") class _WaylandLayoutBackend(_BaseLayoutBackend): def __init__(self, qtile: Qtile) -> None: self.set_keymap = qtile.core.set_keymap self._layout: str = "" def get_keyboard(self) -> str: return self._layout def set_keyboard(self, layout: str, options: str | None) -> None: maybe_variant: str | None = None if " " in layout: layout_name, maybe_variant = layout.split(" ", maxsplit=1) else: layout_name = layout self.set_keymap(layout_name, options, maybe_variant) self._layout = layout layout_backends = { "x11": _X11LayoutBackend, "wayland": _WaylandLayoutBackend, } class KeyboardLayout(base.InLoopPollText): """Widget for changing and displaying the current keyboard layout To use this widget effectively you need to specify keyboard layouts you want to use (using "configured_keyboards") and bind function "next_keyboard" to specific keys in order to change layouts. For example: Key([mod], "space", lazy.widget["keyboardlayout"].next_keyboard(), desc="Next keyboard layout."), When running Qtile with the X11 backend, this widget requires setxkbmap to be available. Xmodmap will also be used if .Xmodmap file is available. """ defaults = [ ("update_interval", 1, "Update time in seconds."), ( "configured_keyboards", ["us"], "A list of predefined keyboard layouts " "represented as strings. For example: " "['us', 'us colemak', 'es', 'fr'].", ), ( "display_map", {}, "Custom display of layout. Key should be in format " "'layout variant'. For example: " "{'us': 'us', 'lt sgs': 'sgs', 'ru phonetic': 'ru'}", ), ("option", None, "string of setxkbmap option. Ex., 'compose:menu,grp_led:scroll'"), ] def __init__(self, **config): base.InLoopPollText.__init__(self, **config) self.add_defaults(KeyboardLayout.defaults) self.add_callbacks({"Button1": self.next_keyboard}) def _configure(self, qtile, bar): base.InLoopPollText._configure(self, qtile, bar) if qtile.core.name not in layout_backends: raise ConfigError("KeyboardLayout does not support backend: " + qtile.core.name) self.backend = layout_backends[qtile.core.name](qtile) self.backend.set_keyboard(self.configured_keyboards[0], self.option) @expose_command() def next_keyboard(self): """set the next layout in the list of configured keyboard layouts as new current layout in use If the current keyboard layout is not in the list, it will set as new layout the first one in the list. """ current_keyboard = self.backend.get_keyboard() if current_keyboard in self.configured_keyboards: # iterate the list circularly next_keyboard = self.configured_keyboards[ (self.configured_keyboards.index(current_keyboard) + 1) % len(self.configured_keyboards) ] else: next_keyboard = self.configured_keyboards[0] self.backend.set_keyboard(next_keyboard, self.option) self.tick() def poll(self): keyboard = self.backend.get_keyboard() if keyboard in self.display_map.keys(): return self.display_map[keyboard] return keyboard.upper() qtile-0.31.0/libqtile/widget/imapwidget.py0000664000175000017500000000722714762660347020442 0ustar epsilonepsilon# Copyright (c) 2015 David R. Andersen # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import imaplib import re import keyring from libqtile.confreader import ConfigError from libqtile.log_utils import logger from libqtile.widget import base class ImapWidget(base.ThreadPoolText): """Email IMAP widget This widget will scan one of your imap email boxes and report the number of unseen messages present. I've configured it to only work with imap with ssl. Your password is obtained from the Gnome Keyring. Writing your password to the keyring initially is as simple as (changing out and for your userid and password): 1) create the file ~/.local/share/python_keyring/keyringrc.cfg with the following contents:: [backend] default-keyring=keyring.backends.Gnome.Keyring keyring-path=/home//.local/share/keyring/ 2) Execute the following python shell script once:: #!/usr/bin/env python3 import keyring user = password = keyring.set_password('imapwidget', user, password) mbox names must include the path to the mbox (except for the default INBOX). So, for example if your mailroot is ``~/Maildir``, and you want to look at the mailbox at HomeMail/fred, the mbox setting would be: ``mbox="~/Maildir/HomeMail/fred"``. Note the nested sets of quotes! Labels can be whatever you choose, of course. Widget requirements: keyring_. .. _keyring: https://pypi.org/project/keyring/ """ defaults = [ ("mbox", '"INBOX"', "mailbox to fetch"), ("label", "INBOX", "label for display"), ("user", None, "email username"), ("server", None, "email server name"), ] def __init__(self, **config): base.ThreadPoolText.__init__(self, "", **config) self.add_defaults(ImapWidget.defaults) if self.user is None: raise ConfigError("You must set the 'user' parameter for the IMAP widget.") password = keyring.get_password("imapwidget", self.user) if password is not None: self.password = password else: logger.critical("Gnome Keyring Error") def poll(self): im = imaplib.IMAP4_SSL(self.server, 993) if self.password == "Gnome Keyring Error": text = "Gnome Keyring Error" else: im.login(self.user, self.password) status, response = im.status(self.mbox, "(UNSEEN)") text = response[0].decode() text = self.label + ": " + re.sub(r"\).*$", "", re.sub(r"^.*N\s", "", text)) im.logout() return text qtile-0.31.0/libqtile/widget/mpd2widget.py0000664000175000017500000003016714762660347020355 0ustar epsilonepsilon""" A widget for Music Player Daemon (MPD) based on python-mpd2. This widget exists since python-mpd library is no longer supported. """ from collections import defaultdict from html import escape from mpd import CommandError, ConnectionError, MPDClient from libqtile import utils from libqtile.log_utils import logger from libqtile.widget import base # Mouse Interaction # TODO: Volume inc/dec support keys = { # Left mouse button 1: "toggle", # Right mouse button 3: "stop", # Scroll up 4: "previous", # Scroll down 5: "next", } # To display mpd state play_states = {"play": "\u25b6", "pause": "\u23f8", "stop": "\u25a0"} def option(char): """ old status mapping method. Deprecated. """ def _convert(elements, key, space): if key in elements and elements[key] != "0": elements[key] = char else: elements[key] = space return _convert # Changes to formatter will still use this dicitionary as a fallback prepare_status = { "repeat": option("r"), "random": option("z"), "single": option("1"), "consume": option("c"), "updating_db": option("U"), } # dictionary for new formatting method. This is now default. status_dict = {"repeat": "r", "random": "z", "single": "1", "consume": "c", "updating_db": "U"} default_idle_message = "MPD IDLE" default_idle_format = ( "{play_status} {idle_message}" + "[{repeat}{random}{single}{consume}{updating_db}]" ) default_format = ( "{play_status} {artist}/{title} " + "[{repeat}{random}{single}{consume}{updating_db}]" ) default_undefined_status_value = "Undefined" def default_cmd(): return None format_fns = { "all": escape, } class Mpd2(base.ThreadPoolText): r"""Mpd2 Object. Parameters ========== status_format: format string to display status For a full list of values, see: MPDClient.status() and MPDClient.currentsong() https://musicpd.org/doc/protocol/command_reference.html#command_status https://musicpd.org/doc/protocol/tags.html Default:: '{play_status} {artist}/{title} \ [{repeat}{random}{single}{consume}{updating_db}]' ``play_status`` is a string from ``play_states`` dict Note that the ``time`` property of the song renamed to ``fulltime`` to prevent conflicts with status information during formating. idle_format: format string to display status when no song is in queue. Default:: '{play_status} {idle_message} \ [{repeat}{random}{single}{consume}{updating_db}]' Note that the ``artist`` key fallbacks to similar keys in specific order. (``artist`` -> ``albumartist`` -> ``performer`` -> -> ``composer`` -> ``conductor`` -> ``ensemble``) idle_message: text to display instead of song information when MPD is idle. (i.e. no song in queue) Default:: "MPD IDLE" undefined_value: text to display when status key is undefined Default:: "Undefined" prepare_status: dict of functions to replace values in status with custom characters. ``f(status, key, space_element) => str`` New functionality allows use of a dictionary of plain strings. Default:: status_dict = { 'repeat': 'r', 'random': 'z', 'single': '1', 'consume': 'c', 'updating_db': 'U' } format_fns: A dict of functions to format the various elements. 'Tag': f(str) => str Default:: { 'all': lambda s: cgi.escape(s) } N.B. if 'all' is present, it is processed on every element of song_info before any other formatting is done. mouse_buttons: A dict of mouse button numbers to actions Widget requirements: python-mpd2_. .. _python-mpd2: https://pypi.org/project/python-mpd2/ """ defaults = [ ("update_interval", 1, "Interval of update widget"), ("host", "localhost", "Host of mpd server"), ("port", 6600, "Port of mpd server"), ("password", None, "Password for auth on mpd server"), ("mouse_buttons", keys, "b_num -> action."), ("play_states", play_states, "Play state mapping"), ("format_fns", format_fns, "Dictionary of format methods"), ("command", default_cmd, "command to be executed by mapped mouse button."), ("prepare_status", status_dict, "characters to show the status of MPD"), ("status_format", default_format, "format for displayed song info."), ("idle_format", default_idle_format, "format for status when mpd has no playlist."), ("idle_message", default_idle_message, "text to display when mpd is idle."), ( "undefined_value", default_undefined_status_value, "text to display when status key is undefined.", ), ("timeout", 30, "MPDClient timeout"), ("idletimeout", 5, "MPDClient idle command timeout"), ("no_connection", "No connection", "Text when mpd is disconnected"), ("color_progress", None, "Text color to indicate track progress."), ("space", "-", "Space keeper"), ] def __init__(self, **config): """Constructor.""" super().__init__("", **config) self.add_defaults(Mpd2.defaults) if self.color_progress: self.color_progress = utils.hex(self.color_progress) def _configure(self, qtile, bar): super()._configure(qtile, bar) self.client = MPDClient() self.client.timeout = self.timeout self.client.idletimeout = self.idletimeout @property def connected(self): """Attempt connection to mpd server.""" try: self.client.ping() # pylint: disable=E1101 except (OSError, ConnectionError): try: self.client.connect(self.host, self.port) if self.password: self.client.password(self.password) # pylint: disable=E1101 except (OSError, ConnectionError, CommandError): return False return True def poll(self): """ Called by qtile manager. poll the mpd server and update widget. """ if self.connected: return self.update_status() else: return self.no_connection def update_status(self): """get updated info from mpd server and call format.""" self.client.command_list_ok_begin() self.client.status() # pylint: disable=E1101 self.client.currentsong() # pylint: disable=E1101 status, current_song = self.client.command_list_end() return self.formatter(status, current_song) def button_press(self, x, y, button): """handle click event on widget.""" base.ThreadPoolText.button_press(self, x, y, button) m_name = self.mouse_buttons[button] if self.connected: if hasattr(self, m_name): self.__try_call(m_name) elif hasattr(self.client, m_name): self.__try_call(m_name, self.client) def __try_call(self, attr_name, obj=None): err1 = "Class {Class} has no attribute {attr}." err2 = 'attribute "{Class}.{attr}" is not callable.' context = obj or self try: getattr(context, attr_name)() except (AttributeError, TypeError) as e: if isinstance(e, AttributeError): err = err1.format(Class=type(context).__name__, attr=attr_name) else: err = err2.format(Class=type(context).__name__, attr=attr_name) logger.exception("%s %s", err, e.args[0]) def toggle(self): """toggle play/pause.""" status = self.client.status() # pylint: disable=E1101 play_status = status["state"] if play_status == "play": self.client.pause() # pylint: disable=E1101 else: self.client.play() # pylint: disable=E1101 def formatter(self, status, current_song): """format song info.""" song_info = defaultdict(lambda: self.undefined_value) song_info["play_status"] = self.play_states[status["state"]] if status["state"] == "stop" and current_song == {}: song_info["idle_message"] = self.idle_message fmt = self.idle_format else: fmt = self.status_format for k in current_song: song_info[k] = current_song[k] song_info["fulltime"] = song_info["time"] del song_info["time"] song_info.update(status) if song_info["updating_db"] == self.undefined_value: song_info["updating_db"] = "0" if not callable(self.prepare_status["repeat"]): for k in self.prepare_status: if k in status and status[k] != "0": # Much more direct. song_info[k] = self.prepare_status[k] else: song_info[k] = self.space else: self.prepare_formatting(song_info) # 'remaining' isn't actually in the information provided by mpd # so we construct it from 'fulltime' and 'elapsed'. # 'elapsed' is always less than or equal to 'fulltime', if it exists. # Remaining should default to '00:00' if either or both are missing. # These values are also used for coloring text by progress, if wanted. if "remaining" in self.status_format or self.color_progress: total = ( float(song_info["fulltime"]) if song_info["fulltime"] != self.undefined_value else 0.0 ) elapsed = ( float(song_info["elapsed"]) if song_info["elapsed"] != self.undefined_value else 0.0 ) song_info["remaining"] = f"{float(total - elapsed):.2f}" if "song" in self.status_format and song_info["song"] != self.undefined_value: song_info["currentsong"] = str(int(song_info["song"]) + 1) if "artist" in self.status_format and song_info["artist"] == self.undefined_value: artist_keys = ("albumartist", "performer", "composer", "conductor", "ensemble") for key in artist_keys: if song_info[key] != self.undefined_value: song_info["artist"] = song_info[key] break # mpd serializes tags containing commas as lists. for key in song_info: if isinstance(song_info[key], list): song_info[key] = ", ".join(song_info[key]) # Now we apply the user formatting to selected elements in song_info. # if 'all' is defined, it is applied first. # the reason for this is that, if the format functions do pango markup. # we don't want to do anything that would mess it up, e.g. `escape`ing. if "all" in self.format_fns: for key in song_info: song_info[key] = self.format_fns["all"](song_info[key]) for fmt_fn in self.format_fns: if fmt_fn in song_info and fmt_fn != "all": song_info[fmt_fn] = self.format_fns[fmt_fn](song_info[fmt_fn]) # fmt = self.status_format if not isinstance(fmt, str): fmt = str(fmt) formatted = fmt.format_map(song_info) if self.color_progress and status["state"] != "stop": try: progress = int(len(formatted) * elapsed / total) formatted = f'{formatted[:progress]}{formatted[progress:]}' except (ZeroDivisionError, ValueError): pass return formatted def prepare_formatting(self, status): """old way of preparing status formatting.""" for key in self.prepare_status: self.prepare_status[key](status, key, self.space) def finalize(self): """finalize.""" super().finalize() try: self.client.close() # pylint: disable=E1101 self.client.disconnect() except ConnectionError: pass qtile-0.31.0/libqtile/widget/widgetbox.py0000664000175000017500000001414514762660347020301 0ustar epsilonepsilon# Copyright (c) 2020 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations from typing import TYPE_CHECKING from libqtile.command.base import expose_command from libqtile.log_utils import logger from libqtile.pangocffi import markup_escape_text from libqtile.widget import Systray, base if TYPE_CHECKING: from typing import Any class WidgetBox(base._TextBox): """A widget to declutter your bar. WidgetBox is a widget that hides widgets by default but shows them when the box is opened. Widgets that are hidden will still update etc. as if they were on the main bar. Button clicks are passed to widgets when they are visible so callbacks will work. Widgets in the box also remain accessible via command interfaces. Widgets can only be added to the box via the configuration file. The widget is configured by adding widgets to the "widgets" parameter as follows:: widget.WidgetBox(widgets=[ widget.TextBox(text="This widget is in the box"), widget.Memory() ] ), """ orientations = base.ORIENTATION_HORIZONTAL defaults: list[tuple[str, Any, str]] = [ ( "close_button_location", "left", "Location of close button when box open ('left' or 'right')", ), ("text_closed", "[<]", "Text when box is closed"), ("text_open", "[>]", "Text when box is open"), ("widgets", list(), "A list of widgets to include in the box"), ("start_opened", False, "Spawn the box opened"), ] def __init__(self, _widgets: list[base._Widget] | None = None, **config): base._TextBox.__init__(self, **config) self.add_defaults(WidgetBox.defaults) self.box_is_open = False self.add_callbacks({"Button1": self.toggle}) if _widgets: logger.warning( "The use of a positional argument in WidgetBox is deprecated. " "Please update your config to use widgets=[...]." ) self.widgets = _widgets self.close_button_location: str if self.close_button_location not in ["left", "right"]: val = self.close_button_location logger.warning("Invalid value for 'close_button_location': %s", val) self.close_button_location = "left" def _configure(self, qtile, bar): base._TextBox._configure(self, qtile, bar) self.text = markup_escape_text(self.text_open if self.box_is_open else self.text_closed) if self.configured: return for idx, w in enumerate(self.widgets): if w.configured: w = w.create_mirror() self.widgets[idx] = w self.qtile.register_widget(w) w._configure(self.qtile, self.bar) w.offsety = self.bar.border_width[0] # In case the widget is mirrored, we need to draw it once so the # mirror can copy the surface but draw it off screen w.offsetx = self.bar.width self.qtile.call_soon(w.draw) # Setting the configured flag for widgets was moved to Bar._configure so we need to # set it here. w.configured = True # Disable drawing of the widget's contents for w in self.widgets: w.drawer.disable() # We're being cautious: `box_is_open` should never be True here... if self.start_opened and not self.box_is_open: self.qtile.call_soon(self.toggle) def set_box_label(self): self.text = markup_escape_text(self.text_open if self.box_is_open else self.text_closed) def toggle_widgets(self): for widget in self.widgets: try: self.bar.widgets.remove(widget) # Override drawer.drawer with a no-op widget.drawer.disable() # Systray widget needs some additional steps to hide as the icons # are separate _Window instances. # Systray unhides icons when it draws so we only need to hide them. if isinstance(widget, Systray): for icon in widget.tray_icons: icon.hide() except ValueError: continue index = self.bar.widgets.index(self) if self.close_button_location == "left": index += 1 if self.box_is_open: # Need to reverse list as widgets get added in front of eachother. for widget in self.widgets[::-1]: # enable drawing again widget.drawer.enable() self.bar.widgets.insert(index, widget) @expose_command() def toggle(self): """Toggle box state""" self.box_is_open = not self.box_is_open self.toggle_widgets() self.set_box_label() self.bar.draw() @expose_command() def open(self): """Open the widgetbox.""" if not self.box_is_open: self.toggle() @expose_command() def close(self): """Close the widgetbox.""" if self.box_is_open: self.toggle() qtile-0.31.0/libqtile/widget/windowtabs.py0000664000175000017500000000653414762660347020471 0ustar epsilonepsilon# Copyright (c) 2012-2013 Craig Barnes # Copyright (c) 2012 roger # Copyright (c) 2012, 2014 Tycho Andersen # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Adi Sieker # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from libqtile import bar, hook, pangocffi from libqtile.log_utils import logger from libqtile.widget import base class WindowTabs(base._TextBox): """ Displays the name of each window in the current group. Contrary to TaskList this is not an interactive widget. The window that currently has focus is highlighted. """ defaults = [ ("separator", " | ", "Task separator text."), ("selected", ("", ""), "Selected task indicator"), ( "parse_text", lambda window_name: window_name, "Function to modify window names. It must accept " "a string argument (original window name) and return " "a string with the modified name.", ), ] def __init__(self, **config): width = config.pop("width", bar.STRETCH) base._TextBox.__init__(self, width=width, **config) self.add_defaults(WindowTabs.defaults) if not isinstance(self.selected, tuple | list): self.selected = (self.selected, self.selected) def _configure(self, qtile, bar): base._TextBox._configure(self, qtile, bar) hook.subscribe.client_name_updated(self.update) hook.subscribe.focus_change(self.update) hook.subscribe.float_change(self.update) self.add_callbacks({"Button1": self.bar.screen.group.next_window}) def update(self, *args): names = [] for w in self.bar.screen.group.windows: try: name = self.parse_text(w.name if w and w.name else "") except: # noqa: E722 logger.exception("parse_text function failed:") name = w.name if w and w.name else "(unnamed)" state = "" if w.maximized: state = "[] " elif w.minimized: state = "_ " elif w.floating: state = "V " task = pangocffi.markup_escape_text(state + name) if w is self.bar.screen.group.current_window: task = task.join(self.selected) names.append(task) self.text = self.separator.join(names) self.bar.draw() qtile-0.31.0/libqtile/widget/image.py0000664000175000017500000000754514762660347017375 0ustar epsilonepsilon# Copyright (c) 2013 dequis # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Adi Sieker # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import os from libqtile import bar from libqtile.command.base import expose_command from libqtile.images import Img from libqtile.log_utils import logger from libqtile.widget import base class Image(base._Widget, base.MarginMixin): """Display a PNG image on the bar""" orientations = base.ORIENTATION_BOTH defaults = [ ("scale", True, "Enable/Disable image scaling"), ("rotate", 0.0, "rotate the image in degrees counter-clockwise"), ("filename", None, "Image filename. Can contain '~'"), ] def __init__(self, length=bar.CALCULATED, **config): base._Widget.__init__(self, length, **config) self.add_defaults(Image.defaults) self.add_defaults(base.MarginMixin.defaults) # make the default 0 instead self._variable_defaults["margin"] = 0 def _configure(self, qtile, bar): base._Widget._configure(self, qtile, bar) self._update_image() def _update_image(self): self.img = None if not self.filename: logger.warning("Image filename not set!") return self.filename = os.path.expanduser(self.filename) if not os.path.exists(self.filename): logger.warning("Image does not exist: %s", self.filename) return img = Img.from_path(self.filename) self.img = img img.theta = self.rotate if not self.scale: return if self.bar.horizontal: new_height = self.bar.height - (self.margin_y * 2) img.resize(height=new_height) else: new_width = self.bar.width - (self.margin_x * 2) img.resize(width=new_width) def draw(self): if self.img is None: return self.drawer.clear(self.background or self.bar.background) self.drawer.ctx.save() self.drawer.ctx.translate(self.margin_x, self.margin_y) self.drawer.ctx.set_source(self.img.pattern) self.drawer.ctx.paint() self.drawer.ctx.restore() if self.bar.horizontal: self.drawer.draw(offsetx=self.offset, offsety=self.offsety, width=self.width) else: self.drawer.draw(offsety=self.offset, offsetx=self.offsetx, height=self.width) def calculate_length(self): if self.img is None: return 0 if self.bar.horizontal: return self.img.width + (self.margin_x * 2) else: return self.img.height + (self.margin_y * 2) @expose_command() def update(self, filename): old_length = self.calculate_length() self.filename = filename self._update_image() if self.calculate_length() == old_length: self.draw() else: self.bar.draw() qtile-0.31.0/libqtile/widget/prompt.py0000664000175000017500000006775214762660347017642 0ustar epsilonepsilon# Copyright (c) 2010-2011 Aldo Cortesi # Copyright (c) 2010 Philip Kranz # Copyright (c) 2011 Mounier Florian # Copyright (c) 2011 Paul Colomiets # Copyright (c) 2011-2012 roger # Copyright (c) 2011-2012, 2014 Tycho Andersen # Copyright (c) 2012 Dustin Lacewell # Copyright (c) 2012 Laurie Clark-Michalek # Copyright (c) 2012-2014 Craig Barnes # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014 ramnes # Copyright (c) 2014 Sean Vig # Copyright (C) 2015, Juan Riquelme González # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations import abc import glob import os import pickle import string from collections import deque from libqtile import hook, pangocffi, utils from libqtile.command.base import CommandObject, SelectError, expose_command from libqtile.command.client import InteractiveCommandClient from libqtile.command.interface import CommandError, QtileCommandInterface from libqtile.log_utils import logger from libqtile.widget import base class AbstractCompleter(metaclass=abc.ABCMeta): @abc.abstractmethod def __init__(self, qtile: CommandObject) -> None: pass @abc.abstractmethod def actual(self) -> str | None: pass @abc.abstractmethod def reset(self) -> None: pass @abc.abstractmethod def complete(self, txt: str, aliases: dict[str, str] | None = None) -> str: """ Perform the requested completion on the given text. The completer can optionally support aliases, which map strings to commands. The completer should include the aliases in the completion results. """ # pragma: no cover class NullCompleter(AbstractCompleter): def __init__(self, qtile) -> None: self.qtile = qtile def actual(self) -> str: return "" def reset(self) -> None: pass def complete(self, txt: str, _aliases: dict[str, str] | None = None) -> str: return txt class FileCompleter(AbstractCompleter): def __init__(self, qtile, _testing=False) -> None: self._testing = _testing self.qtile = qtile self.thisfinal = None # type: str | None self.lookup = None # type: list[tuple[str, str]] | None self.reset() def actual(self) -> str | None: return self.thisfinal def reset(self) -> None: self.lookup = None def complete(self, txt: str, _aliases: dict[str, str] | None = None) -> str: """Returns the next completion for txt, or None if there is no completion""" if self.lookup is None: self.lookup = [] if txt == "" or txt[0] not in "~/": txt = "~/" + txt path = os.path.expanduser(txt) if os.path.isdir(path): files = glob.glob(os.path.join(path, "*")) prefix = txt else: files = glob.glob(path + "*") prefix = os.path.dirname(txt) prefix = prefix.rstrip("/") or "/" for f in files: display = os.path.join(prefix, os.path.basename(f)) if os.path.isdir(f): display += "/" self.lookup.append((display, f)) self.lookup.sort() self.offset = -1 self.lookup.append((txt, txt)) self.offset += 1 if self.offset >= len(self.lookup): self.offset = 0 ret = self.lookup[self.offset] self.thisfinal = ret[1] return ret[0] class QshCompleter(AbstractCompleter): def __init__(self, qtile: CommandObject) -> None: q = QtileCommandInterface(qtile) self.client = InteractiveCommandClient(q) self.thisfinal = None # type: str | None self.reset() def actual(self) -> str | None: return self.thisfinal def reset(self) -> None: self.lookup = None # type: list[tuple[str, str]] | None self.path = "" self.offset = -1 def complete(self, txt: str, _aliases: dict[str, str] | None = None) -> str: txt = txt.lower() if self.lookup is None: self.lookup = [] path = txt.split(".")[:-1] self.path = ".".join(path) term = txt.split(".")[-1] if len(self.path) > 0: self.path += "." contains_cmd = f"self.client.{self.path}_contains" try: contains = eval(contains_cmd) except AttributeError: contains = [] for obj in contains: if obj.lower().startswith(term): self.lookup.append((obj, obj)) commands_cmd = f"self.client.{self.path}commands()" try: commands = eval(commands_cmd) except (CommandError, AttributeError): commands = [] for cmd in commands: if cmd.lower().startswith(term): self.lookup.append((cmd + "()", cmd + "()")) self.offset = -1 self.lookup.append((term, term)) self.offset += 1 if self.offset >= len(self.lookup): self.offset = 0 ret = self.lookup[self.offset] self.thisfinal = self.path + ret[0] return self.path + ret[0] class GroupCompleter(AbstractCompleter): def __init__(self, qtile: CommandObject) -> None: self.qtile = qtile self.thisfinal = None # type: str | None self.lookup = None # type: list[tuple[str, str]] | None self.offset = -1 def actual(self) -> str | None: """Returns the current actual value""" return self.thisfinal def reset(self) -> None: self.lookup = None self.offset = -1 def complete(self, txt: str, _aliases: dict[str, str] | None = None) -> str: """Returns the next completion for txt, or None if there is no completion""" txt = txt.lower() if not self.lookup: self.lookup = [] for group in self.qtile.groups_map.keys(): if group.lower().startswith(txt): self.lookup.append((group, group)) self.lookup.sort() self.offset = -1 self.lookup.append((txt, txt)) self.offset += 1 if self.offset >= len(self.lookup): self.offset = 0 ret = self.lookup[self.offset] self.thisfinal = ret[1] return ret[0] class WindowCompleter(AbstractCompleter): def __init__(self, qtile: CommandObject) -> None: self.qtile = qtile self.thisfinal = None # type: str | None self.lookup = None # type: list[tuple[str, str]] | None self.offset = -1 def actual(self) -> str | None: """Returns the current actual value""" return self.thisfinal def reset(self) -> None: self.lookup = None self.offset = -1 def complete(self, txt: str, _aliases: dict[str, str] | None = None) -> str: """Returns the next completion for txt, or None if there is no completion""" if self.lookup is None: self.lookup = [] for wid, window in self.qtile.windows_map.items(): if window.group and window.name.lower().startswith(txt): self.lookup.append((window.name, wid)) self.lookup.sort() self.offset = -1 self.lookup.append((txt, txt)) self.offset += 1 if self.offset >= len(self.lookup): self.offset = 0 ret = self.lookup[self.offset] self.thisfinal = ret[1] return ret[0] class CommandCompleter: """ Parameters ========== _testing : disables reloading of the lookup table to make testing possible. """ DEFAULTPATH = "/bin:/usr/bin:/usr/local/bin" def __init__(self, qtile, _testing=False): self.lookup = None # type: list[tuple[str, str]] | None self.offset = -1 self.thisfinal = None # type: str | None self._testing = _testing def actual(self) -> str | None: """Returns the current actual value""" return self.thisfinal def executable(self, fpath: str): return os.access(fpath, os.X_OK) def reset(self) -> None: self.lookup = None self.offset = -1 def complete(self, txt: str, aliases: dict[str, str] | None = None) -> str: """Returns the next completion for txt, or None if there is no completion""" if self.lookup is None: # Lookup is a set of (display value, actual value) tuples. self.lookup = [] if txt and txt[0] in "~/": path = os.path.expanduser(txt) if os.path.isdir(path): files = glob.glob(os.path.join(path, "*")) prefix = txt else: files = glob.glob(path + "*") prefix = os.path.dirname(txt) prefix = prefix.rstrip("/") or "/" for f in files: if self.executable(f): display = os.path.join(prefix, os.path.basename(f)) if os.path.isdir(f): display += "/" self.lookup.append((display, f)) else: dirs = os.environ.get("PATH", self.DEFAULTPATH).split(":") for d in dirs: try: d = os.path.expanduser(d) for cmd in glob.iglob(os.path.join(d, f"{txt}*")): if self.executable(cmd): self.lookup.append( (os.path.basename(cmd), cmd), ) except OSError: pass if aliases: for alias in aliases: if alias.startswith(txt): self.lookup.append((alias, aliases[alias])) self.lookup.sort() self.offset = -1 self.lookup.append((txt, txt)) self.offset += 1 if self.offset >= len(self.lookup): self.offset = 0 ret = self.lookup[self.offset] self.thisfinal = ret[1] return ret[0] class Prompt(base._TextBox): """A widget that prompts for user input Input should be started using the ``.start_input()`` method on this class. """ completers = { "file": FileCompleter, "qshell": QshCompleter, "cmd": CommandCompleter, "group": GroupCompleter, "window": WindowCompleter, None: NullCompleter, } defaults = [ ("cursor", True, "Show a cursor"), ("cursorblink", 0.5, "Cursor blink rate. 0 to disable."), ("cursor_color", "bef098", "Color for the cursor and text over it."), ("prompt", "{prompt}: ", "Text displayed at the prompt"), ("record_history", True, "Keep a record of executed commands"), ("max_history", 100, "Commands to keep in history. 0 for no limit."), ("ignore_dups_history", False, "Don't store duplicates in history"), ( "bell_style", "audible", "Alert at the begin/end of the command history. " + "Possible values: 'audible' (X11 only), 'visual' and None.", ), ("visual_bell_color", "ff0000", "Color for the visual bell (changes prompt background)."), ("visual_bell_time", 0.2, "Visual bell duration (in seconds)."), ] def __init__(self, **config) -> None: base._TextBox.__init__(self, "", **config) self.add_defaults(Prompt.defaults) self.active = False self.completer = None # type: AbstractCompleter | None self.aliases: dict[str, str] | None = None # If history record is on, get saved history or create history record if self.record_history: self.history_path = os.path.join(utils.get_cache_dir(), "prompt_history") if os.path.exists(self.history_path): with open(self.history_path, "rb") as f: try: self.history = pickle.load(f) if self.ignore_dups_history: self._dedup_history() except: # noqa: E722 # unfortunately, pickle doesn't wrap its errors, so we # can't detect what's a pickle error and what's not. logger.exception("failed to load prompt history") self.history = { x: deque(maxlen=self.max_history) for x in self.completers } # self.history of size does not match. if len(self.history) != len(self.completers): self.history = { x: deque(maxlen=self.max_history) for x in self.completers } if self.max_history != self.history[list(self.history)[0]].maxlen: self.history = { x: deque(self.history[x], self.max_history) for x in self.completers } else: self.history = {x: deque(maxlen=self.max_history) for x in self.completers} def _configure(self, qtile, bar) -> None: self.markup = True base._TextBox._configure(self, qtile, bar) def f(win): if self.active and not win == self.bar.window: self._unfocus() def prevent_focus_steal(win): if self.active: win.can_steal_focus = False hook.subscribe.client_new(prevent_focus_steal) hook.subscribe.client_focus(f) # Define key handlers (action to do when a specific key is hit) keyhandlers = { "Tab": self._trigger_complete, "BackSpace": self._delete_char(), "Delete": self._delete_char(False), "KP_Delete": self._delete_char(False), "Escape": self._unfocus, "Return": self._send_cmd, "KP_Enter": self._send_cmd, "Up": self._get_prev_cmd, "KP_Up": self._get_prev_cmd, "Down": self._get_next_cmd, "KP_Down": self._get_next_cmd, "Left": self._move_cursor(), "KP_Left": self._move_cursor(), "Right": self._move_cursor("right"), "KP_Right": self._move_cursor("right"), } self.keyhandlers = {qtile.core.keysym_from_name(k): v for k, v in keyhandlers.items()} printables = {x: self._write_char for x in range(127) if chr(x) in string.printable} self.keyhandlers.update(printables) self.tab = qtile.core.keysym_from_name("Tab") self.bell_style: str if self.bell_style == "audible" and qtile.core.name != "x11": self.bell_style = "visual" logger.warning("Prompt widget only supports audible bell under X11") if self.bell_style == "visual": self.original_background = self.background def start_input( self, prompt, callback, complete=None, strict_completer=False, allow_empty_input=False, aliases: dict[str, str] | None = None, ) -> None: """Run the prompt Displays a prompt and starts to take one line of keyboard input from the user. When done, calls the callback with the input string as argument. If history record is enabled, also allows to browse between previous commands with ↑ and ↓, and execute them (untouched or modified). When history is exhausted, fires an alert. It tries to mimic, in some way, the shell behavior. Parameters ========== complete : Tab-completion. Can be None, "cmd", "file", "group", "qshell" or "window". prompt : text displayed at the prompt, e.g. "spawn: " callback : function to call with returned value. complete : completer to use. strict_completer : When True the return value wil be the exact completer result where available. allow_empty_input : When True, an empty value will still call the callback function aliases : Dictionary mapping aliases to commands. If the entered command is a key in this dict, the command it maps to will be executed instead. """ if self.cursor and self.cursorblink and not self.active: self.timeout_add(self.cursorblink, self._blink) self.display = self.prompt.format(prompt=prompt) self.display = pangocffi.markup_escape_text(self.display) self.active = True self.user_input = "" self.archived_input = "" self.show_cursor = self.cursor self.cursor_position = 0 self.callback = callback self.aliases = aliases self.completer = self.completers[complete](self.qtile) self.strict_completer = strict_completer self.allow_empty_input = allow_empty_input self._update() self.bar.widget_grab_keyboard(self) if self.record_history: self.completer_history = self.history[complete] self.position = len(self.completer_history) def calculate_length(self) -> int: if self.text: width = min(self.layout.width, self.bar.width) + self.actual_padding * 2 return width else: return 0 def _blink(self) -> None: self.show_cursor = not self.show_cursor self._update() if self.active: self.timeout_add(self.cursorblink, self._blink) def _highlight_text(self, text) -> str: color = utils.hex(self.cursor_color) text = f'{text}' if self.show_cursor: text = f"{text}" return text def _update(self) -> None: if self.active: self.text = self.archived_input or self.user_input cursor = pangocffi.markup_escape_text(" ") if self.cursor_position < len(self.text): txt1 = self.text[: self.cursor_position] txt2 = self.text[self.cursor_position] txt3 = self.text[self.cursor_position + 1 :] for text in (txt1, txt2, txt3): text = pangocffi.markup_escape_text(text) txt2 = self._highlight_text(txt2) self.text = f"{txt1}{txt2}{txt3}{cursor}" else: self.text = pangocffi.markup_escape_text(self.text) self.text += self._highlight_text(cursor) self.text = self.display + self.text else: self.text = "" self.bar.draw() def _trigger_complete(self) -> None: # Trigger the auto completion in user input assert self.completer is not None self.user_input = self.completer.complete(self.user_input, self.aliases) self.cursor_position = len(self.user_input) def _history_to_input(self) -> None: # Move actual command (when exploring history) to user input and update # history position (right after the end) if self.archived_input: self.user_input = self.archived_input self.archived_input = "" self.position = len(self.completer_history) def _insert_before_cursor(self, charcode) -> None: # Insert a character (given their charcode) in input, before the cursor txt1 = self.user_input[: self.cursor_position] txt2 = self.user_input[self.cursor_position :] self.user_input = txt1 + chr(charcode) + txt2 self.cursor_position += 1 def _delete_char(self, backspace=True): # Return a function that deletes character from the input text. # If backspace is True, function will emulate backspace, else Delete. def f(): self._history_to_input() step = -1 if backspace else 0 if not backspace and self.cursor_position == len(self.user_input): self._alert() elif len(self.user_input) > 0 and self.cursor_position + step > -1: txt1 = self.user_input[: self.cursor_position + step] txt2 = self.user_input[self.cursor_position + step + 1 :] self.user_input = txt1 + txt2 if step: self.cursor_position += step else: self._alert() return f def _write_char(self): # Add pressed (legal) char key to user input. # No LookupString in XCB... oh, the shame! Unicode users beware! self._history_to_input() self._insert_before_cursor(self.key) def _unfocus(self): # Remove focus from the widget self.active = False self._update() self.bar.widget_ungrab_keyboard() def _send_cmd(self): # Send the prompted text for execution self._unfocus() if self.strict_completer: self.user_input = self.actual_value or self.user_input del self.actual_value self._history_to_input() if self.user_input or self.allow_empty_input: # If history record is activated, also save command in history if self.record_history: # ensure no dups in history if self.ignore_dups_history and (self.user_input in self.completer_history): self.completer_history.remove(self.user_input) self.position -= 1 self.completer_history.append(self.user_input) if self.position < self.max_history: self.position += 1 os.makedirs(os.path.dirname(self.history_path), exist_ok=True) with open(self.history_path, mode="wb") as f: pickle.dump(self.history, f, protocol=2) self.callback(self.user_input) def _alert(self): # Fire an alert (audible or visual), if bell style is not None. if self.bell_style == "audible": self.qtile.core.conn.conn.core.Bell(0) elif self.bell_style == "visual": self.background = self.visual_bell_color self.timeout_add(self.visual_bell_time, self._stop_visual_alert) def _stop_visual_alert(self): self.background = self.original_background self._update() def _get_prev_cmd(self): # Get the previous command in history. # If there isn't more previous commands, ring system bell if self.record_history: if not self.position: self._alert() else: self.position -= 1 self.archived_input = self.completer_history[self.position] self.cursor_position = len(self.archived_input) def _get_next_cmd(self): # Get the next command in history. # If the last command was already reached, ring system bell. if self.record_history: if self.position == len(self.completer_history): self._alert() elif self.position < len(self.completer_history): self.position += 1 if self.position == len(self.completer_history): self.archived_input = "" else: self.archived_input = self.completer_history[self.position] self.cursor_position = len(self.archived_input) def _cursor_to_left(self): # Move cursor to left, if possible if self.cursor_position: self.cursor_position -= 1 else: self._alert() def _cursor_to_right(self): # move cursor to right, if possible command = self.archived_input or self.user_input if self.cursor_position < len(command): self.cursor_position += 1 else: self._alert() def _move_cursor(self, direction="left"): # Move the cursor to left or right, according to direction if direction == "left": return self._cursor_to_left elif direction == "right": return self._cursor_to_right def _get_keyhandler(self, k): # Return the action (a function) to do according the pressed key (k). self.key = k if k in self.keyhandlers: if k != self.tab: self.actual_value = self.completer.actual() self.completer.reset() return self.keyhandlers[k] def process_key_press(self, keysym: int): """Key press handler for the minibuffer. Currently only supports ASCII characters. """ handle_key = self._get_keyhandler(keysym) if handle_key: handle_key() del self.key self._update() @expose_command() def fake_keypress(self, key: str) -> None: self.process_key_press(self.qtile.core.keysym_from_name(key)) @expose_command() def info(self): """Returns a dictionary of info for this object""" return dict( name=self.name, width=self.width, text=self.text, active=self.active, ) @expose_command() def exec_general(self, prompt, object_name, cmd_name, selector=None, completer=None): """ Execute a cmd of any object. For example layout, group, window, widget , etc with a string that is obtained from start_input. Parameters ========== prompt : Text displayed at the prompt. object_name : Name of a object in Qtile. This string has to be 'layout', 'widget', 'bar', 'window' or 'screen'. cmd_name : Execution command of selected object using object_name and selector. selector : This value select a specific object within a object list that is obtained by object_name. If this value is None, current object is selected. e.g. current layout, current window and current screen. completer: Completer to use. config example: Key([alt, 'shift'], 'a', lazy.widget['prompt'].exec_general( 'section(add)', 'layout', 'add_section')) """ try: obj = self.qtile.select([(object_name, selector)]) except SelectError: logger.warning("cannot select a object") return cmd = obj.command(cmd_name) if not cmd: logger.warning("command not found") return def f(args): if args: cmd(args) self.start_input(prompt, f, completer) def _dedup_history(self): """Filter the history deque, clearing all duplicate values.""" self.history = {x: self._dedup_deque(self.history[x]) for x in self.completers} def _dedup_deque(self, dq): return deque(_LastUpdatedOrdereddict.fromkeys(dq)) class _LastUpdatedOrdereddict(dict): """Store items in the order the keys were last added.""" def __setitem__(self, key, value): if key in self: del self[key] super().__setitem__(key, value) qtile-0.31.0/libqtile/widget/volume.py0000664000175000017500000002366214762660347017620 0ustar epsilonepsilon# Copyright (c) 2010, 2012, 2014 roger # Copyright (c) 2011 Kirk Strauser # Copyright (c) 2011 Florian Mounier # Copyright (c) 2011 Mounier Florian # Copyright (c) 2011 Roger Duran # Copyright (c) 2012-2015 Tycho Andersen # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2013 Craig Barnes # Copyright (c) 2014-2015 Sean Vig # Copyright (c) 2014 Adi Sieker # Copyright (c) 2014 dmpayton # Copyright (c) 2014 Jody Frankowski # Copyright (c) 2016 Christoph Lassner # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import re import subprocess from libqtile import bar from libqtile.command.base import expose_command from libqtile.log_utils import logger from libqtile.widget import base __all__ = [ "Volume", ] re_vol = re.compile(r"(\d?\d?\d?)%") class Volume(base._TextBox): """Widget that display and change volume By default, this widget uses ``amixer`` to get and set the volume so users will need to make sure this is installed. Alternatively, users may set the relevant parameters for the widget to use a different application. If theme_path is set it draw widget as icons. """ orientations = base.ORIENTATION_HORIZONTAL defaults = [ ("cardid", None, "Card Id"), ("device", "default", "Device Name"), ("channel", "Master", "Channel"), ("padding", 3, "Padding left and right. Calculated if None."), ("update_interval", 0.2, "Update time in seconds."), ("theme_path", None, "Path of the icons"), ( "emoji", False, "Use emoji to display volume states, only if ``theme_path`` is not set." "The specified font needs to contain the correct unicode characters.", ), ( "emoji_list", ["\U0001f507", "\U0001f508", "\U0001f509", "\U0001f50a"], "List of emojis/font-symbols to display volume states, only if ``emoji`` is set." " List contains 4 symbols, from lowest volume to highest.", ), ("mute_command", None, "Mute command"), ("mute_foreground", None, "Foreground color for mute volume."), ("mute_format", "M", "Format to display when volume is muted."), ("unmute_format", "{volume}%", "Format of text to display when volume is not muted."), ("volume_app", None, "App to control volume"), ("volume_up_command", None, "Volume up command"), ("volume_down_command", None, "Volume down command"), ( "get_volume_command", None, "Command to get the current volume. " "The expected output should include 1-3 numbers and a ``%`` sign.", ), ("check_mute_command", None, "Command to check mute status"), ( "check_mute_string", "[off]", "String expected from check_mute_command when volume is muted." "When the output of the command matches this string, the" "audio source is treated as muted.", ), ( "step", 2, "Volume change for up an down commands in percentage." "Only used if ``volume_up_command`` and ``volume_down_command`` are not set.", ), ] def __init__(self, **config): base._TextBox.__init__(self, "0", **config) self.add_defaults(Volume.defaults) self.surfaces = {} self.volume = None self.is_mute = False self.add_callbacks( { "Button1": self.mute, "Button3": self.run_app, "Button4": self.increase_vol, "Button5": self.decrease_vol, } ) def _configure(self, qtile, parent_bar): if self.theme_path: self.length_type = bar.STATIC self.length = 0 base._TextBox._configure(self, qtile, parent_bar) self.unmute_foreground = self.foreground def timer_setup(self): self.timeout_add(self.update_interval, self.update) if self.theme_path: self.setup_images() def create_amixer_command(self, *args): cmd = ["amixer"] if self.cardid is not None: cmd.extend(["-c", str(self.cardid)]) if self.device is not None: cmd.extend(["-D", str(self.device)]) cmd.extend([x for x in args]) return subprocess.list2cmdline(cmd) def button_press(self, x, y, button): base._TextBox.button_press(self, x, y, button) self.draw() def update(self): vol, muted = self.get_volume() if vol != self.volume or muted != self.is_mute: self.volume = vol self.is_mute = muted # Update the underlying canvas size before actually attempting # to figure out how big it is and draw it. self._update_drawer() self.bar.draw() self.timeout_add(self.update_interval, self.update) def _update_drawer(self): if self.mute_foreground is not None: self.foreground = self.mute_foreground if self.is_mute else self.unmute_foreground if self.theme_path: self.drawer.clear(self.background or self.bar.background) if self.volume <= 0 or self.is_mute: img_name = "audio-volume-muted" elif self.volume <= 30: img_name = "audio-volume-low" elif self.volume < 80: img_name = "audio-volume-medium" else: # self.volume >= 80: img_name = "audio-volume-high" self.drawer.ctx.set_source(self.surfaces[img_name]) self.drawer.ctx.paint() elif self.emoji: if len(self.emoji_list) < 4: self.emoji_list = ["\U0001f507", "\U0001f508", "\U0001f509", "\U0001f50a"] logger.warning( "Emoji list given has less than 4 items. Falling back to default emojis." ) if self.volume <= 0 or self.is_mute: self.text = self.emoji_list[0] elif self.volume <= 30: self.text = self.emoji_list[1] elif self.volume < 80: self.text = self.emoji_list[2] elif self.volume >= 80: self.text = self.emoji_list[3] else: self.text = ( self.mute_format if self.is_mute or self.volume < 0 else self.unmute_format ).format(volume=self.volume) def setup_images(self): from libqtile import images names = ( "audio-volume-high", "audio-volume-low", "audio-volume-medium", "audio-volume-muted", ) d_images = images.Loader(self.theme_path)(*names) for name, img in d_images.items(): new_height = self.bar.height - 1 img.resize(height=new_height) if img.width > self.length: self.length = img.width + self.actual_padding * 2 self.surfaces[name] = img.pattern def get_volume(self): try: if self.get_volume_command is not None: get_volume_cmd = self.get_volume_command else: get_volume_cmd = self.create_amixer_command("sget", self.channel) mixer_out = subprocess.getoutput(get_volume_cmd) except subprocess.CalledProcessError: return -1, False check_mute = mixer_out if self.check_mute_command: check_mute = subprocess.getoutput(self.check_mute_command) muted = self.check_mute_string in check_mute volgroups = re_vol.search(mixer_out) if volgroups: return int(volgroups.groups()[0]), muted else: # this shouldn't happen return -1, muted def draw(self): if self.theme_path: self.drawer.draw(offsetx=self.offset, offsety=self.offsety, width=self.length) else: base._TextBox.draw(self) @expose_command() def increase_vol(self): if self.volume_up_command is not None: volume_up_cmd = self.volume_up_command else: volume_up_cmd = self.create_amixer_command( "-q", "sset", self.channel, f"{self.step}%+" ) subprocess.call(volume_up_cmd, shell=True) @expose_command() def decrease_vol(self): if self.volume_down_command is not None: volume_down_cmd = self.volume_down_command else: volume_down_cmd = self.create_amixer_command( "-q", "sset", self.channel, f"{self.step}%-" ) subprocess.call(volume_down_cmd, shell=True) @expose_command() def mute(self): if self.mute_command is not None: mute_cmd = self.mute_command else: mute_cmd = self.create_amixer_command("-q", "sset", self.channel, "toggle") subprocess.call(mute_cmd, shell=True) @expose_command() def run_app(self): if self.volume_app is not None: subprocess.Popen(self.volume_app, shell=True) qtile-0.31.0/libqtile/widget/vertical_clock.py0000664000175000017500000001246514762660347021274 0ustar epsilonepsilon# Copyright (c) 2024 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from datetime import datetime, timezone from libqtile.confreader import ConfigError from libqtile.widget import base from libqtile.widget.clock import Clock class VerticalClock(Clock): """ A simple but flexible text-based clock for vertical bars. Unlike the ``Clock`` widget, ``VerticalClock`` will display text horizontally in the bar. """ orientations = base.ORIENTATION_VERTICAL defaults = [ ( "format", ["%H", "%M"], "A list of Python datetime format string. Each string is printed as a separate line.", ), ( "foreground", "fff", "Text colour. A single string will be applied to all fields. " "Alternatively, users can provide a list of strings with each colour being applied to the corresponding text format.", ), ( "fontsize", None, "Font size. A single value will be applied to all fields. " "Alternatively, users can provide a list of sizes with each size being applied to the corresponding text format.", ), ] def __init__(self, **config): Clock.__init__(self, **config) self.add_defaults(VerticalClock.defaults) self.layouts = [] def _to_list(self, value): return [value] * len(self.format) def _configure(self, qtile, bar): base._Widget._configure(self, qtile, bar) if self.fontsize is None: self.fontsize = self._to_list(self.bar.width - self.bar.width / 5) elif isinstance(self.fontsize, int): self.fontsize = self._to_list(self.fontsize) elif not isinstance(self.fontsize, list): raise ConfigError("fontsize should be an integer or a list of integers.") if isinstance(self.foreground, str): self.foreground = self._to_list(self.foreground) elif not isinstance(self.fontsize, list): raise ConfigError("foreground should be a string or a list of strings.") if len(self.fontsize) != len(self.format): raise ConfigError("'fontsize' list should have same number of items as 'format'.") if len(self.foreground) != len(self.format): raise ConfigError("'foreground' list should have same number of items as 'format'.") self.layouts = [ self.drawer.textlayout( self.formatted_text, fg, self.font, size, self.fontshadow, markup=self.markup, ) for _, fg, size in zip(self.format, self.foreground, self.fontsize) ] def calculate_length(self): return sum(l.height + self.actual_padding for l in self.layouts) + self.actual_padding def update(self, time): for layout, fmt in zip(self.layouts, self.format): layout.text = time.strftime(fmt) layout.width = self.bar.width self.draw() @property def can_draw(self): can_draw = ( all(layout is not None and not layout.finalized() for layout in self.layouts) and self.offsetx is not None ) # if the bar hasn't placed us yet return can_draw # adding .5 to get a proper seconds value because glib could # theoreticaly call our method too early and we could get something # like (x-1).999 instead of x.000 def poll(self): if self.timezone: now = datetime.now(timezone.utc).astimezone(self.timezone) else: now = datetime.now(timezone.utc).astimezone() return now + self.DELTA def draw(self): if not self.can_draw: return offset = self.actual_padding self.drawer.clear(self.background or self.bar.background) for layout in self.layouts: self.drawer.ctx.save() self.drawer.ctx.translate(0, offset) layout.draw(0, 0) offset += layout.height + self.actual_padding self.drawer.ctx.restore() self.drawer.draw( offsetx=self.offsetx, offsety=self.offsety, width=self.width, height=self.height ) def finalize(self): for layout in self.layouts: layout.finalize() base._Widget.finalize(self) qtile-0.31.0/libqtile/widget/nvidia_sensors.py0000664000175000017500000000470414762660347021333 0ustar epsilonepsilonimport csv import re from libqtile.widget import base sensors_mapping = { "fan_speed": "fan.speed", "perf": "pstate", "temp": "temperature.gpu", } def _all_sensors_names_correct(sensors): return all(map(lambda x: x in sensors_mapping, sensors)) class NvidiaSensors(base.ThreadPoolText): """Displays temperature, fan speed and performance level Nvidia GPU.""" defaults = [ ( "format", "{temp}°C", "Display string format. Three options available: " "``{temp}`` - temperature, ``{fan_speed}`` and ``{perf}`` - " "performance level", ), ("foreground_alert", "ff0000", "Foreground colour alert"), ( "gpu_bus_id", "", "GPU's Bus ID, ex: ``01:00.0``. If leave empty will display all " "available GPU's", ), ("update_interval", 2, "Update interval in seconds."), ( "threshold", 70, "If the current temperature value is above, " "then change to foreground_alert colour", ), ] def __init__(self, **config): base.ThreadPoolText.__init__(self, "", **config) self.add_defaults(NvidiaSensors.defaults) self.foreground_normal = self.foreground def _get_sensors_data(self, command): return csv.reader( self.call_process(command, shell=True).strip().replace(" ", "").split("\n") ) def _parse_format_string(self): return {sensor for sensor in re.findall("{(.+?)}", self.format)} def poll(self): sensors = self._parse_format_string() if not _all_sensors_names_correct(sensors): return "Wrong sensor name" bus_id = f"-i {self.gpu_bus_id}" if self.gpu_bus_id else "" command = "nvidia-smi {} --query-gpu={} --format=csv,noheader".format( bus_id, ",".join(sensors_mapping[sensor] for sensor in sensors) ) try: sensors_data = [dict(zip(sensors, gpu)) for gpu in self._get_sensors_data(command)] for gpu in sensors_data: if gpu.get("temp"): if int(gpu["temp"]) > self.threshold: self.foreground = self.foreground_alert else: self.foreground = self.foreground_normal return " - ".join([self.format.format(**gpu) for gpu in sensors_data]) except Exception: return None qtile-0.31.0/libqtile/widget/swaync.py0000664000175000017500000001207014762660347017604 0ustar epsilonepsilon# Copyright (c) 2024 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import asyncio import json import shutil from libqtile.command.base import expose_command from libqtile.log_utils import logger from libqtile.utils import create_task from libqtile.widget.base import _TextBox class SwayNCReader: """ A class to subscribe and listen to the output of the sway notification centre client. Clients can subscribe to the reader and will receive a parsed JSON object whenever a new message is received. """ def __init__(self): self._swaync = None self._finalized = False self._process = None self.callbacks = [] self.cmd = None def set_path(self, path): if self._swaync is not None and path != self._swaync: logger.warning("A client is trying to set a different path to swaync. Ignoring.") return self._swaync = path self.cmd = f"{self._swaync} -swb" def subscribe(self, callback): needs_starting = not self.callbacks if callback not in self.callbacks: self.callbacks.append(callback) if needs_starting: create_task(self.run()) def unsubscribe(self, callback): if callback in self.callbacks: self.callbacks.remove(callback) if not self.callbacks and self._process is not None: self.stop() async def run(self): if self.cmd is None: return self._process = await asyncio.create_subprocess_shell( self.cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.DEVNULL, ) while not self._finalized: out = await self._process.stdout.readline() # process has exited so clear text and exit loop if not out: self.update("") self._process = None break try: message = json.loads(out.decode().strip()) self.update(message) except Exception: pass def stop(self): if self._process is None: return self._process.terminate() self._process = None def update(self, msg): for callback in self.callbacks: callback(msg) # Create a single instance of the reader. reader = SwayNCReader() class SwayNC(_TextBox): """ A simple widget for the Sway Notification Center. The widget can display the number of notifications as well as the do not disturb status. Left-clicking on the widget will toggle the panel. Right-clicking will toggle the do not disturb status. """ supported_backends = {"wayland"} defaults = [ ("swaync_client", shutil.which("swaync-client"), "Command to execute."), ( "dnd_status_text", ("DND ", ""), "Text to show do-not-disturb status. Tuple of text ('on', 'off').", ), ("format", "{dnd}{num}", "Text to display."), ] def __init__(self, **config): _TextBox.__init__(self, "", **config) self.add_defaults(SwayNC.defaults) self.add_callbacks({"Button1": self.toggle_panel, "Button3": self.toggle_dnd}) def _configure(self, qtile, bar): _TextBox._configure(self, qtile, bar) reader.set_path(self.swaync_client) reader.subscribe(self._message_handler) def _message_handler(self, msg): dnd = self.dnd_status_text[0 if "dnd" in msg.get("class", "") else 1] num = msg.get("text", "0") self.update(self.format.format(dnd=dnd, num=num)) @expose_command def toggle_panel(self): """Show swaync client panel.""" if self.swaync_client is not None: self.qtile.spawn(f"{self.swaync_client} -t -sw") @expose_command def toggle_dnd(self): """Toggle do not disturb status.""" if self.swaync_client is not None: self.qtile.spawn(f"{self.swaync_client} -d -sw") def finalize(self): reader.unsubscribe(self._message_handler) _TextBox.finalize(self) qtile-0.31.0/libqtile/widget/statusnotifier.py0000664000175000017500000001276014762660347021371 0ustar epsilonepsilon# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from typing import TYPE_CHECKING from libqtile import bar from libqtile.widget import base from libqtile.widget.helpers.status_notifier import has_xdg, host if TYPE_CHECKING: from libqtile.widget.helpers.status_notifier import StatusNotifierItem class StatusNotifier(base._Widget): """ A 'system tray' widget using the freedesktop StatusNotifierItem specification. As per the specification, app icons are first retrieved from the user's current theme. If this is not available then the app may provide its own icon. In order to use this functionality, users are recommended to install the `pyxdg `__ module to support retrieving icons from the selected theme. If the icon specified by StatusNotifierItem can not be found in the user's current theme and no other icons are provided by the app, a fallback icon is used. Left-clicking an icon will trigger an activate event. .. note:: Context menus are not currently supported by the official widget. However, a modded version of the widget which provides basic menu support is available from elParaguayo's `qtile-extras `_ repo. """ orientations = base.ORIENTATION_BOTH defaults = [ ("icon_size", 16, "Icon width"), ("icon_theme", None, "Name of theme to use for app icons"), ("padding", 3, "Padding between icons"), ] def __init__(self, **config): base._Widget.__init__(self, bar.CALCULATED, **config) self.add_defaults(StatusNotifier.defaults) self.add_callbacks( { "Button1": self.activate, } ) self.selected_item: StatusNotifierItem | None = None @property def available_icons(self): return [item for item in host.items if item.has_icons] def calculate_length(self): if not host.items: return 0 return len(self.available_icons) * (self.icon_size + self.padding) + self.padding def _configure(self, qtile, bar): if has_xdg and self.icon_theme: host.icon_theme = self.icon_theme # This is called last as it starts timers including _config_async. base._Widget._configure(self, qtile, bar) def draw_callback(self, x=None): self.bar.draw() async def _config_async(self): await host.start( on_item_added=self.draw_callback, on_item_removed=self.draw_callback, on_icon_changed=self.draw_callback, ) def find_icon_at_pos(self, x, y): """returns StatusNotifierItem object for icon in given position""" offset = self.padding val = x if self.bar.horizontal else y if val < offset: return None for icon in self.available_icons: offset += self.icon_size if val < offset: return icon offset += self.padding return None def button_press(self, x, y, button): icon = self.find_icon_at_pos(x, y) self.selected_item = icon if icon else None name = f"Button{button}" if name in self.mouse_callbacks: self.mouse_callbacks[name]() def _draw_icon(self, icon, x, y): self.drawer.ctx.set_source_surface(icon, x, y) self.drawer.ctx.paint() def draw(self): self.drawer.clear(self.background or self.bar.background) xoffset = self.padding if self.bar.horizontal else (self.bar.width - self.icon_size) // 2 yoffset = (self.bar.height - self.icon_size) // 2 if self.bar.horizontal else self.padding for item in self.available_icons: icon = item.get_icon(self.icon_size) self._draw_icon(icon, xoffset, yoffset) if self.bar.horizontal: xoffset += self.icon_size + self.padding else: yoffset += self.icon_size + self.padding self.drawer.draw(offsetx=self.offsetx, offsety=self.offsety, width=self.length) def activate(self): """Primary action when clicking on an icon""" if not self.selected_item: return self.selected_item.activate() def finalize(self): host.unregister_callbacks( on_item_added=self.draw_callback, on_item_removed=self.draw_callback, on_icon_changed=self.draw_callback, ) base._Widget.finalize(self) qtile-0.31.0/libqtile/widget/wlan.py0000664000175000017500000001042714762660347017245 0ustar epsilonepsilon# Copyright (c) 2012 Sebastian Bechtel # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014 Sebastian Kricner # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Tycho Andersen # Copyright (c) 2014 Craig Barnes # Copyright (c) 2015 farebord # Copyright (c) 2015 Jörg Thalheim (Mic92) # Copyright (c) 2016 Juhani Imberg # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import iwlib from libqtile.log_utils import logger from libqtile.pangocffi import markup_escape_text from libqtile.widget import base def get_status(interface_name): interface = iwlib.get_iwconfig(interface_name) if "stats" not in interface: return None, None quality = interface["stats"]["quality"] essid = bytes(interface["ESSID"]).decode() return essid, quality class Wlan(base.InLoopPollText): """ Displays Wifi SSID and quality. Widget requirements: iwlib_. .. _iwlib: https://pypi.org/project/iwlib/ """ orientations = base.ORIENTATION_HORIZONTAL defaults = [ ("interface", "wlan0", "The interface to monitor"), ( "ethernet_interface", "eth0", "The ethernet interface to monitor, NOTE: If you do not have a wlan device in your system, ethernet functionality will not work, use the Net widget instead", ), ("update_interval", 1, "The update interval."), ("disconnected_message", "Disconnected", "String to show when the wlan is diconnected."), ("ethernet_message", "eth", "String to show when ethernet is being used"), ( "use_ethernet", False, "Activate or deactivate checking for ethernet when no wlan connection is detected", ), ( "format", "{essid} {quality}/70", 'Display format. For percents you can use "{essid} {percent:2.0%}"', ), ] def __init__(self, **config): base.InLoopPollText.__init__(self, **config) self.add_defaults(Wlan.defaults) self.ethernetInterfaceNotFound = False def poll(self): try: essid, quality = get_status(self.interface) disconnected = essid is None if disconnected: if self.use_ethernet: try: with open( f"/sys/class/net/{self.ethernet_interface}/operstate" ) as statfile: if statfile.read().strip() == "up": return self.ethernet_message else: return self.disconnected_message except FileNotFoundError: if not self.ethernetInterfaceNotFound: logger.error("Ethernet interface has not been found!") self.ethernetInterfaceNotFound = True return self.disconnected_message else: return self.disconnected_message return self.format.format( essid=markup_escape_text(essid), quality=quality, percent=(quality / 70) ) except OSError: logger.error( "Probably your wlan device is switched off or " " otherwise not present in your system." ) qtile-0.31.0/libqtile/widget/helpers/0000775000175000017500000000000014762660347017370 5ustar epsilonepsilonqtile-0.31.0/libqtile/widget/helpers/status_notifier/0000775000175000017500000000000014762660347022612 5ustar epsilonepsilonqtile-0.31.0/libqtile/widget/helpers/status_notifier/statusnotifier.py0000664000175000017500000006472614762660347026266 0ustar epsilonepsilon# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from asyncio import current_task from collections.abc import Callable from contextlib import suppress from functools import partial from pathlib import Path # dbus_fast is incompatible with deferred type evaluation import cairocffi from dbus_fast import InterfaceNotFoundError, InvalidBusNameError, InvalidObjectPathError from dbus_fast.aio import MessageBus from dbus_fast.constants import PropertyAccess from dbus_fast.errors import DBusError from dbus_fast.service import ServiceInterface, dbus_property, method, signal try: from xdg.IconTheme import getIconPath has_xdg = True except ImportError: has_xdg = False from libqtile.images import Img from libqtile.log_utils import logger from libqtile.utils import add_signal_receiver, create_task ICON_FORMATS = [".png", ".svg"] # StatusNotifier seems to have two potential interface names. # While KDE appears to be the default, we should also listen # for items on freedesktop. BUS_NAMES = ["org.kde.StatusNotifierWatcher", "org.freedesktop.StatusNotifierWatcher"] ITEM_INTERFACES = ["org.kde.StatusNotifierItem", "org.freedesktop.StatusNotifierItem"] STATUSNOTIFIER_PATH = "/StatusNotifierItem" PROTOCOL_VERSION = 0 STATUS_NOTIFIER_ITEM_SPEC = """ """ class StatusNotifierItem: # noqa: E303 """ Class object which represents an StatusNotiferItem object. The item is responsible for interacting with the application. """ icon_map = { "Icon": ("_icon", "get_icon_pixmap"), "Attention": ("_attention_icon", "get_attention_icon_pixmap"), "Overlay": ("_overlay_icon", "get_overlay_icon_pixmap"), } def __init__(self, bus, service, path=None, icon_theme=None): self.bus = bus self.service = service self.surfaces = {} self._pixmaps = {} self._icon = None self._overlay_icon = None self._attention_icon = None self.on_icon_changed = None self.icon_theme = icon_theme self.icon = None self.path = path if path else STATUSNOTIFIER_PATH self.last_icon_name = None def __eq__(self, other): # Convenience method to find Item in list by service path if isinstance(other, StatusNotifierItem): return other.service == self.service elif isinstance(other, str): return other == self.service else: return False async def start(self): # Create a proxy object connecting for the item. # Some apps provide the incorrect path to the StatusNotifier object # We can try falling back to the default if that fails. # See: https://github.com/qtile/qtile/issues/3418 # Note: this loop will run a maximum of two times and returns False # if the no object is available. found_path = False while not found_path: try: introspection = await self.bus.introspect(self.service, self.path) found_path = True except InvalidBusNameError: # This is probably an Ayatana indicator which doesn't provide the service name. # We'll pick it up via the message handler so we can ignore this. return False except InvalidObjectPathError: logger.info("Cannot find %s path on %s.", self.path, self.service) if self.path == STATUSNOTIFIER_PATH: return False # Try the default ('/StatusNotifierItem') self.path = STATUSNOTIFIER_PATH try: obj = self.bus.get_proxy_object(self.service, self.path, introspection) except InvalidBusNameError: return False # Try to connect to the bus object and verify there's a valid # interface available # TODO: This may not ever fail given we've specified the underying # schema so dbus-fast has not attempted any introspection. interface_found = False for interface in ITEM_INTERFACES: try: self.item = obj.get_interface(interface) interface_found = True break except InterfaceNotFoundError: continue if not interface_found: logger.info( "Unable to find StatusNotifierItem interface on %s. Falling back to default spec.", self.service, ) try: obj = self.bus.get_proxy_object( self.service, STATUSNOTIFIER_PATH, STATUS_NOTIFIER_ITEM_SPEC ) self.item = obj.get_interface("org.kde.StatusNotifierItem") except InterfaceNotFoundError: logger.warning( "Failed to find StatusNotifierItem interface on %s and fallback to default spec also failed.", self.service, ) return False # Trying to get the local icon (first without fallback because there might be application-provided icons) await self._get_local_icon(fallback=False) # If there's no XDG icon, try to use icon provided by application if self.icon: self.item.on_new_icon(self._update_local_icon) else: # Get initial application icons: for icon in ["Icon", "Attention", "Overlay"]: await self._get_icon(icon) if self.has_icons: # Attach listeners for when the icon is updated self.item.on_new_icon(self._new_icon) self.item.on_new_attention_icon(self._new_attention_icon) self.item.on_new_overlay_icon(self._new_overlay_icon) if not self.has_icons: logger.warning( "Cannot find icon in current theme and no icon provided by StatusNotifierItem." ) # No "local" icon and no application-provided icons are available. # The "local" icon may be updated at a later time, so "_update_local_icon" # gets registered for "on_new_icon" with the option to fall back to # a default icon. self.item.on_new_icon(self._update_local_icon) await self._get_local_icon() return True async def _get_local_icon(self, fallback=True): # Default to XDG icon # Some implementations don't provide an IconName property so we # need to catch an error if we can't read it. # We can't use hasattr to check this as the method will be created # where we've used the default XML spec to provide the object introspection icon_name = "" try: icon_name = await self.item.get_icon_name() except DBusError: return # We only need to do these searches if there's an icon name provided by # the app. We also don't want to do the recursive lookup with an empty # icon name as, otherwise, the glob will match things that are not images. if icon_name: if icon_name == self.last_icon_name: current_task().remove_done_callback(self._redraw) return self.last_icon_name = icon_name self.icon = None try: icon_path = await self.item.get_icon_theme_path() except (AttributeError, DBusError): icon_path = None icon = None if icon_path: icon = self._get_custom_icon(icon_name, Path(icon_path)) if icon: self.icon = icon else: self.icon = self._get_xdg_icon(icon_name) else: self.icon = None if fallback: for icon in ["Icon", "Attention", "Overlay"]: await self._get_icon(icon) if not self.has_icons and fallback: # Use fallback icon libqtile/resources/status_notifier/fallback_icon.png logger.warning("Could not find icon for '%s'. Using fallback icon.", icon_name) path = Path(__file__).parent / "fallback_icon.png" self.icon = Img.from_path(path.resolve().as_posix()) def _create_task_and_draw(self, coro): task = create_task(coro) task.add_done_callback(self._redraw) def _update_local_icon(self): self._create_task_and_draw(self._get_local_icon()) def _new_icon(self): self._create_task_and_draw(self._get_icon("Icon")) def _new_attention_icon(self): self._create_task_and_draw(self._get_icon("Attention")) def _new_overlay_icon(self): self._create_task_and_draw(self._get_icon("Overlay")) def _get_custom_icon(self, icon_name, icon_path): icon = None for ext in ICON_FORMATS: path = icon_path / f"{icon_name}{ext}" if path.is_file(): icon = path break else: # No icon found at the image path, let's search recursively glob = icon_path.rglob(f"{icon_name}.*") found = [ icon for icon in glob if icon.is_file() and icon.suffix.lower() in ICON_FORMATS ] # Found a matching icon in subfolder if found: # We'd prefer an svg file svg = [icon for icon in found if icon.suffix.lower() == ".svg"] if svg: icon = svg[0] else: # If not, we'll take what there is # NOTE: not clear how we can handle multiple matches with different icon sizes 16x16, 32x32 etc icon = found[0] if icon is not None: return Img.from_path(icon.resolve().as_posix()) return None def _get_xdg_icon(self, icon_name): if not has_xdg: return path = getIconPath(icon_name, theme=self.icon_theme, extensions=["png", "svg"]) if not path: return None return Img.from_path(path) async def _get_icon(self, icon_name): """ Requests the pixmap for the given `icon_name` and adds to an internal dictionary for later retrieval. """ attr, method = self.icon_map[icon_name] pixmap = getattr(self.item, method, None) if pixmap is None: return icon_pixmap = await pixmap() # Items can present multiple pixmaps for different # size of icons. We want to keep these so we can pick # the best size when redering the icon later. # Also, the bytes sent for the pixmap are big-endian # but Cairo expects little-endian so we need to # reorder them. self._pixmaps[icon_name] = { size: self._reorder_bytes(icon_bytes) for size, _, icon_bytes in icon_pixmap } def _reorder_bytes(self, icon_bytes): """ Method loops over the array and reverses every 4 bytes (representing one RGBA pixel). """ arr = bytearray(icon_bytes) for i in range(0, len(arr), 4): arr[i : i + 4] = arr[i : i + 4][::-1] return arr def _redraw(self, result): """Method to invalidate icon cache and redraw icons.""" self._invalidate_icons() if self.on_icon_changed is not None: self.on_icon_changed(self) def _invalidate_icons(self): self.surfaces = {} def _get_sizes(self): """Returns list of available icon sizes.""" if not self._pixmaps.get("Icon", False): return [] return sorted([size for size in self._pixmaps["Icon"]]) def _get_surfaces(self, size): """ Creates a Cairo ImageSurface for each available icon for the given size. """ raw_surfaces = {} for icon in self._pixmaps: if size in self._pixmaps[icon]: srf = cairocffi.ImageSurface.create_for_data( self._pixmaps[icon][size], cairocffi.FORMAT_ARGB32, size, size ) raw_surfaces[icon] = srf return raw_surfaces def get_icon(self, size): """ Returns a cairo ImageSurface for the selected `size`. Will pick the appropriate icon and add any overlay as required. """ # Use existing icon if generated previously if size in self.surfaces: return self.surfaces[size] # Create a blank ImageSurface to hold the icon icon = cairocffi.ImageSurface(cairocffi.FORMAT_ARGB32, size, size) if self.icon: base_icon = self.icon.surface icon_size = base_icon.get_width() overlay = None else: # Find best matching icon size: # We get all available sizes and filter this list so it only shows # the icon sizes bigger than the requested size (we prefer to # shrink icons rather than scale them up) all_sizes = self._get_sizes() sizes = [s for s in all_sizes if s >= size] # TODO: This is messy. Shouldn't return blank icon # If there are no sizes at all (i.e. no icon) then we return empty # icon if not all_sizes: return icon # Choose the first available size. If there are none (i.e. we # request icon size bigger than the largest provided by the app), # we just take the largest icon icon_size = sizes[0] if sizes else all_sizes[-1] srfs = self._get_surfaces(icon_size) # TODO: This shouldn't happen... if not srfs: return icon # TODO: Check spec for when to use "attention" base_icon = srfs.get("Attention", srfs["Icon"]) overlay = srfs.get("Overlay", None) with cairocffi.Context(icon) as ctx: scale = size / icon_size ctx.scale(scale, scale) ctx.set_source_surface(base_icon) ctx.paint() if overlay: ctx.set_source_surface(overlay) ctx.paint() # Store the surface for next time self.surfaces[size] = icon return icon def activate(self): if hasattr(self.item, "call_activate"): create_task(self._activate()) async def _activate(self): # Call Activate method and pass window position hints await self.item.call_activate(0, 0) @property def has_icons(self): return any(bool(icon) for icon in self._pixmaps.values()) or self.icon is not None class StatusNotifierWatcher(ServiceInterface): # noqa: E303 """ DBus service that creates a StatusNotifierWatcher interface on the bus and listens for applications wanting to register items. """ def __init__(self, service: str): super().__init__(service) self._items: list[str] = [] self._hosts: list[str] = [] self.service = service self.on_item_added: Callable | None = None self.on_host_added: Callable | None = None self.on_item_removed: Callable | None = None self.on_host_removed: Callable | None = None async def start(self): # Set up and register the service on ths bus self.bus = await MessageBus().connect() self.bus.add_message_handler(self._message_handler) self.bus.export("/StatusNotifierWatcher", self) await self.bus.request_name(self.service) # We need to listen for interfaces being removed from # the bus so we can remove icons when the application # is closed. await self._setup_listeners() def _message_handler(self, message): """ Low level method to check incoming messages. Ayatana indicators seem to register themselves by passing their object path rather than the service providing that object. We therefore need to identify the sender of the message in order to register the service. Returning False so senders receieve a reply (returning True prevents reply being sent) """ if message.member != "RegisterStatusNotifierItem": return False # If the argument is not an object path (starting with "/") then we assume # it is the bus name and we don't need to do anything else. if not message.body[0].startswith("/"): return False if message.sender not in self._items: self._items.append(message.sender) if self.on_item_added is not None: self.on_item_added(message.sender, message.body[0]) self.StatusNotifierItemRegistered(message.sender) return False async def _setup_listeners(self): """ Register a MatchRule to receive signals when interfaces are added and removed from the bus. """ await add_signal_receiver( self._name_owner_changed, session_bus=True, signal_name="NameOwnerChanged", dbus_interface="org.freedesktop.DBus", use_bus=self.bus, preserve=True, ) def _name_owner_changed(self, message): # We need to track when an interface has been removed from the bus # We use the NameOwnerChanged signal and check if the new owner is # empty. name, _, new_owner = message.body # Check if one of our registered items or hosts has been removed. # If so, remove from our list and emit relevant signal if new_owner == "" and name in self._items: self._items.remove(name) self.StatusNotifierItemUnregistered(name) if new_owner == "" and name in self._hosts: self._hosts.remove(name) self.StatusNotifierHostUnregistered(name) @method() def RegisterStatusNotifierItem(self, service: "s"): # type: ignore # noqa: F821, N802 if service not in self._items: self._items.append(service) if self.on_item_added is not None: self.on_item_added(service) self.StatusNotifierItemRegistered(service) @method() def RegisterStatusNotifierHost(self, service: "s"): # type: ignore # noqa: F821, N802 if service not in self._hosts: self._hosts.append(service) self.StatusNotifierHostRegistered(service) @dbus_property(access=PropertyAccess.READ) def RegisteredStatusNotifierItems(self) -> "as": # type: ignore # noqa: F722, F821, N802 return self._items @dbus_property(access=PropertyAccess.READ) def IsStatusNotifierHostRegistered(self) -> "b": # type: ignore # noqa: F821, N802 # Note: applications may not register items unless this # returns True return len(self._hosts) > 0 @dbus_property(access=PropertyAccess.READ) def ProtocolVersion(self) -> "i": # type: ignore # noqa: F821, N802 return PROTOCOL_VERSION @signal() def StatusNotifierItemRegistered(self, service) -> "s": # type: ignore # noqa: F821, N802 return service @signal() def StatusNotifierItemUnregistered(self, service) -> "s": # type: ignore # noqa: F821, N802 if self.on_item_removed is not None: self.on_item_removed(service) return service @signal() def StatusNotifierHostRegistered(self, service) -> "s": # type: ignore # noqa: F821, N802 if self.on_host_added is not None: self.on_host_added(service) return service @signal() def StatusNotifierHostUnregistered(self, service) -> "s": # type: ignore # noqa: F821, N802 if self.on_host_removed is not None: self.on_host_removed(service) return service class StatusNotifierHost: # noqa: E303 """ Host object to act as a bridge between the widget and the DBus objects. The Host collates items returned from multiple watcher interfaces and collates them into a single list for the widget to access. """ def __init__(self): self.watchers: list[StatusNotifierWatcher] = [] self.items: list[StatusNotifierItem] = [] self.name = "qtile" self.icon_theme: str = None self.started = False self._on_item_added: list[Callable] = [] self._on_item_removed: list[Callable] = [] self._on_icon_changed: list[Callable] = [] async def start( self, on_item_added: Callable | None = None, on_item_removed: Callable | None = None, on_icon_changed: Callable | None = None, ): """ Starts the host if not already started. Widgets should register their callbacks via this method. """ if on_item_added: self._on_item_added.append(on_item_added) if on_item_removed: self._on_item_removed.append(on_item_removed) if on_icon_changed: self._on_icon_changed.append(on_icon_changed) if self.started: if on_item_added: for item in self.items: on_item_added(item) else: self.bus = await MessageBus().connect() await self.bus.request_name("org.freedesktop.StatusNotifierHost-qtile") for iface in BUS_NAMES: w = StatusNotifierWatcher(iface) w.on_item_added = self.add_item w.on_item_removed = self.remove_item await w.start() # Not quite following spec here as we're not registering # the host on the bus. w.RegisterStatusNotifierHost(self.name) self.watchers.append(w) self.started = True def item_added(self, item, service, future): success = future.result() # If StatusNotifierItem object was created successfully then we # add to our list and redraw the bar if success: self.items.append(item) for callback in self._on_item_added: callback(item) # It's an invalid item so let's remove it from the watchers else: for w in self.watchers: try: w._items.remove(service) except ValueError: pass def add_item(self, service, path=None): """ Creates a StatusNotifierItem for the given service and tries to start it. """ item = StatusNotifierItem(self.bus, service, path=path, icon_theme=self.icon_theme) item.on_icon_changed = self.item_icon_changed if item not in self.items: task = create_task(item.start()) task.add_done_callback(partial(self.item_added, item, service)) def remove_item(self, interface): # Check if the interface is in out list of items and, if so, # remove it and redraw the bar if interface in self.items: self.items.remove(interface) for callback in self._on_item_removed: callback(interface) def item_icon_changed(self, item): for callback in self._on_icon_changed: callback(item) def unregister_callbacks( self, on_item_added=None, on_item_removed=None, on_icon_changed=None ): if on_item_added is not None: with suppress(ValueError): self._on_item_added.remove(on_item_added) if on_item_removed is not None: with suppress(ValueError): self._on_item_removed.remove(on_item_removed) if on_icon_changed is not None: with suppress(ValueError): self._on_icon_changed.remove(on_icon_changed) host = StatusNotifierHost() # noqa: E303 qtile-0.31.0/libqtile/widget/helpers/status_notifier/__init__.py0000664000175000017500000000231014762660347024717 0ustar epsilonepsilon# Copyright (c) 2021-4 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from libqtile.widget.helpers.status_notifier.statusnotifier import ( # noqa: F401 StatusNotifierItem, has_xdg, host, ) qtile-0.31.0/libqtile/widget/helpers/status_notifier/fallback_icon.png0000664000175000017500000000150614762660347026071 0ustar epsilonepsilonPNG  IHDR@@iqbKGD pHYs  tIME .,*~_''Yw N ;`E4EO\T{>*ޢ)VIGh?KK@((&Hk"O`phě'q`m'#䜻YyιV\kD ȹ|>55e*];!`9W(Tۭʿ#>Xk/5b  @ |;,K@ w>FIENDB`qtile-0.31.0/libqtile/widget/stock_ticker.py0000664000175000017500000000700714762660347020770 0ustar epsilonepsilon# Copyright (c) 2017 Tycho Andersen # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import locale from urllib.parse import urlencode from libqtile.log_utils import logger from libqtile.widget.generic_poll_text import GenPollUrl class StockTicker(GenPollUrl): """ A stock ticker widget, based on the alphavantage API. Users must acquire an API key from https://www.alphavantage.co/support/#api-key The widget defaults to the TIME_SERIES_INTRADAY API function (i.e. stock symbols), but arbitrary Alpha Vantage API queries can be made by passing extra arguments to the constructor. :: # Display AMZN widget.StockTicker(apikey=..., symbol="AMZN") # Display BTC widget.StockTicker( apikey=..., function="DIGITAL_CURRENCY_INTRADAY", symbol="BTC", market="USD" ) """ defaults = [ ("interval", "1min", "The default latency to query"), ("func", "TIME_SERIES_INTRADAY", "The default API function to query"), ("function", "TIME_SERIES_INTRADAY", "DEPRECATED: Use `func`."), ] def __init__(self, **config): if "function" in config: logger.warning("`function` parameter is deprecated. Please rename to `func`") config["func"] = config.pop("function") GenPollUrl.__init__(self, **config) self.add_defaults(StockTicker.defaults) self.sign = locale.localeconv()["currency_symbol"] self.query = {"interval": self.interval, "outputsize": "compact", "function": self.func} for k, v in config.items(): self.query[k] = v @property def url(self): url = "https://www.alphavantage.co/query?" + urlencode(self.query) return url def parse(self, body): last = None for k, v in body["Meta Data"].items(): # In instead of ==, because of the number prefix that is inconsistent if "Last Refreshed" in k: last = v # Unfortunately, the actual data key is not consistently named, but # since there are only two and one is "Meta Data", we can just use the # other one. other = None for k, v in body.items(): if k != "Meta Data": other = v break # The actual price is also not consistently named... price = None for k, v in other[last].items(): if "price" in k or "close" in k: price = f"{float(v):0.2f}" break return f"{self.symbol}: {self.sign}{price}" qtile-0.31.0/libqtile/widget/she.py0000664000175000017500000000413014762660347017055 0ustar epsilonepsilon# Copyright (c) 2012, 2014 Tycho Andersen # Copyright (c) 2012, 2014 Craig Barnes # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from libqtile.widget import base class She(base.InLoopPollText): """Widget to display the Super Hybrid Engine status Can display either the mode or CPU speed on eeepc computers. """ defaults = [ ("device", "/sys/devices/platform/eeepc/cpufv", "sys path to cpufv"), ("format", "speed", 'Type of info to display "speed" or "name"'), ("update_interval", 0.5, "Update Time in seconds."), ] def __init__(self, **config): base.InLoopPollText.__init__(self, **config) self.add_defaults(She.defaults) self.modes = { "0x300": {"name": "Performance", "speed": "1.6GHz"}, "0x301": {"name": "Normal", "speed": "1.2GHz"}, "0x302": {"name": "PoswerSave", "speed": "800MHz"}, } def poll(self): with open(self.device) as f: mode = f.read().strip() if mode in self.modes: return self.modes[mode][self.format] else: return mode qtile-0.31.0/libqtile/widget/systray.py0000664000175000017500000002467014762660347020027 0ustar epsilonepsilon# Copyright (c) 2010 Aldo Cortesi # Copyright (c) 2010-2011 dequis # Copyright (c) 2010, 2012 roger # Copyright (c) 2011 Mounier Florian # Copyright (c) 2011-2012, 2014 Tycho Andersen # Copyright (c) 2012 dmpayton # Copyright (c) 2012-2013 Craig Barnes # Copyright (c) 2013 hbc # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014 Sean Vig # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations import xcffib from xcffib.xproto import ClientMessageData, ClientMessageEvent, EventMask, SetMode from libqtile import bar from libqtile.backend.x11 import window from libqtile.confreader import ConfigError from libqtile.widget import base XEMBED_PROTOCOL_VERSION = 0 class Icon(window._Window): _window_mask = EventMask.StructureNotify | EventMask.PropertyChange | EventMask.Exposure def __init__(self, win, qtile, systray): window._Window.__init__(self, win, qtile) self.hidden = True self.systray = systray # win.get_name() may return None when apps provide a temporary window before the icon window # we need something in self.name in order to sort icons so we use the window's WID. self.name = win.get_name() or str(win.wid) self.update_size() self._wm_class: list[str] | None = None def __eq__(self, other): if not isinstance(other, Icon): return False return self.window.wid == other.window.wid def update_size(self): icon_size = self.systray.icon_size self.update_hints() width = self.hints.get("min_width", icon_size) height = self.hints.get("min_height", icon_size) width = max(width, icon_size) height = max(height, icon_size) if height > icon_size: width = width * icon_size // height height = icon_size self.width = width self.height = height return False def handle_PropertyNotify(self, e): # noqa: N802 name = self.qtile.core.conn.atoms.get_name(e.atom) if name == "_XEMBED_INFO": info = self.window.get_property("_XEMBED_INFO", unpack=int) if info and info[1]: self.systray.bar.draw() return False def handle_DestroyNotify(self, event): # noqa: N802 wid = event.window icon = self.qtile.windows_map.pop(wid) self.systray.tray_icons.remove(icon) self.systray.bar.draw() return False handle_UnmapNotify = handle_DestroyNotify # noqa: N815 # Mypy doesn't like the inheritance of height and width as _Widget's # properties are read only but _Window's have a getter and setter. class Systray(base._Widget, window._Window): # type: ignore[misc] """ A widget that manages system tray. Only one Systray widget is allowed. Adding additional Systray widgets will result in a ConfigError. .. note:: Icons will not render correctly where the bar/widget is drawn with a semi-transparent background. Instead, icons will be drawn with a transparent background. If using this widget it is therefore recommended to use a fully opaque background colour or a fully transparent one. """ _instances = 0 _window_mask = EventMask.StructureNotify | EventMask.Exposure orientations = base.ORIENTATION_BOTH supported_backends = {"x11"} defaults = [ ("icon_size", 20, "Icon width"), ("padding", 5, "Padding between icons"), ] def __init__(self, **config): base._Widget.__init__(self, bar.CALCULATED, **config) self.add_defaults(Systray.defaults) self.tray_icons = [] self.screen = 0 self._name = config.get("name", "systray") self._wm_class: list[str] | None = None def calculate_length(self): if self.bar.horizontal: length = sum(i.width for i in self.tray_icons) else: length = sum(i.height for i in self.tray_icons) length += self.padding * len(self.tray_icons) return length def _configure(self, qtile, bar): base._Widget._configure(self, qtile, bar) if self.configured: return if Systray._instances > 0: raise ConfigError("Only one Systray can be used.") self.conn = conn = qtile.core.conn win = conn.create_window(-1, -1, 1, 1) window._Window.__init__(self, window.XWindow(conn, win.wid), qtile) qtile.windows_map[win.wid] = self # window._Window.__init__ overwrites the widget name so we need to restore it self.name = self._name # Even when we have multiple "Screen"s, we are setting up as the system # tray on a particular X display, that is the screen we need to # reference in the atom if qtile.current_screen: self.screen = qtile.current_screen.index self.bar = bar atoms = conn.atoms # We need tray to tell icons which visual to use. # This needs to be the same as the bar/widget. # This mainly benefits transparent bars. conn.conn.core.ChangeProperty( xcffib.xproto.PropMode.Replace, win.wid, atoms["_NET_SYSTEM_TRAY_VISUAL"], xcffib.xproto.Atom.VISUALID, 32, 1, [self.drawer._visual.visual_id], ) conn.conn.core.SetSelectionOwner( win.wid, atoms[f"_NET_SYSTEM_TRAY_S{self.screen:d}"], xcffib.CurrentTime ) data = [ xcffib.CurrentTime, atoms[f"_NET_SYSTEM_TRAY_S{self.screen:d}"], win.wid, 0, 0, ] union = ClientMessageData.synthetic(data, "I" * 5) event = ClientMessageEvent.synthetic( format=32, window=qtile.core._root.wid, type=atoms["MANAGER"], data=union ) qtile.core._root.send_event(event, mask=EventMask.StructureNotify) Systray._instances += 1 def create_mirror(self): """ Systray cannot be mirrored as we do not use a Drawer object to render icons. Return new, unconfigured instance so that, when the bar tries to configure it again, a ConfigError is raised. """ return Systray() def handle_ClientMessage(self, event): # noqa: N802 atoms = self.conn.atoms opcode = event.type data = event.data.data32 message = data[1] wid = data[2] parent = self.bar.window.window if opcode == atoms["_NET_SYSTEM_TRAY_OPCODE"] and message == 0: w = window.XWindow(self.conn, wid) icon = Icon(w, self.qtile, self) if icon not in self.tray_icons: self.tray_icons.append(icon) self.tray_icons.sort(key=lambda icon: icon.name) self.qtile.windows_map[wid] = icon self.conn.conn.core.ChangeSaveSet(SetMode.Insert, wid) self.conn.conn.core.ReparentWindow(wid, parent.wid, 0, 0) self.conn.conn.flush() info = icon.window.get_property("_XEMBED_INFO", unpack=int) if not info: self.bar.draw() return False if info[1]: self.bar.draw() return False def draw(self): offset = self.padding self.drawer.clear(self.background or self.bar.background) self.drawer.draw(offsetx=self.offset, offsety=self.offsety, width=self.length) for pos, icon in enumerate(self.tray_icons): icon.window.set_attribute(backpixmap=self.drawer.pixmap) if self.bar.horizontal: xoffset = self.offsetx + offset yoffset = self.bar.height // 2 - self.icon_size // 2 + self.offsety step = icon.width else: xoffset = self.bar.width // 2 - self.icon_size // 2 + self.offsetx yoffset = self.offsety + offset step = icon.height icon.place(xoffset, yoffset, icon.width, self.icon_size, 0, None) if icon.hidden: icon.unhide() data = [ self.conn.atoms["_XEMBED_EMBEDDED_NOTIFY"], xcffib.xproto.Time.CurrentTime, 0, self.bar.window.wid, XEMBED_PROTOCOL_VERSION, ] u = xcffib.xproto.ClientMessageData.synthetic(data, "I" * 5) event = xcffib.xproto.ClientMessageEvent.synthetic( format=32, window=icon.wid, type=self.conn.atoms["_XEMBED"], data=u ) self.window.send_event(event) offset += step + self.padding def finalize(self): base._Widget.finalize(self) atoms = self.conn.atoms try: self.conn.conn.core.SetSelectionOwner( 0, atoms[f"_NET_SYSTEM_TRAY_S{self.screen:d}"], xcffib.CurrentTime, ) self.hide() root = self.qtile.core._root.wid for icon in self.tray_icons: self.conn.conn.core.ReparentWindow(icon.window.wid, root, 0, 0) self.conn.conn.flush() self.conn.conn.core.DestroyWindow(self.wid) except xcffib.ConnectionException: self.hidden = True # Usually set in self.hide() del self.qtile.windows_map[self.wid] Systray._instances -= 1 def info(self): info = window._Window.info(self) info["widget"] = base._Widget.info(self) return info qtile-0.31.0/libqtile/widget/caps_num_lock_indicator.py0000664000175000017500000000412314762660347023151 0ustar epsilonepsilon# Copyright (C) 2018 Juan Riquelme González # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import re import subprocess from libqtile.widget import base class CapsNumLockIndicator(base.ThreadPoolText): """Really simple widget to show the current Caps/Num Lock state.""" defaults = [("update_interval", 0.5, "Update Time in seconds.")] def __init__(self, **config): base.ThreadPoolText.__init__(self, "", **config) self.add_defaults(CapsNumLockIndicator.defaults) def get_indicators(self): """Return a list with the current state of the keys.""" try: output = self.call_process(["xset", "q"]) except subprocess.CalledProcessError as err: output = err.output return [] if output.startswith("Keyboard"): indicators = re.findall(r"(Caps|Num)\s+Lock:\s*(\w*)", output) return indicators def poll(self): """Poll content for the text box.""" indicators = self.get_indicators() status = " ".join([" ".join(indicator) for indicator in indicators]) return status qtile-0.31.0/libqtile/widget/notify.py0000664000175000017500000002110314762660347017605 0ustar epsilonepsilon# Copyright (c) 2011 Florian Mounier # Copyright (c) 2011 Mounier Florian # Copyright (c) 2012 roger # Copyright (c) 2012-2014 Tycho Andersen # Copyright (c) 2012-2013 Craig Barnes # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Adi Sieker # Copyright (c) 2020 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from os import path from libqtile import bar, pangocffi, utils from libqtile.command.base import expose_command from libqtile.log_utils import logger from libqtile.notify import ClosedReason, notifier from libqtile.widget import base class Notify(base._TextBox): """ A notify widget This widget can handle actions provided by notification clients. However, only the default action is supported, so if a client provides multiple actions then only the default (first) action can be invoked. Some programs will provide their own notification windows if the notification server does not support actions, so if you want your notifications to handle more than one action then specify ``False`` for the ``action`` option to disable all action handling. Unfortunately we cannot specify the capability for exactly one action. """ defaults = [ ("foreground_urgent", "ff0000", "Foreground urgent priority colour"), ("foreground_low", "dddddd", "Foreground low priority colour"), ("default_timeout_low", 5, "Default timeout (seconds) for low urgency notifications."), ("default_timeout", 10, "Default timeout (seconds) for normal notifications"), ("default_timeout_urgent", None, "Default timeout (seconds) for urgent notifications"), ("audiofile", None, "Audiofile played during notifications"), ("action", True, "Enable handling of default action upon right click"), ( "parse_text", None, "Function to parse and modify notifications. " "e.g. function in config that removes line returns:" "def my_func(text)" " return text.replace('\n', '')" "then set option parse_text=my_func", ), ("background_urgent", "440000", "Background urgent priority colour"), ("background_low", "444444", "Background low priority colour"), ] capabilities = {"body", "actions"} def __init__(self, width=bar.CALCULATED, **config): base._TextBox.__init__(self, "", width, **config) self.add_defaults(Notify.defaults) self.current_id = 0 default_callbacks = { "Button1": self.clear, "Button4": self.prev, "Button5": self.next, } if self.action: default_callbacks["Button3"] = self._invoke else: self.capabilities = Notify.capabilities.difference({"actions"}) self.add_callbacks(default_callbacks) self.background_normal = self.background def _configure(self, qtile, bar): base._TextBox._configure(self, qtile, bar) self.layout = self.drawer.textlayout( self.text, self.foreground, self.font, self.fontsize, self.fontshadow, markup=True ) if notifier is None: logger.warning("You must install dbus-fast to use the Notify widget.") # Create a tuple of our default timeouts. Urgency is an integer of 0-2 # (see https://specifications.freedesktop.org/notification-spec/notification-spec-latest.html#urgency-levels) # so they will work as the index of the tuple. self._timeouts = ( self.default_timeout_low, self.default_timeout, self.default_timeout_urgent, ) async def _config_async(self): if notifier is None: return await notifier.register(self.update, self.capabilities, on_close=self.on_close) def set_notif_text(self, notif): self.text = pangocffi.markup_escape_text(notif.summary) urgency = getattr(notif.hints.get("urgency"), "value", 1) if urgency != 1: self.text = f'{self.text}' self.background = self.background_urgent if urgency == 2 else self.background_low else: self.background = self.background_normal if notif.body: self.text = f'{self.text} - {pangocffi.markup_escape_text(notif.body)}' if callable(self.parse_text): try: self.text = self.parse_text(self.text) except: # noqa: E722 logger.exception("parse_text function failed:") if self.audiofile and path.exists(self.audiofile): self.qtile.spawn(f"aplay -q '{self.audiofile}'") def update(self, notif): self.qtile.call_soon_threadsafe(self.real_update, notif) def real_update(self, notif): self.set_notif_text(notif) self.current_id = notif.id - 1 if notif.timeout and notif.timeout > 0: self.timeout_add( notif.timeout / 1000, self.clear, method_args=(ClosedReason.expired,) ) else: urgency = getattr(notif.hints.get("urgency"), "value", 1) try: timeout = self._timeouts[urgency] except IndexError: logger.warning( "Notification had an unexpected urgency value. Treating as normal priority." ) timeout = self._timeouts[1] if timeout: self.timeout_add(timeout, self.clear, method_args=(ClosedReason.expired,)) self.bar.draw() return True @expose_command() def display(self): if notifier is None: return self.set_notif_text(notifier.notifications[self.current_id]) self.bar.draw() @expose_command() def clear(self, reason=ClosedReason.dismissed): """Clear the notification""" if notifier is None: return notifier._service.NotificationClosed(notifier.notifications[self.current_id].id, reason) self.text = "" self.background = self.background_normal self.current_id = len(notifier.notifications) - 1 self.bar.draw() def on_close(self, nid): if self.current_id < len(notifier.notifications): notif = notifier.notifications[self.current_id] if notif.id == nid: self.clear(ClosedReason.method) @expose_command() def prev(self): """Show previous notification.""" if self.current_id > 0: self.current_id -= 1 self.display() @expose_command() def next(self): if notifier is None: return """Show next notification.""" if self.current_id < len(notifier.notifications) - 1: self.current_id += 1 self.display() def _invoke(self): if self.current_id < len(notifier.notifications): notif = notifier.notifications[self.current_id] if notif.actions: notifier._service.ActionInvoked(notif.id, notif.actions[0]) self.clear() @expose_command() def toggle(self): """Toggle showing/clearing the notification""" if self.text == "": self.display() else: self.clear() @expose_command() def invoke(self): """Invoke the notification's default action""" if self.action: self._invoke() def finalize(self): if notifier is not None: notifier.unregister(self.update, on_close=self.on_close) base._TextBox.finalize(self) qtile-0.31.0/libqtile/widget/quick_exit.py0000664000175000017500000000524014762660347020446 0ustar epsilonepsilon# Copyright (c) 2019, Shunsuke Mie # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from libqtile.command.base import expose_command from libqtile.widget import base class QuickExit(base._TextBox): """ A button to shut down Qtile. When clicked, a countdown starts. Clicking the button again stops the countdown and prevents Qtile from shutting down. """ defaults = [ ("default_text", "[ shutdown ]", "The text displayed on the button."), ("countdown_format", "[ {} seconds ]", "The text displayed when counting down."), ("timer_interval", 1, "The countdown interval."), ("countdown_start", 5, "The number to count down from."), ] def __init__(self, **config): base._TextBox.__init__(self, "", **config) self.add_defaults(QuickExit.defaults) self.is_counting = False self.text = self.default_text self.countdown = self.countdown_start self.add_callbacks({"Button1": self.trigger}) def __reset(self): self.is_counting = False self.countdown = self.countdown_start self.text = self.default_text self.timer.cancel() def update(self): if not self.is_counting: return self.countdown -= 1 self.text = self.countdown_format.format(self.countdown) self.timer = self.timeout_add(self.timer_interval, self.update) self.draw() if self.countdown == 0: self.qtile.stop() return @expose_command() def trigger(self): if not self.is_counting: self.is_counting = True self.update() else: self.__reset() self.draw() qtile-0.31.0/libqtile/widget/__init__.py0000664000175000017500000000707714762660347020052 0ustar epsilonepsilon# Copyright (c) 2014 Rock Neurotiko # Copyright (c) 2014 roger # Copyright (c) 2015 David R. Andersen # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from libqtile.utils import lazify_imports from libqtile.widget.import_error import make_error widgets = { "AGroupBox": "groupbox", "Backlight": "backlight", "Battery": "battery", "BatteryIcon": "battery", "Bluetooth": "bluetooth", "CPU": "cpu", "CPUGraph": "graph", "Canto": "canto", "CapsNumLockIndicator": "caps_num_lock_indicator", "CheckUpdates": "check_updates", "Chord": "chord", "Clipboard": "clipboard", "Clock": "clock", "Cmus": "cmus", "Countdown": "countdown", "CryptoTicker": "crypto_ticker", "CurrentLayout": "currentlayout", "CurrentLayoutIcon": "currentlayout", "CurrentScreen": "currentscreen", "DF": "df", "DoNotDisturb": "do_not_disturb", "GenPollText": "generic_poll_text", "GenPollUrl": "generic_poll_text", "GenPollCommand": "generic_poll_text", "GmailChecker": "gmail_checker", "GroupBox": "groupbox", "HDD": "hdd", "HDDBusyGraph": "graph", "HDDGraph": "graph", "IdleRPG": "idlerpg", "Image": "image", "ImapWidget": "imapwidget", "KeyboardKbdd": "keyboardkbdd", "KeyboardLayout": "keyboardlayout", "KhalCalendar": "khal_calendar", "LaunchBar": "launchbar", "Load": "load", "Maildir": "maildir", "Memory": "memory", "MemoryGraph": "graph", "Mirror": "base", "Moc": "moc", "Mpd2": "mpd2widget", "Mpris2": "mpris2widget", "Net": "net", "NetGraph": "graph", "Notify": "notify", "NvidiaSensors": "nvidia_sensors", "OpenWeather": "open_weather", "Plasma": "plasma", "Pomodoro": "pomodoro", "Prompt": "prompt", "PulseVolume": "pulse_volume", "QuickExit": "quick_exit", "Redshift": "redshift", "ScreenSplit": "screensplit", "Sep": "sep", "She": "she", "Spacer": "spacer", "StatusNotifier": "statusnotifier", "StockTicker": "stock_ticker", "SwapGraph": "graph", "SwayNC": "swaync", "Systray": "systray", "TaskList": "tasklist", "TextBox": "textbox", "ThermalSensor": "sensors", "ThermalZone": "thermal_zone", "TunedManager": "tuned_manager", "VerticalClock": "vertical_clock", "Volume": "volume", "Wallpaper": "wallpaper", "WidgetBox": "widgetbox", "WindowCount": "window_count", "WindowName": "windowname", "WindowTabs": "windowtabs", "Wlan": "wlan", "Wttr": "wttr", } __all__, __dir__, __getattr__ = lazify_imports(widgets, __package__, fallback=make_error) qtile-0.31.0/libqtile/widget/tuned_manager.py0000664000175000017500000000753514762660347021123 0ustar epsilonepsilon# Copyright (c) 2025 Emma Nora Theuer # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import re import subprocess from libqtile import bar from libqtile.widget import base class TunedManager(base.ThreadPoolText): """ A widget to interact with the Tuned power management daemon. It always displays the name of the currently active profile. The user can define a list of profiles to be used, 3 default profiles exist. These 3 are the default profiles on RHEL and Fedora Linux. A left click on the widget will go to the next layout in the list, a right click will go to the previous one. If the end of the list is reached, it cycles back to the beginning. Scrolling can also be used to cycle through the list, though keep in mind that switching the profile takes a while and so scrolling through the list quickly is not feasible. """ orientations = base.ORIENTATION_HORIZONTAL defaults = [ ( "modes", ["powersave", "balanced-battery", "throughput-performance"], "The modes to cycle through", ) ] def __init__(self, **config): base.ThreadPoolText.__init__(self, "", **config) self.add_defaults(TunedManager.defaults) self.length_type = bar.CALCULATED self.regex = re.compile(r"Current active profile:\s+(\S+)") self.current_mode = self.find_mode() self.add_callbacks( { "Button1": self.next_mode, # Left Click "Button3": self.previous_mode, # Right Click "Button4": self.next_mode, # Scroll up "Button5": self.previous_mode, # Scroll down } ) def _configure(self, qtile, bar): base.ThreadPoolText._configure(self, qtile, bar) self.text = self.find_mode() def poll(self): return self.find_mode() def find_mode(self): result = subprocess.run("tuned-adm active", shell=True, capture_output=True, text=True) output = result.stdout mode = self.regex.findall(output) if not mode: return "" return mode[0] def update_bar(self): self.current_mode = self.find_mode() self.text = self.current_mode self.bar.draw() def execute_command(self, index: int): argument = self.modes[index] # pyright: ignore try: subprocess.run(["tuned-adm", "profile", argument], check=True) self.update_bar() except subprocess.CalledProcessError as e: self.update(f"Error setting mode: {e}") def _change_mode(self, step=1): next_index: int = (self.modes.index(self.current_mode) + step) % len(self.modes) # pyright: ignore self.execute_command(next_index) def next_mode(self): self._change_mode() def previous_mode(self): self._change_mode(step=-1) qtile-0.31.0/libqtile/widget/screensplit.py0000664000175000017500000000557014762660347020642 0ustar epsilonepsilon# Copyright (c) 2022 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import libqtile.layout from libqtile import hook from libqtile.lazy import lazy from libqtile.widget import TextBox class ScreenSplit(TextBox): """ A simple widget to show the name of the current split and layout for the ``ScreenSplit`` layout. """ defaults = [("format", "{split_name} ({layout})", "Format string.")] def __init__(self, **config): TextBox.__init__(self, "", **config) self.add_defaults(ScreenSplit.defaults) self.add_callbacks( { "Button4": lazy.layout.previous_split().when(layout="screensplit"), "Button5": lazy.layout.next_split().when(layout="screensplit"), } ) self._drawn = False def _configure(self, qtile, bar): TextBox._configure(self, qtile, bar) self._set_hooks() def _set_hooks(self): hook.subscribe.layout_change(self._update) def _update(self, layout, group): if not self.configured: return if isinstance(layout, libqtile.layout.ScreenSplit) and group is self.bar.screen.group: split_name = layout.active_split.name layout_name = layout.active_layout.name self.set_text(split_name, layout_name) else: self.clear() def set_text(self, split_name, layout): self.update(self.format.format(split_name=split_name, layout=layout)) def clear(self): self.update("") def finalize(self): hook.unsubscribe.layout_change(self._update) TextBox.finalize(self) def draw(self): # Force the widget to check layout the first time it's drawn if not self._drawn: self._update(self.bar.screen.group.layout, self.bar.screen.group) self._drawn = True return TextBox.draw(self) qtile-0.31.0/libqtile/widget/windowname.py0000664000175000017500000001031614762660347020451 0ustar epsilonepsilon# Copyright (c) 2008, 2010 Aldo Cortesi # Copyright (c) 2010 matt # Copyright (c) 2011 Mounier Florian # Copyright (c) 2012 Tim Neumann # Copyright (c) 2013 Craig Barnes # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Tycho Andersen # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from libqtile import bar, hook, pangocffi from libqtile.log_utils import logger from libqtile.widget import base class WindowName(base._TextBox): """Displays the name of the window that currently has focus""" defaults = [ ("for_current_screen", False, "instead of this bars screen use currently active screen"), ( "empty_group_string", " ", "string to display when no windows are focused on current group", ), ("format", "{state}{name}", "format of the text"), ( "parse_text", None, "Function to parse and modify window names. " "e.g. function in config that removes excess " "strings from window name: " "def my_func(text)" ' for string in [" - Chromium", " - Firefox"]:' ' text = text.replace(string, "")' " return text" "then set option parse_text=my_func", ), ] def __init__(self, width=bar.STRETCH, **config): base._TextBox.__init__(self, width=width, **config) self.add_defaults(WindowName.defaults) def _configure(self, qtile, bar): base._TextBox._configure(self, qtile, bar) hook.subscribe.client_name_updated(self.hook_response) hook.subscribe.focus_change(self.hook_response) hook.subscribe.float_change(self.hook_response) hook.subscribe.current_screen_change(self.hook_response_current_screen) def remove_hooks(self): hook.unsubscribe.client_name_updated(self.hook_response) hook.unsubscribe.focus_change(self.hook_response) hook.unsubscribe.float_change(self.hook_response) hook.unsubscribe.current_screen_change(self.hook_response_current_screen) def hook_response(self, *args): if self.for_current_screen: w = self.qtile.current_screen.group.current_window else: w = self.bar.screen.group.current_window state = "" if w: if w.maximized: state = "[] " elif w.minimized: state = "_ " elif w.floating: state = "V " var = {} var["state"] = state var["name"] = w.name if callable(self.parse_text): try: var["name"] = self.parse_text(var["name"]) except: # noqa: E722 logger.exception("parse_text function failed:") wm_class = w.get_wm_class() var["class"] = wm_class[0] if wm_class else "" unescaped = self.format.format(**var) else: unescaped = self.empty_group_string self.update(pangocffi.markup_escape_text(unescaped)) def hook_response_current_screen(self, *args): if self.for_current_screen: self.hook_response() def finalize(self): self.remove_hooks() base._TextBox.finalize(self) qtile-0.31.0/libqtile/widget/groupbox.py0000664000175000017500000003600114762660347020145 0ustar epsilonepsilon# Copyright (c) 2008, 2010 Aldo Cortesi # Copyright (c) 2009 Ben Duffield # Copyright (c) 2010 aldo # Copyright (c) 2010-2012 roger # Copyright (c) 2011 Florian Mounier # Copyright (c) 2011 Kenji_Takahashi # Copyright (c) 2011-2015 Tycho Andersen # Copyright (c) 2012-2013 dequis # Copyright (c) 2012 Craig Barnes # Copyright (c) 2013 xarvh # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Filipe Nepomuceno # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import itertools from functools import partial from typing import Any from libqtile import hook from libqtile.widget import base class _GroupBase(base._TextBox, base.PaddingMixin, base.MarginMixin): defaults: list[tuple[str, Any, str]] = [ ("borderwidth", 3, "Current group border width"), ("center_aligned", True, "center-aligned group box"), ("markup", False, "Whether or not to use pango markup"), ] def __init__(self, **config): base._TextBox.__init__(self, **config) self.add_defaults(_GroupBase.defaults) self.add_defaults(base.PaddingMixin.defaults) self.add_defaults(base.MarginMixin.defaults) def box_width(self, groups): width, _ = self.drawer.max_layout_size( [self.fmt.format(i.label) for i in groups], self.font, self.fontsize, self.markup ) return width + self.padding_x * 2 + self.borderwidth * 2 def _configure(self, qtile, bar): base._Widget._configure(self, qtile, bar) if self.fontsize is None: calc = self.bar.height - self.margin_y * 2 - self.borderwidth * 2 - self.padding_y * 2 self.fontsize = max(calc, 1) self.layout = self.drawer.textlayout( "", "ffffff", self.font, self.fontsize, self.fontshadow, markup=self.markup ) self.setup_hooks() def _hook_response(self, *args, **kwargs): self.bar.draw() def setup_hooks(self): hook.subscribe.client_managed(self._hook_response) hook.subscribe.client_urgent_hint_changed(self._hook_response) hook.subscribe.client_killed(self._hook_response) hook.subscribe.setgroup(self._hook_response) hook.subscribe.group_window_add(self._hook_response) hook.subscribe.current_screen_change(self._hook_response) hook.subscribe.changegroup(self._hook_response) def remove_hooks(self): hook.unsubscribe.client_managed(self._hook_response) hook.unsubscribe.client_urgent_hint_changed(self._hook_response) hook.unsubscribe.client_killed(self._hook_response) hook.unsubscribe.setgroup(self._hook_response) hook.unsubscribe.group_window_add(self._hook_response) hook.unsubscribe.current_screen_change(self._hook_response) hook.unsubscribe.changegroup(self._hook_response) def drawbox( self, offset, text, bordercolor, textcolor, highlight_color=None, width=None, rounded=False, block=False, line=False, highlighted=False, ): self.layout.text = self.fmt.format(text) self.layout.font_family = self.font self.layout.font_size = self.fontsize self.layout.colour = textcolor if width is not None: self.layout.width = width if line: pad_y = [ (self.bar.height - self.layout.height - self.borderwidth) / 2, (self.bar.height - self.layout.height + self.borderwidth) / 2, ] else: pad_y = self.padding_y if bordercolor is None: # border colour is set to None when we don't want to draw a border at all # Rather than dealing with alpha blending issues, we just set border width # to 0. border_width = 0 framecolor = self.background or self.bar.background else: border_width = self.borderwidth framecolor = bordercolor framed = self.layout.framed(border_width, framecolor, 0, pad_y, highlight_color) y = self.margin_y if self.center_aligned: for t in base.MarginMixin.defaults: if t[0] == "margin": y += (self.bar.height - framed.height) / 2 - t[1] break if block and bordercolor is not None: framed.draw_fill(offset, y, rounded) elif line: framed.draw_line(offset, y, highlighted) else: framed.draw(offset, y, rounded) def finalize(self): self.remove_hooks() base._TextBox.finalize(self) class AGroupBox(_GroupBase): """A widget that graphically displays the current group""" orientations = base.ORIENTATION_HORIZONTAL defaults = [("border", "000000", "group box border color")] def __init__(self, **config): _GroupBase.__init__(self, **config) self.add_defaults(AGroupBox.defaults) def _configure(self, qtile, bar): _GroupBase._configure(self, qtile, bar) self.add_callbacks({"Button1": partial(self.bar.screen.next_group, warp=False)}) def calculate_length(self): return self.box_width(self.qtile.groups) + self.margin_x * 2 def draw(self): self.drawer.clear(self.background or self.bar.background) e = next(i for i in self.qtile.groups if i.name == self.bar.screen.group.name) self.drawbox(self.margin_x, e.name, self.border, self.foreground) self.drawer.draw(offsetx=self.offset, offsety=self.offsety, width=self.width) class GroupBox(_GroupBase): """ A widget that graphically displays the current group. All groups are displayed by their label. If the label of a group is the empty string that group will not be displayed. """ orientations = base.ORIENTATION_HORIZONTAL defaults = [ ("block_highlight_text_color", None, "Selected group font colour"), ("active", "FFFFFF", "Active group font colour"), ("inactive", "404040", "Inactive group font colour"), ( "highlight_method", "border", "Method of highlighting ('border', 'block', 'text', or 'line')" "Uses `*_border` color settings", ), ("rounded", True, "To round or not to round box borders"), ( "this_current_screen_border", "215578", "Border or line colour for group on this screen when focused.", ), ( "this_screen_border", "215578", "Border or line colour for group on this screen when unfocused.", ), ( "other_current_screen_border", "404040", "Border or line colour for group on other screen when focused.", ), ( "other_screen_border", "404040", "Border or line colour for group on other screen when unfocused.", ), ( "highlight_color", ["000000", "282828"], "Active group highlight color when using 'line' highlight method.", ), ( "urgent_alert_method", "border", "Method for alerting you of WM urgent " "hints (one of 'border', 'text', 'block', or 'line')", ), ("urgent_text", "FF0000", "Urgent group font color"), ("urgent_border", "FF0000", "Urgent border or line color"), ("disable_drag", False, "Disable dragging and dropping of group names on widget"), ("invert_mouse_wheel", False, "Whether to invert mouse wheel group movement"), ("use_mouse_wheel", True, "Whether to use mouse wheel events"), ( "visible_groups", None, "Groups that will be visible. " "If set to None or [], all groups will be visible." "Visible groups are identified by name not by their displayed label.", ), ( "hide_unused", False, "Hide groups that have no windows and that are not displayed on any screen.", ), ("spacing", None, "Spacing between groups" "(if set to None, will be equal to margin_x)"), ("toggle", True, "Enable toggling of group when clicking on same group name"), ] def __init__(self, **config): _GroupBase.__init__(self, **config) self.add_defaults(GroupBox.defaults) self.clicked = None self.click = None default_callbacks = {"Button1": self.select_group} if self.use_mouse_wheel: default_callbacks.update( { "Button5" if self.invert_mouse_wheel else "Button4": self.prev_group, "Button4" if self.invert_mouse_wheel else "Button5": self.next_group, } ) self.add_callbacks(default_callbacks) def _configure(self, qtile, bar): _GroupBase._configure(self, qtile, bar) if self.spacing is None: self.spacing = self.margin_x @property def groups(self): """ returns list of visible groups. The existing groups are filtered by the visible_groups attribute and their label. Groups with an empty string as label are never contained. Groups that are not named in visible_groups are not returned. """ groups = filter(lambda g: g.label, self.qtile.groups) if self.hide_unused: groups = filter(lambda g: g.windows or g.screen, groups) if self.visible_groups: groups = filter(lambda g: g.name in self.visible_groups, groups) return list(groups) def get_clicked_group(self): group = None new_width = self.margin_x - self.spacing / 2.0 width = 0 for g in self.groups: new_width += self.box_width([g]) + self.spacing if width <= self.click <= new_width: group = g break width = new_width return group def button_press(self, x, y, button): self.click = x _GroupBase.button_press(self, x, y, button) def next_group(self): group = None current_group = self.qtile.current_group i = itertools.cycle(self.qtile.groups) while next(i) != current_group: pass while group is None or group not in self.groups: group = next(i) self.go_to_group(group) def prev_group(self): group = None current_group = self.qtile.current_group i = itertools.cycle(reversed(self.qtile.groups)) while next(i) != current_group: pass while group is None or group not in self.groups: group = next(i) self.go_to_group(group) def select_group(self): self.clicked = None group = self.get_clicked_group() if not self.disable_drag: self.clicked = group self.go_to_group(group) def go_to_group(self, group): if group: if self.bar.screen.group != group or not self.disable_drag or not self.toggle: self.bar.screen.set_group(group, warp=False) else: self.bar.screen.toggle_group(group, warp=False) def button_release(self, x, y, button): self.click = x if button not in (5, 4): group = self.get_clicked_group() if group and self.clicked: group.switch_groups(self.clicked.name) self.clicked = None def calculate_length(self): width = self.margin_x * 2 + (len(self.groups) - 1) * self.spacing for g in self.groups: width += self.box_width([g]) return width def group_has_urgent(self, group): return any(w.urgent for w in group.windows) def draw(self): self.drawer.clear(self.background or self.bar.background) offset = self.margin_x for i, g in enumerate(self.groups): to_highlight = False is_block = self.highlight_method == "block" is_line = self.highlight_method == "line" bw = self.box_width([g]) if self.group_has_urgent(g) and self.urgent_alert_method == "text": text_color = self.urgent_text elif g.windows: text_color = self.active else: text_color = self.inactive if g.screen: if self.highlight_method == "text": border = None text_color = self.this_current_screen_border else: if self.block_highlight_text_color: text_color = self.block_highlight_text_color if self.bar.screen.group.name == g.name: if self.qtile.current_screen == self.bar.screen: border = self.this_current_screen_border to_highlight = True else: border = self.this_screen_border else: if self.qtile.current_screen == g.screen: border = self.other_current_screen_border else: border = self.other_screen_border elif self.group_has_urgent(g) and self.urgent_alert_method in ( "border", "block", "line", ): border = self.urgent_border if self.urgent_alert_method == "block": is_block = True elif self.urgent_alert_method == "line": is_line = True else: border = None self.drawbox( offset, g.label, border, text_color, highlight_color=self.highlight_color, width=bw, rounded=self.rounded, block=is_block, line=is_line, highlighted=to_highlight, ) offset += bw + self.spacing self.drawer.draw(offsetx=self.offset, offsety=self.offsety, width=self.width) qtile-0.31.0/libqtile/widget/currentscreen.py0000664000175000017500000000426214762660347021166 0ustar epsilonepsilon# Copyright (c) 2015 Alexander Fasching # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from libqtile import bar, hook from libqtile.widget import base class CurrentScreen(base._TextBox): """Indicates whether the screen this widget is on is currently active or not""" defaults = [ ("active_text", "A", "Text displayed when the screen is active"), ("inactive_text", "I", "Text displayed when the screen is inactive"), ("active_color", "00ff00", "Color when screen is active"), ("inactive_color", "ff0000", "Color when screen is inactive"), ] def __init__(self, width=bar.CALCULATED, **config): base._TextBox.__init__(self, "", width, **config) self.add_defaults(CurrentScreen.defaults) def _configure(self, qtile, bar): base._TextBox._configure(self, qtile, bar) hook.subscribe.current_screen_change(self.update_text) self.update_text() def update_text(self): if self.qtile.current_screen == self.bar.screen: self.foreground = self.active_color self.update(self.active_text) else: self.foreground = self.inactive_color self.update(self.inactive_text) qtile-0.31.0/libqtile/widget/memory.py0000664000175000017500000000713314762660347017614 0ustar epsilonepsilon# Copyright (c) 2015 Jörg Thalheim (Mic92) # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import psutil from libqtile.widget import base __all__ = ["Memory"] class Memory(base.ThreadPoolText): """Display memory/swap usage. The following fields are available in the `format` string: - ``MemUsed``: Memory in use. - ``MemTotal``: Total amount of memory. - ``MemFree``: Amount of memory free. - ``Available``: Amount of memory available. - ``NotAvailable``: Equal to ``MemTotal`` - ``MemAvailable`` - ``MemPercent``: Memory in use as a percentage. - ``Buffers``: Buffer amount. - ``Active``: Active memory. - ``Inactive``: Inactive memory. - ``Shmem``: Shared memory. - ``SwapTotal``: Total amount of swap. - ``SwapFree``: Amount of swap free. - ``SwapUsed``: Amount of swap in use. - ``SwapPercent``: Swap in use as a percentage. - ``mm``: Measure unit for memory. - ``ms``: Measure unit for swap. Widget requirements: psutil_. .. _psutil: https://pypi.org/project/psutil/ """ defaults = [ ("format", "{MemUsed: .0f}{mm}/{MemTotal: .0f}{mm}", "Formatting for field names."), ("update_interval", 1.0, "Update interval for the Memory"), ("measure_mem", "M", "Measurement for Memory (G, M, K, B)"), ("measure_swap", "M", "Measurement for Swap (G, M, K, B)"), ] measures = {"G": 1024 * 1024 * 1024, "M": 1024 * 1024, "K": 1024, "B": 1} def __init__(self, **config): super().__init__("", **config) self.add_defaults(Memory.defaults) self.calc_mem = self.measures[self.measure_mem] self.calc_swap = self.measures[self.measure_swap] def poll(self): mem = psutil.virtual_memory() swap = psutil.swap_memory() val = {} val["MemUsed"] = mem.used / self.calc_mem val["MemTotal"] = mem.total / self.calc_mem val["MemFree"] = mem.free / self.calc_mem val["Available"] = mem.available / self.calc_mem val["NotAvailable"] = (mem.total - mem.available) / self.calc_mem val["MemPercent"] = mem.percent val["Buffers"] = mem.buffers / self.calc_mem val["Active"] = mem.active / self.calc_mem val["Inactive"] = mem.inactive / self.calc_mem val["Shmem"] = mem.shared / self.calc_mem val["SwapTotal"] = swap.total / self.calc_swap val["SwapFree"] = swap.free / self.calc_swap val["SwapUsed"] = swap.used / self.calc_swap val["SwapPercent"] = swap.percent val["mm"] = self.measure_mem val["ms"] = self.measure_swap return self.format.format(**val) qtile-0.31.0/libqtile/widget/import_error.py0000664000175000017500000000273414762660347021031 0ustar epsilonepsilon# Copyright (c) 2018 Roger Duran # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from libqtile.widget.base import _TextBox class ImportErrorWidget(_TextBox): def __init__(self, class_name, *args, **config): _TextBox.__init__(self, **config) self.text = f"Import Error: {class_name}" def make_error(module_path, class_name): def import_error_wrapper(*args, **kwargs): return ImportErrorWidget(class_name, *args, **kwargs) return import_error_wrapper qtile-0.31.0/libqtile/widget/open_weather.py0000664000175000017500000002255214762660347020766 0ustar epsilonepsilon# Copyright (c) 2020 Himanshu Chauhan # Copyright (c) 2020 Stephan Ehlers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import time from typing import Any from urllib.parse import urlencode from libqtile.widget.generic_poll_text import GenPollUrl # See documentation: https://openweathermap.org/current QUERY_URL = "https://api.openweathermap.org/data/2.5/weather?" DEFAULT_APP_ID = "7834197c2338888258f8cb94ae14ef49" class OpenWeatherResponseError(Exception): def __init__(self, resp_code, err_str=None): self.resp_code = resp_code self.err_str = err_str def flatten_json(obj): out = {} def __inner(_json, name=""): if type(_json) is dict: for key, value in _json.items(): __inner(value, name + key + "_") elif type(_json) is list: for i in range(len(_json)): __inner(_json[i], name + str(i) + "_") else: out[name[:-1]] = _json __inner(obj) return out def degrees_to_direction(degrees): val = int(degrees / 22.5 + 0.5) arr = [ "N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW", ] return arr[(val % 16)] class _OpenWeatherResponseParser: def __init__(self, response, dateformat, timeformat): self.dateformat = dateformat self.timeformat = timeformat self.data = self._parse(response) self._remap(self.data) if int(self.data["cod"]) != 200: raise OpenWeatherResponseError(int(self.data["cod"])) def _parse(self, response): return flatten_json(response) def _remap(self, data): data["location_lat"] = data.get("coord_lat", None) data["location_long"] = data.get("coord_lon", None) data["location_city"] = data.get("name", None) data["location_cityid"] = data.get("id", None) data["location_country"] = data.get("sys_country", None) data["sunrise"] = self._get_sunrise_time() data["sunset"] = self._get_sunset_time() data["isotime"] = self._get_dt() data["wind_direction"] = self._get_wind_direction() data["weather"] = data.get("weather_0_main", None) data["weather_details"] = data.get("weather_0_description", None) data["humidity"] = data.get("main_humidity", None) data["pressure"] = data.get("main_pressure", None) data["temp"] = data.get("main_temp", None) def _get_wind_direction(self): wd = self.data.get("wind_deg", None) if wd is None: return None return degrees_to_direction(wd) def _get_sunrise_time(self): dt = self.data.get("sys_sunrise", None) if dt is None: return None return time.strftime(self.timeformat, time.localtime(dt)) def _get_sunset_time(self): dt = self.data.get("sys_sunset", None) if dt is None: return None return time.strftime(self.timeformat, time.localtime(dt)) def _get_dt(self): dt = self.data.get("dt", None) if dt is None: return None return time.strftime(self.dateformat + self.timeformat, time.localtime(dt)) class OpenWeather(GenPollUrl): """A weather widget, data provided by the OpenWeather API. Some format options: - location_city - location_cityid - location_country - location_lat - location_long - weather - weather_details - units_temperature - units_wind_speed - isotime - humidity - pressure - sunrise - sunset - temp - visibility - wind_speed - wind_deg - wind_direction - main_feels_like - main_temp_min - main_temp_max - clouds_all - icon Icon support is available but you will need a suitable font installed. A default icon mapping is provided (``OpenWeather.symbols``) but changes can be made by setting ``weather_symbols``. Available icon codes can be viewed here: https://openweathermap.org/weather-conditions#Icon-list """ symbols = { "Unknown": "✨", "01d": "☀️", "01n": "🌕", "02d": "🌤️", "02n": "☁️", "03d": "🌥️", "03n": "☁️", "04d": "☁️", "04n": "☁️", "09d": "🌧️", "09n": "🌧️", "10d": "⛈", "10n": "⛈", "11d": "🌩", "11n": "🌩", "13d": "❄️", "13n": "❄️", "50d": "🌫", "50n": "🌫", } defaults: list[tuple[str, Any, str]] = [ # One of (cityid, location, zip, coordinates) must be set. ( "app_key", DEFAULT_APP_ID, """Open Weather access key. A default is provided, but for prolonged use obtaining your own is suggested: https://home.openweathermap.org/users/sign_up""", ), ( "cityid", None, """City ID. Can be looked up on e.g.: https://openweathermap.org/find Takes precedence over location and coordinates. Note that this is not equal to a WOEID.""", ), ( "location", None, """Name of the city. Country name can be appended like cambridge,NZ. Takes precedence over zip-code.""", ), ( "zip", None, """Zip code (USA) or "zip code,country code" for other countries. E.g. 12345,NZ. Takes precedence over coordinates.""", ), ( "coordinates", None, """Dictionary containing latitude and longitude Example: coordinates={"longitude": "77.22", "latitude": "28.67"}""", ), ( "format", "{location_city}: {main_temp} °{units_temperature} {humidity}% {weather_details}", "Display format", ), ("metric", True, "True to use metric/C, False to use imperial/F"), ( "dateformat", "%Y-%m-%d ", """Format for dates, defaults to ISO. For details see: https://docs.python.org/3/library/time.html#time.strftime""", ), ( "timeformat", "%H:%M", """Format for times, defaults to ISO. For details see: https://docs.python.org/3/library/time.html#time.strftime""", ), ( "language", "en", """Language of response. List of languages supported can be seen at: https://openweathermap.org/current under Multilingual support""", ), ( "weather_symbols", dict(), "Dictionary of weather symbols. Can be used to override default symbols.", ), ] def __init__(self, **config): GenPollUrl.__init__(self, **config) self.add_defaults(OpenWeather.defaults) self.symbols.update(self.weather_symbols) @property def url(self): if not self.cityid and not self.location and not self.zip and not self.coordinates: return None params = { "appid": self.app_key or DEFAULT_APP_ID, "units": "metric" if self.metric else "imperial", } if self.cityid: params["id"] = self.cityid elif self.location: params["q"] = self.location elif self.zip: params["zip"] = self.zip elif self.coordinates: params["lat"] = self.coordinates["latitude"] params["lon"] = self.coordinates["longitude"] if self.language: params["lang"] = self.language return QUERY_URL + urlencode(params) def parse(self, response): try: rp = _OpenWeatherResponseParser(response, self.dateformat, self.timeformat) except OpenWeatherResponseError as e: return f"Error {e.resp_code}" data = rp.data data["units_temperature"] = "C" if self.metric else "F" data["units_wind_speed"] = "Km/h" if self.metric else "m/h" data["icon"] = self.symbols.get(data["weather_0_icon"], self.symbols["Unknown"]) return self.format.format(**data) qtile-0.31.0/libqtile/widget/bluetooth.py0000664000175000017500000006014514762660347020313 0ustar epsilonepsilon# Copyright (c) 2023 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import asyncio import contextlib from enum import Enum from dbus_fast.aio import MessageBus from dbus_fast.constants import BusType from dbus_fast.errors import DBusError, InterfaceNotFoundError from libqtile.command.base import expose_command from libqtile.log_utils import logger from libqtile.utils import create_task from libqtile.widget import base BLUEZ_SERVICE = "org.bluez" BLUEZ_DEVICE = "org.bluez.Device1" BLUEZ_ADAPTER = "org.bluez.Adapter1" BLUEZ_BATTERY = "org.bluez.Battery1" OBJECT_MANAGER_INTERFACE = "org.freedesktop.DBus.ObjectManager" PROPERTIES_INTERFACE = "org.freedesktop.DBus.Properties" def _catch_dbus_error(msg): """Decorator to catch DBusErrors and log a message.""" def _wrapper(func): async def f(self): try: await func(self) except DBusError: logger.warning(msg, self.name) return f return _wrapper class DeviceState(Enum): CONNECTED = 0 PAIRED = 1 UNPAIRED = 2 class _BluetoothBase: """Base class with some common requirements for devices and adapters.""" def __init__(self, path, interface, properties_interface, widget): self.path = path self.interface = interface self.widget = widget self.properties = properties_interface self.properties.on_properties_changed(self.properties_changed) self._name = "" def __repr__(self): """Neater repr to help debugging.""" return f"<{self.__class__.__name__}: {self.name} ({self.path})>" def __del__(self): """Remove signal listener when garbage collected.""" with contextlib.suppress(RuntimeError): self.properties.off_properties_changed(self.properties_changed) def properties_changed(self, _interface_name, _changed_properties, _invalidated_properties): """Handler for properties_changed signal.""" create_task(self.update_props()) async def update_props(self): raise NotImplementedError class BluetoothDevice(_BluetoothBase): """ Helper class to represent an org.bluez.Device1 object. Exposes basic properties/methods and also listens to signals to update properties as needed. """ def __init__(self, path, interface, properties_interface, widget): _BluetoothBase.__init__(self, path, interface, properties_interface, widget) self._connected = False self._paired = False self._status = DeviceState.UNPAIRED self._adapter = "" self.has_name = False self.battery_device = None self._battery = 0 @_catch_dbus_error("Unable to connect to device: %s.") async def connect(self): await self.interface.call_connect() @_catch_dbus_error("Unable to disconnect device: %s.") async def disconnect(self): await self.interface.call_disconnect() @_catch_dbus_error("Unable to pair device: %s.") async def pair_and_connect(self): await self.interface.call_pair() await self.connect() async def action(self): """Helper method to call appropriate method based on device status.""" if self.connected: await self.disconnect() elif self.paired and not self.connected: await self.connect() elif not self.paired: await self.pair_and_connect() @property def name(self): return self._name @property def connected(self): return self._connected @property def paired(self): return self._paired @property def status(self): return self._status @property def battery(self): if self.battery_device: return self._battery return "" @property def adapter(self): a = self.widget.adapters.get(self._adapter) if a: return a.name else: return "Unknown" def add_battery(self): """Triggers adding battery interface.""" def refresh(_): if self.battery_device: create_task(self.update_props()) task = create_task(self.get_battery()) task.add_done_callback(refresh) def remove_battery(self): self.battery_device = None self._battery = 0 async def get_battery(self): proxy = await self.widget.get_proxy(self.path) with contextlib.suppress(InterfaceNotFoundError): self.battery_device = proxy.get_interface(BLUEZ_BATTERY) async def update_props(self, setup=False): """Refresh all the properties for the device.""" # Some devices don't report a name so we fall back to the device address try: self._name = await self.interface.get_name() self.has_name = True except (AttributeError, DBusError): self._name = await self.interface.get_address() self.has_name = False self._connected, self._paired, self._adapter = await asyncio.gather( self.interface.get_connected(), self.interface.get_paired(), self.interface.get_adapter(), ) # If we're setting up, let's see if a battery device is available # This may happen if the device is already connected when the widget starts if setup: await self.get_battery() if self.battery_device: self._battery = await self.battery_device.get_percentage() if self._connected: self._status = DeviceState.CONNECTED elif self._paired and not self._connected: self._status = DeviceState.PAIRED else: self._status = DeviceState.UNPAIRED if not setup: self.widget.refresh() async def check(self): """Checks if device belongs to requested adapter.""" await self.update_props(setup=True) if not self.widget.adapter_paths: return True, self for path in self.widget.adapter_paths: if path == self._adapter: return True, self return False, self class BluetoothAdapter(_BluetoothBase): """ Helper class for Bluetooth adapters. Exposes basic properties/methods and also listens to signals to update properties as needed. """ def __init__(self, path, interface, properties_interface, widget): _BluetoothBase.__init__(self, path, interface, properties_interface, widget) self._discovering = False self._powered = False create_task(self.update_props(setup=True)) @_catch_dbus_error("Unable to start discovery on adapter: %s.") async def start_discovery(self): await self.interface.call_start_discovery() @_catch_dbus_error("Unable to stop discovery on adapter: %s.") async def stop_discovery(self): await self.interface.call_stop_discovery() @_catch_dbus_error("Unable to set power state for adapter: %s.") async def power(self): await self.interface.set_powered(not self._powered) @property def discovering(self): return self._discovering @property def powered(self): return self._powered @property def name(self): return self._name async def discover(self): if self.discovering: await self.stop_discovery() else: await self.start_discovery() async def update_props(self, setup=False): self._discovering = await self.interface.get_discovering() self._powered = await self.interface.get_powered() self._name = await self.interface.get_name() if not setup: self.widget.refresh() class Bluetooth(base._TextBox, base.MarginMixin): """ Bluetooth widget that provides following functionality: - View multiple adapters/devices (adapters can be filtered) - Set power and discovery status for adapters - Connect/disconnect/pair devices The widget works by providing a menu in the bar. Different items are accessed by scrolling up and down on the widget. Clicking on an adapter will open a submenu allowing you to set power and discovery status. Clicking on a device will perform an action based on the status of that device: - Connected devices will be disconnected - Disconnected devices will be connected - Unpaired devices (which appear if discovery is on) will be paired and connected Symbols are used to show the status of adapters and devices. Battery level for bluetooth devices can also be shown if available. This functionality is not available by default on all distros. If it doesn't work, you can try adding ``Experimental = true`` to ``/etc/bluetooth/main.conf``. """ defaults = [ ("hide_unnamed_devices", False, "Devices with no name will be hidden from scan results"), ("symbol_connected", "*", "Symbol to indicate device is connected"), ("symbol_paired", "-", "Symbol to indicate device is paired but unconnected"), ("symbol_unknown", "?", "Symbol to indicate device is unpaired"), ("symbol_powered", ("*", "-"), "Symbols when adapter is powered and unpowered."), ( "symbol_discovery", ("D", ""), "Symbols when adapter is discovering and not discovering", ), ( "device_format", "Device: {name}{battery_level} [{symbol}]", "Text to display when showing bluetooth device. " "The ``{adapter`` field is also available if you're using multiple adapters.", ), ( "device_battery_format", " ({battery}%)", "Text to be shown if device reports battery level", ), ( "adapter_format", "Adapter: {name} [{powered}{discovery}]", "Text to display when showing adapter device.", ), ( "adapter_paths", [], "List of DBus object path for bluetooth adapter (e.g. '/org/bluez/hci0'). " "Empty list will show all adapters.", ), ( "default_text", "BT {connected_devices}", "Text to show when not scrolling through menu. " "Available fields: 'connected_devices' list of connected devices, " "'num_connected_devices' number of connected devices, " "'adapters' list of bluetooth adapters, 'num_adapters' number of bluetooth adapters.", ), ( "default_show_battery", False, "Include battery level of 'connected_devices' in 'default_text'. Uses 'device_battery_format'.", ), ("separator", ", ", "Separator for lists in 'default_text'."), ( "default_timeout", None, "Time before reverting to default_text. If 'None', text will stay on selected item.", ), ( "device", None, "Device path, can be found with d-feet or similar dbus explorer. " "When set, the widget will default to showing this device status.", ), ] def __init__(self, **config): base._TextBox.__init__(self, **config) self.add_defaults(Bluetooth.defaults) self.add_defaults(base.MarginMixin.defaults) self.connected = False self.bus = None self.devices = {} self.adapters = {} self._lines = [] self._line_index = 0 self._adapter_index = 0 self._setting_up = True self.show_adapter = False self.device_found = False self.add_callbacks( {"Button1": self.click, "Button4": self.scroll_up, "Button5": self.scroll_down} ) self.timer = None self.object_manager = None def _configure(self, qtile, bar): base._TextBox._configure(self, qtile, bar) self.symbols = (self.symbol_connected, self.symbol_paired, self.symbol_unknown) async def _config_async(self): await self._connect() async def _connect(self): """Connect to bus and set up key listeners.""" self.bus = await MessageBus(bus_type=BusType.SYSTEM).connect() # Get the object manager proxy = await self.get_proxy("/") self.object_manager = proxy.get_interface(OBJECT_MANAGER_INTERFACE) # Subscribe to signals for new and removed interfaces self.object_manager.on_interfaces_added(self._interface_added) self.object_manager.on_interfaces_removed(self._interface_removed) await self._get_managed_objects() self.refresh() async def get_proxy(self, path): """Provides proxy object after introspecting the given path.""" device_introspection = await self.bus.introspect(BLUEZ_SERVICE, path) proxy = self.bus.get_proxy_object(BLUEZ_SERVICE, path, device_introspection) return proxy async def _get_managed_objects(self): """ Retrieve list of managed objects. These are devices that have previously been paired but may or may not currently be connected. Additionally, if the device is scanning, available objects will also appear here, albeit temporarily. """ self._setting_up = True objects = await self.object_manager.call_get_managed_objects() for path, interfaces in objects.items(): self._interface_added(path, interfaces) self._setting_up = False def _interface_added(self, path, interfaces): """Handles the object based on the interface type.""" task = None # Create device or adapter for device_type in (BLUEZ_DEVICE, BLUEZ_ADAPTER): if device_type in interfaces: task = create_task(self._add_object(path, device_type)) break if task is not None and not self._setting_up: task.add_done_callback(lambda *args: self.refresh()) # Battery interface is added after the device is connected # so no task will have been created at this point. if not task and BLUEZ_BATTERY in interfaces: if device := self.devices.get(path): # Get device to load battery interface device.add_battery() def _interface_removed(self, path, interfaces): # Object has been removed so remove from our list of available devices updated = False if BLUEZ_DEVICE in interfaces: with contextlib.suppress(KeyError): del self.devices[path] updated = True elif BLUEZ_ADAPTER in interfaces: with contextlib.suppress(KeyError): del self.adapters[path] updated = True elif BLUEZ_BATTERY in interfaces: device = self.devices.get(path) if device: device.remove_battery() if updated and not self._setting_up: self.refresh() async def _add_object(self, path, device_type): proxy = await self.get_proxy(path) # Check if object is a valid bluetooth device and ignore it if not try: interface = proxy.get_interface(device_type) except InterfaceNotFoundError: return # Get the properties interface so we can listed to signals properties = proxy.get_interface(PROPERTIES_INTERFACE) # Create an object to represent this device and add to our list if device_type == BLUEZ_DEVICE: # The device will be added to self.devices as part of the __init__ # process after checking whether we need to filter out the device # if it's connected to an unwatched adapter # This is preferable to substring matching on 'path' which could # result in false positives (in very rare situations) device = BluetoothDevice(path, interface, properties, self) task = create_task(device.check()) task.add_done_callback(self._add_device) elif device_type == BLUEZ_ADAPTER: if not self.adapter_paths or path in self.adapter_paths: adapter = BluetoothAdapter(path, interface, properties, self) self.adapters[path] = adapter def _add_device(self, task): success, device = task.result() if success: self.devices[device.path] = device self.refresh() def refresh(self): if self._setting_up: return # Store lines in a variable as we'll need to access them elsewhere # Each entry is a tuple of (display text, callable) self._lines = [] # If we've clicked on an adapter then we're just showing the adapter submenu if self.show_adapter: self._lines.extend(self._get_adapter_menu(self._shown_adapter)) # Otherwise we're in default behavior else: # Line 1 is the text to be formatted according to "default_text" connected = [d for d in self.devices.values() if d.connected] adapters = [a.name for a in self.adapters.values()] if self.default_show_battery: connected_devices = [ "{name}{battery}".format( name=d.name, battery=self.device_battery_format.format(battery=d.battery) if d.battery else "", ) for d in connected ] else: connected_devices = [d.name for d in connected] self._lines.append( ( self.default_text.format( connected_devices=self.separator.join(connected_devices), num_connected_devices=len(connected_devices), adapters=self.separator.join(adapters), num_adapters=len(adapters), ), lambda: None, ) ) # Next is the adapters... def show(adapter): """Function to trigger the adapter submenu.""" self.show_adapter = True self._shown_adapter = adapter # Store the current menu position self._adapter_index = self._line_index # Change menu position to the first item in the submenu self._line_index = 0 self.refresh() for adapter in self.adapters.values(): self._lines.append((adapter, lambda a=adapter: show(a))) # Finally, loop over all the devices for device in self.devices.values(): self._lines.append((device, lambda d=device: create_task(d.action()))) if self._lines: # If user has set default device, check if it should be shown # This will only force the display to that widget the first time the device is found # i.e. once user has scrolled to a different device, it will no longer return to the # set device. if not self.device_found and self.device is not None: for i, (obj, _) in enumerate(self._lines): if isinstance(obj, BluetoothDevice): if self.device in obj.path: self._line_index = i self.device_found = True break self.show_line() else: self.update("") def _get_adapter_menu(self, adapter): """Builds a submenu for the selected adapter.""" state = "off" if adapter.powered else "on" discovery = "off" if adapter.discovering else "on" def exit(): self.show_adapter = False # Restore menu position self._line_index = self._adapter_index self.refresh() return [ (f"Turn power {state}", lambda a=adapter: create_task(a.power())), (f"Turn discovery {discovery}", lambda a=adapter: create_task(a.discover())), ("Exit", lambda: exit()), ] def show_line(self): """Formats the text of the current menu item.""" if not self._lines: return obj = None # If devices disappear we may have an invalid line index while obj is None: try: obj, action = self._lines[self._line_index] except IndexError: self._line_index -= 1 self.update(self.format_object(obj)) def format_object(self, obj): """Takes the given object and returns a formatted string representing the object.""" if isinstance(obj, BluetoothDevice): # status.value is 0 for connected, 1 for paired (and unconnected), 2 for unpaired symbol = self.symbols[obj.status.value] if obj.battery: battery_level = self.device_battery_format.format(battery=obj.battery) else: battery_level = "" return self.device_format.format( symbol=symbol, name=obj.name, adapter=obj.adapter, battery_level=battery_level ) elif isinstance(obj, BluetoothAdapter): powered = 0 if obj.powered else 1 discovery = 0 if obj.discovering else 1 return self.adapter_format.format( powered=self.symbol_powered[powered], discovery=self.symbol_discovery[discovery], name=obj.name, ) elif isinstance(obj, str): return obj # Shouldn't happen but let's be safe! return "" @expose_command def scroll_up(self): """Scroll up to next item.""" self._scroll(1) @expose_command def scroll_down(self): """Scroll down to next item.""" self._scroll(-1) @expose_command def click(self): """Perform default action on visible item.""" with contextlib.suppress(IndexError): _, action = self._lines[self._line_index] action() def _scroll(self, step): if self.timer is not None: self.timer.cancel() if self._lines: self._line_index = (self._line_index + step) % len(self._lines) self.show_line() if self.default_timeout is not None: self.timer = self.timeout_add(self.default_timeout, self.hide) def hide(self): """Revert widget contents to default.""" self._line_index = 0 self.show_adapter = False self.refresh() def finalize(self): # if we failed to connect, there is nothing to finalize. if self.bus is None: return # Remove dbus signal handlers before finalising. # Clearing dicts will call the __del__ method on the stored objects # which has been defined to remove signal handlers self.devices.clear() self.adapters.clear() # Remove object manager's handlers if self.object_manager is not None: self.object_manager.off_interfaces_added(self._interface_added) self.object_manager.off_interfaces_removed(self._interface_removed) # Disconnect the bus connection self.bus.disconnect() self.bus = None base._TextBox.finalize(self) qtile-0.31.0/libqtile/widget/tasklist.py0000664000175000017500000005214614762660347020146 0ustar epsilonepsilon# Copyright (c) 2012-2014 roger # Copyright (c) 2012-2015 Tycho Andersen # Copyright (c) 2013 dequis # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2013 Craig Barnes # Copyright (c) 2014 Sean Vig # Copyright (c) 2018 Piotr Przymus # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import cairocffi try: from xdg.IconTheme import getIconPath has_xdg = True except ImportError: has_xdg = False import libqtile.bar from libqtile import hook, pangocffi from libqtile.images import Img from libqtile.log_utils import logger from libqtile.widget import base class TaskList(base._Widget, base.PaddingMixin, base.MarginMixin): """Displays the icon and name of each window in the current group Contrary to WindowTabs this is an interactive widget. The window that currently has focus is highlighted. Optional requirements: `pyxdg `__ is needed to use theme icons and to display icons on Wayland. """ orientations = base.ORIENTATION_HORIZONTAL defaults = [ ("font", "sans", "Default font"), ("fontsize", None, "Font size. Calculated if None."), ("foreground", "ffffff", "Foreground colour"), ("fontshadow", None, "font shadow color, default is None(no shadow)"), ("borderwidth", 2, "Current group border width"), ("border", "215578", "Border colour"), ("rounded", True, "To round or not to round borders"), ( "highlight_method", "border", "Method of highlighting (one of 'border' or 'block') " "Uses `*_border` color settings", ), ("urgent_border", "FF0000", "Urgent border color"), ( "urgent_alert_method", "border", "Method for alerting you of WM urgent " "hints (one of 'border' or 'text')", ), ( "unfocused_border", None, "Border color for unfocused windows. " "Affects only hightlight_method 'border' and 'block'. " "Defaults to None, which means no special color.", ), ( "max_title_width", None, "Max size in pixels of task title." "(if set to None, as much as available.)", ), ( "title_width_method", None, "Method to compute the width of task title. (None, 'uniform'.)" "Defaults to None, the normal behaviour.", ), ( "parse_text", None, "Function to parse and modify window names. " "e.g. function in config that removes excess " "strings from window name: " "def my_func(text)" ' for string in [" - Chromium", " - Firefox"]:' ' text = text.replace(string, "")' " return text" "then set option parse_text=my_func", ), ("spacing", None, "Spacing between tasks." "(if set to None, will be equal to margin_x)"), ( "txt_minimized", "_ ", "Text representation of the minimized window state. " 'e.g., "_ " or "\U0001f5d5 "', ), ( "txt_maximized", "[] ", "Text representation of the maximized window state. " 'e.g., "[] " or "\U0001f5d6 "', ), ( "txt_floating", "V ", "Text representation of the floating window state. " 'e.g., "V " or "\U0001f5d7 "', ), ( "markup_normal", None, "Text markup of the normal window state. Supports pangomarkup with markup=True." 'e.g., "{}" or "{}"', ), ( "markup_minimized", None, "Text markup of the minimized window state. Supports pangomarkup with markup=True." 'e.g., "{}" or "{}"', ), ( "markup_maximized", None, "Text markup of the maximized window state. Supports pangomarkup with markup=True." 'e.g., "{}" or "{}"', ), ( "markup_floating", None, "Text markup of the floating window state. Supports pangomarkup with markup=True." 'e.g., "{}" or "{}"', ), ( "markup_focused", None, "Text markup of the focused window state. Supports pangomarkup with markup=True." 'e.g., "{}" or "{}"', ), ( "markup_focused_floating", None, "Text markup of the focused and floating window state. Supports pangomarkup with markup=True." 'e.g., "{}" or "{}"', ), ( "icon_size", None, "Icon size. " "(Calculated if set to None. Icons are hidden if set to 0.)", ), ( "theme_mode", None, "When to use theme icons. `None` = never, `preferred` = use if available, " "`fallback` = use if app does not provide icon directly. " "`preferred` and `fallback` have identical behaviour on Wayland.", ), ( "theme_path", None, "Path to icon theme to be used by pyxdg for icons. ``None`` will use default icon theme.", ), ( "window_name_location", False, "Whether to show the location of the window in the title.", ), ( "window_name_location_offset", 0, "The offset given to the window location", ), ( "stretch", True, "Widget fills available space in bar. Set to `False` to limit widget width to size of its contents.", ), ] def __init__(self, **config): base._Widget.__init__(self, libqtile.bar.STRETCH, **config) self.add_defaults(TaskList.defaults) self.add_defaults(base.PaddingMixin.defaults) self.add_defaults(base.MarginMixin.defaults) self._icons_cache = {} self._box_end_positions = [] self.markup = False self.clicked = None if self.spacing is None: self.spacing = self.margin_x self.add_callbacks({"Button1": self.select_window}) def box_width(self, text): """ calculate box width for given text. If max_title_width is given, the returned width is limited to it. """ width, _ = self.drawer.max_layout_size( [text], self.font, self.fontsize, markup=self.markup ) width = width + 2 * (self.padding_x + self.borderwidth) return width def get_taskname(self, window): """ Get display name for given window. Depending on its state minimized, maximized and floating appropriate characters are prepended. """ state = "" markup_str = self.markup_normal # Enforce markup and new string format behaviour when # at least one markup_* option is used. # Mixing non markup and markup may cause problems. if ( self.markup_minimized or self.markup_maximized or self.markup_floating or self.markup_focused or self.markup_focused_floating ): enforce_markup = True else: enforce_markup = False if window is None: pass elif window.minimized: state = self.txt_minimized markup_str = self.markup_minimized elif window.maximized: state = self.txt_maximized markup_str = self.markup_maximized elif window is window.group.current_window: if window.floating: state = self.txt_floating markup_str = self.markup_focused_floating or self.markup_floating else: markup_str = self.markup_focused elif window.floating: state = self.txt_floating markup_str = self.markup_floating window_location = ( f"[{window.group.windows.index(window) + self.window_name_location_offset}] " if self.window_name_location else "" ) window_name = window_location + window.name if window and window.name else "?" if callable(self.parse_text): try: window_name = self.parse_text(window_name) except: # noqa: E722 logger.exception("parse_text function failed:") # Emulate default widget behavior if markup_str is None if enforce_markup and markup_str is None: markup_str = f"{state}{{}}" if markup_str is not None: self.markup = True window_name = pangocffi.markup_escape_text(window_name) return markup_str.format(window_name) return f"{state}{window_name}" @property def windows(self): if self.qtile.core.name == "x11": windows = [] for w in self.bar.screen.group.windows: wm_states = list(w.window.get_property("_NET_WM_STATE", "ATOM", unpack=int)) skip_taskbar = w.qtile.core.conn.atoms["_NET_WM_STATE_SKIP_TASKBAR"] if w.window.get_wm_type() in ("normal", None) and skip_taskbar not in wm_states: windows.append(w) return windows return self.bar.screen.group.windows @property def max_width(self): width = self.bar.width width -= sum( w.width for w in self.bar.widgets if w is not self and w.length_type != libqtile.bar.STRETCH ) return width def calc_box_widths(self): """ Calculate box width for each window in current group. If the available space is less than overall size of boxes, the boxes are shrunk by percentage if greater than average. """ windows = self.windows window_count = len(windows) # if no windows present for current group just return empty list if not window_count: return [] # Determine available and max average width for task name boxes. width_total = self.max_width - 2 * self.margin_x - (window_count - 1) * self.spacing width_avg = width_total / window_count names = [self.get_taskname(w) for w in windows] if self.icon_size == 0: icons = len(windows) * [None] else: icons = [self.get_window_icon(w) for w in windows] # Obey title_width_method if specified if self.title_width_method == "uniform": width_uniform = width_total // window_count width_boxes = [width_uniform for w in range(window_count)] else: # Default behaviour: calculated width for each task according to # icon and task name consisting # of state abbreviation and window name width_boxes = [ ( self.box_width(names[idx]) + ((self.icon_size + self.padding_x) if icons[idx] else 0) ) for idx in range(window_count) ] # Obey max_title_width if specified if self.max_title_width: width_boxes = [min(w, self.max_title_width) for w in width_boxes] width_sum = sum(width_boxes) # calculated box width are to wide for available widget space: if width_sum > width_total: # sum the width of tasks shorter than calculated average # and calculate a ratio to shrink boxes greater than width_avg width_shorter_sum = sum([w for w in width_boxes if w < width_avg]) ratio = (width_total - width_shorter_sum) / (width_sum - width_shorter_sum) # determine new box widths by shrinking boxes greater than avg width_boxes = [(w if w < width_avg else w * ratio) for w in width_boxes] return zip(windows, icons, names, width_boxes) def calculate_length(self): width = 0 box_widths = [box[3] for box in self.calc_box_widths()] if box_widths: width += self.spacing * len(box_widths) - 1 width += sum(w for w in box_widths) return width def _configure(self, qtile, bar): base._Widget._configure(self, qtile, bar) if not self.stretch: self.length_type = libqtile.bar.CALCULATED if not has_xdg and self.theme_mode is not None: logger.warning("You must install pyxdg to use theme icons.") self.theme_mode = None if self.theme_mode and self.theme_mode not in ["preferred", "fallback"]: logger.warning( "Unexpected theme_mode (%s). Theme icons will be disabled.", self.theme_mode ) self.theme_mode = None if qtile.core.name == "wayland" and self.theme_mode is None and self.icon_size != 0: # Disable icons self.icon_size = 0 if self.icon_size is None: self.icon_size = self.bar.height - 2 * (self.borderwidth + self.margin_y) if self.fontsize is None: calc = self.bar.height - self.margin_y * 2 - self.borderwidth * 2 - self.padding_y * 2 self.fontsize = max(calc, 1) self.layout = self.drawer.textlayout( "", "ffffff", self.font, self.fontsize, self.fontshadow, wrap=False ) self.setup_hooks() def update(self, window=None): if not window or window in self.windows: self.bar.draw() def remove_icon_cache(self, window): wid = window.wid if wid in self._icons_cache: self._icons_cache.pop(wid) def invalidate_cache(self, window): self.remove_icon_cache(window) self.update(window) def setup_hooks(self): hook.subscribe.client_name_updated(self.update) hook.subscribe.focus_change(self.update) hook.subscribe.float_change(self.update) hook.subscribe.client_urgent_hint_changed(self.update) hook.subscribe.net_wm_icon_change(self.invalidate_cache) hook.subscribe.client_killed(self.remove_icon_cache) def drawtext(self, text, textcolor, width): if self.markup: self.layout.markup = self.markup self.layout.text = text self.layout.font_family = self.font self.layout.font_size = self.fontsize self.layout.colour = textcolor if width is not None: self.layout.width = width def drawbox( self, offset, text, bordercolor, textcolor, width=None, rounded=False, block=False, icon=None, ): self.drawtext(text, textcolor, width) icon_padding = (self.icon_size + self.padding_x) if icon else 0 padding_x = [self.padding_x + icon_padding, self.padding_x] if bordercolor is None: # border colour is set to None when we don't want to draw a border at all # Rather than dealing with alpha blending issues, we just set border width # to 0. border_width = 0 framecolor = self.background or self.bar.background else: border_width = self.borderwidth framecolor = bordercolor framed = self.layout.framed(border_width, framecolor, padding_x, self.padding_y) if block and bordercolor is not None: framed.draw_fill(offset, self.margin_y, rounded) else: framed.draw(offset, self.margin_y, rounded) if icon: self.draw_icon(icon, offset) def get_clicked(self, x, y): box_start = self.margin_x for box_end, win in zip(self._box_end_positions, self.windows): if box_start <= x <= box_end: return win else: box_start = box_end + self.spacing # not found any , return None return None def button_press(self, x, y, button): self.clicked = self.get_clicked(x, y) base._Widget.button_press(self, x, y, button) def select_window(self): if self.clicked: current_win = self.bar.screen.group.current_window window = self.clicked if window is not current_win: window.group.focus(window, False) if window.floating: window.bring_to_front() else: window.toggle_minimize() def _get_class_icon(self, window): if not getattr(window, "icons", False): return None icons = sorted( iter(window.icons.items()), key=lambda x: abs(self.icon_size - int(x[0].split("x")[0])), ) icon = icons[0] width, height = map(int, icon[0].split("x")) img = cairocffi.ImageSurface.create_for_data( icon[1], cairocffi.FORMAT_ARGB32, width, height ) return img def _get_theme_icon(self, window): classes = window.get_wm_class() if not classes: return None icon = None for cl in classes: for app in set([cl, cl.lower()]): icon = getIconPath(app, theme=self.theme_path) if icon is not None: break else: continue break if not icon: return None img = Img.from_path(icon) return img.surface def get_window_icon(self, window): if not getattr(window, "icons", False) and self.theme_mode is None: return None cache = self._icons_cache.get(window.wid) if cache: return cache surface = None img = None if self.qtile.core.name == "x11": img = self._get_class_icon(window) if self.theme_mode == "preferred" or (self.theme_mode == "fallback" and img is None): xdg_img = self._get_theme_icon(window) if xdg_img: img = xdg_img if img is not None: surface = cairocffi.SurfacePattern(img) height = img.get_height() width = img.get_width() scaler = cairocffi.Matrix() if height != self.icon_size: sp = height / self.icon_size height = self.icon_size width /= sp scaler.scale(sp, sp) surface.set_matrix(scaler) self._icons_cache[window.wid] = surface return surface def draw_icon(self, surface, offset): if not surface: return x = offset + self.borderwidth + self.padding_x y = (self.height - self.icon_size) // 2 self.drawer.ctx.save() self.drawer.ctx.translate(x, y) self.drawer.ctx.set_source(surface) self.drawer.ctx.paint() self.drawer.ctx.restore() def draw(self): self.drawer.clear(self.background or self.bar.background) offset = self.margin_x self._box_end_positions = [] for w, icon, task, bw in self.calc_box_widths(): self._box_end_positions.append(offset + bw) if w.urgent: border = self.urgent_border text_color = border elif w is w.group.current_window: border = self.border text_color = border else: border = self.unfocused_border or None text_color = self.foreground if self.highlight_method == "text": border = None else: text_color = self.foreground textwidth = ( bw - 2 * self.padding_x - ((self.icon_size + self.padding_x) if icon else 0) ) self.drawbox( offset, task, border, text_color, rounded=self.rounded, block=(self.highlight_method == "block"), width=textwidth, icon=icon, ) offset += bw + self.spacing self.drawer.draw(offsetx=self.offset, offsety=self.offsety, width=self.width) qtile-0.31.0/libqtile/widget/chord.py0000664000175000017500000000566114762660347017407 0ustar epsilonepsilon# Copyright (c) 2014 Sean Vig # Copyright (c) 2014 roger # Copyright (c) 2014 Adi Sieker # Copyright (c) 2014 Tycho Andersen # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from libqtile import bar, hook from libqtile.widget import base class Chord(base._TextBox): """Display current key chord""" defaults = [ ( "chords_colors", {}, "colors per chord in form of tuple {'chord_name': ('bg', 'fg')}. " "Where a chord name is not in the dictionary, the default ``background`` and ``foreground``" " values will be used.", ), ( "name_transform", lambda txt: txt, "preprocessor for chord name it is pure function string -> string", ), ] def __init__(self, width=bar.CALCULATED, **config): base._TextBox.__init__(self, "", width, **config) self.add_defaults(Chord.defaults) def _configure(self, qtile, bar): base._TextBox._configure(self, qtile, bar) self.default_background = self.background self.default_foreground = self.foreground self.text = "" self._setup_hooks() def _setup_hooks(self): def hook_enter_chord(chord_name): if chord_name is True: self.text = "" self.reset_colours() return self.text = self.name_transform(chord_name) if chord_name in self.chords_colors: (self.background, self.foreground) = self.chords_colors.get(chord_name) else: self.reset_colours() self.bar.draw() hook.subscribe.enter_chord(hook_enter_chord) hook.subscribe.leave_chord(self.clear) def reset_colours(self): self.background = self.default_background self.foreground = self.default_foreground def clear(self, *args): self.reset_colours() self.text = "" self.bar.draw() qtile-0.31.0/libqtile/widget/window_count.py0000664000175000017500000000641214762660347021022 0ustar epsilonepsilon# Copyright (c) 2020 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations from typing import Any from libqtile import bar, hook from libqtile.command.base import expose_command from libqtile.widget import base class WindowCount(base._TextBox): """ A simple widget to display the number of windows in the current group of the screen on which the widget is. """ defaults: list[tuple[str, Any, str]] = [ ("font", "sans", "Text font"), ("fontsize", None, "Font pixel size. Calculated if None."), ("fontshadow", None, "font shadow color, default is None(no shadow)"), ("padding", None, "Padding left and right. Calculated if None."), ("foreground", "#ffffff", "Foreground colour."), ("text_format", "{num}", "Format for message"), ("show_zero", False, "Show window count when no windows"), ] def __init__(self, width=bar.CALCULATED, **config): base._TextBox.__init__(self, width=width, **config) self.add_defaults(WindowCount.defaults) self._count = 0 def _configure(self, qtile, bar): base._TextBox._configure(self, qtile, bar) self._setup_hooks() self._wincount() def _setup_hooks(self): hook.subscribe.client_killed(self._win_killed) hook.subscribe.client_managed(self._wincount) hook.subscribe.current_screen_change(self._wincount) hook.subscribe.group_window_add(self._wincount) hook.subscribe.setgroup(self._wincount) def _wincount(self, *args): try: self._count = len(self.bar.screen.group.windows) except AttributeError: self._count = 0 self.update(self.text_format.format(num=self._count)) def _win_killed(self, window): try: self._count = len(self.bar.screen.group.windows) except AttributeError: self._count = 0 self.update(self.text_format.format(num=self._count)) def calculate_length(self): if self.text and (self._count or self.show_zero): return min(self.layout.width, self.bar.width) + self.actual_padding * 2 else: return 0 @expose_command() def get(self): """Retrieve the current text.""" return self.text qtile-0.31.0/libqtile/widget/clock.py0000664000175000017500000001157414762660347017403 0ustar epsilonepsilon# Copyright (c) 2010 Aldo Cortesi # Copyright (c) 2012 Andrew Grigorev # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Tycho Andersen # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations import sys import time from datetime import datetime, timedelta, timezone, tzinfo from libqtile.command.base import expose_command from libqtile.log_utils import logger from libqtile.widget import base try: import pytz except ImportError: pass try: import dateutil.tz except ImportError: pass class Clock(base.InLoopPollText): """A simple but flexible text-based clock""" defaults = [ ("format", "%H:%M", "A Python datetime format string"), ("update_interval", 1.0, "Update interval for the clock"), ( "timezone", None, "The timezone to use for this clock, either as" ' string if pytz or dateutil is installed (e.g. "US/Central" or' " anything in /usr/share/zoneinfo), or as tzinfo (e.g." " datetime.timezone.utc). None means the system local timezone and is" " the default.", ), ] DELTA = timedelta(seconds=0.5) def __init__(self, **config): base.InLoopPollText.__init__(self, **config) self.add_defaults(Clock.defaults) self.timezone = self._lift_timezone(self.timezone) if self.timezone is None: logger.debug("Defaulting to the system local timezone.") def _lift_timezone(self, timezone): if isinstance(timezone, tzinfo): return timezone elif isinstance(timezone, str): # Empty string can be used to force use of system time if not timezone: return None # A string timezone needs to be converted to a tzinfo object if "pytz" in sys.modules: return pytz.timezone(timezone) elif "dateutil" in sys.modules: return dateutil.tz.gettz(timezone) else: logger.warning( "Clock widget can not infer its timezone from a" " string without pytz or dateutil. Install one" " of these libraries, or give it a" " datetime.tzinfo instance." ) elif timezone is None: pass else: logger.warning("Invalid timezone value %s.", timezone) return None def tick(self): self.update(self.poll()) return self.update_interval - time.time() % self.update_interval # adding .5 to get a proper seconds value because glib could # theoreticaly call our method too early and we could get something # like (x-1).999 instead of x.000 def poll(self): if self.timezone: now = datetime.now(timezone.utc).astimezone(self.timezone) else: now = datetime.now(timezone.utc).astimezone() return (now + self.DELTA).strftime(self.format) @expose_command def update_timezone(self, timezone: str | tzinfo | None = None): """ Force the clock to update timezone information. If the method is called with no arguments then the widget will reload the timzeone set on the computer (e.g. via ``timedatectl set-timezone ..``). This will have no effect if you have previously set a ``timezone`` value. Alternatively, you can pass a timezone string (e.g. ``"Europe/Lisbon"``) to change the specified timezone. Setting this to an empty string will cause the clock to rely on the system timezone. """ self.timezone = self._lift_timezone(timezone) # Force python to update timezone info (e.g. if system time has changed) time.tzset() self.update(self.poll()) @expose_command def use_system_timezone(self): """Force clock to use system timezone.""" self.update_timezone("") qtile-0.31.0/libqtile/widget/do_not_disturb.py0000664000175000017500000000607114762660347021322 0ustar epsilonepsilon# Copyright (c) 2024 Sprinter05 # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from subprocess import check_output from libqtile.lazy import lazy from libqtile.log_utils import logger from libqtile.widget import base class DoNotDisturb(base.InLoopPollText): """ Displays Do Not Disturb status for notification server Dunst by default. Can be used with other servers by changing the poll command and mouse callbacks. """ defaults = [ ( "poll_function", None, "Function that returns the notification server status. " "Define the function on your configuration file and " "pass it like poll_function=my_func. " "Must return either true or false", ), ("enabled_icon", "X", "Icon that displays when do not disturb is enabled"), ("disabled_icon", "O", "Icon that displays when do not disturb is disabled"), ("update_interval", 1, "How often in seconds the text must update"), ] def __init__(self, **config): base.InLoopPollText.__init__(self, **config) self.add_defaults(DoNotDisturb.defaults) self.status_retrieved_error = False if self.poll_function is None: self.add_callbacks( { "Button1": lazy.spawn("dunstctl set-paused toggle"), "Button3": lazy.spawn("dunstctl history-pop"), } ) def dunst_status(self): status = check_output(["dunstctl", "is-paused"]).strip() if status == b"true": return True return False def poll(self): check = None if self.poll_function is None: check = self.dunst_status() elif callable(self.poll_function): check = self.poll_function() else: if not self.status_retrieved_error: logger.error("Custom poll function cannot be called") self.status_retrieved_error = True if check: return self.enabled_icon else: return self.disabled_icon qtile-0.31.0/libqtile/widget/sep.py0000664000175000017500000000554014762660347017073 0ustar epsilonepsilon# Copyright (c) 2010 Aldo Cortesi # Copyright (c) 2011 Mounier Florian # Copyright (c) 2012, 2015 Tycho Andersen # Copyright (c) 2012 Craig Barnes # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014 Sean Vig # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from libqtile.widget import base class Sep(base._Widget): """A visible widget separator""" orientations = base.ORIENTATION_BOTH defaults = [ ("padding", 2, "Padding on either side of separator."), ("linewidth", 1, "Width of separator line."), ("foreground", "888888", "Separator line colour."), ("size_percent", 80, "Size as a percentage of bar size (0-100)."), ] def __init__(self, **config): length = config.get("padding", 2) * 2 + config.get("linewidth", 1) base._Widget.__init__(self, length, **config) self.add_defaults(Sep.defaults) self.length = self.padding + self.linewidth def draw(self): self.drawer.clear(self.background or self.bar.background) if self.bar.horizontal: margin_top = (self.bar.height / float(100) * (100 - self.size_percent)) / 2.0 self.drawer.draw_vbar( self.foreground, float(self.length) / 2, margin_top, self.bar.height - margin_top, linewidth=self.linewidth, ) self.drawer.draw(offsetx=self.offset, offsety=self.offsety, width=self.length) else: margin_left = (self.bar.width / float(100) * (100 - self.size_percent)) / 2.0 self.drawer.draw_hbar( self.foreground, margin_left, self.bar.width - margin_left, float(self.length) / 2, linewidth=self.linewidth, ) self.drawer.draw(offsety=self.offset, offsetx=self.offsetx, height=self.length) qtile-0.31.0/libqtile/widget/clipboard.py0000664000175000017500000001044214762660347020240 0ustar epsilonepsilon# Copyright (c) 2014 Sean Vig # Copyright (c) 2014 roger # Copyright (c) 2014 Adi Sieker # Copyright (c) 2014 Tycho Andersen # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from libqtile import bar, hook, pangocffi from libqtile.backend.x11 import xcbq from libqtile.widget import base class Clipboard(base._TextBox): """Display current clipboard contents""" defaults = [ ("selection", "CLIPBOARD", "the selection to display(CLIPBOARD or PRIMARY)"), ( "max_width", 10, "maximum number of characters to display " "(None for all, useful when width is bar.STRETCH)", ), ("timeout", 10, "Default timeout (seconds) for display text, None to keep forever"), ( "blacklist", ["keepassx"], "list with blacklisted wm_class, sadly not every " "clipboard window sets them, keepassx does." "Clipboard contents from blacklisted wm_classes " "will be replaced by the value of ``blacklist_text``.", ), ("blacklist_text", "***********", "text to display when the wm_class is blacklisted"), ] def __init__(self, width=bar.CALCULATED, **config): base._TextBox.__init__(self, "", width, **config) self.add_defaults(Clipboard.defaults) self.timeout_id = None def _configure(self, qtile, bar): base._TextBox._configure(self, qtile, bar) self.text = "" self.setup_hooks() def clear(self, *args): self.text = "" self.bar.draw() def is_blacklisted(self, owner_id): if not self.blacklist: return False if owner_id in self.qtile.windows_map: owner = self.qtile.windows_map[owner_id].window else: owner = xcbq.window.XWindow(self.qtile.core.conn, owner_id) owner_class = owner.get_wm_class() if owner_class: for wm_class in self.blacklist: if wm_class in owner_class: return True def setup_hooks(self): def hook_change(name, selection): if name != self.selection: return if self.is_blacklisted(selection["owner"]): text = self.blacklist_text else: text = selection["selection"].replace("\n", " ") text = text.strip() if self.max_width is not None and len(text) > self.max_width: text = text[: self.max_width] + "..." self.text = pangocffi.markup_escape_text(text) if self.timeout_id: self.timeout_id.cancel() self.timeout_id = None if self.timeout: self.timeout_id = self.timeout_add(self.timeout, self.clear) self.bar.draw() def hook_notify(name, selection): if name != self.selection: return if self.timeout_id: self.timeout_id.cancel() self.timeout_id = None # only clear if don't change don't apply in .5 seconds if self.timeout: self.timeout_id = self.timeout_add(self.timeout, self.clear) self.bar.draw() hook.subscribe.selection_notify(hook_notify) hook.subscribe.selection_change(hook_change) qtile-0.31.0/libqtile/widget/currentlayout.py0000664000175000017500000002147114762660347021225 0ustar epsilonepsilon# Copyright (c) 2011 Florian Mounier # Copyright (c) 2011 Kenji_Takahashi # Copyright (c) 2012 roger # Copyright (c) 2012, 2014 Tycho Andersen # Copyright (c) 2012 Maximilian Köhl # Copyright (c) 2013 Craig Barnes # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Adi Sieker # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import itertools import os from libqtile import bar, hook from libqtile.images import Img from libqtile.log_utils import logger from libqtile.widget import base class CurrentLayout(base._TextBox): """ Display the name of the current layout of the current group of the screen, the bar containing the widget, is on. """ def __init__(self, width=bar.CALCULATED, **config): base._TextBox.__init__(self, "", width, **config) def _configure(self, qtile, bar): base._TextBox._configure(self, qtile, bar) layout_id = self.bar.screen.group.current_layout self.text = self.bar.screen.group.layouts[layout_id].name self.setup_hooks() self.add_callbacks( { "Button1": qtile.next_layout, "Button2": qtile.prev_layout, } ) def hook_response(self, layout, group): if group.screen is not None and group.screen == self.bar.screen: self.text = layout.name self.bar.draw() def setup_hooks(self): hook.subscribe.layout_change(self.hook_response) def remove_hooks(self): hook.unsubscribe.layout_change(self.hook_response) def finalize(self): self.remove_hooks() base._TextBox.finalize(self) class CurrentLayoutIcon(base._TextBox): """ Display the icon representing the current layout of the current group of the screen on which the bar containing the widget is. If you are using custom layouts, a default icon with question mark will be displayed for them. If you want to use custom icon for your own layout, for example, `FooGrid`, then create a file named "layout-foogrid.png" and place it in `~/.icons` directory. You can as well use other directories, but then you need to specify those directories in `custom_icon_paths` argument for this plugin. The widget will look for icons with a `png` or `svg` extension. The order of icon search is: - dirs in `custom_icon_paths` config argument - `~/.icons` - built-in qtile icons """ orientations = base.ORIENTATION_HORIZONTAL defaults = [ ("scale", 1, "Scale factor relative to the bar height. Defaults to 1"), ( "custom_icon_paths", [], "List of folders where to search icons before" "using built-in icons or icons in ~/.icons dir. " "This can also be used to provide" "missing icons for custom layouts. " "Defaults to empty list.", ), ] def __init__(self, **config): base._TextBox.__init__(self, "", **config) self.add_defaults(CurrentLayoutIcon.defaults) self.length_type = bar.STATIC self.length = 0 def _configure(self, qtile, bar): base._TextBox._configure(self, qtile, bar) layout_id = self.bar.screen.group.current_layout self.text = self.bar.screen.group.layouts[layout_id].name self.current_layout = self.text self.icons_loaded = False self.icon_paths = [] self.surfaces = {} self._update_icon_paths() self._setup_images() self._setup_hooks() self.add_callbacks( { "Button1": qtile.next_layout, "Button2": qtile.prev_layout, } ) def hook_response(self, layout, group): if group.screen is not None and group.screen == self.bar.screen: self.current_layout = layout.name self.bar.draw() def _setup_hooks(self): """ Listens for layout change and performs a redraw when it occurs. """ hook.subscribe.layout_change(self.hook_response) def _remove_hooks(self): """ Listens for layout change and performs a redraw when it occurs. """ hook.unsubscribe.layout_change(self.hook_response) def draw(self): if self.icons_loaded: try: surface = self.surfaces[self.current_layout] except KeyError: logger.error("No icon for layout %s", self.current_layout) else: self.drawer.clear(self.background or self.bar.background) self.drawer.ctx.save() self.drawer.ctx.translate( (self.width - surface.width) / 2, (self.bar.height - surface.height) / 2, ) self.drawer.ctx.set_source(surface.pattern) self.drawer.ctx.paint() self.drawer.ctx.restore() self.drawer.draw(offsetx=self.offset, offsety=self.offsety, width=self.length) else: # Fallback to text self.text = self.current_layout[0].upper() base._TextBox.draw(self) def _get_layout_names(self): """ Returns a sequence of tuples of layout name and lowercased class name strings for each available layout. """ layouts = itertools.chain( self.qtile.config.layouts, (layout for group in self.qtile.config.groups for layout in group.layouts), ) return set((layout.name, layout.__class__.__name__.lower()) for layout in layouts) def _update_icon_paths(self): self.icon_paths = [] # We allow user to override icon search path self.icon_paths.extend(os.path.expanduser(path) for path in self.custom_icon_paths) # We also look in ~/.icons/ and ~/.local/share/icons self.icon_paths.append(os.path.expanduser("~/.icons")) self.icon_paths.append(os.path.expanduser("~/.local/share/icons")) # Default icons are in libqtile/resources/layout-icons. # If using default config without any custom icons, # this path will be used. root = os.sep.join(os.path.abspath(__file__).split(os.sep)[:-2]) self.icon_paths.append(os.path.join(root, "resources", "layout-icons")) def find_icon_file_path(self, layout_name): for icon_path in self.icon_paths: for extension in ["png", "svg"]: icon_filename = f"layout-{layout_name}.{extension}" icon_file_path = os.path.join(icon_path, icon_filename) if os.path.isfile(icon_file_path): return icon_file_path def _setup_images(self): """ Loads layout icons. """ for names in self._get_layout_names(): layout_name = names[0] # Python doesn't have an ordered set but we can use a dictionary instead # First key is the layout's name (which may have been set by the user), # the second is the class name. If these are the same (i.e. the user hasn't # set a name) then there is only one key in the dictionary. layouts = dict.fromkeys(names) for layout in layouts.keys(): icon_file_path = self.find_icon_file_path(layout) if icon_file_path: break else: logger.warning('No icon found for layout "%s"', layout_name) icon_file_path = self.find_icon_file_path("unknown") img = Img.from_path(icon_file_path) new_height = (self.bar.height - 2) * self.scale img.resize(height=new_height) if img.width > self.length: self.length = img.width + self.actual_padding * 2 self.surfaces[layout_name] = img self.icons_loaded = True def finalize(self): self._remove_hooks() base._TextBox.finalize(self) qtile-0.31.0/libqtile/widget/base.py0000664000175000017500000010625714762660347017225 0ustar epsilonepsilon# Copyright (c) 2008-2010 Aldo Cortesi # Copyright (c) 2011 Florian Mounier # Copyright (c) 2011 Kenji_Takahashi # Copyright (c) 2011 Paul Colomiets # Copyright (c) 2012 roger # Copyright (c) 2012 Craig Barnes # Copyright (c) 2012-2015 Tycho Andersen # Copyright (c) 2013 dequis # Copyright (c) 2013 David R. Andersen # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014-2015 Sean Vig # Copyright (c) 2014 Justin Bronder # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations import asyncio import copy import math import subprocess from typing import TYPE_CHECKING from libqtile import bar, configurable, confreader from libqtile.command import interface from libqtile.command.base import CommandError, CommandObject, expose_command from libqtile.lazy import LazyCall from libqtile.log_utils import logger from libqtile.utils import ColorType, create_task if TYPE_CHECKING: from typing import Any from libqtile.command.base import ItemT # Each widget class must define which bar orientation(s) it supports by setting # these bits in an 'orientations' class attribute. Simply having the attribute # inherited by superclasses is discouraged, because if a superclass that was # only supporting one orientation, adds support for the other, its subclasses # will have to be adapted too, in general. ORIENTATION_NONE is only added for # completeness' sake. # +------------------------+--------------------+--------------------+ # | Widget bits | Horizontal bar | Vertical bar | # +========================+====================+====================+ # | ORIENTATION_NONE | ConfigError raised | ConfigError raised | # +------------------------+--------------------+--------------------+ # | ORIENTATION_HORIZONTAL | Widget displayed | ConfigError raised | # | | horizontally | | # +------------------------+--------------------+--------------------+ # | ORIENTATION_VERTICAL | ConfigError raised | Widget displayed | # | | | vertically | # +------------------------+--------------------+--------------------+ # | ORIENTATION_BOTH | Widget displayed | Widget displayed | # | | horizontally | vertically | # +------------------------+--------------------+--------------------+ class _Orientations(int): def __new__(cls, value, doc): return super().__new__(cls, value) def __init__(self, value, doc): self.doc = doc def __str__(self): return self.doc def __repr__(self): return self.doc ORIENTATION_NONE = _Orientations(0, "none") ORIENTATION_HORIZONTAL = _Orientations(1, "horizontal only") ORIENTATION_VERTICAL = _Orientations(2, "vertical only") ORIENTATION_BOTH = _Orientations(3, "horizontal and vertical") class _Widget(CommandObject, configurable.Configurable): """Base Widget class If length is set to the special value `bar.STRETCH`, the bar itself will set the length to the maximum remaining space, after all other widgets have been configured. In horizontal bars, 'length' corresponds to the width of the widget; in vertical bars, it corresponds to the widget's height. The offsetx and offsety attributes are set by the Bar after all widgets have been configured. Callback functions can be assigned to button presses by passing a dict to the 'callbacks' kwarg. No arguments are passed to the function so, if you need access to the qtile object, it needs to be imported into your code. ``lazy`` functions can also be passed as callback functions and can be used in the same way as keybindings. For example: .. code-block:: python from libqtile import qtile def open_calendar(): qtile.spawn('gsimplecal next_month') clock = widget.Clock( mouse_callbacks={ 'Button1': open_calendar, 'Button3': lazy.spawn('gsimplecal prev_month') } ) When the clock widget receives a click with button 1, the ``open_calendar`` function will be executed. """ orientations = ORIENTATION_BOTH # Default (empty set) is for all backends to be supported. Widgets can override this # to explicitly confirm which backends are supported supported_backends: set[str] = set() offsetx: int = 0 offsety: int = 0 defaults: list[tuple[str, Any, str]] = [ ("background", None, "Widget background color"), ( "mouse_callbacks", {}, "Dict of mouse button press callback functions. Accepts functions and ``lazy`` calls.", ), ("hide_crash", False, "Don't display error in bar if widget crashes on startup."), ] def __init__(self, length, **config): """ length: bar.STRETCH, bar.CALCULATED, or a specified length. """ CommandObject.__init__(self) self.name = self.__class__.__name__.lower() if "name" in config: self.name = config["name"] configurable.Configurable.__init__(self, **config) self.add_defaults(_Widget.defaults) if length in (bar.CALCULATED, bar.STRETCH): self.length_type = length self.length = 0 elif isinstance(length, int): self.length_type = bar.STATIC self.length = length else: raise confreader.ConfigError("Widget width must be an int") self.configured = False self._futures: list[asyncio.Handle] = [] self._mirrors: set[_Widget] = set() self.finalized = False @property def length(self): if self.length_type == bar.CALCULATED: return int(self.calculate_length()) return self._length @length.setter def length(self, value): self._length = value @property def width(self): if self.bar.horizontal: return self.length return self.bar.width @property def height(self): if self.bar.horizontal: return self.bar.height return self.length @property def offset(self): if self.bar.horizontal: return self.offsetx return self.offsety def _test_orientation_compatibility(self, horizontal): if horizontal: if not self.orientations & ORIENTATION_HORIZONTAL: raise confreader.ConfigError( self.__class__.__name__ + " is not compatible with the orientation of the bar." ) elif not self.orientations & ORIENTATION_VERTICAL: raise confreader.ConfigError( self.__class__.__name__ + " is not compatible with the orientation of the bar." ) def timer_setup(self): """This is called exactly once, after the widget has been configured and timers are available to be set up.""" def _configure(self, qtile, bar): self._test_orientation_compatibility(bar.horizontal) self.qtile = qtile self.bar = bar self.drawer = bar.window.create_drawer(self.bar.width, self.bar.height) # Clear this flag as widget may be restarted (e.g. if screen removed and re-added) self.finalized = False # Timers are added to futures list so they can be cancelled if the `finalize` method is # called before the timers have fired. if not self.configured: timer = self.qtile.call_soon(self.timer_setup) async_timer = self.qtile.call_soon(asyncio.create_task, self._config_async()) # Add these to our list of futures so they can be cancelled. self._futures.extend([timer, async_timer]) async def _config_async(self): """ This is called once when the main eventloop has started. this happens after _configure has been run. Widgets that need to use asyncio coroutines after this point may wish to initialise the relevant code (e.g. connections to dbus using dbus_fast) here. """ def finalize(self): for future in self._futures: future.cancel() if hasattr(self, "layout") and self.layout: self.layout.finalize() self.drawer.finalize() self.finalized = True # Reset configuration status so the widget can be reconfigured # e.g. when screen is re-added self.configured = False def clear(self): self.drawer.set_source_rgb(self.bar.background) self.drawer.fillrect(self.offsetx, self.offsety, self.width, self.height) @expose_command() def info(self): """Info for this object.""" return dict( name=self.name, offset=self.offset, length=self.length, width=self.width, height=self.height, ) def add_callbacks(self, defaults): """Add default callbacks with a lower priority than user-specified callbacks.""" defaults.update(self.mouse_callbacks) self.mouse_callbacks = defaults def button_press(self, x, y, button): name = f"Button{button}" if name in self.mouse_callbacks: cmd = self.mouse_callbacks[name] if isinstance(cmd, LazyCall): if cmd.check(self.qtile): status, val = self.qtile.server.call( (cmd.selectors, cmd.name, cmd.args, cmd.kwargs, False) ) if status in (interface.ERROR, interface.EXCEPTION): logger.error("Mouse callback command error %s: %s", cmd.name, val) else: cmd() def button_release(self, x, y, button): pass def get(self, q, name): """ Utility function for quick retrieval of a widget by name. """ w = q.widgets_map.get(name) if not w: raise CommandError(f"No such widget: {name}") return w def _items(self, name: str) -> ItemT: if name == "bar": return True, [] elif name == "screen": return True, [] return None def _select(self, name, sel): if name == "bar": return self.bar elif name == "screen": return self.bar.screen def draw(self): """ Method that draws the widget. You may call this explicitly to redraw the widget, but only if the length of the widget hasn't changed. If it has, you must call bar.draw instead. """ raise NotImplementedError def calculate_length(self): """ Must be implemented if the widget can take CALCULATED for length. It must return the width of the widget if it's installed in a horizontal bar; it must return the height of the widget if it's installed in a vertical bar. Usually you will test the orientation of the bar with 'self.bar.horizontal'. """ raise NotImplementedError def timeout_add(self, seconds, method, method_args=()): """ This method calls ``.call_later`` with given arguments. """ # Don't add timers for finalised widgets if self.finalized: return future = self.qtile.call_later(seconds, self._wrapper, method, *method_args) self._futures.append(future) return future def call_process(self, command, **kwargs): """ This method uses `subprocess.check_output` to run the given command and return the string from stdout, which is decoded when using Python 3. """ return subprocess.check_output(command, **kwargs, encoding="utf-8") def _remove_dead_timers(self): """Remove completed and cancelled timers from the list.""" def is_ready(timer): return timer in self.qtile._eventloop._ready self._futures = [ timer for timer in self._futures # Filter out certain handles... if not ( timer.cancelled() # Once a scheduled timer is ready to be run its _scheduled flag is set to False # and it's added to the loop's `_ready` queue or ( isinstance(timer, asyncio.TimerHandle) and not timer._scheduled and not is_ready(timer) ) # Callbacks scheduled via `call_soon` are put into the loop's `_ready` queue # and are removed once they've been executed or (isinstance(timer, asyncio.Handle) and not is_ready(timer)) ) ] def _wrapper(self, method, *method_args): self._remove_dead_timers() try: if asyncio.iscoroutinefunction(method): create_task(method(*method_args)) elif asyncio.iscoroutine(method): create_task(method) else: method(*method_args) except: # noqa: E722 logger.exception("got exception from widget timer") def create_mirror(self): return Mirror(self, background=self.background) def clone(self): return copy.deepcopy(self) def mouse_enter(self, x, y): pass def mouse_leave(self, x, y): pass def _draw_with_mirrors(self) -> None: self._old_draw() for mirror in self._mirrors: if not mirror.configured: continue # If the widget and mirror are on the same bar then we could have an # infinite loop when we call bar.draw(). mirror.draw() will trigger a resize # if it's the wrong size. if mirror.length_type == bar.CALCULATED and mirror.bar is not self.bar: mirror.bar.draw() else: mirror.draw() def add_mirror(self, widget: _Widget): if not self._mirrors: self._old_draw = self.draw self.draw = self._draw_with_mirrors # type: ignore self._mirrors.add(widget) if not self.drawer.has_mirrors: self.drawer.has_mirrors = True def remove_mirror(self, widget: _Widget): try: self._mirrors.remove(widget) except KeyError: pass if not self._mirrors: self.drawer.has_mirrors = False if hasattr(self, "_old_draw"): # Deletes the reference to draw and falls back to the original del self.draw del self._old_draw class _TextBox(_Widget): """ Base class for widgets that are just boxes containing text. """ orientations = ORIENTATION_BOTH defaults = [ ("font", "sans", "Default font"), ("fontsize", None, "Font size. Calculated if None."), ("padding", None, "Padding. Calculated if None."), ("foreground", "ffffff", "Foreground colour"), ("fontshadow", None, "font shadow color, default is None(no shadow)"), ("markup", True, "Whether or not to use pango markup"), ( "fmt", "{}", "Format to apply to the string returned by the widget. Main purpose: applying markup. " "For a widget that returns ``foo``, using ``fmt='{}'`` would give you ``foo``. " "To control what the widget outputs in the first place, use the ``format`` paramater of the widget (if it has one).", ), ("max_chars", 0, "Maximum number of characters to display in widget."), ( "scroll", False, "Whether text should be scrolled. When True, you must set the widget's ``width``.", ), ( "scroll_repeat", True, "Whether text should restart scrolling once the text has ended", ), ( "scroll_delay", 2, "Number of seconds to pause before starting scrolling and restarting/clearing text at end", ), ("scroll_step", 1, "Number of pixels to scroll with each step"), ("scroll_interval", 0.1, "Time in seconds before next scrolling step"), ( "scroll_clear", False, "Whether text should scroll completely away (True) or stop when the end of the text is shown (False)", ), ("scroll_hide", False, "Whether the widget should hide when scrolling has finished"), ( "scroll_fixed_width", False, "When ``scroll=True`` the ``width`` parameter is a maximum width and, when text is shorter than this, the widget will resize. " "Setting ``scroll_fixed_width=True`` will force the widget to have a fixed width, regardless of the size of the text.", ), ("rotate", True, "Rotate text in vertical bar."), ( "direction", "default", "Override the text direction in vertical bar, has no effect on text in horizontal bar." "default: text displayed based on vertical bar position (left/right)" "ttb: text read from top to bottom, btt: text read from bottom to top." "'default', 'ttb', 'btt'", ), ] # type: list[tuple[str, Any, str]] def __init__(self, text=" ", width=bar.CALCULATED, **config): self.layout = None _Widget.__init__(self, width, **config) self.add_defaults(_TextBox.defaults) self.text = text self._is_scrolling = False self._should_scroll = False self._scroll_offset = 0 self._scroll_queued = False self._scroll_timer = None self._scroll_width = width @property def text(self): return self._text @text.setter def text(self, value): if len(value) > self.max_chars > 0: value = value[: self.max_chars] + "…" self._text = value if self.layout: # Reset the layout width # Reason is because, if we've manually set the layout width, # adding longer text will result in wrapping which increases the height of the layout. del self.layout.width self.layout.text = self.formatted_text if self.scroll: self.check_width() self.reset_scroll() elif not self.bar.horizontal and not self.rotate: self.layout.width = self.bar.width - 2 * self.actual_padding @property def formatted_text(self): return self.fmt.format(self._text) @property def foreground(self): return self._foreground @foreground.setter def foreground(self, fg): self._foreground = fg if self.layout: self.layout.colour = fg @property def font(self): return self._font @font.setter def font(self, value): self._font = value if self.layout: self.layout.font = value @property def fontshadow(self): return self._fontshadow @fontshadow.setter def fontshadow(self, value): self._fontshadow = value if self.layout: self.layout.font_shadow = value @property def actual_padding(self): if self.padding is None: return self.fontsize / 2 else: return self.padding def _configure(self, qtile, bar): _Widget._configure(self, qtile, bar) if self.fontsize is None: self.fontsize = self.bar.height - self.bar.height / 5 if self.direction not in ("default", "ttb", "btt"): logger.warning( "Invalid value set for direction: %s. Valid values are: 'default', 'ttb', 'btt'. " "direction has been set to 'default'", self.direction, ) self.direction = "default" self.layout = self.drawer.textlayout( self.formatted_text, self.foreground, self.font, self.fontsize, self.fontshadow, markup=self.markup, ) if not isinstance(self._scroll_width, int) and self.scroll: if not self.bar.horizontal and not self.rotate: self._scroll_width = self.bar.width self.scroll_fixed_width = self.bar.width else: logger.warning("%s: You must specify a width when enabling scrolling.", self.name) self.scroll = False if self.scroll: self.check_width() elif not self.bar.horizontal and not self.rotate: self.layout.width = self.bar.width - 2 * self.actual_padding def check_width(self): """ Check whether the widget needs to have calculated or fixed width and whether the text should be scrolled. """ if self.layout.width > self._scroll_width: if self.bar.horizontal or self.rotate: self.length_type = bar.STATIC self.length = self._scroll_width self._is_scrolling = True self._should_scroll = True else: if not self.bar.horizontal and not self.rotate: self.layout.width = self.scroll_fixed_width elif self.scroll_fixed_width: self.length_type = bar.STATIC self.length = self._scroll_width else: self.length_type = bar.CALCULATED self._should_scroll = False def calculate_length(self): if self.text: if self.bar.horizontal: return min(self.layout.width, self.bar.width) + self.actual_padding * 2 else: if self.rotate: return min(self.layout.width, self.bar.height) + self.actual_padding * 2 else: return self.layout.height + self.actual_padding * 2 else: return 0 def can_draw(self): can_draw = ( self.layout is not None and not self.layout.finalized() and self.offsetx is not None ) # if the bar hasn't placed us yet return can_draw def draw(self): if not self.can_draw(): return self.drawer.clear(self.background or self.bar.background) # size = self.bar.height if self.bar.horizontal else self.bar.width self.drawer.ctx.save() if not self.bar.horizontal and self.rotate: # Left bar reads bottom to top # Can be overriden to read bottom to top all the time with vertical_text_direction if ( self.bar.screen.left is self.bar and self.direction == "default" ) or self.direction == "btt": self.drawer.ctx.rotate(-90 * math.pi / 180.0) self.drawer.ctx.translate(-self.length, 0) # Right bar is top to bottom # Can be overriden to read top to bottom all the time with vertical_text_direction elif ( self.bar.screen.right is self.bar and self.direction == "default" ) or self.direction == "ttb": self.drawer.ctx.translate(self.bar.width, 0) self.drawer.ctx.rotate(90 * math.pi / 180.0) # If we're scrolling, we clip the context to the scroll width less the padding # Move the text layout position (and we only see the clipped portion) if self._should_scroll: self.drawer.ctx.rectangle( self.actual_padding, 0, self._scroll_width - 2 * self.actual_padding, self.bar.size, ) self.drawer.ctx.clip() if self.bar.horizontal: size = self.bar.height else: if self.rotate: size = self.bar.width else: size = self.layout.height + self.actual_padding * 2 self.layout.draw( (self.actual_padding or 0) - self._scroll_offset, int(size / 2.0 - self.layout.height / 2.0) + 1, ) self.drawer.ctx.restore() self.drawer.draw( offsetx=self.offsetx, offsety=self.offsety, width=self.width, height=self.height ) # We only want to scroll if: # - User has asked us to scroll and the scroll width is smaller than the layout (should_scroll=True) # - We are still scrolling (is_scrolling=True) # - We haven't already queued the next scroll (scroll_queued=False) if self._should_scroll and self._is_scrolling and not self._scroll_queued: self._scroll_queued = True if self._scroll_offset == 0: interval = self.scroll_delay else: interval = self.scroll_interval self._scroll_timer = self.timeout_add(interval, self.do_scroll) def do_scroll(self): # Allow the next scroll tick to be queued self._scroll_queued = False # If we're still scrolling, adjust the next offset if self._is_scrolling: self._scroll_offset += self.scroll_step # Check whether we need to stop scrolling when: # - we've scrolled all the text off the widget (scroll_clear = True) # - the final pixel is visible (scroll_clear = False) if (self.scroll_clear and self._scroll_offset > self.layout.width) or ( not self.scroll_clear and (self.layout.width - self._scroll_offset) < (self._scroll_width - 2 * self.actual_padding) ): self._is_scrolling = False # We've reached the end of the scroll so what next? if not self._is_scrolling: if self.scroll_repeat: # Pause and restart scrolling self._scroll_timer = self.timeout_add(self.scroll_delay, self.reset_scroll) elif self.scroll_hide: # Clear the text self._scroll_timer = self.timeout_add(self.scroll_delay, self.hide_scroll) # If neither of these options then the text is no longer updated. self.draw() def reset_scroll(self): self._scroll_offset = 0 self._is_scrolling = True self._scroll_queued = False if self._scroll_timer: self._scroll_timer.cancel() self.draw() def hide_scroll(self): self.update("") @expose_command() def set_font( self, font: str | None = None, fontsize: int = 0, fontshadow: ColorType = "", ): """ Change the font used by this widget. If font is None, the current font is used. """ if font is not None: self.font = font if fontsize != 0: self.fontsize = fontsize if fontshadow != "": self.fontshadow = fontshadow self.bar.draw() @expose_command() def info(self): d = _Widget.info(self) d["foreground"] = self.foreground d["text"] = self.formatted_text return d def update(self, text): """Update the widget text.""" # Don't try to update text in dead layouts # This is mainly required for ThreadPoolText based widgets as the # polling function cannot be cancelled and so may be called after the widget # is finalised. if not self.can_draw(): return if self.text == text: return if text is None: text = "" old_width = self.layout.width self.text = text # If our width hasn't changed, we just draw ourselves. Otherwise, # we draw the whole bar. if self.layout.width == old_width and (self.bar.horizontal or self.rotate): self.draw() else: self.bar.draw() class InLoopPollText(_TextBox): """A common interface for polling some 'fast' information, munging it, and rendering the result in a text box. You probably want to use ThreadPoolText instead. ('fast' here means that this runs /in/ the event loop, so don't block! If you want to run something nontrivial, use ThreadPoolText.)""" defaults = [ ( "update_interval", 600, "Update interval in seconds, if none, the widget updates only once.", ), ] # type: list[tuple[str, Any, str]] def __init__(self, default_text="N/A", **config): _TextBox.__init__(self, default_text, **config) self.add_defaults(InLoopPollText.defaults) def timer_setup(self): update_interval = self.tick() # If self.update_interval is defined and .tick() returns None, re-call # after self.update_interval if update_interval is None and self.update_interval is not None: self.timeout_add(self.update_interval, self.timer_setup) # We can change the update interval by returning something from .tick() elif update_interval: self.timeout_add(update_interval, self.timer_setup) # If update_interval is False, we won't re-call def button_press(self, x, y, button): self.tick() _TextBox.button_press(self, x, y, button) def poll(self): return "N/A" def tick(self): text = self.poll() self.update(text) class ThreadPoolText(_TextBox): """A common interface for wrapping blocking events which when triggered will update a textbox. The poll method is intended to wrap a blocking function which may take quite a while to return anything. It will be executed as a future and should return updated text when completed. It may also return None to disable any further updates. param: text - Initial text to display. """ defaults = [ ( "update_interval", 600, "Update interval in seconds, if none, the widget updates only once.", ), ] # type: list[tuple[str, Any, str]] def __init__(self, text="N/A", **config): super().__init__(text, **config) self.add_defaults(ThreadPoolText.defaults) def timer_setup(self): def on_done(future): try: result = future.result() except Exception: result = None logger.exception("poll() raised exceptions, not rescheduling") if result is not None: try: self.update(result) if self.update_interval is not None: self.timeout_add(self.update_interval, self.timer_setup) except Exception: logger.exception("Failed to reschedule timer for %s.", self.name) else: logger.warning("%s's poll() returned None, not rescheduling", self.name) self.future = self.qtile.run_in_executor(self.poll) self.future.add_done_callback(on_done) def poll(self): pass @expose_command() def force_update(self): """Immediately poll the widget. Existing timers are unaffected.""" self.update(self.poll()) # these two classes below look SUSPICIOUSLY similar class PaddingMixin(configurable.Configurable): """Mixin that provides padding(_x|_y|) To use it, subclass and add this to __init__: self.add_defaults(base.PaddingMixin.defaults) """ defaults = [ ("padding", 3, "Padding inside the box"), ("padding_x", None, "X Padding. Overrides 'padding' if set"), ("padding_y", None, "Y Padding. Overrides 'padding' if set"), ] # type: list[tuple[str, Any, str]] padding_x = configurable.ExtraFallback("padding_x", "padding") padding_y = configurable.ExtraFallback("padding_y", "padding") class MarginMixin(configurable.Configurable): """Mixin that provides margin(_x|_y|) To use it, subclass and add this to __init__: self.add_defaults(base.MarginMixin.defaults) """ defaults = [ ("margin", 3, "Margin inside the box"), ("margin_x", None, "X Margin. Overrides 'margin' if set"), ("margin_y", None, "Y Margin. Overrides 'margin' if set"), ] # type: list[tuple[str, Any, str]] margin_x = configurable.ExtraFallback("margin_x", "margin") margin_y = configurable.ExtraFallback("margin_y", "margin") class Mirror(_Widget): """ A widget for showing the same widget content in more than one place, for instance, on bars across multiple screens. You don't need to use it directly; instead, just instantiate your widget once and hand it in to multiple bars. For instance:: cpu = widget.CPUGraph() clock = widget.Clock() screens = [ Screen(top=bar.Bar([widget.GroupBox(), cpu, clock])), Screen(top=bar.Bar([widget.GroupBox(), cpu, clock])), ] Widgets can be passed to more than one bar, so that there don't need to be any duplicates executing the same code all the time, and they'll always be visually identical. This works for all widgets that use `drawers` (and nothing else) to display their contents. Currently, this is all widgets except for `Systray`. """ def __init__(self, reflection, **config): _Widget.__init__(self, reflection.length, **config) self.reflects = reflection self._length = 0 self.length_type = self.reflects.length_type if self.length_type is bar.STATIC: self._length = self.reflects._length def _configure(self, qtile, bar): _Widget._configure(self, qtile, bar) self.reflects.add_mirror(self) # We need to fill the background once before `draw` is called so, if # there's no reflection, the mirror matches its parent bar. self.drawer.clear(self.background or self.bar.background) def calculate_length(self): return self.reflects.calculate_length() @property def length(self): if self.length_type != bar.STRETCH: return self.reflects.length return self._length @length.setter def length(self, value): self._length = value def draw(self): if self.length <= 0: return self.drawer.clear_rect() self.reflects.drawer.paint_to(self.drawer) self.drawer.draw(offsetx=self.offset, offsety=self.offsety, width=self.width) def button_press(self, x, y, button): self.reflects.button_press(x, y, button) def mouse_enter(self, x, y): self.reflects.mouse_enter(x, y) def mouse_leave(self, x, y): self.reflects.mouse_leave(x, y) def finalize(self): self.reflects.remove_mirror(self) _Widget.finalize(self) qtile-0.31.0/libqtile/widget/khal_calendar.py0000664000175000017500000001124314762660347021051 0ustar epsilonepsilon################################################################### # This widget will display the next appointment on your calendar in # the qtile status bar. Appointments within the "reminder" time will be # highlighted. Authentication credentials are stored on disk. # # This widget uses the khal command line calendar utility available at # https://github.com/geier/khal # # This widget also requires the dateutil.parser module. # If you get a strange "AttributeError: 'module' object has no attribute # GoogleCalendar" error, you are probably missing a module. Check # carefully. # # Thanks to the creator of the YahooWeather widget (dmpayton). This code # borrows liberally from that one. # # Copyright (c) 2016 by David R. Andersen # New khal output format adjustment, 2016 Christoph Lassner # Licensed under the Gnu Public License # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. ################################################################### import datetime import string import subprocess import dateutil.parser from libqtile.widget import base class KhalCalendar(base.ThreadPoolText): """Khal calendar widget This widget will display the next appointment on your Khal calendar in the qtile status bar. Appointments within the "reminder" time will be highlighted. Widget requirements: dateutil_. .. _dateutil: https://pypi.org/project/python-dateutil/ """ defaults = [ ("reminder_color", "FF0000", "color of calendar entries during reminder time"), ("foreground", "FFFF33", "default foreground color"), ("remindertime", 10, "reminder time in minutes"), ("lookahead", 7, "days to look ahead in the calendar"), ] def __init__(self, **config): base.ThreadPoolText.__init__(self, "", **config) self.add_defaults(KhalCalendar.defaults) self.text = "Calendar not initialized." self.default_foreground = self.foreground def poll(self): # get today and tomorrow now = datetime.datetime.now() # get reminder time in datetime format remtime = datetime.timedelta(minutes=self.remindertime) # parse khal output for the next seven days # and get the next event args = ["khal", "list", "now", str(self.lookahead) + "d"] cal = subprocess.Popen(args, stdout=subprocess.PIPE) output = cal.communicate()[0].decode("utf-8") if output == "No events\n": return "No appointments in next " + str(self.lookahead) + " days" output = output.split("\n") date = "unknown" starttime = None endtime = None # output[0] = 'Friday, 15/04/1976' outputsplitted = output[0].split(" ") date = outputsplitted[1] # output[1] = '[ ][12:00-13:00] dentist' try: output_nb = output[1].strip(" ") starttime = dateutil.parser.parse(date + " " + output_nb[:5], ignoretz=True) endtime = dateutil.parser.parse(date + " " + output_nb[6:11], ignoretz=True) except ValueError: # all day event output contains no start nor end time. starttime = dateutil.parser.parse(date + " 00:00", ignoretz=True) endtime = starttime + datetime.timedelta(hours=23, minutes=59) data = output[0].replace(",", "") + " " + output[1] # get rid of any garbage in appointment added by khal data = "".join(filter(lambda x: x in string.printable, data)) # colorize the event if it is within reminder time if (starttime - remtime <= now) and (endtime > now): self.foreground = self.reminder_color else: self.foreground = self.default_foreground return data qtile-0.31.0/libqtile/widget/check_updates.py0000664000175000017500000001444014762660347021105 0ustar epsilonepsilon# Copyright (c) 2015 Ali Mousavi # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import os from subprocess import CalledProcessError, Popen from libqtile.log_utils import logger from libqtile.widget import base # format: "Distro": ("cmd", "number of lines to subtract from output") CMD_DICT = { "Arch": ("pacman -Qu", 0), "Arch_checkupdates": ("checkupdates", 0), "Arch_Sup": ("pacman -Sup", 0), "Arch_paru": ("paru -Qu", 0), "Arch_paru_Sup": ("paru -Sup", 0), "Arch_yay": ("yay -Qu", 0), "Debian": ("apt-show-versions -u -b", 0), "Gentoo_eix": ("EIX_LIMIT=0 eix -u# --world", 0), "Guix": ("guix upgrade --dry-run", 0), "Ubuntu": ("aptitude search ~U", 0), "Fedora": ("dnf list --upgrades -q", 1), "FreeBSD": ("pkg upgrade -n | awk '/\t/ { print $0 }'", 0), "Mandriva": ("urpmq --auto-select", 0), "Void": ("xbps-install -nuMS", 0), } # We need the spaces here to ensure the indentation is correct in the docstring CMD_DOC_COMMANDS = "\n".join(f" * ``'{k}'`` runs ``{v}``" for k, v in CMD_DICT.items()) class CheckUpdates(base.ThreadPoolText): # The docstring includes some dynamic content so we need to compile that content # first and then set the docstring to that content. _doc = f""" Shows number of pending updates in different unix systems. The following built-in options are available via the ``distro`` parameter: {CMD_DOC_COMMANDS} .. note:: It is common for package managers to return a non-zero code when there are no updates. As a result, the widget will treat *any* error as if there are no updates. If you are using a custom commmand/script, you should therefore ensure that it returns zero when it completes if you wish to see the output of your command. In addition, as no errors are recorded to the log, if the widget is showing no updates and you believe that to be incorrect, you should run the appropriate command in a terminal to view any error messages. """ __doc__ = _doc defaults = [ ("distro", "Arch", "Name of your distribution"), ( "custom_command", None, "Custom shell command for checking updates (counts the lines of the output)", ), ( "custom_command_modify", (lambda x: x), "Lambda function to modify line count from custom_command", ), ( "initial_text", "", "Draw the widget immediately with an initial text, " "useful if it takes time to check system updates.", ), ("update_interval", 60, "Update interval in seconds."), ("execute", None, "Command to execute on click"), ("display_format", "Updates: {updates}", "Display format if updates available"), ("colour_no_updates", "ffffff", "Colour when there's no updates."), ("colour_have_updates", "ffffff", "Colour when there are updates."), ("restart_indicator", "", "Indicator to represent reboot is required. (Ubuntu only)"), ("no_update_string", "", "String to display if no updates available"), ] def __init__(self, **config): base.ThreadPoolText.__init__(self, config.pop("initial_text", ""), **config) self.add_defaults(CheckUpdates.defaults) # Helpful to have this as a variable as we can shorten it for testing self.execute_polling_interval = 1 if self.custom_command: # Use custom_command self.cmd = self.custom_command else: # Check if distro name is valid. try: self.cmd = CMD_DICT[self.distro][0] self.custom_command_modify = lambda x: x - CMD_DICT[self.distro][1] except KeyError: distros = sorted(CMD_DICT.keys()) logger.error( "%s is not a valid distro name. Use one of the list: %s.", self.distro, str(distros), ) self.cmd = None if self.execute: self.add_callbacks({"Button1": self.do_execute}) def _check_updates(self): # type: () -> str try: updates = self.call_process(self.cmd, shell=True) except CalledProcessError: updates = "" num_updates = self.custom_command_modify(len(updates.splitlines())) if num_updates < 0: num_updates = 0 if num_updates == 0: self.layout.colour = self.colour_no_updates return self.no_update_string num_updates = str(num_updates) if self.restart_indicator and os.path.exists("/var/run/reboot-required"): num_updates += self.restart_indicator self.layout.colour = self.colour_have_updates return self.display_format.format(updates=num_updates) def poll(self): # type: () -> str if not self.cmd: return "N/A" return self._check_updates() def do_execute(self): self._process = Popen(self.execute, shell=True) self.timeout_add(self.execute_polling_interval, self._refresh_count) def _refresh_count(self): if self._process.poll() is None: self.timeout_add(self.execute_polling_interval, self._refresh_count) else: self.timer_setup() qtile-0.31.0/libqtile/widget/spacer.py0000664000175000017500000000451114762660347017556 0ustar epsilonepsilon# Copyright (c) 2008, 2010 Aldo Cortesi # Copyright (c) 2011 Mounier Florian # Copyright (c) 2012 Tim Neumann # Copyright (c) 2012 Craig Barnes # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Adi Sieker # Copyright (c) 2014 Tycho Andersen # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from libqtile import bar from libqtile.widget import base class Spacer(base._Widget): """Just an empty space on the bar Often used with length equal to bar.STRETCH to push bar widgets to the right or bottom edge of the screen. Parameters ========== length : Length of the widget. Can be either ``bar.STRETCH`` or a length in pixels. width : DEPRECATED, same as ``length``. """ orientations = base.ORIENTATION_BOTH defaults = [("background", None, "Widget background color")] def __init__(self, length=bar.STRETCH, **config): """ """ base._Widget.__init__(self, length, **config) self.add_defaults(Spacer.defaults) def draw(self): if self.length > 0: self.drawer.clear(self.background or self.bar.background) if self.bar.horizontal: self.drawer.draw(offsetx=self.offset, offsety=self.offsety, width=self.length) else: self.drawer.draw(offsety=self.offset, offsetx=self.offsetx, height=self.length) qtile-0.31.0/libqtile/widget/canto.py0000664000175000017500000000471714762660347017415 0ustar epsilonepsilon# Copyright (c) 2011 Kenji_Takahashi # Copyright (c) 2011 Mounier Florian # Copyright (c) 2012, 2014 Tycho Andersen # Copyright (c) 2014-2015 Sean Vig # Copyright (c) 2014 Adi Sieker # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from subprocess import call from libqtile.widget import base class Canto(base.ThreadPoolText): """Display RSS feeds updates using the canto console reader Widget requirements: canto_ .. _canto: https://codezen.org/canto-ng/ """ defaults = [ ("fetch", False, "Whether to fetch new items on update"), ("feeds", [], "List of feeds to display, empty for all"), ("one_format", "{name}: {number}", "One feed display format"), ("all_format", "{number}", "All feeds display format"), ] def __init__(self, **config): base.ThreadPoolText.__init__(self, "", **config) self.add_defaults(Canto.defaults) def poll(self): if not self.feeds: arg = "-a" if self.fetch: arg += "u" output = self.all_format.format(number=self.call_process(["canto", arg])[:-1]) return output else: if self.fetch: call(["canto", "-u"]) return "".join( [ self.one_format.format( name=feed, number=self.call_process(["canto", "-n", feed])[:-1] ) for feed in self.feeds ] ) qtile-0.31.0/libqtile/widget/crashme.py0000664000175000017500000000423014762660347017721 0ustar epsilonepsilon# Copyright (c) 2012 Florian Mounier # Copyright (c) 2012 roger # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2013 Craig Barnes # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Adi Sieker # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from libqtile import bar from libqtile.widget import base class _CrashMe(base._TextBox): """A developer widget to force a crash in qtile Pressing left mouse button causes a zero divison error. Pressing the right mouse button causes a cairo draw error. Parameters ========== width : A fixed width, or bar.CALCULATED to calculate the width automatically (which is recommended). """ def __init__(self, width=bar.CALCULATED, **config): base._TextBox.__init__(self, "Crash me !", width, **config) def _configure(self, qtile, bar): base._Widget._configure(self, qtile, bar) self.layout = self.drawer.textlayout( self.text, self.foreground, self.font, self.fontsize, self.fontshadow, markup=True ) def button_press(self, x, y, button): if button == 1: 1 / 0 elif button == 3: self.text = "\xc3GError" qtile-0.31.0/libqtile/widget/load.py0000664000175000017500000000431714762660347017224 0ustar epsilonepsilon# Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from itertools import cycle from psutil import getloadavg from libqtile.command.base import expose_command from libqtile.widget import base class Load(base.ThreadPoolText): """ A small widget to show the load averages of the system. Depends on psutil. """ defaults = [ ("update_interval", 1.0, "The update interval for the widget"), ("format", "Load({time}):{load:.2f}", "The format in which to display the results."), ] times = ["1m", "5m", "15m"] def __init__(self, **config): super().__init__("", **config) self.add_defaults(Load.defaults) self.add_callbacks({"Button1": self.next_load}) self.cycled_times = cycle(Load.times) self.set_time() def set_time(self): self.time = next(self.cycled_times) @expose_command() def next_load(self): self.set_time() self.update(self.poll()) def poll(self): loads = {} ( loads["1m"], loads["5m"], loads["15m"], ) = getloadavg() # Gets the load averages as a dictionary. load = loads[self.time] return self.format.format(time=self.time, load=load) qtile-0.31.0/libqtile/widget/battery.py0000664000175000017500000005637214762660347017767 0ustar epsilonepsilon# Copyright (c) 2011 matt # Copyright (c) 2011 Paul Colomiets # Copyright (c) 2011-2014 Tycho Andersen # Copyright (c) 2012 dmpayton # Copyright (c) 2012 hbc # Copyright (c) 2012 Tim Neumann # Copyright (c) 2012 uberj # Copyright (c) 2012-2013 Craig Barnes # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 dequis # Copyright (c) 2014 Sebastien Blot # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations import os import platform import re from abc import ABC, abstractmethod from enum import Enum, unique from pathlib import Path from subprocess import CalledProcessError, check_output from typing import TYPE_CHECKING, NamedTuple from libqtile import bar, configurable, images from libqtile.command.base import expose_command from libqtile.images import Img from libqtile.log_utils import logger from libqtile.utils import send_notification from libqtile.widget import base if TYPE_CHECKING: from typing import Any from libqtile.utils import ColorsType @unique class BatteryState(Enum): CHARGING = 1 DISCHARGING = 2 FULL = 3 EMPTY = 4 NOT_CHARGING = 5 UNKNOWN = 6 class BatteryStatus(NamedTuple): state: BatteryState percent: float power: float time: int charge_start_threshold: int charge_end_threshold: int class _Battery(ABC): """ Battery interface class specifying what member functions a battery implementation should provide. """ @abstractmethod def update_status(self) -> BatteryStatus: """Read the battery status Reads the battery status from the system and returns the result. Raises RuntimeError on error. """ def load_battery(**config) -> _Battery: """Default battery loading function Loads and returns the _Battery interface suitable for the current running platform. Parameters ---------- config: Dictionary of config options that are passed to the generated battery. Return ------ The configured _Battery for the current platform. """ system = platform.system() if system == "FreeBSD": return _FreeBSDBattery(str(config.get("battery", 0))) elif system == "Linux": return _LinuxBattery(**config) else: raise RuntimeError("Unknown platform!") class _FreeBSDBattery(_Battery): """ A battery class compatible with FreeBSD. Reads battery status using acpiconf. Takes a battery setting containing the number of the battery that should be monitored. """ def __init__(self, battery="0") -> None: self.battery = battery def update_status(self) -> BatteryStatus: try: info = check_output(["acpiconf", "-i", self.battery]).decode("utf-8") except CalledProcessError: raise RuntimeError("acpiconf exited incorrectly") stat_match = re.search(r"State:\t+([a-z]+)", info) if stat_match is None: raise RuntimeError("Could not get battery state!") stat = stat_match.group(1) if stat == "charging": state = BatteryState.CHARGING elif stat == "discharging": state = BatteryState.DISCHARGING elif stat == "high": state = BatteryState.FULL else: state = BatteryState.UNKNOWN percent_re = re.search(r"Remaining capacity:\t+([0-9]+)", info) if percent_re: percent = int(percent_re.group(1)) / 100 else: raise RuntimeError("Could not get battery percentage!") power_re = re.search(r"Present rate:\t+(?:[0-9]+ mA )*\(?([0-9]+) mW", info) if power_re: power = float(power_re.group(1)) / 1000 else: raise RuntimeError("Could not get battery power!") time_re = re.search(r"Remaining time:\t+([0-9]+:[0-9]+|unknown)", info) if time_re: if time_re.group(1) == "unknown": time = 0 else: hours, _, minutes = time_re.group(1).partition(":") time = int(hours) * 3600 + int(minutes) * 60 else: raise RuntimeError("Could not get remaining battery time!") return BatteryStatus( state, percent=percent, power=power, time=time, charge_start_threshold=0, charge_end_threshold=100, ) def connected_to_thunderbolt(): try: sysfs = "/sys/bus/thunderbolt/devices" entries = os.listdir(sysfs) for e in entries: try: name = Path(sysfs, e, "device_name").read_text() except FileNotFoundError: continue else: logger.debug("found dock %s", name) return True except OSError: logger.debug("failed to detect thunderbot %s", exc_info=True) return False def thunderbolt_smart_charge() -> tuple[int, int]: # if we are thunderbolt docked, set the thresholds to 40/50, per # https://support.lenovo.com/us/en/solutions/ht078208-how-can-i-increase-battery-life-thinkpad-and-lenovo-vbke-series-notebooks if connected_to_thunderbolt(): return (40, 50) return (0, 90) class _LinuxBattery(_Battery, configurable.Configurable): defaults = [ ("status_file", None, "Name of status file in /sys/class/power_supply/battery_name"), ( "energy_now_file", None, "Name of file with the current energy in /sys/class/power_supply/battery_name", ), ( "energy_full_file", None, "Name of file with the maximum energy in /sys/class/power_supply/battery_name", ), ( "power_now_file", None, "Name of file with the current power draw in /sys/class/power_supply/battery_name", ), ( "charge_controller", None, """ A function that takes no arguments and returns (start, end) charge thresholds, e.g. ``lambda: (0, 90)``; set to None to disable smart charging. """, ), ( "force_charge", False, "Whether or not to ignore the result of charge_controller() and charge to 100%", ), ] filenames: dict = {} BAT_DIR = "/sys/class/power_supply" BATTERY_INFO_FILES = { "energy_now_file": ["energy_now", "charge_now"], "energy_full_file": ["energy_full", "charge_full"], "power_now_file": ["power_now", "current_now"], "voltage_now_file": ["voltage_now"], "status_file": ["status"], } def __init__(self, **config): _LinuxBattery.defaults.append( ("battery", self._get_battery_name(), "ACPI name of a battery, usually BAT0") ) configurable.Configurable.__init__(self, **config) self.add_defaults(_LinuxBattery.defaults) if isinstance(self.battery, int): self.battery = f"BAT{self.battery}" self.charge_threshold_supported = True def _get_battery_name(self): if os.path.isdir(self.BAT_DIR): bats = [f for f in os.listdir(self.BAT_DIR) if f.startswith("BAT")] if bats: return bats[0] return "BAT0" def _load_file(self, name) -> tuple[str, str] | None: path = os.path.join(self.BAT_DIR, self.battery, name) if "energy" in name or "power" in name: value_type = "uW" elif "charge" in name: value_type = "uAh" elif "current" in name: value_type = "uA" else: value_type = "" try: with open(path) as f: return f.read().strip(), value_type except OSError as e: logger.debug("Failed to read '%s':", path, exc_info=True) if isinstance(e, FileNotFoundError): # Let's try another file if this one doesn't exist return None # Do not fail if the file exists but we can not read it this time # See https://github.com/qtile/qtile/pull/1516 for rationale return "-1", "N/A" def _get_param(self, name) -> tuple[str, str]: if name in self.filenames and self.filenames[name]: result = self._load_file(self.filenames[name]) if result is not None: return result # Don't have the file name cached, figure it out # Don't modify the global list! Copy with [:] file_list = self.BATTERY_INFO_FILES.get(name, [])[:] user_file_name = getattr(self, name, None) if user_file_name is not None: file_list.insert(0, user_file_name) # Iterate over the possibilities, and return the first valid value for filename in file_list: value = self._load_file(filename) if value is not None: self.filenames[name] = filename return value raise RuntimeError(f"Unable to read status for {name}") def set_battery_charge_thresholds(self, start, end): if not self.charge_threshold_supported: return battery_dir = "/sys/class/power_supply" path = os.path.join(battery_dir, self.battery, "charge_control_start_threshold") try: with open(path, "w+") as f: f.write(str(start)) except FileNotFoundError: self.charge_threshold_supported = False except OSError: logger.debug("Failed to write %s", path, exc_info=True) path = os.path.join(battery_dir, self.battery, "charge_control_end_threshold") try: with open(path, "w+") as f: f.write(str(end)) except FileNotFoundError: self.charge_threshold_supported = False except OSError: logger.debug("Failed to write %s", path, exc_info=True) return (start, end) def update_status(self) -> BatteryStatus: charge_start_threshold = 0 charge_end_threshold = 100 if self.charge_controller is not None and self.charge_threshold_supported: (charge_start_threshold, charge_end_threshold) = self.charge_controller() if self.force_charge: charge_start_threshold = 0 charge_end_threshold = 100 self.set_battery_charge_thresholds(charge_start_threshold, charge_end_threshold) stat = self._get_param("status_file")[0] if stat == "Full": state = BatteryState.FULL elif stat == "Charging": state = BatteryState.CHARGING elif stat == "Discharging": state = BatteryState.DISCHARGING elif stat == "Not charging": state = BatteryState.NOT_CHARGING else: state = BatteryState.UNKNOWN now_str, now_unit = self._get_param("energy_now_file") full_str, full_unit = self._get_param("energy_full_file") power_str, power_unit = self._get_param("power_now_file") # the units of energy is uWh or uAh, multiply to get to uWs or uAs now = 3600 * float(now_str) full = 3600 * float(full_str) power = float(power_str) if now_unit != full_unit: raise RuntimeError("Current and full energy units do not match") if full == 0: percent = 0.0 else: percent = now / full if power == 0: time = 0 elif state == BatteryState.DISCHARGING: time = int(now / power) else: time = int((full - now) / power) if power_unit == "uA": voltage = float(self._get_param("voltage_now_file")[0]) power = voltage * power / 1e12 elif power_unit == "uW": power = power / 1e6 return BatteryStatus( state=state, percent=percent, power=power, time=time, charge_start_threshold=charge_start_threshold, charge_end_threshold=charge_end_threshold, ) class Battery(base.ThreadPoolText): """ A text-based battery monitoring widget supporting both Linux and FreeBSD. The Linux version of this widget has functionality to charge "smartly" (i.e. not to 100%) under user defined conditions, and provides some implementations for doing so. For example, to only charge the battery to 90%, use: .. code-block:: python Battery(..., charge_controller=lambda: (0, 90)) The battery widget also supplies some charging algorithms. To only charge the battery between 40-50% while connected to a thunderbolt docking station, but 90% all other times, use: .. code-block:: python from libqtile.widget.battery import thunderbolt_smart_charge Battery(..., charge_controller=thunderbolt_smart_charge) To temporarily disable/re-enable this (e.g. if you know you're going mobile and need to charge) use either: .. code-block:: bash qtile cmd-obj -o bar top widget battery -f charge_to_full qtile cmd-obj -o bar top widget battery -f charge_dynamically or bind a key to: .. code-block:: python Key([mod, "shift"], "c", lazy.widget['battery'].charge_to_full()) Key([mod, "shift"], "x", lazy.widget['battery'].charge_dynamically()) note that this functionality requires qtile to be able to write to certain files in sysfs, so make sure that qtile's udev rules are installed correctly. """ background: ColorsType | None low_background: ColorsType | None defaults = [ ("charge_char", "^", "Character to indicate the battery is charging"), ("discharge_char", "V", "Character to indicate the battery is discharging"), ("full_char", "=", "Character to indicate the battery is full"), ("empty_char", "x", "Character to indicate the battery is empty"), ("not_charging_char", "*", "Character to indicate the batter is not charging"), ("unknown_char", "?", "Character to indicate the battery status is unknown"), ("format", "{char} {percent:2.0%} {hour:d}:{min:02d} {watt:.2f} W", "Display format"), ("hide_threshold", None, "Hide the text when there is enough energy 0 <= x < 1"), ( "full_short_text", "Full", "Short text to indicate battery is full; see `show_short_text`", ), ( "empty_short_text", "Empty", "Short text to indicate battery is empty; see `show_short_text`", ), ( "show_short_text", True, "Show only characters rather than formatted text when battery is full or empty", ), ("low_percentage", 0.10, "Indicates when to use the low_foreground color 0 < x < 1"), ("low_foreground", "FF0000", "Font color on low battery"), ("low_background", None, "Background color on low battery"), ("update_interval", 60, "Seconds between status updates"), ("battery", 0, "Which battery should be monitored (battery number or name)"), ("notify_below", None, "Send a notification below this battery level."), ("notification_timeout", 10, "Time in seconds to display notification. 0 for no expiry."), ] def __init__(self, **config) -> None: base.ThreadPoolText.__init__(self, "", **config) self.add_defaults(self.defaults) self._battery = self._load_battery(**config) self._has_notified = False self.timeout = int(self.notification_timeout * 1000) def _configure(self, qtile, bar): if not self.low_background: self.low_background = self.background self.normal_background = self.background base.ThreadPoolText._configure(self, qtile, bar) @expose_command() def charge_to_full(self): self._battery.force_charge = True @expose_command() def charge_dynamically(self): self._battery.force_charge = False @staticmethod def _load_battery(**config): """Function used to load the Battery object Battery behavior can be changed by overloading this function in a base class. """ return load_battery(**config) def poll(self) -> str: """Determine the text to display Function returning a string with battery information to display on the status bar. Should only use the public interface in _Battery to get necessary information for constructing the string. """ try: status = self._battery.update_status() except RuntimeError as e: return f"Error: {e}" if self.notify_below: percent = int(status.percent * 100) if percent < self.notify_below: if not self._has_notified: send_notification( "Warning", f"Battery at {status.percent:2.0%}", urgent=True, timeout=self.timeout, ) self._has_notified = True elif self._has_notified: self._has_notified = False return self.build_string(status) def build_string(self, status: BatteryStatus) -> str: """Determine the string to return for the given battery state Parameters ---------- status: The current status of the battery Returns ------- str The string to display for the current status. """ if self.hide_threshold is not None and status.percent > self.hide_threshold: return "" if self.layout is not None: if status.state == BatteryState.DISCHARGING and status.percent < self.low_percentage: self.layout.colour = self.low_foreground self.background = self.low_background else: self.layout.colour = self.foreground self.background = self.normal_background if status.state == BatteryState.CHARGING: char = self.charge_char elif status.state == BatteryState.DISCHARGING: char = self.discharge_char elif status.state == BatteryState.FULL: if self.show_short_text: return self.full_short_text char = self.full_char elif status.state == BatteryState.EMPTY or ( status.state == BatteryState.UNKNOWN and status.percent == 0 ): if self.show_short_text: return self.empty_short_text char = self.empty_char elif status.state == BatteryState.NOT_CHARGING: char = self.not_charging_char else: char = self.unknown_char hour = status.time // 3600 minute = (status.time // 60) % 60 return self.format.format( char=char, percent=status.percent, watt=status.power, hour=hour, min=minute ) def default_icon_path() -> str: """Get the default path to battery icons""" dir_path = Path(__file__).resolve() / ".." / ".." / "resources" / "battery-icons" return str(dir_path.resolve()) class BatteryIcon(base._Widget): """Battery life indicator widget.""" orientations = base.ORIENTATION_HORIZONTAL defaults: list[tuple[str, Any, str]] = [ ("battery", 0, "Which battery should be monitored"), ("update_interval", 60, "Seconds between status updates"), ("theme_path", default_icon_path(), "Path of the icons"), ("scale", 1, "Scale factor relative to the bar height. " "Defaults to 1"), ("padding", 0, "Additional padding either side of the icon"), ] icon_names = ( "battery-missing", "battery-caution", "battery-low", "battery-good", "battery-full", "battery-caution-charging", "battery-low-charging", "battery-good-charging", "battery-full-charging", "battery-full-charged", ) def __init__(self, **config) -> None: base._Widget.__init__(self, length=bar.CALCULATED, **config) self.add_defaults(self.defaults) self.scale: float = 1.0 / self.scale self.image_padding = 0 self.images: dict[str, Img] = {} self.current_icon = "battery-missing" self._battery = self._load_battery(**config) @staticmethod def _load_battery(**config): """Function used to load the Battery object Battery behavior can be changed by overloading this function in a base class. """ return load_battery(**config) def timer_setup(self) -> None: self.update() self.timeout_add(self.update_interval, self.timer_setup) def _configure(self, qtile, bar) -> None: base._Widget._configure(self, qtile, bar) self.setup_images() def setup_images(self) -> None: d_imgs = images.Loader(self.theme_path)(*self.icon_names) new_height = self.bar.height * self.scale for key, img in d_imgs.items(): img.resize(height=new_height) self.images[key] = img def calculate_length(self): if not self.images: return 0 icon = self.images[self.current_icon] return icon.width + 2 * self.padding def update(self) -> None: status = self._battery.update_status() icon = self._get_icon_key(status) if icon != self.current_icon: self.current_icon = icon self.draw() def draw(self) -> None: self.drawer.clear(self.background or self.bar.background) image = self.images[self.current_icon] self.drawer.ctx.save() self.drawer.ctx.translate(self.padding, (self.bar.height - image.height) // 2) self.drawer.ctx.set_source(image.pattern) self.drawer.ctx.paint() self.drawer.ctx.restore() self.drawer.draw(offsetx=self.offset, offsety=self.offsety, width=self.length) @staticmethod def _get_icon_key(status: BatteryStatus) -> str: key = "battery" percent = status.percent if percent < 0.2: key += "-caution" elif percent < 0.4: key += "-low" elif percent < 0.8: key += "-good" else: key += "-full" state = status.state if state == BatteryState.CHARGING: key += "-charging" elif state == BatteryState.FULL: key += "-charged" return key qtile-0.31.0/libqtile/widget/textbox.py0000664000175000017500000000430214762660347017774 0ustar epsilonepsilon# Copyright (c) 2008, 2010 Aldo Cortesi # Copyright (c) 2011 Mounier Florian # Copyright (c) 2012, 2015 Tycho Andersen # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2013 Craig Barnes # Copyright (c) 2014 Sean Vig # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from typing import Any from libqtile import bar from libqtile.command.base import expose_command from libqtile.widget import base class TextBox(base._TextBox): """A flexible textbox that can be updated from bound keys, scripts, and qshell.""" defaults: list[tuple[str, Any, str]] = [ ("font", "sans", "Text font"), ("fontsize", None, "Font pixel size. Calculated if None."), ("fontshadow", None, "font shadow color, default is None(no shadow)"), ("padding", None, "Padding left and right. Calculated if None."), ("foreground", "#ffffff", "Foreground colour."), ] def __init__(self, text=" ", width=bar.CALCULATED, **config): base._TextBox.__init__(self, text=text, width=width, **config) @expose_command() def get(self): """Retrieve the text in a TextBox widget""" return self.text @expose_command() def update(self, text): base._TextBox.update(self, text) qtile-0.31.0/libqtile/widget/cmus.py0000664000175000017500000002044414762660347017253 0ustar epsilonepsilon# Copyright (C) 2015, Juan Riquelme González # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import datetime import math import subprocess from functools import partial from libqtile import pangocffi from libqtile.widget import base def format_time(time_seconds_string): """Format time in seconds as [h:]mm:ss.""" return str(datetime.timedelta(seconds=float(time_seconds_string))).lstrip("0").lstrip(":") class Cmus(base.ThreadPoolText): """A simple Cmus widget. Show the metadata of now listening song and allow basic mouse control from the bar: - toggle pause (or play if stopped) on left click; - skip forward in playlist on scroll up; - skip backward in playlist on scroll down. The following fields (extracted from ``cmus-remote -C status``) are available in the `format` string: - ``status``: cmus playback status, one of "playing", "paused" or "stopped". - ``file`` - ``position``: Current position in [h:]mm:ss. - ``position_percent``: Current position in percent. - ``remaining``: Remaining time in [h:]mm:ss. - ``remaining_percent``: Remaining time in percent. - ``duration``: Total length in [h:]mm:ss. - ``artist`` - ``album`` - ``albumartist`` - ``composer`` - ``comment`` - ``date`` - ``discnumber`` - ``genre`` - ``title``: Title or filename if no title is available. - ``tracknumber`` - ``stream`` - ``status_text``: Text indicating the playback status, corresponds to one of `playing_text`, `paused_text` or `stopped_text`. Cmus (https://cmus.github.io) should be installed. """ defaults = [ ("format", "{status_text}{artist} - {title}", "Format of playback info."), ("stream_format", "{status_text}{stream}", "Format of playback info for streams."), ( "no_artist_format", "{status_text}{title}", "Format of playback info if no artist available.", ), ("playing_text", "♫ ", "Text to display when playing, if chosen."), ("playing_color", "00ff00", "Text colour when playing."), ("paused_text", "♫ ", "Text to display when paused, if chosen."), ("paused_color", "cecece", "Text color when paused."), ("stopped_text", "♫ ", "Text to display when stopped, if chosen."), ("stopped_color", "cecece", "Text color when stopped."), ("update_interval", 0.5, "Update Time in seconds."), ( "play_icon", "♫ ", "DEPRECATED Text to display when playing, paused, and stopped, if chosen.", ), ("play_color", "", "DEPRECATED Text colour when playing."), ("noplay_color", "", "DEPRECATED Text colour when paused or stopped."), ] def __init__(self, **config): base.ThreadPoolText.__init__(self, "", **config) self.add_defaults(Cmus.defaults) self.status = "" self.local = None self.add_callbacks( { "Button1": self.play, "Button4": partial(subprocess.Popen, ["cmus-remote", "-n"]), "Button5": partial(subprocess.Popen, ["cmus-remote", "-r"]), } ) def _configure(self, qtile, parent_bar): base.ThreadPoolText._configure(self, qtile, parent_bar) # Backwards compatibility if self.play_color: self.playing_color = self.play_color self.paused_color = self.play_color if self.noplay_color: self.stopped_color = self.noplay_color def get_info(self): """Return a dictionary with info about the current cmus status.""" try: output = self.call_process(["cmus-remote", "-C", "status"]) except subprocess.CalledProcessError as err: output = err.output if output.startswith("status"): output = output.splitlines() info = { "status": "", "file": "", "position": "", "position_percent": "", "remaining": "", "remaining_percent": "", "duration": "", "artist": "", "album": "", "albumartist": "", "composer": "", "comment": "", "date": "", "discnumber": "", "genre": "", "title": "", "tracknumber": "", "stream": "", "status_text": "", "play_icon": self.play_icon, } for line in output: if line.startswith("set"): break for data in info: match = data + " " if match in line: index = line.index(data) if index < 5: info[data] = line[len(data) + index :].strip() break # Set status text status = info["status"] info["status_text"] = getattr(self, f"{status}_text", self.stopped_text) # Format and process duration and position if info["position"] != "" and info["duration"] != "" and int(info["duration"]) > 0: info["position_percent"] = ( str(math.floor(int(info["position"]) / int(info["duration"]) * 100)) + "%" ) info["remaining_percent"] = ( str( math.ceil( (int(info["duration"]) - int(info["position"])) / int(info["duration"]) * 100 ) ) + "%" ) info["remaining"] = format_time(int(info["duration"]) - int(info["position"])) info["position"] = format_time(info["position"]) info["duration"] = format_time(info["duration"]) else: info["duration"] = "" info["position"] = "" return info def now_playing(self): """Return a string with the now playing info.""" info = self.get_info() now_playing = "" if info: display_format = self.format status = info["status"] if self.status != status: self.status = status if self.status == "playing": self.layout.colour = self.playing_color elif self.status == "paused": self.layout.colour = self.paused_color else: self.layout.colour = self.stopped_color self.local = info["file"].startswith("/") if self.local: if not info["title"]: info["title"] = info["file"].split("/")[-1] if not info["artist"]: display_format = self.no_artist_format elif info["stream"]: display_format = self.stream_format # Handle case if cmus was started and no file is selected yet elif not info["file"]: display_format = "" now_playing = display_format.format(**info) if now_playing.strip() == info["status_text"].strip(): now_playing = "" return pangocffi.markup_escape_text(now_playing) def play(self): """Play music if stopped, else toggle pause.""" if self.status in ("playing", "paused"): subprocess.Popen(["cmus-remote", "-u"]) elif self.status == "stopped": subprocess.Popen(["cmus-remote", "-p"]) def poll(self): """Poll content for the text box.""" return self.now_playing() qtile-0.31.0/libqtile/widget/net.py0000664000175000017500000001631514762660347017074 0ustar epsilonepsilon# Copyright (c) 2014 Rock Neurotiko # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations from math import log import psutil from libqtile.log_utils import logger from libqtile.widget import base class Net(base.ThreadPoolText): """ Displays interface down and up speed The following fields are available in the `format` string: - ``interface``: name of the interface - ``down``: download speed - ``down_suffix``: suffix for the download speed - ``down_cumulative``: cumulative download traffic - ``down_cumulative_suffix``: suffix for the cumulative download traffic - ``up``: upload speed - ``up_suffix``: suffix for the upload speed - ``up_cumulative``: cumulative upload traffic - ``up_cumulative_suffix``: suffix for the cumulative upload traffic - ``total``: total speed - ``total_suffix``: suffix for the total speed - ``total_cumulative``: cumulative total traffic - ``total_cumulative_suffix``: suffix for the cumulative total traffic Widget requirements: psutil_. .. _psutil: https://pypi.org/project/psutil/ """ defaults = [ ( "format", "{interface}: {down:6.2f}{down_suffix:<2}\u2193\u2191{up:6.2f}{up_suffix:<2}", "Display format of down/upload/total speed of given interfaces", ), ( "interface", None, "List of interfaces or single NIC as string to monitor, \ None to display all active NICs combined", ), ("update_interval", 1, "The update interval."), ("use_bits", False, "Use bits instead of bytes per second?"), ("prefix", None, "Use a specific prefix for the unit of the speed."), ( "cumulative_prefix", None, "Use a specific prefix for the unit of the cumulative traffic.", ), ] def __init__(self, **config): base.ThreadPoolText.__init__(self, "", **config) self.add_defaults(Net.defaults) self.factor = 1000.0 self.allowed_prefixes = ["", "k", "M", "G", "T", "P", "E", "Z", "Y"] if self.use_bits: self.base_unit = "b" self.byte_multiplier = 8 else: self.base_unit = "B" self.byte_multiplier = 1 self.units = list(map(lambda p: p + self.base_unit, self.allowed_prefixes)) if not isinstance(self.interface, list): if self.interface is None: self.interface = ["all"] elif isinstance(self.interface, str): self.interface = [self.interface] else: raise AttributeError( f"Invalid Argument passed: {self.interface}\nAllowed Types: list, str, None" ) self.stats = self.get_stats() def convert_b(self, num_bytes: float, prefix: str | None = None) -> tuple[float, str]: """Converts the number of bytes to the correct unit""" num_bytes *= self.byte_multiplier if prefix is None: if num_bytes > 0: power = int(log(num_bytes) / log(self.factor)) power = min(power, len(self.units) - 1) else: power = 0 else: power = self.allowed_prefixes.index(prefix) converted_bytes = num_bytes / self.factor**power unit = self.units[power] return converted_bytes, unit def get_stats(self): interfaces = {} if self.interface == ["all"]: net = psutil.net_io_counters(pernic=False) interfaces["all"] = { "down": net.bytes_recv, "up": net.bytes_sent, "total": net.bytes_recv + net.bytes_sent, } return interfaces else: net = psutil.net_io_counters(pernic=True) for iface in net: down = net[iface].bytes_recv up = net[iface].bytes_sent interfaces[iface] = { "down": down, "up": up, "total": down + up, } return interfaces def poll(self): ret_stat = [] try: new_stats = self.get_stats() for intf in self.interface: down = new_stats[intf]["down"] - self.stats[intf]["down"] up = new_stats[intf]["up"] - self.stats[intf]["up"] total = new_stats[intf]["total"] - self.stats[intf]["total"] down = down / self.update_interval up = up / self.update_interval total = total / self.update_interval down, down_suffix = self.convert_b(down, self.prefix) down_cumulative, down_cumulative_suffix = self.convert_b( new_stats[intf]["down"], self.cumulative_prefix ) up, up_suffix = self.convert_b(up, self.prefix) up_cumulative, up_cumulative_suffix = self.convert_b( new_stats[intf]["up"], self.cumulative_prefix ) total, total_suffix = self.convert_b(total, self.prefix) total_cumulative, total_cumulative_suffix = self.convert_b( new_stats[intf]["total"], self.cumulative_prefix ) self.stats[intf] = new_stats[intf] ret_stat.append( self.format.format( interface=intf, down=down, down_suffix=down_suffix, down_cumulative=down_cumulative, down_cumulative_suffix=down_cumulative_suffix, up=up, up_suffix=up_suffix, up_cumulative=up_cumulative, up_cumulative_suffix=up_cumulative_suffix, total=total, total_suffix=total_suffix, total_cumulative=total_cumulative, total_cumulative_suffix=total_cumulative_suffix, ) ) return " ".join(ret_stat) except Exception: logger.exception("Net widget errored while polling:") qtile-0.31.0/libqtile/widget/cpu.py0000664000175000017500000000420414762660347017067 0ustar epsilonepsilon# Copyright (c) 2019 Niko Järvinen (b10011) # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import psutil from libqtile.widget import base class CPU(base.ThreadPoolText): """ A simple widget to display CPU load and frequency. Widget requirements: psutil_. .. _psutil: https://pypi.org/project/psutil/ """ defaults = [ ("update_interval", 1.0, "Update interval for the CPU widget"), ( "format", "CPU {freq_current}GHz {load_percent}%", "CPU display format", ), ] def __init__(self, **config): super().__init__("", **config) self.add_defaults(CPU.defaults) def poll(self): variables = dict() variables["load_percent"] = round(psutil.cpu_percent(), 1) freq = psutil.cpu_freq() if psutil.__version__ == "5.9.0": variables["freq_current"] = round(freq.current, 1) else: variables["freq_current"] = round(freq.current / 1000, 1) variables["freq_max"] = round(freq.max / 1000, 1) variables["freq_min"] = round(freq.min / 1000, 1) return self.format.format(**variables) qtile-0.31.0/libqtile/widget/crypto_ticker.py0000664000175000017500000001017614762660347021166 0ustar epsilonepsilon# Copyright (c) 2013 Jendrik Poloczek # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014 Aborilov Pavel # Copyright (c) 2014 Sean Vig # Copyright (c) 2014-2015 Tycho Andersen # Copyright (c) 2021 Graeme Holliday # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import locale from libqtile.confreader import ConfigError from libqtile.log_utils import logger from libqtile.widget.generic_poll_text import GenPollUrl _DEFAULT_CURRENCY = str(locale.localeconv()["int_curr_symbol"]) _DEFAULT_SYMBOL = str(locale.localeconv()["currency_symbol"]) class CryptoTicker(GenPollUrl): """ A cryptocurrency ticker widget, data provided by the coinbase.com or the binance.com API. Defaults to displaying currency in whatever the current locale is. Examples: # display the average price of bitcoin in local currency widget.CryptoTicker() # display it in Euros: widget.CryptoTicker(currency="EUR") # or a different cryptocurrency! widget.CryptoTicker(crypto="ETH") # change the currency symbol: widget.CryptoTicker(currency="EUR", symbol="€") # display from Binance API widget.CryptoTicker(api="binance", currency="USDT") """ QUERY_URL_DICT = { "coinbase": ( "https://api.coinbase.com/v2/prices/{}-{}/spot", lambda x: float(x["data"]["amount"]), ), "binance": ( "https://api.binance.com/api/v3/ticker/price?symbol={}{}", lambda x: float(x["price"]), ), } defaults = [ ( "currency", _DEFAULT_CURRENCY.strip(), "The baseline currency that the value of the crypto is displayed in.", ), ("symbol", _DEFAULT_SYMBOL, "The symbol for the baseline currency."), ("crypto", "BTC", "The cryptocurrency to display."), ("format", "{crypto}: {symbol}{amount:.2f}", "Display string formatting."), ("api", "coinbase", "API that provides the data."), ] def __init__(self, **config): GenPollUrl.__init__(self, **config) self.add_defaults(CryptoTicker.defaults) # set up USD as the currency if no locale is set if self.currency == "": self.currency = "USD" # set up $ as the symbol if no locale is set if self.symbol == "": self.symbol = "$" def _configure(self, qtile, bar): try: GenPollUrl._configure(self, qtile, bar) self.query_url = self.QUERY_URL_DICT[self.api][0] except KeyError: apis = sorted(self.QUERY_URL_DICT.keys()) logger.error( "%s is not a valid API. Use one of the list: %s.", self.api, apis, ) raise ConfigError("Unknown provider passed as 'api' to CryptoTicker") @property def url(self): return self.query_url.format(self.crypto, self.currency) def parse(self, body): variables = dict() variables["crypto"] = self.crypto variables["symbol"] = self.symbol variables["amount"] = self.QUERY_URL_DICT[self.api][1](body) return self.format.format(**variables) qtile-0.31.0/libqtile/widget/generic_poll_text.py0000664000175000017500000000714114762660347022011 0ustar epsilonepsilonimport json import subprocess from http.client import HTTPException from typing import Any from urllib.error import URLError from urllib.request import Request, urlopen from libqtile.log_utils import logger from libqtile.widget import base try: import xmltodict def xmlparse(body): return xmltodict.parse(body) except ImportError: # TODO: we could implement a similar parser by hand, but i'm lazy, so let's # punt for now def xmlparse(body): raise Exception("no xmltodict library") class GenPollText(base.ThreadPoolText): """A generic text widget that polls using poll function to get the text""" defaults = [ ("func", None, "Poll Function"), ] def __init__(self, **config): base.ThreadPoolText.__init__(self, "", **config) self.add_defaults(GenPollText.defaults) def poll(self): if not self.func: return "You need a poll function" return self.func() class GenPollUrl(base.ThreadPoolText): """A generic text widget that polls an url and parses it using parse function""" defaults: list[tuple[str, Any, str]] = [ ("url", None, "Url"), ("data", None, "Post Data"), ("parse", None, "Parse Function"), ("json", True, "Is Json?"), ("user_agent", "Qtile", "Set the user agent"), ("headers", {}, "Extra Headers"), ("xml", False, "Is XML?"), ] def __init__(self, **config): base.ThreadPoolText.__init__(self, "", **config) self.add_defaults(GenPollUrl.defaults) self.headers["User-agent"] = self.user_agent if self.json: self.headers["Content-Type"] = "application/json" if self.data and not isinstance(self.data, str): self.data = json.dumps(self.data).encode() def fetch(self): req = Request(self.url, self.data, self.headers) res = urlopen(req) charset = res.headers.get_content_charset() body = res.read() if charset: body = body.decode(charset) if self.json: body = json.loads(body) if self.xml: body = xmlparse(body) return body def poll(self): if not self.parse or not self.url: return "Invalid config" try: body = self.fetch() except URLError: return "No network" except HTTPException: return "Request failed" try: text = self.parse(body) except Exception: logger.exception("got exception polling widget") text = "Can't parse" return text class GenPollCommand(base.ThreadPoolText): """A generic text widget to display output from scripts or shell commands""" defaults = [ ("update_interval", 60, "update time in seconds"), ("cmd", None, "command line as a string or list of arguments to execute"), ("shell", False, "run command through shell to enable piping and shell expansion"), ("parse", None, "Function to parse output of command"), ] def __init__(self, **config): base.ThreadPoolText.__init__(self, "", **config) self.add_defaults(GenPollCommand.defaults) def _configure(self, qtile, bar): base.ThreadPoolText._configure(self, qtile, bar) self.add_callbacks({"Button1": self.force_update}) def poll(self): process = subprocess.run( self.cmd, capture_output=True, text=True, shell=self.shell, ) if self.parse: return self.parse(process.stdout) return process.stdout.strip() qtile-0.31.0/libqtile/widget/keyboardkbdd.py0000664000175000017500000000776314762660347020742 0ustar epsilonepsilon# Copyright (c) 2015 Ali Mousavi # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # type: ignore import re from libqtile.log_utils import logger from libqtile.utils import add_signal_receiver from libqtile.widget import base class KeyboardKbdd(base.ThreadPoolText): """Widget for changing keyboard layouts per window, using kbdd kbdd should be installed and running, you can get it from: https://github.com/qnikst/kbdd The widget also requires dbus-fast_. .. _dbus-fast: https://pypi.org/project/dbus-fast/ """ defaults = [ ("update_interval", 1, "Update interval in seconds."), ( "configured_keyboards", ["us", "ir"], "your predefined list of keyboard layouts." "example: ['us', 'ir', 'es']", ), ( "colours", None, "foreground colour for each layout" "either 'None' or a list of colours." "example: ['ffffff', 'E6F0AF']. ", ), ] def __init__(self, **config): base.ThreadPoolText.__init__(self, "", **config) self.add_defaults(KeyboardKbdd.defaults) self.keyboard = self.configured_keyboards[0] self.is_kbdd_running = self._check_kbdd() if not self.is_kbdd_running: self.keyboard = "N/A" def _check_kbdd(self): try: running_list = self.call_process(["ps", "axw"]) except FileNotFoundError: logger.error("'ps' is not installed. Cannot check if kbdd is running.") return False if re.search("kbdd", running_list): self.keyboard = self.configured_keyboards[0] return True logger.error("kbdd is not running.") return False async def _config_async(self): subscribed = await add_signal_receiver( self._signal_received, session_bus=True, signal_name="layoutChanged", dbus_interface="ru.gentoo.kbdd", ) if not subscribed: logger.warning("Could not subscribe to kbdd signal.") def _signal_received(self, message): self._layout_changed(*message.body) def _layout_changed(self, layout_changed): """ Handler for "layoutChanged" dbus signal. """ if self.colours: self._set_colour(layout_changed) self.keyboard = self.configured_keyboards[layout_changed] def _set_colour(self, index): if isinstance(self.colours, list): try: self.layout.colour = self.colours[index] except IndexError: self._set_colour(index - 1) else: logger.error( 'variable "colours" should be a list, to set a\ colour for all layouts, use "foreground".' ) def poll(self): if not self.is_kbdd_running: if self._check_kbdd(): self.is_kbdd_running = True return self.configured_keyboards[0] return self.keyboard qtile-0.31.0/libqtile/widget/idlerpg.py0000664000175000017500000000421714762660347017732 0ustar epsilonepsilon# Copyright (c) 2016 Tycho Andersen # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import datetime from libqtile.widget.generic_poll_text import GenPollUrl class IdleRPG(GenPollUrl): """ A widget for monitoring and displaying IdleRPG stats. :: # display idlerpg stats for the player 'pants' on freenode's #idlerpg widget.IdleRPG(url="http://xethron.lolhosting.net/xml.php?player=pants") Widget requirements: xmltodict_. .. _xmltodict: https://pypi.org/project/xmltodict/ """ defaults = [ ("format", "IdleRPG: {online} TTL: {ttl}", "Display format"), ] def __init__(self, **config): GenPollUrl.__init__(self, **config) self.add_defaults(IdleRPG.defaults) self.json = False self.xml = True def parse(self, body): formatted = {} for k, v in body["player"].items(): if k == "ttl": formatted[k] = str(datetime.timedelta(seconds=int(v))) elif k == "online": formatted[k] = "online" if v == "1" else "offline" else: formatted[k] = v return self.format.format(**formatted) qtile-0.31.0/libqtile/widget/maildir.py0000664000175000017500000001072114762660347017722 0ustar epsilonepsilon# Copyright (c) 2011 Timo Schmiade # Copyright (c) 2012 Phil Jackson # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Tycho Andersen # Copyright (c) 2016 Christoph Lassner # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations import mailbox import os.path from libqtile.widget import base class Maildir(base.ThreadPoolText): """A simple widget showing the number of new mails in maildir mailboxes""" defaults = [ ("maildir_path", "~/Mail", "path to the Maildir folder"), ( "sub_folders", [{"path": "INBOX", "label": "Home mail"}, {"path": "spam", "label": "Home junk"}], "List of subfolders to scan. Each subfolder is a dict of `path` " "and `label`.", ), ("separator", " ", "the string to put between the subfolder strings."), ( "total", False, "Whether or not to sum subfolders into a grand \ total. The first label will be used.", ), ( "hide_when_empty", False, "Whether not to display anything if " "the subfolder has no new mail", ), ("empty_color", None, "Display color when no new mail is available"), ("nonempty_color", None, "Display color when new mail is available"), ("subfolder_fmt", "{label}: {value}", "Display format for one subfolder"), ] def __init__(self, **config): base.ThreadPoolText.__init__(self, "", **config) self.add_defaults(Maildir.defaults) # if it looks like a list of strings then we just convert them # and use the name as the label if isinstance(self.sub_folders[0], str): self.sub_folders = [{"path": folder, "label": folder} for folder in self.sub_folders] def poll(self): """Scans the mailbox for new messages Returns ======= A string representing the current mailbox state """ state = {} def to_maildir_fmt(paths): for path in iter(paths): yield path.rsplit(":")[0] for sub_folder in self.sub_folders: path = os.path.join(os.path.expanduser(self.maildir_path), sub_folder["path"]) maildir = mailbox.Maildir(path) state[sub_folder["label"]] = 0 for file in to_maildir_fmt(os.listdir(os.path.join(path, "new"))): if file in maildir: state[sub_folder["label"]] += 1 return self.format_text(state) def _format_one(self, label: str, value: int) -> str: if value == 0 and self.hide_when_empty: return "" s = self.subfolder_fmt.format(label=label, value=value) color = self.empty_color if value == 0 else self.nonempty_color if color is None: # default to self.foreground return s return s.join((f'', "")) def format_text(self, state: dict[str, int]) -> str: """Converts the state of the subfolders to a string Parameters ========== state: dict[str, int] a dictionary mapping subfolder labels to new mail values Returns ======= a string representation of the given state """ if self.total: return self._format_one(self.sub_folders[0]["label"], sum(state.values())) else: return self.separator.join(self._format_one(*item) for item in state.items()) qtile-0.31.0/libqtile/widget/backlight.py0000664000175000017500000001321514762660347020232 0ustar epsilonepsilon# Copyright (c) 2012 Tim Neumann # Copyright (c) 2012, 2014 Tycho Andersen # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014 Sean Vig # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import enum import os import shlex from functools import partial from libqtile.command.base import expose_command from libqtile.log_utils import logger from libqtile.widget import base BACKLIGHT_DIR = "/sys/class/backlight" @enum.unique class ChangeDirection(enum.Enum): UP = 0 DOWN = 1 class Backlight(base.InLoopPollText): """A simple widget to show the current brightness of a monitor. If the change_command parameter is set to None, the widget will attempt to use the interface at /sys/class to change brightness. This depends on having the correct udev rules, so be sure Qtile's udev rules are installed correctly. You can also bind keyboard shortcuts to the backlight widget with: .. code-block:: python from libqtile.widget import backlight Key( [], "XF86MonBrightnessUp", lazy.widget['backlight'].change_backlight(backlight.ChangeDirection.UP) ) Key( [], "XF86MonBrightnessDown", lazy.widget['backlight'].change_backlight(backlight.ChangeDirection.DOWN) ) """ filenames: dict = {} defaults = [ ("backlight_name", "acpi_video0", "ACPI name of a backlight device"), ( "brightness_file", "brightness", "Name of file with the " "current brightness in /sys/class/backlight/backlight_name", ), ( "max_brightness_file", "max_brightness", "Name of file with the " "maximum brightness in /sys/class/backlight/backlight_name", ), ("update_interval", 0.2, "The delay in seconds between updates"), ("step", 10, "Percent of backlight every scroll changed"), ("format", "{percent:2.0%}", "Display format"), ("change_command", "xbacklight -set {0}", "Execute command to change value"), ("min_brightness", 0, "Minimum brightness percentage"), ] def __init__(self, **config): base.InLoopPollText.__init__(self, **config) self.add_defaults(Backlight.defaults) self._future = None self.brightness_file = os.path.join( BACKLIGHT_DIR, self.backlight_name, self.brightness_file, ) self.max_brightness_file = os.path.join( BACKLIGHT_DIR, self.backlight_name, self.max_brightness_file, ) self.add_callbacks( { "Button4": partial(self.change_backlight, ChangeDirection.UP), "Button5": partial(self.change_backlight, ChangeDirection.DOWN), } ) def finalize(self): if self._future and not self._future.done(): self._future.cancel() base.InLoopPollText.finalize(self) def _load_file(self, path): try: with open(path) as f: return float(f.read().strip()) except FileNotFoundError: logger.debug("Failed to get %s", path) raise RuntimeError(f"Unable to read status for {os.path.basename(path)}") def _get_info(self): brightness = self._load_file(self.brightness_file) max_value = self._load_file(self.max_brightness_file) return brightness / max_value def poll(self): try: percent = self._get_info() except RuntimeError as e: return f"Error: {e}" return self.format.format(percent=percent) def _change_backlight(self, value): if self.change_command is None: value = self._load_file(self.max_brightness_file) * value / 100 try: with open(self.brightness_file, "w") as f: f.write(str(round(value))) except PermissionError: logger.warning( "Cannot set brightness: no write permission for %s", self.brightness_file ) else: self.call_process(shlex.split(self.change_command.format(value))) @expose_command() def change_backlight(self, direction, step=None): if not step: step = self.step if self._future and not self._future.done(): return new = now = self._get_info() * 100 if direction is ChangeDirection.DOWN: new = max(now - step, self.min_brightness) elif direction is ChangeDirection.UP: new = min(now + step, 100) if new != now: self._future = self.qtile.run_in_executor(self._change_backlight, new) qtile-0.31.0/libqtile/widget/countdown.py0000664000175000017500000000447314762660347020330 0ustar epsilonepsilon# Copyright (c) 2014 Sean Vig # Copyright (c) 2014 roger # Copyright (c) 2014 Tycho Andersen # Copyright (c) 2014 Adi Sieker # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from datetime import datetime from libqtile.widget import base class Countdown(base.InLoopPollText): """A simple countdown timer text widget""" defaults = [ ( "format", "{D}d {H}h {M}m {S}s", "Format of the displayed text. Available variables:" "{D} == days, {H} == hours, {M} == minutes, {S} seconds.", ), ("update_interval", 1.0, "Update interval in seconds for the clock"), ("date", datetime.now(), "The datetime for the end of the countdown"), ] def __init__(self, **config): base.InLoopPollText.__init__(self, **config) self.add_defaults(Countdown.defaults) def poll(self): now = datetime.now() days = hours = minutes = seconds = 0 if not self.date < now: delta = self.date - now days = delta.days hours, rem = divmod(delta.seconds, 3600) minutes, seconds = divmod(rem, 60) data = { "D": "%02d" % days, "H": "%02d" % hours, "M": "%02d" % minutes, "S": "%02d" % seconds, } return self.format.format(**data) qtile-0.31.0/libqtile/widget/launchbar.py0000664000175000017500000002626714762660347020254 0ustar epsilonepsilon# Copyright (c) 2014 Tycho Andersen # Copyright (c) 2014 dequis # Copyright (c) 2014-2015 Joseph Razik # Copyright (c) 2014 Sean Vig # Copyright (c) 2015 reus # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations import os.path import cairocffi try: from xdg.IconTheme import getIconPath has_xdg = True except ImportError: has_xdg = False from libqtile import bar from libqtile.images import Img from libqtile.log_utils import logger from libqtile.widget import base class LaunchBar(base._Widget): """ This module defines a widget that displays icons to launch softwares or commands when clicked -- a launchbar. Only png icon files are displayed, not xpm because cairo doesn't support loading of xpm file. The order of displaying (from left to right) is in the order of the list. If no icon was found for the name provided and if default_icon is set to None then the name is printed instead. If default_icon is defined then this icon is displayed instead. To execute a software: - ('thunderbird', 'thunderbird -safe-mode', 'launch thunderbird in safe mode') To execute a python command in qtile, begin with by 'qshell:' - ('/path/to/icon.png', 'qshell:self.qtile.shutdown()', 'logout from qtile') Optional requirements: `pyxdg `__ for finding the icon path if it is not provided in the ``progs`` tuple. """ orientations = base.ORIENTATION_HORIZONTAL defaults = [ ("padding", 2, "Padding between icons"), ( "default_icon", "/usr/share/icons/oxygen/256x256/mimetypes/application-x-executable.png", "Default icon not found", ), ("font", "sans", "Text font"), ("fontsize", None, "Font pixel size. Calculated if None."), ("fontshadow", None, "Font shadow color, default is None (no shadow)"), ("foreground", "#ffffff", "Text colour."), ( "progs", [], "A list of tuples (software_name or icon_path, command_to_execute, comment), for example:" " [('thunderbird', 'thunderbird -safe-mode', 'launch thunderbird in safe mode'), " " ('/path/to/icon.png', 'qshell:self.qtile.shutdown()', 'logout from qtile')]", ), ("text_only", False, "Don't use any icons."), ("icon_size", None, "Size of icons. ``None`` to fit to bar."), ("padding_y", 0, "Vertical adjustment for icons."), ( "theme_path", None, "Path to icon theme to be used by pyxdg for icons. ``None`` will use default icon theme.", ), ] def __init__( self, _progs: list[tuple[str, str, str]] | None = None, width=bar.CALCULATED, **config ): base._Widget.__init__(self, width, **config) self.add_defaults(LaunchBar.defaults) self.surfaces: dict[str, Img | base._TextBox] = {} self.icons_files: dict[str, str | None] = {} self.icons_widths: dict[str, int] = {} self.icons_offsets: dict[str, int] = {} if _progs: logger.warning( "The use of a positional argument in LaunchBar is deprecated. " "Please update your config to use progs=[...]." ) config["progs"] = _progs # For now, ignore the comments but may be one day it will be useful self.progs = dict( enumerate( [ { "name": prog[0], "cmd": prog[1], "comment": prog[2] if len(prog) > 2 else None, } for prog in config.get("progs", list()) ] ) ) self.progs_name = set([prog["name"] for prog in self.progs.values()]) self.length_type = bar.STATIC self.length = 0 def _configure(self, qtile, pbar): base._Widget._configure(self, qtile, pbar) self.lookup_icons() self.setup_images() self.length = self.calculate_length() def setup_images(self): """Create image structures for each icon files.""" self._icon_size = self.icon_size if self.icon_size is not None else self.bar.height - 4 self._icon_padding = (self.bar.height - self._icon_size) // 2 for img_name, iconfile in self.icons_files.items(): if iconfile is None or self.text_only: # Only warn the user that there's no icon if they haven't set text only mode if not self.text_only: logger.warning( 'No icon found for application "%s" (%s) switch to text mode', img_name, iconfile, ) # if no icon is found and no default icon was set, we just # print the name, based on a textbox. textbox = base._TextBox() textbox._configure(self.qtile, self.bar) textbox.layout = self.drawer.textlayout( textbox.text, self.foreground, self.font, self.fontsize, self.fontshadow, markup=textbox.markup, ) # the name will be displayed textbox.text = img_name textbox.calculate_length() self.icons_widths[img_name] = textbox.width self.surfaces[img_name] = textbox continue else: try: img = Img.from_path(iconfile) except cairocffi.Error: logger.exception( 'Error loading icon for application "%s" (%s)', img_name, iconfile ) return input_width = img.width input_height = img.height sp = input_height / (self._icon_size) width = int(input_width / sp) imgpat = cairocffi.SurfacePattern(img.surface) scaler = cairocffi.Matrix() scaler.scale(sp, sp) scaler.translate(self.padding * -1, -2) imgpat.set_matrix(scaler) imgpat.set_filter(cairocffi.FILTER_BEST) self.surfaces[img_name] = imgpat self.icons_widths[img_name] = width def _lookup_icon(self, name): """Search for the icon corresponding to one command.""" self.icons_files[name] = None # expands ~ if name is a path and does nothing if not ipath = os.path.expanduser(name) # if the software_name is directly an absolute path icon file if os.path.isabs(ipath): # name start with '/' thus it's an absolute path root, ext = os.path.splitext(ipath) img_extensions = [".tif", ".tiff", ".bmp", ".jpg", ".jpeg", ".gif", ".png", ".svg"] if ext in img_extensions: self.icons_files[name] = ipath if os.path.isfile(ipath) else None else: # try to add the extension for extension in img_extensions: if os.path.isfile(ipath + extension): self.icons_files[name] = ipath + extension break elif has_xdg: self.icons_files[name] = getIconPath(name, theme=self.theme_path) # no search method found an icon, so default icon if self.icons_files[name] is None: self.icons_files[name] = self.default_icon def lookup_icons(self): """Search for the icons corresponding to the commands to execute.""" if self.default_icon is not None: if not os.path.isfile(self.default_icon): # if the default icon provided is not found, switch to # text mode self.default_icon = None for name in self.progs_name: self._lookup_icon(name) def get_icon_in_position(self, x, y): """Determine which icon is clicked according to its position.""" for i in self.progs: if x < ( self.icons_offsets[i] + self.icons_widths[self.progs[i]["name"]] + self.padding / 2 ): return i def button_press(self, x, y, button): """Launch the associated command to the clicked icon.""" base._Widget.button_press(self, x, y, button) if button == 1: icon = self.get_icon_in_position(x, y) if icon is not None: cmd = self.progs[icon]["cmd"] if cmd.startswith("qshell:"): exec(cmd[7:].lstrip()) else: self.qtile.spawn(cmd) self.draw() def draw(self): """Draw the icons in the widget.""" self.drawer.clear(self.background or self.bar.background) xoffset = 0 for i in sorted(self.progs.keys()): self.drawer.ctx.save() self.drawer.ctx.translate(xoffset, 0) self.icons_offsets[i] = xoffset + self.padding name = self.progs[i]["name"] icon_width = self.icons_widths[name] if isinstance(self.surfaces[name], base._TextBox): # display the name if no icon was found and no default icon textbox = self.surfaces[name] textbox.layout.draw( self.padding + textbox.actual_padding, int((self.bar.height - textbox.layout.height) / 2.0) + 1, ) else: # display an icon # Translate to vertically centre the icon self.drawer.ctx.translate(0, self._icon_padding + self.padding_y) self.drawer.ctx.set_source(self.surfaces[name]) self.drawer.ctx.paint() self.drawer.ctx.restore() xoffset += icon_width + self.padding self.drawer.draw(offsetx=self.offset, offsety=self.offsety, width=self.width) def calculate_length(self): """Compute the width of the widget according to each icon width.""" return sum( self.icons_widths[prg["name"]] for prg in self.progs.values() ) + self.padding * (len(self.progs) + 1) qtile-0.31.0/libqtile/widget/redshift.py0000664000175000017500000004133514762660347020116 0ustar epsilonepsilon# Copyright (c) 2024 Saath Satheeshkumar (saths008) # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import subprocess from shutil import which from libqtile.command.base import expose_command from libqtile.log_utils import logger from libqtile.widget.base import _TextBox class GammaGroup: """ GammaGroup is a helper class to group redshift gamma settings. """ def __init__(self, red: float, green: float, blue: float): self.gamma_red = red self.gamma_green = green self.gamma_blue = blue def _format_gamma(self, format_txt: str) -> str: return format_txt.format( gamma_red=self.gamma_red, gamma_green=self.gamma_green, gamma_blue=self.gamma_blue, ) def _redshift_fmt(self) -> str: return f"{self.gamma_red}:{self.gamma_green}:{self.gamma_blue}" def __repr__(self) -> str: return f"Gamma: {self._redshift_fmt()}" def __str__(self) -> str: return ( f"GammaGroup(red={self.gamma_red}, green={self.gamma_green}, blue={self.gamma_blue})" ) class Redshift(_TextBox): """ Redshift widget provides the following functionality: - Call redshift with a specific brightness, temperature and gamma config without using your location. - Increase/Decrease the brightness/temperature The redshift command can be called by just left-clicking the widget and disabling it the same way. If the widget is enabled, scrolling through the widget will show the settings: - brightness (limited to 2 dp) - gamma (can't be increased/decreased) - temperature (limited to 2 dp) - finally back to the default enabled/disabled text When at the temperature/brightness settings, the left-click/right-click mouse buttons can be used to increase/decrease respectively. Widget requirements: redshift_ .. _redshift: https://github.com/jonls/redshift """ defaults = [ ( "brightness", 1.0, "Redshift brightness. " "Brightness has a lower bound of 0.1 and an upper bound of 1.0", ), ("brightness_step", 0.1, "The amount to increase/decrease the brightness by. "), ( "disabled_txt", "󱠃", "Text to show when redshift is disabled. NOTE: by default, a nerd icon is used. " "Available fields: 'brightness' brightness to set when redshift is enabled, " "'is_enabled' boolean to state whether the widget is enabled or not, " "'gamma_blue' gamma blue value to set when redshift is enabled, " "'gamma_green' gamma green value to set when redshift is enabled, " "'gamma_red' gamma red value to set when redshift is enabled, " "'temperature' temperature to set when redshift is enabled, ", ), ( "enabled_txt", "󰛨", "Text to show when redshift is disabled. NOTE: by default, a nerd icon is used. " "Available fields: see disabled_txt's available fields for all of them", ), ( "gamma_red", 1.0, "Redshift gamma red setting. " "gamma_red has a lower bound of 0.1 and an upper bound of 10.0 .", ), ( "gamma_blue", 1.0, "Redshift gamma blue. " "gamma_blue has a lower bound of 0.1 and an upper bound of 10.0 .", ), ( "gamma_green", 1.0, "Redshift gamma green. " "gamma_green has a lower bound of 0.1 and an upper bound of 10.0 .", ), ("font", "sans", "Default font"), ("fontsize", 20, "Font size"), ("foreground", "ffffff", "Font colour for information text"), ("redshift_path", which("redshift"), "Path to redshift executable"), ( "temperature", 1700, "Redshift temperature to set when enabled. " "Temperature has a lower bound of 1000 and an upper bound of 25000.", ), ( "temperature_step", 100, "The amount to increase/decrease the temperature by.", ), ( "temperature_fmt", "Temperature: {temperature}", "Text to display when showing temperature text. " "Available fields: 'temperature' temperature to set when redshift is enabled. ", ), ( "brightness_fmt", "Brightness: {brightness}", "Text to display when showing brightness text. " "Available fields: 'brightness' brightness to set when redshift is enabled. ", ), ( "gamma_fmt", "Gamma: {gamma_red}:{gamma_green}:{gamma_blue}", "Text to display when showing gamma text. " "Available fields: 'gamma_red' gamma red value to set when redshift is enabled, " "'gamma_blue' gamma blue value to set when redshift is enabled, " "'gamma_green' gamma green value to set when redshift is enabled. ", ), ] # declare the same defaults as above but with types # to stop LSP complaints brightness: float brightness_step: float disabled_txt: str enabled_txt: str gamma_red: float gamma_blue: float gamma_green: float redshift_path: str temperature: float temperature_step: float temperature_fmt: str brightness_fmt: str gamma_fmt: str supported_backends = {"x11"} def __init__(self, **config): _TextBox.__init__(self, **config) self.add_defaults(Redshift.defaults) self.is_enabled = False self._line_index = 0 self._lines = [] self.brightness_idx = 1 self.temperature_idx = 2 self.gamma_idx = 3 # redshift's limits self.brightness_lower_lim = 0.1 self.brightness_upper_lim = 1.0 self.temperature_lower_lim = 1000 self.temperature_upper_lim = 25000 self.gamma_lower_lim = 0.1 self.gamma_upper_lim = 10.0 # Make sure fields are initialised to values # in bounds self._assert_brightness() self._assert_temperature() self.gamma_val = self._assert_gamma() self.add_callbacks( { "Button1": self.click, "Button3": self.right_click, "Button4": self.scroll_up, "Button5": self.scroll_down, } ) self.error = None def _configure(self, qtile, bar): _TextBox._configure(self, qtile, bar) # disable redshift so we have a known initial state self.reset_redshift() # set text after bar configuration is done self.qtile.call_soon(self._set_text) @expose_command def scroll_up(self): """ Scroll up to next item. """ self._scroll(1) @expose_command def scroll_down(self): """ Scroll down to next item. """ self._scroll(-1) def show_line(self): """ Update the text with the the current index in lines. """ assert self._lines, "lines arr should have been initialised" line = self._lines[self._line_index] self.update(line) @expose_command def click(self): """ Has no action for the gamma line. Either enable or disable the widget if we are currently on the first line. Else decrease brightness/temperature accordingly """ if self.error or self._line_index == self.gamma_idx: return elif self._line_index == self.brightness_idx: self.decrease_brightness() elif self._line_index == self.temperature_idx: self.decrease_temperature() else: self.reset_redshift() if self.is_enabled else self.run_redshift() self.is_enabled = not self.is_enabled self._set_text() @expose_command def right_click(self): """ Has no action for the first line of the widget nor the gamma line. Increase brightness/temperature accordingly. """ if self.error or self._line_index == 0 or self._line_index == self.gamma_idx: return elif self._line_index == self.brightness_idx: self.increase_brightness() elif self._line_index == self.temperature_idx: self.increase_temperature() self._set_text() def _scroll(self, step): """ Scroll up/down dictated by 'step' the items and then display that item. Has no effect if the widget is disabled. """ if self.error or not self.is_enabled: return self._line_index = (self._line_index + step) % len(self._lines) self.show_line() @expose_command def decrease_brightness(self): """ Decrease brightness by a single step. """ step = self.brightness_step * -1 self._change_brightness(step) @expose_command def increase_brightness(self): """ Increase brightness by a single step. """ self._change_brightness(self.brightness_step) def _change_brightness(self, step): """ Control the change brightness logic for both decreasing and increasing. """ # To limit the text displayed from float calcs self.brightness = round(self.brightness + step, 2) # ensure that brightness stays in the valid bounds if self.brightness < self.brightness_lower_lim: self.brightness = self.brightness_lower_lim elif self.brightness > self.brightness_upper_lim: self.brightness = self.brightness_upper_lim self.run_redshift() @expose_command def decrease_temperature(self): """ Decrease redshift temperature by a single step. """ step = self.temperature_step * -1 self._change_temperature(step) @expose_command def increase_temperature(self): """ Increase redshift temperature by a single step. """ self._change_temperature(self.temperature_step) def _change_temperature(self, step): """ Control the change temperature logic for both decreasing and increasing. """ # To limit the text displayed from float calcs self.temperature = round(self.temperature + step, 2) # ensure that temperature stays in the valid bounds if self.temperature < self.temperature_lower_lim: self.temperature = self.temperature_lower_lim elif self.temperature > self.temperature_upper_lim: self.temperature = self.temperature_upper_lim self.run_redshift() def _set_lines(self): """ Set the list of formatted text items to scroll through. """ first_text = "" if self.is_enabled: first_text = self.enabled_txt else: first_text = self.disabled_txt first_text = self._format_first_text(first_text) self._lines = [first_text, "", "", ""] self._lines[self.brightness_idx] = self._format_brightness_text() self._lines[self.temperature_idx] = self._format_temp_text() self._lines[self.gamma_idx] = self._format_gamma_text() def _set_text(self): """ Update the lines array and the widget text. """ # Update in case values have changed if self.error: return self._set_lines() text = "" if self._line_index == 0: if self.is_enabled: text = self._format_first_text(self.enabled_txt) else: text = self._format_first_text(self.disabled_txt) else: text = self._lines[self._line_index] self.update(text) def _format_temp_text(self) -> str: """ Format the temperature text. """ return self.temperature_fmt.format(temperature=self.temperature) def _format_brightness_text(self) -> str: """ Format the brightness text. """ return self.brightness_fmt.format(brightness=self.brightness) def _format_gamma_text(self) -> str: """ Format the gamma text. """ return self.gamma_val._format_gamma(self.gamma_fmt) def _format_first_text(self, txt: str) -> str: """ Format the enabled/disabled text (aka first_text). """ return txt.format( brightness=self.brightness, temperature=self.temperature, gamma_red=self.gamma_red, gamma_green=self.gamma_green, gamma_blue=self.gamma_blue, is_enabled=self.is_enabled, ) def _assert_brightness(self): """ assert that self.brightness is within the correct limits """ assert ( self.brightness >= self.brightness_lower_lim and self.brightness <= self.brightness_upper_lim ), f"redshift: self.brightness is not initialised within the acceptable range, see the widget defaults docs: {self.brightness}" def _assert_gamma(self) -> GammaGroup: """ assert that self.gamma is within the correct limits. If it is, produce a GammaGroup instance """ gamma_vals = [ self.gamma_red, self.gamma_green, self.gamma_blue, ] gamma_group = GammaGroup(self.gamma_red, self.gamma_green, self.gamma_blue) for _, val in enumerate(gamma_vals): assert ( val >= self.gamma_lower_lim and val <= self.gamma_upper_lim ), f"redshift: self.gamma_red, self.gamma_green or self.gamma_blue have not been initialised within the acceptable range, see the docs: {gamma_group}" return gamma_group def _assert_temperature(self): """ assert that self.temperature is within the correct limits """ assert ( self.temperature >= self.temperature_lower_lim and self.temperature <= self.temperature_upper_lim ), f"redshift: self.temperature is not initialised within the acceptable range, see the docs: {self.temperature}" @expose_command def reset_redshift(self): """ Call reset on redshift to reset to default settings. """ try: subprocess.run([self.redshift_path, "-x"], check=True) except (TypeError, FileNotFoundError) as e: self.widget_error( f"redshift: could not find redshift executable, check redshift_path: {e}" ) except subprocess.CalledProcessError as e: self.widget_error(f"redshift: could not enable redshift: {e}") @expose_command def run_redshift(self): """ Run redshift command with defined parameters. """ try: subprocess.run( [ self.redshift_path, "-P", "-O", str(self.temperature), "-b", str(self.brightness), "-g", self.gamma_val._redshift_fmt(), ], check=True, ) except (TypeError, FileNotFoundError) as e: self.widget_error( f"redshift: could not find redshift executable, check redshift_path: {e}" ) except subprocess.CalledProcessError as e: self.widget_error(f"redshift: could not enable redshift: {e}") def widget_error(self, error_msg: str): """ Cause the widget to display an error and log the given error message """ self.error = error_msg logger.exception(self.error) self.update("Redshift widget crashed!") qtile-0.31.0/libqtile/widget/pomodoro.py0000664000175000017500000001565714762660347020154 0ustar epsilonepsilon# Copyright (c) 2017 Zordsdavini # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from datetime import datetime, timedelta from time import time from libqtile.command.base import expose_command from libqtile.utils import send_notification from libqtile.widget import base class Pomodoro(base.ThreadPoolText): """Pomodoro technique widget""" defaults = [ ("num_pomodori", 4, "Number of pomodori to do in a cycle"), ("length_pomodori", 25, "Length of one pomodori in minutes"), ("length_short_break", 5, "Length of a short break in minutes"), ("length_long_break", 15, "Length of a long break in minutes"), ("color_inactive", "ff0000", "Colour then pomodoro is inactive"), ("color_active", "00ff00", "Colour then pomodoro is running"), ("color_break", "ffff00", "Colour then it is break time"), ("notification_on", True, "Turn notifications on"), ("prefix_inactive", "POMODORO", "Prefix when app is inactive"), ("prefix_active", "", "Prefix then app is active"), ("prefix_break", "B ", "Prefix during short break"), ("prefix_long_break", "LB ", "Prefix during long break"), ("prefix_paused", "PAUSE", "Prefix during pause"), ( "update_interval", 1, "Update interval in seconds, if none, the " "widget updates whenever the event loop is idle.", ), ] STATUS_START = "start" STATUS_INACTIVE = "inactive" STATUS_ACTIVE = "active" STATUS_BREAK = "break" STATUS_LONG_BREAK = "long_break" STATUS_PAUSED = "paused" status = "inactive" paused_status = None end_time = datetime.now() time_left = None pomodoros = 1 def __init__(self, **config): base.ThreadPoolText.__init__(self, "", **config) self.add_defaults(Pomodoro.defaults) self.prefix = { "inactive": self.prefix_inactive, "active": self.prefix_active, "break": self.prefix_break, "long_break": self.prefix_long_break, "paused": self.prefix_paused, } self.add_callbacks( { "Button1": self.toggle_break, "Button3": self.toggle_active, } ) def tick(self): self.update(self.poll()) return self.update_interval - time() % self.update_interval def _update(self): if self.status in [self.STATUS_INACTIVE, self.STATUS_PAUSED]: return if self.end_time > datetime.now() and self.status != self.STATUS_START: return if self.status == self.STATUS_ACTIVE and self.pomodoros == self.num_pomodori: self.status = self.STATUS_LONG_BREAK self.end_time = datetime.now() + timedelta(minutes=self.length_long_break) self.pomodoros = 1 if self.notification_on: self._send_notification( "normal", "Please take a long break! End Time: " + self.end_time.strftime("%H:%M"), ) return if self.status == self.STATUS_ACTIVE: self.status = self.STATUS_BREAK self.end_time = datetime.now() + timedelta(minutes=self.length_short_break) self.pomodoros += 1 if self.notification_on: self._send_notification( "normal", "Please take a short break! End Time: " + self.end_time.strftime("%H:%M"), ) return self.status = self.STATUS_ACTIVE self.end_time = datetime.now() + timedelta(minutes=self.length_pomodori) if self.notification_on: self._send_notification( "critical", "Please start with the next Pomodori! End Time: " + self.end_time.strftime("%H:%M"), ) return def _get_text(self): self._update() if self.status in [self.STATUS_INACTIVE, self.STATUS_PAUSED]: self.layout.colour = self.color_inactive return self.prefix[self.status] time_left = self.end_time - datetime.now() if self.status == self.STATUS_ACTIVE: self.layout.colour = self.color_active else: self.layout.colour = self.color_break time_string = "%d:%02d:%02d" % ( time_left.seconds // 3600, time_left.seconds % 3600 // 60, time_left.seconds % 60, ) return self.prefix[self.status] + time_string @expose_command() def toggle_break(self): if self.status == self.STATUS_INACTIVE: self.status = self.STATUS_START return if self.paused_status is None: self.paused_status = self.status self.time_left = self.end_time - datetime.now() self.status = self.STATUS_PAUSED if self.notification_on: self._send_notification("low", "Pomodoro has been paused") else: self.status = self.paused_status self.paused_status = None self.end_time = self.time_left + datetime.now() if self.notification_on: if self.status == self.STATUS_ACTIVE: status = "Pomodoro" else: status = "break" self._send_notification( "normal", f"Please continue on {status}! End Time: " + self.end_time.strftime("%H:%M"), ) @expose_command() def toggle_active(self): if self.status != self.STATUS_INACTIVE: self.status = self.STATUS_INACTIVE if self.notification_on: self._send_notification("critical", "Pomodoro has been suspended") else: self.status = self.STATUS_START def _send_notification(self, urgent, message): send_notification("Pomodoro", message, urgent=urgent) def poll(self): return self._get_text() qtile-0.31.0/libqtile/widget/mpris2widget.py0000664000175000017500000003622714762660347020732 0ustar epsilonepsilon# Copyright (c) 2014 Sebastian Kricner # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Adi Sieker # Copyright (c) 2014 Tycho Andersen # Copyright (c) 2020 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations import asyncio import re import string from typing import TYPE_CHECKING from dbus_fast import Message, Variant from dbus_fast.aio import MessageBus from dbus_fast.constants import MessageType from libqtile import pangocffi from libqtile.command.base import expose_command from libqtile.log_utils import logger from libqtile.utils import _send_dbus_message, add_signal_receiver, create_task from libqtile.widget import base if TYPE_CHECKING: from typing import Any MPRIS_PATH = "/org/mpris/MediaPlayer2" MPRIS_OBJECT = "org.mpris.MediaPlayer2" MPRIS_PLAYER = "org.mpris.MediaPlayer2.Player" PROPERTIES_INTERFACE = "org.freedesktop.DBus.Properties" MPRIS_REGEX = re.compile(r"(\{(.*?):(.*?)(:.*?)?\})") class Mpris2Formatter(string.Formatter): """ Custom string formatter for MPRIS2 metadata. Keys have a colon (e.g. "xesam:title") which causes issues with python's string formatting as the colon splits the identifier from the format specification. This formatter handles this issue by changing the first colon to an underscore and then formatting the incoming kwargs to match. Additionally, a default value is returned when an identifier is not provided by the kwarg data. """ def __init__(self, default=""): string.Formatter.__init__(self) self._default = default def get_value(self, key, args, kwargs): """ Replaces colon in kwarg keys with an underscore before getting value. Missing identifiers are replaced with the default value. """ kwargs = {k.replace(":", "_"): v for k, v in kwargs.items()} try: return pangocffi.markup_escape_text( string.Formatter.get_value(self, key, args, kwargs) ) except (IndexError, KeyError): return self._default def parse(self, format_string): """ Replaces first colon in format string with an underscore. This will cause issues if any identifier is provided that does not contain a colon. This should not happen according to the MPRIS2 specification! """ format_string = MPRIS_REGEX.sub(r"{\2_\3\4}", format_string) return string.Formatter.parse(self, format_string) class Mpris2(base._TextBox): """An MPRIS 2 widget A widget which displays the current track/artist of your favorite MPRIS player. This widget scrolls the text if neccessary and information that is displayed is configurable. The widget relies on players broadcasting signals when the metadata or playback status changes. If you are getting inconsistent results then you can enable background polling of the player by setting the `poll_interval` parameter. This is disabled by default. Basic mouse controls are also available: button 1 = play/pause, scroll up = next track, scroll down = previous track. Widget requirements: dbus-fast_. .. _dbus-fast: https://pypi.org/project/dbus-fast/ """ defaults = [ ("name", "audacious", "Name of the MPRIS widget."), ( "objname", None, "DBUS MPRIS 2 compatible player identifier" "- Find it out with dbus-monitor - " "Also see: http://specifications.freedesktop.org/" "mpris-spec/latest/#Bus-Name-Policy. " "``None`` will listen for notifications from all MPRIS2 compatible players.", ), ( "format", "{xesam:title} - {xesam:album} - {xesam:artist}", "Format string for displaying metadata. " "See http://www.freedesktop.org/wiki/Specifications/mpris-spec/metadata/#index5h3 " "for available values. The special filed '{qtile:player}' can be used to display the " "player name.", ), ("separator", ", ", "Separator for metadata fields that are a list."), ( "display_metadata", ["xesam:title", "xesam:album", "xesam:artist"], "(Deprecated) Which metadata identifiers to display. ", ), ("scroll", True, "Whether text should scroll."), ("playing_text", "{track}", "Text to show when playing"), ("paused_text", "Paused: {track}", "Text to show when paused"), ("stopped_text", "", "Text to show when stopped"), ( "stop_pause_text", None, "(Deprecated) Optional text to display when in the stopped/paused state", ), ( "no_metadata_text", "No metadata for current track", "Text to show when track has no metadata", ), ( "poll_interval", 0, "Periodic background polling interval of player (0 to disable polling).", ), ] def __init__(self, **config): base._TextBox.__init__(self, "", **config) self.add_defaults(Mpris2.defaults) self.is_playing = False self.count = 0 self.displaytext = "" self.track_info = "" self.status = "{track}" self.add_callbacks( { "Button1": self.play_pause, "Button4": self.next, "Button5": self.previous, } ) paused = "" stopped = "" if "stop_pause_text" in config: logger.warning( "The use of 'stop_pause_text' is deprecated. Please use 'paused_text' and 'stopped_text' instead." ) if "paused_text" not in config: paused = self.stop_pause_text if "stopped_text" not in config: stopped = self.stop_pause_text if "display_metadata" in config: logger.warning( "The use of `display_metadata is deprecated. Please use `format` instead." ) self.format = " - ".join(f"{{{s}}}" for s in config["display_metadata"]) self._formatter = Mpris2Formatter() self.prefixes = { "Playing": self.playing_text, "Paused": paused or self.paused_text, "Stopped": stopped or self.stopped_text, } self._current_player: str | None = None self.player_names: dict[str, str] = {} self._background_poll: asyncio.TimerHandle | None = None self.bus: MessageBus | None = None @property def player(self) -> str: if self._current_player is None: return "None" else: return self.player_names.get(self._current_player, "Unknown") async def _config_async(self): # These two listeners create separate bus connections. Each connection only has one # callback so we don't need any logic to identify the message and the appropriate # handler in this code. # Set up a listener for NameOwner changes so we can remove players when they close await add_signal_receiver( self._name_owner_changed, session_bus=True, signal_name="NameOwnerChanged", dbus_interface="org.freedesktop.DBus", ) # Listen out for signals from any Mpris2 compatible player subscribe = await add_signal_receiver( self.message, session_bus=True, signal_name="PropertiesChanged", bus_name=self.objname, path="/org/mpris/MediaPlayer2", dbus_interface="org.freedesktop.DBus.Properties", ) if not subscribe: logger.warning("Unable to add signal receiver for Mpris2 players") # If the user has specified a player to be monitored, we can poll it now. if self.objname is not None: await self._check_player() def _name_owner_changed(self, message): # We need to track when an interface has been removed from the bus # We use the NameOwnerChanged signal and check if the new owner is # empty. name, _, new_owner = message.body # Check if the current player has closed if new_owner == "" and name == self._current_player: self._current_player = None self.update("") # Cancel any scheduled background poll self._set_background_poll(False) def message(self, message): create_task(self.process_message(message)) async def process_message(self, message): current_player = message.sender if current_player not in self.player_names: self.player_names[current_player] = await self.get_player_name(current_player) self._current_player = current_player self.parse_message(*message.body) async def _send_message(self, destination, interface, path, member, signature, body): bus, message = await _send_dbus_message( session_bus=True, message_type=MessageType.METHOD_CALL, destination=destination, interface=interface, path=path, member=member, signature=signature, body=body, bus=self.bus, ) # We should reuse the same bus connection for repeated calls. if self.bus is None: self.bus = bus return message async def _check_player(self): """Check for player at startup and retrieve metadata.""" if not (self.objname or self._current_player): return message = await self._send_message( self.objname if self.objname else self._current_player, PROPERTIES_INTERFACE, MPRIS_PATH, "GetAll", "s", [MPRIS_PLAYER], ) # If we get an error here it will be because the player object doesn't exist if message.message_type != MessageType.METHOD_RETURN: self._current_player = None self.update("") return if message.body: self._current_player = message.sender self.parse_message(self.objname, message.body[0], []) def _set_background_poll(self, poll=True): if self._background_poll is not None: self._background_poll.cancel() if poll: self._background_poll = self.timeout_add(self.poll_interval, self._check_player) async def get_player_name(self, player): message = await self._send_message( player, PROPERTIES_INTERFACE, MPRIS_PATH, "Get", "ss", [MPRIS_OBJECT, "Identity"], ) if message.message_type != MessageType.METHOD_RETURN: logger.warning("Could not retrieve identity of player on %s.", player) return "" return message.body[0].value def parse_message( self, _interface_name: str, changed_properties: dict[str, Any], _invalidated_properties: list[str], ) -> None: """ http://specifications.freedesktop.org/mpris-spec/latest/Track_List_Interface.html#Mapping:Metadata_Map """ if not self.configured: return if "Metadata" not in changed_properties and "PlaybackStatus" not in changed_properties: return self.displaytext = "" metadata = changed_properties.get("Metadata") if metadata: self.track_info = self.get_track_info(metadata.value) playbackstatus = getattr(changed_properties.get("PlaybackStatus"), "value", None) if playbackstatus: self.is_playing = playbackstatus == "Playing" self.status = self.prefixes.get(playbackstatus, "{track}") if not self.track_info: self.track_info = self.no_metadata_text self.displaytext = self.status.format(track=self.track_info) if self.text != self.displaytext: self.update(self.displaytext) if self.poll_interval: self._set_background_poll() def get_track_info(self, metadata: dict[str, Variant]) -> str: self.metadata = {} for key in metadata: new_key = key val = getattr(metadata.get(key), "value", None) if isinstance(val, str): self.metadata[new_key] = val elif isinstance(val, list): self.metadata[new_key] = self.separator.join(y for y in val if isinstance(y, str)) if self.player is not None: self.metadata["qtile:player"] = self.player return self._formatter.format(self.format, **self.metadata).replace("\n", "") def _player_cmd(self, cmd: str) -> None: if self._current_player is None: return task = create_task(self._send_player_cmd(cmd)) assert task task.add_done_callback(self._task_callback) async def _send_player_cmd(self, cmd: str) -> Message | None: message = await self._send_message( self._current_player, MPRIS_PLAYER, MPRIS_PATH, cmd, "", [], ) return message def _task_callback(self, task: asyncio.Task) -> None: message = task.result() # This happens if we can't connect to dbus. Logger call is made # elsewhere so we don't need to do any more here. if message is None: return if message.message_type != MessageType.METHOD_RETURN: logger.warning("Unable to send command to player.") @expose_command() def play_pause(self) -> None: """Toggle the playback status.""" self._player_cmd("PlayPause") @expose_command() def next(self) -> None: """Play the next track.""" self._player_cmd("Next") @expose_command() def previous(self) -> None: """Play the previous track.""" self._player_cmd("Previous") @expose_command() def stop(self) -> None: """Stop playback.""" self._player_cmd("Stop") @expose_command() def info(self): """What's the current state of the widget?""" d = base._TextBox.info(self) d.update(dict(isplaying=self.is_playing, player=self.player)) return d qtile-0.31.0/libqtile/widget/df.py0000664000175000017500000000576314762660347016704 0ustar epsilonepsilon# Copyright (c) 2015, Roger Duran. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import os from libqtile.widget import base class DF(base.ThreadPoolText): """Disk Free Widget By default the widget only displays if the space is less than warn_space. """ defaults = [ ("partition", "/", "the partition to check space"), ("warn_color", "ff0000", "Warning color"), ("warn_space", 2, "Warning space in scale defined by the ``measure`` option."), ("visible_on_warn", True, "Only display if warning"), ("measure", "G", "Measurement (G, M, B)"), ( "format", "{p} ({uf}{m}|{r:.0f}%)", "String format (p: partition, s: size, " "f: free space, uf: user free space, m: measure, r: ratio (uf/s))", ), ("update_interval", 60, "The update interval."), ] measures = {"G": 1024 * 1024 * 1024, "M": 1024 * 1024, "B": 1024} def __init__(self, **config): base.ThreadPoolText.__init__(self, "", **config) self.add_defaults(DF.defaults) self.user_free = 0 self.calc = self.measures[self.measure] def draw(self): if self.user_free <= self.warn_space: self.layout.colour = self.warn_color else: self.layout.colour = self.foreground base.ThreadPoolText.draw(self) def poll(self): statvfs = os.statvfs(self.partition) size = statvfs.f_frsize * statvfs.f_blocks // self.calc free = statvfs.f_frsize * statvfs.f_bfree // self.calc self.user_free = statvfs.f_frsize * statvfs.f_bavail // self.calc if self.visible_on_warn and self.user_free >= self.warn_space: text = "" else: text = self.format.format( p=self.partition, s=size, f=free, uf=self.user_free, m=self.measure, r=(size - self.user_free) / size * 100, ) return text qtile-0.31.0/libqtile/widget/hdd.py0000664000175000017500000000413714762660347017044 0ustar epsilonepsilon# Copyright (c) 2024 Florian G. Hechler # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from libqtile.widget import base class HDD(base.ThreadPoolText): """ Displays HDD usage in percent based on the number of milliseconds the device has been performing I/O operations. """ defaults = [ ("device", "sda", "Block device to monitor (e.g. sda)"), ( "format", "HDD {HDDPercent}%", "HDD display format", ), ] def __init__(self, **config): super().__init__("", **config) self.add_defaults(HDD.defaults) self.path = f"/sys/block/{self.device}/stat" self._prev = 0 def poll(self): variables = dict() # Field index 9 contains the number of milliseconds the device has been performing I/O operations with open(self.path) as f: io_ticks = int(f.read().split()[9]) variables["HDDPercent"] = round( max(min(((io_ticks - self._prev) / self.update_interval) / 10, 100.0), 0.0), 1 ) self._prev = io_ticks return self.format.format(**variables) qtile-0.31.0/libqtile/widget/plasma.py0000664000175000017500000001061314762660347017556 0ustar epsilonepsilon# Copyright (c) 2024 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from typing import Any from libqtile import bar, hook from libqtile.command.base import expose_command from libqtile.layout import plasma from libqtile.layout.plasma import AddMode from libqtile.widget import base class Plasma(base._TextBox): """ A simple widget to indicate in which direction new windows will be added in the Plasma layout. """ defaults: list[tuple[str, Any, str]] = [ ("horizontal", "H", "Text to display if horizontal mode"), ("vertical", "V", "Text to display if horizontal mode"), ("split", "S", "Text to append to mode if ``horizontal/vertical_split`` not set"), ("horizontal_split", None, "Text to display for horizontal split mode"), ("vertical_split", None, "Text to display for horizontal split mode"), ("format", "{mode:>2}", "Format appearance of text"), ] def __init__(self, text="", width=bar.CALCULATED, **config): base._TextBox.__init__(self, text=text, width=width, **config) self.add_defaults(Plasma.defaults) self.add_callbacks({"Button1": self.next_mode}) if self.horizontal_split is None: self.horizontal_split = self.horizontal + self.split if self.vertical_split is None: self.vertical_split = self.vertical + self.split self.modes = [ AddMode.HORIZONTAL, AddMode.HORIZONTAL | AddMode.SPLIT, AddMode.VERTICAL, AddMode.VERTICAL | AddMode.SPLIT, ] self._mode = self.modes[0] self._layout = None def _configure(self, qtile, bar): base._TextBox._configure(self, qtile, bar) hook.subscribe.plasma_add_mode(self.mode_changed) hook.subscribe.layout_change(self.layout_changed) self.mode_changed(bar.screen.group.layout) def mode_changed(self, layout): """Update text depending on add_mode of layout.""" if not isinstance(layout, plasma.Plasma): self.update("") self._layout = None return self._layout = layout if layout.group and layout.group.screen is not self.bar.screen: return if layout.horizontal: if layout.split: mode = self.horizontal_split self._mode = AddMode.HORIZONTAL | AddMode.SPLIT else: mode = self.horizontal self._mode = AddMode.HORIZONTAL else: if layout.split: mode = self.vertical_split self._mode = AddMode.VERTICAL | AddMode.SPLIT else: mode = self.vertical self._mode = AddMode.VERTICAL self.update(self.format.format(mode=mode)) def layout_changed(self, layout, group): """Update widget when layout changes.""" if group.screen is self.bar.screen: self.mode_changed(layout) def finalize(self): hook.unsubscribe.plasma_add_mode(self.mode_changed) hook.unsubscribe.layout_change(self.layout_changed) base._TextBox.finalize(self) @expose_command() def next_mode(self): """Change the add mode for the Plasma layout.""" if self._layout is None: return index = self.modes.index(self._mode) index = (index + 1) % len(self.modes) self._layout.add_mode = self.modes[index] qtile-0.31.0/libqtile/widget/pulse_volume.py0000664000175000017500000002216414762660347021024 0ustar epsilonepsilon# Copyright (c) 2023 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import asyncio import pulsectl_asyncio from pulsectl import PulseError from libqtile import qtile from libqtile.command.base import expose_command from libqtile.log_utils import logger from libqtile.utils import create_task from libqtile.widget.volume import Volume lock = asyncio.Lock() class PulseConnection: """ Class object to manage the connection to the pulse server and send volume/mute status to subscribed clients. """ def __init__(self): self._subscribed = False self._event_handler = None self.default_sink = None self.default_sink_name = None self.pulse = None self.configured = False self.callbacks = set() self.qtile = qtile self.timer = None async def _configure(self): # Use a lock to prevent multiple connection attempts async with lock: if self.configured: return # Create pulse async object but don't connect self.pulse = pulsectl_asyncio.PulseAsync("qtile-pulse") # Try to connect await self._check_pulse_connection() self.configured = True async def _check_pulse_connection(self): """ The PulseAsync object subscribes to connection state events so we need to check periodically whether the connection has been lost. """ if not self.pulse.connected: # Check if we were previously connected to the server and, # if so, stop the event handler if self._subscribed: if self._event_handler is not None: self._event_handler.cancel() self._event_handler = None self._subscribed = False try: await self.pulse.connect() logger.debug("Connection to pulseaudio ready") except PulseError: logger.warning("Failed to connect to pulseaudio, retrying in 10s") else: # We're connected so get details of the default sink await self.get_server_info() # Start event listeners for sink and server events self._event_handler = create_task(self._event_listener()) self._subscribed = True # Set a timer to check status in 10 seconds time self.timer = self.qtile.call_later(10, create_task, self._check_pulse_connection()) async def _event_listener(self): """Listens for sink and server events from the server.""" async for event in self.pulse.subscribe_events("sink", "server"): # Sink events will signify volume changes if event.facility == "sink": await self.get_sink_info() # Server events include when the default sink changes elif event.facility == "server": await self.get_server_info() async def get_server_info(self): """Updates the default sink name.""" info = await self.pulse.server_info() self.default_sink_name = info.default_sink_name await self.get_sink_info() async def get_sink_info(self): """Gets a reference to the default sink and triggers update for subscribed clients.""" sinks = [ sink for sink in await self.pulse.sink_list() if sink.name == self.default_sink_name ] if not sinks: logger.warning("Could not get info for default sink") self.default_sink = None return self.default_sink = sinks[0] self.update_clients() def get_volume(self): """Gets volume and mute status for default sink.""" if not self.pulse.connected: return None, None if self.default_sink: mute = self.default_sink.mute base = self.default_sink.base_volume if not base: return -1, mute current = self.default_sink.volume.value_flat return round(current * 100 / base), mute return -1, 0 def update_clients(self): """Sends volume and mute status to subscribed clients.""" for callback in self.callbacks: callback(*self.get_volume()) def subscribe(self, callback): """ Subscribes a client for callback events. The first subscription will trigger the connection to the pulse server. """ need_configure = not bool(self.callbacks) self.callbacks.add(callback) if need_configure: create_task(self._configure()) def unsubscribe(self, callback): """ Unsubscribes a client from callback events. Removing the last client closes the connection with the pulse server and cancels future calls to connect. """ self.callbacks.discard(callback) if not self.callbacks: self.pulse.close() # Prevent future calls to connect to the server if self.timer: self.timer.cancel() self.timer = None self.configured = False pulse = PulseConnection() class PulseVolume(Volume): """ Volume widget for systems using PulseAudio. The widget connects to the PulseAudio server by using the libpulse library and so should be updated virtually instantly rather than needing to poll the volume status regularly (NB this means that the ``update_interval`` parameter serves no purpose for this widget). The widget relies on the `pulsectl_asyncio `__ library to access the libpulse bindings. If you are using python 3.11 you must use ``pulsectl_asyncio >= 1.0.0``. """ defaults = [ ("limit_max_volume", False, "Limit maximum volume to 100%"), ] def __init__(self, **config): Volume.__init__(self, **config) self.add_defaults(PulseVolume.defaults) self.volume = 0 self.is_mute = 0 self._previous_state = (-1.0, -1) def _configure(self, qtile, bar): Volume._configure(self, qtile, bar) if self.theme_path: self.setup_images() pulse.subscribe(self.get_vals) async def _change_volume(self, volume): """Sets volume on default sink.""" await pulse.pulse.volume_set_all_chans(pulse.default_sink, volume) async def _mute(self): """Toggles mute status of default sink.""" await pulse.pulse.sink_mute(pulse.default_sink.index, not pulse.default_sink.mute) @expose_command() def mute(self): """Mute the sound device.""" create_task(self._mute()) @expose_command() def increase_vol(self, value=None): """Increase volume.""" if not value: value = pulse.default_sink.volume.value_flat + (self.step / 100.0) base = pulse.default_sink.base_volume if self.limit_max_volume and value > base: value = base create_task(self._change_volume(value)) @expose_command() def decrease_vol(self, value=None): """Decrease volume.""" if not value: value = pulse.default_sink.volume.value_flat - (self.step / 100.0) value = max(value, 0) create_task(self._change_volume(value)) def get_vals(self, vol, muted): if (vol, muted) != self._previous_state: self.volume = vol self.is_mute = muted self._previous_state = (vol, muted) self.update() def update(self): """ same method as in Volume widgets except that here we don't need to manually re-schedule update """ if pulse.pulse is None or not pulse.pulse.connected: return # Update the underlying canvas size before actually attempting # to figure out how big it is and draw it. length = self.length self._update_drawer() if self.length == length: self.draw() else: self.bar.draw() def finalize(self): # Close the connection to the server pulse.unsubscribe(self.get_vals) Volume.finalize(self) qtile-0.31.0/libqtile/widget/wttr.py0000664000175000017500000000621014762660347017277 0ustar epsilonepsilonfrom urllib.parse import quote, urlencode from libqtile.widget import GenPollUrl class Wttr(GenPollUrl): """Display weather widget provided by wttr.in_. .. _wttr.in: https://github.com/chubin/wttr.in/ To specify your own custom output format, use the special %-notation (example: 'My_city: %t(%f), wind: %w'): - %c Weather condition, - %C Weather condition textual name, - %h Humidity, - %t Temperature (Actual), - %f Temperature (Feels Like), - %w Wind, - %l Location, - %m Moonphase 🌑🌒🌓🌔🌕🌖🌗🌘, - %M Moonday, - %p precipitation (mm), - %P pressure (hPa), - %D Dawn !, - %S Sunrise !, - %z Zenith !, - %s Sunset !, - %d Dusk !. (!times are shown in the local timezone) Add the character ``~`` at the beginning to get weather for some special location: ``~Vostok Station`` or ``~Eiffel Tower``. Also can use IP-addresses (direct) or domain names (prefixed with @) to specify a location: ``@github.com``, ``123.456.678.123`` Specify multiple locations as dictionary :: location={ 'Minsk': 'Minsk', '64.127146,-21.873472': 'Reykjavik', } Cities will change randomly every update. """ defaults = [ ( "format", "3", 'Display text format. Choose presets in range 1-4 (Ex. ``"1"``) ' "or build your own custom output format, use the special " "%-notation. See https://github.com/chubin/wttr.in#one-line-output", ), ("json", False, "Is Json?"), ( "lang", "en", "Display text language. List of supported languages " "https://wttr.in/:translation", ), ( "location", {}, "Dictionary. Key is a city or place name, or GPS coordinates. " "Value is a display name. If the dictionary is empty, " "the location will be determined based on your IP address.", ), ( "units", "m", "``'m'`` - metric, ``'M'`` - show wind speed in m/s, " "``'u'`` - United States units", ), ( "update_interval", 600, "Update interval in seconds. Recommendation: if you want to " "display multiple locations alternately, maybe set a smaller " "interval, ex. ``30``.", ), ] def __init__(self, **config): GenPollUrl.__init__(self, json=False, **config) self.add_defaults(Wttr.defaults) self.url = self._get_url() def _get_url(self): params = { "format": self.format, "lang": self.lang, } location = ":".join(quote(loc) for loc in self.location) url = f"https://wttr.in/{location}?{self.units}&{urlencode(params)}" return url def parse(self, response): for coord in self.location: response = response.strip().replace(coord, self.location[coord]) return response qtile-0.31.0/libqtile/widget/thermal_zone.py0000664000175000017500000000362314762660347020773 0ustar epsilonepsilonfrom libqtile.log_utils import logger from libqtile.widget import base class ThermalZone(base.ThreadPoolText): """Thermal zone widget. This widget was made to read thermal zone files and transform values to human readable format. You can set zone parameter to any standard thermal zone file from /sys/class/thermal directory. """ orientations = base.ORIENTATION_HORIZONTAL defaults = [ ("update_interval", 2.0, "Update interval"), ("zone", "/sys/class/thermal/thermal_zone0/temp", "Thermal zone"), ("format", "{temp}°C", "Display format"), ("format_crit", "{temp}°C CRIT!", "Critical display format"), ("hidden", False, "Set True to only show if critical value reached"), ("fgcolor_crit", "ff0000", "Font color on critical values"), ("fgcolor_high", "ffaa00", "Font color on high values"), ("fgcolor_normal", "ffffff", "Font color on normal values"), ("crit", 70, "Critical temperature level"), ("high", 50, "High themperature level"), ] def __init__(self, **config): super().__init__("", **config) self.add_defaults(ThermalZone.defaults) def poll(self): try: with open(self.zone) as f: value = round(int(f.read().rstrip()) / 1000) except OSError: logger.exception("%s does not exist", self.zone) return "err!" variables = dict() variables["temp"] = str(value) output = self.format.format(**variables) if value < self.high: self.layout.colour = self.fgcolor_normal elif value < self.crit: self.layout.colour = self.fgcolor_high elif value >= self.crit: self.layout.colour = self.fgcolor_crit output = self.format_crit.format(**variables) if self.hidden and value < self.crit: output = "" return output qtile-0.31.0/libqtile/widget/gmail_checker.py0000664000175000017500000000527014762660347021061 0ustar epsilonepsilon# Copyright (c) 2014 Sean Vig # Copyright (c) 2014, 2019 zordsdavini # Copyright (c) 2014 Alexandr Kriptonov # Copyright (c) 2014 Tycho Andersen # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import imaplib import re from libqtile.log_utils import logger from libqtile.widget import base class GmailChecker(base.ThreadPoolText): """ A simple gmail checker. If 'status_only_unseen' is True - set 'fmt' for one argument, ex. 'unseen: {0}' """ defaults = [ ("update_interval", 30, "Update time in seconds."), ("username", None, "username"), ("password", None, "password"), ("email_path", "INBOX", "email_path"), ("display_fmt", "inbox[{0}],unseen[{1}]", "Display format"), ("status_only_unseen", False, "Only show unseen messages"), ] def __init__(self, **config): base.ThreadPoolText.__init__(self, "", **config) self.add_defaults(GmailChecker.defaults) def poll(self): self.gmail = imaplib.IMAP4_SSL("imap.gmail.com") self.gmail.login(self.username, self.password) answer, raw_data = self.gmail.status(self.email_path, "(MESSAGES UNSEEN)") if answer == "OK": dec = raw_data[0].decode() messages = int(re.search(r"MESSAGES\s+(\d+)", dec).group(1)) unseen = int(re.search(r"UNSEEN\s+(\d+)", dec).group(1)) if self.status_only_unseen: return self.display_fmt.format(unseen) else: return self.display_fmt.format(messages, unseen) else: logger.exception( "GmailChecker UNKNOWN error, answer: %s, raw_data: %s", answer, raw_data ) return "UNKNOWN ERROR" qtile-0.31.0/libqtile/widget/graph.py0000664000175000017500000003442614762660347017412 0ustar epsilonepsilon# Copyright (c) 2010 Aldo Cortesi # Copyright (c) 2010-2011 Paul Colomiets # Copyright (c) 2010, 2014 roger # Copyright (c) 2011 Mounier Florian # Copyright (c) 2011 Kenji_Takahashi # Copyright (c) 2012 Mika Fischer # Copyright (c) 2012, 2014-2015 Tycho Andersen # Copyright (c) 2012-2013 Craig Barnes # Copyright (c) 2013 dequis # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2013 Mickael FALCK # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Adi Sieker # Copyright (c) 2014 Florian Scherf # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import itertools import operator import time from os import statvfs import cairocffi import psutil from libqtile.log_utils import logger from libqtile.widget import base __all__ = [ "CPUGraph", "MemoryGraph", "SwapGraph", "NetGraph", "HDDGraph", "HDDBusyGraph", ] class _Graph(base._Widget): fixed_upper_bound = False defaults = [ ("graph_color", "18BAEB", "Graph color"), ("fill_color", "1667EB.3", "Fill color for linefill graph"), ("border_color", "215578", "Widget border color"), ("border_width", 2, "Widget border width"), ("margin_x", 3, "Margin X"), ("margin_y", 3, "Margin Y"), ("samples", 100, "Count of graph samples."), ("frequency", 1, "Update frequency in seconds"), ("type", "linefill", "'box', 'line', 'linefill'"), ("line_width", 3, "Line width"), ("start_pos", "bottom", "Drawer starting position ('bottom'/'top')"), ] def __init__(self, width=100, **config): base._Widget.__init__(self, width, **config) self.add_defaults(_Graph.defaults) self.values = [0] * self.samples self.maxvalue = 0 self.oldtime = time.time() self.lag_cycles = 0 def _configure(self, qtile, bar): super()._configure(qtile, bar) if self.type == "box": self.drawer.ctx.set_antialias(cairocffi.ANTIALIAS_NONE) def timer_setup(self): self.timeout_add(self.frequency, self.update) @property def graphwidth(self): return self.width - self.border_width * 2 - self.margin_x * 2 @property def graphheight(self): return self.bar.height - self.margin_y * 2 - self.border_width * 2 def step(self): return self.graphwidth / float(self.samples) def _for_each_step(self, values): yield from enumerate( itertools.islice( values, max(int(-(self.graphwidth / self.step()) + len(values)), 0), len(values), ) ) def _prepare_context(self): self.drawer.ctx.set_line_join(cairocffi.LINE_JOIN_ROUND) if self.graph_color is not None: self.drawer.set_source_rgb(self.graph_color) self.drawer.ctx.set_line_width(self.line_width) def draw_box(self, x, y, values): self._prepare_context() for _, val in self._for_each_step(values): val = self.val(val) self.drawer.ctx.rectangle(x, y - val, self.step(), val) x += self.step() self.drawer.ctx.fill() self.drawer.ctx.stroke() def draw_line(self, x, y, values): self._prepare_context() for _, val in self._for_each_step(values): self.drawer.ctx.line_to(x, y - self.val(val)) x += self.step() self.drawer.ctx.stroke() def draw_linefill(self, x, y, values): self._prepare_context() for index, val in self._for_each_step(values): self.drawer.ctx.line_to(x + index * self.step(), y - self.val(val)) self.drawer.ctx.stroke_preserve() self.drawer.ctx.line_to( x + (len(values) - 1) * self.step(), y - 1 + self.line_width / 2.0 ) self.drawer.ctx.line_to(x, y - 1 + self.line_width / 2.0) self.drawer.set_source_rgb(self.fill_color) self.drawer.ctx.fill() def val(self, val): if self.start_pos == "bottom": return val elif self.start_pos == "top": return -val else: raise ValueError(f"Unknown starting position: {self.start_pos}.") def draw(self): self.drawer.clear(self.background or self.bar.background) if self.border_width: self.drawer.set_source_rgb(self.border_color) self.drawer.ctx.set_line_width(self.border_width) self.drawer.ctx.rectangle( self.margin_x + self.border_width / 2.0, self.margin_y + self.border_width / 2.0, self.graphwidth + self.border_width, self.bar.height - self.margin_y * 2 - self.border_width, ) self.drawer.ctx.stroke() x = self.margin_x + self.border_width y = self.margin_y + self.border_width if self.start_pos == "bottom": y += self.graphheight elif not self.start_pos == "top": raise ValueError(f"Unknown starting position: {self.start_pos}.") k = 1.0 / (self.maxvalue or 1) scaled = [self.graphheight * val * k for val in reversed(self.values)] if self.type == "box": self.draw_box(x, y, scaled) elif self.type == "line": self.draw_line(x, y, scaled) elif self.type == "linefill": self.draw_linefill(x, y, scaled) else: raise ValueError(f"Unknown graph type: {self.type}.") self.drawer.draw(offsetx=self.offset, offsety=self.offsety, width=self.width) def push(self, value): if self.lag_cycles > self.samples: # compensate lag by sending the same value up to # the graph samples limit self.lag_cycles = 1 self.values = ([value] * min(self.samples, self.lag_cycles)) + self.values self.values = self.values[: self.samples] if not self.fixed_upper_bound: self.maxvalue = max(self.values) self.draw() def update(self): # lag detection newtime = time.time() self.lag_cycles = int((newtime - self.oldtime) / self.frequency) self.oldtime = newtime self.update_graph() self.timeout_add(self.frequency, self.update) def fulfill(self, value): self.values = [value] * len(self.values) class CPUGraph(_Graph): """Display CPU usage graph. Widget requirements: psutil_. .. _psutil: https://pypi.org/project/psutil/ """ orientations = base.ORIENTATION_HORIZONTAL defaults = [ ("core", "all", "Which core to show (all/0/1/2/...)"), ] fixed_upper_bound = True def __init__(self, **config): _Graph.__init__(self, **config) self.add_defaults(CPUGraph.defaults) self.maxvalue = 100 self.oldvalues = self._getvalues() def _getvalues(self): if isinstance(self.core, int): if self.core > psutil.cpu_count() - 1: raise ValueError(f"No such core: {self.core}") cpu = psutil.cpu_times(percpu=True)[self.core] else: cpu = psutil.cpu_times() user = cpu.user * 100 nice = cpu.nice * 100 sys = cpu.system * 100 idle = cpu.idle * 100 return (int(user), int(nice), int(sys), int(idle)) def update_graph(self): nval = self._getvalues() oval = self.oldvalues busy = nval[0] + nval[1] + nval[2] - oval[0] - oval[1] - oval[2] total = busy + nval[3] - oval[3] # sometimes this value is zero for unknown reason (time shift?) # we just sent the previous value, because it gives us no info about # cpu load, if it's zero. if total: push_value = busy * 100.0 / total self.push(push_value) else: self.push(self.values[0]) self.oldvalues = nval class MemoryGraph(_Graph): """Displays a memory usage graph. Widget requirements: psutil_. .. _psutil: https://pypi.org/project/psutil/ """ orientations = base.ORIENTATION_HORIZONTAL fixed_upper_bound = True def __init__(self, **config): _Graph.__init__(self, **config) val = self._getvalues() self.maxvalue = val["MemTotal"] mem = val["MemTotal"] - val["MemFree"] - val["Buffers"] - val["Cached"] self.fulfill(mem) def _getvalues(self): val = {} mem = psutil.virtual_memory() val["MemTotal"] = int(mem.total / 1024 / 1024) val["MemFree"] = int(mem.free / 1024 / 1024) val["Buffers"] = int(mem.buffers / 1024 / 1024) val["Cached"] = int(mem.cached / 1024 / 1024) return val def update_graph(self): val = self._getvalues() self.push(val["MemTotal"] - val["MemFree"] - val["Buffers"] - val["Cached"]) class SwapGraph(_Graph): """Display a swap info graph. Widget requirements: psutil_. .. _psutil: https://pypi.org/project/psutil/ """ orientations = base.ORIENTATION_HORIZONTAL fixed_upper_bound = True def __init__(self, **config): _Graph.__init__(self, **config) val = self._getvalues() self.maxvalue = val["SwapTotal"] swap = val["SwapTotal"] - val["SwapFree"] self.fulfill(swap) def _getvalues(self): val = {} swap = psutil.swap_memory() val["SwapTotal"] = int(swap.total / 1024 / 1024) val["SwapFree"] = int(swap.free / 1024 / 1024) return val def update_graph(self): val = self._getvalues() swap = val["SwapTotal"] - val["SwapFree"] # can change, swapon/off if self.maxvalue != val["SwapTotal"]: self.maxvalue = val["SwapTotal"] self.fulfill(swap) self.push(swap) class NetGraph(_Graph): """Display a network usage graph. Widget requirements: psutil_. .. _psutil: https://pypi.org/project/psutil/""" orientations = base.ORIENTATION_HORIZONTAL defaults = [ ("interface", "auto", "Interface to display info for ('auto' for detection)"), ("bandwidth_type", "down", "down(load)/up(load)"), ] def __init__(self, **config): _Graph.__init__(self, **config) self.add_defaults(NetGraph.defaults) if self.interface == "auto": try: self.interface = self.get_main_iface() except RuntimeError: logger.warning( "NetGraph - Automatic interface detection failed, falling back to 'eth0'" ) self.interface = "eth0" if self.bandwidth_type != "down" and self.bandwidth_type != "up": raise ValueError(f"bandwidth type {self.bandwidth_type} not known!") self.bytes = 0 self.bytes = self._get_values() def _get_values(self): net = psutil.net_io_counters(pernic=True) if self.bandwidth_type == "up": return net[self.interface].bytes_sent if self.bandwidth_type == "down": return net[self.interface].bytes_recv def update_graph(self): val = self._get_values() change = val - self.bytes self.bytes = val self.push(change) @staticmethod def get_main_iface(): # XXX: psutil doesn't have the facility to get the main interface, # so I'll just return the interface that has received the most traffic. # # I could do this with netifaces, but that's another dependency. # # Oh. and there is probably a better way to do this. net = psutil.net_io_counters(pernic=True) iface = {} for entry in net: iface[entry] = net[entry].bytes_recv return sorted(iface.items(), key=operator.itemgetter(1))[-1][0] class HDDGraph(_Graph): """Display HDD free or used space graph""" fixed_upper_bound = True orientations = base.ORIENTATION_HORIZONTAL defaults = [("path", "/", "Partition mount point."), ("space_type", "used", "free/used")] def __init__(self, **config): _Graph.__init__(self, **config) self.add_defaults(HDDGraph.defaults) stats = statvfs(self.path) self.maxvalue = stats.f_blocks * stats.f_frsize values = self._get_values() self.fulfill(values) def _get_values(self): stats = statvfs(self.path) if self.space_type == "used": return (stats.f_blocks - stats.f_bfree) * stats.f_frsize else: return stats.f_bavail * stats.f_frsize def update_graph(self): val = self._get_values() self.push(val) class HDDBusyGraph(_Graph): """Display HDD busy time graph Parses /sys/block//stat file and extracts overall device IO usage, based on ``io_ticks``'s value. See https://www.kernel.org/doc/Documentation/block/stat.txt """ orientations = base.ORIENTATION_HORIZONTAL defaults = [("device", "sda", "Block device to display info for")] def __init__(self, **config): _Graph.__init__(self, **config) self.add_defaults(HDDBusyGraph.defaults) self.path = f"/sys/block/{self.device}/stat" self._prev = 0 def _get_values(self): try: # io_ticks is field number 9 with open(self.path) as f: io_ticks = int(f.read().split()[9]) except OSError: return 0 activity = io_ticks - self._prev self._prev = io_ticks return activity def update_graph(self): val = self._get_values() self.push(val) qtile-0.31.0/libqtile/widget/sensors.py0000664000175000017500000001061714762660347020001 0ustar epsilonepsilon# Copyright (c) 2012 TiN # Copyright (c) 2012, 2014 Tycho Andersen # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014-2015 Sean Vig # Copyright (c) 2014 Adi Sieker # Copyright (c) 2014 Foster McLane # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import psutil from libqtile.widget import base class ThermalSensor(base.ThreadPoolText): """Widget to display temperature sensor information For using the thermal sensor widget you need to have lm-sensors installed. You can get a list of the tag_sensors executing "sensors" in your terminal. Then you can choose which you want, otherwise it will display the first available. Widget requirements: psutil_. .. _psutil: https://pypi.org/project/psutil/ """ defaults = [ ( "format", "{temp:.1f}{unit}", "Display string format. Three options available: " "``{temp}`` - temperature, " "``{tag}`` - tag of the temperature sensor, and " "``{unit}`` - °C or °F", ), ("metric", True, "True to use metric/C, False to use imperial/F"), ("update_interval", 2, "Update interval in seconds"), ("tag_sensor", None, 'Tag of the temperature sensor. For example: "temp1" or "Core 0"'), ( "threshold", 70, "If the current temperature value is above, " "then change to foreground_alert colour", ), ("foreground_alert", "ff0000", "Foreground colour alert"), ] def __init__(self, **config): base.ThreadPoolText.__init__(self, **config) self.add_defaults(ThermalSensor.defaults) temp_values = self.get_temp_sensors() if temp_values is None: self.data = "sensors command not found" elif len(temp_values) == 0: self.data = "Temperature sensors not found" elif self.tag_sensor is None: for k in temp_values: self.tag_sensor = k break def _configure(self, qtile, bar): self.unit = "°C" if self.metric else "°F" base.ThreadPoolText._configure(self, qtile, bar) self.foreground_normal = self.foreground def get_temp_sensors(self): """ Reads temperatures from sys-fs via psutil. Output will be read Fahrenheit if user has specified it to be. """ temperature_list = {} temps = psutil.sensors_temperatures(fahrenheit=not self.metric) empty_index = 0 for kernel_module in temps: for sensor in temps[kernel_module]: label = sensor.label if not label: label = "{}-{}".format( kernel_module if kernel_module else "UNKNOWN", str(empty_index) ) empty_index += 1 temperature_list[label] = sensor.current return temperature_list def poll(self): temp_values = self.get_temp_sensors() # Temperature not available if (temp_values is None) or (self.tag_sensor not in temp_values): return "N/A" temp_value = temp_values.get(self.tag_sensor) if temp_value > self.threshold: self.layout.colour = self.foreground_alert else: self.layout.colour = self.foreground_normal val = dict(temp=temp_value, tag=self.tag_sensor, unit=self.unit) return self.format.format(**val) qtile-0.31.0/libqtile/widget/wallpaper.py0000664000175000017500000000771714762660347020303 0ustar epsilonepsilon# Copyright (c) 2015 Muhammed Abuali # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # # To use this widget, you will need to install feh wallpaper changer import os import random import subprocess from libqtile.log_utils import logger from libqtile.widget import base class Wallpaper(base._TextBox): defaults = [ ("directory", "~/Pictures/wallpapers/", "Wallpaper Directory"), ("wallpaper", None, "Wallpaper"), ( "wallpaper_command", ["feh", "--bg-fill"], "Wallpaper command. If None, the" "wallpaper will be painted without the use of a helper.", ), ( "random_selection", False, "If set, use random initial wallpaper and " "randomly cycle through the wallpapers.", ), ("label", None, "Use a fixed label instead of image name."), ( "option", "fill", "How to fit the wallpaper when wallpaper_command is" "None. None, 'fill' or 'stretch'.", ), ] def __init__(self, **config): base._TextBox.__init__(self, "empty", **config) self.add_defaults(Wallpaper.defaults) self.index = 0 self.images = [] self.get_wallpapers() if self.random_selection: # Random selection after reading all files self.index = random.randint(0, len(self.images) - 1) self.add_callbacks({"Button1": self.set_wallpaper}) def _configure(self, qtile, bar): base._TextBox._configure(self, qtile, bar) if not self.bar.screen.wallpaper: self.set_wallpaper() def get_path(self, file): return os.path.join(os.path.expanduser(self.directory), file) def get_wallpapers(self): try: # get path of all files in the directory self.images = list( filter( os.path.isfile, map(self.get_path, os.listdir(os.path.expanduser(self.directory))), ) ) except OSError as e: logger.exception("I/O error(%s): %s", e.errno, e.strerror) def set_wallpaper(self): if len(self.images) == 0: if self.wallpaper is None: self.text = "empty" return else: self.images.append(self.wallpaper) if self.random_selection: self.index = random.randint(0, len(self.images) - 1) else: self.index += 1 self.index %= len(self.images) cur_image = self.images[self.index] if self.label is None: self.text = os.path.basename(cur_image) else: self.text = self.label if self.wallpaper_command: self.wallpaper_command.append(cur_image) subprocess.call(self.wallpaper_command) self.wallpaper_command.pop() else: self.qtile.paint_screen(self.bar.screen, cur_image, self.option) self.draw() qtile-0.31.0/libqtile/widget/config_error.py0000664000175000017500000000273114762660347020761 0ustar epsilonepsilon# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from libqtile.widget.base import _TextBox class ConfigErrorWidget(_TextBox): def __init__(self, **config): _TextBox.__init__(self, **config) self.class_name = self.widget.__class__.__name__ self.text = f"Widget crashed: {self.class_name} (click to hide)" self.add_callbacks({"Button1": self._hide}) def _hide(self): self.text = "" self.bar.draw() qtile-0.31.0/libqtile/widget/moc.py0000664000175000017500000000725714762660347017071 0ustar epsilonepsilon# Copyright (C) 2015, zordsdavini # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os import subprocess from functools import partial from libqtile.widget import base class Moc(base.ThreadPoolText): """A simple MOC widget. Show the artist and album of now listening song and allow basic mouse control from the bar: - toggle pause (or play if stopped) on left click; - skip forward in playlist on scroll up; - skip backward in playlist on scroll down. MOC (http://moc.daper.net) should be installed. """ defaults = [ ("play_color", "00ff00", "Text colour when playing."), ("noplay_color", "cecece", "Text colour when not playing."), ("update_interval", 0.5, "Update Time in seconds."), ] def __init__(self, **config): base.ThreadPoolText.__init__(self, "", **config) self.add_defaults(Moc.defaults) self.status = "" self.local = None self.add_callbacks( { "Button1": self.play, "Button4": partial(subprocess.Popen, ["mocp", "-f"]), "Button5": partial(subprocess.Popen, ["mocp", "-r"]), } ) def get_info(self): """Return a dictionary with info about the current MOC status.""" try: output = self.call_process(["mocp", "-i"]) except subprocess.CalledProcessError as err: output = err.output if output.startswith("State"): output = output.splitlines() info = {"State": "", "File": "", "SongTitle": "", "Artist": "", "Album": ""} for line in output: for data in info: if data in line: info[data] = line[len(data) + 2 :].strip() break return info def now_playing(self): """Return a string with the now playing info (Artist - Song Title).""" info = self.get_info() now_playing = "" if info: status = info["State"] if self.status != status: self.status = status if self.status == "PLAY": self.layout.colour = self.play_color else: self.layout.colour = self.noplay_color title = info["SongTitle"] artist = info["Artist"] if title and artist: now_playing = f"♫ {artist} - {title}" elif title: now_playing = f"♫ {title}" else: basename = os.path.basename(info["File"]) filename = os.path.splitext(basename)[0] now_playing = f"♫ {filename}" if self.status == "STOP": now_playing = "♫" return now_playing def play(self): """Play music if stopped, else toggle pause.""" if self.status in ("PLAY", "PAUSE"): subprocess.Popen(["mocp", "-G"]) elif self.status == "STOP": subprocess.Popen(["mocp", "-p"]) def poll(self): """Poll content for the text box.""" return self.now_playing() qtile-0.31.0/libqtile/layout/0000775000175000017500000000000014762660347015760 5ustar epsilonepsilonqtile-0.31.0/libqtile/layout/zoomy.py0000664000175000017500000001130014762660347017502 0ustar epsilonepsilon# Copyright (c) 2011 Mounier Florian # Copyright (c) 2011 Paul Colomiets # Copyright (c) 2012 Craig Barnes # Copyright (c) 2012, 2014 Tycho Andersen # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014 ramnes # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 dmpayton # Copyright (c) 2014 dequis # Copyright (c) 2017 Dirk Hartmann # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations from typing import TYPE_CHECKING import libqtile from libqtile.command.base import expose_command from libqtile.layout.base import _SimpleLayoutBase if TYPE_CHECKING: from libqtile.backend.base import Window from libqtile.config import ScreenRect class Zoomy(_SimpleLayoutBase): """A layout with single active windows, and few other previews at the right""" defaults = [ ("columnwidth", 150, "Width of the right column"), ("property_name", "ZOOM", "Property to set on zoomed window (X11 only)"), ("property_small", "0.1", "Property value to set on zoomed window (X11 only)"), ("property_big", "1.0", "Property value to set on normal window (X11 only)"), ("margin", 0, "Margin of the layout (int or list of ints [N E S W])"), ] def __init__(self, **config): _SimpleLayoutBase.__init__(self, **config) self.add_defaults(Zoomy.defaults) def add_client(self, client: Window) -> None: # type: ignore[override] self.clients.append_head(client) def configure(self, client: Window, screen_rect: ScreenRect) -> None: left, right = screen_rect.hsplit(screen_rect.width - self.columnwidth) if client is self.clients.current_client: client.place( left.x, left.y, left.width, left.height, 0, None, margin=self.margin, ) else: h = right.width * left.height // left.width client_index = self.clients.index(client) focused_index = self.clients.current_index offset = client_index - focused_index - 1 if offset < 0: offset += len(self.clients) if h * (len(self.clients) - 1) < right.height: client.place( right.x, right.y + h * offset, right.width, h, 0, None, margin=self.margin, ) else: hh = (right.height - h) // (len(self.clients) - 1) client.place( right.x, right.y + hh * offset, right.width, h, 0, None, margin=self.margin, ) client.unhide() def focus(self, win): if self.property_name and libqtile.qtile.core.name != "x11": self.property_name = "" if ( self.clients.current_client and self.property_name and self.clients.current_client.window.get_property(self.property_name, "UTF8_STRING") is not None ): self.clients.current_client.window.set_property( self.property_name, self.property_small, "UTF8_STRING", format=8 ) _SimpleLayoutBase.focus(self, win) if self.property_name: win.window.set_property( self.property_name, self.property_big, "UTF8_STRING", format=8 ) @expose_command("down") def next(self) -> None: _SimpleLayoutBase.next(self) @expose_command("up") def previous(self) -> None: _SimpleLayoutBase.previous(self) qtile-0.31.0/libqtile/layout/max.py0000664000175000017500000000656414762660347017132 0ustar epsilonepsilon# Copyright (c) 2008, Aldo Cortesi. All rights reserved. # Copyright (c) 2017, Dirk Hartmann. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations from typing import TYPE_CHECKING from libqtile.command.base import expose_command from libqtile.layout.base import _SimpleLayoutBase if TYPE_CHECKING: from libqtile.backend.base import Window from libqtile.config import ScreenRect class Max(_SimpleLayoutBase): """Maximized layout A simple layout that only displays one window at a time, filling the screen_rect. This is suitable for use on laptops and other devices with small screens. Conceptually, the windows are managed as a stack, with commands to switch to next and previous windows in the stack. """ defaults = [ ("margin", 0, "Margin of the layout (int or list of ints [N E S W])"), ("border_focus", "#0000ff", "Border colour(s) for the window when focused"), ("border_normal", "#000000", "Border colour(s) for the window when not focused"), ("border_width", 0, "Border width."), ("only_focused", True, "Only draw the focused window"), ] def __init__(self, **config): _SimpleLayoutBase.__init__(self, **config) self.add_defaults(Max.defaults) def add_client(self, client: Window) -> None: # type: ignore[override] return super().add_client(client, 1) def configure(self, client: Window, screen_rect: ScreenRect) -> None: if not self.only_focused or (self.clients and client is self.clients.current_client): client.place( screen_rect.x, screen_rect.y, screen_rect.width - self.border_width * 2, screen_rect.height - self.border_width * 2, self.border_width, self.border_focus if client.has_focus else self.border_normal, margin=self.margin, ) client.unhide() if ( not self.only_focused and self.clients and client is self.clients.current_client and len(self.clients) > 1 ): client.move_to_top() else: client.hide() @expose_command("previous") def up(self): _SimpleLayoutBase.previous(self) @expose_command("next") def down(self): _SimpleLayoutBase.next(self) qtile-0.31.0/libqtile/layout/stack.py0000664000175000017500000003003214762660347017435 0ustar epsilonepsilon# Copyright (c) 2008, Aldo Cortesi. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations from typing import TYPE_CHECKING from libqtile.command.base import expose_command from libqtile.layout.base import Layout, _ClientList if TYPE_CHECKING: from typing import Any, Self from libqtile.backend.base import Window from libqtile.config import ScreenRect from libqtile.group import _Group class _WinStack(_ClientList): # shortcuts for current client and index used in Columns layout cw = _ClientList.current_client def __init__(self, autosplit=False): _ClientList.__init__(self) self.split = autosplit def toggle_split(self): self.split = False if self.split else True def __str__(self): return f"_WinStack: {self.cw}, {str([client.name for client in self.clients])}" @expose_command() def info(self) -> dict[str, Any]: info = _ClientList.info(self) info["split"] = self.split return info class Stack(Layout): """A layout composed of stacks of windows The stack layout divides the screen_rect horizontally into a set of stacks. Commands allow you to switch between stacks, to next and previous windows within a stack, and to split a stack to show all windows in the stack, or unsplit it to show only the current window. Unlike the columns layout the number of stacks is fixed. """ defaults = [ ("border_focus", "#0000ff", "Border colour(s) for the focused window."), ("border_normal", "#000000", "Border colour(s) for un-focused windows."), ( "border_focus_stack", None, "Border colour(s) for the focused stacked window. If 'None' will \ default to border_focus.", ), ( "border_normal_stack", None, "Border colour(s) for un-focused stacked windows. If 'None' will \ default to border_normal.", ), ("border_width", 1, "Border width."), ("autosplit", False, "Auto split all new stacks."), ("num_stacks", 2, "Number of stacks."), ("fair", False, "Add new windows to the stacks in a round robin way."), ("margin", 0, "Margin of the layout (int or list of ints [N E S W])"), ] def __init__(self, **config): Layout.__init__(self, **config) self.add_defaults(Stack.defaults) if self.num_stacks <= 0: # Catch stupid mistakes early and generate a useful message raise ValueError("num_stacks must be at least 1") self.stacks = [_WinStack(autosplit=self.autosplit) for i in range(self.num_stacks)] @property def current_stack(self): return self.stacks[self.current_stack_offset] @property def current_stack_offset(self): for i, s in enumerate(self.stacks): if self.group.current_window in s: return i return 0 @property def clients(self): client_list = [] for stack in self.stacks: client_list.extend(stack.clients) return client_list def clone(self, group: _Group) -> Self: c = Layout.clone(self, group) # These are mutable c.stacks = [_WinStack(autosplit=self.autosplit) for i in self.stacks] return c def _find_next(self, lst, offset): for i in lst[offset + 1 :]: if i: return i for i in lst[:offset]: if i: return i def delete_current_stack(self): if len(self.stacks) > 1: off = self.current_stack_offset or 0 s = self.stacks[off] self.stacks.remove(s) off = min(off, len(self.stacks) - 1) self.stacks[off].join(s, 1) if self.stacks[off]: self.group.focus(self.stacks[off].cw, False) def next_stack(self): n = self._find_next(self.stacks, self.current_stack_offset) if n: self.group.focus(n.cw, True) def previous_stack(self): n = self._find_next( list(reversed(self.stacks)), len(self.stacks) - self.current_stack_offset - 1 ) if n: self.group.focus(n.cw, True) def focus(self, client: Window) -> None: for i in self.stacks: if client in i: i.focus(client) def focus_first(self) -> Window | None: if self.stacks: return self.stacks[0].focus_first() return None def focus_last(self) -> Window | None: if self.stacks: return self.stacks[-1].focus_last() return None def focus_next(self, client: Window) -> Window | None: iterator = iter(self.stacks) for i in iterator: if client in i: if next_ := i.focus_next(client): return next_ break else: return None if i := next(iterator, None): return i.focus_first() return None def focus_previous(self, client: Window) -> Window | None: iterator = reversed(self.stacks) for i in iterator: if client in i: if nxt := i.focus_previous(client): return nxt break else: return None if i := next(iterator, None): return i.focus_last() return None def add_client(self, client: Window) -> None: for i in self.stacks: if not i: i.add_client(client) return if self.fair: target = min(self.stacks, key=len) target.add_client(client) else: self.current_stack.add_client(client) def remove(self, client: Window) -> Window | None: current_offset = self.current_stack_offset for i in self.stacks: if client in i: i.remove(client) break if self.stacks[current_offset].cw: return self.stacks[current_offset].cw else: n = self._find_next( list(reversed(self.stacks)), len(self.stacks) - current_offset - 1 ) if n: return n.cw return None def configure(self, client: Window, screen_rect: ScreenRect) -> None: # pylint: disable=undefined-loop-variable # We made sure that self.stacks is not empty, so s is defined. for i, s in enumerate(self.stacks): if client in s: break else: client.hide() return if client.has_focus: if self.border_focus_stack: if s.split: px = self.border_focus else: px = self.border_focus_stack else: px = self.border_focus else: if self.border_normal_stack: if s.split: px = self.border_normal else: px = self.border_normal_stack else: px = self.border_normal column_width = int(screen_rect.width / len(self.stacks)) xoffset = screen_rect.x + i * column_width window_width = column_width - 2 * self.border_width if s.split: column_height = int(screen_rect.height / len(s)) window_height = column_height - 2 * self.border_width yoffset = screen_rect.y + s.index(client) * column_height client.place( xoffset, yoffset, window_width, window_height, self.border_width, px, margin=self.margin, ) client.unhide() else: if client == s.cw: client.place( xoffset, screen_rect.y, window_width, screen_rect.height - 2 * self.border_width, self.border_width, px, margin=self.margin, ) client.unhide() else: client.hide() def get_windows(self): return self.clients @expose_command() def info(self) -> dict[str, Any]: d = Layout.info(self) d["stacks"] = [i.info() for i in self.stacks] d["current_stack"] = self.current_stack_offset d["clients"] = [c.name for c in self.clients] return d @expose_command() def toggle_split(self): """Toggle vertical split on the current stack""" self.current_stack.toggle_split() self.group.layout_all() @expose_command() def down(self): """Switch to the next window in this stack""" self.current_stack.current_index += 1 self.group.focus(self.current_stack.cw, False) @expose_command() def up(self): """Switch to the previous window in this stack""" self.current_stack.current_index -= 1 self.group.focus(self.current_stack.cw, False) @expose_command() def shuffle_up(self): """Shuffle the order of this stack up""" self.current_stack.shuffle_up() self.group.layout_all() @expose_command() def shuffle_down(self): """Shuffle the order of this stack down""" self.current_stack.shuffle_down() self.group.layout_all() @expose_command() def delete(self): """Delete the current stack from the layout""" self.delete_current_stack() @expose_command() def add(self): """Add another stack to the layout""" newstack = _WinStack(autosplit=self.autosplit) if self.autosplit: newstack.split = True self.stacks.append(newstack) self.group.layout_all() @expose_command() def rotate(self): """Rotate order of the stacks""" if self.stacks: self.stacks.insert(0, self.stacks.pop()) self.group.layout_all() @expose_command() def next(self) -> None: """Focus next stack""" return self.next_stack() @expose_command() def previous(self) -> None: """Focus previous stack""" self.previous_stack() @expose_command() def client_to_next(self): """Send the current client to the next stack""" return self.client_to_stack(self.current_stack_offset + 1) @expose_command() def client_to_previous(self): """Send the current client to the previous stack""" return self.client_to_stack(self.current_stack_offset - 1) @expose_command() def client_to_stack(self, n): """ Send the current client to stack n, where n is an integer offset. If is too large or less than 0, it is wrapped modulo the number of stacks. """ if not self.current_stack: return next = n % len(self.stacks) win = self.current_stack.cw self.current_stack.remove(win) self.stacks[next].add_client(win) self.stacks[next].focus(win) self.group.layout_all() qtile-0.31.0/libqtile/layout/matrix.py0000664000175000017500000001524514762660347017645 0ustar epsilonepsilon# Copyright (c) 2013 Mattias Svala # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014 ramnes # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 dmpayton # Copyright (c) 2014 dequis # Copyright (c) 2014 Tycho Andersen # Copyright (c) 2017 Dirk Hartmann # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations import math from typing import TYPE_CHECKING from libqtile.command.base import expose_command from libqtile.layout.base import _SimpleLayoutBase if TYPE_CHECKING: from typing import Any, Self from libqtile.backend.base import Window from libqtile.config import ScreenRect from libqtile.group import _Group class Matrix(_SimpleLayoutBase): """ This layout divides the screen into a matrix of equally sized cells and places one window in each cell. The number of columns is configurable and can also be changed interactively. """ columns: int defaults = [ ("border_focus", "#0000ff", "Border colour(s) for the focused window."), ("border_normal", "#000000", "Border colour(s) for un-focused windows."), ("border_width", 1, "Border width."), ("columns", 2, "Number of columns"), ("margin", 0, "Margin of the layout (int or list of ints [N E S W])"), ] def __init__(self, **config): _SimpleLayoutBase.__init__(self, **config) self.add_defaults(Matrix.defaults) @property def rows(self): """Calc current number of rows, basd on number of clients and columns""" return int(math.ceil(len(self.clients) / self.columns)) @property def row(self): """Calc row index of current client""" return self.clients.current_index // self.columns @property def column(self): """Calc column index of current client""" return self.clients.current_index % self.columns @expose_command() def info(self) -> dict[str, Any]: d = _SimpleLayoutBase.info(self) d["rows"] = [[win.name for win in self.get_row(i)] for i in range(self.rows)] d["current_window"] = self.column, self.row return d def clone(self, group: _Group) -> Self: c = _SimpleLayoutBase.clone(self, group) c.columns = self.columns return c def get_row(self, row): """Get all clients in given row""" assert row < self.rows return self.clients[row * self.columns : row * self.columns + self.columns] def get_column(self, column): """Get all clients in given column""" assert column < self.columns return [self.clients[i] for i in range(column, len(self.clients), self.columns)] def add_client(self, client: Window) -> None: # type: ignore[override] """Add client to Layout. Note that for Matrix the clients are appended at end of list. If needed a new row in matrix is created""" return self.clients.append(client) def configure(self, client: Window, screen_rect: ScreenRect) -> None: if client not in self.clients: return idx = self.clients.index(client) row = idx // self.columns col = idx % self.columns column_size = int(math.ceil(len(self.clients) / self.columns)) if client.has_focus: px = self.border_focus else: px = self.border_normal # calculate position and size column_width = int(screen_rect.width / float(self.columns)) row_height = int(screen_rect.height / float(column_size)) xoffset = screen_rect.x + col * column_width yoffset = screen_rect.y + row * row_height win_width = column_width - 2 * self.border_width win_height = row_height - 2 * self.border_width # place client.place( xoffset, yoffset, win_width, win_height, self.border_width, px, margin=self.margin, ) client.unhide() @expose_command() def previous(self) -> None: _SimpleLayoutBase.previous(self) @expose_command() def next(self) -> None: _SimpleLayoutBase.next(self) def horizontal_traversal(self, direction): """ Internal method for determining left or right client. Negative direction is to left """ column, row = self.column, self.row column = (column + direction) % len(self.get_row(row)) self.clients.current_index = row * self.columns + column self.group.focus(self.clients.current_client) def vertical_traversal(self, direction): """ internal method for determining above or below client. Negative direction is to top """ column, row = self.column, self.row row = (row + direction) % len(self.get_column(column)) self.clients.current_index = row * self.columns + column self.group.focus(self.clients.current_client) @expose_command() def left(self): """Switch to the next window on current row""" self.horizontal_traversal(-1) @expose_command() def right(self): """Switch to the next window on current row""" self.horizontal_traversal(+1) @expose_command() def up(self): """Switch to the previous window in current column""" self.vertical_traversal(-1) @expose_command() def down(self): """Switch to the next window in current column""" self.vertical_traversal(+1) @expose_command() def delete(self): """Decrease number of columns""" self.columns -= 1 self.group.layout_all() @expose_command() def add(self): """Increase number of columns""" self.columns += 1 self.group.layout_all() qtile-0.31.0/libqtile/layout/columns.py0000664000175000017500000005220014762660347020011 0ustar epsilonepsilon# Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations from typing import TYPE_CHECKING from libqtile.command.base import expose_command from libqtile.layout.base import Layout, _ClientList from libqtile.log_utils import logger if TYPE_CHECKING: from typing import Any, Self from libqtile.backend.base import Window from libqtile.config import ScreenRect from libqtile.group import _Group class _Column(_ClientList): # shortcuts for current client and index used in Columns layout cw = _ClientList.current_client current = _ClientList.current_index def __init__(self, split, insert_position, width=100): _ClientList.__init__(self) self.width = width self.split = split self.insert_position = insert_position self.heights = {} @expose_command() def info(self) -> dict[str, Any]: info = _ClientList.info(self) info.update( dict( heights=[self.heights[c] for c in self.clients], split=self.split, ) ) return info def toggle_split(self): self.split = not self.split def add_client(self, client, height=100): _ClientList.add_client(self, client, self.insert_position) self.heights[client] = height delta = 100 - height if delta != 0: n = len(self) growth = [int(delta / n)] * n growth[0] += delta - sum(growth) for c, g in zip(self, growth): self.heights[c] += g def remove(self, client: Window) -> None: _ClientList.remove(self, client) delta = self.heights[client] - 100 del self.heights[client] if delta != 0: n = len(self) growth = [int(delta / n)] * n growth[0] += delta - sum(growth) for c, g in zip(self, growth): self.heights[c] += g def __str__(self): cur = self.current return "_Column: " + ", ".join( [ "[%s: %d]" % (c.name, self.heights[c]) if c == cur else "%s: %d" % (c.name, self.heights[c]) for c in self.clients ] ) class Columns(Layout): """Extension of the Stack layout. The screen is split into columns, which can be dynamically added or removed. Each column can present its windows in 2 modes: split or stacked. In split mode, all windows are presented simultaneously, spliting the column space. In stacked mode, only a single window is presented from the stack of windows. Columns and windows can be resized and windows can be shuffled around. This layout can also emulate wmii's default layout via: layout.Columns(num_columns=1, insert_position=1) Or the "Vertical", and "Max", depending on the default parameters. An example key configuration is:: Key([mod], "j", lazy.layout.down()), Key([mod], "k", lazy.layout.up()), Key([mod], "h", lazy.layout.left()), Key([mod], "l", lazy.layout.right()), Key([mod, "shift"], "j", lazy.layout.shuffle_down()), Key([mod, "shift"], "k", lazy.layout.shuffle_up()), Key([mod, "shift"], "h", lazy.layout.shuffle_left()), Key([mod, "shift"], "l", lazy.layout.shuffle_right()), Key([mod, "control"], "j", lazy.layout.grow_down()), Key([mod, "control"], "k", lazy.layout.grow_up()), Key([mod, "control"], "h", lazy.layout.grow_left()), Key([mod, "control"], "l", lazy.layout.grow_right()), Key([mod, "shift", "control"], "h", lazy.layout.swap_column_left()), Key([mod, "shift", "control"], "l", lazy.layout.swap_column_right()), Key([mod], "Return", lazy.layout.toggle_split()), Key([mod], "n", lazy.layout.normalize()), """ _left = 0 _right = 1 defaults = [ ("border_focus", "#881111", "Border colour(s) for the focused window."), ("border_normal", "#220000", "Border colour(s) for un-focused windows."), ( "border_focus_stack", "#881111", "Border colour(s) for the focused window in stacked columns.", ), ( "border_normal_stack", "#220000", "Border colour(s) for un-focused windows in stacked columns.", ), ("border_width", 2, "Border width."), ("single_border_width", None, "Border width for single window."), ("border_on_single", False, "Draw a border when there is one only window."), ("margin", 0, "Margin of the layout (int or list of ints [N E S W])."), ( "margin_on_single", None, "Margin when only one window. (int or list of ints [N E S W])", ), ("split", True, "New columns presentation mode."), ("num_columns", 2, "Preferred number of columns."), ("grow_amount", 10, "Amount by which to grow a window/column."), ("fair", False, "Add new windows to the column with least windows."), ( "insert_position", 0, "Position relative to the current window where new ones are inserted " "(0 means right above the current window, 1 means right after).", ), ("wrap_focus_columns", True, "Wrap the screen when moving focus across columns."), ("wrap_focus_rows", True, "Wrap the screen when moving focus across rows."), ("wrap_focus_stacks", True, "Wrap the screen when moving focus across stacked."), ( "align", _right, "Which side of screen new windows will be added to " "(one of ``Columns._left`` or ``Columns._right``). " "Ignored if 'fair=True'.", ), ("initial_ratio", 1, "Ratio of first column to second column."), ] def __init__(self, **config): Layout.__init__(self, **config) self.add_defaults(Columns.defaults) if not self.border_on_single: self.single_border_width = 0 elif self.single_border_width is None: self.single_border_width = self.border_width if self.margin_on_single is None: self.margin_on_single = self.margin self.columns = [_Column(self.split, self.insert_position)] self.current = 0 if self.align not in (Columns._left, Columns._right): logger.warning( "Unexpected value for `align`. Must be Columns._left or Columns._right." ) self.align = Columns._right def clone(self, group: _Group) -> Self: c = Layout.clone(self, group) c.columns = [_Column(self.split, self.insert_position)] return c def get_windows(self): clients = [] for c in self.columns: clients.extend(c.clients) return clients @expose_command() def info(self) -> dict[str, Any]: d = Layout.info(self) d["clients"] = [] d["columns"] = [] for c in self.columns: cinfo = c.info() d["clients"].extend(cinfo["clients"]) d["columns"].append(cinfo) d["current"] = self.current return d def focus(self, client: Window) -> None: for i, c in enumerate(self.columns): if client in c: c.focus(client) self.current = i break @property def cc(self): return self.columns[self.current] def get_ratio_widths(self): # Total width is 200 # main + secondary = 200 # main = secondary * ratio # secondary column is therefore 200 / (1 + ratio) # main column is 200 - secondary column secondary = 200 // (1 + self.initial_ratio) main = 200 - secondary return main, secondary def add_column(self, prepend=False): c = _Column(self.split, self.insert_position) if prepend: self.columns.insert(0, c) self.current += 1 else: self.columns.append(c) if len(self.columns) == 2 and not self.fair: main, secondary = self.get_ratio_widths() self.cc.width = main c.width = secondary return c def remove_column(self, col): if len(self.columns) == 1: logger.warning("Trying to remove all columns.") return idx = self.columns.index(col) del self.columns[idx] if idx <= self.current: self.current = max(0, self.current - 1) delta = col.width - 100 if delta != 0: n = len(self.columns) growth = [int(delta / n)] * n growth[0] += delta - sum(growth) for c, g in zip(self.columns, growth): c.width += g def add_client(self, client: Window) -> None: c = self.cc if len(c) > 0 and len(self.columns) < self.num_columns: prepend = self.align is Columns._left c = self.add_column(prepend=prepend) if self.fair: least = min(self.columns, key=len) if len(least) < len(c): c = least self.current = self.columns.index(c) c.add_client(client) def remove(self, client): remove = None for c in self.columns: if client in c: c.remove(client) if len(c) == 0 and len(self.columns) > 1: remove = c break if remove is not None: self.remove_column(c) return self.columns[self.current].cw def configure(self, client: Window, screen_rect: ScreenRect) -> None: pos = 0 for col in self.columns: if client in col: break pos += col.width else: client.hide() return if client.has_focus: color = self.border_focus if col.split else self.border_focus_stack else: color = self.border_normal if col.split else self.border_normal_stack is_single = len(self.columns) == 1 and (len(col) == 1 or not col.split) border = self.single_border_width if is_single else self.border_width margin_size = self.margin_on_single if is_single else self.margin width = int(0.5 + col.width * screen_rect.width * 0.01 / len(self.columns)) x = screen_rect.x + int(0.5 + pos * screen_rect.width * 0.01 / len(self.columns)) if col.split: pos = 0 for c in col: if client == c: break pos += col.heights[c] height = int(0.5 + col.heights[client] * screen_rect.height * 0.01 / len(col)) y = screen_rect.y + int(0.5 + pos * screen_rect.height * 0.01 / len(col)) client.place( x, y, width - 2 * border, height - 2 * border, border, color, margin=margin_size ) client.unhide() elif client == col.cw: client.place( x, screen_rect.y, width - 2 * border, screen_rect.height - 2 * border, border, color, margin=margin_size, ) client.unhide() else: client.hide() def focus_first(self) -> Window | None: """Returns first client in first column of layout""" if self.columns: return self.columns[0].focus_first() return None def focus_last(self) -> Window | None: """Returns last client in last column of layout""" if self.columns: return self.columns[-1].focus_last() return None def focus_next(self, win: Window) -> None: """Returns the next client after 'win' in layout, or None if there is no such client""" # First: try to get next window in column of win (self.columns is non-empty) # pylint: disable=undefined-loop-variable for idx, col in enumerate(self.columns): if win in col: if nxt := col.focus_next(win): return nxt break # if there was no next, get first client from next column if idx + 1 < len(self.columns): return self.columns[idx + 1].focus_first() return None def focus_previous(self, win: Window) -> Window | None: """Returns the client previous to 'win' in layout. or None if there is no such client""" # First: try to focus previous client in column (self.columns is non-empty) # pylint: disable=undefined-loop-variable for idx, col in enumerate(self.columns): if win in col: if prev := col.focus_previous(win): return prev break # If there was no previous, get last from previous column if idx > 0: return self.columns[idx - 1].focus_last() return None @expose_command() def toggle_split(self): self.cc.toggle_split() self.group.layout_all() @expose_command() def left(self): if self.wrap_focus_columns: if len(self.columns) > 1: self.current = (self.current - 1) % len(self.columns) else: if self.current > 0: self.current = self.current - 1 self.group.focus(self.cc.cw, True) @expose_command() def right(self): if self.wrap_focus_columns: if len(self.columns) > 1: self.current = (self.current + 1) % len(self.columns) else: if len(self.columns) - 1 > self.current: self.current = self.current + 1 self.group.focus(self.cc.cw, True) def want_wrap(self, col): if col.split: return self.wrap_focus_rows return self.wrap_focus_stacks @expose_command() def up(self): col = self.cc if self.want_wrap(col): if len(col) > 1: col.current_index -= 1 else: if col.current_index > 0: col.current_index -= 1 self.group.focus(col.cw, True) @expose_command() def down(self): col = self.cc if self.want_wrap(col): if len(col) > 1: col.current_index += 1 else: if col.current_index < len(col) - 1: col.current_index += 1 self.group.focus(col.cw, True) @expose_command() def next(self) -> None: if self.cc.split and self.cc.current < len(self.cc) - 1: self.cc.current += 1 elif self.columns: self.current = (self.current + 1) % len(self.columns) if self.cc.split: self.cc.current = 0 self.group.focus(self.cc.cw, True) @expose_command() def previous(self) -> None: if self.cc.split and self.cc.current > 0: self.cc.current -= 1 elif self.columns: self.current = (self.current - 1) % len(self.columns) if self.cc.split: self.cc.current = len(self.cc) - 1 self.group.focus(self.cc.cw, True) @expose_command() def shuffle_left(self): cur = self.cc client = cur.cw if client is None: return if self.current > 0: self.current -= 1 new = self.cc new.add_client(client, cur.heights[client]) cur.remove(client) if len(cur) == 0: self.remove_column(cur) elif len(cur) > 1: new = self.add_column(True) new.add_client(client, cur.heights[client]) cur.remove(client) self.current = 0 else: return self.group.layout_all() @expose_command() def shuffle_right(self): cur = self.cc client = cur.cw if client is None: return if self.current + 1 < len(self.columns): self.current += 1 new = self.cc new.add_client(client, cur.heights[client]) cur.remove(client) if len(cur) == 0: self.remove_column(cur) elif len(cur) > 1: new = self.add_column() new.add_client(client, cur.heights[client]) cur.remove(client) self.current = len(self.columns) - 1 else: return self.group.layout_all() @expose_command() def shuffle_up(self): if self.cc.current_index > 0: self.cc.shuffle_up() self.group.layout_all() @expose_command() def shuffle_down(self): if self.cc.current_index + 1 < len(self.cc): self.cc.shuffle_down() self.group.layout_all() @expose_command() def grow_left(self): if self.current > 0: if self.columns[self.current - 1].width > self.grow_amount: self.columns[self.current - 1].width -= self.grow_amount self.cc.width += self.grow_amount self.group.layout_all() elif len(self.columns) > 1: if self.columns[0].width > self.grow_amount: self.columns[1].width += self.grow_amount self.cc.width -= self.grow_amount self.group.layout_all() @expose_command() def grow_right(self): if self.current + 1 < len(self.columns): if self.columns[self.current + 1].width > self.grow_amount: self.columns[self.current + 1].width -= self.grow_amount self.cc.width += self.grow_amount self.group.layout_all() elif len(self.columns) > 1: if self.cc.width > self.grow_amount: self.cc.width -= self.grow_amount self.columns[self.current - 1].width += self.grow_amount self.group.layout_all() @expose_command() def grow_up(self): col = self.cc if col.current > 0: if col.heights[col[col.current - 1]] > self.grow_amount: col.heights[col[col.current - 1]] -= self.grow_amount col.heights[col.cw] += self.grow_amount self.group.layout_all() elif len(col) > 1: if col.heights[col.cw] > self.grow_amount: col.heights[col[1]] += self.grow_amount col.heights[col.cw] -= self.grow_amount self.group.layout_all() @expose_command() def grow_down(self): col = self.cc if col.current + 1 < len(col): if col.heights[col[col.current + 1]] > self.grow_amount: col.heights[col[col.current + 1]] -= self.grow_amount col.heights[col.cw] += self.grow_amount self.group.layout_all() elif len(col) > 1: if col.heights[col.cw] > self.grow_amount: col.heights[col[col.current - 1]] += self.grow_amount col.heights[col.cw] -= self.grow_amount self.group.layout_all() @expose_command() def normalize(self): """Give columns equal widths.""" for col in self.columns: for client in col: col.heights[client] = 100 col.width = 100 self.group.layout_all() @expose_command() def reset(self): """Resets column widths, respecting 'initial_ratio' value.""" if self.initial_ratio == 1 or len(self.columns) == 1 or self.fair: self.normalize() return main, secondary = self.get_ratio_widths() if self.align == Columns._right: self.columns[0].width = main self.columns[1].width = secondary else: self.columns[-1].width = main self.columns[-2].width = secondary self.group.layout_all() def swap_column(self, src, dst): self.columns[src], self.columns[dst] = self.columns[dst], self.columns[src] self.current = dst self.group.layout_all() @expose_command() def swap_column_left(self): src = self.current dst = src - 1 if src > 0 else len(self.columns) - 1 self.swap_column(src, dst) @expose_command() def swap_column_right(self): src = self.current dst = src + 1 if src < len(self.columns) - 1 else 0 self.swap_column(src, dst) qtile-0.31.0/libqtile/layout/spiral.py0000664000175000017500000003444114762660347017632 0ustar epsilonepsilon# Copyright (c) 2022 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations from typing import TYPE_CHECKING from libqtile.command.base import expose_command from libqtile.layout.base import _SimpleLayoutBase from libqtile.log_utils import logger if TYPE_CHECKING: from typing import Any, Self from libqtile.backend.base import Window from libqtile.group import _Group Rect = tuple[int, int, int, int] GOLDEN_RATIO = 1.618 class Spiral(_SimpleLayoutBase): """ A mathematical layout. Renders windows in a spiral form by splitting the screen based on a selected ratio. The direction of the split is changed every time in a defined order resulting in a spiral formation. The main window can be sized with ``lazy.layout.grow_main()`` and ``lazy.layout.shrink_main()``. All other windows are sized by ``lazy.layout.increase_ratio()`` and ``lazy.layout.decrease_ratio()``. NB if ``main_pane_ratio`` is not set then it will also be adjusted according to ``ratio``. However, as soon ``shrink_main()`` or ``grow_main()`` have been called once then the master pane will only change size following further calls to those methods. Users are able to choose the location of the main (i.e. largest) pane and the direction of the rotation. Some examples: ``main_pane="left", clockwise=True`` :: ---------------------- |1 |2 | | | | | | | | |----------| | |5 |6 |3 | | |-----| | | |4 | | ---------------------- ``main_pane="top", clockwise=False`` :: ---------------------- |1 | | | | | |--------------------| |2 |5 |4 | | |----------| | |3 | ---------------------- """ split_ratio: float defaults = [ ("border_focus", "#0000ff", "Border colour(s) for the focused window."), ("border_normal", "#000000", "Border colour(s) for un-focused windows."), ("border_width", 1, "Border width."), ("border_on_single", True, "Draw border when there is only one window."), ("margin", 0, "Margin of the layout (int or list of ints [N E S W])"), ("ratio", 1 / GOLDEN_RATIO, "Ratio of the tiles"), ( "main_pane_ratio", None, "Ratio for biggest window or 'None' to use same ratio for all windows.", ), ("ratio_increment", 0.1, "Amount to increment per ratio increment"), ("main_pane", "left", "Location of biggest window 'top', 'bottom', 'left', 'right'"), ("clockwise", True, "Direction of spiral"), ( "new_client_position", "top", "Place new windows: " " 'after_current' - after the active window," " 'before_current' - before the active window," " 'top' - in the main pane," " 'bottom '- at the bottom of the stack. NB windows that are added too low in the stack" " may be hidden if there is no remaining space in the spiral.", ), ] def __init__(self, **config): _SimpleLayoutBase.__init__(self, **config) self.add_defaults(Spiral.defaults) self.dirty = True # need to recalculate self.layout_info = [] self.last_size = None self.last_screen = None self.initial_ratio = self.ratio self.initial_main_pane_ratio = self.main_pane_ratio self.main_pane = self.main_pane.lower() if self.main_pane not in ["top", "left", "bottom", "right"]: logger.warning( "Unknown main_pane location: %s. Defaulting to 'left'.", self.main_pane ) self.main_pane = "left" # Calculate the order of transformations required based on position of main pane # and rotation direction # Lists are longer so we can pick any side and have the next 4 transformations if self.clockwise: order = ["left", "top", "right", "bottom", "left", "top", "right"] else: order = ["left", "bottom", "right", "top", "left", "bottom", "right"] idx = order.index(self.main_pane) self.splits = order[idx : idx + 4] def clone(self, group: _Group) -> Self: return _SimpleLayoutBase.clone(self, group) def add_client(self, client: Window) -> None: # type: ignore[override] self.dirty = True self.clients.add_client(client, client_position=self.new_client_position) def remove(self, w: Window) -> Window | None: self.dirty = True return _SimpleLayoutBase.remove(self, w) def configure(self, win, screen): # force recalc if not self.last_screen or self.last_screen != screen: self.last_screen = screen self.dirty = True if self.last_size and not self.dirty: if screen.width != self.last_size[0] or screen.height != self.last_size[1]: self.dirty = True if self.dirty: self.layout_info = self.get_spiral(screen.x, screen.y, screen.width, screen.height) self.dirty = False try: idx = self.clients.index(win) except ValueError: win.hide() return try: x, y, w, h = self.layout_info[idx] # IndexError will arise if we're unable to create a window due to the dimensions # being too small. If that's the case, hide the window. except IndexError: win.hide() return if win.has_focus: bc = self.border_focus else: bc = self.border_normal (x, y, w, h), margins = self._fix_double_margins(x, y, w, h) if len(self.clients) == 1 and not self.border_on_single: border_width = 0 else: border_width = self.border_width win.place( x, y, w - border_width * 2, h - border_width * 2, border_width, bc, margin=margins, ) win.unhide() def split_left(self, rect: Rect) -> tuple[Rect, Rect]: rect_x, rect_y, rect_w, rect_h = rect win_w = int(rect_w * self.split_ratio) win_h = rect_h win_x = rect_x win_y = rect_y rect_x = win_x + win_w rect_y = win_y rect_w = rect_w - win_w return (win_x, win_y, win_w, win_h), (rect_x, rect_y, rect_w, rect_h) def split_right(self, rect: Rect) -> tuple[Rect, Rect]: rect_x, rect_y, rect_w, rect_h = rect win_w = int(rect_w * self.split_ratio) win_h = rect_h win_x = rect_x + (rect_w - win_w) win_y = rect_y rect_x = win_x - (rect_w - win_w) rect_y = win_y rect_w = rect_w - win_w return (win_x, win_y, win_w, win_h), (rect_x, rect_y, rect_w, rect_h) def split_top(self, rect: Rect) -> tuple[Rect, Rect]: rect_x, rect_y, rect_w, rect_h = rect win_w = rect_w win_h = int(rect_h * self.split_ratio) win_x = rect_x win_y = rect_y rect_x = win_x rect_y = win_y + win_h rect_h = rect_h - win_h return (win_x, win_y, win_w, win_h), (rect_x, rect_y, rect_w, rect_h) def split_bottom(self, rect: Rect) -> tuple[Rect, Rect]: rect_x, rect_y, rect_w, rect_h = rect win_w = rect_w win_h = int(rect_h * self.split_ratio) win_x = rect_x win_y = rect_y + (rect_h - win_h) rect_x = win_x rect_y = win_y - (rect_h - win_h) rect_h = rect_h - win_h return (win_x, win_y, win_w, win_h), (rect_x, rect_y, rect_w, rect_h) def _fix_double_margins( self, win_x: int, win_y: int, win_w: int, win_h: int ) -> tuple[Rect, list[int]]: """Prevent doubling up of margins by halving margins for internal margins.""" if isinstance(self.margin, int): margins = [self.margin] * 4 else: margins = self.margin # Top if win_y - margins[0] > self.last_screen.y: win_y -= margins[0] // 2 win_h += margins[0] // 2 # Right if win_x + win_w + margins[1] < (self.last_screen.x + self.last_screen.width): win_w += margins[1] // 2 # Bottom if win_y + win_h + margins[2] < (self.last_screen.y + self.last_screen.height): win_h += margins[2] // 2 # Left if win_x - margins[3] > self.last_screen.x: win_x -= margins[3] // 2 win_w += margins[3] // 2 return (win_x, win_y, win_w, win_h), margins def has_invalid_size(self, win: Rect) -> bool: """ Checks if window would have an invalid size. A window that would have negative height or width (after adjusting for margins and borders) will return True. """ if isinstance(self.margin, int): margin = [self.margin] * 4 else: margin = self.margin return any( [ win[2] <= margin[1] + margin[3] + 2 * self.border_width, win[3] <= margin[0] + margin[2] + 2 * self.border_width, ] ) def get_spiral(self, x, y, width, height) -> list[Rect]: """ Calculates positions of windows in the spiral. Returns a list of tuples (x, y, w, h) for positioning windows. """ num_windows = len(self.clients) direction = 0 spiral = [] rect = (x, y, width, height) for c in range(num_windows): if c == 0 and self.main_pane_ratio is not None: self.split_ratio = self.main_pane_ratio else: self.split_ratio = self.ratio # If there's another window to draw after this one then we need to # split the current rect, if not, the window can take the full rect split = c < (num_windows - 1) if not split: spiral.append(rect) continue # Get the dimensions of the window and remaining rect # Calls self.split_[direction name] win, new_rect = getattr(self, f"split_{self.splits[direction]}")(rect) # If the window would have negative/zero dimensions then it can't be displayed if self.has_invalid_size(win): # Use the available rect from before the split spiral.append(rect) break spiral.append(win) direction = (direction + 1) % 4 rect = new_rect return spiral def info(self) -> dict[str, Any]: d = _SimpleLayoutBase.info(self) focused = self.clients.current_client d["ratio"] = self.ratio d["focused"] = focused.name if focused else None d["layout_info"] = self.layout_info d["main_pane"] = self.main_pane d["clockwise"] = self.clockwise return d @expose_command("up") def previous(self) -> None: _SimpleLayoutBase.previous(self) @expose_command("down") def next(self) -> None: _SimpleLayoutBase.next(self) def _set_ratio(self, prop: str, value: float): if not (0 <= value <= 1): logger.warning( "Invalid value for %s: %s. Value must be between 0 and 1.", prop, value ) return setattr(self, prop, value) # Force layout to be recalculated self.dirty = True self.group.layout_all() @expose_command() def shuffle_down(self): if self.clients: self.clients.shuffle_down() self.group.layout_all() @expose_command() def shuffle_up(self): if self.clients: self.clients.shuffle_up() self.group.layout_all() @expose_command() def decrease_ratio(self): """Decrease spiral ratio.""" self._set_ratio("ratio", self.ratio - self.ratio_increment) @expose_command() def increase_ratio(self): """Increase spiral ratio.""" self._set_ratio("ratio", self.ratio + self.ratio_increment) @expose_command() def shrink_main(self): """Shrink the main window.""" if self.main_pane_ratio is None: self.main_pane_ratio = self.ratio self._set_ratio("main_pane_ratio", self.main_pane_ratio - self.ratio_increment) @expose_command() def grow_main(self): """Grow the main window.""" if self.main_pane_ratio is None: self.main_pane_ratio = self.ratio self._set_ratio("main_pane_ratio", self.main_pane_ratio + self.ratio_increment) @expose_command() def set_ratio(self, ratio: float): """Set the ratio for all windows.""" self._set_ratio("ratio", ratio) @expose_command() def set_master_ratio(self, ratio: float): """Set the ratio for the main window.""" self._set_ratio("main_pane_ratio", ratio) @expose_command() def reset(self): """Reset ratios to values set in config.""" self.ratio = self.initial_ratio self.main_pane_ratio = self.initial_main_pane_ratio # Force layout to be recalculated self.dirty = True self.group.layout_all() qtile-0.31.0/libqtile/layout/tile.py0000664000175000017500000002234414762660347017274 0ustar epsilonepsilon# Copyright (c) 2010 Aldo Cortesi # Copyright (c) 2010-2011 Paul Colomiets # Copyright (c) 2011 Mounier Florian # Copyright (c) 2011 Tzbob # Copyright (c) 2012 roger # Copyright (c) 2012-2014 Tycho Andersen # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014 ramnes # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 dmpayton # Copyright (c) 2014 dequis # Copyright (c) 2017 Dirk Hartmann. # Copyright (c) 2018 Nazar Mokrynskyi # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations from typing import TYPE_CHECKING from libqtile.command.base import expose_command from libqtile.config import _Match from libqtile.layout.base import _SimpleLayoutBase if TYPE_CHECKING: from typing import Any, Self from libqtile.backend.base import Window from libqtile.config import ScreenRect from libqtile.group import _Group class Tile(_SimpleLayoutBase): """A layout with two stacks of windows dividing the screen The Tile layout divides the screen_rect horizontally into two stacks. The maximum amount of "master" windows can be configured; surplus windows will be displayed in the slave stack on the right. Within their stacks, the windows will be tiled vertically. The windows can be rotated in their entirety by calling up() or down() or, if shift_windows is set to True, individually. """ defaults = [ ("border_focus", "#0000ff", "Border colour(s) for the focused window."), ("border_normal", "#000000", "Border colour(s) for un-focused windows."), ("border_on_single", True, "Whether to draw border if there is only one window."), ("border_width", 1, "Border width."), ("margin", 0, "Margin of the layout (int or list of ints [N E S W])"), ("margin_on_single", True, "Whether to draw margin if there is only one window."), ("ratio", 0.618, "Width-percentage of screen size reserved for master windows."), ("max_ratio", 0.85, "Maximum width of master windows"), ("min_ratio", 0.15, "Minimum width of master windows"), ( "master_length", 1, "Amount of windows displayed in the master stack. Surplus windows " "will be moved to the slave stack.", ), ( "expand", True, "Expand the master windows to the full screen width if no slaves " "are present.", ), ( "ratio_increment", 0.05, "By which amount to change ratio when decrease_ratio or " "increase_ratio are called.", ), ( "add_on_top", True, "Add new clients before all the others, potentially pushing other " "windows into slave stack.", ), ( "add_after_last", False, "Add new clients after all the others. If this is True, it " "overrides add_on_top.", ), ( "shift_windows", False, "Allow to shift windows within the layout. If False, the layout " "will be rotated instead.", ), ( "master_match", None, "A Match object defining which window(s) should be kept masters (single or a list " "of Match-objects).", ), ] def __init__(self, **config): _SimpleLayoutBase.__init__(self, **config) self.add_defaults(Tile.defaults) self._initial_ratio = self.ratio @property def ratio_size(self): return self.ratio @ratio_size.setter def ratio_size(self, ratio): self.ratio = min(max(ratio, self.min_ratio), self.max_ratio) @property def master_windows(self): return self.clients[: self.master_length] @property def slave_windows(self): return self.clients[self.master_length :] @expose_command("shuffle_left") def shuffle_up(self): if self.shift_windows: self.clients.shuffle_up() else: self.clients.rotate_down() self.group.layout_all() @expose_command("shuffle_right") def shuffle_down(self): if self.shift_windows: self.clients.shuffle_down() else: self.clients.rotate_up() self.group.layout_all() def reset_master(self, match=None): if not match and not self.master_match: return if self.clients: master_match = match or self.master_match if isinstance(master_match, _Match): master_match = [master_match] masters = [] for c in self.clients: for match in master_match: if match.compare(c): masters.append(c) for client in reversed(masters): self.clients.remove(client) self.clients.append_head(client) def clone(self, group: _Group) -> Self: c = _SimpleLayoutBase.clone(self, group) return c def add_client(self, client, offset_to_current=1): if self.add_after_last: self.clients.append(client) elif self.add_on_top: self.clients.append_head(client) else: super().add_client(client, offset_to_current) self.reset_master() def configure(self, client: Window, screen_rect: ScreenRect) -> None: screen_width = screen_rect.width screen_height = screen_rect.height border_width = self.border_width if self.clients and client in self.clients: pos = self.clients.index(client) if client in self.master_windows: w = ( int(screen_width * self.ratio_size) if len(self.slave_windows) or not self.expand else screen_width ) h = screen_height // self.master_length x = screen_rect.x y = screen_rect.y + pos * h else: w = screen_width - int(screen_width * self.ratio_size) h = screen_height // (len(self.slave_windows)) x = screen_rect.x + int(screen_width * self.ratio_size) sublist = self.clients[self.master_length :] if client not in sublist: raise ValueError("Client not in layout. This shouldn't happen.") y = screen_rect.y + sublist.index(client) * h if client.has_focus: bc = self.border_focus else: bc = self.border_normal if not self.border_on_single and len(self.clients) == 1: border_width = 0 else: border_width = self.border_width client.place( x, y, w - border_width * 2, h - border_width * 2, border_width, bc, margin=0 if (not self.margin_on_single and len(self.clients) == 1) else self.margin, ) client.unhide() else: client.hide() @expose_command() def info(self) -> dict[str, Any]: d = _SimpleLayoutBase.info(self) d.update( dict( master=[c.name for c in self.master_windows], slave=[c.name for c in self.slave_windows], ) ) return d @expose_command(["left", "up"]) def previous(self) -> None: _SimpleLayoutBase.previous(self) @expose_command(["right", "down"]) def next(self) -> None: _SimpleLayoutBase.next(self) @expose_command("normalize") def reset(self): self.ratio_size = self._initial_ratio self.group.layout_all() @expose_command() def decrease_ratio(self): self.ratio_size -= self.ratio_increment self.group.layout_all() @expose_command() def increase_ratio(self): self.ratio_size += self.ratio_increment self.group.layout_all() @expose_command() def decrease_nmaster(self): self.master_length -= 1 if self.master_length <= 0: self.master_length = 1 self.group.layout_all() @expose_command() def increase_nmaster(self): self.master_length += 1 self.group.layout_all() qtile-0.31.0/libqtile/layout/__init__.py0000664000175000017500000000442414762660347020075 0ustar epsilonepsilon# Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Florian Scherf # Copyright (c) 2014 Tycho Andersen # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # https://bitbucket.org/tarek/flake8/issue/141/improve-flake8-statement-to-ignore # is annoying, so we ignore libqtile/layout/__init__.py completely # flake8: noqa __all__ = [ "Bsp", "Columns", "Floating", "Matrix", "Max", "Plasma", "RatioTile", "ScreenSplit", "Slice", "Spiral", "Stack", "Tile", "TreeTab", "VerticalTile", "MonadTall", "MonadThreeCol", "MonadWide", "Zoomy", ] from libqtile.layout.bsp import Bsp from libqtile.layout.columns import Columns from libqtile.layout.floating import Floating from libqtile.layout.matrix import Matrix from libqtile.layout.max import Max from libqtile.layout.plasma import Plasma from libqtile.layout.ratiotile import RatioTile from libqtile.layout.screensplit import ScreenSplit from libqtile.layout.slice import Slice from libqtile.layout.spiral import Spiral from libqtile.layout.stack import Stack from libqtile.layout.tile import Tile from libqtile.layout.tree import TreeTab from libqtile.layout.verticaltile import VerticalTile from libqtile.layout.xmonad import MonadTall, MonadThreeCol, MonadWide from libqtile.layout.zoomy import Zoomy qtile-0.31.0/libqtile/layout/screensplit.py0000664000175000017500000002725514762660347020700 0ustar epsilonepsilon# Copyright (c) 2022 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations from typing import TYPE_CHECKING from libqtile import hook from libqtile.command.base import expose_command from libqtile.config import ScreenRect, _Match from libqtile.layout import Columns, Max from libqtile.layout.base import Layout from libqtile.log_utils import logger if TYPE_CHECKING: from collections.abc import Callable from typing import Any from libqtile.backend.base import Window from libqtile.group import _Group Rect = tuple[float, float, float, float] class Split: def __init__( self, *, name: str, rect: Rect, layout: Layout, matches: list[_Match] = list() ) -> None: # Check that rect is correctly defined if not isinstance(rect, tuple | list): raise ValueError("Split rect should be a list/tuple.") if len(rect) != 4 or not all(isinstance(x, float | int) for x in rect): raise ValueError("Split rect should have 4 float/int members.") if isinstance(layout, ScreenSplit): raise ValueError("ScreenSplit layouts cannot be nested.") if matches: if isinstance(matches, list): if not all(isinstance(m, _Match) for m in matches): raise ValueError("Invalid object in 'matches'.") else: raise ValueError("'matches' must be a list of 'Match' objects.") self.name = name self.rect = rect self.layout = layout self.matches = matches def clone(self, group) -> Split: return Split( name=self.name, rect=self.rect, layout=self.layout.clone(group), matches=self.matches ) class ScreenSplit(Layout): """ A layout that allows you to split the screen into separate areas, each of which can be assigned its own layout. This layout is intended to be used on large monitors where separate layouts may be desirable. However, unlike creating virtual screens, this layout retains the full screen configuration meaning that full screen windows will continue to fill the entire screen. Each split is defined as a dictionary with the following keys: - ``name``: this is used with the ``ScreenSplit`` widget (see below) - ``rect``: a tuple of (x, y, width, height) with each value being between 0 and 1. These are relative values based on the screen's dimensions e.g. a value of ``(0.5, 0, 0.5, 1)`` would define an area starting at the top middle of the screen and extending to the bottom left corner. - ``layout``: the layout to occupy the defined split. - ``matches``: (optional) list of ``Match`` objects which define which windows will open in the defined split. Different splits can be selected by using the following ``lazy.layout.next_split()`` and ``lazy.layout.previous_split()`` commands. To identify which split is active, users can use the ``ScreenSplit`` widget will show the name of the split and the relevant layout. Scrolling up and down on the widget will change the active split. .. note:: While keybindings will be passed to the active split's layout, bindings using the ``.when(layout=...)``` syntax will not be applied as the primary layout is ``ScreenSplit``. """ defaults = [ ( "splits", [ {"name": "top", "rect": (0, 0, 1, 0.5), "layout": Max()}, {"name": "bottom", "rect": (0, 0.5, 1, 0.5), "layout": Columns()}, ], "Screen splits. See documentation for details.", ) ] def __init__(self, **config): Layout.__init__(self, **config) self.add_defaults(ScreenSplit.defaults) self._split_index = 0 self.layouts = {} self._move_win = None self._has_matches = None splits = [] for s in self.splits: try: split_obj = Split(**s) except TypeError: raise ValueError("Splits must define 'name', 'rect' and 'layout'.") splits.append(split_obj) self.splits = splits self.hooks_set = False def _should_check(self, win): return win not in self.layouts and self._move_win is None @property def has_matches(self): if self._has_matches is None: self._has_matches = any(split.matches for split in self.splits) return self._has_matches @property def active_split(self): return self.splits[self._split_index] @active_split.setter def active_split(self, split): for i, sp in enumerate(self.splits): if sp == split: self._split_index = i hook.fire("layout_change", self, self.group) @property def active_layout(self): return self.active_split.layout @expose_command def commands(self): c = super().commands() c.extend(self.active_layout.commands()) return c def command(self, name: str) -> Callable | None: if name in self._commands: return self._commands.get(name) elif name in self.active_split.layout._commands: return getattr(self.active_split.layout, name) return None def _get_rect(self, rect: Rect, screen: ScreenRect) -> ScreenRect: x, y, w, h = rect return ScreenRect( int(screen.x + x * screen.width), int(screen.y + y * screen.height), int(screen.width * w), int(screen.height * h), ) def _set_hooks(self) -> None: if not self.hooks_set: hook.subscribe.focus_change(self.focus_split) self.hooks_set = True def _unset_hooks(self) -> None: if self.hooks_set: hook.unsubscribe.focus_change(self.focus_split) self.hooks_set = False def _match_win(self, win: Window) -> Split | None: for split in self.splits: if not split.matches: continue for m in split.matches: if win.match(m): return split return None def clone(self, group: _Group) -> ScreenSplit: result = Layout.clone(self, group) new_splits = [split.clone(group) for split in self.splits] result.splits = new_splits return result def add_client(self, win: Window) -> None: split = None # If this is a new window and we're not moving this window between splits # then we should check for match rules if self.has_matches and self._should_check(win): split = self._match_win(win) if split is not None: self.active_split = split self.active_layout.add_client(win) self.layouts[win] = self.active_split def remove(self, win: Window) -> None: self.layouts[win].layout.remove(win) del self.layouts[win] def hide(self) -> None: self._unset_hooks() def show(self, _rect) -> None: self._set_hooks() def configure(self, client: Window, screen_rect: ScreenRect) -> None: if client not in self.layouts: logger.warning("Unknown client: %s", client) return layout = self.layouts[client].layout rect = self._get_rect(self.layouts[client].rect, screen_rect) layout.configure(client, rect) def get_windows(self) -> list[Window]: return self.active_layout.get_windows() def _change_split(self, step: int = 1) -> None: self._split_index = (self._split_index + step) % len(self.splits) def _move_win_to_split(self, step: int = 1) -> None: # We get the ID of the next split now as removing window from a group # will shift focus to another window which could change the active # split. next_split = (self._split_index + step) % len(self.splits) self._move_win = self.group.current_window self.group.remove(self._move_win) self._split_index = next_split self.group.add(self._move_win) self.layouts[self._move_win] = self.active_split self._move_win = None hook.fire("layout_change", self, self.group) @expose_command def next(self) -> None: """Move to next client.""" self.__getattr__("next") @expose_command def previous(self) -> None: """Move to previous client.""" self.__getattr__("previous") def focus_first(self) -> Window: return self.active_layout.focus_first() def focus_last(self) -> Window: return self.active_layout.focus_last() def focus_next(self, win: Window) -> Window: return self.active_layout.focus_next(win) def focus_previous(self, win: Window) -> Window: return self.active_layout.focus_previous(win) def focus_split(self, win: Window | None = None) -> None: if win is None: win = self.group.current_window for split in self.splits: if win in split.layout.get_windows(): if split is not self.active_split: self.active_split = split hook.fire("layout_change", self, self.group) break def focus(self, client: Window) -> None: self.focus_split(client) self.active_layout.focus(client) @expose_command def next_split(self) -> None: """Move to next split.""" self._change_split() hook.fire("layout_change", self, self.group) @expose_command def previous_split(self) -> None: """Move to previous client.""" self._change_split(-1) hook.fire("layout_change", self, self.group) @expose_command def move_window_to_next_split(self) -> None: """Move current window to next split.""" self._move_win_to_split() @expose_command def move_window_to_previous_split(self) -> None: """Move current window to previous split.""" self._move_win_to_split(-1) @expose_command def info(self) -> dict[str, Any]: inf = Layout.info(self) inf["current_split"] = self.active_split.name inf["current_layout"] = self.active_layout.name inf["current_clients"] = [] inf["clients"] = [] inf["splits"] = [] for split in self.splits: clients = split.layout.info()["clients"] s_info = { "name": split.name, "rect": split.rect, "layout": split.layout.name, "clients": clients, } inf["splits"].append(s_info) inf["clients"].extend(clients) if split is self.active_split: inf["current_clients"] = clients return inf qtile-0.31.0/libqtile/layout/base.py0000664000175000017500000004375614762660347017263 0ustar epsilonepsilon# Copyright (c) 2008, Aldo Cortesi. All rights reserved. # Copyright (c) 2017 Dirk Hartmann # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations import copy from abc import ABCMeta, abstractmethod from typing import TYPE_CHECKING, overload from libqtile import configurable from libqtile.backend.base import Window from libqtile.command.base import CommandObject, expose_command from libqtile.command.interface import CommandError from libqtile.config import ScreenRect if TYPE_CHECKING: from collections.abc import Iterator, Sequence from typing import Any, Self from libqtile.command.base import ItemT from libqtile.group import _Group class Layout(CommandObject, configurable.Configurable, metaclass=ABCMeta): """This class defines the API that should be exposed by all layouts""" defaults: list[tuple[str, Any, str]] = [] def __init__(self, **config: Any) -> None: # name is a little odd; we can't resolve it until the class is defined # (i.e., we can't figure it out to define it in Layout.defaults), so # we resolve it here instead. if "name" not in config: config["name"] = self.__class__.__name__.lower() CommandObject.__init__(self) configurable.Configurable.__init__(self, **config) self.add_defaults(Layout.defaults) self._group: _Group | None = None def layout(self, windows: Sequence[Window], screen_rect: ScreenRect) -> None: for i in windows: self.configure(i, screen_rect) def finalize(self) -> None: pass @property def group(self) -> _Group: """ Returns the group this layout is attached to. Layouts start out unattached, and are attached when the group is configured and each layout is cloned for every group. """ if self._group is None: raise RuntimeError("Layout group accessed too early") return self._group def clone(self, group: _Group) -> Self: """Duplicate a layout Make a copy of this layout. This is done to provide each group with a unique instance of every layout. Parameters ========== group: Group to attach new layout instance to. """ c = copy.copy(self) c._group = group return c def _items(self, name: str) -> ItemT: if name == "screen" and self.group.screen is not None: return True, [] elif name == "group": return True, [] return None def _select(self, name: str, sel: str | int | None) -> CommandObject | None: if name == "screen": return self.group.screen elif name == "group": return self.group return None def show(self, screen_rect: ScreenRect) -> None: """Called when layout is being shown""" def hide(self) -> None: """Called when layout is being hidden""" def swap(self, c1: Window, c2: Window) -> None: """Swap the two given clients c1 and c2""" raise CommandError(f"layout: {self.name} does not support swapping windows") def focus(self, client: Window) -> None: """Called whenever the focus changes""" def blur(self) -> None: """Called whenever focus is gone from this layout""" @expose_command() def info(self) -> dict[str, Any]: """Returns a dictionary of layout information""" return dict(name=self.name, group=self.group.name if self.group else None) @abstractmethod def add_client(self, client: Window) -> None: """Called whenever a window is added to the group Called whether the layout is current or not. The layout should just add the window to its internal datastructures, without mapping or configuring. """ @abstractmethod def remove(self, client: Window) -> Window | None: """Called whenever a window is removed from the group Called whether the layout is current or not. The layout should just de-register the window from its data structures, without unmapping the window. Returns the "next" window that should gain focus or None. """ @abstractmethod def configure(self, client: Window, screen_rect: ScreenRect) -> None: """Configure the layout This method should: - Configure the dimensions and borders of a window using the `.place()` method. - Call either `.hide()` or `.unhide()` on the window. """ @abstractmethod def focus_first(self) -> Window | None: """Called when the first client in Layout shall be focused. This method should: - Return the first client in Layout, if any. - Not focus the client itself, this is done by caller. """ @abstractmethod def focus_last(self) -> Window | None: """Called when the last client in Layout shall be focused. This method should: - Return the last client in Layout, if any. - Not focus the client itself, this is done by caller. """ @abstractmethod def focus_next(self, win: Window) -> Window | None: """Called when the next client in Layout shall be focused. This method should: - Return the next client in Layout, if any. - Return None if the next client would be the first client. - Not focus the client itself, this is done by caller. Do not implement a full cycle here, because the Groups cycling relies on returning None here if the end of Layout is hit, such that Floating clients are included in cycle. Parameters ========== win: The currently focused client. """ @abstractmethod def focus_previous(self, win: Window) -> Window | None: """Called when the previous client in Layout shall be focused. This method should: - Return the previous client in Layout, if any. - Return None if the previous client would be the last client. - Not focus the client itself, this is done by caller. Do not implement a full cycle here, because the Groups cycling relies on returning None here if the end of Layout is hit, such that Floating clients are included in cycle. Parameters ========== win: The currently focused client. """ @abstractmethod def next(self) -> None: pass @abstractmethod def previous(self) -> None: pass class _ClientList: """ ClientList maintains a list of clients and a current client. The collection is meant as a base or utility class for special layouts, which need to maintain one or several collections of windows, for example Columns or Stack, which use this class as base for their internal helper. The property 'current_index' get and set the index to the current client, whereas 'current_client' property can be used with clients directly. The collection implements focus_xxx methods as desired for Group. """ def __init__(self) -> None: self._current_idx: int = 0 self.clients: list[Window] = [] @property def current_index(self) -> int: return self._current_idx @current_index.setter def current_index(self, x: int) -> None: if len(self): self._current_idx = abs(x % len(self)) else: self._current_idx = 0 @property def current_client(self) -> Window | None: if not self.clients: return None return self.clients[self._current_idx] @current_client.setter def current_client(self, client: Window) -> None: self._current_idx = self.clients.index(client) def focus(self, client: Window) -> None: """ Mark the given client as the current focused client in collection. This is equivalent to setting current_client. """ self.current_client = client def focus_first(self) -> Window | None: """ Returns the first client in collection. """ return self[0] def focus_next(self, win: Window) -> Window | None: """ Returns the client next from win in collection. """ try: return self[self.index(win) + 1] except IndexError: return None def focus_last(self) -> Window | None: """ Returns the last client in collection. """ return self[-1] def focus_previous(self, win: Window) -> Window | None: """ Returns the client previous to win in collection. """ idx = self.index(win) if idx > 0: return self[idx - 1] return None def add_client( self, client: Window, offset_to_current: int = 0, client_position: str | None = None ) -> None: """ Insert the given client into collection at position of the current. Use parameter 'offset_to_current' to specify where the client shall be inserted. Defaults to zero, which means at position of current client. Positive values are after the client. Use parameter 'client_position' to insert the given client at 4 specific positions: top, bottom, after_current, before_current. """ if client_position is not None: if client_position == "after_current": return self.add_client(client, offset_to_current=1) elif client_position == "before_current": return self.add_client(client, offset_to_current=0) elif client_position == "top": self.append_head(client) else: # ie client_position == "bottom" self.append(client) else: pos = max(0, self._current_idx + offset_to_current) if pos < len(self.clients): self.clients.insert(pos, client) else: self.clients.append(client) self.current_client = client def append_head(self, client: Window) -> None: """ Append the given client in front of list. """ self.clients.insert(0, client) def append(self, client: Window) -> None: """ Append the given client to the end of the collection. """ self.clients.append(client) def remove(self, client: Window) -> Window | None: """ Remove the given client from collection. """ if client not in self.clients: return None idx = self.clients.index(client) del self.clients[idx] if len(self) == 0: self._current_idx = 0 elif idx <= self._current_idx: self._current_idx = max(0, self._current_idx - 1) return self[self._current_idx] def rotate_up(self, maintain_index: bool = True) -> None: """ Rotate the list. The first client is moved to last position. If maintain_index is True the current_index is adjusted, such that the same client stays current and goes up in list. """ if len(self.clients) > 1: self.clients.append(self.clients.pop(0)) if maintain_index: self.current_index -= 1 def rotate_down(self, maintain_index: bool = True) -> None: """ Rotate the list. The last client is moved to first position. If maintain_index is True the current_index is adjusted, such that the same client stays current and goes down in list. """ if len(self.clients) > 1: self.clients.insert(0, self.clients.pop()) if maintain_index: self.current_index += 1 def swap(self, c1: Window, c2: Window, focus: int = 1) -> None: """ Swap the two given clients in list. The optional argument 'focus' can be 1 or 2. In case of 1, the first client c1 is focused, in case of 2 the c2 and the current_index is not changed otherwise. """ i1 = self.clients.index(c1) i2 = self.clients.index(c2) self.clients[i1], self.clients[i2] = self.clients[i2], self.clients[i1] if focus == 1: self.current_index = i1 elif focus == 2: self.current_index = i2 def shuffle_up(self, maintain_index: bool = True) -> None: """ Shuffle the list. The current client swaps position with its predecessor. If maintain_index is True the current_index is adjusted, such that the same client stays current and goes up in list. """ idx = self._current_idx if idx > 0: self.clients[idx], self.clients[idx - 1] = self.clients[idx - 1], self.clients[idx] if maintain_index: self.current_index -= 1 def shuffle_down(self, maintain_index: bool = True) -> None: """ Shuffle the list. The current client swaps position with its successor. If maintain_index is True the current_index is adjusted, such that the same client stays current and goes down in list. """ idx = self._current_idx if idx + 1 < len(self.clients): self.clients[idx], self.clients[idx + 1] = self.clients[idx + 1], self.clients[idx] if maintain_index: self.current_index += 1 def join(self, other: _ClientList, offset_to_current: int = 0) -> None: """ Add clients from 'other' _ClientList to self. 'offset_to_current' works as described for add() """ pos = max(0, self.current_index + offset_to_current) if pos < len(self.clients): self.clients = self.clients[:pos:] + other.clients + self.clients[pos::] else: self.clients.extend(other.clients) def index(self, client: Window) -> int: return self.clients.index(client) def __len__(self) -> int: return len(self.clients) @overload def __getitem__(self, i: int) -> Window | None: ... @overload def __getitem__(self, i: slice) -> list[Window]: ... def __getitem__(self, i: int | slice) -> Window | None | list[Window]: if isinstance(i, slice): return self.clients[i] try: return self.clients[i] except IndexError: return None def __setitem__(self, i: int, value: Window) -> None: self.clients[i] = value def __iter__(self) -> Iterator[Window]: return self.clients.__iter__() def __contains__(self, client: Window) -> bool: return client in self.clients def __str__(self) -> str: curr = self.current_client return "_ClientList: " + ", ".join( [("[%s]" if c == curr else "%s") % c.name for c in self.clients] ) @expose_command() def info(self) -> dict[str, Any]: return dict( clients=[c.name for c in self.clients], current=self._current_idx, ) class _SimpleLayoutBase(Layout): """ Basic layout class for simple layouts, which need to maintain a single list of clients. This class offers full fledged list of clients and focus cycling. Basic Layouts like Max and Matrix are based on this class """ def __init__(self, **config: Any) -> None: Layout.__init__(self, **config) self.clients = _ClientList() def clone(self, group: _Group) -> Self: c = Layout.clone(self, group) c.clients = _ClientList() return c def focus(self, client: Window) -> None: self.clients.current_client = client def focus_first(self) -> Window | None: return self.clients.focus_first() def focus_last(self) -> Window | None: return self.clients.focus_last() def focus_next(self, window: Window) -> Window | None: return self.clients.focus_next(window) def focus_previous(self, window: Window) -> Window | None: return self.clients.focus_previous(window) def previous(self) -> None: if self.clients.current_client is None: return client = self.focus_previous(self.clients.current_client) or self.focus_last() self.group.focus(client, True) def swap(self, window1: Window, window2: Window) -> None: self.clients.swap(window1, window2) self.group.layout_all() self.group.focus(window1) def next(self) -> None: if self.clients.current_client is None: return client = self.focus_next(self.clients.current_client) or self.focus_first() self.group.focus(client, True) def add_client( self, client: Window, offset_to_current: int = 0, client_position: str | None = None ) -> None: self.clients.add_client(client, offset_to_current, client_position) def remove(self, client: Window) -> Window | None: return self.clients.remove(client) def get_windows(self) -> list[Window]: return self.clients.clients @expose_command() def info(self) -> dict[str, Any]: d = Layout.info(self) d.update(self.clients.info()) return d qtile-0.31.0/libqtile/layout/slice.py0000664000175000017500000002402414762660347017433 0ustar epsilonepsilon# Copyright (c) 2011 Florian Mounier # Copyright (c) 2012, 2015 Tycho Andersen # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014 ramnes # Copyright (c) 2014 Sean Vig # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """ Slice layout. Serves as example of delegating layouts (or sublayouts) """ from __future__ import annotations from typing import TYPE_CHECKING from libqtile.backend.base import Window from libqtile.command.base import expose_command from libqtile.config import ScreenRect from libqtile.layout.base import Layout from libqtile.layout.max import Max if TYPE_CHECKING: from collections.abc import Sequence from typing import Any, Self from libqtile.group import _Group class Single(Layout): """Layout with single window Just like Max but asserts that window is the one """ def __init__(self): Layout.__init__(self) self.window = None self.focused = False def add_client(self, window): assert self.window is None self.window = window def remove(self, window: Window) -> None: if self.window is not window: raise ValueError("Cannot remove, window not managed by layout") self.window = None def configure(self, window, screen_rect): if window is self.window: window.place( screen_rect.x, screen_rect.y, screen_rect.width, screen_rect.height, 0, None, ) window.unhide() else: window.hide() def empty(self): """Is the layout empty Returns True if the layout empty (and is willing to accept windows) """ return self.window is None def focus_first(self) -> Window | None: self.focused = True return self.window def focus_last(self) -> Window | None: self.focused = True return self.window def focus_next(self, window: Window) -> Window | None: if self.focused: self.focused = False return None return self.window def focus_previous(self, window: Window) -> Window | None: if self.focused: self.focused = False return None return self.window def next(self) -> None: pass def previous(self) -> None: pass def get_windows(self): return self.window def info(self) -> dict[str, Any]: d = Layout.info(self) d["window"] = self.window.name if self.window else "" return d class Slice(Layout): """Slice layout This layout cuts piece of screen_rect and places a single window on that piece, and delegates other window placement to other layout """ defaults = [ ("width", 256, "Slice width."), ("side", "left", "Position of the slice (left, right, top, bottom)."), ("match", None, "Match-object describing which window(s) to move to the slice."), ("fallback", Max(), "Layout to be used for the non-slice area."), ] fallback: Layout def __init__(self, **config): self.layouts = {} Layout.__init__(self, **config) self.add_defaults(Slice.defaults) self._slice = Single() def clone(self, group: _Group) -> Self: res = Layout.clone(self, group) res._slice = self._slice.clone(group) res.fallback = self.fallback.clone(group) return res def delegate_layout(self, windows, mapping): """Delegates layouting actual windows Parameters =========== windows: windows to layout mapping: mapping from layout to ScreenRect for each layout """ grouped = {} for w in windows: lay = self.layouts[w] if lay in grouped: grouped[lay].append(w) else: grouped[lay] = [w] for lay, wins in grouped.items(): lay.layout(wins, mapping[lay]) def layout(self, windows: Sequence[Window], screen_rect: ScreenRect) -> None: win, sub = self._get_screen_rects(screen_rect) self.delegate_layout( windows, { self._slice: win, self.fallback: sub, }, ) def show(self, screen_rect: ScreenRect) -> None: win, sub = self._get_screen_rects(screen_rect) self._slice.show(win) self.fallback.show(sub) def configure(self, win, screen_rect): raise NotImplementedError("Should not be called") def _get_layouts(self): return (self._slice, self.fallback) def _get_active_layout(self): return self.fallback # always def _get_screen_rects(self, screen): if self.side == "left": win, sub = screen.hsplit(self.width) elif self.side == "right": sub, win = screen.hsplit(screen.width - self.width) elif self.side == "top": win, sub = screen.vsplit(self.width) elif self.side == "bottom": sub, win = screen.vsplit(screen.height - self.width) else: raise NotImplementedError(self.side) return (win, sub) def add_client(self, win): if self._slice.empty() and self.match and self.match.compare(win): self._slice.add_client(win) self.layouts[win] = self._slice else: self.fallback.add_client(win) self.layouts[win] = self.fallback def remove(self, win: Window) -> Window: lay = self.layouts.pop(win) focus = lay.remove(win) if not focus: layouts = self._get_layouts() idx = layouts.index(lay) while idx < len(layouts) - 1 and not focus: idx += 1 focus = layouts[idx].focus_first() return focus def hide(self) -> None: for lay in self._get_layouts(): lay.hide() def focus(self, win): self.layouts[win].focus(win) def blur(self) -> None: for lay in self._get_layouts(): lay.blur() def focus_first(self) -> Window | None: layouts = self._get_layouts() for lay in layouts: win = lay.focus_first() if win: return win return None def focus_last(self) -> None: layouts = self._get_layouts() for lay in reversed(layouts): win = lay.focus_last() if win: return win return None def focus_next(self, win: Window) -> Window | None: layouts = self._get_layouts() cur = self.layouts[win] focus = cur.focus_next(win) if not focus: idx = layouts.index(cur) while idx < len(layouts) - 1 and not focus: idx += 1 focus = layouts[idx].focus_first() return focus def focus_previous(self, win: Window) -> Window | None: layouts = self._get_layouts() cur = self.layouts[win] focus = cur.focus_previous(win) if not focus: idx = layouts.index(cur) while idx > 0 and not focus: idx -= 1 focus = layouts[idx].focus_last() return focus def __getattr__(self, name): """Delegate unimplemented command calls to active layout. For exposed commands that don't exist on the Slice class, this looks for an implementation on the active layout. """ if "fallback" in self.__dict__: cmd = self.command(name) if cmd: return cmd return super().__getattr__(name) @expose_command() def next(self) -> None: self.fallback.next() @expose_command() def previous(self) -> None: self.fallback.previous() @expose_command() def commands(self): cmds = self._get_active_layout().commands() cmds.extend(cmd for cmd in Layout.commands(self) if cmd not in cmds) return cmds def get_windows(self): clients = list() for layout in self._get_layouts(): if layout.get_windows() is not None: clients.extend(layout.get_windows()) return clients def command(self, name: str): if name in self._commands: return self._commands.get(name) elif name in self._get_active_layout()._commands: return getattr(self._get_active_layout(), name) @expose_command() def move_to_slice(self): """Moves the current window to the slice.""" win = self.group.current_window old_slice = self._slice.window if old_slice: self._slice.remove(old_slice) self.fallback.add_client(old_slice) self.layouts[old_slice] = self.fallback self.fallback.remove(win) self._slice.add_client(win) self.layouts[win] = self._slice self.group.layout_all() @expose_command() def info(self) -> dict[str, Any]: d = Layout.info(self) for layout in self._get_layouts(): d[layout.name] = layout.info() return d qtile-0.31.0/libqtile/layout/ratiotile.py0000664000175000017500000002654214762660347020337 0ustar epsilonepsilon# Copyright (c) 2011 Florian Mounier # Copyright (c) 2012-2013, 2015 Tycho Andersen # Copyright (c) 2013 Björn Lindström # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014 ramnes # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 dmpayton # Copyright (c) 2014 dequis # Copyright (c) 2017 Dirk Hartmann # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations import math from typing import TYPE_CHECKING from libqtile.command.base import expose_command from libqtile.layout.base import _SimpleLayoutBase if TYPE_CHECKING: from typing import Any, Self from libqtile.backend.base import Window from libqtile.group import _Group ROWCOL = 1 # do rows at a time left to right top down COLROW = 2 # do cols top to bottom, left to right GOLDEN_RATIO = 1.618 class GridInfo: """ Calculates sizes for grids >>> gi = GridInfo(.5, 5, 600, 480) >>> gi.calc() (1, 5, 1) >>> gi.get_sizes() [(0, 0, 120, 480), (120, 0, 120, 480), (240, 0, 120, 480), (360, 0, 120, 480), (480, 0, 120, 480)] >>> gi = GridInfo(6, 5, 600, 480) >>> gi.get_sizes() [(0, 0, 600, 96), (0, 96, 600, 96), (0, 192, 600, 96), (0, 288, 600, 96), (0, 384, 600, 96)] >>> gi = GridInfo(1, 5, 600, 480) >>> gi.get_sizes() [(0, 0, 200, 240), (200, 0, 200, 240), (400, 0, 200, 240), (0, 240, 300, 240), (200, 240, 200, 240)] >>> foo = GridInfo(1.6, 7, 400,370) >>> foo.get_sizes(500,580) """ def __init__(self, ratio, num_windows, width, height): self.ratio = ratio self.num_windows = num_windows self.width = width self.height = height self.num_rows = 0 self.num_cols = 0 def calc(self, num_windows, width, height): """returns (rows, cols, orientation) tuple given input""" best_ratio = None best_rows_cols_orientation = None for rows, cols, orientation in self._possible_grids(num_windows): sample_width = width / cols sample_height = height / rows sample_ratio = sample_width / sample_height diff = abs(sample_ratio - self.ratio) if best_ratio is None or diff < best_ratio: best_ratio = diff best_rows_cols_orientation = (rows, cols, orientation) return best_rows_cols_orientation def _possible_grids(self, num_windows): """ iterates over possible grids given a number of windows """ if num_windows < 2: end = 2 else: end = num_windows // 2 + 1 for rows in range(1, end): cols = int(math.ceil(num_windows / rows)) yield (rows, cols, ROWCOL) if rows != cols: # also want the reverse test yield (cols, rows, COLROW) def get_sizes_advanced(self, total_width, total_height, xoffset=0, yoffset=0): """after every row/column recalculate remaining area""" results = [] width = total_width height = total_height while len(results) < self.num_windows: remaining = self.num_windows - len(results) orien, sizes = self._get_row_or_col(remaining, width, height, xoffset, yoffset) results.extend(sizes) if orien == ROWCOL: # adjust height/yoffset height -= sizes[-1][-1] yoffset += sizes[-1][-1] else: width -= sizes[-1][-2] xoffset += sizes[-1][-2] return results def _get_row_or_col(self, num_windows, width, height, xoffset, yoffset): """process one row (or col) at a time""" rows, cols, orientation = self.calc(num_windows, width, height) results = [] if orientation == ROWCOL: x = 0 y = 0 for i, col in enumerate(range(cols)): w_width = width // cols w_height = height // rows if i == cols - 1: w_width = width - x results.append((x + xoffset, y + yoffset, w_width, w_height)) x += w_width elif orientation == COLROW: x = 0 y = 0 for i, col in enumerate(range(rows)): w_width = width // cols w_height = height // rows if i == rows - 1: w_height = height - y results.append((x + xoffset, y + yoffset, w_width, w_height)) y += w_height return orientation, results def get_sizes(self, total_width, total_height, xoffset=0, yoffset=0): width = 0 height = 0 results = [] rows, cols, orientation = self.calc(self.num_windows, total_width, total_height) if orientation == ROWCOL: y = 0 for i, row in enumerate(range(rows)): x = 0 width = total_width // cols for j, col in enumerate(range(cols)): height = total_height // rows if i == rows - 1 and j == 0: # last row remaining = self.num_windows - len(results) width = total_width // remaining elif j == cols - 1 or len(results) + 1 == self.num_windows: # since we are dealing with integers, # make last column (or item) take up remaining space width = total_width - x results.append((x + xoffset, y + yoffset, width, height)) if len(results) == self.num_windows: return results x += width y += height else: x = 0 for i, col in enumerate(range(cols)): y = 0 height = total_height // rows for j, row in enumerate(range(rows)): width = total_width // cols # down first if i == cols - 1 and j == 0: remaining = self.num_windows - len(results) height = total_height // remaining elif j == rows - 1 or len(results) + 1 == self.num_windows: height = total_height - y results.append( ( x + xoffset, # i * width + xoffset, y + yoffset, # j * height + yoffset, width, height, ) ) if len(results) == self.num_windows: return results y += height x += width return results class RatioTile(_SimpleLayoutBase): """Tries to tile all windows in the width/height ratio passed in""" defaults = [ ("border_focus", "#0000ff", "Border colour(s) for the focused window."), ("border_normal", "#000000", "Border colour(s) for un-focused windows."), ("border_width", 1, "Border width."), ("margin", 0, "Margin of the layout (int or list of ints [N E S W])"), ("ratio", GOLDEN_RATIO, "Ratio of the tiles"), ("ratio_increment", 0.1, "Amount to increment per ratio increment"), ("fancy", False, "Use a different method to calculate window sizes."), ] def __init__(self, **config): _SimpleLayoutBase.__init__(self, **config) self.add_defaults(RatioTile.defaults) self.dirty = True # need to recalculate self.layout_info = [] self.last_size = None self.last_screen = None def clone(self, group: _Group) -> Self: return _SimpleLayoutBase.clone(self, group) def add_client(self, w: Window) -> None: # type: ignore[override] self.dirty = True self.clients.append_head(w) def remove(self, w: Window) -> Window | None: self.dirty = True return _SimpleLayoutBase.remove(self, w) def configure(self, win, screen): # force recalc if not self.last_screen or self.last_screen != screen: self.last_screen = screen self.dirty = True if self.last_size and not self.dirty: if screen.width != self.last_size[0] or screen.height != self.last_size[1]: self.dirty = True if self.dirty: gi = GridInfo(self.ratio, len(self.clients), screen.width, screen.height) self.last_size = (screen.width, screen.height) if self.fancy: method = gi.get_sizes_advanced else: method = gi.get_sizes self.layout_info = method(screen.width, screen.height, screen.x, screen.y) self.dirty = False try: idx = self.clients.index(win) except ValueError: win.hide() return x, y, w, h = self.layout_info[idx] if win.has_focus: bc = self.border_focus else: bc = self.border_normal win.place( x, y, w - self.border_width * 2, h - self.border_width * 2, self.border_width, bc, margin=self.margin, ) win.unhide() @expose_command() def info(self) -> dict[str, Any]: d = _SimpleLayoutBase.info(self) focused = self.clients.current_client d["ratio"] = self.ratio d["focused"] = focused.name if focused else None d["layout_info"] = self.layout_info return d @expose_command("down") def previous(self) -> None: _SimpleLayoutBase.previous(self) @expose_command("up") def next(self) -> None: _SimpleLayoutBase.next(self) @expose_command() def shuffle_down(self): if self.clients: self.clients.rotate_up() self.group.layout_all() @expose_command() def shuffle_up(self): if self.clients: self.clients.rotate_down() self.group.layout_all() @expose_command() def decrease_ratio(self): new_ratio = self.ratio - self.ratio_increment if new_ratio < 0: return self.ratio = new_ratio self.dirty = True self.group.layout_all() @expose_command() def increase_ratio(self): self.ratio += self.ratio_increment self.dirty = True self.group.layout_all() qtile-0.31.0/libqtile/layout/tree.py0000664000175000017500000006217614762660347017305 0ustar epsilonepsilon# Copyright (c) 2011 Mounier Florian # Copyright (c) 2011 Paul Colomiets # Copyright (c) 2012 roger # Copyright (c) 2012-2014 Tycho Andersen # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2013 Arnas Udovicius # Copyright (c) 2014 ramnes # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Nathan Hoad # Copyright (c) 2014 dequis # Copyright (c) 2014 Thomas Sarboni # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations from typing import TYPE_CHECKING from libqtile import hook from libqtile.command.base import expose_command from libqtile.config import ScreenRect from libqtile.layout.base import Layout if TYPE_CHECKING: from collections.abc import Sequence from typing import Any, Self from libqtile.backend import base from libqtile.group import _Group to_superscript = dict(zip(map(ord, "0123456789"), map(ord, "⁰¹²³⁴⁵⁶⁷⁸⁹"))) class TreeNode: def __init__(self): self.children = [] self.parent = None self.expanded = True self._children_top = None self._children_bot = None def add_client(self, node, hint=None): """Add a node below this node The `hint` is a node to place the new node after in this nodes children. """ node.parent = self if hint is not None: try: idx = self.children.index(hint) except ValueError: pass else: self.children.insert(idx + 1, node) return self.children.append(node) def draw(self, layout, top, level=0): """Draw the node and its children to a layout Draws this node to the given layout (presumably a TreeTab), starting from a y-offset of `top` and at the given level. """ self._children_top = top if self.expanded: for i in self.children: top = i.draw(layout, top, level) self._children_bot = top return top def button_press(self, x, y): """Returns self or sibling which got the click""" # if we store the locations of each child, it would be possible to do # this without having to traverse the tree... if not (self._children_top <= y < self._children_bot): return for i in self.children: res = i.button_press(x, y) if res is not None: return res def add_superscript(self, title): """Prepend superscript denoting the number of hidden children""" if not self.expanded and self.children: return f"{len(self.children):d}".translate(to_superscript) + title return title def get_first_window(self): """Find the first Window under this node Returns self if this is a `Window`, otherwise finds first `Window` by depth-first search """ if isinstance(self, Window): return self if self.expanded: for i in self.children: node = i.get_first_window() if node: return node def get_last_window(self): """Find the last Window under this node Finds last `Window` by depth-first search, otherwise returns self if this is a `Window`. """ if self.expanded: for i in reversed(self.children): node = i.get_last_window() if node: return node if isinstance(self, Window): return self def get_next_window(self): if self.children and self.expanded: return self.children[0] node = self while not isinstance(node, Root): parent = node.parent idx = parent.children.index(node) for i in range(idx + 1, len(parent.children)): res = parent.children[i].get_first_window() if res: return res node = parent def get_prev_window(self): node = self while not isinstance(node, Root): parent = node.parent idx = parent.children.index(node) if idx == 0 and isinstance(parent, Window): return parent for i in range(idx - 1, -1, -1): res = parent.children[i].get_last_window() if res: return res node = parent class Root(TreeNode): def __init__(self, sections, default_section=None): super().__init__() self.sections = {} for section in sections: self.add_section(section) if default_section is None: self.def_section = self.children[0] else: self.def_section = self.sections[default_section] def add_client(self, win, hint=None): """Add a new window Adds a new `Window` to the tree. The location of the new node is controlled by looking: * `hint` kwarg - place the node next to this node * win.tree_section - place the window in the given section, by name * default section - fallback to default section (first section, if not otherwise set) """ parent = None if hint is not None: parent = hint.parent if parent is None: sect = getattr(win, "tree_section", None) if sect is not None: parent = self.sections.get(sect) if parent is None: parent = self.def_section node = Window(win) parent.add_client(node, hint=hint) return node def add_section(self, name): """Add a new Section with the given name""" if name in self.sections: raise ValueError("Duplicate section name") node = Section(name) node.parent = self self.sections[name] = node self.children.append(node) def del_section(self, name): """Remove the Section with the given name""" if name not in self.sections: raise ValueError("Section name not found") if len(self.children) == 1: raise ValueError("Can't delete last section") sec = self.sections[name] # move the children of the deleted section to the previous section # if delecting the first section, add children to second section idx = max(self.children.index(sec), 1) next_sec = self.children[idx - 1] # delete old section, reparent children to next section del self.children[idx] next_sec.children.extend(sec.children) for i in sec.children: i.parent = next_sec del self.sections[name] class Section(TreeNode): def __init__(self, title): super().__init__() self.title = title def draw(self, layout, top, level=0): del layout._layout.width # no centering # draw a horizontal line above the section layout._drawer.draw_hbar(layout.section_fg, 0, layout.panel_width, top, linewidth=1) # draw the section title layout._layout.font_size = layout.section_fontsize layout._layout.text = self.add_superscript(self.title) layout._layout.colour = layout.section_fg layout._layout.draw(x=layout.section_left, y=top + layout.section_top) top += layout._layout.height + layout.section_top + layout.section_padding # run the TreeNode draw to draw children (if expanded) top = super().draw(layout, top, level) return top + layout.section_bottom class Window(TreeNode): def __init__(self, win): super().__init__() self.window = win self._title_top = None def draw(self, layout, top, level=0): self._title_top = top # setup parameters for drawing self left = layout.padding_left + level * layout.level_shift layout._layout.font_size = layout.fontsize layout._layout.text = self.add_superscript(self.window.name) if self.window is layout._focused: fg = layout.active_fg bg = layout.active_bg elif self.window.urgent: fg = layout.urgent_fg bg = layout.urgent_bg else: fg = layout.inactive_fg bg = layout.inactive_bg layout._layout.colour = fg layout._layout.width = layout.panel_width - left # get a text frame from the above framed = layout._layout.framed( layout.border_width, bg, layout.padding_x, layout.padding_y ) # draw the text frame at the given point framed.draw_fill(left, top) top += framed.height + layout.vspace + layout.border_width # run the TreeNode draw to draw children (if expanded) return super().draw(layout, top, level + 1) def button_press(self, x, y): """Returns self if clicked on title else returns sibling""" if self._title_top <= y < self._children_top: return self return super().button_press(x, y) def remove(self) -> None: """Removes this Window If this window has children, the first child takes the place of this window, and any remaining children are reparented to that node """ if self.children: head = self.children[0] # add the first child to our parent, next to ourselves self.parent.add_client(head, hint=self) # move remaining children to be under the new head for i in self.children[1:]: head.add_client(i) self.parent.children.remove(self) del self.children class TreeTab(Layout): """Tree Tab Layout This layout works just like Max but displays tree of the windows at the left border of the screen_rect, which allows you to overview all opened windows. It's designed to work with ``uzbl-browser`` but works with other windows too. The panel at the left border contains sections, each of which contains windows. Initially the panel looks like flat lists inside its section, and looks like trees if some of the windows are "moved" left or right. For example, it looks like below with two sections initially: :: +------------+ |Section Foo | +------------+ | Window A | +------------+ | Window B | +------------+ | Window C | +------------+ |Section Bar | +------------+ And then it will look like below if "Window B" is moved right and "Window C" is moved right too: :: +------------+ |Section Foo | +------------+ | Window A | +------------+ | Window B | +------------+ | Window C | +------------+ |Section Bar | +------------+ """ defaults = [ ("bg_color", "000000", "Background color of tabs"), ("active_bg", "000080", "Background color of active tab"), ("active_fg", "ffffff", "Foreground color of active tab"), ("urgent_bg", "ff0000", "Background color of urgent tab"), ("urgent_fg", "ffffff", "Foreground color of urgent tab"), ("inactive_bg", "606060", "Background color of inactive tab"), ("inactive_fg", "ffffff", "Foreground color of inactive tab"), ("margin_left", 6, "Left margin of tab panel"), ("margin_y", 6, "Vertical margin of tab panel"), ("padding_left", 6, "Left padding for tabs"), ("padding_x", 6, "Left padding for tab label"), ("padding_y", 2, "Top padding for tab label"), ("border_width", 2, "Width of the border"), ("vspace", 2, "Space between tabs"), ("level_shift", 8, "Shift for children tabs"), ("font", "sans", "Font"), ("fontsize", 14, "Font pixel size."), ("fontshadow", None, "font shadow color, default is None (no shadow)"), ("section_fontsize", 11, "Font pixel size of section label"), ("section_fg", "ffffff", "Color of section label"), ("section_top", 4, "Top margin of section label"), ("section_bottom", 6, "Bottom margin of section"), ("section_padding", 4, "Bottom of margin section label"), ("section_left", 4, "Left margin of section label"), ("panel_width", 150, "Width of the left panel"), ("sections", ["Default"], "Titles of section instances"), ("previous_on_rm", False, "Focus previous window on close instead of first."), ("place_right", False, "Place the tab panel on the right side"), ] def __init__(self, **config): Layout.__init__(self, **config) self.add_defaults(TreeTab.defaults) self._focused = None self._panel = None self._drawer = None self._layout = None self._tree = Root(self.sections) self._nodes = {} def clone(self, group: _Group) -> Self: c = Layout.clone(self, group) c._focused = None c._panel = None c._tree = Root(self.sections) return c def focus(self, win): self._focused = win def focus_first(self) -> base.Window | None: win = self._tree.get_first_window() if win: return win.window return None def focus_last(self) -> base.Window | None: win = self._tree.get_last_window() if win: return win.window return None def focus_next(self, client: base.Window) -> base.Window | None: win = self._nodes[client].get_next_window() if win: return win.window return None def focus_previous(self, client: base.Window) -> base.Window | None: win = self._nodes[client].get_prev_window() if win: return win.window return None def blur(self) -> None: # Does not clear current window, will change if new one # will be focused. This works better when floating window # will be next focused one pass def add_client(self, win): if self._focused: node = self._tree.add_client(win, hint=self._nodes[self._focused]) else: node = self._tree.add_client(win) self._nodes[win] = node def remove(self, win: base.Window) -> None: if win not in self._nodes: return if self.previous_on_rm: self._focused = self.focus_previous(win) else: self._focused = self.focus_first() if self._focused is win: self._focused = None self._nodes[win].remove() del self._nodes[win] self.draw_panel() def _create_panel(self, screen_rect): self._panel = self.group.qtile.core.create_internal( screen_rect.x, screen_rect.y, self.panel_width, 100 ) self._create_drawer(screen_rect) self._panel.process_window_expose = self.draw_panel self._panel.process_button_click = self.process_button_click hook.subscribe.client_name_updated(self.draw_panel) hook.subscribe.focus_change(self.draw_panel) def draw_panel(self, *args): if not self._panel: return self._drawer.clear(self.bg_color) self._tree.draw(self, 0) self._drawer.draw(offsetx=0, width=self.panel_width) def process_button_click(self, x, y, _buttom): node = self._tree.button_press(x, y) if node: self.group.focus(node.window, False) def configure(self, client: base.Window, screen_rect: ScreenRect) -> None: if self._nodes and client is self._focused: client.place( screen_rect.x, screen_rect.y, screen_rect.width, screen_rect.height, 0, None ) client.unhide() else: client.hide() def finalize(self) -> None: if self._panel: self._panel.kill() Layout.finalize(self) if self._drawer is not None: self._drawer.finalize() def get_windows(self): clients = [] for section in self._tree.children: for window in section.children: clients.append(window.window) return clients @expose_command() def info(self) -> dict[str, Any]: def show_section_tree(root): """ Show a section tree in a nested list, whose every element has the form: `[root, [subtrees]]`. For `[root, [subtrees]]`, The first element is the root node, and the second is its a list of its subtrees. For example, a section with below windows hierarchy on the panel: - a - d - e - f - b - g - h - c will return [ [a, [d, [e]], [f]], [b, [g], [h]], [c], ] """ tree = [] if isinstance(root, Window): tree.append(root.window.name) if root.expanded and root.children: for child in root.children: tree.append(show_section_tree(child)) return tree d = Layout.info(self) d["clients"] = sorted([x.name for x in self._nodes]) d["sections"] = [x.title for x in self._tree.children] trees = {} for section in self._tree.children: trees[section.title] = show_section_tree(section) d["client_trees"] = trees return d def show(self, screen_rect: ScreenRect) -> None: if not self._panel: self._create_panel(screen_rect) if self.place_right: body, panel = screen_rect.hsplit(screen_rect.width - self.panel_width) else: panel, body = screen_rect.hsplit(self.panel_width) self._resize_panel(panel) self._panel.unhide() def hide(self) -> None: if self._panel: self._panel.hide() @expose_command("down") def next(self) -> None: """Switch down in the window list""" win = None if self._focused: win = self._nodes[self._focused].get_next_window() if not win: win = self._tree.get_first_window() if win: self.group.focus(win.window, False) self._focused = win.window if win else None @expose_command("up") def previous(self) -> None: """Switch up in the window list""" win = None if self._focused: win = self._nodes[self._focused].get_prev_window() if not win: win = self._tree.get_last_window() if win: self.group.focus(win.window, False) self._focused = win.window if win else None @expose_command() def move_up(self): win = self._focused if not win: return node = self._nodes[win] p = node.parent.children idx = p.index(node) if idx > 0: p[idx] = p[idx - 1] p[idx - 1] = node self.draw_panel() @expose_command() def move_down(self): win = self._focused if not win: return node = self._nodes[win] p = node.parent.children idx = p.index(node) if idx < len(p) - 1: p[idx] = p[idx + 1] p[idx + 1] = node self.draw_panel() @expose_command() def move_left(self): win = self._focused if not win: return node = self._nodes[win] if not isinstance(node.parent, Section): node.parent.children.remove(node) node.parent.parent.add_client(node) self.draw_panel() @expose_command() def add_section(self, name): """Add named section to tree""" self._tree.add_section(name) self.draw_panel() @expose_command() def del_section(self, name): """Remove named section from tree""" self._tree.del_section(name) self.draw_panel() @expose_command() def section_up(self): win = self._focused if not win: return node = self._nodes[win] snode = node while not isinstance(snode, Section): snode = snode.parent idx = snode.parent.children.index(snode) if idx > 0: node.parent.children.remove(node) snode.parent.children[idx - 1].add_client(node) self.draw_panel() @expose_command() def section_down(self): win = self._focused if not win: return node = self._nodes[win] snode = node while not isinstance(snode, Section): snode = snode.parent idx = snode.parent.children.index(snode) if idx < len(snode.parent.children) - 1: node.parent.children.remove(node) snode.parent.children[idx + 1].add_client(node) self.draw_panel() @expose_command() def sort_windows(self, sorter, create_sections=True): """Sorts window to sections using sorter function Parameters ========== sorter: function with single arg returning string returns name of the section where window should be create_sections: if this parameter is True (default), if sorter returns unknown section name it will be created dynamically """ for sec in self._tree.children: for win in sec.children[:]: nname = sorter(win.window) if nname is None or nname == sec.title: continue try: nsec = self._tree.sections[nname] except KeyError: if create_sections: self._tree.add_section(nname) nsec = self._tree.sections[nname] else: continue sec.children.remove(win) nsec.children.append(win) win.parent = nsec self.draw_panel() @expose_command() def move_right(self): win = self._focused if not win: return node = self._nodes[win] idx = node.parent.children.index(node) if idx > 0: node.parent.children.remove(node) node.parent.children[idx - 1].add_client(node) self.draw_panel() @expose_command() def expand_branch(self): if not self._focused: return self._nodes[self._focused].expanded = True self.draw_panel() @expose_command() def collapse_branch(self): if not self._focused: return self._nodes[self._focused].expanded = False self.draw_panel() @expose_command() def increase_ratio(self): self.panel_width += 10 self.group.layout_all() @expose_command() def decrease_ratio(self): self.panel_width -= 10 self.group.layout_all() def _create_drawer(self, screen_rect): # Create a new drawer object if the screen is a different height # e.g. if moving between screens with different resolutions if self._drawer is not None and ( self._drawer.height != screen_rect.height or self.panel_width != self._drawer.width ): self._drawer.finalize() self._drawer = None if self._drawer is None: self._drawer = self._panel.create_drawer( self.panel_width, screen_rect.height, ) self._drawer.clear(self.bg_color) self._layout = self._drawer.textlayout( "", "ffffff", self.font, self.fontsize, self.fontshadow, wrap=False ) def layout(self, windows: Sequence[base.Window], screen_rect: ScreenRect) -> None: if self.place_right: body, panel = screen_rect.hsplit(screen_rect.width - self.panel_width) else: panel, body = screen_rect.hsplit(self.panel_width) self._resize_panel(panel) Layout.layout(self, windows, body) def _resize_panel(self, screen_rect): if self._panel: self._panel.place( screen_rect.x, screen_rect.y, screen_rect.width, screen_rect.height, 0, None ) self._create_drawer(screen_rect) self.draw_panel() qtile-0.31.0/libqtile/layout/plasma.py0000664000175000017500000010567514762660347017625 0ustar epsilonepsilon# Copyright (c) 2017 numirias # Copyright (c) 2024 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import copy import time from enum import Enum, Flag, auto from math import isclose from typing import NamedTuple from libqtile import hook from libqtile.backend.base import Window from libqtile.command.base import expose_command from libqtile.hook import Hook, qtile_hooks from libqtile.layout.base import Layout plasma_hook = Hook( "plasma_add_mode", """ Used to flag when the add mode of the Plasma layout has changed. The hooked function should take one argument being the layout object. """, ) qtile_hooks.register_hook(plasma_hook) class NotRestorableError(Exception): pass class Point(NamedTuple): x: int y: int class Dimensions(NamedTuple): x: int y: int width: int height: int class Orient(Flag): HORIZONTAL = 0 VERTICAL = 1 class Direction(Enum): UP = auto() DOWN = auto() LEFT = auto() RIGHT = auto() @property def orient(self): return Orient.HORIZONTAL if self in [self.LEFT, self.RIGHT] else Orient.VERTICAL @property def offset(self): return 1 if self in [self.RIGHT, self.DOWN] else -1 class Priority(Enum): FIXED = auto() BALANCED = auto() class AddMode(Flag): HORIZONTAL = auto() VERTICAL = auto() SPLIT = auto() @property def orient(self): return Orient.VERTICAL if self & self.VERTICAL else Orient.HORIZONTAL border_check = { Direction.UP: lambda a, b: isclose(a.y, b.y_end), Direction.DOWN: lambda a, b: isclose(a.y_end, b.y), Direction.LEFT: lambda a, b: isclose(a.x, b.x_end), Direction.RIGHT: lambda a, b: isclose(a.x_end, b.x), } def flatten(value): """Flattens a nested list of lists into a single list.""" out = [] for x in value: if not isinstance(x, list): out.append(x) else: out.extend(flatten(x)) return out class Node: """ A tree node. Each node represents a container that can hold a payload and child nodes. """ min_size_default = 100 root_orient = Orient.HORIZONTAL priority = Priority.FIXED def __init__(self, payload=None, x=None, y=None, width=None, height=None): self.payload = payload self._x = x self._y = y self._width = width self._height = height self._size = None self.children = [] self.last_accessed = 0 self.parent = None self.restorables = {} def __repr__(self): info = self.payload or "" if self: info += " +%d" % len(self) return f"" # Define dunder methods to treat Node objects like an iterable def __contains__(self, node): if node is self: return True for child in self: if node in child: return True return False def __iter__(self): yield from self.children def __getitem__(self, key): return self.children[key] def __setitem__(self, key, value): self.children[key] = value def __len__(self): return len(self.children) @property def root(self): try: # Walk way up the tree until we find the root return self.parent.root except AttributeError: # Node has no parent (self.parent is None) so node must be root return self @property def is_root(self): return self.parent is None @property def is_leaf(self): return not self.children @property def index(self): return self.parent.children.index(self) @property def tree(self): return [c.tree if c else c for c in self] @property def siblings(self): if self.is_root: return list() return [c for c in self.parent if c is not self] @property def first_leaf(self): if self.is_leaf: return self return self[0].first_leaf @property def last_leaf(self): if self.is_leaf: return self return self[-1].last_leaf @property def recent_leaf(self): if self.is_leaf: return self return max(self, key=lambda n: n.last_accessed).recent_leaf @property def prev_leaf(self): if self.is_root: return self.last_leaf idx = self.index - 1 if idx < 0: return self.parent.prev_leaf return self.parent[idx].last_leaf @property def next_leaf(self): if self.is_root: return self.first_leaf idx = self.index + 1 if idx >= len(self.parent): return self.parent.next_leaf return self.parent[idx].first_leaf @property def all_leafs(self): if self.is_leaf: yield self for child in self: yield from child.all_leafs @property def orient(self): if self.is_root: return self.root_orient return ~self.parent.orient @property def horizontal(self): return self.orient is Orient.HORIZONTAL @property def vertical(self): return self.orient is Orient.VERTICAL @property def x(self): if self.is_root: return self._x if self.horizontal: return self.parent.x return self.parent.x + self.size_offset @x.setter def x(self, val): if not self.is_root: return self._x = val @property def y(self): if self.is_root: return self._y if self.vertical: return self.parent.y return self.parent.y + self.size_offset @y.setter def y(self, val): if not self.is_root: return self._y = val @property def pos(self): return Point(self.x, self.y) @property def width(self): if self.is_root: return self._width if self.horizontal: return self.parent.width return self.size @width.setter def width(self, val): if self.is_root: self._width = val elif self.horizontal: self.parent.size = val else: self.size = val @property def height(self): if self.is_root: return self._height if self.vertical: return self.parent.height return self.size @height.setter def height(self, val): if self.is_root: self._height = val elif self.vertical: self.parent.size = val else: self.size = val @property def x_end(self): return self.x + self.width @property def y_end(self): return self.y + self.height @property def x_center(self): return self.x + self.width / 2 @property def y_center(self): return self.y + self.height / 2 @property def center(self): return Point(self.x_center, self.y_center) @property def pixel_perfect(self): """ Return pixel-perfect int dimensions (x, y, width, height) which compensate for gaps in the layout grid caused by plain int conversions. """ x, y, width, height = self.x, self.y, self.width, self.height threshold = 0.99999 if (x - int(x)) + (width - int(width)) > threshold: width += 1 if (y - int(y)) + (height - int(height)) > threshold: height += 1 return Dimensions(*map(int, (x, y, width, height))) @property def capacity(self): return self.width if self.horizontal else self.height @property def size(self): """Return amount of space taken in parent container.""" if self.is_root: return None if self.fixed: return self._size if self.flexible: # Distribute space evenly among flexible nodes taken = sum(n.size for n in self.siblings if not n.flexible) flexibles = [n for n in self.parent if n.flexible] return (self.parent.capacity - taken) / len(flexibles) return max(sum(gc.min_size for gc in c) for c in self) @size.setter def size(self, val): if self.is_root or not self.siblings: return if val is None: self.reset_size() return occupied = sum(s.min_size_bound for s in self.siblings) val = max(min(val, self.parent.capacity - occupied), self.min_size_bound) self.force_size(val) def force_size(self, val): """Set size without considering available space.""" Node.fit_into(self.siblings, self.parent.capacity - val) if val == 0: return if self: Node.fit_into([self], val) self._size = val @property def size_offset(self): return sum(c.size for c in self.parent[: self.index]) @staticmethod def fit_into(nodes, space): """Resize nodes to fit them into the available space.""" if not nodes: return occupied = sum(n.min_size for n in nodes) if space >= occupied and any(n.flexible for n in nodes): # If any flexible node exists, it will occupy the space # automatically, not requiring any action. return nodes_left = nodes[:] space_left = space if space < occupied: for node in nodes: if node.min_size_bound != node.min_size: continue # Substract nodes that are already at their minimal possible # size because they can't be shrinked any further. space_left -= node.min_size nodes_left.remove(node) if not nodes_left: return factor = space_left / sum(n.size for n in nodes_left) for node in nodes_left: new_size = node.size * factor if node.fixed: node._size = new_size # pylint: disable=protected-access for child in node: Node.fit_into(child, new_size) @property def fixed(self): """A node is fixed if it has a specified size.""" return self._size is not None @property def min_size(self): if self.fixed: return self._size if self.is_leaf: return self.min_size_default size = max(sum(gc.min_size for gc in c) for c in self) return max(size, self.min_size_default) @property def min_size_bound(self): if self.is_leaf: return self.min_size_default return max(sum(gc.min_size_bound for gc in c) or self.min_size_default for c in self) def reset_size(self): self._size = None @property def flexible(self): """ A node is flexible if its size isn't (explicitly or implicitly) determined. """ if self.fixed: return False return all((any(gc.flexible for gc in c) or c.is_leaf) for c in self) def access(self): self.last_accessed = time.time() try: self.parent.access() except AttributeError: pass def neighbor(self, direction): """Return adjacent leaf node in specified direction.""" if self.is_root: return None if direction.orient is self.parent.orient: target_idx = self.index + direction.offset if 0 <= target_idx < len(self.parent): return self.parent[target_idx].recent_leaf if self.parent.is_root: return None return self.parent.parent.neighbor(direction) return self.parent.neighbor(direction) @property def up(self): return self.neighbor(Direction.UP) @property def down(self): return self.neighbor(Direction.DOWN) @property def left(self): return self.neighbor(Direction.LEFT) @property def right(self): return self.neighbor(Direction.RIGHT) def common_border(self, node, direction): """Return whether a common border with given node in specified direction exists. """ if not border_check[direction](self, node): return False if direction in [Direction.UP, Direction.DOWN]: detached = node.x >= self.x_end or node.x_end <= self.x else: detached = node.y >= self.y_end or node.y_end <= self.y return not detached def close_neighbor(self, direction): """Return visually adjacent leaf node in specified direction.""" nodes = [n for n in self.root.all_leafs if self.common_border(n, direction)] if not nodes: return None most_recent = max(nodes, key=lambda n: n.last_accessed) if most_recent.last_accessed > 0: return most_recent if direction in [Direction.UP, Direction.DOWN]: match = lambda n: n.x <= self.x_center <= n.x_end # noqa: E731 else: match = lambda n: n.y <= self.y_center <= n.y_end # noqa: E731 return next(n for n in nodes if match(n)) @property def close_up(self): return self.close_neighbor(Direction.UP) @property def close_down(self): return self.close_neighbor(Direction.DOWN) @property def close_left(self): return self.close_neighbor(Direction.LEFT) @property def close_right(self): return self.close_neighbor(Direction.RIGHT) def add_child(self, node, idx=None): if idx is None: idx = len(self) self.children.insert(idx, node) node.parent = self if len(self) == 1: return total = self.capacity if Node.priority is Priority.FIXED: # Prioritising windows with fixed sizes means the most space the siblings # must fit into is total width less the minimum size for a new node. # However, the new node doesn't have a fixed size so will expand to fit # available space space = total - Node.min_size_default else: # Balanced approach means that space for existing nodes is reduced so that # all nodes would be distributed evenly if none had fixed widths space = total - (total / len(self)) Node.fit_into(node.siblings, space) def add_child_after(self, new, old): self.add_child(new, idx=old.index + 1) def remove_child(self, node): node._save_restore_state() # pylint: disable=W0212 node.force_size(0) self.children.remove(node) if len(self) == 1: child = self[0] if self.is_root: # A single child doesn't need a fixed size child.reset_size() else: # Collapse tree with a single child self.parent.replace_child(self, child) Node.fit_into(child, self.capacity) def remove(self): self.parent.remove_child(self) def replace_child(self, old, new): self[old.index] = new new.parent = self new._size = old._size # pylint: disable=protected-access def flip_with(self, node, reverse=False): """Join with node in a new, orthogonal container.""" container = Node() self.parent.replace_child(self, container) self.reset_size() for child in [node, self] if reverse else [self, node]: container.add_child(child) def add_node(self, node, mode=None): """Add node according to the mode. This can result in adding it as a child, joining with it in a new flipped sub-container, or splitting the space with it. """ if self.is_root: self.add_child(node) elif mode is None: self.parent.add_child_after(node, self) elif mode.orient is self.parent.orient: if mode & AddMode.SPLIT: node._size = 0 # pylint: disable=protected-access self.parent.add_child_after(node, self) self._size = node._size = self.size / 2 else: self.parent.add_child_after(node, self) else: self.flip_with(node) def restore(self, node): """Restore node. Try to add the node in a place where a node with the same payload has previously been. """ restorables = self.root.restorables try: parent, idx, sizes, fixed, flip = restorables[node.payload] except KeyError: raise NotRestorableError() # pylint: disable=raise-missing-from if parent not in self.root: # Don't try to restore if parent is not part of the tree anymore raise NotRestorableError() node.reset_size() if flip: old_parent_size = parent.size parent.flip_with(node, reverse=(idx == 0)) node.size, parent.size = sizes Node.fit_into(parent, old_parent_size) else: parent.add_child(node, idx=idx) node.size = sizes[0] if len(sizes) == 2: node.siblings[0].size = sizes[1] if not fixed: node.reset_size() del restorables[node.payload] def _save_restore_state(self): parent = self.parent sizes = (self.size,) flip = False if len(self.siblings) == 1: # If there is only one node left in the container, we need to save # its size too because the size will be lost. sizes += (self.siblings[0]._size,) # pylint: disable=W0212 if not self.parent.is_root: flip = True parent = self.siblings[0] self.root.restorables[self.payload] = (parent, self.index, sizes, self.fixed, flip) def move(self, direction): """Move this node in `direction`. Return whether node was moved.""" if self.is_root: return False if direction.orient is self.parent.orient: old_idx = self.index new_idx = old_idx + direction.offset if 0 <= new_idx < len(self.parent): p = self.parent p[old_idx], p[new_idx] = p[new_idx], p[old_idx] return True new_sibling = self.parent.parent else: new_sibling = self.parent try: new_parent = new_sibling.parent idx = new_sibling.index except AttributeError: return False self.reset_size() self.parent.remove_child(self) new_parent.add_child(self, idx + (1 if direction.offset == 1 else 0)) return True def move_up(self): return self.move(Direction.UP) def move_down(self): return self.move(Direction.DOWN) def move_right(self): return self.move(Direction.RIGHT) def move_left(self): return self.move(Direction.LEFT) def _move_and_integrate(self, direction): old_parent = self.parent self.move(direction) if self.parent is not old_parent: self.integrate(direction) def integrate(self, direction): if direction.orient != self.parent.orient: self._move_and_integrate(direction) return target_idx = self.index + direction.offset if target_idx < 0 or target_idx >= len(self.parent): self._move_and_integrate(direction) return self.reset_size() target = self.parent[target_idx] self.parent.remove_child(self) if target.is_leaf: target.flip_with(self) else: target.add_child(self) def integrate_up(self): self.integrate(Direction.UP) def integrate_down(self): self.integrate(Direction.DOWN) def integrate_left(self): self.integrate(Direction.LEFT) def integrate_right(self): self.integrate(Direction.RIGHT) def find_payload(self, payload): if self.payload is payload: return self for child in self: needle = child.find_payload(payload) if needle is not None: return needle return None class Plasma(Layout): """ A flexible tree-based layout. Each tree node represents a container whose children are aligned either horizontally or vertically. Each window is attached to a leaf of the tree and takes either a calculated relative amount or a custom absolute amount of space in its parent container. Windows can be resized, rearranged and integrated into other containers. Windows in a container will all open in the same direction. Calling ``lazy.layout.mode_vertical/horizontal()`` will insert a new container allowing windows to be added in the new direction. You can use the ``Plasma`` widget to show which mode will apply when opening a new window based on the currently focused node. Windows can be focused selectively by using ``lazy.layout.up/down/left/right()`` to focus the nearest window in that direction relative to the currently focused window. "Integrating" windows is best explained with an illustation. Starting with three windows, a, b, c. b is currently focused. Calling ``lazy.layout.integrate_left()`` will have the following effect: :: ---------------------- ---------------------- | a | b | c | | a | c | | | | | | | | | | | | --> | | | | | | | |----------| | | | | | | b | | | | | | | | | | | | | | | | ---------------------- ---------------------- Finally, windows can me moved around the layout with ``lazy.layout.move_up/down/left/right()``. Example keybindings: .. code:: python from libqtile.config import EzKey from libqtile.lazy import lazy ... keymap = { 'M-h': lazy.layout.left(), 'M-j': lazy.layout.down(), 'M-k': lazy.layout.up(), 'M-l': lazy.layout.right(), 'M-S-h': lazy.layout.move_left(), 'M-S-j': lazy.layout.move_down(), 'M-S-k': lazy.layout.move_up(), 'M-S-l': lazy.layout.move_right(), 'M-A-h': lazy.layout.integrate_left(), 'M-A-j': lazy.layout.integrate_down(), 'M-A-k': lazy.layout.integrate_up(), 'M-A-l': lazy.layout.integrate_right(), 'M-d': lazy.layout.mode_horizontal(), 'M-v': lazy.layout.mode_vertical(), 'M-S-d': lazy.layout.mode_horizontal_split(), 'M-S-v': lazy.layout.mode_vertical_split(), 'M-a': lazy.layout.grow_width(30), 'M-x': lazy.layout.grow_width(-30), 'M-S-a': lazy.layout.grow_height(30), 'M-S-x': lazy.layout.grow_height(-30), 'M-C-5': lazy.layout.size(500), 'M-C-8': lazy.layout.size(800), 'M-n': lazy.layout.reset_size(), } keys = [EzKey(k, v) for k, v in keymap.items()] Acknowledgements: This layout was developed by numirias and published at https://github.com/numirias/qtile-plasma A few minor amendments have been made to that layout as part of incorporating this into the main qtile codebase but the majority of the work is theirs. """ defaults = [ ("name", "Plasma", "Layout name"), ("border_normal", "#333333", "Unfocused window border color"), ("border_focus", "#00e891", "Focused window border color"), ("border_normal_fixed", "#333333", "Unfocused fixed-size window border color"), ("border_focus_fixed", "#00e8dc", "Focused fixed-size window border color"), ("border_width", 1, "Border width"), ("border_width_single", 0, "Border width for single window"), ("margin", 0, "Layout margin"), ( "fair", False, "When ``False`` effort will be made to preserve nodes with a fixed size. " "Set to ``True`` to enable new windows to take more space from fixed size nodes.", ), ] # If windows are added before configure() was called, the screen size is # still unknown, so we need to set some arbitrary initial root dimensions default_dimensions = (0, 0, 1000, 1000) def __init__(self, **config): Layout.__init__(self, **config) self.add_defaults(Plasma.defaults) self.root = Node(None, *self.default_dimensions) self._focused = None self._add_mode = None Node.priority = Priority.BALANCED if self.fair else Priority.FIXED def swap(self, c1: Window, c2: Window) -> None: node_c1 = node_c2 = None for leaf in self.root.all_leafs: if leaf.payload is not None: if c1 == leaf.payload: node_c1 = leaf elif c2 == leaf.payload: node_c2 = leaf if node_c1 is not None and node_c2 is not None: node_c1.payload, node_c2.payload = node_c2.payload, node_c1.payload self.group.layout_all() self.group.focus(c1) return @staticmethod def convert_names(tree): return [Plasma.convert_names(n) if isinstance(n, list) else n.payload.name for n in tree] @property def add_mode(self): if self._add_mode is None: node = self.root_or_focused_node if node.width >= node.height: return AddMode.HORIZONTAL else: return AddMode.VERTICAL return self._add_mode @add_mode.setter def add_mode(self, value): self._add_mode = value # We trigger a redraw so that the different borders can be drawn based on the add_mode # We check self._group to avoid raising a runtime error from libqtile.layout.base if self._group is not None: hook.fire("plasma_add_mode", self) self.group.layout_all() @property def focused(self): return self._focused @focused.setter def focused(self, value): self._focused = value hook.fire("plasma_add_mode", self) @property def focused_node(self): return self.root.find_payload(self.focused) @property def root_or_focused_node(self): return self.root if self.focused_node is None else self.focused_node @property def horizontal(self): if self.focused_node is None: return True if self.add_mode is not None: if self.add_mode & AddMode.HORIZONTAL: return True else: return False if self.focused_node.parent is None: if self.focused_node.orient is Orient.HORIZONTAL: return True else: return False return self.focused_node.parent.horizontal @property def vertical(self): return not self.horizontal @property def split(self): if self.add_mode is not None and self.add_mode & AddMode.SPLIT: return True return False @expose_command def info(self): info = super().info() tree = self.convert_names(self.root.tree) info["tree"] = tree info["clients"] = flatten(tree) return info def clone(self, group): clone = copy.copy(self) clone._group = group clone.root = Node(None, *self.default_dimensions) clone.focused = None clone.add_mode = None return clone def get_windows(self): clients = [] for leaf in self.root.all_leafs: if leaf.payload is not None: clients.append(leaf.payload) return clients def add_client(self, client): new = Node(client) try: self.root.restore(new) except NotRestorableError: self.root_or_focused_node.add_node(new, self.add_mode) self.add_mode = None def remove(self, client): self.root.find_payload(client).remove() def configure(self, client, screen_rect): self.root.x = screen_rect.x self.root.y = screen_rect.y self.root.width = screen_rect.width self.root.height = screen_rect.height node = self.root.find_payload(client) border_width = self.border_width_single if self.root.tree == [node] else self.border_width border_color = getattr( self, "border_" + ("focus" if client.has_focus else "normal") + ("" if node.flexible else "_fixed"), ) x, y, width, height = node.pixel_perfect client.place( x, y, width - 2 * border_width, height - 2 * border_width, border_width, border_color, margin=self.margin, ) # Always keep tiles below floating windows client.unhide() def focus(self, client): self.focused = client self.root.find_payload(client).access() def focus_first(self): return self.root.first_leaf.payload def focus_last(self): return self.root.last_leaf.payload def focus_next(self, win): next_leaf = self.root.find_payload(win).next_leaf return None if next_leaf is self.root.first_leaf else next_leaf.payload def focus_previous(self, win): prev_leaf = self.root.find_payload(win).prev_leaf return None if prev_leaf is self.root.last_leaf else prev_leaf.payload def focus_node(self, node): if node is None: return self.group.focus(node.payload) def refocus(self): self.group.focus(self.focused) @expose_command def next(self): """Focus next window.""" self.focus_node(self.focused_node.next_leaf) @expose_command def previous(self): """Focus previous window.""" self.focus_node(self.focused_node.prev_leaf) @expose_command def recent(self): """Focus most recently focused window. (Toggles between the two latest active windows.) """ nodes = [n for n in self.root.all_leafs if n is not self.focused_node] most_recent = max(nodes, key=lambda n: n.last_accessed) self.focus_node(most_recent) @expose_command def left(self): """Focus window to the left.""" self.focus_node(self.focused_node.close_left) @expose_command def right(self): """Focus window to the right.""" self.focus_node(self.focused_node.close_right) @expose_command def up(self): """Focus window above.""" self.focus_node(self.focused_node.close_up) @expose_command def down(self): """Focus window below.""" self.focus_node(self.focused_node.close_down) @expose_command def move_left(self): """Move current window left.""" self.focused_node.move_left() self.refocus() @expose_command def move_right(self): """Move current window right.""" self.focused_node.move_right() self.refocus() @expose_command def move_up(self): """Move current window up.""" self.focused_node.move_up() self.refocus() @expose_command def move_down(self): """Move current window down.""" self.focused_node.move_down() self.refocus() @expose_command def integrate_left(self): """Integrate current window left.""" self.focused_node.integrate_left() self.refocus() @expose_command def integrate_right(self): """Integrate current window right.""" self.focused_node.integrate_right() self.refocus() @expose_command def integrate_up(self): """Integrate current window up.""" self.focused_node.integrate_up() self.refocus() @expose_command def integrate_down(self): """Integrate current window down.""" self.focused_node.integrate_down() self.refocus() @expose_command def mode_horizontal(self): """Next window will be added horizontally.""" self.add_mode = AddMode.HORIZONTAL @expose_command def mode_vertical(self): """Next window will be added vertically.""" self.add_mode = AddMode.VERTICAL @expose_command def mode_horizontal_split(self): """Next window will be added horizontally, splitting space of current window. """ self.add_mode = AddMode.HORIZONTAL | AddMode.SPLIT @expose_command def mode_vertical_split(self): """Next window will be added vertically, splitting space of current window. """ self.add_mode = AddMode.VERTICAL | AddMode.SPLIT @expose_command def set_size(self, x: int): """Change size of current window. (It's recommended to use `width()`/`height()` instead.) """ self.focused_node.size = x self.refocus() @expose_command def set_width(self, x: int): """Set width of current window.""" self.focused_node.width = x self.refocus() @expose_command def set_height(self, x: int): """Set height of current window.""" self.focused_node.height = x self.refocus() @expose_command def reset_size(self): """Reset size of current window to automatic (relative) sizing.""" self.focused_node.reset_size() self.refocus() @expose_command def grow(self, x: int): """Grow size of current window. (It's recommended to use `grow_width()`/`grow_height()` instead.) """ self.focused_node.size += x self.refocus() @expose_command def grow_width(self, x: int): """Grow width of current window.""" self.focused_node.width += x self.refocus() @expose_command def grow_height(self, x: int): """Grow height of current window.""" self.focused_node.height += x self.refocus() qtile-0.31.0/libqtile/layout/bsp.py0000664000175000017500000005015114762660347017120 0ustar epsilonepsilon# Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations from typing import TYPE_CHECKING from libqtile.command.base import expose_command from libqtile.layout.base import Layout if TYPE_CHECKING: from collections.abc import Generator from typing import Any, Self from libqtile.backend.base import Window from libqtile.config import ScreenRect from libqtile.group import _Group class _BspNode: def __init__(self, parent: _BspNode | None = None) -> None: self.parent = parent self.children: list[_BspNode] = [] self.split_horizontal: bool = False self.split_ratio: float = 50 self.client: Window | None = None self.x: int = 0 self.y: int = 0 self.w: int = 16 self.h: int = 9 def __iter__(self) -> Generator[_BspNode, None, None]: yield self for child in self.children: yield from child def clients(self) -> Generator[Window, None, None]: if self.client: yield self.client else: for child in self.children: yield from child.clients() def _shortest(self, length: int) -> tuple[_BspNode, int]: if len(self.children) == 0: return self, length child0, length0 = self.children[0]._shortest(length + 1) child1, length1 = self.children[1]._shortest(length + 1) if length1 < length0: return child1, length1 return child0, length0 def get_shortest(self) -> _BspNode: node, _ = self._shortest(0) return node def insert(self, client: Window, idx: int, ratio: float) -> _BspNode: if self.client is None: self.client = client return self self.children = [_BspNode(self), _BspNode(self)] self.children[1 - idx].client = self.client self.children[idx].client = client self.client = None self.split_horizontal = True if self.w > self.h * ratio else False return self.children[idx] def remove(self, child: _BspNode) -> _BspNode: keep = self.children[1 if child is self.children[0] else 0] self.children = keep.children for c in self.children: c.parent = self self.split_horizontal = keep.split_horizontal self.split_ratio = keep.split_ratio self.client = keep.client return self def distribute(self) -> tuple[int, int]: if len(self.children) == 0: return 1, 1 h0, v0 = self.children[0].distribute() h1, v1 = self.children[1].distribute() if self.split_horizontal: h = h0 + h1 v = max(v0, v1) self.split_ratio = 100 * h0 / h else: h = max(h0, h1) v = v0 + v1 self.split_ratio = 100 * v0 / v return h, v def calc_geom(self, x: int, y: int, w: int, h: int) -> None: self.x = x self.y = y self.w = w self.h = h if len(self.children) > 1: if self.split_horizontal: w0 = int(self.split_ratio * w * 0.01 + 0.5) self.children[0].calc_geom(x, y, w0, h) self.children[1].calc_geom(x + w0, y, w - w0, h) else: h0 = int(self.split_ratio * h * 0.01 + 0.5) self.children[0].calc_geom(x, y, w, h0) self.children[1].calc_geom(x, y + h0, w, h - h0) class Bsp(Layout): """This layout is inspired by bspwm, but it does not try to copy its features. The first client occupies the entire screen space. When a new client is created, the selected space is partitioned in 2 and the new client occupies one of those subspaces, leaving the old client with the other. The partition can be either horizontal or vertical according to the dimensions of the current space: if its width/height ratio is above a pre-configured value, the subspaces are created side-by-side, otherwise, they are created on top of each other. The partition direction can be freely toggled. All subspaces can be resized and clients can be shuffled around. All clients are organized at the leaves of a full binary tree. An example key configuration is:: Key([mod], "j", lazy.layout.down()), Key([mod], "k", lazy.layout.up()), Key([mod], "h", lazy.layout.left()), Key([mod], "l", lazy.layout.right()), Key([mod, "shift"], "j", lazy.layout.shuffle_down()), Key([mod, "shift"], "k", lazy.layout.shuffle_up()), Key([mod, "shift"], "h", lazy.layout.shuffle_left()), Key([mod, "shift"], "l", lazy.layout.shuffle_right()), Key([mod, "mod1"], "j", lazy.layout.flip_down()), Key([mod, "mod1"], "k", lazy.layout.flip_up()), Key([mod, "mod1"], "h", lazy.layout.flip_left()), Key([mod, "mod1"], "l", lazy.layout.flip_right()), Key([mod, "control"], "j", lazy.layout.grow_down()), Key([mod, "control"], "k", lazy.layout.grow_up()), Key([mod, "control"], "h", lazy.layout.grow_left()), Key([mod, "control"], "l", lazy.layout.grow_right()), Key([mod, "shift"], "n", lazy.layout.normalize()), Key([mod], "Return", lazy.layout.toggle_split()), """ defaults = [ ("border_focus", "#881111", "Border colour(s) for the focused window."), ("border_normal", "#220000", "Border colour(s) for un-focused windows."), ("border_width", 2, "Border width."), ("border_on_single", False, "Draw border when there is only one window."), ( "margin_on_single", None, "Margin when there is only one window (int or list of ints [N E S W], 'None' to use 'margin' value).", ), ("margin", 0, "Margin of the layout (int or list of ints [N E S W])."), ("ratio", 1.6, "Width/height ratio that defines the partition direction."), ("grow_amount", 10, "Amount by which to grow a window/column."), ("lower_right", True, "New client occupies lower or right subspace."), ("fair", True, "New clients are inserted in the shortest branch."), ( "wrap_clients", False, "Whether client list should be wrapped when using ``next`` and ``previous`` commands.", ), ] def __init__(self, **config): Layout.__init__(self, **config) self.add_defaults(Bsp.defaults) self.root = _BspNode() self.current = self.root if self.margin_on_single is None: self.margin_on_single = self.margin def clone(self, group: _Group) -> Self: c = Layout.clone(self, group) c.root = _BspNode() c.current = c.root return c def get_windows(self): return list(self.root.clients()) @expose_command() def info(self) -> dict[str, Any]: return dict(name=self.name, clients=[c.name for c in self.root.clients()]) def get_node(self, client): for node in self.root: if client is node.client: return node def focus(self, client: Window) -> None: self.current = self.get_node(client) def add_client(self, client: Window) -> None: node = self.root.get_shortest() if self.fair else self.current self.current = node.insert(client, int(self.lower_right), self.ratio) def remove(self, client): node = self.get_node(client) if node: if node.parent: node = node.parent.remove(node) newclient = next(node.clients(), None) if newclient is None: self.current = self.root else: self.current = self.get_node(newclient) return newclient node.client = None self.current = self.root def configure(self, client: Window, screen_rect: ScreenRect) -> None: self.root.calc_geom(screen_rect.x, screen_rect.y, screen_rect.width, screen_rect.height) node = self.get_node(client) color = self.border_focus if client.has_focus else self.border_normal border = 0 if node is self.root and not self.border_on_single else self.border_width margin = self.margin_on_single if node is self.root else self.margin if node is not None: client.place( node.x, node.y, node.w - 2 * border, node.h - 2 * border, border, color, margin=margin, ) client.unhide() @expose_command() def toggle_split(self): if self.current.parent: self.current.parent.split_horizontal = not self.current.parent.split_horizontal self.group.layout_all() def focus_first(self) -> Window | None: return next(self.root.clients(), None) def focus_last(self) -> Window | None: clients = list(self.root.clients()) return clients[-1] if len(clients) else None def focus_next(self, client: Window, wrap: bool = False) -> Window | None: clients = list(self.root.clients()) if client in clients: idx = clients.index(client) if not wrap and idx + 1 < len(clients): return clients[(idx + 1)] elif wrap: return clients[(idx + 1) % len(clients)] return None def focus_previous(self, client: Window, wrap: bool = False) -> Window | None: clients = list(self.root.clients()) if client in clients: idx = clients.index(client) if not wrap and idx > 0: return clients[(idx - 1)] elif wrap: return clients[(idx - 1) % len(clients)] return None @expose_command() def next(self) -> None: client = self.focus_next(self.current.client, wrap=self.wrap_clients) if client: self.group.focus(client, True) @expose_command() def previous(self) -> None: client = self.focus_previous(self.current.client, wrap=self.wrap_clients) if client: self.group.focus(client, True) def find_left(self): child = self.current parent = child.parent while parent: if parent.split_horizontal and child is parent.children[1]: neighbor = parent.children[0] center = self.current.y + self.current.h * 0.5 while neighbor.client is None: if neighbor.split_horizontal or neighbor.children[1].y < center: neighbor = neighbor.children[1] else: neighbor = neighbor.children[0] return neighbor child = parent parent = child.parent def find_right(self): child = self.current parent = child.parent while parent: if parent.split_horizontal and child is parent.children[0]: neighbor = parent.children[1] center = self.current.y + self.current.h * 0.5 while neighbor.client is None: if neighbor.split_horizontal or neighbor.children[1].y > center: neighbor = neighbor.children[0] else: neighbor = neighbor.children[1] return neighbor child = parent parent = child.parent def find_up(self): child = self.current parent = child.parent while parent: if not parent.split_horizontal and child is parent.children[1]: neighbor = parent.children[0] center = self.current.x + self.current.w * 0.5 while neighbor.client is None: if not neighbor.split_horizontal or neighbor.children[1].x < center: neighbor = neighbor.children[1] else: neighbor = neighbor.children[0] return neighbor child = parent parent = child.parent def find_down(self): child = self.current parent = child.parent while parent: if not parent.split_horizontal and child is parent.children[0]: neighbor = parent.children[1] center = self.current.x + self.current.w * 0.5 while neighbor.client is None: if not neighbor.split_horizontal or neighbor.children[1].x > center: neighbor = neighbor.children[0] else: neighbor = neighbor.children[1] return neighbor child = parent parent = child.parent @expose_command() def left(self): node = self.find_left() if node: self.group.focus(node.client, True) @expose_command() def right(self): node = self.find_right() if node: self.group.focus(node.client, True) @expose_command() def up(self): node = self.find_up() if node: self.group.focus(node.client, True) @expose_command() def down(self): node = self.find_down() if node: self.group.focus(node.client, True) @expose_command() def shuffle_left(self): node = self.find_left() if node: node.client, self.current.client = self.current.client, node.client self.current = node self.group.layout_all() elif self.current is not self.root: node = self.current self.remove(node.client) newroot = _BspNode() newroot.split_horizontal = True newroot.children = [node, self.root] self.root.parent = newroot node.parent = newroot self.root = newroot self.current = node self.group.layout_all() @expose_command() def shuffle_right(self): node = self.find_right() if node: node.client, self.current.client = self.current.client, node.client self.current = node self.group.layout_all() elif self.current is not self.root: node = self.current self.remove(node.client) newroot = _BspNode() newroot.split_horizontal = True newroot.children = [self.root, node] self.root.parent = newroot node.parent = newroot self.root = newroot self.current = node self.group.layout_all() @expose_command() def shuffle_up(self): node = self.find_up() if node: node.client, self.current.client = self.current.client, node.client self.current = node self.group.layout_all() elif self.current is not self.root: node = self.current self.remove(node.client) newroot = _BspNode() newroot.split_horizontal = False newroot.children = [node, self.root] self.root.parent = newroot node.parent = newroot self.root = newroot self.current = node self.group.layout_all() @expose_command() def shuffle_down(self): node = self.find_down() if node: node.client, self.current.client = self.current.client, node.client self.current = node self.group.layout_all() elif self.current is not self.root: node = self.current self.remove(node.client) newroot = _BspNode() newroot.split_horizontal = False newroot.children = [self.root, node] self.root.parent = newroot node.parent = newroot self.root = newroot self.current = node self.group.layout_all() @expose_command() def grow_left(self): child = self.current parent = child.parent while parent: if parent.split_horizontal and child is parent.children[1]: parent.split_ratio = max(5, parent.split_ratio - self.grow_amount) self.group.layout_all() break child = parent parent = child.parent @expose_command() def grow_right(self): child = self.current parent = child.parent while parent: if parent.split_horizontal and child is parent.children[0]: parent.split_ratio = min(95, parent.split_ratio + self.grow_amount) self.group.layout_all() break child = parent parent = child.parent @expose_command() def grow_up(self): child = self.current parent = child.parent while parent: if not parent.split_horizontal and child is parent.children[1]: parent.split_ratio = max(5, parent.split_ratio - self.grow_amount) self.group.layout_all() break child = parent parent = child.parent @expose_command() def grow_down(self): child = self.current parent = child.parent while parent: if not parent.split_horizontal and child is parent.children[0]: parent.split_ratio = min(95, parent.split_ratio + self.grow_amount) self.group.layout_all() break child = parent parent = child.parent @expose_command() def flip_left(self): child = self.current parent = child.parent while parent: if parent.split_horizontal and child is parent.children[1]: parent.children = parent.children[::-1] self.group.layout_all() break child = parent parent = child.parent @expose_command() def flip_right(self): child = self.current parent = child.parent while parent: if parent.split_horizontal and child is parent.children[0]: parent.children = parent.children[::-1] self.group.layout_all() break child = parent parent = child.parent @expose_command() def flip_up(self): child = self.current parent = child.parent while parent: if not parent.split_horizontal and child is parent.children[1]: parent.children = parent.children[::-1] self.group.layout_all() break child = parent parent = child.parent @expose_command() def flip_down(self): child = self.current parent = child.parent while parent: if not parent.split_horizontal and child is parent.children[0]: parent.children = parent.children[::-1] self.group.layout_all() break child = parent parent = child.parent @expose_command() def normalize(self): distribute = True for node in self.root: if node.split_ratio != 50: node.split_ratio = 50 distribute = False if distribute: self.root.distribute() self.group.layout_all() qtile-0.31.0/libqtile/layout/floating.py0000664000175000017500000002670214762660347020144 0ustar epsilonepsilon# Copyright (c) 2010 matt # Copyright (c) 2010-2011 Paul Colomiets # Copyright (c) 2011 Mounier Florian # Copyright (c) 2012 Craig Barnes # Copyright (c) 2012, 2014-2015 Tycho Andersen # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2013 Julien Iguchi-Cartigny # Copyright (c) 2014 ramnes # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 dequis # Copyright (c) 2018 Nazar Mokrynskyi # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations from typing import TYPE_CHECKING from libqtile.command.base import expose_command from libqtile.config import Match, _Match from libqtile.layout.base import Layout if TYPE_CHECKING: from typing import Any from libqtile.backend.base import Window from libqtile.config import ScreenRect class Floating(Layout): """ Floating layout, which does nothing with windows but handles focus order """ default_float_rules: list[_Match] = [ Match(wm_type="utility"), Match(wm_type="notification"), Match(wm_type="toolbar"), Match(wm_type="splash"), Match(wm_type="dialog"), Match(wm_class="file_progress"), Match(wm_class="confirm"), Match(wm_class="dialog"), Match(wm_class="download"), Match(wm_class="error"), Match(wm_class="notification"), Match(wm_class="splash"), Match(wm_class="toolbar"), Match(func=lambda c: c.has_fixed_size()), Match(func=lambda c: c.has_fixed_ratio()), ] defaults = [ ("border_focus", "#0000ff", "Border colour(s) for the focused window."), ("border_normal", "#000000", "Border colour(s) for un-focused windows."), ("border_width", 1, "Border width."), ("max_border_width", 0, "Border width for maximize."), ("fullscreen_border_width", 0, "Border width for fullscreen."), ] def __init__( self, float_rules: list[_Match] | None = None, no_reposition_rules=None, **config ): """ If you have certain apps that you always want to float you can provide ``float_rules`` to do so. ``float_rules`` are a list of Match objects:: from libqtile.config import Match Match(title=WM_NAME, wm_class=WM_CLASS, role=WM_WINDOW_ROLE) When a new window is opened its ``match`` method is called with each of these rules. If one matches, the window will float. The following will float GIMP and Skype:: from libqtile.config import Match float_rules=[Match(wm_class="skype"), Match(wm_class="gimp")] The following ``Match`` will float all windows that are transient windows for a parent window: Match(func=lambda c: bool(c.is_transient_for())) Specify these in the ``floating_layout`` in your config. Floating layout will try to center most of floating windows by default, but if you don't want this to happen for certain windows that are centered by mistake, you can use ``no_reposition_rules`` option to specify them and layout will rely on windows to position themselves in correct location on the screen. """ Layout.__init__(self, **config) self.clients: list[Window] = [] self.focused: Window | None = None if float_rules is None: float_rules = self.default_float_rules self.float_rules = float_rules self.no_reposition_rules = no_reposition_rules or [] self.add_defaults(Floating.defaults) def match(self, win): """Used to default float some windows""" return any(win.match(rule) for rule in self.float_rules) def find_clients(self, group): """Find all clients belonging to a given group""" return [c for c in self.clients if c.group is group] def to_screen(self, group, new_screen): """Adjust offsets of clients within current screen""" for win in self.find_clients(group): if win.maximized: win.maximized = True elif win.fullscreen: win.fullscreen = True else: # If the window hasn't been floated before, it will be configured in # .configure() if win.float_x is not None and win.float_y is not None: # By default, place window at same offset from top corner new_x = new_screen.x + win.float_x new_y = new_screen.y + win.float_y # make sure window isn't off screen left/right... new_x = min(new_x, new_screen.x + new_screen.width - win.width) new_x = max(new_x, new_screen.x) # and up/down new_y = min(new_y, new_screen.y + new_screen.height - win.height) new_y = max(new_y, new_screen.y) win.x = new_x win.y = new_y win.group = new_screen.group def focus_first(self, group=None): if group is None: clients = self.clients else: clients = self.find_clients(group) if clients: return clients[0] def focus_next(self, win: Window) -> Window | None: if win not in self.clients or win.group is None: return None clients = self.find_clients(win.group) idx = clients.index(win) if len(clients) > idx + 1: return clients[idx + 1] return None def focus_last(self, group=None): if group is None: clients = self.clients else: clients = self.find_clients(group) if clients: return clients[-1] def focus_previous(self, win): if win not in self.clients or win.group is None: return clients = self.find_clients(win.group) idx = clients.index(win) if idx > 0: return clients[idx - 1] def focus(self, client: Window) -> None: self.focused = client def blur(self) -> None: self.focused = None def on_screen(self, client, screen_rect): if client.x < screen_rect.x: # client's left edge return False if screen_rect.x + screen_rect.width < client.x + client.width: # right return False if client.y < screen_rect.y: # top return False if screen_rect.y + screen_rect.width < client.y + client.height: # bottom return False return True def compute_client_position(self, client, screen_rect): """recompute client.x and client.y, returning whether or not to place this client above other windows or not""" above = True if client.has_user_set_position() and not self.on_screen(client, screen_rect): # move to screen client.x = screen_rect.x + client.x client.y = screen_rect.y + client.y if not client.has_user_set_position() or not self.on_screen(client, screen_rect): # client has not been properly placed before or it is off screen transient_for = client.is_transient_for() if transient_for is not None: # if transient for a window, place in the center of the window center_x = transient_for.x + transient_for.width / 2 center_y = transient_for.y + transient_for.height / 2 above = False else: center_x = screen_rect.x + screen_rect.width / 2 center_y = screen_rect.y + screen_rect.height / 2 x = center_x - client.width / 2 y = center_y - client.height / 2 # don't go off the right... x = min(x, screen_rect.x + screen_rect.width - client.width) # or left... x = max(x, screen_rect.x) # or bottom... y = min(y, screen_rect.y + screen_rect.height - client.height) # or top y = max(y, screen_rect.y) client.x = int(round(x)) client.y = int(round(y)) return above def configure(self, client: Window, screen_rect: ScreenRect) -> None: if client.has_focus: bc = self.border_focus else: bc = self.border_normal if client.maximized: bw = self.max_border_width elif client.fullscreen: bw = self.fullscreen_border_width else: bw = self.border_width # 'sun-awt-X11-XWindowPeer' is a dropdown used in Java application, # don't reposition it anywhere, let Java app to control it cls = client.get_wm_class() or "" is_java_dropdown = "sun-awt-X11-XWindowPeer" in cls if is_java_dropdown: client.paint_borders(bc, bw) client.bring_to_front() # alternatively, users may have asked us explicitly to leave the client alone elif any(m.compare(client) for m in self.no_reposition_rules): client.paint_borders(bc, bw) client.bring_to_front() else: above = False # We definitely have a screen here, so let's be sure we'll float on screen if client.float_x is None or client.float_y is None: # this window hasn't been placed before, let's put it in a sensible spot above = self.compute_client_position(client, screen_rect) client.place( client.x, client.y, client.width, client.height, bw, bc, above, respect_hints=True, ) client.unhide() def add_client(self, client: Window) -> None: self.clients.append(client) self.focused = client def remove(self, client: Window) -> Window | None: if client not in self.clients: return None next_focus = self.focus_next(client) if client is self.focused: self.blur() self.clients.remove(client) return next_focus def get_windows(self): return self.clients @expose_command() def info(self) -> dict[str, Any]: d = dict( name=self.name, clients=[c.name for c in self.clients], ) return d @expose_command() def next(self) -> None: # This can't ever be called, but implement the abstract method pass @expose_command() def previous(self) -> None: # This can't ever be called, but implement the abstract method pass qtile-0.31.0/libqtile/layout/verticaltile.py0000664000175000017500000002123414762660347021023 0ustar epsilonepsilon# Copyright (c) 2014, Florian Scherf . All rights reserved. # Copyright (c) 2017, Dirk Hartmann. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations from typing import TYPE_CHECKING from libqtile.command.base import expose_command from libqtile.layout.base import _SimpleLayoutBase if TYPE_CHECKING: from typing import Self from libqtile.backend.base import Window from libqtile.group import _Group class VerticalTile(_SimpleLayoutBase): """Tiling layout that works nice on vertically mounted monitors The available height gets divided by the number of panes, if no pane is maximized. If one pane has been maximized, the available height gets split in master and secondary area. The maximized pane (master pane) gets the full height of the master area and the other panes (secondary panes) share the remaining space. The master area (at default 75%) can grow and shrink via keybindings. :: ----------------- ----------------- --- | | | | | | 1 | <-- Panes | | | | | | | | | |---------------| | | | | | | | | | | | 2 | <-----+ | 1 | | Master Area | | | | | | |---------------| | | | | | | | | | | | 3 | <-----+ | | | | | | | | | |---------------| | |---------------| --- | | | | 2 | | | 4 | <-----+ |---------------| | Secondary Area | | | 3 | | ----------------- ----------------- --- Normal behavior. One maximized pane in the master area No maximized pane. and two secondary panes in the No specific areas. secondary area. :: ----------------------------------- In some cases, VerticalTile can be | | useful on horizontal mounted | 1 | monitors too. | | For example, if you want to have a |---------------------------------| web browser and a shell below it. | | | 2 | | | ----------------------------------- Suggested keybindings: :: Key([modkey], 'j', lazy.layout.down()), Key([modkey], 'k', lazy.layout.up()), Key([modkey], 'Tab', lazy.layout.next()), Key([modkey, 'shift'], 'Tab', lazy.layout.next()), Key([modkey, 'shift'], 'j', lazy.layout.shuffle_down()), Key([modkey, 'shift'], 'k', lazy.layout.shuffle_up()), Key([modkey], 'm', lazy.layout.maximize()), Key([modkey], 'n', lazy.layout.normalize()), """ defaults = [ ("border_focus", "#FF0000", "Border color(s) for the focused window."), ("border_normal", "#FFFFFF", "Border color(s) for un-focused windows."), ("border_width", 1, "Border width."), ("single_border_width", None, "Border width for single window."), ("single_margin", None, "Margin for single window."), ("margin", 0, "Border margin (int or list of ints [N E S W])."), ] ratio = 0.75 steps = 0.05 def __init__(self, **config): _SimpleLayoutBase.__init__(self, **config) self.add_defaults(VerticalTile.defaults) if self.single_border_width is None: self.single_border_width = self.border_width if self.single_margin is None: self.single_margin = self.margin self.maximized = None def add_client(self, window): return self.clients.add_client(window, 1) def remove(self, window: Window) -> Window | None: if self.maximized is window: self.maximized = None return self.clients.remove(window) def clone(self, group: _Group) -> Self: c = _SimpleLayoutBase.clone(self, group) c.maximized = None return c def configure(self, window, screen_rect): if self.clients and window in self.clients: n = len(self.clients) index = self.clients.index(window) # border border_width = self.border_width if n > 1 else self.single_border_width border_color = self.border_focus if window.has_focus else self.border_normal # margin margin = self.margin if n > 1 else self.single_margin # width width = screen_rect.width - border_width * 2 # y y = screen_rect.y # height if n > 1: main_area_height = int(screen_rect.height * self.ratio) sec_area_height = screen_rect.height - main_area_height main_pane_height = main_area_height - border_width * 2 sec_pane_height = sec_area_height // (n - 1) - border_width * 2 normal_pane_height = (screen_rect.height // n) - (border_width * 2) if self.maximized: y += (index * sec_pane_height) + (border_width * 2 * index) if window is self.maximized: height = main_pane_height else: height = sec_pane_height if index > self.clients.index(self.maximized): y = y - sec_pane_height + main_pane_height else: height = normal_pane_height y += (index * normal_pane_height) + (border_width * 2 * index) else: height = screen_rect.height - 2 * border_width window.place( screen_rect.x, y, width, height, border_width, border_color, margin=margin ) window.unhide() else: window.hide() def _grow(self): if self.ratio + self.steps < 1: self.ratio += self.steps self.group.layout_all() def _shrink(self): if self.ratio - self.steps > 0: self.ratio -= self.steps self.group.layout_all() @expose_command("up") def previous(self) -> None: _SimpleLayoutBase.previous(self) @expose_command("down") def next(self) -> None: _SimpleLayoutBase.next(self) @expose_command() def shuffle_up(self): self.clients.shuffle_up() self.group.layout_all() @expose_command() def shuffle_down(self): self.clients.shuffle_down() self.group.layout_all() @expose_command() def maximize(self): if self.clients: self.maximized = self.clients.current_client self.group.layout_all() @expose_command() def normalize(self): self.maximized = None self.group.layout_all() @expose_command() def grow(self): if not self.maximized: return if self.clients.current_client is self.maximized: self._grow() else: self._shrink() @expose_command() def shrink(self): if not self.maximized: return if self.clients.current_client is self.maximized: self._shrink() else: self._grow() qtile-0.31.0/libqtile/layout/xmonad.py0000664000175000017500000015265214762660347017633 0ustar epsilonepsilon# Copyright (c) 2011-2012 Dustin Lacewell # Copyright (c) 2011 Mounier Florian # Copyright (c) 2012 Craig Barnes # Copyright (c) 2012 Maximilian Köhl # Copyright (c) 2012, 2014-2015 Tycho Andersen # Copyright (c) 2013 jpic # Copyright (c) 2013 babadoo # Copyright (c) 2013 Jure Ham # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014 ramnes # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 dmpayton # Copyright (c) 2014 dequis # Copyright (c) 2014 Florian Scherf # Copyright (c) 2017 Dirk Hartmann # Copyright (c) 2024 Marco Paganini (auto_maximization code). # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations import math from collections import namedtuple from typing import TYPE_CHECKING from libqtile.command.base import expose_command from libqtile.layout.base import _SimpleLayoutBase if TYPE_CHECKING: from typing import Any, Self from libqtile.backend.base import Window from libqtile.config import ScreenRect from libqtile.group import _Group class MonadTall(_SimpleLayoutBase): """Emulate the behavior of XMonad's default tiling scheme. Main-Pane: A main pane that contains a single window takes up a vertical portion of the screen_rect based on the ratio setting. This ratio can be adjusted with the ``grow_main`` and ``shrink_main`` or, while the main pane is in focus, ``grow`` and ``shrink``. You may also set the ratio directly with ``set_ratio``. :: --------------------- | | | | | | | | | | | | | | | | | | --------------------- Using the ``flip`` method will switch which horizontal side the main pane will occupy. The main pane is considered the "top" of the stack. :: --------------------- | | | | | | | | | | | | | | | | | | --------------------- Secondary-panes: Occupying the rest of the screen_rect are one or more secondary panes. The secondary panes will share the vertical space of the screen_rect however they can be resized at will with the ``grow`` and ``shrink`` methods. The other secondary panes will adjust their sizes to smoothly fill all of the space. :: --------------------- --------------------- | | | | |______| | |______| | | | | | | | | | | |______| | | | | | | | |______| | | | | | | --------------------- --------------------- Panes can be moved with the ``shuffle_up`` and ``shuffle_down`` methods. As mentioned the main pane is considered the top of the stack; moving up is counter-clockwise and moving down is clockwise. :: --------------------- --------------------- | | | | | | | |______| | |Focus | | | | | | | | |______| | |______| | | | | |______| | | | | | | --------------------- --------------------- Setting ``auto_maximize`` will cause the focused secondary pane to be automatically maximized on focus. The non-maximized panes will shrink to the height specified by ``min_secondary_size``. The opposite is true if the layout is "flipped". :: --------------------- --------------------- | | 2 | | 2 | | | |______| |_______| | | | 3 | | 3 | | | 1 |______| |_______| 1 | | | 4 | | 4 | | | | | | | | --------------------- --------------------- Normalizing/Resetting: To restore all secondary client windows to their default size ratios use the ``normalize`` method. To reset all client windows to their default sizes, including the primary window, use the ``reset`` method. Maximizing: To toggle a client window between its minimum and maximum sizes simply use the ``maximize`` on a focused client. Suggested Bindings:: Key([modkey], "h", lazy.layout.left()), Key([modkey], "l", lazy.layout.right()), Key([modkey], "j", lazy.layout.down()), Key([modkey], "k", lazy.layout.up()), Key([modkey, "shift"], "h", lazy.layout.swap_left()), Key([modkey, "shift"], "l", lazy.layout.swap_right()), Key([modkey, "shift"], "j", lazy.layout.shuffle_down()), Key([modkey, "shift"], "k", lazy.layout.shuffle_up()), Key([modkey], "i", lazy.layout.grow()), Key([modkey], "m", lazy.layout.shrink()), Key([modkey], "n", lazy.layout.reset()), Key([modkey, "shift"], "n", lazy.layout.normalize()), Key([modkey], "o", lazy.layout.maximize()), Key([modkey, "shift"], "s", lazy.layout.toggle_auto_maximize()), Key([modkey, "shift"], "space", lazy.layout.flip()), """ _left = 0 _right = 1 defaults = [ ("auto_maximize", False, "Maximize secondary windows on focus."), ("border_focus", "#ff0000", "Border colour(s) for the focused window."), ("border_normal", "#000000", "Border colour(s) for un-focused windows."), ("border_width", 2, "Border width."), ("single_border_width", None, "Border width for single window"), ("single_margin", None, "Margin size for single window"), ("margin", 0, "Margin of the layout"), ( "ratio", 0.5, "The percent of the screen-space the master pane should occupy " "by default.", ), ( "min_ratio", 0.25, "The percent of the screen-space the master pane should occupy " "at minimum.", ), ( "max_ratio", 0.75, "The percent of the screen-space the master pane should occupy " "at maximum.", ), ("min_secondary_size", 85, "minimum size in pixel for a secondary pane window "), ( "align", _left, "Which side master plane will be placed " "(one of ``MonadTall._left`` or ``MonadTall._right``)", ), ("change_ratio", 0.05, "Resize ratio"), ("change_size", 20, "Resize change in pixels"), ( "new_client_position", "after_current", "Place new windows: " " after_current - after the active window." " before_current - before the active window," " top - at the top of the stack," " bottom - at the bottom of the stack,", ), ] def __init__(self, **config): _SimpleLayoutBase.__init__(self, **config) self.add_defaults(MonadTall.defaults) if self.single_border_width is None: self.single_border_width = self.border_width if self.single_margin is None: self.single_margin = self.margin self.relative_sizes = [] self._screen_rect = None self.default_ratio = self.ratio # screen_rect is a property as the MonadThreeCol layout needs to perform # additional actions when the attribute is modified @property def screen_rect(self): return self._screen_rect @screen_rect.setter def screen_rect(self, value): self._screen_rect = value @property def focused(self): return self.clients.current_index def _get_relative_size_from_absolute(self, absolute_size): return absolute_size / self.screen_rect.height def _get_absolute_size_from_relative(self, relative_size): return int(relative_size * self.screen_rect.height) def clone(self, group: _Group) -> Self: "Clone layout for other groups" c = _SimpleLayoutBase.clone(self, group) c.relative_sizes = [] c.screen_rect = group.screen.get_rect() if group.screen else None c.ratio = self.ratio c.align = self.align return c def add_client(self, client: Window) -> None: # type: ignore[override] "Add client to layout" self.clients.add_client(client, client_position=self.new_client_position) self.do_normalize = True def focus(self, client): super().focus(client) # Only maximize the window in the secondary pane when focus is *not* in # the main pane. Doing so in the main pane causes the last secondary # window to always be in focus when switching from secondary -> main. if self.focused != 0: self._maximize_focused_secondary() def remove(self, client: Window) -> Window | None: "Remove client from layout" p = super().remove(client) self.do_normalize = True # When auto_maximize is set and the user closes the first (topmost) # secondary window, focus goes back to the main window. In this case, # we WANT to force redraw of the windows in the secondary pane so we # get a maximized topmost window again. if self.auto_maximize and self.focused == 0 and len(self.clients) > 2: # This will also trigger secondary maximization, if needed. self.focus(self.clients[1]) return p @expose_command() def set_ratio(self, ratio): "Directly set the main pane ratio" ratio = min(self.max_ratio, ratio) self.ratio = max(self.min_ratio, ratio) self.group.layout_all() @expose_command() def normalize(self, redraw=True): "Evenly distribute screen-space among secondary clients" n = len(self.clients) - 1 # exclude main client, 0 # if secondary clients exist if n > 0 and self.screen_rect is not None: self.relative_sizes = [1.0 / n] * n # reset main pane ratio if redraw: self.group.layout_all() self.do_normalize = False @expose_command() def reset(self, ratio=None, redraw=True): "Reset Layout." self.ratio = ratio or self.default_ratio if self.align == self._right: self.align = self._left self.normalize(redraw) @expose_command() def toggle_auto_maximize(self): "Toggle auto maximize secondary window on focus." self.auto_maximize = not self.auto_maximize self.normalize(True) if self.focused != 0: self._maximize_focused_secondary() def _maximize_main(self): "Toggle the main pane between min and max size" if self.ratio <= 0.5 * (self.max_ratio + self.min_ratio): self.ratio = self.max_ratio else: self.ratio = self.min_ratio self.group.layout_all() def _maximize_secondary(self): "Toggle the focused secondary pane between min and max size" n = len(self.clients) - 2 # total shrinking clients # total size of collapsed secondaries collapsed_size = self.min_secondary_size * n nidx = self.focused - 1 # focused size index # total height of maximized secondary maxed_size = self.group.screen.dheight - collapsed_size # if maximized or nearly maximized if ( abs(self._get_absolute_size_from_relative(self.relative_sizes[nidx]) - maxed_size) < self.change_size ): # minimize self._shrink_secondary( self._get_absolute_size_from_relative(self.relative_sizes[nidx]) - self.min_secondary_size ) # otherwise maximize else: self._grow_secondary(maxed_size) def _maximize_focused_secondary(self): "Maximize the 'non-maximized' focused secondary pane" # Return immediately if no self.group.screen # (this may happen when moving windows across screens.) if self.group.screen is None: return # If auto_maximize is off, return immediately. if not self.auto_maximize: return # if we have 1 or 2 panes, do nothing. if len(self.clients) < 3: return # Recalculate relative_sizes self.normalize(redraw=False) if len(self.relative_sizes) == 0: return # If the focused window (self.focused) is 0 (main pane), adjust # focused to work directly on the secondary pane windows. focused = max(1, self.focused) n = len(self.clients) - 2 # total shrinking clients # total size of collapsed secondaries collapsed_size = self.min_secondary_size * n nidx = max(0, focused - 1) # focused size index # total height of maximized secondary maxed_size = self.group.screen.dheight - collapsed_size # Maximize if window is not already maximized. if ( abs(self._get_absolute_size_from_relative(self.relative_sizes[nidx]) - maxed_size) >= self.change_size ): self._grow_secondary(maxed_size) self.group.layout_all() @expose_command() def maximize(self): "Grow the currently focused client to the max size" # if we have 1 or 2 panes or main pane is focused if len(self.clients) < 3 or self.focused == 0: self._maximize_main() # secondary is focused else: self._maximize_secondary() self.group.layout_all() def configure(self, client: Window, screen_rect: ScreenRect) -> None: "Position client based on order and sizes" self.screen_rect = screen_rect # if no sizes or normalize flag is set, normalize if not self.relative_sizes or self.do_normalize: self.normalize(False) # if client not in this layout if not self.clients or client not in self.clients: client.hide() return # determine focus border-color if client.has_focus: px = self.border_focus else: px = self.border_normal # single client - fullscreen if len(self.clients) == 1: client.place( self.screen_rect.x, self.screen_rect.y, self.screen_rect.width - 2 * self.single_border_width, self.screen_rect.height - 2 * self.single_border_width, self.single_border_width, px, margin=self.single_margin, ) client.unhide() return cidx = self.clients.index(client) self._configure_specific(client, screen_rect, px, cidx) client.unhide() def _configure_specific(self, client, screen_rect, px, cidx): """Specific configuration for xmonad tall.""" self.screen_rect = screen_rect # calculate main/secondary pane size width_main = int(self.screen_rect.width * self.ratio) width_shared = self.screen_rect.width - width_main # calculate client's x offset if self.align == self._left: # left or up orientation if cidx == 0: # main client xpos = self.screen_rect.x else: # secondary client xpos = self.screen_rect.x + width_main else: # right or down orientation if cidx == 0: # main client xpos = self.screen_rect.x + width_shared - self.margin else: # secondary client xpos = self.screen_rect.x # calculate client height and place if cidx > 0: # secondary client width = width_shared - 2 * self.border_width # ypos is the sum of all clients above it ypos = self.screen_rect.y + self._get_absolute_size_from_relative( sum(self.relative_sizes[: cidx - 1]) ) # get height from precalculated height list height = self._get_absolute_size_from_relative(self.relative_sizes[cidx - 1]) # fix double margin if cidx > 1: ypos -= self.margin height += self.margin # place client based on calculated dimensions client.place( xpos, ypos, width, height - 2 * self.border_width, self.border_width, px, margin=self.margin, ) else: # main client client.place( xpos, self.screen_rect.y, width_main, self.screen_rect.height, self.border_width, px, margin=[ self.margin, 2 * self.border_width, self.margin + 2 * self.border_width, self.margin, ], ) @expose_command() def info(self) -> dict[str, Any]: d = _SimpleLayoutBase.info(self) d.update( dict( main=d["clients"][0] if self.clients else None, secondary=d["clients"][1::] if self.clients else [], ) ) return d def get_shrink_margin(self, cidx): "Return how many remaining pixels a client can shrink" return max( 0, self._get_absolute_size_from_relative(self.relative_sizes[cidx]) - self.min_secondary_size, ) def _shrink(self, cidx, amt): """Reduce the size of a client Will only shrink the client until it reaches the configured minimum size. Any amount that was prevented in the resize is returned. """ # get max resizable amount margin = self.get_shrink_margin(cidx) if amt > margin: # too much self.relative_sizes[cidx] -= self._get_relative_size_from_absolute(margin) return amt - margin else: self.relative_sizes[cidx] -= self._get_relative_size_from_absolute(amt) return 0 def shrink_up(self, cidx, amt): """Shrink the window up Will shrink all secondary clients above the specified index in order. Each client will attempt to shrink as much as it is able before the next client is resized. Any amount that was unable to be applied to the clients is returned. """ left = amt # track unused shrink amount # for each client before specified index for idx in range(cidx): # shrink by whatever is left-over of original amount left -= left - self._shrink(idx, left) # return unused shrink amount return left def shrink_up_shared(self, cidx, amt): """Shrink the shared space Will shrink all secondary clients above the specified index by an equal share of the provided amount. After applying the shared amount to all affected clients, any amount left over will be applied in a non-equal manner with ``shrink_up``. Any amount that was unable to be applied to the clients is returned. """ # split shrink amount among number of clients per_amt = amt / cidx left = amt # track unused shrink amount # for each client before specified index for idx in range(cidx): # shrink by equal amount and track left-over left -= per_amt - self._shrink(idx, per_amt) # apply non-equal shrinkage secondary pass # in order to use up any left over shrink amounts left = self.shrink_up(cidx, left) # return whatever could not be applied return left def shrink_down(self, cidx, amt): """Shrink current window down Will shrink all secondary clients below the specified index in order. Each client will attempt to shrink as much as it is able before the next client is resized. Any amount that was unable to be applied to the clients is returned. """ left = amt # track unused shrink amount # for each client after specified index for idx in range(cidx + 1, len(self.relative_sizes)): # shrink by current total left-over amount left -= left - self._shrink(idx, left) # return unused shrink amount return left def shrink_down_shared(self, cidx, amt): """Shrink secondary clients Will shrink all secondary clients below the specified index by an equal share of the provided amount. After applying the shared amount to all affected clients, any amount left over will be applied in a non-equal manner with ``shrink_down``. Any amount that was unable to be applied to the clients is returned. """ # split shrink amount among number of clients per_amt = amt / (len(self.relative_sizes) - 1 - cidx) left = amt # track unused shrink amount # for each client after specified index for idx in range(cidx + 1, len(self.relative_sizes)): # shrink by equal amount and track left-over left -= per_amt - self._shrink(idx, per_amt) # apply non-equal shrinkage secondary pass # in order to use up any left over shrink amounts left = self.shrink_down(cidx, left) # return whatever could not be applied return left def _grow_main(self, amt): """Will grow the client that is currently in the main pane""" self.ratio += amt self.ratio = min(self.max_ratio, self.ratio) def _grow_solo_secondary(self, amt): """Will grow the solitary client in the secondary pane""" self.ratio -= amt self.ratio = max(self.min_ratio, self.ratio) def _grow_secondary(self, amt): """Will grow the focused client in the secondary pane""" half_change_size = amt / 2 # track unshrinkable amounts left = amt # first secondary (top) if self.focused == 1: # only shrink downwards left -= amt - self.shrink_down_shared(0, amt) # last secondary (bottom) elif self.focused == len(self.clients) - 1: # only shrink upwards left -= amt - self.shrink_up(len(self.relative_sizes) - 1, amt) # middle secondary else: # get size index idx = self.focused - 1 # shrink up and down left -= half_change_size - self.shrink_up_shared(idx, half_change_size) left -= half_change_size - self.shrink_down_shared(idx, half_change_size) left -= half_change_size - self.shrink_up_shared(idx, half_change_size) left -= half_change_size - self.shrink_down_shared(idx, half_change_size) # calculate how much shrinkage took place diff = amt - left # grow client by diff amount self.relative_sizes[self.focused - 1] += self._get_relative_size_from_absolute(diff) @expose_command() def grow(self): """Grow current window Will grow the currently focused client reducing the size of those around it. Growing will stop when no other secondary clients can reduce their size any further. """ if self.focused == 0: self._grow_main(self.change_ratio) elif len(self.clients) == 2: self._grow_solo_secondary(self.change_ratio) else: self._grow_secondary(self.change_size) self.group.layout_all() @expose_command() def grow_main(self): """Grow main pane Will grow the main pane, reducing the size of clients in the secondary pane. """ self._grow_main(self.change_ratio) self.group.layout_all() @expose_command() def shrink_main(self): """Shrink main pane Will shrink the main pane, increasing the size of clients in the secondary pane. """ self._shrink_main(self.change_ratio) self.group.layout_all() def _grow(self, cidx, amt): "Grow secondary client by specified amount" self.relative_sizes[cidx] += self._get_relative_size_from_absolute(amt) def grow_up_shared(self, cidx, amt): """Grow higher secondary clients Will grow all secondary clients above the specified index by an equal share of the provided amount. """ # split grow amount among number of clients per_amt = amt / cidx for idx in range(cidx): self._grow(idx, per_amt) def grow_down_shared(self, cidx, amt): """Grow lower secondary clients Will grow all secondary clients below the specified index by an equal share of the provided amount. """ # split grow amount among number of clients per_amt = amt / (len(self.relative_sizes) - 1 - cidx) for idx in range(cidx + 1, len(self.relative_sizes)): self._grow(idx, per_amt) def _shrink_main(self, amt): """Will shrink the client that currently in the main pane""" self.ratio -= amt self.ratio = max(self.min_ratio, self.ratio) def _shrink_solo_secondary(self, amt): """Will shrink the solitary client in the secondary pane""" self.ratio += amt self.ratio = min(self.max_ratio, self.ratio) def _shrink_secondary(self, amt): """Will shrink the focused client in the secondary pane""" # get focused client client = self.clients[self.focused] # get default change size change = amt # get left-over height after change left = client.height - amt # if change would violate min_secondary_size if left < self.min_secondary_size: # just reduce to min_secondary_size change = client.height - self.min_secondary_size # calculate half of that change half_change = change / 2 # first secondary (top) if self.focused == 1: # only grow downwards self.grow_down_shared(0, change) # last secondary (bottom) elif self.focused == len(self.clients) - 1: # only grow upwards self.grow_up_shared(len(self.relative_sizes) - 1, change) # middle secondary else: idx = self.focused - 1 # grow up and down self.grow_up_shared(idx, half_change) self.grow_down_shared(idx, half_change) # shrink client by total change self.relative_sizes[self.focused - 1] -= self._get_relative_size_from_absolute(change) @expose_command() def next(self) -> None: _SimpleLayoutBase.next(self) @expose_command() def previous(self) -> None: _SimpleLayoutBase.previous(self) @expose_command() def shrink(self): """Shrink current window Will shrink the currently focused client reducing the size of those around it. Shrinking will stop when the client has reached the minimum size. """ if self.focused == 0: self._shrink_main(self.change_ratio) elif len(self.clients) == 2: self._shrink_solo_secondary(self.change_ratio) else: self._shrink_secondary(self.change_size) self.group.layout_all() @expose_command() def shuffle_up(self): """Shuffle the client up the stack""" self.clients.shuffle_up() self.group.layout_all() self.group.focus(self.clients.current_client) @expose_command() def shuffle_down(self): """Shuffle the client down the stack""" self.clients.shuffle_down() self.group.layout_all() self.group.focus(self.clients[self.focused]) @expose_command() def flip(self): """Flip the layout horizontally""" self.align = self._left if self.align == self._right else self._right self.group.layout_all() def _get_closest(self, x, y, clients): """Get closest window to a point x,y""" target = min( clients, key=lambda c: math.hypot(c.x - x, c.y - y), default=self.clients.current_client, ) return target @expose_command() def swap(self, window1: Window, window2: Window) -> None: """Swap two windows""" _SimpleLayoutBase.swap(self, window1, window2) @expose_command("shuffle_left") def swap_left(self): """Swap current window with closest window to the left""" win = self.clients.current_client x, y = win.x, win.y candidates = [c for c in self.clients if c.info()["x"] < x] target = self._get_closest(x, y, candidates) self.swap(win, target) @expose_command("shuffle_right") def swap_right(self): """Swap current window with closest window to the right""" win = self.clients.current_client x, y = win.x, win.y candidates = [c for c in self.clients if c.info()["x"] > x] target = self._get_closest(x, y, candidates) self.swap(win, target) @expose_command() def swap_main(self): """Swap current window to main pane""" if self.align == self._left: self.swap_left() elif self.align == self._right: self.swap_right() @expose_command() def left(self): """Focus on the closest window to the left of the current window""" win = self.clients.current_client x, y = win.x, win.y candidates = [c for c in self.clients if c.info()["x"] < x] self.clients.current_client = self._get_closest(x, y, candidates) self.group.focus(self.clients.current_client) @expose_command() def right(self): """Focus on the closest window to the right of the current window""" win = self.clients.current_client x, y = win.x, win.y candidates = [c for c in self.clients if c.info()["x"] > x] self.clients.current_client = self._get_closest(x, y, candidates) self.group.focus(self.clients.current_client) @expose_command() def up(self): """Focus on the closest window above the current window""" self.previous() @expose_command() def down(self): """Focus on the closest window below the current window""" self.next() class MonadWide(MonadTall): """Emulate the behavior of XMonad's horizontal tiling scheme. This layout attempts to emulate the behavior of XMonad wide tiling scheme. Main-Pane: A main pane that contains a single window takes up a horizontal portion of the screen_rect based on the ratio setting. This ratio can be adjusted with the ``grow_main`` and ``shrink_main`` or, while the main pane is in focus, ``grow`` and ``shrink``. :: --------------------- | | | | | | |___________________| | | | | --------------------- Using the ``flip`` method will switch which vertical side the main pane will occupy. The main pane is considered the "top" of the stack. :: --------------------- | | |___________________| | | | | | | | | --------------------- Secondary-panes: Occupying the rest of the screen_rect are one or more secondary panes. The secondary panes will share the horizontal space of the screen_rect however they can be resized at will with the ``grow`` and ``shrink`` methods. The other secondary panes will adjust their sizes to smoothly fill all of the space. :: --------------------- --------------------- | | | | | | | | | | | | |___________________| |___________________| | | | | | | | | | | | | | | | | --------------------- --------------------- Panes can be moved with the ``shuffle_up`` and ``shuffle_down`` methods. As mentioned the main pane is considered the top of the stack; moving up is counter-clockwise and moving down is clockwise. The opposite is true if the layout is "flipped". :: --------------------- --------------------- | | | 2 | 3 | 4 | | 1 | |_____|_______|_____| | | | | |___________________| | | | | | | | 1 | | 2 | 3 | 4 | | | --------------------- --------------------- Normalizing/Resetting: To restore all secondary client windows to their default size ratios use the ``normalize`` method. To reset all client windows to their default sizes, including the primary window, use the ``reset`` method. Maximizing: To toggle a client window between its minimum and maximum sizes simply use the ``maximize`` on a focused client. Suggested Bindings:: Key([modkey], "h", lazy.layout.left()), Key([modkey], "l", lazy.layout.right()), Key([modkey], "j", lazy.layout.down()), Key([modkey], "k", lazy.layout.up()), Key([modkey, "shift"], "h", lazy.layout.swap_left()), Key([modkey, "shift"], "l", lazy.layout.swap_right()), Key([modkey, "shift"], "j", lazy.layout.shuffle_down()), Key([modkey, "shift"], "k", lazy.layout.shuffle_up()), Key([modkey], "i", lazy.layout.grow()), Key([modkey], "m", lazy.layout.shrink()), Key([modkey], "n", lazy.layout.reset()), Key([modkey, "shift"], "n", lazy.layout.normalize()), Key([modkey], "o", lazy.layout.maximize()), Key([modkey, "shift"], "space", lazy.layout.flip()), """ _up = 0 _down = 1 def _get_relative_size_from_absolute(self, absolute_size): return absolute_size / self.screen_rect.width def _get_absolute_size_from_relative(self, relative_size): return int(relative_size * self.screen_rect.width) def _maximize_secondary(self): """Toggle the focused secondary pane between min and max size.""" n = len(self.clients) - 2 # total shrinking clients # total size of collapsed secondaries collapsed_size = self.min_secondary_size * n nidx = self.focused - 1 # focused size index # total width of maximized secondary maxed_size = self.screen_rect.width - collapsed_size # if maximized or nearly maximized if ( abs(self._get_absolute_size_from_relative(self.relative_sizes[nidx]) - maxed_size) < self.change_size ): # minimize self._shrink_secondary( self._get_absolute_size_from_relative(self.relative_sizes[nidx]) - self.min_secondary_size ) # otherwise maximize else: self._grow_secondary(maxed_size) def _configure_specific(self, client, screen_rect, px, cidx): """Specific configuration for xmonad wide.""" self.screen_rect = screen_rect # calculate main/secondary column widths height_main = int(self.screen_rect.height * self.ratio) height_shared = self.screen_rect.height - height_main # calculate client's x offset if self.align == self._up: # up orientation if cidx == 0: # main client ypos = self.screen_rect.y else: # secondary client ypos = self.screen_rect.y + height_main else: # right or down orientation if cidx == 0: # main client ypos = self.screen_rect.y + height_shared - self.margin else: # secondary client ypos = self.screen_rect.y # calculate client height and place if cidx > 0: # secondary client height = height_shared - 2 * self.border_width # xpos is the sum of all clients left of it xpos = self.screen_rect.x + self._get_absolute_size_from_relative( sum(self.relative_sizes[: cidx - 1]) ) # get width from precalculated width list width = self._get_absolute_size_from_relative(self.relative_sizes[cidx - 1]) # fix double margin if cidx > 1: xpos -= self.margin width += self.margin # place client based on calculated dimensions client.place( xpos, ypos, width - 2 * self.border_width, height, self.border_width, px, margin=self.margin, ) else: # main client client.place( self.screen_rect.x, ypos, self.screen_rect.width, height_main, self.border_width, px, margin=[ self.margin, self.margin + 2 * self.border_width, 2 * self.border_width, self.margin, ], ) def _shrink_secondary(self, amt): """Will shrink the focused client in the secondary pane""" # get focused client client = self.clients[self.focused] # get default change size change = amt # get left-over height after change left = client.width - amt # if change would violate min_secondary_size if left < self.min_secondary_size: # just reduce to min_secondary_size change = client.width - self.min_secondary_size # calculate half of that change half_change = change / 2 # first secondary (top) if self.focused == 1: # only grow downwards self.grow_down_shared(0, change) # last secondary (bottom) elif self.focused == len(self.clients) - 1: # only grow upwards self.grow_up_shared(len(self.relative_sizes) - 1, change) # middle secondary else: idx = self.focused - 1 # grow up and down self.grow_up_shared(idx, half_change) self.grow_down_shared(idx, half_change) # shrink client by total change self.relative_sizes[self.focused - 1] -= self._get_relative_size_from_absolute(change) @expose_command() def swap_left(self): """Swap current window with closest window to the down""" win = self.clients.current_client x, y = win.x, win.y candidates = [c for c in self.clients.clients if c.info()["y"] > y] target = self._get_closest(x, y, candidates) self.swap(win, target) @expose_command() def swap_right(self): """Swap current window with closest window to the up""" win = self.clients.current_client x, y = win.x, win.y candidates = [c for c in self.clients if c.info()["y"] < y] target = self._get_closest(x, y, candidates) self.swap(win, target) @expose_command() def swap_main(self): """Swap current window to main pane""" if self.align == self._up: self.swap_right() elif self.align == self._down: self.swap_left() @expose_command() def left(self): """Focus on the closest window to the left of the current window""" self.previous() @expose_command() def right(self): """Focus on the closest window to the right of the current window""" self.next() @expose_command() def up(self): """Focus on the closest window above the current window""" win = self.clients.current_client x, y = win.x, win.y candidates = [c for c in self.clients if c.info()["y"] < y] self.clients.current_client = self._get_closest(x, y, candidates) self.group.focus(self.clients.current_client) @expose_command() def down(self): """Focus on the closest window below the current window""" win = self.clients.current_client x, y = win.x, win.y candidates = [c for c in self.clients if c.info()["y"] > y] self.clients.current_client = self._get_closest(x, y, candidates) self.group.focus(self.clients.current_client) class MonadThreeCol(MonadTall): """Emulate the behavior of XMonad's ThreeColumns layout. A layout similar to tall but with three columns. With an ultra wide display this layout can be used for a huge main window - ideally at the center of the screen - and up to six reasonable sized secondary windows. Main-Pane: A main pane that contains a single window takes up a vertical portion of the screen_rect based on the ratio setting. This ratio can be adjusted with the ``grow_main`` and ``shrink_main`` or, while the main pane is in focus, ``grow`` and ``shrink``. The main pane can also be centered. :: --------------------------- --------------------------- | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | --------------------------- --------------------------- Secondary-panes: Occupying the rest of the screen_rect are one or more secondary panes. The secondary panes will be divided into two columns and share the vertical space of each column. However they can be resized at will with the ``grow`` and ``shrink`` methods. The other secondary panes will adjust their sizes to smoothly fill all of the space. :: --------------------------- --------------------------- | | | | | |______| | | |______| | | | | | | | |______| | | |______| | |______| | | | | | | | | | | |______| | | | | | | | | | --------------------------- --------------------------- Panes can be moved with the ``shuffle_up`` and ``shuffle_down`` methods. As mentioned the main pane is considered the top of the stack; moving up is counter-clockwise and moving down is clockwise. A secondary pane can also be promoted to the main pane with the ``swap_main`` method. Normalizing/Resetting: To restore all secondary client windows to their default size ratios use the ``normalize`` method. To reset all client windows to their default sizes, including the primary window, use the ``reset`` method. Maximizing: To maximized a client window simply use the ``maximize`` on a focused client. """ defaults = [ ("main_centered", True, "Place the main pane at the center of the screen"), ( "new_client_position", "top", "Place new windows: " " after_current - after the active window." " before_current - before the active window," " top - at the top of the stack," " bottom - at the bottom of the stack,", ), ] __column = namedtuple("__column", "name count start end") def __init__(self, **config): MonadTall.__init__(self, **config) self.add_defaults(MonadThreeCol.defaults) # mypy doesn't like the setter when the getter isn't present # see https://github.com/python/mypy/issues/5936 @MonadTall.screen_rect.setter # type: ignore[attr-defined] def screen_rect(self, value): # If the screen_rect size has change then we need to normalise secondary # windows so they're resized to fill the new space correctly if value != self._screen_rect: self.do_normalize = True self._screen_rect = value def _configure_specific(self, client, screen_rect, border_color, index): """Specific configuration for xmonad three columns.""" if index == 0: self._configure_main(client) elif self._get_column(index - 1).name == "left": self._configure_left(client, index) else: self._configure_right(client, index) def _configure_main(self, client): """Configure the main client""" width = self._get_main_width() height = self.screen_rect.height left = self.screen_rect.x top = self.screen_rect.y if self.main_centered and len(self.clients) > 2: left += (self.screen_rect.width - width) // 2 self._place_client(client, left, top, width, height) def _configure_left(self, client, index): """Configure the left column""" width = self._get_secondary_widths()[0] height = self._get_secondary_height(index) left = self.screen_rect.x top = self.screen_rect.y + self._get_relative_sizes_above(index) if not self.main_centered or len(self.clients) == 2: left += self._get_main_width() self._place_client(client, left, top, width, height) def _configure_right(self, client, index): """Configure the right column""" widths = self._get_secondary_widths() height = self._get_secondary_height(index) left = self.screen_rect.x + widths[0] + self._get_main_width() top = self.screen_rect.y + self._get_relative_sizes_above(index) self._place_client(client, left, top, widths[1], height) def _get_main_width(self): """Calculate the main client's width""" return int(self.screen_rect.width * self.ratio) def _get_secondary_widths(self): """Calculate secondary clients' widths""" width = self.screen_rect.width - self._get_main_width() if len(self.clients) == 2: return [width, 0] return self._split_integer(width, 2) def _get_secondary_height(self, index): """Return the height of the provided index""" return self.relative_sizes[index - 1] def _get_relative_sizes_above(self, index): """Return the sum of the heights of all clients above the provided index""" column = self._get_column(index - 1) return sum(self.relative_sizes[column.start : index - 1]) def _place_client(self, client, left, top, width, height): """Place a client on the screen Will prevent double margins by applying east and south margins only when the client is the rightmost or the bottommost window. """ # Create a temporary margin list for the client if isinstance(self.margin, int): margin = [self.margin] * 4 else: # We need to copy this list otherwise we'd be modifying self.margin! margin = self.margin.copy() rightmost = left + width - self.screen_rect.x + margin[1] >= self.screen_rect.width bottommost = top + height - self.screen_rect.y + margin[2] >= self.screen_rect.height if not rightmost: margin[1] = 0 if not bottommost: margin[2] = 0 client.place( left, top, width - 2 * self.border_width, height - 2 * self.border_width, self.border_width, self.border_focus if client.has_focus else self.border_normal, margin=margin, ) @expose_command() def normalize(self, redraw=True): """Evenly distribute screen-space among secondary clients""" if self.screen_rect is not None: self.relative_sizes = [] height = self.screen_rect.height left, right = self._get_columns() if left.count > 0: self.relative_sizes += self._split_integer(height, left.count) if right.count > 0: self.relative_sizes += self._split_integer(height, right.count) if redraw: self.group.layout_all() self.do_normalize = False @expose_command() def swap_main(self): """Swap current window to main pane""" self.swap(self.clients.current_client, self.clients[0]) def _maximize_secondary(self): """Maximize the focused secondary pane""" focused = self.focused - 1 column = self._get_column(focused) if column.count == 1: return max_height = self.screen_rect.height - ((column.count - 1) * self.min_secondary_size) for i in range(column.start, column.end): self.relative_sizes[i] = max_height if i == focused else self.min_secondary_size def _grow_secondary(self, amt): """Grow the focused client in the secondary pane""" self._resize_secondary(amt) def _shrink_secondary(self, amt): """Shrink the focused client in the secondary pane""" self._resize_secondary(-amt) def _resize_secondary(self, amt): """Resize the focused secondary client If amt is positive, the client will grow. Conversely, if it's negative, the client will shrink. All other clients in the same column will get grown/shrunk so to accommodate the new height. """ focused = self.focused - 1 column = self._get_column(focused) if column.count == 1: return # Resizing is accomplished by doing the following: # - calculate how much each client in the column must shrink/grow # so that the focused window can grow/shrink. # - iterate over all clients in the column and change their height # (grow or shrink) as long as they can still be resized (both main # and secondary windows). min_height = self.min_secondary_size idx = column.start step = amt // (column.count - 1) visited = 0 while amt != 0: if idx != focused: focused_new_height = self.relative_sizes[focused] + step new_height = self.relative_sizes[idx] - step if focused_new_height >= min_height and new_height >= min_height: self.relative_sizes[focused] += step self.relative_sizes[idx] -= step amt -= step visited += 1 idx += 1 if idx == column.end: if visited == 0: break idx = column.start visited = 0 self.group.layout_all() def _get_column(self, index): """Get the column containing the provided index""" left, right = self._get_columns() return left if index < left.count else right def _get_columns(self): """Get all columns""" clients = len(self.clients) - 1 clients = (clients // 2 + clients % 2, clients // 2) return ( MonadThreeCol.__column( name="left", count=clients[0], start=0, end=clients[0], ), MonadThreeCol.__column( name="right", count=clients[1], start=clients[0], end=clients[0] + clients[1], ), ) def info(self) -> dict[str, Any]: left, right = self._get_columns() d = MonadTall.info(self) d.update( secondary=dict( left=d["clients"][1 : left.end + 1] if left.count > 0 else [], right=d["clients"][right.start + 1 :] if right.count > 0 else [], ) ) return d @staticmethod def _split_integer(value, parts): """Divide an integer into equal parts and distribute the remainder""" result = [value // parts] * parts for i in range(value % parts): result[i] += 1 return result qtile-0.31.0/libqtile/hook.py0000664000175000017500000006671114762660347015770 0ustar epsilonepsilon# Copyright (c) 2009-2010 Aldo Cortesi # Copyright (c) 2010 Lee McCuller # Copyright (c) 2010 matt # Copyright (c) 2010, 2014 dequis # Copyright (c) 2010, 2012, 2014 roger # Copyright (c) 2011 Florian Mounier # Copyright (c) 2011 Kenji_Takahashi # Copyright (c) 2011 Paul Colomiets # Copyright (c) 2011 Tzbob # Copyright (c) 2012-2015 Tycho Andersen # Copyright (c) 2012 Craig Barnes # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014 Sean Vig # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations import asyncio import contextlib from typing import TYPE_CHECKING from libqtile import backend, utils from libqtile.log_utils import logger from libqtile.resources.sleep import inhibitor if TYPE_CHECKING: from collections.abc import Callable HookHandler = Callable[[Callable], Callable] subscriptions = {} # type: dict def clear(): subscriptions.clear() def _fire_async_event(co): from libqtile.utils import create_task loop = None with contextlib.suppress(RuntimeError): loop = asyncio.get_running_loop() if loop is None: asyncio.run(co) else: create_task(co) # Custom hook functions receive a single argument, "self", which will refer to the # Subscribe/Unsubscribe classes. def _resume_func(self): def f(func): inhibitor.want_resume() return self._subscribe("resume", func) return f def _suspend_func(self): def f(func): inhibitor.want_sleep() return self._subscribe("suspend", func) return f def _user_hook_func(self): def wrapper(hook_name): def f(func): name = f"user_{hook_name}" if name not in self.hooks: self.hooks[name] = None return self._subscribe(name, func) return f return wrapper class Hook: def __init__(self, name: str, doc: str = "", func: Callable | None = None) -> None: self.name = name self.doc = doc self.func = func class HookHandlerCollection: def __init__(self, registry_name: str, check_name=True): self.hooks: dict[str, HookHandler] = {} if check_name and registry_name in subscriptions: raise NameError("A hook registry already exists with that name: {registry_name}") elif registry_name not in subscriptions: subscriptions[registry_name] = {} self.registry_name = registry_name def __getattr__(self, name: str) -> HookHandler: if name not in self.hooks: raise AttributeError return self.hooks[name] def _register(self, hook: Hook) -> None: def _hook_func(func): return self._subscribe(hook.name, func) hooked = _hook_func if hook.func is None else hook.func(self) hooked.__doc__ = hook.doc self.hooks[hook.name] = hooked class Subscribe(HookHandlerCollection): def _subscribe(self, event: str, func: Callable) -> Callable: registry = subscriptions.setdefault(self.registry_name, dict()) lst = registry.setdefault(event, []) if func not in lst: lst.append(func) return func class Unsubscribe(HookHandlerCollection): """ This class mirrors subscribe, except the _subscribe member has been overridden to remove calls from hooks. """ def _subscribe(self, event: str, func: Callable) -> None: registry = subscriptions.setdefault(self.registry_name, dict()) lst = registry.setdefault(event, []) try: lst.remove(func) except ValueError: logger.warning( f"Tried to unsubscribe a hook ({event}) that was not currently subscribed." ) class Registry: def __init__(self, name: str, hooks: list[Hook] = list()) -> None: self.name = name self.subscribe = Subscribe(name) self.unsubscribe = Unsubscribe(name, check_name=False) for hook in hooks: self.register_hook(hook) def register_hook(self, hook: Hook) -> None: if hook.name in self.subscribe.hooks: raise utils.QtileError( f"Unable to register hook. A hook with that name already exists: {hook.name}" ) logger.debug("Registered new hook: '%s'.", hook.name) self.subscribe._register(hook) self.unsubscribe._register(hook) def fire(self, event, *args, **kwargs): if event not in self.subscribe.hooks: raise utils.QtileError(f"Unknown event: {event}") # Do not fire for Internal windows if any(isinstance(arg, backend.base.window.Internal) for arg in args): return # We should check if the registry name is in the subscriptions dict # A name can disappear if the config is reloaded (which clears subscriptions) # but there are no hook subscriptions. This is not an issue for qtile core but # third party libraries will need this to prevent KeyErrors when firing hooks if self.name not in subscriptions: subscriptions[self.name] = dict() for i in subscriptions[self.name].get(event, []): try: if asyncio.iscoroutinefunction(i): _fire_async_event(i(*args, **kwargs)) elif asyncio.iscoroutine(i): _fire_async_event(i) else: i(*args, **kwargs) except: # noqa: E722 logger.exception("Error in hook %s", event) hooks: list[Hook] = [ Hook( "startup_once", """Called when Qtile has started on first start This hook is called exactly once per session (i.e. not on each ``lazy.restart()``). **Arguments** None Example: .. code:: python import os import subprocess from libqtile import hook @hook.subscribe.startup_once def autostart(): script = os.path.expanduser("~/.config/qtile/autostart.sh") subprocess.run([script]) """, ), Hook( "startup", """ Called when qtile is started. Unlike ``startup_once``, this hook is fired on every start, including restarts. When restarting, this hook is fired after qtile has restarted but before qtile tries to restore the session to the same state that it was in before the restart. **Arguments** None Example: .. code:: python import subprocess from libqtile import hook from libqtile.utils import send_notification @hook.subscribe.startup def run_every_startup(): send_notification("qtile", "Startup") """, ), Hook( "startup_complete", """ Called when qtile is started after all resources initialized. This is the same as ``startup`` with the only difference being that this hook is fired after the saved state has been restored. **Arguments** None Example: .. code:: python import subprocess from libqtile import hook from libqtile.utils import send_notification @hook.subscribe.startup_complete def run_every_startup(): send_notification("qtile", "Startup complete") """, ), Hook( "shutdown", """ Called before qtile is shutdown. Using a long-running command in this function will cause the shutdown to be delayed. This hook is only fired when qtile is shutting down, if you want a command to be run when the system sleeps then you should use the ``suspend`` hook instead. **Arguments** None Example: .. code:: python import os import subprocess from libqtile import hook @hook.subscribe.shutdown def autostart: script = os.path.expanduser("~/.config/qtile/shutdown.sh") subprocess.run([script]) """, ), Hook( "restart", """ Called before qtile is restarted. This hook fires before qtile restarts but after qtile has checked that it is able to restart (i.e. the config file is valid). **Arguments** None Example: .. code:: python from libqtile import hook from libqtile.utils import send_notification @hook.subscribe.restart def run_every_startup(): send_notification("qtile", "Restarting...") """, ), Hook( "setgroup", """ Called when group is put on screen. This hook is fired in 3 situations: 1) When the screen changes to a new group 2) When two groups are switched 3) When a screen is focused **Arguments** None Example: .. code:: python from libqtile import hook from libqtile.utils import send_notification @hook.subscribe.setgroup def setgroup(): send_notification("qtile", "Group set") """, ), Hook( "addgroup", """ Called when a new group is added **Arguments** * name of new group Example: .. code:: python from libqtile import hook from libqtile.utils import send_notification @hook.subscribe.addgroup def group_added(group_name): send_notification("qtile", f"New group added: {group_name}") """, ), Hook( "delgroup", """ Called when group is deleted **Arguments** * name of deleted group Example: .. code:: python from libqtile import hook from libqtile.utils import send_notification @hook.subscribe.delgroup def group_deleted(group_name): send_notification("qtile", f"Group deleted: {group_name}") """, ), Hook( "changegroup", """ Called whenever a group change occurs. The following changes will result in this hook being fired: 1) New group added (unlike ``addgroup``, no group name is passed with this hook) 2) Group deleted (unlike ``delgroup``, no group name is passed with this hook) 3) Groups order is changed 4) Group is renamed **Arguments** None Example: .. code:: python from libqtile import hook from libqtile.utils import send_notification @hook.subscribe.changegroup def change_group(): send_notification("qtile", "Change group event") """, ), Hook( "focus_change", """ Called when focus is changed, including moving focus between groups or when focus is lost completely (i.e. when a window is closed.) **Arguments** None Example: .. code:: python from libqtile import hook from libqtile.utils import send_notification @hook.subscribe.focus_change def focus_changed(): send_notification("qtile", "Focus changed.") """, ), Hook( "float_change", """ Called when a change in float state is made (e.g. toggle floating, minimised and fullscreen states) **Arguments** None Example: .. code:: python from libqtile import hook from libqtile.utils import send_notification @hook.subscribe.float_change def float_change(): send_notification("qtile", "Window float state changed.") """, ), Hook( "group_window_add", """Called when a new window is added to a group **Arguments** * ``Group`` receiving the new window * ``Window`` added to the group Example: .. code:: python from libqtile import hook from libqtile.utils import send_notification @hook.subscribe.group_window_add def group_window_add(group, window): send_notification("qtile", f"Window {window.name} added to {group.name}") """, ), Hook( "group_window_remove", """Called when a window is removed from a group **Arguments** * ``Group`` removing the window * ``Window`` removed from the group Example: .. code:: python from libqtile import hook from libqtile.utils import send_notification @hook.subscribe.group_window_remove def group_window_remove(group, window): send_notification("qtile", f"Window {window.name} removed from {group.name}") """, ), Hook( "client_new", """ Called before Qtile starts managing a new client Use this hook to declare windows static, or add them to a group on startup. This hook is not called for internal windows. **Arguments** * ``Window`` object Example: .. code:: python from libqtile import hook @hook.subscribe.client_new def new_client(client): if client.name == "xterm": client.togroup("a") elif client.name == "dzen": client.static(0) """, ), Hook( "client_managed", """ Called after Qtile starts managing a new client Called after a window is assigned to a group, or when a window is made static. This hook is not called for internal windows. **Arguments** * ``Window`` object of the managed window Example: .. code:: python from libqtile import hook from libqtile.utils import send_notification @hook.subscribe.client_managed def client_managed(client): send_notification("qtile", f"{client.name} has been managed by qtile") """, ), Hook( "client_killed", """ Called after a client has been unmanaged. This hook is not called for internal windows. **Arguments** * ``Window`` object of the killed window. Example: .. code:: python from libqtile import hook from libqtile.utils import send_notification @hook.subscribe.client_killed def client_killed(client): send_notification("qtile", f"{client.name} has been killed") """, ), Hook( "client_focus", """ Called whenever focus moves to a client window **Arguments** * ``Window`` object of the new focus. Example: .. code:: python from libqtile import hook from libqtile.utils import send_notification @hook.subscribe.client_focus def client_focus(client): send_notification("qtile", f"{client.name} has been focused") """, ), Hook( "client_mouse_enter", """ Called when the mouse enters a client **Arguments** * ``Window`` of window entered Example: .. code:: python from libqtile import hook from libqtile.utils import send_notification @hook.subscribe.client_mouse_enter def client_mouse_enter(client): send_notification("qtile", f"Mouse has entered {client.name}") """, ), Hook( "client_name_updated", """ Called when the client name changes **Arguments** * ``Window`` of client with updated name Example: .. code:: python from libqtile import hook from libqtile.utils import send_notification @hook.subscribe.client_name_updated def client_name_updated(client): send_notification( "qtile", f"Client's has been updated to {client.name}" ) """, ), Hook( "client_urgent_hint_changed", """ Called when the client urgent hint changes **Arguments** * ``Window`` of client with hint change Example: .. code:: python from libqtile import hook from libqtile.utils import send_notification @hook.subscribe.client_urgent_hint_changed def client_urgency_change(client): send_notification( "qtile", f"{client.name} has changed its urgency state" ) """, ), Hook( "layout_change", """ Called on layout change event (including when a new group is displayed on the screen) **Arguments** * layout object for new layout * group object on which layout is changed Example: .. code:: python from libqtile import hook from libqtile.utils import send_notification @hook.subscribe.layout_change def layout_change(layout, group): send_notification( "qtile", f"{layout.name} is now on group {group.name}" ) """, ), Hook( "net_wm_icon_change", """ Called on ``_NET_WM_ICON`` change X11 only. Called when a window notifies that it has changed its icon. **Arguments** * ``Window`` of client with changed icon Example: .. code:: python from libqtile import hook from libqtile.utils import send_notification @hook.subscribe.net_wm_icon_change def icon_change(client): send_notification("qtile", f"{client.name} has changed its icon") """, ), Hook( "selection_notify", """ Called on selection notify X11 only. Fired when a selection is made in a window. **Arguments** * name of the selection * dictionary describing selection, containing ``owner`` and ``selection`` as keys The selection owner will typically be ``"PRIMARY"`` when contents is highlighted and ``"CLIPBOARD"`` when contents is actively copied to the clipboard, e.g. with Ctrl + C. Example: .. code:: python from libqtile import hook from libqtile.utils import send_notification @hook.subscribe.selection_notify def selection_notify(name, selection): send_notification( "qtile", f"Window {selection['owner']} has made a selection in the {name} selection." ) """, ), Hook( "selection_change", """ Called on selection change X11 only. Fired when a selection property is changed (e.g. new selection created or existing selection is emptied) **Arguments** * name of the selection * dictionary describing selection, containing ``owner`` and ``selection`` as keys The selection owner will typically be ``"PRIMARY"`` when contents is highlighted and ``"CLIPBOARD"`` when contents is actively copied to the clipboard, e.g. with Ctrl + C. Example: .. code:: python from libqtile import hook from libqtile.utils import send_notification @hook.subscribe.selection_change def selection_change(name, selection): send_notification( "qtile", f"Window {selection['owner']} has changed the {name} selection." ) """, ), Hook( "screen_change", """ Called when the output configuration is changed (e.g. via randr in X11). .. note:: If you have ``reconfigure_screens = True`` in your config then qtile will automatically reconfigure your screens when it detects a change to the screen configuration. This hook is fired *before* that reconfiguration takes place. The ``screens_reconfigured`` hook should be used where you want to trigger an event after the reconfiguration. **Arguments** * ``xproto.randr.ScreenChangeNotify`` event (X11) or None (Wayland). Example: .. code:: python from libqtile import hook from libqtile.utils import send_notification @hook.subscribe.screen_change def screen_change(event): send_notification("qtile", "Screen change detected.") """, ), Hook( "screens_reconfigured", """ Called once ``qtile.reconfigure_screens`` has completed (e.g. if ``reconfigure_screens`` is set to ``True`` in your config). **Arguments** None Example: .. code:: python from libqtile import hook from libqtile.utils import send_notification @hook.subscribe.screens_reconfigured def screen_reconf(): send_notification("qtile", "Screens have been reconfigured.") """, ), Hook( "current_screen_change", """ Called when the current screen (i.e. the screen with focus) changes **Arguments** None Example: .. code:: python from libqtile import hook from libqtile.utils import send_notification @hook.subscribe.current_screen_change def screen_change(): send_notification("qtile", "Current screen change detected.") """, ), Hook( "enter_chord", """ Called when key chord begins Note: if you only want to use this chord to display the chord name then you should use the ``Chord`` widget. **Arguments** * name of chord(mode) Example: .. code:: python from libqtile import hook from libqtile.utils import send_notification @hook.subscribe.enter_chord def enter_chord(chord_name): send_notification("qtile", "Started {chord_name} key chord.") """, ), Hook( "leave_chord", """ Called when key chord ends **Arguments** None Example: .. code:: python from libqtile import hook from libqtile.utils import send_notification @hook.subscribe.leave_chord ded leave_chord(): send_notification("qtile", "Key chord exited") """, ), Hook( "resume", """ Called when system wakes up from sleep, suspend or hibernate. Relies on systemd's inhibitor dbus interface, via the dbus-fast package. Note: the hook is not fired when resuming from shutdown/reboot events. Use the "startup" hooks for those scenarios. **Arguments** None """, _resume_func, ), Hook( "suspend", """ Called when system is about to sleep, suspend or hibernate. Relies on systemd's inhibitor dbus interface, via the dbus-fast package. When this hook is used, qtile will set an inhibitor that prevent the system from sleeping. The inhibitor is removed as soon as your function exits. You should therefore not use long-running code in this function. Please note, this inhibitor will also only delay, not block, the computer's ability to sleep. The default delay is 5 seconds. If your function has not completed within that time, the machine will still sleep (see important note below). You can increase this delay by setting ``InhibitDelayMaxSec`` in ``logind.conf.`` see: https://www.freedesktop.org/software/systemd/man/logind.conf.html In addition, closing a laptop lid will ignore inhibitors by default. You can override this by setting ``LidSwitchIgnoreInhibited=no`` in ``/etc/systemd/logind.conf``. .. important:: The logind service creates an inhibitor by passing a reference to a lock file which must be closed to release the lock. Additional references to the lock may be created if you spawn processes with the ``subprocess`` module and these processes are running when the machine tries to suspend. As a result, it is strongly recommended that you launch any processes with ``qtile.spawn(...)`` as this will not create additional copies of the lock. **Arguments** None Example: .. code:: python from libqtile import hook, qtile @hook.subscribe.suspend def lock_on_sleep(): # Run screen locker qtile.spawn("/path/to/screen_locker") """, _suspend_func, ), Hook( "user", """ Use to create user-defined hooks. The purpose of these hooks is to allow a hook to be fired by an external application. Hooked functions can receive arguments but it is up to the application firing the hook to ensure the correct arguments are passed. No checking will be performed by qtile. Example: .. code:: python from libqtile import hook from libqtile.log_utils import logger @hook.subscribe.user("my_custom_hook") def hooked_function(): logger.warning("Custom hook received.") The external script can then call the hook with the following command: .. code:: qtile cmd-obj -o cmd -f fire_user_hook -a my_custom_hook .. note:: If the script will be run by a different user then you will need to pass the path to the socket file used by the current process. One way to achieve this is to specify a path for the socket when starting qtile e.g. ``qtile start -s /tmp/qtile.socket``. When firing the hook, you should then call ``qtile cmd-obj -o cmd -f fire_user_hook -a my_custom_hook -s /tmp/qtile.socket`` However, the same socket will need to be passed wherever you run ``qtile cmd-obj`` or ``qtile shell``. """, _user_hook_func, ), ] qtile_hooks = Registry("qtile", hooks) subscribe = qtile_hooks.subscribe unsubscribe = qtile_hooks.unsubscribe fire = qtile_hooks.fire qtile-0.31.0/libqtile/core/0000775000175000017500000000000014762660347015373 5ustar epsilonepsilonqtile-0.31.0/libqtile/core/manager.py0000664000175000017500000017473714762660347017402 0ustar epsilonepsilon# Copyright (c) 2008, Aldo Cortesi. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations import asyncio import faulthandler import io import logging import os import pickle import shlex import shutil import signal import subprocess import sys import tempfile from collections import defaultdict from logging.handlers import RotatingFileHandler from pathlib import Path from typing import TYPE_CHECKING import libqtile from libqtile import bar, hook, ipc, utils from libqtile.backend import base from libqtile.command import interface from libqtile.command.base import CommandError, CommandException, CommandObject, expose_command from libqtile.command.client import InteractiveCommandClient from libqtile.command.interface import IPCCommandServer, QtileCommandInterface from libqtile.config import Click, Drag, Key, KeyChord, Match, Mouse, Rule, Screen, ScreenRect from libqtile.config import ScratchPad as ScratchPadConfig from libqtile.core.lifecycle import lifecycle from libqtile.core.loop import LoopContext, QtileEventLoopPolicy from libqtile.core.state import QtileState from libqtile.dgroups import DGroups from libqtile.extension.base import _Extension from libqtile.group import _Group from libqtile.log_utils import logger from libqtile.resources.sleep import inhibitor from libqtile.scratchpad import ScratchPad from libqtile.scripts.main import VERSION from libqtile.utils import cancel_tasks, get_cache_dir, lget, remove_dbus_rules, send_notification from libqtile.widget.base import _Widget if TYPE_CHECKING: from collections.abc import Callable from typing import Any, Literal from libqtile.command.base import ItemT from libqtile.confreader import Config from libqtile.layout.base import Layout from libqtile.utils import ColorType class Qtile(CommandObject): """This object is the `root` of the command graph""" current_screen: Screen dgroups: DGroups _eventloop: asyncio.AbstractEventLoop def __init__( self, kore: base.Core, config: Config, no_spawn: bool = False, state: str | None = None, socket_path: str | None = None, ) -> None: self.core: base.Core = kore self.config = config self.no_spawn = no_spawn self._state: QtileState | str | None = state self.socket_path = socket_path self._drag: tuple | None = None self._mouse_map: defaultdict[int, list[Mouse]] = defaultdict(list) self.windows_map: dict[int, base.WindowType] = {} self.widgets_map: dict[str, _Widget] = {} self.renamed_widgets: list[str] self.groups_map: dict[str, _Group] = {} self.groups: list[_Group] = [] self.keys_map: dict[tuple[int, int], Key | KeyChord] = {} self.chord_stack: list[KeyChord] = [] self.screens: list[Screen] = [] libqtile.init(self) self._stopped_event: asyncio.Event | None = None self.server = IPCCommandServer(self) def load_config(self, initial: bool = False) -> None: try: self.config.load() self.config.validate() except Exception as e: logger.exception("Configuration error:") send_notification("Configuration error", str(e)) if hasattr(self.core, "wmname"): self.core.wmname = getattr(self.config, "wmname", "qtile") # type: ignore self.dgroups = DGroups(self, self.config.groups, self.config.dgroups_key_binder) _Widget.global_defaults = self.config.widget_defaults _Extension.global_defaults = self.config.extension_defaults for installed_extension in _Extension.installed_extensions: installed_extension._configure(self) for i in self.groups: self.groups_map[i.name] = i for grp in self.config.groups: if isinstance(grp, ScratchPadConfig): sp = ScratchPad(grp.name, grp.dropdowns, grp.label, grp.single) sp._configure([self.config.floating_layout], self.config.floating_layout, self) self.groups.append(sp) self.groups_map[sp.name] = sp self._process_screens(reloading=not initial) # Map and Grab keys for key in self.config.keys: self.grab_key(key) for button in self.config.mouse: self.grab_button(button) # no_spawn is set after the very first startup; we only want to run the # startup hook once. if not self.no_spawn: hook.fire("startup_once") self.no_spawn = True hook.fire("startup") if self._state: if isinstance(self._state, str): try: with open(self._state, "rb") as f: st = pickle.load(f) st.apply(self) except: # noqa: E722 logger.exception("failed restoring state") finally: os.remove(self._state) else: self._state.apply(self) self.core.on_config_load(initial) if self._state: for screen in self.screens: screen.group.layout.show(screen.get_rect()) screen.group.layout_all() self._state = None self.update_desktops() hook.subscribe.setgroup(self.update_desktops) if self.config.reconfigure_screens: hook.subscribe.screen_change(self.reconfigure_screens) # Start the sleep inhibitor process to listen to sleep signals # NB: the inhibitor will only connect to the dbus service if the # user has used the "suspend" or "resume" hooks in their config. inhibitor.start() if initial: hook.fire("startup_complete") def _prepare_socket_path( self, socket_path: str | None = None, ) -> str: if socket_path is None: socket_path = ipc.find_sockfile(self.core.display_name) if os.path.exists(socket_path): os.unlink(socket_path) return socket_path def loop(self) -> None: asyncio.run(self.async_loop()) async def async_loop(self) -> None: """Run the event loop Finalizes the Qtile instance on exit. """ self._eventloop = asyncio.get_running_loop() # Set the event loop policy to facilitate access to main event loop asyncio.set_event_loop_policy(QtileEventLoopPolicy(self)) self._stopped_event = asyncio.Event() self.core.qtile = self self.load_config(initial=True) self.core.setup_listener() faulthandler.enable(all_threads=True) faulthandler.register(signal.SIGUSR2, all_threads=True) try: signals = { signal.SIGTERM: self.stop, signal.SIGINT: self.stop, signal.SIGHUP: self.stop, signal.SIGUSR1: self.reload_config, } if self.core.name == "x11": # the wayland backend installs its own SIGCHLD handler after # the XWayland X server has initialized (as a workaround). the # x11 backend can just do it here. signals[signal.SIGCHLD] = utils.reap_zombies async with ( LoopContext(signals), ipc.Server( self._prepare_socket_path(self.socket_path), self.server.call, ), ): await self._stopped_event.wait() finally: self.finalize() self.core.remove_listener() def stop(self, exitcode: int = 0) -> None: hook.fire("shutdown") lifecycle.behavior = lifecycle.behavior.TERMINATE lifecycle.exitcode = exitcode self.core.graceful_shutdown() self._stop() @expose_command() def restart(self) -> None: """Restart Qtile. Can also be triggered by sending Qtile a SIGUSR2 signal. """ if not self.core.supports_restarting: raise CommandError(f"Backend does not support restarting: {self.core.name}") try: self.config.load() except Exception as error: logger.exception("Preventing restart because of a configuration error:") send_notification("Configuration error", str(error.__context__)) return hook.fire("restart") lifecycle.behavior = lifecycle.behavior.RESTART state_file = os.path.join(tempfile.gettempdir(), "qtile-state") with open(state_file, "wb") as f: self.dump_state(f) lifecycle.state_file = state_file self._stop() def _stop(self) -> None: logger.debug("Stopping qtile") if self._stopped_event is not None: self._stopped_event.set() def dump_state(self, buf: Any) -> None: try: pickle.dump(QtileState(self), buf, protocol=0) except: # noqa: E722 logger.exception("Unable to pickle qtile state") @expose_command() def reload_config(self) -> None: """ Reload the configuration file. Can also be triggered by sending Qtile a SIGUSR1 signal. """ logger.debug("Reloading the configuration file") try: self.config.load() except Exception as error: logger.exception("Configuration error:") send_notification("Configuration error", str(error)) return self._state = QtileState(self, restart=False) self._finalize_configurables() hook.clear() self.ungrab_keys() self.chord_stack.clear() self.core.ungrab_buttons() self._mouse_map.clear() self.groups_map.clear() self.groups.clear() self.screens.clear() remove_dbus_rules() self.load_config() def _finalize_configurables(self) -> None: """ Finalize objects that are instantiated within the config file. In addition to shutdown, these are finalized and then regenerated when reloading the config. """ try: for widget in self.widgets_map.values(): widget.finalize() self.widgets_map.clear() # For layouts we need to finalize each clone of a layout in each group for group in self.groups: for layout in group.layouts: layout.finalize() for screen in self.screens: screen.finalize_gaps() except: # noqa: E722 logger.exception("exception during finalize") hook.clear() def finalize(self) -> None: self._finalize_configurables() remove_dbus_rules() inhibitor.stop() cancel_tasks() self.core.finalize() def add_autogen_group(self, screen_idx: int) -> _Group: name = f"autogen_{screen_idx + 1}" self.add_group(name) logger.warning("Too few groups in config. Added group: %s", name) return self.groups_map[name] def get_available_group(self, screen_idx: int) -> _Group | None: for group in self.groups: # Groups belonging to a screen or a scratchpad are not 'available' # to be assigned to a screen if group.screen or isinstance(group, ScratchPad): continue # Only return groups that can be tied to this screen # And thus do not have a "screen affinity" explicitly set for another screen if group.screen_affinity is None or group.screen_affinity == screen_idx: return group return None def _process_screens(self, reloading: bool = False) -> None: current_groups = [s.group for s in self.screens] screens = [] if hasattr(self.config, "fake_screens"): screen_info = [ ScreenRect(s.x, s.y, s.width, s.height) for s in self.config.fake_screens ] config = self.config.fake_screens else: # Alias screens with the same x and y coordinates, taking largest xywh = {} # type: dict[tuple[int, int], tuple[int, int]] for info in self.core.get_screen_info(): pos = (info.x, info.y) width, height = xywh.get(pos, (0, 0)) xywh[pos] = (max(width, info.width), max(height, info.height)) screen_info = [ScreenRect(x, y, w, h) for (x, y), (w, h) in xywh.items()] config = self.config.screens for i, info in enumerate(screen_info): if i + 1 > len(config): scr = Screen() else: scr = config[i] if not hasattr(self, "current_screen") or reloading: self.current_screen = scr reloading = False grp = None if i < len(current_groups): grp = current_groups[i] else: # We need to assign a new group # Get an available group or create a new one grp = self.get_available_group(i) if grp is None: grp = self.add_autogen_group(i) # If the screen has changed position and/or size, or is a new screen then make sure that any gaps/bars # are reconfigured reconfigure_gaps = ( (info.x, info.y, info.width, info.height) != (scr.x, scr.y, scr.width, scr.height) ) or (i + 1 > len(self.screens)) if not hasattr(scr, "group"): # Ensure that this screen actually *has* a group, as it won't get # assigned one during `__init__` because they are created in the config, # where the groups also are. This lets us type `Screen.group` as # `_Group` rather than `_Group | None` which would need lots of other # changes to check for `None`s, and conceptually all screens should have # a group anyway. scr.group = grp scr._configure( self, i, info.x, info.y, info.width, info.height, grp, reconfigure_gaps=reconfigure_gaps, ) screens.append(scr) for screen in self.screens: if screen not in screens: for gap in screen.gaps: if isinstance(gap, bar.Bar) and gap.window: gap.finalize() self.screens = screens @expose_command() def reconfigure_screens(self, *_: list[Any], **__: dict[Any, Any]) -> None: """ This can be used to set up screens again during run time. Intended usage is to be called when the screen_change hook is fired, responding to changes in physical monitor setup by configuring qtile.screens accordingly. The args are ignored; it is here in case this function is hooked directly to screen_change. """ logger.info("Reconfiguring screens.") self._process_screens() for group in self.groups: if group.screen: if group.screen in self.screens: group.layout_all() else: group.hide() hook.fire("screens_reconfigured") def paint_screen(self, screen: Screen, image_path: str, mode: str | None = None) -> None: self.core.painter.paint(screen, image_path, mode) def fill_screen(self, screen: Screen, background: ColorType) -> None: self.core.painter.fill(screen, background) def process_key_event(self, keysym: int, mask: int) -> tuple[Key | KeyChord | None, bool]: key = self.keys_map.get((keysym, mask), None) if key is None: logger.debug("Ignoring unknown keysym: %s, mask: %s", keysym, mask) return (None, False) if isinstance(key, KeyChord): self.grab_chord(key) else: # Keep track if we have executed a command executed = False for cmd in key.commands: if cmd.check(self): status, val = self.server.call( (cmd.selectors, cmd.name, cmd.args, cmd.kwargs, False) ) if status in (interface.ERROR, interface.EXCEPTION): logger.error("KB command error %s: %s", cmd.name, val) executed = True if self.chord_stack and (not self.chord_stack[-1].mode or key.key == "Escape"): self.ungrab_chord() # We never swallow when no commands have been executed, # even when key.swallow is set to True elif not executed: return (key, False) # Return whether we have handled the key based on the key's swallow parameter return (key, key.swallow) def grab_keys(self) -> None: """Re-grab all of the keys configured in the key map Useful when a keyboard mapping event is received. """ self.core.ungrab_keys() keys = self.keys_map.copy() self.keys_map.clear() for key in keys.values(): self.grab_key(key) def grab_key(self, key: Key | KeyChord) -> None: """Grab the given key event""" syms = self.core.grab_key(key) if syms in self.keys_map: if self.keys_map[syms] == key: # We've already bound this key definition return logger.warning("Key spec duplicated, overriding previous: %s", key) self.keys_map[syms] = key def ungrab_key(self, key: Key | KeyChord) -> None: """Ungrab a given key event""" keysym, mask_key = self.core.ungrab_key(key) self.keys_map.pop((keysym, mask_key)) def ungrab_keys(self) -> None: """Ungrab all key events""" self.core.ungrab_keys() self.keys_map.clear() def grab_chord(self, chord: KeyChord) -> None: self.chord_stack.append(chord) if self.chord_stack: hook.fire("enter_chord", chord.name) self.ungrab_keys() for key in chord.submappings: self.grab_key(key) @expose_command() def ungrab_chord(self) -> None: """Leave a chord mode""" hook.fire("leave_chord") self.ungrab_keys() if not self.chord_stack: logger.debug("ungrab_chord was called when no chord mode was active") return # The first pop is necessary: Otherwise we would be stuck in a mode; # we could not leave it: the code below would re-enter the old mode. self.chord_stack.pop() # Find another named mode or load the root keybindings: while self.chord_stack: chord = self.chord_stack.pop() if chord.mode: self.grab_chord(chord) break else: for key in self.config.keys: self.grab_key(key) @expose_command() def ungrab_all_chords(self) -> None: """Leave all chord modes and grab the root bindings""" hook.fire("leave_chord") self.ungrab_keys() self.chord_stack.clear() for key in self.config.keys: self.grab_key(key) def grab_button(self, button: Mouse) -> None: """Grab the given mouse button event""" try: button.modmask = self.core.grab_button(button) except utils.QtileError: logger.warning("Unknown modifier(s): %s", button.modifiers) return self._mouse_map[button.button_code].append(button) def update_desktops(self) -> None: try: index = self.groups.index(self.current_group) # TODO: we should really only except ValueError here, AttributeError is # an annoying chicken and egg because we're accessing current_screen # (via current_group), and when we set up the initial groups, there # aren't any screens yet. This can probably be changed when #475 is # fixed. except (ValueError, AttributeError): index = 0 self.core.update_desktops(self.groups, index) def add_group( self, name: str, layout: str | None = None, layouts: list[Layout] | None = None, label: str | None = None, index: int | None = None, screen_affinity: int | None = None, persist: bool | None = False, ) -> bool: if name not in self.groups_map.keys(): g = _Group( name, layout, label=label, screen_affinity=screen_affinity, persist=persist ) if index is None: self.groups.append(g) else: self.groups.insert(index, g) if not layouts: layouts = self.config.layouts g._configure(layouts, self.config.floating_layout, self) self.groups_map[name] = g hook.fire("addgroup", name) hook.fire("changegroup") self.update_desktops() return True return False def delete_group(self, name: str) -> None: # one group per screen is needed if len(self.groups) == len(self.screens): raise ValueError("Can't delete all groups.") if name in self.groups_map.keys(): group = self.groups_map[name] # Find a group that's not currently on a screen to bring to the front. target = group.get_previous_group(skip_managed=True) # move windows to other group for i in list(group.windows): i.togroup(target.name) # if group to be deleted is currently active if self.current_group.name == name: # switch to target group self.current_screen.set_group(target, save_prev=False) self.groups.remove(group) del self.groups_map[name] hook.fire("delgroup", name) hook.fire("changegroup") self.update_desktops() def register_widget(self, w: _Widget) -> None: """ Register a bar widget If a widget with the same name already exists, the new widget will be automatically renamed by appending numeric suffixes. For example, if the widget is named "foo", we will attempt "foo_1", "foo_2", and so on, until a free name is found. This naming convention is only used for qtile.widgets_map as every widget MUST be registered here to ensure that objects are finalised correctly. Widgets can still be accessed by their name when using lazy.screen.widget[name] or lazy.bar["top"].widget[name] unless there are duplicate widgets in the bar/screen. A warning will be provided where renaming has occurred. """ # Find unoccupied name by appending numeric suffixes name = w.name i = 0 while name in self.widgets_map: i += 1 name = f"{w.name}_{i}" if name != w.name: self.renamed_widgets.append(name) self.widgets_map[name] = w @property def current_layout(self) -> Layout: return self.current_group.layout @property def current_group(self) -> _Group: return self.current_screen.group @property def current_window(self) -> base.Window | None: return self.current_screen.group.current_window def reserve_space( self, reserved_space: tuple[int, int, int, int], # [left, right, top, bottom] screen: Screen, ) -> None: """ Reserve some space at the edge(s) of a screen. The requested space is added to space reserved previously: repeated calls to this method are not idempotent. """ for i, pos in enumerate(["left", "right", "top", "bottom"]): if space := reserved_space[i]: if gap := getattr(screen, pos): gap.adjust_reserved_space(space) elif 0 < space: gap = bar.Gap(0) gap.screen = screen setattr(screen, pos, gap) gap.adjust_reserved_space(space) screen.resize() def free_reserved_space( self, reserved_space: tuple[int, int, int, int], # [left, right, top, bottom] screen: Screen, ) -> None: """ Free up space that has previously been reserved at the edge(s) of a screen. """ # mypy can't work out that the new tuple is also length 4 (see mypy #7509) reserved_space = tuple(-i for i in reserved_space) # type: ignore self.reserve_space(reserved_space, screen) def manage(self, win: base.WindowType) -> None: if isinstance(win, base.Internal): self.windows_map[win.wid] = win return if win.wid in self.windows_map: return hook.fire("client_new", win) # Window may be defunct because # it's been declared static in hook. if win.defunct: return self.windows_map[win.wid] = win if self.current_screen and isinstance(win, base.Window): # Window may have been bound to a group in the hook. if not win.group and self.current_screen.group: self.current_screen.group.add(win, focus=win.can_steal_focus) hook.fire("client_managed", win) def unmanage(self, wid: int) -> None: c = self.windows_map.get(wid) if c: group = None if isinstance(c, base.Static): if c.reserved_space: self.free_reserved_space(c.reserved_space, c.screen) elif isinstance(c, base.Window): if c.group: group = c.group c.group.remove(c) del self.windows_map[wid] if isinstance(c, base.Window): # Put the group back on the window so hooked functions can access it. c.group = group hook.fire("client_killed", c) def find_screen(self, x: int, y: int) -> Screen | None: """Find a screen based on the x and y offset""" result = [] for i in self.screens: if i.x <= x <= i.x + i.width and i.y <= y <= i.y + i.height: result.append(i) if len(result) == 1: return result[0] return None def find_closest_screen(self, x: int, y: int) -> Screen: """ If find_screen returns None, then this basically extends a screen vertically and horizontally and see if x,y lies in the band. Only works if it can find a SINGLE closest screen, else we revert to _find_closest_closest. Useful when dragging a window out of a screen onto another but having leftmost corner above viewport. """ normal = self.find_screen(x, y) if normal is not None: return normal x_match = [] y_match = [] for i in self.screens: if i.x <= x <= i.x + i.width: x_match.append(i) if i.y <= y <= i.y + i.height: y_match.append(i) if len(x_match) == 1: return x_match[0] if len(y_match) == 1: return y_match[0] return self._find_closest_closest(x, y, x_match + y_match) def _find_closest_closest(self, x: int, y: int, candidate_screens: list[Screen]) -> Screen: """ if find_closest_screen can't determine one, we've got multiple screens, so figure out who is closer. We'll calculate using the square of the distance from the center of a screen. Note that this could return None if x, y is right/below all screens. """ closest_distance: float | None = None if not candidate_screens: # try all screens candidate_screens = self.screens # if left corner is below and right of screen # it can't really be a candidate candidate_screens = [ s for s in candidate_screens if x < s.x + s.width and y < s.y + s.height ] closest_screen = lget(candidate_screens, 0) for s in candidate_screens: middle_x = s.x + s.width / 2 middle_y = s.y + s.height / 2 distance = (x - middle_x) ** 2 + (y - middle_y) ** 2 if closest_distance is None or distance < closest_distance: closest_distance = distance closest_screen = s return closest_screen or self.screens[0] def _focus_hovered_window(self) -> None: window = self.core.hovered_window if window: if isinstance(window, base.Window): window.focus() def process_button_click(self, button_code: int, modmask: int, x: int, y: int) -> bool: handled = False for m in self._mouse_map[button_code]: if not m.modmask == modmask: continue if isinstance(m, Click): if self.config.follow_mouse_focus == "click_or_drag_only": self._focus_hovered_window() for i in m.commands: if i.check(self): status, val = self.server.call( (i.selectors, i.name, i.args, i.kwargs, False) ) if status in (interface.ERROR, interface.EXCEPTION): logger.error("Mouse command error %s: %s", i.name, val) handled = True elif ( isinstance(m, Drag) and self.current_window and not self.current_window.fullscreen ): if self.config.follow_mouse_focus == "click_or_drag_only": self._focus_hovered_window() if m.start: i = m.start status, val = self.server.call((i.selectors, i.name, i.args, i.kwargs, False)) if status in (interface.ERROR, interface.EXCEPTION): logger.error("Mouse command error %s: %s", i.name, val) continue else: val = (0, 0) if m.warp_pointer and self.current_window is not None: win_size = self.current_window.get_size() win_pos = self.current_window.get_position() x = win_size[0] + win_pos[0] y = win_size[1] + win_pos[1] self.core.warp_pointer(x, y) self._drag = (x, y, val[0], val[1], m.commands) self.core.grab_pointer() handled = True return handled def process_button_release(self, button_code: int, modmask: int) -> bool: if self._drag is not None: for m in self._mouse_map[button_code]: if isinstance(m, Drag): self._drag = None self.core.ungrab_pointer() return True return False def process_button_motion(self, x: int, y: int) -> None: if self._drag is None: return ox, oy, rx, ry, cmd = self._drag dx = x - ox dy = y - oy if dx or dy: for i in cmd: if i.check(self): status, val = self.server.call( (i.selectors, i.name, i.args + (rx + dx, ry + dy), i.kwargs, False) ) if status in (interface.ERROR, interface.EXCEPTION): logger.error("Mouse command error %s: %s", i.name, val) def warp_to_screen(self) -> None: if self.current_screen: scr = self.current_screen self.core.warp_pointer(scr.x + scr.dwidth // 2, scr.y + scr.dheight // 2) def focus_screen(self, n: int, warp: bool = True) -> None: """Have Qtile move to screen and put focus there""" if n >= len(self.screens): return old = self.current_screen self.current_screen = self.screens[n] if old != self.current_screen: hook.fire("current_screen_change") hook.fire("setgroup") old.group.layout_all() self.current_group.focus(self.current_window, warp) if self.current_window is None and warp: self.warp_to_screen() def move_to_group(self, group: str) -> None: """Create a group if it doesn't exist and move the current window there""" if self.current_window and group: self.add_group(group) self.current_window.togroup(group) def _items(self, name: str) -> ItemT: if name == "group": return True, list(self.groups_map.keys()) elif name == "layout": return True, list(range(len(self.current_group.layouts))) elif name == "widget": return False, list(self.widgets_map.keys()) elif name == "bar": return False, [x.position for x in self.current_screen.gaps if isinstance(x, bar.Bar)] elif name == "window": windows: list[str | int] windows = [ k for k, v in self.windows_map.items() if isinstance(v, CommandObject) and not isinstance(v, _Widget) ] return True, windows elif name == "screen": return True, list(range(len(self.screens))) elif name == "core": return True, [] return None def _select(self, name: str, sel: str | int | None) -> CommandObject | None: if name == "group": if sel is None: return self.current_group else: return self.groups_map.get(sel) # type: ignore elif name == "layout": if sel is None: return self.current_group.layout else: return lget(self.current_group.layouts, int(sel)) elif name == "widget": return self.widgets_map.get(sel) # type: ignore elif name == "bar": gap = getattr(self.current_screen, sel) # type: ignore if isinstance(gap, bar.Bar): return gap elif name == "window": if sel is None: return self.current_window else: windows: dict[str | int, base.WindowType] windows = { k: v for k, v in self.windows_map.items() if isinstance(v, CommandObject) and not isinstance(v, _Widget) } return windows.get(sel) elif name == "screen": if sel is None: return self.current_screen else: return lget(self.screens, int(sel)) elif name == "core": return self.core return None def call_soon(self, func: Callable, *args: Any) -> asyncio.Handle: """A wrapper for the event loop's call_soon which also flushes the core's event queue after func is called.""" def f() -> None: func(*args) self.core.flush() return self._eventloop.call_soon(f) def call_soon_threadsafe(self, func: Callable, *args: Any) -> asyncio.Handle: """Another event loop proxy, see `call_soon`.""" def f() -> None: func(*args) self.core.flush() return self._eventloop.call_soon_threadsafe(f) def call_later(self, delay: int | float, func: Callable, *args: Any) -> asyncio.TimerHandle: """Another event loop proxy, see `call_soon`.""" def f() -> None: func(*args) self.core.flush() return self._eventloop.call_later(delay, f) def run_in_executor(self, func: Callable, *args: Any) -> asyncio.Future: """A wrapper for running a function in the event loop's default executor.""" return self._eventloop.run_in_executor(None, func, *args) @expose_command() def debug(self) -> None: """Set log level to DEBUG""" logger.setLevel(logging.DEBUG) logger.debug("Switching to DEBUG threshold") @expose_command() def info(self) -> None: """Set log level to INFO""" logger.setLevel(logging.INFO) logger.info("Switching to INFO threshold") @expose_command() def warning(self) -> None: """Set log level to WARNING""" logger.setLevel(logging.WARNING) logger.warning("Switching to WARNING threshold") @expose_command() def error(self) -> None: """Set log level to ERROR""" logger.setLevel(logging.ERROR) logger.error("Switching to ERROR threshold") @expose_command() def critical(self) -> None: """Set log level to CRITICAL""" logger.setLevel(logging.CRITICAL) logger.critical("Switching to CRITICAL threshold") @expose_command() def loglevel(self) -> int: return logger.level @expose_command() def loglevelname(self) -> str: return logging.getLevelName(logger.level) @expose_command() def pause(self) -> None: """Drops into pdb""" import pdb pdb.set_trace() @expose_command() def get_groups(self) -> dict[str, dict[str, Any]]: """ Return a dictionary containing information for all groups Examples ======== get_groups() """ return {i.name: i.info() for i in self.groups} @expose_command() def display_kb(self) -> str: """Display table of key bindings""" class FormatTable: def __init__(self) -> None: self.max_col_size: list[int] = [] self.rows: list[list[str]] = [] def add(self, row: list[str]) -> None: n = len(row) - len(self.max_col_size) if n > 0: self.max_col_size += [0] * n for i, f in enumerate(row): if len(f) > self.max_col_size[i]: self.max_col_size[i] = len(f) self.rows.append(row) def getformat(self) -> tuple[str, int]: format_string = " ".join( f"%-{max_col_size + 2:d}s" for max_col_size in self.max_col_size ) return format_string + "\n", len(self.max_col_size) def expandlist(self, list_: list[str], n: int) -> list[str]: if not list_: return ["-" * max_col_size for max_col_size in self.max_col_size] n -= len(list_) if n > 0: list_ += [""] * n return list_ def __str__(self) -> str: format_, n = self.getformat() return "".join(format_ % tuple(self.expandlist(row, n)) for row in self.rows) result = FormatTable() result.add(["Mode", "KeySym", "Mod", "Command", "Desc"]) result.add([]) rows = [] def walk_binding(k: Key | KeyChord, mode: str) -> None: nonlocal rows modifiers = ", ".join(k.modifiers) if isinstance(k.key, int): name = hex(k.key) else: name = k.key if isinstance(k, Key): if not k.commands: return allargs = ", ".join( [ value.__name__ if callable(value) else repr(value) for value in k.commands[0].args ] + [ f"{keyword} = {repr(value)}" for keyword, value in k.commands[0].kwargs.items() ] ) rows.append( [ mode, name, modifiers, f"{k.commands[0].name:s}({allargs:s})", k.desc, ] ) return if isinstance(k, KeyChord): new_mode_s = k.name if k.name else "" new_mode = ( k.name if mode == "" else "{}>{}".format(mode, k.name if k.name else "_") ) rows.append([mode, name, modifiers, "", f"Enter {new_mode_s:s} mode"]) for s in k.submappings: walk_binding(s, new_mode) return raise TypeError(f"Unexpected type: {type(k)}") for k in self.config.keys: walk_binding(k, "") rows.sort() for row in rows: result.add(row) return str(result) @expose_command() def list_widgets(self) -> list[str]: """List of all addressible widget names""" return list(self.widgets_map.keys()) @expose_command() def to_layout_index(self, index: int, name: str | None = None) -> None: """ Switch to the layout with the given index in self.layouts. Parameters ========== index : Index of the layout in the list of layouts. name : Group name. If not specified, the current group is assumed. """ if name is not None: group = self.groups_map[name] else: group = self.current_group group.use_layout(index) @expose_command() def next_layout(self, name: str | None = None) -> None: """ Switch to the next layout. Parameters ========== name : Group name. If not specified, the current group is assumed """ if name is not None: group = self.groups_map[name] else: group = self.current_group group.use_next_layout() @expose_command() def prev_layout(self, name: str | None = None) -> None: """ Switch to the previous layout. Parameters ========== name : Group name. If not specified, the current group is assumed """ if name is not None: group = self.groups_map[name] else: group = self.current_group group.use_previous_layout() @expose_command() def get_screens(self) -> list[dict[str, Any]]: """Return a list of dictionaries providing information on all screens""" lst = [ dict( index=i.index, group=i.group.name if i.group is not None else None, x=i.x, y=i.y, width=i.width, height=i.height, gaps=dict( top=i.top.geometry() if i.top else None, bottom=i.bottom.geometry() if i.bottom else None, left=i.left.geometry() if i.left else None, right=i.right.geometry() if i.right else None, ), ) for i in self.screens ] return lst @expose_command() def simulate_keypress(self, modifiers: list[str], key: str) -> None: """Simulates a keypress on the focused window. This triggers internal bindings only; for full simulation see external tools such as xdotool or ydotool. Parameters ========== modifiers : A list of modifier specification strings. Modifiers can be one of "shift", "lock", "control" and "mod1" - "mod5". key : Key specification. Examples ======== simulate_keypress(["control", "mod2"], "k") """ try: self.core.simulate_keypress(modifiers, key) except utils.QtileError as e: raise CommandError(str(e)) @expose_command() def validate_config(self) -> None: try: self.config.load() except Exception as error: send_notification("Configuration check", str(error)) else: send_notification("Configuration check", "No error found!") @expose_command() def spawn( self, cmd: list[str] | str, shell: bool = False, env: dict[str, str] = dict() ) -> int: """ Spawn a new process. Parameters ========== cmd: The command to execute either as a single string or list of strings. shell: Whether to execute the command in a new shell by prepending it with "/bin/sh -c". This enables the use of shell syntax within the command (e.g. pipes). env: Dictionary of environmental variables to pass with command. Examples ======== spawn("firefox") spawn(["xterm", "-T", "Temporary terminal"]) spawn("screenshot | xclip", shell=True) """ if isinstance(cmd, str): args = shlex.split(cmd) else: args = list(cmd) cmd = subprocess.list2cmdline(args) to_lookup = args[0] if shell: args = ["/bin/sh", "-c", cmd] if shutil.which(to_lookup) is None: logger.error("couldn't find `%s`", to_lookup) return -1 if len(env) == 0: env = os.environ.copy() # if qtile was installed in a virutal env, we don't # necessarily want to propagate that to children # applications, since it may change e.g. the behavior # of shells that spawn python applications env.pop("VIRTUAL_ENV", None) # std{in,out,err} should be /dev/null with open("/dev/null") as null: file_actions: list[tuple] = [ (os.POSIX_SPAWN_DUP2, 0, null.fileno()), (os.POSIX_SPAWN_DUP2, 1, null.fileno()), (os.POSIX_SPAWN_DUP2, 2, null.fileno()), ] if sys.version_info.major >= 3 and sys.version_info.minor >= 13: # we should close all fds so that child processes don't # accidentally write to our x11 event loop or whatever; we never # used to do this, so it seems fine to only do it on python 3.13 or # above, where this nice API to do it exists. file_actions.append((os.POSIX_SPAWN_CLOSEFROM, 3)) # type: ignore try: return os.posix_spawnp(args[0], args, env, file_actions=file_actions) except OSError as e: logger.warning("failed to execute: %s: %s", str(args), str(e)) return -1 @expose_command() def status(self) -> Literal["OK"]: """Return "OK" if Qtile is running""" return "OK" @expose_command() def sync(self) -> None: """ Sync the backend's event queue. Should only be used for development. """ self.core.flush() @expose_command() def to_screen(self, n: int) -> None: """Warp focus to screen n, where n is a 0-based screen number Examples ======== to_screen(0) """ self.focus_screen(n) @expose_command() def next_screen(self) -> None: """Move to next screen""" self.focus_screen((self.screens.index(self.current_screen) + 1) % len(self.screens)) @expose_command() def prev_screen(self) -> None: """Move to the previous screen""" self.focus_screen((self.screens.index(self.current_screen) - 1) % len(self.screens)) @expose_command() def windows(self) -> list[dict[str, Any]]: """Return info for each client window""" return [ i.info() for i in self.windows_map.values() if not isinstance(i, base.Internal | _Widget) and isinstance(i, CommandObject) ] @expose_command() def internal_windows(self) -> list[dict[str, Any]]: """Return info for each internal window (bars, for example)""" return [i.info() for i in self.windows_map.values() if isinstance(i, base.Internal)] @expose_command() def qtile_info(self) -> dict: """Returns a dictionary of info on the Qtile instance""" config_path = self.config.file_path dictionary = { "version": VERSION, "log_level": self.loglevelname(), } if isinstance(logger.handlers[0], RotatingFileHandler): log_path = logger.handlers[0].baseFilename dictionary["log_path"] = log_path if isinstance(config_path, str): dictionary["config_path"] = config_path elif isinstance(config_path, Path): dictionary["config_path"] = config_path.as_posix() return dictionary @expose_command() def shutdown(self, exitcode: int = 0) -> None: """Quit Qtile Parameters ========== exitcode : Set exit status of Qtile. Can be e.g. used to make login managers poweroff or restart the system. (default: 0) """ self.stop(exitcode) @expose_command() def switch_groups(self, namea: str, nameb: str) -> None: """Switch position of two groups by name""" if namea not in self.groups_map or nameb not in self.groups_map: return indexa = self.groups.index(self.groups_map[namea]) indexb = self.groups.index(self.groups_map[nameb]) self.groups[indexa], self.groups[indexb] = self.groups[indexb], self.groups[indexa] hook.fire("setgroup") # update window _NET_WM_DESKTOP for group in (self.groups[indexa], self.groups[indexb]): for w in group.windows: w.group = group def find_window(self, wid: int) -> None: window = self.windows_map.get(wid) if isinstance(window, base.Window) and window.group: if not window.group.screen: self.current_screen.set_group(window.group) window.group.focus(window, False) @expose_command() def findwindow(self, prompt: str = "window", widget: str = "prompt") -> None: """Launch prompt widget to find a window of the given name Parameters ========== prompt : Text with which to prompt user (default: "window") widget : Name of the prompt widget (default: "prompt") """ mb = self.widgets_map.get(widget) if not mb: logger.error("No widget named '%s' present.", widget) return mb.start_input(prompt, self.find_window, "window", strict_completer=True) @expose_command() def switch_window(self, location: int) -> None: """ Change to the window at the specified index in the current group. """ windows = self.current_group.windows if location < 1 or location > len(windows): return self.current_group.focus(windows[location - 1]) @expose_command() def change_window_order(self, new_location: int) -> None: """ Change the order of the current window within the current group. """ if new_location < 1 or new_location > len(self.current_group.windows): return windows = self.current_group.windows current_window_index = windows.index(self.current_window) temp = windows[current_window_index] windows[current_window_index] = windows[new_location - 1] windows[new_location - 1] = temp @expose_command() def next_urgent(self) -> None: """Focus next window with urgent hint""" try: nxt = [w for w in self.windows_map.values() if w.urgent][0] assert isinstance(nxt, base.Window) if nxt.group: nxt.group.toscreen() nxt.group.focus(nxt) else: self.current_screen.group.add(nxt) self.current_screen.group.focus(nxt) except IndexError: pass # no window had urgent set @expose_command() def togroup(self, prompt: str = "group", widget: str = "prompt") -> None: """Launch prompt widget to move current window to a given group Parameters ========== prompt : Text with which to prompt user (default: "group") widget : Name of the prompt widget (default: "prompt") """ if not self.current_window: logger.warning("No window to move") return mb = self.widgets_map.get(widget) if not mb: logger.error("No widget named '%s' present.", widget) return mb.start_input(prompt, self.move_to_group, "group", strict_completer=True) @expose_command() def switchgroup(self, prompt: str = "group", widget: str = "prompt") -> None: """Launch prompt widget to switch to a given group to the current screen Parameters ========== prompt : Text with which to prompt user (default: "group") widget : Name of the prompt widget (default: "prompt") """ def f(group: str) -> None: if group: try: self.groups_map[group].toscreen() except KeyError: logger.warning("No group named '%s' present.", group) mb = self.widgets_map.get(widget) if not mb: logger.error("No widget named '%s' present.", widget) return mb.start_input(prompt, f, "group", strict_completer=True) @expose_command() def labelgroup(self, prompt: str = "label", widget: str = "prompt") -> None: """Launch prompt widget to label the current group Parameters ========== prompt : Text with which to prompt user (default: "label") widget : Name of the prompt widget (default: "prompt") """ def f(name: str) -> None: self.current_group.set_label(name or None) try: mb = self.widgets_map[widget] mb.start_input(prompt, f, allow_empty_input=True) except KeyError: logger.error("No widget named '%s' present.", widget) @expose_command() def spawncmd( self, prompt: str = "spawn", widget: str = "prompt", command: str = "%s", complete: str = "cmd", shell: bool = True, aliases: dict[str, str] | None = None, ) -> None: """Spawn a command using a prompt widget, with tab-completion. Parameters ========== prompt : Text with which to prompt user (default: "spawn: "). widget : Name of the prompt widget (default: "prompt"). command : command template (default: "%s"). complete : Tab completion function (default: "cmd") shell : Execute the command with /bin/sh (default: True) aliases : Dictionary mapping aliases to commands. If the entered command is a key in this dict, the command it maps to will be executed instead. """ def f(args: str) -> None: if args: if aliases and args in aliases: args = aliases[args] self.spawn(command % args, shell=shell) try: mb = self.widgets_map[widget] mb.start_input(prompt, f, complete, aliases=aliases) except KeyError: logger.error("No widget named '%s' present.", widget) @expose_command() def qtilecmd( self, prompt: str = "command", widget: str = "prompt", messenger: str = "xmessage", ) -> None: """Execute a Qtile command using the client syntax Tab completion aids navigation of the command tree Parameters ========== prompt : Text to display at the prompt (default: "command: ") widget : Name of the prompt widget (default: "prompt") messenger : Command to display output, set this to None to disable (default: "xmessage") """ def f(cmd: str) -> None: if cmd: # c here is used in eval() below q = QtileCommandInterface(self) c = InteractiveCommandClient(q) # noqa: F841 try: cmd_arg = str(cmd).split(" ") except AttributeError: return cmd_len = len(cmd_arg) if cmd_len == 0: logger.debug("No command entered.") return try: result = eval(f"c.{cmd:s}") except (CommandError, CommandException, AttributeError): logger.exception("Command errored:") result = None if result is not None: from pprint import pformat message = pformat(result) if messenger: self.spawn(f'{messenger:s} "{message:s}"') logger.debug(result) mb = self.widgets_map[widget] if not mb: logger.error("No widget named %s present.", widget) return mb.start_input(prompt, f, "qshell") @expose_command() def addgroup( self, group: str, label: str | None = None, layout: str | None = None, layouts: list[Layout] | None = None, index: int | None = None, persist: bool | None = False, ) -> bool: """Add a group with the given name""" return self.add_group( name=group, layout=layout, layouts=layouts, label=label, index=index, persist=persist ) @expose_command() def delgroup(self, group: str) -> None: """Delete a group with the given name""" self.delete_group(group) @expose_command() def add_rule( self, match_args: dict[str, Any], rule_args: dict[str, Any], min_priorty: bool = False, ) -> int | None: """Add a dgroup rule, returns rule_id needed to remove it Parameters ========== match_args : config.Match arguments rule_args : config.Rule arguments min_priorty : If the rule is added with minimum priority (last) (default: False) """ if not self.dgroups: logger.warning("No dgroups created") return None match = Match(**match_args) rule = Rule([match], **rule_args) return self.dgroups.add_rule(rule, min_priorty) @expose_command() def remove_rule(self, rule_id: int) -> None: """Remove a dgroup rule by rule_id""" self.dgroups.remove_rule(rule_id) @expose_command() def hide_show_bar( self, position: Literal["top", "bottom", "left", "right", "all"] = "all", screen: Literal["current", "all"] = "current", ) -> None: """Toggle visibility of a given bar Parameters ========== position : one of: "top", "bottom", "left", "right", or "all" (default: "all") screen : one of: "current", "all" (default: "current") """ to_mod = [self.current_screen] if screen == "all": to_mod = self.screens for s in to_mod: self.hide_show_bar_screen(s, position) def hide_show_bar_screen( self, screen: Screen, position: Literal["top", "bottom", "left", "right", "all"] = "all", ) -> None: if position in ["top", "bottom", "left", "right"]: bar = getattr(screen, position) if bar: bar.show(not bar.is_show()) self.current_group.layout_all() else: logger.warning("Not found bar in position '%s' for hide/show.", position) elif position == "all": is_show = None for bar in [screen.left, screen.right, screen.top, screen.bottom]: if isinstance(bar, libqtile.bar.Bar): if is_show is None: is_show = not bar.is_show() bar.show(is_show) if is_show is not None: self.current_group.layout_all() else: logger.warning("Not found bar for hide/show.") else: logger.warning("Invalid position value:%s", position) @expose_command() def get_state(self) -> str: """Get pickled state for restarting qtile""" buf = io.BytesIO() self.dump_state(buf) state = buf.getvalue().decode(errors="backslashreplace") logger.debug("State = %s", state) return state @expose_command() def tracemalloc_toggle(self) -> None: """Toggle tracemalloc status Running tracemalloc is required for `qtile top` """ import tracemalloc if not tracemalloc.is_tracing(): tracemalloc.start() else: tracemalloc.stop() @expose_command() def tracemalloc_dump(self) -> tuple[bool, str]: """Dump tracemalloc snapshot""" import tracemalloc if not tracemalloc.is_tracing(): return False, "Trace not started" cache_directory = get_cache_dir() malloc_dump = os.path.join(cache_directory, "qtile_tracemalloc.dump") tracemalloc.take_snapshot().dump(malloc_dump) return True, malloc_dump @expose_command() def get_test_data(self) -> Any: """ Returns any content arbitrarily set in the self.test_data attribute. Useful in tests. """ return self.test_data @expose_command() def run_extension(self, extension: _Extension) -> None: """Run extensions""" extension.run() @expose_command() def fire_user_hook(self, hook_name: str, *args: Any) -> None: """Fire a custom hook.""" hook.fire(f"user_{hook_name}", *args) qtile-0.31.0/libqtile/core/lifecycle.py0000664000175000017500000000316114762660347017705 0ustar epsilonepsilonimport atexit import enum import os import sys from libqtile.log_utils import logger __all__ = [ "lifecycle", ] Behavior = enum.Enum("Behavior", "NONE TERMINATE RESTART") class LifeCycle: # This class exists mostly to move os.execv to the absolute last thing that # the python VM does before termination. # Be very careful about what references this class owns. Any object # referenced here when atexit fires will NOT be finalized properly. def __init__(self) -> None: self.behavior = Behavior.NONE self.exitcode: int = 0 self.state_file: str | None = None atexit.register(self._atexit) def _atexit(self) -> None: if self.behavior is Behavior.RESTART: argv = [sys.executable] + sys.argv if "--no-spawn" not in argv: argv.append("--no-spawn") argv = [s for s in argv if not s.startswith("--with-state")] if self.state_file is not None: argv.append("--with-state=" + self.state_file) logger.warning("Restarting Qtile with os.execv(...)") # No other code will execute after the following line does os.execv(sys.executable, argv) elif self.behavior is Behavior.TERMINATE: logger.warning("Qtile will now terminate") # the if statement should be unnecessary, but keeps code coverage working # calling os._exit prevents later injected atexit handlers from running if self.exitcode: os._exit(self.exitcode) elif self.behavior is Behavior.NONE: pass lifecycle = LifeCycle() qtile-0.31.0/libqtile/core/__init__.py0000664000175000017500000000000014762660347017472 0ustar epsilonepsilonqtile-0.31.0/libqtile/core/state.py0000664000175000017500000001060714762660347017071 0ustar epsilonepsilon# Copyright (c) 2012, Tycho Andersen. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations from typing import TYPE_CHECKING from libqtile import hook from libqtile.backend.base import Window from libqtile.scratchpad import ScratchPad if TYPE_CHECKING: from libqtile.backend.base import Static from libqtile.core.manager import Qtile class QtileState: """Represents the state of the Qtile object This is used for restoring state across restarts or config reloads. If `restart` is True, the current set of groups will be saved in the state. This is useful when restarting for Qtile version updates rather than reloading the config. ScratchPad groups are saved for both reloading and restarting. """ def __init__(self, qtile: Qtile, restart: bool = True) -> None: self.groups = [] self.screens = {} self.current_screen = 0 self.scratchpads = {} self.orphans: list[int] = [] self.restart = restart # True when restarting, False when config reloading for group in qtile.groups: if isinstance(group, ScratchPad): self.scratchpads[group.name] = group.get_state() elif restart: self.groups.append((group.name, group.layout.name, group.label)) for index, screen in enumerate(qtile.screens): self.screens[index] = screen.group.name if screen == qtile.current_screen: self.current_screen = index def apply(self, qtile: Qtile) -> None: """ Rearrange the windows in the specified Qtile object according to this QtileState. """ for group, layout, label in self.groups: try: qtile.groups_map[group].layout = layout qtile.groups_map[group].label = label except KeyError: qtile.add_group(group, layout, label=label) for screen, group in self.screens.items(): try: group = qtile.groups_map[group] qtile.screens[screen].set_group(group) except (KeyError, IndexError): pass # group or screen missing for group in qtile.groups: if isinstance(group, ScratchPad) and group.name in self.scratchpads: orphans = group.restore_state(self.scratchpads.pop(group.name), self.restart) self.orphans.extend(orphans) for sp_state in self.scratchpads.values(): for _, wid, _ in sp_state: self.orphans.append(wid) if self.orphans: if self.restart: hook.subscribe.client_new(self.handle_orphan_dropdowns) else: for wid in self.orphans: win = qtile.windows_map[wid] if isinstance(win, Window): win.group = qtile.current_group qtile.focus_screen(self.current_screen) def handle_orphan_dropdowns(self, client: Window | Static) -> None: """ Remove any windows from now non-existent scratchpad groups. """ client_wid = client.wid if client_wid in self.orphans: self.orphans.remove(client_wid) if isinstance(client, Window): client.group = None if not self.orphans: hook.unsubscribe.client_new(self.handle_orphan_dropdowns) qtile-0.31.0/libqtile/core/loop.py0000664000175000017500000000503614762660347016722 0ustar epsilonepsilonfrom __future__ import annotations import asyncio import contextlib import signal from typing import TYPE_CHECKING from libqtile.log_utils import logger if TYPE_CHECKING: from collections.abc import Callable from libqtile.core.manager import Qtile class LoopContext(contextlib.AbstractAsyncContextManager): def __init__( self, signals: dict[signal.Signals, Callable] | None = None, ) -> None: super().__init__() self._signals = signals or {} self._stopped = False async def __aenter__(self) -> LoopContext: self._stopped = False loop = asyncio.get_running_loop() loop.set_exception_handler(self._handle_exception) for sig, handler in self._signals.items(): loop.add_signal_handler(sig, handler) return self async def __aexit__(self, *args) -> None: # type: ignore self._stopped = True await self._cancel_all_tasks() loop = asyncio.get_running_loop() map(loop.remove_signal_handler, self._signals.keys()) loop.set_exception_handler(None) async def _cancel_all_tasks(self) -> None: # we don't want to cancel this task, so filter all_tasks # generator to filter in place pending = (task for task in asyncio.all_tasks() if task is not asyncio.current_task()) for task in pending: task.cancel() with contextlib.suppress(asyncio.CancelledError): await task def _handle_exception( self, loop: asyncio.AbstractEventLoop, context: dict, ) -> None: if "exception" in context: exc = context["exception"] # CancelledErrors happen when we simply cancel the main task during # a normal restart procedure if not isinstance(exc, asyncio.CancelledError): logger.exception("Exception in event loop:", exc_info=exc) # noqa: G202 else: logger.error("unhandled error in event loop: %s", context["msg"]) class QtileEventLoopPolicy(asyncio.DefaultEventLoopPolicy): """ Asyncio policy to ensure the main event loop is accessible even if `get_event_loop()` is called from a different thread. """ def __init__(self, qtile: Qtile) -> None: asyncio.DefaultEventLoopPolicy.__init__(self) self.qtile = qtile def get_event_loop(self) -> asyncio.AbstractEventLoop: if isinstance(self.qtile._eventloop, asyncio.AbstractEventLoop): return self.qtile._eventloop raise RuntimeError qtile-0.31.0/libqtile/ipc.py0000664000175000017500000002201214762660347015565 0ustar epsilonepsilon# Copyright (c) 2008, Aldo Cortesi. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """ A simple IPC mechanism for communicating between two local processes. We use marshal to serialize data - this means that both client and server must run the same Python version, and that clients must be trusted (as un-marshalling untrusted data can result in arbitrary code execution). """ from __future__ import annotations import asyncio import fcntl import json import marshal import os.path import socket import struct from typing import Any from libqtile.log_utils import logger from libqtile.utils import get_cache_dir HDRFORMAT = "!L" HDRLEN = struct.calcsize(HDRFORMAT) SOCKBASE = "qtilesocket.%s" class IPCError(Exception): pass def find_sockfile(display: str | None = None): """ Finds the appropriate socket file for the given display. If unspecified, the socket file is determined as follows: - If WAYLAND_DISPLAY is set, use it. - else if DISPLAY is set, use that. - else check for the existence of a socket file for WAYLAND_DISPLAY=wayland-0 and if it exists, use it. - else check for the existence of a socket file for DISPLAY=:0 and if it exists, use it. - else raise an IPCError. """ cache_directory = get_cache_dir() if display: return os.path.join(cache_directory, SOCKBASE % display) display = os.environ.get("WAYLAND_DISPLAY") if display: return os.path.join(cache_directory, SOCKBASE % display) display = os.environ.get("DISPLAY") if display: return os.path.join(cache_directory, SOCKBASE % display) sockfile = os.path.join(cache_directory, SOCKBASE % "wayland-0") if os.path.exists(sockfile): return sockfile sockfile = os.path.join(cache_directory, SOCKBASE % ":0") if os.path.exists(sockfile): return sockfile raise IPCError("Could not find socket file.") class _IPC: """A helper class to handle properly packing and unpacking messages""" @staticmethod def unpack(data: bytes, *, is_json: bool | None = None) -> tuple[Any, bool]: """Unpack the incoming message Parameters ---------- data: bytes The incoming message to unpack is_json: bool | None If the message should be unpacked as json. By default, try to unpack json and fallback gracefully to marshalled bytes. Returns ------- tuple[Any, bool] A tuple of the unpacked object and a boolean denoting if the message was deserialized using json. If True, the return message should be packed as json. """ if is_json is None or is_json: try: return json.loads(data.decode()), True except ValueError as e: if is_json: raise IPCError("Unable to decode json data") from e try: assert len(data) >= HDRLEN size = struct.unpack(HDRFORMAT, data[:HDRLEN])[0] assert size >= len(data[HDRLEN:]) return marshal.loads(data[HDRLEN : HDRLEN + size]), False except AssertionError as e: raise IPCError("error reading reply! (probably the socket was disconnected)") from e @staticmethod def pack(msg: Any, *, is_json: bool = False) -> bytes: """Pack the object into a message to pass""" if is_json: json_obj = json.dumps(msg, default=_IPC._json_encoder) return json_obj.encode() msg_bytes = marshal.dumps(msg) size = struct.pack(HDRFORMAT, len(msg_bytes)) return size + msg_bytes @staticmethod def _json_encoder(field: Any) -> Any: """Convert non-serializable types to ones understood by stdlib json module""" if isinstance(field, set): return list(field) raise ValueError(f"Tried to JSON serialize unsupported type {type(field)}: {field}") class Client: def __init__(self, socket_path: str, is_json=False) -> None: """Create a new IPC client Parameters ---------- socket_path: str The file path to the file that is used to open the connection to the running IPC server. is_json: bool Pack and unpack messages as json """ self.socket_path = socket_path self.is_json = is_json def call(self, data: Any) -> Any: return self.send(data) def send(self, msg: Any) -> Any: """Send the message and return the response from the server If any exception is raised by the server, that will propogate out of this call. """ return asyncio.run(self.async_send(msg)) async def async_send(self, msg: Any) -> Any: """Send the message to the server Connect to the server, then pack and send the message to the server, then wait for and return the response from the server. """ try: reader, writer = await asyncio.wait_for( asyncio.open_unix_connection(path=self.socket_path), timeout=3 ) except (ConnectionRefusedError, FileNotFoundError): raise IPCError(f"Could not open {self.socket_path}") try: send_data = _IPC.pack(msg, is_json=self.is_json) writer.write(send_data) writer.write_eof() read_data = await asyncio.wait_for(reader.read(), timeout=10) except asyncio.TimeoutError: raise IPCError("Server not responding") finally: # see the note in Server._server_callback() writer.close() await writer.wait_closed() data, _ = _IPC.unpack(read_data, is_json=self.is_json) return data class Server: def __init__(self, socket_path: str, handler) -> None: self.socket_path = socket_path self.handler = handler self.server = None # type: asyncio.AbstractServer | None if os.path.exists(socket_path): os.unlink(socket_path) self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM, 0) flags = fcntl.fcntl(self.sock.fileno(), fcntl.F_GETFD) fcntl.fcntl(self.sock.fileno(), fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC) self.sock.bind(self.socket_path) async def _server_callback( self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter ) -> None: """Callback when a connection is made to the server Read the data sent from the client, execute the requested command, and send the reply back to the client. """ try: logger.debug("Connection made to server") data = await reader.read() logger.debug("EOF received by server") req, is_json = _IPC.unpack(data) except IPCError: logger.warning("Invalid data received, closing connection") else: rep = self.handler(req) result = _IPC.pack(rep, is_json=is_json) logger.debug("Sending result on receive EOF") writer.write(result) logger.debug("Closing connection on receive EOF") writer.write_eof() finally: writer.close() await writer.wait_closed() async def __aenter__(self) -> Server: """Start and return the server""" await self.start() return self async def __aexit__(self, _exc_type, _exc_value, _tb) -> None: """Close and shutdown the server""" await self.close() async def start(self) -> None: """Start the server""" assert self.server is None logger.debug("Starting server") server_coroutine = asyncio.start_unix_server(self._server_callback, sock=self.sock) self.server = await server_coroutine async def close(self) -> None: """Close and shutdown the server""" assert self.server is not None logger.debug("Stopping server on close") self.server.close() await self.server.wait_closed() self.server = None qtile-0.31.0/libqtile/resources/0000775000175000017500000000000014762660347016455 5ustar epsilonepsilonqtile-0.31.0/libqtile/resources/sleep.py0000664000175000017500000001637114762660347020147 0ustar epsilonepsilon# Copyright (c) 2022-23, elParaguayo. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations import fcntl import os from libqtile import hook from libqtile.log_utils import logger from libqtile.utils import create_task try: from dbus_fast.aio import MessageBus from dbus_fast.constants import BusType from dbus_fast.errors import DBusError has_dbus = True except ImportError: has_dbus = False LOGIND_SERVICE = "org.freedesktop.login1" LOGIND_INTERFACE = LOGIND_SERVICE + ".Manager" LOGIND_PATH = "/org/freedesktop/login1" class Inhibitor: """ Class definition to access systemd's login1 service on dbus. Listens for `PrepareForSleep` signals and fires appropriate hooks when the signal is received. Can also set a sleep inhibitor which will be run after the "suspend" hook has been fired. This helps hooked functions to complete before the system goes to sleep. However, the inhibitor is set to only delay sleep, not block it. """ def __init__(self) -> None: self.bus: MessageBus | None = None self.sleep = False self.resume = False self.fd: int = -1 def want_sleep(self) -> None: """ Convenience method to set flag to show we want to know when the system is going down for sleep. """ if not has_dbus: logger.warning("dbus-fast must be installed to listen to sleep signals") self.sleep = True def want_resume(self) -> None: """ Convenience method to set flag to show we want to know when the system is waking from sleep. """ if not has_dbus: logger.warning("dbus-fast must be installed to listen to resume signals") self.resume = True def start(self) -> None: """ Will create connection to dbus only if we want to listen out for a sleep or wake signal. """ if not has_dbus: return if not (self.sleep or self.resume): return create_task(self._start()) async def _start(self) -> None: """ Creates the bus connection and connects to the org.freedesktop.login1.Manager interface. Starts an inhibitor if we are listening for sleep events. Attaches handler to the "PrepareForSleep" signal. """ # Connect to bus and Manager interface try: self.bus = await MessageBus(bus_type=BusType.SYSTEM, negotiate_unix_fd=True).connect() except FileNotFoundError: self.bus = None logger.warning( "Could not find logind service. Suspend and resume hooks will be unavailable." ) return try: introspection = await self.bus.introspect(LOGIND_SERVICE, LOGIND_PATH) except DBusError: logger.warning( "Could not find logind service. Suspend and resume hooks will be unavailable." ) self.bus.disconnect() self.bus = None return obj = self.bus.get_proxy_object(LOGIND_SERVICE, LOGIND_PATH, introspection) self.login = obj.get_interface(LOGIND_INTERFACE) # If we want to know when the system is sleeping when we request an inhibitor if self.sleep: self.take() # Finally, attach a handler for the "PrepareForSleep" signal self.login.on_prepare_for_sleep(self.prepare_for_sleep) def take(self) -> None: """Create an inhibitor.""" # Shouldn't happen but, if we already have an inhibitor in place, # close it before requesting a new one if self.fd > 0: self.release() # Check that inhibitor was released if self.fd < 0: create_task(self._take()) async def _take(self) -> None: """Sends the request to dbus to create an inhibitor.""" # The "Inhibit" method returns a file descriptor self.fd = await self.login.call_inhibit( "sleep", # what: The lock type. We only want to inhibit sleep "qtile", # who: Name of program requesting inhibitor "Run hooked functions before suspend", # why: Short description of purpose "delay", # mode: "delay" or "block" ) # We need to set CLOEXEC flag for the file descriptor # See: https://github.com/qtile/qtile/pull/4388#issuecomment-1675410090 # for explanation flags = fcntl.fcntl(self.fd, fcntl.F_GETFD) fcntl.fcntl(self.fd, fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC) def release(self) -> None: """Closes the file descriptor to release the inhibitor.""" if self.fd > 0: os.close(self.fd) else: logger.warning("No inhibitor available to release.") try: os.fstat(self.fd) except OSError: # File descriptor was successfully closed self.fd = -1 else: # We could read the file descriptor so it's still open logger.warning("Unable to release inhibitor.") def prepare_for_sleep(self, start: bool) -> None: """ Handler for "PrepareForSleep" signal. Value of "sleep" is: - True when the machine is about to sleep - False when the event is over i.e. the machine has woken up """ if start: hook.fire("suspend") # Note: lock is released after the "suspend" hook has been fired # Hooked functions should therefore be synchronous to ensure they # complete before the inhbitor is released. self.release() else: # If we're listening for suspend events, we need to request a new # inhibitor if self.sleep: self.take() hook.fire("resume") def stop(self) -> None: """ Deactivates the inhibitor, removing lock and signal handler before closing bus connection. """ if not has_dbus or self.bus is None: return if self.fd > 0: self.release() if self.sleep or self.resume: self.login.off_prepare_for_sleep(self.prepare_for_sleep) self.bus.disconnect() self.bus = None inhibitor = Inhibitor() qtile-0.31.0/libqtile/resources/default_config.py0000664000175000017500000002240014762660347021776 0ustar epsilonepsilon# Copyright (c) 2010 Aldo Cortesi # Copyright (c) 2010, 2014 dequis # Copyright (c) 2012 Randall Ma # Copyright (c) 2012-2014 Tycho Andersen # Copyright (c) 2012 Craig Barnes # Copyright (c) 2013 horsik # Copyright (c) 2013 Tao Sauvage # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from libqtile import bar, layout, qtile, widget from libqtile.config import Click, Drag, Group, Key, Match, Screen from libqtile.lazy import lazy from libqtile.utils import guess_terminal mod = "mod4" terminal = guess_terminal() keys = [ # A list of available commands that can be bound to keys can be found # at https://docs.qtile.org/en/latest/manual/config/lazy.html # Switch between windows Key([mod], "h", lazy.layout.left(), desc="Move focus to left"), Key([mod], "l", lazy.layout.right(), desc="Move focus to right"), Key([mod], "j", lazy.layout.down(), desc="Move focus down"), Key([mod], "k", lazy.layout.up(), desc="Move focus up"), Key([mod], "space", lazy.layout.next(), desc="Move window focus to other window"), # Move windows between left/right columns or move up/down in current stack. # Moving out of range in Columns layout will create new column. Key([mod, "shift"], "h", lazy.layout.shuffle_left(), desc="Move window to the left"), Key([mod, "shift"], "l", lazy.layout.shuffle_right(), desc="Move window to the right"), Key([mod, "shift"], "j", lazy.layout.shuffle_down(), desc="Move window down"), Key([mod, "shift"], "k", lazy.layout.shuffle_up(), desc="Move window up"), # Grow windows. If current window is on the edge of screen and direction # will be to screen edge - window would shrink. Key([mod, "control"], "h", lazy.layout.grow_left(), desc="Grow window to the left"), Key([mod, "control"], "l", lazy.layout.grow_right(), desc="Grow window to the right"), Key([mod, "control"], "j", lazy.layout.grow_down(), desc="Grow window down"), Key([mod, "control"], "k", lazy.layout.grow_up(), desc="Grow window up"), Key([mod], "n", lazy.layout.normalize(), desc="Reset all window sizes"), # Toggle between split and unsplit sides of stack. # Split = all windows displayed # Unsplit = 1 window displayed, like Max layout, but still with # multiple stack panes Key( [mod, "shift"], "Return", lazy.layout.toggle_split(), desc="Toggle between split and unsplit sides of stack", ), Key([mod], "Return", lazy.spawn(terminal), desc="Launch terminal"), # Toggle between different layouts as defined below Key([mod], "Tab", lazy.next_layout(), desc="Toggle between layouts"), Key([mod], "w", lazy.window.kill(), desc="Kill focused window"), Key( [mod], "f", lazy.window.toggle_fullscreen(), desc="Toggle fullscreen on the focused window", ), Key([mod], "t", lazy.window.toggle_floating(), desc="Toggle floating on the focused window"), Key([mod, "control"], "r", lazy.reload_config(), desc="Reload the config"), Key([mod, "control"], "q", lazy.shutdown(), desc="Shutdown Qtile"), Key([mod], "r", lazy.spawncmd(), desc="Spawn a command using a prompt widget"), ] # Add key bindings to switch VTs in Wayland. # We can't check qtile.core.name in default config as it is loaded before qtile is started # We therefore defer the check until the key binding is run by using .when(func=...) for vt in range(1, 8): keys.append( Key( ["control", "mod1"], f"f{vt}", lazy.core.change_vt(vt).when(func=lambda: qtile.core.name == "wayland"), desc=f"Switch to VT{vt}", ) ) groups = [Group(i) for i in "123456789"] for i in groups: keys.extend( [ # mod + group number = switch to group Key( [mod], i.name, lazy.group[i.name].toscreen(), desc=f"Switch to group {i.name}", ), # mod + shift + group number = switch to & move focused window to group Key( [mod, "shift"], i.name, lazy.window.togroup(i.name, switch_group=True), desc=f"Switch to & move focused window to group {i.name}", ), # Or, use below if you prefer not to switch to that group. # # mod + shift + group number = move focused window to group # Key([mod, "shift"], i.name, lazy.window.togroup(i.name), # desc="move focused window to group {}".format(i.name)), ] ) layouts = [ layout.Columns(border_focus_stack=["#d75f5f", "#8f3d3d"], border_width=4), layout.Max(), # Try more layouts by unleashing below layouts. # layout.Stack(num_stacks=2), # layout.Bsp(), # layout.Matrix(), # layout.MonadTall(), # layout.MonadWide(), # layout.RatioTile(), # layout.Tile(), # layout.TreeTab(), # layout.VerticalTile(), # layout.Zoomy(), ] widget_defaults = dict( font="sans", fontsize=12, padding=3, ) extension_defaults = widget_defaults.copy() screens = [ Screen( bottom=bar.Bar( [ widget.CurrentLayout(), widget.GroupBox(), widget.Prompt(), widget.WindowName(), widget.Chord( chords_colors={ "launch": ("#ff0000", "#ffffff"), }, name_transform=lambda name: name.upper(), ), widget.TextBox("default config", name="default"), widget.TextBox("Press <M-r> to spawn", foreground="#d75f5f"), # NB Systray is incompatible with Wayland, consider using StatusNotifier instead # widget.StatusNotifier(), widget.Systray(), widget.Clock(format="%Y-%m-%d %a %I:%M %p"), widget.QuickExit(), ], 24, # border_width=[2, 0, 2, 0], # Draw top and bottom borders # border_color=["ff00ff", "000000", "ff00ff", "000000"] # Borders are magenta ), # You can uncomment this variable if you see that on X11 floating resize/moving is laggy # By default we handle these events delayed to already improve performance, however your system might still be struggling # This variable is set to None (no cap) by default, but you can set it to 60 to indicate that you limit it to 60 events per second # x11_drag_polling_rate = 60, ), ] # Drag floating layouts. mouse = [ Drag([mod], "Button1", lazy.window.set_position_floating(), start=lazy.window.get_position()), Drag([mod], "Button3", lazy.window.set_size_floating(), start=lazy.window.get_size()), Click([mod], "Button2", lazy.window.bring_to_front()), ] dgroups_key_binder = None dgroups_app_rules = [] # type: list follow_mouse_focus = True bring_front_click = False floats_kept_above = True cursor_warp = False floating_layout = layout.Floating( float_rules=[ # Run the utility of `xprop` to see the wm class and name of an X client. *layout.Floating.default_float_rules, Match(wm_class="confirmreset"), # gitk Match(wm_class="makebranch"), # gitk Match(wm_class="maketag"), # gitk Match(wm_class="ssh-askpass"), # ssh-askpass Match(title="branchdialog"), # gitk Match(title="pinentry"), # GPG key password entry ] ) auto_fullscreen = True focus_on_window_activation = "smart" reconfigure_screens = True # If things like steam games want to auto-minimize themselves when losing # focus, should we respect this or not? auto_minimize = True # When using the Wayland backend, this can be used to configure input devices. wl_input_rules = None # xcursor theme (string or None) and size (integer) for Wayland backend wl_xcursor_theme = None wl_xcursor_size = 24 # XXX: Gasp! We're lying here. In fact, nobody really uses or cares about this # string besides java UI toolkits; you can see several discussions on the # mailing lists, GitHub issues, and other WM documentation that suggest setting # this string if your java app doesn't work correctly. We may as well just lie # and say that we're a working one by default. # # We choose LG3D to maximize irony: it is a 3D non-reparenting WM written in # java that happens to be on java's whitelist. wmname = "LG3D" qtile-0.31.0/libqtile/resources/layout-icons/0000775000175000017500000000000014762660347021103 5ustar epsilonepsilonqtile-0.31.0/libqtile/resources/layout-icons/layout-max.png0000664000175000017500000000111614762660347023710 0ustar epsilonepsilonPNG  IHDR@@iqbKGDC pHYs  tIME dC[iTXtCommentCreated with GIMPd.eIDATxQ _̆ (1apXu(rlpl""+ <'ˆp'a2! x&Dςxyh@_>B@4p_Ԓex2 qъ!$pH hϲɖ@9`RvN0  BBr"^`\b*&ś!`&XDwCH/!4j&,#0!.M1`"⛃Tv⻷ʅÙ#>y̐|*xJ;'$Lpf` loF|@"oy:$5/Rn}D%=j^x61@aL&d0\]Emy@Nsx"K fS2PV  fX΃"T$;v8l^2ك}IENDB`qtile-0.31.0/libqtile/resources/layout-icons/layout-stack.png0000664000175000017500000000076714762660347024243 0ustar epsilonepsilonPNG  IHDR@@iqbKGD pHYs  tIME  ''TiTXtCommentCreated with GIMPd.e[IDATx C2NU% 4P\-qZkrIN7Qk5d1]|O,4*,0j&@{ex`n!ʄkw%rNҀ4@:Xk+`w6DՈH3Z ~T3"hGT^iv<8)7l ]v4W3V?p'. a}2tP4aV/T߾0/Y HA M`V'Re1+[Z"#gtG<kA c$'P`E̩*ģ0Nv84FјylĂ IENDB`qtile-0.31.0/libqtile/resources/layout-icons/layout-plasma.png0000664000175000017500000001057014762660347024404 0ustar epsilonepsilonPNG  IHDR@@iqeXIfII* @@(1 2iCreated with GIMP77GIMP 2.10.362024:04:28 06:49:31Created with GIMPiCCPICC profilex}=H@_H"2TE8jP! :\MGbYWWAqvpRtZxp܏wwT38 edRI!_¯"F1&1S4<=||K,s>`2'2ݰ77->q%xܠ ?r]vsa?όN,ue{ϴr{(% xiTXtXML:com.adobe.xmp )hbKGDC pHYs  tIME1"ztEXtCommentCreated with GIMPWIDATxۻ 0 Fwq %'CCsU#3s4c4/BDf<ӫ]O#hWl#@ՊS\2͆>O; ɦ[73aub4u5'IENDB`qtile-0.31.0/libqtile/resources/layout-icons/layout-verticaltile.png0000664000175000017500000000076414762660347025622 0ustar epsilonepsilonPNG  IHDR@@iqbKGDC pHYs  tIME B NiTXtCommentCreated with GIMPd.eXIDATxZ Ӂ7DAL㣰iB6ݽj9| ff}&OОɦ5<# 5< +wcAA DV *D ~#])W Ѿ1@, 4G_- &kA pμT#$D@agFN+Y+-/H|  sQBJ&ߝ#`рPI C-o[F˶N [ş"ˠTE!5 AR@pl?*#`XdeH䤘FPbAAp4sIENDB`qtile-0.31.0/libqtile/resources/layout-icons/layout-bsp.png0000664000175000017500000000077714762660347023723 0ustar epsilonepsilonPNG  IHDR@@iqbKGDC pHYs  tIME 'ӘiTXtCommentCreated with GIMPd.ecIDATxK DQePm0 7i#Zk ݀fo,c:,}€ i{H& <Dg,aB^ʄJpV2 (awhAvUM8~ENjI8 ~V3NюSGn.^oGƒvӉ=oVp pфQؽa&,Y> 2hVF?\}<?r Z.zƼC?.u lߩ 7.mIO8?S`gpggdmxQ{9h^_qs/%tEXtdate:create2022-02-13T19:05:57+00:00%tEXtdate:modify2022-02-13T19:05:57+00:00~tEXtSoftwarewww.inkscape.org<IENDB`qtile-0.31.0/libqtile/resources/layout-icons/layout-matrix.png0000664000175000017500000000077714762660347024443 0ustar epsilonepsilonPNG  IHDR@@iqbKGDC pHYs  tIME ?iTXtCommentCreated with GIMPd.ecIDATx DaezIYmǧJ\QƔR Q\1,'#V}) _v!@ XKXVD%x ` ^ZXVD#-Q8 QNipٜ4 Nŕs` sO5VboBG0Gu87F+No'·ĎSkJHk_; 5I~}h}pWǀQ;WE\v5Fo0! @|$#0v<Xk*z/F$#5 ]b9HrM5`x5֢Eknhk\ijl3IENDB`qtile-0.31.0/libqtile/resources/layout-icons/layout-monadwide.png0000664000175000017500000000076414762660347025102 0ustar epsilonepsilonPNG  IHDR@@iqbKGD pHYs  tIME 8/iTXtCommentCreated with GIMPd.eXIDATx Du5D1rIr).C9_'OFJ* `,@-Zecx^D'x  ^[x^D`^`Z 0((rpf<ڸ_#(>"|/%#*|+"÷ :Ka c۶D=Dxc/Eh /N; 3|J0 ! @Y?,SPG`BePMQ6.GFߡrKiv` v-f &ij5hapPq9 IENDB`qtile-0.31.0/libqtile/resources/layout-icons/layout-zoomy.png0000664000175000017500000000045414762660347024304 0ustar epsilonepsilonPNG  IHDR@@iqbKGDC pHYs  tIME ;6iTXtCommentCreated with GIMPd.eIDATxA @W/{N7ɱP+TU0䆅{"p}jE=6(;,FN(yWqIENDB`qtile-0.31.0/libqtile/resources/layout-icons/layout-monadtall.png0000664000175000017500000000100214762660347025070 0ustar epsilonepsilonPNG  IHDR@@iqbKGDC pHYs  tIME 1֑aiTXtCommentCreated with GIMPd.efIDATxK0D(2FQ;eRjDU|x||Q p\_ v-'q0GMTD`D?#E@( >Bd=E@Vx/,Q(l(bMx3?P EPkl$)]{sN=b7֜#|KaU+,V))AV,G~pV+P3| BkW4B8XaKjl$@#`zw-0RɬF<#դ85]n5ȳt-5`-1k@-a> 9e:J0^>;L+L>~+lv8lIENDB`qtile-0.31.0/libqtile/resources/layout-icons/layout-tile.png0000664000175000017500000000062714762660347024066 0ustar epsilonepsilonPNG  IHDR@@iqbKGDC pHYs  tIME 1kx,iTXtCommentCreated with GIMPd.eIDATx0 EQ^G@ iHZV/Ö@uu$;vqw.@VD8\ŷX#qHŹ@VbI_xqл#{[)_yXƷ={GBhkt¢"'?2& JRύ f>ݻ@g(-bO-Wxvdbht?gIENDB`qtile-0.31.0/libqtile/resources/layout-icons/layout-monadthreecol.png0000664000175000017500000001432614762660347025756 0ustar epsilonepsilonPNG  IHDR@@iqzTXtRaw profile type exifxڵYY()f㰘s N;]^z2`,B IS?$i N蠃Svn@Im}F;Ÿc_-"ʬb=  J2^<ݗj%hVNZc8j99g= U ?2Ұ/]%0Y 5@_Q5tJ GB$]pǘ[۬6:A{{\ּ7ۅY-g_e^\tцZMih-PjQ]_b+,noOqp2eDZuWEQT1sYLRF̪;J*_r!hÆ6\~*4R@hN/=X+ǒ $(&n`DaT |UA`uɈչT#ZǞpe0d2 Cz]*-R+eAc'ZWidoFVUV9pTYZ>2c3y:9:3:#vDL*dM.( :l>, -J Wlf;aVUT[U_C?/X5٘Z[DN'9cR c3gZKf9|0 S35Tn0G2;7W戩{OގX+=,ү?+&2=\I-X.f {My:AG[x/ЁS&UKGb}ej{m.YKoi d5pP׷lk i08̴1kDVHɃI7 sWXzıL{ltokcsݨ涕jo}0= &*4WXzoUD h7M8*w\Gw<*T1Z >G [aGܗJ@Sl..t|5 @6(dc\tAΞ=5CY4F#8F@6F,6;G^M4ne4e|!dG2lO 7{TQ5B9UpHU&ڈ(xPR;a}c^cCsk8 q F(k),! JV}JNa+`B-~r&8Rwjx+cU|"̄l oֿVdլFq13YЦ{#{J0p#r^l>MM/w,^-JrڸB{W[W W5&fY[[|s*cP;]4J|}?咽-Q]@Qn}I]nGķ `T^ٖ8q~0n?O8O8O !Km=o؅9 WBoɠs-,ټodΞyG:ze^Љ~KvA3}hP5muN}TH=y| tqb@yvq7O|~:<aKi*}ʺއk1H,jP1@s{׺i#^q-{pebz2Aer);9YAԎ*In#w.tK㊊NJpݰzِS/m]j5H"ԗ* zxLS<ȅõҔ|Frsp6Um g43#P_|B@3~rz2 iTXtXML:com.adobe.xmp :ObKGD pHYs  tIME 40tEXtCommentCreated with GIMPWIDATxA P'mJ`_R<*Eff+ "bvxMV9W oV .Gb_đ ǽ `^@ (4qQIENDB`qtile-0.31.0/libqtile/resources/layout-icons/layout-wmii.png0000664000175000017500000000072514762660347024075 0ustar epsilonepsilonPNG  IHDR@@iqbKGDC pHYs  tIME  W|f9iTXtCommentCreated with GIMPd.e9IDATxK0 (YUjL$6Yd߳j vCD#.5x=yǘ4E"}eD-Ezմ/)4 JP%`#g,ɀoȮ%pjoMgCeh+Ғ L+ҒfU>{ 4`h6e+hM(y`d`t>LgzwHWV/e\!![kO&sFܑS`vo|hu9!i0L_@`t(#D/UH$RG<j-D‰/Fn{뛡""뚗cHH$GWp:+YmDs㡟ɝ*U`R++4K\ EoaCE@>"IENDB`qtile-0.31.0/libqtile/resources/layout-icons/layout-unknown.png0000664000175000017500000000150614762660347024625 0ustar epsilonepsilonPNG  IHDR@@iqbKGD pHYs  tIME .,*~_''Yw N ;`E4EO\T{>*ޢ)VIGh?KK@((&Hk"O`phě'q`m'#䜻YyιV\kD ȹ|>55e*];!`9W(Tۭʿ#>Xk/5b  @ |;,K@ w>FIENDB`qtile-0.31.0/libqtile/resources/layout-icons/layout-screensplit.png0000664000175000017500000001546314762660347025470 0ustar epsilonepsilonPNG  IHDR@@iq UzTXtRaw profile type exifxڭZiU̴0朷cg/]ҕP?3bmVo KP7^T%{[W|zN GW>՟ox{?qg/by hVO肴u׮ ޝ׾ߢJ}Q9pXYQ}NjR k)qxWAwu5PC\Ɯmsm ,Rt|q'w7yۿgX/XG+u|z.EwMp}.ba 7oa\b+Y/',k=n".ŊHKR"eu+T\ԦJt}RPfgX%rp Pz>h5tuP[D$do±$VK/DdF5Ǹj h=6E&"y8@k dk']`",m@@X)处!=P]E% QJEk pXsCQ1XU/0,ZqEboV[BT% %#)r`ב#[䈡WP˜%֊X C6ЈJT!>_Ӏooܒʖ~ٱxVh(ޚ"V p̣.*7 )WU¬[??'c6MӖ81,v!Ek}X]dSL%cFXx÷I`! Kf-rghR1u)Q7q˵Zߍ7|Ǎ9<6(;C04!8h 6 &J2XJ(H296T&ZiyhiⱢu#c`X: 9NU4:D[w AuU Rnϸ+wq†mVǓ:Z/hq ./Ano"[/߆lHtX.B@ r|JWmvbNG<)LrN`;A䬰yT@vqz q&c*7uspocuX6djìd̆$6u4c3|ix2qbH6h󼵟iIkqN2o)^Ž<9x Doa * c^'*19 S +dִ3.]BqGPsF;Aķzɼ$ mŕ#Y^gy''KyKRJ=5UPj}MaB{ h*s\ 'Av4 ta- ) zeeI-eх3@{ BuǝbL+΢:NUN =f#f\˷j>4 ?k8y*zT֛(ӈtْ6%dͮёe)EM +}[d;248]_wI~I~rn?!IwPGRLWmҬo8[ Mw#x]Uu`L?ԫA`ݻ[B_8!2pϏ 0 y8hDQp46"p.6w IZMM#qh ۛ?G[b8"W*E9w'<\4R Ú2Ձ2 +7sKCK]+N[?֕To]!c 1lU[|lsC}J?Lǜ27)^^k~S3e43z=G6]EsEy?GgaV׉6+}0+*9I$~ qX,ƬhjēU)_ȸr⬕yOPN_^:a$EH PAV )ڏ{D.\`Gd17):_lcmv?Wz_ӟZZ.[\Olʎ)}SVޚ8}U88F `{oirM> xiTXtXML:com.adobe.xmp nt6bKGDC pHYs  tIME'9tEXtCommentCreated with GIMPW%IDATx0 Dk%B/U=r2q֎ңP "dwf)& fQf0 A?']1֪ul w3uiઈ{8jo!zwtggvYÊeue6?mg4Y,ÊeJ XfJV3gX_CVH4x V3) ז,?г-@P[/D'yIENDB`qtile-0.31.0/libqtile/resources/__init__.py0000664000175000017500000000000014762660347020554 0ustar epsilonepsilonqtile-0.31.0/libqtile/resources/battery-icons/0000775000175000017500000000000014762660347021240 5ustar epsilonepsilonqtile-0.31.0/libqtile/resources/battery-icons/battery-full-charging.png0000664000175000017500000000234214762660347026141 0ustar epsilonepsilonPNG  IHDRw=sBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org< tEXtTitleBatteryUD?PtEXtAuthorLapo Calamandreiߑ*#IDATHMlTUwf:SLVj@Rc$1.q! 1$,P4~ĸ 1H ȇ"„v25@5)-ͻt,eD҄9ォD[?x˻'|>d |TRjR -ohMNAD I$#eeYwklLQ8̰ I 5r"H PWo*0;zVtV{iW"yb[6Q^ƒ\>CM[qe#Z;µ&[;{pk//+ybըãXdRc;ǃ'o쁸"J),t:m_q6W=fPeOo; vK}^),qGOQ^A.s˻,^;BA*]EXE!1L@j7FfG\"\O>;@*&@t;8"f su#,vɷ|y;PCb)#&d=W_Yg )%sgbOGnP}T !DbʖkjB2' `]$CRuhL.xqF:[T,Z|ݗ/Spf/-+SUu=O&yJ?P*zÆ dfu%mTE\.URU˲5%,tGJ)-vm7N5\].,2{gسl=skcaxDmFizJ=xRT/v~wof&O>գlӒI!%ucH^`J]71sBߋ* hoKf,-%-SaJ=xUA~l\CZ*c}$i9EN|迋0 v$]׭ήxӻo>vDj,{>¡q$ "G D"]_寺~9w*?sS"y k0ʨ@um)<4<9.Y\^iW.6 bf{ d5 @8܊D"NSS8HRJ;r\Ck-KhYp=#c*-Zm]f_RWE(HExus7d*Ձd @JYP8 eKJY^RJ 2!. R2`rP!bfh~B/j͑ ,+Ձf0,4-̎TpuDFQ4VoD2s" 譭n_8899[{gvu(Q"3sy=B<ϫAfYf]M0CG@`f=ooagα,IENDB`qtile-0.31.0/libqtile/resources/battery-icons/battery-missing.png0000664000175000017500000000156214762660347025073 0ustar epsilonepsilonPNG  IHDRw=sBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org< tEXtTitleBatteryUD?PtEXtAuthorLapo Calamandreiߑ*IDATHOQAc@&*3t#QcLt_,NT&?6L'bb$ "B}9@ PJo};s FGGN}ŅK2fgg/777ƺ08 `eesg0TKKKˋG+5tb|||&rdB9mNxZS52sY3V `5yTJY:,Q4%T([#"0T/1b(Zk3#ZkW Ak}e7J)!N>F O~xBSLpE?=YCk \zn:Rʠ&1@o_^x녔į, J}~uuUHmBlYJ4?H*< v{ucx, " 'D9\5hP_fn K|έK,`!ߺi}_U:KO$|G I'I4f {=ُ՝;T*'8УɵZmܒ*Z6y2ZDr@ RJ@)334+0=Z H)0FqB}o@WRhV@DRJ?>x'|ޱH7AJ)Ȼ>X!}?S1uv ])bd!*>.>];38~lCoP,} =` 3A!+J/AmE??@qɩs3rzu[VWBYNQ#zC$\7Ds0sp-0˟;Y(D/dk$0 1tmXjy Z3R09^6YaZ5FcѠeYFF!099<$ 0Q }FݓG:[-` 5NcZm4ц2ăԽc]/;FՕsR9nvɬeT 0V`5w9jD[f/ mG"iT64qn=*^h2K5^{D,&P竢q6̢lɗdбÝz+~>HIENDB`qtile-0.31.0/libqtile/resources/battery-icons/battery-full.png0000664000175000017500000000130314762660347024355 0ustar epsilonepsilonPNG  IHDRw=sBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org< tEXtTitleBatteryUD?PtEXtAuthorLapo Calamandreiߑ*IDATH=kQ3dA ˀ]"ƅ`'Uw?V X! jL@dqea"&{y{sEU'ZՋg1@ǏoF;?7ܽAi2 xmm{(zfZXk3p9U%Fᜫ*äEdqџ|Xk Z?C6 2Ѐ*"c#08pr&>cUAoeY+W%PESTụ1 iVK "-`1˲Υ[E1X+m`qR"Q[ӹz\<{ "cU-yCu-AKfUe`QV1 FUTE_[~dHpz iIENDB`qtile-0.31.0/libqtile/resources/battery-icons/battery-caution.png0000664000175000017500000000216214762660347025061 0ustar epsilonepsilonPNG  IHDRw=sBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org< tEXtTitleBatteryUD?PtEXtAuthorLapo Calamandreiߑ*IDATH]h\Egfu7! XCh5Ҡ>>XhIHJbQQ ~D| *E !/&AD 4&ƻ3ΌɦIu?g9dō4qu7Nc1ze/.(JD800"6 Zlih~~>fRZ)=3yNDV ?]n5ِjڭm=X^^$BwGo8kKKK?...69竫S=22~X\3:`!".0 ՞0 1VkpqHJ #" c( "& j. Eκ8|SJk cZ3!R03HnfPN"LqI)&''tɉ9RN&@J(:@sT*2c- YogoM= ] /O掿w> Zi TVUI@-!ÉFP9^ADb>ouww;Ƙsssb>(X, ܇iV#YX gZ[U/h;o$"8<(E޻_SEhf&Dd0tZKˋbc,-/ "%zTX[Ok_d@Rxqfj* n]/4-,,z+i}U&pYiC"al"" @A@9[dYe@TnQنS@, (0$Ql}~WrDr0q|,="E"Z;oF"nj+0JDaMW ƣ(,s̼ `@oX?D#~O0B[~Uo ԜIENDB`qtile-0.31.0/libqtile/resources/battery-icons/battery-caution-charging.png0000664000175000017500000000244314762660347026643 0ustar epsilonepsilonPNG  IHDRw=sBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org< tEXtTitleBatteryUD?PtEXtAuthorLapo Calamandreiߑ*dIDATH[lUgfV텭@iIaP* ,Z"!<@E}   hbcEB6HV6Ks=>vAD dΙ/wϜ#@xP2h3 ڜR]ow>(9L L}<'O7oE"'4`=\׵EBU48u{ 8c{']uUu\׵|Ɇq1R`Yֽ l_yWrH ٶm<)i¶M\8 s IXy뺮iwcY~s@>x(_UcSG-*Vɝc&<PTUeY%KzW;y=Url`$;i=NLY O,YUU0M77 |m嬾OVc7Hz&5۶=)ii0MWoUNӕy+?4Eu-_:nSŋ@Zi[%]uHRȷ`pg^ϓJKנ:`H!S9KhmAʰuj*CHr;!D.F/_.uyٙEEE1 Xa{{aqaԄ~ H455 έ+){ޣ7Ѩ@" 7v0A&B@RBڐi~?0 q!#RJc".;.9ΑnWU}bb&`J)ʹX ٖ?En 9/RRtdIENDB`qtile-0.31.0/libqtile/resources/battery-icons/battery-low-charging.png0000664000175000017500000000243414762660347026002 0ustar epsilonepsilonPNG  IHDRשsBITO pHYs B(xtEXtSoftwarewww.inkscape.org< tEXtTitleBatteryUD?PtEXtAuthorLapo Calamandreiߑ*:PLTE$$${{QQQUUU[[[___cccfffiii```bbbgggjjjlllppprrrܟߴᵵ06828: ⿿27939;SWYooo|||NSSehh/5727:y{y?l3::PwIqU{Nt7aFmGmJr\^\\_\ceccfd}/46057Gmhop8bDklǎԐէ/57лѫ.466=8poLN%Ex)Ct  ĭQ[ݚL#@@=' "4xPv7,մд_xJkX8Mm#?sPȠ6gᚰFl#mk {"i |Gd2MCCl =@da꭯i93듇O(KHLF4] ȻCIENDB`qtile-0.31.0/libqtile/resources/battery-icons/battery-empty.png0000664000175000017500000000134714762660347024561 0ustar epsilonepsilonPNG  IHDRw=sBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org< tEXtTitleBatteryUD?PtEXtAuthorLapo Calamandreiߑ*(IDATHjTAseM,Be-,RD&[y)X _BHI - %xܽ;3̱H ]f423m۔l޽z"l^- loVlflf_{o{;<:c;lQLU13Mx(β TUSJVe}3õgXD86%f~p}}=Ǯ,K1n۽`HD-3+5ZBXL&͢(03ѿu None: # Readline is imported here to prevent issues with terminal resizing # which would result from readline being imported when qtile is first # started self.readline = import_module("readline") self._command_client = CommandClient(client) self._completekey = completekey self._builtins = [i[3:] for i in dir(self) if i.startswith("do_")] def complete(self, arg, state) -> str | None: buf = self.readline.get_line_buffer() completers = self._complete(buf, arg) if completers and state < len(completers): return completers[state] return None def _complete(self, buf, arg) -> list[str]: if not re.search(r" |\(", buf) or buf.startswith("help "): options = self._builtins + self._command_client.commands lst = [i for i in options if i.startswith(arg)] return lst elif buf.startswith(("cd ", "ls ")): path, sep, last = arg.rpartition("/") node, rest_path = self._find_path(path) if node is None: return [] children, items = self._ls(node, rest_path) options = children + items completions = [path + sep + i for i in options if i.startswith(last)] if len(completions) == 1: # add a slash to continue completing the next part of the path return [completions[0] + "/"] return completions return [] @property def prompt(self) -> str: return f"{format_selectors(self._command_client.selectors)} > " def columnize(self, lst, update_termwidth=True) -> str: if update_termwidth: self.termwidth = terminal_width() ret = [] if lst: lst = list(map(str, lst)) mx = max(map(len, lst)) cols = self.termwidth // (mx + 2) or 1 # We want `(n-1) * cols + 1 <= len(lst) <= n * cols` to return `n` # If we subtract 1, then do `// cols`, we get `n - 1`, so we can then add 1 rows = (len(lst) - 1) // cols + 1 for i in range(rows): # Because Python array slicing can go beyond the array bounds, # we don't need to be careful with the values here sl = lst[i * cols : (i + 1) * cols] sl = [x + " " * (mx - len(x)) for x in sl] ret.append(" ".join(sl)) return "\n".join(ret) def _ls(self, client: CommandClient, object_type: str | None) -> tuple[list[str], list[str]]: if object_type is not None: allow_root, items = client.items(object_type) str_items = [str(i) for i in items] if allow_root: children = client.navigate(object_type, None).children else: children = [] return children, str_items else: return client.children, [] def _find_path(self, path: str) -> tuple[CommandClient | None, str | None]: """Find an object relative to the current node Finds and returns the command graph node that is defined relative to the current node. """ root = self._command_client.root if path.startswith("/") else self._command_client parts = [i for i in path.split("/") if i] return self._find_node(root, *parts) def _find_node( self, src: CommandClient, *paths: str ) -> tuple[CommandClient | None, str | None]: """Find an object in the command graph Return the object in the command graph at the specified path relative to the given node. """ if len(paths) == 0: return src, None path, *next_path = paths if path == "..": return self._find_node(src.parent or src, *next_path) if path not in src.children: return None, None if len(next_path) == 0: return src, path item, *maybe_next_path = next_path allow_root, items = src.items(path) for transformation in [str, int]: try: transformed_item = transformation(item) except ValueError: continue if transformed_item in items: next_node = src.navigate(path, transformed_item) return self._find_node(next_node, *maybe_next_path) if not allow_root: return None, None next_node = src.navigate(path, None) return self._find_node(next_node, *next_path) def do_cd(self, arg: str | None) -> str: """Change to another path. Examples ======== cd layout/0 cd ../layout """ if arg is None: self._command_client = self._command_client.root return "/" next_node, rest_path = self._find_path(arg) if next_node is None: return "No such path." if rest_path is None: self._command_client = next_node else: allow_root, _ = next_node.items(rest_path) if not allow_root: return f"Item required for {rest_path}" self._command_client = next_node.navigate(rest_path, None) return format_selectors(self._command_client.selectors) or "/" def do_ls(self, arg: str | None) -> str: """List contained items on a node. Examples ======== > ls > ls ../layout """ if arg: node, rest_path = self._find_path(arg) if not node: return "No such path." base_path = arg.rstrip("/") + "/" else: node = self._command_client rest_path = None base_path = "" assert node is not None objects, items = self._ls(node, rest_path) formatted_ls = [f"{base_path}{i}/" for i in objects] + [ f"{base_path[:-1]}[{i}]/" for i in items ] return self.columnize(formatted_ls) def do_pwd(self, arg) -> str: """Returns the current working location This is the same information as presented in the qshell prompt, but is very useful when running iqshell. Examples ======== > pwd / > cd bar/top bar['top']> pwd bar['top'] """ return format_selectors(self._command_client.selectors) or "/" def do_help(self, arg: str | None) -> str: """Give help on commands and builtins When invoked without arguments, provides an overview of all commands. When passed as an argument, also provides a detailed help on a specific command or builtin. Examples ======== > help > help command """ if not arg: lst = [ "help command -- Help for a specific command.", "", "Builtins", "========", self.columnize(self._builtins), ] cmds = self._command_client.commands if cmds: lst.extend( [ "", "Commands for this object", "========================", self.columnize(cmds), ] ) return "\n".join(lst) elif arg in self._command_client.commands: return self._command_client.call("doc", arg) elif arg in self._builtins: c = getattr(self, "do_" + arg) ret = inspect.getdoc(c) assert ret is not None return ret else: return f"No such command: {arg}" def do_exit(self, args) -> None: """Exit qshell""" sys.exit(0) do_quit = do_exit do_q = do_exit def process_line(self, line: str) -> Any: builtin_match = re.fullmatch(r"(?P\w+)(?:\s+(?P\S*))?", line) if builtin_match: cmd = builtin_match.group("cmd") args = builtin_match.group("arg") if cmd in self._builtins: builtin = getattr(self, "do_" + cmd) val = builtin(args) return val else: return f"Invalid builtin: {cmd}" command_match = re.fullmatch(r"(?P\w+)\((?P.*)\)", line) if command_match: cmd = command_match.group("cmd") args = command_match.group("args") if args: cmd_args = tuple(map(str.strip, args.split(","))) else: cmd_args = () if cmd not in self._command_client.commands: return f"Command does not exist: {cmd}" try: return self._command_client.call(cmd, *cmd_args) except CommandException as e: return f"Caught command exception (is the command invoked incorrectly?): {e}\n" return f"Invalid command: {line}" def loop(self) -> None: self.readline.set_completer(self.complete) self.readline.parse_and_bind(self._completekey + ": complete") self.readline.set_completer_delims(" ()|") while True: try: line = input(self.prompt) except (EOFError, KeyboardInterrupt): print() return if not line: continue try: val = self.process_line(line) except CommandError as e: val = f"Caught command error (is the current path still valid?): {e}\n" if isinstance(val, str): print(val) elif val: pprint.pprint(val) qtile-0.31.0/libqtile/confreader.py0000664000175000017500000001354414762660347017134 0ustar epsilonepsilon# Copyright (c) 2008, Aldo Cortesi # Copyright (c) 2011, Andrew Grigorev # # All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations import importlib import sys from pathlib import Path from typing import TYPE_CHECKING if TYPE_CHECKING: from types import FunctionType from typing import Any, Literal from libqtile.config import Group, Key, Mouse, Rule, Screen from libqtile.layout.base import Layout class ConfigError(Exception): pass config_pyi_header = """ from typing import Any from typing import Literal from libqtile.config import Group, Key, Mouse, Rule, Screen from libqtile.layout.base import Layout """ class Config: # All configuration options keys: list[Key] mouse: list[Mouse] groups: list[Group] dgroups_key_binder: Any dgroups_app_rules: list[Rule] follow_mouse_focus: bool | Literal["click_or_drag_only"] focus_on_window_activation: Literal["focus", "smart", "urgent", "never"] | FunctionType cursor_warp: bool layouts: list[Layout] floating_layout: Layout screens: list[Screen] auto_fullscreen: bool widget_defaults: dict[str, Any] extension_defaults: dict[str, Any] bring_front_click: bool | Literal["floating_only"] floats_kept_above: bool reconfigure_screens: bool wmname: str auto_minimize: bool # Really we'd want to check this Any is libqtile.backend.wayland.ImportConfig, but # doing so forces the import, creating a hard dependency for wlroots. wl_input_rules: dict[str, Any] | None wl_xcursor_theme: str | None wl_xcursor_size: int def __init__(self, file_path=None, **settings): """Create a Config() object from settings Only attributes found in Config.__annotations__ will be added to object. config attribute precedence is 1.) **settings 2.) self 3.) default_config """ self.file_path = file_path self.update(**settings) def update(self, *, fake_screens=None, **settings): from libqtile.resources import default_config if fake_screens: self.fake_screens = fake_screens default = vars(default_config) for key in self.__annotations__.keys(): try: value = settings[key] except KeyError: value = getattr(self, key, default[key]) setattr(self, key, value) def _reload_config_submodules(self, path: Path) -> None: """Reloads python files from same folder as config file.""" folder = path.parent for module in sys.modules.copy().values(): # Skip built-ins and anything with no filepath. if hasattr(module, "__file__") and module.__file__ is not None: subpath = Path(module.__file__) if subpath == path: # do not reevaluate config itself here, we want only # reload all submodules. Also we cant reevaluate config # here, because it will cache all current modules before they # are reloaded. Thus, config file should be reloaded after # this routine. continue # Check if the module is in the config folder or subfolder # and the file still exists. If so, reload it if folder in subpath.parents and subpath.exists(): importlib.reload(module) def load(self): if not self.file_path: return path = Path(self.file_path) name = path.stem sys.path.insert(0, path.parent.as_posix()) if name in sys.modules: self._reload_config_submodules(path) config = importlib.reload(sys.modules[name]) else: config = importlib.import_module(name) self.update(**vars(config)) def validate(self) -> None: """ Validate the configuration against the X11 core, if it makes sense. """ try: from libqtile.backend.x11 import core except ImportError: return valid_keys = core.get_keys() valid_mods = core.get_modifiers() # we explicitly do not want to set self.keys and self.mouse above, # because they are dynamically resolved from the default_config. so we # need to ignore the errors here about missing attributes. for k in self.keys: if isinstance(k.key, str) and k.key.lower() not in valid_keys: raise ConfigError(f"No such key: {k.key}") for m in k.modifiers: if m.lower() not in valid_mods: raise ConfigError(f"No such modifier: {m}") for ms in self.mouse: for m in ms.modifiers: if m.lower() not in valid_mods: raise ConfigError(f"No such modifier: {m}") qtile-0.31.0/libqtile/bar.py0000664000175000017500000007205414762660347015571 0ustar epsilonepsilon# Copyright (c) 2008, Aldo Cortesi. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations import typing from collections import defaultdict from libqtile import configurable, hook from libqtile.command.base import CommandObject, expose_command from libqtile.log_utils import logger from libqtile.utils import has_transparency, is_valid_colors if typing.TYPE_CHECKING: import asyncio from typing import Any from libqtile.backend.base import Drawer, Internal, WindowType from libqtile.command.base import ItemT from libqtile.config import Screen from libqtile.core.manager import Qtile from libqtile.utils import ColorsType from libqtile.widget.base import _Widget NESW = ("top", "right", "bottom", "left") class Gap: """A gap placed along one of the edges of the screen Qtile will avoid covering gaps with windows. Parameters ========== size : The "thickness" of the gap, i.e. the height of a horizontal gap, or the width of a vertical gap. """ def __init__(self, size: int) -> None: # 'size' corresponds to the height of a horizontal gap, or the width # of a vertical gap self._size = size self._initial_size = size # '_length' corresponds to the width of a horizontal gap, or the height # of a vertical gap self._length: int = 0 self.qtile: Qtile | None = None self.screen: Screen | None = None self.x: int = 0 self.y: int = 0 self.width: int = 0 self.height: int = 0 self.horizontal: bool = False # Additional reserved around the gap/bar, used when space is dynamically # reserved e.g. by third-party bars. self.margin: list[int] = [0, 0, 0, 0] # [N, E, S, W] def _configure(self, qtile: Qtile, screen: Screen, reconfigure: bool = False) -> None: self.qtile = qtile self.screen = screen self._size = self._initial_size # If both horizontal and vertical gaps are present, screen corners are # given to the horizontal ones if screen.top is self: self.x = screen.x + self.margin[3] self.y = screen.y + self.margin[0] self._length = screen.width - self.margin[1] - self.margin[3] self.width = self._length self.height = self._initial_size self.horizontal = True self._size += self.margin[0] + self.margin[2] elif screen.bottom is self: self.x = screen.x + self.margin[3] self.y = screen.dy + screen.dheight - self.margin[2] self._length = screen.width - self.margin[1] - self.margin[3] self.width = self._length self.height = self._initial_size self.horizontal = True self._size += self.margin[0] + self.margin[2] elif screen.left is self: self.x = screen.x + self.margin[3] self.y = screen.dy + self.margin[0] self._length = screen.dheight - self.margin[0] - self.margin[2] self.width = self._initial_size self.height = self._length self.horizontal = False self._size += self.margin[1] + self.margin[3] else: # right self.x = screen.dx + screen.dwidth - self.margin[1] self.y = screen.dy + self.margin[0] self._length = screen.dheight - self.margin[0] - self.margin[2] self.width = self._initial_size self.height = self._length self.horizontal = False self._size += self.margin[1] + self.margin[3] def draw(self) -> None: pass def finalize(self) -> None: pass def geometry(self) -> tuple[int, int, int, int]: return (self.x, self.y, self.width, self.height) @property def size(self) -> int: # Enforce immutability of gap.size/bar.size return self._size @property def position(self) -> str: for i in NESW: if getattr(self.screen, i) is self: return i assert False, "Not reached" def adjust_reserved_space(self, size: int) -> None: for i, side in enumerate(NESW): if getattr(self.screen, side) is self: self.margin[i] += size if self.margin[i] < 0: raise ValueError("Gap/Bar can't reserve negative space.") @expose_command() def info(self) -> dict[str, Any]: """ Info for this object. """ return dict(position=self.position) class Obj: def __init__(self, name: str) -> None: self.name = name def __str__(self) -> str: return self.name def __repr__(self) -> str: return self.name STRETCH = Obj("STRETCH") CALCULATED = Obj("CALCULATED") STATIC = Obj("STATIC") class Bar(Gap, configurable.Configurable, CommandObject): """A bar, which can contain widgets Parameters ========== widgets : A list of widget objects. size : The "thickness" of the bar, i.e. the height of a horizontal bar, or the width of a vertical bar. """ defaults = [ ("background", "#000000", "Background colour."), ("opacity", 1, "Bar window opacity."), ("margin", 0, "Space around bar as int or list of ints [N E S W]."), ("border_color", "#000000", "Border colour as str or list of str [N E S W]"), ("border_width", 0, "Width of border as int of list of ints [N E S W]"), ( "reserve", True, "Reserve screen space (when set to 'False', bar will be drawn above windows).", ), ] def __init__(self, widgets: list[_Widget], size: int, **config: Any) -> None: Gap.__init__(self, size) configurable.Configurable.__init__(self, **config) self.add_defaults(Bar.defaults) # We need to create a new widget list here as users may have the same list for multiple # screens. In that scenario, if we replace the widget with a mirror, it replaces it in every # bar as python is referring to the same list. self.widgets = widgets.copy() self.window: Internal | None = None self.drawer: Drawer self._configured = False self._draw_queued = False self.future: asyncio.Handle | None = None # The part of the margins that was reserved by clients self._reserved_space: list[int] = [0, 0, 0, 0] # [N, E, S, W] self._reserved_space_updated = False # Size saved when hiding the bar self._saved_size = 0 # Previous window when the bar grabs the keyboard self._saved_focus: WindowType | None = None # Track widgets that are receiving input self._has_cursor: _Widget | None = None self._has_keyboard: _Widget | None = None # Because Gap.__init__ also sets self.margin self.margin = config.get("margin", self.margin) # Hacky solution that shows limitations of typing Configurable. We want the # option to accept `int | list[int]` but the attribute to be `list[int]`. self.margin: list[int] if isinstance(self.margin, int): # type: ignore [unreachable] self.margin = [self.margin] * 4 # type: ignore [unreachable] self.border_width: list[int] if isinstance(self.border_width, int): # type: ignore [unreachable] self.border_width = [self.border_width] * 4 # type: ignore [unreachable] self.border_color: ColorsType # Check if colours are valid but don't convert to rgba here if is_valid_colors(self.border_color): if not isinstance(self.border_color, list): self.border_color = [self.border_color] * 4 else: logger.warning("Invalid border_color specified. Borders will not be displayed.") self.border_width = [0, 0, 0, 0] def _configure(self, qtile: Qtile, screen: Screen, reconfigure: bool = False) -> None: """ Configure the bar. `reconfigure` is set to True when screen dimensions change, forcing a recalculation of the bar's dimensions. """ # We only want to adjust margin sizes once unless there's new space being # reserved or we're reconfiguring the bar because the screen has changed if not self._configured or self._reserved_space_updated or reconfigure: Gap._configure(self, qtile, screen) if any(self.margin) or any(self.border_width) or self._reserved_space_updated: # Increase the margin size for the border. The border will be drawn # in this space so the empty space will just be the margin. margin = [b + s for b, s in zip(self.border_width, self._reserved_space)] if self.horizontal: self.x += margin[3] - self.border_width[3] self.width -= margin[1] + margin[3] self._length = self.width self._size += margin[0] + margin[2] if screen.top is self: self.y += margin[0] - self.border_width[0] else: self.y -= margin[2] + self.border_width[2] else: self.y += margin[0] - self.border_width[0] self.height -= margin[0] + margin[2] self._length = self.height self._size += margin[1] + margin[3] if screen.left is self: self.x += margin[3] - self.border_width[3] else: self.x -= margin[1] + self.border_width[1] if screen.bottom is self and not self.reserve: self.y -= self.height + self.margin[2] elif screen.right is self and not self.reserve: self.x -= self.width + self.margin[1] self._reserved_space_updated = False width = self.width + (self.border_width[1] + self.border_width[3]) height = self.height + (self.border_width[0] + self.border_width[2]) if self.window: # We get _configure()-ed with an existing window when screens are getting # reconfigured but this screen is present both before and after self.window.place(self.x, self.y, width, height, 0, None) else: # Whereas we won't have a window if we're startup up for the first time or # the window has been killed by us no longer using the bar's screen # X11 only: # To preserve correct display of SysTray widget, we need a 24-bit # window where the user requests an opaque bar. if qtile.core.name == "x11": depth = ( 32 if has_transparency(self.background) else qtile.core.conn.default_screen.root_depth ) self.window = qtile.core.create_internal( # type: ignore [call-arg] self.x, self.y, width, height, depth ) else: self.window = qtile.core.create_internal(self.x, self.y, width, height) self.window.opacity = self.opacity self.window.unhide() self.window.process_window_expose = self.draw self.window.process_button_click = self.process_button_click self.window.process_button_release = self.process_button_release self.window.process_pointer_enter = self.process_pointer_enter self.window.process_pointer_leave = self.process_pointer_leave self.window.process_pointer_motion = self.process_pointer_motion self.window.process_key_press = self.process_key_press if hasattr(self, "drawer"): self.drawer.width = width self.drawer.height = height else: self.drawer = self.window.create_drawer(width, height) self.drawer.clear(self.background) crashed_widgets: set[_Widget] = set() qtile.renamed_widgets = [] if self._configured: for i in self.widgets: if not self._configure_widget(i): crashed_widgets.add(i) else: for idx, i in enumerate(self.widgets): # Create a mirror if this widget is already configured but isn't a Mirror # We don't do isinstance(i, Mirror) because importing Mirror (at the top) # would give a circular import as libqtile.widget.base imports lbqtile.bar if i.configured and i.__class__.__name__ != "Mirror": i = i.create_mirror() self.widgets[idx] = i if self._configure_widget(i): qtile.register_widget(i) else: crashed_widgets.add(i) # Alert the user that we've renamed some widgets if qtile.renamed_widgets: logger.info( "The following widgets were renamed in qtile.widgets_map: %s " "To bind commands, rename the widget or use lazy.widget[new_name].", ", ".join(qtile.renamed_widgets), ) qtile.renamed_widgets.clear() hook.subscribe.setgroup(self.set_layer) hook.subscribe.startup_complete(self.set_layer) self._remove_crashed_widgets(crashed_widgets) self.draw() self._resize(self._length, self.widgets) self._configured = True def _configure_widget(self, widget: _Widget) -> bool: assert self.qtile is not None if widget.supported_backends and (self.qtile.core.name not in widget.supported_backends): logger.warning( "Widget removed: %s does not support %s.", widget.__class__.__name__, self.qtile.core.name, ) return False try: widget._configure(self.qtile, self) if self.horizontal: widget.offsety = self.border_width[0] else: widget.offsetx = self.border_width[3] widget.configured = True except Exception: logger.exception( "%s widget crashed during _configure with error:", widget.__class__.__name__ ) return False return True def _remove_crashed_widgets(self, crashed_widgets: set[_Widget]) -> None: if not crashed_widgets: return assert self.qtile is not None from libqtile.widget.config_error import ConfigErrorWidget for i in crashed_widgets: index = self.widgets.index(i) # Widgets that aren't available on the current backend should not # be shown as "crashed" as the behaviour is expected. Only notify # for genuine crashes. if not i.supported_backends or (self.qtile.core.name in i.supported_backends): if not i.hide_crash: crash = ConfigErrorWidget(widget=i) crash._configure(self.qtile, self) if self.horizontal: crash.offsety = self.border_width[0] else: crash.offsetx = self.border_width[3] self.widgets.insert(index, crash) self.widgets.remove(i) def _items(self, name: str) -> ItemT: if name == "screen" and self.screen is not None: return True, [] elif name == "widget" and self.widgets: return False, [w.name for w in self.widgets] return None def _select(self, name: str, sel: str | int | None) -> CommandObject | None: if name == "screen": return self.screen elif name == "widget": for widget in self.widgets: if widget.name == sel: return widget return None def finalize(self) -> None: if self.future: self.future.cancel() self.drawer.finalize() del self.drawer if self.window: self.window.kill() self.window = None self.widgets.clear() def _resize(self, length: int, widgets: list[_Widget]) -> None: # We want consecutive stretch widgets to split one 'block' of space between them stretches = [] consecutive_stretches: defaultdict[_Widget, list[_Widget]] = defaultdict(list) prev_stretch: _Widget | None = None for widget in widgets: if widget.length_type == STRETCH: if prev_stretch: consecutive_stretches[prev_stretch].append(widget) else: stretches.append(widget) prev_stretch = widget else: prev_stretch = None if stretches: stretchspace = length - sum(i.length for i in widgets if i.length_type != STRETCH) stretchspace = max(stretchspace, 0) num_stretches = len(stretches) if num_stretches == 1: stretches[0].length = stretchspace else: block = 0 blocks = [] for i in widgets: if i.length_type != STRETCH: block += i.length elif i in stretches: # False for consecutive_stretches blocks.append(block) block = 0 if block: blocks.append(block) interval = length // num_stretches for idx, i in enumerate(stretches): if idx == 0: i.length = interval - blocks[0] - blocks[1] // 2 elif idx == num_stretches - 1: i.length = interval - blocks[-1] - blocks[-2] // 2 else: i.length = int(interval - blocks[idx] / 2 - blocks[idx + 1] / 2) stretchspace -= i.length stretches[0].length += stretchspace // 2 stretches[-1].length += stretchspace - stretchspace // 2 for i, followers in consecutive_stretches.items(): length = i.length // (len(followers) + 1) rem = i.length - length i.length = length for f in followers: f.length = length rem -= length i.length += rem if self.horizontal: offset = self.border_width[3] for i in widgets: i.offsetx = offset offset += i.length else: offset = self.border_width[0] for i in widgets: i.offsety = offset offset += i.length def get_widget_in_position(self, x: int, y: int) -> _Widget | None: if self.horizontal: for i in self.widgets: if x < i.offsetx + i.length: return i else: for i in self.widgets: if y < i.offsety + i.length: return i return None def process_button_click(self, x: int, y: int, button: int) -> None: assert self.qtile is not None # If we're clicking on a bar that's not on the current screen, focus that screen if self.screen and self.screen is not self.qtile.current_screen: if self.qtile.core.name == "x11" and self.qtile.current_window: self.qtile.current_window._grab_click() index = self.qtile.screens.index(self.screen) self.qtile.focus_screen(index, warp=False) widget = self.get_widget_in_position(x, y) if widget: widget.button_press( x - widget.offsetx, y - widget.offsety, button, ) def process_button_release(self, x: int, y: int, button: int) -> None: widget = self.get_widget_in_position(x, y) if widget: widget.button_release( x - widget.offsetx, y - widget.offsety, button, ) def process_pointer_enter(self, x: int, y: int) -> None: widget = self.get_widget_in_position(x, y) if widget: widget.mouse_enter( x - widget.offsetx, y - widget.offsety, ) self._has_cursor = widget def process_pointer_leave(self, x: int, y: int) -> None: if self._has_cursor: self._has_cursor.mouse_leave( x - self._has_cursor.offsetx, y - self._has_cursor.offsety, ) self._has_cursor = None def process_pointer_motion(self, x: int, y: int) -> None: widget = self.get_widget_in_position(x, y) if widget and self._has_cursor and widget is not self._has_cursor: self._has_cursor.mouse_leave( x - self._has_cursor.offsetx, y - self._has_cursor.offsety, ) widget.mouse_enter( x - widget.offsetx, y - widget.offsety, ) self._has_cursor = widget def process_key_press(self, keycode: int) -> None: if self._has_keyboard: self._has_keyboard.process_key_press(keycode) def widget_grab_keyboard(self, widget: _Widget) -> None: """ A widget can call this method to grab the keyboard focus and receive keyboard messages. When done, widget_ungrab_keyboard() must be called. """ assert self.qtile is not None self._has_keyboard = widget self._saved_focus = self.qtile.current_window if self.window: self.window.focus(False) def widget_ungrab_keyboard(self) -> None: """ Removes keyboard focus from the widget. """ if self._saved_focus is not None: self._saved_focus.focus(False) self._has_keyboard = None def draw(self) -> None: assert self.qtile is not None if not self.widgets: return # calling self._actual_draw in this case would cause a NameError. if not self._draw_queued: # Delay actually drawing the bar until the event loop is idle, and only once # even if this method is called multiple times during the same task. self.future = self.qtile.call_soon(self._actual_draw) self._draw_queued = True def _actual_draw(self) -> None: self._draw_queued = False self._resize(self._length, self.widgets) # We draw the border before the widgets if any(self.border_width): # The border is drawn "outside" of the bar (i.e. not in the space that the # widgets occupy) so we need to add the additional space width = self.width + self.border_width[1] + self.border_width[3] height = self.height + self.border_width[0] + self.border_width[2] # line_opts is a list of tuples where each tuple represents the borders # in the order N, E, S, W. The border tuple contains two pairs of # co-ordinates for the start and end of the border. rects = [ (0, 0, width, self.border_width[0]), ( width - (self.border_width[1]), self.border_width[0], self.border_width[1], height - self.border_width[0] - self.border_width[2], ), (0, height - self.border_width[2], width, self.border_width[2]), ( 0, self.border_width[0], self.border_width[3], height - self.border_width[0] - self.border_width[2], ), ] for border_width, colour, rect in zip(self.border_width, self.border_color, rects): if not border_width: continue # Draw the border self.drawer.clear_rect(*rect) self.drawer.ctx.rectangle(*rect) self.drawer.set_source_rgb(colour) # type: ignore[arg-type] self.drawer.ctx.fill() src_x, src_y, width, height = rect self.drawer.draw( offsetx=src_x, offsety=src_y, width=width, height=height, src_x=src_x, src_y=src_y, ) for i in self.widgets: i.draw() # We need to check if there is any unoccupied space in the bar # This can happen where there are no SPACER-type widgets to fill # empty space. # In that scenario, we fill the empty space with the bar background colour # We do this, instead of just filling the bar completely at the start of this # method to avoid flickering. # Widgets are offset by the top/left border but this is not included in self._length # so we adjust the end of the bar area for this offset if self.horizontal: bar_end = self._length + self.border_width[3] else: bar_end = self._length + self.border_width[0] widget_end = i.offset + i.length if widget_end < bar_end: # Defines a rectangle for the area enclosed by the bar's borders and the end of the # last widget. if self.horizontal: rect = (widget_end, self.border_width[0], bar_end - widget_end, self.height) else: rect = (self.border_width[3], widget_end, self.width, bar_end - widget_end) # Clear that area (i.e. don't clear borders) and fill with background colour self.drawer.clear_rect(*rect) self.drawer.ctx.rectangle(*rect) self.drawer.set_source_rgb(self.background) self.drawer.ctx.fill() x, y, w, h = rect self.drawer.draw(offsetx=x, offsety=y, height=h, width=w, src_x=x, src_y=y) @expose_command() def info(self) -> dict[str, Any]: return dict( size=self._size, length=self._length, width=self.width, height=self.height, position=self.position, widgets=[i.info() for i in self.widgets], window=self.window.wid if self.window else None, ) def is_show(self) -> bool: return self._size != 0 def show(self, is_show: bool = True) -> None: if is_show != self.is_show(): if is_show: self._size = self._saved_size if self.window: self.window.unhide() else: self._saved_size = self._size self._size = 0 if self.window: self.window.hide() if self.screen and self.screen.group: self.screen.group.layout_all() def adjust_reserved_space(self, size: int) -> None: if self._size: # is this necessary? self._size = self._initial_size for i, side in enumerate(NESW): if getattr(self.screen, side) is self: self._reserved_space[i] += size if self._reserved_space[i] < 0: raise ValueError("Gap/Bar can't reserve negative space.") self._reserved_space_updated = True @expose_command() def fake_button_press(self, x: int, y: int, button: int = 1) -> None: """ Fake a mouse-button-press on the bar. Coordinates are relative to the top-left corner of the bar. Parameters ========== x : X coordinate of the mouse button press. y : Y coordinate of the mouse button press. button: Mouse button, for more details, see :ref:`mouse-events`. """ self.process_button_click(x, y, button) def set_layer(self) -> None: if self.window: if self.reserve: self.window.keep_below(enable=True) else: # Bar is not reserving screen space so let's keep above other windows self.window.keep_above(enable=True) BarType = Bar | Gap qtile-0.31.0/libqtile/lazy.py0000664000175000017500000001551214762660347016000 0ustar epsilonepsilon# Copyright (c) 2019, Sean Vig. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations from typing import TYPE_CHECKING from libqtile.command.client import InteractiveCommandClient from libqtile.command.graph import CommandGraphCall, CommandGraphNode from libqtile.command.interface import CommandInterface from libqtile.log_utils import logger if TYPE_CHECKING: from collections.abc import Callable, Iterable from libqtile.command.graph import SelectorType from libqtile.config import _Match class LazyCall: def __init__(self, call: CommandGraphCall, args: tuple, kwargs: dict) -> None: """The lazily evaluated command graph call Parameters ---------- call: CommandGraphCall The call that is made args: tuple The args passed to the call when it is evaluated. kwargs: dict The kwargs passed to the call when it is evaluated. """ self._call = call self._args = args self._kwargs = kwargs self._focused: _Match | None = None self._if_no_focused: bool = False self._layouts: set[str] = set() self._when_floating: bool | None = None self._condition: bool | None = None self._func: Callable[[], bool] = lambda: True def __call__(self, *args, **kwargs): """Convenience method to allow users to pass arguments to functions decorated with `@lazy.function`. @lazy.function def my_function(qtile, pos_arg, keyword_arg=False): pass ... Key(... my_function("Positional argument", keyword_arg=True)) """ # We need to return a new object so the arguments are not shared between # a single instance of the LazyCall object. return LazyCall(self._call, (*self._args, *args), {**self._kwargs, **kwargs}) @property def selectors(self) -> list[SelectorType]: """The selectors for the given call""" return self._call.selectors @property def name(self) -> str: """The name of the given call""" return self._call.name @property def args(self) -> tuple: """The args to the given call""" return self._args @property def kwargs(self) -> dict: """The kwargs to the given call""" return self._kwargs def when( self, focused: _Match | None = None, if_no_focused: bool = False, layout: Iterable[str] | str | None = None, when_floating: bool | None = None, func: Callable | None = None, condition: bool | None = None, ) -> LazyCall: """Enable call only for matching criteria. Keyword parameters ---------- focused: Match or None Match criteria to enable call for the current window. if_no_focused: bool Whether or not the `focused` attribute should also match when there is no focused window. This is useful when the `focused` attribute is e.g. set to a regex that should also match when there is no focused window. By default this is set to `False` so that the focused attribute only matches when there is actually a focused window. layout: str, Iterable[str], or None Restrict call to one or more layouts. If None, enable the call for all layouts. when_floating: bool Enable call when the current window is floating. func: callable Enable call when the result of the callable evaluates to True condition: a boolean value to determine whether the lazy object should be run. Unlike 'func', the condition is evaluated once when the config file is first loaded. """ self._focused = focused self._if_no_focused = if_no_focused self._condition = condition if func is not None: self._func = func if layout is not None: self._layouts = {layout} if isinstance(layout, str) else set(layout) self._when_floating = when_floating return self def check(self, q) -> bool: cur_win_floating = q.current_window and q.current_window.floating if self._condition is False: return False if self._focused: if q.current_window and not self._focused.compare(q.current_window): return False if not q.current_window and not self._if_no_focused: return False if cur_win_floating and self._when_floating is False: return False if not cur_win_floating and self._when_floating: return False if self._layouts and q.current_layout.name not in self._layouts: return False if self._func is not None: try: result = self._func() except Exception: logger.exception("Error when running function in lazy call. Ignoring.") result = True if not result: return False return True class LazyCommandInterface(CommandInterface): """A lazy loading command object Allows all commands and items to be resolved at run time, and returns lazily evaluated commands. """ def execute(self, call: CommandGraphCall, args: tuple, kwargs: dict) -> LazyCall: """Lazily evaluate the given call""" return LazyCall(call, args, kwargs) def has_command(self, node: CommandGraphNode, command: str) -> bool: """Lazily resolve the given command""" return True def has_item(self, node: CommandGraphNode, object_type: str, item: str | int) -> bool: """Lazily resolve the given item""" return True lazy = InteractiveCommandClient(LazyCommandInterface()) qtile-0.31.0/libqtile/utils.py0000664000175000017500000004571014762660347016164 0ustar epsilonepsilon# Copyright (c) 2008, Aldo Cortesi. All rights reserved. # Copyright (c) 2020, Matt Colligan. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations import asyncio import glob import importlib import os import traceback from collections import defaultdict from collections.abc import Sequence from pathlib import Path from random import randint from shutil import which from typing import TYPE_CHECKING try: from dbus_fast import AuthError, Message, Variant from dbus_fast.aio import MessageBus from dbus_fast.constants import BusType, MessageType has_dbus = True except ImportError: has_dbus = False from libqtile.log_utils import logger ColorType = str | tuple[int, int, int] | tuple[int, int, int, float] ColorsType = ColorType | list[ColorType] if TYPE_CHECKING: from collections.abc import Callable, Coroutine from typing import Any, TypeVar T = TypeVar("T") dbus_bus_connections = set() # Create a list to collect references to tasks so they're not garbage collected # before they've run TASKS: list[asyncio.Task[None]] = [] def create_task(coro: Coroutine) -> asyncio.Task | None: """ Wrapper for asyncio.create_task. Stores task so garbage collector doesn't remove it and removes reference when it's done. See: https://textual.textualize.io/blog/2023/02/11/the-heisenbug-lurking-in-your-async-code/ for more info about the issue this solves. """ loop = asyncio.get_running_loop() if not loop: return None def tidy(task: asyncio.Task) -> None: TASKS.remove(task) task = asyncio.create_task(coro) TASKS.append(task) task.add_done_callback(tidy) return task def cancel_tasks() -> None: """Cancel scheduled tasks.""" for task in TASKS: task.cancel() class QtileError(Exception): pass def lget(o: list[T], v: int) -> T | None: try: return o[v] except (IndexError, TypeError): return None def rgb(x: ColorType) -> tuple[float, float, float, float]: """ Returns a valid RGBA tuple. Here are some valid specifications: #ff0000 with alpha: #ff000080 ff0000 with alpha: ff0000.5 (255, 0, 0) with alpha: (255, 0, 0, 0.5) Which is returned as (1.0, 0.0, 0.0, 0.5). """ if isinstance(x, tuple | list): if len(x) == 4: alpha = x[-1] else: alpha = 1.0 return (x[0] / 255.0, x[1] / 255.0, x[2] / 255.0, alpha) elif isinstance(x, str): if x.startswith("#"): x = x[1:] if "." in x: x, alpha_str = x.split(".") alpha = float("0." + alpha_str) else: alpha = 1.0 if len(x) not in (3, 6, 8): raise ValueError("RGB specifier must be 3, 6 or 8 characters long.") if len(x) == 3: # Multiplying by 17: 0xA * 17 = 0xAA etc. vals = tuple(int(i, 16) * 17 for i in x) else: vals = tuple(int(i, 16) for i in (x[0:2], x[2:4], x[4:6])) if len(x) == 8: alpha = int(x[6:8], 16) / 255.0 vals += (alpha,) # type: ignore return rgb(vals) # type: ignore raise ValueError("Invalid RGB specifier.") def hex(x: ColorType) -> str: r, g, b, _ = rgb(x) return f"#{int(r * 255):02x}{int(g * 255):02x}{int(b * 255):02x}" def has_transparency(colour: ColorsType) -> bool: """ Returns True if the colour is not fully opaque. Where a list of colours is passed, returns True if any colour is not fully opaque. """ if isinstance(colour, str | tuple): return rgb(colour)[3] < 1 return any(has_transparency(c) for c in colour) def remove_transparency(colour: ColorsType): # type: ignore """ Returns a tuple of (r, g, b) with no alpha. """ if isinstance(colour, str | tuple): return tuple(x * 255.0 for x in rgb(colour)[:3]) return [remove_transparency(c) for c in colour] def is_valid_colors(color: ColorsType) -> bool: """ Returns whether the argument is a valid color or list of colors. """ if not isinstance(color, list): color = [color] try: list(rgb(c) for c in color) return True except (ValueError, TypeError): return False def scrub_to_utf8(text: str | bytes) -> str: if not text: return "" elif isinstance(text, str): return text else: return text.decode("utf-8", "ignore") def get_cache_dir() -> str: """ Returns the cache directory and create if it doesn't exists """ cache_directory = os.path.expandvars("$XDG_CACHE_HOME") if cache_directory == "$XDG_CACHE_HOME": # if variable wasn't set cache_directory = os.path.expanduser("~/.cache") cache_directory = os.path.join(cache_directory, "qtile") if not os.path.exists(cache_directory): os.makedirs(cache_directory) return cache_directory def get_config_file() -> Path: config_home = Path(os.getenv("XDG_CONFIG_HOME", "~/.config")).expanduser() config_file = config_home.joinpath("qtile/config.py") if config_file.exists(): return config_file xdg_config_dirs = os.getenv("XDG_CONFIG_DIRS", "/etc/xdg/").split(":") for config_dir in xdg_config_dirs: system_wide_config = Path(config_dir).expanduser().joinpath("qtile/config.py") if system_wide_config.exists(): return system_wide_config return config_file def describe_attributes(obj: Any, attrs: list[str], func: Callable = lambda x: x) -> str: """ Helper for __repr__ functions to list attributes with truthy values only (or values that return a truthy value by func) """ pairs = [] for attr in attrs: value = getattr(obj, attr, None) if func(value): pairs.append(f"{attr}={value}") return ", ".join(pairs) def import_class( module_path: str, class_name: str, fallback: Callable | None = None, ) -> Any: """Import a class safely Try to import the class module, and if it fails because of an ImportError it logs on WARNING, and logs the traceback on DEBUG level """ try: module = importlib.import_module(module_path, __package__) return getattr(module, class_name) except ImportError: logger.exception("Unmet dependencies for '%s.%s':", module_path, class_name) if fallback: logger.debug("%s", traceback.format_exc()) return fallback(module_path, class_name) raise def lazify_imports( registry: dict[str, str], package: str, fallback: Callable | None = None, ) -> tuple[tuple[str, ...], Callable, Callable]: """Leverage PEP 562 to make imports lazy in an __init__.py The registry must be a dictionary with the items to import as keys and the modules they belong to as a value. """ __all__ = tuple(registry.keys()) def __dir__() -> tuple[str, ...]: # noqa: N807 return __all__ def __getattr__(name: str) -> Any: # noqa: N807 if name not in registry: raise AttributeError module_path = f"{package}.{registry[name]}" return import_class(module_path, name, fallback=fallback) return __all__, __dir__, __getattr__ def send_notification( title: str, message: str, urgent: bool = False, timeout: int = -1, id_: int | None = None, ) -> int: """ Send a notification. The id_ argument, if passed, requests the notification server to replace a visible notification with the same ID. An ID is returned for each call; this would then be passed when calling this function again to replace that notification. See: https://developer.gnome.org/notification-spec/ """ if not has_dbus: logger.warning("dbus-fast is not installed. Unable to send notifications.") return -1 id_ = randint(10, 1000) if id_ is None else id_ urgency = 2 if urgent else 1 try: loop = asyncio.get_event_loop() except RuntimeError: logger.warning("Eventloop has not started. Cannot send notification.") else: loop.create_task(_notify(title, message, urgency, timeout, id_)) return id_ async def _notify( title: str, message: str, urgency: int, timeout: int, id_: int, ) -> None: notification = [ "qtile", # Application name id_, # id "", # icon title, # summary message, # body [], # actions {"urgency": Variant("y", urgency)}, # hints timeout, ] # timeout bus, msg = await _send_dbus_message( True, MessageType.METHOD_CALL, "org.freedesktop.Notifications", "org.freedesktop.Notifications", "/org/freedesktop/Notifications", "Notify", "susssasa{sv}i", notification, ) if msg and msg.message_type == MessageType.ERROR: logger.warning("Unable to send notification. Is a notification server running?") # a new bus connection is made each time a notification is sent so # we disconnect when the notification is done if bus: bus.disconnect() def guess_terminal(preference: str | Sequence | None = None) -> str | None: """Try to guess terminal.""" test_terminals = [] if isinstance(preference, str): test_terminals += [preference] elif isinstance(preference, Sequence): test_terminals += list(preference) if "WAYLAND_DISPLAY" in os.environ: # Wayland-only terminals test_terminals += ["foot"] test_terminals += [ "roxterm", "sakura", "hyper", "alacritty", "terminator", "termite", "gnome-terminal", "konsole", "xfce4-terminal", "lxterminal", "mate-terminal", "kitty", "ghostty", "yakuake", "tilda", "guake", "eterm", "st", "urxvt", "wezterm", "xterm", "x-terminal-emulator", ] for terminal in test_terminals: logger.debug("Guessing terminal: %s", terminal) if not which(terminal, os.X_OK): continue logger.info("Terminal found: %s", terminal) return terminal logger.error("Default terminal has not been found.") return None def scan_files(dirpath: str, *names: str) -> defaultdict[str, list[str]]: """ Search a folder recursively for files matching those passed as arguments, with globbing. Returns a dict with keys equal to entries in names, and values a list of matching paths. E.g.: >>> scan_files('/wallpapers', '*.png', '*.jpg') defaultdict(, {'*.png': ['/wallpapers/w1.png'], '*.jpg': ['/wallpapers/w2.jpg', '/wallpapers/w3.jpg']}) """ dirpath = os.path.expanduser(dirpath) files = defaultdict(list) for name in names: found = glob.glob(os.path.join(dirpath, "**", name), recursive=True) files[name].extend(found) return files async def _send_dbus_message( session_bus: bool, message_type: MessageType, destination: str | None, interface: str | None, path: str | None, member: str | None, signature: str, body: Any, negotiate_unix_fd: bool = False, bus: MessageBus | None = None, preserve: bool = False, ) -> tuple[MessageBus | None, Message | None]: """ Private method to send messages to dbus via dbus_fast. An existing bus connection can be passed, if left empty, a new bus connection will be created. Returns a tuple of the bus object and message response. """ if bus is None: if session_bus: bus_type = BusType.SESSION else: bus_type = BusType.SYSTEM try: bus = await MessageBus( bus_type=bus_type, negotiate_unix_fd=negotiate_unix_fd ).connect() except (AuthError, Exception): logger.warning("Unable to connect to dbus.") return None, None if isinstance(body, str): body = [body] # Ignore types here: dbus-fast has default values of `None` for certain # parameters but the signature is `str` so passing `None` results in an # error in mypy. msg = await bus.call( Message( message_type=message_type, destination=destination, interface=interface, path=path, member=member, signature=signature, body=body, ) ) # Keep details of bus connections so we can close them on exit # dbus_bus_connetions is a set so we don't need to worry about # duplicates if not preserve: dbus_bus_connections.add(bus) return bus, msg async def add_signal_receiver( callback: Callable, session_bus: bool = False, signal_name: str | None = None, dbus_interface: str | None = None, bus_name: str | None = None, path: str | None = None, check_service: bool = False, use_bus: MessageBus | None = None, preserve: bool = False, ) -> bool: """ Helper function which aims to recreate python-dbus's add_signal_receiver method in dbus_fast with asyncio calls. If check_service is `True` the method will raise a wanrning and return False if the service is not visible on the bus. If the `bus_name` is None, no check will be performed. Returns True if subscription is successful. """ if not has_dbus: logger.warning("dbus-fast is not installed. Unable to subscribe to signals") return False if bus_name and check_service: found = await find_dbus_service(bus_name, session_bus) if not found: logger.warning( "The %s name was not found on the bus. No callback will be attached.", bus_name ) return False match_args = { "sender": bus_name, "member": signal_name, "path": path, "interface": dbus_interface, } rule = "type='signal'," rule += ",".join(f"{k}='{v}'" for k, v in match_args.items() if v) logger.debug("Adding dbus match rule: %s", rule) bus, msg = await _send_dbus_message( session_bus, MessageType.METHOD_CALL, "org.freedesktop.DBus", "org.freedesktop.DBus", "/org/freedesktop/DBus", "AddMatch", "s", [rule], bus=use_bus, preserve=preserve, ) # Check if message sent successfully if bus and msg and msg.message_type == MessageType.METHOD_RETURN: def match_message(msg: Message, match_args: dict[str, str | None]) -> bool: return all(getattr(msg, k) == v for k, v in match_args.items() if v) async def resolve_sender(signal_msg: Message) -> tuple[str, Message]: """Looks up a pretty bus name to retrieve the unique name.""" _, sender_msg = await _send_dbus_message( session_bus, MessageType.METHOD_CALL, "org.freedesktop.DBus", "org.freedesktop.DBus", "/org/freedesktop/DBus", "GetNameOwner", "s", [match_args["sender"]], bus=bus, ) if sender_msg and sender_msg.message_type == MessageType.METHOD_RETURN: return sender_msg.body[0], signal_msg return "", signal_msg def check_message(task: asyncio.Task) -> None: new_match_args = match_args.copy() new_sender, signal_message = task.result() new_match_args["sender"] = new_sender if match_message(signal_message, new_match_args): callback(signal_message) def signal_callback_wrapper(msg: Message) -> None: """Custom wrapper to only run callback if message matches our rule.""" if msg.message_type == MessageType.SIGNAL: if match_message(msg, match_args): callback(msg) elif "sender" in match_args: # If the message didn't match and we're trying to match the sender # We may need to convert the pretty name to the bus's unique name first task = create_task(resolve_sender(msg)) if task: task.add_done_callback(check_message) bus.add_message_handler(signal_callback_wrapper) return True else: return False async def find_dbus_service(service: str, session_bus: bool) -> bool: """Looks up service name to see if it is currently available on dbus.""" # We're using low level interface here to reduce unnecessary calls for # introspection etc. bus, msg = await _send_dbus_message( session_bus, MessageType.METHOD_CALL, "org.freedesktop.DBus", "org.freedesktop.DBus", "/org/freedesktop/DBus", "ListNames", "", [], ) if bus is None or msg is None or (msg and msg.message_type != MessageType.METHOD_RETURN): logger.warning("Unable to send lookup call to dbus.") return False bus.disconnect() names = msg.body[0] return service in names def remove_dbus_rules() -> None: # Disconnecting the bus connections is enough to remove the match rules. while dbus_bus_connections: bus = dbus_bus_connections.pop() try: bus.disconnect() except OSError: # Socket has already shut down pass # We need to manually close the socket until https://github.com/altdesktop/python-dbus-next/pull/148 # gets merged. There's no error on multiple calls to 'close()'. bus._sock.close() def reap_zombies() -> None: """ A SIGCHLD handler that reaps all zombies until there are no more. """ try: # One signal might mean mulitple children have exited. Reap everything # that has exited, until there's nothing left. while True: wait_result = os.waitid(os.P_ALL, 0, os.WEXITED | os.WNOHANG) if wait_result is None: return except ChildProcessError: pass qtile-0.31.0/scripts/0000775000175000017500000000000014762660347014325 5ustar epsilonepsilonqtile-0.31.0/scripts/qtb0000775000175000017500000000242214762660347015041 0ustar epsilonepsilon#!/usr/bin/env python3 """ Command-line interaction with Qtile TextBox widgets. """ import sys from argparse import ArgumentParser from libqtile import command def main(): parser = ArgumentParser() parser.add_argument("--version", action="version", version="%(prog)s 0.2") parser.add_argument("-l", "--list", action="store_true", help="List addressable widgets") parser.add_argument( "-s", "--socket", type=str, default=None, help="Use specified communication socket" ) # in order for the -l option to work, we can't require these args, so we # check for them later parser.add_argument("name", type=str, required=False, help="Name of widget to change") parser.add_argument("text", type=str, required=False, help="Text to to place in widget") args = parser.parse_args() client = command.Client(args.socket) if args.list: for i in client.list_widgets(): print(i) else: if args.name is None or args.text is None: parser.error("Please specify widget name and argument.") try: client.widget.__getitem__(args.name).update(args.text) except command.CommandError as v: print(v, file=sys.stderr) sys.exit(1) if __name__ == "__main__": main() qtile-0.31.0/scripts/wephyr0000775000175000017500000000257014762660347015575 0ustar epsilonepsilon#!/usr/bin/env python # # The Wayland backend's equivalent to the xephyr script # # The QTILE_XEPHYR environmental variable is set to 2 in this script, which can # be used by configs to detect when they are run via this script. # import os import signal import subprocess import sys import time from pathlib import Path BASE_DIR = Path(__file__).parent.parent.resolve() sys.path.insert(0, BASE_DIR) from libqtile.utils import get_cache_dir, guess_terminal # noqa: E402 CACHE_DIR = Path(get_cache_dir()) QTILE = BASE_DIR / "bin" / "qtile" # This script can be configured with environmental variables and arguments: outputs = os.environ.get("OUTPUTS", 1) app = os.environ.get("APP", guess_terminal()) log_level = os.environ.get("LOG_LEVEL", "INFO") cmd = [QTILE.as_posix(), "start", "-b", "wayland", "-l", log_level] cmd.extend(sys.argv[1:]) # Find the display that the app needs display = os.environ.get("WAYLAND_DISPLAY", "") if display: display = display[:-1] + str(int(display[-1]) + 1) else: display = "wayland-0" os.environ["QTILE_XEPHYR"] = "2" os.environ["WLR_X11_OUTPUTS"] = str(outputs) os.environ["WLR_WL_OUTPUTS"] = str(outputs) proc = subprocess.Popen(cmd) time.sleep(1) os.environ["WAYLAND_DISPLAY"] = display app_proc = subprocess.Popen(app) # Suppress KeyboardInterrupt messages def noop(signal, frame): pass signal.signal(signal.SIGINT, noop) proc.wait() qtile-0.31.0/scripts/ffibuild0000775000175000017500000000231014762660347016033 0ustar epsilonepsilon#!/usr/bin/env python3 """ Build script for libqtile's CFFI helpers. """ from __future__ import annotations import sys from subprocess import run from typing import NamedTuple class Builder(NamedTuple): """ A build script. If 'features' is None, this script is required. """ name: str path: str features: str | None scripts = [ Builder( "Wayland CFFI", "./libqtile/backend/wayland/cffi/build.py", "Wayland backend", ), ] def build(builder: Builder, verbose: bool) -> bool: p = run(["python3", builder.path], capture_output=True) if p.returncode: print(" Failed!") if builder.features: print(" This is optional and is needed for:", builder.features) else: print(" This component is required.") if verbose: print(p.stderr.decode()) return True return False if __name__ == "__main__": verbose = "-v" in sys.argv errors = False for builder in scripts: errors |= build(builder, verbose) if errors and not verbose: print("\nFailures for optional components can be ignored.") print("Pass -v to print full stack traces.") qtile-0.31.0/scripts/take-screenshots0000775000175000017500000000277314762660347017546 0ustar epsilonepsilon#!/usr/bin/env bash # environment variables PROJECT_DIR=$(dirname "$(dirname "$(readlink -f "$0")")") XDISPLAY=${XDISPLAY:-:1} SCREEN_SIZE=${SCREEN_SIZE:-960x540} LOG_PATH=${PROJECT_DIR}/docs/screenshots/take_all.log GEOMETRY=${GEOMETRY:-240x135} DELAY=${DELAY:-1x1} BORDER_FOCUS=${BORDER_FOCUS:-#ff0000} BORDER_NORMAL=${BORDER_NORMAL:-#000000} BORDER_WIDTH=${BORDER_WIDTH:-8} MARGIN=${MARGIN:-10} if [[ -z "${PYTHON}" ]]; then if [[ -f "${PROJECT_DIR}/venv/bin/python3" ]]; then PYTHON="${PROJECT_DIR}/venv/bin/python3" else PYTHON=python3 fi fi # run command in nested X window with specific env vars nested() { env \ DISPLAY=${XDISPLAY} \ PYTHON="${PYTHON}" \ LOG_PATH="${LOG_PATH}" \ GEOMETRY="${GEOMETRY}" \ DELAY="${DELAY}" \ BORDER_FOCUS="${BORDER_FOCUS}" \ BORDER_NORMAL="${BORDER_NORMAL}" \ BORDER_WIDTH="${BORDER_WIDTH}" \ MARGIN="${MARGIN}" \ "$@" } rm "${LOG_PATH}" &>/dev/null touch "${LOG_PATH}" tail -f "${LOG_PATH}" & TAIL_PID=$! Xephyr +extension RANDR -screen ${SCREEN_SIZE} ${XDISPLAY} -ac &>/dev/null & XEPHYR_PID=$! ( sleep 1 nested "${PYTHON}" "${PROJECT_DIR}/bin/qtile" -l CRITICAL -c "${PROJECT_DIR}/docs/screenshots/config.py" & QTILE_PID=$! case $1 in -i|--interactive) nested xterm ;; *) sleep 1 nested xterm -e "${PYTHON}" "${PROJECT_DIR}/docs/screenshots/take_all.py" "$@" kill $TAIL_PID nested xterm -e qtile cmd-obj -o cmd -f shutdown ;; esac wait $QTILE_PID kill $XEPHYR_PID ) qtile-0.31.0/scripts/iqshell0000775000175000017500000000221314762660347015712 0ustar epsilonepsilon#!/bin/sh # Copyright (c) 2008, Aldo Cortesi. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. jupyter console --kernel qtile shell qtile-0.31.0/scripts/gen-keybinding-img0000775000175000017500000002744614762660347017734 0ustar epsilonepsilon#!/usr/bin/env python3 ####################################### # Qtile keybindings image generator # ####################################### import getopt import os import sys import cairocffi as cairo from cairocffi import ImageSurface this_dir = os.path.dirname(__file__) base_dir = os.path.abspath(os.path.join(this_dir, "..")) sys.path.insert(0, base_dir) BUTTON_NAME_Y = 65 BUTTON_NAME_X = 10 COMMAND_Y = 20 COMMAND_X = 10 LEGEND = ["modifiers", "layout", "group", "window", "other"] CUSTOM_KEYS = { "Backspace": 2, "Tab": 1.5, "\\": 1.5, "Return": 2.45, "shift": 2, "space": 5, } class Button: def __init__(self, key, x, y, width, height): self.key = key self.x = x self.y = y self.width = width self.height = height class Pos: WIDTH = 78 HEIGHT = 70 GAP = 5 def __init__(self, x, y): self.x = x self.row_x = x self.y = y self.custom_width = {} for i, val in CUSTOM_KEYS.items(): self.custom_width[i] = val * self.WIDTH def get_pos(self, name): if name in self.custom_width: width = self.custom_width[name] else: width = self.WIDTH info = Button(name, self.x, self.y, width, self.HEIGHT) self.x = self.x + self.GAP + width return info def skip_x(self, times=1): self.x = self.x + self.GAP + times * self.WIDTH def next_row(self): self.x = self.row_x self.y = self.y + self.GAP + self.HEIGHT class KeyboardPNGFactory: def __init__(self, modifiers, keys): self.keys = keys self.modifiers = modifiers.split("-") self.key_pos = self.calculate_pos(20, 140) def rgb_red(self, context): context.set_source_rgb(0.8431372549, 0.3725490196, 0.3725490196) def rgb_green(self, context): context.set_source_rgb(0.6862745098, 0.6862745098, 0) def rgb_yellow(self, context): context.set_source_rgb(1, 0.6862745098, 0) def rgb_cyan(self, context): context.set_source_rgb(0.5137254902, 0.6784313725, 0.6784313725) def rgb_violet(self, context): context.set_source_rgb(0.831372549, 0.5215686275, 0.6784313725) def calculate_pos(self, x, y): pos = Pos(x, y) key_pos = {} for c in "`1234567890-=": key_pos[c] = pos.get_pos(c) key_pos["Backspace"] = pos.get_pos("Backspace") pos.next_row() key_pos["Tab"] = pos.get_pos("Tab") for c in "qwertyuiop[]\\": key_pos[c] = pos.get_pos(c) pos.next_row() pos.skip_x(1.6) for c in "asdfghjkl;'": key_pos[c] = pos.get_pos(c) key_pos["Return"] = pos.get_pos("Return") pos.next_row() key_pos["shift"] = pos.get_pos("shift") for c in "zxcvbnm": key_pos[c] = pos.get_pos(c) key_pos["period"] = pos.get_pos("period") key_pos["comma"] = pos.get_pos("comma") key_pos["/"] = pos.get_pos("/") pos.next_row() key_pos["control"] = pos.get_pos("control") pos.skip_x() key_pos["mod4"] = pos.get_pos("mod4") key_pos["mod1"] = pos.get_pos("mod1") key_pos["space"] = pos.get_pos("space") key_pos["Print"] = pos.get_pos("Print") pos.skip_x(3) key_pos["Up"] = pos.get_pos("Up") pos.next_row() pos.skip_x(12.33) key_pos["Left"] = pos.get_pos("Left") key_pos["Down"] = pos.get_pos("Down") key_pos["Right"] = pos.get_pos("Right") pos.next_row() for legend in LEGEND: key_pos[legend] = pos.get_pos(legend) pos.skip_x(5) key_pos["Button1"] = pos.get_pos("Button1") key_pos["Button2"] = pos.get_pos("Button2") key_pos["Button3"] = pos.get_pos("Button3") pos.next_row() key_pos["FN_KEYS"] = pos.get_pos("FN_KEYS") return key_pos def add_logo(self, context): logo_img = os.path.abspath(os.path.join(this_dir, "..", "logo.png")) context.save() context.scale(0.5) logo = ImageSurface.create_from_png(logo_img) context.set_source_surface(logo, 20, 50) context.paint() context.restore() def render(self, filename): surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 1280, 800) context = cairo.Context(surface) with context: context.set_source_rgb(1, 1, 1) context.paint() self.add_logo(context) context.move_to(210, 80) context.set_font_size(28) context.show_text("Keybindings for Qtile") context.move_to(210, 100) context.set_font_size(18) if len([i for i in self.modifiers if i]): context.show_text("Modifiers: " + ", ".join(self.modifiers)) else: context.show_text("No modifiers used.") for i in self.key_pos.values(): if i.key in ["FN_KEYS"]: continue self.draw_button(context, i.key, i.x, i.y, i.width, i.height) # draw functional fn = [i for i in keys.values() if i.key[:4] == "XF86"] if len(fn): fn_pos = self.key_pos["FN_KEYS"] x = fn_pos.x for i in fn: self.draw_button(context, i.key, x, fn_pos.y, fn_pos.width, fn_pos.height) x += Pos.GAP + Pos.WIDTH # draw mouse base context.rectangle(830, 660, 244, 90) context.set_source_rgb(0, 0, 0) context.stroke() context.set_font_size(28) context.move_to(900, 720) context.show_text("MOUSE") surface.write_to_png(filename) def draw_button(self, context, key, x, y, width, height): fn = False if key[:4] == "XF86": fn = True if key in LEGEND: if key == "modifiers": self.rgb_red(context) elif key == "group": self.rgb_green(context) elif key == "layout": self.rgb_cyan(context) elif key == "window": self.rgb_yellow(context) else: self.rgb_violet(context) context.rectangle(x, y, width, height) context.fill() if key in self.modifiers: context.rectangle(x, y, width, height) self.rgb_red(context) context.fill() if key in self.keys: k = self.keys[key] context.rectangle(x, y, width, height) self.set_key_color(context, k) context.fill() self.show_multiline(context, x + COMMAND_X, y + COMMAND_Y, k) context.rectangle(x, y, width, height) context.set_source_rgb(0, 0, 0) context.stroke() if fn: key = key[4:] context.set_font_size(10) else: context.set_font_size(14) context.move_to(x + BUTTON_NAME_X, y + BUTTON_NAME_Y) context.show_text(self.translate(key)) def show_multiline(self, context, x, y, key): """Cairo doesn't support multiline. Added with word wrapping.""" c_width = 14 if key.key in CUSTOM_KEYS: c_width *= CUSTOM_KEYS[key.key] context.set_font_size(10) context.set_source_rgb(0, 0, 0) context.move_to(x, y) words = key.command.split(" ") words.reverse() printable = last_word = words.pop() while len(words): last_word = words.pop() if len(printable + " " + last_word) < c_width: printable += " " + last_word continue context.show_text(printable) y += 10 context.move_to(x, y) printable = last_word if last_word is not None: context.show_text(printable) def set_key_color(self, context, key): if key.scope == "group": self.rgb_green(context) elif key.scope == "layout": self.rgb_cyan(context) elif key.scope == "window": self.rgb_yellow(context) else: self.rgb_violet(context) def translate(self, text): dictionary = { "period": ",", "comma": ".", "Left": "←", "Down": "↓", "Right": "→", "Up": "↑", "AudioRaiseVolume": "Volume up", "AudioLowerVolume": "Volume down", "AudioMute": "Audio mute", "AudioMicMute": "Mic mute", "MonBrightnessUp": "Brightness up", "MonBrightnessDown": "Brightness down", } if text not in dictionary: return text return dictionary[text] class KInfo: NAME_MAP = { "togroup": "to group", "toscreen": "to screen", } KEY_MAP = { "grave": "`", "semicolon": ";", "slash": "/", "backslash": "\\", "comma": ",", "period": ".", "bracketleft": "[", "bracketright": "]", "quote": "'", "minus": "-", "equals": "=", } def __init__(self, key): if key.key in self.KEY_MAP: self.key = self.KEY_MAP[key.key] else: self.key = key.key self.command = self.get_command(key) self.scope = self.get_scope(key) def get_command(self, key): if hasattr(key, "desc") and key.desc: return key.desc cmd = key.commands[0] command = cmd.name if command in self.NAME_MAP: command = self.NAME_MAP[command] command = command.replace("_", " ") if len(cmd.args): if isinstance(cmd.args[0], str): command += " " + cmd.args[0] return command def get_scope(self, key): selectors = key.commands[0].selectors if len(selectors): return selectors[0][0] class MInfo(KInfo): def __init__(self, mouse): self.key = mouse.button self.command = self.get_command(mouse) self.scope = self.get_scope(mouse) def get_kb_map(config_path=None): from libqtile.confreader import Config c = Config(config_path) if config_path: c.load() kb_map = {} for key in c.keys: mod = "-".join(key.modifiers) if mod not in kb_map: kb_map[mod] = {} info = KInfo(key) kb_map[mod][info.key] = info for mouse in c.mouse: mod = "-".join(mouse.modifiers) if mod not in kb_map: kb_map[mod] = {} info = MInfo(mouse) kb_map[mod][info.key] = info return kb_map help_doc = """ usage: gen-keybinding-img [-h] [-c CONFIGFILE] [-o OUTPUT_DIR] Qtile keybindings image generator optional arguments: -h, --help show this help message and exit -c CONFIGFILE, --config CONFIGFILE use specified configuration file. If no presented default will be used -o OUTPUT_DIR, --output-dir OUTPUT_DIR set directory to export all images to """ if __name__ == "__main__": config_path = None output_dir = "" try: opts, args = getopt.getopt(sys.argv[1:], "hc:o:", ["help=", "config=", "output-dir="]) except getopt.GetoptError: print(help_doc) sys.exit(2) for opt, arg in opts: if opt in ("-h", "--help"): print(help_doc) sys.exit() elif opt in ("-c", "--config"): config_path = arg elif opt in ("-o", "--output-dir"): output_dir = arg kb_map = get_kb_map(config_path) for modifier, keys in kb_map.items(): if not modifier: filename = "no_modifier.png" else: filename = f"{modifier}.png" output_file = os.path.abspath(os.path.join(output_dir, filename)) f = KeyboardPNGFactory(modifier, keys) f.render(output_file) qtile-0.31.0/scripts/dqtile-cmd0000775000175000017500000000305714762660347016303 0ustar epsilonepsilon#!/usr/bin/env bash usage() { echo "$(tput bold)dqtile-cmd$(tput sgr0) A Rofi/dmenu interface to qtile cmd-obj. Accepts all arguments of qtile cmd-obj (see below). " qtile cmd-obj -h | sed "s/qtile cmd-obj/dqtile-cmd/" echo " If both rofi and dmenu are present rofi will be selected as default, to change this us --force-dmenu as the first argument. " exit } case $1 in -h|--help) usage ;; --force-dmenu) FORCE_DMENU=1; shift;; esac action=$(qtile cmd-obj $@) # Path to menu application if [[ -n $(command -v rofi) ]] && [[ -z "$FORCE_DMENU" ]]; then menu="$(command -v rofi) -dmenu -columns 1" global_mesg="Alt-1 Prompt for args and show function help (if -f is present) .. Go back to menu. C-u Clear input Esc Exit" action=$(echo -e "$action" | $menu -mesg "$global_mesg") # For rofi elif [[ -n $(command -v dmenu) ]]; then menu="cut -f 1 | sed -e 's/ *$//g' | $(command -v dmenu)" action=$(echo -e "$action" | eval $menu) # For dmenu else echo >&2 "Rofi or dmenu not found" exit fi action_info=$? # get the return code from rofi action=$(echo "$action"| cut -f 1 | sed -e 's/ *$//g') # if kb-mod-1 key was pressed in rofi if [ "$action_info" -eq "10" ]; then # only run when -f is present (then -i makes sense) if [[ $action == *"-f"* ]]; then info=$(qtile cmd-obj $action -i) action=$($menu -mesg "$global_mesg Help $info" -filter "$action -a ") fi; fi; case $action in "") ;; # exit ..)$0;; # Go back to main menu *) $0 "$action" ;; esac qtile-0.31.0/scripts/xephyr0000775000175000017500000000107114762660347015571 0ustar epsilonepsilon#!/usr/bin/env bash HERE=$(dirname $(readlink -f $0)) SCREEN_SIZE=${SCREEN_SIZE:-800x600} XDISPLAY=${XDISPLAY:-:1} LOG_LEVEL=${LOG_LEVEL:-INFO} APP=${APP:-$(python -c "from libqtile.utils import guess_terminal; print(guess_terminal())")} if [[ -z $PYTHON ]]; then PYTHON=python3 fi Xephyr +extension RANDR -screen ${SCREEN_SIZE} ${XDISPLAY} -ac & XEPHYR_PID=$! ( sleep 1 env DISPLAY=${XDISPLAY} QTILE_XEPHYR=1 ${PYTHON} "${HERE}"/../bin/qtile start -l ${LOG_LEVEL} $@ & QTILE_PID=$! env DISPLAY=${XDISPLAY} ${APP} & wait $QTILE_PID kill $XEPHYR_PID ) qtile-0.31.0/scripts/ubuntu_wayland_setup0000775000175000017500000000626614762660347020546 0ustar epsilonepsilon#!/usr/bin/env bash # # Helper script to install the non-Python Wayland dependencies on Ubuntu. This # is required because most of the time the wlroots version we depend on is more # recent than that which can be found in the Ubuntu package repository. This # script is used for CI on GitHub and the ReadTheDocs environment. set -e # The versions we want WAYLAND=1.22.0 WAYLAND_PROTOCOLS=1.32 WLROOTS=0.17.3 SEATD=0.6.4 LIBDRM=2.4.121 PIXMAN=0.42.0 XWAYLAND=22.1.9 HWDATA=0.364 # Packaged dependencies sudo apt update sudo apt-get install -y --no-install-recommends \ libepoxy-dev \ libegl1-mesa-dev \ libgbm-dev \ libgles2-mesa-dev \ libinput-dev \ libpciaccess-dev \ libxcb-composite0-dev \ libxcb-dri3-dev \ libxcb-ewmh-dev \ libxcb-icccm4-dev \ libxcb-image0-dev \ libxcb-present-dev \ libxcb-render0-dev \ libxcb-res0-dev \ libxcb-xfixes0-dev \ libxcb-xinput-dev \ libxcb1-dev \ libxfont-dev \ libxkbcommon-dev \ libxshmfence-dev \ libtirpc-dev \ xfonts-utils \ xserver-xorg-dev \ ninja-build \ meson # Build wayland wget https://gitlab.freedesktop.org/wayland/wayland/-/releases/$WAYLAND/downloads/wayland-$WAYLAND.tar.xz tar -xJf wayland-$WAYLAND.tar.xz cd wayland-$WAYLAND meson build -Ddocumentation=false --prefix=/usr ninja -C build sudo ninja -C build install cd ../ # Build wayland-protocols wget https://gitlab.freedesktop.org/wayland/wayland-protocols/-/releases/$WAYLAND_PROTOCOLS/downloads/wayland-protocols-$WAYLAND_PROTOCOLS.tar.xz tar -xJf wayland-protocols-$WAYLAND_PROTOCOLS.tar.xz cd wayland-protocols-$WAYLAND_PROTOCOLS meson build -Dtests=false --prefix=/usr ninja -C build sudo ninja -C build install cd ../ # Build libdrm wget https://gitlab.freedesktop.org/mesa/drm/-/archive/libdrm-$LIBDRM/drm-libdrm-$LIBDRM.tar.gz tar -xzf drm-libdrm-$LIBDRM.tar.gz cd drm-libdrm-$LIBDRM meson build --prefix=/usr ninja -C build sudo ninja -C build install cd ../ # Build seatd wget https://github.com/kennylevinsen/seatd/archive/refs/tags/$SEATD.tar.gz tar -xzf $SEATD.tar.gz cd seatd-$SEATD meson build --prefix=/usr ninja -C build sudo ninja -C build install cd ../ # Build pixman wget https://gitlab.freedesktop.org/pixman/pixman/-/archive/pixman-$PIXMAN/pixman-pixman-$PIXMAN.tar.gz tar -xzf pixman-pixman-$PIXMAN.tar.gz cd pixman-pixman-$PIXMAN meson build --prefix=/usr ninja -C build sudo ninja -C build install cd ../ # Build hwdata wget https://github.com/vcrhonek/hwdata/archive/refs/tags/v$HWDATA.tar.gz tar -xzf v$HWDATA.tar.gz cd hwdata-$HWDATA ./configure --prefix=/usr --libdir=/lib --datadir=/usr/share make sudo make install cd ../ # Build xwayland wget https://gitlab.freedesktop.org/xorg/xserver/-/archive/xwayland-$XWAYLAND/xserver-xwayland-$XWAYLAND.tar.gz tar -xzf xserver-xwayland-$XWAYLAND.tar.gz cd xserver-xwayland-$XWAYLAND meson build --prefix=/usr ninja -C build sudo ninja -C build install cd ../ # Build wlroots wget https://gitlab.freedesktop.org/wlroots/wlroots/-/archive/$WLROOTS/wlroots-$WLROOTS.tar.gz tar -xzf wlroots-$WLROOTS.tar.gz cd wlroots-$WLROOTS meson build -Dexamples=false --prefix=/usr -Dxwayland=enabled ninja -C build sudo ninja -C build install cd ../ qtile-0.31.0/scripts/addlicence.sh0000775000175000017500000000470214762660347016742 0ustar epsilonepsilon#!/bin/bash # Copyright (c) 2008, Aldo Cortesi. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. set -e set -x # path to: http://0pointer.de/public/copyright.py COPYRIGHT=~/packages/copyright.py LICENSE="# # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the \"Software\"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. " files=$(licensecheck -r . | grep -v 'bin/libqtile' | grep UNKNOWN | cut -d: -f1) for f in $files; do { echo "$($COPYRIGHT "$f")"; echo "$LICENSE"; cat "$f"; } > newfile && mv newfile "$f" done qtile-0.31.0/stubs/0000775000175000017500000000000014762660347013776 5ustar epsilonepsilonqtile-0.31.0/stubs/keyring.pyi0000664000175000017500000000000014762660347016157 0ustar epsilonepsilonqtile-0.31.0/stubs/setproctitle.pyi0000664000175000017500000000003514762660347017240 0ustar epsilonepsilondef setproctitle(title): ... qtile-0.31.0/stubs/iwlib.pyi0000664000175000017500000000004114762660347015622 0ustar epsilonepsilondef get_iwconfig(interface): ... qtile-0.31.0/stubs/cairocffi/0000775000175000017500000000000014762660347015723 5ustar epsilonepsilonqtile-0.31.0/stubs/cairocffi/ffi_build.pyi0000664000175000017500000000001214762660347020362 0ustar epsilonepsilonffi = ... qtile-0.31.0/stubs/cairocffi/xcb.pyi0000664000175000017500000000041414762660347017221 0ustar epsilonepsilonfrom typing import Any from .surfaces import Surface as Surface class XCBSurface(Surface): def __init__( self, conn: Any, drawable: Any, visual: Any, width: Any, height: Any ) -> None: ... def set_size(self, width: Any, height: Any) -> None: ... qtile-0.31.0/stubs/cairocffi/__init__.pyi0000664000175000017500000000146514762660347020213 0ustar epsilonepsilonfrom typing import Any from .constants import * from .context import Context as Context from .patterns import Gradient as Gradient from .patterns import LinearGradient as LinearGradient from .patterns import Pattern as Pattern from .patterns import RadialGradient as RadialGradient from .patterns import SolidPattern as SolidPattern from .patterns import SurfacePattern as SurfacePattern from .surfaces import ImageSurface as ImageSurface from .surfaces import RecordingSurface as RecordingSurface from .surfaces import Surface as Surface from .xcb import XCBSurface as XCBSurface VERSION: Any version: str version_info: Any cairo: Any class CairoError(Exception): status: Any = ... def __init__(self, message: Any, status: Any) -> None: ... Error = CairoError STATUS_TO_EXCEPTION: Any OPERATOR_SOURCE: Any qtile-0.31.0/stubs/cairocffi/context.pyi0000664000175000017500000001220414762660347020131 0ustar epsilonepsilonfrom typing import Any class Context: def __init__(self, target: Any) -> None: ... def get_target(self): ... def save(self) -> None: ... def restore(self) -> None: ... def __enter__(self): ... def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: ... def push_group(self) -> None: ... def push_group_with_content(self, content: Any) -> None: ... def pop_group(self): ... def pop_group_to_source(self) -> None: ... def get_group_target(self): ... def set_source_rgba( self, red: float, green: float, blue: float, alpha: float = ... ) -> None: ... def set_source_rgb(self, red: float, green: float, blue: float) -> None: ... def set_source_surface(self, surface: Any, x: int = ..., y: int = ...) -> None: ... def set_source(self, source: Any) -> None: ... def get_source(self): ... def set_antialias(self, antialias: Any) -> None: ... def get_antialias(self): ... def set_dash(self, dashes: Any, offset: int = ...) -> None: ... def get_dash(self): ... def get_dash_count(self): ... def set_fill_rule(self, fill_rule: Any) -> None: ... def get_fill_rule(self): ... def set_line_cap(self, line_cap: Any) -> None: ... def get_line_cap(self): ... def set_line_join(self, line_join: Any) -> None: ... def get_line_join(self): ... def set_line_width(self, width: Any) -> None: ... def get_line_width(self): ... def set_miter_limit(self, limit: Any) -> None: ... def get_miter_limit(self): ... def set_operator(self, operator: Any) -> None: ... def get_operator(self): ... def set_tolerance(self, tolerance: Any) -> None: ... def get_tolerance(self): ... def translate(self, tx: Any, ty: Any) -> None: ... def scale(self, sx: Any, sy: Any | None = ...) -> None: ... def rotate(self, radians: Any) -> None: ... def transform(self, matrix: Any) -> None: ... def set_matrix(self, matrix: Any) -> None: ... def get_matrix(self): ... def identity_matrix(self) -> None: ... def user_to_device(self, x: Any, y: Any): ... def user_to_device_distance(self, dx: Any, dy: Any): ... def device_to_user(self, x: Any, y: Any): ... def device_to_user_distance(self, dx: Any, dy: Any): ... def has_current_point(self): ... def get_current_point(self): ... def new_path(self) -> None: ... def new_sub_path(self) -> None: ... def move_to(self, x: Any, y: Any) -> None: ... def rel_move_to(self, dx: Any, dy: Any) -> None: ... def line_to(self, x: Any, y: Any) -> None: ... def rel_line_to(self, dx: Any, dy: Any) -> None: ... def rectangle(self, x: Any, y: Any, width: Any, height: Any) -> None: ... def arc(self, xc: Any, yc: Any, radius: Any, angle1: Any, angle2: Any) -> None: ... def arc_negative(self, xc: Any, yc: Any, radius: Any, angle1: Any, angle2: Any) -> None: ... def curve_to(self, x1: Any, y1: Any, x2: Any, y2: Any, x3: Any, y3: Any) -> None: ... def rel_curve_to( self, dx1: Any, dy1: Any, dx2: Any, dy2: Any, dx3: Any, dy3: Any ) -> None: ... def text_path(self, text: Any) -> None: ... def glyph_path(self, glyphs: Any) -> None: ... def close_path(self) -> None: ... def copy_path(self): ... def copy_path_flat(self): ... def append_path(self, path: Any) -> None: ... def path_extents(self): ... def paint(self) -> None: ... def paint_with_alpha(self, alpha: Any) -> None: ... def mask(self, pattern: Any) -> None: ... def mask_surface(self, surface: Any, surface_x: int = ..., surface_y: int = ...) -> None: ... def fill(self) -> None: ... def fill_preserve(self) -> None: ... def fill_extents(self): ... def in_fill(self, x: Any, y: Any): ... def stroke(self) -> None: ... def stroke_preserve(self) -> None: ... def stroke_extents(self): ... def in_stroke(self, x: Any, y: Any): ... def clip(self) -> None: ... def clip_preserve(self) -> None: ... def clip_extents(self): ... def copy_clip_rectangle_list(self): ... def in_clip(self, x: Any, y: Any): ... def reset_clip(self) -> None: ... def select_font_face( self, family: str = ..., slant: Any = ..., weight: Any = ... ) -> None: ... def set_font_face(self, font_face: Any) -> None: ... def get_font_face(self): ... def set_font_size(self, size: Any) -> None: ... def set_font_matrix(self, matrix: Any) -> None: ... def get_font_matrix(self): ... def set_font_options(self, font_options: Any) -> None: ... def get_font_options(self): ... def set_scaled_font(self, scaled_font: Any) -> None: ... def get_scaled_font(self): ... def font_extents(self): ... def text_extents(self, text: Any): ... def glyph_extents(self, glyphs: Any): ... def show_text(self, text: Any) -> None: ... def show_glyphs(self, glyphs: Any) -> None: ... def show_text_glyphs( self, text: Any, glyphs: Any, clusters: Any, cluster_flags: int = ... ) -> None: ... def show_page(self) -> None: ... def copy_page(self) -> None: ... def tag_begin(self, tag_name: Any, attributes: Any | None = ...) -> None: ... def tag_end(self, tag_name: Any) -> None: ... qtile-0.31.0/stubs/cairocffi/patterns.pyi0000664000175000017500000000232314762660347020306 0ustar epsilonepsilonfrom typing import Any class Pattern: def __init__(self, pointer: Any) -> None: ... def set_extend(self, extend: Any) -> None: ... def get_extend(self): ... def set_filter(self, filter: Any) -> None: ... def get_filter(self): ... def set_matrix(self, matrix: Any) -> None: ... def get_matrix(self): ... class SolidPattern(Pattern): def __init__(self, red: Any, green: Any, blue: Any, alpha: int = ...) -> None: ... def get_rgba(self): ... class SurfacePattern(Pattern): def __init__(self, surface: Any) -> None: ... def get_surface(self): ... class Gradient(Pattern): def add_color_stop_rgba( self, offset: Any, red: float, green: float, blue: float, alpha: float = ... ) -> None: ... def add_color_stop_rgb(self, offset: Any, red: Any, green: Any, blue: Any) -> None: ... def get_color_stops(self): ... class LinearGradient(Gradient): def __init__(self, x0: Any, y0: Any, x1: Any, y1: Any) -> None: ... def get_linear_points(self): ... class RadialGradient(Gradient): def __init__( self, cx0: Any, cy0: Any, radius0: Any, cx1: Any, cy1: Any, radius1: Any ) -> None: ... def get_radial_circles(self): ... PATTERN_TYPE_TO_CLASS: Any qtile-0.31.0/stubs/cairocffi/ffi.pyi0000664000175000017500000000001214762660347017203 0ustar epsilonepsilonffi = ... qtile-0.31.0/stubs/cairocffi/pixbuf.pyi0000664000175000017500000000043014762660347017740 0ustar epsilonepsilonfrom typing import Any class ImageLoadingError(ValueError): ... class Pixbuf: def __init__(self, pointer: Any) -> None: ... def __getattr__(self, name: Any): ... def decode_to_image_surface( image_data: Any, width: Any | None = ..., height: Any | None = ... ): ... qtile-0.31.0/stubs/cairocffi/surfaces.pyi0000664000175000017500000000423014762660347020260 0ustar epsilonepsilonfrom typing import Any SURFACE_TARGET_KEY: Any def from_buffer(obj: Any): ... class Surface: _pointer: Any def __init__(self, pointer: Any, target_keep_alive: Any | None = ...) -> None: ... def create_similar(self, content: Any, width: Any, height: Any): ... def create_similar_image(self, content: Any, width: Any, height: Any): ... def create_for_rectangle(self, x: Any, y: Any, width: Any, height: Any): ... def get_content(self): ... def has_show_text_glyphs(self): ... def set_device_offset(self, x_offset: Any, y_offset: Any) -> None: ... def get_device_offset(self): ... def set_fallback_resolution(self, x_pixels_per_inch: Any, y_pixels_per_inch: Any) -> None: ... def get_fallback_resolution(self): ... def get_font_options(self): ... def set_device_scale(self, x_scale: Any, y_scale: Any) -> None: ... def get_device_scale(self): ... def set_mime_data(self, mime_type: Any, data: Any) -> None: ... def get_mime_data(self, mime_type: Any): ... def supports_mime_type(self, mime_type: Any): ... def mark_dirty(self) -> None: ... def mark_dirty_rectangle(self, x: Any, y: Any, width: Any, height: Any) -> None: ... def show_page(self) -> None: ... def copy_page(self) -> None: ... def flush(self) -> None: ... def finish(self) -> None: ... def write_to_png(self, target: Any | None = ...): ... class ImageSurface(Surface): def __init__( self, format: Any, width: Any, height: Any, data: Any | None = ..., stride: Any | None = ..., ) -> None: ... @classmethod def create_for_data( cls, data: Any, format: Any, width: Any, height: Any, stride: Any | None = ... ): ... @staticmethod def format_stride_for_width(format: Any, width: Any): ... @classmethod def create_from_png(cls, source: Any): ... def get_data(self): ... def get_format(self): ... def get_width(self): ... def get_height(self): ... def get_stride(self): ... class RecordingSurface(Surface): def __init__(self, content: Any, extents: Any) -> None: ... def get_extents(self): ... def ink_extents(self): ... qtile-0.31.0/stubs/cairocffi/constants.pyi0000664000175000017500000000002314762660347020455 0ustar epsilonepsilonFORMAT_ARGB32: int qtile-0.31.0/stubs/xdg/0000775000175000017500000000000014762660347014560 5ustar epsilonepsilonqtile-0.31.0/stubs/xdg/__init__.pyi0000664000175000017500000000000014762660347017030 0ustar epsilonepsilonqtile-0.31.0/stubs/xdg/IconTheme/0000775000175000017500000000000014762660347016433 5ustar epsilonepsilonqtile-0.31.0/stubs/xdg/IconTheme/__init__.pyi0000664000175000017500000000003714762660347020715 0ustar epsilonepsilondef getIconPath(iconname): ... qtile-0.31.0/stubs/psutil.pyi0000664000175000017500000000665514762660347016055 0ustar epsilonepsilon# Stubs for psutil (Python 3.7) # # NOTE: This dynamically typed stub was automatically generated by stubgen. from typing import Any PROCFS_PATH: str RLIMIT_MSGQUEUE: Any RLIMIT_NICE: Any RLIMIT_RTPRIO: Any RLIMIT_RTTIME: Any RLIMIT_SIGPENDING: Any version_info: Any AF_LINK: Any POWER_TIME_UNLIMITED: Any POWER_TIME_UNKNOWN: Any class Process: def __init__(self, pid: Any | None = ...) -> None: ... def __eq__(self, other: Any): ... def __ne__(self, other: Any): ... def __hash__(self): ... @property def pid(self): ... def oneshot(self) -> None: ... def as_dict(self, attrs: Any | None = ..., ad_value: Any | None = ...): ... def parent(self): ... def is_running(self): ... def ppid(self): ... def name(self): ... def exe(self): ... def cmdline(self): ... def status(self): ... def username(self): ... def create_time(self): ... def cwd(self): ... def nice(self, value: Any | None = ...): ... def uids(self): ... def gids(self): ... def terminal(self): ... def num_fds(self): ... def io_counters(self): ... def ionice(self, ioclass: Any | None = ..., value: Any | None = ...): ... def rlimit(self, resource: Any, limits: Any | None = ...): ... def cpu_affinity(self, cpus: Any | None = ...): ... def cpu_num(self): ... def environ(self): ... def num_handles(self): ... def num_ctx_switches(self): ... def num_threads(self): ... def threads(self): ... def children(self, recursive: bool = ...): ... def cpu_percent(self, interval: Any | None = ...): ... def cpu_times(self): ... def memory_info(self): ... def memory_info_ex(self): ... def memory_full_info(self): ... def memory_percent(self, memtype: str = ...): ... def memory_maps(self, grouped: bool = ...): ... def open_files(self): ... def connections(self, kind: str = ...): ... def send_signal(self, sig: Any) -> None: ... def suspend(self) -> None: ... def resume(self) -> None: ... def terminate(self) -> None: ... def kill(self) -> None: ... def wait(self, timeout: Any | None = ...): ... class Popen(Process): def __init__(self, *args: Any, **kwargs: Any) -> None: ... def __dir__(self): ... def __enter__(self): ... def __exit__(self, *args: Any, **kwargs: Any): ... def __getattribute__(self, name: Any): ... def wait(self, timeout: Any | None = ...): ... def pids(): ... def pid_exists(pid: Any): ... def process_iter(attrs: Any | None = ..., ad_value: Any | None = ...): ... def wait_procs(procs: Any, timeout: Any | None = ..., callback: Any | None = ...): ... def cpu_count(logical: bool = ...): ... def cpu_times(percpu: bool = ...): ... def cpu_percent(interval: Any | None = ..., percpu: bool = ...): ... def cpu_times_percent(interval: Any | None = ..., percpu: bool = ...): ... def cpu_stats(): ... def cpu_freq(percpu: bool = ...): ... def virtual_memory(): ... def swap_memory(): ... def disk_usage(path: Any): ... def disk_partitions(all: bool = ...): ... def disk_io_counters(perdisk: bool = ..., nowrap: bool = ...): ... def net_io_counters(pernic: bool = ..., nowrap: bool = ...): ... def net_connections(kind: str = ...): ... def net_if_addrs(): ... def net_if_stats(): ... def sensors_temperatures(fahrenheit: bool = ...): ... def sensors_fans(): ... def sensors_battery(): ... def boot_time(): ... def users(): ... def getloadavg(): ... # Names in __all__ with no definition: # __version__ qtile-0.31.0/stubs/mailbox.pyi0000664000175000017500000000002314762660347016147 0ustar epsilonepsilonclass Maildir: ... qtile-0.31.0/stubs/cffi.pyi0000664000175000017500000000047214762660347015433 0ustar epsilonepsilon__version_info__ = ... # type: list class FFI: def cdef(self, csource, override=False, packed=False): ... def compile(self, tmpdir=".", verbose=0, target=None, debug=None): ... def include(self, ffi_to_include): ... def set_source(self, module_name, source, source_extension=".c", **kwargs): ... qtile-0.31.0/stubs/xmltodict.pyi0000664000175000017500000000000014762660347016516 0ustar epsilonepsilonqtile-0.31.0/stubs/IPython/0000775000175000017500000000000014762660347015370 5ustar epsilonepsilonqtile-0.31.0/stubs/IPython/__init__.pyi0000664000175000017500000000000014762660347017640 0ustar epsilonepsilonqtile-0.31.0/stubs/IPython/utils/0000775000175000017500000000000014762660347016530 5ustar epsilonepsilonqtile-0.31.0/stubs/IPython/utils/__init__.pyi0000664000175000017500000000000014762660347021000 0ustar epsilonepsilonqtile-0.31.0/stubs/IPython/utils/tempdir.pyi0000664000175000017500000000016314762660347020717 0ustar epsilonepsilonfrom tempfile import TemporaryDirectory as _TemporaryDirectory class TemporaryDirectory(_TemporaryDirectory): ... qtile-0.31.0/stubs/jupyter_client/0000775000175000017500000000000014762660347017036 5ustar epsilonepsilonqtile-0.31.0/stubs/jupyter_client/__init__.pyi0000664000175000017500000000000014762660347021306 0ustar epsilonepsilonqtile-0.31.0/stubs/jupyter_client/kernelspec.pyi0000664000175000017500000000014314762660347021712 0ustar epsilonepsilondef install_kernel_spec(source_dir, kernel_name=None, user=False, replace=False, prefix=None): ... qtile-0.31.0/stubs/pulsectl_asyncio/0000775000175000017500000000000014762660347017356 5ustar epsilonepsilonqtile-0.31.0/stubs/pulsectl_asyncio/pa_asyncio_mainloop.pyi0000664000175000017500000001007414762660347024126 0ustar epsilonepsilonimport asyncio import ctypes as c from _typeshed import Incomplete from pulsectl._pulsectl import PA_MAINLOOP_API class pa_mainloop_api(PA_MAINLOOP_API): ... time_t_size: Incomplete time_t = c.c_longlong class timeval(c.Structure): def to_float(self) -> float: ... PA_IO_EVENT_NULL: int PA_IO_EVENT_INPUT: int PA_IO_EVENT_OUTPUT: int PA_IO_EVENT_HANGUP: int PA_IO_EVENT_ERROR: int pa_defer_event_p = c.c_void_p pa_defer_event_cb_t: Incomplete pa_defer_event_destroy_cb_t: Incomplete pa_io_event_p = c.c_void_p pa_io_event_flags = c.c_int pa_io_event_cb_t: Incomplete pa_io_event_destroy_cb_t: Incomplete pa_time_event_p = c.c_void_p pa_time_event_cb_t: Incomplete pa_time_event_destroy_cb_t: Incomplete pa_io_new_t: Incomplete pa_io_enable_t: Incomplete pa_io_set_destroy_t: Incomplete pa_io_free_t: Incomplete pa_time_new_t: Incomplete pa_time_restart_t: Incomplete pa_time_set_destroy_t: Incomplete pa_time_free_t: Incomplete pa_defer_new_t: Incomplete pa_defer_enable_t: Incomplete pa_defer_free_t: Incomplete pa_defer_set_destroy_t: Incomplete pa_quit_t: Incomplete class PythonMainLoop: loop: Incomplete io_events: Incomplete defer_events: Incomplete time_events: Incomplete io_reader_events: Incomplete io_writer_events: Incomplete api_pointer: Incomplete retval: Incomplete def __init__(self, loop: asyncio.AbstractEventLoop) -> None: ... def register_unregister_io_event( self, event: PythonIOEvent, reader: bool, writer: bool ) -> None: ... def stop(self, retval: int) -> None: ... class PythonIOEvent: python_main_loop: Incomplete fd: Incomplete callback: Incomplete userdata: Incomplete on_destroy_callback: Incomplete writer: bool reader: bool self_pointer: Incomplete def __init__( self, python_main_loop: PythonMainLoop, fd: int, callback: pa_io_event_cb_t, userdata: c.c_void_p, ) -> None: ... def read(self) -> None: ... def write(self) -> None: ... def free(self) -> None: ... def set_destroy(self, callback: pa_io_event_destroy_cb_t) -> None: ... class PythonTimeEvent: python_main_loop: Incomplete callback: Incomplete userdata: Incomplete on_destroy_callback: Incomplete handle: Incomplete self_pointer: Incomplete def __init__( self, python_main_loop: PythonMainLoop, callback: pa_time_event_cb_t, userdata: c.c_void_p ) -> None: ... def restart(self, ts: timeval) -> None: ... def free(self) -> None: ... def set_destroy(self, callback: pa_io_event_destroy_cb_t) -> None: ... class PythonDeferEvent: python_main_loop: Incomplete callback: Incomplete userdata: Incomplete on_destroy_callback: Incomplete enabled: bool self_pointer: Incomplete handle: Incomplete def __init__( self, python_main_loop: PythonMainLoop, callback: pa_defer_event_cb_t, userdata: c.c_void_p, ) -> None: ... def call(self) -> None: ... def enable(self, enable: bool) -> None: ... def free(self) -> None: ... def set_destroy(self, callback: pa_io_event_destroy_cb_t) -> None: ... def aio_io_new( main_loop: None, fd: int, flags: int, cb: pa_io_event_cb_t, userdata: c.c_void_p ) -> int: ... def aio_io_enable(e: pa_io_event_p, flags: int) -> None: ... def aio_io_set_destroy(e: pa_io_event_p, cb: pa_io_event_destroy_cb_t) -> None: ... def aio_io_free(e: pa_io_event_p) -> None: ... def aio_time_new( main_loop: None, ts: None, cb: pa_io_event_cb_t, userdata: c.c_void_p ) -> int: ... def aio_time_restart(e: pa_time_event_p, ts: None) -> None: ... def aio_time_set_destroy(e: pa_time_event_p, cb: pa_time_event_destroy_cb_t) -> None: ... def aio_time_free(e: pa_io_event_p) -> None: ... def aio_defer_new(main_loop: None, cb: pa_defer_event_cb_t, userdata: c.c_void_p) -> int: ... def aio_defer_enable(e: pa_defer_event_p, enable: bool) -> None: ... def aio_defer_set_destroy(e: pa_defer_event_p, cb: pa_defer_event_destroy_cb_t) -> None: ... def aio_defer_free(e: pa_io_event_p) -> None: ... def aio_quit(main_loop: None, retval: int) -> None: ... qtile-0.31.0/stubs/pulsectl_asyncio/__init__.pyi0000664000175000017500000000006514762660347021641 0ustar epsilonepsilonfrom .pulsectl_async import PulseAsync as PulseAsync qtile-0.31.0/stubs/pulsectl_asyncio/pulsectl_async.pyi0000664000175000017500000000755214762660347023142 0ustar epsilonepsilonimport asyncio from collections.abc import AsyncIterator from _typeshed import Incomplete from pulsectl.pulsectl import PulseEventInfo from .pa_asyncio_mainloop import PythonMainLoop as PythonMainLoop class _pulse_op_cb: raw: Incomplete future: Incomplete async_pulse: Incomplete def __init__(self, async_pulse: PulseAsync, raw: bool = ...) -> None: ... async def __aenter__(self): ... async def __aexit__(self, exc_type, exc_val, exc_tb) -> None: ... class PulseAsync: name: Incomplete server: Incomplete def __init__( self, client_name: Incomplete | None = ..., server: Incomplete | None = ..., loop: asyncio.AbstractEventLoop | None = ..., ) -> None: ... event_types: Incomplete event_facilities: Incomplete event_masks: Incomplete event_callback: Incomplete waiting_futures: Incomplete channel_list_enum: Incomplete def init(self, loop: asyncio.AbstractEventLoop | None): ... async def connect( self, autospawn: bool = ..., wait: bool = ..., timeout: Incomplete | None = ... ) -> None: ... @property def connected(self): ... def disconnect(self) -> None: ... def close(self) -> None: ... def __enter__(self): ... def __exit__(self, err_t, err, err_tb) -> None: ... async def __aenter__(self): ... async def __aexit__(self, err_t, err, err_tb) -> None: ... get_sink_by_name: Incomplete get_source_by_name: Incomplete get_card_by_name: Incomplete sink_input_list: Incomplete sink_input_info: Incomplete source_output_list: Incomplete source_output_info: Incomplete sink_list: Incomplete sink_info: Incomplete source_list: Incomplete source_info: Incomplete card_list: Incomplete card_info: Incomplete client_list: Incomplete client_info: Incomplete server_info: Incomplete module_info: Incomplete module_list: Incomplete card_profile_set_by_index: Incomplete sink_default_set: Incomplete source_default_set: Incomplete sink_input_mute: Incomplete sink_input_move: Incomplete sink_mute: Incomplete sink_input_volume_set: Incomplete sink_volume_set: Incomplete sink_suspend: Incomplete sink_port_set: Incomplete source_output_mute: Incomplete source_output_move: Incomplete source_mute: Incomplete source_output_volume_set: Incomplete source_volume_set: Incomplete source_suspend: Incomplete source_port_set: Incomplete async def module_load(self, name, args: str = ...): ... module_unload: Incomplete async def stream_restore_test(self): ... stream_restore_read: Incomplete stream_restore_list = stream_restore_read def stream_restore_write( obj_name_or_list, mode: str = ..., apply_immediately: bool = ..., **obj_kws ): ... def stream_restore_delete(obj_name_or_list): ... async def default_set(self, obj) -> None: ... async def mute(self, obj, mute: bool = ...) -> None: ... async def port_set(self, obj, port) -> None: ... async def card_profile_set(self, card, profile) -> None: ... async def volume_set(self, obj, vol) -> None: ... async def volume_set_all_chans(self, obj, vol) -> None: ... async def volume_change_all_chans(self, obj, inc) -> None: ... async def volume_get_all_chans(self, obj): ... async def subscribe_events(self, *masks) -> AsyncIterator[PulseEventInfo]: ... async def get_peak_sample(self, source, timeout, stream_idx: Incomplete | None = ...): ... async def subscribe_peak_sample( self, source, rate: int = ..., stream_idx: Incomplete | None = ..., allow_suspend: bool = ..., ) -> AsyncIterator[float]: ... async def play_sample( self, name, sink: Incomplete | None = ..., volume: float = ..., proplist_str: Incomplete | None = ..., ) -> None: ... qtile-0.31.0/stubs/ipykernel/0000775000175000017500000000000014762660347016000 5ustar epsilonepsilonqtile-0.31.0/stubs/ipykernel/__init__.pyi0000664000175000017500000000000014762660347020250 0ustar epsilonepsilonqtile-0.31.0/stubs/ipykernel/kernelbase.pyi0000664000175000017500000000002214762660347020630 0ustar epsilonepsilonclass Kernel: ... qtile-0.31.0/stubs/ipykernel/kernelapp.pyi0000664000175000017500000000002714762660347020503 0ustar epsilonepsilonclass IPKernelApp: ... qtile-0.31.0/stubs/pulsectl/0000775000175000017500000000000014762660347015631 5ustar epsilonepsilonqtile-0.31.0/stubs/pulsectl/__init__.pyi0000664000175000017500000000335614762660347020122 0ustar epsilonepsilonfrom .pulsectl import Pulse as Pulse from .pulsectl import PulseCardInfo as PulseCardInfo from .pulsectl import PulseCardPortInfo as PulseCardPortInfo from .pulsectl import PulseCardProfileInfo as PulseCardProfileInfo from .pulsectl import PulseClientInfo as PulseClientInfo from .pulsectl import PulseDirectionEnum as PulseDirectionEnum from .pulsectl import PulseDisconnected as PulseDisconnected from .pulsectl import PulseError as PulseError from .pulsectl import PulseEventFacilityEnum as PulseEventFacilityEnum from .pulsectl import PulseEventInfo as PulseEventInfo from .pulsectl import PulseEventMaskEnum as PulseEventMaskEnum from .pulsectl import PulseEventTypeEnum as PulseEventTypeEnum from .pulsectl import PulseExtStreamRestoreInfo as PulseExtStreamRestoreInfo from .pulsectl import PulseIndexError as PulseIndexError from .pulsectl import PulseLoopStop as PulseLoopStop from .pulsectl import PulseModuleInfo as PulseModuleInfo from .pulsectl import PulseObject as PulseObject from .pulsectl import PulseOperationFailed as PulseOperationFailed from .pulsectl import PulseOperationInvalid as PulseOperationInvalid from .pulsectl import PulsePortAvailableEnum as PulsePortAvailableEnum from .pulsectl import PulsePortInfo as PulsePortInfo from .pulsectl import PulseServerInfo as PulseServerInfo from .pulsectl import PulseSinkInfo as PulseSinkInfo from .pulsectl import PulseSinkInputInfo as PulseSinkInputInfo from .pulsectl import PulseSourceInfo as PulseSourceInfo from .pulsectl import PulseSourceOutputInfo as PulseSourceOutputInfo from .pulsectl import PulseStateEnum as PulseStateEnum from .pulsectl import PulseUpdateEnum as PulseUpdateEnum from .pulsectl import PulseVolumeInfo as PulseVolumeInfo from .pulsectl import connect_to_cli as connect_to_cli qtile-0.31.0/stubs/pulsectl/_pulsectl.pyi0000664000175000017500000000752114762660347020353 0ustar epsilonepsilonimport time from ctypes import * from _typeshed import Incomplete force_str: Incomplete force_bytes: Incomplete class c_str_p_type: c_type = c_char_p def __call__(self, val): ... def from_param(self, val): ... unicode: Incomplete c_str_p: Incomplete mono_time = time.monotonic c_str_p = c_char_p PA_INVALID: Incomplete PA_VOLUME_NORM: int PA_VOLUME_MAX: Incomplete PA_VOLUME_INVALID: Incomplete pa_sw_volume_from_dB: Incomplete PA_VOLUME_UI_MAX: int PA_CHANNELS_MAX: int PA_USEC_T = c_uint64 PA_CONTEXT_NOAUTOSPAWN: int PA_CONTEXT_NOFAIL: int PA_CONTEXT_UNCONNECTED: int PA_CONTEXT_CONNECTING: int PA_CONTEXT_AUTHORIZING: int PA_CONTEXT_SETTING_NAME: int PA_CONTEXT_READY: int PA_CONTEXT_FAILED: int PA_CONTEXT_TERMINATED: int PA_SUBSCRIPTION_MASK_NULL: int PA_SUBSCRIPTION_MASK_SINK: int PA_SUBSCRIPTION_MASK_SOURCE: int PA_SUBSCRIPTION_MASK_SINK_INPUT: int PA_SUBSCRIPTION_MASK_SOURCE_OUTPUT: int PA_SUBSCRIPTION_MASK_MODULE: int PA_SUBSCRIPTION_MASK_CLIENT: int PA_SUBSCRIPTION_MASK_SAMPLE_CACHE: int PA_SUBSCRIPTION_MASK_SERVER: int PA_SUBSCRIPTION_MASK_AUTOLOAD: int PA_SUBSCRIPTION_MASK_CARD: int PA_SUBSCRIPTION_MASK_ALL: int PA_SUBSCRIPTION_EVENT_SINK: int PA_SUBSCRIPTION_EVENT_SOURCE: int PA_SUBSCRIPTION_EVENT_SINK_INPUT: int PA_SUBSCRIPTION_EVENT_SOURCE_OUTPUT: int PA_SUBSCRIPTION_EVENT_MODULE: int PA_SUBSCRIPTION_EVENT_CLIENT: int PA_SUBSCRIPTION_EVENT_SAMPLE_CACHE: int PA_SUBSCRIPTION_EVENT_SERVER: int PA_SUBSCRIPTION_EVENT_AUTOLOAD: int PA_SUBSCRIPTION_EVENT_CARD: int PA_SUBSCRIPTION_EVENT_FACILITY_MASK: int PA_SUBSCRIPTION_EVENT_NEW: int PA_SUBSCRIPTION_EVENT_CHANGE: int PA_SUBSCRIPTION_EVENT_REMOVE: int PA_SUBSCRIPTION_EVENT_TYPE_MASK: int PA_SAMPLE_FLOAT32LE: int PA_SAMPLE_FLOAT32BE: int PA_SAMPLE_FLOAT32NE: Incomplete PA_STREAM_DONT_MOVE: int PA_STREAM_PEAK_DETECT: int PA_STREAM_ADJUST_LATENCY: int PA_STREAM_DONT_INHIBIT_AUTO_SUSPEND: int def c_enum_map(**values): ... k: Incomplete PA_EVENT_TYPE_MAP: Incomplete PA_EVENT_FACILITY_MAP: Incomplete PA_EVENT_MASK_MAP: Incomplete PA_UPDATE_MAP: Incomplete PA_PORT_AVAILABLE_MAP: Incomplete PA_DIRECTION_MAP: Incomplete PA_OBJ_STATE_MAP: Incomplete class PA_MAINLOOP(Structure): ... class PA_STREAM(Structure): ... class PA_MAINLOOP_API(Structure): ... class PA_CONTEXT(Structure): ... class PA_PROPLIST(Structure): ... class PA_OPERATION(Structure): ... class PA_SIGNAL_EVENT(Structure): ... class PA_IO_EVENT(Structure): ... class PA_SAMPLE_SPEC(Structure): ... class PA_CHANNEL_MAP(Structure): ... class PA_CVOLUME(Structure): ... class PA_PORT_INFO(Structure): ... class PA_SINK_INPUT_INFO(Structure): ... class PA_SINK_INFO(Structure): ... class PA_SOURCE_OUTPUT_INFO(Structure): ... class PA_SOURCE_INFO(Structure): ... class PA_CLIENT_INFO(Structure): ... class PA_SERVER_INFO(Structure): ... class PA_CARD_PROFILE_INFO(Structure): ... class PA_CARD_PORT_INFO(Structure): ... class PA_CARD_INFO(Structure): ... class PA_MODULE_INFO(Structure): ... class PA_EXT_STREAM_RESTORE_INFO(Structure): ... class PA_BUFFER_ATTR(Structure): ... class POLLFD(Structure): ... PA_POLL_FUNC_T: Incomplete PA_SIGNAL_CB_T: Incomplete PA_STATE_CB_T: Incomplete PA_CLIENT_INFO_CB_T: Incomplete PA_SERVER_INFO_CB_T: Incomplete PA_SINK_INPUT_INFO_CB_T: Incomplete PA_SINK_INFO_CB_T: Incomplete PA_SOURCE_OUTPUT_INFO_CB_T: Incomplete PA_SOURCE_INFO_CB_T: Incomplete PA_CONTEXT_DRAIN_CB_T: Incomplete PA_CONTEXT_INDEX_CB_T: Incomplete PA_CONTEXT_SUCCESS_CB_T: Incomplete PA_EXT_STREAM_RESTORE_TEST_CB_T: Incomplete PA_EXT_STREAM_RESTORE_READ_CB_T: Incomplete PA_CARD_INFO_CB_T: Incomplete PA_MODULE_INFO_CB_T: Incomplete PA_SUBSCRIBE_CB_T: Incomplete PA_STREAM_REQUEST_CB_T: Incomplete PA_STREAM_NOTIFY_CB_T: Incomplete class LibPulse: func_defs: Incomplete class CallError(Exception): ... funcs: Incomplete def __init__(self) -> None: ... def __getattr__(self, k): ... def return_value(self): ... pa: Incomplete qtile-0.31.0/stubs/pulsectl/pulsectl.pyi0000664000175000017500000001555714762660347020224 0ustar epsilonepsilonfrom collections import defaultdict as defaultdict from _typeshed import Incomplete long: Incomplete unicode: Incomplete print_err: Incomplete def wrapper_with_sig_info(func, wrapper, index_arg: bool = ...): ... range: Incomplete map: Incomplete is_str: Incomplete is_str_native: Incomplete is_num: Incomplete is_list: Incomplete is_dict: Incomplete def assert_pulse_object(obj) -> None: ... class FakeLock: def __enter__(self): ... def __exit__(self, *err) -> None: ... class EnumValue: def __init__(self, t, value, c_value: Incomplete | None = ...) -> None: ... def __eq__(self, val): ... def __ne__(self, val): ... def __lt__(self, val): ... def __hash__(self): ... class Enum: def __init__(self, name, values_or_map) -> None: ... def __getitem__(self, k, *default): ... def __contains__(self, k) -> bool: ... PulseEventTypeEnum: Incomplete PulseEventFacilityEnum: Incomplete PulseEventMaskEnum: Incomplete PulseStateEnum: Incomplete PulseUpdateEnum: Incomplete PulsePortAvailableEnum: Incomplete PulseDirectionEnum: Incomplete class PulseError(Exception): ... class PulseOperationFailed(PulseError): ... class PulseOperationInvalid(PulseOperationFailed): ... class PulseIndexError(PulseError): ... class PulseLoopStop(Exception): ... class PulseDisconnected(Exception): ... class PulseObject: c_struct_wrappers: Incomplete volume: Incomplete base_volume: Incomplete port_list: Incomplete port_active: Incomplete channel_list_raw: Incomplete state: Incomplete state_values: Incomplete corked: Incomplete def __init__( self, struct: Incomplete | None = ..., *field_data_list, **field_data_dict ) -> None: ... class PulsePortInfo(PulseObject): c_struct_fields: str def __eq__(self, o): ... def __hash__(self): ... class PulseClientInfo(PulseObject): c_struct_fields: str class PulseServerInfo(PulseObject): c_struct_fields: str class PulseModuleInfo(PulseObject): c_struct_fields: str class PulseSinkInfo(PulseObject): c_struct_fields: str class PulseSinkInputInfo(PulseObject): c_struct_fields: str class PulseSourceInfo(PulseObject): c_struct_fields: str class PulseSourceOutputInfo(PulseObject): c_struct_fields: str class PulseCardProfileInfo(PulseObject): c_struct_fields: str class PulseCardPortInfo(PulsePortInfo): c_struct_fields: str class PulseCardInfo(PulseObject): c_struct_fields: str c_struct_wrappers: Incomplete profile_list: Incomplete profile_active: Incomplete def __init__(self, struct) -> None: ... class PulseVolumeInfo(PulseObject): values: Incomplete def __init__( self, struct_or_values: Incomplete | None = ..., channels: Incomplete | None = ... ) -> None: ... @property def value_flat(self): ... @value_flat.setter def value_flat(self, v) -> None: ... def to_struct(self): ... class PulseExtStreamRestoreInfo(PulseObject): c_struct_fields: str @classmethod def struct_from_value( cls, name, volume, channel_list: Incomplete | None = ..., mute: bool = ..., device: Incomplete | None = ..., ): ... def __init__( self, struct_or_name: Incomplete | None = ..., volume: Incomplete | None = ..., channel_list: Incomplete | None = ..., mute: bool = ..., device: Incomplete | None = ..., ) -> None: ... def to_struct(self): ... class PulseEventInfo(PulseObject): def __init__(self, ev_t, facility, index) -> None: ... class Pulse: name: Incomplete def __init__( self, client_name: Incomplete | None = ..., server: Incomplete | None = ..., connect: bool = ..., threading_lock: bool = ..., ) -> None: ... event_types: Incomplete event_facilities: Incomplete event_masks: Incomplete event_callback: Incomplete channel_list_enum: Incomplete def init(self) -> None: ... connected: bool def connect( self, autospawn: bool = ..., wait: bool = ..., timeout: Incomplete | None = ... ) -> None: ... def disconnect(self) -> None: ... def close(self) -> None: ... def __enter__(self): ... def __exit__(self, err_t, err, err_tb) -> None: ... get_sink_by_name: Incomplete get_source_by_name: Incomplete get_card_by_name: Incomplete sink_input_list: Incomplete sink_input_info: Incomplete source_output_list: Incomplete source_output_info: Incomplete sink_list: Incomplete sink_info: Incomplete source_list: Incomplete source_info: Incomplete card_list: Incomplete card_info: Incomplete client_list: Incomplete client_info: Incomplete server_info: Incomplete module_info: Incomplete module_list: Incomplete card_profile_set_by_index: Incomplete sink_default_set: Incomplete source_default_set: Incomplete sink_input_mute: Incomplete sink_input_move: Incomplete sink_mute: Incomplete sink_input_volume_set: Incomplete sink_volume_set: Incomplete sink_suspend: Incomplete sink_port_set: Incomplete source_output_mute: Incomplete source_output_move: Incomplete source_mute: Incomplete source_output_volume_set: Incomplete source_volume_set: Incomplete source_suspend: Incomplete source_port_set: Incomplete def module_load(self, name, args: str = ...): ... module_unload: Incomplete def stream_restore_test(self): ... stream_restore_read: Incomplete stream_restore_list = stream_restore_read def stream_restore_write( obj_name_or_list, mode: str = ..., apply_immediately: bool = ..., **obj_kws ): ... def stream_restore_delete(obj_name_or_list): ... def default_set(self, obj) -> None: ... def mute(self, obj, mute: bool = ...) -> None: ... def port_set(self, obj, port) -> None: ... def card_profile_set(self, card, profile) -> None: ... def volume_set(self, obj, vol) -> None: ... def volume_set_all_chans(self, obj, vol) -> None: ... def volume_change_all_chans(self, obj, inc) -> None: ... def volume_get_all_chans(self, obj): ... def event_mask_set(self, *masks) -> None: ... def event_callback_set(self, func) -> None: ... def event_listen( self, timeout: Incomplete | None = ..., raise_on_disconnect: bool = ... ) -> None: ... def event_listen_stop(self) -> None: ... def set_poll_func(self, func, func_err_handler: Incomplete | None = ...) -> None: ... def get_peak_sample(self, source, timeout, stream_idx: Incomplete | None = ...): ... def play_sample( self, name, sink: Incomplete | None = ..., volume: float = ..., proplist_str: Incomplete | None = ..., ) -> None: ... def connect_to_cli( server: Incomplete | None = ..., as_file: bool = ..., socket_timeout: float = ..., attempts: int = ..., retry_delay: float = ..., ): ... qtile-0.31.0/stubs/pulsectl/lookup.pyi0000664000175000017500000000026014762660347017663 0ustar epsilonepsilonfrom _typeshed import Incomplete lookup_types: Incomplete lookup_key_defaults: Incomplete def pulse_obj_lookup(pulse, obj_lookup, prop_default: Incomplete | None = ...): ... qtile-0.31.0/stubs/mpd.pyi0000664000175000017500000000011014762660347015271 0ustar epsilonepsilonclass MPDClient: ... class ConnectionError: ... class CommandError: ... qtile-0.31.0/stubs/xml/0000775000175000017500000000000014762660347014576 5ustar epsilonepsilonqtile-0.31.0/stubs/xml/__init__.pyi0000664000175000017500000000000014762660347017046 0ustar epsilonepsilonqtile-0.31.0/stubs/xml/dom/0000775000175000017500000000000014762660347015355 5ustar epsilonepsilonqtile-0.31.0/stubs/xml/dom/__init__.pyi0000664000175000017500000000000014762660347017625 0ustar epsilonepsilonqtile-0.31.0/stubs/xml/dom/minidom/0000775000175000017500000000000014762660347017011 5ustar epsilonepsilonqtile-0.31.0/stubs/xml/dom/minidom/__init__.pyi0000664000175000017500000000005214762660347021270 0ustar epsilonepsilondef parseString(string, parser=None): ... qtile-0.31.0/tox.ini0000664000175000017500000000723014762660347014153 0ustar epsilonepsilon[tox] skip_missing_interpreters = True skipsdist=True minversion = 4.0.12 envlist = # Python environments with specific backend py{py3,310,311,312,313}-{x11,wayland} docs, packaging-{x11,wayland}, # For running pytest locally test-{x11,wayland,both} # Set up some variables that can be used multiple times [base] deps = setuptools >= 40.5.0 dbus-fast wheel cffi xcffib >= 1.4.0 cairocffi >= 1.7.0 # We use "!x11" here so pywayland is installed both in CI wayland environment # But also when users run locally (as both backends are run in that scenario) !x11: pywayland==0.4.17 !x11: xkbcommon >= 0.3 testdeps = pytest >= 6.2.1 {wayland,x11}: coverage libcst >= 1.0.0 PyGObject isort mypy commands = # pywayland has to be installed before pywlroots !x11: pip install pywlroots==0.17.0 pip install --break-system-packages . !x11: {toxinidir}/scripts/ffibuild # These are the environments that should be triggered by Github Actions [testenv:py{py3,310,311,312,313}-{wayland,x11}] # This is required in order to get UTF-8 output inside of the subprocesses # that our tests use. setenv = LC_CTYPE = en_US.UTF-8 # Pass Display down to have it for the tests available passenv = DISPLAY,WAYLAND_DISPLAY,LDFLAGS,CFLAGS allowlist_externals = */ffibuild convert deps = {[base]deps} {[base]testdeps} commands = {[base]commands} # pypy3 is very slow when running coverage reports so we skip it pypy3-x11: python3 -m pytest --backend=x11 {posargs} pypy3-wayland: python3 -m pytest --backend=wayland {posargs} py3{10,11,12,13}-x11: coverage run -m pytest --backend=x11 {posargs} py3{10,11,12,13}-wayland: coverage run -m pytest --backend=wayland {posargs} # Coverage is only run via GithubActions # Coverage runs tests in parallel so we need to combine results into a single file !pypy3-{wayland,x11}: coverage combine -q # Include a text summary in the build log !pypy3-{wayland,x11}: coverage report -m # Create an xml summary to be submitted to coveralls.io !pypy3-{wayland,x11}: coverage xml # Basic environment for local testing [testenv:test] # This is required in order to get UTF-8 output inside of the subprocesses # that our tests use. setenv = LC_CTYPE = en_US.UTF-8 # Pass Display down to have it for the tests available passenv = DISPLAY,WAYLAND_DISPLAY,LDFLAGS,CFLAGS allowlist_externals = */ffibuild convert deps = {[base]deps} {[base]testdeps} commands = {[base]commands} python -m pytest {posargs} # Additional local environments with specified backends [testenv:test-{x11,wayland,both}] # This is required in order to get UTF-8 output inside of the subprocesses # that our tests use. setenv = LC_CTYPE = en_US.UTF-8 # Pass Display down to have it for the tests available passenv = DISPLAY,WAYLAND_DISPLAY,LDFLAGS,CFLAGS allowlist_externals = */ffibuild convert deps = {[base]deps} {[base]testdeps} commands = {[base]commands} x11: python -m pytest --backend=x11 {posargs} wayland: python -m pytest --backend=wayland {posargs} both: python -m pytest --backend=wayland --backend=x11 {posargs} [testenv:packaging-{x11,wayland}] deps = check-manifest twine build commands = check-manifest python3 -m build --sdist . twine check dist/* [testenv:docs] deps = -r{toxinidir}/docs/requirements.txt commands = pip install -r{toxinidir}/requirements.txt python3 setup.py build_sphinx -W [gh-actions] python = pypy-3.10: pypy3 3.10: py310 3.11: py311 3.12: py312 3.13: py313, packaging [gh-actions:env] BACKEND = x11: x11 wayland: wayland qtile-0.31.0/Makefile0000664000175000017500000000140514762660347014276 0ustar epsilonepsilon.DEFAULT_GOAL := help .PHONY: help help: ## Show this help @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[1m%-15s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) .PHONY: check check: ## Run the test suite @PY=`python --version | sed 's/.*\.\([0-9]*\)\..*/py3\1/'`; \ echo TOXENV=$$PY tox; \ TOXENV=$$PY tox .PHONY: lint lint: ## Check the source code pre-commit run -a .PHONY: clean clean: ## Clean generated files -rm -rf dist qtile.egg-info docs/_build build/ .tox/ .mypy_cache/ .pytest_cache/ .eggs/ .PHONY: run-ffibuild run-ffibuild: ## Build FFI modules ./scripts/ffibuild .PHONY: update-flake update-flake: ## Update the Nix flake.lock file, requires Nix installed with flake support, see: https://nixos.wiki/wiki/Flakes nix flake update qtile-0.31.0/.coveragerc0000664000175000017500000000033514762660347014760 0ustar epsilonepsilon[run] source = libqtile concurrency = multiprocessing parallel = true sigterm = true dynamic_context = test_function [report] omit = libqtile/interactive/* libqtile/scripts/* libqtile/ffi_build.py test/* qtile-0.31.0/CONTRIBUTING.md0000664000175000017500000000030614762660347015066 0ustar epsilonepsilon# How to contribute Instead of making this document a copy of [the _contributing_ section of our documentation](https://docs.qtile.org/en/latest/manual/contributing.html), we just link to it here. qtile-0.31.0/resources/0000775000175000017500000000000014762660347014650 5ustar epsilonepsilonqtile-0.31.0/resources/README0000664000175000017500000000063414762660347015533 0ustar epsilonepsilonobjgraph.dot: The Qtile command graph, in graphviz dot format. Render like so: circo -Tpng objgraph.dot > objgraph.png qtile.desktop: A desktop file for qtile telling session managers about qtile and how to run it. This file should be installed to /usr/share/xsessions qtile-wayland.desktop: A desktop file to start qtile in wayland mode. 99-qtile.rules: udev rules for hardware that qtile can manage qtile-0.31.0/resources/qtile-wayland.desktop0000664000175000017500000000017314762660347021017 0ustar epsilonepsilon[Desktop Entry] Name=Qtile (Wayland) Comment=Qtile Session Exec=qtile start -b wayland Type=Application Keywords=wm;tiling qtile-0.31.0/resources/qtile.desktop0000664000175000017500000000014614762660347017362 0ustar epsilonepsilon[Desktop Entry] Name=Qtile Comment=Qtile Session Exec=qtile start Type=Application Keywords=wm;tiling qtile-0.31.0/resources/99-qtile.rules0000664000175000017500000000220214762660347017275 0ustar epsilonepsilon# for controlling LCD backlight ACTION=="add", SUBSYSTEM=="backlight", RUN+="qtile udev --group sudo backlight --device %k" # keyboard backlight ACTION=="add", SUBSYSTEM=="leds", RUN+="qtile udev --group sudo backlight --device %k" # fancy battery charge control, needs to be per ACPI implementation, so we need # to periodically check the kernel for more of these: # # $ ~/packages/linux/drivers/platform/x86 master git grep -l charge_control_end_threshold # asus-wmi.c # huawei-wmi.c # lg-laptop.c # msi-ec.c # system76_acpi.c # thinkpad_acpi.c # toshiba_acpi.c # # Last checked as of 6.8-rc4. ACTION=="add" KERNEL=="asus-wmi" RUN+="qtile udev --group sudo battery" ACTION=="add" KERNEL=="huawei-wmi" RUN+="qtile udev --group sudo battery" ACTION=="add" KERNEL=="lg-laptop" RUN+="qtile udev --group sudo battery" ACTION=="add" KERNEL=="msi-ec" RUN+="qtile udev --group sudo battery" ACTION=="add" KERNEL=="thinkpad_acpi" RUN+="qtile udev --group sudo battery" ACTION=="add" KERNEL=="system76_acpi" RUN+="qtile udev --group sudo battery" ACTION=="add" KERNEL=="toshiba_acpi" RUN+="qtile udev --group sudo battery" qtile-0.31.0/resources/objgraph.dot0000664000175000017500000000100214762660347017145 0ustar epsilonepsilon digraph G { root = "root"; splines = true; root -> bar; root -> group; root -> layout; root -> screen; root -> widget; root -> window; bar -> screen; group -> layout; group -> screen; group -> window; layout -> group; layout -> screen; layout -> window; screen -> bar; screen -> layout; screen -> window; widget -> bar; widget -> group; widget -> screen; window -> group; window -> screen; window -> layout; } qtile-0.31.0/dev.sh0000775000175000017500000000114314762660347013752 0ustar epsilonepsilon#!/bin/sh set -e set -x echo "Creating dev environment in ./venv..." python=python3 if [ "$#" -eq 1 ]; then python=$1 fi ${python} -m venv venv . venv/bin/activate pip install -U pip setuptools wheel echo "Installing other required packages..." pip install -r requirements.txt pip install -r requirements-dev.txt echo "Installing pre-commit hooks..." pre-commit install echo "" echo " * Created virtualenv environment in ./venv." echo " * Installed all dependencies into the virtualenv." echo " * You can now activate the $(python3 --version) virtualenv with this command: \`. venv/bin/activate\`" qtile-0.31.0/CHANGELOG0000664000175000017500000016114314762660347014056 0ustar epsilonepsilonQtile x.xx.x, released xxxx-xx-xx: * features * bugfixes Qtile 0.31.0, released 2025-03-07: !!! breaking changes !!! - Spiral layout: `shuffle_up` and `shuffle_down` commands now shuffle the focused window instead of rotating all windows. * features - Add TunedManager Widget to show the currently active power profile and cycle through a list of configurable power profiles * bugfixes - Fix StatusNotifier displaying wrong icon when icon changes too quickly (e.g. nm-applet) - PulseVolume widget: fix breakage with xrandr/wlr-randr + reload config #5111 Qtile 0.30.0, released 2025-01-06: !!! breaking changes !!! - dbus-fast is now required for dbus support. dbus-next was removed as the package is unmaintained. * features - Add `SwayNC` widget to interact with Sway Notification Centre (wayland only) - Add `swap` method to Plasma layout - New `click_or_drag_only` option for follow_mouse_focus to change the focus to the window under the mouse when click or drag - Customize battery widget "Full" and "Empty" short text with `full_short_text` and `empty_short_text` * bugfixes - Make MonadWide layout up/down focus navigation behave like MonadTall's left/right Qtile 0.29.0, released 2024-10-19: * features - A Nix flake has been added which can be used by Nix(OS) users to update to the latest version easily - Add `group_window_remove` hook when window is removed from group - Switched to ruff for formatting * bugfixes - Add returns to lazy.function and qtile.core.function - Fix a bug with newer versions of cairo breaking the bar - Fix a few TreeTab rendering bugs - Fix a TaskList crash - Fix a scratchpad window size bug Qtile 0.28.1, released 2024-08-12: * bugfixes - fix a crash in the StatusNotifier widget #4959 #4960 Qtile 0.28.0, released 2024-08-11: * bugfixes - various bug fixes to widgets from previous releases - fix xrandr commands racing with qtile startup Qtile 0.27.0, released 2024-07-12: * features - Make default `Plasma` add mode dynamic - Add `background` parameter to `Screen` to paint a solid colour background - Add ability to use key codes to bind keys. Will benefit users who change keyboard layouts but wish to retain same bindings, irrespective of layout. - Wayland: Add support for idle-notify-v1 protocol needed by swayidle. - Wayland: Make keybinds repeat according to the keyboard's repeat rate and delay. Previously the keybinds did not repeat. * bugfixes - Fix `Plasma` layout with `ScreenSplit` by implementing `get_windows` - Fix border bug in fullscreening/maximizing wayland windows - Fix automatic fullscreening for many XWayland applications (e.g. games) by checking if they want to fullscreen on map Qtile 0.26.0, released 2024-05-21: !!! breaking changes !!! - this release drops support for python 3.9 - deleted the (very old) libqtile/command_* deprecation wrappers - SIGUSR2 no longer restarts qtile, instead it dumps stack traces - lazy..when(when_floating=X) now behaves differently: the lazy call will be executed independently of the current window's float state by default, and can be limited to when it is floating or tiled by passing when_floating as True or False respectively. - Dropped support for KDE idle protocol on Wayland !!! Notice for packagers - Wayland backend !!! - Qtile's Wayland backend now requires wlroots 0.17.x, pywlroots 0.17.x and pywayland >= 0.4.17. !!! Notice for pip package - Pypy !!! - We currently do not build for pypy-3.10 as there seems to be a resolution error in either pypy or pip (https://github.com/pypy/pypy/issues/4956) * features - For Wayland you can now set the cursor theme and size to forcefully use in Qtile, set `wl_xcursor_theme` and `wl_xcursor_size` in the configuration - automatically lift types to their annotated type when specified via the `qtile cmd-obj` command line - Add `Plasma` layout. The original layout (https://github.com/numirias/qtile-plasma) appears to be unmaintained so we have added this to the main codebase. - Add ability to specify muted and unmuted formats for `Volume` and `PulseVolume` widgets. - Add back server-side opacity support for Wayland backend * bugfixes Qtile 0.25.0, released 2024-04-06: * features - The Battery widget now supports dynamic charge control, allowing for protecting battery life. - To support the above (plus the other widgets that modify sysfs), qtile now ships with its own udev rules, located at /resources/99-qtile.rules; distro packagers will probably want to install this rule set. * bugfixes - Fix groups marked with `persist=False` not being deleted when their last window is moved to another group. - Fallback icon in StatusNotifier widget Qtile 0.24.0, released 2024-01-20: !!! config breakage/changes !!! - Matches no longer use "include/substring" style matching. But match the string exactly. Previously on X11, if the WM_TYPE of a spawned window is e.g. dialog a match with wm_type dialognoonereadschangelogs would return true. Additionally a window with an empty WM_CLASS (which can happen) would match anything. If you rely this style of substring matching, pass a regex to your match or use a function with func=. Using a list of strings inside Match with role, title, wm_class, wm_instance_class, wm_type are also deprecated, use a regex. Right now we replace the property with a regex if it's a list and warn with a deprecation message. You can use "qtile migrate" to migrate your config to this. * features - Change how `tox` runs tests. See https://docs.qtile.org/en/latest/manual/contributing.html#running-tests-locally for more information on how to run tests locally. - Add `ScreenSplit` layout which allows multiple layouts per screen. Also adds `ScreenSplit` widget to display name of active split. - Updated `Bluetooth` widget which allows users to manage multiple devices in a single widget - Add `align` option to `Columns` layout so new windows can be added to left or right column. - `.when()` have two new parameters: - `func: Callable`: Enable call when the result of the callable evaluates to True - `condition: bool`: a boolean value to determine whether the lazy object should be run. Unlike `func`, the condition is evaluated once when the config file is first loaded. - Add ability to have bar drawns over windows by adding `reserve=False` to bar's config to stop the bar reserving screen space. - Add ability for third-party code (widgets, layouts) to create hooks - Add ability to create user-defined hooks which can be fired from external scripts * bugfixes - Fix two bugs in stacking transient windows in X11 - Checking configs containing `qtile.core.name` with `python config.py` don't fail anymore (but `qtile.core.name` will be `None`) - Fix an error if a wayland xwindow has unknown wm_type Qtile 0.23.0, released 2023-09-24: !!! Dependency Changes !!! - xcffib must be upgraded to >= 1.4.0 - cairocffi must be upgraded to >= 1.6.0 - New optional dependency `pulsectl-asyncio` required for `PulseVolume` widget !!! Notice for packagers - wlroots (optional dependency) bump !!! - Qtile's wayland backend now requires on wlroots 0.16 (and pywlroots 0.16) !!! config breakage/changes !!! - The `cmd_` prefix has been dropped from all commands (this means command names are common when accessed via the command interface or internal python objects). - Custom widgets should now expose command methods with the `@expose_command` decorator (available via `from libqtile.command.base import expose_command`). - Some commands have been renamed (in addition to dropping the 'cmd_' prefix): `hints` -> `get_hints` `groups` -> `get_groups` `screens` -> `get_screens` - Layouts need to rename some methods: - `add` to `add_client` - `cmd_next` to `next` - `cmd_previous` to `previous` - Layouts or widgets that redefine the `commands` property need to update the signature: `@expose_command()` `def commands(self) -> list[str]:` - `Window.getsize` has been renamed `Window.get_size` (i.e. merged with the get_size command). - `Window.getposition` has been renamed `Window.get_position` (i.e. merged with the get_position command). - The `StockTicker` widget `function` option is being deprecated: rename it to `func`. - The formatting of `NetWidget` has changed, if you use the `format` parameter in your config include `up_suffix`, `total_suffix` and `down_suffix` to display the respective units. - The `Notify` widget now has separate `default_timeout` properties for differenct urgency levels. Previously, `default_timeout` was `None` which meant that there was no timeout for all notifications (unless this had been set by the client sending the notification). Now, `default_timeout` is for normal urgency notifications and this has been set to a default of 10 seconds. `default_timeout_urgent`, for critical notifications, has a timeout of `None`. - The `PulseVolume` widget now depends on a third party library, `pulsectl-asyncio`, to interact with the pulse audio server. Users will now see an `ImportError` until they install that library. * features - Add ability to set icon size in `LaunchBar` widget. - Add 'warp_pointer' option to `Drag` that when set will warp the pointer to the bottom right of the window when dragging begins. - Add `currentsong` status to `Mpd2` widget. - Add ability to disable group toggling in `GroupBox` widget - Add ability to have different border color when windows are stacked in Stack layout. Requires setting `border_focus_stack` and `border_normal_stack` variables. - Add ability to have different single border width for Columns layout by setting 'single_border_width' key. - Add ability to have different border and margin widths when VerticalTile layout only contains 1 window by setting 'single_border_width' and 'single_margin' keys. - New widget: GenPollCommand - Add `format` and `play_icon` parameters for styling cmus widget. - Add ability to add a group at a specified index - Add ability to spawn the `WidgetBox` widget opened. - Add ability to swap focused window based on index, and change the order of windows inside current group - Add ability to update the widget only once if `update_interval` is None. - Add `move_to_slice` command to move current window to single layout in `Slice` layout - Made the `NetWidget` text formattable. - Qtile no longer floods the log following X server disconnection, instead handling those errors. - `Key` and `KeyChord` bindings now have another argument `swallow`. It indicates whether or not the pressed keys should be passed on to the focused client. By default the keys are not passed (swallowed), so this argument is set to `True`. When set to `False`, the keys are passed to the focused client. A key is never swallowed if the function is not executed, e.g. due to failing the `.when()` check. - Add ability to set custom "Undefined" status key value to `Mpd2Widget`. - `Mpd2Widget` now searches for artist name in all similar keys (i.e `albumartist`, `performer`, etc.). - Add svg support to `CustomLayoutIcon` - added layering controls for X11 (Wayland support coming soon!): - `lazy.window.keep_above()/keep_below()` marks windows to be kept above/below other windows permanently. Calling the functions with no arguments toggles the state, otherwise pass `enable=True` or `enable=False`. - `lazy.window.move_up()/move_down()` moves windows up and down the z axis. - added `only_focused` setting to Max layout, allowing to draw multiple clients on top of each other when set to False - Add `suspend` hook to run functions before system goes to sleep. * bugfixes - Fix bug where Window.center() centers window on the wrong screen when using multiple monitors. - Fix `Notify` bug when apps close notifications. - Fix `CPU` precision bug with specific version of `psutil` - Fix config being reevaluated twice during reload (e.g. all hooks from config were doubled) - Fix `PulseVolume` high CPU usage when update_interval set to 0. - Fix `Battery` widget on FreeBSD without explicit `battery` index given. - Fix XMonad layout faulty call to nonexistent _shrink_up - Fix setting tiled position by mouse for layouts using _SimpleLayoutBase. To support this in other layouts, add a swap method taking two windows. - Fix unfullscreening bug in conjunction with Chromium based clients when auto_fullscreen is set to `False`. - Ensure `CurrentLayoutIcon` expands paths for custom folders. - Fix vertical alignment of icons in `TaskList` widget - Fix laggy resize/positioning of floating windows in X11 by handling motion notify events later. We also introduced a cap setting if you want to limit these events further, e.g. for limiting resource usage. This is configurable with the x11_drag_polling_rate variable for each `Screen` which is set to None by default, indicating no cap. * python version support - We have added support for python 3.11 and pypy 3.9. - python 3.7, 3.8 and pypy 3.7 are not longer supported. - Fix bug where `StatusNotifier` does not update icons Qtile 0.22.0, released 2022-09-22: !!! Config breakage !!! - lazy.qtile.display_kb() no longer receives any arguments. If you passed it any arguments (which were ignored previously), remove them. - If you have a custom startup Python script that you use instead of `qtile start` and run init_log manually, the signature has changed. Please check the source for the updated arguments. - `KeyChord`'s signature has changed. ``mode`` is now a boolean to indicate whether the mode should persist. The ``name`` parameter should be used to name the chord (e.g. for the ``Chord`` widget). * features - Add ability to draw borders and add margins to the `Max` layout. - The default XWayland cursor is now set at startup to left_ptr, so an xsetroot call is not needed to avoid the ugly X cursor. - Wayland: primary clipboard should now behave same way as with X after selecting something it should be copied into clipboard - Add `resume` hook when computer resumes from sleep/suspend/hibernate. - Add `text_only` option for `LaunchBar` widget. - Add `force_update` command to `ThreadPoolText` widgets to simplify updating from key bindings - Add scrolling ability to `_TextBox`-based widgets. - Add player controls (via mouse callbacks) to `Mpris2` widget. - Wayland: input inhibitor protocol support added (pywayland>=0.4.14 & pywlroots>=0.15.19) - Add commands to control Pomodoro widget. - Add icon theme support to `TaskList` widget (available on X11 and Wayland backends). - Wayland: Use `qtile cmd-obj -o core -f get_inputs` to get input device identifiers for configuring inputs. Also input configs will be updated by config reloads (pywlroots>=0.15.21) * bugfixes - Widgets that are incompatible with a backend (e.g. Systray on Wayland) will no longer show as a ConfigError in the bar. Instead the widget is silently removed from the bar and a message included in the logs. - Reduce error messages in `StatusNotifier` widget from certain apps. - Reset colours in `Chord` widget - Prevent crash in `LaunchBar` when using SVG icons - Improve scrolling in `Mpris2` widget (options to repeat scrolling etc.) Qtile 0.21.0, released 2022-03-23: * features - Add `lazy.window.center()` command to center a floating window on the screen. - Wayland: added power-output-management-v1 protocol support, added idle protocol, added idle inhibit protocol - Add MonadThreeCol layout based on XMonad's ThreeColumns. - Add `lazy.screen.set_wallpaper` command. - Added ability to scale the battery icon's size - Add Spiral layout - Add `toggle` argument to `Window.togroup` with the same functionality as in `Group.toscreen`. - Added `margin_on_single` and `border_on_single` to Bsp layout * bugfixes - Fix `Systray` crash on `reconfigure_screens`. - Fix bug where widgets can't be mirrored in same bar. - Fix various issues with setting fullscreen windows floating and vice versa. - Fix a bug where a .when() check for lazy functions errors out when matching on focused windows when none is focused. By default we do not match on focused windows, to change this set `if_no_focused` to True. - Widget with duplicate names will be automatically renamed by appending numeric suffixes - Fix resizing of wallpaper when screen scale changes (X11) - Two small bugfixes for `StatusNotifier` - better handling of Ayatana indicators - Fix bug where StatusNotifierItem crashes due to invalid object paths (e.g. Zoom) Qtile 0.20.0, released 2022-01-24: * features - Add `place_right` option in the TreeTab layout to place the tab panel on the right side - X11: Add support for _NET_DESKTOP_VIEWPORT. E.g. can be used by rofi to map on current output. - Wayland: Bump wlroots version. 0.15.x wlroots and 0.15.2+ pywlroots are required. - Add XWayland support to the Wayland backend. XWayland will start up as needed, if it is installed. * bugfixes - Remove non-commandable windows from IPC. Fixes bug where IPC would fail when trying to get info on all windows but Systray has icons (which are non-commandable `_Window`s.) - Fix bug where bars were not reconfigured correctly when screen layout changes. - Fix a Wayland bug where layer-shell surface like dunst would freeze up and stop updating. - Change timing of `screens_reconfigured` hook. Will now be called ONLY if `cmd_reconfigure_screens` has been called and completed. - Fix order of icons in Systray widget when restarting/reloading config. - Fix rounding error in PulseVolume widget's reported volume. - Fix bug where Volume widget did not load images where `theme_path` had been set in `widget_defaults`. - Remove ability to have multiple `Systray` widgets. Additional `Systray` widgets will result in a ConfigError. - Release notification name from dbus when finalising `Notify` widget. This allows other notification managers to request the name. - Fix bug where `Battery` widget did not retrieve `background` from `widget_defaults`. - Fix bug where widgets in a `WidgetBox` are rendered on top of bar borders. - Add ability to swap focused window based on index, and change the order of windows inside current group Qtile 0.19.0, released 2021-12-22: * features - Add ability to draw borders to the Bar. Can customise size and colour per edge. - Add `StatusNotifier` widget implementing the `StatusNotifierItem` specification. NB Widget does not provide context menus. - Add `total` bandwidth format value to the Net widget. - Scratchpad groups could be defined as single so that only one of the scratchpad in the group is visible at a given time. - All scratchpads in a Scratchpad group can be hidden with hide_all() function. - For saving states of scratchpads during restart, we use wids instead of pids. - Scratchpads can now be defined with an optional matcher to match with window properties. - `Qtile.cmd_reload_config` is added for reloading the config without completely restarting. - Window.cmd_togroup's argument `groupName` should be changed to `group_name`. For the time being a log warning is in place and a migration is added. In the future `groupName` will fail. - Add `min/max_ratio` to Tile layout and fix bug where windows can extend offscreen. - Add ability for widget `mouse_callbacks` to take `lazy` calls (similar to keybindings) - Add `aliases` to `lazy.spawncmd()` which takes a dictionary mapping convenient aliases to full command lines. - Add a new 'prefix' option to the net widget to display speeds with a static unit (e.g. MB). - `lazy.group.toscreen()` now does not toggle groups by default. To get this behaviour back, use `lazy.group.toscreen(toggle=True)` - Tile layout has new `margin_on_single` and `border_on_single` option to specify whether to draw margin and border when there is only one window. - Thermal zone widget. - Allow TextBox-based widgets to display in vertical bars. - Added a focused attribute to `lazy.function.when` which can be used to Match on focused windows. - Allow to update Image widget with update() function by giving a new path. * bugfixes - Windows are now properly re-ordered in the layouts when toggled on and off fullscreen Qtile 0.18.1, released 2021-09-16: * features - All layouts will accept a list of colors for border_* options with which they will draw multiple borders on the appropriate windows. Qtile 0.18.0, released 2021-07-04: !!! Config breakage !!! - The `qtile` entry point doesn't run `qtile start` by default anymore - New optional dependency for dbus related features: dbus-next. Replaces previous reliance on dbus/Glib and allows qtile to use async dbus calls within asyncio's eventloop. - widget.BatteryIcon no longer has a fallback text mode; use widget.Battery instead - MonadX layout key new_at_current is deprecated, use new_client_position. - `libqtile.window` has been moved to `libqtile.backend.x11.window`; a migration has been added for this. !!! deprecation warning !!! - 'main' config functions, deprecated in 0.16.1, will no longer be executed. !!! Notice for packagers - new dependencies !!! - Tests now require the 'dbus-next' python module plus 'dbus-launch' and 'notify-send' applications * features - added transparency in x11 and wayland backends - added measure_mem and measure_swap attributes to memory widget to allow user to choose measurement units. - memory widget can now be displayed with decimal values - new "qtile migrate" command, which will attempt to upgrade previous configs to the current version in the case of qtile API breaks. - A new `reconfigure_screens` config setting. When `True` (default) it hooks `Qtile.reconfigure_screens` to the `screen_change` hook, reconfiguring qtile's screens in response to randr events. This removes the need to restart qtile when adding/removing external monitors. - improved key chord / sequence functionality. Leaving a chord with `mode` set brings you to a named mode you activated before, see #2264. A new command, `lazy.ungrab_all_chords`, was introduced to return to the root bindings. The `enter_chord` hook is now always called with a string argument. The third argument to `KeyChord` was renamed from `submaping` to `submapping` (typo fix). - added new argument for CheckUpdates widget: `custom_command_modify` which allows user to modify the the line count of the output of `custom_command` with a lambda function (i.e. `lambda x: x-3`). Argument defaults to `lambda x: x` and is overridden by `distro` argument's internal lambda. - added new argument for the WindowName, WindowTabs and Tasklist widgets: `parse_text` which allows users to define a function that takes a window name as an input, modify it in some way (e.g. str.replace(), str.upper() or regex) and show that modification on screen. - A Wayland backend has been added which can be used by calling `qtile start -b wayland` directly in your TTY. It requires the latest releases of wlroots, python-xkbcommon, pywayland and pywlroots. It is expected to be unstable so please let us know if you find any bugs! - The 'focus` argument to `Click` and `Drag` objects in your config are no longer necessary (and are ignored). Qtile 0.17.0, released 2021-02-13: !!! Python version breakage !!! - Python 3.5 and 3.6 are no longer supported !!! Config breakage !!! - Pacman widget has been removed. Use CheckUpdates instead. - Mpris widget has been removed. Use Mpris2 instead. - property "masterWindows" of Tile layout renamed to master_length - Match objects now only allow one string argument for their wm name/class/etc. properties. to update your config, do e.g. Group('www', spawn='firefox', layout='xmonad', - matches=[Match(wm_class=['Firefox', 'google-chrome', 'Google-chrome'])]), + matches=[Match(wm_class='Firefox'), Match(wm_class='google-chrome'), Match(wm_class='Google-chrome')]), - properties wname, wmclass and role of Slice-layout replaced by Match- type property "match" - rules specified in `layout.Floating`'s `float_rules` are now evaluated with AND-semantics instead of OR-semantics, i.e. if you specify 2 different property rules, both have to match - check the new `float_rules` for `floating_layout` in the default config and extend your own rules appropriately: some non-configurable auto-floating rules were made explicit and added to the default config - using `dict`s for `layout.Floating`'s `float_rules` is now deprecated, please use `config.Match` objects instead - `no_reposition_match` in `layout.Floating` has been removed; use the list of `config.Match`-objects `no_reposition_rules` instead - Command line has been modernized to a single entry point, the `qtile` binary. Translations are below: qtile -> qtile start qtile-cmd -> qtile cmd-obj qtile-run -> qtile run-cmd qtile-top -> qtile top qshell -> qtile shell iqshell and dqtile-cmd are no longer distributed with the package, as they were either user or developer scripts. Both are still available in the qtile repo in /scripts. Running `qtile` without arguments will continue to work for the forseeable future, but will be eventually deprecated. qtile prints a warning when run in this configuration. - Qtile.cmd_focus_by_click is no longer an available command. - Qtile.cmd_get_info is no longer an available command. - libqtile.command_* has been deprecated, it has been moved to libqtile.command.* - libqtile.widget.base.ThreadedPollText has been removed; out of tree widgets can use ThreadPoolText in the same package instead. - the YahooWeather widget was removed since Yahoo retired their free tier of the weather API - Deprecated hook `window_name_change` got removed, use `client_name_updated` instead. - show_state attribute from WindowName widget has been removed. Use format attribute instead. show_state = True -> format = '{state}{name}' show_state = False -> format = '{name}' - mouse_callbacks no longer receives the qtile object as an argument (they receive no arguments); import it via `from libqtile import qtile` instead. * features - new WidgetBox widget - new restart and shutdown hooks - rules specified in `layout.Floating`'s `float_rules` are now evaluated with AND-semantics, allowing for more complex and specific rules - Python 3.9 support - switch to Github Actions for CI - Columns layout has new `margin_on_single` option to specify margin size when there is only one window (default -1: use `margin` option). - new OpenWeather widget to replace YahooWeather - new format attribute for WindowName widget - new max_chars attribute for WindowName widget - libqtile now exports type information - add a new `qtile check` subcommand, which will check qtile configs for various things: - validates configs against the newly exported type information if mypy is present in the environment - validates that qtile can import the config file (e.g. that syntax is correct, ends in a .py extension, etc.) - validates Key and Mouse mod/keysym arguments are ok. - Columns layout now enables column swapping by using swap_column_left and swap_column_right !!! warning !!! - When (re)starting, Qtile passes its state to the new process in a file now, where previously it passed state directly as a string. This fixes a bug where some character encodings (i.e. in group names) were getting messed up in the conversion to/from said string. This change will cause issues if you update Qtile then restart it, causing the running old version to pass state in the previous format to the new process which recognises the new. Qtile 0.16.1, released 2020-08-11: !!! Config breakage !!! - Hooks 'addgroup', 'delgroup' and 'screen_change' will no longer receive the qtile object as an argument. It can be accessed directly at libqtile.qtile. !!! deprecation warning !!! - defining a main function in your config is deprecated. You should use @hook.subscribe.startup_complete instead. If you need access to the qtile object, import it from libqtile directly. * bugfixes - include tests in the release for distros to consume - don't resize 0th screen incorrectly on root ConfigureNotify - expose qtile object as libqtile.qtile (note that we still consider anything not prefixed with cmd_ to be a private API) - fix transparent borders - MonadTall, MonadWide, and TreeTab now work with Slice Qtile 0.16.0, released 2020-07-20: !!! Config breakage !!! - Imports from libqtile.widget are now made through a function proxy to avoid the side effects of importing all widgets at once. If you subclass a widget in your config, import it from its own module. e.g. from libqtile.widget.pomodoro import Pomodoro * features - added `guess_terminal` in utils - added keybinding cheet sheet image generator - custom keyboardlayout display - added native support for key chords - validate config before restart and refuse to restart with a bad config - added a bunch of type annotations to config objects (more to come) * bugfixes - major focus rework; Java-based IDEs such as PyCharm, NetBrains, etc. now focus correctly - fix a bug where spotify (or any window with focus-to=parent) was closed, nothing would be focused and no hotkeys would work - support windows unsetting the input hint - respects window's/user's location setting if present (WM_SIZE_HINTS) - fixed YahooWeather widget for new API - fix a bug where _NET_WM_DESKTOPS wasn't correctly updated when switching screens in some cases - fix a crash in the BSP layout - fix a stacktrace when unknown keysyms are encounted - make qtile --version output more sane - fix a rendering issue with special characters in window names - keyboard widget no longer re-sets the keyboard settings every second - fix qtile-top with the new IPC model - Image widget respects its background setting now - correctly re-draw non-focused screens on qtile restart - fix a crash when decoding images - fix the .when() constraint for lazy objects Qtile 0.15.1, released 2020-04-14 * bugfixes - fix qtile reload (it was crashing) Qtile 0.15.0, released 2020-04-12: !!! Config breakage !!! - removed the mpd widget, which depended on python-mpd. - the Clock widget now requires pytz to handle timezones that are passed as string - libqtile.command.Client does not exist anymore and has been replaced by libqtile.command_client.CommandClient !!! deprecation warning !!! - libqtile.command.lazy is deprecated in favor of libqtile.lazy.lazy * features - Python 3.8 support - `wallpaper` and `wallpaper_mode` for screens - bars can now have margins - `lazy.toscreen` called twice will now toggle the groups (optional with the `toggle` parameter) - `lazy.window.togroup` now has `switch_group` parameter to follow the window to the group it is sent to - qtile now copies the default config if the config file does not exist - all widgets now use Pango markup by default - add an `fmt` option for all textbox widgets - new PulseVolume widget for controlling PulseAudio - new QuickExit widget, mainly for the default config - new non-graph CPU widget - KeyboardLayout widget: new `options` parameter - CheckUpdates widget: support ArchLinux yay - GroupBox widget: new `block_highlight_text_color` parameter - Mpd2 widget: new `color_progress` parameter - Maildir widget can now display the inbox grand total - the Net widget can now use bits as unit - Spacer widget: new `background_color` parameter - More consistent resize behavior in Columns layout - various improvements of the default config - large documentation update and improvements (e.g. widget dependencies) * bugfixes - qtile binary: don't fail if we can't set the locale - don't print help if qtile-cmd function returns nothing - Monad layout: fix margins when flipped Qtile 0.14.2, released 2019-06-19: * bugfixes - previous release still exhibited same issues with package data, really fix it this time Qtile 0.14.1, released 2019-06-19: * bugfixes - properly include png files in the package data to install included icons Qtile 0.14.0, released 2019-06-19: !!! Python version breakage !!! - Python 2 is no longer supported - Python 3.4 and older is no longer supported !!! Config breakage !!! - Many internal things were renamed from camel case to snake case. If your config uses main(), or any lazy.function() invocations that interact directly with the qtile object, you may need to forward port them. Also note that we do *not* consider the qtile object to be a stable api, so you will need to continue forward porting these things for future refactorings (for wayland, etc.). A better approach may be to add an upstream API for what you want to do ;) - Maildir's subFolder and maildirPath changed to maildir_path and sub_folder. - the graph widget requires the psutil library to be installed * features - add custom `change_command` to backlight widget - add CommandSet extension to list available commands - simplify battery monitoring widget interface and add freebsd compatible battery widget implementation - track last known mouse coordinates on the qtile manager - allow configuration of warping behavior in columns layout * bugfixes - with cursor warp enabled, the cursor is warped on screen change - fix stepping groups to skip the scratch pad group - fix stack layout to properly shuffle - silence errors when unmapping windows Qtile 0.13.0, released 2018-12-23: !!! deprecation warning !!! - wmii layout is deprecated in terms of columns layout, which has the same behavior with different defaults, see the wmii definition for more details * features - add svg handling for images - allow addgroup command to set the layout - add command to get current log level - allow groupbox to hide unused groups - add caps lock indicator widget - add custom_command to check_update widget * bugfixes - better shutdown handling - fix clientlist current client tracking - fix typo in up command on ratiotile layout - various fixes to check_update widget - fix 0 case for resize screen Qtile 0.12.0, released 2018-07-20: !!! Config breakage !!! - Tile layout commands up/down/shuffle_up/shuffle_down changed to be more consistent with other layouts - move qcmd to qtile-cmd because of conflict with renameutils, move dqcmd to dqtile-cmd for symmetry * features - add `add_after_last` option to Tile layout to add windows to the end of the list. - add new formatting options to TaskList - allow Volume to open app on right click * bugfixes - fix floating of file transfer windows and java drop-downs - fix exception when calling `cmd_next` and `cmd_previous` on layout without windows - fix caps lock affected behaviour of key bindings - re-create cache dir if it is deleted while qtile is running - fix CheckUpdates widget color when no updates - handle cases where BAT_DIR does not exist - fix the wallpaper widget when using `wallpaper_command` - fix Tile layout order to not reverse on reset - fix calling `focus_previous/next` with no windows - fix floating bug is BSP layout Qtile 0.11.1, released 2018-03-01: * bug fix - fixed pip install of qtile Qtile 0.11.0, released 2018-02-28: !!! Completely changed extension configuration, see the documentation !!! !!! `extention` subpackage renamed to `extension` !!! !!! `extentions` configuration variable changed to `extension_defaults` !!! * features - qshell improvements - new MonadWide layout - new Bsp layout - new pomodoro widget - new stock ticker widget - new `client_name_updated` hook - new RunCommand and J4DmenuDesktop extension - task list expands to fill space, configurable via `spacing` parameter - add group.focus_by_name() and group.info_by_name() - add disk usage ratio to df widget - allow displayed group name to differ from group name - enable custom TaskList icon size - add qcmd and dqcmd to extend functionality around qtile.command functionality - add ScratchPad group that has configurable drop downs * bugfixes - fix race condition in Window.fullscreen - fix for string formatting in qtile_top - fix unicode literal in tasklist - move mpris2 initialization out of constructor - fix wlan widget variable naming and division - normalize behavior of layouts on various commands - add better fallback to default config - update btc widget to use coinbase - fix cursor warp when using default layout implementation - don't crash when using widget with unmet dependencies - fix floating window default location Qtile 0.10.7, released 2017-02-14: * features - new MPD widget, widget.MPD2, based on `mpd2` library - add option to ignore duplicates in prompt widget - add additional margin options to GroupBox widget - add option to ignore mouse wheel to GroupBox widget - add `watts` formatting string option to Battery widgets - add volume commands to Volume widget - add Window.focus command * bugfixes - place transient windows in the middle of their parents - fix TreeTab layout - fix CurrentLayoutIcon in Python 3 - fix xcb handling for xcffib 0.5.0 - fix bug in Screen.resize - fix Qtile.display_kb command Qtile 0.10.6, released 2016-05-24: !!! qsh renamed to qshell !!! This avoids name collision with other packages * features - Test framework changed to pytest - Add `startup_complete` hook * bugfixes - Restore dynamic groups on restart - Correct placement of transient_for windows - Major bug fixes with floating window handling * file path changes (XDG Base Directory specification) - the default log file path changed from ~/.qtile.log to ~/.local/share/qtile/qtile.log - the cache directory changed from ~/.cache to ~/.cache/qtile - the prompt widget's history file changed from ~/.qtile_history to ~/.cache/qtile/prompt_history Qtile 0.10.5, released 2016-03-06: !!! Python 3.2 support dropped !!! !!! GoogleCalendar widget dropped for KhalCalendar widget !!! !!! qtile-session script removed in favor of qtile script !!! * features - new Columns layout, composed of dynamic and configurable columns of windows - new iPython kernel for qsh, called iqsh, see docs for installing - new qsh command `display_kb` to show current key binding - add json interface to IPC server - add commands for resizing MonadTall main panel - wlan widget shows when you are disconnected and uses a configurable format * bugfixes - fix path handling in PromptWidget - fix KeyboardLayout widget cycling keyboard - properly guard against setting screen to too large screen index Qtile 0.10.4, released 2016-01-19: !!! Config breakage !!! - positional arguments to Slice layout removed, now `side` and `width` must be passed in as keyword arguments * features - add alt coin support to BitcoinTracker widget * bugfixes - don't use six.moves assignment (fix for >=setuptools-19.3) - improved floating and fullscreen handling - support empty or non-charging secondary battery in BatteryWidget - fix GoogleCalendar widget crash Qtile 0.10.3, released 2015-12-25: * features - add wmii layout - add BSD support to graph widgets * bugfixes - fix (some) fullscreen problems - update google calendar widget to latest google api - improve multiple keyboard layout support - fix displaying Systray widget on secondary monitor - fix spawn file descriptor handling in Python 3 - remove duplicate assert code in test_verticaltile.py - allow padding_{x,y} and margin_{x,y} widget attrs to be set to 0 Qtile 0.10.2, released 2015-10-19: * features - add qtile-top memory monitoring - GroupBox can set visible groups - new GroupBox highlighting, line - allow window state to be hidden on WindowName widget - cmd_togroup can move to current group when None sent - added MOC playback widget - added memory usage widget - log truncation, max log size, and number of log backups configurable - add a command to change to specific layout index (lazy.to_layout_index(index)) * bugfixes - fixed memory leak in dgroups - margin fixes for MonalTall layout - improved cursor warp - remove deprecated imp for Python >= 3.3 - properly close file for NetGraph - fix MondadTall layout grow/shrink secondary panes for Python 2 - Clock widget uses datetime.now() rather than .fromtimestamp() - fix Python 3 compatibility of ThermalSensor widget - various Systray fixes, including implementing XEMBED protocol - print exception to log during loading config - fixed xmonad layout margins between main and secondary panes - clear last window name from group widgets when closed - add toggleable window border to single xmonad layout * config breakage - layouts.VerticalTile `windows` is now `clients` - layouts.VerticalTile focus_next/focus_previous now take a single argument, similar to other layouts Qtile 0.10.1, released 2015-07-08: This release fixes a problem that made the PyPI package uninstallable, qtile will work with a pip install now Qtile 0.10.0, released 2015-07-07: !!! Config breakage !!! - various deprecated commands have been removed: Screen.cmd_nextgroup: use cmd_next_group Screen.cmd_prevgroup: use cmd_prev_group Qtile.cmd_nextlayout: use cmd_next_layout Qtile.cmd_prevlayout: use cmd_prev_layout Qtile.cmd_to_next_screen: use cmd_next_screen Qtile.cmd_to_prev_screen: use cmd_prev_screen - Clock widget: remove fmt kwarg, use format kwarg - GmailChecker widget: remove settings parameter - Maildir widget: remove maildirPath, subFolders, and separator kwargs * Dependency updates - cffi>=1.1 is now required, along with xcffib>=0.3 and cairocffi>=0.7 (the cffi 1.0 compatible versions of each) - Care must be taken that xcffib is installed *before* cairocffi * features - add support for themed cursors using xcb-cursor if available - add CheckUpdate widget, for checking package updates, this deprecates the Pacman widget - add KeyboardKbdd widget, for changing keyboard layouts - add Cmus widget, for showing song playing in cmus - add Wallpaper widget, for showing and cycling wallpaper - add EzConfig classes allowing shortcuts to define key bindings - allow GroupBox urgent highlighting through text - Bar can be placed vertically on sides of screens (widgets must be adapted for vertical viewing) - add recognizing brightness keys * bugfixes - deprecation warnings were not printing to logs, this has been fixed - fix calculation of y property of Gap - fix focus after closing floating windows and floating windows - fix various Python 3 related int/float problems - remember screen focus across restarts - handle length 1 list passed to Drawer.set_source_rgb without raising divide by zero error - properly close files opened in Graph widget - handle _NET_WM_STATE_DEMANDS_ATTENTION as setting urgency - fix get_wm_transient_for, request WINDOW, not ATOM Qtile 0.9.1, released 2015-02-13: This is primarily a unicode bugfix release for 0.9.0; there were several nits related to the python2/3 unicode conversion that were simply wrong. This release also adds license headers to each file, which is necessary for distro maintainers to package Qtile. * bugfixes - fix python2's importing of gobject - fix unicode handling in several places Qtile 0.9.0, released 2015-01-20: * !!! Dependency Changes !!! New dependencies will need to be installed for Qtile to work - drop xpyb for xcffib (XCB bindings) - drop py2cairo for cairocffi (Cairo bindings) - drop PyGTK for asyncio (event loop, pangocairo bindings managed internally) - Qtile still depends on gobject if you want to use anything that uses dbus (e.g. the mpris widgets or the libnotify widget) * features - add Python 3 and pypy support (made possible by dependency changes) - new layout for vertical monitors - add startup_once hook, which is called exactly once per session (i.e. it is not called when qtile is restarted via lazy.restart()). This eliminates the need for the execute_once() function found in lots of user configs. - add a command for showing/hiding the bar (lazy.hide_show_bar()) - warn when a widget's dependencies cannot be imported - make qtile.log more useful via better warnings in general, including deprecation and various other warnings that were previously nonexistent - new text-polling widget super classes, which enable easy implementation of various widgets that need to poll things outside the event loop. - add man pages - large documentation update, widget/layout documentation is now autogenerated from the docstrings - new ImapWidget for checking imap mailboxes * bugfixes - change default wmname to "LG3D" (this prevents some java apps from not working out of the box) - all code passes flake8 - default log level is now WARNING - all widgets now use our config framework - windows with the "About" role float by default - got rid of a bunch of unnecessary bare except: clauses Qtile 0.8.0, released 2014-08-18: * features - massive widget/layout documentation update - new widget debuginfo for use in Qtile development - stack has new autosplit, fair options - matrix, ratiotile, stack, xmonad, zoomy get 'margin' option - new launchbar widget - support for matching WM_CLASS and pid in Match - add support for adding dgroups rules dynamically and via ipc - Clock supports non-system timezones - new mpris2 widget - volume widget can use emoji instead of numbers - add an 'eval' function to qsh at every object level - bar gradients support more colors - new Clipboard widget (very handy!) * bugfixes - bitcoin ticker widget switched from MtGox (dead) to btc-e - all widgets now use Qtile's defaults system, so their defaults are settable globally, etc. - fix behavior when screens are cloned - all widgets use a unified polling framework - "dialog" WM_TYPEs float by default - respect xrandr --primary - use a consistent font size in the default config - default config supports mouse movements and floating - fix a bug where the bar was not redrawn correctly in some multiscreen environments - add travis-ci support and make tests vastly more robust * config breakage - libqtile.layout.Stack's `stacks` parameter is now `num_stacks` Qtile 0.7.0, released 2014-03-30: * features - new disk free percentage widget - new widget to display static image - per core CPU graphs - add "screen affinity" in dynamic groups - volume widget changes volume linear-ly instead of log-ly - only draw bar when idle, vastly reducing the number of bar draws and speeding things up - new Gmail widget - Tile now supports automatically managing master windows via the `master_match` parameter. - include support for minimum height, width, size increment hints * bugfixes - don't crash on any exception in main loop - don't crash on exceptions in hooks - fix a ZeroDivisionError in CPU graph - remove a lot of duplicate and unused code - Steam windows are placed more correctly - Fixed several crashes in qsh - performance improvements for some layouts - keyboard layout widget behaves better with multiple keyboard configurations * config breakage - Tile's shuffleMatch is renamed to resetMaster Qtile 0.6, released 2013-05-11: !!! Config breakage !!! This release breaks your config file in several ways: - The Textbox widget no longer takes a ``name'' positional parameter, since it was redundant; you can use the ``name'' kwarg to define it. - manager.Group (now _Group) is not used to configure groups any more; config.Group replaces it. For simple configurations (i.e. Group("a") type configs), this should be a drop in replacement. config.Group also provides many more options for showing and hiding groups, assigning windows to groups by default, etc. - The Key, Screen, Drag, and Click objects have moved from the manager module to the config module. - The Match object has moved from the dgroups module to the config module. - The addgroup hook now takes two parameters: the qtile object and the name of the group added: @hook.subscribe def addgroup_hook(qtile, name): pass - The nextgroup and prevgroup commands are now on Screen instead of Group. For most people, you should be able to just: sed -i -e 's/libqtile.manager/libqtile.config' config.py ...dgroups users will need to go to a bit more work, but hopefully configuration will be much simpler now for new users. * features - New widgets: task list, - New layout: Matrix - Added ability to drag and drop groups on GroupBox - added "next urgent window" command - added font shadowing on widgets - maildir widget supports multiple folders - new config option log_level to set logging level (any of logging.{DEBUG, INFO, WARNING, ERROR, CRITICAL}) - add option to battery widget to hide while level is above a certain amount - vastly simplify configuration of dynamic groups - MPD widget now supports lots of metadata options * bugfixes - don't crash on restart when the config has errors - save layout and selected group state on restart - varous EWMH properties implemented correctly - fix non-black systray icon backgrounds - drastically reduce the number of timeout_add calls in most widgets - restart on RandR attach events to allow for new screens - log level defaults to ERROR - default config options are no longer initialized when users define their corresponding option (preventing duplicate widgets, etc.) - don't try to load config in qsh (not used) - fix font alignment across Textbox based widgets Qtile 0.5, released 2012-11-11: (Note, this is not complete! Many, many changes have gone in to 0.5, by a large number of contributors. Thanks to everyone who reported a bug or fixed one!) * features - Test framework is now nose - Documentation is now in sphinx - Several install guides for various OSes - New widgets: battery based icon, MPRIS1, canto, current layout, yahoo weather, sensors, screen brightness, notifiy, pacman, windowtabs, she, crashme, wifi. - Several improvements to old widgets (e.g. battery widget displays low battery in red, GroupBox now has a better indication of which screen has focus in multi-screen setups, improvements to Prompt, etc.) - Desktop notification service. - More sane way to handle configuration files - Promote dgroups to a first class entity in libqtile - Allow layouts to be named on an instance level, so you can: layouts = [ # a layout just for gimp layout.Slice('left', 192, name='gimp', role='gimp-toolbox', fallback=layout.Slice('right', 256, role='gimp-dock', fallback=layout.Stack(stacks=1, **border_args))) ] ... dynamic_groups = { 'gimp': {'layout': 'gimp'} } Dgroups(..., dynamic_groups, ...) - New Layout: Zoomy - Add a session manager to re-exec qtile if things go south - Support for WM_TAKE_FOCUS protocol - Basic .desktop file for support in login managers - Qsh reconnects after qtile is restarted from within it - Textbox supports pango markup - Examples moved to qtile-examples repository. * bugfixes - Fix several classes of X races in a more sane way - Minor typo fixes to most widgets - Fix several crashes when drawing systray icons too early - Create directories for qtile socket as necessary - PEP8 formatting updates (though we're not totally there yet) - All unit tests pass - Lots of bugfixes to MonadTall - Create IPC socket directory if necessary - Better error if two widgets have STRETCH length - Autofloat window classes can now be overridden - xkeysyms updated # vim :set ts=4 sw=4 sts=4 et : qtile-0.31.0/docs/0000775000175000017500000000000014762660347013566 5ustar epsilonepsilonqtile-0.31.0/docs/_static/0000775000175000017500000000000014762660347015214 5ustar epsilonepsilonqtile-0.31.0/docs/_static/layouts/0000775000175000017500000000000014762660347016714 5ustar epsilonepsilonqtile-0.31.0/docs/_static/layouts/twobytwo.png0000664000175000017500000012252614762660347021330 0ustar epsilonepsilonPNG  IHDR @IxeXIfII*  (1 2iHHGIMP 2.10.362024:03:26 03:42:14zK$iCCPICC profilex}=HPOS";HqP.*XP VhФ!Iqq\ ,V\uupg'E)"q;fiVOt̤b.*^DjeIRy?׀Z0m MOaeY%>'0AG+q.,̈GR+]ʦFWz_k7:Z.;\OlʮE) k8}4 pp({}sN{~?Qr7N xiTXtXML:com.adobe.xmp o]ObKGD pHYs  tIME*>]q IDATxil]sr'E(Q [%[v'.;N2)  4j=j1JUYڕ{-[;%Y IIDqs3/\$dK s""""""r8*Sp DDD*4T """""r'v3,NI CDD>dstDD[]* dzޭZ0k-ƘOfaSױ)'ZU)ߎ2kT~E~/+.""΂ep#g.`-\dќFtϥlNF wbvbCdrt=6mppm#@ sxrN2ߪ;Kw.тsy. e((?nZϝ`׮]=|>nN}l\D3"O-̢`?]8p:g=s+Ž&uL|lDct=EO\D!$,Q~.>(}H5ȝM@ǻ9ǩ!prz|HTr~9IvΞ393^r×x柟cr(!`s lP)=X~!^л\@;H:GX%{őy0O~4czF`wK085?>dP;Tc]v=K?J.fy k.{Y3w,a0so'"N_'W`bq*jA\(ly~&?Ɖu)Mh\Ho*ɏ;y3T#7{ " ;krS\ā ^`H] c}}),C?N<_@="""""w 1lڹOYbNȥ.r b#GXDaP /`8n͗x @GNb< ޷~K[V=Ιg lܸI8gvYԼ{0u31I2u=ln"EsMBla18gβsLf3%.cCyWo?碑A@.IT 1dfI'}" 0: BE|k8NqB7N"Lc$1Xq]$cR9ͭ\?Fe455 B#/.;A>rI`C'HTy][$c㓤*luIc0 S10p0ؿw dΝ5dtS[ C0.D rY&) "]G1 ٱm Kp4?ui-͋)R6 Kyu4筯S'O>؈>Ӗ(׷Jyj\ L7^WmN@<ϻm;EQ] Kȸ47TݶI!>O8V8s\Ԗv߫f O\nW&4A˒zMD*J@'tV@ȕȻ+wī ȈΞ1餫Ϝ|~' T""r{aWWDDX( LDDC#S#""@@DD>=VDDDDD% """""DDDDDDD (c@DDJ1`j*S"`?W"""Gۀ7DD% ""y䔢umDDDz? ~Ė` BD68Gޏ]oK/O~Y!"r;$lV[DDe kA@d L 9r覶AF' F.! ߗ(1ZgX ;  򓳎5?!T5ad^ZXF2©|F!lnGEDadSESE!W L=M ܓeӿ \?oyf]Fa@~ϼ3t_(np!y?}ϼ06JI ( 9wh/!tAY^Ga~LO>B} #)m+Zv9v?cGOE!^~ B~/p\><_/?◜LﯵSeP_K,yy ovㄑ-IS9Sz.} 9?¬c͞T" { UrQ.ӛ W8K#Yo>_7XgƘt}.N6ߌlÒ ?N>LC2{:Mo;?+KY[}ʱcS~_s>zN Ov"/x*uI{(5;t-m;xx;_k\|뗜slU_dp,@!^z*SO(=ww>:3–>VdC!@&^烳c;eyŗRr'K^!x<ﲶz)z]Gw$|W=]F>z\#'/~FT-<eQϿ.䈢o2uv9ϡa隇etOWW v׾Mjݍjߺ|!d(Ͽtcݹj9{†ftf3!?z_z/ǻY?[_܂{\18ϥ>bA^:֯-^wǯ wE䞡g% 8;(d{,o[ƕ~Ll@ξ7( o:k*Srœ54ponep>ΟE=/|q~/ƾcxeO= ~?bfla+lzylȑؾq)S`%L26ͮl3%AQ_ylM=ǎ{9бq ;qP'߷p2ot~#kjgжl9~Ci?^ld>>y֒abm.q:=#7GiQ!Ȟq_p^c `8s0K/˻+ܷxgSuIszElk3[ZNK˫ym)eG|iD̛YsO(7e{;uHՁ(Ȁy,0 q&Ym?lJɌo 2s<1wC2dtl(dwn}<-sN0.cM)n#yj?uolcu[+/?6hn_ʼ1ݸz aĬcmzlD&3D ?1:E2.#XA WለR8o?fy|QhXE:Q[Άf΃Mr0=YR_Ke >y Yڲt8Ekb{QQ]Cs[C}m--yYNN.<앋K=@ssbj7^fqZn#T΅tv ܷ2yXn f-`UzGϲxFR]Qͺksl>qBFT56Ҵz5͵Oem+8xYѱ(c`#n'~"0~"e±YJM]%}gOZ͎+Y s~8ڥ,s!5ynn^;OoPJѱn%cYr iĶ%T!v$ռͫ[3UQ]ȺMkI8/ ]:[U4,eÆ[ZYrɼ BK<&]'A]J-6ڲTeᓜ4Igͤ|)˭;(7P!""[:&u4}}}tww۲ehnnZ)_Z5Y,@EݻYۈ C?זo`aUQ|Bo'Sw,P"""w}_KZ\W-r?u6%$ ɷy:4 =.? >sS"""wsmw4Q(% """"""J@DDDDDD Q""""""J@DDDDDD% """""DDDDDD(% """"""J@DDDDDD (Q""""""J@DDDDDDQ""""""DDDDDD(% """""DDDDDDD (Q""""""J@DDDDDDQ""""""DDDDDD(% """""DDDDDDD (Q""""""9OE 2[1 X PFC(~(=b*50V$"""J@D>*~a%*~Dei ;|(-= "%uk,Z0IA M 4aTP""!D{!8[bXQ2""!DJQgH$1@b A*[,DDDCD 'ђrXYq;J!hADDDCD 'P.S 31hXQQQ"r<j, ]|qvk""!DdkY1bUqj7 ``#VQQQQP]E wl3ڀe*0xCǫ`,p\.i8ˢ=g? ODDCCD cXf-T=T+1p%y`!Br|hhVwM,\KӴ.]J*8u0K:.d;b*@% " J[281Y;&Ak-8X=|gIs3u *@% " qf*шCd-qV+JA<\%NӾb PDDCCD q/i&:ld;!"\q]\ס"ͪ58DD??D,@}C#-- 1nŪs0XkJ7K<uiU5*H% "7ֱf- c1`-RkL9O]]=KT"""J@Dnw]V <)+DD8QZSlrYz 蹶"""J@DQ]]C&88f*DQ ǔ8$Kۈ*P% "׷ڧ 48`RTc `7:ƀyēIWR""!D: Oh87y Xkp\F!"\b0q}"RQeq_|\*"!@@>o\h=T e Zqktu% IXf<Ygr?7P*[% "WiM;-Sɝ$>xoIdufrŌ?lݺj&r7zK*dQ\mX"""J@sÖx7̸ThƸr[≮ЅkT"""J@.LQ1boAy  IDATE3Fd*R7Xj K>"""ףO|.,jl]QyLƄSPB;#vLu'=  ED??D՚uI$䖭ŠX2ڍM=Il8D5u֫V-$1FrEk'ji[\DDDCCD |^ϢTWUN*|% ySS[%ͤIu!B:@ 8 :BB!O&'q[`,^LEQUtk_J\14bE`C($pU,inV(~(~(qX־p=wƓh q66""quZ;q–_,o>@.xx"Ϊ5*|9h~ j.0BBEMM$q<Cd-6,Q.pK8CchbqSZ\s]\%LҼd [8$b MZf,+"!J@DaOO?R (L\cytEԊ[Jnǡج67򃥦vV@/q-c& IC*bqҟs7,#4|&*]ophJűYlCe$SIbcpJ]X{,#3K8e1ESezrWzbF +ETX|*CpiQP% "uM8zaA/`hb..O3El|y1ȋ8b;܍MjET0dƐspwLbLŅ"_ƱQ+h>Q"rVI[TаdP׀WH{_¶}ɶ07Oxf?cqbp\8Wq4]TZgC9Oyb'&9H63̄߅~qʫ,tXQ9$(~(~{1ꐸo ſYcC ʄ~Iy15 ȧWPHb㐨"ˑLB:'?2@-`Nʜ#6rw" lDwK+=*4Sܟ?K ::""D^Tᰩ8<8S=; [ŀ3wnp88qׂ<a,',n77J6=1xa" aPZϷhxu3(,wXI!f$2KݫpTZlB/>T63Ԍ'mxpK-]0#x"xxmk|F#dQ"rX/O}r͌@4]OPID;>=exZQfv婿[TI^׬13ȕIDz1V.$(~(~Ń9<bvBf\LP(v}Zxeyg6<J?_U,U,1|S,"!J@DW.r'|g-C^rMkQi.vj>W|j<--,={ɏtED??D -?/А~W+X~ Y16f8byCy0<]q8km""D.Cҝ1̊_ћc7+3ֳBDѣ,Q"rw@(~ܓ(% """"""J@DDDDDD Q""""""J@DDDDDD% """""DDDDDD(% """"""J@DDDDDD (Q""""""J@DDDDDDQ""""""DDDDDD(% """""DDDDDDD (Q""""""J@DDDDDDQ""""""DDDDDD(% """""DDDDDDD (Q""""""5OE ""w-[~E!"r\~#% """e[Z/ DDnĝݜ&''Zτ1*)+Z1ޞ٬ RD>S\g^% """ g~@AZ*[ x\e)"}vI' \@GOK4-`QXC{U.!ȄW֍8 x\ïD3gYp]t;B% """sHTV|KEd:E2E~/QIeXn`npǡI|[".Ӯ \)Q@2 XW6TZ*}S묪"NDD>3gumok/"Z*=k-LcaDD>O=| .|F%!""J@DDDb1nם22::J7GUUyO2|>O6]P2.d0 ?ߋe&"J@DDDnJ<zQ166Ɗ+;b=gϞ%_sO|>OKK˂… d2Oeb[23f8C,DCY{I{*Oc0q?2<ו"z/^_cҥ 7z(n: So/_L6}^P(L&u!OufٲeaHOO+VumZKww7\e8˖-UrƦ9,!T >܎ݖ 8'Ma2::J"'XsD,0Q(_;v _}s%.7Z+W8u466TVVfh3Bf^ wvv.7oJfg||qH&ň ʕ+^EͩS룱:!"N<l۶m (_<裬]g}˿$ |k..^x/Jrff*dxxK]vq9z)4|gy%KwŖ|FOQf35,MR866ƥKpCCC444dxR*A({;v{yMZ0=O?4/m6yWBEE5a/f֙L&ٸ8̖yk-|[bxx}o| .sTUU-YnTR}v;FP(gppB@?Sa8CRۍO?=!Q3v:;;yGپ}TWWk-[nٳ|_ߦkzDK,Zc ss/$q‘fx"/^ҥKTWW¦MXJ@DDJ>J@ʆyg9|0?iw:Ӽ Nb||VEÖ~'`s|ezc{{ 7ow۶mZ6}2 wK_҂, C^xY|;z3ǵ~zZZZf [hѬcSO=/_׿5ZyoTf"Ŷ/DƧ}5?eRʽޘҥKG2իWsN\׽wR"""\r+/b~u_2?~[#~F|uyOF[[Z >gqU5ۅ c۶mS~k->u/i=vohhm^ZpU^K(5)CE .]KKK K.e֭x{5'.6kkkyGYn-b(6~a^}UVZŎ;xWfz@f|OpeFGG׿NOOڗ@tuu>S0ʯ:uBMf *o8pl6}/^ŊG[[]]]Yfک444Lv9N8͛)Gk-yEn˗bǎ84+Vj?AבC!---,Z7K8>% ""rO%W߄>?;w }'x&y'rTVV}oֽ3kk_57c{dX,SO=E,P(OP]]g۶mSڿ?CCC,Y PWWۛ20dPUUEKK |'澙0 yW9uDbj}L{l{@(XkwC:fڵ߿&!9{,o&cfp0 hbrr%Kh":::H$IBwٝJ@DD䞻(/˴^pMeeT뻵}kԅ\lڴi$a{3?YkIS?S__vVf{XI$܄y .]J|o'Zr},M6Q]]ŋYYY9+=?[l!LESOTf"7#"y.\@&=Ca'WODSS_I&n,d٩;gsf=GDD6X:@H>f^`644tǍֻי = YjVO=3 `gf=fAB'lިq lfBL]܆-ySR__ϒ%Kؾ};yqo2)o{s9||}DD}kEf],/$9 c}_Ⱥ1z?1|e&rG7ܚ'q؜>QZ[[Yn555Tu .<=w2,>D4' xΌҘM7WX16-:zǡ]z79C:flllA'ǡOebJ=NMM ]]]md8uN94saw|>r={yȂq/_>5r1ũZo6`>\/iK&4 Ng.XlXf"x1;w~&ys%RTcNoLb<U[j' ÛzZrI;w-yqKػ5 ;1#[W`m8΍C1f{""p73Ӯy?Ҽ""'\(ۯﶰx%_}A@aveŊ>}>wqw9VHBYbɒVlNlKO;qg`0.y5L_b04so;7u|;%ۉc;:lYBI$E[k9Il@PbY|~sD"""_W& YDޫ -v /_ O,"rm. {3첪L\x'Ӊr]%ka͗~_Y}v""/vEqp]D"Q-""7Xbexxt:}+bxi"""""rc 444PUu7#NLLpy|JoV\ [_233 tx<~ExSNͻq\׽zc n\&HyOJ6%]͞|>O6R33*@^Z7Eg>yw2Ghoo]ӾGqw~)ۿT`^/\vRF5COOW|G>ZK}k|_^Wi}ˬJ˖Γ8Xkd2i/svcr<㟹\ ze*rKh eCLNNr9R===q  ؿ?:_WYnPhBRo+ߘ[k# 3V~?sG|~i3[*,1R<::G}իinnO>͛7>wl߾>m۶F6lذ[k9z(cxGu΂ `ݻ\.7x6kI?i&T_W\p}k=Є 뛱ѪvA@6~N>ytvvdY~NXHҿGFF/?L{{рs/^yHٳロq֭[g-!. IDATw_o|lܸqKyYz5Dw}7GַuUeʕbb1:::8p@y *\nFGGo t555lݺu\pU.ǫ=Ap19<ǎرc<AgΜarrrvDY?GZp(~>h'eIUe|8uAAKK k׮% 8}}}9sF'_DDnrʠ;t'Of֭f˺:,9s_~ _-[L[rbˤ*_~QvɓO>'|yǙ$.2 <.:;;D"q:;;iJUWWfiiiȑ#022yR)%_*BМ)U11{R\P(42hzw?z ˿Yˈ(@l &246:  ˉ0:1 AprQ_+VD4S|JL~ӧOoh4L&sgddiϛϝ;?8 L&-='z9 B* 'Nʏ rY{杲xky ׿uĬьE}o~ONNq2UUU{シXk7Y1ȗ81Q\`K0yXۈ1S.\(_R5::JKK w}b=Z"""D2S$a֭u]Fz?H$³>+Gy^z >q}ʟ577kH$xxʡ~.yaVX1k"gtiXT*MMM,[YYww7\Ȼʕ+EDg3#(555 6jjjF t<{9J/kxxÇ_>P(C=Dsss\:N֯_O<'NH$hii!_'ËܪN<ɋ/(fBkA@&ٳQh4Jgg'˖-c֭{F8b1|kWTrS[[;@>Kcѓfg,8 u\lXț bu]}/mρ8qASSW&p3U&(u4@ ""r K yNMMe?dɒkLCCÜ? _ֺ:::ߌLvşt~>S֬YSa``<%\w,+_k%X""rm h4J**t9&''].~Yk۫\0 tiY2R33ہ1:\t45k֐ff}#K,(]vd2KDD~bQ]_#X,F}}=y yN@p햙/s79M r癧 UoC\ EaU*@|3M juDDnw^W'BDDN#G#N e|Ata\HDDDDDT  """""DDDDDDD """"""*@DDDDDD Q""""""*@DDDDDDT  """""DDDDDDT """"""*@DDDDDDQ""""""*@DDDDDD.wշ6f`ɏC!"rK/@6&[ XN/lt .|_cN?Gz2{y*NN;d_e0 ,S/~y`}R&Ja|? Jcou5|fg(?v{߱oߟ`Z#yɻ9(M Qgyo'??U#>A1T,N5$AsbGt|s?s*/q~]G/WN6/3}=zB2> /gc9se{?Ȅ?c}Ɇs>?=+/W>>:@PA>;?),~Ϟ畷9.Sp"z Sg?OW|G9E:_p=;?l_T h[~sxS`}v|$7XG݇ xEDP~(?vpxltL.o̞c[[/ s.1&YvmZǷ^<̷yg{h?w?74k0΃ky],o^;M'W~ɔ糤G=B}̝pGY{ <~25ٷφ:0ȲG'2p|/v7xghN9nE>%:f ?qa);/ ijn ~Uy&wtQ9}%پm)˞7~Aorw4n!{|gdM_ 4gϑ:_rH2Az:?f9_CC׊{΢Ng#qמ >'^8x#Oap|Kc<~W#_iR$P~(?-x'M{<\8?iw/by}.-ȃȤLDk[o0~ޝПGYQ]R%s^g;9^hn%|xѩ91GKxyy;6w髬O@SsNZi&^;a}sO?>wC+8鋾k6k>o/w[?ft<yWԑF^o}4}v :pVn|ő-u 718Ɓ~Lם}=ƺI`rb+5f <̦3?~=cKXRb\G ,O o}B4Rc.ΈS!N%7XOge"5̺^{$P$P~(?D(mwҺ#!56YLJϧxlgM<2TogJu;_=COuظlXe$5Xsp8y'c8{|rE|WM,3L{w&J1dy'dh,Wj9Q&3LG E=pkNB$QChn6nŸ\g"=Fz,E"\QLerx7|^= D}t(NvjxoC,)ou,JU"x:Gz|pлWh<^ø^êk 9h%ټQobyw'6_]ztq0;l➻ffvb/ HHS^|"P~(?r p\-t]ҟh8LO2zZ1&L]CXcs*屪g1m u$jY8LJҹz ]?hBIPUSK{w}g-{iM6xKz,Τw-?wt)NuMt?UT/˺ttm]Yeq?F5tuvRSUwޝLDؼ" ljerqu.{gtaIZz"wq?{? }ߺDd||,>խD{XIm}5}ǎM.g˪4{CAM]sbȧz)͸m8+2Vme6څӲMm$1y>p/_9|1t44rDXn5 dttzu/Xvu}K$ Q'J}RVch%^$5t&I"lۼxH,Wwz?-|7)?ܒa Opqo!/fMǧlѢEcŔk̪=g ࣏>s6yv5>$K[s.w3ugZ:6xCl޼qk 9k-0Y;kU:q_yk+Q:| e9Xktԩy}:Z[-~Շ0꾒/<}r7)?ܒqM WHߨ[|Ak[gQxȗ y&-Y(?Dr5띣0^em[ؔ""N~\F/UluC"""""DDDDDDT """"""*@DDDDDDQ""""""*@DDDDDDT Q""""""DDDDDDT """""DDDDDDDQ""""""*@DDDDDD Q""""""DDDDDDT  """""DDDDDDDQ""""""*@DDDDDD Q""""""DDDDDDT  """""DDDDDDD\wNt ԅ3`s0܈CDȗZr,dpC;luDDD!DK\ΎdB1p8rd% = "EwXu`b`/p6BWDCDwWc@Mq *`mdDDD!D J[oF1B`  WX""Q"Et` (fG1@DS MDDD"*@D MuBhT2.D^^6""Q"r<k-* SC?B!HDκ鏉C!Ddjkh[r\ c}0k:"'\QUU'"P~\IS E+V>7\l2Ad SuxUU]&;MG?NC!Dd1,[qUUUxx'~%y`|2 lyjkkY{G ""e%Xr['tvu1s?8c%|\kqh,彄a@CDµwtݽH8c 8NIZ1,CD1lh}C 6caҴSq3 Q~(?DT.S!ѺBo4>T mm{(?"opWD:z݊3ucBՌ>Frjj>dNC!DnW+V"Y3K6z7a3 Nb. WSZaMdWqNWO{bC!DnWPVjՄBN,ɅV2+'=CUfBc}1B&oܐwB809|FBu[#J.T ';F4eɲeݽkNC!Dn7upqpZr&2^٣,cG6#X#ˑe}$N>e4X,T%Q~(?DfMrYdM5bSA>A8XAqBT@a]%B$kG濵 IDATjhko׉Q~(?DTm 8,Yy[I8X` X8uZ[~88m!H\% FXֻB'_DD.[#( np8(Yljini!kxPA65.8crkquq]x,F{[8$bs~r,eս""凨=*ݧ.'79Nz2)5$^,Tq Z`;u)}TXkR8SsZ[Z׵P' DBePMDD۳)]5jDXk1p>Xf2( g[)@DD=&tuWR` !B(}wgY\y!8 S(KTVC ()Clw3Ov`2_xR@ 8;=CYrDDD "7t'ˡa 8dHg.cG~O}/wa&'~⸅p\0Wq8]b9Y.a$CoC:5Ąׁ 9 TZE`.Ò^DCT ǖDB W5``x;OĿ{oO+|j%uؚF8D3b8#$9a/?K&5H:bxϤ C})Om+=QB(?DfR尶8tq.8-{zBxn)< Sn) |@tb8n(Nǵ8& ' ́MR9M@)D/>/d6FQ~(?D ˛Ƨ؈T -SѨC9k &24NɸMrbŞ)[VɊxP)H>[xN4:lXgz1EDQ"rjX8 R|׈WcP0Ӄ3k S`*BX lx./ jvG=ԋ)"P~mCM>jep=q =Ln兡 : L/Ȭ2M4kb9 J? o_T[r @K롪51"__sR"">4"7e-ah*:h0|=6)0S`~[8{#+{eQ/4ucy-l&5ՐlpR/[+|_ = uC!*@DnXW'M b^9g Dk!AuB.yp--^!tJ Ω8n^,υXŒ%D ͙ Dž 66y^TCT܈b!YPPh)6꥞]6 ^0t 7e& zJ:fzX<|n/V⺅/7ngʉg8+Sb'.gyKDD "7M.+m3SQЀ t?S0?W6i˳^)7\ ²aV/tͱq-kz<: ' ""凨ܽ^![I%!+4F?Rpi<_7?um1:@0a0`s_Xi\*{E'99tA/C!*@Dn$!XPP4ATi'䦆)8h |xj D*>Ǭ_ `*l-NRzp Aaon5kVDDD "UKB8Bh3mF!uņ+e z&@={ש6!R񤴭UQ"xxM+B4&F""凨Q\ToƼ|Ca&Z$)2d埙[$$kZxT\1Hz-hF/C!*@Dn[;3x׭ ; S1W;iS CR-sϫXXe/Ti.{rsYu,5XDD "73YFΆLga%z^.s3z^[|nL셜^`CTH̰hLK+o,} |P 0Ӈ;'," q 0 PDD "7N 1bʆ^7 \laje{XRC!*@Dn,p2M) """""DDDDDDDQ""""""*@DDDDDD Q""""""DDDDDDT  """""DDDDDDD """"""*@DDDDDD Q""""""*@DDDDDDT  """""DDDDDDT """"""*@DDDDDD Q""""""*@DDDDDDT  """""DDDDDDT """"""*@DDDDDz~lu`=ܒǦu :6@zED{h['?[OPF4u kzpBD" ugYp[t5[CD7_~Hŋ]Uqbqcxj:-†pr&'qjjjo\-ueU~ˏ EhVNB45u&IzD18 " оx^p""j[s7RXkgaDDnS5/@D}u]l6KSSBDD6,>""*@(@0W睒,N3::J>{d,|<͒N}KxD-@凈 +AK,YP#G\e;F$uMl6KGGǂ̙3R/e79ۛCQr*$N8p ;Zk\rI.-elj3q/ ۉCބ^ gzyOإ>ZKbj0tP(DSSSrXi`s:;;q]Ǐh"|ԩS,Yu}k-Ǐ'zN$aѢE9sO>MOO===sL&qs;"7ZnX,Wk-s9}4DQ{C7w~\.5^?ٲe {oϹRtvvD&!R]]M:&isW6p{6L.ׯ/Z28C,cllp8L$a``SNtRjjjKyahjjqC100@{{;6m* 8x ?<۷o;^ooIӼ -{={v㭭吪<~k-v׿u9 &''ke{=N8O?M" 6͖ܐ2<>@(QOb8cnJ0fi]Q866ƹs8s ϟ6.8|^CT~`fx衇Xj]: cydfc144 /?1LkXK_lڴ|>ϻK*>+_ʂΙWbpppsy'FkժUtttL졹yڱ6mTޙAo۷ϻ~/ͱu 6\COO} A8w>}u蠫7yn*W~(?^5_cQWWYr%-1)5ƾ{^{e˖e^}Y_NVJc=0<補:u}e!=H$ [F+=vark׮6^_ٽ{7t={K(uAVGÅ_ ϣ99֖oll,?v >3֯_?N>ttt̚5kB,0nC!ʏ;?nBwy'۶m+D8WХ]SSc=FKK 555 / k@2ݻwSOͺ}^{5>L4-/JCy o޽k-O>$D;]vҢw媴7ǎ7qlZN>ϟ/(<99I[[FK\.T~(?ㆸ xYELk-PP(u /ڵk/W.SZK"(Y۹>wvv8ǮCN4uy<HUU]]]yx <[zsKTWWǺuD"skRSSCkkky`2wy'6l ODtD.Gw̙3t'V>,=9aKK ---w}bi7]tt;)P~(?yㆸ {P(t/ޅ4>/^f!K&zJ˔z:VcLyto͡~9Cx3՛]p:g +l(qѰpppؼy3=0fb11 2V)>(?n!nB/]7zn!/Z-3T/t}_.c̴/v _9\pobCfS8^x߹Q:;;Yr%fVy,4j,+|\)@(?n zC]8 .Ç{7gYTW^Vs!P~(?û/&ڻ. ]7|ŮhZ-3p8\ۿϙȕ㮻0ưm۶ꍼ8s*?o+?MV~3P~(?no XOAADd6V>6凈-⊈\9Aݛ8""d~\fŶf%ꏀMvowٛ8""d~\DDDDDDHJ"""""DDDDDDT """"""*@DDDDDDQ""""""*@DDDDDDT Q""""""DDDDDDTyxUս?{yəON@0 "(A^vVo[;y{i}oVkZVE)*B!̐2 !1!u5#p@@ p@@ p@@ @8 @ @8 @ @ @ @ @ @ @ @ {Ɉ5b1#*Dd[j&!JM @ ȟ|:ܳnŒW zH~(9@ ki, coWkm o&;f_K;8b{T"d%)JN TJ|o'f8ѭ^ӡ-5ml|QIBg0² h$@d$YK6 pdy#WoG& KAy˨_bkƵTyq$nږ$ eL"_[]'4iG ZO߇!X^ŔqYt1ةLio>9TVUQUUEeekky7m%ۂ(- 6L$r\s, 8J=6n)ѡxbK)u2hYos>J"k45|qJx7ƽa'Ι s6~NVT@ YYQzwX=FlNSխ}.}4= gI :5L),]0,+h/O|Zq8ͭ!q%D,7X71yu7c?:92ߡ'RJuIxC-+'O)4]gLK֠2Qj_v8?T̗>xJR<_>L!NwN+3& ̚uKc1HtaO(+-tzp3JVKuV5yNx7[O UBkWho6t:O`ȥ֕L_r/Mo5c6S~lO gR1DEϖ m } /9ڱ9WVd΋kjfKd\?6{-~Ŗ!?OEnR#[|'ESjbw8q[*&c@.fw6eγ<y28b%M;He1TAғ0CA:]: ZN$Ndd5wʹwav5k Zy"cXk3\HJXztɬYhܐM2c*p_ ݀VwE5RpjnY}gz`Տ!8h}+:FU'շ0(%l~u=7.Ex}~n2ؼ9v( .`a@U}v*_Ik4A?;?C 3yV ^8y"bDވߤ#c1sb#[{YǪ55=TXTZsz+ dU>uƆO;\Cd)S/.]En =AW,A[xU%SF_k D!r-,.r9`hrz)Vg0Wg{[SO='U5)/tq)ajsPl#8h0ʛ苒B+F<mK9H:;7ߺq=%M݁lw#ݗ(}Fzj`uuU<;l%d8ۼ_ev\q_Gs)n\ ng6!>C {w=/7ʔfy>lc]l~ KΝ Tyy>==ٌ8m@<{ ]$rKpm̹h=i{{H|IUREax=kZ V~t ˆ)'w`UQR +&TX4M2DEFo[53ނNNpΈn|t6th"I([NUW\ ?^r30.{ΠJeI/E G{|>>{AiH:BG(B*#@ 73?oա*ΒChL?rmrE}ݳ) <Jӽd(xVBQA*+U#%@U- ý|Л"M*toiw's X{NC]9dp # %|frr+7"das$ܟ2_ƃ+qz"a54RUa3*LوlBIe5fBim6]61j h Y lh6#NbIf$)/54,(Bn |T|m ^6*_F[D&^ƌi㉩fz!:GuNx8'Y~=;r_O#xWч~zv!sd;]nԌN?|B6NO9('7`%e~Bmc2!]0 i@^/ե6'.Lc_j&Uyw1uA$bԎ5Z^ʄI|y]XsdR!:> }v3 ֌``3"%>z4vbtvveQc]tGӣ{=h.v]}*iZ#~hO;k$ci j@ɤЛr8tG_Y(sƻiny%"ᛈ)Wڇon]an+yjfMnN4u5YW=.vzoe7O#{qT.%sl{w$h& ^K<},e6{_piY8Nu!JV;Zy &MO~"8-k}kQx'uС} 7{@4,_Uںcp΀8u0m)jMS][?Yu:lcNm@f+%jddF-;:8N3N;‚BrIޥ87RHs*Hw2ơKj%-eԳl-u㲚h@o"E,9gH,, 﨣Gfɉ3|GdVW0i"aD^;q&rX!qR Mz+FzӝџPAFr5zrOfo=3C!Zbjȗ$p6}r;7to:Mz;3u۾` YHQi5 l{T5-΁qA^Ͼ G/h6BKX ܷG(7Lb߶xs![Z0z'Iok/w[qF~Yeنߓ޷ߡllLΣl=۷/C,BC7vJ#O?Sxo[_m'ϼm nvbz9``ꕌwDyk'aZ9wQnwl]Ѿ@sˀ6> 3'xn'C V܎?ݣIV[It"i,L۶vEyA>t>ɮm}hUk`VZHH 2Y0Y<wL>uדeEQԩDbO|/jn*i g%#\Q-p5YE8xk"k&OEO9[HCgyj9AdOyXT%<$KƓmb$}1˨2L?z]d@a{0X'Fckm~&^ݰg kZ $Y Ƅlx|ԃ[TU ࿉S$ku䒎Ir/9b` 44~dӅAR ör@]ۂO$1xH|$Q3 Qj@ S:dIENDB`qtile-0.31.0/docs/_static/no_scrollbars.css0000664000175000017500000000051614762660347020572 0ustar epsilonepsilon/* override table width restrictions */ .wy-table-responsive table td, .wy-table-responsive table th { /* !important prevents the common CSS stylesheets from overriding this as on RTD they are loaded after this stylesheet */ white-space: normal !important; } .wy-table-responsive { overflow: visible !important; } qtile-0.31.0/docs/_static/favicon.ico0000664000175000017500000000217614762660347017343 0ustar epsilonepsilon h(    3"U"w"3fwwD"wD"fw3w"f"ÏLJLJÏqtile-0.31.0/docs/_static/objgraph.png0000664000175000017500000016202414762660347017523 0ustar epsilonepsilonPNG  IHDRB0wbKGDtIME &*dO IDATxwXWo& ( DDDDq!2ĭu(bUD}Խۺ**uq 2*`] .P@P!_SIs].M ;>/^@zz:ӑL!77===bBGG&&&033 LMMѤIXYYv_0LhyF$%%!::ÇL011 ajjZjA(B__\d2"//yyy@JJ ӑ"Ѻuk888}h޼9B}aʏ%wRJIIKp%!%%Bvvvh߾=5k+++C[[ud2$%%!>>=BBBbbblSNѣzVZ)ja%w҈ǡCp1AOO]tA֭[CGGGqd2#22W^EPP`jjbĈҥ ccN}{Ł `8p >|g?@DDԩaÆテ-1 Sͱp"&&+ 0zh9S>|wFBBtiӦ|>0ΨU\\,YSNe˖:u*F===C0"¥K}v>}M6ҥK1|pQ+è1|pn8ubcc1y*gϞ8~8ݻcԨQGPP1 SΨTqq1֮]-Z 66DMqܽ{M6E^0rHp0gT&>>#Găbę3g0c yv°aøa*Gv;w`ɒ%6@~p]=#Fw}bbɝQ*"‚ 0zh| A&MR/@:tݺuÛ7oa*u3J#J1e߿~~~ꫯϣ^z\0L’;4SNŁp tޝp*4|W7`bbuH T[Q~ qEKW^F!ߴaXrg*,88 .Į]u8gϞEBBΝu8 T[j Xj|R~~>._TL0p>)22... BΝa ZL,ZzzzXdر{ ^|8}"+;̛7&L@aa!0 Xrgضm~WNd2d[.6oެX߿_2,Y|ڵK 1 SΔۖ-[о}{qRDDnݺTzdd$.]Zr0{llٲJa%w\ 닩Sr }g[1cN͛]t'O=}cƌ;7o(..֭[ _;v,8PNNN8|a#)SvViiiѻwH*%OḱciTfMpNNhт|}}>|РA>} 5mTd255K.ѻwܜ\]]I&}XfGq0 Ôk3咐kkk7qD>… 𾥝 ̚5 :u 6 ==Ǐ?:8sss%ZlDcV\ڵkC~_'X[[ѣGH$*)a%w\RSSajj:uKKK߿ hbbb pa@`` ,?G(~TΡCЪU+At:x/oذ!ׯpٳgcIIIGRRR/fff "a|Ann.Vxb$''ӧh׮燩Sѣ/XXXNooo^wޅ 0|c^x={b߾}弲fee{aʂ%w\޲oxbXv-鉋/:::%0$+VD"U`mm+Wb !..R)uYYY000(E‡m$0Lyny\ԩ4٠A ۶mX,FÆ 1x` ƍb:tpms_+-?~?J舔WEPTb)LargʥCظq?~\C?ҥK6mbccѡCס٠A  D"ׯ[[[Z~=z;0’;Sn| |}}޼y˗/c{.YSNaĈ5j~/ODxBD"Aڵࠐԩ`2bɝQ}Ş={LUcL>f}+oVH 8::cǎpqqA6m>C0U K҅c]6кukC41qDļy^Ǜ7o}xx8޼y]]]888prrbbɝQTxzz"<<W̙3}իok׮jܸqׯ_Gxx8akk WWWjabɝQT5k`հuXj+̛7 SSSNcJOOGXXBCCh :u3жm[62#HOOGJJ ^zt!33Dqnܸ0~Z+ +__!::<@BBsmuuu!QfMhkkxJFaa!|4hhڴ)lllЮ];iF!S5Ψͱcǰl2ܻw_Ǝ;qFd2x{{cܹ5CW~xx8BCCЕN:SNhԨjl\rAAAǝ;w H`ff4mְ[sss&| -- "-[znݺA___WΨKZd2>|+WĽ{iӦ]}v@KK f͂7 M)ׯƍBQQ4hΝ;:uBBF8wnܸ>'''899ɧ.s#UJNNFDDnݺ7o͛dpvvF>}0rHZJ4N0m4dggN:#1s?|hA֭1uTxzz@nnnpuuS޼y" 077ǠAлwot޽ 88ϟɓ' '''xzzj:DXrg8quxxxN:عs'BCC௿B1`ywae[\\[n!((gϞEdd$1l09NNN\șbܾ}ׯ_GHH}}}Ν;J/"CŨQйsg+T*իW@H$=3gt+dXrg3f@>}ЂIHHcpܸqRpttDvkkk|$''~*555QQQFdd$BCC[[[4 Ahh(BBB---o^޲wqqT_XhΝ;f͚a̙o4'''֭[}bհ:4XrgԦؾ};.\+Vxo677׮]Cpp0nݺ۷o#//Y&W_ 4QRff&ҐT$&&"!!=Ç >o]vE=Tz)_.ػw/ V2q}GPPP+[V-|yP__dCaa!rssQTT$?G aÆhڴ|r֭ѶmJsyBCC]Jh֬|7n\Ế{]txx~zX7ʕ+^exb<}˖-Ü9sx M;rw!Jqĉ -G///wTsss~ه>k_uM41]]] 011ݻw #""PXXzɻТE2'Ǐ16lFBBB/^yU$'Hn:Z ͚5ZhuX̿èIOO:uDiii*ɓ$\_+((Zj}WOv4p@Znݼy$I$%%oD"Џ?HeϏjԨA;w^{!۪XrgTB&ъ+ѤI!O$?K}>KUCqq1EGGӦMhȐ!TN@Խ{wZ|9]|rss;qԨQ#:w\dM<ϟ/ՁD"sǣ?$ɸ!Dm۶ wM!>/`e aɽwѣQFֹ͟?N>M#---!~ /..qQ5رcj:pQѡ'Rqq10Ē;dO>%{{{266˗/W8W@e*%GSL![[[xdbbQHDb.\Hyyy d2=z4ӕ+W( Lzzz7߰|%;4!!!dbbB-[ @FFF$ ?0nҤIbɽJOO'&>B!ӑ#G\tuu͛^f 5jК5kc+o0Jݻ7nܨܹ3Q\\*> UcaÆA(b۶mXt)<GGGGE̙3\STD"ԩS1e8vX焧sHOOD"u~戊*մ9@---R3gСC1p@UYecٲe$9CCCXZZM6}vܿ\,X'Ob_Zbɝ)8 <2 aaaJYoZ*{n|ܙR2n֬Ř1c͛믿FzzB;􄗗bccH 77޽{w|?e1e<}иqRSz,3%z*PVɓ'j7//?G%$$лwTKLihт/^B!>}Z=gWIRs~~~4f""J?ZmԼys&NH(&&Td2@}MhYhlR)e11c̜9 _iw^dffbΜ9fee8'==]1AaĉXbz--00-[TS2j(5 [XX|4Wٳd;vT#)) :PPP)obb4ŔKG$ fΜ;wbɒ%Xtiή"?~<[Q طoF___ԩSr 1:u ##Ce2_1 222гgOȑ#XlZ;!55Wk o*c05kŋᅢcؽ{7@__111-T\\/Tk-1^?=X888ٳg СCT*O?///4lP3)RSSVΝ;0a\x7o`ƍ lٲEEE%舔ڵKxUKIIZbgǎرcѮ];=zfǏ٦ g#o>L2M6-Z .111ppp@XX 5kqR?.]iӦ!66:t'OR\R=2L)ן nFpK&ҥKє)SXZlIJ/la-jC"`ƌŲe˰xJׯ\TANNNFKR$''#99!!!D:ǃ.rrrcѢEĮ]43 ^ {DZcǰdɒJj*ՕP*m۶\D999ggglٲϞ=SyUǏ1l0̟?KU܇iwwwCR 2bTZZZ:Zx._VZaj[Meeeahٲ%-[u8KUѣGƍ#""Z:^ٳ'ס0UHnn.wήT=U|>M4+W B$ȑ#G^5D^~=z\T{UL&ŋTlڴđ\sN:'Np @ܼy/_& a``DyH---\ӧOPXvmgϞ֭Ο?ϖS /_!1\(_ff&׏h׮]\S#GRV>ڧZVRiԥKU?ٙV\IQQQ$Hh޽$ hر:SRRΎիGׯ_WUj277'{{Rz^>>$cǎuHg޽{G$ i֭\Sj}YXXu:+?@:t }}}y".={PffRni Ji˖-dllLjբrss^޽{Gk֬!CCC266-[T[%w HZ"ccc|2˗/I[[o߮zYri„ dmmMbX̵Ɔ[ VUdffҒ%KHOOi޼yo>|P$!###קK{%w t%222"{{{z ɜ9s\XrWlڵkߟ͉+t;99ђ%K(!!P.++vI5D 6N>^***ӧO{իGV Ccʀ%w i& 4rH:2IOO'===ڰaf]5=zDK.%'''200}k׮*UMDF;w^zH$R/{nڵ+|222)SPPPPO.\)SPڵSnń,k|"@@?FN]p!ӻw^7K'J)44&OL0Yf4~xxF~Vbb"mذ:vH<ˆ~GW^ ڰa9::ϧ5jP߾}i˖-t-*,,[,nݺE[l}R5瓣#mڴ^x8`[jdѣG8x uHeƍc޼yX`g[]nn.Μ9#G֭[x|7]]]4o=z7|͛sjĉ?p=BHRS(֭[?YNzz:.^s!88҂=c?55jT%\R)={x[CLL `nnnݺO>իLLL*TSy^ɅaС]6N8kkkC*5k`ݺux ^?K_K:tgΜAll,^~ {gdd$@eԨQ& lٲԫW݃n)"""-[@$H^ LLLPn]@[[l PTT\իWHIIAzz:1bH$x{{cǎpttDF*0[[Ō3W_Y&!Knn.6mڄSrؙd2ƃ*C׮]1l05j8bno^;=/ub 4h~~~ "m.\@||<W^!55(((@NN$|FA$AOO077G6m`jj 333XYYiӦh߾=uֱ]:. 0VXXH&M"GK,7n$]]]JOO,T{999ti=z45i҄BGM2]Fp+YaB\L&QF/Z} . 5j0%c-J&-- CEll,;V_/_&M1T8~8N:۷oիW5kDѫW/9/yʕ+d] ~XfM˞>}: addTs! Q\\`۶me ,W"QQQpwwagguHw^y:*K&Ƒ#Gpܿ999/755E߾}1x` 4/W͛7m۶dmKDN]h~7bԩST&שS2 ߙ~ jªU7!89`M={ׯ_sRH$PT|^^;wƏO ]B6mJcƌsΕzc󢣣AT^On;gΜ2vOv_xJ$mnkժU[)/^dk׮-sLǒ;$ ͞=мy4z ۻw/ J&'ڽ{7׏JQ988O111lo%  ѣ~֮]+. I&e^fǎ ?>xs>:ܹs_:F_A((('O隘ʋ%w9rSNu8J'ɨUV4rHC,=%%Ou*tjkk=͙3ٔ4nݺGʕ+x|)C AϞ=K=Q .vmmm###~%lDff&Ν;^+..ƹs琙Y*HJJkb322/mh0]T),,pT*((Э[ETrOKK#+b1Ҍ3ʕ+rOOOO*ߊ֭[4|pM@+={8Ӟ={J,_~m>2ǒd``@:tܨgϞ\&wLFݣSΝPñf͚BW;w=5@RRmۖj׮]CbtuuI #>Oƍ+]~K@ ]XƸq˷6YŰ"2V^M|>Ǝ[-Znׯ_'t5CHY{AAٳ^arG4h ڷoZ)ڪuC^^թS|||(;;l">{ϞR>RRR>[·554h@[ll3gxiРA$+E $GwQjHOOl cɽÇo((44vIb @YYYGɝgϞX,ݻw+ 6|]__:vH/2vfɽzHHH [[[255r}rU܉<<<^6>,ѣG?ŽƌC<,YR-ӴiӨaÆTXXX󋋋͛4w\jݺG]4p@ڳgO sKUŋvԶm[JJJ:"":<H䞐@<ׯ邌 TVq $رc\ȹѡ_dggѣGiĈubI&QPP`ɽj۰a T Q^TR;Q @Ô&Od2狋#"<6m?$''СC8K$%%ݻwΆT*Żw >CݺuѴiSjժh9r}]&apqqKT_۱dR)q!\|~ӺubȐ!4hڵksy LK#>>OF>}I5k 777C)3t Fݹ)#NGp5\v xR)Qn]ԭ[Ճ% 5.((dhݺ5йsg888@(,ez &M}aᾮ]"%%ZɽuVbaĈ@RRR)@[[M4AΝ1l0@$q5Sy&5kuH q9={Pmƍܹsu8LY_*RHH͘16lHА @+WǏӣG*4x#==i˖-4n8$GC~qAGǗp/~ʔ)W$%%ѺuMa?Ν;ӏ?H?:O^eڵ_~u84dj׮JP=U6 Q-&'Oj۶-YXUiU{졁ikkʕ+dIښ-ZD*+RiƌԲeKQ|>|> 8T_* 5?7~YpaW[u$H@...*Q>$7nЀSiʕJ-++8@...lmmi$H(%%j֬"$Qv觟~xNAU( <<!K---:uDzzzt)ڪˇ:;Q۶m ݿ_u1ʣI}%JjiULL M8b1YYYɻ+m6ѩs.䮹AԤI:%&&P(.u%@{Vy](%};ԱcGp2UǏʝ܋hhڕdsNӣ;j7H={6mۖ ԭ[ @;vRlllhرJ\XrԽ{wѨ7իW1:;% NeJ\͛7-~~~ʎS';;;2227nT/_ѣݻw.+''8@԰aC hʔ)Rȑ#5z@ K!66,--aÆ5#իWTF ڴiTwr?y$ &Nʜ_|IvvvdiiYe:ϧ#FP5?,W~zyvZ˸{.-Y@a1u֍֮]A2ڶmKCUY{uܙ^zu8e`266\թNDT^=DJi0U+֭[k鲐J4c \s/\@VVV)e6nX9t)=z4YYY),#I&EjpYQOaɽJ?ǣiӦiܘoߒ!^Zr?2{l˔]{nn.9::R˖-+P0|200( MO>ad2=|֭[G]v%###95k$GGGZ`ݹsGEԧONcP+oR~HKKvu8zj244oߪ^.L&:uꐶ6nlJGMׯ4$L&QFQ >;'//-[Fbךɓ֖bG D{Tݑ*>Xr|ݻGTn] 8 IDAT:r!cccZ`"Z|fJTСC]SUEaa!988|rH`` իWO^C,M6._\Ӈ:uuJ{r)Y&9;;kBQ7n$]]]NFsܥR)R͚52)/& Um޼YTZ>$]]]:|0UߣG$L&JJ'::ٳgE)Xrd2-_x<M8R[.gJDD?#[rR?e_L쬒GgΜ]eqԺu2%42:t(mۖ0%w{I$Ѷm۸vAb.{aa!ըQ5RQ'O~? Wҡek_~8}RV={˗򽍉BT{d2DEE:Dx? p SE|/޽{}}}u]jO>?~w0aڵXp!kڵ \Ua2 k֬ȑ#9K앁.Fݻwŋի!13gΐ@ Pv4h ֡l{.ŕ~SE v]rssiȑ$hÆ \TG%>O4.[dcci>4  %СChڴ)b1zDkX|9VZiӦ "DEE&L@pp0X]\\Z "Rݻ̓PN)ӧOꊠ \ps:$!"Yh޼9pƃu8?|6߼yj ⯿Btt4~7||2w͛Ea۶mŤIHlٲ.]Ç ֣n7oƍu(r  JݻsR?l[nôiӸ&gϞ~R&Mn:ޝ՜[7%TCȒR}_АTciѦY2J(J3؋d7mڷ=?/߱.xx+n}l6m ={`@mm-:w,<^KK wA࿟o.Q P"EEE ҥKѴiSq1c`ĈHHHh///?Ejt#F+Wp}q^PP֭[K$$&&V\?8x =z>࿃tttD>]cS= < .1UUUҥK/رcPSSKqej@)ŽZ G޼ioZ;w3ƍݻSTsJYYvgggHl 6 QQQ8uV\)ْ :v… ɡ'µjՊZ+֭Chh(ݻWcĭ8Ȫ}...02$!!hժRRRO;ؤ̙3)ܹ̿pvvN;h˽uԊ[[[[;УGaa|@۶mjAb0۷oF$''uaOOO <GEj <zBTT^~M;hqӧn߾-\. #|>6mooo2P|H# +}Hhh(@VXA;Bh˽_~0`~7}АW\AFFΝK;J(_8p RSSje6ښv1sLi;wNYf$;,X!!!ϖvW_ю(QQQ{.iGaTuu5?bŊ"sݝ-@ׯGyy9oN;䖯UUUѣ~g899I2Tz)z쉳g\K.E"ؖ owÇxThjj!??_!fHOUUUjl_ F~cʕ0R(11FFF(,,DrrBW^puu.\.-[B>|v ̙]]],X@yVhh(bbbeQGyٻw/###(T~vj*)|s\̙3عs$2IGa.+~__pp"l%PQQ ?vSaAAADIIDEEoz֭6m4ĉ6)iѢ7MMMR\\L;'ITwGwN;l-;;;Ϙ9s&qCz`رhݺ59"Bnܸ?!ֿ2EWޓ555XYYÇHIIGԻo20n8'00QE$|7(ߵdիׯ/3+V'F;Hdff"$$+V匌KHHQTTL2v$6Q.]qqqx8 QvFo'OÇEM"ʰ~zyyyHHH?,Wо}{ҎPg1FFFHMMEϞ=iG*!!!ʂ(rk޽ wI-8һwoLȓ'ODuj(//'DKKhтlݺҎ%r/_$M6%P-_UUE,X@8Y~\L:ңGbggG;Jm O3"+ÇIn#VVV… D?ݝhii555NiwwwҶm[RQQA; UXYz.KݻG;Jbq#Ȃ hG{"-o֒P2tPݛxzzR),,$"Ǐ'\.tܙxxxW^Q#)EEEDCCxyyюB˗/:=zwҎ#߿?d& Eet2fΜK.=z4vލ={nnn?QRR"˃#55+ƍcѢEh֬~w<}W)nk.p8ǥ`Xz矸q[I~W`ȵOn*J$%%!22.\k=z@߾}ѯ_? ;wFNЮ];()) _^^͛w΢"dgg#++ =­[еkW 6 &M„ |yVQQ]P[VWW n:_^9ZlQL|mۢ%%%l(KBfff033ҥKHKK۷'O@ ТE 4k M4AII ZhB^~*TVV ߶m[C ? Hۓ:CEE.]J; #ٰݻwɓ'ӎ$Ο?D$$$Ўp6l؀ŋc=JL$rj [(++Cee%֬Y+++ 4-[*:v숎;SNlw૯·~ q<BSS2s5 ;wv/"-w@-[!??v${}4i:::jq8q+VPH&[>W^a崣0b{nO?~7oƜ9sйsgq1Ƃ xb^7'LLL0rHQ֪U wwwQjkkI9t ӧO9aÆ!<<QQQl\ݼy111Xz5( MEEӧiǑ;R_#""PWWMÇ)'^x{{ ݺulF߾}1i$Q͛~v#VI]].\`|DLL n߾+WҎˆݻ1|p#%% <@XXXJCzz:8rE/'lN)tĤIзo_Qbnݺښvر\.-%bR]!DܹsHIIar&;;8q_o[SF4} ::_&H2M*[nÇ?y =*D8ֿ.>PQQaeQWWWWWQdTSSS ^=BXXjŧ pqq8g9]vEpp{~2 'kkk1bnZXxx8LLL ͲM6ENN:vH+*UasveTuu5-ZCaXv-&b (L=m޼666ذa6oL;̒-_?EΝ;?Ў#KKKܿ6TVVBOOs̑"![~N۶mQ^^f>M*o3---̝;v.]###(..ҥKiGahÆ -[hGYˠ|߿PQQi]vaȑ055EJJ iGK555=ڷoO;@?#@@;Lb]m۶ M6eeHUU₵k"""cɭxnnn0__iǑI˘bڵ K,Aia!++ Æ CDD91񁭭p.#{ݡ5kЎ"Xq1l'lq ǏN; ***ppp˗/qIqd+2Xh[Iu#޽;8L#yzzBYY-jXq!Cyy9+X:=111uV\I; #jjjӎ#SXq555󃃃ha>I&_~0". '''Qd +2"((yyyǠAPVV)8{,RSS,9q֭[q82wySN0sN5 HNNfxyyaԨQ066;w';L6gW UUUa…8|06n܈իW$&&… 8<( x hGz.acc^R&++ Dtt4֬Y ;%^^^033iGad׮] zb-w){ĉ0ڴiæQvu8u(o߾￑:xc4ܥO^zю?ֿ.]<==ahh &Ўˆٶm |r" :::ą hǓ*.bccqݻv]ݻw8~8P#F@.]pqDGG \._O.ż0~x 4v3fÇĉiGb@__3f̠TlٲYYY \5B^zE1a]J˸t( /..hӦ RSSٲRӧ8r.0ʫӧOc֬Y(((!H8tc?R:t(( mǎ=z4Vإ/:w [[[Q1@AA8jkk?z\~~SI?VܥPZZΜ9Vڢ vvvXd ֯_'N@MMv,"((nnnPVf7 噉 Ν;qw)cѴ(ϟ믿FTTN:I!???lͣ#F <<JJJ=L%Xq2nBtt4V^M;B***ʦWI|۷PUU)S~ z/Vܥ7 0eQN@@_PUU(͚5 K '^K",, +Wd% sҥKY+));#6oH8b#Q`mmM;xf̘111?~iiiKJcѢEԤ .s\;Xqh߾=+!~~~;v,F|W#1o>bҥ0RbΝ9s&8|>kݖ/^믿BEEvV^^  <<>>>X|9 jjjmۖvFJp8oqQ;O>EAA󑗗|eee @fjjjxPWWкuki]v:v(3!.m ӎ"^| 333ȑ#iGbx\]]iGa(#ɓ'v_7nÆ 0iFXТE Nff]]JJJc ??c4it߿? ===k$p2GYhG"tk֬8ryɓ'ѵkWڑzի Ў#5tuu)h혘x#!!xݻ7 ѽ{wt ΝÖ-[Ю];fx077ǰa`aa###+';ePVVƢE/ IDAThGKx{{ ӧOiӦS1 O"66vFB(?Űaðe 0Ɯ3gX$o۷{۸v.]ݻwcŊh޼9FSbya-wеkW8;;cÆ ȝRԩS׏" _~ׯ9B;T{aa!BCCqq$%%Yf7n&MÇK.#Kff&.^Ƣ氲ٳѲeK䐍r*00555lήܿ&&&|2Ξ=K;"##q\vF !8{,fΜ;bٳ'0̙3Gf ;hkkcΜ9 ށʕ+ѩS'ܹsݺVTXq9ZGDGGr ,,,hGbMQ !ƌٳؿ?&L&MЎh0a@<GFq|\wJ+2OOO=FFF0"_aptt#o߾I\~bܸqptt.^(kNAHH^xȝ;w`llWptti˗/#>>k֬i}3f >}<ű9 AٳgHHH`]egg#((nnnԷd&;;-¯ CCC^{Ȑ!کK &M©SDLKp_^z!-- #???hiia޼y0 qF 4/Hmر#m&DCCClܸDzbvU"&&vP[[%KĚǾ}S:tϟÑSSSEv.Yxp8_0zhZcX]̼0`6grss1|p>|ǏL1f͚~i cС( 8۷oqݻwqIZʧZY###z ɰׯ_#00K.m" }>}zsyx}7TСCamm BBDDf͚[[[ܸq&&&x`899a̖[[[,]SLIѨ5uTDFF~ "##GЎ W^vgҤI2i$RTT$kGxzzMMM3C7o$HFFHgkkK4iBJKK !===2q#w%۶m#-Z oXyy99s @+~zrMN???9H>}Ⱦ}dԩ=zDZlI !2|A>|E?~;w͛=w1y1QVV&H*2|pȺu!H^?iӆZvD?~())ŋ9|%KeeeG!XYY >qDnU dJJJbmm-|lƍm۶ Wq%/gvv6p8d̙|>())nˋ iYYY@xx8"##qFpٷo*++tRQ/---~Çً DDDzN|L555@uucǎ_~̿;w~ﱎ;w8}t7cػ9###"55SLmڴeee\.q9dee!!! ˜1c x}(**{ODiiGΊ9QF)))ѣH AAAiGa^|)sΙ3pܹ...3gpitM6mu4iu!vJ%[/_Dv>uVE,//탫+J̙3K.q ӎPySN0_C(**Bee٥K3v킊 1m4Vd &cccܻw׮]>['Nܹs߿3fP(**B?z +"͛v3#&&111Xz5;Lю4!Cr,Xd iӦF1hР說*xo%+WxjJJ ^~ 7 Xnڶm 9O> B_1ssÊ;g!ʳsuuuHKKc(8@///^Ռj۶-%N:8pyQ(())Ms6lͺGEqqpO$ѣGǃ9LLL```}}}̟?N:!11՘2e \\\nSSSjd\@2mڴhjjׯ_ӎB@ +QRR"666jYx('N.+ъT8Bٵkܹ3CTUUN:OZ"RVV۷ PUVV3g>>> e1,<}tՋvF!pAQƁp`ooXq ?(T=~8wouW~MXj6mڄBq^AA<<>/_ƥKhGa>#??{u%%%t=z􀾾>unݺ{CHH [bٲe3gڵk[M2CR: v*JJJȴi#۷o磤(FM,,,hǐ+Pw!D|#<OwCZj%ŋEI8pp\Z簖{# 9g>}: qYXXXЎH49s潵5p@}{"o0a|bͩ(:GGGl۶ 3gάX{#x{{c֬YեGall-Zʕ+3cѴ0лwza+++<{ 111_`ggkkV!Xj gg=!""<ɓ'iG@ 6صkWsmDEEb$%%!!!/_F-p\b߾}oZ X`222plR߁|),,Ă #G4+_OOO̘1={G"^~gΜݻH;#弽ѧOmuɈƳgp%$%%˸}6ajjf͚ٳsy<4iM6郃lmm}Ł0n8I|k2ﯿ=\.Ο?SS/;ER6 *&&p8r5Q$ۤ{SN$11v׏"x!QVVfbRu$==kkkҩS'8SSS|rA^|)|NPPп)++Cȋ/ꕱ߯!vvv[[[RXXبĉiǐ0FInn.8_D^?`[nv^RRBN>M֭[GFEҪU+2qDIIEEG}Fs\ҿEYO8AMEE jjjD[[DDD伬s$!!vdŊEڑ4~Aff&QQQ!En-$448;;CCCa[OO̙3ݻܺuzVX555I]]][:wLlBJJJuNYVRRBHΝ/0|p2rH1*??3 qM^?Ņt҅m("b|>\~ر4oޜ(++ o%K'NۀODrrrȲeˈ:iٲ%YjyH!͞={FVZE455:Y|X+XqoK.ŋ͵k׈.&㈄~˗/IӦMɶmhGyeee̙3dƍdرD]] iӦe˖… buQܭBAڵkG\.0a #UUUb. UUU$,,5p\ұcG)NbŽƌCk1&$$4k֌|7իW㈌~;i۶-WYYY$,,A [:::֖ڵܼyIŖPSSC"##ԩS #dȑ$<<ҎJKKIXX={6iٲ%xdԩ$22|_Mk>}7(",][akbʕhڴ)8RM  ##)iO>2 aff+V :uWlx< 333DEEŘ={6\.oÆ àA}#==qqqx".^@Çh۶H習ڴiLLL0fQD*//HMMEhh-0.Eю"uˑ&\(&)) PWW)͛333y0ܽ{W>P\\Xؽ{7VXuuu`߿?ݻ ѥKgwnݺׯիHNNFYYtuu1l0:tǏG-ĞCXqk׮?ĩShG4XZZ!11#12 pqq&M^x^!z*jkk sssxxx믿h;@D}'O=~PUU| EXq' 8&LEdh"XXX 44[a{Euu5\\\hG8BܹD$&&ҥKx ``` O0777Is'N***•-Ǐ]]bΜ93goܸ$ڵ ***ؼy3rss{.˅}-uUUx{С+++}E=@:SI[nɓájkkd.h90SUU-[A!>$VVV"==/_FBBPXX555 2077)iǕr#,, khI[eeeaCa``uuu899&NTWW~JJJ P\\ 4k ;o?m,{=xzzo߾:u*( +++ܸqᰴAAA(**+(b',䉉HOOGmm-:u333lذfff044ږ$5o;v}:w oo_ŋ6lpп@&MЩS'9UCxx8=*DXYYyHIIA޽iGbχ/͛:ЎhܻwIII‘<E>}_cѢEѵkWq/qڵOwB-*,,Ñ B%:]ڱǷ~K;Jٳ?ƌ`hjjҎȉdee͍v/R]]-ŞyQe˖Դ笫ܹs_4d>%!7oFΝӎ`pssqAюȡ?7ooF-Cvvpn żžvZbxr* 1^ Ol۷/jkk_~$`ѢE `=f<-^/"ϟ?'***d޽D6oܠ$_?bȐ!dҤI^mm-IOO'd̙D[[[A3 UCq㘐=ࣣ Ҡ󺹹.+cC455Ew^ĉ\CcHNNq__ǺYNJBEEvzٳt.5 C 9LMMuVwlƇرcqutϟGϞ=?zIο`())YYYrso߾}3V4551|Q>u{al2b/… _|wmI&066ĉ SSS6wY`ܸq(..Ei:\\\H8;;#''烛0Ŋ;w^lڴIdII {bµFܼ`nnoy: q IDAT{ޝ[njj+V:ފhǎXt))U<\.&M-Zرc.Z0c֭h޼9iGwb(..|W"66111=ovH377… affˠ*PUUʼn'0qD\{-[l2<Su?C!C#G0k,:::Xz5VX!kBDD```p'q~%={4a+Tx{=11)))(--: C &&& C]ٳg3f >|.]ٳ޽D-кuk|7&/^XYZo۶mPQQŋiG:[pttĶmKFܹHܹGf}6ꠧ333l+a9rL>7n$:K!((_ի5Y]]}p_8OhjjM6OƎKTUUqd8_?򬪪$$$???ҥKL&M333|rArssiGU8.ƍ !lڴ:t M4!,b-w@*]~3fG||<L;#nvӷZj"ɉ |S%%%ӧѬY3DDD`̘1\,\P;K /eeeؾ};n#G cSxo:}nSSSŋ8|L4\FFƎ|W8}_Җ/_eeexyyQ,R]vK,E۷oDze˰yf4Zii)RRR<)) PWW lll`jj}˟?`ס-jkk1m4YfT\|?w}'u 0i?غu+/^,5K|}qQЎȨL$$$ 11.]|ֆ96m |_Ѯ];Diw\.puu\.۶mC(tqߵk***l2QYHLLD~hGbdDmm-^x8p իW̬A'_|`23e0eB]] ZH222pu;@@ [˱e899IE_#Fȑ#l%|{[QYY ---fff022j[BCC ,azFڤbĉ(,,D^.]Ў  00v]ZZpvvƁj*/lS =ܻwO8x8z SSSv4Taa!vZ6BYݻNNN <UUUڱs\t C a+~,eeej·~w"22SLHKK.ߚB4op8NJ@EE-5z`p\رCrrr!wE&)dqߵk5ZnTуZfq|>t邡CbÆ 033F ?+]effbĉ@˖-q)ӎ%TXXАvpŽL޺uk*oWWWL2b&*j\rEx=55U8_~033annmmmj9QWWj8{,,--QVVCCCĠcǎcgٲeعs'(2KΝ;QUU˗Kpppc/`ʕb"/;; U^j|JcѢEl`!W! +** ]]] 6vPŽ~~~pvvx3s;vDψ߻ђ:]houo>bҥ0"RRR[[[@II {ikע~~~4*vBmm[fϞ mmmQ[‘/^%'' j ppp9 $3]/555󃃃ڶmK;#wĉ6m &&ƴc}Pmm-vލvaƌ4)o[NNN[ooo[666طo%qK>yuϟCf[+ϴ0" 522BTTڵkG;G۷oE)Lq߱cD[Ctt4lق~I"eD#//HIIABBP^^ bܹ011!C[]|b޼y ZŎ>uuuXblXx1('8B6oM(زe \\\$j>OB;wb&p6UEbfϞ!Cwrбcǐ 777QF˃%ľGqq1eΗ4Qc|FsW^8s kIB 畧T;w}/#M<==acc#ՃOKKKɓQPP:ԩS0`Xn:4k֌F5rȑ# ~^qq1iժYfRW]]Yz5p8~ UUUbS?uuu$## Æ #z" dΜ9d׮]ӎKMxx8rݻ0Cttt߿?QVV&\. >SL0;@\]]iGrr@]]X[EEE={6Ο?{~۵O؞?;&a/Q \UQZۊu[7jmZ[Z-GU[qXu@QAQ*(Cd(#$S**# }9yG+;|cĈXr%Ta* "Xrݛ>}:v /bŊ7U5 ,6-[vŽ3g6~:QUU/U!pvZ!JѱcGB,_\I̒%KGY?~׮]NQSS777AOO=z4۱ҥKw&M29./ڃ2}DEEq+$$$zzzٳ' e˖͍+VСC;(((ǏacccǎɉX 6sL|Emq/))?|k֬QZCMr޽ZPSS+++cpssC^TUv9ٟ/y "^O<x{{c߾}066f9Yݸqiii5j:galڴ(jG-{II -ZmܫWb.c66/7IIIX,9uuuَD"Ο?sαSahh}}}5D"xyy‚8jG-u@Drn9s& ={h6/WB Gpwwǜ9s:WcX8p Q8oq|Ǩbbb0x`c5ɴi@Dؼy3QCDvJ֭[x!=z H$(++x<LLL XYY޽;|>:v숹sbҥMVUU9s`֭Xh-[֬1aCnnnhɵڜC(J0 íѣ:t(q8!HdZ 0fرTlFlb[n믿؎X9sŋqE$&&"++ %`kk CCChiigϞ999Çe2 RqqqӧOb̘1qKQ^9}[ll+mN?3SNlOXXz p^cǎş afi D7Em)KRbFNNZhOOOk׮֭:v} quܸqDHH 1d=ny3&&&HHHh(£G6AAA puum6mN5ͭ[p!DEE5.55ÇǓ'O`llblǒ ۷pwwg;RwٴpB277'ԫW/ $ly}ڱc1tttHWW… o|_DDiiiѨQD9UUee%D" cǒ  G]v ھ};T*e;qƑ3I$p{n!@@={:_gyf;ZSHqD4|pxdmmM˗/w*bz+))Hׯ gggھ};bk^xAǏ'G*_o> &PH:::ZnMÆ P:s ,{npC,ٳ I&GEs,bܜ(jOի4tP@tAlƑJ| ikk=t]ٳ'lْ?vD+//K.ѪUOvuE P޽iIlG #((jrׯ|Ң͛7=ͱwmv'^\\LӦM#GBN:%a޽{4i$Ծ}{ԩeeeK!233)22ON{&@@ܜhժUt%*//g;BpŝM[ne; _M6Emڴ!HT5.JEԢE *ʆ&#G9mۖ"##I_N#@@_|UWWIJKK̙3JÆ #333@:::Jo>>Q+Df"kkkb; nJZZZݝ?~\6}v@+V`;Fhtqs04ydzEBBlx@OO...e KKKrTDxx8'EbĈ~:tttm6?X u!88k$ Sᅬ/^ 99Y6&qttիW1qD <QQQ6l+Yb1D$$$ 33DbŊ ѣSblڴ /eҥKCYY=*ϟ---Ec4 U۞ٺػw/'N(e휜f*011 F- Ly8aݺuَ"""0o<@SSSS)H$BVVƏ\*Q/0|p?T޻<Ӄ?Ξ= _VVWʊybb"?~ @]SL+sG!""sv dIEEw^_~PQ1}tx<[(>5Qo+WDnn.ڨnp7nܐ'$$֭[J+ϟWWWKiT8oӦMٳَ1bddd@WWw֨7oĵk닖-[Gԫؿ?ammL*aرO'77W-11IIIx9 ___,_[Q cV>}x9lllpQQ1m40 M6E㼵aڴiXr%z行L*G[[wF^~3Fϟ#))Yynn.|>@Yc.qطuVM'悈j*,^ヽ{ؘdC\rCv؎qZCCC:uꄐ,ZpY$$$ƍH$+fΜ WWWp89ƚ5k0uTmۖ8ǏGtt4`ꫯ4޴i@Dزe Q4CDofgg gϞ2sJ8::]v`}P(D߾}accv<0 ={࣏>b;Rl۶ 3gݻw5V[n߾ ___dggCOO;|||6_ǎsZ<}fff֭RSSَx?wͶߺuKtuu1{l[ vJU0a+ILL >CTUU111c;k!JahVj۷'NTbz*<;wNcs8oXp!QԞT*ŷ~#GŋGRRFrok'ٳx)G.jݦM 8pJ*",, |lmmَ֞={aÆa`k׮}}}jɒ%َ^{Y>66]v3Dd޽eee裏kkk\z0ݻJXz5k ""x8q" $׽={m<G^:L8p(j--- ÇGnn.q! 0X֭[ann???hu8p M:Unj=zD6m";… iԻwo""ʢ-[R||<ӥmʔ)dbbBwܑS#G-;w"ϑ?h@W8TJ={ѣGEoKzI<`%*v[lm۶E㽶ʕ+:D"!ԣGJHN06u,w!9 ѣ0 E-emZ'MjeU+Ą$ q4kɽ:txSN8}t_H/\>}8 СCѳgOB 4ؼy3v-[_~%$ x=rv)**‹/^ nݺ)iDe;IIIÑ֭[#::BX*7@__g;oX-ߪU+ZXX ##=uR)ڷo)RQQhTj vk.B555/qi.b`ׯg; ?0'|ݻwy\Spuu-qXΝ;s|6   ===8paaalGSyoFRR +++p}xزe򨴿;wǃ:v숏>kA IDATHHH8Js=oꫯ C?DqM 99'n87}t~'pƟ  Ahh(/q)L4 Ϟ=CHHB!Я_?̟?ÇَQSV >4K4hJKKT899xΟ?tܙ8mZDBBngӣW'J)##~ϨG YYYњ5k(66^xBzho6mٲ() !ahh.[~Xv]-oYYYٳ';̜9S&++ }ˑHšZZZի 777B٪Wj`8pwlwYYYETT|||؎hl/..7^cO`cc#F(:((( 3f}0`@݃H$Bbb"D"6o X ssspuuE>}{:c֭'N@@@*++ٱYZ`$ ~Gpޠ^g/-Z8~8\*qyu $%%!!!AVsss!еkWY ԩ\4rh"رR;5WD+V`ҥ`Νr6`jj sss(m^N0֮]lI\\YYYQ>}(??,G4vX!k׮D۷o7oD"a5*,oe˖TZZvw-%@@-[ .I礫KmڴQ˟AgdGX`._;ve˖?P2"?/C }`hhj&K=]OHHݻQQQYVZ#/刈ٳ+6oqa7UUU֭bbb`mmvfoŊď?4@$ Y[[ӁtLN5tErvv&@@'N{khx9^ؘIGGnv4Tڼy3M0a@:uݧ\#""HOO?~vt5j׮ 233x#){UU>UTT(|>|ȭS@vEEcƌQ,ٴpB233#CCCZp!D"ŋ0駟弗tROBPU׭[GhѢE #?r-/bڻw/yxxrvv+VԹu2<}~!GZ%Snn. i~ƍH|>уONt]6Xs-۶m#mmma;J?\v}̙T]]v,RFqԪU+V|Mo4g&Խ{wZ`;vJJJ2X,Zz5 2I__FMz뢲tԩYXXPllB26yyyt &WWW"dnnN~~~=UJGs,b((((*O<ilGb2?Lhƌ # ڡÇ… 믿;wFnн{w899VVVh۶[{*#77>DVVdѾ}{:tht-))q)㏘6mZ>xH$H$홯޽{ َ+w۳g&N nv'Om۶8vycB;IRXXX?F֭2G1V/_իWq \v ݃T*|C__:::044!J3TVVB6^6md СC&eJXl-[ɓ'cƍiҘۈtHR]Vv6Ein]*_޽{7qT¾}0qDԠؿ?LMMَE={ 00ǏǮ]2GqX+uB^^l(++x<Zl ]]]XZZVVV011QXR%%%пknѥK㰪 ,5*Yp!BCCY;PT,D,9(--ń p1^^?~8YONNFUUkݷݻ\n4~1 >ٳ'qXaÆڵv,NDGvv6n޼7S\qo _ oǏ͛vf ɵ ǏP9w777ѣlGaMdd$L`߾}066f;JQTq?z(Aٳr<\qoǏcܸqáCЮ];#{VƍH$Z\ٳg{!>>lQcƍ`!!!Xd ר*NNN@JJF_9jw܁Iɺ\WRRRzͥ{yyAWWOf;`z*c,E|(Wܛ'NDtt4{IcHR|kuY˗K.ӓ8JCӧÉ'`kkv,޽{wi#uw9 "Z K,|m۶AOOXu W^;\\\TCuu5.^vڹs'N D???DFFB__X*O ^*19,RVxĉԪU+ٳ'eggCDKŅ iii+)//՜Μ9jeO?ǣ5kְY....^(19ݻǣGw}HԾzׯ |x)D"Rcۓ'O닔!::g;V"3Bt ׯ_ox n 0i$ 21c۱8bkk [[[gB/0o<<Z-{yRSRRpq}lUaÆ8qَҮ]۷oN:GuCDu>y0|ڵk XrR)-[SRUUۑ4N,SRREDDИ1cJv)_(rO{&T*Tٶmۈ Tnou׎rJ1b%&&ʾBSN(%ኻ>| ӓR)ZBvv6 :aBcһKv&X, 1cV萻;}Mdffǣ{*,bcce˖En24ٳgz~7>G`Q'(WܕH,ӌ3aZdFcU.u{.ڵNJ]v]wttɓ';uؑ$Rs)˦Md?3lGjjjjԔjjj8F.]u~ݺuJTEV+@ н{w̚5 iiiؽ{7 َQǏ(..=w$&&' -UUU Bdd$/ȞL4G`` ~'TWW6c!??hٲ%&LvqYr=mڴAtt4զ~)Μ9ݻw o߾pwwG~Я_?jՊ ooo 'O֘Qbbk+$$$o߾o|033krV΢5 999ؿ?Rqa۷k-8::_~:uF7ŰaPZZ :tlR;۷GNNNo|_YYk XYYʕ+ܞjB9oqdcc˗/cРAxqF#q`h۶m>;u &`֭HOOGaa!1bdff>CΝann] =SxyyÅ ® Ǐ/Wɿ͓'O@ )Ν;vu-?,_ n #ӣ76/_f 1鑧'-^=JEEErN^ ;vɀ?C)j7o͛o}Ee⊻ 9|0፜Jq7oYXXЋ/6fFFm߾&OLGvJSN]vݻw6K< '''b֭[rSzҥKsȑW300[G}pUȑ#!>}௿b;G e,X@{w'Oq-СC7;;;XXX`̘1FyE8;;### ͛7(y? Àa|zOaal5=0±cЧOEF尅 Ϋ id``@;qT}ѢEdffFJ.]$WUV hd:q={^Yx<1 C_5_ o]ZJ@ cǎ)8%Ms*Nܹs -- !!!Jk?ʑblܸ-Rс'<==,##"/_FTTBCCѭ[7xzzSYYqÁϢ***PYY2H$% rrr`dd===H$ٓ#CUVt GTܶm0sL 22FFFlGj~oEDD߿Oņ 6+@ll,ѦM>|vvvlGjv,С̙oF7Fyy9p9r׮]A[[0`^zA .6FVV.]8#==R:::ܹ3`ii6mmڴ /^!Q^^ #//ŝ;wpmTUUK.  򂽽=EU6CRRF WWWDEEalƍ!H0gԛ!{=$''ڵkӧv0 Caa!  OOOBvh.***p8q'ODVVLLLзo_C.]о}{߂Jؾ};,,,xDEE}].䰈y9@ hsҚ ,-+//'333ZhnϟÉa200xۘT*tڲe ?:v([շo_;w._~E!۫Zf uЁmذ َZ~z :t@?\qo:Dլ [(/^ ?RmӧO> ???hXϞ=GҢEӓtttYZZR@@EDD_6I~~>͝;ؘΝ;ljL3gs̛79pŽvuؑ:t@׮]c;Jc_(//O6T*Pbx<]VsTVV˗),, F&&&LLLLJBCCҥKrݽOY($$ ‚֮]gϞ=,,,Ȉ/_NeeelWܛ"CCC:tqT{UUk׎fΜ9 FȈ.]y% ]v6l@~!YYYwww/bbbӧr3--Mg҇"KKKjѢ:*(++P266&+++f;-iԩ0 -[1.[l!mmmzlwRarrr޽{k.:ulO>C999"x&|YӜ &he낂 }+7@ <{h ewXL4uTX'N ===@cǎJ#iJZZZlll(006oL7nܨe-,,(**Qښ:t@'OlƆlll())8:p]͜;wLMMgϞM:Q7, wRk(TJ|8""HV^^NϟzȈPVhĈzj}S$/ //zL}iǎCT\\,<}J믿\qWCYYYԥKj۶-ƲG%(K$ԩM8Qs5FYY 2!cccDlGjXLIIINԶm[Y_M'ORz+x<7o[uxx8x233>WCݹsڵkG P׮]ѣGlGRLھ};M8|#a,//շnݚvYen§Rm?x<؎1DB .$ahܹj{jGEp]ٳhȐ!r}9QFq߿?xݿU4TJ/&aَ͛oo7,a K.\폠/_NFFFvuIII5j ptܙHJpDwy;wFTTBh25 .\@-p)ӇX155ӧO޽{#..ZZZrN^cǎݻw>v$%CDj B'Od;Z9|0_pm899… ѣ2PB~ IDAT224Yص-=0@駟bȐ!055ņ 䒣D.*a?ɓ'ظq#q4UXZZ… :u*|}}j*̟?XaCn؎h;UUU2e 6oެ=דdaaaGGGtppp-lmmx 믿B(6z4ܹgΜ>ϣʌO>cǎE۶mَX)aիӄ TryGGG0K*_|!cV󨒴4:y$eeeX,{,YB]t#4rHHMC*3}rHi kcǎ 5&M.JŅ%%%ԿbLMM)%%<]UU7T,{bb"3F.cmݺ,,,J.q8O>HNNf;Rsq$''z=##tzLٓ<̙3x)Ǝ+8 ???TWWe1c ??ΝxኻsrrBBB鉽{YYl ޽{2УGO?EBBLMMYɢN.\+l 443f@=˃/' Ν gggdggJOOG`` Ν#FgϞaܹ`Fܹs ,}EJJ &N&}-ZW^pB4ۗ8͛Ghj pYĉ:n}H$ߗ@ HgPg4o<\{߻Q=d#mmm5ʕ+kE-[x")SDV,2MM+_*88Bw kb׮]X~= bc˗cСJٳgBxx8̐@fPw>Bŵ033͛7e777G`` N<۷o˾~I8|h߾=\]]s `ٲe(--ҥKΧ% zv퐛y8;ŋq\]]q-#gB$)^{zz:!  ݻ+5&(,,D֭:ǢEp1<;v@VVjjjjf޼y`kk }}}kN:ʁ%qi~133C~~>4W9۷/^ 333BİI億o w'O0k,\|-[T""lذf͂7^yM.]_~%%%k/^x6667 wN,,,py`ԨQ q;gՕ+Wv.H0g|)֭T 333(tK"<<[lk_7|cΝW[XX ##R۷WH*((@6mX͠y-l۶ ֭CHHP^^v0~q^G<~HH yGEqq1<<<~zmPJׯ_Ν;H$unݻ7VXݻǫc{ذaxQDL˾0 ***d/--{k)'OSaau]v%---ڽ{7>}.\HA9YYYQ^^uo/4qΩlѣnݚΟ?v*(( >i˖- {РAԿH"7,a;F|>_.Twܡ9sjݺ5ر***hΝdhhHCv"---8p YYY4j(YfɓÇ^#ΎiرT\\L7ȊƍIWW:uDMM8\q4Hyy9@ X5h˗/дiikkӡC6Bn6iƍlh͛7sϲ+J|rx4ydlyxx( ? Ņ!KKKT\1c.]Jά_572/,_(+F;r;=z8Nŝ޽{4H$"tMMM%SSSbH GS>|-kkkHĉʑ@ȑ#.)((4j|Njժ^OQW^J/͑T*ѣGS>}Ԯ?Esw\Ђ ͝;W[*d\\2UVV+?=ڠq|}}յQ***ޞ1ݿY߫W/bڵkGYYYSޯ#aߟ.\@FFFGPy!!!dbbB7od;;GCzzz{QQQ^ZM R/O5jlNܽ{VXAvvv]d Ð %''8x |Y$m˖-(::(;G]vdeeh@4ܹs.5#G$F-^)&/o.gtI2e 9::ZT.uִs:^~xyf>j۰ax<َs"##|֜5y!SSSgߛJ 4LŋdkkgCBB<&UO<7Ґ!CM6.R޽/WRNNNZZZh޼yT\\"""_MDIRuW_}E<6mvοpŝ#wRk]p:t/^(%ڶm۠ԳgF]ϛ7Ε/nLLLd9#H诿/\\\jxdiiI#F[H{Q3y//#_IWWփuVTTDޤGlW9rjժz] VJ\~6x˗/έ~R:|0?j,ikk#ѹseYXX4$viG%+++СSɑzwKc.]jRw*dffǏ|TWWٳgׯcǎ022>tttЦMim۶䄖-[" HMMu/^K.xk|[pp0INNbbb epvvƐ!CѱQs,^aaaXx1+7:oaa!̙{bкuF`޼yؽ{7ƍjՊX:Q/III҂X,~ky<ƍtu쪪*$''#..~:oH$Zm۶F˖-;v=Gbb"cY;L333X[[k׮-ZѣGxy>:HNNÇqYܺu /^A_۶m1tPar bҤIoX[ƞ={0vXL>X`Ν CCC9UXv-֬Y=zÆ c; 3wB\~tH$WzG L6 usm=zǎCll,`cc~[npttd}Ç@ff&nܸWwAUǟ-VBVEEE.% [pE C6mZ]0qsYml0]/[ڸfd[Ғ r$VB(x\.p󋥋~0_y}gܾn$22@,-{?2u^`mmNcСMhh(}|hll$//T 8www!$${imme֭$''ckkKbb">,'W\wat:VZEBBBx|=^QSSj@@(jaac(=F;wNMJJ2o.YDݿm,[kkzQu͚5ĉUEQ_]xclڪ:99WTWWWuŊj^^=1Z׫_}m65,,l@4h뫮YF---V1ʕ+UN]l_;-P_xu+چV lRmU__ڵK SzL]ڷobmjii通 ^[[n߾]UCݸqѣ3F}Բ2sGw---jFF/&MaСCՙ3goZUUeEKKuVɓ'_Ժ:sG^͛7>>>*9Rݶm,tlZZZOXn555(Btt4K,!<< sGAٳ~V^}UyFat̙3Ivv6ϟ7vE(3AAADGGcb棪*{|G\z???f͚EDD>>>h4d?~t:#66[L*),TU>`ڵhZbccgۛ;-9p'N`Ϟ=\t+V/9~8S\\իWQUUh4xxx?N``駟Fuu5cǎݜ={'Or1)--ڵk5H"""xǰ1g .n ,YBII qqq[777s3NY~=$''SO;-zq6Bzz:_~%:F`ʊq1k,ON@@7m&//)//kkk~_0j(\]]NNN 4[[[>`LNGGGׯ_:hllӧOSSSNҒq?AAA&9 )lڴ^{I&׿';]tkײk.̙om\vy#333g`0???""" ׷ϳwkooӧOj444PWWg\888䄣#Æ cxzz2j(F-#RmHLL %%%ObʕOrrr>>Ϳox㍟t^ɓ撛Kvv6.]BQE`0@PPaaa_l}¢NiooNlllaРA.ݗ_~ITTVVV2vXsGm]ҥKIMMeǎ,]j߿;6)(( ++cǎqu,,,0 Ù6mӦM#00#Gr>!#+ԉ~UTTĬYc:֮]{St:6oLRR.\յ 呗GFF ,--F0~xBCC !((WBY~s1f͚O< Ea͚51Eaii+פsZVQ>s\]]p999dggٳgQF^___BCC O q_={ɓ'3sL{サf<޽ŋjHKK¢xzzH}}}Cb|>aw%]\GGAAA<"%ߎ;Xz5ׯ_gÆ $''nsc777OԩS v݂b.L_?󧊏C|rnj!CPUU%B%]ɓ'!''sn~ܹm.\`ذaLq7 ¤/_N\\qAznmm}S]PZZw0.Lѣs9裏Jtt4TUUQQQ3g+)8%nlܸќхwa2111XXX[??dɒ%\xF3---TWW9uDFFLwa/_ɉCa8w N6m7-]wc!_.L"77FCHH`ٲe<3EQx衇:u*gΜy93+**%11_W̞= ILL4!PUUŪUz=yyywc>|cee?eee}GkkkBBB8r-_'G0l|}}MԪU(**⭷"-- fXvv6?a֭?~LϟoVSSCPP˖-c˖-#(>*...xyyqp dJͶBa R܅I|SN8Ayy9z*^z饛˴:ƕ+Wnm?Սk׮-!N<`mIIItuu~zFşgϟS:;;SYY= ~>g~:Gs[/_0!ĽK0|IIIk###yw(//gܸqʩS;w% ]HWW...lذg}?c1"??pt:]?f)BTT0yuuu駟2c /7{@aVVV,];w(,--{|˶///|@{n<<< 5w!]D0{'O続bӦMSYYɾ}w5AڛC}]kBwa2nnn,_+V`0Vzz:g&)) ooo)//'55'(%!!f-[PSSc3I7B| jmmӓV\i8VEEf8Bwar1 .ȑ#;΀sU8qO/?F|0bs0Iq.EWWԐ#iXB{wqYZZ?*Xj?0:MMMwKz=?MJJ | eee 23f)SƦ3QXXHVVո0{l͛7,!mRŀjIKK#33BZ-֌3OOOϰapttdذa75:]ŋTVVRQQAEEOHH?8&MׅwqǹpŜ8qXkjjts  C A`ooOkk+Bwwwixnnn///7nO^z.^HCC:fcA\{ G W_fi{u/`N1ڊ 4/]I{-hCYv wYq G!6Ut\Μ xEY׶wYɵ j"pkoM 'VDAʆaDtP/Y,?ܰ:n@&(bAdf| 碥h&,sYDNCu?ch:+u!%]+&7kQmix9C$S薬Kܬ>XYzr_on %OnQ{ۢ[ngc٥jPy_ϊƴ|ץ)P8hEϜHOI <}AR ̞_:?Θzꪱ!Sy让?a9*7Zzka7pebE?\ 8h^o[`[g#VX# lp_2}p~Zo~2[&@"%˪eb jvO+~UjutUS7yx=A-c~DC([6JQ{ґ+tVRwR!8j9ktٸi-4}+puLӗ@d/c4`KkGĠm|#+!pKUY;YqMz6MvZ1D| (l~CSaP:;Rqz[-1[Lpbm;v^bVDq7L,s,dsuPz$Pj8EeQZ_Ep*1ko;l~D!%VUZP8hXrY JHchڶD%24i6s35ڳZXn Ο)w/OY$$$HdIȒȒ %/&HIENDB`qtile-0.31.0/docs/_static/widgets/groupbox.png0000664000175000017500000000463214762660347021242 0ustar epsilonepsilonPNG  IHDR*9hsRGBbKGD pHYs  tIME :3(ՙtEXtCommentCreated with GIMPWIDATxILRhlR[BD2r51qv!!acbBc cu@H@(VP J2Җ TP^Ti9;@V(!|@fD* U0JG:x k|.<vQ hZbccCׯ_q:G" e W1MjpTc/Cި(1 FCC@ ʭ.YIbFt#$q7 E 999 11Z?Ek300YB3fl|s>rE~;::p\v[PhtF#j/kS'~}OiEħe,ZԄ4ZmD _֫uzQz:zxPZI!JȨՑ!_BO QrkaSE@ C!|faOѣ|ѣGeUgϦ@<|Bbɒ%;v6)//UPEam6={&|R<ˏN OKKŋdΝKzzz2Re…fsضInJ]]^ZV^ƪ̙3466t6wbl2>|HOOn{`mZx"G˳gXrOOO';;Û7ox-~ɓ'c2RӧDss3nf3'O<]vqE:::ؿ?wʕ+L0!޽{p;wP0n:eeeAk#l6PĨjg޼yPRRjܹsX,OߩT*֮]K}}}1cNWVVի=Ft4>R)y{^&T]WW739s搚JIIIPW^W3ol6YYYꢽO>!B33328C2<<\|}vff&MMMԩS1 rp88Nrssvvz=)))$''HKKjJ&?[D>#C#[<v7nׯ_҂&,5JVK nS[[Kyy9hZ4diy b$PT?~cCtԩ?|7d޿/y>LCشio/?4㉎KĬY0#plg=yݻG,$Y'B?&//`؆ HHHe1} h4l߾?ũNIIa˖-<}N'~ `p-)) ш ^gg'===X,j52i$n,>"]FYY%%%L&씅o޼ҥK)--ڵkdeeqhh,x(**ҥKTTTPRRRdݤSXX<^;wpi\.Cӱ~ߛ`X_| L&Sp_*xHLL ^6޽{'y߸q>?ٳg{aǎ~qq1;wr V+V#^f1V|2:tSNPQQ](++ K~:@cmFee tr擜 /tՔQ FGzzzpÇ?]9V+,Z(dy D1_˯mul̙'YM yjݷ^q~>%Op2#,~q?ǧeaX7CteVIpNG~~~ޯL"b4s"ќ/iM5WS(-*mmm#NE4SqP%Isq%F^!:;;Q#ܑC-IO ?&ԉDBb+_-j!D|8Έ/O_ 9b_ B@_  cN)m IENDB`qtile-0.31.0/docs/_static/widgets/widget_tutorial_wifi.png0000664000175000017500000000116114762660347023613 0ustar epsilonepsilonPNG  IHDRf pHYs+#IDAT8O@gV1Q!CL Kj56 _12{;;ό,A&Dlv!@AItkbXv MdAD6(ĥRF`t6"Eyiy8"x<r]w>O&!1LvEѡr8λ|>iEDlu]:GD5"*M{ZQ=(BVh; C$1qlIIgROgYo2E[RJ)gLY@ `:aXJ)zP}^~ijl(RuǣR=" ,EQ߿\.U,ِZ8|>8v<8hR,QA6iRH86Z9!\dIENDB`qtile-0.31.0/docs/_static/widgets/systray.png0000664000175000017500000000541114762660347021107 0ustar epsilonepsilonPNG  IHDRU(IEsRGBbKGD pHYs  tIME  {RtEXtCommentCreated with GIMPW dIDATh{\u?߽==Gf@^@l!)PƐtuWvݍJ [˂[RKX(A(deY@$,L21d^=3==3}If0GT[w?stbgdAjjj*P+P+P+VZa ܼVXFoܷEOO??bSLCiM3l;3]Rā-&e[aڊB *t6{Vqq~X0"+|+Kgm˗zj3}BQV|{̌]+%VAq.3U \vre/!S7*Aڥmlt1?RTbrr\.4bqk)z(E5R\v[q\)yvEc8&lZ y.l|J냧R 9pa[|##]߱g2޽{eddBpƦkx@!ZR|s;$OlS̋*(~^ܩ4*)%pOI M $sڜԂK3@GWlOңib1RA8&pLӜqۅp 7%᭰?$ިX8 ׃{=G k8I+!k:X@&"U\ц4$C t0,hi+7}x$p8L:x] g*(mZ7zW?q#>5WhNZ. vƊMIxHa:RU .4K e{3.i $@3\tka6aD465311Aoo/|~N4%k ‚ڐ.l(Ib3H"eβK#"=N x3JÔv jhX Ofz8V#\RjݳqvocưLH2Ͽ|q@Z4'N8;Z.ًrU"p}%C(rIG ,+C\4cݯw.BwMH`QP((g^Z_;w=SuIUT9-i֨$!b/6sg4!{ZM˵J`M(.UDXRsGVuyD+ Q__͛Yf 7nD(H$`{ZaA3]?]kkYx1eaY۷o?%з}=Q]LזFT\ ԂR <cYwk{F D_2_& B8@ Q䶕@vhgiC\%|jkd5OӸrfФIYU( @S.MC#`Æ ڵɳӨ\a2sDG`PEd,WdO'Yز_y+ ^m3<'sBaGhOEx@-Mdr\Ȳ|߽$^p}|_~}8˱,mC zrQ\0\1c+ƪURhRR*='<==OU \!X9tʁ yr Y>2e_ݑmzb]A(VJJxCZk,o!h ,;OδX:8/: %l6`A(GWP< 'ǭaEsHɜ?WPddž鼴 0av|NO{!b chZpMh</NH@jд"0ToF0txI”QH_O=Az"& %+hw~#~I!I>WYMx!~uaPiYF@yC8(c[(Y̭]TKab QV~N:ZgZ5W(Lұ,92dўuܤ5ߚ6C0)撡B ! X #YOXHhsg>dn"^@.W 6JJO=Dyjƒɹ7'SOƨ۲܈,QlhhK^wMX6GL%֔)X S0' l{޻"l}R 2sI^z"!x[%?RĐnZ= 0 ʛu3H6 C2 "]pۥe=O$.(έ^d۔ u-oe;sIO⮸ aP0$l%ܿ kb D-D.%2eDd,Jv?W^+1'LHHĩoʛT-9f鄝»|s!s 2P~9~ Ι+.&lEi#o[9"nOI~zD: @CIEعo=,l9\칋1ƪoĬQY3H  !Jd޿cDpUUTc/<jF#Sd,a>^ހrQs4RWJ##Q.BDBL_LvRk|hxՔ=̉X MX س102 <)s("udnc5ғ{_$yJ?6;Aj$lGwὰA5&|HQ~)~׷eHx ep@T@CK/ }]8gOJXd4zBu/anfNA涯`TՂq%&Vށ{!/Kwرݴ9`56a(`+z۲`+X39#} 'Aw:?z! eH )'->B$_AbJ|wRSN`5ud?M\3hWo/ڨp|DA8äyzi0@eo};_:9k$Ş84-Q=Y)! %5a>r! U4BẌLJ"R=x6"zb3 slゔȲ> K~yΙ+HUWgjGXu@ R͏P)C~I2XBvOk<R 9DT6zg)ozQ=܃*vR |ܽ@t{@`ԕ,2/ / "/盄-ǖN?ĊIZ-@$Hy"öq;a-}`ϫ{wc,1e&֔0j&aTTD OFNA>hkAdU㤯̨rm$Hc:%[#{G1!-('zFĊp\Cb? #]E/P[ !GTʓx;2D=-<\BfEJ_;)^i)U_!fd߹#Q՟4#l{Ci^M˰O[doDر !q#d (D@JPZ/eS^J!g)m~ғ۲kbq|?֌yVO/!X&a7-Q-7$2OTGr}!VKw\DOcNGzBnJ{o0 yXq5BG 4g_L7)E -)?k{5c.[HP;8|ʞY*bd*\,l9.?G=[2qs!Vcw[q1-HW(#ĈJ `G0~5gVwo9D}(Β 3 n=$?wS~z5?MbZSWuww)Iө.RsSkC%v"'`Z,"}os6ebTMQSQ[pY5'0.T*n=RJ$ؽ s<"-!-^5VB<>Z˯T-%2P3qT0.`-s7=NUHW"Yisp^Q[9r%[aD]T"UL#)DBS.!R\u A dqr R];[t"SIa͜xfq"pNbY#K|f`MYp|?Q>ƾ*b"h;2V|泰/VrDIԱ_9AX"]~5B@J*jH%\jKO=DGU{ ?o ^ c@ W&ܯr~ct)2tVHgs\0dEҺ,+oY ,UZJ/Vp.a/8G,UMVo$lۇ,($yc͘pA" Y>Ž(-oz OCma]=,̺CAlx=||`+ִ5uJXrzG!H_{'΢!v ec7%.8ަ')dV|27}pHW4$Ò^O=?p9q֬fY 5D]턭Zv_jl=jd#+Ia@kD݈D#Sp8DJue!i$/EbTT+(ƶƕϵRZ]d5 =Tg' JT 2 …c!):) )m#ޫ'!jhƚ9 n:"HebNIU-Q8֔X=fbύ g;9D:$߾QtFXOÜX1i*阓bN(Y*!0AO#,K9<Jկ۽ nT*@T ѐ3RK#R,}&)YދOQ~aXel=Beۉ3{T[4Q@H1ȀrH8žN= vg[_8vjE,,Szxl7"iuuv#Ko&D!n*V}#+pV`#B=šޢ%UAO,Xp⒰<,P ϵD#Km$Pp @*VX;,y~+>*'aRq_ыGr}ʎc"q/ǚ٤lp -f|bXAga>6Gͥ#څa>̉TeːWiŷ"Ga֙ q~` !mi֓!롼WG ê5?vR%5zĂ)K73"s'>[mOש N#Jx|&1aNhYv1UBرj9'ԆpߔGJ@8,J, (s4\b#%zWF̝0-7|i dcTP|ӫ1'%+@Id4meܹ9q_ |G40N*H uQoJN;7bN8J .a)Ub7 !zm$>Uptǐ7_ـڋH|^"ݴnRd1'z/ ;TK'~vrn= u+oAag+'~}oX:VV5gEC࿼K΢$?t#FEQpXL]2ѭ;}wt)~s>9iZD2Z\Ra-;~is0ƞ>cb=7)Yc"$VT٧cN:DM [Cǐ\Up8gSަpo@6sŪr"tڕ^uC 7 n#FTT"5 \_7VQ:퐹X33Ta^zY̝}$XxqXj{ bCR!"%QW;۲kr.jhRT+Vr5]M_jRZz"aY8K/$"b<W Ҍo֬"?8O,ŸZ'rĩM=pnѮRIq0A@br*E}\/a_7"*بU4MH4m%m ]o O$m{ 7m&1I4hi\QQ`PD\aea<̰0|gyLYʷ~!!UqPM} mFb3&̠D}5~XyݛR%]||V]m,_EAœRgN|GUѥK>F EwFɏk=ItXj `y ܦMurEG22yr-7Zb͝<& I iK,W|CV@: PzF +铚O>$#"(qr"½U.=E5tuK%jw9n뫗|9-// OWoq"nELpzp>M 6/ n$e>qFv\ Gh)4/j( aXuZ a}k~tez{tOj(Cs:Y78z"͟2O쯢w,G X}RPTje=U~rXv&NC}_rJNpt1_jpʻPl7e/|w_rJO)u攙zZc9 `yi孔Kgu k@zxdއF5P--"x#ƪ>^mRt~vM0)X`KTn3VPxAI7lƷ^v'@6TE.ǁDEr6=N))ԫo.^kDG9ashM⚘EerF0XH7 @ )t #۠ۼ3 xú9k_9XUaiE jhm2ScAAݟ-z >lܴw2\$fVf [nбHz~"M3Vش0?z^+S#2dPRde%j( lEXUm[ۄ+Syjў2~x3NL}99 dH3o5]2~6@evT0u6PP1X֔[L ƺzC8j1M#{V6.OT˙d*=QJK3(kUV6M Fw'-jf*O0UWѩSô{'=[wPI*>\L9׃FQy[5hp;PG!Ѵ bm:K+ 6ִiy4;q;8zTvy-.o}dqyltSCjW_{㚨+p<c' JdB"Rft8IC_jJKVFxhz;{9{5fm2XAAA\,UL m@Nӥ|b *FO+njAy*Sz^zSi:m!x-Z4O+?uP q3 H5 _ơ7v*|UѳE239`UALc]e9ŐXɃAX~}G +V/fnasݓgm.%b`) _ r[ ͫTYWI8v~A Sy&DN`ƪob5ITfWs(b+눩MIz**l{O/wШ *,R焵d0fdQA T֧NO=f4wP%[boYt2}E<,RU".@o|=eY1h7 xky|asàr}*w ~$<`lu{KF - TS-gmJB 瓢NjNMci^<>TJc֜9<%ƒjc+"@X1+iP-z%u^Qlq"f5h}5Lcq'x/+/3}Ǚn(I.H*XT&tY]S0JL4s>T`μ_nDA.4_pj@5>v"XlJR3=1H;r+ ӠrXMg0 OqpYMEUYID9r,!>1,]3z՚rX+¯ltc:v,.w9<[$eťHNk”71XH >Sy2 ! RݐXAfoKws97Lڰs}KVVwD1DASyG{ktARy99(Vgq_;g, Q~T`ᓆ;#KT]"u7l҇$}Ț EL{F6cur*Ӊ} T+!>n:Xr}Y+8w;UYkNT`{PuX0EZ8yߊ+)WX~ܘ' Т@v23([_*L*Uh-\'A3ufF?A{r>Y[~ *wU[UkCK~N,e'Q7 r%}>ŭ5v尐nk_Qcciuph􋏏Z!;< ڔsRSܘ19 'Kjwex=j88_*wc, ! E?Wn\q^-?*exX2 V@0' ް1aVQ}5Mޯݟ\!`$>s}|x3<:7e]bBc(Ȁa*O0VhDa IdiA UNvnA҂`ϓ-]lQDA1tiyl3րa*O0V]mqt״h4~x\-S2E[Tgrו"w~U.^ TƸ|+ Z] *$=r\OS&K{3Z (kvlZ>sO 8)W8.|b#hv'4)8!L9CdWkOBuA˗kT ɉn8Pp@n^* HHκXa}LyBG-5`A*~s~y҃v`ymuzЎEe P'K=y.͗LΜrg)&,<܍AZva\$ qz@a8e$NZF3Ryݵw@|ip4.ħTR Yt`ٜ E! RNB"EVڼ5j㗎sr*QA*we:n2b0~R<}9R,0/xٔ&2Kqz^h8HV9K )|]bWM}(픎y䱴bٯesU #iq0-dj0؝EErی/ JPnX, R F1ERm@CϧRb h-@i܇(ӗ -" RPK A[4X}J8y}U ^TQe>T1'c,_m7YE琎H&v+  j9s6{lz q|s#IaP3'+2&#+;^d3iNC+IQ hJk-1ؾBך) Wxԡo\(2qqquﮅm9PYQ)i[NT- "@!EFG`JN)q{ȽyCbQU4sL^ p@ z X*h>~:!:2P2`Ƀ0O(lX -Rk6,Z!TpP+5uVtoB^DA0 9X{`-徟P,fB醳2ë{?AEqf3v]F0 v1b,eAʢ $1j͹?;6pWZrN*1pwɹ..R%l`Q״cT^`sEGcofS7%l4TirȩeQ:#'IH*夣uKhu q+bc Py X_ I`*Ș}qWtDiwu]QIG!OPP@PB!ȅ# Q=nˮ0\Ί~A ]h# \ Λُ7R|xRGMNjoqPBj1̢ hg%_Vro6/3VfX :!0hVB(D<ĶWlqK {臹Bךd&+{9,A@qPuw1qzL}1X:gSQS:ghaBz`hkcqg))C=P I5DGbU#zc 2``LnٱWFOf .$*E`l; էPA<]Q*VhnC,֎ս=КJjMVrVEBBG"_!SFC9i+ [t)x\!DA~ߐ}D.fAZ|[9(=J0f 1 wȁC!Y(rQEASc]9{%t w`O kyP|s;N_XDNB;VsgY iӲLL|wL xǾg\aAwj}*V_*6X6dS yHI] G6a8& T`X?=)Eee`$j1" Z!-L5@ ս^ 6cM6ـ5X6qT<S, rQ?a'+1c@0012*ۿ0z%Jۙw(Ư6TN!#B;S cA9YkX&J Y000VhDaՌUtt*aK?]S:ȸ['Wxdۇ&!,r&Jk씱B;V߹}gQBX[ͫOS s zfĤOk}"xo3 cm[Ϸn qtile-0.31.0/docs/sphinx_qtile.py0000664000175000017500000000555614762660347016662 0ustar epsilonepsilon# Copyright (c) 2015 dmpayton # Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import os from subprocess import CalledProcessError, run from qtile_docs.collapsible import ( CollapsibleNode, CollapsibleSection, depart_collapsible_node, visit_collapsible_node, ) from qtile_docs.commands import QtileCommands from qtile_docs.graph import QtileGraph from qtile_docs.hooks import QtileHooks from qtile_docs.migrations import QtileMigrations from qtile_docs.module import QtileModule from qtile_docs.qtile_class import QtileClass def generate_keybinding_images(): this_dir = os.path.dirname(__file__) base_dir = os.path.abspath(os.path.join(this_dir, "..")) run(["make", "-C", base_dir, "run-ffibuild"]) run(["make", "-C", this_dir, "genkeyimg"]) def generate_widget_screenshots(): this_dir = os.path.dirname(__file__) try: run(["make", "-C", this_dir, "genwidgetscreenshots"], check=True) except CalledProcessError: raise Exception("Widget screenshots failed to build.") def setup(app): generate_keybinding_images() # screenshots will be skipped unless QTILE_BUILD_SCREENSHOTS environment variable is set # Variable is set for ReadTheDocs at https://readthedocs.org/dashboard/qtile/environmentvariables/ if os.getenv("QTILE_BUILD_SCREENSHOTS", False): generate_widget_screenshots() else: print("Skipping screenshot builds...") app.add_directive("qtile_class", QtileClass) app.add_directive("qtile_hooks", QtileHooks) app.add_directive("qtile_module", QtileModule) app.add_directive("qtile_commands", QtileCommands) app.add_directive("qtile_graph", QtileGraph) app.add_directive("qtile_migrations", QtileMigrations) app.add_directive("collapsible", CollapsibleSection) app.add_node(CollapsibleNode, html=(visit_collapsible_node, depart_collapsible_node)) qtile-0.31.0/docs/manual/0000775000175000017500000000000014762660347015043 5ustar epsilonepsilonqtile-0.31.0/docs/manual/commands/0000775000175000017500000000000014762660347016644 5ustar epsilonepsilonqtile-0.31.0/docs/manual/commands/navigation.rst0000664000175000017500000000311114762660347021531 0ustar epsilonepsilon.. _object_graph_selectors: Navigating the command graph ============================ As noted previously, some objects require a selector to ensure that the correct object is selected, while other nodes provide a default object without a selector. The table below shows what selectors are required for the diferent nodes and whether the selector is optional (i.e. if it can be omitted to select the default object). .. list-table:: :widths: 15 30 15 40 :header-rows: 1 * - Object - Key - Optional? - Example * - :doc:`bar ` - | "top", "bottom" | (Note: if accessing this node from the root, users on multi-monitor setups may wish to navigate via a ``screen`` node to ensure that they select the correct object.) - No - | c.screen.bar["bottom"] * - :doc:`group ` - Name string - Yes - | c.group["one"] | c.group * - :doc:`layout ` - Integer index - Yes - | c.layout[2] | c.layout * - :doc:`screen ` - Integer index - Yes - | c.screen[1] | c.screen * - :doc:`widget ` - | Widget name | (This is usually the name of the widget class in lower case but can be set by passing the ``name`` parameter to the widget.) - No - | c.widget["textbox"] * - :doc:`window ` - Integer window ID - Yes - | c.window[123456] | c.window * - :doc:`core ` - No - n/a - | c.core qtile-0.31.0/docs/manual/commands/command_graph.rst0000664000175000017500000000750514762660347022204 0ustar epsilonepsilon The Command Graph ================= The objects in Qtile's command graph come in eight flavours, matching the eight basic components of the window manager: ``layouts``, ``windows``, ``groups``, ``bars``, ``widgets``, ``screens``, ``core``, and a special ``root`` node. Objects are addressed by a path specification that starts at the root and follows the available paths in the graph. This is what the graph looks like: .. qtile_graph:: :root: all :api_page_root: api/ Each arrow can be read as "holds a reference to". So, we can see that a ``widget`` object *holds a reference to* objects of type ``bar``, ``screen`` and ``group``. Let's start with some simple examples of how the addressing works. Which particular objects we hold reference to depends on the context - for instance, widgets hold a reference to the screen that they appear on, and the bar they are attached to. Let's look at an example, starting at the root node. The following script runs the ``status`` command on the root node, which, in this case, is represented by the ``InteractiveCommandClient`` object: .. code-block:: python from libqtile.command.client import InteractiveCommandClient c = InteractiveCommandClient() print(c.status()) The ``InteractiveCommandClient`` is a class that allows us to traverse the command graph using attributes to select child nodes or commands. In this example, we have resolved the ``status()`` command on the root object. The interactive command client will automatically find and connect to a running Qtile instance, and which it will use to dispatch the call and print out the return. An alternative is to use the ``CommandClient``, which allows for a more precise resolution of command graph objects, but is not as easy to interact with from a REPL: .. code-block:: python from libqtile.command.client import CommandClient c = CommandClient() print(c.call("status")()) Like the interactive client, the command client will automatically connect to a running Qtile instance. Here, we first resolve the ``status()`` command with the ``.call("status")``, which simply located the function, then we can invoke the call with no arguments. For the rest of this example, we will use the interactive command client. From the graph, we can see that the root node holds a reference to ``group`` nodes. We can access the "info" command on the current group like so: .. code-block:: python c.group.info() To access a specific group, regardless of whether or not it is current, we use the Python mapping lookup syntax. This command sends group "b" to screen 1 (by the :meth:`libqtile.config.Group.toscreen` method): .. code-block:: python c.group["b"].toscreen(1) In different contexts, it is possible to access a default object, where in other contexts a key is required. From the root of the graph, the current ``group``, ``layout``, ``screen`` and ``window`` can be accessed by simply leaving the key specifier out. The key specifier is mandatory for ``widget`` and ``bar`` nodes. With this context, we can now drill down deeper in the graph, following the edges in the graphic above. To access the screen currently displaying group "b", we can do this: .. code-block:: python c.group["b"].screen.info() Be aware, however, that group "b" might not currently be displayed. In that case, it has no associated screen, the path resolves to a non-existent node, and we get an exception: .. code-block:: python libqtile.command.CommandError: No object screen in path 'group['b'].screen' The graph is not a tree, since it can contain cycles. This path (redundantly) specifies the group belonging to the screen that belongs to group "b": .. code-block:: python c.group["b"].screen.group This amount of connectivity makes it easy to reach out from a given object when callbacks and events fire on that object to related objects. qtile-0.31.0/docs/manual/commands/advanced.rst0000664000175000017500000002145314762660347021150 0ustar epsilonepsilon========================= Command graph development ========================= This page provides further detail on how Qtile's command graph works. If you just want to script your Qtile window manager the :doc:`earlier information `, in addition to the documentation on the :doc:`available commands ` should be enough to get started. To develop the Qtile manager itself, we can dig into how Qtile represents these objects, which will lead to the way the commands are dispatched. Client-Server Scripting Model ============================= Qtile has a client-server control model - the main Qtile instance listens on a named pipe, over which marshalled command calls and response data is passed. This allows Qtile to be controlled fully from external scripts. Remote interaction occurs through an instance of the ``libqtile.command.interface.IPCCommandInterface`` class. This class establishes a connection to the currently running instance of Qtile. A ``libqtile.command.client.InteractiveCommandClient`` can use this connection to dispatch commands to the running instance. Commands then appear as methods with the appropriate signature on the ``InteractiveCommandClient`` object. The object hierarchy is described in the :ref:`commands-api` section of this manual. Full command documentation is available through the :ref:`Qtile Shell `. Digging Deeper: Command Objects =============================== All of the configured objects setup by Qtile are ``CommandObject`` subclasses. These objects are so named because we can issue commands against them using the command scripting API. Looking through the code, the commands that are exposed are commands that are decorated with the ``@expose_command()`` decorator. When writing custom layouts, widgets, or any other object, you can add your own custom functions and, once you add the decorator, they will be callable using the standard command infrastructure. An available command can be extracted by calling ``.command()`` with the name of the command. In addition to having a set of associated commands, each command object also has a collection of items associated with it. This is what forms the graph that is shown above. For a given object type, the ``items()`` method returns all of the names of the associated objects of that type and whether or not there is a defaultable value. For example, from the root, ``.items("group")`` returns the name of all of the groups and that there is a default value, the currently focused group. To navigate from one command object to the next, the ``.select()`` method is used. This method resolves a requested object from the command graph by iteratively selecting objects. A selector like ``[("group", "b"), ("screen", None)]`` would be to first resolve group "b", then the screen associated to the group. The Command Graph ================= In order to help in specifying command objects, there is the abstract command graph structure. The command graph structure allows us to address any valid command object and issue any command against it without needing to have any Qtile instance running or have anything to resolve the objects to. This is particularly useful when constructing lazy calls, where the Qtile instance does not exist to specify the path that will be resolved when the command is executed. The only limitation of traversing the command graph is that it must follow the allowed edges specified in the first section above. Every object in the command graph is represented by a ``CommandGraphNode``. Any call can be resolved from a given node. In addition, each node knows about all of the children objects that can be reached from it and have the ability to ``.navigate()`` to the other nodes in the command graph. Each of the object types are represented as ``CommandGraphObject`` types and the root node of the graph, the ``CommandGraphRoot`` represents the Qtile instance. When a call is performed on an object, it returns a ``CommandGraphCall``. Each call will know its own name as well as be able to resolve the path through the command graph to be able to find itself. Note that the command graph itself can standalone, there is no other functionality within Qtile that it relies on. While we could have started here and built up, it is helpful to understand the objects that the graph is meant to represent, as the graph is just a representation of a traversal of the real objects in a running Qtile window manager. In order to tie the running Qtile instance to the abstract command graph, we move on to the command interface. .. _command-interface: Executing graph commands: Command Interface =========================================== The ``CommandInterface`` is what lets us take an abstract call on the command graph and resolve it against a running command object. Put another way, this is what takes the graph traversal ``.group["b"].screen.info()`` and executes the ``info()`` command against the addressed ``screen`` object. Additional functionality can be used to check that a given traversal resolves to actual objcets and that the requested command actually exists. Note that by construction of the command graph, the traversals here must be feasible, even if they cannot be resolved for a given configuration state. For example, it is possible to check the screen assoctiated to a group, even though the group may not be on a screen, but it is not possible to check the widget associated to a group. The simplest form of the command interface is the ``QtileCommandInterface``, which can take an in-process ``Qtile`` instance as the root ``CommandObject`` and execute requested commands. This is typically how we run the unit tests for Qtile. The other primary example of this is the ``IPCCommandInterface`` which is able to then route all calls through an IPC client connected to a running Qtile instance. In this case, the command graph call can be constructed on the client side without having to dispatch to Qtile and once the call is constructed and deemed valid, the call can be executed. In both of these cases, executing a command on a command interface will return the result of executing the command on a running Qtile instance. To support lazy execution, the ``LazyCommandInterface`` instead returns a ``LazyCall`` which is able to be resolved later by the running Qtile instance when it is configured to fire. Tying it together: Command Client ================================= So far, we have our running Command Objects and the Command Interface to dispatch commands against these objects as well as the Command Graph structure itself which encodes how to traverse the connections between the objects. The final component which ties everything together is the Command Client, which allows us to navigate through the graph to resolve objects, find their associated commands, and execute the commands against the held command interface. The idea of the command client is that it is created with a reference into the command graph and a command interface. All navigation can be done against the command graph, and traversal is done by creating a new command client starting from the new node. When a command is executed against a node, that command is dispatched to the held command interface. The key decision here is how to perform the traversal. The command client exists in two different flavors: the standard ``CommandClient`` which is useful for handling more programatic traversal of the graph, calling methods to traverse the graph, and the ``InteractiveCommandClient`` which behaves more like a standard Python object, traversing by accessing properties and performing key lookups. Returning to our examples above, we now have the full context to see what is going on when we call: .. code-block:: python from libqtile.command.client import CommandClient c = CommandClient() print(c.call("status")()) from libqtile.command.client import InteractiveCommandClient c = InteractiveCommandClient() print(c.status()) In both cases, the command clients are constructed with the default command interface, which sets up an IPC connection to the running Qtile instance, and starts the client at the graph root. When we call ``c.call("status")`` or ``c.status``, we navigate the command client to the ``status`` command on the root graph object. When these are invoked, the commands graph calls are dispatched via the IPC command interface and the results then sent back and printed on the local command line. The power that can be realized by separating out the traversal and resolution of objects in the command graph from actually invoking or looking up any objects within the graph can be seen in the ``lazy`` module. By creating a lazy evaluated command client, we can expose the graph traversal and object resolution functionality via the same ``InteractiveCommandClient`` that is used to perform live command execution in the Qtile prompt. qtile-0.31.0/docs/manual/commands/api/0000775000175000017500000000000014762660347017415 5ustar epsilonepsilonqtile-0.31.0/docs/manual/commands/api/backend.rst0000664000175000017500000000144614762660347021543 0ustar epsilonepsilonBackend core objects ==================== The backend core is the link between the Qtile objects (windows, layouts, groups etc.) and the specific backend (X11 or Wayland). This core should be largely invisible to users and, as a result, these objects do not expose many commands. Nevertheless, both backends do contain important commands, notably ``set_keymap`` on X11 and ``change_vt`` used to change to a different TTY on Wayland. The backend core has no access to other nodes on the command graph. .. qtile_graph:: :root: core | X11 backend ----------- .. qtile_commands:: libqtile.backend.x11.core :object-node: core :no-title: .. _wayland_backend_commands: Wayland backend --------------- .. qtile_commands:: libqtile.backend.wayland.core :object-node: core :no-title: qtile-0.31.0/docs/manual/commands/api/groups.rst0000664000175000017500000000077614762660347021500 0ustar epsilonepsilonGroup objects ============= Groups are Qtile's workspaces. Groups are not responsible for the positioning of windows (that is handled by the :doc:`layouts `) so the available commands are somewhat more limited in scope. Groups have access to the layouts in that group, the windows in the group and the screen displaying the group. .. qtile_graph:: :root: group | .. qtile_commands:: libqtile.group :baseclass: libqtile.group._Group :includebase: :object-node: group :no-title:qtile-0.31.0/docs/manual/commands/api/screens.rst0000664000175000017500000000070214762660347021610 0ustar epsilonepsilonScreen objects ============== Screens are the display area that holds bars and an active group. Screen commands include changing the current group and changing the wallpaper. Screens can access objects displayed on that screen e.g. bar, widgets, groups, layouts and windows. .. qtile_graph:: :root: screen | .. qtile_commands:: libqtile.config :baseclass: libqtile.config.Screen :includebase: :object-node: screen :no-title: qtile-0.31.0/docs/manual/commands/api/bars.rst0000664000175000017500000000106614762660347021101 0ustar epsilonepsilonBar objects =========== The bar is primarily used to display widgets on the screen. As a result, the bar does not need many of its own commands. To select a bar on the command graph, you must use a selector (as there is no default bar). The selector is the position of the bar on the screen i.e. "top", "bottom", "left" or "right". The bar can access the screen it's on and the widgets it contains via the command graph. .. qtile_graph:: :root: bar .. qtile_commands:: libqtile.bar :object-node: bar :object-selector-string: position :no-title: qtile-0.31.0/docs/manual/commands/api/widgets.rst0000664000175000017500000000073414762660347021621 0ustar epsilonepsilonWidget objects ============== Widgets are small scripts that are used to provide content or add functionality to the bar. Some widgets will expose commands in order for functionality to be triggered indirectly (e.g. via a keypress). Widgets can access the parent bar and screen via the command graph. .. qtile_graph:: :root: widget | .. qtile_commands:: libqtile.widget :baseclass: libqtile.widget.base._Widget :object-node: widget :object-selector-name: qtile-0.31.0/docs/manual/commands/api/root.rst0000664000175000017500000000104514762660347021132 0ustar epsilonepsilonQtile root object ================= The root node represents the main Qtile manager instance. Many of the commands on this node are therefore related to the running of the application itself. The root can access every other node in the command graph. Certain objects can be accessed without a selector resulting in the current object being selected (e.g. current group, screen, layout, window). .. qtile_graph:: :root: root | .. qtile_commands:: libqtile.core.manager :baseclass: libqtile.core.manager.Qtile :includebase: :no-title: qtile-0.31.0/docs/manual/commands/api/layouts.rst0000664000175000017500000000070514762660347021651 0ustar epsilonepsilonLayout objects ============== Layouts position windows according to their specific rules. Layout commands typically include moving windows around the layout and changing the size of windows. Layouts can access the windows being displayed, the group holding the layout and the screen displaying the layout. .. qtile_graph:: :root: layout | .. qtile_commands:: libqtile.layout :baseclass: libqtile.layout.base.Layout :object-node: layout qtile-0.31.0/docs/manual/commands/api/windows.rst0000664000175000017500000000077514762660347021652 0ustar epsilonepsilonWindow objects ============== The size and position of windows is determined by the current layout. Nevertheless, windows can still change their appearance in multiple ways (toggling floating state, fullscreen, opacity). Windows can access objects relevant to the display of the window (i.e. the screen, group and layout). .. qtile_graph:: :root: window | .. qtile_commands:: libqtile.backend.base :baseclass: libqtile.backend.base.Window :object-node: window :includebase: :no-title:qtile-0.31.0/docs/manual/commands/api/index.rst0000664000175000017500000000075114762660347021261 0ustar epsilonepsilon============ Commands API ============ The following pages list all the commands that are exposed by Qtile's command graph. As a result, all of these commands are accessible by any of the various interfaces provided by Qtile (e.g. the ``lazy`` interface for keybindings and mouse callbacks). .. toctree:: :maxdepth: 1 Qtile root Layouts Windows Groups Bars Widgets Screens Core qtile-0.31.0/docs/manual/commands/interfaces.rst0000664000175000017500000001341214762660347021522 0ustar epsilonepsilon.. _scripting-interfaces: ========== Interfaces ========== Introduction ============ This page provides an overview of the various interfaces available to interact with Qtile's command graph. * ``lazy`` calls * when running ``qtile shell`` * when running ``qtile cmd-obj`` * when using ``CommandClient`` or ``InteractiveCommandClient`` in python The way that these commands are called varies depending on which option you select. However, all interfaces follow the same, basic approach: navigate to the desired object and then execute a command on that object. The following examples illustrate this principle by showing how the same command can be accessed by the various interfaces: .. code:: bash Lazy call: lazy.widget["volume"].increase_volume() qtile shell: > cd widget/volume widget[volume] > increase_volume() qtile cmd-obj: qtile cmd-obj -o widget volume -f increase_volume CommandClient: >>> from libqtile.command.client import CommandClient >>> c = CommandClient() >>> c.navigate("widget", "volume").call("increase_volume") InteractiveCommandClient: >>> from libqtile.command.client import InteractiveCommandClient >>> c = InteractiveCommandClient() >>> c.widget["volume"].increase_volume() The Interfaces ============== From the examples above, you can see that there are five main interfaces which can be used to interact with Qtile's command graph. Which one you choose will depend on how you intend to use it as each interface is suited to different scenarios. * The ``lazy`` interface is used in config scripts to bind commands to keys and mouse callbacks. * The ``qtile shell`` is a tool for exploring the graph by presenting it as a file structure. It is not designed to be used for scripting. * For users creating shell scripts, the ``qtile cmd-obj`` interface would be the recommended choice. * For users wanting to control Qtile from a python script, there are two available interfaces ``libqtile.command.client.CommandClient`` and ``libqtile.command.client.InteractiveCommandClient``. Users are advised to use the ``InteractiveCommandClient`` as this simplifies the syntax for navigating the graph and calling commands. .. _interface-lazy: The Lazy interface ~~~~~~~~~~~~~~~~~~ The :data:`lazy.lazy` object is a special helper object to specify a command for later execution. Lazy objects are typically users' first exposure to Qtile's command graph but they may not realise it. However, understanding this will help users when they try using some of the other interfaces listed on this page. The basic syntax for a lazy command is: .. code:: python lazy.node[selector].command(arguments) No node is required when accessing commands on the root node. In addition, multiple nodes can be sequenced if required to navigate to a specific object. For example, bind a key that would focus the next window on the active group on screen 2, you would create a lazy object as follows: .. code:: python lazy.screen[1].group.next_window() .. note:: As noted above, ``lazy`` calls do not call the relevant command but only create a reference to it. While this makes it ideal for binding commands to key presses and ``mouse_callbacks`` for widgets, it also means that ``lazy`` calls cannot be included in user-defined functions. qtile shell ~~~~~~~~~~~ The qtile shell maps the command graph to a virtual filesystem that can be navigated in a similar way. While it is unlikely to be used for scripting, the ``qtile shell`` interface provides an excellent means for users to navigate and familiarise themselves with the command graph. For more information, please refer to :doc:`/manual/commands/shell/qtile-shell` qtile cmd-obj ~~~~~~~~~~~~~ ``qtile cmd-obj`` is a command line interface for executing commands on the command graph. It can be used as a standalone command (e.g. executed directly from the terminal) or incorporated into shell scripts. For more information, please refer to :doc:`/manual/commands/shell/qtile-cmd` CommandClient ~~~~~~~~~~~~~ The ``CommandClient`` interface is a low-level python interface for accessing and navigating the command graph. The low-level nature means that navigation steps must be called explicitly, rather than being inferred from the body of the calling command. For example: .. code:: python from libqtile.command.client import CommandClient c = CommandClient() # Call info command on clock widget info = c.navigate("widget", "clock").call("info") # Call info command on the screen displaying the clock widget info = c.navigate("widget", "clock").navigate("screen", None).call("info") Note from the last example that each navigation step must be called separately. The arguments passed to ``navigate()`` are ``node`` and ``selector``. ``selector`` is ``None`` when you wish to access the default object on that node (e.g. the current screen). More technical explanation about the python command clients can be found at :ref:`command-interface`. InteractiveCommandClient ~~~~~~~~~~~~~~~~~~~~~~~~ The ``InteractiveCommandClient`` is likely to be the more popular interface for users wishing to access the command graph via external python scripts. One of the key differences between the ``InteractiveCommandClient`` and the above ``CommandClient`` is that the ``InteractiveCommandClient`` removes the need to call ``navigate`` and ``call`` explicitly. Instead, the syntax mimics that of the ``lazy`` interface. For example, to call the same commands in the above example: .. code:: python from libqtile.command.client import InteractiveCommandClient c = InteractiveCommandClient() # Call info command on clock widget info = c.widget["clock"].info() # Call info command on the screen displaying the clock widget info = c.widget["clock"].screen.info() qtile-0.31.0/docs/manual/commands/keybindings.rst0000664000175000017500000000236514762660347021712 0ustar epsilonepsilon.. _keybinding-img: ===================== Keybindings in images ===================== Default configuration ===================== .. don't delete LS_PNG and END_LS_PNG (it is used for `make genkeyimg`) .. LS_PNG .. image:: /_static/keybindings/mod4.png .. image:: /_static/keybindings/mod4-shift.png .. image:: /_static/keybindings/mod4-control.png .. END_LS_PNG Generate your own images ======================== Qtile provides a tiny helper script to generate keybindings images from a config file. In the repository, the script is located under ``scripts/gen-keybinding-img``. This script accepts a configuration file and an output directory. If no argument is given, the default configuration will be used and files will be placed in same directory where the command has been run. :: usage: gen-keybinding-img [-h] [-c CONFIGFILE] [-o OUTPUT_DIR] Qtile keybindings image generator optional arguments: -h, --help show this help message and exit -c CONFIGFILE, --config CONFIGFILE use specified configuration file. If no presented default will be used -o OUTPUT_DIR, --output-dir OUTPUT_DIR set directory to export all images to qtile-0.31.0/docs/manual/commands/shell/0000775000175000017500000000000014762660347017753 5ustar epsilonepsilonqtile-0.31.0/docs/manual/commands/shell/qtile-shell.rst0000664000175000017500000000564514762660347022742 0ustar epsilonepsilon.. _qtile-shell: =========== qtile shell =========== The Qtile command shell is a command-line shell interface that provides access to the full complement of Qtile command functions. The shell features command name completion, and full command documentation can be accessed from the shell itself. The shell uses GNU Readline when it's available, so the interface can be configured to, for example, obey VI keybindings with an appropriate ``.inputrc`` file. See the GNU Readline documentation for more information. Navigating the Object Graph =========================== The shell presents a filesystem-like interface to the command graph - the builtin "cd" and "ls" commands act like their familiar shell counterparts: .. code-block:: bash > ls layout/ widget/ screen/ bar/ window/ group/ > cd screen layout/ window/ bar/ widget/ > cd .. / > ls layout/ widget/ screen/ bar/ window/ group/ If you try to access an object that has no "default" value then you will see an error message: .. code-block:: bash > ls layout/ widget/ screen/ bar/ window/ group/ > cd bar Item required for bar > ls bar bar[bottom]/ > cd bar/bottom bar['bottom']> ls screen/ widget/ Please refer to :ref:`object_graph_selectors` for a summary of which objects need a specified selector and the type of selector required. Using ``ls`` will show which selectors are available for an object. Please see below for an explanation about how Qtile displays shell paths. Alternatively, the ``items()`` command can be run on the parent object to show which selectors are available. The first value shows whether a selector is optional (``False`` means that a selector is required) and the second value is a list of selectors: .. code-block:: bash > ls layout/ widget/ screen/ bar/ window/ group/ > items(bar) (False, ['bottom']) Displaying the shell path ========================= Note that the shell provides a "short-hand" for specifying node keys (as opposed to children). The following is a valid shell path: .. code-block:: bash > cd group/4/window/31457314 The command prompt will, however, always display the Python node path that should be used in scripts and key bindings: .. code-block:: bash group['4'].window[31457314]> Live Documentation ================== The shell ``help`` command provides the canonical documentation for the Qtile API: .. code-block:: bash > cd layout/1 layout[1]> help help command -- Help for a specific command. Builtins ======== cd exit help ls q quit Commands for this object ======================== add commands current delete doc down get_info items next previous rotate shuffle_down shuffle_up toggle_split up layout[1]> help previous previous() Focus previous stack. qtile-0.31.0/docs/manual/commands/shell/qtile-run.rst0000664000175000017500000000050014762660347022420 0ustar epsilonepsilon============= qtile run-cmd ============= Run a command applying rules to the new windows, ie, you can start a window in a specific group, make it floating, intrusive, etc. The Windows must have NET_WM_PID. .. code-block:: bash # run xterm floating on group "test-group" qtile run-cmd -g test-group -f xterm qtile-0.31.0/docs/manual/commands/shell/iqshell.rst0000664000175000017500000000453714762660347022157 0ustar epsilonepsilon.. _iqshell: ======= iqshell ======= In addition to the standard ``qtile shell`` shell interface, we provide a kernel capable of running through Jupyter that hooks into the qshell client. The command structure and syntax is the same as qshell, so it is recommended you read that for more information about that. Dependencies ============ In order to run iqshell, you must have `ipykernel`_ and `jupyter_console`_. You can install the dependencies when you are installing qtile by running: .. code-block:: bash $ pip install qtile[ipython] Otherwise, you can just install these two packages separately, either through PyPI or through your distribution package manager. .. _ipykernel: https://pypi.python.org/pypi/ipykernel .. _jupyter_console: https://pypi.python.org/pypi/jupyter_console Installing and Running the Kernel ================================= Once you have the required dependencies, you can run the kernel right away by running: .. code-block:: bash $ python3 -m libqtile.interactive.iqshell_kernel However, this will merely spawn a kernel instance, you will have to run a separate frontend that connects to this kernel. A more convenient way to run the kernel is by registering the kernel with Jupyter. To register the kernel itself, run: .. code-block:: bash $ python3 -m libqtile.interactive.iqshell_install If you run this as a non-root user, or pass the ``--user`` flag, this will install to the user Jupyter kernel directory. You can now invoke the kernel directly when starting a Jupyter frontend, for example: .. code-block:: bash $ jupyter console --kernel qshell The ``iqshell`` script will launch a Jupyter terminal console with the qshell kernel. iqshell vs qtile shell ====================== One of the main drawbacks of running through a Jupyter kernel is the frontend has no way to query the current node of the kernel, and as such, there is no way to set a custom prompt. In order to query your current node, you can call ``pwd``. This, however, enables many of the benefits of running in a Jupyter frontend, including being able to save, run, and re-run code cells in frontends such as the Jupyter notebook. The Jupyter kernel also enables more advanced help, text completion, and introspection capabilities (however, these are currently not implemented at a level much beyond what is available in the standard qtile shell). qtile-0.31.0/docs/manual/commands/shell/qtile-start.rst0000664000175000017500000000044314762660347022757 0ustar epsilonepsilon.. _qtile-start: =========== qtile start =========== This is the entry point for the window manager, and what you should run from your ``.xsession`` or similar. This will make an attempt to detect if qtile is already running and fail if it is. See ``qtile start --help`` for more details. qtile-0.31.0/docs/manual/commands/shell/qtile-cmd.rst0000664000175000017500000002057614762660347022376 0ustar epsilonepsilonqtile cmd-obj ============= This is a simple tool to expose qtile.command functionality to shell. This can be used standalone or in other shell scripts. How it works ------------ ``qtile cmd-obj`` works by selecting a command object and calling a specified function of that object. As per :ref:`commands-api`, Qtile's command graph has seven nodes: ``layout``, ``window``, ``group``, ``bar``, ``widget``, ``screen``, and a special ``root`` node. These are the objects that can be accessed via ``qtile cmd-obj``. Running the command against a selected object without a function (``-f``) will run the ``help`` command and list the commands available to the object. Commands shown with an asterisk ("*") require arguments to be passed via the ``-a`` flag. Selecting an object ~~~~~~~~~~~~~~~~~~~ With the exception of ``cmd``, all objects need an identifier so the correct object can be selected. Refer to :ref:`object_graph_selectors` for more information. .. note:: You will see from the graph on :ref:`commands-api` that certain objects can be accessed from other objects. For example, ``qtile cmd-obj -o group term layout`` will list the commands for the current layout on the ``term`` group. Information on functions ~~~~~~~~~~~~~~~~~~~~~~~~ Running a function with the ``-i`` flag will provide additional detail about that function (i.e. what it does and what arguments it expects). Passing arguments to functions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Arguments can be passed to a function by using the ``-a`` flag. For example, to change the label for the group named "1" to "A", you would run ``qtile cmd-obj -o group 1 -f set_label -a A``. .. warning:: It is not currently possible to pass non-string arguments to functions via ``qtile cmd-obj``. Doing so will result in an error. Examples: --------- Output of ``qtile cmd-obj -h`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code:: text usage: qtile cmd-obj [-h] [--object OBJ_SPEC [OBJ_SPEC ...]] [--function FUNCTION] [--args ARGS [ARGS ...]] [--info] Simple tool to expose qtile.command functionality to shell. optional arguments: -h, --help show this help message and exit --object OBJ_SPEC [OBJ_SPEC ...], -o OBJ_SPEC [OBJ_SPEC ...] Specify path to object (space separated). If no --function flag display available commands. --function FUNCTION, -f FUNCTION Select function to execute. --args ARGS [ARGS ...], -a ARGS [ARGS ...] Set arguments supplied to function. --info, -i With both --object and --function args prints documentation for function. Examples: qtile cmd-obj qtile cmd-obj -o root # same as above, root node is default qtile cmd-obj -o root -f prev_layout -i qtile cmd-obj -o root -f prev_layout -a 3 # prev_layout on group 3 qtile cmd-obj -o group 3 -f focus_back qtile cmd-obj -o widget textbox -f update -a "New text" qtile cmd-obj -f restart # restart qtile Output of ``qtile cmd-obj -o group 3`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code:: text -o group 3 -f commands Returns a list of possible commands for this object -o group 3 -f doc * Returns the documentation for a specified command name -o group 3 -f eval * Evaluates code in the same context as this function -o group 3 -f focus_back Focus the window that had focus before the current one got it. -o group 3 -f focus_by_name * Focus the first window with the given name. Do nothing if the name is -o group 3 -f function * Call a function with current object as argument -o group 3 -f info Returns a dictionary of info for this group -o group 3 -f info_by_name * Get the info for the first window with the given name without giving it -o group 3 -f items * Returns a list of contained items for the specified name -o group 3 -f next_window Focus the next window in group. -o group 3 -f prev_window Focus the previous window in group. -o group 3 -f set_label * Set the display name of current group to be used in GroupBox widget. -o group 3 -f setlayout -o group 3 -f switch_groups * Switch position of current group with name -o group 3 -f toscreen * Pull a group to a specified screen. -o group 3 -f unminimize_all Unminimise all windows in this group Output of ``qtile cmd-obj -o root`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code:: text -o root -f add_rule * Add a dgroup rule, returns rule_id needed to remove it -o root -f addgroup * Add a group with the given name -o root -f commands Returns a list of possible commands for this object -o root -f critical Set log level to CRITICAL -o root -f debug Set log level to DEBUG -o root -f delgroup * Delete a group with the given name -o root -f display_kb * Display table of key bindings -o root -f doc * Returns the documentation for a specified command name -o root -f error Set log level to ERROR -o root -f eval * Evaluates code in the same context as this function -o root -f findwindow * Launch prompt widget to find a window of the given name -o root -f focus_by_click * Bring a window to the front -o root -f function * Call a function with current object as argument -o root -f get_info Prints info for all groups -o root -f get_state Get pickled state for restarting qtile -o root -f get_test_data Returns any content arbitrarily set in the self.test_data attribute. -o root -f groups Return a dictionary containing information for all groups -o root -f hide_show_bar * Toggle visibility of a given bar -o root -f info Set log level to INFO -o root -f internal_windows Return info for each internal window (bars, for example) -o root -f items * Returns a list of contained items for the specified name -o root -f list_widgets List of all addressible widget names -o root -f next_layout * Switch to the next layout. -o root -f next_screen Move to next screen -o root -f next_urgent Focus next window with urgent hint -o root -f pause Drops into pdb -o root -f prev_layout * Switch to the previous layout. -o root -f prev_screen Move to the previous screen -o root -f qtile_info Returns a dictionary of info on the Qtile instance -o root -f qtilecmd * Execute a Qtile command using the client syntax -o root -f remove_rule * Remove a dgroup rule by rule_id -o root -f restart Restart qtile -o root -f run_extension * Run extensions -o root -f run_external * Run external Python script -o root -f screens Return a list of dictionaries providing information on all screens -o root -f shutdown Quit Qtile -o root -f simulate_keypress * Simulates a keypress on the focused window. -o root -f spawn * Run cmd in a shell. -o root -f spawncmd * Spawn a command using a prompt widget, with tab-completion. -o root -f status Return "OK" if Qtile is running -o root -f switch_groups * Switch position of groupa to groupb -o root -f switchgroup * Launch prompt widget to switch to a given group to the current screen -o root -f sync Sync the X display. Should only be used for development -o root -f to_layout_index * Switch to the layout with the given index in self.layouts. -o root -f to_screen * Warp focus to screen n, where n is a 0-based screen number -o root -f togroup * Launch prompt widget to move current window to a given group -o root -f tracemalloc_dump Dump tracemalloc snapshot -o root -f tracemalloc_toggle Toggle tracemalloc status -o root -f warning Set log level to WARNING -o root -f windows Return info for each client window qtile-0.31.0/docs/manual/commands/shell/dqtile-cmd.rst0000664000175000017500000000305114762660347022527 0ustar epsilonepsilondqtile-cmd ========== A Rofi/dmenu interface to qtile-cmd. Accepts all arguments of qtile-cmd. Examples: --------- Output of ``dqtile-cmd -o cmd`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. image:: dqtile-cmd.png Output of ``dqtile-cmd -h`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code:: text dqtile-cmd A Rofi/dmenu interface to qtile-cmd. Excepts all arguments of qtile-cmd (see below). usage: dqtile-cmd [-h] [--object OBJ_SPEC [OBJ_SPEC ...]] [--function FUNCTION] [--args ARGS [ARGS ...]] [--info] Simple tool to expose qtile.command functionality to shell. optional arguments: -h, --help show this help message and exit --object OBJ_SPEC [OBJ_SPEC ...], -o OBJ_SPEC [OBJ_SPEC ...] Specify path to object (space separated). If no --function flag display available commands. --function FUNCTION, -f FUNCTION Select function to execute. --args ARGS [ARGS ...], -a ARGS [ARGS ...] Set arguments supplied to function. --info, -i With both --object and --function args prints documentation for function. Examples: dqtile-cmd dqtile-cmd -o cmd dqtile-cmd -o cmd -f prev_layout -i dqtile-cmd -o cmd -f prev_layout -a 3 # prev_layout on group 3 dqtile-cmd -o group 3 -f focus_back If both rofi and dmenu are present rofi will be selected as default, to change this us --force-dmenu as the first argument. qtile-0.31.0/docs/manual/commands/shell/index.rst0000664000175000017500000000072014762660347021613 0ustar epsilonepsilon============ Entry points ============ Qtile uses a subcommand structure; various subcommands are listed below. Additionally, two other commands available in the scripts/ section of the repository are also documented below. .. toctree:: :maxdepth: 1 qtile start qtile shell qtile migrate qtile cmd-obj qtile run-cmd qtile top dqtile-cmd iqshell qtile-0.31.0/docs/manual/commands/shell/dqtile-cmd.png0000664000175000017500000021653414762660347022517 0ustar epsilonepsilonPNG  IHDR`DbKGD pHYs  tIME 3(XΊiTXtCommentCreated with GIMPd.e IDATxwXGG=zEi 6,XXblKKD-7X[55آF Kowœw <<773;3=QJJDDDDDDD吘U@DDDDDD z1%""""""bKDDDDDDĠA/1}sBϞ戈|DDDDDDD zzлiaxv ˀ |0bDD)sH1q28m~_?7=PJM vaӬ-Jin ˀ p ~i[wM݀?o+KbR268V'"""""AW1bh퉝 gmLqUiS3t\~ VņSaaj(}G#":+MNc&!;'G),ڴ Låk!1JT/1l DDDDDD=]v8WuSкfV L;S{ti+ۡM#pSEYAOW?-G-C&1FΣ'd;n$ܝjja!2:[J^{ 4/0D*4cRiN^MD p-R~"5Hg U?uw˧pn*nu"""""LLĤd_rjI Z(k 3f"Bn`,ϛr;㧔>ZUJ+oįlni""""" jX8RP7lQ/E67CRr䜘05k@|ֻ{__VD\yYjz:[Q9^:~_T@_$Q׭+-V+;pq)YY2ߔN9:4oCDDDDD162B^*s|xVW)8y8r\>|0?F}N+X8{NV9f} ?b}xQ.Z*B}bD,ټ͘п_V-1&|?;++_ek6 Oۇ=h6L6[D)))rVGbV1%""""""bKDDDDDDĠA/^""""""7H^ЩS' wBBB-^""""""*D)))rVGKDDDDDD z/A!cpgdom;|ℜ:u{x331u*!ēȨ2SK6Pm:18t{P4t1@c|>'x־cƭ z/,gUoc2UneԷ w lj޲!AlB"*hsYaDT1g'[atl× l`%^4ݻRCC4(b$;|;f822p'';' 5ptbݸ|G{gUce{TT-m168>ۣڟ6"a<27ÐՒ0"*$%8z"Z7srY`ajNWϏX,~WBd7󁣽 +vA=TM6E\zVM\Wi޴dN{ݩFGi?m|oEhA;_P4 !ZWzo{gQ/ѥu {+AG#4ԲĤdDEUeva=x6q28wuӶh(N]G* ?Ġ":>Ag!ąC~;ósoܸ@v抵o+vK #&ޓgZn 7n?z>kE~oO+֟GbȌp Ap !3El߻Qx|Vh7rVR׮#hXطJ-ڡQ!yZmMh9p ~i[wsӾA`<(04?+1Y)炦^vR@\X7 ˀ hƿ/^|B*PvM vaӬ-j}qRUɩItjUh!CTB۟&IU^hZ! ok:TomW |G~y*a!бyn/ۺ FFع` FF;eNuWЩ=ۭM*7==BӨ/CBU렧+*.swG nð|#  jV/F+v/'پEB۟rҞ7uuja]9\9𨙈[Ψ^ GF~LqWrd2$&%+SS c*hzc롣ǘ8Oi;l^h>@bۼ}`"D)iiJD"{)^;U Ss^כ*R9هfFZG BQ_^ūU_cU%+3jϨP"o6XŲw#пR94W{T)H$BCoOU*[?!^%ޓgHNME)3͹inűj0%!̕Pd8NBʡjN^M*ݫkocj~W8 SޯIuoQ'gM<Dk^=r"d2Ezf&E}<"3+^_S6L%eyqR[xJRU۳~sedgЛwEJ*5mh4P)ꥊ׆Z U𴯢K,95Y071Vk22IHM MQj>Xjjm7Xks3Ŀzy| y?`9 NQr$(_QAf*"A=w7\ubm-"4}GOTוH_v~V湁gTL/RtmzwY/1NZz򮴕& uoa㽪rӞ!>1Ii!"*WAoVv6_ a322a8||Aiq"hf,E[l FFh\>*"_w79Qi|e@}j{e|آ)'&Y>8iv^(IubޛAAj=rd2zM[-\ WZVvBKoO|Iӧ;r^$&%+UZZ 3+ SK$B,c;vD;+B"49jCB[BIjUb4/_M޾ʡN~BO^ >X,sUKD0=zI)8=ݕkT G]Fu+e+ѿc;[5e^.y>l9^\Z??t%+~؅K67G-Ǫ>p}6gWz$MQ=e\3긺{`/,CRJ* 1s8xϒ&[iW6qijvuDe߷'ڎ|~ҍ[8~|qMQ-F{вA=v4^~=~Xkzm3WaǑj m/ =Y - j9VZjV[ f>sè]l.g Ic.m4T%B{733i53c)MLJ_ 5}`48F} O"pEh]ZPy8)T7ܩ/t{B˫qCWhZU۳Pϯ sML""G_X,w5~?wA_b֊er<=[M};߷`抵O\.Gĉj [70?Y]xswÞ0k:j ܸ7~;{#C|7vQ]%v+ 1wdeg#x8dfe7VN\FiX5}2y#8Fu0m LD$.E| `cacƈOD91o/Orq„0{ztyޞȑЦa×|k]5z*>uXg?xc각2cnj*v~lR)ZI;u*lwH3y:/=TPn5qzrZ_ 8TE ?_x*PN5\^(0= + -Y(̬lt2D))) h抵Xk/"O`ec6mâM} :NٳVýC}+Sg1hٿD.$"* Ĭ"IIKǗ؅K~>7LBDG`f,ņ *~eF΀ H{b>yǫ8V~=YY9D;2̉MHDPpC -J{OνacGbȌp Ap !3El\r9V 2 5vE=a֨5] ?;Gh]HIZϞC h33)8wg޸qRmW lfmuBDDDDĠccav;,\F{Ԩb֜XDeɩΆZߟ47+hm;;zā}=̗yn@(0 :GOGc9sH Vڨ05k@|ֻ{__VD[ 3Pӛ! q^#*"\u1:z2Gm_W߸qP忀*-=_{|4Wb^AW* Y_#*&_,\Z6i;pZի)U~=}YB$ŲFDDDDbFoǭH%IWQ OC&a|pG26^j羽]뱓6a F솗qqϨASBŘGJNfZJJM#l9^\*bO0 ffx#/GҪJ5݄փGX{^RV-?oʝ{2.v֖#|=Eznسxf\~_̀>7ƏXa޸Qz DDϣ6̛=6ܱ#㐙&>X9m2zL/cGFXw?6?S-|Tz|C>'F^j]ۡ-}3WEt|r9"Nڨ?!j8嘵r&|ipd~qsU96X9mfZ{7!33(%%Ej """""?YDDOa IDATDDDDD z1%""""""bKDDDDDDĠMN`!)Lrnjv=(ҧ\>ޗq031Ǝsܺ:؄D ܁(J1f"3`HxK7nRmrprSnfWL[ȩ3Xw?'QQޫaXWLߌX>GJNaChUX> ʄBn9xb8{Rhƭ RZv HqfXXM=xn]`{4kqDRG#1d\wˀ 8wƐs"69L\ aݴ-lO1qj؅K5M"0\{_)ka߲VaFL'jWAbǑ I *Z{V89+<1X9m2Ϟt3 99tO""!`Ot>O[/ )5 ;?/EKC!,dp:;DFѶ5jh;GvAfo]N q{_wzf&t3$2JhkX I)0khot?އ !:b1UX,h@DDDDT^XpĚ024@CoO4T*>!xXg?xc각2cn\Ug|Y+G on5qzrZ_ 8TE ?_*;v<|2+M.ws5eUظ2NCUߍ?FTl,ű* Ւqp++d^DGVpq;Gddf`R̃HA{W l>n܊a!95 bD4PK| fER\WquA=w7L"""""" k"o]<'Sdene%#3 Cf̅;=yRJЛGG,FS:>1ȑ5Wb ZϢ^`fa޸Qz=.^U<͌HNKc '"""" .\ҲUV p|> ll4؃ ) >iTϮXu3ܤRұYjM R+wŖy3aoc]:؟0plgȝ H̄nzDF)ܼty\}r <#ùZU"z"""""0D)))XyZ}a;,:p\Wn#:>6h3F| K3SϽ;R)ZφMX!e4r$ = ܱ?O4r|lw&٣*-ƄKpyD'@.wzA/Qy!f^"""""""DDDDDDD z1%""""""z:ub{'$$$2^%"""""rK"g5Qy+DDDDDDTn廧ԣ""""""*3 VKDDDDDD^""""""bKDDDDDDĠ=jsaoeN?k3N0ֶ-}RN⏨ꅺNV틛B$>+{^>>nt94Qn7IXڥ#^ v 92DƦbo tb;])xJL$o\lTLתf&*jQ 6R_6OCjF67?YVf0җH >9ʠw.+[޿HT[9ɋ$\ {vk{1d?W q*T{Y&7uB6.ng\5‘V~"ο;)ٸ0ALb:쭌z|S Zԭg/qS Pi9nk(E%)kS >n+S}~?Kp*Tf5IP-}P\R, +-`aԌl\ G*vdWX?io»%yPOϢSŪΑ[]f7_;Ojt"\UnE|{8L\u}jpbdagsl;z:w/S_j2%tZTFΰ0G#:*͊2 vAհ=tmV&Fz<&㤆+th]we9Eԟ*ۭ {KC~Ī[14Gΰ63PJ _T9*djXb\W/V\_?2җ Ч ^w9#O_-Rɢ+t/>!`giٛ;0L~K#B50?̖aޘWajoULQ°W/<ԣN rl7Nh94PB6SMB!HJBx)PxXC^:5l̬JOƗꢮ%BvhkaUÞs]7#V~˱+ο䂫bpA,jyFi6Tr{1pjaR)HIVsBr&^?b[ )?W2I*W*Kޘq"5okd1u$uDױrRY`/՗1Cwt iTDũޗ]m,:1z/}[;.?S_j _R vz/Uhe;CWvMأވ8 ] .=39SU"k=~;?>rgC8V2O?繟W$Yyk/y*++j&B --#mBqz$> [sCE5Gzf.GzWQfXDg?Fvb[O ~݀j9B5,!pZFٓ2?z:45MpA, XwG {+c{ykBUHyZM >/@r)M쐒"i{_dAAZ~fƣ8ruGuE{PҲNR/f̞[S#U)6ELHt01UL ioRh4&F?c\;h! #BbˉS1f?W[B/w=OD]'+5^#U!_kR]P_i4HNšCZ#_jz T?mS7dsiX}Tyٯq M'OU#Mkk<~܍FjF6zڡ.܊BvLwݨpf`z?_4pEp}莇O+V4)>jR#>t zaR:qV9=g95 jۢEnC]uXz:"U/>. fR=x״~U MIm熚xEGqX&z:!aO} XiYZ2g`;T+Nާq2'vkM#\ ZLHw*U?MIL.G6.0!!%_bS]kM=ϛ~=-Nh94G8x8HL\?n7*(%%EiwzBDD[Ҵz{ץ_koX =眨P۠k#Cdl*% Y JvA'e :TG'RU'C#QJLT6B:^0җE|}<ޝ\f*8??ǑeeD5LZx VkbVW7߻wBDDDDDDeڟ^""""""*^"""""""3QP<{|_GIߠ~g(N]V&}0mz\y[#y }"p9 0n"Ӝo>y;#5nUDDĠJ('GCe6[gX1De[2\!95M|τ^ĿJXXkT,& gV=Q'ah׋x}@Cxduu-Yii)Uвv=pm 15l+LDDS{m,=zhR_&j'˱iz D]1WWYYً8a*zP=zb`UAoǚe|<8+vݾ#Xc?)aH֯R~ƪ=p5.b!54D@OJiWhg:m؊j%[E>G3+, Y̚nܳ77--} =6ι~c0[')vAu{;pRKZN4K<~;~?OqY^<>Qq֥22p&:FR*Pe3bL΂ޱ IDATgbӄDfV9b)~!ߊpR WǪ*o3m|c~992yOzVUqV#؞]Я]>2 Ul*#-?Ƴ/K='zܤFDTJ<9~ `pL  gm ¼QƏĤ= qst陙:/? QkC+3VoDZNX !^RұܟX, ~rrdᤖ,lxC`O5>Үg3c)yإ+J˟Fݧm#?ʑ`ޘOo BS 9nv#"* J|w`ɄQ`Ut6yEDb}X/`۹ESx9hR ӾC?`$} ..4PJvƿ^iH$gVᏟZ>i?RjӤj Iɸ{5X`+m_M/BUqn-'$q k"yMBKM.(U^YNςz~OwaÎb/ (R,"ݐyf3yggfgwv)7a뱀mEucf<.%Q'(ޛOI*:076B'S~%-]s!weg3TTdǓ&\<0<ؤǥ9nwBHU+8gTb:YԐZ%i3lz‘;|>EW $$C{u4*46qOc kCC$Dtl&\9\KJK}\gߓ*J}uB$pNȧ~i |kЃ(H`<=>Dvn.TT^ݾp@H톽TT2 ZqpbN>~SAYIr0Obq~oSCJ]W_"RSE3W'y:CTl^y+u6|(+ +~q&=N&9!|^&{*Yv A$DJZԲlT'@GbS>]M gJ_MO~]6İ:1~)\KI>u??$<]RNn3p`N 5BJr4p{!h$+kV} >=5Ob$2F χHvzP]qT~U =0{#bpmhDhTGeTVB71UWSp@R1նǃ8{bq3] aRW1E޲rrKH]Ң{n0&󑒞Lzi71?%E$2ezZ&!/04Q1s0bJ\ݜRݾ?Qc.6Vc[$aH̄Wz5DBdf|}V^[߱6FܸOjT~M\B]s!wptBQYUnRT5С52r!\b_[z&ߡ5v>z޼ik  #^+H•p=+FN8}=/޼Efv\*I.e..6lGHX8;Ox8C cSHJMíp] ,3MvP0ݼYPmS 'nx2ѱ'-zjkʻʻ}aGk7mÚAhk俟@JI@l|bWxujӴj &=\ob'ӪbcĒ8}n>~#0~z/q+.qVUVFzs"߾+ʼQYUnRT͎X;e,՛&.(Iz!]cס!58h 棧lAؚo& |,Gn~>m1oR%zjaxC^aɎ}RWWR$X8] -F}J)TUj|LNf>B!#y<4u[꽖lAG민~v Z̡GK .y;WI}!];`\Ruka1Xw iH$e^{-McXwKwCvN.uQ߱6j2S~5z?(ZpSx>*ʍB^FFۈ "3q,7mYw ]Ƶ-`v گ!|llld|IVNG0Ƌo9ڱ BBHG^E|#zҳ`jh]c`Ou$j7WBL!B!Jfz=!B!V+B!BB!BnzUVVEB!BH!jYK!B!E^B!B!4%B!Bh 8w6 wCw_޸ƃFz0h A7iʍyq3 ͻy7œ >HIϨP'cY 0Gdày7֟m"|eۏ}QBxMk7VIrm,) & wëUo f`עYѺ)^@Tiq 5eޠTOibޓ _,f ݀{Sp4gږ{Vx-݋fԚhQSwҾ[q\t5m~]nl{EGiY8p\=$yR)c?.r\ǖ9>JԲŶR_}CS6r#5!!9Tݮ)b^Z8 lŊɣR/7ED*|w hس/4B>}C}~1g`fyVMK휿vnw[,d!<;.\ʃMخܝ>WMO]8Xh%X;sΫc2}Lr Azf& ;k@OYl;K][*JJ8bt R0[j%"55(++WmTu='ڹ^q\y+ga_y^PxEd?wQ?7нUSN6~\+6,:6nCЅ@؆SL_V϶~r[^{v#_,ƼQC 5EK#:l"_X: /=3:#Ɂف[ᙤ9oеPP A-KA4ՅE+"v>~{PL۸p)6c^A;F`WgⓒxpoMqC\r'rY**{vV*b=w.{nF Z'mtê)=U[_|MwҾTjrVDLCWK#"9o)*%nֵЈ(ȻR_}Y[y"eKKRqvkv2 &v0-LqgfuR3%LC$@HJI+:[˪rg!0 rbJj3@:EƅKy?.y{1}4ss1o0ť++)I+zrm^z\H_*CYٰ5U҃HY8~C]kKnBdڡWpRgK+(&_|zɥ} ɻBUE}Z"c?EYEY B5eښOv2ERkOMYI *kA`-OvRqʨ\;[ڥka 4ԑ*,=3 ybn*:+}m eA_G iR+z0&.6..]A0̲ظRn yYe ؘՐn_ B]^aܼ|$1B_D}V^ʻ}ar%oyVe.ݾΓgHB] mE}^D!15 um8/\/V_M^Fe\Ƕ^m: Fꊣѫⓒ`.Ե,6̌aDnp8sжA=ddg# &k qJoL1q6 @-pc:۸ȻpRe;kwf FNBω1k{iku{ ^m[Oc] >Ocsp@U S6q %u~'1q&z)X}'(+ 0owj ;k4Ɯ\de+UGGSCWnr:x0cedAMU솙q5tmޘؖ[5[ *n je$Ky KWf}gvMR/ö^rȫ<74uƒm}?MzX8f̍|aab'74DhGk i5usªicڭp?\a"ʃݶ A8?99yyh\f-)=fU|dCoTI7i`_6kߺEo8.iޛjh guh/x|LND"X=O,'ΠsL?F/Vh]/ݾT9!Hfj;yϸO=bL-*eKy^C`N%$'oؖPcbO-ϛe% KWf}gvMR/K*.C^A^FF _YYiB!i=z$"S0!nYL\I=g|BM?D#B!|+^}@lGgfa x8ӀРB!}z$8ee{X "L!B!Jfz-!B!g^?LTI!OI<`^ndNKя}*_\W u-1ϰa8n}L(+)P-ܜ1Nt;7HM7?(۳iԯSUz\Q,c]Z~9<< B^ 3oE#'Gc46Rhjh Z9_MzϘñk|pJx&yoDs7gL+*8e_/znʗa yc Sp]<,:ZZ`D.ǽpw#ktfعz3<ЫMs2v͉IwڭpFv-e6f5X5@pHJMC(8XRE!4%\OLBFV6:7iPe;#W@G N;v<'kKxOh@ЭY#dfTƞ0gfy%&Vx!/_ {ֵ5 QӨ$4&h.9,͙׳P@zlzW?A/!Рl!Oo IDAT1Dz5U4vɿLI$p.^EL{hDp_zX_@׉V8u=ѯ]K,ݹjjX1al z,lcn gc/Rś5mVN'8|ٰ4g@I |Kӑ={!7/y]3A[Im>|Ǧ#'q79>&@](D:ׯ'sIȷcJ moOcnspA@z)ol{`l<쏏)pB7,ޱWᷕ qzNm>g|V3 ^C޸<W[Ƕ Y'K] Jo\%$bD.0hB z=׿`F"15`̿gRư?_?~Zb/={.;7+ٌɩX:zV8mO㟱0s7D& йq}y +<}4Gx1C.` ,ƥG .M·܅D"a^_ lyU0 ؤ6o 1OwTAbZ?VcߢoJm s\DOw~2XTl5aPXF-C sĶfoAڴxU(`Ύ_6=w¥u|Nq5,9<w(4"!4DB ą]-M˜y1jK_=*ʟUU|棾=;K}V_[ _oaSӔj/fL 1nԵDнEc(+ ;e%6͘;ZZMrĶ_.= <goV_dž~}L`bymUU͙szƶ|&”TPP Թ,WK>ZaNu8>E B>23REFFE#'Gg[$ 4mPOϪ?,I](Fou55dwܳhPԴNu*46q7.q)VֽNm> $] ĀًbDxˆ+)Ts7PM3'OHIOb_ /z3):I]2Ձ£_Uz>ضObаT5z6~-]s!weg3TThX4>zsпWթt,򑜖ЗQhT0fkAGSy!FFcF_Hj,hkjH-SWS@L0fݩy<y/bX 9-ۤQqK#=*ñ㧱 x!":6Vgʗo ϗp5۲ 捙i"/QM{_ )-Z޸OfN8wun8xcv)\\ik>lkz";7~+_/,_|HJ*GXаv͛mk>Mq&`֬YsK.HLL䔀\/.½,s&1oR?QMu2E$B < HUՍ}g/_KAi\ x"^D׎m::z<z0n°?boJΎXy¼ą)4΍̸II@bJ2pm| -3 z!1 A e% zqMm>"߾åj* ޳Wf4B8)dhkϧ+:vYaXc $gTlj B¡zMGOAê zyUN=bWx!"!NAU-oOI @tlmGL'jT3>!vwktI'ϰraԮe=szښԨ>Y3j?A߶- }ǑA޽3:HOFz014gx"7oô Bח~zNu~1u&*K:vҀިidCXuhty iy:c),sf50k?t+hy{a"}F!];`\Ruka1Xw iH$R%=647b [pżʜ>(h!XwǯހD"+*gYnZI Bg6662VG!Drr8u6^~1B!|M4%"W|#zҳ`jh]c`OA&BQ4L!B!Jf>B!Bԕ^B!B!{AWz !B!|J=*++B!B! 5e,]%B!ݢA/!B!B!B!1nF p{&2;{oډ7c™ԴA`a:{j!_A݇Xs?X4E 0n@/t5hA]?9|Lc>8|`W{:uk1i(Ȋ̲Ip>3}YXs?\d02DVM0PQCC!!_,ȅ+SBYI !Uɫ<{ 7Ēq!~x:_Aoī7m3ٹypwrQO^E'G3}L l5=4/%B굞& $͇}_Yc=NۖBXmGOk9!Bޯ$6>iݮ%<) ;zvسxx<]%.@GSJƏnMw~aʊ и~+̸SUQAFkG+ 9oÀNm8mۚ}G X\0u뀬jh!(vx!mٍu}w,=2'HXv8c&nu1wV3@w?{v+3=:c k 8:p+vĻxֱGfU`.32uz3|<ͻ0 b1q#`^ݨԺ'\ǼM;.>nmlHԮe.|!afOetٰ \²:ڼզRU}HLB=G{f[lIc^4}PWX^ض48tG ¨>?K}q0n] gԞ w[=ҭ{8|**l{ΣԀ4DBhB|V>{SBK]c\}}CAD4^WmFc׺صh&|s/\,[aOxl{2t55{H`?Y{ςmڡ؊{ށh*Ӂ&|Dz^S.>ͳ'cR zO|Xjݜ\ǖ9>JlucY 7[Z4 :`TdkCw sr?lE `iδ-lH|m+=`?]-Mpűפ>&/v\Syb[?hm}U꽴LgH ֑@lgfūXuTUЦA=NۑWqPюB!UJ g_PS-l󅊒X]©v6p7 /oV̺,3͢1Cѿ#ig{ 6Ϟ ֞nx-^ ZF~xGEtlF-Z֢~n:[Ө-AYYBm"jƯgfp^ СL<-_ye-RYjɵK}z8\W'& ttey(+ L !2}J@ nzfVuG@+O79oеPP A-Kך¢EoMPj;BswgiOVl"olx6`Њ2joѺy<RW2mB~dfpqJYCTLYLCn^kcWV{6~4 N\,;v:4PM+tJ~I[I} +}m\',y`oa!/Ex+^ CL\B!T^a浅1,Njzި!BYI |>pzUA)0. W@|RJ^~>L(+P[Sɜ/!9ZRK(h :+Щ [شpP zkݯ;"^Ө57ӪZV'}fpw+wԕ=wݱMcޫ[x7!%yϫO޺)~j`!>起0ɵKBRhi#15UjYzf2=Lc}TyBZf <͇χ?l ?J}##+[j\{2?yl%K,Q #3~Ӽ7!"|^knUm up9=67ye)Swu+BEY6f5VSUE'VԿj!G0똛4BHE"a{joL rVۖ5v >g.bR勡$T' w&_<{^,Ia/6V:GRjs eW=F8K-z9+qkR!#K~WoKbjvqm}\׶;4E8~_I_-Ky}唯OHIhЮ\|1c`Xyu#qA7 gRCk,u55,BX̚5kn0fp+,:~kڬf>3HFOL$HuB5X4ArZ:>a=:C_[ Ҵ:9ΓgF.c}=mX4ť[>cxϮ rW=4UM٧ñGmK {W>2'&!C,yfa~ۡ&Ӽ7AMU+@Pxࡧ-O"'7ζV9nEJz~&zXbAK]Cq| srJ ~]FZf'ҭ  O􂖺S-cPUVFRZt55e⨩.c1)zZKHD5=]f)^QAy%~J"bЅ+ysF `RmW啯5@UEzZۆc1paֻ+w>Yk/;'z:,u,nc]x% ζWuۏ@kCڭ¢Øyնs,I) !lk%BF!*gyRsWOAtlEh~`ᘡRgrwz4Dhy ޽6f5zDtab l;2=}y>&Ѫ7_cuhרbg#<lō΁i54sw~ݙMu|c `eFst/>$%sl {}/264Mx+|n7=NmL5eSc&)H$#b=x=$ #u ^QAy%~oC)sa#;ʼ%=6gkuS󠫥zvRnQS m=u,q>HM ϝЯCRe$2"!ῡz)g/#}=tkSBK!9P z )꽇fDR0B!TʠWG!3;Ky=id ?L3'P{@!B* z\y|Dz W FKxCB!fB!B!UZE7)|B!BWJAB!B!U ]%B!*u7""B!B!ʰtB!BwB!BhK!B!Рp# ?| =A2`8.yPp9\ g!e!O+F2`8~V&a)^}/]B ]W~xUVV !Р!`a`Y`[Ϙ^;S![axJjYDW!=3K4lbHi`N߸UqR1SCU{ęBףD!PIF& dcYe@G ND'THWXljU=W4A w@61XnR%_)gBA,BBbcxj*h\=-MғH$8x ]`􄱾^Bpz,M_Xs?԰b(ؚ`;x[CRmM~ V8¼uWNBz.Ӎ, IDATdobbq<0}/054)/AxSlk6nXc/W,DMjRkn$&O0(HdJ~Xs?9a/L|>J~ڃDZt0hii;lkQ׺Vx`i%dKA lAsd>K CbJF-X\{<}}P׺VL5V?Ϭ>x9A]HE/Yͳ&@ `̿kd/!5[yg-ּ@Yk__Kв lj2mU ۾ ۧ;6LC#[]ZH핳B_FTOL<ؤǩwBXzjgZ$DJZԲlT)`'0x@z4rK\m>|>4DB_2@L'=Qߛ.}pR~EIIOo+yF@oct#p+|@I ʽ wNd\/be f u55_ٙ@L{| /"*6N)\+O#'Go"3;G.z-?Z{lkWLĥn@[C&RJzbܼ+ůS˘]~(^yl+.qVUVFzs"߾+ʼ!oU=o[hiSbqL] *hR;47jū8xB!<홟sGOaٞ5!];`TA͇iu7c}Ʌ.;FZfedX0r7 Hpe mv?b06;pWnXc>Qu0:CnoQ=aFRi UU1q@/SdmY<+E,-e$׶Uh^fu~ǐD";\c`9rfoya2GkmPP aPZpz8zg;ҵi?Py}S8wh `kkN7yߪ_Bȷ!u5""B vxWuBi#CDxM/p!+B!-zO B<}/^AbJ ,JOfoW$W!†no&B!RfNxB!B!VOz !B!>%B!Rl"*B!B!X{^B!B!-z !B!ТB!BEop0œWox{CF`/K>|GMF)s%&W_a%L}|ş0X5L( }0Emض'zN˵OQUv%l>p,R.|3qՏ K!$wT)bȜh醠 #ʕD/ 1zl 'E0ӅbL&#'[6 t+og o>`5SƢCcQ_WfT.d)BEox! IԴ<+F۱ʘIIɸ?,6hY[4˙:5}|@_k׀Xo!TTʥ Ǚ.?Qa8.!t8X41dR|S9=\?B#`޸*텁'W]h5<{NC:w(z$<yh;z }]S_w1y9Uoof׷˷ՈIi ;~}>B>¢IG=_gbؼ wЦ' O}SSaKZ)&΂OѦGaְv?o^Tup3%WG ^ ;+KpwIJ#ۯ06H(``VOJKW¼q"YzTFqIO]g6i6*Cf5`yqU k-,ҏüqrueӎh4h,|3{rGOo ,tD1SaV!ypa4 ] @8sL L&W)|jU湓1o@"~)iiX?{B`q0CPha-kT{ma!ؿ|^ 7q$"Bn*MX}'`l'a)hߨ.9'$ iY6WH.)r4y SD@,3?Y}$::aNC*)3DGy {#`ZONM+--=3lĤ~ݰ~8ǣHȠQBc=rm+¹5F-\g{\rLtt%t "BТ7l*((2,DRSG?zO1X1q$Mq4nJ:=ΰI+K?Li&Vl݅u3ƕ :gZ_^/=6ϝ N߽%BHIۛ=[M@O,F q&XaƐ>1yѲ)<1{mY4W\t{]4G1h厨/y #J rΥ aeVF#8ZZ?LgHLNC%w;b *YY nq okXS>y7)cbV#&gpũѴ'`DZ3@&T7!a6kT&Z3OQ?n4Q>dYS,c+ú\YT07129o.ʗ-XHijj0|w9ov@f 2=ΰI#K?՟+f m1pb f&jُ6X7my#+9}^x@!%0%.1I_brJ}nmXʅo|RyKhkiaz[qWU`ls>L&ǟ_;iUC%>ضi#}=1,_M|w9s(eh;QO(baJqͶeݯnIMO[LM`kiۏ*q5j|g@'ۊJw1:`9GhtzYrRRi66:^ ׶S'>1 &J$bhjb_ 2i  BH2S\@SK< Z~Zx-:z-19Rr> ໘XRa25PRFHHNVڞIKI]ꌚΐdr-] 2,DiԴtAI"~dҬ!ۡog[XV*yѣUS6g !Q@gd")%Uib9>e(b@ٷ -**R V."iP]D9||Iw|VzDe yRRf=U~ghK"%5MOrSۭ*B=oܿ7D)m~a \||=iU 4k7ct$髑_)jgŗП~00'Hbѿ7ѣe<"~:bb1i?vSO)>Ul)b^dr}4Iu1+ARʏ'r~\ynREvVJ3-9||I.UG;qҶs!$Y {NCI1/>bͨjggOrceV;b͎}HIMCHJMѹi4YSzڣiذ0:67E"WQx8-=)V0Е(:r =uaY4^uDBk!"&-_hka`eV}r>U "وn-@$C% & SV >)m,ذeLѾQ=Deo>~ы:ǪI>sU*U}GtvH$_n r~\yn24+:^/޾KбI}Ǹw_>KYmFMйKѵy#\g̱x@!U-ߌkkwu_`;˿ͰiQ> = 꺻(~⃋.X>q~Bl[#u EX1? mٿ8[L`[Bsoϟajdk`9-F_^ ocb%`EՓ`nl=r roQܪ>˿/2h3r222Pӵ VL3ts TARƼi6إd5;;(>1cF'{l-M Deo?w՝wyR9pA4lq6-\ΏK?ϭ\ؚ%G(&Erj,ʘ+sR*qk~l,ڏ jeUZU nþ3℀~01h- Z@sRR ~L7%%J( ق9G14gZ֭IB_RP;ϊrnD 8b累H{dr9E\D/IĜt  Toѡax*Yz{zM'֡a^[anТݧX$eoPRhkjbP8X#1%>G+oTJP\ܑ)SSTT 55KG-zUp}ܱQ_BG[ ]`\01W)=\'"%O IDAT9<{ <0k2wzTtk.^E95mCWGK "0<^Ŋ݊ף[1hP4YZ3!%-6 gv,=~5acQc`|=+ؾ^}x] ſϯDCޏu7FEJ6J818r*SSac^͚ H1ں /޿9f  0 ыd(_ j_jyq.+f7no~x~,\3Kr/YʅvA\?AW,F*F*Vߣ\\=Y,`ν~ ul".1 .v6ү;˙Y9,K?|=e-k|X~g%L'h9$b,5cuĥa*[N,ش՝t?&P篔KMO~=pXc = wD\ ; U+UDж]j/΍}p8h>:0sPoAQI ,RLHIMcً}K\)~Clb'sO^E >3).CA*)۷d6G`WB[͇XZcR4OwU'ȿRlX˹8gKz{!"#:Ú1o7DxW. ._̯r,Xx.aݔlD" _RqipױǚucYʅKc/qi,Rr\.;>'[m-MDʝYl=z [6ۛy{vjXfڮΨQ2=r7lKR9=|r&~ SRiDٰxì eLaYsw2 ɩiȔJqS=-*}s45DX;i td= bd Z׭ (gKSlVc#xTr>rڽ*YZzhU!3J)F'bPۖpz*U>'.Jeӭ#ɩi\YX˹8gKz_+[)CQ/^v7-MM^/,{~*˂>XqrzvQ)}ة8y:y{rj\K{2cI5|cY˅Kc/qi,Rr񙨰dHJMU Q~ pwSt ΋ޫ@&U]?r]z_ %-Mg/(K5vrk>XK9Rn4Ƕ bQ*y]?BW&ӭ>G}WŀP,<}/Z/] (n=u93hkjⷭ;ѵiCTηG&WR._t㻜B|f/ K;2uW>"95$}8.Xq_uk-V.<}X>aXrX ~\KT:\w|&*,z<~^3(M,Obr @C$BB054diuYNd/@rJ&sB"y@/OR.|T΄`]6ily2ChY[q1-!9.^Wޛ)y|oY]SC|`7OgJmF`ڍHNMCEr[_أ!6Pt~|sϹ˅ڰwG_p =O_ŘeS.>cQ\⁖FbsnyÚucY˅S%pُK˭_k8#3Qakki'(^}G$b%$*mKJMET }] 2>|~P S'$)]r|LHq};^^J d gGMwq hkjB(BO"FѳEUg/44Dqo7B~>pd2n<ƜXh}]U*.]ukb@mP{[{RΏ`iHMOi /NN <5DzKA>۟%fѫ5W)H]ynQaUcwR[TrE+}TiۿOTh,7|L6kV`ϙ kb2?y fe&z1S)eL%&)nzEգ=Zש}d*Os/+\X3w[JZzwDm/|+Z|ǃoU4HJMUV\vZ9{Z.ťcPPW|.Qs&нy#$bU8y ϜǼp7 ҥЭy#u$m ŹqUL\n^p+?8q)8{&_R*5w9BZN6caw<.{Ϝ8qz4,;>s|'*}#D"!9svP{^1˂q%\_c])Ouneŵ k|~.z# +$a8~%&O_u[&q>H$Mq%| S7t)4#Š5=|=e- jw>K<{5V;aŠBjYPli:',9숙{aݞCXe,ѷus+RŚ.g9BԶ%F/͚ oP6W$޹ 6mCjZ:ʖ2F ʨ\*}ϰz^|O\.WҬor}?G{ ioTk*P ĪYebѹ]-sa|/ZovLf/Azf&87F/ V)m= зusk @;b9.l1_wOeS~^XǚucYʥZuDt<**JB!U;b3~9!T~/N!B!آE/!B!bno&B!R̈́B!BwB!B).^B!B!VMIIR!B!Rh*>%B!RlѢB!B-z !B!F$fWml\ݺ-ʩxn㗯)9Ѣ7>BJ뷰dv)ZƨUNK^|GM7(^_ʮ_j=fw6mP/E[u</޽hijYY4텱=;POWG7ECWG-Dоו!R)S*Ő9K AF@SC#_ڄK3m>Q pA:Eoy^ l=|K6+WrxP24Pdc{!-#nŚ{q] ^ PigߢïPOb`V t\B!$$%Stv!Zm7;9`˼)&]iBq~eLFW2+}L Ѵ'O-gf}GpA<9f>gd`i Er=n<B!/z#O!F3?L UJO.cش(_.W!}aQO^gԡ5v8 ѿ}K,_=gOsBҿv͇xUqDz^C1{f(/k1Ѳ)<1{mY4W\/+Yul,Z?}<ʔ2RY ;+^wi}sź߷jB!̋|xkks3\۶Ni$(ˣD M Q+,b@ٞ$BRimX2ajlS)=R.|dKLB9&?m^ڔ)JD$+y:ыVb牳ޢRR羣ۋlЩi}Nu  >\4t\B!yhm(^rcOJS)UTLL»?P(@)#$$+y ߓe] 4 .mrG2 Elll?Ud=Z6Ġz2OO?K<ȎJ'_Fg|<{N.x)V~|{P_Oqs=W48noZjLm?P fxR/@T *Ba^IJ|N\4ma'\WݪB(`SOoRjo]0OR.|[aci0|/!%ѳoQ\zzt| (~n+ARJ o}.΋k<`7{.ϏK5\3ڧժGnyQK, 1uP/M_O`@_:Q8#y9.!"2e̯7dfsJ)6=>.ݾi@%+KLBW twLJO9p;1n]l˛sB":[68rk)'J`_;ajd8O$m܆L Zek>-pmbk8]-MM󑷰dSD"~{SOǥ~YKHIc8~ )x!X2|Qδ55c}}?qו`C&x eL!cJ 8l/~] OF DRws;?.!!KX8sk Hȼ_%+ l>pnBO"k7`C(꺻`DG!xVyf7G;l[8E !Bc3ʟHks3lO@O.pd9.X>q~Bl[#u E~vɩi(czfo㪣~ )iEX 1qq(ml*1,ʘ7ј?A.wz`1[\.[ƃժbe(,so'놉Akar9v~\˯}z;vPǭjnnUn<~障nZ6AЄ?}߯甹;v =[5cޯE9Z: 3lĈ+ Fu^y=.!Kc~)) T*Bxb.ܶQB0!™XT|B§z52ϰzc'B!Bx%v8dT(WûT8Bwt{3!B!B-/77B!B)4\AB!B! }K!B!IoTT !B!BN'B!B-ZB!BE/!B!B"܍ۨ}^{/Njfz}\Ydʪ%}?BJioĕ;ƦCǕmSW#n{?g F/ .TaQx RDT-.qC yari/U~pР"P/T!⌙{CC$ByE 썸u{Q៻+Aek+6\}zJNzJ[{ <}֣FfM'֡%E~KRG> y,r.#-1}ERJ*Z񆋝M;S#.U&)1551oc(mO#x~ؕ@n*[ǵ KKx7kp*B<$QʝXc/jVabRzr;NEsx%x:9`t0+eޣة֬!]rk 6m {+KEz{#."da-y`7?aGZecѠz5iF|GJetv@@ﮰ(m KLB#q=[4QzoI`QdJ}ªx%d2ʗ->Z^jgo k~Y}]=!hwm݅3Cɥ~vc߸_q.Ҟ7픵r9.\q2{255Da%T]K3s:e?!vϥYˏ~Dޓg_NX2j>#hn _[fOP o;z6ۈzJ)ȧcjr>~ǂa)k; 'b] ɘT,x+>X.qs)gbr$tut|0hke]u0~Z\s5:qJl=z [6ۛy{B*)ۮ~]TT+LQ5TFG.8}bʬ @Z.xד'C*f<s45DX;iVEgk=8~3:ޱW!ֆ"ΎtR7k1^RsvۏAbJ 6͘9Uo2â),˨nkjdYCb╨Z SP]/|~w>Dz?vϥYʏ~DآW&!)5UZ(BWG𻏟N@3O8/z{\Vus۫rUGWG)ii}>{&^|uK<-zYʅoG}WgJYY)@У'hmg[kn;? KQA#R8}W?W?%b%$*mKJMET }] 2>|~BzHLNUڞtYʅOBz1֨ nZڼ'~ ?ЗHPEC/uݚЦb۫~vz )o/54DO@} /y82 7FcP^xVK :poIrai/ٞtgdbRĆG]/ D>XǏXpQEYq"5=]qśwUl+r> p۽2R++,*WO$OR.|\cp,KBבHMOGjUWoUou)-aeq,Z|V֩ zL}018w_]Mjx(Dnr$>~^~9ҫ#u]opʵ"m G! \ZWR\u'mMTB_OgGDBsTD`e'#@(B[X7hX LM'l=v UTȱ38%KLBRJ'HMO1?ȭ~1`m/\s~vwrۏQCPÅ[w:ah|+;~|uP: rf-?u#R6X5~V܇ +B[S uePli:',9숙{aݞCXe,ѷus+RecQN`6l)cpr߼RtrngJ0{ 33hA1zi!}z>;7r94_o=!14Ƕv`^!pPCx8ch_L}c[>~;S|r'Ƶ fe!jGV:&۩\, :p ڶ {^Pb$@1ʯZsn-7k{|N_6Om4oW ~3~=D-튵~_k?WW>r:~k[9)<IIIJwGEEQB!B)4T~/] B!RlѢB!B-z !B!ZB!B!D/"B!B B!BHwzSRT!B!bOz !B![%B!B^B!B!EO^ɗ= //S+G at p7@ȱ|KAsq 特^HɤAE^R)Yn0hQ._ҫP,%B[6Hu>6 tbo&ɱI֯ ]jͨ/ѫu3j,*4Ҹ_|Z`? !)6c2`T.~~ekhd~nEekMD(xNAP}q&a׺;Yq*'ǞCww@嶽0pbzbk`ph]nBGA0omGO^Ca .=&ρʷpI/{o ɾg,vDAcq盲c)?0gcm\A/*8vV8r :eG_al>{76=1l^n=TwvC48鱘6-~ATM:)~*ǾÚ{s~Q5u|piq%=~uwѢ[eLJř7eb d2JiNހPۭ*6ϝy#B$OKIKÒ_릏BÕϨ+U[M۶"ByV[>q$"BnjM/{O̔fb]X7co\ @sܹWGDޏ1\Ώr)ӱjl9tζ\P3޼H){"|I {o,dG\L&ÄecPX0 1㾛==ʖ5eb 2?g_HLN_s_r9e`=+kudSA"B8ߚx'/NwU'Z)~Q_ x!+&ĺi16P[pIyIa *иy1D.%sV񅥽裾׸U)%/~2n]oIoo^s?$bl7:Z=f,D;6|?Ì!}5zl '{^[jWjy*]![ul,H3̯&^l[ lL-x?Aw@b?s5f9z~J o[2_^#ɛMio'cq&T|K [Pr|s:3浽 V),̥_[=>$:npm:}`b`MO"Hgvb\0fjL  Je).FJ+kUʥ(Ѐ'aVf|Z~Ul FG0CugG25T.cm)o2XA;pVz0Г K:OےrG\㮦45E_*Yhn^iqr?T=;IL06S3}O MFf0_h/gq~_YMO=s!K ST9^|\enVQυvhm(^s.>+mKLNAFzOUX24SBJ !9Yi{^K!  бq=l?zEhkT@ϏT>HVgmҬ!)*ϸUjZ:RRӔ|gRߠxyP9b'( 0ЕM&$huUG|)vZvϴx=>C=]4t/R~Y[hz1:47G;tUiRpuP%Um ~f*4ܸ/)շ\ ∈k8>$x9XcmB 95&FʋΣkâ8gSRA5vյu͵n0\1^lE鮙1fd<<:˹sk.Ǖ@GEH) ȻlցQ4;7nv6ˑvv4ɫ.CL󤧃bvF 17aJ+.L˓U=Wiu+Im4,j|D}_Du ~[uRh ٹy1yb.GN`5p^} cC kFϥkprA\M}!3'/^EԩjCS32=^&GAaӳe??'3nƆh}c; J_?v"nás~N~ҹ5Sxr DvJKAJz.ܸ%^/i]6xAy/+kBHB [%%LYu]0sP(xal 7,1EE8{e7fPWUU{ 5>,DB!c/VW )9"j hKT(.gnyb:5S,?BA/a]jrѺ^l<"}mM㿙=/PVT-pBv^<L%EEr}0Ct~mPWuzEq!_Bޫw`ރ*JBWSCB!>3"4|:;`o`WޣУEct6f&޼l 5,; ^ƃ>- vh-n>{>YJecШc?]o>J@ً}$čGX z @Fۦ~'qO{a}Z\m147iПڬ!Fw(w#4 .֧?7D .7yo}Ѻ^e5)99uIc`cf"e()* Ŧ'~ga W␛S= <z.Ј}x=lL1cPo8X+X0>gM}1k/Fd>Ƈ +Fw$:aTѦ^FvҎ {yv07E_56BϗI?fp')Diۯ9,}>FI.7˴eޘďib?B!r0!"53cĢ9g 2G"< 7oѿuBN^boA7b/L\3`@ُMDZp QG5s6uq#'vm-k !qf"=+##*Ze IDAT)ǰ"0˯PQVʽ G͇TK]?p# .陸|UJ3.p,40qz/wsXc_ |u,bژ~wiW'N/B$cUd4Q/{5m:6 8u:B!zU\\-c`@(+*bʍz3s#L[-km`Ix%ߛ\:H¦x9v O}w3.5 2 IeLD́P,޹%#ߩʬf\(%=Z6E+gI3c!jbPq1 [fcIo76cRďB zw? 5e+|UW>@m7gV%& " n1uMoؼ­5G]:cw)gˑ06ܡpPd2 u*(v05ГyyL83 r1p L774@ɳqU/s:so:z(x]v\+{Z#d^f~>ͤhɿm3&bhS/zXc7 anthc_eY>;[[W@"CV0i$%z4O.x'(~.wq#0c>赳Ś}  ppM02] 3}T4,´`wn&۪ E1.(}vl+aXWGFCQuNJqB1SpU -[M+6#" :rsE?99Dqw! &_R j**+(+:əغ}/.`g&L%8 $Je{\?p0o,lm.pMƗi&wlۅ6h+}Z7Lj.#K#0Z/hj` R3Ħulv!-3 Ru"!2Y-x4WйI}ZS6ߚ7nQ^h5f2;ţHWG*믨=(ʗKJDy2-+Z'>X:xV3SPQR'ϠD=yO IޕvH_E\v襫,(*.ijolzE!D2}g9?R"#+[lZN~>KJ!6%9=r9h#;WJlVn^et0ƆqhG*rϬjO2e4x))*^~z?>.kdF8z [y,QY<.V9\NsZi\Lnem,iƙ˅:_kV^}Էڛ<ǏB<_ˡgʀn?G'o t.Ht\w)uŦP۹ĥ0L㬧bd] v{|Ѷ^mOM@ x{} 㗯O;hY]ĉ+qаc[n4>?d43pnfULqI ^K7:X5+(,w&+} ?G]wWp#+7vBʴ`we|UT_rwqƒg/`ilkSc |W++~B/ٲ baTJhX{?,ʽȄ S=h;F~a!y!gnMz*O {OCotPi}]qQ l _Ar=w_FZ^08qn_rXc򠢬Q`+"/~IEꟗx<,¯ JnFvrJ!)KZ- 1ggznhMYMNҁ_zV6_I)ܡ5벘+is`hW?Ku]pU >bWI\-= #C衞+z\-u5X,.Lyv`aӱExـHtkiejx5}>/CFPGZG3}oǏBXa#*2y~?zvA #D9 ?G[.3Gx^[_ۖX]-w($ΥoQш: G{ n+\+Djft55#vx[1W,؇iipƔQC6цe kWy\6@sêhD Pskrڇ.M1ڶ~aI}& pٴv6rqeQ!+s$ zD-'kƩe_A!x.hIԆKJ0pN hY`Ւ zl!E/r\l^2%0?]Vup ,ͿBP^eO3Y.S6f&1k2VEFc]/(]dmQʈ!=OQ!U΃/ԹX:vRA[`]A!Jggg'*PHUC ^KanhBX+(h9@[ `ѓKS !B<*mu,lM2z{-! pUd@}۴@/MB!ۛs_ioNWB!B"7ӠB!B3%B!B zB!BHUEVyyYB!B!rCUUC⿥+B!B-B!BA/!B!B*87hgo坸/&ߠ]Gʓ#`m7z ~:Z!k#C{Tqfko>u>{ɺv@A;OE ^\£`ڴ%Xa=hѕˤ6.z{R)PdC.Ec_/N EXMyHR/_wߝ|>U"w.ܸ-u5ۊ BD?v BS}j3DFg{KIˈ8O!)lS[VqLM\ȏECGk ()(P\_BޟQbr rrѥy#8]yiŤTA\RVRŸ9 nZ.x^]0e`/"3'uy:m W$5cz֕BH^yvޓ(}- |A«Dhf3C}<{=`p2Wsg充Ga]Hq\EGyMy7wSӐ~.NM\&,[bŤHIϨP~fz| ;hXB|/_㗠 xGa.gegib-ܿ6Lt?Sl0Rzrj*nوul"v)ͼgCGS[LySѱI ^}ߥ[aDp(}9cmjLQ<u*v *JuUUo>pyaD1hzƁOl9 |:apBp8pfp]j;ĥC; '/c(?9>`D_'}pzMPRPsD'} EY_6qZ@y#z+c40Qc㗦JHk>qEf^{sGn, >;Yoտacnƃ᠉7^$^G&^ a4-'rWx;(4.=c1Aśw ѵE+}PS-/:W[]:p8iM+4e|L8R|ibSl!l ޡ4fسhf1?v/߾CVnh TW׏Zy~E«70ÀжAl'nV8kxTh8[=!7ƣ?ELqmzy2s)6M E2sXPZFVٮ?as\9.JJY V$.U9pKcXU6C+&AvlsC #0i@O:/^_+|%K}(fgUex eICIQQW6x$?rWh9:u 5-*bS"6MKC鬖&..5#=DBXxF"'/56qD/$~ BgBGS]lNOh}8*{B z,pdg/Liٹy(*.as]-Ͳ>iҬqik"+W ƥ2>)(7/_?ؐv"DR_͑ /G;l?|iY</]!CpcOV>ef(̒i> Q;/ps|/G;\}yi^x;ٳ^^nr9sٟ ͇/~Q$.@GEbn3c?4˓ AԭecX7>ʢ|j\ lIGyM>Zˬ\-4Al9s+i7ixؘ\D_̶^%gĤRYRrQZ4)(*=z:!-ioܖy>zOG~Րmqy<C:CrZ7;r3aal!ahY@-؅еy#4]Uy#wDqӰ1tnnSvv%`l.F<õ5Dǎ#' [s3pnaI= RPX|Lz/Jp2WTZر5DrZ:z!95b/KMs@c?;n-"_- %ht45`ga.v01oҮ/LM ͇N㦡wīw8y:7k ̦\L]9(cK?T2-߸3  žWmڭyxwգ#zEHe 5V}{O2|]3GMAAQj{bQ}꼯VMyw"B<:ܭL~.`~#:D @cy:Ԅ:n}gL" L, Vߙ)C!7fO{[˕V?e%%13l8cPRa㗞`iKi5r8a'>C_[-c%ͧu=ݰbh,޺ _B3& +iySNN؍yyYB!'0.d^2DN,؇#p8T MG!BxHJMhPzxV>]QpA/!Bak#X h #)("D:\XaoW !ۛ !B!ȹLo"B!Rm)HsM!B!K!B!*w7>>B!B!DnItB!BHE^B!B!4%B!Bq߼Q!+qx\02boA}x%%X66jyE,@zV6:5/2 qA>~0I,}VW mrd~QՀ!BAOD)7 f5 f$C]U姈!lHžcX0bZ&|3卪¢"Vx>B!?wiGzp)y?BzƵjdD8SA4卟;_kkդ2!RW>5TP{uD B=}QgE;huv:XO޽GqG8z*lLнy#,ضj**X:v-EK`IDAT}Z@}PXT,s6Bg~)n>a FëakfzDv#GT)b^A牳ll?+qχ v̞ OM>(+W!P}۴/u$CuX.zX?|PSUE=WI4ll7&qa߭En>J@ً}$,=q2 v;ǖ1΁ +cv "'^ hQ|2z(R33{?F,Zs.sIx$cΠ{F['!Ŀ|c=]|/L\3`@ُMDZp Q7s6uq#'vm-k ڃGv ZE:aYbXƆ5/x\l8x J]2B!x~?FPSUh&]%r8 F"&܈7#j E#GH mu/~piбAPTT! eޘl\.X2z &~H8!or߫LINȮ`Ԭ,l>c׼*-0ߘEBN^>_SAEI 37lLjE+2Gԯʢ}BA㧡qá૪ϰuzj9*/191gЧushz_0n5|>z lM<[!:.xݒi`P竢DPRzࣩ!Q[nGp8p8VGFCQuNJn=sB1SpU 0/]Uee(*(Th>XBѳ j~o Vn@rZ: tYOY/,Dn~ѱq5Ƭ}D-eޘ惄oM:yC^=`.b4bĿzf.oL"~A(bDWv끶h+A!D:EYr{v긻>yz?@(Drx6UtQ5UU]PSQA^AhG/^! OuwPИE?{n.b.6,KtG˒v#D:N:F)8r zj[}qf~>rެL%8 $RސrQB!"Ģh8taeԌJoL7qp8p}ad-u5<|J.!o޻O$y^:Ol!:fo//=Kbc_X(v]ZNr88|rIrF>_Bn_cisջĦ{A] XXǰvr B|rdj}j:Aܼr'coedyUտI;z(*.[G/^}w')r^]!TG~UϖMs%BVG&HheQLG&q4 Q y8w m ˍUy}Ze{ZإUnGm.5Ѯ~܉+/7.k| ^)8?fA9X#t,}v6r41bvˌ^Q/,*=#XДi?BYso!e4}]g,ɟ-oTr븻hS}}\'^^vP," 6 . kr`:7i< Db< FN^>bAnBhbcĪhLXʊJY}x&?zvA #D9 ?G[.3Gx^[_ۖX]vT-'kƩe_A!x.hU6f&1k2VEFc]/(]dmQvXyY }Oo}8pشVc&m;$.;YKc#p9\[{]M 4Ȯj;5oTr%%8'vǬ}0f/oj9CaUd4_P(ĹK]-f:7iǹ }:*Qgchaa-TUVƸ{?>g-)!=OQ"#'nBo22 v7ю!Jdgg'*PF3 =^4BBAop;tyy03G6- oi&B#BJ䫾\ l`ܥh #k34_F-r+k;aL׉ޭ-"1 EC_/9Wzoib >,k?_XcmOU'~}{[~!РR$& +']7#Њ]pQGOW>Z"jnӤ?lY:m LԿ*~}{)B^bƼ ;psUsD Bl>x  U"P 3>y C0S[D:б5&:_LKMkQyNc鎽x?WG_G œE{M+S*0ucb]HJI#6 ".b^`rKJP FhӦPXT$-=O=ݰff!PLlOK=7U<5`b:4]7dk\ϔoL<{#}\9<[5!PT౮LcؿLM=`~£p.l7lųķp@؄p؊q S1u<1 nv׾F":lzL !zo?J@?gΞtZ &©KrX9}f<[cоʼnqR^AB Y.5 맏ǜu 3u.G ѥYC\umy` h7z &B;@ɀKÈPth-݇n[_/A`mfGA{e \޾ Ҏj_+7w BP}ƥ/`b,؇3CEI g`ܥu=ӂ4ç/0erb{_!tл&9/(+UUB^F^{aD1hzƁ(.)WqǜuFh>ZWE+"Ac?oĿ|-mhGI:hkPVD쇍)O&~Hx^/xS8hުht,h aT.|(**Vhӡ&G.h3r2?}gKV1 !.5-VXTbJ~+F]]iGY<%ƥ/`_N֥W IRa긄ރRWY>HJICřJ3B7\" #;GWn,wYśw ѵE#΢i૊ (7)[?I1zF^bQ n[hVWX_f֯f s(+)a8{oPy[ِ?ЮLvBqgsYWYq!O.˖]aٲ+ƅzpd/lmfM Ty>Iaеy#l'~ߗIr|P϶apAmz>/]!CD԰yhHLh*;.禯s#|/fij KSc(,4yPUQMKPy|ވզ9Z !s vs**􊒤\V?HJJylRWqQ%lseZxy(-iBHU^v8u%N` ^nr9s=m6G Hbvr 17NRS<ޞrQZ4‚(ʭ]mqo۸"Ml{pQ@iSi߳q7:6G̓󸮖&x6IRI8.aʭ b.]EaQ˗J+Boԩ3?P\\Ȫ3C}l>x {m u\sVnBM sL\7k!3'k#!95>t^)ҳa zZ~lLkGڥ5zX#Za-PT\VV&>k `[C_L tzAN"T, Ŷ #pm()*]j.ܸm{he!V)s&zPVTDZVt44yK_9뷣ٸ| ?7gt׎|o_W |9q#]7g_ƅivqr2p82N_{[W;ܹM $z렩qA,$e #;ΞE 2s  En2M}zpt4ձd^8XzjQ|HK&RRan15}cs|!=[IIyLRM\\q]ѻq4zi.՚q Sf&Xyw@+| /M`al$v~!Ȏ[7{9a3BUY Mja Vv(NCS@owԴb]V;&Ēm{ʍpƨ`sxu=ݰbh,޺ _B3&력gjJ/xS8&Cn~ Q޲ n>L;!=BPa .yvCz:Ѳ?oGMAAQj{bQ}Oxĕ8 YZ.0ż;q B!)i/Pq4.rY_i0hm-'a;=nv6=AV~!'NNNeQTb~`B$6NlO`B֣4<aPӍLONUU;t(|UCn~>7A-OX6nB!:.!VY\/ 9df#~눠nRp!B%t{3ߝ ۶`B!Bd"7ӠB!BH*H0B!B!DзkB!BA/!B!B^B!B!B!BlHOIENDB`qtile-0.31.0/docs/manual/commands/shell/qtile-migrate.rst0000664000175000017500000001033214762660347023250 0ustar epsilonepsilonqtile migrate ============= ``qtile migrate`` is a tool to help users update their configs to reflect any breaking changes/deprecations introduced in later versions. The tool can automatically apply updates but it can also be used to highlight impacted lines, allowing users to update their configs manually. The tool can take a number of options when running: .. list-table:: :widths: 10 45 45 :header-rows: 1 * - Argument - Description - Default * - ``-c``, ``--config`` - Sets the path to the config file - ``~/.config/qtile/config.py`` * - ``--list-migrations`` - Lists all the available migrations that can be run by the tool. - n/a * - ``--info ID`` - Show more detail about the migration implement by ID. - n/a * - ``--after-version VERSION`` - Only runs migrations relating to changes implemented after release VERSION. - Not set (i.e. runs all migrations). * - ``-r ID``, ``--run-migrations ID`` - Run selected migrations identified by ID. Comma separated list if using multiple values. - Not set (i.e. runs all migrations). * - ``--yes`` - Automatically apply changes without asking user for confirmation. - Not set (i.e. users will need to confirm application of changes). * - ``--show-diff`` - When used with ``--yes`` will cause diffs to still be shown for information purposes only. - Not set. * - ``--no-colour`` - Disables colour output for diff. - Not set * - ``--lint`` - Outputs linting lines showing location of changes. No changes are made to the config. - Not set. Available migrations -------------------- The following migrations are currently available. .. qtile_migrations:: :summary: Running migrations ------------------ Assuming your config file is in the default location, running ``qtile migrate`` is sufficent to start the migration process. Let's say you had a config file with the following contents: .. code:: python import libqtile.command_client keys = [ KeyChord( [mod], "x", [Key([], "Up", lazy.layout.grow()), Key([], "Down", lazy.layout.shrink())], mode="Resize layout", ) ] qtile.cmd_spawn("alacritty") Running ``qtile migrate`` will run each available migration and, where the migration would result in changes, a diff will be shown and you will be asked whether you wish to apply the changes. .. code:: UpdateKeychordArgs: Updates ``KeyChord`` argument signature. --- original +++ modified @@ -5,7 +5,8 @@ [mod], "x", [Key([], "Up", lazy.layout.grow()), Key([], "Down", lazy.layout.shrink())], - mode="Resize layout", + name="Resize layout", + mode=True, ) ] Apply changes? (y)es, (n)o, (s)kip file, (q)uit. You will see from the output above that you are shown the name of the migration being applied and its purpose, along with the changes that will be implemented. If you select ``quit`` the migration will be stopped and any applied changes will be reversed. Once all migrations have been run on a file, you will then be asked whether you want to save changes to the file: .. code:: Save all changes to config.py? (y)es, (n)o. At the end of the migration, backups of your original config will still be in your config folder. NB these will be overwritten if you re-run ``qtile migrate``. Linting ------- If you don't want the script to modify your config directly, you can use the ``--lint`` option to show you where changes are required. Running ``qtile migrate --lint`` on the same config as shown above will result in the following output: .. code:: config.py: [Ln 1, Col 7]: The 'libqtile.command_*' modules have been moved to 'libqtile.command.*'. (ModuleRenames) [Ln 8, Col 8]: The use of mode='mode name' for KeyChord is deprecated. Use mode=True and value='mode name'. (UpdateKeychordArgs) [Ln 12, Col 6]: Use of 'cmd_' prefix is deprecated. 'cmd_spawn' should be replaced with 'spawn' (RemoveCmdPrefix) Explanations of migrations -------------------------- The table below provides more detail of the available migrations. .. qtile_migrations:: :help: qtile-0.31.0/docs/manual/commands/shell/qtile-top.rst0000664000175000017500000000106414762660347022424 0ustar epsilonepsilon========= qtile top ========= ``qtile top`` is a ``top``-like tool to measure memory usage of Qtile's internals. .. note:: To use ``qtile shell`` you need to have ``tracemalloc`` enabled. You can do this by setting the environmental variable ``PYTHONTRACEMALLOC=1`` before starting qtile. Alternatively, you can force start ``tracemalloc`` but you will lose early traces: .. code-block:: >>> from libqtile.command.client import InteractiveCommandClient >>> i=InteractiveCommandClient() >>> i.eval("import tracemalloc;tracemalloc.start()") qtile-0.31.0/docs/manual/commands/index.rst0000664000175000017500000000420514762660347020506 0ustar epsilonepsilon.. _commands-api: ============ Architecture ============ This page explains how Qtile's API works and how it can be accessed. Users who just want to find a list of commands can jump to :doc:`the API commands page `. Qtile's command API is based on a graph of objects, where each object has a set of associated commands, combined with a number of interfaces that are used to navigate the graph and execute associated commands. This page gives an overview of the command graph and the various interfaces accessible by users. The documentation also contains details of all the commands that are exposed by objects on the graph. .. note:: While users are able to access the internal python objects (e.g. via a ``qtile`` instance), this is not part of the "official" API. These objects and method are not currently included in the documentation but can be viewed by looking at the source code on github. Changes to commonly-used internal objects will be kept to a minimum. The graph and object commands are used in a number of different places: * Commands can be :ref:`bound to keys ` in the Qtile configuration file using the ``lazy`` interface. * Commands can be called from a script using one of the various :ref:`available interfaces ` to interact with Qtile from Python or shell scripts. A couple of additional options are available if you are looking for more interactive access: * Commands can be :ref:`called through qtile shell `, the Qtile shell. * The shell can also be hooked into a Jupyter kernel :ref:`called iqshell ` (NB this interface is currently broken). If the explanations in the pages below seems a bit complex, please take a moment to explore the API using the ``qtile shell`` command shell. The shell provides a way to navigate the graph, allowing you to see how nodes are connected. Available nodes can be displayed with the ``ls`` command while command lists and detailed documentation can be accessed from the built-in ``help`` command. Commands can also be executed from this shell. .. toctree:: :maxdepth: 1 command_graph navigation advanced qtile-0.31.0/docs/manual/hacking.rst0000664000175000017500000002520314762660347017203 0ustar epsilonepsilon.. _hacking: ================ Hacking on Qtile ================ Requirements ============ Here are Qtile's additional dependencies that may be required for tests: ================= =================== ================================================== Dependency Ubuntu Package Needed for ================= =================== ================================================== pytest_ python3-pytest Running tests pre-commit_ pre-commit Running linters PyGObject python3-gi Running tests (test windows) Xephyr_ xserver-xephyr Testing with X11 backend (optional, see below) mypy python3-mypy Testing ``qtile check`` (optional) imagemagick>=6.8 imagemagick ``test/test_images*`` (optional) gtk-layer-shell libgtk-layer-shell0 Testing notification windows in Wayland (optional) dbus-launch dbus-x11 Testing dbus-using widgets (optional) notifiy-send libnotify-bin Testing ``Notify`` widget (optional) xvfb xvfb Testing with X11 headless (optional) ================= =================== ================================================== .. _pytest: https://docs.pytest.org .. _Xephyr: https://freedesktop.org/wiki/Software/Xephyr .. _pre-commit: https://pre-commit.com/ Backends -------- The test suite can be run using the X11 or Wayland backend, or both. By default, only the X11 backend is used for tests. To test a single backend or both backends, specify as arguments to pytest: .. code-block:: bash pytest --backend wayland # Test just Wayland backend pytest --backend x11 --backend wayland # Test both Testing with the X11 backend requires Xephyr_ (and xvfb for headless mode) in addition to the core dependencies. Building cffi module ==================== Qtile ships with a small in-tree pangocairo binding built using cffi, ``pangocffi.py``, and also binds to xcursor with cffi. The bindings are not built at run time and will have to be generated manually when the code is downloaded or when any changes are made to the cffi library. This can be done by calling: .. code-block:: bash ./scripts/ffibuild Setting up the environment ========================== In the root of the project, run ``./dev.sh``. It will create a virtualenv called ``venv``. Activate this virtualenv with ``. venv/bin/activate``. Deactivate it with the ``deactivate`` command. Building the documentation ========================== To build the documentation, you will also need to install `graphviz `_. Go into the ``docs/`` directory and run ``pip install -r requirements.txt``. Build the documentation with ``make html``. Check the result by opening ``_build/html/index.html`` in your browser. .. note:: To speed up local testing, screenshots are not generated each time the documentation is built. You can enable screenshots by setting the ``QTILE_BUILD_SCREENSHOTS`` environmental variable at build time e.g. ``QTILE_BUILD_SCREENSHOTS=1 make html``. You can also export the variable so it will apply to all local builds ``export QTILE_BUILD_SCREENSHOTS=1`` (but remember to unset it if you want to skip building screenshots). Development and testing ======================= In practice, the development cycle looks something like this: 1. make minor code change #. run appropriate test: ``pytest tests/test_module.py`` or ``pytest -k PATTERN`` #. GOTO 1, until hackage is complete #. run entire test suite to make sure you didn't break anything else: ``pytest`` #. try to commit, get changes and feedback from the pre-commit hooks #. GOTO 5, until your changes actually get committed Tests and pre-commit hooks will be run by our CI on every pull request as well so you can see whether or not your contribution passes. Coding style ============ While not all of our code follows `PEP8 `_, we do try to adhere to it where possible. All new code should be PEP8 compliant. The ``make lint`` command (or ``pre-commit run -a``) will run our linters and formatters with our configuration over the whole libqtile to ensure your patch complies with reasonable formatting constraints. We also request that git commit messages follow the `standard format `_. Logging ======= Logs are important to us because they are our best way to see what Qtile is doing when something abnormal happens. However, our goal is not to have as many logs as possible, as this hinders readability. What we want are relevant logs. To decide which log level to use, refer to the following scenarios: * ERROR: a problem affects the behavior of Qtile in a way that is noticeable to the end user, and we can't work around it. * WARNING: a problem causes Qtile to operate in a suboptimal manner. * INFO: the state of Qtile has changed. * DEBUG: information is worth giving to help the developer better understand which branch the process is in. Be careful not to overuse DEBUG and clutter the logs. No information should be duplicated between two messages. Also, keep in mind that any other level than DEBUG is aimed at users who don't necessarily have advanced programming knowledge; adapt your message accordingly. If it can't make sense to your grandma, it's probably meant to be a DEBUG message. Using Xephyr ============ Qtile has a very extensive test suite, using the Xephyr nested X server. When tests are run, a nested X server with a nested instance of Qtile is fired up, and then tests interact with the Qtile instance through the client API. The fact that we can do this is a great demonstration of just how completely scriptable Qtile is. In fact, Qtile is designed expressly to be scriptable enough to allow unit testing in a nested environment. The Qtile repo includes a tiny helper script to let you quickly pull up a nested instance of Qtile in Xephyr, using your current configuration. Run it from the top-level of the repository, like this:: ./scripts/xephyr Change the screen size by setting the ``SCREEN_SIZE`` environment variable. Default: 800x600. Example:: SCREEN_SIZE=1920x1080 ./scripts/xephyr Change the log level by setting the ``LOG_LEVEL`` environment variable. Default: INFO. Example:: LOG_LEVEL=DEBUG ./scripts/xephyr The script will also pass any additional options to Qtile. For example, you can use a specific configuration file like this:: ./scripts/xephyr -c ~/.config/qtile/other_config.py Once the Xephyr window is running and focused, you can enable capturing the keyboard shortcuts by hitting Control+Shift. Hitting them again will disable the capture and let you use your personal keyboard shortcuts again. You can close the Xephyr window by enabling the capture of keyboard shortcuts and hit Mod4+Control+Q. Mod4 (or Mod) is usually the Super key (or Windows key). You can also close the Xephyr window by running ``qtile cmd-obj -o cmd -f shutdown`` in a terminal (from inside the Xephyr window of course). You don't need to run the Xephyr script in order to run the tests as the test runner will launch its own Xephyr instances. Second X Session ================ Some users prefer to test Qtile in a second, completely separate X session: Just switch to a new tty and run ``startx`` normally to use the ``~/.xinitrc`` X startup script. It's likely though that you want to use a different, customized startup script for testing purposes, for example ``~/.config/qtile/xinitrc``. You can do so by launching X with: .. code-block:: bash startx ~/.config/qtile/xinitrc ``startx`` deals with multiple X sessions automatically. If you want to use ``xinit`` instead, you need to first copy ``/etc/X11/xinit/xserverrc`` to ``~/.xserverrc``; when launching it, you have to specify a new session number: .. code-block:: bash xinit ~/.config/qtile/xinitrc -- :1 Examples of custom X startup scripts are available in `qtile-examples `_. Debugging in PyCharm ==================== Make sure to have all the requirements installed and your development environment setup. PyCharm should automatically detect the ``venv`` virtualenv when opening the project. If you are using another viirtualenv, just instruct PyCharm to use it in ``Settings -> Project: qtile -> Project interpreter``. In the project tree, on the left, right-click on the ``libqtile`` folder, and click on ``Mark Directory as -> Sources Root``. Next, add a Configuration using a Python template with these fields: - Script path: ``bin/qtile``, or the absolute path to it - Parameters: ``-c libqtile/resources/default_config.py``, or nothing if you want to use your own config file in ``~/.config/qtile/config.py`` - Environment variables: ``PYTHONUNBUFFERED=1;DISPLAY=:1`` - Working directory: the root of the project - Add contents root to PYTHONPATH: yes - Add source root to PYTHONPATH: yes Then, in a terminal, run: Xephyr +extension RANDR -screen 1920x1040 :1 -ac & Note that we used the same display, ``:1``, in both the terminal command and the PyCharm configuration environment variables. Feel free to change the screen size to fit your own screen. Finally, place your breakpoints in the code and click on ``Debug``! Once you finished debugging, you can close the Xephyr window with ``kill PID`` (use the ``jobs`` builtin to get its PID). Debugging in VSCode =================== Make sure to have all the requirements installed and your development environment setup. Open the root of the repo in VSCode. If you have created it, VSCode should detect the ``venv`` virtualenv, if not, select it. Create a launch.json file with the following lines. .. code-block:: json { "version": "0.2.0", "configurations": [ { "name": "Python: Qtile", "type": "python", "request": "launch", "program": "${workspaceFolder}/bin/qtile", "cwd": "${workspaceFolder}", "args": ["-c", "libqtile/resources/default_config.py"], "console": "integratedTerminal", "env": {"PYTHONUNBUFFERED":"1", "DISPLAY":":1"} } ] } Then, in a terminal, run: Xephyr +extension RANDR -screen 1920x1040 :1 -ac & Note that we used the same display, ``:1``, in both the terminal command and the VSCode configuration environment variables. Then ``debug`` usually in VSCode. Feel free to change the screen size to fit your own screen. Resources ========= Here are a number of resources that may come in handy: * `Inter-Client Conventions Manual `_ * `Extended Window Manager Hints `_ * `A reasonable basic Xlib Manual `_ qtile-0.31.0/docs/manual/ref/0000775000175000017500000000000014762660347015617 5ustar epsilonepsilonqtile-0.31.0/docs/manual/ref/extensions.rst0000664000175000017500000000024714762660347020553 0ustar epsilonepsilon=================== Built-in Extensions =================== .. qtile_module:: libqtile.extension :baseclass: libqtile.extension.base._Extension :no-commands: qtile-0.31.0/docs/manual/ref/widgets.rst0000664000175000017500000000025214762660347020016 0ustar epsilonepsilon.. _ref-widgets: ================ Built-in Widgets ================ .. qtile_module:: libqtile.widget :baseclass: libqtile.widget.base._Widget :exclude: Mirror qtile-0.31.0/docs/manual/ref/layouts.rst0000664000175000017500000000022414762660347020047 0ustar epsilonepsilon.. _ref-layouts: ================ Built-in Layouts ================ .. qtile_module:: libqtile.layout :baseclass: libqtile.layout.base.Layout qtile-0.31.0/docs/manual/ref/hooks.rst0000664000175000017500000000014714762660347017476 0ustar epsilonepsilon.. _ref-hooks: ============== Built-in Hooks ============== .. qtile_hooks:: libqtile.hook.subscribe qtile-0.31.0/docs/manual/wayland.rst0000664000175000017500000000370714762660347017243 0ustar epsilonepsilon===================================== Running Qtile as a Wayland Compositor ===================================== .. _wayland: Some functionality may not yet be implemented in the Wayland compositor. Please see the `Wayland To Do List `__ discussion for the current state of development. Also checkout the `unresolved Wayland-specific issues `__ and :ref:`troubleshooting ` for tips on how to debug Wayland problems. .. note:: We currently support wlroots>=0.17.0,<0.18.0 and pywlroots>=0.17.0,<0.18.0. Backend-Specific Configuration ============================== If you want your config file to work with different backends but want some options set differently per backend, you can check the name of the current backend in your config as follows: .. code-block:: python from libqtile import qtile if qtile.core.name == "x11": term = "urxvt" elif qtile.core.name == "wayland": term = "foot" Running X11-Only Programs ========================= Qtile supports XWayland but requires that `wlroots` and `pywlroots` were built with XWayland support, and that XWayland is installed on the system from startup. XWayland will be started the first time it is needed. XWayland windows sometimes don't receive mouse events ----------------------------------------------------- There is currently a known bug (https://github.com/qtile/qtile/issues/3675) which causes pointer events (hover/click/scroll) to propagate to the wrong window when switching focus. Input Device Configuration ========================== .. qtile_class:: libqtile.backend.wayland.InputConfig If you want to change keyboard configuration during runtime, you can use the core's `set_keymap` command (see below). Core Commands ============= See the :ref:`wayland_backend_commands` section in the API Commands documentation. qtile-0.31.0/docs/manual/license.rst0000664000175000017500000000220314762660347017214 0ustar epsilonepsilon======= License ======= This project is distributed under the MIT license. Copyright (c) 2008, Aldo Cortesi All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. qtile-0.31.0/docs/manual/install/0000775000175000017500000000000014762660347016511 5ustar epsilonepsilonqtile-0.31.0/docs/manual/install/slackware.rst0000664000175000017500000000166714762660347021231 0ustar epsilonepsilon======================= Installing on Slackware ======================= Qtile is available on the `SlackBuilds.org `_ as: ======================= ======================= Package Name Description ======================= ======================= qtile stable branch (release) ======================= ======================= Using slpkg (third party package manager) ========================================= The easy way to install Qtile is with `slpkg `_. For example: .. code-block:: bash slpkg -s sbo qtile Manual installation =================== Download dependencies first and install them. The order in which you need to install is: - pycparser - cffi - futures - python-xcffib - trollius - cairocffi - qtile Please see the HOWTO for more information on `SlackBuild Usage HOWTO `_. qtile-0.31.0/docs/manual/install/nixos.rst0000664000175000017500000000676714762660347020423 0ustar epsilonepsilon ======================== Installing on NixOS ======================== Qtile is available in the NixOS repos. To set qtile as your window manager, include this in your configuration.nix file: .. code-block:: nix services.xserver.windowManager.qtile.enable = true; Other options for qtile can be declared within the `services.xserver.windowManager.qtile` attribute set. You may add extra packages in the qtile python environment by putting them in the `extraPackages` list. .. code-block:: nix services.xserver.windowManager.qtile = { enable = true; extraPackages = python3Packages: with python3Packages; [ qtile-extras ]; }; The Qtile package creates desktop files for both X11 and Wayland, to use one of the backends choose the right session in your display manager. The configuration file can be changed from its default location (`$XDG_CONFIG/qtile/config.py`) by setting the `configFile` attribute: .. code-block:: nix qtile = { enable = true; configFile = ./my_qtile_config.py; }; .. note:: Some options may change over time, please refer to see all the options for the latest stable: `search.nixos.org `__ if you have any doubt Home manager ************ If you are using home-manager, you can copy your qtile configuration by using the following: .. code-block:: nix xdg.configFile."qtile/config.py".source = ./my_qtile_config.py; or, if you have a directory containing multiple python files: .. code-block:: nix xdg.configFile."qtile" = { source = ./src; recursive = true; }; Flake ***** Qtile also has a flake in the repository. This can be used for the following use cases: - Run a bleeding edge version of Qtile by using it as an overlay in your flake config - Hack on Qtile with a Nix develop shell Note that flakes are an experimental NixOS feature but they are already widely used. This section is meant for users that already use flakes. To run a bleeding edge version of Qtile with the flake, add the Qtile repo to your inputs and define the overlay. An example flake is the following: .. code-block:: nix { description = "A very basic flake"; inputs = { nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable"; qtile-flake = { url = "github:qtile/qtile"; inputs.nixpkgs.follows = "nixpkgs"; }; }; outputs = { self, nixpkgs, qtile-flake }: { nixosConfigurations.demo = nixpkgs.lib.nixosSystem { system = "x86_64-linux"; modules = [ (_: { nixpkgs.overlays = [ qtile-flake.overlays.default ]; }) ({ config, pkgs, lib, ...}: { services.xserver = { enable = true; windowManager.qtile.enable = true; }; # make qtile X11 the default session services.displayManager.defaultSession = lib.mkForce "qtile"; # rest of your NixOS config }) ]; }; }; } This flake can also be tested with a vm: .. code-block:: console sudo nixos-rebuild build-vm --flake .#demo Gives you a script to run that runs Qemu to test your config. For this to work you have to set a user with a password. To hack on Qtile with Nix, simply run `nix develop` in a checkout of the repo. In the development shell, there are a few useful things: - `qtile-run-tests-wayland`: Run all Wayland tests - `qtile-run-tests-x11`: Run all X11 tests qtile-0.31.0/docs/manual/install/freebsd.rst0000664000175000017500000000034114762660347020653 0ustar epsilonepsilon======================= Installing on FreeBSD ======================= Qtile is available via `FreeBSD Ports `_. It can be installed with .. code-block:: bash pkg install qtile qtile-0.31.0/docs/manual/install/ubuntu.rst0000664000175000017500000000116214762660347020565 0ustar epsilonepsilon============================== Installing on Ubuntu or Debian 11 (bullseye) or greater ============================== Ubuntu and Debian >=11 comes with the necessary packages for installing Qtile. Starting from a minimal Debian installation, the following packages are required: .. code-block:: bash sudo apt install xserver-xorg xinit sudo apt install libpangocairo-1.0-0 sudo apt install python3-pip python3-xcffib python3-cairocffi Either Qtile can then be downloaded from the package index or the Github repository can be used, see :ref:`installing-from-source`: .. code-block:: bash pip install qtile qtile-0.31.0/docs/manual/install/arch.rst0000664000175000017500000000050714762660347020162 0ustar epsilonepsilon======================== Installing on Arch Linux ======================== Stable versions of Qtile are currently packaged for Arch Linux. To install this package, run: .. code-block:: bash pacman -S qtile Please see the ArchWiki for more information on `Qtile`_. .. _Qtile: https://wiki.archlinux.org/index.php/Qtile qtile-0.31.0/docs/manual/install/index.rst0000664000175000017500000002222714762660347020357 0ustar epsilonepsilon============ Installation ============ Distro Guides ============= Below are the preferred installation methods for specific distros. If you are running something else, please see `Installing From Source`_. .. toctree:: :maxdepth: 1 Arch Fedora Funtoo Ubuntu/Debian Slackware FreeBSD NixOS .. _installing-from-source: Installing From Source ====================== Python interpreters ------------------- We aim to always support the last three versions of CPython, the reference Python interpreter. We usually support the latest stable version of PyPy_ as well. You can check the versions and interpreters we currently run our test suite against in our `tox configuration file`_. There are not many differences between versions aside from Python features you may or may not be able to use in your config. PyPy should be faster at runtime than any corresponding CPython version under most circumstances, especially for bits of Python code that are run many times. CPython should start up faster than PyPy and has better compatibility for external libraries. .. _`tox configuration file`: https://github.com/qtile/qtile/blob/master/tox.ini .. _PyPy: https://www.pypy.org/ Core Dependencies ----------------- Here are Qtile's core runtime dependencies and the package names that provide them in Ubuntu. Note that Qtile can run with one of two backends -- X11 and Wayland -- so only the dependencies of one of these is required. +-------------------+-------------------------+-----------------------------------------+ | Dependency | Ubuntu Package | Needed for | +===================+=========================+=========================================+ | **Core Dependencies** | +-------------------+-------------------------+-----------------------------------------+ | CFFI_ | python3-cffi | Bars and popups | +-------------------+-------------------------+-----------------------------------------+ | cairocffi_ | python3-cairocffi | Drawing on bars and popups | +-------------------+-------------------------+-----------------------------------------+ | libpangocairo | libpangocairo-1.0-0 | Writing on bars and popups | +-------------------+-------------------------+-----------------------------------------+ | dbus-fast_ | -- | Sending notifications with dbus | | | | (optional). | +-------------------+-------------------------+-----------------------------------------+ | **X11** | +-------------------+-------------------------+-----------------------------------------+ | X server | xserver-xorg | X11 backends | +-------------------+-------------------------+-----------------------------------------+ | xcffib_ | python3-xcffib | required for X11 backend | +-------------------+-------------------------+-----------------------------------------+ | **Wayland** | +-------------------+-------------------------+-----------------------------------------+ | wlroots_ | libwlroots-dev | Wayland backend (see below) | +-------------------+-------------------------+-----------------------------------------+ | pywlroots_ | -- | python bindings for the wlroots library| +-------------------+-------------------------+-----------------------------------------+ | pywayland_ | -- | python bindings for the wayland library| +-------------------+-------------------------+-----------------------------------------+ | python-xkbcommon_ | -- | required for wayland backeds | +-------------------+-------------------------+-----------------------------------------+ .. _CFFI: https://cffi.readthedocs.io/en/latest/installation.html .. _xcffib: https://github.com/tych0/xcffib#installation .. _wlroots: https://gitlab.freedesktop.org/wlroots/wlroots .. _pywlroots: https://github.com/flacjacket/pywlroots .. _pywayland: https://pywayland.readthedocs.io/en/latest/install.html .. _python-xkbcommon: https://github.com/sde1000/python-xkbcommon .. _cairocffi: https://cairocffi.readthedocs.io/en/stable/overview.html .. _dbus-fast: https://dbus-fast.readthedocs.io/en/latest/ Qtile ----- With the dependencies in place, you can now install the stable version of qtile from PyPI: .. code-block:: bash pip install qtile Or with sets of dependencies: .. code-block:: bash pip install qtile[wayland] # for Wayland dependencies pip install qtile[widgets] # for all widget dependencies pip install qtile[all] # for all dependencies Or install qtile-git with: .. code-block:: bash git clone https://github.com/qtile/qtile.git cd qtile pip install . pip install --config-setting backend=wayland . # adds wayland dependencies .. _starting-qtile: Starting Qtile ============== There are several ways to start Qtile. The most common way is via an entry in your X session manager's menu. The default Qtile behavior can be invoked by creating a `qtile.desktop `_ file in ``/usr/share/xsessions``. A second way to start Qtile is a custom X session. This way allows you to invoke Qtile with custom arguments, and also allows you to do any setup you want (e.g. special keyboard bindings like mapping caps lock to control, setting your desktop background, etc.) before Qtile starts. If you're using an X session manager, you still may need to create a ``custom.desktop`` file similar to the ``qtile.desktop`` file above, but with ``Exec=/etc/X11/xsession``. Then, create your own ``~/.xsession``. There are several examples of user defined ``xsession`` s in the `qtile-examples `_ repository. If there is no display manager such as SDDM, LightDM or other and there is need to start Qtile directly from ``~/.xinitrc`` do that by adding ``exec qtile start`` at the end. In very special cases, ex. Qtile crashing during session, then suggestion would be to start through a loop to save running applications: .. code-block:: bash while true; do qtile done Wayland ======= Qtile can be run as a Wayland compositor rather than an X11 window manager. For this, Qtile uses wlroots_, a compositor library which is undergoing fast development. Be aware that some distributions package outdated versions of wlroots. More up-to-date distributions such as Arch Linux may package pywayland, pywlroots and python-xkbcommon. Also note that we may not have yet caught up with the latest wlroots release ourselves. .. note:: We currently support wlroots>=0.17.0,<0.18.0, pywlroots>=0.17.0,<0.18.0 and pywayland >= 0.4.17. With the Wayland dependencies in place, Qtile can be run either from a TTY, or within an existing X11 or Wayland session where it will run inside a nested window: .. code-block:: bash qtile start -b wayland See the :ref:`Wayland ` page for more information on running Qtile as a Wayland compositor. Similar to the xsession example above, a wayland session file can be used to start qtile from a login manager. To use this, you should create a `qtile-wayland.desktop `_ file in ``/usr/share/wayland-sessions``. udev rules ========== Qtile has widgets that support managing various kinds of hardware (LCD backlight, keyboard backlight, battery charge thresholds) via the kernel's exposed sysfs endpoints. However, to make this work, Qtile needs permission to write to these files. There is a udev rules file at ``/resources/99-qtile.rules`` in the tree, which users installing from source will want to install at ``/etc/udev/rules.d/`` on their system. By default, this rules file changes the group of the relevant files to the ``sudo`` group, and changes the file mode to be g+w (i.e. writable by all members of the sudo group). The theory here is that most systems qtile is installed on will also have the primary user in the ``sudo`` group. However, you can change this to whatever you like with the ``--group`` argument; see the sample udev rules. Note that this file invokes Qtile's hidden ``udev`` from udevd, so udevd will need ``qtile`` in its ``$PATH``. For distro packaging this shouldn't be a problem, since /usr/bin is typically in udev's path. Alternatively, users can install from source using uv, which will do all the right ``$PYTHONPATH`` setup etc., so you only need to change the path to the final executable in the udev rules: .. code-block:: bash # copy the in-tree udev rules file to the right place to make udev see it, # and change the rules to point at our wrapper script above. sed "s,qtile,$HOME/.local/bin/qtile,g" ./resources/99-qtile.rules | sudo tee /etc/udev/rules.d/99-qtile.rules qtile-0.31.0/docs/manual/install/without-dm.rst0000664000175000017500000000265414762660347021353 0ustar epsilonepsilon==================== Running from systemd ==================== This case will cover automatic login to Qtile after booting the system without using display manager. It logins in virtual console and init X by running through session. Automatic login to virtual console ---------------------------------- To get login into virtual console as an example edit `getty` service by running `systemctl edit getty@tty1` and add instructions to `/etc/systemd/system/getty@tty1.service.d/override.conf`:: [Service] ExecStart= ExecStart=-/usr/bin/agetty --autologin username --noclear %I $TERM `username` should be changed to current user name. Check more for other `examples `_. Autostart X session ------------------- After login X session should be started. That can be done by `.bash_profile` if bash is used or `.zprofile` in case of zsh. Other shells can be adjusted by given examples. .. code-block:: bash if systemctl -q is-active graphical.target && [[ ! $DISPLAY && $XDG_VTNR -eq 1 ]]; then exec startx fi And to start Qtile itself `.xinitrc` should be fixed: :: # some apps that should be started before Qtile, ex. # # [[ -f ~/.Xresources ]] && xrdb -merge ~/.Xresources # ~/.fehbg & # nm-applet & # blueman-applet & # dunst & # # or # # source ~/.xsession exec qtile start qtile-0.31.0/docs/manual/install/funtoo.rst0000664000175000017500000000204614762660347020557 0ustar epsilonepsilon==================== Installing on Funtoo ==================== Latest versions of Qtile are available on Funtoo. To install it, run: .. code-block:: bash emerge -av x11-wm/qtile You can also install the development version from GitHub: .. code-block:: bash echo "x11-wm/qtile-9999 **" >> /etc/portage/package.accept_keywords emerge -av qtile Customize ========= You can customize your installation with the following useflags: - dbus - widget-khal-calendar - widget-imap - widget-keyboardkbdd - widget-launchbar - widget-mpd - widget-mpris - widget-wlan The dbus useflag is enabled by default. Disable it only if you know what it is and know you don't use/need it. All widget-* useflags are disabled by default because these widgets require additional dependencies while not everyone will use them. Enable only widgets you need to avoid extra dependencies thanks to these useflags. Visit `Funtoo Qtile documentation`_ for more details on Qtile installation on Funtoo. .. _Funtoo Qtile documentation: https://www.funtoo.org/Package:Qtile qtile-0.31.0/docs/manual/install/fedora.rst0000664000175000017500000000047514762660347020511 0ustar epsilonepsilon==================== Installing on Fedora 40 or later. ==================== Both Qtile X11 and Wayland stable versions can be installed via the DNF Package Manager. If you want to pick a specific version(Such as the wayland version) then double-click Tab after "qtile". .. code-block:: bash dnf install qtile qtile-0.31.0/docs/manual/changelog.rst0000664000175000017500000000010314762660347017516 0ustar epsilonepsilon========= Changelog ========= .. literalinclude:: ../../CHANGELOG qtile-0.31.0/docs/manual/howto/0000775000175000017500000000000014762660347016203 5ustar epsilonepsilonqtile-0.31.0/docs/manual/howto/layout.rst0000664000175000017500000005321514762660347020260 0ustar epsilonepsilon.. _layout-creation: ====================== How to create a layout ====================== The aim of this page is to explain the main components of qtile layouts, how they work, and how you can use them to create your own layouts or hack existing layouts to make them work the way you want them. .. note:: It is highly recommended that users wishing to create their own layout refer to the source documentation of existing layouts to familiarise themselves with the code. What is a layout? ================= In Qtile, a layout is essentially a set of rules that determine how windows should be displayed on the screen. The layout is responsible for positioning all windows other than floating windows, "static" windows, internal windows (e.g. the bar) and windows that have requested not to be managed by the window manager. Base classes ============ To simplify the creation of layouts, a couple of base classes are available to users. The ``Layout`` class ~~~~~~~~~~~~~~~~~~~~ As a bare minimum, all layouts should inherit the base ``Layout`` class object as this class defines a number of methods required for basic usage and will also raise errors if the required methods are not implemented. Further information on these required methods is set out below. The ``_SimpleLayoutBase`` class ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This class implements everything needed for a basic layout with the exception of the ``configure`` method. Therefore, unless your layout requires special logic for updating and navigating the list of clients, it is strongly recommended that your layout inherits this base class The ``_ClientList`` class ~~~~~~~~~~~~~~~~~~~~~~~~~ This class defines a list of clients and the current client. The collection is meant as a base or utility class for special layouts, which need to maintain one or several collections of windows, for example Columns or Stack, which use this class as base for their internal helper. The property ``current_index`` get and set the index to the current client, whereas ``current_client`` property can be used with clients directly. Required methods ================ To create a minimal, functioning layout your layout must include the methods listed below: * ``__init__`` * ``configure`` * ``add_client`` * ``remove`` * ``focus_first`` * ``focus_last`` * ``focus_next`` * ``focus_previous`` * ``next`` * ``previous`` As noted above, if you create a layout based on the ``_SimpleLayoutBase`` class, you will only need to define ``configure`` (and ``_init__``, if you have custom parameters). However, for the purposes of this document, we will show examples of all required methods. ``__init__`` ~~~~~~~~~~~~ Initialise your layout's variables here. The main use of this method will be to load any default parameters defined by layout. These are defined in a class attribute called ``defaults``. The format of this attribute is a list of tuples. .. code:: python from libqtile.layout import base class TwoByTwo(base.Layout): """ A simple layout with a fixed two by two grid. By default, unfocused windows are smaller than the focused window. """ defaults = [ ("border_width", 5, "Window border width"), ("border_colour", "00ff00", "Window border colour"), ("margin_focused", 5, "Margin width for focused windows"), ("margin_unfocused", 50, "Margin width for unfocused windows") ] def __init__(self, **config): base.Layout.__init__(self, **config) self.add_defaults(TwoByTwo.defaults) self.clients = [] self.current_client = None Once the layouts is initialised, these parameters are available at ``self.border_width`` etc. ``configure`` ~~~~~~~~~~~~~ This is where the magic happens! This method is responsible for determining how to position a window on the screen. This method should therefore configure the dimensions and borders of a window using the window's ``.place()`` method. The layout can also call either ``hide()`` or ``.unhide()`` on the window. .. code:: python def configure(self, client: Window, screen_rect: ScreenRect) -> None: """Simple example breaking screen into four quarters.""" try: index = self.clients.index(client) except ValueError: # Layout not expecting this window so ignore it return # We're only showing first 4 windows if index > 3: client.hide() return # Unhide the window in case it was hiddent before client.unhide() # List to help us calculate x and y values of quarters = [ (0, 0), (0.5, 0), (0, 0.5), (0.5, 0.5) ] # Calculate size and position for each window xpos, ypos = quarters[index] x = int(screen_rect.width * xpos) + screen_rect.x y = int(screen_rect.height * ypos) + screen_rect.y w = screen_rect.width // 2 h = screen_rect.height // 2 if client is self.current_client: margin = self.margin_focused else: margin = self.margin_unfocused client.place( x, y, w - self.border_width * 2, h - self.border_width * 2, self.border_width, self.border_colour, margin=[margin] * 4, ) ``add_client`` ~~~~~~~~~~~~~~ This method is called whenever a window is added to the group, regardless of whether the layout is current or not. The layout should just add the window to its internal datastructures, without mapping or configuring/displaying. .. code:: python def add_client(self, client: Window) -> None: # Assumes self.clients is simple list self.clients.insert(0, client) self.current_client = client ``remove`` ~~~~~~~~~~ This method is called whenever a window is removed from the group, regardless of whether the layout is current or not. The layout should just de-register the window from its data structures, without unmapping the window. The method must also return the "next" window that should gain focus or ``None`` if there are no other windows. .. code:: python def remove(self, client: Window) -> Window | None: # Assumes self.clients is a simple list # Client already removed so ignore this if client not in self.clients: return None # Client is only window in the list elif len(self.clients) == 1: self.clients.remove(client) self.current_client = None # There are no other windows so return None return None else: # Find position of client in our list index = self.clients.index(client) # Remove client self.clients.remove(client) # Ensure the index value is not greater than list size # i.e. if we closed the last window in the list, we need to return # the first one (index 0). index %= len(self.clients) next_client = self.clients[index] self.current_client = next_client return next_client ``focus_first`` ~~~~~~~~~~~~~~~ This method is called when the first client in the layout should be focused. This method should just return the first client in the layout, if any. NB the method should not focus the client itself, this is done by caller. .. code:: python def focus_first(self) -> Window | None: if not self.clients: return None return self.client[0] ``focus_last`` ~~~~~~~~~~~~~~ This method is called when the last client in the layout should be focused. This method should just return the last client in the layout, if any. NB the method should not focus the client itself, this is done by caller. .. code:: python def focus_last(self) -> Window | None: if not self.clients: return None return self.client[-1] ``focus_next`` ~~~~~~~~~~~~~~ This method is called the next client in the layout should be focused. This method should return the next client in the layout, if any. NB the layout should not cycle clients when reaching the end of the list as there are other method in the group for cycling windows which focus floating windows once the the end of the tiled client list is reached. In addition, the method should not focus the client. .. code:: python def focus_next(self, win: Window) -> Window | None: try: return self.clients[self.clients.index(win) + 1] except IndexError: return None ``focus_previous`` ~~~~~~~~~~~~~~~~~~ This method is called the previous client in the layout should be focused. This method should return the previous client in the layout, if any. NB the layout should not cycle clients when reaching the end of the list as there are other method in the group for cycling windows which focus floating windows once the the end of the tiled client list is reached. In addition, the method should not focus the client. .. code:: python def focus_previous(self, win: Window) -> Window | None: if not self.clients or self.clients.index(win) == 0 return None try: return self.clients[self.clients.index(win) - 1] except IndexError: return None ``next`` ~~~~~~~~ This method focuses the next tiled window and can cycle back to the beginning of the list. .. code:: python def next(self) -> None: if self.current_client is None: return # Get the next client or, if at the end of the list, get the first client = self.focus_next(self.current_client) or self.focus_first() self.group.focus(client, True) ``previous`` ~~~~~~~~~~~~ This method focuses the previous tiled window and can cycle back to the end of the list. .. code:: python def previous(self) -> None: if self.current_client is None: return # Get the previous client or, if at the end of the list, get the last client = self.focus_previous(self.current_client) or self.focus_last() self.group.focus(client, True) Additional methods ================== While not essential to implement, the following methods can also be defined: * ``clone`` * ``show`` * ``hide`` * ``swap`` * ``focus`` * ``blur`` ``clone`` ~~~~~~~~~ Each group gets a copy of the layout. The ``clone`` method is used to create this copy. The default implementation in ``Layout`` is as follows: .. code:: python def clone(self, group: _Group) -> Self: c = copy.copy(self) c._group = group return c ``show`` ~~~~~~~~ This method can be used to run code when the layout is being displayed. The method receives one argument, the ``ScreenRect`` for the screen showing the layout. The default implementation is a no-op: .. code:: python def show(self, screen_rect: ScreenRect) -> None: pass ``hide`` ~~~~~~~~ This method can be used to run code when the layout is being hidden. The default implementation is a no-op: .. code:: python def hide(self) -> None: pass ``swap`` ~~~~~~~~ This method is used to change the position of two windows in the layout. .. code:: python def swap(self, c1: Window, c2: Window) -> None: if c1 not in self.clients and c2 not in self.clients: return index1 = self.clients.index(c1) index2 = self.clients.index(c2) self.clients[index1], self.clients[index2] = self.clients[index2], self.clients[index1] ``focus`` ~~~~~~~~~ This method is called when a given window is being focused. .. code:: python def focus(self, client: Window) -> None: if client not in self.clients: self.current_client = None return index = self.clients.index(client) # Check if window is not visible if index > 3: c = self.clients.pop(index) self.clients.insert(0, c) self.current_client = client ``blur`` ~~~~~~~~ This method is called when the layout loses focus. .. code:: python def blur(self) -> None: self.current_client = None Adding commands =============== Adding commands allows users to modify the behaviour of the layout. To make commands available via the command interface (e.g. via ``lazy.layout`` calls), the layout must include the following import: .. code:: python from libqtile.command.base import expose_command Commands are then decorated with ``@expose_command``. For example: .. code:: python @expose_command def rotate(self, clockwise: bool = True) -> None: if not self.clients: return if clockwise: client = self.clients.pop(-1) self.clients.insert(0, client) else: client = self.clients.pop(0) self.clients.append(client) # Check if current client has been rotated off the screen if self.current_client and self.clients.index(self.current_client) > 3: if clockwise: self.current_client = self.clients[3] else: self.current_client = self.clients[0] # Redraw the layout self.group.layout_all() The ``info`` command ~~~~~~~~~~~~~~~~~~~~ Layouts should also implement an ``info`` method to provide information about the layout. As a minimum, the test suite (see below) will expect a layout to return the following information: * Its name * Its group * The clients managed by the layout NB the last item is not included in ``Layout``'s implementation of the method so it should be added when defining a class that inherits that base. .. code:: python @expose_command def info(self) -> dict[str, Any]: inf = base.Layout.info(self) inf["clients"] = self.clients return inf Adding layout to main repo ========================== If you think your layout is amazing and you want to share with other users by including it in the main repo then there are a couple of extra steps that you need to take. Add to list of layouts ~~~~~~~~~~~~~~~~~~~~~~ You must save the layout in ``libqtile/layout`` and then add a line importing the layout definition to ``libqtile/layout/__init__.py`` e.g. .. code:: python from libqtile.layout.twobytwo import TwoByTwo Add tests ~~~~~~~~~ Basic functionality for all layouts is handled automatically by the core test suite. However, you should create tests for any custom functionality of your layout (e.g. testing the ``rotate`` command defined above). Full example ============ The full code for the example layout is as follows: .. code:: python from __future__ import annotations from typing import TYPE_CHECKING from libqtile.command.base import expose_command from libqtile.layout import base if TYPE_CHECKING: from libqtile.backend.base import Window from libqtile.config import ScreenRect from libqtile.group import _Group class TwoByTwo(base.Layout): """ A simple layout with a fixed two by two grid. By default, unfocused windows are smaller than the focused window. """ defaults = [ ("border_width", 5, "Window border width"), ("border_colour", "00ff00", "Window border colour"), ("margin_focused", 5, "Margin width for focused windows"), ("margin_unfocused", 50, "Margin width for unfocused windows") ] def __init__(self, **config): base.Layout.__init__(self, **config) self.add_defaults(TwoByTwo.defaults) self.clients = [] self.current_client = None def configure(self, client: Window, screen_rect: ScreenRect) -> None: """Simple example breaking screen into four quarters.""" try: index = self.clients.index(client) except ValueError: # Layout not expecting this window so ignore it return # We're only showing first 4 windows if index > 3: client.hide() return # Unhide the window in case it was hiddent before client.unhide() # List to help us calculate x and y values of quarters = [ (0, 0), (0.5, 0), (0, 0.5), (0.5, 0.5) ] # Calculate size and position for each window xpos, ypos = quarters[index] x = int(screen_rect.width * xpos) + screen_rect.x y = int(screen_rect.height * ypos) + screen_rect.y w = screen_rect.width // 2 h = screen_rect.height // 2 if client is self.current_client: margin = self.margin_focused else: margin = self.margin_unfocused client.place( x, y, w - self.border_width * 2, h - self.border_width * 2, self.border_width, self.border_colour, margin=[margin] * 4, ) def add_client(self, client: Window) -> None: # Assumes self.clients is simple list self.clients.insert(0, client) self.current_client = client def remove(self, client: Window) -> Window | None: # Assumes self.clients is a simple list # Client already removed so ignore this if client not in self.clients: return None # Client is only window in the list elif len(self.clients) == 1: self.clients.remove(client) self.current_client = None # There are no other windows so return None return None else: # Find position of client in our list index = self.clients.index(client) # Remove client self.clients.remove(client) # Ensure the index value is not greater than list size # i.e. if we closed the last window in the list, we need to return # the first one (index 0). index %= len(self.clients) next_client = self.clients[index] self.current_client = next_client return next_client def focus_first(self) -> Window | None: if not self.clients: return None return self.client[0] def focus_last(self) -> Window | None: if not self.clients: return None return self.client[-1] def focus_next(self, win: Window) -> Window | None: try: return self.clients[self.clients.index(win) + 1] except IndexError: return None def focus_previous(self, win: Window) -> Window | None: if not self.clients or self.clients.index(win) == 0: return None try: return self.clients[self.clients.index(win) - 1] except IndexError: return None def next(self) -> None: if self.current_client is None: return # Get the next client or, if at the end of the list, get the first client = self.focus_next(self.current_client) or self.focus_first() self.group.focus(client, True) def previous(self) -> None: if self.current_client is None: return # Get the previous client or, if at the end of the list, get the last client = self.focus_previous(self.current_client) or self.focus_last() self.group.focus(client, True) def swap(self, c1: Window, c2: Window) -> None: if c1 not in self.clients and c2 not in self.clients: return index1 = self.clients.index(c1) index2 = self.clients.index(c2) self.clients[index1], self.clients[index2] = self.clients[index2], self.clients[index1] def focus(self, client: Window) -> None: if client not in self.clients: self.current_client = None return index = self.clients.index(client) # Check if window is not visible if index > 3: c = self.clients.pop(index) self.clients.insert(0, c) self.current_client = client def blur(self) -> None: self.current_client = None @expose_command def rotate(self, clockwise: bool = True) -> None: if not self.clients: return if clockwise: client = self.clients.pop(-1) self.clients.insert(0, client) else: client = self.clients.pop(0) self.clients.append(client) # Check if current client has been rotated off the screen if self.current_client and self.clients.index(self.current_client) > 3: if clockwise: self.current_client = self.clients[3] else: self.current_client = self.clients[0] # Redraw the layout self.group.layout_all() @expose_command def info(self) -> dict[str, Any]: inf = base.Layout.info(self) inf["clients"] = self.clients return inf This should result in a layout looking like this: |layout_image|. .. |layout_image| image:: ../../_static/layouts/twobytwo.png Getting help ============ If you still need help with developing your widget then please submit a question in the `qtile-dev group `_ or submit an issue on the github page if you believe there's an error in the codebase. qtile-0.31.0/docs/manual/howto/widget.rst0000664000175000017500000005474514762660347020237 0ustar epsilonepsilon.. _widget-creation: ====================== How to create a widget ====================== The aim of this page is to explain the main components of qtile widgets, how they work, and how you can use them to create your own widgets. .. note:: This page is not meant to be an exhaustive summary of everything needed to make a widget. It is highly recommended that users wishing to create their own widget refer to the source documentation of existing widgets to familiarise themselves with the code. However, the detail below may prove helpful when read in conjunction with the source code. What is a widget? ================= In Qtile, a widget is a small drawing that is displayed on the user's bar. The widget can display text, images and drawings. In addition, the widget can be configured to update based on timers, hooks, dbus_events etc. and can also respond to mouse events (clicks, scrolls and hover). Widget base classes =================== Qtile provides a number of base classes for widgets than can be used to implement commonly required features (e.g. display text). Your widget should inherit one of these classes. Whichever base class you inherit for your widget, if you override either the ``__init__`` and/or ``_configure`` methods, you should make sure that your widget calls the equivalent method from the superclass. .. code:: python class MyCustomWidget(base._TextBox): def __init__(self, **config): super().__init__("", **config) # My widget's initialisation code here The functions of the various base classes are explained further below. _Widget ------- This is the base widget class that defines the core components required for a widget. All other base classes are based off this class. This is like a blank canvas so you're free to do what you want but you don't have any of the extra functionality provided by the other base classes. The ``base._Widget`` class is therefore typically used for widgets that want to draw graphics on the widget as opposed to displaying text. _TextBox -------- The ``base._TextBox`` class builds on the bare widget and adds a ``drawer.TextLayout`` which is accessible via the ``self.layout`` property. The widget will adjust its size to fit the amount of text to be displayed. Text can be updated via the ``self.text`` property but note that this does not trigger a redrawing of the widget. Parameters including ``font``, ``fontsize``, ``fontshadow``, ``padding`` and ``foreground`` (font colour) can be configured. It is recommended not to hard-code these parameters as users may wish to have consistency across units. InLoopPollText -------------- The ``base.InLoopPollText`` class builds on the ``base._TextBox`` by adding a timer to periodically refresh the displayed text. Widgets using this class should override the ``poll`` method to include a function that returns the required text. .. note:: This loop runs in the event loop so it is important that the poll method does not call some blocking function. If this is required, widgets should inherit the ``base.ThreadPoolText`` class (see below). ThreadPoolText -------------- The ``base.ThreadPoolText`` class is very similar to the ``base.InLoopPollText`` class. The key difference is that the ``poll`` method is run asynchronously and triggers a callback once the function completes. This allows widgets to get text from long-running functions without blocking Qtile. Mixins ====== As well as inheriting from one of the base classes above, widgets can also inherit one or more mixins to provide some additional functionality to the widget. PaddingMixin ------------ This provides the ``padding(_x|_y|)`` attributes which can be used to change the appearance of the widget. If you use this mixin in your widget, you need to add the following line to your ``__init__`` method: .. code:: python self.add_defaults(base.PaddingMixin.defaults) MarginMixin ----------- The ``MarginMixin`` is essentially effectively exactly the same as the ``PaddingMixin`` but, instead, it provides the ``margin(_x|_y|)`` attributes. As above, if you use this mixin in your widget, you need to add the following line to your ``__init__`` method: .. code:: python self.add_defaults(base.MarginMixin.defaults) Configuration ============= Now you know which class to base your widget on, you need to know how the widget gets configured. Defining Parameters ------------------- Each widget will likely have a number of parameters that users can change to customise the look and feel and/or behaviour of the widget for their own needs. The widget should therefore provide the default values of these parameters as a class attribute called ``defaults``. The format of this attribute is a list of tuples. .. code:: python defaults = [ ("parameter_name", default_parameter_value, "Short text explaining what parameter does") ] Users can override the default value when creating their ``config.py`` file. .. code:: python MyCustomWidget(parameter_name=updated_value) Once the widget is initialised, these parameters are available at ``self.parameter_name``. The __init__ method ------------------- Parameters that should not be changed by users can be defined in the ``__init__`` method. This method is run when the widgets are initially created. This happens before the ``qtile`` object is available. The _configure method --------------------- The ``_configure`` method is called by the ``bar`` object and sets the ``self.bar`` and ``self.qtile`` attributes of the widget. It also creates the ``self.drawer`` attribute which is necessary for displaying any content. Once this method has been run, your widget should be ready to display content as the bar will draw once it has finished its configuration. Calls to methods required to prepare the content for your widget should therefore be made from this method rather than ``__init__``. Displaying output ================= A Qtile widget is just a drawing that is displayed at a certain location the user's bar. The widget's job is therefore to create a small drawing surface that can be placed in the appropriate location on the bar. The "draw" method ----------------- The ``draw`` method is called when the widget needs to update its appearance. This can be triggered by the widget itself (e.g. if the content has changed) or by the bar (e.g. if the bar needs to redraw its entire contents). This method therefore needs to contain all the relevant code to draw the various components that make up the widget. Examples of displaying text, icons and drawings are set out below. It is important to note that the bar controls the placing of the widget by assigning the ``offsetx`` value (for horizontal positioning) and ``offsety`` value (for vertical positioning). Widgets should use this at the end of the ``draw`` method. Both ``offsetx`` and ``offsety`` are required as both values will be set if the bar is drawing a border. .. code:: python self.drawer.draw(offsetx=self.offsetx, offsety=self.offsety, width=self.width) .. note:: If you need to trigger a redrawing of your widget, you should call ``self.draw()`` if the width of your widget is unchanged. Otherwise you need to call ``self.bar.draw()`` as this method means the bar recalculates the position of all widgets. Displaying text --------------- Text is displayed by using a ``drawer.TextLayout`` object. If all you are doing is displaying text then it's highly recommended that you use the ``base._TextBox`` superclass as this simplifies adding and updating text. If you wish to implement this manually then you can create a your own ``drawer.TextLayout`` by using the ``self.drawer.textlayout`` method of the widget (only available after the `_configure` method has been run). object to include in your widget. Some additional formatting of Text can be displayed using pango markup and ensuring the ``markup`` parameter is set to ``True``. .. code:: python self.textlayout = self.drawer.textlayout( "Text", "fffff", # Font colour "sans", # Font family 12, # Font size None, # Font shadow markup=False, # Pango markup (False by default) wrap=True # Wrap long lines (True by default) ) Displaying icons and images --------------------------- Qtile provides a helper library to convert images to a ``surface`` that can be drawn by the widget. If the images are static then you should only load them once when the widget is configured. Given the small size of the bar, this is most commonly used to draw icons but the same method applies to other images. .. code:: python from libqtile import images def setup_images(self): self.surfaces = {} # File names to load (will become keys to the `surfaces` dictionary) names = ( "audio-volume-muted", "audio-volume-low", "audio-volume-medium", "audio-volume-high" ) d_images = images.Loader(self.imagefolder)(*names) # images.Loader can take more than one folder as an argument for name, img in d_images.items(): new_height = self.bar.height - 1 img.resize(height=new_height) # Resize images to fit widget self.surfaces[name] = img.pattern # Images added to the `surfaces` dictionary Drawing the image is then just a matter of painting it to the relevant surface: .. code:: python def draw(self): self.drawer.ctx.set_source(self.surfaces[img_name]) # Use correct key here for your image self.drawer.ctx.paint() self.drawer.draw(offsetx=self.offset, width=self.length) Drawing shapes -------------- It is possible to draw shapes directly to the widget. The ``Drawer`` class (available in your widget after configuration as ``self.drawer``) provides some basic functions ``rounded_rectangle``, ``rounded_fillrect``, ``rectangle`` and ``fillrect``. In addition, you can access the `Cairo`_ context drawing functions via ``self.drawer.ctx``. .. _Cairo: https://pycairo.readthedocs.io/en/latest/reference/context.html For example, the following code can draw a wifi icon showing signal strength: .. code:: python import math ... def to_rads(self, degrees): return degrees * math.pi / 180.0 def draw_wifi(self, percentage): WIFI_HEIGHT = 12 WIFI_ARC_DEGREES = 90 y_margin = (self.bar.height - WIFI_HEIGHT) / 2 half_arc = WIFI_ARC_DEGREES / 2 # Draw grey background self.drawer.ctx.new_sub_path() self.drawer.ctx.move_to(WIFI_HEIGHT, y_margin + WIFI_HEIGHT) self.drawer.ctx.arc(WIFI_HEIGHT, y_margin + WIFI_HEIGHT, WIFI_HEIGHT, self.to_rads(270 - half_arc), self.to_rads(270 + half_arc)) self.drawer.set_source_rgb("666666") self.drawer.ctx.fill() # Draw white section to represent signal strength self.drawer.ctx.new_sub_path() self.drawer.ctx.move_to(WIFI_HEIGHT, y_margin + WIFI_HEIGHT) self.drawer.ctx.arc(WIFI_HEIGHT y_margin + WIFI_HEIGHT, WIFI_HEIGHT * percentage, self.to_rads(270 - half_arc), self.to_rads(270 + half_arc)) self.drawer.set_source_rgb("ffffff") self.drawer.ctx.fill() This creates something looking like this: |wifi_image|. .. |wifi_image| image:: ../../_static/widgets/widget_tutorial_wifi.png Background ---------- At the start of the ``draw`` method, the widget should clear the drawer by drawing the background. Usually this is done by including the following line at the start of the method: .. code:: python self.drawer.clear(self.background or self.bar.background) The background can be a single colour or a list of colours which will result in a linear gradient from top to bottom. Updating the widget =================== Widgets will usually need to update their content periodically. There are numerous ways that this can be done. Some of the most common ones are summarised below. Timers ------ A non-blocking timer can be called by using the ``self.timeout_add`` method. .. code:: python self.timeout_add(delay_in_seconds, method_to_call, (method_args)) .. note:: Consider using the ``ThreadPoolText`` superclass where you are calling a function repeatedly and displaying its output as text. Hooks ----- Qtile has a number of hooks built in which are triggered on certain events. The ``WindowCount`` widget is a good example of using hooks to trigger updates. It includes the following method which is run when the widget is configured: .. code:: python from libqtile import hook ... def _setup_hooks(self): hook.subscribe.client_killed(self._win_killed) hook.subscribe.client_managed(self._wincount) hook.subscribe.current_screen_change(self._wincount) hook.subscribe.setgroup(self._wincount) Read the :ref:`ref-hooks` page for details of which hooks are available and which arguments are passed to the callback function. Using dbus ---------- Qtile uses ``dbus-fast`` for interacting with dbus. If you just want to listen for signals then Qtile provides a helper method called ``add_signal_receiver`` which can subscribe to a signal and trigger a callback whenever that signal is broadcast. .. note:: Qtile uses the ``asyncio`` based functions of ``dbus-fast`` so your widget must make sure, where necessary, calls to dbus are made via coroutines. There is a ``_config_async`` coroutine in the base widget class which can be overridden to provide an entry point for asyncio calls in your widget. For example, the Mpris2 widget uses the following code: .. code:: python from libqtile.utils import add_signal_receiver ... async def _config_async(self): subscribe = await add_signal_receiver( self.message, # Callback function session_bus=True, signal_name="PropertiesChanged", bus_name=self.objname, path="/org/mpris/MediaPlayer2", dbus_interface="org.freedesktop.DBus.Properties") ``dbus-fast`` can also be used to query properties, call methods etc. on dbus interfaces. Refer to the `dbus-fast documentation `_ for more information on how to use the module. .. _mouse-events: Mouse events ============ By default, widgets handle button presses and will call any function that is bound to the button in the ``mouse_callbacks`` dictionary. The dictionary keys are as follows: - ``Button1``: Left click - ``Button2``: Middle click - ``Button3``: Right click - ``Button4``: Scroll up - ``Button5``: Scroll down - ``Button6``: Scroll left - ``Button7``: Scroll right You can then define your button bindings in your widget (e.g. in ``__init__``): .. code:: python class MyWidget(widget.TextBox) def __init__(self, *args, **config): widget.TextBox.__init__(self, *args, **kwargs) self.add_callbacks( { "Button1": self.left_click_method, "Button3": self.right_click_method } ) .. note:: As well as functions, you can also bind ``LazyCall`` objects to button presses. For example: .. code:: python self.add_callbacks( { "Button1": lazy.spawn("xterm"), } ) In addition to button presses, you can also respond to mouse enter and leave events. For example, to make a clock show a longer date when you put your mouse over it, you can do the following: .. code:: python class MouseOverClock(widget.Clock): defaults = [ ( "long_format", "%A %d %B %Y | %H:%M", "Format to show when mouse is over widget." ) ] def __init__(self, **config): widget.Clock.__init__(self, **config) self.add_defaults(MouseOverClock.defaults) self.short_format = self.format def mouse_enter(self, *args, **kwargs): self.format = self.long_format self.bar.draw() def mouse_leave(self, *args, **kwargs): self.format = self.short_format self.bar.draw() Exposing commands to the IPC interface ====================================== If you want to control your widget via ``lazy`` or scripting commands (such as ``qtile cmd-obj``), you will need to expose the relevant methods in your widget. Exposing commands is done by adding the ``@expose_command()`` decorator to your method. For example: .. code:: python from libqtile.command.base import expose_command from libqtile.widget import TextBox class ExposedWidget(TextBox): @expose_command() def uppercase(self): self.update(self.text.upper()) Text in the ``ExposedWidget`` can now be made into upper case by calling ``lazy.widget["exposedwidget"].uppercase()`` or ``qtile cmd-onj -o widget exposedwidget -f uppercase``. If you want to expose a method under multiple names, you can pass these additional names to the decorator. For example, decorating a method with: .. code:: python @expose_command(["extra", "additional"]) def mymethod(self): ... will make make the method visible under ``mymethod``, ``extra`` and ``additional``. Debugging ========= You can use the ``logger`` object to record messages in the Qtile log file to help debug your development. .. code:: python from libqtile.log_utils import logger ... logger.debug("Callback function triggered") .. note:: The default log level for the Qtile log is ``INFO`` so you may either want to change this when debugging or use ``logger.info`` instead. Debugging messages should be removed from your code before submitting pull requests. Submitting the widget to the official repo ========================================== The following sections are only relevant for users who wish for their widgets to be submitted as a PR for inclusion in the main Qtile repo. Including the widget in libqtile.widget --------------------------------------- You should include your widget in the ``widgets`` dict in ``libqtile.widget.__init__.py``. The relevant format is ``{"ClassName": "modulename"}``. This has a number of benefits: - Lazy imports - Graceful handling of import errors (useful where widget relies on third party modules) - Inclusion in basic unit testing (see below) Testing ------- Any new widgets should include an accompanying unit test. Basic initialisation and configurations (using defaults) will automatically be tested by ``test/widgets/test_widget_init_configure.py`` if the widget has been included in ``libqtile.widget.__init__.py`` (see above). However, where possible, it is strongly encouraged that widgets include additional unit tests that test specific functionality of the widget (e.g. reaction to hooks). See :ref:`unit-testing` for more. Documentation ------------- It is really important that we maintain good documentation for Qtile. Any new widgets must therefore include sufficient documentation in order for users to understand how to use/configure the widget. The majority of the documentation is generated automatically from your module. The widget's docstring will be used as the description of the widget. Any parameters defined in the widget's ``defaults`` attribute will also be displayed. It is essential that there is a clear explanation of each new parameter defined by the widget. Screenshots ~~~~~~~~~~~ While not essential, it is strongly recommended that the documentation includes one or more screenshots. Screenshots can be generated automatically with a minimal amount of coding by using the fixtures created by Qtile's test suite. A screenshot file must satisfy the following criteria: - Be named ``ss_[widgetname].py`` - Any function that takes a screenshot must be prefixed with ``ss_`` - Define a pytest fixture named ``widget`` An example screenshot file is below: .. code:: python import pytest from libqtile.widget import wttr from test.widgets.docs_screenshots.conftest import vertical_bar, widget_config RESPONSE = "London: +17°C" @pytest.fixture def widget(monkeypatch): def result(self): return RESPONSE monkeypatch.setattr("libqtile.widget.wttr.Wttr.fetch", result) yield wttr.Wttr @widget_config([{"location": {"London": "Home"}}]) def ss_wttr(screenshot_manager): screenshot_manager.take_screenshot() @vertical_bar def ss_wttr_vertical(screenshot_manager): screenshot_manager.take_screenshot() The ``widget`` fixture returns the widget class (not an instance of the widget). Any monkeypatching of the widget should be included in this fixture. The screenshot function (here, called ``ss_wttr``) must take an argument called ``screenshot_manager``. The function can also be parameterized, in which case, each dict object will be used to configure the widget for the screenshot (and the configuration will be displayed in the docs). If you want to include parameterizations but also want to show the default configuration, you should include an empty dict (``{}``) as the first object in the list. Taking a screenshot is then as simple as calling ``screenshot_manager.take_screenshot()``. The method can be called multiple times in the same function. Screenshots can also be taken in a vertical bar orientation by using the ``@vertical_bar`` decorator as shown in the above example. ``screenshot_manager.take_screenshot()`` only takes a picture of the widget. If you need to take a screenshot of the bar then you need a few extra steps: .. code:: python def ss_bar_screenshot(screenshot_manager): # Generate a filename for the screenshot target = screenshot_manager.target() # Get the bar object bar = screenshot_manager.c.bar["top"] # Take a screenshot. Will take screenshot of whole bar unless # a `width` parameter is set. bar.take_screenshot(target, width=width) Getting help ============ If you still need help with developing your widget then please submit a question in the `qtile-dev group `_ or submit an issue on the github page if you believe there's an error in the codebase. qtile-0.31.0/docs/manual/howto/git.rst0000664000175000017500000001212314762660347017517 0ustar epsilonepsilon.. _using-git: ============= Using ``git`` ============= ``git`` is the version control system that is used to manage all of the source code. It is very powerful, but might be frightening at first. This page should give you a quick overview, but for a complete guide you will have to search the web on your own. Another great resource to get started practically without having to try out the newly-learned commands on a pre-existing repository is `learn git branching `_. You should probably learn the basic ``git`` vocabulary and then come back to find out how you can use all that practically. This guide will be oriented on how to create a pull request and things might be in a different order compared to the introductory guides. .. warning:: This guide is not complete and never will be. If something isn't clear, consult other sources until you are confident you know what you are doing. I want to try out a feature somebody is working on ================================================== If you see a pull request on `GitHub `_ that you want to try out, have a look at the line where it says:: user wants to merge n commits into qtile:master from user:branch Right now you probably have one *remote* from which you can fetch changes, the ``origin``. If you cloned ``qtile/qtile``, ``git remote show origin`` will spit out the *upstream* url. If you cloned your fork, ``origin`` points to it and you probably want to ``git remote add upstream https://www.github.com/qtile/qtile``. To try out somebody's work, you can add their fork as a new remote:: git remote add https://www.github.com/user/qtile where you fill in the username from the line we asked you to search for before. Then you can load data from that remote with ``git fetch`` and then ultimately check out the branch with ``git checkout /``. **Alternatively**, it is also possible to fetch and checkout pull requests without needing to add other remotes. The upstream remote is sufficient:: git fetch upstream pull//head:pr git checkout pr The numeric pull request id can be found in the url or next to the title (preceeded by a # symbol). .. note:: Having the feature branch checked out doesn't mean that it is installed and will be loaded when you restart qtile. You might still need to install it with ``pip``. I committed changes and the tests failed ======================================== You can easily change your last commit: After you have done your work, ``git add`` everything you need and use ``git commit --amend`` to change your last commit. This causes the git history of your local clone to be diverged from your fork on GitHub, so you need to force-push your changes with:: git push -f where origin might be your user name or ``origin`` if you cloned your fork and feature-branch is to be replaced by the name of the branch you are working on. Assuming the feature branch is currently checked out, you can usually omit it and just specify the origin. I was told to rebase my work ============================ If *upstream/master* is changed and you happened to change the same files as the commits that were added upstream, you should rebase your work onto the most recent *upstream/master*. Checkout your master, pull from *upstream*, checkout your branch again and then rebase it:: git checkout master git pull upstream/master git checkout git rebase upstream/master You will be asked to solve conflicts where your diff cannot be applied with confidence to the work that was pushed upstream. If that is the case, open the files in your text editor and resolve the conflicts manually. You possibly need to ``git rebase --continue`` after you have resolved conflicts for one commit if you are rebasing multiple commits. Note that the above doesn't work if you didn't create a branch. In that case you will find guides elsewhere to fix this problem, ideally by creating a branch and resetting your master branch to where it should be. I was told to squash some commits ================================= If you introduce changes in one commit and replace them in another, you are told to squash these changes into one single commit without the intermediate step:: git rebase -i master opens a text editor with your commits and a comment block reminding you what you can do with your commits. You can reword them to change the commit message, reorder them or choose ``fixup`` to squash the changes of a commit into the commit on the line above. This also changes your git history and you will need to force-push your changes afterwards. Note that interactive rebasing also allows you to split, reorder and edit commits. I was told to edit a commit message =================================== If you need to edit the commit message of the last commit you did, use:: git commit --amend to open an editor giving you the possibility to reword the message. If you want to reword the message of an older commit or multiple commits, use ``git rebase -i`` as above with the ``reword`` command in the editor. qtile-0.31.0/docs/manual/troubleshooting.rst0000664000175000017500000000607014762660347021027 0ustar epsilonepsilon=============== Troubleshooting =============== So something has gone wrong... what do you do? ============================================== When Qtile is running, it logs error messages (and other messages) to its log file. This is found at ``~/.local/share/qtile/qtile.log``. This is the first place to check to see what is going on. If you are getting unexpected errors from normal usage or your configuration (and you're not doing something wacky) and believe you have found a bug, then please :ref:`report a bug `. If you are :ref:`hacking on Qtile ` and you want to debug your changes, this log is your best friend. You can send messages to the log from within libqtile by using the ``logger``: .. code-block:: python from libqtile.log_utils import logger logger.warning("Your message here") logger.warning(variable_you_want_to_print) try: # some changes here that might error except Exception: logger.exception("Uh oh!") ``logger.warning`` is convenient because its messages will always be visibile in the log. ``logger.exception`` is helpful because it will print the full traceback of an error to the log. By sticking these amongst your changes you can look more closely at the effects of any changes you made to Qtile's internals. .. _capturing-an-xtrace: X11: Capturing an ``xtrace`` ============================ Occasionally, a bug will be low level enough to require an ``xtrace`` of Qtile's conversations with the X server. To capture one of these, create an ``xinitrc`` or similar file with: .. code-block:: bash exec xtrace qtile >> ~/qtile.log This will put the xtrace output in Qtile's logfile as well. You can then demonstrate the bug, and paste the contents of this file into the bug report. Note that xtrace may be named ``x11trace`` on some platforms, for example, on Fedora and Arch. .. _debugging-wayland: Debugging in Wayland ===================== To get incredibly verbose output of communications between clients and the server, you can set ``WAYLAND_DEBUG=1`` in the environment before starting the process. This applies to the server itself, so be aware that running ``qtile`` with this set will generate lots of output for Qtile **and** all clients that it launches. If you're including this output with a bug report please try to cut out just the relevant portions. If you're hacking on Qtile and would like this debug log output for it rather than any clients, it can be helpful to run the helper script at ``scripts/wephyr`` in the source from an existing session. You can then run clients from another terminal using the ``WAYLAND_DISPLAY`` value printed by Qtile, so that the debug logs printed by Qtile are only the server's. If you suspect a client may be responsible for a bug, it can be helpful to look at the issue trackers for other compositors, such as `sway `_. Similarly if you're hacking on Qtile's internals and think you've found an unexpected quirk it may be helpful to search the issue tracker for `wlroots `_. qtile-0.31.0/docs/manual/faq.rst0000664000175000017500000002103714762660347016347 0ustar epsilonepsilon========================== Frequently Asked Questions ========================== Why the name Qtile? =================== Users often wonder, why the Q? Does it have something to do with Qt? No. Below is an IRC excerpt where cortesi explains the great trial that ultimately brought Qtile into existence, thanks to the benevolence of the Open Source Gods. Praise be to the OSG! :: ramnes: what does Qtile mean? ramnes: what's the Q? @tych0: ramnes: it doesn't :) @tych0: cortesi was just looking for the first letter that wasn't registered in a domain name with "tile" as a suffix @tych0: qtile it was :) cortesi: tych0, dx: we really should have something more compelling to explain the name. one day i was swimming at manly beach in sydney, where i lived at the time. suddenly, i saw an enormous great white right beside me. it went for my leg with massive, gaping jaws, but quick as a flash, i thumb-punched it in both eyes. when it reared back in agony, i saw that it had a jagged, gnarly scar on its stomach... a scar shaped like the letter "Q". cortesi: while it was distracted, i surfed a wave to shore. i knew that i had to dedicate my next open source project to the ocean gods, in thanks for my lucky escape. and thus, qtile got its name... When I first start xterm/urxvt/rxvt containing an instance of Vim, I see text and layout corruption. What gives? ================================================================================================================ Vim is not handling terminal resizes correctly. You can fix the problem by starting your xterm with the "-wf" option, like so: .. code-block:: bash xterm -wf -e vim Alternatively, you can just cycle through your layouts a few times, which usually seems to fix it. How do I know which modifier specification maps to which key? ============================================================= To see a list of modifier names and their matching keys, use the ``xmodmap`` command. On my system, the output looks like this: .. code-block:: bash $ xmodmap xmodmap: up to 3 keys per modifier, (keycodes in parentheses): shift Shift_L (0x32), Shift_R (0x3e) lock Caps_Lock (0x9) control Control_L (0x25), Control_R (0x69) mod1 Alt_L (0x40), Alt_R (0x6c), Meta_L (0xcd) mod2 Num_Lock (0x4d) mod3 mod4 Super_L (0xce), Hyper_L (0xcf) mod5 ISO_Level3_Shift (0x5c), Mode_switch (0xcb) My "pointer mouse cursor" isn't the one I expect it to be! ========================================================== Qtile should set the default cursor to left_ptr, you must install xcb-util-cursor if you want support for themed cursors. LibreOffice menus don't appear or don't stay visible ==================================================== A workaround for problem with the mouse in libreoffice is setting the environment variable »SAL_USE_VCLPLUGIN=gen«. It is dependent on your system configuration as to where to do this. e.g. ArchLinux with libreoffice-fresh in /etc/profile.d/libreoffice-fresh.sh. How can I get my groups to stick to screens? ============================================ This behaviour can be replicated by configuring your keybindings to not move groups between screens. For example if you want groups ``"1"``, ``"2"`` and ``"3"`` on one screen and ``"q"``, ``"w"``, and ``"e"`` on the other, instead of binding keys to ``lazy.group[name].toscreen()``, use this: .. code-block:: python groups = [ # Screen affinity here is used to make # sure the groups startup on the right screens Group(name="1", screen_affinity=0), Group(name="2", screen_affinity=0), Group(name="3", screen_affinity=0), Group(name="q", screen_affinity=1), Group(name="w", screen_affinity=1), Group(name="e", screen_affinity=1), ] def go_to_group(name: str): def _inner(qtile): if len(qtile.screens) == 1: qtile.groups_map[name].toscreen() return if name in '123': qtile.focus_screen(0) qtile.groups_map[name].toscreen() else: qtile.focus_screen(1) qtile.groups_map[name].toscreen() return _inner for i in groups: keys.append(Key([mod], i.name, lazy.function(go_to_group(i.name)))) To be able to move windows across these groups which switching groups, a similar function can be used: .. code-block:: python def go_to_group_and_move_window(name: str): def _inner(qtile): if len(qtile.screens) == 1: qtile.current_window.togroup(name, switch_group=True) return if name in "123": qtile.current_window.togroup(name, switch_group=False) qtile.focus_screen(0) qtile.groups_map[name].toscreen() else: qtile.current_window.togroup(name, switch_group=False) qtile.focus_screen(1) qtile.groups_map[name].toscreen() return _inner for i in groups: keys.append(Key([mod, "shift"], i.name, lazy.function(go_to_group_and_move_window(i.name)))) If you use the ``GroupBox`` widget you can make it reflect this behaviour: .. code-block:: python groupbox1 = widget.GroupBox(visible_groups=['1', '2', '3']) groupbox2 = widget.GroupBox(visible_groups=['q', 'w', 'e']) And if you jump between having single and double screens then modifying the visible groups on the fly may be useful: .. code-block:: python @hook.subscribe.screens_reconfigured async def _(): if len(qtile.screens) > 1: groupbox1.visible_groups = ['1', '2', '3'] else: groupbox1.visible_groups = ['1', '2', '3', 'q', 'w', 'e'] if hasattr(groupbox1, 'bar'): groupbox1.bar.draw() Where can I find example configurations and other scripts? ========================================================== Please visit our `qtile-examples`_ repo which contains examples of users' configurations, scripts and other useful links. .. _`qtile-examples`: https://github.com/qtile/qtile-examples Where are the log files for Qtile? ================================== The log files for qtile are at ``~/.local/share/qtile/qtile.log``. How can I match the bar with picom? =================================== You can use ``"QTILE_INTERNAL:32c = 1"`` in your picom.conf to match the bar. This will match all internal Qtile windows, so if you want to avoid that or to target bars individually, you can set a custom property and match that: .. code-block:: python mybar = Bar(...) @hook.subscribe.startup def _(): mybar.window.window.set_property("QTILE_BAR", 1, "CARDINAL", 32) This would enable matching on ``mybar``'s window using ``"QTILE_BAR:32c = 1"``. See `2526`_ and `1515`_ for more discussion. .. _`2526`: https://github.com/qtile/qtile/issues/2526 .. _`1515`: https://github.com/qtile/qtile/issues/1515 Why do get a warning that fonts cannot be loaded? ================================================= When installing Qtile on a new system, when running the test suite or the Xephyr script (``./scripts/xephyr``), you might see errors in the output like the following or similar: * Xephyr script:: xterm: cannot load font "-Misc-Fixed-medium-R-*-*-13-120-75-75-C-120-ISO10646-1" xterm: cannot load font "-misc-fixed-medium-r-semicondensed--13-120-75-75-c-60-iso10646-1" * ``pytest``:: ---------- Captured stderr call ---------- Warning: Cannot convert string "8x13" to type FontStruct Warning: Unable to load any usable ISO8859 font Warning: Unable to load any usable ISO8859 font Error: Aborting: no font found -------- Captured stderr teardown -------- Qtile exited with exitcode: -9 If it happens, it might be because you're missing fonts on your system. On ArchLinux, you can fix this by installing ``xorg-fonts-misc``:: sudo pacman -S xorg-fonts-misc Try to search for "xorg fonts misc" with your distribution name on the internet to find how to install them. I've upgraded and Qtile's broken. What do I do? =============================================== If you've recently upgraded, the first thing to do is check the :doc:`changelog ` and see if any breaking changes were made. Next, check your log file (see above) to see if any error messages explain what the problem is. If you're still stuck, come and ask for help on Discord, IRC or GitHub. qtile-0.31.0/docs/manual/stacking.rst0000664000175000017500000000475614762660347017414 0ustar epsilonepsilonWindow stacking =============== A number of window commands (``move_up/down()``, ``bring_to_front()`` etc.) relate to the stacking order of windows. The aim of this page is to provide more details as to how stacking is implemented in Qtile. .. important:: Currently, stacking is only implemented in the X11 background. Support will be added to the Wayland backend in future and this page will be updated accordingly. Layer priority groups ~~~~~~~~~~~~~~~~~~~~~ We have tried to adhere to the `EWMH specification`_. Windows are therefore stacked, from the bottom, according to the following priority rules: - windows of type _NET_WM_TYPE_DESKTOP - windows having state _NET_WM_STATE_BELOW - windows not belonging in any other layer - windows of type _NET_WM_TYPE_DOCK (unless they have state _NET_WM_TYPE_BELOW) and windows having state _NET_WM_STATE_ABOVE - focused windows having state _NET_WM_STATE_FULLSCREEN Qtile had then added an additional layer so that ``Scratchpad`` windows are placed above everything else. .. _EWMH specification: https://specifications.freedesktop.org/wm-spec/1.3/ar01s07.html#STACKINGORDER Tiled windows will open in the default, "windows not belonging in any other layer", layer. If ``floats_kept_above`` is set to ``True`` in the config then new floating windows will have the ``_NET_WM_STATE_ABOVE`` property set which will ensure they remain above tiled windows. Moving windows ~~~~~~~~~~~~~~ Imagine you have four tiled windows stacked (from the top) as follows: .. code:: "One" "Two" "Three" "Four" If you call ``move_up()`` on window "Four", the result will be: .. code:: "One" "Two" "Four" "Three" If you now call ``move_to_top()`` on window "Three", the result will be: .. code:: "Three" "One" "Two" "Four" .. note:: ``bring_to_front()`` has a special behaviour in Qtile. This will bring any window to the very top of the stack, disregarding the priority rules set out above. When that window loses focus, it will be restacked in the appropriate location. This can cause undesirable results if the config contains ``bring_front_click=True`` and the user has an app like a dock which is activated by mousing over the window. In this situation, tiled windows will be displayed above the dock making it difficult to activate. To fix this, set ``bring_front_click`` to ``False`` to disable the behaviour completely, or ``"floating_only"`` to only have this behaviour apply to floating windows. qtile-0.31.0/docs/manual/config/0000775000175000017500000000000014762660347016310 5ustar epsilonepsilonqtile-0.31.0/docs/manual/config/match.rst0000664000175000017500000000563414762660347020146 0ustar epsilonepsilon.. _match: ================ Matching windows ================ Qtile's config provides a number of situations where the behaviour depends on whether the relevant window matches some specified criteria. These situations include: - Defining which windows should be floated by default - Assigning windows to specific groups - Assigning window to a master section of a layout In each instance, the criteria are defined via a ``Match`` object. The properties of the object will be compared to a :class:`~libqtile.base.Window` to determine if its properties *match*. It can match by title, wm_class, role, wm_type, wm_instance_class, net_wm_pid, or wid. Additionally, a function may be passed, which takes in the :class:`~libqtile.base.Window` to be compared against and returns a boolean. A basic rule would therefore look something like: .. code:: python Match(wm_class="mpv") This would match against any window whose class was ``mpv``. Where a string is provided as an argument then the value must match exactly. More flexibility can be achieved by using regular expressions. For example: .. code:: python import re Match(wm_class=re.compile(r"mpv")) This would still match a window whose class was ``mpv`` but it would also match any class starting with ``mpv`` e.g. ``mpvideo``. .. note:: When providing a regular expression, qtile applies the ``.match`` method. This matches from the start of the string so, if you want to match any substring, you will need to adapt the regular expression accordingly e.g. .. code:: python import re Match(wm_class=re.compile(r".*mpv")) This would match any string containing ``mpv`` Creating advanced rules ======================= While the ``func`` parameter allows users to create more complex matches, this requires a knowledge of qtile's internal objects. An alternative is to combine Match objects using logical operators ``&`` (and), ``|`` (or), ``~`` (not) and ``^`` (xor). For example, to create rule that matches all windows with a fixed aspect ratio except for mpv windows, you would provide the following: .. code:: python Match(func=lambda c: c.has_fixed_ratio()) & ~Match(wm_class="mpv") It is also possible to use wrappers for ``Match`` objects if you do not want to use the operators. The following wrappers are available: - ``MatchAll(Match(...), ...)`` equivalent to "and" test. All matches must match. - ``MatchAny(Match(...), ...)`` equivalent to "or" test. At least one match must match. - ``MatchOnlyOne(Match(...), Match(...))`` equivalent to "xor". Only one match must match. - ``InvertMatch(Match(...))`` equivalent to "not". Inverts the result of the match. So, to recreate the above rule using the wrappers, you would write the following: .. code:: python from libqtile.config import InvertMatch, Match, MatchAll MatchAll(Match(func=lambda c: c.has_fixed_ratio()), InvertMatch(Match(wm_class="mpv"))) qtile-0.31.0/docs/manual/config/groups.rst0000664000175000017500000000672214762660347020370 0ustar epsilonepsilon====== Groups ====== A group is a container for a bunch of windows, analogous to workspaces in other window managers. Each client window managed by the window manager belongs to exactly one group. The ``groups`` config file variable should be initialized to a list of :class:`~libqtile.config.Group` objects. :class:`~libqtile.config.Group` objects provide several options for group configuration. Groups can be configured to show and hide themselves when they're not empty, spawn applications for them when they start, automatically acquire certain groups, and various other options. Example ------- :: from libqtile.config import Group, Match groups = [ Group("a"), Group("b"), Group("c", matches=[Match(wm_class="Firefox")]), ] # allow mod3+1 through mod3+0 to bind to groups; if you bind your groups # by hand in your config, you don't need to do this. from libqtile.dgroups import simple_key_binder dgroups_key_binder = simple_key_binder("mod3") Reference --------- .. qtile_class:: libqtile.config.Group .. autofunction:: libqtile.dgroups.simple_key_binder Group Matching ============== .. qtile_class:: libqtile.config.Match :no-commands: .. qtile_class:: libqtile.config.Rule :no-commands: ScratchPad and DropDown ======================= :class:`~libqtile.config.ScratchPad` is a special - by default invisible - group which acts as a container for :class:`~libqtile.config.DropDown` configurations. A :class:`~libqtile.config.DropDown` can be configured to spawn a defined process and bind thats process' window to it. The associated window can then be shown and hidden by the lazy command :meth:`dropdown_toggle` (see :ref:`lazy`) from the ScratchPad group. Thus - for example - your favorite terminal emulator turns into a quake-like terminal by the control of Qtile. If the DropDown window turns visible it is placed as a floating window on top of the current group. If the DropDown is hidden, it is simply switched back to the ScratchPad group. Example ------- :: from libqtile.config import Group, ScratchPad, DropDown, Key from libqtile.lazy import lazy groups = [ ScratchPad("scratchpad", [ # define a drop down terminal. # it is placed in the upper third of screen by default. DropDown("term", "urxvt", opacity=0.8), # define another terminal exclusively for ``qtile shell` at different position DropDown("qtile shell", "urxvt -hold -e 'qtile shell'", x=0.05, y=0.4, width=0.9, height=0.6, opacity=0.9, on_focus_lost_hide=True) ]), Group("a"), ] keys = [ # toggle visibiliy of above defined DropDown named "term" Key([], 'F11', lazy.group['scratchpad'].dropdown_toggle('term')), Key([], 'F12', lazy.group['scratchpad'].dropdown_toggle('qtile shell')), ] Note that if the window is set to not floating, it is detached from DropDown and ScratchPad, and a new process is spawned next time the DropDown is set visible. Some programs run in a server-like mode where the spawned process does not directly own the window that is created, which is instead created by a background process. In this case, the window may not be correctly caught in the scratchpad group. To work around this, you can pass a :class:`~libqtile.config.Match` object to the corresponding :class:`~libqtile.config.DropDown`. See below. Reference --------- .. qtile_class:: libqtile.config.ScratchPad .. qtile_class:: libqtile.config.DropDown qtile-0.31.0/docs/manual/config/lazy.rst0000664000175000017500000001605314762660347020026 0ustar epsilonepsilon.. _lazy: ============ Lazy objects ============ Lazy objects are a way of executing any of the commands available in Qtile's :doc:`commands API `. The name "lazy" refers to the fact that the commands are not executed at the time of the call. Instead, the lazy object creates a reference to the relevant command and this is only executed when the relevant event is triggered (e.g. on a keypress). Typically, for config files, the commands are used to manipulate windows, layouts and groups as well application commands like exiting, restarting, reloading the config file etc. Example ------- :: from libqtile.config import Key from libqtile.lazy import lazy keys = [ Key( ["mod1"], "k", lazy.layout.down() ), Key( ["mod1"], "j", lazy.layout.up() ) ] .. note:: As noted above, ``lazy`` calls do not call the relevant command but only create a reference to it. While this makes it ideal for binding commands to key presses and ``mouse_callbacks`` for widgets, it also means that ``lazy`` calls cannot be included in user-defined functions. Lazy functions ============== This is overview of the commonly used functions for the key bindings. These functions can be called from commands on the REPLACE object or on another object in the command tree. Some examples are given below. For a complete list of available commands, please refer to :doc:`/manual/commands/api/index`. General functions ----------------- .. list-table:: :widths: 20 80 :header-rows: 1 * - function - description * - ``lazy.spawn("application")`` - Run the ``application`` * - ``lazy.spawncmd()`` - Open command prompt on the bar. See prompt widget. * - ``lazy.reload_config()`` - Reload the config. * - ``lazy.restart()`` - Restart Qtile. In X11, it won't close your windows. * - ``lazy.shutdown()`` - Close the whole Qtile Group functions --------------- .. list-table:: :widths: 20 80 :header-rows: 1 * - function - description * - ``lazy.next_layout()`` - Use next layout on the actual group * - ``lazy.prev_layout()`` - Use previous layout on the actual group * - ``lazy.screen.next_group()`` - Move to the group on the right * - ``lazy.screen.prev_group()`` - Move to the group on the left * - ``lazy.screen.toggle_group()`` - Move to the last visited group * - ``lazy.group.next_window()`` - Switch window focus to next window in group * - ``lazy.group.prev_window()`` - Switch window focus to previous window in group * - ``lazy.group["group_name"].toscreen()`` - Move to the group called ``group_name``. Takes an optional ``toggle`` parameter (defaults to False). If this group is already on the screen, it does nothing by default; to toggle with the last used group instead, use ``toggle=True``. * - ``lazy.layout.increase_ratio()`` - Increase the space for master window at the expense of slave windows * - ``lazy.layout.decrease_ratio()`` - Decrease the space for master window in the advantage of slave windows Window functions ---------------- .. list-table:: :widths: 20 80 :header-rows: 1 * - function - description * - ``lazy.window.kill()`` - Close the focused window * - ``lazy.layout.next()`` - Switch window focus to other pane(s) of stack * - ``lazy.window.togroup("group_name")`` - Move focused window to the group called ``group_name`` * - ``lazy.window.toggle_floating()`` - Put the focused window to/from floating mode * - ``lazy.window.toggle_fullscreen()`` - Put the focused window to/from fullscreen mode * - ``lazy.window.move_up()`` - Move the window above the next window in the stack. * - ``lazy.window.move_down()`` - Move the window below the previous window in the stack. * - ``lazy.window.move_to_top()`` - Move the window above all other windows with similar priority (i.e. a "normal" window will not be moved above a ``kept_above`` window). * - ``lazy.window.move_to_bottom()`` - Move the window below all other windows with similar priority (i.e. a "normal" window will not be moved below a ``kept_below`` window). * - ``lazy.window.keep_above()`` - Keep window above other windows. * - ``lazy.window.keep_below()`` - Keep window below other windows. * - ``lazy.window.bring_to_front()`` - Bring window above all other windows. Ignores ``kept_above`` priority. Screen functions ---------------- .. list-table:: :widths: 20 80 :header-rows: 1 * - function - description * - ``lazy.screen.set_wallpaper(path, mode=None)`` - Set the wallpaper to the specificied image. Possible modes: ``None`` no resizing, ``'fill'`` centre and resize to fill screen, ``'stretch'`` stretch to fill screen. ScratchPad DropDown functions ----------------------------- .. list-table:: :widths: 20 80 :header-rows: 1 * - function - description * - ``lazy.group["group_name"].dropdown_toggle("name")`` - Toggles the visibility of the specified DropDown window. On first use, the configured process is spawned. * - ``lazy.group["group_name"].hide_all()`` - Hides all DropDown windows. * - ``lazy.group["group_name"].dropdown_reconfigure("name", **configuration)`` - Update the configuration of the named DropDown. User-defined functions ---------------------- .. list-table:: :widths: 20 80 :header-rows: 1 * - function - description * - ``lazy.function(func, *args, **kwargs)`` - Calls ``func(qtile, *args, **kwargs)``. NB. the ``qtile`` object is automatically passed as the first argument. Examples -------- ``lazy.function`` can also be used as a decorator for functions. :: from libqtile.config import Key from libqtile.lazy import lazy @lazy.function def my_function(qtile): ... keys = [ Key( ["mod1"], "k", my_function ) ] Additionally, you can pass arguments to user-defined function in one of two ways: 1) In-line definition Arguments can be added to the ``lazy.function`` call. :: from libqtile.config import Key from libqtile.lazy import lazy from libqtile.log_utils import logger def multiply(qtile, value, multiplier=10): logger.warning(f"Multiplication results: {value * multiplier}") keys = [ Key( ["mod1"], "k", lazy.function(multiply, 10, multiplier=2) ) ] 2) Decorator Arguments can also be passed to the decorated function. :: from libqtile.config import Key from libqtile.lazy import lazy from libqtile.log_utils import logger @lazy.function def multiply(qtile, value, multiplier=10): logger.warning(f"Multiplication results: {value * multiplier}") keys = [ Key( ["mod1"], "k", multiply(10, multiplier=2) ) ] qtile-0.31.0/docs/manual/config/screens.rst0000664000175000017500000001237714762660347020516 0ustar epsilonepsilon======= Screens ======= The ``screens`` configuration variable is where the physical screens, their associated ``bars``, and the ``widgets`` contained within the bars are defined (see :ref:`ref-widgets` for a listing of available widgets). Example ======= Tying together screens, bars and widgets, we get something like this: :: from libqtile.config import Screen from libqtile import bar, widget window_name = widget.WindowName() screens = [ Screen( bottom=bar.Bar([ widget.GroupBox(), window_name, ], 30), ), Screen( bottom=bar.Bar([ widget.GroupBox(), window_name, ], 30), ) ] Note that a widget can be passed to multiple bars (and likewise multiple times to the same bar). Its contents is mirrored across all copies so this is useful where you want identical content (e.g. the name of the focussed window, like in this example). Bars support both solid background colors and gradients by supplying a list of colors that make up a linear gradient. For example, :code:`bar.Bar(..., background="#000000")` will give you a black back ground (the default), while :code:`bar.Bar(..., background=["#000000", "#FFFFFF"])` will give you a background that fades from black to white. Bars (and widgets) also support transparency by adding an alpha value to the desired color. For example, :code:`bar.Bar(..., background="#00000000")` will result in a fully transparent bar. Widget contents will not be impacted i.e. this is different to the ``opacity`` parameter which sets the transparency of the entire window. .. note:: In X11 backends, transparency will be disabled in a bar if the ``background`` color is fully opaque. Users can add borders to the bar by using the ``border_width`` and ``border_color`` parameters. Providing a single value sets the value for all four sides while sides can be customised individually by setting four values in a list (top, right, bottom, left) e.g. ``border_width=[2, 0, 2, 0]`` would draw a border 2 pixels thick on the top and bottom of the bar. Multiple Screens ================ You will see from the example above that ``screens`` is a list of individual ``Screen`` objects. The order of the screens in this list should match the order of screens as seen by your display server. X11 ~~~ You can view the current order of your screens by running ``xrandr --listmonitors``. Examples of how to set the order of your screens can be found on the `Arch wiki `_. Wayland ~~~~~~~ The Wayland backend supports the wlr-output-management protocol for configuration of outputs by tools such as `Kanshi `_. Fake Screens ============ instead of using the variable `screens` the variable `fake_screens` can be used to set split a physical monitor into multiple screens. They can be used like this: :: from libqtile.config import Screen from libqtile import bar, widget # screens look like this # 600 300 # |-------------|-----| # | 480| |580 # | A | B | # |----------|--| | # | 400|--|-----| # | C | |400 # |----------| D | # 500 |--------| # 400 # # Notice there is a hole in the middle # also D goes down below the others fake_screens = [ Screen( bottom=bar.Bar( [ widget.Prompt(), widget.Sep(), widget.WindowName(), widget.Sep(), widget.Systray(), widget.Sep(), widget.Clock(format='%H:%M:%S %d.%m.%Y') ], 24, background="#555555" ), x=0, y=0, width=600, height=480 ), Screen( top=bar.Bar( [ widget.GroupBox(), widget.WindowName(), widget.Clock() ], 30, ), x=600, y=0, width=300, height=580 ), Screen( top=bar.Bar( [ widget.GroupBox(), widget.WindowName(), widget.Clock() ], 30, ), x=0, y=480, width=500, height=400 ), Screen( top=bar.Bar( [ widget.GroupBox(), widget.WindowName(), widget.Clock() ], 30, ), x=500, y=580, width=400, height=400 ), ] Third-party bars ================ There might be some reasons to use third-party bars. For instance you can come from another window manager and you have already configured dzen2, xmobar, or something else. They definitely can be used with Qtile too. In fact, any additional configurations aren't needed. Just run the bar and qtile will adapt. Reference ========= .. qtile_class:: libqtile.config.Screen .. qtile_class:: libqtile.bar.Bar .. qtile_class:: libqtile.bar.Gap qtile-0.31.0/docs/manual/config/layouts.rst0000664000175000017500000000135714762660347020550 0ustar epsilonepsilon======= Layouts ======= A layout is an algorithm for laying out windows in a group on your screen. Since Qtile is a tiling window manager, this usually means that we try to use space as efficiently as possible, and give the user ample commands that can be bound to keys to interact with layouts. The ``layouts`` variable defines the list of layouts you will use with Qtile. The first layout in the list is the default. If you define more than one layout, you will probably also want to define key bindings to let you switch to the next and previous layouts. See :ref:`ref-layouts` for a listing of available layouts. Example ======= :: from libqtile import layout layouts = [ layout.Max(), layout.Stack(stacks=2) ] qtile-0.31.0/docs/manual/config/default.rst0000664000175000017500000000054214762660347020467 0ustar epsilonepsilon.. _default_config: =================== Default Config File =================== The below default config file is included with the Qtile package and will be copied to your home config folder (~/.config/qtile/config.py) if no config file exists when you start Qtile for the first time. .. literalinclude:: ../../../libqtile/resources/default_config.py qtile-0.31.0/docs/manual/config/hooks.rst0000664000175000017500000000457614762660347020201 0ustar epsilonepsilon===== Hooks ===== Qtile provides a mechanism for subscribing to certain events in ``libqtile.hook``. To subscribe to a hook in your configuration, simply decorate a function with the hook you wish to subscribe to. See :ref:`ref-hooks` for a listing of available hooks. Examples ======== Automatic floating dialogs -------------------------- Let's say we wanted to automatically float all dialog windows (this code is not actually necessary; Qtile floats all dialogs by default). We would subscribe to the ``client_new`` hook to tell us when a new window has opened and, if the type is "dialog", as can set the window to float. In our configuration file it would look something like this: .. code-block:: python from libqtile import hook @hook.subscribe.client_new def floating_dialogs(window): dialog = window.window.get_wm_type() == 'dialog' transient = window.window.get_wm_transient_for() if dialog or transient: window.floating = True A list of available hooks can be found in the :ref:`Built-in Hooks ` reference. Autostart --------- If you want to run commands or spawn some applications when Qtile starts, you'll want to look at the ``startup`` and ``startup_once`` hooks. ``startup`` is emitted every time Qtile starts (including restarts), whereas ``startup_once`` is only emitted on the very first startup. Let's create an executable file ``~/.config/qtile/autostart.sh`` that will start a few programs when Qtile first runs. Remember to `chmod +x ~/.config/qtile/autostart.sh` so that it can be executed. .. code-block:: bash #!/bin/sh pidgin & dropbox start & We can then subscribe to ``startup_once`` to run this script: .. code-block:: python import os import subprocess from libqtile import hook @hook.subscribe.startup_once def autostart(): home = os.path.expanduser('~/.config/qtile/autostart.sh') subprocess.call(home) Accessing the qtile object -------------------------- If you want to do something with the ``Qtile`` manager instance inside a hook, it can be imported into your config: .. code-block:: python from libqtile import qtile Async hooks ----------- Hooks can also be defined as coroutine functions using ``async def``, which will run them asynchronously in the event loop: .. code-block:: python @hook.subscribe.focus_change async def _(): ... qtile-0.31.0/docs/manual/config/keys.rst0000664000175000017500000002024014762660347020013 0ustar epsilonepsilon.. _config-keys: ==== Keys ==== The ``keys`` variable defines Qtile's key bindings. Default Key Bindings -------------------- The mod key for the default config is ``mod4``, which is typically bound to the "Super" keys, which are things like the windows key and the mac command key. The basic operation is: * ``mod + k`` or ``mod + j``: switch windows on the current stack * ``mod + ``: put focus on the other pane of the stack (when in stack layout) * ``mod + ``: switch layouts * ``mod + w``: close window * ``mod + + r``: reload the config * ``mod + ``: switch to that group * ``mod + + ``: send a window to that group * ``mod + ``: start terminal guessed by ``libqtile.utils.guess_terminal`` * ``mod + r``: start a little prompt in the bar so users can run arbitrary commands The default config defines one screen and 8 groups, one for each letter in ``asdfuiop``. It has a basic bottom bar that includes a group box, the current window name, a little text reminder that you're using the default config, a system tray, and a clock. The default configuration has several more advanced key combinations, but the above should be enough for basic usage of qtile. See :ref:`Keybindings in images ` for visual keybindings in keyboard layout. Defining key bindings --------------------- Individual key bindings are defined with :class:`~libqtile.config.Key` as demonstrated in the following example. Note that you may specify more than one callback functions. :: from libqtile.config import Key keys = [ # Pressing "Meta + Shift + a". Key(["mod4", "shift"], "a", callback, ...), # Pressing "Control + p". Key(["control"], "p", callback, ...), # Pressing "Meta + Tab". Key(["mod4", "mod1"], "Tab", callback, ...), ] The above may also be written more concisely with the help of the :class:`~libqtile.config.EzKey` helper class. The following example is functionally equivalent to the above:: from libqtile.config import EzKey as Key keys = [ Key("M-S-a", callback, ...), Key("C-p", callback, ...), Key("M-A-", callback, ...), ] The :class:`~libqtile.config.EzKey` modifier keys (i.e. ``MASC``) can be overwritten through the ``EzKey.modifier_keys`` dictionary. The defaults are:: modifier_keys = { 'M': 'mod4', 'A': 'mod1', 'S': 'shift', 'C': 'control', } Callbacks can also be configured to work only under certain conditions by using the :meth:`~libqtile.lazy.LazyCall.when` method. Currently, the following conditions are supported: :: from libqtile.config import Key keys = [ # Only trigger callback for a specific layout Key( [mod, 'shift'], "j", lazy.layout.grow().when(layout='verticaltile'), lazy.layout.grow_down().when(layout='columns') ), # Limit action to when the current window is not floating Key([mod], "f", lazy.window.toggle_fullscreen().when(when_floating=False)) # Limit action to when the current window is floating Key([mod], "f", lazy.window.toggle_fullscreen().when(when_floating=True)) # Also matches are supported on the current window # For example to match on the wm_class for fullscreen do the following Key([mod], "f", lazy.window.toggle_fullscreen().when(focused=Match(wm_class="yourclasshere")) ] KeyChords ========= Qtile also allows sequences of keys to trigger callbacks. These sequences are known as chords and are defined with :class:`~libqtile.config.KeyChord`. Chords are added to the ``keys`` section of the config file. :: from libqtile.config import Key, KeyChord keys = [ KeyChord([mod], "z", [ Key([], "x", lazy.spawn("xterm")) ]) ] The above code will launch xterm when the user presses Mod + z, followed by x. .. warning:: Users should note that key chords are aborted by pressing . In the above example, if the user presses Mod + z, any following key presses will still be sent to the currently focussed window. If has not been pressed, the next press of x will launch xterm. Modes ----- Chords can optionally persist until a user presses . This can be done by setting ``mode=True``. This can be useful for configuring a subset of commands for a particular situations (i.e. similar to vim modes). :: from libqtile.config import Key, KeyChord keys = [ KeyChord([mod], "z", [ Key([], "g", lazy.layout.grow()), Key([], "s", lazy.layout.shrink()), Key([], "n", lazy.layout.normalize()), Key([], "m", lazy.layout.maximize())], mode=True, name="Windows" ) ] In the above example, pressing Mod + z triggers the "Windows" mode. Users can then resize windows by just pressing g (to grow the window), s to shrink it etc. as many times as needed. To exit the mode, press . .. note:: The Chord widget (:class:`~libqtile.widget.Chord`) will display the name of the active chord (as set by the ``name`` parameter). This is particularly useful where the chord is a persistent mode as this will indicate when the chord's mode is still active. Chains ------ Chords can also be chained to make even longer sequences. :: from libqtile.config import Key, KeyChord keys = [ KeyChord([mod], "z", [ KeyChord([], "x", [ Key([], "c", lazy.spawn("xterm")) ]) ]) ] Modes can also be added to chains if required. The following example demonstrates the behaviour when using the ``mode`` argument in chains: :: from libqtile.config import Key, KeyChord keys = [ KeyChord([mod], "z", [ KeyChord([], "y", [ KeyChord([], "x", [ Key([], "c", lazy.spawn("xterm")) ], mode=True, name="inner") ]) ], mode=True, name="outer") ] After pressing Mod+z y x c, the "inner" mode will remain active. When pressing , the "inner" mode is exited. Since the mode in between does not have ``mode`` set, it is also left. Arriving at the "outer" mode (which has this argument set) stops the "leave" action and "outer" now becomes the active mode. .. note:: If you want to bind a custom key to leave the current mode (e.g. Control + G in addition to ````), you can specify ``lazy.ungrab_chord()`` as the key action. To leave all modes and return to the root bindings, use ``lazy.ungrab_all_chords()``. Modifiers ========= On most systems ``mod1`` is the Alt key - you can see which modifiers, which are enclosed in a list, map to which keys on your system by running the ``xmodmap`` command. This example binds ``Alt-k`` to the "down" command on the current layout. This command is standard on all the included layouts, and switches to the next window (where "next" is defined differently in different layouts). The matching "up" command switches to the previous window. Modifiers include: "shift", "lock", "control", "mod1", "mod2", "mod3", "mod4", and "mod5". They can be used in combination by appending more than one modifier to the list: :: Key( ["mod1", "control"], "k", lazy.layout.shuffle_down() ) Special keys ============ These are most commonly used special keys. For complete list please see `the code `_. You can create bindings on them just like for the regular keys. For example ``Key(["mod1"], "F4", lazy.window.kill())``. .. list-table:: * - ``Return`` * - ``BackSpace`` * - ``Tab`` * - ``space`` * - ``Home``, ``End`` * - ``Left``, ``Up``, ``Right``, ``Down`` * - ``F1``, ``F2``, ``F3``, ... * - * - ``XF86AudioRaiseVolume`` * - ``XF86AudioLowerVolume`` * - ``XF86AudioMute`` * - ``XF86AudioNext`` * - ``XF86AudioPrev`` * - ``XF86MonBrightnessUp`` * - ``XF86MonBrightnessDown`` Reference ========= .. qtile_class:: libqtile.config.Key :no-commands: .. qtile_class:: libqtile.config.KeyChord :no-commands: .. qtile_class:: libqtile.config.EzKey :no-commands: qtile-0.31.0/docs/manual/config/index.rst0000664000175000017500000001625514762660347020162 0ustar epsilonepsilon=============== The config file =============== Qtile is configured in Python. A script (``~/.config/qtile/config.py`` by default) is evaluated, and a small set of configuration variables are pulled from its global namespace. Configuration lookup order ========================== Qtile looks in the following places for a configuration file, in order: * The location specified by the ``-c`` argument. * ``$XDG_CONFIG_HOME/qtile/config.py``, if it is set * ``~/.config/qtile/config.py`` * first ``qtile/config.py`` found in ``$XDG_CONFIG_DIRS`` (defaults to ``/etc/xdg``) * It reads the module ``libqtile.resources.default_config``, included by default with every Qtile installation. Qtile will try to create the configuration file as a copy of the default config, if it doesn't exist yet, this one will be placed inside of ``$XDG_CONFIG_HOME/qtile/config.py`` (if set) or ``~/.config/qtile/config.py``. Default Configuration ===================== The :ref:`default configuration` is invoked when qtile cannot find a configuration file. In addition, if qtile is restarted or the config is reloaded, qtile will load the default configuration if the config file it finds has some kind of error in it. The documentation below describes the configuration lookup process, as well as what the key bindings are in the default config. The default config is not intended to be suitable for all users; it's mostly just there so qtile does /something/ when fired up, and so that it doesn't crash and cause you to lose all your work if you reload a bad config. Configuration variables ======================= A Qtile configuration consists of a file with a bunch of variables in it, which qtile imports and then runs as a Python file to derive its final configuration. The documentation below describes the most common configuration variables; more advanced configuration can be found in the `qtile-examples `_ repository, which includes a number of real-world configurations that demonstrate how you can tune Qtile to your liking. (Feel free to issue a pull request to add your own configuration to the mix!) .. toctree:: :maxdepth: 1 lazy groups keys layouts mouse screens hooks match In addition to the above variables, there are several other boolean configuration variables that control specific aspects of Qtile's behavior: .. list-table:: :widths: 10 10 80 :header-rows: 1 * - variable - default - description * - ``auto_fullscreen`` - ``True`` - If a window requests to be fullscreen, it is automatically fullscreened. Set this to false if you only want windows to be fullscreen if you ask them to be. * - ``bring_front_click`` - ``False`` - When clicked, should the window be brought to the front or not. If this is set to "floating_only", only floating windows will get affected (This sets the X Stack Mode to Above.). This will ignore the layering rules and will therefore bring windows above other windows, even if they have been set as "kept_above". This may cause issues with docks and other similar apps as these may end up hidden behind other windows. Setting this to ``False`` or ``"floating_only"`` may therefore be required when using these apps. * - ``cursor_warp`` - ``False`` - If true, the cursor follows the focus as directed by the keyboard, warping to the center of the focused window. When switching focus between screens, If there are no windows in the screen, the cursor will warp to the center of the screen. * - ``dgroups_key_binder`` - ``None`` - A function which generates group binding hotkeys. It takes a single argument, the DGroups object, and can use that to set up dynamic key bindings. A sample implementation is available in `libqtile/dgroups.py `_ called simple_key_binder(), which will bind groups to mod+shift+0-10 by default. * - ``dgroups_app_rules`` - ``[]`` - A list of Rule objects which can send windows to various groups based on matching criteria. * - ``extension_defaults`` - same as ``widget_defaults`` - Default settings for extensions. * - ``floating_layout`` - ``layout.Floating(float_rules=[...])`` - The default floating layout to use. This allows you to set custom floating rules among other things if you wish. See the configuration file for the default `float_rules`. * - ``floats_kept_above`` - ``True`` - Floating windows are kept above tiled windows (Currently x11 only. Wayland support coming soon.) * - ``focus_on_window_activation`` - ``'smart'`` - Behavior of the _NET_ACTIVE_WINDOW message sent by applications - urgent: urgent flag is set for the window - focus: automatically focus the window - smart: automatically focus if the window is in the current group - never: never automatically focus any window that requests it - can also be a function which takes the window as an argument: - returns True: focus window - returns False: doesn't do anything * - ``follow_mouse_focus`` - ``True`` - Controls whether or not focus follows the mouse around as it moves across windows in a layout. Otherwise set this to ``"click_or_drag_only"`` to change focus only when doing a :class:`~libqtile.config.Click` or :class:`~libqtile.config.Drag` action. * - ``widget_defaults`` - ``dict(font='sans', fontsize=12, padding=3)`` - Default settings for bar widgets. * - ``reconfigure_screens`` - ``True`` - Controls whether or not to automatically reconfigure screens when there are changes in randr output configuration. * - ``wmname`` - ``'LG3D'`` - Gasp! We're lying here. In fact, nobody really uses or cares about this string besides java UI toolkits; you can see several discussions on the mailing lists, GitHub issues, and other WM documentation that suggest setting this string if your java app doesn't work correctly. We may as well just lie and say that we're a working one by default. We choose LG3D to maximize irony: it is a 3D non-reparenting WM written in java that happens to be on java's whitelist. * - ``auto_minimize`` - ``True`` - If things like steam games want to auto-minimize themselves when losing focus, should we respect this or not? Testing your configuration ========================== The best way to test changes to your configuration is with the provided scripts at `./scripts/xephyr`_ (X11) or `./scripts/wephyr`_ (Wayland). This will run Qtile with your ``config.py`` inside a nested window and prevent your running instance of Qtile from crashing if something goes wrong. .. _./scripts/xephyr: https://github.com/qtile/qtile/blob/master/scripts/xephyr .. _./scripts/wephyr: https://github.com/qtile/qtile/blob/master/scripts/wephyr See :ref:`Hacking Qtile ` for more information on using Xephyr. qtile-0.31.0/docs/manual/config/mouse.rst0000664000175000017500000000306314762660347020174 0ustar epsilonepsilon===== Mouse ===== The ``mouse`` config file variable defines a set of global mouse actions, and is a list of :class:`~libqtile.config.Click` and :class:`~libqtile.config.Drag` objects, which define what to do when a window is clicked or dragged. Default Mouse Bindings ---------------------- By default, holding your ``mod`` key and left-clicking (and holding) a window will allow you to drag it around as a floating window. Holding your ``mod`` key and right-clicking (and holding) a window will resize the window (and also make it float if it is not already floating). Example ======= :: from libqtile.config import Click, Drag mouse = [ Drag([mod], "Button1", lazy.window.set_position_floating(), start=lazy.window.get_position()), Drag([mod], "Button3", lazy.window.set_size_floating(), start=lazy.window.get_size()), Click([mod], "Button2", lazy.window.bring_to_front()) ] The above example can also be written more concisely with the help of the ``EzClick`` and ``EzDrag`` helpers:: from libqtile.config import EzClick as Click, EzDrag as Drag mouse = [ Drag("M-1", lazy.window.set_position_floating(), start=lazy.window.get_position()), Drag("M-3", lazy.window.set_size_floating(), start=lazy.window.get_size()), Click("M-2", lazy.window.bring_to_front()) ] Reference ========= .. qtile_class:: libqtile.config.Click :no-commands: .. qtile_class:: libqtile.config.Drag :no-commands: .. qtile_class:: libqtile.config.EzClick :no-commands: qtile-0.31.0/docs/manual/releasing.rst0000664000175000017500000000322414762660347017547 0ustar epsilonepsilon=============== Releasing Qtile =============== Here is a short document about how to tag Qtile releases. 1. create a "Release vX.XX.XX" commit. I like to thank all the contributors; I find that list with something like: .. bash:: git log --oneline --no-merges --format=%an $(git describe --tags --abbrev=0)..HEAD | sort -u note that this can sometimes generate duplicates if people commit with slightly different names; I typically clean it up manually. Be sure that you GPG-sign (i.e. the ``-S`` argument to ``git commit``) this commit. 2. Create a GPG-signed annotated tag (``git tag -a -s vX.XX.XX``); I usually just use exactly the same commit message as the actual release commit above. 3. Push your tag to qtile/qtile directly: .. bash:: git push origin vX.XX.XX 4. Check the `Release action `_; the "Test PyPI upload" action should build and upload the wheels to the test environment. If this step breaks, you can delete your tag (``git push ... :vX.XX.XX``), fix the bug, re-tag, and hopefully that will work. 5. Create a new `Github release `_; this is what will trigger the actions in ``.github/workflows/release.yml`` to actually do the real pypi upload. 6. Make sure all of these actions complete as green. The release should show up in a few minutes after completion here: https://pypi.org/project/qtile/ 7. Push your tag commit to master. 8. Update `the release string `_ on qtile.org. 8. Relax and enjoy a $beverage. Thanks for releasing! qtile-0.31.0/docs/manual/how-to-migrate.rst0000664000175000017500000002716614762660347020454 0ustar epsilonepsilonHow to write a migration script =============================== Qtile's migration scripts should provide two functions: * Update config files to fix any breaking changes introduced by a commit * Provide linting summary of errors in existing configs To do this, we use `LibCST `_ to parse the config file and make changes as appropriate. Basic tips for using ``LibCST`` are included below but it is recommended that you read their documentation to familiarise yourself with the available functionalities. Stucture of a migration file ---------------------------- Migrations should be saved as a new file in ``libqtile/scripts/migrations``. A basic migration will look like this: .. code:: python from libqtile.scripts.migrations._base import MigrationTransformer, _QtileMigrator, add_migration class MyMigration(MigrationTransformer): """The class that actually modifies the code.""" ... class Migrator(_QtileMigrator): ID = "MyMigrationName" SUMMARY = "Summary of migration." HELP = """ Longer text explaining purpose of the migration and, ideally, giving code examples. """ AFTER_VERSION = "0.22.1" TESTS = [] visitor = MyMigration add_migration(Migrator) Providing details about the migration ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The purpose of ``Migrator`` class in the code above is to provide the information about the migration. It is important that the information is as helpful as possible as it is used in multiple places. * The ``ID`` attribute is a short, unique name to identify the migration. This allows users to select specific migrations to run via ``qtile migrate --run-migrations ID``. * The ``SUMMARY`` attribute is used to provide a brief summary of the migration and is used when a user runs ``qtile migrate --list-migrations``. It is also used in the documentation. * Similarly, the ``HELP`` attribute is used for the script (``qtie migrate --info ID``) and the documentation. This text should be longer and can include example code. As it is used in the documentation, it should use RST syntax (e.g. ``.. code:: python`` for codeblocks etc.). * ``AFTER_VERSION`` should be set the name of the current release. This allows users to filter migrations to those that were added after the last release. * The ``visitor`` attribute is a link to the class definition (not and instance of the class) for the transformer that you wish to use. * The ``add_migration`` call at the end is required to ensure the migration is loaded into the list of available migrations. * See below for details on ``TESTS``. How migrations are run ~~~~~~~~~~~~~~~~~~~~~~ You are pretty much free to transform the code as you see fit. By default, the script will run the ``visit`` method on the parsed code and will pass the ``visitor`` attribute of the ``_QtileMigrator`` class object. Therefore, if all your transformations can be performed in a single visitor, it is not necessary to do anything further in the ``Migrator`` class. However, if you want to run mutiple visitors, transformers, codemods, this is possible by overriding the ``run`` method of the ``_QtileMigrator`` class. For example, the ``RemoveCmdPrefix`` migrator has the following code: .. code:: python def run(self, original): # Run the base migrations transformer = CmdPrefixTransformer() updated = original.visit(transformer) self.update_lint(transformer) # Check if we need to add an import line if transformer.needs_import: # We use the built-in visitor to add the import context = codemod.CodemodContext() AddImportsVisitor.add_needed_import( context, "libqtile.command.base", "expose_command" ) visitor = AddImportsVisitor(context) # Run the visitor over the updated code updated = updated.visit(visitor) return original, updated In this migration, it may be required to add an import statement. ``LibCST`` has a built-in transformation for doing this so we can run that after our own transformation has been performed. .. important:: The ``run`` method must return a tuple of the original code and the updated code. Transforming the code ~~~~~~~~~~~~~~~~~~~~~ It is recommended that you use a `transformed `_ to update the code. For convenience, a ``MigrationTransformer`` class is defined in ``libqtile.scripts.migrations._base``. This class definition includes some metadata information and a ``lint`` method for outputting details of errors. Let's look at an example transformer to understand how the migration works. The code below shows how to change a positional argument to a keyword argument in the ``WidgetBox`` widget. .. code:: python class WidgetboxArgsTransformer(MigrationTransformer): @m.call_if_inside( m.Call(func=m.Name("WidgetBox")) | m.Call(func=m.Attribute(attr=m.Name("WidgetBox"))) ) @m.leave(m.Arg(keyword=None)) def update_widgetbox_args(self, original_node, updated_node) -> cst.Arg: """Changes positional argumentto 'widgets' kwargs.""" self.lint( original_node, "The positional argument should be replaced with a keyword argument named 'widgets'.", ) return updated_node.with_changes(keyword=cst.Name("widgets"), equal=EQUALS_NO_SPACE) Our class (which inherits from ``MigrationTransformer``) defines a single method to perform the transformation. We take advantage of ``LibCST`` and its `Matchers `_ to narrow the scope of when the transformation is run. We are looking to modify an argument so we use the ``@m.leave(m.Arg())`` decorator to call the function at end of parsing an argument. We can restrict when this is called by specify ``m.Arg(keyword=None)`` so that it is only called for positional arguments. Furthermore, as we only want this called for ``WidgetBox`` instantiation lines, we add an additional decorator ``@m.call_if_inside(m.Call())``. This ensures the method is only called when we're in a call. On its own, that's not helpful as args would almost always be part of a call. However, we can say we only want to match calls to ``WidgetBox``. The reason for the long syntax above is that ``LibCST`` parses ``WidgetBox()`` and ``widget.WidgetBox()`` differently. In the first one, ``WidgetBox`` is in the ``func`` property of the call. However, in the second, the ``func`` is an ``Attribute`` as it is a dotted name and so we need to check the ``attr`` property. The decorated method takes two arguments, ``original_mode`` and ``updated_node`` (note: The ``original_node`` should not be modified). The method should also confirm the return type. The above method provides a linting message by calling ``self.lint`` and passing the original node and a helpful message. Finally, the method updates the code by calling ``updated_node.with_changes()``. In this instance, we add a keyword (``"widgets"``) to the argument. We also remove spaces around the equals sign as these are added by default by ``LibCST``. The updated node is returned. Helper classes ~~~~~~~~~~~~~~ Helper classes are provided for common transformations. * ``RenamerTransformer`` will update all instances of a name, replacing it with another. The class will also handle the necessary linting. .. code:: python class RenameHookTransformer(RenamerTransformer): from_to = ("window_name_change", "client_name_updated") Testing the migration ~~~~~~~~~~~~~~~~~~~~~ All migrations must be tested, ideally with a number of scenarios to confirm that the migration works as expected. Unlike other tests, the tests for the migrations are defined within the ``TESTS`` attribute. This is a list that should take a ``Check``, ``Change`` or ``NoChange`` object (all are imported from ``libqtile.scripts.migrations._base``). A ``Change`` object needs two parameters, the input code and the expected output. A ``NoChange`` object just defines the input (as the output should be the same). A ``Check`` object is identical to ``Change`` however, when running the test suite, the migrated code will be verified with ``qtile check``. The code will therefore need to include all relevant imports etc. Based on the above, the following is recommended as best practice: * Define one ``Check`` test which addresses every situation anticipated by the migration * Use as many ``Change`` tests as required to test individual scenarios in a minimal way * Use ``NoChange`` tests where there are specific cases that should not be modified * Depending on the simplicity of the migration, a single ``Check`` may be all that is required For example, the ``RemoveCmdPrefix`` migration has the following ``TESTS``: .. code:: python TESTS = [ Change("""qtile.cmd_spawn("alacritty")""", """qtile.spawn("alacritty")"""), Change("""qtile.cmd_groups()""", """qtile.get_groups()"""), Change("""qtile.cmd_screens()""", """qtile.get_screens()"""), Change("""qtile.current_window.cmd_hints()""", """qtile.current_window.get_hints()"""), Change( """qtile.current_window.cmd_opacity(0.5)""", """qtile.current_window.set_opacity(0.5)""", ), Change( """ class MyWidget(widget.Clock): def cmd_my_command(self): pass """, """ from libqtile.command.base import expose_command class MyWidget(widget.Clock): @expose_command def my_command(self): pass """ ), NoChange( """ def cmd_some_other_func(): pass """ ), Check( """ from libqtile import qtile, widget class MyClock(widget.Clock): def cmd_my_exposed_command(self): pass def my_func(qtile): qtile.cmd_spawn("rickroll") hints = qtile.current_window.cmd_hints() groups = qtile.cmd_groups() screens = qtile.cmd_screens() qtile.current_window.cmd_opacity(0.5) def cmd_some_other_func(): pass """, """ from libqtile import qtile, widget from libqtile.command.base import expose_command class MyClock(widget.Clock): @expose_command def my_exposed_command(self): pass def my_func(qtile): qtile.spawn("rickroll") hints = qtile.current_window.get_hints() groups = qtile.get_groups() screens = qtile.get_screens() qtile.current_window.set_opacity(0.5) def cmd_some_other_func(): pass """ ) ] The tests check: * ``cmd_`` prefix is removed on method calls, updating specific changes as required * Exposed methods in a class should use the ``expose_command`` decorator (adding the import if it's not already included) * No change is made to a function definition (as it's not part of a class definition) .. note:: Tests will fail in the following scenarios: * If no tests are defined * If a ``Change`` test does not result in linting output * If no ``Check`` test is defined You can check your tests by running ``pytest -k ``. Note, ``mpypy`` must be installed for the ``Check`` tests to be run. qtile-0.31.0/docs/manual/contributing.rst0000664000175000017500000002164014762660347020307 0ustar epsilonepsilon============ Contributing ============ .. _reporting: Reporting bugs ============== Perhaps the easiest way to contribute to Qtile is to report any bugs you run into on the `GitHub issue tracker `_. Useful bug reports are ones that get bugs fixed. A useful bug report normally has two qualities: 1. **Reproducible.** If your bug is not reproducible it will never get fixed. You should clearly mention the steps to reproduce the bug. Do not assume or skip any reproducing step. Describe the issue, step-by-step, so that it is easy to reproduce and fix. 2. **Specific.** Do not write an essay about the problem. Be specific and to the point. Try to summarize the problem in a succinct manner. Do not combine multiple problems even if they seem to be similar. Write different reports for each problem. Ensure to include any appropriate log entries from ``~/.local/share/qtile/qtile.log`` and/or ``~/.xsession-errors``! Sometimes, an ``xtrace`` is requested. If that is the case, refer to :ref:`capturing an xtrace `. Writing code ============ To get started writing code for Qtile, check out our guide to :ref:`hacking`. A more detailed page on creating widgets is available :ref:`here `. .. important:: Use a separate **git branch** to make rebasing easy. Ideally, you would ``git checkout -b `` before starting your work. See also: :ref:`using git `. .. _submitting-a-pr: Submit a pull request --------------------- You've done your hacking and are ready to submit your patch to Qtile. Great! Now it's time to submit a `pull request `_ to our `issue tracker `_ on GitHub. .. important:: Pull requests are not considered complete until they include all of the following: * **Code** that conforms to our linters and formatters. Run ``pre-commit install`` to install pre-commit hooks that will make sure your code is compliant before any commit. * **Unit tests** that pass locally and in our CI environment (More below). *Please add unit tests* to ensure that your code works and stays working! * **Documentation** updates on an as needed basis. * A ``qtile migrate`` **migration** is required for config-breaking changes. See :doc:`here ` for current migrations and see below for further information. * **Code** that does not include *unrelated changes*. Examples for this are formatting changes, replacing quotes or whitespace in other parts of the code or "fixing" linter warnings popping up in your editor on existing code. *Do not include anything like the above!* * **Widgets** don't need to catch their own exceptions, or introduce their own polling infrastructure. The code in ``libqtile.widget.base.*`` does all of this. Your widget should generally only include whatever parsing/rendering code is necessary, any other changes should go at the framework level. Make sure to double-check that you are not re-implementing parts of ``libqtile.widget.base``. * **Commit messages** are more important that Github PR notes, since this is what people see when they are spelunking via ``git blame``. Please include all relevant detail in the actual git commit message (things like exact stack traces, copy/pastes of discussion in IRC/mailing lists, links to specifications or other API docs are all good). If your PR fixes a Github issue, it might also be wise to link to it with ``#1234`` in the commit message. * PRs with **multiple commits** should not introduce code in one patch to then change it in a later patch. Please do a patch-by-patch review of your PR, and make sure each commit passes CI and makes logical sense on its own. In other words: *do* introduce your feature in one commit and maybe add the tests and documentation in a seperate commit. *Don't* push commits that partially implement a feature and are basically broken. .. note:: Others might ban *force-pushes*, we allow them and prefer them over incomplete commits or commits that have a bad and meaningless commit description. Feel free to add your contribution (no matter how small) to the appropriate place in the CHANGELOG as well! .. _unit-testing: Unit testing ------------ We must test each *unit* of code to ensure that new changes to the code do not break existing functionality. The framework we use to test Qtile is `pytest `_. How pytest works is outside of the scope of this documentation, but there are tutorials online that explain how it is used. Our tests are written inside the ``test`` folder at the top level of the repository. Reading through these, you can get a feel for the approach we take to test a given unit. Most of the tests involve an object called ``manager``. This is the test manager (defined in test/helpers.py), which exposes a command client at ``manager.c`` that we use to test a Qtile instance running in a separate thread as if we were using a command client from within a running Qtile session. For any Qtile-specific question on testing, feel free to ask on our `issue tracker `_ or on IRC (#qtile on irc.oftc.net). .. _running-tests-locally: Running tests locally --------------------- This section gives an overview about ``tox`` so that you don't have to search `its documentation `_ just to get started. Checks are grouped in so-called ``environments``. Some of them are configured to check that the code works (the usual unit test, e.g. ``py39``, ``pypy3``), others make sure that your code conforms to the style guide (``pep8``, ``codestyle``, ``mypy``). A third kind of test verifies that the documentation and packaging processes work (``docs``, ``docstyle``, ``packaging``). We have configured ``tox`` to run the full suite of tests whenever a pull request is submitted/updated. To reduce the amount of time taken by these tests, we have created separate environments for both python versions and backends (e.g. tests for x11 and wayland run in parallel for each python version that we currently support). These environments were designed with automation in mind so there are separate ``test`` environments which should be used for running qtile's tests locally. By default, tests will only run on x11 backend (but see below for information on how to set the backend). The following examples show how to run tests locally: * To run the functional tests, use ``tox -e test``. You can specify to only run a specific test file or even a specific test within that file with the following commands: .. code-block:: bash tox -e test # Run all tests in default python version tox -e test -- -x test/widgets/test_widgetbox.py # run a single file tox -e test -- -x test/widgets/test_widgetbox.py::test_widgetbox_widget tox -e test -- --backend=wayland --backend=x11 # run tests on both backends tox -e test-both # same as above tox -e test-wayland # Just run tests on wayland backend * To run style and building checks, use ``tox -e docs,packaging,pep8,...``. You can use ``-p auto`` to run the environments in parallel. .. important:: The CI is configured to run all the environments. Hence it can be time- consuming to make all the tests pass. As stated above, pull requests that don't pass the tests are considered incomplete. Don't forget that this does not only include the functionality, but the style, typing annotations (if necessary) and documentation as well! Writing migrations ------------------ Migrations are needed when a commit introduces a change which makes a breaking change to a user's config. Examples include renaming classes, methods, arguments and moving modules or class definitions. Where these changes are made, it is strongly encouraged to support the old syntax where possible and warn the user about the deprecations. Whether or not a deprecation warning is provided, a migration script should be provided that will modify the user's config when they run ``qtile migrate``. Click here for detailed instructions on :doc:`how-to-migrate`. .. toctree:: :maxdepth: 1 :hidden: how-to-migrate Deprecation Policy ------------------ Interfaces that have been deprecated for at least two years after the first release containing the deprecation notice can be deleted. Since all new breaking changes should have a migration, users can use ``qtile migrate`` to bootstrap across versions when migrations are deleted if necessary. Deprecated interfaces that do not have a migration (i.e. whose deprecation was noted before the migration feature was introduced) are all fair game to be deleted, since the migration feature is more than two years old. qtile-0.31.0/docs/requirements.txt0000664000175000017500000000027214762660347017053 0ustar epsilonepsilonsetuptools_scm # Sphinx 7.0.0 is not currently supported by the sphinx RTD theme sphinx<7.0.0 sphinx_rtd_theme funcparserlib==1.0.0a0 numpydoc pytest xcffib >= 1.4.0 PyGObject dbus-fast qtile-0.31.0/docs/BUILDING0000664000175000017500000000067614762660347014717 0ustar epsilonepsilonTo build in a virtual environment (requires the 'virtualenv', 'pip' and 'graphiz' tools installed in your distro): $ cd ./docs $ virtualenv qtile $ source qtile/bin/activate $ pip install -r requirements.txt $ make html $ deactivate =============================================================================== To build on Ubuntu: $ sudo apt-get install graphiz Install: https://github.com/:snide/sphinx_rtd_theme $ cd ./docs $ make html qtile-0.31.0/docs/conf.py0000664000175000017500000002576214762660347015101 0ustar epsilonepsilon# # Qtile documentation build configuration file, created by # sphinx-quickstart on Sat Feb 11 15:20:21 2012. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import os import sys from unittest.mock import MagicMock import setuptools_scm class Mock(MagicMock): # xcbq does a dir() on objects and pull stuff out of them and tries to sort # the result. MagicMock has a bunch of stuff that can't be sorted, so let's # like about dir(). def __dir__(self): return [] MOCK_MODULES = [ "libqtile._ffi_pango", "libqtile.backend.wayland._ffi", "libqtile.backend.x11._ffi_xcursors", "cairocffi.ffi", "cairocffi.xcb", "cairocffi.pixbuf", "cffi", "dateutil", "dateutil.parser", "dbus_fast", "dbus_fast.aio", "dbus_fast.errors", "dbus_fast.service", "dbus_fast.constants", "iwlib", "keyring", "mpd", "psutil", "pulsectl", "pulsectl_asyncio", "pywayland", "pywayland.protocol.wayland", "pywayland.protocol.wayland.wl_output", "pywayland.server", "pywayland.utils", "wlroots", "wlroots.helper", "wlroots.util", "wlroots.util.box", "wlroots.util.clock", "wlroots.util.edges", "wlroots.util.region", "wlroots.wlr_types", "wlroots.wlr_types.cursor", "wlroots.wlr_types.foreign_toplevel_management_v1", "wlroots.wlr_types.idle_inhibit_v1", "wlroots.wlr_types.idle_notify_v1", "wlroots.wlr_types.keyboard", "wlroots.wlr_types.layer_shell_v1", "wlroots.wlr_types.output_management_v1", "wlroots.wlr_types.pointer_constraints_v1", "wlroots.wlr_types.output", "wlroots.wlr_types.output_power_management_v1", "wlroots.wlr_types.scene", "wlroots.wlr_types.server_decoration", "wlroots.wlr_types.virtual_keyboard_v1", "wlroots.wlr_types.virtual_pointer_v1", "wlroots.wlr_types.xdg_shell", "xcffib", "xcffib.ffi", "xcffib.randr", "xcffib.render", "xcffib.wrappers", "xcffib.xfixes", "xcffib.xinerama", "xcffib.xproto", "xcffib.xtest", "xdg.IconTheme", "xkbcommon", ] sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES) # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath(".")) sys.path.insert(0, os.path.abspath("../")) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ "sphinx.ext.autodoc", "sphinx.ext.autosummary", "sphinx.ext.coverage", "sphinx.ext.graphviz", "sphinx.ext.todo", "sphinx.ext.viewcode", "numpydoc", "sphinx_qtile", ] numpydoc_show_class_members = False numpydoc_class_members_toctree = False # Add any paths that contain templates here, relative to this directory. templates_path = [] # The suffix of source filenames. source_suffix = ".rst" # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = "index" # General information about the project. project = "Qtile" copyright = "2008-2021, Aldo Cortesi and contributers" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = setuptools_scm.get_version(root="..") # The full version, including alpha/beta/rc tags. release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = True # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. html_favicon = "_static/favicon.ico" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] html_extra_path = ["_static/screenshots"] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # smartypants was deprecated in favour of smartquotes # We want to disable this so users can copy an paste text into their configs smartquotes = False # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {'index': 'index.html'} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = "Qtiledoc" # -- Options for LaTeX output -------------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # 'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ("index", "Qtile.tex", "Qtile Documentation", "Aldo Cortesi", "manual"), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). # man_pages = [] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( "index", "Qtile", "Qtile Documentation", "Aldo Cortesi", "Qtile", "A hackable tiling window manager.", "Miscellaneous", ), ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' html_theme = "sphinx_rtd_theme" # only import and set the theme path if we're building docs locally if not os.environ.get("READTHEDOCS"): import sphinx_rtd_theme html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] graphviz_dot_args = ["-Lg"] # A workaround for the responsive tables always having annoying scrollbars. def setup(app): app.add_css_file("no_scrollbars.css") app.add_css_file("split_code.css") # readthedocs config, see https://about.readthedocs.com/blog/2024/07/addons-by-default/ # Define the canonical URL if you are using a custom domain on Read the Docs html_baseurl = os.environ.get("READTHEDOCS_CANONICAL_URL", "") html_context = {} # Tell Jinja2 templates the build is running on Read the Docs if os.environ.get("READTHEDOCS", "") == "True": html_context["READTHEDOCS"] = True qtile-0.31.0/docs/build.sh0000664000175000017500000000221214762660347015216 0ustar epsilonepsilon#!/bin/sh # Copyright (c) 2012 dmpayton # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. rm -rf _build/* while true; do make html sleep 5 done qtile-0.31.0/docs/screenshots/0000775000175000017500000000000014762660347016126 5ustar epsilonepsilonqtile-0.31.0/docs/screenshots/take_all.py0000775000175000017500000004547314762660347020274 0ustar epsilonepsilon#!/usr/bin/env python3 import logging import os import sys import time import traceback from collections import namedtuple from pathlib import Path from screenshots import Client, Screenshooter def env(name, default): return os.environ.get(name, default) Spec = namedtuple( "Spec", "commands before after geometry delay windows", defaults=[None, None, None, env("GEOMETRY", "240x135"), env("DELAY", "1x1"), 3], ) specs = { "bsp": { "2-windows": Spec(windows=2), "3-windows": Spec(windows=3), "4-windows": Spec(windows=4), "5-windows": Spec(windows=5), "8-windows": Spec( windows=8, before=[ "up", "grow_down", "left", "grow_left", "down", "right", "grow_left", "grow_left", "toggle_split", "left", "left", "grow_right", "grow_right", "grow_up", "grow_up", "up", "toggle_split", ], ), "toggle_split-from-down-left": Spec(commands=["toggle_split"]), "toggle_split-from-right": Spec(commands=["toggle_split"], before=["right"]), # "next": Spec(commands=["next"]), # no effects? # "previous": Spec(commands=["previous"]), # no effects? "left": Spec(commands=["left"], before=["right"]), "right": Spec(commands=["right"]), "up": Spec(commands=["up"]), "down": Spec(commands=["down"], before=["up"]), "shuffle_left": Spec(commands=["shuffle_left"], before=["right"]), "shuffle_right": Spec(commands=["shuffle_right"]), "shuffle_up": Spec(commands=["shuffle_up"]), "shuffle_down": Spec(commands=["shuffle_down"], before=["up"]), "grow_left": Spec(commands=["grow_left"], before=["right"]), "grow_right": Spec(commands=["grow_right"]), "grow_up": Spec(commands=["grow_up"]), "grow_down": Spec(commands=["grow_down"], before=["up"]), "flip_left": Spec(commands=["flip_left"], before=["right"]), "flip_right": Spec(commands=["flip_right"]), "flip_up": Spec(commands=["flip_up"]), "flip_down": Spec(commands=["flip_down"], before=["up"]), "normalize": Spec( commands=["normalize"], before=["grow_up", "grow_up", "grow_right", "grow_right"], ), }, "columns": { "2-windows": Spec(windows=2), "3-windows": Spec(windows=3), "4-windows": Spec(windows=4), "5-windows": Spec(windows=4, before=["left", "spawn"]), "toggle_split": Spec( commands=[ "toggle_split", "toggle_split", "down", "toggle_split", "toggle_split", ], windows=4, ), "left": Spec(commands=["left"]), "right": Spec(commands=["right"], before=["left"]), "up": Spec(commands=["up"], before=["down"]), "down": Spec(commands=["down"]), "next": Spec(commands=["next"]), "previous": Spec(commands=["previous"]), "shuffle_left": Spec(commands=["shuffle_left"]), "shuffle_right": Spec(commands=["shuffle_right"], before=["left"]), "shuffle_up": Spec(commands=["shuffle_up"], before=["down"]), "shuffle_down": Spec(commands=["shuffle_down"]), "grow_left": Spec(commands=["grow_left"]), "grow_right": Spec(commands=["grow_right"], before=["left"]), "grow_up": Spec(commands=["grow_up"], before=["down"]), "grow_down": Spec(commands=["grow_down"]), "normalize": Spec( commands=["normalize"], before=["grow_down", "grow_down", "grow_left", "grow_left"], ), }, "floating": { # Floating info clients lists clients from all groups, # breaking our "kill windows" method. # "2-windows": Spec(windows=2), # "3-windows": Spec(windows=3), # "4-windows": Spec(windows=4), }, "matrix": { "2-windows": Spec(windows=2), "3-windows": Spec(windows=3), "4-windows": Spec(windows=4), "5-windows": Spec(windows=5), "5-windows-add": Spec(windows=5, before=["add"]), "left": Spec(commands=["left"], windows=4), "right": Spec(commands=["right"], before=["up", "left"], windows=4), "up": Spec(commands=["up"], windows=4), "down": Spec(commands=["down"], before=["up"], windows=4), "add-delete": Spec( commands=["add", "add", "delete", "delete", "delete", "add"], after=["delete"], windows=5, ), }, "max": {"max": Spec(windows=1)}, "monadtall": { "2-windows": Spec(windows=2), "3-windows": Spec(windows=3), "4-windows": Spec(windows=4), "5-windows": Spec(windows=5), "normalize": Spec( commands=["normalize"], windows=4, before=["maximize", "shrink_main", "shrink_main"], after=["reset"], ), "normalize-from-main": Spec( commands=["normalize"], windows=4, before=["maximize", "shrink_main", "shrink_main", "left"], after=["reset"], ), "reset": Spec( commands=["reset"], windows=4, before=["maximize", "shrink_main", "shrink_main"], ), "maximize": Spec(commands=["maximize"], windows=4, after=["reset"]), "maximize-main": Spec(commands=["maximize"], windows=4, before=["left"], after=["reset"]), "grow": Spec(commands=["grow", "grow", "grow", "grow"], delay="1x2"), "grow_main": Spec( commands=["grow_main", "grow_main", "grow_main"], after=["reset"], delay="1x2", ), "shrink_main": Spec( commands=["shrink_main", "shrink_main", "shrink_main"], after=["reset"], delay="1x2", ), "shrink": Spec(commands=["shrink", "shrink", "shrink", "shrink"], delay="1x2"), "shuffle_up": Spec(commands=["shuffle_up"]), "shuffle_down": Spec(commands=["shuffle_down"], before=["up"]), "flip": Spec(commands=["flip"], after=["flip"]), # "swap": Spec(commands=["swap"]), # requires 2 args: window1 and window2 "swap_left": Spec(commands=["swap_left"], after=["reset"]), "swap_right": Spec(commands=["swap_right"], before=["left"], after=["reset"]), "swap_main": Spec(commands=["swap_main"], after=["reset"]), "left": Spec(commands=["left"]), "right": Spec(commands=["right"], before=["left"]), }, "monadwide": { # There seems to be a problem with directions. Up cycles through windows # clock-wise, down cycles through windows counter-clock-wise, left and right # works normally in the secondary columns, while left from main does nothing # and right from main moves to the center of the second column. It's like # the directions are mixed between normal orientation # and a 90° rotation to the left, like monadtall. Up and down are reversed # compared to monadtall. "2-windows": Spec(windows=2), "3-windows": Spec(windows=3), "4-windows": Spec(windows=4), "5-windows": Spec(windows=5), "normalize": Spec( commands=["normalize"], windows=4, before=["maximize", "shrink_main", "shrink_main"], after=["reset"], ), "normalize-from-main": Spec( commands=["normalize"], windows=4, before=["maximize", "shrink_main", "shrink_main", "down"], after=["reset"], ), "reset": Spec( commands=["reset"], windows=4, before=["maximize", "shrink_main", "shrink_main"], ), "maximize": Spec(commands=["maximize"], windows=4, after=["reset"]), "maximize-main": Spec(commands=["maximize"], windows=4, before=["down"], after=["reset"]), "grow": Spec(commands=["grow", "grow", "grow", "grow"], delay="1x2"), "grow_main": Spec( commands=["grow_main", "grow_main", "grow_main"], after=["reset"], delay="1x2", ), "shrink_main": Spec( commands=["shrink_main", "shrink_main", "shrink_main"], after=["reset"], delay="1x2", ), "shrink": Spec(commands=["shrink", "shrink", "shrink", "shrink"], delay="1x2"), "shuffle_up": Spec(commands=["shuffle_up"]), "shuffle_down": Spec(commands=["shuffle_down"], before=["down"]), "flip": Spec(commands=["flip"], after=["flip"]), # "swap": Spec(commands=["swap"]), # requires 2 args: window1 and window2 "swap_left": Spec(commands=["swap_left"], before=["flip"], after=["flip"]), "swap_right": Spec(commands=["swap_right"], before=["left"]), "swap_main": Spec(commands=["swap_main"]), "left": Spec(commands=["left"]), "right": Spec(commands=["right"], before=["left"]), }, "ratiotile": { "2-windows": Spec(windows=2), "3-windows": Spec(windows=3), "4-windows": Spec(windows=4), "5-windows": Spec(windows=5), "6-windows": Spec(windows=6), "7-windows": Spec(windows=7), "shuffle_down": Spec( commands=["shuffle_down", "shuffle_down", "shuffle_down"], windows=5, delay="1x2", ), "shuffle_up": Spec( commands=["shuffle_up", "shuffle_up", "shuffle_up"], windows=5, delay="1x2" ), # decrease_ratio does not seem to work # "decrease_ratio": Spec( # commands=["decrease_ratio", "decrease_ratio", "decrease_ratio", "decrease_ratio"], # windows=5, delay="1x2"), # increase_ratio does not seem to work # "increase_ratio": Spec( # commands=["increase_ratio", "increase_ratio", "increase_ratio", "increase_ratio"], # windows=5, delay="1x2"), }, "slice": { # Slice layout freezes the session # "next": Spec(commands=["next"]), # "previous": Spec(commands=["previous"]), }, "stack": { # There seems to be a confusion between Stack and Columns layouts. # The Columns layout says: "Extension of the Stack layout" # and "The screen is split into columns, which can be dynamically added # or removed", but there are no commands available to add or remove columns. # Inversely, the Stack layout says: "Unlike the columns layout # the number of stacks is fixed", yet the two commands # "cmd_add" and "cmd_delete" allow for a dynamic number of stacks! "2-windows": Spec(windows=2), "3-windows": Spec(windows=3), "4-windows": Spec(windows=4), "5-windows": Spec(windows=5), "toggle_split": Spec( commands=["toggle_split"], windows=4, before=["down", "down"], after=["toggle_split"], ), "down": Spec(commands=["down"], windows=4), "up": Spec(commands=["up"], before=["down"], windows=4), "shuffle_down": Spec(commands=["shuffle_down"], windows=4), "shuffle_up": Spec(commands=["shuffle_up"], before=["down"], windows=4), "add-delete": Spec( commands=["add", "add", "spawn", "spawn", "spawn", "delete", "delete"] ), "rotate": Spec(commands=["rotate"]), "next": Spec(commands=["next"], before=["add", "spawn"], after=["delete"]), "previous": Spec(commands=["previous"], before=["add", "spawn"], after=["delete"]), "client_to_next": Spec( commands=["client_to_next"], before=["add", "spawn"], after=["delete"] ), "client_to_previous": Spec( commands=["client_to_previous"], before=["add", "spawn"], after=["delete"] ), # "client_to_stack": Spec(commands=["client_to_stack"]), # requires 1 argument }, "tile": { # Tile: no docstring at all in the code. "2-windows": Spec(windows=2), "3-windows": Spec(windows=3), "4-windows": Spec(windows=4), "5-windows": Spec(windows=5), "shuffle_down": Spec( commands=["shuffle_down", "shuffle_down", "shuffle_down"], windows=4 ), "shuffle_up": Spec(commands=["shuffle_up", "shuffle_up", "shuffle_up"], windows=4), "increase-decrease-ratio": Spec( commands=[ "increase_ratio", "increase_ratio", "increase_ratio", "decrease_ratio", "decrease_ratio", "decrease_ratio", ], before=["down"], delay="1x3", ), "increase-decrease-nmaster": Spec( commands=[ "increase_nmaster", "increase_nmaster", "increase_nmaster", "decrease_nmaster", "decrease_nmaster", "decrease_nmaster", ], delay="1x3", ), }, "treetab": { # TreeTab info clients lists clients from all groups, # breaking our "kill windows" method. # See https://github.com/qtile/qtile/issues/1459 # "1-window": Spec(windows=1), # "2-windows": Spec(windows=2), # "3-windows": Spec(windows=3), # "4-windows": Spec(windows=4), # "down": Spec(commands=["down"]), # "up": Spec(commands=["up"]), # "move_down": Spec(commands=["move_down"]), # "move_up": Spec(commands=["move_up"]), # "move_left": Spec(commands=["move_left"]), # "move_right": Spec(commands=["move_right"]), # "add_section": Spec(commands=["add_section"]), # "del_section": Spec(commands=["del_section"]), # "section_up": Spec(commands=["section_up"]), # "section_down": Spec(commands=["section_down"]), # "sort_windows": Spec(commands=["sort_windows"]), # "expand_branch": Spec(commands=["expand_branch"]), # "collapse_branch": Spec(commands=["collapse_branch"]), # "decrease_ratio": Spec(commands=["decrease_ratio"]), # "increase_ratio": Spec(commands=["increase_ratio"]), }, "verticaltile": { "3-windows": Spec(windows=3), "4-windows": Spec(before=["up", "maximize"], windows=4), "shuffle_down": Spec(commands=["shuffle_down", "shuffle_down"], before=["up", "up"]), "shuffle_up": Spec(commands=["shuffle_up", "shuffle_up"]), "shuffle_down-maximize": Spec( commands=["shuffle_down", "shuffle_down"], before=["up", "maximize", "up"] ), "shuffle_up-maximize": Spec( commands=["shuffle_up", "shuffle_up"], before=["up", "maximize", "down"] ), "maximize": Spec(commands=["maximize"]), "normalize": Spec(commands=["normalize"], before=["up", "maximize", "shrink", "shrink"]), "grow-shrink": Spec( commands=["grow", "grow", "shrink", "shrink"], before=["maximize", "shrink", "shrink"], after=["normalize"], delay="1x2", ), }, "zoomy": { "3-windows": Spec(windows=3), "4-windows": Spec(windows=4), "next-or-down": Spec(commands=["next", "next"], windows=4), "previous-or-up": Spec(commands=["previous", "previous"], windows=4), }, } client = Client() output_dir = Path("docs") / "screenshots" / "layout" def take(name, layout, spec): """Take the specified screenshots and optionally animate them.""" # prepare the layout try: client.prepare_layout(layout, spec.windows, spec.before or []) except Exception: client.kill_group_windows() return False, "While preparing layout:\n" + traceback.format_exc() time.sleep(0.5) # initialize screenshooter, create output directory layout_dir = output_dir / layout layout_dir.mkdir(parents=True, exist_ok=True) commands = spec.commands or [] screen = Screenshooter(layout_dir / name, spec.geometry, spec.delay) errors = [] # take initial screenshot (without number if it's the only one) screen.shoot(numbered=bool(commands)) # take screenshots for each command, animate them at the end if commands: for command in commands: try: client.run_layout_command(command) except Exception: errors.append(f"While running command {command}:\n{traceback.format_exc()}") break time.sleep(0.05) screen.shoot() screen.animate(clear=True) # cleanup the layout try: client.clean_layout(spec.after or []) except Exception: errors.append("While cleaning layout:\n" + traceback.format_exc()) if errors: return False, "\n\n".join(errors) return True, "" def get_selection(args): """Parse args of the form LAYOUT, LAYOUT:NAME or LAYOUT:NAME1,NAME2.""" if not args: return [(layout, sorted(specs[layout].keys())) for layout in sorted(specs.keys())] errors = [] selection = [] for arg in args: if ":" in arg: layout, names = arg.split(":") if layout not in specs: errors.append("There is no spec for layout " + layout) continue names = names.split(",") for name in names: if name not in specs[layout]: errors.append(f"There is no spec for {layout}:{name}") selection.append((layout, names)) else: if arg not in specs: errors.append("There is no spec for layout " + arg) continue selection.append((arg, sorted(specs[arg].keys()))) if errors: raise LookupError("\n".join(errors)) return selection def main(args=None): logging.basicConfig( filename=env("LOG_PATH", "docs/screenshots/take_all.log"), format="%(asctime)s - %(levelname)s - %(message)s", level=logging.INFO, ) # get selection of specs, exit if they don't exist try: selection = get_selection(args) except LookupError: logging.exception("Wrong selection:") return 1 # switch to group original_group = client.current_group() client.switch_to_group("s") # take screenshots/animations for each selected spec ok = True for layout, names in selection: for name in names: success, errors = take(name, layout, specs[layout][name]) if success: logging.info("Shooting %s:%s - OK!", layout, name) else: ok = False logging.error("Shooting %s:%S - failed:\n%s", layout, name, errors) # switch back to original group client.switch_to_group(original_group) return 0 if ok else 1 if __name__ == "__main__": sys.exit(main(sys.argv[1:])) qtile-0.31.0/docs/screenshots/config.py0000664000175000017500000000350514762660347017750 0ustar epsilonepsilonimport os from libqtile import bar, layout, widget from libqtile.config import Key, Screen from libqtile.lazy import lazy def env(name, default): return os.environ.get(name, default) mod = "mod4" keys = [ Key([mod, "control"], "q", lazy.shutdown()), Key([mod], "Return", lazy.spawn("xterm")), Key([mod], "Left", lazy.layout.left()), Key([mod], "Right", lazy.layout.right()), Key([mod], "Up", lazy.layout.up()), Key([mod], "Down", lazy.layout.down()), Key([mod], "Tab", lazy.next_layout()), Key([mod, "shift"], "Tab", lazy.prev_layout()), ] border_focus = env("BORDER_FOCUS", "#ff0000") border_normal = env("BORDER_NORMAL", "#000000") border_width = int(env("BORDER_WIDTH", 8)) margin = int(env("MARGIN", 10)) borders = dict(border_focus=border_focus, border_normal=border_normal, border_width=border_width) style = dict(margin=margin, **borders) layouts = [ layout.Max(name="max"), layout.Bsp(name="bsp", **style), layout.Columns(name="columns", **style), layout.Floating(name="floating", **borders), layout.Matrix(name="matrix", **style), layout.MonadTall(name="monadtall", **style), layout.MonadWide(name="monadwide", **style), layout.RatioTile(name="ratiotile", **style), # layout.Slice(name="slice"), # Makes the session freeze layout.Stack(name="stack", autosplit=True, **style), layout.Tile(name="tile", **style), layout.TreeTab(name="treetab", border_width=border_width), layout.VerticalTile(name="verticaltile", **style), layout.Zoomy(name="zoomy", margin=margin), ] screens = [ Screen( bottom=bar.Bar( [ widget.GroupBox(), widget.WindowName(), widget.CurrentLayout(), widget.Clock(format="%Y-%m-%d %a %I:%M %p"), ], 24, ) ) ] qtile-0.31.0/docs/screenshots/take_one.py0000775000175000017500000001017714762660347020276 0ustar epsilonepsilon#!/usr/bin/env python3 import argparse import os import sys import time from screenshots import Client, Screenshooter def env(name, default): return os.environ.get(name, default) if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument( "-a", "--commands-after", dest="commands_after", default="", help="Commands to run after finishing to take screenshots. Space-separated string.", ) parser.add_argument( "-b", "--commands-before", dest="commands_before", default="", help="Commands to run before starting to take screenshots. Space-separated string.", ) parser.add_argument( "-c", "--clear", dest="clear", action="store_true", default=False, help="Whether to delete the PNG files after animating them into GIFs.", ) parser.add_argument( "-C", "--comment", dest="comment", default="", help="Comment to append at the end of the screenshot filenames.", ) parser.add_argument( "-d", "--delay", dest="delay", default=env("DELAY", "1x1"), help="Delay between each frame of the animated GIF. Default: 1x1.", ) parser.add_argument( "-g", "--screenshot-group", dest="screenshot_group", default="s", help="Group to switch to to take screenshots.", ) parser.add_argument( "-G", "--geometry", dest="geometry", default=env("GEOMETRY", "240x135"), help="The size of the generated screenshots (WIDTHxHEIGHT).", ) parser.add_argument( "-n", "--name", dest="name", default="", help="The name of the generated screenshot files . Don't append the extension.", ) parser.add_argument( "-o", "--output-dir", dest="output_dir", default="docs/screenshots/layout", help="Directory in which to write the screenshot files.", ) parser.add_argument( "-w", "--windows", dest="windows", type=int, default=3, help="Number of windows to spawn.", ) parser.add_argument( "layout", choices=[ "bsp", "columns", "matrix", "monadtall", "monadwide", "ratiotile", # "slice", "stack", "tile", "treetab", "verticaltile", "zoomy", ], help="Layout to use.", ) parser.add_argument( "commands", nargs=argparse.ONE_OR_MORE, help="Commands to run and take screenshots for.", ) args = parser.parse_args() client = Client() # keep current group in memory, to switch back to it later original_group = client.current_group() # prepare layout client.switch_to_group(args.screenshot_group) client.prepare_layout( args.layout, args.windows, args.commands_before.split(" ") if args.commands_before else [], ) # wait a bit to make sure everything is in place time.sleep(0.5) # prepare screenshot output path prefix output_dir = os.path.join(args.output_dir, args.layout) os.makedirs(output_dir, exist_ok=True) name = args.name or "_".join(args.commands) or args.layout if args.comment: name += f"-{args.comment}" output_prefix = os.path.join(output_dir, name) print(f"Shooting {output_prefix}") # run commands and take a screenshot between each, animate into a gif at the end screen = Screenshooter(output_prefix, args.geometry, args.delay) screen.shoot() for cmd in args.commands: client.run_layout_command(cmd) time.sleep(0.05) screen.shoot() screen.animate(clear=args.clear) if args.commands_after: for cmd in args.commands_after.split(" "): client.run_layout_command(cmd) time.sleep(0.05) # kill windows client.kill_group_windows() # switch back to original group client.switch_to_group(original_group) sys.exit(0) qtile-0.31.0/docs/screenshots/screenshots.py0000664000175000017500000001057414762660347021047 0ustar epsilonepsilonimport os import subprocess import time from libqtile.command_client import InteractiveCommandClient from libqtile.command_interface import IPCCommandInterface from libqtile.ipc import Client as IPCClient from libqtile.ipc import find_sockfile class Client: COLORS = [ "#44cc44", # green "#cc44cc", # magenta "#4444cc", # blue "#cccc44", # yellow "#44cccc", # cyan "#cccccc", # white "#777777", # gray "#ffa500", # orange "#333333", # black ] def __init__(self): self.color = 0 self.client = InteractiveCommandClient(IPCCommandInterface(IPCClient(find_sockfile()))) def current_group(self): return self.client.group[self.client.group.info().get("name")] def switch_to_group(self, group): if isinstance(group, str): self.client.group[group].toscreen() else: group.toscreen() def spawn_window(self, color=None): if color is None: color = self.color self.color += 1 if isinstance(color, int): color = Client.COLORS[color] self.client.spawn(f"xterm +ls -hold -e printf '\e]11;{color}\007'") # noqa: W605 def prepare_layout(self, layout, windows, commands=None): # set selected layout self.client.group.setlayout(layout) # spawn windows for i in range(windows): self.spawn_window() time.sleep(0.05) # prepare layout if commands: for cmd in commands: self.run_layout_command(cmd) time.sleep(0.05) def clean_layout(self, commands=None): if commands: for cmd in commands: self.run_layout_command(cmd) time.sleep(0.05) self.kill_group_windows() def run_layout_command(self, cmd): if cmd == "spawn": self.spawn_window() else: getattr(self.client.layout, cmd)() def kill_group_windows(self): while len(self.client.layout.info().get("clients")) > 0: try: self.client.window.kill() except Exception: pass self.color = 0 class Screenshooter: def __init__(self, output_prefix, geometry, animation_delay): self.output_prefix = output_prefix self.geometry = geometry self.number = 1 self.animation_delay = animation_delay self.output_paths = [] def shoot(self, numbered=True, compress="lossless"): if numbered: output_path = f"{self.output_prefix}.{self.number}.png" else: output_path = f"{self.output_prefix}.png" thumbnail_path = output_path.replace(".png", "-thumb.png") # take screenshot with scrot subprocess.call(["scrot", "-o", "-t", self.geometry, output_path]) # only keep the thumbnail os.rename(thumbnail_path, output_path) # compress PNG (only if pngquant is available) if compress: self.compress(compress, output_path) # add this path to the animation command self.output_paths.append(output_path) self.number += 1 def compress(self, method, file_path): compress_command = [ "pngquant", {"lossless": "--speed=1", "lossy": "--quality=0-90"}.get(method), "--strip", "--skip-if-larger", "--force", "--output", file_path, file_path, ] try: subprocess.call(compress_command) except FileNotFoundError: pass def animate(self, delays=None, clear=False): # TODO: use delays to build animation with custom delay between each frame animate_command = [ "convert", "-loop", "0", "-colors", "80", "-delay", self.animation_delay, ] + self.output_paths # last screenshot lasts two seconds in the gif, to see when the loop ends animate_command.extend( [ "-delay", "2x1", animate_command.pop(), f"{self.output_prefix}.gif", ] ) subprocess.call(animate_command) if clear: for output_path in self.output_paths: os.remove(output_path) qtile-0.31.0/docs/index.rst0000664000175000017500000000360614762660347015434 0ustar epsilonepsilon.. image:: /_static/qtile-logo.svg :align: center ======================================= Everything you need to know about Qtile ======================================= Qtile is a full-featured, hackable tiling window manager written and configured in Python. It's available both as an X11 window manager and also as :ref:`a Wayland compositor `. This documentation is designed to help you :doc:`install ` and :doc:`configure ` Qtile. Once it's up and running you'll probably want to start adding your own :doc:`customisations ` to have it running exactly the way you want. You'll find a lot of what you need within these docs but, if you still have some questions, you can find support in the following places: :IRC: irc://irc.oftc.net:6667/qtile :Discord: https://discord.gg/ehh233wCrC (Bridged with IRC) :Q&A: https://github.com/qtile/qtile/discussions/categories/q-a :Mailing List: https://groups.google.com/group/qtile-dev .. toctree:: :maxdepth: 1 :caption: Getting Started :hidden: manual/install/index Wayland manual/troubleshooting manual/commands/shell/index .. toctree:: :maxdepth: 1 :caption: Configuration :hidden: manual/config/default manual/config/index manual/ref/layouts manual/ref/widgets manual/ref/hooks manual/ref/extensions manual/commands/keybindings manual/stacking .. toctree:: :maxdepth: 1 :caption: Scripting :hidden: manual/commands/index manual/commands/interfaces manual/commands/api/index .. toctree:: :maxdepth: 1 :caption: Hacking :hidden: manual/hacking manual/contributing .. toctree:: :maxdepth: 1 :caption: Miscellaneous :hidden: manual/faq manual/howto/widget manual/howto/layout manual/howto/git manual/license manual/changelog qtile-0.31.0/docs/requirements-rtd.txt0000664000175000017500000000067014762660347017644 0ustar epsilonepsilon# ReadTheDocs currently builds on Ubuntu 20.04 which # seems to have some mismatch of the libffi library # and the latest release of cffi. # We can pin the version here for the docs build. cffi<=1.15.0 setuptools_scm # Sphinx 7.0.0 is not currently supported by the sphinx RTD theme sphinx<7.0.0 sphinx_rtd_theme funcparserlib==1.0.0a0 numpydoc pytest PyGObject dbus-fast cairocffi>=1.6.0 xcffib>=1.4.0 libcst>=1.0.0 readthedocs-sphinx-ext qtile-0.31.0/docs/Makefile0000664000175000017500000001507614762660347015237 0ustar epsilonepsilon# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build -E -W --keep-going PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext coverage genkeyimg genwidgetscreenshots help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" @echo " coverage to run coverage check of the documentation (if enabled)" @echo " genkeyimg to generate keybindings images. Will save in _static/keybindings" clean: -rm -rf $(BUILDDIR)/* html: genkeyimg $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Qtile.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Qtile.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/Qtile" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Qtile" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." coverage: $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage @echo "Testing of coverage in the sources finished, look at the " \ "results in $(BUILDDIR)/coverage/python.txt." genkeyimg: mkdir -p ./_static/keybindings rm -f ./_static/keybindings/*.png ../scripts/gen-keybinding-img -o ./_static/keybindings sed -i '/^\.\. LS_PNG/,/^\.\. END_LS_PNG/{/^\.\. image/d}' manual/commands/keybindings.rst sed -i '/^\.\. END_LS_PNG/e find _static/keybindings/* | awk '"'"'{ print length, $$0 }'"'"' | sort -n | cut -d" " -f2- | awk '"'"'{print ".. image:: /" $$1}'"'"'' manual/commands/keybindings.rst @echo @echo "Keybinding images have been generated." genwidgetscreenshots: rm -rf screenshots/widgets @echo "Generating screenshots for widgets using pytest fixtures." pytest -o python_files="ss_*.py" -o python_functions="ss_*" --backend x11 ../test @echo @echo "Generated widget screenshots" qtile-0.31.0/docs/qtile_docs/0000775000175000017500000000000014762660347015714 5ustar epsilonepsilonqtile-0.31.0/docs/qtile_docs/migrations.py0000664000175000017500000000375214762660347020451 0ustar epsilonepsilon# Copyright (c) 2023 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from docutils.parsers.rst import Directive, directives from libqtile.scripts.migrations import MIGRATIONS, load_migrations from qtile_docs.base import SimpleDirectiveMixin from qtile_docs.templates import qtile_migrations_full_template, qtile_migrations_template class QtileMigrations(SimpleDirectiveMixin, Directive): """ A custom directive that is used to display details about the migrations available to `qtile migrate`. """ required_arguments = 0 option_spec = { "summary": directives.flag, "help": directives.flag, } def make_rst(self): load_migrations() context = {"migrations": [(m, len(m.ID)) for m in MIGRATIONS]} if "summary" in self.options: rst = qtile_migrations_template.render(**context) elif "help" in self.options: rst = qtile_migrations_full_template.render(**context) yield from rst.splitlines() qtile-0.31.0/docs/qtile_docs/__init__.py0000664000175000017500000000000014762660347020013 0ustar epsilonepsilonqtile-0.31.0/docs/qtile_docs/base.py0000664000175000017500000000507714762660347017211 0ustar epsilonepsilon# Copyright (c) 2015 dmpayton # Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import inspect import pprint from docutils import nodes from docutils.parsers.rst import directives from docutils.statemachine import ViewList from sphinx.util.nodes import nested_parse_with_titles from libqtile.command.graph import _COMMAND_GRAPH_MAP def sphinx_escape(s): return pprint.pformat(s, compact=False, width=10000) def tidy_args(method): """Takes a method and returns a comma separated string of the method's arguments.""" sig = inspect.signature(method) args = str(sig).rsplit("(")[1].rsplit(")")[0].split(",") args = [f"{arg.strip()}" for arg in args[1:]] return ", ".join(args).replace("'", "") def command_nodes(argument): """Validator for directive options. Ensures argument is a valid command graph node.""" return directives.choice(argument, ["root"] + [node for node in _COMMAND_GRAPH_MAP]) class SimpleDirectiveMixin: """ Base class for custom Sphinx directives. Directives inheriting this class need to define a `make_rst` method which provides a generator yielding individual lines of rst. """ has_content = True required_arguments = 1 def make_rst(self): raise NotImplementedError def run(self): node = nodes.section() node.document = self.state.document result = ViewList() for line in self.make_rst(): result.append(line, f"<{self.__class__.__name__}>") nested_parse_with_titles(self.state, result, node) return node.children qtile-0.31.0/docs/qtile_docs/collapsible.py0000664000175000017500000000373614762660347020570 0ustar epsilonepsilon# Copyright (c) 2022 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from docutils import nodes from docutils.parsers.rst import Directive, directives from sphinx.util.nodes import nested_parse_with_titles class CollapsibleNode(nodes.Body, nodes.Element): def __init__(self, source, summary, *args, **kwargs): self.summary = summary super().__init__(source, *args, **kwargs) def visit_collapsible_node(self, node): self.body.append("
\n") if node.summary: self.body.append(f"{node.summary}\n") def depart_collapsible_node(self, node): self.body.append("
\n") class CollapsibleSection(Directive): option_spec = { "summary": directives.unchanged, } has_content = True def run(self): summary = self.options.get("summary", "") node = CollapsibleNode("\n".join(self.content), summary) nested_parse_with_titles(self.state, self.content, node) return [node] qtile-0.31.0/docs/qtile_docs/hooks.py0000664000175000017500000000314314762660347017412 0ustar epsilonepsilon# Copyright (c) 2015 dmpayton # Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from docutils.parsers.rst import Directive from libqtile.utils import import_class from qtile_docs.base import SimpleDirectiveMixin from qtile_docs.templates import qtile_hooks_template class QtileHooks(SimpleDirectiveMixin, Directive): def make_rst(self): module, class_name = self.arguments[0].rsplit(".", 1) obj = import_class(module, class_name) for method in sorted(obj.hooks): rst = qtile_hooks_template.render(method=method) yield from rst.splitlines() qtile-0.31.0/docs/qtile_docs/commands.py0000664000175000017500000001401414762660347020067 0ustar epsilonepsilon# Copyright (c) 2015 dmpayton # Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import importlib import inspect from docutils.parsers.rst import Directive, directives from libqtile import command from libqtile.utils import import_class from qtile_docs.base import SimpleDirectiveMixin, command_nodes from qtile_docs.templates import qtile_commands_template class QtileCommands(SimpleDirectiveMixin, Directive): """ A custom directive that is used to display the commands exposed to the command graph for a given object. This is used to ensure the public API is up to date in our documentation. Any CommandObject with a command decorated with `@expose_command` will appear in the list of available commands. """ optional_arguments = 8 option_spec = { "baseclass": directives.unchanged, # includebase: Includes the baseclass object in the results "includebase": directives.flag, # object-node: name of the node on the command graph (e.g. widget, group etc.) "object-node": command_nodes, # object-selector-name: the object's selector is the object's name "object-selector-name": directives.flag, # object-selector-id: the object's selector is a numeric ID "object-selector-id": directives.flag, # object-selector-string: the object's selector is a custom strinh "object-selector-string": str, # no-title: don't include the object's name in the output # (useful when object has generic name e.g. backend Cors objects) "no-title": directives.flag, # exclude-command-object-methods: exclude methods inherited from CommandObject "exclude-command-object-methods": directives.flag, } def get_class_commands(self, cls, exclude_inherited=False): commands = [] for attr in dir(cls): # Some attributes will result in an AttributeError so we can skip them if not hasattr(cls, attr): continue method = getattr(cls, attr) # If it's not a callable method then we don't need to look at it if not callable(method): continue # Check if method has been exposed to the command graph if getattr(method, "_cmd", False): # If we're excluding inherited commands then check for them here if exclude_inherited and hasattr(command.base.CommandObject, method): continue commands.append(attr) return commands def make_interface_syntax(self, obj): """ Builds strings to show the lazy and cmd-obj syntax used to access the commands for the given object. """ lazy = "lazy" cmdobj = ["qtile", "cmd-obj", "-o"] # Get the node on the command graph node = self.options.get("object-node", "") if not node: # cmd-obj needs the root note to be specified cmdobj.append("cmd") else: lazy += f".{node}" cmdobj.append(node) # Give an example of an object selector if "object-selector-name" in self.options: name = obj.__name__.lower() lazy += f'["{name}"]' cmdobj.append(name) elif "object-selector-id" in self.options: lazy += "[ID]" cmdobj.append("[ID]") elif "object-selector-string" in self.options: selector = self.options["object-selector-string"] lazy += f'["{selector}"]' cmdobj.append(selector) # Add syntax to call the command lazy += ".()" cmdobj.extend(["-f", ""]) interfaces = {"lazy": lazy, "cmdobj": " ".join(cmdobj)} return interfaces def make_rst(self): module = importlib.import_module(self.arguments[0]) exclude_inherited = "exclude-command-object-methods" in self.options includebase = "includebase" in self.options no_title = "no-title" in self.options baseclass = self.options.get("baseclass", "libqtile.command.base.CommandObject") self.baseclass = import_class(*baseclass.rsplit(".", 1)) for item in dir(module): obj = import_class(self.arguments[0], item) if ( not inspect.isclass(obj) or not issubclass(obj, self.baseclass) or (obj is self.baseclass and not includebase) ): continue commands = sorted(self.get_class_commands(obj, exclude_inherited)) context = { "objectname": f"{obj.__module__}.{obj.__name__}", "module": obj.__module__, "baseclass": obj.__name__, "underline": "=" * len(obj.__name__), "commands": commands, "no_title": no_title, "interfaces": self.make_interface_syntax(obj), } rst = qtile_commands_template.render(**context) yield from rst.splitlines() qtile-0.31.0/docs/qtile_docs/templates/0000775000175000017500000000000014762660347017712 5ustar epsilonepsilonqtile-0.31.0/docs/qtile_docs/templates/command.py0000664000175000017500000000335614762660347021711 0ustar epsilonepsilon# Copyright (c) 2015 dmpayton # Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from jinja2 import Template qtile_commands_template = Template( """ {% if not no_title %} {{ baseclass }} {{ underline }} {% endif %} .. py:currentmodule:: {{ module }} .. py:class:: {{ baseclass }} **API commands** To access commands on this object via the command graph, use one of the following options: .. list-table:: * - {{ interfaces["lazy"] }} * - {{ interfaces["cmdobj"] }} The following commands are available for this object: .. autosummary:: {% for cmd in commands %} {{ cmd }} {% endfor %} **Command documentation** {% for cmd in commands %} .. automethod:: {{ cmd }} {% endfor %} """ ) qtile-0.31.0/docs/qtile_docs/templates/migrations.py0000664000175000017500000000314714762660347022445 0ustar epsilonepsilon# Copyright (c) 2023 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from jinja2 import Template qtile_migrations_template = Template( """ .. list-table:: :widths: 30 20 50 :header-rows: 1 * - ID - Changes introduced after version - Summary {% for m, _ in migrations %} * - `{{ m.ID }}`_ - {{ m.AFTER_VERSION }} - {{ m.SUMMARY }} {% endfor %} """ ) qtile_migrations_full_template = Template( """ {% for m, len in migrations %} {{ m.ID }} {{ "~" * len }} .. list-table:: * - Migration introduced after version - {{ m.AFTER_VERSION}} {{ m.show_help() }} {% endfor %} """ ) qtile-0.31.0/docs/qtile_docs/templates/__init__.py0000664000175000017500000000350514762660347022026 0ustar epsilonepsilon# Copyright (c) 2015 dmpayton # Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from qtile_docs.templates.command import qtile_commands_template # noqa: F401 from qtile_docs.templates.graph import qtile_graph_template # noqa: F401 from qtile_docs.templates.hook import qtile_hooks_template # noqa: F401 from qtile_docs.templates.migrations import ( # noqa: F401 qtile_migrations_full_template, qtile_migrations_template, ) from qtile_docs.templates.module import qtile_module_template # noqa: F401 from qtile_docs.templates.qtile_class import qtile_class_template # noqa: F401 __all__ = [ "qtile_commands_template", "qtile_graph_template", "qtile_hooks_template", "qtile_module_template", "qtile_class_template", "qtile_migrations_template", "qtile_migrations_full_template", ] qtile-0.31.0/docs/qtile_docs/templates/hook.py0000664000175000017500000000234114762660347021224 0ustar epsilonepsilon# Copyright (c) 2015 dmpayton # Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from jinja2 import Template qtile_hooks_template = Template( """ .. automethod:: libqtile.hook.subscribe.{{ method }} """ ) qtile-0.31.0/docs/qtile_docs/templates/module.py0000664000175000017500000000247214762660347021556 0ustar epsilonepsilon# Copyright (c) 2015 dmpayton # Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from jinja2 import Template qtile_module_template = Template( """ .. qtile_class:: {{ module }}.{{ class_name }} {% if no_config %}:no-config:{% endif %} {% if no_commands %}:no-commands:{% endif %} """ ) qtile-0.31.0/docs/qtile_docs/templates/graph.py0000664000175000017500000000236214762660347021370 0ustar epsilonepsilon# Copyright (c) 2022 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from jinja2 import Template qtile_graph_template = Template( """ .. graphviz:: :layout: neato :align: center {% for line in graph %} {{ line }}{% endfor %} """ ) qtile-0.31.0/docs/qtile_docs/templates/qtile_class.py0000664000175000017500000000524014762660347022570 0ustar epsilonepsilon# Copyright (c) 2015 dmpayton # Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from jinja2 import Template qtile_class_template = Template( """ {{ class_name }} {{ class_underline }} .. autoclass:: {{ module }}.{{ class_name }}{% for arg in extra_arguments %} {{ arg }}{% endfor %} :noindex: {% if is_widget %} .. compound:: Supported bar orientations: {{ obj.orientations }} {% if supported_backends %} Only available on the following backends: {{ ", ".join(obj.supported_backends) }} {% endif %} {% endif %} {% if is_widget and screen_shots %} .. raw:: html {% for sshot, conf in screen_shots.items() %} {% if conf %} {% else %} {% endif %} {% endfor %}
example config
{{ conf }}default
{% endif %} {% if configurable %} **Configuration options** .. list-table:: :widths: 20 20 60 :header-rows: 1 * - key - default - description {% for key, default, description in defaults %} * - ``{{ key }}`` - ``{{ default }}`` - {{ description[1:-1] }} {% endfor %} {% endif %} {% if commandable %} **Available commands** Click to view the available commands for :py:class:`{{ class_name }} <{{ obj.__module__ }}.{{ class_name }}>` {% endif %} """ ) qtile-0.31.0/docs/qtile_docs/module.py0000664000175000017500000000512014762660347017551 0ustar epsilonepsilon# Copyright (c) 2015 dmpayton # Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import importlib import inspect from docutils.parsers.rst import Directive, directives from libqtile.utils import import_class from qtile_docs.base import SimpleDirectiveMixin from qtile_docs.templates import qtile_module_template class QtileModule(SimpleDirectiveMixin, Directive): optional_arguments = 3 option_spec = { "baseclass": directives.unchanged, "no-config": directives.flag, "no-commands": directives.flag, # Comma separated list of object names to skip "exclude": directives.unchanged, } def make_rst(self): module = importlib.import_module(self.arguments[0]) baseclass = None if "baseclass" in self.options: baseclass = import_class(*self.options["baseclass"].rsplit(".", 1)) for item in dir(module): if item in self.options.get("exclude", "").split(","): continue obj = import_class(self.arguments[0], item) if not inspect.isclass(obj) or (baseclass and not issubclass(obj, baseclass)): continue context = { "module": self.arguments[0], "class_name": item, "no_config": "no-config" in self.options, "no_commands": "no-commands" in self.options, } rst = qtile_module_template.render(**context) for line in rst.splitlines(): if not line.strip(): continue yield line qtile-0.31.0/docs/qtile_docs/graph.py0000664000175000017500000001435314762660347017375 0ustar epsilonepsilon# Copyright (c) 2022 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from dataclasses import dataclass from docutils.parsers.rst import Directive, directives from libqtile.command import graph from qtile_docs.base import SimpleDirectiveMixin from qtile_docs.templates import qtile_graph_template DISABLED_COLOUR = "Gray" @dataclass class Node: node: graph.CommandGraphNode x: float y: float fillcolor: str color: str url: str @property def name(self): return getattr(self.node, "object_type", "root") @property def children(self): return self.node.children def node_args(self, enabled=True, highlight=False, relative_url=""): """Returns a dict of arguments that can be formatted for graphviz.""" return { "pos": f"{self.x},{self.y}!", "color": self.color if enabled else DISABLED_COLOUR, "fillcolor": self.fillcolor if enabled else DISABLED_COLOUR, "href": f"{relative_url}{self.url}", "style": "filled", "label": self.name, "fontname": "bold" if highlight else "regular", } ROOT = graph.CommandGraphRoot() # Define our nodes with their positions, colours and link to API docs page. NODES = [ Node(ROOT, 0, 0, "Gray", "DarkGray", "root.html"), Node(graph._BarGraphNode, -1.94, -0.44, "Violet", "Purple", "bars.html"), Node( graph._CoreGraphNode, -1.56, 1.24, "SlateBlue1", "SlateBlue", "backend.html", ), Node( graph._GroupGraphNode, 1.56, 1.24, "Orange", "OrangeRed", "groups.html", ), Node( graph._LayoutGraphNode, 1.94, -0.44, "Gold", "Goldenrod", "layouts.html", ), Node( graph._ScreenGraphNode, 0.86, -1.8, "LimeGreen", "DarkGreen", "screens.html", ), Node( graph._WidgetGraphNode, -0.86, -1.8, "LightBlue", "Blue", "widgets.html", ), Node(graph._WindowGraphNode, 0, 2, "Tomato", "Red", "windows.html"), ] # Convenient dict to access node object via node name NODES_MAP = {n.name: n for n in NODES} COMMAND_MAP = {n.name: n.children for n in NODES} # Generate a list of all routest in the map. # Each route is a tuple of (start, end, bidirectional) ROUTES = [] for node, children in COMMAND_MAP.items(): for child in children: route = (node, child, node in COMMAND_MAP[child]) # Check that the reverse route is not in the list already if (child, node, node in COMMAND_MAP[child]) not in ROUTES: ROUTES.append(route) class QtileGraph(SimpleDirectiveMixin, Directive): required_arguments = 0 option_spec = { "root": directives.unchanged, "api_page_root": directives.unchanged, } def make_nodes(self): """Generates the node definition lines.""" node_lines = [] for name, node in NODES_MAP.items(): args_dict = node.node_args( name in self.visible_nodes, name == self.graph_name, self.options.get("api_page_root", ""), ) args_string = ", ".join(f'{k}="{v}"' for k, v in args_dict.items()) node_lines.extend([f"node [{args_string}];", f"{name};", ""]) return node_lines def make_routes(self): """Generates the route definition lines.""" route_lines = [] for r in ROUTES: args = {} if r not in self.visible_routes: args["color"] = DISABLED_COLOUR if r[2]: args["dir"] = "both" line = f"{r[0]} -> {r[1]}" if args: args_string = ", ".join(f'{k}="{v}"' for k, v in args.items()) line += f" [{args_string}]" line += ";" route_lines.append(line) return route_lines def find_linked_nodes_routes(self, node): """Identifies routes connected to the selected node.""" nodes = [] routes = [] for r in ROUTES: # Our node is the starting node if r[0] == node: nodes.append(r[1]) routes.append(r) # Our node is the ending node and it's a bidirectional route elif r[1] == node and r[2]: nodes.append(r[0]) routes.append(r) return (nodes, routes) def make_rst(self): self.graph_name = self.options.get("root", "all") if self.graph_name == "all": self.visible_nodes = [n for n in NODES_MAP] self.visible_routes = ROUTES[:] else: linked_nodes, linked_routes = self.find_linked_nodes_routes(self.graph_name) self.visible_nodes = [self.graph_name] self.visible_nodes.extend(linked_nodes) self.visible_routes = linked_routes graph = [] graph.append(f"strict digraph {self.graph_name} {{") graph.append('bgcolor="transparent"') graph.extend(self.make_nodes()) graph.extend(self.make_routes()) graph.append("}") rst = qtile_graph_template.render(graph=graph) yield from rst.splitlines() qtile-0.31.0/docs/qtile_docs/qtile_class.py0000664000175000017500000001076314762660347020600 0ustar epsilonepsilon# Copyright (c) 2015 dmpayton # Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import inspect import json from pathlib import Path from unittest.mock import MagicMock from docutils.parsers.rst import Directive, directives from libqtile import command, configurable, widget from libqtile.utils import import_class from qtile_docs.base import SimpleDirectiveMixin, sphinx_escape from qtile_docs.templates import qtile_class_template class QtileClass(SimpleDirectiveMixin, Directive): optional_arguments = 2 option_spec = { "no-config": directives.flag, "no-commands": directives.flag, "noindex": directives.flag, } def make_rst(self): module, class_name = self.arguments[0].rsplit(".", 1) obj = import_class(module, class_name) is_configurable = "no-config" not in self.options is_commandable = "no-commands" not in self.options is_widget = issubclass(obj, widget.base._Widget) arguments = [ f":{i}:" for i in self.options.keys() if i not in ("no-config", "no-commands") ] # build up a dict of defaults using reverse MRO defaults = {} for klass in reversed(obj.mro()): if not issubclass(klass, configurable.Configurable): continue if not hasattr(klass, "defaults"): continue klass_defaults = getattr(klass, "defaults") defaults.update({d[0]: d[1:] for d in klass_defaults}) # turn the dict into a list of ("value", "default", "description") tuples defaults = [ (k, sphinx_escape(v[0]), sphinx_escape(v[1])) for k, v in sorted(defaults.items()) ] if len(defaults) == 0: is_configurable = False is_widget = issubclass(obj, widget.base._Widget) if is_widget: index = ( Path(__file__).parent.parent / "_static" / "screenshots" / "widgets" / "shots.json" ) try: with open(index) as f: shots = json.load(f) except (FileNotFoundError, json.JSONDecodeError): shots = {} widget_shots = shots.get(class_name.lower(), dict()) else: widget_shots = {} widget_shots = { f"../../widgets/{class_name.lower()}/{k}.png": v for k, v in widget_shots.items() } context = { "module": module, "class_name": class_name, "class_underline": "=" * len(class_name), "obj": obj, "defaults": defaults, "configurable": is_configurable and issubclass(obj, configurable.Configurable), "commandable": is_commandable and issubclass(obj, command.base.CommandObject), "is_widget": is_widget, "extra_arguments": arguments, "screen_shots": widget_shots, "supported_backends": is_widget and obj.supported_backends, } if context["commandable"]: context["commands"] = [ # Command methods have the "_cmd" attribute so we check for this # However, some modules are Mocked so we need to exclude them attr.__name__ for _, attr in inspect.getmembers(obj) if hasattr(attr, "_cmd") and not isinstance(attr, MagicMock) ] rst = qtile_class_template.render(**context) yield from rst.splitlines() qtile-0.31.0/test/0000775000175000017500000000000014762660347013615 5ustar epsilonepsilonqtile-0.31.0/test/configs/0000775000175000017500000000000014762660347015245 5ustar epsilonepsilonqtile-0.31.0/test/configs/basic.py0000664000175000017500000000234214762660347016701 0ustar epsilonepsilon# Copyright (c) 2011 Florian Mounier # Copyright (c) 2012 Tycho Andersen # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from libqtile import config keys = [config.Key(["control"], "k", "focusnext")] screens = [] layouts = [] groups = [] qtile-0.31.0/test/configs/syntaxerr.py0000664000175000017500000000233214762660347017656 0ustar epsilonepsilon# noqa: E902 # Copyright (c) 2008 Aldo Cortesi # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # # It is really dumb that we need a giant copyright notice for this one line # file. Fucking lawyers :( # noqa: E305 sdf [ # noqa: E999 qtile-0.31.0/test/configs/reloading.py0000664000175000017500000000446514762660347017574 0ustar epsilonepsilon# This is used for test_manager.py::test_reload_config # # The exported configuration variables have a different value depending on whether # libqtile has a 'test_data' attribute (see below) import sys from pathlib import Path from libqtile import bar, layout, qtile, widget from libqtile.config import Drag, DropDown, Group, Key, Match, Rule, ScratchPad, Screen from libqtile.dgroups import simple_key_binder from libqtile.lazy import lazy keys = [ Key(["mod4"], "h", lazy.layout.left(), desc="Move focus to left"), ] groups = [Group(i) for i in "12345"] layouts = [ layout.Columns(border_focus_stack=["#d75f5f", "#8f3d3d"], border_width=4), ] screens = [ Screen( bottom=bar.Bar( [ widget.CurrentLayout(), widget.GroupBox(), widget.Clock(format="%Y-%m-%d %a %I:%M %p"), widget.QuickExit(), ], 24, ), ), ] widget_defaults = dict() mouse = [ Drag( ["mod4"], "Button1", lazy.window.set_position_floating(), start=lazy.window.get_position(), ), ] windowpy = Path(__file__).parent.parent / "scripts" / "window.py" script = " ".join([sys.executable, windowpy.as_posix(), "--name", "dd", "dd", "normal"]) dropdowns = [DropDown("dropdown1", script)] dgroups_key_binder = None dgroups_app_rules = [] floating_layout = layout.Floating(float_rules=[Match(title="one")]) wmname = "LG3D" if hasattr(qtile, "test_data"): # Add more items or change values qtile.test_data is set keys.append( Key(["mod4"], "l", lazy.layout.right(), desc="Move focus to right"), ) groups.extend([Group(i) for i in "6789"]) layouts.append(layout.Max()) screens = [ Screen(top=bar.Bar([widget.CurrentLayout()], 32)), ] widget_defaults["background"] = "#ff0000" mouse.append( Drag( ["mod4"], "Button3", lazy.window.set_size_floating(), start=lazy.window.get_size(), ), ) dropdowns.append(DropDown("dropdown2", script)) dgroups_key_binder = simple_key_binder dgroups_app_rules = [Rule(Match(wm_class="test"), float=True)] floating_layout = layout.Floating() wmname = "TEST" qtile.test_data_config_evaluations += 1 groups.append(ScratchPad("S", dropdowns)) qtile-0.31.0/test/layouts/0000775000175000017500000000000014762660347015315 5ustar epsilonepsilonqtile-0.31.0/test/layouts/test_stack.py0000664000175000017500000002021414762660347020032 0ustar epsilonepsilon# Copyright (c) 2011 Florian Mounier # Copyright (c) 2012, 2014-2015 Tycho Andersen # Copyright (c) 2013 Mattias Svala # Copyright (c) 2013 Craig Barnes # Copyright (c) 2014 ramnes # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Adi Sieker # Copyright (c) 2014 Chris Wesseling # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest import libqtile.config from libqtile import layout from libqtile.confreader import Config from test.layouts.layout_utils import assert_focus_path, assert_focused class StackConfig(Config): auto_fullscreen = True groups = [ libqtile.config.Group("a"), libqtile.config.Group("b"), libqtile.config.Group("c"), libqtile.config.Group("d"), ] layouts = [ layout.Stack(num_stacks=2), layout.Stack(num_stacks=1), ] floating_layout = libqtile.resources.default_config.floating_layout keys = [] mouse = [] screens = [] follow_mouse_focus = False stack_config = pytest.mark.parametrize("manager", [StackConfig], indirect=True) def _stacks(manager): stacks = [] for i in manager.c.layout.info()["stacks"]: windows = i["clients"] current = i["current"] stacks.append(windows[current:] + windows[:current]) return stacks @stack_config def test_stack_commands(manager): assert manager.c.layout.info()["current_stack"] == 0 manager.test_window("one") assert _stacks(manager) == [["one"], []] assert manager.c.layout.info()["current_stack"] == 0 manager.test_window("two") assert _stacks(manager) == [["one"], ["two"]] assert manager.c.layout.info()["current_stack"] == 1 manager.test_window("three") assert _stacks(manager) == [["one"], ["three", "two"]] assert manager.c.layout.info()["current_stack"] == 1 manager.c.layout.delete() assert _stacks(manager) == [["one", "three", "two"]] info = manager.c.get_groups()["a"] assert info["focus"] == "one" manager.c.layout.delete() assert len(_stacks(manager)) == 1 manager.c.layout.add() assert _stacks(manager) == [["one", "three", "two"], []] manager.c.layout.rotate() assert _stacks(manager) == [[], ["one", "three", "two"]] @stack_config def test_stack_down(manager): manager.c.layout.down() @stack_config def test_stack_addremove(manager): one = manager.test_window("one") manager.c.layout.next() two = manager.test_window("two") three = manager.test_window("three") assert _stacks(manager) == [["one"], ["three", "two"]] assert manager.c.layout.info()["current_stack"] == 1 manager.kill_window(three) assert manager.c.layout.info()["current_stack"] == 1 manager.kill_window(two) assert manager.c.layout.info()["current_stack"] == 0 manager.c.layout.next() two = manager.test_window("two") manager.c.layout.next() assert manager.c.layout.info()["current_stack"] == 0 manager.kill_window(one) assert manager.c.layout.info()["current_stack"] == 1 @stack_config def test_stack_rotation(manager): manager.c.layout.delete() manager.test_window("one") manager.test_window("two") manager.test_window("three") assert _stacks(manager) == [["three", "two", "one"]] manager.c.layout.down() assert _stacks(manager) == [["two", "one", "three"]] manager.c.layout.up() assert _stacks(manager) == [["three", "two", "one"]] manager.c.layout.down() manager.c.layout.down() assert _stacks(manager) == [["one", "three", "two"]] @stack_config def test_stack_nextprev(manager): manager.c.layout.add() one = manager.test_window("one") two = manager.test_window("two") three = manager.test_window("three") assert manager.c.get_groups()["a"]["focus"] == "three" manager.c.layout.next() assert manager.c.get_groups()["a"]["focus"] == "one" manager.c.layout.previous() assert manager.c.get_groups()["a"]["focus"] == "three" manager.c.layout.previous() assert manager.c.get_groups()["a"]["focus"] == "two" manager.c.layout.next() manager.c.layout.next() manager.c.layout.next() assert manager.c.get_groups()["a"]["focus"] == "two" manager.kill_window(three) manager.c.layout.next() assert manager.c.get_groups()["a"]["focus"] == "one" manager.c.layout.previous() assert manager.c.get_groups()["a"]["focus"] == "two" manager.c.layout.next() manager.kill_window(two) manager.c.layout.next() assert manager.c.get_groups()["a"]["focus"] == "one" manager.kill_window(one) manager.c.layout.next() assert manager.c.get_groups()["a"]["focus"] is None manager.c.layout.previous() assert manager.c.get_groups()["a"]["focus"] is None @stack_config def test_stack_window_removal(manager): manager.c.layout.next() manager.test_window("one") two = manager.test_window("two") manager.c.layout.down() manager.kill_window(two) @stack_config def test_stack_split(manager): manager.test_window("one") manager.test_window("two") manager.test_window("three") stacks = manager.c.layout.info()["stacks"] assert not stacks[1]["split"] manager.c.layout.toggle_split() stacks = manager.c.layout.info()["stacks"] assert stacks[1]["split"] @stack_config def test_stack_shuffle(manager): manager.c.next_layout() manager.test_window("one") manager.test_window("two") manager.test_window("three") stack = manager.c.layout.info()["stacks"][0] assert stack["clients"][stack["current"]] == "three" for i in range(5): manager.c.layout.shuffle_up() stack = manager.c.layout.info()["stacks"][0] assert stack["clients"][stack["current"]] == "three" for i in range(5): manager.c.layout.shuffle_down() stack = manager.c.layout.info()["stacks"][0] assert stack["clients"][stack["current"]] == "three" @stack_config def test_stack_client_to(manager): manager.test_window("one") manager.test_window("two") assert manager.c.layout.info()["stacks"][0]["clients"] == ["one"] manager.c.layout.client_to_previous() assert manager.c.layout.info()["stacks"][0]["clients"] == ["two", "one"] manager.c.layout.client_to_previous() assert manager.c.layout.info()["stacks"][0]["clients"] == ["one"] assert manager.c.layout.info()["stacks"][1]["clients"] == ["two"] manager.c.layout.client_to_next() assert manager.c.layout.info()["stacks"][0]["clients"] == ["two", "one"] @stack_config def test_stack_info(manager): manager.test_window("one") assert manager.c.layout.info()["stacks"] @stack_config def test_stack_window_focus_cycle(manager): # setup 3 tiled and two floating clients manager.test_window("one") manager.test_window("two") manager.test_window("float1") manager.c.window.toggle_floating() manager.test_window("float2") manager.c.window.toggle_floating() manager.test_window("three") # test preconditions, stack adds clients at pos of current assert manager.c.layout.info()["clients"] == ["three", "one", "two"] # last added window has focus assert_focused(manager, "three") # assert window focus cycle, according to order in layout assert_focus_path(manager, "one", "two", "float1", "float2", "three") qtile-0.31.0/test/layouts/test_base.py0000664000175000017500000000603414762660347017643 0ustar epsilonepsilon# Copyright (c) 2021 Jeroen Wijenbergh # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest import libqtile from libqtile.command.base import expose_command from libqtile.confreader import Config from libqtile.layout.base import _SimpleLayoutBase class DummyLayout(_SimpleLayoutBase): defaults = [ ("current_offset", 0, ""), ("current_position", None, ""), ] def __init__(self, **config): _SimpleLayoutBase.__init__(self, **config) self.add_defaults(DummyLayout.defaults) def add_client(self, client): return super().add_client( client, offset_to_current=self.current_offset, client_position=self.current_position ) def configure(self, client, screen_rect): pass @expose_command("up") def previous(self): _SimpleLayoutBase.previous() @expose_command("down") def next(self): _SimpleLayoutBase.next() class BaseLayoutConfigBottom(Config): auto_fullscreen = True groups = [libqtile.config.Group("a")] layouts = [DummyLayout(current_position="bottom")] floating_layout = libqtile.resources.default_config.floating_layout keys = [] mouse = [] screens = [] class BaseLayoutConfigTop(Config): auto_fullscreen = True groups = [libqtile.config.Group("a")] layouts = [DummyLayout(current_position="top")] floating_layout = libqtile.resources.default_config.floating_layout keys = [] mouse = [] screens = [] baselayoutconfigbottom = pytest.mark.parametrize( "manager", [BaseLayoutConfigBottom], indirect=True ) baselayoutconfigtop = pytest.mark.parametrize("manager", [BaseLayoutConfigTop], indirect=True) @baselayoutconfigbottom def test_base_client_position_bottom(manager): manager.test_window("one") manager.test_window("two") assert manager.c.layout.info()["clients"] == ["one", "two"] @baselayoutconfigtop def test_base_client_position_top(manager): manager.test_window("one") manager.test_window("two") assert manager.c.layout.info()["clients"] == ["two", "one"] qtile-0.31.0/test/layouts/layout_utils.py0000664000175000017500000000746614762660347020441 0ustar epsilonepsilon# Copyright (c) 2011 Florian Mounier # Copyright (c) 2012, 2014-2015 Tycho Andersen # Copyright (c) 2013 Mattias Svala # Copyright (c) 2013 Craig Barnes # Copyright (c) 2014 ramnes # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Adi Sieker # Copyright (c) 2014 Chris Wesseling # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from libqtile.command.base import CommandError def assert_focused(self, name): """Asserts that window with specified name is currently focused""" info = self.c.window.info() assert info["name"] == name, "Got {!r}, expected {!r}".format(info["name"], name) def assert_unfocused(self, name): """Asserts that window with specified name is currently not focused""" try: info = self.c.window.info() except CommandError: # no current window, all unfocused return assert info["name"] != name, f"Got {name}, expected inequality." def assert_dimensions(self, x, y, w, h, win=None): """Asserts dimensions of window""" if win is None: win = self.c.window info = win.info() assert info["x"] == x, info assert info["y"] == y, info assert info["width"] == w, info assert info["height"] == h, info def assert_dimensions_fit(self, x, y, w, h, win=None): """Asserts that window is within the given bounds""" if win is None: win = self.c.window info = win.info() assert info["x"] >= x, info assert info["y"] >= y, info assert info["width"] <= w, info assert info["height"] <= h, info def assert_focus_path(self, *names): """ Asserts that subsequent calls to next_window() focus the open windows in the given order (and prev_window() in the reverse order) """ for i in names: self.c.group.next_window() assert_focused(self, i) # let's check twice for sure for i in names: self.c.group.next_window() assert_focused(self, i) # Ok, let's check backwards now for i in reversed(names): assert_focused(self, i) self.c.group.prev_window() # and twice for sure for i in reversed(names): assert_focused(self, i) self.c.group.prev_window() def assert_focus_path_unordered(self, *names): """ Wrapper of assert_focus_path that allows the actual focus path to be different from the given one, as long as: 1) the focus order is always the same at every forward cycle 2) the focus order is always the opposite at every reverse cycle 3) all the windows are selected once and only once at every cycle """ unordered_names = list(names) ordered_names = [] while unordered_names: self.c.group.next_window() wname = self.c.window.info()["name"] assert wname in unordered_names unordered_names.remove(wname) ordered_names.append(wname) assert_focus_path(ordered_names) qtile-0.31.0/test/layouts/test_floating.py0000664000175000017500000000620514762660347020534 0ustar epsilonepsilon# Copyright (c) 2008, Aldo Cortesi. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest import libqtile.config from libqtile import layout from libqtile.confreader import Config from test.layouts.layout_utils import assert_focused class FloatingConfig(Config): auto_fullscreen = True groups = [ libqtile.config.Group("a"), ] layouts = [layout.Floating()] floating_layout = layout.Floating( fullscreen_border_width=15, max_border_width=10, ) keys = [] mouse = [] screens = [] follow_mouse_focus = False floating_config = pytest.mark.parametrize("manager", [FloatingConfig], indirect=True) @floating_config def test_float_next_prev_window(manager): # spawn three windows manager.test_window("one") manager.test_window("two") manager.test_window("three") # focus previous windows assert_focused(manager, "three") manager.c.group.prev_window() assert_focused(manager, "two") manager.c.group.prev_window() assert_focused(manager, "one") # checking that it loops around properly manager.c.group.prev_window() assert_focused(manager, "three") # focus next windows # checking that it loops around properly manager.c.group.next_window() assert_focused(manager, "one") manager.c.group.next_window() assert_focused(manager, "two") manager.c.group.next_window() assert_focused(manager, "three") @floating_config def test_border_widths(manager): manager.test_window("one") # Default geometry info = manager.c.window.info() assert info["x"] == 350 assert info["y"] == 250 assert info["width"] == 100 assert info["height"] == 100 # Fullscreen manager.c.window.enable_fullscreen() info = manager.c.window.info() assert info["x"] == 0 assert info["y"] == 0 assert info["width"] == 770 assert info["height"] == 570 manager.c.window.disable_fullscreen() # Maximized manager.c.window.toggle_maximize() info = manager.c.window.info() assert info["x"] == 0 assert info["y"] == 0 assert info["width"] == 780 assert info["height"] == 580 qtile-0.31.0/test/layouts/test_tile.py0000664000175000017500000001407514762660347017672 0ustar epsilonepsilon# Copyright (c) 2011 Florian Mounier # Copyright (c) 2012, 2014-2015 Tycho Andersen # Copyright (c) 2013 Mattias Svala # Copyright (c) 2013 Craig Barnes # Copyright (c) 2014 ramnes # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Adi Sieker # Copyright (c) 2014 Chris Wesseling # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest import libqtile.config from libqtile import layout from libqtile.confreader import Config from test.layouts.layout_utils import assert_focus_path, assert_focused class TileConfig(Config): auto_fullscreen = True groups = [ libqtile.config.Group("a"), libqtile.config.Group("b"), libqtile.config.Group("c"), libqtile.config.Group("d"), ] layouts = [ layout.Tile(), layout.Tile(master_length=2), layout.Tile(add_on_top=False), ] floating_layout = libqtile.resources.default_config.floating_layout keys = [] mouse = [] screens = [] follow_mouse_focus = False tile_config = pytest.mark.parametrize("manager", [TileConfig], indirect=True) @tile_config def test_tile_updown(manager): manager.test_window("one") manager.test_window("two") manager.test_window("three") assert manager.c.layout.info()["clients"] == ["three", "two", "one"] manager.c.layout.shuffle_down() assert manager.c.layout.info()["clients"] == ["two", "one", "three"] manager.c.layout.shuffle_up() assert manager.c.layout.info()["clients"] == ["three", "two", "one"] @tile_config def test_tile_nextprev(manager): manager.test_window("one") manager.test_window("two") manager.test_window("three") assert manager.c.layout.info()["clients"] == ["three", "two", "one"] assert manager.c.get_groups()["a"]["focus"] == "three" manager.c.layout.next() assert manager.c.get_groups()["a"]["focus"] == "two" manager.c.layout.previous() assert manager.c.get_groups()["a"]["focus"] == "three" manager.c.layout.previous() assert manager.c.get_groups()["a"]["focus"] == "one" manager.c.layout.next() manager.c.layout.next() manager.c.layout.next() assert manager.c.get_groups()["a"]["focus"] == "one" @tile_config def test_tile_master_and_slave(manager): manager.test_window("one") manager.test_window("two") manager.test_window("three") assert manager.c.layout.info()["master"] == ["three"] assert manager.c.layout.info()["slave"] == ["two", "one"] manager.c.next_layout() assert manager.c.layout.info()["master"] == ["three", "two"] assert manager.c.layout.info()["slave"] == ["one"] @tile_config def test_tile_remove(manager): one = manager.test_window("one") manager.test_window("two") three = manager.test_window("three") assert manager.c.layout.info()["master"] == ["three"] manager.kill_window(one) assert manager.c.layout.info()["master"] == ["three"] manager.kill_window(three) assert manager.c.layout.info()["master"] == ["two"] @tile_config def test_tile_window_focus_cycle(manager): # setup 3 tiled and two floating clients manager.test_window("one") manager.test_window("two") manager.test_window("float1") manager.c.window.toggle_floating() manager.test_window("float2") manager.c.window.toggle_floating() manager.test_window("three") # test preconditions, Tile adds (by default) clients at pos of current assert manager.c.layout.info()["clients"] == ["three", "two", "one"] # last added window has focus assert_focused(manager, "three") # assert window focus cycle, according to order in layout assert_focus_path(manager, "two", "one", "float1", "float2", "three") @tile_config def test_tile_add_on_top(manager): manager.c.next_layout() manager.c.next_layout() manager.test_window("one") manager.test_window("two") manager.test_window("three") # test first example assert manager.c.layout.info()["master"] == ["one"] assert manager.c.layout.info()["slave"] == ["two", "three"] manager.c.layout.previous() # test second exemple assert_focused(manager, "two") manager.test_window("four") assert manager.c.layout.info()["clients"] == ["one", "two", "four", "three"] assert manager.c.layout.info()["slave"] == ["two", "four", "three"] assert_focus_path(manager, "three", "one", "two", "four") @tile_config def test_tile_min_max_ratios(manager): manager.test_window("one") manager.test_window("two") orig_windows = manager.c.windows() # Defaul increment is 5% so 20 steps would move by 100% # i.e. past the edge for _ in range(20): manager.c.layout.decrease_ratio() # Window should now be 15% of screen width less it's border assert manager.c.windows()[1]["width"] == (800 * 0.15) - 2 for _ in range(20): manager.c.layout.increase_ratio() # Window should now be 85% of screen width less it's border assert manager.c.windows()[1]["width"] == (800 * 0.85) - 2 # Reset the layout to original settings manager.c.layout.reset() # Check the windows match their original sizes assert manager.c.windows() == orig_windows qtile-0.31.0/test/layouts/test_plasma.py0000664000175000017500000012047514762660347020214 0ustar epsilonepsilon# Copyright (c) 2017 numirias # Copyright (c) 2024 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import time from collections import defaultdict import pytest from pytest import approx from libqtile.config import Screen from libqtile.confreader import Config from libqtile.layout.plasma import AddMode, Node, NotRestorableError, Orient, Plasma, Priority from test.layouts.layout_utils import assert_focused Node.min_size_default = 10 class Canvas: horizontal_line = "\u2500" vertical_line = "\u2502" tl_corner = "\u250c" tr_corner = "\u2510" bl_corner = "\u2514" br_corner = "\u2518" def __init__(self, width, height): self.width = width self.height = height self.canvas = defaultdict(lambda: defaultdict(lambda: "#")) def add_box(self, x, y, width, height, name="*"): width = width - 1 height = height - 1 label = str(name)[: width - 1] for i in range(x, width + x): self.canvas[i][y] = self.horizontal_line self.canvas[i][y + height] = self.horizontal_line for i in range(y, height + y): self.canvas[x][i] = self.vertical_line self.canvas[x + width][i] = self.vertical_line for i in range(x + 1, width + x): for j in range(y + 1, y + height): self.canvas[i][j] = "." for i, char in enumerate(label): self.canvas[x + 1 + i][y + 1] = char self.canvas[x][y] = self.tl_corner self.canvas[x + width][y] = self.tr_corner self.canvas[x][y + height] = self.bl_corner self.canvas[x + width][y + height] = self.br_corner def view(self): res = "" for y in range(self.height): for x in range(self.width): res += self.canvas[x][y] res += "\n" return res def tree(node, level=0): res = "{indent}{name} {orient} {repr_} {pos} {size} {parent}\n".format( indent=level * 4 * " ", name="%s" % (node.payload or "*"), orient="H" if node.horizontal else "V", repr_=f"{repr(node)}", pos=f"{node.width:g}*{node.height:g}@{node.x:g}:{node.y:g}", size="size: {}{}".format(node.size, " (auto)" if node.flexible else ""), parent=f"p: {node.parent}", ) for child in node: res += tree(child, level + 1) return res def draw(root): canvas = Canvas(root.width, root.height) def add(node): if node.is_leaf: canvas.add_box(*node.pixel_perfect, node.payload) for child in node: add(child) add(root) return canvas.view() def info(node): print(tree(node)) print(draw(node)) def create_nodes(string): for x in string.split(): yield Node(x) @pytest.fixture def root(): root = Node("root", 0, 0, 120, 50) return root @pytest.fixture def tiny_grid(root): a, b, c = create_nodes("a b c") root.add_child(a) root.add_child(b) b.flip_with(c) return a, b, c @pytest.fixture def small_grid(root): a, b, c, d = create_nodes("a b c d") root.add_child(a) root.add_child(b) b.flip_with(c) c.flip_with(d) return a, b, c, d @pytest.fixture def grid(root): a, b, c, d, e = create_nodes("a b c d e") root.add_child(a) root.add_child(b) b.flip_with(c) c.flip_with(d) c.parent.add_child(e) return a, b, c, d, e @pytest.fixture def complex_grid(root): a, b, c, d, e, f, g = create_nodes("a b c d e f g") root.add_child(a) root.add_child(b) b.flip_with(c) c.flip_with(d) c.parent.add_child(e) c.flip_with(f) f.flip_with(g) return a, b, c, d, e, f, g @pytest.fixture(autouse=True) def reset_node_priority(): yield Node.priority = Priority.FIXED ############## # TEST NODES # ############## def test_single_node(): n = Node(None, 0, 0, 120, 50) assert n.x == 0 assert n.y == 0 assert n.width == 120 assert n.height == 50 assert n.is_root is True assert n.is_leaf is True assert n.parent is None assert n.children == [] assert n.orient == Orient.HORIZONTAL assert n.horizontal is True assert n.vertical is False assert n.size is None assert (n.x, n.y) == n.pos def test_add_child(root): """Single child fills the entire root node's space.""" child = Node("a") root.add_child(child) assert root.children == [child] assert child.parent == root assert root.width == child.width == 120 assert root.height == child.height == 50 assert root.x == child.x == 0 assert root.y == child.y == 0 def test_add_children(root): """Multiple children will split the root node's space.""" a, b = create_nodes("a b") root.add_child(a) root.add_child(b) assert root.width == 120 assert a.width == b.width == 60 assert root.height == 50 assert a.height == b.height == 50 assert a.pos == (0, 0) assert b.pos == (60, 0) c = Node("c") root.add_child(c) assert a.width == b.width == c.width == 40 assert a.pos == (0, 0) assert b.pos == (40, 0) assert c.pos == (80, 0) def test_add_child_after(root, grid): """Test adding nodes in specific positions.""" a, b, c, d, e = grid f = Node("f") g = Node("g") h = Node("h") # add_child is called on the target node's parent # i.e. the node containing the target root.add_child_after(f, a) assert root.tree == [a, f, [b, [c, d, e]]] b.parent.add_child_after(g, b) assert root.tree == [a, f, [b, g, [c, d, e]]] d.parent.add_child_after(h, d) assert root.tree == [a, f, [b, g, [c, d, h, e]]] def test_add_child_after_with_sizes(root): a, b, c = create_nodes("a b c") root.add_child(a) root.add_child(b) a.size += 10 b.size += 10 b.parent.add_child_after(c, b) assert a.size == b.size == 55 assert c.size == 10 def test_remove_child(root): a, b = create_nodes("a b") root.add_child(a) root.add_child(b) root.remove_child(a) assert root.children == [b] root.remove_child(b) assert root.children == [] def test_nested(root): a, b, c, d = create_nodes("a b c d") root.add_child(a) root.add_child(b) b.flip_with(c) assert a.width == 60 assert b.width == 60 assert c.width == 60 assert a.height == 50 assert b.height == 25 assert c.height == 25 b.flip_with(d) assert b.width == 30 assert d.width == 30 def test_leaves(root, grid): a, b, c, d, e = grid assert root.first_leaf is a assert root.last_leaf is e assert b.parent.first_leaf is b assert b.parent.last_leaf is e def test_directions(root, grid): a, b, c, d, e = grid assert a.up is None assert a.right is b assert a.down is None assert a.left is None assert b.up is None assert b.right is None assert b.down is c assert b.left is a assert c.up is b assert c.right is d assert c.down is None assert c.left is a assert d.up is b assert d.right is e assert d.down is None assert d.left is c assert e.up is b assert e.right is None assert e.down is None assert e.left is d def test_prev_next(grid): a, b, c, d, e = grid assert a.next_leaf == b assert b.next_leaf == c assert c.next_leaf == d assert d.next_leaf == e assert e.next_leaf == a assert a.prev_leaf == e assert e.prev_leaf == d assert d.prev_leaf == c assert c.prev_leaf == b assert b.prev_leaf == a def test_siblings(grid): a, b, c, d, e = grid assert d.siblings == [c, e] assert b.siblings == [c.parent] def test_root_siblings(root): siblings = root.siblings assert siblings == [] def test_move_forward(root, grid): a, b, c, d, e = grid assert c.parent.children == [c, d, e] c.move_right() assert c.parent.children == [d, c, e] c.move_right() assert c.parent.children == [d, e, c] c.move_right() assert root.tree == [a, [b, [d, e]], c] def test_move_backward(root, grid): a, b, c, d, e = grid e.move_left() assert c.parent.children == [c, e, d] e.move_left() assert c.parent.children == [e, c, d] e.move_left() assert root.tree == [a, e, [b, [c, d]]] def test_advanced_move(grid): a, b, c, d, e = grid c.move_up() assert b.parent.tree == [b, c, [d, e]] a.move_up() assert b.parent.tree == [b, c, [d, e]] def test_advanced_move2(root, grid): a, b, c, d, e = grid res = c.move_down() assert b.parent.tree == [b, [d, e], c] assert res is True res = e.move_down() assert b.parent.tree == [b, d, e, c] assert res is True res = e.move_left() assert root.tree == [a, e, [b, d, c]] assert res is True res = d.move_right() assert root.tree == [a, e, [b, c], d] assert res is True res = a.move_left() assert root.tree == [a, e, [b, c], d] assert res is False res = d.move_right() assert root.tree == [a, e, [b, c], d] assert res is False def test_move_blocked(root, grid): a, b, c, d, e = grid orig_tree = root.tree.copy() res = a.move_up() assert root.tree == orig_tree assert res is False res = b.move_up() assert root.tree == orig_tree assert res is False def test_move_root(root): a = Node("a") root.add_child(a) root.move_up() assert root.tree == [a] def test_integrate(root): a, b, c, d, e = create_nodes("a b c d e") root.add_child(a) root.add_child(b) root.add_child(c) root.add_child(d) c.integrate_left() assert root.tree == [a, [b, c], d] a.integrate_right() assert root.tree == [[b, c, a], d] a.parent.add_child(e) c.integrate_down() assert root.tree == [[b, [a, c], e], d] e.integrate_up() assert root.tree == [[b, [a, c, e]], d] def test_integrate_nested(root, grid): a, b, c, d, e = grid c.integrate_right() assert root.tree == [a, [b, [[d, c], e]]] def test_move_and_integrate(root, grid): a, b, c, d, e = grid c.integrate_left() assert root.tree == [[a, c], [b, [d, e]]] a.integrate_right() assert root.tree == [c, [b, [d, e], a]] d.integrate_down() assert root.tree == [c, [b, e, [a, d]]] a.integrate_up() assert root.tree == [c, [b, [e, a], d]] e.integrate_left() assert root.tree == [[c, e], [b, a, d]] f = Node("f") a.flip_with(f) g = Node("g") a.flip_with(g) g.integrate_left() assert root.tree == [[c, e, g], [b, [a, f], d]] def test_impossible_integrate(root, grid): a, b, c, d, e = grid orig_tree = root.tree.copy() a.integrate_left() assert orig_tree == root.tree b.integrate_up() assert orig_tree == root.tree def test_impossible_integrate2(root): a, b = create_nodes("a b") root.add_child(a) root.add_child(b) orig_tree = root.tree.copy() b.integrate_up() assert root.tree == orig_tree b.integrate_down() assert root.tree == orig_tree b.integrate_right() assert root.tree == orig_tree a.integrate_up() assert root.tree == orig_tree a.integrate_down() assert root.tree == orig_tree a.integrate_left() assert root.tree == orig_tree def test_find_payload(root, grid): a, b, c, d, e = grid assert root.find_payload(a.payload) is a assert root.find_payload(b.payload) is b assert root.find_payload(d.payload) is d assert root.find_payload("x") is None def test_last_access(grid): a, b, c, d, e = grid f = Node("f") a.flip_with(f) d.access() assert b.down is d b.access() assert f.right is b f.access() assert b.left is f def test_root_without_dimensions(): """A root node with undef. dimensions should be able to add a child.""" root = Node() x = Node("x") root.add_child(x) def test_root(root, grid): for node in grid: assert node.root is root def test_all(root, grid): assert set(root.all_leafs) == set(grid) def test_close_neighbor(root): a, b, c, d = create_nodes("a b c d") root.add_child(a) root.add_child(b) a.flip_with(c) b.flip_with(d) assert a.close_up is None assert a.close_left is None assert a.close_right is b assert a.close_down is c assert b.close_up is None assert b.close_left is a assert b.close_right is None assert b.close_down is d assert c.close_up is a assert c.close_left is None assert c.close_right is d assert c.close_down is None assert d.close_up is b assert d.close_left is c assert d.close_right is None assert d.close_down is None def test_close_neighbor2(root, small_grid): a, b, c, d = small_grid assert b.close_left is a def test_close_neighbor_nested(root, grid): a, b, c, d, e = grid f, g, h, i, j, k, m = create_nodes("f g h i j k m") root.add_child(f) d.flip_with(h) a.flip_with(i) e.flip_with(j) e.parent.add_child(k) f.flip_with(m) f.height = 10 assert b.close_down is d b.flip_with(g) assert b.close_down is c assert d.close_right is e assert e.close_left is d assert m.close_left is e assert e.close_up is g assert m.close_right is None assert h.close_down is None def test_close_neighbor_approx(root, small_grid): """Tolerate floating point errors when calculating common borders.""" root.height += 30 a, b, c, d = small_grid e, f, g = create_nodes("e f g") c.flip_with(f) b.parent.add_child(e) c.parent.add_child(g) assert g.close_down is e def test_points(root, small_grid): a, b, c, d = small_grid assert c.x, c.y == (60, 25) assert c.x + c.width, c.y == (90, 25) assert c.x, c.y + c.height == (60, 50) assert c.x + c.width, c.y + c.height == (90, 50) def test_center(root): assert root.x_center == 60 assert root.y_center == 25 assert root.center == (60, 25) def test_recent_leaf(root, grid): a, b, c, d, e = grid assert d.parent.recent_leaf is c c.access() d.access() assert d.parent.recent_leaf is d b.access() c.access() assert root.recent_leaf is c a.access() assert root.recent_leaf is a def test_recent_close_neighbor(root, grid): a, b, c, d, e = grid assert b.close_down is d c.access() assert b.close_down is c assert a.close_right is c b.access() assert a.close_right is b def test_add_node(root): a, b, c, d, e, f, g = create_nodes("a b c d e f g") root.add_node(a) assert root.tree == [a] root.add_node(b) assert root.tree == [a, b] a.add_node(c) assert root.tree == [a, c, b] c.add_node(d, mode=AddMode.HORIZONTAL) assert root.tree == [a, c, d, b] root.remove_child(d) c.add_node(d, mode=AddMode.VERTICAL) c.parent.add_child_after assert root.tree == [a, [c, d], b] c.add_node(e, mode=AddMode.VERTICAL) assert root.tree == [a, [c, e, d], b] assert a.width == 40 a.add_node(f, mode=AddMode.HORIZONTAL | AddMode.SPLIT) assert root.tree == [a, f, [c, e, d], b] assert a.width == f.width == 20 assert c.parent.width == b.width == 40 a.add_node(g, mode=AddMode.VERTICAL | AddMode.SPLIT) assert root.tree == [[a, g], f, [c, e, d], b] def test_contains(root, grid): x = Node("x") nodes = list(grid) nodes += [n.parent for n in nodes] nodes.append(root) for n in nodes: assert n in root assert x not in root def test_size(grid): a, b, c, d, e = grid assert a.size == a.width == 60 assert b.size == b.height == 25 def test_capacity(root, grid): a, b, c, d, e = grid assert root.capacity == 120 assert b.parent.capacity == 50 assert c.parent.capacity == 60 assert c.capacity == 25 def test_capacity2(root): a, b, c = create_nodes("a b c") root.add_child(a) root.add_child(b) b.flip_with(c) def test_resize(root, grid): a, b, c, d, e = grid a.size += 10 assert a.width == a.size == 70 assert b.height == b.size == 25 assert b.width == 50 assert c.width == d.width == e.width == 50 / 3 assert a.pos == (0, 0) assert b.pos == (70, 0) assert c.pos == (70, 25) assert d.pos == (70 + 50 / 3, 25) assert e.pos == (70 + (50 / 3) * 2, 25) b.size -= 5 assert c.width == d.width == e.width == 50 / 3 assert c.height == d.height == e.height == 30 d.size += 5 assert d.width == 50 / 3 + 5 d.move_up() assert d.size == (50 - b.size) / 2 b.integrate_down() assert b.size == d.size == 25 assert b.parent.size == 25 def test_resize_absolute(grid): a, b, c, d, e = grid b.size = 10 assert b.size == b.height == 10 assert c.parent.size == 40 b.size = 5 assert b.size == 10 def test_resize_absolute2(root): a, b, c = create_nodes("a b c") root.add_child(a) root.add_child(b) root.add_child(c) a.size = 30 b.size = 60 c.size = 40 assert a.size == 30 * (80 / 90) assert b.size == 60 * (80 / 90) assert c.size == 40 def test_resize_absolute_and_relative(root): a, b, c, d = create_nodes("a b c d") root.add_child(a) root.add_child(b) a.size = 20 b.size = 20 assert a.size == 100 assert b.size == 20 root.add_child(c) assert c.size == approx(10) assert a.size == approx(100 * (11 / 12)) assert b.size == approx(20 * (11 / 12)) root.add_child(d) assert c.size == d.size == approx(10) def test_resize_absolute_and_relative_balanced(root): Node.priority = Priority.BALANCED a, b, c, d = create_nodes("a b c d") root.add_child(a) root.add_child(b) a.size = 20 b.size = 20 assert a.size == 100 assert b.size == 20 root.add_child(c) assert c.size == approx(40) assert a.size == approx(100 * (2 / 3)) assert b.size == approx(20 * (2 / 3)) root.add_child(d) assert c.size == d.size == approx(20) def test_resize_absolute_and_relative2(root): a, b, c = create_nodes("a b c") root.add_child(a) root.add_child(b) root.add_child(c) a.size += 10 assert a.size == 50 assert b.size == 35 assert c.size == 35 b.size += 10 assert a.size == 50 assert b.size == 45 assert c.size == 25 def test_resize_flat(root): a, b, c, d, e, f = create_nodes("a b_abs c d e_abs f_abs") root.add_child(a) root.add_child(b) root.add_child(c) root.add_child(d) d.flip_with(e) e.flip_with(f) b.size = b.size e.size = e.size f.size = f.size a.size = 60 assert a.size == 60 assert b.size == 25 assert c.size == 10 assert d.parent.size == 25 assert e.size == f.size == 25 / 2 def test_resize_minimum(grid): a, b, c, d, e = grid b.size -= 100 assert b.size == 10 def test_resize_all_absolute_underflow(root, grid): a, b, c, d, e = grid c.size = 10 d.size = 10 assert e.size == 40 e.size = 10 assert e.size == 10 assert c.size == d.size == 25 def test_resize_all_absolute_overflow(grid): a, b, c, d, e = grid c.size = d.size = 15 e.size = 40 assert e.size == 40 assert c.size == d.size == 10 e.size = 50 assert e.size == 40 assert c.size == d.size == 10 def test_resize_overflow_with_relative(root, grid): a, b, c, d, e = grid c.size = 20 d.size = 40 assert c.size == 10 assert d.size == 40 assert e.size == 10 assert e.flexible d.size = 50 assert c.size == 10 assert d.size == 40 assert e.size == 10 assert e.flexible def test_resize_overflow_with_relative2(root, grid): a, b, c, d, e = grid c.size = 20 d.size = 20 a.size = 70 assert a.size == 70 assert c.size == d.size == 20 assert e.size == 10 a.size = 80 assert a.size == 80 assert c.size == d.size == 15 assert e.size == 10 a.size = 90 assert a.size == 90 assert c.size == d.size == e.size == 10 a.size = 100 assert a.size == 90 def test_resize_only_absolute_remains(root): a, b, c = create_nodes("a b c") root.add_child(a) root.add_child(b) a.size = 20 b.size = 20 root.add_child(c) root.remove_child(c) assert a.size == 100 assert b.size == 20 def test_reset_size(grid): a, b, c, d, e = grid a.size += 5 assert a.size == 65 a.reset_size() assert a.size == 60 def test_size_after_split(root): a, b, c = create_nodes("a b c") root.add_child(a) root.add_child(b) b.size -= 20 b.flip_with(c) assert b.parent.size == 40 assert b.size == c.size == 25 b.remove() assert c.size == 40 def test_only_child_must_be_flexible(root): a, b = create_nodes("a b") root.add_child(a) root.add_child(b) a.size = 10 root.remove_child(b) assert a.flexible def test_deny_only_child_resize(root): a = Node("a") root.add_child(a) a.size = 10 assert a.size == 120 def test_resize_parents(root): a, b, c = create_nodes("a b c") root.add_child(a) root.add_child(b) b.flip_with(c) b.width += 10 assert b.parent.size == 70 assert b.size == c.size == 25 def test_pixelperfect(root, tiny_grid): a, b, c = tiny_grid root._height = 11 root._width = 11 ds = a.pixel_perfect assert all(type(x) is int for x in (ds.x, ds.y, ds.width, ds.height)) assert a.width + b.width == 11 assert a.pixel_perfect.width + b.pixel_perfect.width == 11 assert b.height + c.height == 11 assert b.pixel_perfect.height + c.pixel_perfect.height == 11 def test_pixelperfect_draw(root, complex_grid): root._height = 10 for i in range(40, 50): root._width = i view = draw(root) assert "#" not in view root._width = 50 for i in range(10, 20): root._height = i view = draw(root) assert "#" not in view def test_resize_root(root): a, b, c = create_nodes("a b c") root.add_child(a) root.add_child(b) a.height += 10 root.height += 10 root.width += 10 root.size = 10 assert a._size is b._size is root._size is None def test_set_xy(root, tiny_grid): a, b, c = tiny_grid root.x = 10 root.y = 20 assert root.x == 10 assert root.y == 20 a.x = 30 a.y = 40 assert a.x == root.x == 10 assert a.y == root.y == 20 root.width = 50 root.height = 60 assert root._width == 50 assert root._height == 60 def test_set_width_height(root, tiny_grid): a, b, c = tiny_grid a.width = 70 assert a.width == 70 assert b.width == c.width == 50 b.height = 30 assert b.height == 30 assert c.height == 20 b.width = 80 assert b.width == c.width == 80 assert a.width == 40 a.height = 20 assert a.height == 50 def test_min_size(root, small_grid): a, b, c, d = small_grid c.size += 10 d.size += 20 b.size = 20 assert a.min_size == Node.min_size_default assert b.parent.min_size == 60 assert b.min_size == 20 assert c.parent.min_size == Node.min_size_default assert c.min_size == 20 assert d.min_size == 40 def test_transitive_flexible(root, complex_grid): a, b, c, d, e, f, g = complex_grid assert b.parent.flexible d.size = 20 e.size = 20 f.size = 10 assert b.parent.flexible g.size = 10 assert not b.parent.flexible def test_resize_bubbles(root, small_grid): a, b, c, d = small_grid c.size += 10 d.size += 20 assert c.size == 20 assert d.size == 40 a.size = 30 assert c.size == 30 assert d.size == 60 def test_resize_bubbles2(root, complex_grid): a, b, c, d, e, f, g = complex_grid c.flip_with(Node("h")) f.size += 10 g.size += 10 assert f.size == g.size == 10 assert f.fixed and g.fixed assert d.size == e.size == 20 assert d.flexible and e.flexible a.size -= 40 assert a.size == 20 assert f.size == g.size == 10 assert d.size == e.size == 40 d.size = 10 assert d.size == 10 assert e.size == 70 assert f.size == g.size == 10 assert e.flexible e.size = 10 assert e.fixed def test_resize_bubbles3(root, complex_grid): a, b, c, d, e, f, g = complex_grid h = Node("h") c.flip_with(h) f.size += 10 g.size += 10 assert f.size == g.size == c.size == h.size == 10 a.size = 10 assert a.size == 10 assert f.size == g.size == c.size == h.size == 10 assert d.size == e.size == 45 d.size = 10 assert d.size == 10 assert e.size == 80 e.size = 10 assert e.size == 10 assert f.size == g.size == c.size == h.size == d.size == 100 / 3 def test_resize_nested(root): a, b, c, d, e, f, g, h = create_nodes("a b c_abs d_abs e f g h_abs") nu1, nu2, nd, mu, md1, md2 = create_nodes("nu1_abs nu2_abs nd mu md1_abs md2") ou1, ou2, od, pu, pd1, pd2 = create_nodes("ou1_abs ou2_abs od pu pd1_abs " "pd2_abs") root.add_child(a) root.add_child(b) b.flip_with(c) b.parent.add_child(e) b.parent.add_child(g) c.flip_with(d) e.flip_with(f) g.flip_with(h) b.parent.add_child(nu1) nu1.flip_with(mu) nu1.flip_with(nd) nu1.flip_with(nu2) mu.flip_with(md1) md1.flip_with(md2) b.parent.add_child(ou1) ou1.flip_with(pu) ou1.flip_with(od) ou1.flip_with(ou2) pu.flip_with(pd1) pd1.flip_with(pd2) def assert_first_state(): assert b.parent.size == 60 assert c.size == approx(40) assert d.size == 20 assert e.size == f.size == 30 assert g.size == 40 assert h.size == 20 assert nu1.size == 10 assert nu2.size == 20 assert nd.parent.size == 30 assert mu.parent.size == 30 assert md1.size == 10 assert md2.size == 20 assert ou1.size == 10 assert ou2.size == 20 assert od.parent.size == 30 assert pu.parent.size == 30 assert pd1.size == 10 assert pd2.size == 20 def assert_second_state(): assert a.size == 30 assert b.parent.size == 90 assert c.size == 60 assert d.size == 30 assert e.size == f.size == 45 assert g.size == 70 assert h.size == 20 assert nu1.size == 10 assert nu2.size == 20 assert nd.parent.size == 30 assert mu.parent.size == 60 assert md1.size == 10 assert md2.size == 50 assert ou1.size == 15 assert ou2.size == 30 assert od.parent.size == 45 assert pd1.size == 15 assert pd2.size == 30 assert pu.parent.size == 45 b.parent.size = 60 c.size += 5 d.size -= 5 h.size = 20 nu1.size = 10 nu2.size = 20 md1.size = 10 ou1.size = 10 ou2.size = 20 pd1.size = 10 pd2.size = 20 assert a.size == 60 assert_first_state() a.size -= 30 assert_second_state() a.size += 30 assert a.size == 60 assert_first_state() b.parent.size += 30 assert_second_state() b.parent.size -= 30 assert a.size == 60 assert_first_state() def test_resize_nested_balanced(root): Node.priority = Priority.BALANCED a, b, c, d, e, f, g, h = create_nodes("a b c_abs d_abs e f g h_abs") nu1, nu2, nd, mu, md1, md2 = create_nodes("nu1_abs nu2_abs nd mu md1_abs md2") ou1, ou2, od, pu, pd1, pd2 = create_nodes("ou1_abs ou2_abs od pu pd1_abs " "pd2_abs") root.add_child(a) root.add_child(b) b.flip_with(c) b.parent.add_child(e) b.parent.add_child(g) c.flip_with(d) e.flip_with(f) g.flip_with(h) b.parent.add_child(nu1) nu1.flip_with(mu) nu1.flip_with(nd) nu1.flip_with(nu2) mu.flip_with(md1) md1.flip_with(md2) b.parent.add_child(ou1) ou1.flip_with(pu) ou1.flip_with(od) ou1.flip_with(ou2) pu.flip_with(pd1) pd1.flip_with(pd2) def assert_first_state(): assert b.parent.size == 60 assert c.size == 40 assert d.size == 20 assert e.size == f.size == 30 assert g.size == 40 assert h.size == 20 assert nu1.size == 10 assert nu2.size == 20 assert nd.parent.size == 30 assert mu.parent.size == 30 assert md1.size == 10 assert md2.size == 20 assert ou1.size == 10 assert ou2.size == 20 assert od.parent.size == 30 assert pu.parent.size == 30 assert pd1.size == 10 assert pd2.size == 20 def assert_second_state(): assert a.size == 30 assert b.parent.size == 90 assert c.size == 60 assert d.size == 30 assert e.size == f.size == 45 assert g.size == 70 assert h.size == 20 assert nu1.size == 10 assert nu2.size == 20 assert nd.parent.size == 30 assert mu.parent.size == 60 assert md1.size == 10 assert md2.size == 50 assert ou1.size == 15 assert ou2.size == 30 assert od.parent.size == 45 assert pd1.size == 15 assert pd2.size == 30 assert pu.parent.size == 45 b.parent.size = 60 c.size += 5 d.size -= 5 h.size = 20 nu1.size = 10 nu2.size = 20 md1.size = 10 ou1.size = 10 ou2.size = 20 pd1.size = 10 pd2.size = 20 assert a.size == 60 assert_first_state() a.size -= 30 assert_second_state() a.size += 30 assert a.size == 60 assert_first_state() b.parent.size += 30 assert_second_state() b.parent.size -= 30 assert a.size == 60 assert_first_state() a.size = 30 x = Node("x") root.add_child(x) assert x.size == 40 assert_first_state() x.remove() assert_second_state() a.remove() assert b.width == 120 y = Node("y") root.add_child(y) assert_first_state() def test_resize_max(root, tiny_grid): a, b, c = tiny_grid a.width = 120 assert a.width == 110 assert b.width == c.width == 10 def test_restore(root, grid): a, b, c, d, e = grid tree = root.tree for node in grid: node.remove() root.restore(node) assert root.tree == tree def test_restore_same_payload(root, grid): """Restore a node that's not identical with the removed one but carries the same payload. """ a, b, c, d, e = grid d.remove() new_d = Node("d") root.restore(new_d) assert root.tree == [a, [b, [c, new_d, e]]] def test_restore_unknown(root, grid): a, b, c, d, e = grid with pytest.raises(NotRestorableError): root.restore(Node("x")) d.remove() with pytest.raises(NotRestorableError): root.restore(Node("x")) root.restore(d) assert root.tree == [a, [b, [c, d, e]]] def test_restore_no_parent(root, small_grid): a, b, c, d = small_grid c.remove() d.remove() with pytest.raises(NotRestorableError): root.restore(c) root.restore(d) assert root.tree == [a, [b, d]] def test_restore_bad_index(root, grid): a, b, c, d, e = grid f, g = create_nodes("f g") e.parent.add_child(f) e.parent.add_child(g) g.remove() f.remove() e.remove() root.restore(g) assert root.tree == [a, [b, [c, d, g]]] def test_restore_sizes(root, grid): a, b, c, d, e = grid c.size = 30 c.remove() root.restore(c) assert c.size == 30 c.remove() d.size = 30 e.size = 30 assert d.size == e.size == 30 root.restore(c) assert c.size == 30 assert d.size == e.size == 15 def test_restore_sizes_flip(root, tiny_grid): a, b, c = tiny_grid c.size = 10 c.remove() assert a._size is b._size is None root.restore(c) assert c.size == 10 b.size = 10 c.remove() root.restore(c) assert b.size == 10 b.remove() root.restore(b) assert b.size == 10 def test_restore_root(root): a, b = create_nodes("a b") root.add_child(a) root.add_child(b) a.size = 20 a.remove() root.restore(a) assert a._size == 20 assert b._size is None b.remove() root.restore(b) assert a._size == 20 assert b._size is None def test_restore_root2(root): a, b, c = create_nodes("a b c") root.add_child(a) root.add_child(b) root.add_child(c) b.size = 20 c.size = 40 a.remove() assert b.size == 40 assert c.size == 80 root.restore(a) assert not a.fixed assert a.size == approx(60) assert b.size == approx(20) assert c.size == approx(40) def test_restore_keep_flexible(root, tiny_grid): a, b, c = tiny_grid b.remove() root.restore(b) assert a._size is b._size is c._size is None b.size = 10 b.remove() root.restore(b) assert b._size == 10 assert c._size is None c.remove() root.restore(c) assert b._size == 10 assert c._size is None c.size = 10 b.reset_size() b.remove() root.restore(b) assert b._size is None assert c._size == 10 c.remove() root.restore(c) assert b._size is None assert c._size == 10 def test_resize_with_collapse_and_restore(root, small_grid): a, b, c, d = small_grid root.height = 30 c.size = 30 d.size += 10 b.remove() assert c.size == c.height == 10 assert d.size == d.height == 20 root.restore(b) assert b.height == 15 assert b.width == 60 assert c.height == d.height == 15 assert c.width == 20 assert d.width == 40 def test_node_repr(root, grid): a, b, c, d, e = grid assert repr(root).startswith(" now def test_info(plasma): plasma.test_window("a") plasma.test_window("b") assert client_tree(plasma) == ["a", "b"] def test_windows(plasma): plasma.test_window("a") plasma.test_window("b") plasma.test_window("c") assert_focused(plasma, "c") assert client_tree(plasma) == ["a", ["b", "c"]] def test_split_directions(plasma): plasma.test_window("a") plasma.c.layout.mode_horizontal() plasma.test_window("b") plasma.c.layout.mode_vertical() plasma.test_window("c") assert client_tree(plasma) == ["a", ["b", "c"]] @with_grid def test_layout_directions(plasma): assert_focused(plasma, "d") plasma.c.layout.left() assert_focused(plasma, "c") plasma.c.layout.up() assert_focused(plasma, "a") plasma.c.layout.right() assert_focused(plasma, "b") plasma.c.layout.down() assert_focused(plasma, "d") plasma.c.layout.down() assert_focused(plasma, "d") plasma.c.layout.previous() assert_focused(plasma, "b") plasma.c.layout.next() assert_focused(plasma, "d") @with_grid def test_move(plasma): assert client_tree(plasma) == [["a", "c"], ["b", "d"]] plasma.c.layout.move_up() assert client_tree(plasma) == [["a", "c"], ["d", "b"]] plasma.c.layout.move_down() assert client_tree(plasma) == [["a", "c"], ["b", "d"]] plasma.c.layout.move_left() assert client_tree(plasma) == [["a", "c"], "d", "b"] plasma.c.layout.move_right() assert client_tree(plasma) == [["a", "c"], "b", "d"] @with_grid def test_client_integrate(plasma): plasma.c.layout.integrate_left() assert client_tree(plasma) == [["a", "c", "d"], "b"] plasma.c.layout.integrate_up() assert client_tree(plasma) == [["a", ["c", "d"]], "b"] plasma.c.layout.integrate_up() plasma.c.layout.integrate_down() assert client_tree(plasma) == [["a", ["c", "d"]], "b"] plasma.c.layout.integrate_right() assert client_tree(plasma) == [["a", "c"], ["b", "d"]] def test_sizes(plasma): Node.priority = Priority.BALANCED plasma.test_window("a") plasma.test_window("b") plasma.c.layout.mode_vertical() plasma.test_window("c") info = plasma.c.window.info() assert info["x"] == 400 assert info["y"] == 300 assert info["width"] == 400 - 2 assert info["height"] == 300 - 2 plasma.c.layout.grow_height(50) info = plasma.c.window.info() assert info["height"] == 300 - 2 + 50 plasma.c.layout.grow_width(50) info = plasma.c.window.info() assert info["width"] == 400 - 2 + 50 plasma.c.layout.reset_size() info = plasma.c.window.info() assert info["height"] == 300 - 2 plasma.c.layout.set_height(300) info = plasma.c.window.info() assert info["height"] == 300 - 2 plasma.c.layout.set_width(250) info = plasma.c.window.info() assert info["width"] == 250 - 2 plasma.c.layout.set_size(200) info = plasma.c.window.info() assert info["height"] == 200 - 2 plasma.c.layout.grow(10) info = plasma.c.window.info() assert info["height"] == 210 - 2 def test_remove(plasma): a = plasma.test_window("a") b = plasma.test_window("b") assert client_tree(plasma) == ["a", "b"] plasma.kill_window(a) assert client_tree(plasma) == ["b"] plasma.kill_window(b) assert client_tree(plasma) == [] def test_split_mode(plasma): plasma.test_window("a") plasma.test_window("b") plasma.c.layout.mode_horizontal_split() plasma.test_window("c") assert plasma.c.window.info()["width"] == 200 - 2 plasma.c.layout.mode_vertical() plasma.test_window("d") assert plasma.c.window.info()["height"] == 300 - 2 plasma.test_window("e") assert plasma.c.window.info()["height"] == 200 - 2 plasma.c.layout.mode_vertical_split() plasma.test_window("f") assert plasma.c.window.info()["height"] == 100 - 2 def test_recent(plasma): plasma.test_window("a") plasma.test_window("b") plasma.test_window("c") assert_focused(plasma, "c") plasma.c.layout.recent() assert_focused(plasma, "b") plasma.c.layout.recent() assert_focused(plasma, "c") plasma.c.layout.next() assert_focused(plasma, "a") plasma.c.layout.recent() assert_focused(plasma, "c") def test_bug_10(): """Adding nodes when the correct root dimensions are still unknown should not raise an error. """ layout = Plasma() layout.add_client(object()) layout.add_client(object()) qtile-0.31.0/test/layouts/__init__.py0000664000175000017500000000000014762660347017414 0ustar epsilonepsilonqtile-0.31.0/test/layouts/test_columns.py0000664000175000017500000002335114762660347020412 0ustar epsilonepsilon# # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest import libqtile.config from libqtile import layout from libqtile.confreader import Config from test.helpers import HEIGHT, WIDTH from test.layouts.layout_utils import assert_dimensions, assert_focus_path, assert_focused class ColumnsConfig(Config): auto_fullscreen = True groups = [ libqtile.config.Group("a"), libqtile.config.Group("b"), libqtile.config.Group("c"), libqtile.config.Group("d"), ] layouts = [ layout.Columns(num_columns=3), layout.Columns(margin_on_single=10), layout.Columns(margin_on_single=[10, 20, 30, 40]), ] floating_layout = libqtile.resources.default_config.floating_layout keys = [] mouse = [] screens = [] follow_mouse_focus = False class ColumnsSingleBorderDisabledConfig(ColumnsConfig): layouts = [layout.Columns(border_on_single=False, single_border_width=2, border_width=4)] class ColumnsSingleBorderEnabledConfig(ColumnsConfig): layouts = [layout.Columns(border_on_single=True, single_border_width=2, border_width=4)] class ColumnsLeftAlign(ColumnsConfig): layouts = [layout.Columns(align=layout.Columns._left, border_width=0)] class ColumnsInitialRatio(ColumnsConfig): layouts = [ layout.Columns(initial_ratio=3, border_width=0), layout.Columns(initial_ratio=3, align=layout.Columns._left, border_width=0), ] columns_config = pytest.mark.parametrize("manager", [ColumnsConfig], indirect=True) columns_single_border_disabled_config = pytest.mark.parametrize( "manager", [ColumnsSingleBorderDisabledConfig], indirect=True ) columns_single_border_enabled_config = pytest.mark.parametrize( "manager", [ColumnsSingleBorderEnabledConfig], indirect=True ) columns_left_align = pytest.mark.parametrize("manager", [ColumnsLeftAlign], indirect=True) columns_initial_ratio = pytest.mark.parametrize("manager", [ColumnsInitialRatio], indirect=True) # This currently only tests the window focus cycle @columns_config def test_columns_window_focus_cycle(manager): # setup 3 tiled and two floating clients manager.test_window("one") manager.test_window("two") manager.test_window("three") manager.test_window("float1") manager.c.window.toggle_floating() manager.test_window("float2") manager.c.window.toggle_floating() manager.test_window("four") # test preconditions, columns adds clients at pos after current, in two stacks columns = manager.c.layout.info()["columns"] assert columns[0]["clients"] == ["one"] assert columns[1]["clients"] == ["two"] assert columns[2]["clients"] == ["four", "three"] # last added window has focus assert_focused(manager, "four") # assert window focus cycle, according to order in layout assert_focus_path(manager, "three", "float1", "float2", "one", "two", "four") @columns_config def test_columns_swap_column_left(manager): manager.test_window("1") manager.test_window("2") manager.test_window("3") manager.test_window("4") # test preconditions columns = manager.c.layout.info()["columns"] assert columns[0]["clients"] == ["1"] assert columns[1]["clients"] == ["2"] assert columns[2]["clients"] == ["4", "3"] assert_focused(manager, "4") # assert columns are swapped left manager.c.layout.swap_column_left() columns = manager.c.layout.info()["columns"] assert columns[0]["clients"] == ["1"] assert columns[1]["clients"] == ["4", "3"] assert columns[2]["clients"] == ["2"] manager.c.layout.swap_column_left() columns = manager.c.layout.info()["columns"] assert columns[0]["clients"] == ["4", "3"] assert columns[1]["clients"] == ["1"] assert columns[2]["clients"] == ["2"] manager.c.layout.swap_column_left() columns = manager.c.layout.info()["columns"] assert columns[0]["clients"] == ["2"] assert columns[1]["clients"] == ["1"] assert columns[2]["clients"] == ["4", "3"] @columns_config def test_columns_swap_column_right(manager): manager.test_window("1") manager.test_window("2") manager.test_window("3") manager.test_window("4") # test preconditions assert manager.c.layout.info()["columns"][0]["clients"] == ["1"] assert manager.c.layout.info()["columns"][1]["clients"] == ["2"] assert manager.c.layout.info()["columns"][2]["clients"] == ["4", "3"] assert_focused(manager, "4") # assert columns are swapped right manager.c.layout.swap_column_right() columns = manager.c.layout.info()["columns"] assert columns[0]["clients"] == ["4", "3"] assert columns[1]["clients"] == ["2"] assert columns[2]["clients"] == ["1"] manager.c.layout.swap_column_right() columns = manager.c.layout.info()["columns"] assert columns[0]["clients"] == ["2"] assert columns[1]["clients"] == ["4", "3"] assert columns[2]["clients"] == ["1"] manager.c.layout.swap_column_right() columns = manager.c.layout.info()["columns"] assert columns[0]["clients"] == ["2"] assert columns[1]["clients"] == ["1"] assert columns[2]["clients"] == ["4", "3"] @columns_config def test_columns_margins_single(manager): manager.test_window("1") # no margin info = manager.c.window.info() assert info["x"] == 0 assert info["y"] == 0 assert info["width"] == WIDTH assert info["height"] == HEIGHT # single margin for all sides manager.c.next_layout() info = manager.c.window.info() assert info["x"] == 10 assert info["y"] == 10 assert info["width"] == WIDTH - 20 assert info["height"] == HEIGHT - 20 # one margin for each side (N E S W) manager.c.next_layout() info = manager.c.window.info() assert info["x"] == 40 assert info["y"] == 10 assert info["width"] == WIDTH - 60 assert info["height"] == HEIGHT - 40 @columns_single_border_disabled_config def test_columns_single_border_disabled(manager): manager.test_window("1") assert_dimensions(manager, 0, 0, WIDTH, HEIGHT) manager.test_window("2") assert_dimensions(manager, WIDTH / 2, 0, WIDTH / 2 - 8, HEIGHT - 8) @columns_single_border_enabled_config def test_columns_single_border_enabled(manager): manager.test_window("1") assert_dimensions(manager, 0, 0, WIDTH - 4, HEIGHT - 4) manager.test_window("2") assert_dimensions(manager, WIDTH / 2, 0, WIDTH / 2 - 8, HEIGHT - 8) @columns_left_align def test_columns_left_align(manager): # window 1: fullscreen manager.test_window("1") info = manager.c.window.info() assert info["x"] == 0 assert info["y"] == 0 assert info["width"] == WIDTH assert info["height"] == HEIGHT # window 2: left manager.test_window("2") info = manager.c.window.info() assert info["x"] == 0 assert info["y"] == 0 assert info["width"] == WIDTH / 2 assert info["height"] == HEIGHT # window 3: top left manager.test_window("3") info = manager.c.window.info() assert info["x"] == 0 assert info["y"] == 0 assert info["width"] == WIDTH / 2 assert info["height"] == HEIGHT / 2 @columns_initial_ratio def test_columns_initial_ratio_right(manager): manager.test_window("1") manager.test_window("2") # initial_ratio is 3 (i.e. main column is 3 times size of secondary column) # so secondary column is 1/4 of screen width info = manager.c.window.info() assert info["x"] == 3 * WIDTH / 4 assert info["y"] == 0 assert info["width"] == WIDTH / 4 assert info["height"] == HEIGHT # Growing right means secondary column is now smaller manager.c.layout.grow_right() info = manager.c.window.info() assert info["width"] < WIDTH / 4 # Reset to restore initial_ratio manager.c.layout.reset() info = manager.c.window.info() assert info["width"] == WIDTH / 4 # Normalize to make columns equal manager.c.layout.normalize() info = manager.c.window.info() assert info["width"] == WIDTH / 2 @columns_initial_ratio def test_columns_initial_ratio_left(manager): manager.c.next_layout() manager.test_window("1") manager.test_window("2") # initial_ratio is 3 (i.e. main column is 3 times size of secondary column) # so secondary column is 1/4 of screen width info = manager.c.window.info() assert info["x"] == 0 assert info["y"] == 0 assert info["width"] == WIDTH / 4 assert info["height"] == HEIGHT # Growing right means secondary column is now smaller manager.c.layout.grow_left() info = manager.c.window.info() assert info["width"] < WIDTH / 4 # Reset to restore initial_ratio manager.c.layout.reset() info = manager.c.window.info() assert info["width"] == WIDTH / 4 # Normalize to make columns equal manager.c.layout.normalize() info = manager.c.window.info() assert info["width"] == WIDTH / 2 qtile-0.31.0/test/layouts/test_ratiotile.py0000664000175000017500000003322514762660347020727 0ustar epsilonepsilon# Copyright (c) 2011 Florian Mounier # Copyright (c) 2012, 2014-2015 Tycho Andersen # Copyright (c) 2013 Mattias Svala # Copyright (c) 2013 Craig Barnes # Copyright (c) 2014 ramnes # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Adi Sieker # Copyright (c) 2014 Chris Wesseling # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from time import sleep import pytest import libqtile.config from libqtile import layout from libqtile.confreader import Config from test.layouts.layout_utils import assert_focus_path, assert_focused class RatioTileConfig(Config): auto_fullscreen = True groups = [ libqtile.config.Group("a"), libqtile.config.Group("b"), libqtile.config.Group("c"), libqtile.config.Group("d"), ] layouts = [layout.RatioTile(ratio=0.5), layout.RatioTile(), layout.RatioTile(fancy=True)] floating_layout = libqtile.resources.default_config.floating_layout keys = [] mouse = [] screens = [] follow_mouse_focus = False ratiotile_config = pytest.mark.parametrize("manager", [RatioTileConfig], indirect=True) @ratiotile_config def test_ratiotile_add_windows(manager): for i in range(12): manager.test_window(str(i)) if i == 0: assert manager.c.layout.info()["layout_info"] == [(0, 0, 800, 600)] elif i == 1: assert manager.c.layout.info()["layout_info"] == [ (0, 0, 400, 600), (400, 0, 400, 600), ] elif i == 2: assert manager.c.layout.info()["layout_info"] == [ (0, 0, 266, 600), (266, 0, 266, 600), (532, 0, 268, 600), ] elif i == 3: assert manager.c.layout.info()["layout_info"] == [ (0, 0, 200, 600), (200, 0, 200, 600), (400, 0, 200, 600), (600, 0, 200, 600), ] elif i == 4: assert manager.c.layout.info()["layout_info"] == [ (0, 0, 160, 600), (160, 0, 160, 600), (320, 0, 160, 600), (480, 0, 160, 600), (640, 0, 160, 600), ] elif i == 5: assert manager.c.layout.info()["layout_info"] == [ (0, 0, 133, 600), (133, 0, 133, 600), (266, 0, 133, 600), (399, 0, 133, 600), (532, 0, 133, 600), (665, 0, 135, 600), ] elif i == 6: assert manager.c.layout.info()["layout_info"] == [ (0, 0, 200, 300), (200, 0, 200, 300), (400, 0, 200, 300), (600, 0, 200, 300), (0, 300, 266, 300), (266, 300, 266, 300), (532, 300, 268, 300), ] elif i == 7: assert manager.c.layout.info()["layout_info"] == [ (0, 0, 200, 300), (200, 0, 200, 300), (400, 0, 200, 300), (600, 0, 200, 300), (0, 300, 200, 300), (200, 300, 200, 300), (400, 300, 200, 300), (600, 300, 200, 300), ] elif i == 8: assert manager.c.layout.info()["layout_info"] == [ (0, 0, 160, 300), (160, 0, 160, 300), (320, 0, 160, 300), (480, 0, 160, 300), (640, 0, 160, 300), (0, 300, 200, 300), (200, 300, 200, 300), (400, 300, 200, 300), (600, 300, 200, 300), ] elif i == 9: assert manager.c.layout.info()["layout_info"] == [ (0, 0, 160, 300), (160, 0, 160, 300), (320, 0, 160, 300), (480, 0, 160, 300), (640, 0, 160, 300), (0, 300, 160, 300), (160, 300, 160, 300), (320, 300, 160, 300), (480, 300, 160, 300), (640, 300, 160, 300), ] elif i == 10: assert manager.c.layout.info()["layout_info"] == [ (0, 0, 133, 300), (133, 0, 133, 300), (266, 0, 133, 300), (399, 0, 133, 300), (532, 0, 133, 300), (665, 0, 135, 300), (0, 300, 160, 300), (160, 300, 160, 300), (320, 300, 160, 300), (480, 300, 160, 300), (640, 300, 160, 300), ] elif i == 11: assert manager.c.layout.info()["layout_info"] == [ (0, 0, 133, 300), (133, 0, 133, 300), (266, 0, 133, 300), (399, 0, 133, 300), (532, 0, 133, 300), (665, 0, 135, 300), (0, 300, 133, 300), (133, 300, 133, 300), (266, 300, 133, 300), (399, 300, 133, 300), (532, 300, 133, 300), (665, 300, 135, 300), ] else: assert False @ratiotile_config def test_ratiotile_add_windows_golden_ratio(manager): manager.c.next_layout() for i in range(12): manager.test_window(str(i)) if i == 0: assert manager.c.layout.info()["layout_info"] == [(0, 0, 800, 600)] elif i == 4: # the rest test col order assert manager.c.layout.info()["layout_info"] == [ (0, 0, 400, 200), (0, 200, 400, 200), (0, 400, 400, 200), (400, 0, 400, 300), (400, 300, 400, 300), ] elif i == 5: assert manager.c.layout.info()["layout_info"] == [ (0, 0, 400, 200), (0, 200, 400, 200), (0, 400, 400, 200), (400, 0, 400, 200), (400, 200, 400, 200), (400, 400, 400, 200), ] elif i == 9: assert manager.c.layout.info()["layout_info"] == [ (0, 0, 266, 150), (0, 150, 266, 150), (0, 300, 266, 150), (0, 450, 266, 150), (266, 0, 266, 150), (266, 150, 266, 150), (266, 300, 266, 150), (266, 450, 266, 150), (532, 0, 266, 300), (532, 300, 266, 300), ] elif i == 10: assert manager.c.layout.info()["layout_info"] == [ (0, 0, 266, 150), (0, 150, 266, 150), (0, 300, 266, 150), (0, 450, 266, 150), (266, 0, 266, 150), (266, 150, 266, 150), (266, 300, 266, 150), (266, 450, 266, 150), (532, 0, 266, 200), (532, 200, 266, 200), (532, 400, 266, 200), ] elif i == 11: assert manager.c.layout.info()["layout_info"] == [ (0, 0, 266, 150), (0, 150, 266, 150), (0, 300, 266, 150), (0, 450, 266, 150), (266, 0, 266, 150), (266, 150, 266, 150), (266, 300, 266, 150), (266, 450, 266, 150), (532, 0, 266, 150), (532, 150, 266, 150), (532, 300, 266, 150), (532, 450, 266, 150), ] @ratiotile_config def test_ratiotile_basic(manager): manager.test_window("one") manager.test_window("two") manager.test_window("three") sleep(0.1) assert manager.c.window.info()["width"] == 264 assert manager.c.window.info()["height"] == 598 assert manager.c.window.info()["x"] == 0 assert manager.c.window.info()["y"] == 0 assert manager.c.window.info()["name"] == "three" manager.c.group.next_window() assert manager.c.window.info()["width"] == 264 assert manager.c.window.info()["height"] == 598 assert manager.c.window.info()["x"] == 266 assert manager.c.window.info()["y"] == 0 assert manager.c.window.info()["name"] == "two" manager.c.group.next_window() assert manager.c.window.info()["width"] == 266 assert manager.c.window.info()["height"] == 598 assert manager.c.window.info()["x"] == 532 assert manager.c.window.info()["y"] == 0 assert manager.c.window.info()["name"] == "one" @ratiotile_config def test_ratiotile_window_focus_cycle(manager): # setup 3 tiled and two floating clients manager.test_window("one") manager.test_window("two") manager.test_window("float1") manager.c.window.toggle_floating() manager.test_window("float2") manager.c.window.toggle_floating() manager.test_window("three") # test preconditions, RatioTile adds clients to head assert manager.c.layout.info()["clients"] == ["three", "two", "one"] # last added window has focus assert_focused(manager, "three") # assert window focus cycle, according to order in layout assert_focus_path(manager, "two", "one", "float1", "float2", "three") @ratiotile_config def test_ratiotile_alternative_calculation(manager): manager.c.next_layout() manager.c.next_layout() for i in range(12): manager.test_window(str(i)) if i == 0: assert manager.c.layout.info()["layout_info"] == [(0, 0, 800, 600)] elif i == 4: assert manager.c.layout.info()["layout_info"] == [ (0, 0, 400, 200), (0, 200, 400, 200), (0, 400, 400, 200), (400, 0, 400, 300), (400, 300, 400, 300), ] elif i == 5: assert manager.c.layout.info()["layout_info"] == [ (0, 0, 400, 200), (0, 200, 400, 200), (0, 400, 400, 200), (400, 0, 400, 200), (400, 200, 400, 200), (400, 400, 400, 200), ] elif i == 9: assert manager.c.layout.info()["layout_info"] == [ (0, 0, 266, 150), (0, 150, 266, 150), (0, 300, 266, 150), (0, 450, 266, 150), (266, 0, 267, 200), (266, 200, 267, 200), (266, 400, 267, 200), (533, 0, 267, 200), (533, 200, 267, 200), (533, 400, 267, 200), ] elif i == 10: assert manager.c.layout.info()["layout_info"] == [ (0, 0, 266, 150), (0, 150, 266, 150), (0, 300, 266, 150), (0, 450, 266, 150), (266, 0, 267, 150), (266, 150, 267, 150), (266, 300, 267, 150), (266, 450, 267, 150), (533, 0, 267, 200), (533, 200, 267, 200), (533, 400, 267, 200), ] elif i == 11: assert manager.c.layout.info()["layout_info"] == [ (0, 0, 266, 150), (0, 150, 266, 150), (0, 300, 266, 150), (0, 450, 266, 150), (266, 0, 267, 150), (266, 150, 267, 150), (266, 300, 267, 150), (266, 450, 267, 150), (533, 0, 267, 150), (533, 150, 267, 150), (533, 300, 267, 150), (533, 450, 267, 150), ] @ratiotile_config def test_shuffling(manager): def clients(): return manager.c.layout.info()["clients"] for i in range(3): manager.test_window(str(i)) assert clients() == ["2", "1", "0"] manager.c.layout.shuffle_up() assert clients() == ["0", "2", "1"] manager.c.layout.shuffle_up() assert clients() == ["1", "0", "2"] manager.c.layout.shuffle_down() assert clients() == ["0", "2", "1"] manager.c.layout.shuffle_down() assert clients() == ["2", "1", "0"] @ratiotile_config def test_resizing(manager): def sizes(): return manager.c.layout.info()["layout_info"] for i in range(5): manager.test_window(str(i)) assert sizes() == [ (0, 0, 160, 600), (160, 0, 160, 600), (320, 0, 160, 600), (480, 0, 160, 600), (640, 0, 160, 600), ] manager.c.layout.increase_ratio() assert sizes() == [ (0, 0, 266, 300), (266, 0, 266, 300), (532, 0, 268, 300), (0, 300, 400, 300), (400, 300, 400, 300), ] manager.c.layout.decrease_ratio() assert sizes() == [ (0, 0, 160, 600), (160, 0, 160, 600), (320, 0, 160, 600), (480, 0, 160, 600), (640, 0, 160, 600), ] qtile-0.31.0/test/layouts/test_treetab.py0000664000175000017500000001365414762660347020365 0ustar epsilonepsilon# Copyright (c) 2019 Guangwang Huang # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest import libqtile.config from libqtile import layout from libqtile.confreader import Config from test.layouts.layout_utils import assert_focus_path, assert_focused class TreeTabConfig(Config): auto_fullscreen = True groups = [ libqtile.config.Group("a"), libqtile.config.Group("b"), libqtile.config.Group("c"), libqtile.config.Group("d"), ] layouts = [ layout.TreeTab(sections=["Foo", "Bar"]), ] floating_layout = libqtile.resources.default_config.floating_layout keys = [] mouse = [] screens = [] follow_mouse_focus = False treetab_config = pytest.mark.parametrize("manager", [TreeTabConfig], indirect=True) @treetab_config def test_window(manager): # setup 3 tiled and two floating clients manager.test_window("one") manager.test_window("two") manager.test_window("float1", floating=True) manager.test_window("float2", floating=True) manager.test_window("three") # test preconditions, columns adds clients at pos of current, in two stacks assert manager.c.layout.info()["clients"] == ["one", "three", "two"] assert manager.c.layout.info()["sections"] == ["Foo", "Bar"] assert manager.c.layout.info()["client_trees"] == { "Foo": [["one"], ["two"], ["three"]], "Bar": [], } # last added window has focus assert_focused(manager, "three") manager.c.layout.up() assert_focused(manager, "two") manager.c.layout.down() assert_focused(manager, "three") # test command move_up/down manager.c.layout.move_up() assert manager.c.layout.info()["clients"] == ["one", "three", "two"] assert manager.c.layout.info()["client_trees"] == { "Foo": [["one"], ["three"], ["two"]], "Bar": [], } manager.c.layout.move_down() assert manager.c.layout.info()["client_trees"] == { "Foo": [["one"], ["two"], ["three"]], "Bar": [], } # section_down/up manager.c.layout.up() # focus two manager.c.layout.section_down() assert manager.c.layout.info()["client_trees"] == { "Foo": [["one"], ["three"]], "Bar": [["two"]], } manager.c.layout.section_up() assert manager.c.layout.info()["client_trees"] == { "Foo": [["one"], ["three"], ["two"]], "Bar": [], } # del_section manager.c.layout.up() # focus three manager.c.layout.section_down() manager.c.layout.del_section("Bar") assert manager.c.layout.info()["client_trees"] == {"Foo": [["one"], ["two"], ["three"]]} # add_section manager.c.layout.add_section("Baz") assert manager.c.layout.info()["client_trees"] == { "Foo": [["one"], ["two"], ["three"]], "Baz": [], } manager.c.layout.del_section("Baz") # move_left/right manager.c.layout.move_left() # no effect for top-level children assert manager.c.layout.info()["client_trees"] == {"Foo": [["one"], ["two"], ["three"]]} manager.c.layout.move_right() assert manager.c.layout.info()["client_trees"] == {"Foo": [["one"], ["two", ["three"]]]} manager.c.layout.move_right() # no effect assert manager.c.layout.info()["client_trees"] == {"Foo": [["one"], ["two", ["three"]]]} manager.test_window("four") manager.c.layout.move_right() manager.c.layout.up() manager.test_window("five") assert manager.c.layout.info()["client_trees"] == { "Foo": [["one"], ["two", ["three", ["four"]], ["five"]]] } # expand/collapse_branch, and check focus order manager.c.layout.up() manager.c.layout.up() # focus three manager.c.layout.collapse_branch() assert manager.c.layout.info()["client_trees"] == { "Foo": [["one"], ["two", ["three"], ["five"]]] } assert_focus_path(manager, "five", "float1", "float2", "one", "two", "three") manager.c.layout.expand_branch() assert manager.c.layout.info()["client_trees"] == { "Foo": [["one"], ["two", ["three", ["four"]], ["five"]]] } assert_focus_path(manager, "four", "five", "float1", "float2", "one", "two", "three") @treetab_config def test_sort_windows(manager): manager.test_window("one") manager.test_window("two") manager.test_window("101") manager.test_window("102") manager.test_window("103") assert manager.c.layout.info()["client_trees"] == { "Foo": [["one"], ["two"], ["101"], ["102"], ["103"]], "Bar": [], } """ # TODO how to serialize a function object? i.e. `sorter`: def sorter(window): try: if int(window.name) % 2 == 0: return 'Even' else: return 'Odd' except ValueError: return 'Bar' manager.c.layout.sort_windows(sorter) assert manager.c.layout.info()['client_trees'] == { 'Foo': [], 'Bar': [['one'], ['two']], 'Even': [['102']], 'Odd': [['101'], ['103']] } """ qtile-0.31.0/test/layouts/test_screensplit.py0000664000175000017500000001637314762660347021273 0ustar epsilonepsilon# Copyright (c) 2022 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest import libqtile.config from libqtile import layout from libqtile.config import Match from libqtile.confreader import Config from test.layouts.layout_utils import assert_dimensions @pytest.fixture(scope="function") def ss_manager(manager_nospawn, request): class ScreenSplitConfig(Config): auto_fullscreen = True groups = [libqtile.config.Group("a")] layouts = [layout.ScreenSplit(**getattr(request, "param", dict()))] floating_layout = libqtile.resources.default_config.floating_layout keys = [] mouse = [] screens = [] follow_mouse_focus = False manager_nospawn.start(ScreenSplitConfig) yield manager_nospawn def ss_config(**kwargs): return pytest.mark.parametrize("ss_manager", [kwargs], indirect=True) @ss_config() def test_screensplit(ss_manager): # Max layout is default, occupies top half of screen assert ss_manager.c.layout.info()["current_layout"] == "max" ss_manager.test_window("one") assert_dimensions(ss_manager, 0, 0, 800, 300) ss_manager.test_window("two") assert_dimensions(ss_manager, 0, 0, 800, 300) assert ss_manager.c.layout.info()["current_clients"] == ["one", "two"] ss_manager.c.layout.next_split() assert ss_manager.c.layout.info()["current_layout"] == "columns" assert ss_manager.c.layout.info()["current_clients"] == [] # Columns layout has no border on single... ss_manager.test_window("three") assert_dimensions(ss_manager, 0, 300, 800, 300) # ... but a border of 2 when multiple windows ss_manager.test_window("four") assert_dimensions(ss_manager, 400, 300, 396, 296) assert ss_manager.c.layout.info()["current_clients"] == ["three", "four"] ss_manager.c.layout.next_split() assert ss_manager.c.layout.info()["current_layout"] == "max" assert ss_manager.c.layout.info()["current_clients"] == ["one", "two"] @ss_config() def test_commands_passthrough(ss_manager): assert ss_manager.c.layout.info()["current_layout"] == "max" assert "grow_left" not in ss_manager.c.layout.commands() ss_manager.c.layout.next_split() assert ss_manager.c.layout.info()["current_layout"] == "columns" ss_manager.test_window("one") assert_dimensions(ss_manager, 0, 300, 800, 300) ss_manager.test_window("two") assert_dimensions(ss_manager, 400, 300, 396, 296) assert "grow_left" in ss_manager.c.layout.commands() # Grow window by 40 pixels ss_manager.c.layout.grow_left() assert_dimensions(ss_manager, 360, 300, 436, 296) @ss_config() def test_move_window_to_split(ss_manager): assert ss_manager.c.layout.info()["current_layout"] == "max" ss_manager.test_window("one") assert_dimensions(ss_manager, 0, 0, 800, 300) ss_manager.c.layout.move_window_to_next_split() assert ss_manager.c.layout.info()["current_layout"] == "columns" assert_dimensions(ss_manager, 0, 300, 800, 300) ss_manager.c.layout.move_window_to_previous_split() assert ss_manager.c.layout.info()["current_layout"] == "max" assert_dimensions(ss_manager, 0, 0, 800, 300) @ss_config( splits=[ { "name": "no_match", "rect": (0, 0, 1, 0.5), "layout": layout.Max(), }, { "name": "match", "rect": (0, 0.5, 1, 0.5), "layout": layout.Spiral(), "matches": [Match(title="test")], }, ] ) def test_match_window(ss_manager): assert ss_manager.c.layout.info()["current_layout"] == "max" ss_manager.test_window("one") assert ss_manager.c.layout.info()["current_layout"] == "max" ss_manager.test_window("test") assert ss_manager.c.layout.info()["current_layout"] == "spiral" def test_invalid_splits(): # Test 1: Missing required keys with pytest.raises(ValueError) as e: layout.ScreenSplit(splits=[{"rect": (0, 0, 1, 1)}]) assert str(e.value) == "Splits must define 'name', 'rect' and 'layout'." # Test 2: rect is not list/tuple with pytest.raises(ValueError) as e: layout.ScreenSplit( splits=[{"name": "test", "rect": "0, 0, 1, 1", "layout": layout.Max()}] ) assert str(e.value) == "Split rect should be a list/tuple." # Test 3: Wrong number of items in rect with pytest.raises(ValueError) as e: layout.ScreenSplit(splits=[{"name": "test", "rect": (0, 0, 1), "layout": layout.Max()}]) assert str(e.value) == "Split rect should have 4 float/int members." # Test 4: Not all rect items are numbers with pytest.raises(ValueError) as e: layout.ScreenSplit( splits=[{"name": "test", "rect": (0, 0, 1, "1"), "layout": layout.Max()}] ) assert str(e.value) == "Split rect should have 4 float/int members." # Test 5: Nested ScreenSplit with pytest.raises(ValueError) as e: layout.ScreenSplit( splits=[{"name": "test", "rect": (0, 0, 1, 1), "layout": layout.ScreenSplit()}] ) assert str(e.value) == "ScreenSplit layouts cannot be nested." # Test 6: Matches has invalid object with pytest.raises(ValueError) as e: layout.ScreenSplit( splits=[ {"name": "test", "rect": (0, 0, 1, 1), "layout": layout.Max(), "matches": [True]} ] ) assert str(e.value) == "Invalid object in 'matches'." # Test 7: Single match with pytest.raises(ValueError) as e: layout.ScreenSplit( splits=[ { "name": "test", "rect": (0, 0, 1, 1), "layout": layout.Max(), "matches": Match(wm_class="test"), } ] ) assert str(e.value) == "'matches' must be a list of 'Match' objects." # Test 8: Test valid config - no matches s_split = layout.ScreenSplit( splits=[{"name": "test", "rect": (0, 0, 1, 1), "layout": layout.Max()}] ) assert s_split # Test 9: Test valid config - matches s_split = layout.ScreenSplit( splits=[ { "name": "test", "rect": (0, 0, 1, 1), "layout": layout.Max(), "matches": [Match(wm_class="test")], } ] ) assert s_split qtile-0.31.0/test/layouts/test_verticaltile.py0000664000175000017500000001071714762660347021423 0ustar epsilonepsilon# Copyright (c) 2011 Florian Mounier # Copyright (c) 2012, 2014-2015 Tycho Andersen # Copyright (c) 2013 Mattias Svala # Copyright (c) 2013 Craig Barnes # Copyright (c) 2014 ramnes # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Adi Sieker # Copyright (c) 2014 Chris Wesseling # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest import libqtile.config from libqtile import layout from libqtile.confreader import Config from test.layouts.layout_utils import assert_dimensions, assert_focus_path, assert_focused class VerticalTileConfig(Config): auto_fullscreen = True groups = [ libqtile.config.Group("a"), libqtile.config.Group("b"), libqtile.config.Group("c"), libqtile.config.Group("d"), ] # default border and margin layouts = [layout.VerticalTile(columns=2)] floating_layout = libqtile.resources.default_config.floating_layout keys = [] mouse = [] screens = [] class VerticalTileSingleBorderConfig(VerticalTileConfig): layouts = [layout.VerticalTile(columns=2, single_border_width=2, border_width=8)] class VerticalTileSingleMarginConfig(VerticalTileConfig): layouts = [layout.VerticalTile(columns=2, single_margin=2, margin=8)] verticaltile_config = pytest.mark.parametrize("manager", [VerticalTileConfig], indirect=True) verticaltile_single_border_config = pytest.mark.parametrize( "manager", [VerticalTileSingleBorderConfig], indirect=True ) verticaltile_single_margin_config = pytest.mark.parametrize( "manager", [VerticalTileSingleMarginConfig], indirect=True ) @verticaltile_config def test_verticaltile_simple(manager): manager.test_window("one") assert_dimensions(manager, 0, 0, 798, 598) manager.test_window("two") assert_dimensions(manager, 0, 300, 798, 298) manager.test_window("three") assert_dimensions(manager, 0, 400, 798, 198) @verticaltile_config def test_verticaltile_maximize(manager): manager.test_window("one") assert_dimensions(manager, 0, 0, 798, 598) manager.test_window("two") assert_dimensions(manager, 0, 300, 798, 298) # Maximize the bottom layout, taking 75% of space manager.c.layout.maximize() assert_dimensions(manager, 0, 150, 798, 448) @verticaltile_config def test_verticaltile_window_focus_cycle(manager): # setup 3 tiled and two floating clients manager.test_window("one") manager.test_window("two") manager.test_window("float1") manager.c.window.toggle_floating() manager.test_window("float2") manager.c.window.toggle_floating() manager.test_window("three") # test preconditions assert manager.c.layout.info()["clients"] == ["one", "two", "three"] # last added window has focus assert_focused(manager, "three") # assert window focus cycle, according to order in layout assert_focus_path(manager, "float1", "float2", "one", "two", "three") @verticaltile_single_border_config def test_verticaltile_single_border(manager): manager.test_window("one") assert_dimensions(manager, 0, 0, 796, 596) manager.test_window("two") assert_dimensions(manager, 0, 300, 784, 284) manager.test_window("three") assert_dimensions(manager, 0, 400, 784, 184) @verticaltile_single_margin_config def test_verticaltile_single_margin(manager): manager.test_window("one") assert_dimensions(manager, 2, 2, 794, 594) manager.test_window("two") assert_dimensions(manager, 8, 308, 782, 282) manager.test_window("three") assert_dimensions(manager, 8, 408, 782, 182) qtile-0.31.0/test/layouts/test_zoomy.py0000664000175000017500000000565014762660347020111 0ustar epsilonepsilon# Copyright (c) 2011 Florian Mounier # Copyright (c) 2012, 2014-2015 Tycho Andersen # Copyright (c) 2013 Mattias Svala # Copyright (c) 2013 Craig Barnes # Copyright (c) 2014 ramnes # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Adi Sieker # Copyright (c) 2014 Chris Wesseling # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest import libqtile.config from libqtile import layout from libqtile.confreader import Config from test.layouts.layout_utils import assert_dimensions, assert_focus_path, assert_focused class ZoomyConfig(Config): auto_fullscreen = True groups = [ libqtile.config.Group("a"), ] layouts = [ layout.Zoomy(columnwidth=200), ] floating_layout = libqtile.resources.default_config.floating_layout keys = [] mouse = [] screens = [] zoomy_config = pytest.mark.parametrize("manager", [ZoomyConfig], indirect=True) @zoomy_config def test_zoomy_one(manager): manager.test_window("one") assert_dimensions(manager, 0, 0, 600, 600) manager.test_window("two") assert_dimensions(manager, 0, 0, 600, 600) manager.test_window("three") assert_dimensions(manager, 0, 0, 600, 600) assert_focus_path(manager, "two", "one", "three") # TODO(pc) find a way to check size of inactive windows @zoomy_config def test_zoomy_window_focus_cycle(manager): # setup 3 tiled and two floating clients manager.test_window("one") manager.test_window("two") manager.test_window("float1") manager.c.window.toggle_floating() manager.test_window("float2") manager.c.window.toggle_floating() manager.test_window("three") # test preconditions, Zoomy adds clients at head assert manager.c.layout.info()["clients"] == ["three", "two", "one"] # last added window has focus assert_focused(manager, "three") # assert window focus cycle, according to order in layout assert_focus_path(manager, "two", "one", "float1", "float2", "three") qtile-0.31.0/test/layouts/test_common.py0000664000175000017500000005105414762660347020223 0ustar epsilonepsilon# Copyright (c) 2017 Dario Giovannetti # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest import libqtile import libqtile.config import libqtile.hook from libqtile import layout from libqtile.confreader import Config from test.layouts.layout_utils import ( assert_dimensions_fit, assert_focus_path_unordered, assert_focused, ) try: # Check to see if we should skip tests using notifications on Wayland import gi gi.require_version("Gtk", "3.0") gi.require_version("GtkLayerShell", "0.1") from gi.repository import GtkLayerShell # noqa: F401 has_wayland_notifications = True except (ImportError, ValueError): has_wayland_notifications = False class AllLayoutsConfig(Config): """ Ensure that all layouts behave consistently in some common scenarios. """ groups = [ libqtile.config.Group("a"), libqtile.config.Group("b"), libqtile.config.Group("c"), libqtile.config.Group("d"), ] follow_mouse_focus = False floating_layout = libqtile.resources.default_config.floating_layout screens = [] @staticmethod def iter_layouts(): # Retrieve the layouts dynamically (i.e. do not hard-code a list) to # prevent forgetting to add new future layouts for layout_name in dir(layout): layout_cls = getattr(layout, layout_name) try: test = issubclass(layout_cls, layout.base.Layout) except TypeError: pass else: # Explicitly exclude the Slice layout, since it depends on # other layouts (tested here) and has its own specific tests if test and layout_name != "Slice": yield layout_name, layout_cls @classmethod def generate(cls): """ Generate a configuration for each layout currently in the repo. Each configuration has only the tested layout (i.e. 1 item) in the 'layouts' variable. """ return [ type(layout_name, (cls,), {"layouts": [layout_cls()]}) for layout_name, layout_cls in cls.iter_layouts() ] class AllDelegateLayoutsConfig(AllLayoutsConfig): @classmethod def generate(cls): """ Generate a Slice configuration for each layout currently in the repo. Each layout is made a delegate/fallback layout of the Slice layout. Each configuration has only the tested layout (i.e. 1 item) in the 'layouts' variable. """ return [ type( layout_name, (cls,), {"layouts": [layout.slice.Slice(wname="nevermatch", fallback=layout_cls())]}, ) for layout_name, layout_cls in cls.iter_layouts() ] class AllLayouts(AllLayoutsConfig): """ Like AllLayoutsConfig, but all the layouts in the repo are installed together in the 'layouts' variable. """ layouts = [layout_cls() for layout_name, layout_cls in AllLayoutsConfig.iter_layouts()] class AllLayoutsConfigEvents(AllLayoutsConfig): """ Extends AllLayoutsConfig to test events. """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # TODO: Test more events @libqtile.hook.subscribe.startup def _(): libqtile.qtile.test_data = { "focus_change": 0, "config_name": self.__class__.__name__, } @libqtile.hook.subscribe.focus_change def _(): if hasattr(libqtile.qtile, "test_data"): # focus_change can fire before during startup libqtile.qtile.test_data["focus_change"] += 1 each_layout_config = pytest.mark.parametrize( "manager", AllLayoutsConfig.generate(), indirect=True ) all_layouts_config = pytest.mark.parametrize("manager", [AllLayouts], indirect=True) each_layout_config_events = pytest.mark.parametrize( "manager", AllLayoutsConfigEvents.generate(), indirect=True ) each_delegate_layout_config = pytest.mark.parametrize( "manager", AllDelegateLayoutsConfig.generate(), indirect=True ) @each_layout_config def test_window_order_fullscreen(manager): # Add a window to fullscreen manager.test_window("tofullscreen") # windows to add after the fullscreen window windows_after = ["two", "three"] # Add some windows before for win in windows_after: manager.test_window(win) windows_order = manager.c.layout.info()["clients"] # Focus window and toggle fullscreen manager.c.group.focus_by_name("tofullscreen") manager.c.window.toggle_fullscreen() manager.c.window.toggle_fullscreen() # Windows must be sorted in the same order as they were created assert windows_order == manager.c.layout.info()["clients"] @each_layout_config def test_window_types(manager): if manager.backend.name == "wayland" and not has_wayland_notifications: pytest.skip("Notification tests for Wayland need gtk-layer-shell") manager.test_window("one") # A dialog should take focus and be floating # A notification shouldn't steal focus and should be floating manager.test_window("dialog", floating=True) assert_focused(manager, "dialog") manager.test_notification("notification") assert manager.c.group.info()["focus"] == "dialog" for window in manager.c.windows(): if window["name"] in ("dialog", "notification"): assert window["floating"] @each_layout_config def test_focus_cycle(manager): manager.test_window("one") manager.test_window("two") manager.test_window("float1", floating=True) manager.test_window("float2", floating=True) manager.test_window("three") # Test preconditions (the order of items in 'clients' is managed by each layout) assert set(manager.c.layout.info()["clients"]) == {"one", "two", "three"} assert_focused(manager, "three") # Assert that the layout cycles the focus on all windows assert_focus_path_unordered(manager, "float1", "float2", "one", "two", "three") @each_layout_config def test_swap_window_order(manager): manager.test_window("one") manager.test_window("two") manager.test_window("three") window_order = manager.c.group.info().get("windows") assert window_order == ["one", "two", "three"] # Swap window on index 0 with index 2 manager.c.group.focus_by_name("one") manager.c.group.swap_window_order(2) window_order = manager.c.group.info().get("windows") assert window_order == ["three", "two", "one"] assert_focused(manager, "one") # Swap window on index 1 with index 0 manager.c.group.focus_by_name("two") manager.c.group.swap_window_order(0) window_order = manager.c.group.info().get("windows") assert window_order == ["two", "three", "one"] assert_focused(manager, "two") # Swap window on index 0 with index out of bounds manager.c.group.focus_by_name("two") manager.c.group.swap_window_order(3) window_order = manager.c.group.info().get("windows") assert window_order == ["two", "three", "one"] assert_focused(manager, "two") @each_layout_config def test_focus_back(manager): # No exception must be raised without windows manager.c.group.focus_back() # Nothing must happen with only one window manager.test_window("one") manager.c.group.focus_back() assert_focused(manager, "one") # 2 windows two = manager.test_window("two") assert_focused(manager, "two") manager.c.group.focus_back() assert_focused(manager, "one") manager.c.group.focus_back() assert_focused(manager, "two") # Float a window three = manager.test_window("three") manager.c.group.focus_back() assert_focused(manager, "two") manager.c.window.toggle_floating() manager.c.group.focus_back() assert_focused(manager, "three") # If the previous window is killed, the further previous one must be focused manager.test_window("four") manager.kill_window(two) manager.kill_window(three) assert_focused(manager, "four") manager.c.group.focus_back() assert_focused(manager, "one") # TODO: Test more events @each_layout_config_events def test_focus_change_event(manager): # Test that the correct number of focus_change events are fired e.g. when # opening, closing or switching windows. # If for example a layout explicitly fired a focus_change event even though # group._Group.focus() or group._Group.remove() already fire one, the other # installed layouts would wrongly react to it and cause misbehaviour. # In short, this test prevents layouts from influencing each other in # unexpected ways. # Ensure we are in fact using the right layout assert manager.c.get_test_data()["config_name"].lower() == manager.c.layout.info()["name"] # Spawning a window must fire only 1 focus_change event assert manager.c.get_test_data()["focus_change"] == 0 one = manager.test_window("one") assert manager.c.get_test_data()["focus_change"] == 1 two = manager.test_window("two") assert manager.c.get_test_data()["focus_change"] == 2 three = manager.test_window("three") assert manager.c.get_test_data()["focus_change"] == 3 # Switching window must fire only 1 focus_change event assert_focused(manager, "three") manager.c.group.focus_by_name("one") assert manager.c.get_test_data()["focus_change"] == 4 assert_focused(manager, "one") # Focusing the current window must fire another focus_change event manager.c.group.focus_by_name("one") assert manager.c.get_test_data()["focus_change"] == 5 # Toggling a window floating should not fire focus_change events manager.c.window.toggle_floating() assert manager.c.get_test_data()["focus_change"] == 5 manager.c.window.toggle_floating() assert manager.c.get_test_data()["focus_change"] == 5 # Removing the focused window must fire only 1 focus_change event assert_focused(manager, "one") assert manager.c.group.info()["focus_history"] == ["two", "three", "one"] manager.kill_window(one) assert manager.c.get_test_data()["focus_change"] == 6 # The position where 'one' was after it was floated and unfloated # above depends on the layout, so we can't predict here what window gets # selected after killing it; for this reason, focus 'three' explicitly to # continue testing manager.c.group.focus_by_name("three") assert manager.c.group.info()["focus_history"] == ["two", "three"] assert manager.c.get_test_data()["focus_change"] == 7 # Removing a non-focused window must not fire focus_change events manager.kill_window(two) assert manager.c.get_test_data()["focus_change"] == 7 assert_focused(manager, "three") # Removing the last window must still generate 1 focus_change event manager.kill_window(three) assert manager.c.layout.info()["clients"] == [] assert manager.c.get_test_data()["focus_change"] == 8 @each_layout_config def test_remove(manager): one = manager.test_window("one") two = manager.test_window("two") three = manager.test_window("three") assert_focused(manager, "three") assert manager.c.group.info()["focus_history"] == ["one", "two", "three"] # Removing a focused window must focus another (which one depends on the layout) manager.kill_window(three) assert manager.c.window.info()["name"] in manager.c.layout.info()["clients"] # To continue testing, explicitly set focus on 'two' manager.c.group.focus_by_name("two") manager.test_window("four") assert_focused(manager, "four") assert manager.c.group.info()["focus_history"] == ["one", "two", "four"] # Removing a non-focused window must not change the current focus manager.kill_window(two) assert_focused(manager, "four") assert manager.c.group.info()["focus_history"] == ["one", "four"] # Add more windows and shuffle the focus order five = manager.test_window("five") manager.test_window("six") manager.c.group.focus_by_name("one") seven = manager.test_window("seven") manager.c.group.focus_by_name("six") assert_focused(manager, "six") assert manager.c.group.info()["focus_history"] == ["four", "five", "one", "seven", "six"] manager.kill_window(five) manager.kill_window(one) assert_focused(manager, "six") assert manager.c.group.info()["focus_history"] == ["four", "seven", "six"] manager.c.group.focus_by_name("seven") manager.kill_window(seven) assert manager.c.window.info()["name"] in manager.c.layout.info()["clients"] @each_layout_config def test_remove_floating(manager): one = manager.test_window("one") manager.test_window("two") float1 = manager.test_window("float1", floating=True) assert_focused(manager, "float1") assert set(manager.c.layout.info()["clients"]) == {"one", "two"} assert manager.c.group.info()["focus_history"] == ["one", "two", "float1"] # Removing a focused floating window must focus the one that was focused before manager.kill_window(float1) assert_focused(manager, "two") assert manager.c.group.info()["focus_history"] == ["one", "two"] float2 = manager.test_window("float2", floating=True) assert_focused(manager, "float2") assert manager.c.group.info()["focus_history"] == ["one", "two", "float2"] # Removing a non-focused floating window must not change the current focus manager.c.group.focus_by_name("two") manager.kill_window(float2) assert_focused(manager, "two") assert manager.c.group.info()["focus_history"] == ["one", "two"] # Add more windows and shuffle the focus order manager.test_window("three") float3 = manager.test_window("float3", floating=True) manager.c.group.focus_by_name("one") float4 = manager.test_window("float4", floating=True) float5 = manager.test_window("float5", floating=True) manager.c.group.focus_by_name("three") manager.c.group.focus_by_name("float3") assert manager.c.group.info()["focus_history"] == [ "two", "one", "float4", "float5", "three", "float3", ] manager.kill_window(one) assert_focused(manager, "float3") assert manager.c.group.info()["focus_history"] == [ "two", "float4", "float5", "three", "float3", ] manager.kill_window(float5) assert_focused(manager, "float3") assert manager.c.group.info()["focus_history"] == ["two", "float4", "three", "float3"] # The focus must be given to the previous window even if it's floating manager.c.group.focus_by_name("float4") assert manager.c.group.info()["focus_history"] == ["two", "three", "float3", "float4"] manager.kill_window(float4) assert_focused(manager, "float3") assert manager.c.group.info()["focus_history"] == ["two", "three", "float3"] four = manager.test_window("four") float6 = manager.test_window("float6", floating=True) five = manager.test_window("five") manager.c.group.focus_by_name("float3") assert manager.c.group.info()["focus_history"] == [ "two", "three", "four", "float6", "five", "float3", ] # Killing several unfocused windows before the current one, and then # killing the current window, must focus the remaining most recently # focused window manager.kill_window(five) manager.kill_window(four) manager.kill_window(float6) assert manager.c.group.info()["focus_history"] == ["two", "three", "float3"] manager.kill_window(float3) assert_focused(manager, "three") assert manager.c.group.info()["focus_history"] == ["two", "three"] @each_layout_config def test_desktop_notifications(manager): # Unlike normal floating windows such as dialogs, notifications don't steal # focus when they spawn, so test them separately if manager.backend.name == "wayland" and not has_wayland_notifications: pytest.skip("Notification tests for Wayland need gtk-layer-shell") # A notification fired in an empty group must not take focus notif1 = manager.test_notification("notif1") assert manager.c.group.info()["focus"] is None manager.kill_window(notif1) # A window is spawned while a notification is displayed notif2 = manager.test_notification("notif2") one = manager.test_window("one") assert manager.c.group.info()["focus_history"] == ["one"] manager.kill_window(notif2) # Another notification is fired, but the focus must not change notif3 = manager.test_notification("notif3") assert_focused(manager, "one") manager.kill_window(notif3) # Complicate the scenario with multiple windows and notifications dialog1 = manager.test_window("dialog1", floating=True) manager.test_window("two") notif4 = manager.test_notification("notif4") notif5 = manager.test_notification("notif5") assert manager.c.group.info()["focus_history"] == ["one", "dialog1", "two"] dialog2 = manager.test_window("dialog2", floating=True) manager.kill_window(notif5) manager.test_window("three") manager.kill_window(one) manager.c.group.focus_by_name("two") notif6 = manager.test_notification("notif6") notif7 = manager.test_notification("notif7") manager.kill_window(notif4) notif8 = manager.test_notification("notif8") assert manager.c.group.info()["focus_history"] == ["dialog1", "dialog2", "three", "two"] manager.test_window("dialog3", floating=True) manager.kill_window(dialog1) manager.kill_window(dialog2) manager.kill_window(notif6) manager.c.group.focus_by_name("three") manager.kill_window(notif7) manager.kill_window(notif8) assert manager.c.group.info()["focus_history"] == ["two", "dialog3", "three"] @each_delegate_layout_config def test_only_uses_delegated_screen_rect(manager): manager.test_window("one") manager.c.group.focus_by_name("one") assert_focused(manager, "one") assert_dimensions_fit(manager, 256, 0, 800 - 256, 600) @all_layouts_config def test_cycle_layouts(manager): manager.test_window("one") manager.test_window("two") manager.test_window("three") manager.test_window("four") manager.c.group.focus_by_name("three") assert_focused(manager, "three") # Cycling all the layouts must keep the current window focused initial_layout_name = manager.c.layout.info()["name"] while True: manager.c.next_layout() if manager.c.layout.info()["name"] == initial_layout_name: break # Use manager.c.layout.info()['name'] in the assertion message, so we # know which layout is buggy assert manager.c.window.info()["name"] == "three", manager.c.layout.info()["name"] # Now try backwards while True: manager.c.prev_layout() if manager.c.layout.info()["name"] == initial_layout_name: break # Use manager.c.layout.info()['name'] in the assertion message, so we # know which layout is buggy assert manager.c.window.info()["name"] == "three", manager.c.layout.info()["name"] class AllLayoutsMultipleBorders(AllLayoutsConfig): """ Like AllLayouts, but all the layouts have border_focus set to a list of colors. """ layouts = [ layout_cls(border_focus=["#000", "#111", "#222", "#333", "#444"]) for layout_name, layout_cls in AllLayoutsConfig.iter_layouts() ] @pytest.mark.parametrize("manager", [AllLayoutsMultipleBorders], indirect=True) def test_multiple_borders(manager): manager.test_window("one") manager.test_window("two") initial_layout_name = manager.c.layout.info()["name"] while True: manager.c.next_layout() if manager.c.layout.info()["name"] == initial_layout_name: break qtile-0.31.0/test/layouts/test_matrix.py0000664000175000017500000001154114762660347020234 0ustar epsilonepsilon# Copyright (c) 2011 Florian Mounier # Copyright (c) 2012, 2014-2015 Tycho Andersen # Copyright (c) 2013 Mattias Svala # Copyright (c) 2013 Craig Barnes # Copyright (c) 2014 ramnes # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Adi Sieker # Copyright (c) 2014 Chris Wesseling # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest import libqtile.config from libqtile import layout from libqtile.confreader import Config from test.layouts.layout_utils import assert_focus_path, assert_focused class MatrixConfig(Config): auto_fullscreen = True groups = [ libqtile.config.Group("a"), libqtile.config.Group("b"), libqtile.config.Group("c"), libqtile.config.Group("d"), ] layouts = [layout.Matrix(columns=2)] floating_layout = libqtile.resources.default_config.floating_layout keys = [] mouse = [] screens = [] matrix_config = pytest.mark.parametrize("manager", [MatrixConfig], indirect=True) @matrix_config def test_matrix_simple(manager): manager.test_window("one") assert manager.c.layout.info()["rows"] == [["one"]] manager.test_window("two") assert manager.c.layout.info()["rows"] == [["one", "two"]] manager.test_window("three") assert manager.c.layout.info()["rows"] == [["one", "two"], ["three"]] @matrix_config def test_matrix_navigation(manager): manager.test_window("one") manager.test_window("two") manager.test_window("three") manager.test_window("four") manager.test_window("five") manager.c.layout.right() assert manager.c.layout.info()["current_window"] == (0, 2) manager.c.layout.up() assert manager.c.layout.info()["current_window"] == (0, 1) manager.c.layout.up() assert manager.c.layout.info()["current_window"] == (0, 0) manager.c.layout.up() assert manager.c.layout.info()["current_window"] == (0, 2) manager.c.layout.down() assert manager.c.layout.info()["current_window"] == (0, 0) manager.c.layout.down() assert manager.c.layout.info()["current_window"] == (0, 1) manager.c.layout.right() assert manager.c.layout.info()["current_window"] == (1, 1) manager.c.layout.right() assert manager.c.layout.info()["current_window"] == (0, 1) manager.c.layout.left() assert manager.c.layout.info()["current_window"] == (1, 1) @matrix_config def test_matrix_add_remove_columns(manager): manager.test_window("one") manager.test_window("two") manager.test_window("three") manager.test_window("four") manager.test_window("five") manager.c.layout.add() assert manager.c.layout.info()["rows"] == [["one", "two", "three"], ["four", "five"]] manager.c.layout.delete() assert manager.c.layout.info()["rows"] == [["one", "two"], ["three", "four"], ["five"]] @matrix_config def test_matrix_window_focus_cycle(manager): # setup 3 tiled and two floating clients manager.test_window("one") manager.test_window("two") manager.test_window("float1") manager.c.window.toggle_floating() manager.test_window("float2") manager.c.window.toggle_floating() manager.test_window("three") # test preconditions assert manager.c.layout.info()["clients"] == ["one", "two", "three"] # last added window has focus assert_focused(manager, "three") # assert window focus cycle, according to order in layout assert_focus_path(manager, "float1", "float2", "one", "two", "three") @matrix_config def test_matrix_next_no_clients(manager): manager.c.layout.next() @matrix_config def test_matrix_previous_no_clients(manager): manager.c.layout.previous() def test_unknown_client(): """Simple test to get coverage to 100%!""" matrix = layout.Matrix() # The layout will not configure an unknown client. # Without the return statement in "configure" the following # code would result in an error assert matrix.configure("fakeclient", None) is None qtile-0.31.0/test/layouts/test_xmonad.py0000664000175000017500000010636014762660347020222 0ustar epsilonepsilon# Copyright (c) 2015 Sean Vig # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest import libqtile.config from libqtile import layout from libqtile.confreader import Config from test.helpers import Retry from test.layouts.layout_utils import assert_dimensions, assert_focus_path, assert_focused class MonadTallConfig(Config): auto_fullscreen = True groups = [libqtile.config.Group("a")] layouts = [layout.MonadTall()] floating_layout = libqtile.resources.default_config.floating_layout keys = [] mouse = [] screens = [] follow_mouse_focus = False monadtall_config = pytest.mark.parametrize("manager", [MonadTallConfig], indirect=True) class MonadTallNCPBeforeCurrentConfig(Config): auto_fullscreen = True groups = [libqtile.config.Group("a")] layouts = [layout.MonadTall(new_client_position="before_current")] floating_layout = libqtile.resources.default_config.floating_layout keys = [] mouse = [] screens = [] follow_mouse_focus = False monadtallncpbeforecurrent_config = pytest.mark.parametrize( "manager", [MonadTallNCPBeforeCurrentConfig], indirect=True ) class MonadTallNCPAfterCurrentConfig(Config): auto_fullscreen = True groups = [libqtile.config.Group("a")] layouts = [layout.MonadTall(new_client_position="after_current")] floating_layout = libqtile.resources.default_config.floating_layout keys = [] mouse = [] screens = [] follow_mouse_focus = False monadtallncpaftercurrent_config = pytest.mark.parametrize( "manager", [MonadTallNCPAfterCurrentConfig], indirect=True ) class MonadTallNewCLientPositionBottomConfig(Config): auto_fullscreen = True groups = [libqtile.config.Group("a")] layouts = [layout.MonadTall(new_client_position="bottom")] floating_layout = libqtile.resources.default_config.floating_layout keys = [] mouse = [] screens = [] follow_mouse_focus = False class MonadTallMarginsConfig(Config): auto_fullscreen = True groups = [libqtile.config.Group("a")] layouts = [layout.MonadTall(margin=4)] floating_layout = libqtile.resources.default_config.floating_layout keys = [] mouse = [] screens = [] follow_mouse_focus = False monadtallmargins_config = pytest.mark.parametrize( "manager", [MonadTallMarginsConfig], indirect=True ) class MonadTallStackedConfig(Config): auto_fullscreen = True groups = [libqtile.config.Group("a")] layouts = [layout.MonadTall(auto_maximize=True)] floating_layout = libqtile.resources.default_config.floating_layout keys = [] mouse = [] screens = [] follow_mouse_focus = False monadtallstacked_config = pytest.mark.parametrize( "manager", [MonadTallStackedConfig], indirect=True ) class MonadWideConfig(Config): auto_fullscreen = True groups = [libqtile.config.Group("a")] layouts = [layout.MonadWide()] floating_layout = libqtile.resources.default_config.floating_layout keys = [] mouse = [] screens = [] follow_mouse_focus = False monadwide_config = pytest.mark.parametrize("manager", [MonadWideConfig], indirect=True) class MonadWideNewClientPositionTopConfig(Config): auto_fullscreen = True groups = [libqtile.config.Group("a")] layouts = [layout.MonadWide(new_client_position="top")] floating_layout = libqtile.resources.default_config.floating_layout keys = [] mouse = [] screens = [] follow_mouse_focus = False class MonadWideMarginsConfig(Config): auto_fullscreen = True groups = [libqtile.config.Group("a")] layouts = [layout.MonadWide(margin=4)] floating_layout = libqtile.resources.default_config.floating_layout keys = [] mouse = [] screens = [] follow_mouse_focus = False @monadtall_config def test_tall_add_clients(manager): manager.test_window("one") manager.test_window("two") assert manager.c.layout.info()["main"] == "one" assert manager.c.layout.info()["secondary"] == ["two"] assert_focused(manager, "two") manager.test_window("three") assert manager.c.layout.info()["main"] == "one" assert manager.c.layout.info()["secondary"] == ["two", "three"] assert_focused(manager, "three") manager.c.layout.previous() assert_focused(manager, "two") manager.test_window("four") assert manager.c.layout.info()["main"] == "one" assert manager.c.layout.info()["secondary"] == ["two", "four", "three"] assert_focused(manager, "four") @monadtallncpbeforecurrent_config def test_tall_add_clients_before_current(manager): """Test add client with new_client_position = before_current.""" manager.test_window("one") manager.test_window("two") manager.test_window("three") assert manager.c.layout.info()["main"] == "three" assert manager.c.layout.info()["secondary"] == ["two", "one"] manager.c.layout.next() assert_focused(manager, "two") manager.test_window("four") assert manager.c.layout.info()["main"] == "three" assert manager.c.layout.info()["secondary"] == ["four", "two", "one"] assert_focused(manager, "four") @monadtallncpaftercurrent_config def test_tall_add_clients_after_current(manager): manager.test_window("one") manager.test_window("two") manager.test_window("three") manager.c.layout.previous() assert_focused(manager, "two") manager.test_window("four") assert manager.c.layout.info()["main"] == "one" assert manager.c.layout.info()["secondary"] == ["two", "four", "three"] assert_focused(manager, "four") @pytest.mark.parametrize("manager", [MonadTallNewCLientPositionBottomConfig], indirect=True) def test_tall_add_clients_at_bottom(manager): manager.test_window("one") manager.test_window("two") manager.test_window("three") manager.c.layout.previous() assert_focused(manager, "two") manager.test_window("four") assert manager.c.layout.info()["main"] == "one" assert manager.c.layout.info()["secondary"] == ["two", "three", "four"] @monadwide_config def test_wide_add_clients(manager): manager.test_window("one") manager.test_window("two") assert manager.c.layout.info()["main"] == "one" assert manager.c.layout.info()["secondary"] == ["two"] assert_focused(manager, "two") manager.test_window("three") assert manager.c.layout.info()["main"] == "one" assert manager.c.layout.info()["secondary"] == ["two", "three"] assert_focused(manager, "three") manager.c.layout.previous() assert_focused(manager, "two") manager.test_window("four") assert manager.c.layout.info()["main"] == "one" assert manager.c.layout.info()["secondary"] == ["two", "four", "three"] assert_focused(manager, "four") @pytest.mark.parametrize("manager", [MonadWideNewClientPositionTopConfig], indirect=True) def test_wide_add_clients_new_client_postion_top(manager): manager.test_window("one") manager.test_window("two") assert manager.c.layout.info()["main"] == "two" assert manager.c.layout.info()["secondary"] == ["one"] assert_focused(manager, "two") manager.test_window("three") assert manager.c.layout.info()["main"] == "three" assert manager.c.layout.info()["secondary"] == ["two", "one"] assert_focused(manager, "three") manager.c.layout.next() assert_focused(manager, "two") manager.test_window("four") assert manager.c.layout.info()["main"] == "four" assert manager.c.layout.info()["secondary"] == ["three", "two", "one"] assert_focused(manager, "four") @monadtallmargins_config def test_tall_margins(manager): manager.test_window("one") assert_dimensions(manager, 4, 4, 788, 588) manager.test_window("two") assert_focused(manager, "two") assert_dimensions(manager, 404, 4, 388, 588) manager.c.layout.previous() assert_focused(manager, "one") assert_dimensions(manager, 4, 4, 392, 588) @pytest.mark.parametrize("manager", [MonadWideMarginsConfig], indirect=True) def test_wide_margins(manager): manager.test_window("one") assert_dimensions(manager, 4, 4, 788, 588) manager.test_window("two") assert_focused(manager, "two") assert_dimensions(manager, 4, 304, 788, 288) manager.c.layout.previous() assert_focused(manager, "one") assert_dimensions(manager, 4, 4, 788, 292) @monadtall_config def test_tall_growmain_solosecondary(manager): manager.test_window("one") assert_dimensions(manager, 0, 0, 796, 596) manager.test_window("two") manager.c.layout.previous() assert_focused(manager, "one") assert_dimensions(manager, 0, 0, 396, 596) manager.c.layout.grow() # Grows 5% of 800 = 40 pixels assert_dimensions(manager, 0, 0, 436, 596) manager.c.layout.shrink() assert_dimensions(manager, 0, 0, 396, 596) # Max width is 75% of 800 = 600 pixels for _ in range(10): manager.c.layout.grow() assert_dimensions(manager, 0, 0, 596, 596) # Min width is 25% of 800 = 200 pixels for _ in range(10): manager.c.layout.shrink() assert_dimensions(manager, 0, 0, 196, 596) @monadwide_config def test_wide_growmain_solosecondary(manager): manager.test_window("one") assert_dimensions(manager, 0, 0, 796, 596) manager.test_window("two") manager.c.layout.previous() assert_focused(manager, "one") assert_dimensions(manager, 0, 0, 796, 296) manager.c.layout.grow() # Grows 5% of 800 = 30 pixels assert_dimensions(manager, 0, 0, 796, 326) manager.c.layout.shrink() assert_dimensions(manager, 0, 0, 796, 296) # Max width is 75% of 600 = 450 pixels for _ in range(10): manager.c.layout.grow() assert_dimensions(manager, 0, 0, 796, 446) # Min width is 25% of 600 = 150 pixels for _ in range(10): manager.c.layout.shrink() assert_dimensions(manager, 0, 0, 796, 146) @monadtall_config def test_tall_growmain_multiplesecondary(manager): manager.test_window("one") assert_dimensions(manager, 0, 0, 796, 596) manager.test_window("two") manager.test_window("three") manager.c.layout.previous() manager.c.layout.previous() assert_focused(manager, "one") assert_dimensions(manager, 0, 0, 396, 596) manager.c.layout.grow() # Grows 5% of 800 = 40 pixels assert_dimensions(manager, 0, 0, 436, 596) manager.c.layout.shrink() assert_dimensions(manager, 0, 0, 396, 596) # Max width is 75% of 800 = 600 pixels for _ in range(10): manager.c.layout.grow() assert_dimensions(manager, 0, 0, 596, 596) # Min width is 25% of 800 = 200 pixels for _ in range(10): manager.c.layout.shrink() assert_dimensions(manager, 0, 0, 196, 596) @monadwide_config def test_wide_growmain_multiplesecondary(manager): manager.test_window("one") assert_dimensions(manager, 0, 0, 796, 596) manager.test_window("two") manager.test_window("three") manager.c.layout.previous() manager.c.layout.previous() assert_focused(manager, "one") assert_dimensions(manager, 0, 0, 796, 296) manager.c.layout.grow() # Grows 5% of 600 = 30 pixels assert_dimensions(manager, 0, 0, 796, 326) manager.c.layout.shrink() assert_dimensions(manager, 0, 0, 796, 296) # Max width is 75% of 600 = 450 pixels for _ in range(10): manager.c.layout.grow() assert_dimensions(manager, 0, 0, 796, 446) # Min width is 25% of 600 = 150 pixels for _ in range(10): manager.c.layout.shrink() assert_dimensions(manager, 0, 0, 796, 146) @monadtall_config def test_tall_growsecondary_solosecondary(manager): manager.test_window("one") assert_dimensions(manager, 0, 0, 796, 596) manager.test_window("two") assert_focused(manager, "two") assert_dimensions(manager, 400, 0, 396, 596) manager.c.layout.grow() # Grows 5% of 800 = 40 pixels assert_dimensions(manager, 360, 0, 436, 596) manager.c.layout.shrink() assert_dimensions(manager, 400, 0, 396, 596) # Max width is 75% of 800 = 600 pixels for _ in range(10): manager.c.layout.grow() assert_dimensions(manager, 200, 0, 596, 596) # Min width is 25% of 800 = 200 pixels for _ in range(10): manager.c.layout.shrink() assert_dimensions(manager, 600, 0, 196, 596) @monadwide_config def test_wide_growsecondary_solosecondary(manager): manager.test_window("one") assert_dimensions(manager, 0, 0, 796, 596) manager.test_window("two") assert_focused(manager, "two") assert_dimensions(manager, 0, 300, 796, 296) manager.c.layout.grow() # Grows 5% of 600 = 30 pixels assert_dimensions(manager, 0, 270, 796, 326) manager.c.layout.shrink() assert_dimensions(manager, 0, 300, 796, 296) # Max width is 75% of 600 = 450 pixels for _ in range(10): manager.c.layout.grow() assert_dimensions(manager, 0, 150, 796, 446) # Min width is 25% of 600 = 150 pixels for _ in range(10): manager.c.layout.shrink() assert_dimensions(manager, 0, 450, 796, 146) @monadtall_config def test_tall_growsecondary_multiplesecondary(manager): manager.test_window("one") assert_dimensions(manager, 0, 0, 796, 596) manager.test_window("two") manager.test_window("three") manager.c.layout.previous() assert_focused(manager, "two") assert_dimensions(manager, 400, 0, 396, 296) # Grow 20 pixels manager.c.layout.grow() assert_dimensions(manager, 400, 0, 396, 316) manager.c.layout.shrink() assert_dimensions(manager, 400, 0, 396, 296) # Min height of other is 85 pixels, leaving 515 for _ in range(20): manager.c.layout.grow() assert_dimensions(manager, 400, 0, 396, 511) # Min height of manager is 85 pixels for _ in range(40): manager.c.layout.shrink() assert_dimensions(manager, 400, 0, 396, 85) @monadwide_config def test_wide_growsecondary_multiplesecondary(manager): manager.test_window("one") assert_dimensions(manager, 0, 0, 796, 596) manager.test_window("two") manager.test_window("three") manager.c.layout.previous() assert_focused(manager, "two") assert_dimensions(manager, 0, 300, 396, 296) # Grow 20 pixels manager.c.layout.grow() assert_dimensions(manager, 0, 300, 416, 296) manager.c.layout.shrink() assert_dimensions(manager, 0, 300, 396, 296) # Min width of other is 85 pixels, leaving 715 for _ in range(20): manager.c.layout.grow() assert_dimensions(manager, 0, 300, 710, 296) # TODO why not 711 ? # Min width of manager is 85 pixels for _ in range(40): manager.c.layout.shrink() assert_dimensions(manager, 0, 300, 85, 296) @monadtall_config def test_tall_flip(manager): manager.test_window("one") manager.test_window("two") manager.test_window("three") # Check all the dimensions manager.c.layout.next() assert_focused(manager, "one") assert_dimensions(manager, 0, 0, 396, 596) manager.c.layout.next() assert_focused(manager, "two") assert_dimensions(manager, 400, 0, 396, 296) manager.c.layout.next() assert_focused(manager, "three") assert_dimensions(manager, 400, 300, 396, 296) # Now flip it and do it again manager.c.layout.flip() manager.c.layout.next() assert_focused(manager, "one") assert_dimensions(manager, 400, 0, 396, 596) manager.c.layout.next() assert_focused(manager, "two") assert_dimensions(manager, 0, 0, 396, 296) manager.c.layout.next() assert_focused(manager, "three") assert_dimensions(manager, 0, 300, 396, 296) @monadwide_config def test_wide_flip(manager): manager.test_window("one") manager.test_window("two") manager.test_window("three") # Check all the dimensions manager.c.layout.next() assert_focused(manager, "one") assert_dimensions(manager, 0, 0, 796, 296) manager.c.layout.next() assert_focused(manager, "two") assert_dimensions(manager, 0, 300, 396, 296) manager.c.layout.next() assert_focused(manager, "three") assert_dimensions(manager, 400, 300, 396, 296) # Now flip it and do it again manager.c.layout.flip() manager.c.layout.next() assert_focused(manager, "one") assert_dimensions(manager, 0, 300, 796, 296) manager.c.layout.next() assert_focused(manager, "two") assert_dimensions(manager, 0, 0, 396, 296) manager.c.layout.next() assert_focused(manager, "three") assert_dimensions(manager, 400, 0, 396, 296) @monadtall_config def test_tall_set_and_reset(manager): manager.test_window("one") assert_dimensions(manager, 0, 0, 796, 596) manager.test_window("two") assert_focused(manager, "two") assert_dimensions(manager, 400, 0, 396, 596) manager.c.layout.set_ratio(0.75) assert_focused(manager, "two") assert_dimensions(manager, 600, 0, 196, 596) manager.c.layout.set_ratio(0.25) assert_focused(manager, "two") assert_dimensions(manager, 200, 0, 596, 596) manager.c.layout.reset() assert_focused(manager, "two") assert_dimensions(manager, 400, 0, 396, 596) @monadtallstacked_config def test_tall_stacked_add_two_clients(manager): manager.test_window("one") assert_dimensions(manager, 0, 0, 796, 596) manager.test_window("two") assert_focused(manager, "two") assert_dimensions(manager, 400, 0, 396, 596) manager.test_window("three") assert_focused(manager, "three") assert_dimensions(manager, 400, 85, 396, 511) manager.c.layout.next() assert_focused(manager, "one") assert_dimensions(manager, 0, 0, 396, 596) @monadtallstacked_config def test_tall_stacked_toggle_auto_maximize(manager): # Initial setting: auto_maximize on manager.test_window("one") assert_dimensions(manager, 0, 0, 796, 596) manager.test_window("two") assert_focused(manager, "two") assert_dimensions(manager, 400, 0, 396, 596) manager.test_window("three") assert_focused(manager, "three") assert_dimensions(manager, 400, 85, 396, 511) manager.c.layout.next() assert_focused(manager, "one") assert_dimensions(manager, 0, 0, 396, 596) # Turn off auto_maximize manager.c.layout.toggle_auto_maximize() assert_focused(manager, "one") assert_dimensions(manager, 0, 0, 396, 596) manager.c.layout.next() assert_focused(manager, "two") assert_dimensions(manager, 400, 0, 396, 296) manager.c.layout.next() assert_focused(manager, "three") assert_dimensions(manager, 400, 300, 396, 296) # Turn auto_maximize back on manager.c.layout.toggle_auto_maximize() manager.c.layout.next() assert_focused(manager, "one") assert_dimensions(manager, 0, 0, 396, 596) manager.c.layout.next() assert_focused(manager, "two") assert_dimensions(manager, 400, 0, 396, 511) manager.c.layout.next() assert_focused(manager, "three") assert_dimensions(manager, 400, 85, 396, 511) @monadtallstacked_config def test_tall_stacked_window_kill(manager): @Retry(ignore_exceptions=(AssertionError)) def assert_window_count(num): assert len(manager.c.windows()) == num manager.test_window("one") assert_focused(manager, "one") manager.test_window("two") assert_focused(manager, "two") manager.test_window("three") assert_focused(manager, "three") manager.c.layout.previous() assert_focused(manager, "two") assert_dimensions(manager, 400, 0, 396, 511) manager.c.window.kill() assert_window_count(2) assert_focused(manager, "one") assert_dimensions(manager, 0, 0, 396, 596) manager.c.layout.next() assert_focused(manager, "three") assert_dimensions(manager, 400, 0, 396, 596) @monadwide_config def test_wide_set_and_reset(manager): manager.test_window("one") assert_dimensions(manager, 0, 0, 796, 596) manager.test_window("two") assert_focused(manager, "two") assert_dimensions(manager, 0, 300, 796, 296) manager.c.layout.set_ratio(0.75) assert_focused(manager, "two") assert_dimensions(manager, 0, 450, 796, 146) manager.c.layout.set_ratio(0.25) assert_focused(manager, "two") assert_dimensions(manager, 0, 150, 796, 446) manager.c.layout.reset() assert_focused(manager, "two") assert_dimensions(manager, 0, 300, 796, 296) @monadtall_config def test_tall_shuffle(manager): manager.test_window("one") manager.test_window("two") manager.test_window("three") manager.test_window("four") assert manager.c.layout.info()["main"] == "one" assert manager.c.layout.info()["secondary"] == ["two", "three", "four"] manager.c.layout.shuffle_up() assert manager.c.layout.info()["main"] == "one" assert manager.c.layout.info()["secondary"] == ["two", "four", "three"] manager.c.layout.shuffle_up() assert manager.c.layout.info()["main"] == "one" assert manager.c.layout.info()["secondary"] == ["four", "two", "three"] manager.c.layout.shuffle_up() assert manager.c.layout.info()["main"] == "four" assert manager.c.layout.info()["secondary"] == ["one", "two", "three"] @monadwide_config def test_wide_shuffle(manager): manager.test_window("one") manager.test_window("two") manager.test_window("three") manager.test_window("four") assert manager.c.layout.info()["main"] == "one" assert manager.c.layout.info()["secondary"] == ["two", "three", "four"] manager.c.layout.shuffle_up() assert manager.c.layout.info()["main"] == "one" assert manager.c.layout.info()["secondary"] == ["two", "four", "three"] manager.c.layout.shuffle_up() assert manager.c.layout.info()["main"] == "one" assert manager.c.layout.info()["secondary"] == ["four", "two", "three"] manager.c.layout.shuffle_up() assert manager.c.layout.info()["main"] == "four" assert manager.c.layout.info()["secondary"] == ["one", "two", "three"] @monadtall_config def test_tall_swap(manager): manager.test_window("one") manager.test_window("two") manager.test_window("three") manager.test_window("focused") assert manager.c.layout.info()["main"] == "one" assert manager.c.layout.info()["secondary"] == ["two", "three", "focused"] # Swap a secondary left, left aligned manager.c.layout.swap_left() assert manager.c.layout.info()["main"] == "focused" assert manager.c.layout.info()["secondary"] == ["two", "three", "one"] # Swap a main right, left aligned manager.c.layout.swap_right() assert manager.c.layout.info()["main"] == "two" assert manager.c.layout.info()["secondary"] == ["focused", "three", "one"] # flip over manager.c.layout.flip() manager.c.layout.shuffle_down() assert manager.c.layout.info()["main"] == "two" assert manager.c.layout.info()["secondary"] == ["three", "focused", "one"] # Swap secondary right, right aligned manager.c.layout.swap_right() assert manager.c.layout.info()["main"] == "focused" assert manager.c.layout.info()["secondary"] == ["three", "two", "one"] # Swap main left, right aligned manager.c.layout.swap_left() assert manager.c.layout.info()["main"] == "three" assert manager.c.layout.info()["secondary"] == ["focused", "two", "one"] # Do swap main manager.c.layout.swap_main() assert manager.c.layout.info()["main"] == "focused" assert manager.c.layout.info()["secondary"] == ["three", "two", "one"] # Since the focused window is already to the right this swap shouldn't # change the position of the windows # The swap function will try to get all windows to the right of the # focused window, which will result in a empty list that could cause # an error if not handled # Swap againts right edge manager.c.layout.swap_right() assert manager.c.layout.info()["main"] == "focused" assert manager.c.layout.info()["secondary"] == ["three", "two", "one"] # Same as above but for the swap_left function # Swap againts left edge manager.c.layout.swap_left() manager.c.layout.swap_left() assert manager.c.layout.info()["main"] == "three" assert manager.c.layout.info()["secondary"] == ["focused", "two", "one"] @monadwide_config def test_wide_swap(manager): manager.test_window("one") manager.test_window("two") manager.test_window("three") manager.test_window("focused") assert manager.c.layout.info()["main"] == "one" assert manager.c.layout.info()["secondary"] == ["two", "three", "focused"] # Swap a secondary up manager.c.layout.swap_right() # equivalent to swap_down assert manager.c.layout.info()["main"] == "focused" assert manager.c.layout.info()["secondary"] == ["two", "three", "one"] # Swap a main down manager.c.layout.swap_left() # equivalent to swap up assert manager.c.layout.info()["main"] == "two" assert manager.c.layout.info()["secondary"] == ["focused", "three", "one"] # flip over manager.c.layout.flip() manager.c.layout.shuffle_down() assert manager.c.layout.info()["main"] == "two" assert manager.c.layout.info()["secondary"] == ["three", "focused", "one"] # Swap secondary down manager.c.layout.swap_left() assert manager.c.layout.info()["main"] == "focused" assert manager.c.layout.info()["secondary"] == ["three", "two", "one"] # Swap main up manager.c.layout.swap_right() assert manager.c.layout.info()["main"] == "three" assert manager.c.layout.info()["secondary"] == ["focused", "two", "one"] # Do swap main manager.c.layout.swap_main() assert manager.c.layout.info()["main"] == "focused" assert manager.c.layout.info()["secondary"] == ["three", "two", "one"] # Since the focused window is already to the left this swap shouldn't # change the position of the windows # The swap function will try to get all windows to the left of the # focused window, which will result in a empty list that could cause # an error if not handled # Swap againts left edge manager.c.layout.swap_left() assert manager.c.layout.info()["main"] == "focused" assert manager.c.layout.info()["secondary"] == ["three", "two", "one"] # Same as above but for the swap_right function # Swap againts right edge manager.c.layout.swap_right() manager.c.layout.swap_right() assert manager.c.layout.info()["main"] == "three" assert manager.c.layout.info()["secondary"] == ["focused", "two", "one"] @monadtall_config def test_tall_window_focus_cycle(manager): # setup 3 tiled and two floating clients manager.test_window("one") manager.test_window("two") manager.test_window("float1") manager.c.window.toggle_floating() manager.test_window("float2") manager.c.window.toggle_floating() manager.test_window("three") # test preconditions assert manager.c.layout.info()["clients"] == ["one", "two", "three"] # last added window has focus assert_focused(manager, "three") # starting from the last tiled client, we first cycle through floating ones, # and afterwards through the tiled assert_focus_path(manager, "float1", "float2", "one", "two", "three") @monadwide_config def test_wide_window_focus_cycle(manager): # setup 3 tiled and two floating clients manager.test_window("one") manager.test_window("two") manager.test_window("float1") manager.c.window.toggle_floating() manager.test_window("float2") manager.c.window.toggle_floating() manager.test_window("three") # test preconditions assert manager.c.layout.info()["clients"] == ["one", "two", "three"] # last added window has focus assert_focused(manager, "three") # assert window focus cycle, according to order in layout assert_focus_path(manager, "float1", "float2", "one", "two", "three") @monadtall_config def test_tall_window_directional_focus(manager): # setup 3 tiled and two floating clients manager.test_window("one") manager.test_window("two") manager.test_window("float1") manager.c.window.toggle_floating() manager.test_window("float2") manager.c.window.toggle_floating() manager.test_window("three") # test preconditions assert manager.c.layout.info()["clients"] == ["one", "two", "three"] # last added window has focus assert_focused(manager, "three") # starting from the last tiled client, test that left/right focus changes go # to the closest window in that direction (no cycling), and up/down cycles # focus manager.c.layout.left() assert_focused(manager, "one") manager.c.layout.left() assert_focused(manager, "one") manager.c.layout.right() assert_focused(manager, "two") manager.c.layout.right() assert_focused(manager, "two") manager.c.layout.down() assert_focused(manager, "three") manager.c.layout.down() assert_focused(manager, "one") manager.c.layout.down() assert_focused(manager, "two") manager.c.layout.up() assert_focused(manager, "one") manager.c.layout.up() assert_focused(manager, "three") manager.c.layout.up() assert_focused(manager, "two") @monadwide_config def test_wide_window_directional_focus(manager): # setup 3 tiled and two floating clients manager.test_window("one") manager.test_window("two") manager.test_window("float1") manager.c.window.toggle_floating() manager.test_window("float2") manager.c.window.toggle_floating() manager.test_window("three") # test preconditions assert manager.c.layout.info()["clients"] == ["one", "two", "three"] # last added window has focus assert_focused(manager, "three") # starting from the last tiled client, test that up/down focus changes go to # the closest window in that direction (no cycling), and left/right cycles # focus manager.c.layout.up() assert_focused(manager, "one") manager.c.layout.up() assert_focused(manager, "one") manager.c.layout.down() assert_focused(manager, "two") manager.c.layout.down() assert_focused(manager, "two") manager.c.layout.left() assert_focused(manager, "one") manager.c.layout.left() assert_focused(manager, "three") manager.c.layout.left() assert_focused(manager, "two") manager.c.layout.right() assert_focused(manager, "three") manager.c.layout.right() assert_focused(manager, "one") manager.c.layout.right() assert_focused(manager, "two") # MonadThreeCol class MonadThreeColConfig(Config): auto_fullscreen = True groups = [libqtile.config.Group("a")] layouts = [layout.MonadThreeCol()] floating_layout = libqtile.resources.default_config.floating_layout keys = [] mouse = [] screens = [] follow_mouse_focus = False monadthreecol_config = pytest.mark.parametrize("manager", [MonadThreeColConfig], indirect=True) @monadthreecol_config def test_three_col_add_clients(manager): manager.test_window("one") assert manager.c.layout.info()["main"] == "one" assert manager.c.layout.info()["secondary"] == dict(left=[], right=[]) manager.test_window("two") assert manager.c.layout.info()["main"] == "two" assert manager.c.layout.info()["secondary"] == dict(left=["one"], right=[]) assert_focused(manager, "two") manager.test_window("three") assert manager.c.layout.info()["main"] == "three" assert manager.c.layout.info()["secondary"] == dict(left=["two"], right=["one"]) assert_focused(manager, "three") manager.test_window("four") assert manager.c.layout.info()["main"] == "four" assert manager.c.layout.info()["secondary"] == dict(left=["three", "two"], right=["one"]) assert_focused(manager, "four") manager.test_window("five") assert manager.c.layout.info()["main"] == "five" assert manager.c.layout.info()["secondary"] == dict( left=["four", "three"], right=["two", "one"] ) assert_focused(manager, "five") manager.c.layout.next() assert_focused(manager, "four") manager.c.layout.next() assert_focused(manager, "three") manager.c.layout.next() assert_focused(manager, "two") manager.c.layout.next() assert_focused(manager, "one") @monadthreecol_config def test_three_col_shuffle(manager): manager.test_window("one") manager.test_window("two") manager.test_window("three") manager.test_window("four") manager.test_window("five") manager.c.layout.shuffle_right() assert manager.c.layout.info()["main"] == "two" assert manager.c.layout.info()["secondary"] == dict( left=["four", "three"], right=["five", "one"] ) assert_focused(manager, "five") manager.c.layout.shuffle_down() assert manager.c.layout.info()["main"] == "two" assert manager.c.layout.info()["secondary"] == dict( left=["four", "three"], right=["one", "five"] ) assert_focused(manager, "five") manager.c.layout.shuffle_left() assert manager.c.layout.info()["main"] == "five" assert manager.c.layout.info()["secondary"] == dict( left=["four", "three"], right=["one", "two"] ) assert_focused(manager, "five") manager.c.layout.shuffle_left() assert manager.c.layout.info()["main"] == "four" assert manager.c.layout.info()["secondary"] == dict( left=["five", "three"], right=["one", "two"] ) assert_focused(manager, "five") manager.c.layout.shuffle_down() assert manager.c.layout.info()["main"] == "four" assert manager.c.layout.info()["secondary"] == dict( left=["three", "five"], right=["one", "two"] ) assert_focused(manager, "five") manager.c.layout.shuffle_up() assert manager.c.layout.info()["main"] == "four" assert manager.c.layout.info()["secondary"] == dict( left=["five", "three"], right=["one", "two"] ) assert_focused(manager, "five") manager.c.layout.shuffle_right() assert manager.c.layout.info()["main"] == "five" assert manager.c.layout.info()["secondary"] == dict( left=["four", "three"], right=["one", "two"] ) assert_focused(manager, "five") @monadthreecol_config def test_three_col_swap_main(manager): manager.test_window("one") manager.test_window("two") manager.test_window("three") manager.test_window("four") manager.test_window("five") manager.c.layout.next() manager.c.layout.swap_main() assert manager.c.layout.info()["main"] == "four" assert manager.c.layout.info()["secondary"] == dict( left=["five", "three"], right=["two", "one"] ) assert_focused(manager, "four") manager.c.layout.next() manager.c.layout.swap_main() assert manager.c.layout.info()["main"] == "five" assert manager.c.layout.info()["secondary"] == dict( left=["four", "three"], right=["two", "one"] ) assert_focused(manager, "five") qtile-0.31.0/test/layouts/test_bsp.py0000664000175000017500000000723014762660347017514 0ustar epsilonepsilon# # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest import libqtile.config from libqtile import layout from libqtile.confreader import Config from test.layouts.layout_utils import assert_focus_path, assert_focused class BspConfig(Config): auto_fullscreen = True groups = [ libqtile.config.Group("a"), libqtile.config.Group("b"), libqtile.config.Group("c"), libqtile.config.Group("d"), ] layouts = [layout.Bsp(), layout.Bsp(margin_on_single=10), layout.Bsp(wrap_clients=True)] floating_layout = libqtile.resources.default_config.floating_layout keys = [] mouse = [] screens = [] follow_mouse_focus = False bsp_config = pytest.mark.parametrize("manager", [BspConfig], indirect=True) # This currently only tests the window focus cycle @bsp_config def test_bsp_window_focus_cycle(manager): # setup 3 tiled and two floating clients manager.test_window("one") manager.test_window("two") manager.test_window("float1") manager.c.window.toggle_floating() manager.test_window("float2") manager.c.window.toggle_floating() manager.test_window("three") # test preconditions, columns adds clients at pos of current, in two stacks assert manager.c.layout.info()["clients"] == ["one", "three", "two"] # last added window has focus assert_focused(manager, "three") # assert window focus cycle, according to order in layout assert_focus_path(manager, "two", "float1", "float2", "one", "three") @bsp_config def test_bsp_margin_on_single(manager): manager.test_window("one") info = manager.c.window.info() assert info["x"] == 0 assert info["y"] == 0 manager.c.next_layout() info = manager.c.window.info() assert info["x"] == 10 assert info["y"] == 10 manager.test_window("two") # No longer single window so margin reverts to "margin" which is 0 info = manager.c.window.info() assert info["x"] == 0 @bsp_config def test_bsp_wrap_clients(manager): manager.test_window("one") manager.test_window("two") # Default has no wrapping assert_focused(manager, "two") manager.c.layout.next() assert_focused(manager, "two") manager.c.layout.previous() assert_focused(manager, "one") manager.c.layout.previous() assert_focused(manager, "one") # Switch to layout with wrapping enabled manager.c.next_layout() manager.c.next_layout() assert_focused(manager, "one") manager.c.layout.next() assert_focused(manager, "two") # Layout should wrap here manager.c.layout.next() assert_focused(manager, "one") # and here manager.c.layout.previous() assert_focused(manager, "two") qtile-0.31.0/test/layouts/test_slice.py0000664000175000017500000001413314762660347020027 0ustar epsilonepsilon# Copyright (c) 2011 Florian Mounier # Copyright (c) 2012, 2014-2015 Tycho Andersen # Copyright (c) 2013 Mattias Svala # Copyright (c) 2013 Craig Barnes # Copyright (c) 2014 ramnes # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Adi Sieker # Copyright (c) 2014 Chris Wesseling # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest import libqtile.config from libqtile import layout from libqtile.config import Match from libqtile.confreader import Config from test.layouts.layout_utils import assert_dimensions, assert_focus_path, assert_focused class SliceConfig(Config): auto_fullscreen = True groups = [ libqtile.config.Group("a"), ] layouts = [ layout.Slice( side="left", width=200, match=Match(title="slice"), fallback=layout.Stack(num_stacks=1, border_width=0), ), layout.Slice( side="right", width=200, match=Match(title="slice"), fallback=layout.Stack(num_stacks=1, border_width=0), ), layout.Slice( side="top", width=200, match=Match(title="slice"), fallback=layout.Stack(num_stacks=1, border_width=0), ), layout.Slice( side="bottom", width=200, match=Match(title="slice"), fallback=layout.Stack(num_stacks=1, border_width=0), ), ] floating_layout = libqtile.resources.default_config.floating_layout keys = [] mouse = [] screens = [] follow_mouse_focus = False slice_config = pytest.mark.parametrize("manager", [SliceConfig], indirect=True) @slice_config def test_no_slice(manager): manager.test_window("one") assert_dimensions(manager, 200, 0, 600, 600) manager.test_window("two") assert_dimensions(manager, 200, 0, 600, 600) @slice_config def test_slice_first(manager): manager.test_window("slice") assert_dimensions(manager, 0, 0, 200, 600) manager.test_window("two") assert_dimensions(manager, 200, 0, 600, 600) @slice_config def test_slice_last(manager): manager.test_window("one") assert_dimensions(manager, 200, 0, 600, 600) manager.test_window("slice") assert_dimensions(manager, 0, 0, 200, 600) @slice_config def test_slice_focus(manager): manager.test_window("one") assert_focused(manager, "one") two = manager.test_window("two") assert_focused(manager, "two") slice = manager.test_window("slice") assert_focused(manager, "slice") assert_focus_path(manager, "slice") manager.test_window("three") assert_focus_path(manager, "two", "one", "slice", "three") manager.kill_window(two) assert_focus_path(manager, "one", "slice", "three") manager.kill_window(slice) assert_focus_path(manager, "one", "three") slice = manager.test_window("slice") assert_focus_path(manager, "three", "one", "slice") @slice_config def test_all_slices(manager): manager.test_window("slice") # left assert_dimensions(manager, 0, 0, 200, 600) manager.c.next_layout() # right assert_dimensions(manager, 600, 0, 200, 600) manager.c.next_layout() # top assert_dimensions(manager, 0, 0, 800, 200) manager.c.next_layout() # bottom assert_dimensions(manager, 0, 400, 800, 200) manager.c.next_layout() # left again manager.test_window("one") assert_dimensions(manager, 200, 0, 600, 600) manager.c.next_layout() # right assert_dimensions(manager, 0, 0, 600, 600) manager.c.next_layout() # top assert_dimensions(manager, 0, 200, 800, 400) manager.c.next_layout() # bottom assert_dimensions(manager, 0, 0, 800, 400) @slice_config def test_command_propagation(manager): manager.test_window("slice") manager.test_window("one") manager.test_window("two") info = manager.c.layout.info() assert info["name"] == "slice" org_height = manager.c.window.info()["height"] manager.c.layout.toggle_split() assert manager.c.window.info()["height"] != org_height @slice_config def test_command_propagation_direct_call(manager): manager.test_window("slice") manager.test_window("one") manager.test_window("two") info = manager.c.layout.info() assert info["name"] == "slice" org_height = manager.c.window.info()["height"] manager.c.layout.eval("self.toggle_split()") assert manager.c.window.info()["height"] != org_height @slice_config def test_move_to_slice(manager): manager.test_window("one") manager.test_window("two") info = manager.c.layout.info() # Neither of these windows should be in the slice assert not info["single"]["window"] # Move current window to slice manager.c.layout.move_to_slice() info = manager.c.layout.info() assert info["single"]["window"] == "two" assert info["stack"]["clients"] == ["one"] assert_focused(manager, "two") # Switch focus to "one" and put it in slice manager.c.group.focus_back() assert_focused(manager, "one") manager.c.layout.move_to_slice() info = manager.c.layout.info() assert info["single"]["window"] == "one" assert info["stack"]["clients"] == ["two"] qtile-0.31.0/test/layouts/test_max.py0000664000175000017500000001666114762660347017525 0ustar epsilonepsilon# Copyright (c) 2011 Florian Mounier # Copyright (c) 2012, 2014-2015 Tycho Andersen # Copyright (c) 2013 Mattias Svala # Copyright (c) 2013 Craig Barnes # Copyright (c) 2014 ramnes # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Adi Sieker # Copyright (c) 2014 Chris Wesseling # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest import libqtile.config import libqtile.resources.default_config from libqtile import layout from libqtile.confreader import Config from test.layouts.layout_utils import assert_focus_path, assert_focused class MaxConfig(Config): auto_fullscreen = True groups = [ libqtile.config.Group("a"), libqtile.config.Group("b"), libqtile.config.Group("c"), libqtile.config.Group("d"), ] layouts = [ layout.Max(), layout.Max(margin=5), layout.Max(margin=5, border_width=5), ] floating_layout = libqtile.resources.default_config.floating_layout keys = [] mouse = [] screens = [] max_config = pytest.mark.parametrize("manager", [MaxConfig], indirect=True) class MaxLayeredConfig(Config): auto_fullscreen = True groups = [ libqtile.config.Group("a"), libqtile.config.Group("b"), libqtile.config.Group("c"), libqtile.config.Group("d"), ] layouts = [layout.Max(only_focused=False)] floating_layout = libqtile.layout.floating.Floating() keys = [] mouse = [] screens = [] maxlayered_config = pytest.mark.parametrize("manager", [MaxLayeredConfig], indirect=True) def assert_z_stack(manager, windows): if manager.backend.name != "x11": # TODO: Test wayland backend when proper Z-axis is implemented there return stack = manager.backend.get_all_windows() wins = [(w["name"], stack.index(w["id"])) for w in manager.c.windows()] wins.sort(key=lambda x: x[1]) assert [x[0] for x in wins] == windows @max_config def test_max_simple(manager): manager.test_window("one") assert manager.c.layout.info()["clients"] == ["one"] assert_z_stack(manager, ["one"]) manager.test_window("two") assert manager.c.layout.info()["clients"] == ["one", "two"] assert_z_stack(manager, ["one", "two"]) @maxlayered_config def test_max_layered(manager): manager.test_window("one") assert manager.c.layout.info()["clients"] == ["one"] assert_z_stack(manager, ["one"]) manager.test_window("two") assert manager.c.layout.info()["clients"] == ["one", "two"] assert_z_stack(manager, ["one", "two"]) @max_config def test_max_updown(manager): manager.test_window("one") manager.test_window("two") manager.test_window("three") assert manager.c.layout.info()["clients"] == ["one", "two", "three"] assert_z_stack(manager, ["one", "two", "three"]) manager.c.layout.up() assert manager.c.get_groups()["a"]["focus"] == "two" manager.c.layout.down() assert manager.c.get_groups()["a"]["focus"] == "three" assert_z_stack(manager, ["one", "two", "three"]) manager.c.layout.down() assert manager.c.get_groups()["a"]["focus"] == "one" assert_z_stack(manager, ["one", "two", "three"]) @maxlayered_config def test_layered_max_updown(manager): manager.test_window("one") manager.test_window("two") manager.test_window("three") assert manager.c.layout.info()["clients"] == ["one", "two", "three"] assert_z_stack(manager, ["one", "two", "three"]) manager.c.layout.up() assert manager.c.get_groups()["a"]["focus"] == "two" assert_z_stack(manager, ["one", "three", "two"]) manager.c.layout.up() assert manager.c.get_groups()["a"]["focus"] == "one" assert_z_stack(manager, ["three", "two", "one"]) manager.c.layout.down() assert manager.c.get_groups()["a"]["focus"] == "two" assert_z_stack(manager, ["three", "one", "two"]) manager.c.layout.down() assert manager.c.get_groups()["a"]["focus"] == "three" assert_z_stack(manager, ["one", "two", "three"]) @pytest.mark.parametrize("manager", [MaxConfig, MaxLayeredConfig], indirect=True) def test_max_remove(manager): manager.test_window("one") two = manager.test_window("two") assert manager.c.layout.info()["clients"] == ["one", "two"] assert_z_stack(manager, ["one", "two"]) manager.kill_window(two) assert manager.c.layout.info()["clients"] == ["one"] assert_z_stack(manager, ["one"]) @max_config def test_max_window_focus_cycle(manager): # setup 3 tiled and two floating clients manager.test_window("one") manager.test_window("two") manager.test_window("float1") manager.c.window.toggle_floating() manager.test_window("float2") manager.c.window.toggle_floating() manager.test_window("three") # test preconditions assert manager.c.layout.info()["clients"] == ["one", "two", "three"] # Floats are kept above in stacking order assert_z_stack(manager, ["one", "two", "three", "float1", "float2"]) # last added window has focus assert_focused(manager, "three") # assert window focus cycle, according to order in layout assert_focus_path(manager, "float1", "float2", "one", "two", "three") @maxlayered_config def test_layered_max_window_focus_cycle(manager): # setup 3 tiled and two floating clients manager.test_window("one") manager.test_window("two") manager.test_window("float1") manager.c.window.toggle_floating() manager.test_window("float2") manager.c.window.toggle_floating() manager.test_window("three") # test preconditions assert manager.c.layout.info()["clients"] == ["one", "two", "three"] # Floats kept above by default assert_z_stack(manager, ["one", "two", "three", "float1", "float2"]) # last added window has focus assert_focused(manager, "three") # assert window focus cycle, according to order in layout assert_focus_path(manager, "float1", "float2", "one", "two", "three") @max_config def test_max_window_margins_and_borders(manager): def parse_margin(margin): if isinstance(margin, int): return (margin,) * 4 return margin manager.test_window("one") screen = manager.c.group["a"].screen.info() for _layout in MaxConfig.layouts: window = manager.c.window.info() margin = parse_margin(_layout.margin) border = _layout.border_width assert screen["width"] == window["width"] + margin[0] + margin[2] + border * 2 assert screen["height"] == window["height"] + margin[1] + margin[3] + border * 2 manager.c.next_layout() qtile-0.31.0/test/layouts/test_spiral.py0000664000175000017500000002357414762660347020233 0ustar epsilonepsilon# Copyright (c) 2022 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest import libqtile.config from libqtile import layout from libqtile.confreader import Config from test.helpers import HEIGHT, WIDTH from test.layouts.layout_utils import assert_dimensions class SpiralConfig(Config): auto_fullscreen = True groups = [libqtile.config.Group("a")] layouts = [ layout.Spiral(ratio=0.5, new_client_position="bottom"), # default 'main_pane' is 'left' layout.Spiral(ratio=0.5, new_client_position="bottom", main_pane="top"), layout.Spiral(ratio=0.5, new_client_position="bottom", main_pane="right"), layout.Spiral(ratio=0.5, new_client_position="bottom", main_pane="bottom"), ] floating_layout = libqtile.resources.default_config.floating_layout keys = [] mouse = [] screens = [] follow_mouse_focus = False spiral_config = pytest.mark.parametrize("manager", [SpiralConfig], indirect=True) class AnticlockwiseConfig(SpiralConfig): layouts = [ layout.Spiral( ratio=0.5, new_client_position="bottom", clockwise=False ), # default 'main_pane' is 'left' layout.Spiral(ratio=0.5, new_client_position="bottom", main_pane="top", clockwise=False), layout.Spiral( ratio=0.5, new_client_position="bottom", main_pane="right", clockwise=False ), layout.Spiral( ratio=0.5, new_client_position="bottom", main_pane="bottom", clockwise=False ), ] anticlockwise_config = pytest.mark.parametrize("manager", [AnticlockwiseConfig], indirect=True) class SingleborderDisabledConfig(SpiralConfig): layouts = [layout.Spiral(ratio=0.5, border_width=2, border_on_single=False)] singleborder_disabled_config = pytest.mark.parametrize( "manager", [SingleborderDisabledConfig], indirect=True ) @spiral_config def test_spiral_left(manager): manager.test_window("one") assert_dimensions(manager, 0, 0, 798, 598) manager.test_window("two") assert_dimensions(manager, 400, 0, 398, 598) manager.test_window("three") assert_dimensions(manager, 400, 300, 398, 298) manager.test_window("four") assert_dimensions(manager, 400, 300, 198, 298) manager.test_window("five") assert_dimensions(manager, 400, 300, 198, 148) @spiral_config def test_spiral_top(manager): manager.c.next_layout() manager.test_window("one") assert_dimensions(manager, 0, 0, 798, 598) manager.test_window("two") assert_dimensions(manager, 0, 300, 798, 298) manager.test_window("three") assert_dimensions(manager, 0, 300, 398, 298) manager.test_window("four") assert_dimensions(manager, 0, 300, 398, 148) manager.test_window("five") assert_dimensions(manager, 200, 300, 198, 148) @spiral_config def test_spiral_right(manager): manager.c.next_layout() manager.c.next_layout() manager.test_window("one") assert_dimensions(manager, 0, 0, 798, 598) manager.test_window("two") assert_dimensions(manager, 0, 0, 398, 598) manager.test_window("three") assert_dimensions(manager, 0, 0, 398, 298) manager.test_window("four") assert_dimensions(manager, 200, 0, 198, 298) manager.test_window("five") assert_dimensions(manager, 200, 150, 198, 148) @spiral_config def test_spiral_bottom(manager): manager.c.next_layout() manager.c.next_layout() manager.c.next_layout() manager.test_window("one") assert_dimensions(manager, 0, 0, 798, 598) manager.test_window("two") assert_dimensions(manager, 0, 0, 798, 298) manager.test_window("three") assert_dimensions(manager, 400, 0, 398, 298) manager.test_window("four") assert_dimensions(manager, 400, 150, 398, 148) manager.test_window("five") assert_dimensions(manager, 400, 150, 198, 148) @anticlockwise_config def test_spiral_left_anticlockwise(manager): manager.test_window("one") assert_dimensions(manager, 0, 0, 798, 598) manager.test_window("two") assert_dimensions(manager, 400, 0, 398, 598) manager.test_window("three") assert_dimensions(manager, 400, 0, 398, 298) manager.test_window("four") assert_dimensions(manager, 400, 0, 198, 298) manager.test_window("five") assert_dimensions(manager, 400, 150, 198, 148) @anticlockwise_config def test_spiral_top_anticlockwise(manager): manager.c.next_layout() manager.test_window("one") assert_dimensions(manager, 0, 0, 798, 598) manager.test_window("two") assert_dimensions(manager, 0, 300, 798, 298) manager.test_window("three") assert_dimensions(manager, 400, 300, 398, 298) manager.test_window("four") assert_dimensions(manager, 400, 300, 398, 148) manager.test_window("five") assert_dimensions(manager, 400, 300, 198, 148) @anticlockwise_config def test_spiral_right_anticlockwise(manager): manager.c.next_layout() manager.c.next_layout() manager.test_window("one") assert_dimensions(manager, 0, 0, 798, 598) manager.test_window("two") assert_dimensions(manager, 0, 0, 398, 598) manager.test_window("three") assert_dimensions(manager, 0, 300, 398, 298) manager.test_window("four") assert_dimensions(manager, 200, 300, 198, 298) manager.test_window("five") assert_dimensions(manager, 200, 300, 198, 148) @anticlockwise_config def test_spiral_bottom_anticlockwise(manager): manager.c.next_layout() manager.c.next_layout() manager.c.next_layout() manager.test_window("one") assert_dimensions(manager, 0, 0, 798, 598) manager.test_window("two") assert_dimensions(manager, 0, 0, 798, 298) manager.test_window("three") assert_dimensions(manager, 0, 0, 398, 298) manager.test_window("four") assert_dimensions(manager, 0, 150, 398, 148) manager.test_window("five") assert_dimensions(manager, 200, 150, 198, 148) @spiral_config def test_spiral_shuffle_up(manager): manager.test_window("one") manager.test_window("two") manager.test_window("three") manager.test_window("four") assert manager.c.layout.info()["clients"] == ["one", "two", "three", "four"] manager.c.layout.shuffle_up() assert manager.c.layout.info()["clients"] == ["one", "two", "four", "three"] manager.c.layout.shuffle_up() assert manager.c.layout.info()["clients"] == ["one", "four", "two", "three"] @spiral_config def test_spiral_shuffle_down(manager): manager.test_window("one") manager.test_window("two") manager.test_window("three") manager.test_window("four") # wrap focus back to first window manager.c.layout.down() assert manager.c.layout.info()["clients"] == ["one", "two", "three", "four"] manager.c.layout.shuffle_down() assert manager.c.layout.info()["clients"] == ["two", "one", "three", "four"] manager.c.layout.shuffle_down() assert manager.c.layout.info()["clients"] == ["two", "three", "one", "four"] @spiral_config def test_spiral_shuffle_no_wrap_down(manager): # test that shuffling down doesn't wrap around manager.test_window("one") manager.test_window("two") manager.test_window("three") manager.c.layout.shuffle_down() assert manager.c.layout.info()["clients"] == ["one", "two", "three"] @spiral_config def test_spiral_shuffle_no_wrap_up(manager): # test that shuffling up doesn't wrap around manager.test_window("one") manager.test_window("two") manager.test_window("three") # wrap focus back to first window manager.c.layout.down() manager.c.layout.shuffle_up() assert manager.c.layout.info()["clients"] == ["one", "two", "three"] @singleborder_disabled_config def test_singleborder_disable(manager): manager.test_window("one") assert_dimensions(manager, 0, 0, WIDTH, HEIGHT) manager.test_window("two") assert_dimensions(manager, 0, 0, WIDTH / 2 - 4, HEIGHT - 4) @spiral_config def test_spiral_adjust_master_ratios(manager): manager.test_window("one") assert_dimensions(manager, 0, 0, 798, 598) manager.test_window("two") assert_dimensions(manager, 400, 0, 398, 598) manager.c.layout.grow_main() assert_dimensions(manager, 480, 0, 318, 598) manager.c.layout.grow_main() assert_dimensions(manager, 560, 0, 238, 598) for _ in range(4): manager.c.layout.shrink_main() assert_dimensions(manager, 240, 0, 558, 598) @spiral_config def test_spiral_adjust_ratios(manager): manager.test_window("one") assert_dimensions(manager, 0, 0, 798, 598) manager.test_window("two") assert_dimensions(manager, 400, 0, 398, 598) manager.test_window("three") assert_dimensions(manager, 400, 300, 398, 298) manager.c.layout.increase_ratio() assert_dimensions(manager, 480, 360, 318, 238) manager.c.layout.increase_ratio() assert_dimensions(manager, 560, 420, 238, 178) for _ in range(4): manager.c.layout.decrease_ratio() assert_dimensions(manager, 240, 180, 558, 418) manager.c.layout.reset() assert_dimensions(manager, 400, 300, 398, 298) qtile-0.31.0/test/test_swallow.py0000664000175000017500000000762014762660347016723 0ustar epsilonepsilon# Copyright (c) 2021 Jeroen Wijenbergh # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest import libqtile from libqtile import config from libqtile.backend.x11.core import Core from libqtile.confreader import Config from libqtile.lazy import lazy # Function that increments a counter @lazy.function def swallow_inc(qtile): qtile.test_data += 1 return True # Config with multiple keys and swallow parameters class SwallowConfig(Config): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @libqtile.hook.subscribe.startup def _(): libqtile.qtile.test_data = 0 keys = [ config.Key( ["control"], "k", swallow_inc(), ), config.Key(["control"], "j", swallow_inc(), swallow=False), config.Key(["control"], "i", swallow_inc().when(layout="idonotexist")), config.Key( ["control"], "o", swallow_inc().when(layout="idonotexist"), swallow_inc(), ), ] # Helper to send process_key_event to the core manager # It also looks up the keysym and mask to pass to it def send_process_key_event(manager, key): keysym, mask = Core.lookup_key(None, key) output = manager.c.eval(f"self.process_key_event({keysym}, {mask})[1]") # Assert if eval successful assert output[0] # Convert the string to a bool return output[1] == "True" def get_test_counter(manager): output = manager.c.eval("self.test_data") # Assert if eval successful assert output[0] return int(output[1]) @pytest.mark.parametrize("manager", [SwallowConfig], indirect=True) def test_swallow(manager): # The first key needs to be True as swallowing is not set here # We expect the second key to not be handled, as swallow is set to False # The third needs to not be swallowed as the layout .when(...) check does not succeed # The fourth needs to be True as one of the functions is executed due to passing the .when(...) check expectedexecuted = [True, True, False, True] expectedswallow = [True, False, False, True] # Loop over all the keys in the config and assert prev_counter = 0 for index, key in enumerate(SwallowConfig.keys): assert send_process_key_event(manager, key) == expectedswallow[index] # Test if the function was executed like we expected counter = get_test_counter(manager) if expectedexecuted[index]: assert counter > prev_counter else: assert counter == prev_counter prev_counter = counter not_used_key = config.Key( ["control"], "h", swallow_inc(), ) # This key is not defined in the config so it should not be handled assert not send_process_key_event(manager, not_used_key) # This key is not defined so test data is not incremented assert get_test_counter(manager) == prev_counter qtile-0.31.0/test/test_utils.py0000664000175000017500000001214714762660347016373 0ustar epsilonepsilon# Copyright (c) 2008, 2010 Aldo Cortesi # Copyright (c) 2011 Florian Mounier # Copyright (c) 2011 Anshuman Bhaduri # Copyright (c) 2020 Matt Colligan # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import os from collections import OrderedDict from pathlib import Path from tempfile import TemporaryDirectory import pytest from libqtile import utils def test_rgb_from_hex_number(): assert utils.rgb("ff00ff") == (1, 0, 1, 1) def test_rgb_from_hex_string(): assert utils.rgb("#00ff00") == (0, 1, 0, 1) def test_rgb_from_hex_number_with_alpha(): assert utils.rgb("ff0000.3") == (1, 0, 0, 0.3) def test_rgb_from_hex_string_with_alpha(): assert utils.rgb("#ff0000.5") == (1, 0, 0, 0.5) def test_rgb_from_hex_number_with_hex_alpha(): assert utils.rgb("ff000000") == (1, 0, 0, 0.0) def test_rgb_from_hex_string_with_hex_alpha(): assert utils.rgb("#ff000000") == (1, 0, 0, 0.0) def test_rgb_from_base10_tuple(): assert utils.rgb([255, 255, 0]) == (1, 1, 0, 1) def test_rgb_from_base10_tuple_with_alpha(): assert utils.rgb([255, 255, 0, 0.5]) == (1, 1, 0, 0.5) def test_rgb_from_3_digit_hex_number(): assert utils.rgb("f0f") == (1, 0, 1, 1) def test_rgb_from_3_digit_hex_string(): assert utils.rgb("#f0f") == (1, 0, 1, 1) def test_rgb_from_3_digit_hex_number_with_alpha(): assert utils.rgb("f0f.5") == (1, 0, 1, 0.5) def test_rgb_from_3_digit_hex_string_with_alpha(): assert utils.rgb("#f0f.5") == (1, 0, 1, 0.5) def test_has_transparency(): colours = [ ("#00000000", True), ("#000000ff", False), ("#ff00ff.5", True), ((255, 255, 255, 0.5), True), ((255, 255, 255), False), (["#000000", "#ffffff"], False), (["#000000", "#ffffffaa"], True), ] for colour, expected in colours: assert utils.has_transparency(colour) == expected def test_remove_transparency(): colours = [ ("#00000000", (0.0, 0.0, 0.0)), ("#ffffffff", (255.0, 255.0, 255.0)), ((255, 255, 255, 0.5), (255.0, 255.0, 255.0)), ((255, 255, 255), (255.0, 255.0, 255.0)), (["#000000", "#ffffff"], [(0.0, 0.0, 0.0), (255.0, 255.0, 255.0)]), (["#000000", "#ffffffaa"], [(0.0, 0.0, 0.0), (255.0, 255.0, 255.0)]), ] for colour, expected in colours: assert utils.remove_transparency(colour) == expected def test_scrub_to_utf8(): assert utils.scrub_to_utf8(b"foo") == "foo" def test_guess_terminal_accepts_a_preference(path): term = "shitty" Path(path, term).touch(mode=0o777) assert utils.guess_terminal(term) == term def test_guess_terminal_accepts_a_list_of_preferences(path): term = "shitty" Path(path, term).touch(mode=0o777) assert utils.guess_terminal(["nutty", term]) == term def test_guess_terminal_falls_back_to_defaults(path): Path(path, "kitty").touch(mode=0o777) assert utils.guess_terminal(["nutty", "witty", "petty"]) == "kitty" @pytest.fixture def path(monkeypatch): "Create a TemporaryDirectory as the PATH" with TemporaryDirectory() as d: monkeypatch.setenv("PATH", d) yield d TEST_DIR = os.path.dirname(os.path.abspath(__file__)) DATA_DIR = os.path.join(TEST_DIR, "data") class TestScanFiles: def test_audio_volume_muted(self): name = "audio-volume-muted.*" dfiles = utils.scan_files(DATA_DIR, name) result = dfiles[name] assert len(result) == 2 png = os.path.join(DATA_DIR, "png", "audio-volume-muted.png") assert png in result svg = os.path.join(DATA_DIR, "svg", "audio-volume-muted.svg") assert svg in result def test_only_svg(self): name = "audio-volume-muted.svg" dfiles = utils.scan_files(DATA_DIR, name) result = dfiles[name] assert len(result) == 1 svg = os.path.join(DATA_DIR, "svg", "audio-volume-muted.svg") assert svg in result def test_multiple(self): names = OrderedDict() names["audio-volume-muted.*"] = 2 names["battery-caution-charging.*"] = 1 dfiles = utils.scan_files(DATA_DIR, *names) for name, length in names.items(): assert len(dfiles[name]) == length qtile-0.31.0/test/conftest.py0000664000175000017500000001042314762660347016014 0ustar epsilonepsilon# Copyright (c) 2011 Florian Mounier # Copyright (c) 2011 Anshuman Bhaduri # Copyright (c) 2014 Sean Vig # Copyright (c) 2014-2015 Tycho Andersen # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest from libqtile.backend.base import drawer from test.helpers import BareConfig, TestManager def pytest_addoption(parser): parser.addoption("--debuglog", action="store_true", default=False, help="enable debug output") parser.addoption( "--backend", action="append", choices=("x11", "wayland"), help="Test a specific backend. Can be passed more than once.", ) def pytest_cmdline_main(config): if not config.option.backend: config.option.backend = ["x11"] ignore = config.option.ignore or [] if "wayland" not in config.option.backend: ignore.append("test/backend/wayland") if "x11" not in config.option.backend: ignore.append("test/backend/x11") config.option.ignore = ignore def pytest_generate_tests(metafunc): if "backend" in metafunc.fixturenames: backends = metafunc.config.option.backend metafunc.parametrize("backend_name", backends) @pytest.fixture(scope="session", params=[1]) def outputs(request): return request.param dualmonitor = pytest.mark.parametrize("outputs", [2], indirect=True) multimonitor = pytest.mark.parametrize("outputs", [1, 2], indirect=True) @pytest.fixture(scope="session") def xephyr(request, outputs): if "x11" not in request.config.option.backend: yield return from test.backend.x11.conftest import x11_environment kwargs = getattr(request, "param", {}) with x11_environment(outputs, **kwargs) as x: yield x @pytest.fixture(scope="session") def wayland_session(request, outputs): if "wayland" not in request.config.option.backend: yield return from test.backend.wayland.conftest import wayland_environment with wayland_environment(outputs) as w: yield w @pytest.fixture(scope="function") def backend(request, backend_name, xephyr, wayland_session): if backend_name == "x11": from test.backend.x11.conftest import XBackend yield XBackend({"DISPLAY": xephyr.display}, args=[xephyr.display]) elif backend_name == "wayland": from test.backend.wayland.conftest import WaylandBackend yield WaylandBackend(wayland_session) @pytest.fixture(scope="function") def manager_nospawn(request, backend): with TestManager(backend, request.config.getoption("--debuglog")) as manager: yield manager @pytest.fixture(scope="function") def manager(request, manager_nospawn): config = getattr(request, "param", BareConfig) manager_nospawn.start(config) yield manager_nospawn @pytest.fixture(scope="function") def manager_withlogs(request, manager_nospawn): config = getattr(request, "param", BareConfig) manager_nospawn.start(config, want_logs=True) yield manager_nospawn @pytest.fixture(scope="function") def fake_window(): """ A fake window that can provide a fake drawer to test widgets. """ class FakeWindow: class _NestedWindow: wid = 10 window = _NestedWindow() def create_drawer(self, width, height): return drawer.Drawer(None, self, width, height) return FakeWindow() qtile-0.31.0/test/test_match.py0000664000175000017500000000715114762660347016326 0ustar epsilonepsilon# Copyright (c) 2024 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import re import pytest from libqtile import layout from libqtile.config import Match, Screen from libqtile.confreader import Config @pytest.fixture(scope="function") def manager(manager_nospawn, request): class MatchConfig(Config): rules = getattr(request, "param", list()) if not isinstance(rules, list | tuple): rules = [rules] screens = [Screen()] floating_layout = layout.Floating(float_rules=[*rules]) manager_nospawn.start(MatchConfig) yield manager_nospawn def configure_rules(*args): return pytest.mark.parametrize("manager", [args], indirect=True) def assert_float(manager, name, floating=True): manager.test_window(name) assert manager.c.window.info()["floating"] is floating manager.c.window.kill() @configure_rules(Match(title="floatme")) @pytest.mark.parametrize( "name,result", [("normal", False), ("floatme", True), ("floatmetoo", False)] ) def test_single_rule(manager, name, result): """Single string must be exact match""" assert_float(manager, name, result) @configure_rules(Match(title=re.compile(r"floatme"))) @pytest.mark.parametrize( "name,result", [("normal", False), ("floatme", True), ("floatmetoo", True)] ) def test_single_regex_rule(manager, name, result): """Regex to match substring""" assert_float(manager, name, result) @configure_rules(~Match(title="floatme")) @pytest.mark.parametrize( "name,result", [("normal", True), ("floatme", False), ("floatmetoo", True)] ) def test_not_rule(manager, name, result): """Invert match rule""" assert_float(manager, name, result) @configure_rules(Match(title="floatme") | Match(title="floating")) @pytest.mark.parametrize( "name,result", [("normal", False), ("floatme", True), ("floating", True), ("floatmetoo", False)], ) def test_or_rule(manager, name, result): """Invert match rule""" assert_float(manager, name, result) @configure_rules(Match(title=re.compile(r"^floatme")) & Match(title=re.compile(r".*too$"))) @pytest.mark.parametrize( "name,result", [("normal", False), ("floatme", False), ("floatmetoo", True)] ) def test_and_rule(manager, name, result): """Combine match rules""" assert_float(manager, name, result) @configure_rules(Match(title=re.compile(r"^floatme")) ^ Match(title=re.compile(r".*too$"))) @pytest.mark.parametrize( "name,result", [("normal", False), ("floatme", True), ("floatmetoo", False), ("thisfloatstoo", True)], ) def test_xor_rule(manager, name, result): """Combine match rules""" assert_float(manager, name, result) qtile-0.31.0/test/test_floating.py0000664000175000017500000000765014762660347017041 0ustar epsilonepsilon# Copyright (c) 2023 Yonnji # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest import libqtile.config from libqtile import bar, layout, widget from libqtile.config import Screen from libqtile.confreader import Config class FakeScreenConfig(Config): auto_fullscreen = True floating_layout = layout.Floating() groups = [ libqtile.config.Group( "a", layouts=[floating_layout], ), ] layouts = [ layout.Tile(), ] keys = [] mouse = [] fake_screens = [ Screen( top=bar.Bar( [widget.GroupBox(), widget.WindowName(), widget.Clock()], 10, ), width=1920, height=1080, ), ] screens = [] fakescreen_config = pytest.mark.parametrize("manager", [FakeScreenConfig], indirect=True) @fakescreen_config def test_maximize(manager): """Ensure that maximize saves and restores geometry""" manager.test_window("one") manager.c.window.set_position_floating(50, 20) manager.c.window.set_size_floating(1280, 720) assert manager.c.window.info()["width"] == 1280 assert manager.c.window.info()["height"] == 720 assert manager.c.window.info()["x"] == 50 assert manager.c.window.info()["y"] == 20 assert manager.c.window.info()["group"] == "a" manager.c.window.toggle_maximize() assert manager.c.window.info()["width"] == 1920 assert manager.c.window.info()["height"] == 1070 assert manager.c.window.info()["x"] == 0 assert manager.c.window.info()["y"] == 10 assert manager.c.window.info()["group"] == "a" manager.c.window.toggle_maximize() assert manager.c.window.info()["width"] == 1280 assert manager.c.window.info()["height"] == 720 assert manager.c.window.info()["x"] == 50 assert manager.c.window.info()["y"] == 20 assert manager.c.window.info()["group"] == "a" @fakescreen_config def test_fullscreen(manager): """Ensure that fullscreen saves and restores geometry""" manager.test_window("one") manager.c.window.set_position_floating(50, 20) manager.c.window.set_size_floating(1280, 720) assert manager.c.window.info()["width"] == 1280 assert manager.c.window.info()["height"] == 720 assert manager.c.window.info()["x"] == 50 assert manager.c.window.info()["y"] == 20 assert manager.c.window.info()["group"] == "a" manager.c.window.toggle_fullscreen() assert manager.c.window.info()["width"] == 1920 assert manager.c.window.info()["height"] == 1080 assert manager.c.window.info()["x"] == 0 assert manager.c.window.info()["y"] == 0 assert manager.c.window.info()["group"] == "a" manager.c.window.toggle_fullscreen() assert manager.c.window.info()["width"] == 1280 assert manager.c.window.info()["height"] == 720 assert manager.c.window.info()["x"] == 50 assert manager.c.window.info()["y"] == 20 assert manager.c.window.info()["group"] == "a" qtile-0.31.0/test/test_window.py0000664000175000017500000002720114762660347016537 0ustar epsilonepsilonimport pytest from libqtile import bar, config, hook, layout, log_utils, resources, widget from libqtile.confreader import Config from test.conftest import BareConfig, dualmonitor from test.layouts.layout_utils import assert_focused, assert_unfocused from test.test_manager import ManagerConfig bare_config = pytest.mark.parametrize("manager", [BareConfig], indirect=True) @bare_config def test_info(manager): """ Checks each backend Window implementation provides the required information. """ manager.test_window("one") manager.c.sync() info = manager.c.window.info() assert info["name"] == "one" assert info["group"] == "a" assert info["wm_class"][0] == "TestWindow" assert "x" in info assert "y" in info assert "width" in info assert "height" in info assert "id" in info @bare_config def test_is_visible_hidden(manager): """ Test Window#is_visible() with "hidden" (aka layout calls client.hide()) windows. """ manager.test_window("one") assert_focused(manager, "one") assert manager.c.window.is_visible() manager.c.window.toggle_minimize() assert not manager.c.window.is_visible() manager.c.window.toggle_minimize() assert manager.c.window.is_visible() @bare_config def test_is_visible_minimized(manager): """ Test Window#is_visible() with "minized" (aka floating or other minimization). """ manager.test_window("one") one_id = manager.c.window.info()["id"] manager.test_window("two") two_id = manager.c.window.info()["id"] assert_focused(manager, "two") assert manager.c.window.is_visible() assert not manager.c.window[one_id].is_visible() manager.c.layout.up() assert_focused(manager, "one") assert manager.c.window.is_visible() assert not manager.c.window[two_id].is_visible() @bare_config def test_margin(manager): manager.test_window("one") # No margin manager.c.window.place(10, 20, 50, 60, 0, "000000") assert manager.c.window.info()["x"] == 10 assert manager.c.window.info()["y"] == 20 assert manager.c.window.info()["width"] == 50 assert manager.c.window.info()["height"] == 60 # Margin as int manager.c.window.place(10, 20, 50, 60, 0, "000000", margin=8) assert manager.c.window.info()["x"] == 18 assert manager.c.window.info()["y"] == 28 assert manager.c.window.info()["width"] == 34 assert manager.c.window.info()["height"] == 44 # Margin as list manager.c.window.place(10, 20, 50, 60, 0, "000000", margin=[2, 4, 8, 10]) assert manager.c.window.info()["x"] == 20 assert manager.c.window.info()["y"] == 22 assert manager.c.window.info()["width"] == 36 assert manager.c.window.info()["height"] == 50 @bare_config def test_no_size_hint(manager): manager.test_window("one") manager.c.window.enable_floating() assert manager.c.window.info()["width"] == 100 assert manager.c.window.info()["height"] == 100 manager.c.window.set_size_floating(50, 50) assert manager.c.window.info()["width"] == 50 assert manager.c.window.info()["height"] == 50 manager.c.window.set_size_floating(200, 200) assert manager.c.window.info()["width"] == 200 assert manager.c.window.info()["height"] == 200 @bare_config def test_togroup_toggle(manager): manager.test_window("one") assert manager.c.group.info()["name"] == "a" # Start on "a" assert manager.c.get_groups()["a"]["focus"] == "one" assert manager.c.get_groups()["b"]["focus"] is None manager.c.window.togroup("b", switch_group=True) assert manager.c.group.info()["name"] == "b" # Move the window and switch to "b" assert manager.c.get_groups()["a"]["focus"] is None assert manager.c.get_groups()["b"]["focus"] == "one" manager.c.window.togroup("b", switch_group=True) assert manager.c.group.info()["name"] == "b" # Does not toggle by default assert manager.c.get_groups()["a"]["focus"] is None assert manager.c.get_groups()["b"]["focus"] == "one" manager.c.window.togroup("b", switch_group=True, toggle=True) assert ( manager.c.group.info()["name"] == "a" ) # Explicitly toggling moves the window and switches to "a" assert manager.c.get_groups()["a"]["focus"] == "one" assert manager.c.get_groups()["b"]["focus"] is None manager.c.window.togroup("b", switch_group=True, toggle=True) manager.c.window.togroup("b", switch_group=True, toggle=True) assert manager.c.group.info()["name"] == "a" # Toggling twice roundtrips between the two assert manager.c.get_groups()["a"]["focus"] == "one" assert manager.c.get_groups()["b"]["focus"] is None manager.c.window.togroup("b", toggle=True) assert ( manager.c.group.info()["name"] == "a" ) # Toggling without switching only moves the window assert manager.c.get_groups()["a"]["focus"] is None assert manager.c.get_groups()["b"]["focus"] == "one" class BringFrontClickConfig(ManagerConfig): bring_front_click = True class BringFrontClickFloatingOnlyConfig(ManagerConfig): bring_front_click = "floating_only" @pytest.fixture def bring_front_click(request): return request.param @pytest.mark.parametrize( "manager, bring_front_click", [ (ManagerConfig, False), (BringFrontClickConfig, True), (BringFrontClickFloatingOnlyConfig, "floating_only"), ], indirect=True, ) def test_bring_front_click(manager, bring_front_click): manager.c.group.setlayout("tile") # this is a tiled window. manager.test_window("one") manager.test_window("two") manager.c.window.set_position_floating(50, 50) manager.c.window.set_size_floating(50, 50) manager.test_window("three") manager.c.window.set_position_floating(150, 50) manager.c.window.set_size_floating(50, 50) wids = [x["id"] for x in manager.c.windows()] names = [x["name"] for x in manager.c.windows()] assert names == ["one", "two", "three"] wins = manager.backend.get_all_windows() assert wins.index(wids[0]) < wins.index(wids[1]) < wins.index(wids[2]) # Click on window two manager.backend.fake_click(55, 55) assert manager.c.window.info()["name"] == "two" wins = manager.backend.get_all_windows() if bring_front_click: assert wins.index(wids[0]) < wins.index(wids[2]) < wins.index(wids[1]) else: assert wins.index(wids[0]) < wins.index(wids[1]) < wins.index(wids[2]) # Click on window one manager.backend.fake_click(10, 10) assert manager.c.window.info()["name"] == "one" wins = manager.backend.get_all_windows() if bring_front_click == "floating_only": assert wins.index(wids[0]) < wins.index(wids[2]) < wins.index(wids[1]) elif bring_front_click: assert wins.index(wids[2]) < wins.index(wids[1]) < wins.index(wids[0]) else: assert wins.index(wids[0]) < wins.index(wids[1]) < wins.index(wids[2]) @dualmonitor @bare_config def test_center_window(manager): """Check that floating windows are centered correctly.""" manager.test_window("one") manager.c.window.set_position_floating(50, 50) manager.c.window.set_size_floating(200, 100) info = manager.c.window.info() assert info["x"] == 50 assert info["y"] == 50 assert info["width"] == 200 assert info["height"] == 100 manager.c.window.center() info = manager.c.window.info() assert info["x"] == (800 - 200) / 2 # (screen width - window width) / 2 assert info["y"] == (600 - 100) / 2 # (screen height - window height) / 2 assert info["width"] == 200 assert info["height"] == 100 manager.c.window.togroup("b") # Second screen manager.c.to_screen(1) # Focus screen manager.c.group["b"].toscreen() # Make sure group b is on that screen # Second screen is 640x480 at offset (800, 0) manager.c.window.center() info = manager.c.window.info() assert info["x"] == 800 + (640 - 200) / 2 # offset + (screen width - window width) / 2 assert info["y"] == (480 - 100) / 2 # (screen height - window height) / 2 assert info["width"] == 200 assert info["height"] == 100 class PositionConfig(Config): auto_fullscreen = True groups = [ config.Group("a"), config.Group("b"), ] # MonadTall supports swap, treetab doesn't layouts = [layout.MonadTall(), layout.TreeTab()] floating_layout = resources.default_config.floating_layout keys = [] mouse = [] screens = [] follow_mouse_focus = False position_config = pytest.mark.parametrize("manager", [PositionConfig], indirect=True) @position_config def test_set_position(manager): """Check that windows are positioned correctly.""" manager.test_window("one") # Get first pane coords info = manager.c.window.info() coords = info["x"], info["y"] manager.test_window("two") # Get second pane coords info = manager.c.window.info() two_coords = info["x"], info["y"] # We should have client one and two in the layout assert manager.c.layout.info()["clients"] == ["one", "two"] # Set the position to the first window pane # We need to also set the pointer coords as the function uses them manager.c.eval(f"self.core.warp_pointer({coords[0]}, {coords[1]})") manager.c.window.set_position(coords[0], coords[1]) # Now they should be swapped assert manager.c.layout.info()["clients"] == ["two", "one"] # Swap back to the second pane manager.c.eval(f"self.core.warp_pointer({two_coords[0]}, {two_coords[1]})") manager.c.window.set_position(two_coords[0], two_coords[1]) # Now they should be back to original assert manager.c.layout.info()["clients"] == ["one", "two"] # Test with a layout which doesn't support swap right now (TreeTab) manager.c.layout.next() manager.c.eval(f"self.core.warp_pointer({coords[0]}, {coords[1]})") manager.c.window.set_position(coords[0], coords[1]) # Now they should be the same assert manager.c.layout.info()["clients"] == ["one", "two"] # Now test with floating manager.c.window.enable_floating() manager.c.window.set_position(50, 50) info = manager.c.window.info() # Check if the position matches assert info["x"] == 50 assert info["y"] == 50 # Also check if there is only one client now assert len(manager.c.layout.info()["clients"]) == 1 class WindowNameConfig(BareConfig): screens = [ config.Screen( bottom=bar.Bar( [ widget.WindowName(), ], 20, ), ), ] layouts = [layout.Columns()] @pytest.mark.parametrize("manager", [WindowNameConfig], indirect=True) def test_focus_switch(manager): def _wnd(name): return manager.c.window[{w["name"]: w["id"] for w in manager.c.windows()}[name]] manager.test_window("One") manager.test_window("Two") assert manager.c.widget["windowname"].info()["text"] == "Two" _wnd("One").focus() assert manager.c.widget["windowname"].info()["text"] == "One" def set_steal_focus(win): if win.name != "three": win.can_steal_focus = False @pytest.fixture def hook_fixture(): log_utils.init_log() yield hook.clear() @pytest.mark.usefixtures("hook_fixture") def test_can_steal_focus(manager_nospawn): """ Test Window.can_steal_focus. """ class AntiFocusStealConfig(BareConfig): hook.subscribe.client_new(set_steal_focus) manager_nospawn.start(AntiFocusStealConfig) manager_nospawn.test_window("one") assert_unfocused(manager_nospawn, "one") manager_nospawn.test_window("two") assert_unfocused(manager_nospawn, "one") assert_unfocused(manager_nospawn, "two") manager_nospawn.test_window("three") assert_focused(manager_nospawn, "three") qtile-0.31.0/test/helpers.py0000664000175000017500000003170514762660347015637 0ustar epsilonepsilon""" This file contains various helpers and basic variables for the test suite. Defining them here rather than in conftest.py avoids issues with circular imports between test/conftest.py and test/backend//conftest.py files. """ import faulthandler import functools import logging import multiprocessing import os import signal import subprocess import sys import tempfile import time import traceback from abc import ABCMeta, abstractmethod from pathlib import Path from libqtile import command, config, ipc, layout from libqtile.confreader import Config from libqtile.core.manager import Qtile from libqtile.lazy import lazy from libqtile.log_utils import init_log, logger from libqtile.resources import default_config # the sizes for outputs WIDTH = 800 HEIGHT = 600 SECOND_WIDTH = 640 SECOND_HEIGHT = 480 LOG_PIPE_BUFFER_SIZE = 128 * 1024 max_sleep = 5.0 sleep_time = 0.1 class Retry: def __init__( self, ignore_exceptions=(), dt=sleep_time, tmax=max_sleep, return_on_fail=False, ): self.ignore_exceptions = ignore_exceptions self.dt = dt self.tmax = tmax self.return_on_fail = return_on_fail self.last_failure = None def __call__(self, fn): @functools.wraps(fn) def wrapper(*args, **kwargs): tmax = time.time() + self.tmax dt = self.dt ignore_exceptions = self.ignore_exceptions while time.time() <= tmax: try: return fn(*args, **kwargs) except ignore_exceptions as e: self.last_failure = e except AssertionError: break time.sleep(dt) dt *= 1.5 if self.return_on_fail: return False else: raise self.last_failure return wrapper class BareConfig(Config): auto_fullscreen = True groups = [config.Group("a"), config.Group("b"), config.Group("c"), config.Group("d")] layouts = [layout.stack.Stack(num_stacks=1), layout.stack.Stack(num_stacks=2)] floating_layout = default_config.floating_layout keys = [ config.Key( ["control"], "k", lazy.layout.up(), ), config.Key( ["control"], "j", lazy.layout.down(), ), ] mouse = [] screens = [config.Screen()] follow_mouse_focus = False reconfigure_screens = False class Backend(metaclass=ABCMeta): """A base class to help set up backends passed to TestManager""" def __init__(self, env, args=()): self.env = env self.args = args def create(self): """This is used to instantiate the Core""" return self.core(*self.args) def configure(self, manager): """This is used to do any post-startup configuration with the manager""" @abstractmethod def fake_click(self, x, y): """Click at the specified coordinates""" @abstractmethod def get_all_windows(self): """Get a list of all windows in ascending order of Z position""" @Retry(ignore_exceptions=(ipc.IPCError,), return_on_fail=True) def can_connect_qtile(socket_path, *, ok=None): if ok is not None and not ok(): raise AssertionError() ipc_client = ipc.Client(socket_path) ipc_command = command.interface.IPCCommandInterface(ipc_client) client = command.client.InteractiveCommandClient(ipc_command) val = client.status() if val == "OK": return True return False class TestManager: """Spawn a Qtile instance Setup a Qtile server instance on the given display, with the given socket and log files. The Qtile server must be started, and then stopped when it is done. Windows can be spawned for the Qtile instance to interact with with various `.test_*` methods. """ def __init__(self, backend, debug_log): self.backend = backend self.log_level = logging.DEBUG if debug_log else logging.INFO self.backend.manager = self self.proc = None self.c = None self.testwindows = [] self.logspipe = None def __enter__(self): """Set up resources""" faulthandler.enable(all_threads=True) faulthandler.register(signal.SIGUSR2, all_threads=True) self._sockfile = tempfile.NamedTemporaryFile() self.sockfile = self._sockfile.name return self def __exit__(self, _exc_type, _exc_value, _exc_tb): """Clean up resources""" self.terminate() self._sockfile.close() if self.logspipe is not None: os.close(self.logspipe) def get_log_buffer(self): """Returns any logs that have been written to qtile's log buffer up to this point.""" # default pipe size on linux is 64k. we probably won't write # 64k of logs, but in the event that we do, qtile will hang in # write(). but thanks to e1d2dab16903 ("switch semantics of sigusr2 # to stack dumping") hopefully we will see it's hung in a log write and # look at this. if we do write 64k of logs, we can do some F_SETPIPE_SZ # fiddling with the buffer size to grow it to whatever github allows. return os.read(self.logspipe, 64 * 1024).decode("utf-8") def start(self, config_class, no_spawn=False, state=None): readlogs, writelogs = os.pipe() rpipe, wpipe = multiprocessing.Pipe() def run_qtile(): try: os.environ.pop("DISPLAY", None) os.environ.pop("WAYLAND_DISPLAY", None) kore = self.backend.create() os.environ.update(self.backend.env) init_log(self.log_level) os.close(readlogs) formatter = logging.Formatter("%(levelname)s - %(message)s") handler = logging.StreamHandler(os.fdopen(writelogs, "w")) handler.setFormatter(formatter) logger.addHandler(handler) Qtile( kore, config_class(), socket_path=self.sockfile, no_spawn=no_spawn, state=state, ).loop() except Exception: wpipe.send(traceback.format_exc()) self.proc = multiprocessing.Process(target=run_qtile) self.proc.start() os.close(writelogs) self.logspipe = readlogs # First, wait for socket to appear if can_connect_qtile(self.sockfile, ok=lambda: not rpipe.poll()): ipc_client = ipc.Client(self.sockfile) ipc_command = command.interface.IPCCommandInterface(ipc_client) self.c = command.client.InteractiveCommandClient(ipc_command) self.backend.configure(self) return if rpipe.poll(0.1): error = rpipe.recv() raise AssertionError(f"Error launching qtile, traceback:\n{error}") raise AssertionError("Error launching qtile") def create_manager(self, config_class): """Create a Qtile manager instance in this thread This should only be used when it is known that the manager will throw an error and the returned manager should not be started, otherwise this will likely block the thread. """ init_log(self.log_level) kore = self.backend.create() config = config_class() for attr in dir(default_config): if not hasattr(config, attr): setattr(config, attr, getattr(default_config, attr)) return Qtile(kore, config, socket_path=self.sockfile) def terminate(self): if self.proc is None: print("qtile is not alive", file=sys.stderr) else: # try to send SIGTERM and wait up to 10 sec to quit self.proc.terminate() self.proc.join(10) if self.proc.is_alive(): # uh oh, we're hung somewhere. give it another second to print # some stack traces os.kill(self.proc.pid, signal.SIGUSR2) self.proc.join(1) print("Killing qtile forcefully", file=sys.stderr) # desperate times... this probably messes with multiprocessing... try: os.kill(self.proc.pid, signal.SIGKILL) self.proc.join() except OSError: # The process may have died due to some other error pass if self.proc.exitcode: print("qtile exited with exitcode: %d" % self.proc.exitcode, file=sys.stderr) self.proc = None for proc in self.testwindows[:]: proc.terminate() proc.wait() self.testwindows.remove(proc) def create_window(self, create, failed=None): """ Uses the function `create` to create a window. Waits until qtile actually maps the window and then returns. """ client = self.c start = len(client.windows()) create() @Retry(ignore_exceptions=(RuntimeError,)) def success(): while failed is None or not failed(): if len(client.windows()) > start: return True raise RuntimeError("window has not appeared yet") return success() def _spawn_window(self, *args): """Starts a program which opens a window Spawns a new subprocess for a command that opens a window, given by the arguments to this method. Spawns the new process and checks that qtile maps the new window. """ if not args: raise AssertionError("Trying to run nothing! (missing arguments)") proc = None def spawn(): nonlocal proc # Ensure the client only uses the test display env = os.environ.copy() env.pop("DISPLAY", None) env.pop("WAYLAND_DISPLAY", None) env.update(self.backend.env) proc = subprocess.Popen(args, env=env) def failed(): if proc.poll() is not None: return True return False self.create_window(spawn, failed=failed) self.testwindows.append(proc) return proc def kill_window(self, proc): """Kill a window and check that qtile unmaps it Kills a window created by calling one of the `self.test*` methods, ensuring that qtile removes it from the `windows` attribute. """ assert proc in self.testwindows, "Given process is not a spawned window" start = len(self.c.windows()) proc.terminate() proc.wait() self.testwindows.remove(proc) @Retry(ignore_exceptions=(ValueError,)) def success(): if len(self.c.windows()) < start: return True raise ValueError("window is still in client list!") if not success(): raise AssertionError("Window could not be killed...") def test_window( self, name, floating=False, wm_type="normal", new_title="", urgent_hint=False, export_sni=False, ): """ Create a simple window in X or Wayland. If `floating` is True then the wmclass is set to "dialog", which triggers auto-floating based on `default_float_rules`. `wm_type` can be changed from "normal" to "notification", which creates a window that not only floats but does not grab focus. Setting `export_sni` to True will publish a simplified StatusNotifierItem interface on DBus. Windows created with this method must have their process killed explicitly, no matter what type they are. """ python = sys.executable path = Path(__file__).parent / "scripts" / "window.py" wmclass = "dialog" if floating else "TestWindow" args = [python, path, "--name", wmclass, name, wm_type, new_title] if urgent_hint: args.append("urgent_hint") if export_sni: args.append("export_sni_interface") return self._spawn_window(*args) def test_notification(self, name="notification"): return self.test_window(name, wm_type="notification") def groupconsistency(self): groups = self.c.get_groups() screens = self.c.get_screens() seen = set() for g in groups.values(): scrn = g["screen"] if scrn is not None: if scrn in seen: raise AssertionError("Screen referenced from more than one group.") seen.add(scrn) assert screens[scrn]["group"] == g["name"] assert len(seen) == len(screens), "Not all screens had an attached group." @Retry(ignore_exceptions=(AssertionError,)) def assert_window_died(client, window_info): client.sync() wid = window_info["id"] assert wid not in set([x["id"] for x in client.windows()]), f"window {wid} still here" qtile-0.31.0/test/test_manager.py0000664000175000017500000013366314762660347016654 0ustar epsilonepsilon# Copyright (c) 2011 Florian Mounier # Copyright (c) 2011 Anshuman Bhaduri # Copyright (c) 2012-2014 Tycho Andersen # Copyright (c) 2013 xarvh # Copyright (c) 2013 Craig Barnes # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Adi Sieker # Copyright (c) 2014 Sebastien Blot # Copyright (c) 2020 Mikel Ward # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import logging from pathlib import Path import pytest import libqtile.bar import libqtile.config import libqtile.confreader import libqtile.hook import libqtile.layout import libqtile.widget from libqtile.command.client import SelectError from libqtile.command.interface import CommandError, CommandException from libqtile.config import Match from libqtile.confreader import Config from libqtile.group import _Group from libqtile.lazy import lazy from test.conftest import dualmonitor, multimonitor from test.helpers import BareConfig, Retry, assert_window_died from test.layouts.layout_utils import assert_focused configs_dir = Path(__file__).resolve().parent / "configs" class ManagerConfig(Config): auto_fullscreen = True groups = [ libqtile.config.Group("a"), libqtile.config.Group("b"), libqtile.config.Group("c"), libqtile.config.Group("d"), ] layouts = [ libqtile.layout.stack.Stack(num_stacks=1), libqtile.layout.stack.Stack(num_stacks=2), libqtile.layout.tile.Tile(ratio=0.5), libqtile.layout.max.Max(), ] floating_layout = libqtile.layout.floating.Floating( float_rules=[ *libqtile.layout.floating.Floating.default_float_rules, Match(wm_class="float"), Match(title="float"), ] ) keys = [ libqtile.config.Key( ["control"], "k", lazy.layout.up(), ), libqtile.config.Key( ["control"], "j", lazy.layout.down(), ), ] mouse = [] screens = [ libqtile.config.Screen( bottom=libqtile.bar.Bar( [ libqtile.widget.Prompt(), libqtile.widget.GroupBox(), ], 20, ), ) ] follow_mouse_focus = True reconfigure_screens = False manager_config = pytest.mark.parametrize("manager", [ManagerConfig], indirect=True) @dualmonitor @manager_config def test_screen_dim(manager): manager.test_window("one") assert manager.c.screen.info()["index"] == 0 assert manager.c.screen.info()["x"] == 0 assert manager.c.screen.info()["width"] == 800 assert manager.c.group.info()["name"] == "a" assert manager.c.group.info()["focus"] == "one" manager.c.to_screen(1) manager.test_window("one") assert manager.c.screen.info()["index"] == 1 assert manager.c.screen.info()["x"] == 800 assert manager.c.screen.info()["width"] == 640 assert manager.c.group.info()["name"] == "b" assert manager.c.group.info()["focus"] == "one" manager.c.to_screen(0) assert manager.c.screen.info()["index"] == 0 assert manager.c.screen.info()["x"] == 0 assert manager.c.screen.info()["width"] == 800 assert manager.c.group.info()["name"] == "a" assert manager.c.group.info()["focus"] == "one" @pytest.mark.parametrize("xephyr", [{"xoffset": 0}], indirect=True) @manager_config def test_clone_dim(manager): manager.test_window("one") assert manager.c.screen.info()["index"] == 0 assert manager.c.screen.info()["x"] == 0 assert manager.c.screen.info()["width"] == 800 assert manager.c.group.info()["name"] == "a" assert manager.c.group.info()["focus"] == "one" assert len(manager.c.get_screens()) == 1 @dualmonitor @manager_config def test_to_screen(manager): assert manager.c.screen.info()["index"] == 0 manager.c.to_screen(1) assert manager.c.screen.info()["index"] == 1 manager.test_window("one") manager.c.to_screen(0) manager.test_window("two") ga = manager.c.get_groups()["a"] assert ga["windows"] == ["two"] gb = manager.c.get_groups()["b"] assert gb["windows"] == ["one"] assert manager.c.window.info()["name"] == "two" manager.c.next_screen() assert manager.c.window.info()["name"] == "one" manager.c.next_screen() assert manager.c.window.info()["name"] == "two" manager.c.prev_screen() assert manager.c.window.info()["name"] == "one" @dualmonitor @manager_config def test_togroup(manager): manager.test_window("one") with pytest.raises(CommandError): manager.c.window.togroup("nonexistent") assert manager.c.get_groups()["a"]["focus"] == "one" manager.c.window.togroup("a") assert manager.c.get_groups()["a"]["focus"] == "one" manager.c.window.togroup("b", switch_group=True) assert manager.c.get_groups()["b"]["focus"] == "one" assert manager.c.get_groups()["a"]["focus"] is None assert manager.c.group.info()["name"] == "b" manager.c.window.togroup("a") assert manager.c.get_groups()["a"]["focus"] == "one" assert manager.c.group.info()["name"] == "b" manager.c.to_screen(1) manager.c.window.togroup("c") assert manager.c.get_groups()["c"]["focus"] == "one" @manager_config def test_resize(manager): manager.c.screen[0].resize(x=10, y=10, w=100, h=100) @Retry(ignore_exceptions=(AssertionError)) def run(): d = manager.c.screen[0].info() assert d["width"] == 100, "screen did not resize" assert d["height"] == 100, "screen did not resize" return d d = run() assert d["x"] == d["y"] == 10 def test_minimal(manager): assert manager.c.status() == "OK" @manager_config def test_events(manager): assert manager.c.status() == "OK" # FIXME: failing test disabled. For some reason we don't seem # to have a keymap in Xnest or Xephyr 99% of the time. @manager_config def test_keypress(manager): manager.test_window("one") manager.test_window("two") with pytest.raises(CommandError): manager.c.simulate_keypress(["unknown"], "j") assert manager.c.get_groups()["a"]["focus"] == "two" manager.c.simulate_keypress(["control"], "j") assert manager.c.get_groups()["a"]["focus"] == "one" class TooFewGroupsConfig(ManagerConfig): groups = [] @pytest.mark.parametrize("manager", [TooFewGroupsConfig], indirect=True) @multimonitor def test_too_few_groups(manager): assert manager.c.get_groups() assert len(manager.c.get_groups()) == len(manager.c.get_screens()) class _ChordsConfig(Config): groups = [libqtile.config.Group("a")] layouts = [libqtile.layout.max.Max()] floating_layout = libqtile.resources.default_config.floating_layout keys = [ libqtile.config.Key( [], "k", lazy.layout.up(), ), libqtile.config.KeyChord( ["control"], "a", [ libqtile.config.Key( [], "j", lazy.layout.down(), ) ], ), libqtile.config.KeyChord( ["control"], "b", [ libqtile.config.Key( [], "j", lazy.layout.down(), ) ], "test", ), libqtile.config.KeyChord( ["control"], "d", [ libqtile.config.KeyChord( [], "a", [ libqtile.config.KeyChord( [], "1", [ libqtile.config.Key([], "u", lazy.ungrab_chord()), libqtile.config.Key([], "v", lazy.ungrab_all_chords()), libqtile.config.Key([], "j", lazy.layout.down()), ], "inner_named", ), ], ), libqtile.config.Key([], "z", lazy.layout.down()), ], "nesting_test", ), ] mouse = [] screens = [ libqtile.config.Screen( bottom=libqtile.bar.Bar( [ libqtile.widget.GroupBox(), ], 20, ), ) ] auto_fullscreen = True chords_config = pytest.mark.parametrize("manager", [_ChordsConfig], indirect=True) @chords_config def test_immediate_chord(manager): manager.test_window("three") manager.test_window("two") manager.test_window("one") assert manager.c.get_groups()["a"]["focus"] == "one" # use normal bind to shift focus up manager.c.simulate_keypress([], "k") assert manager.c.get_groups()["a"]["focus"] == "two" # enter into key chord and "k" binding no longer working manager.c.simulate_keypress(["control"], "a") manager.c.simulate_keypress([], "k") assert manager.c.get_groups()["a"]["focus"] == "two" # leave chord using "Escape", "k" bind work again manager.c.simulate_keypress([], "Escape") manager.c.simulate_keypress([], "k") assert manager.c.get_groups()["a"]["focus"] == "three" # enter key chord and use it's "j" binding to shift focus down manager.c.simulate_keypress(["control"], "a") manager.c.simulate_keypress([], "j") assert manager.c.get_groups()["a"]["focus"] == "two" # in immediate chord we leave it after use any # bind from it, "j" bind no longer working manager.c.simulate_keypress([], "j") assert manager.c.get_groups()["a"]["focus"] == "two" @chords_config def test_mode_chord(manager): manager.test_window("three") manager.test_window("two") manager.test_window("one") assert manager.c.get_groups()["a"]["focus"] == "one" # use normal bind to shift focus up manager.c.simulate_keypress([], "k") assert manager.c.get_groups()["a"]["focus"] == "two" # enter into key chord and "k" binding no longer working manager.c.simulate_keypress(["control"], "b") manager.c.simulate_keypress([], "k") assert manager.c.get_groups()["a"]["focus"] == "two" # leave chord using "Escape", "k" bind work again manager.c.simulate_keypress([], "Escape") manager.c.simulate_keypress([], "k") assert manager.c.get_groups()["a"]["focus"] == "three" # enter key chord and use it's "j" binding to shift focus down manager.c.simulate_keypress(["control"], "b") manager.c.simulate_keypress([], "j") assert manager.c.get_groups()["a"]["focus"] == "two" # in mode chord we __not__ leave it after use any # bind from it, "j" bind still working manager.c.simulate_keypress([], "j") assert manager.c.get_groups()["a"]["focus"] == "one" # only way to exit mode chord is by hit "Escape" manager.c.simulate_keypress([], "Escape") manager.c.simulate_keypress([], "j") assert manager.c.get_groups()["a"]["focus"] == "one" @chords_config def test_chord_stack(manager): manager.test_window("two") manager.test_window("one") assert manager.c.get_groups()["a"]["focus"] == "one" manager.c.simulate_keypress(["control"], "d") # ["nesting_test"] # "z" should work, "k" shouldn't: manager.c.simulate_keypress([], "z") assert manager.c.get_groups()["a"]["focus"] == "two" manager.c.simulate_keypress([], "z") assert manager.c.get_groups()["a"]["focus"] == "one" manager.c.simulate_keypress([], "k") assert manager.c.get_groups()["a"]["focus"] == "one" # enter ["nesting_test", "", "inner_named"]: manager.c.simulate_keypress([], "a") manager.c.simulate_keypress([], "1") # "j" should work: manager.c.simulate_keypress([], "j") assert manager.c.get_groups()["a"]["focus"] == "two" manager.c.simulate_keypress([], "j") assert manager.c.get_groups()["a"]["focus"] == "one" # leave "inner_named" ~> ["nesting_test"]: manager.c.simulate_keypress([], "u") manager.c.simulate_keypress([], "z") assert manager.c.get_groups()["a"]["focus"] == "two" manager.c.simulate_keypress([], "z") assert manager.c.get_groups()["a"]["focus"] == "one" manager.c.simulate_keypress([], "k") assert manager.c.get_groups()["a"]["focus"] == "one" # enter ["nesting_test", "", "inner_named"]: manager.c.simulate_keypress([], "a") manager.c.simulate_keypress([], "1") # leave all: ~> [] manager.c.simulate_keypress([], "v") # "k" should work, "z" shouldn't: manager.c.simulate_keypress([], "k") assert manager.c.get_groups()["a"]["focus"] == "two" manager.c.simulate_keypress([], "k") assert manager.c.get_groups()["a"]["focus"] == "one" manager.c.simulate_keypress([], "z") assert manager.c.get_groups()["a"]["focus"] == "one" @manager_config def test_spawn(manager): # Spawn something with a pid greater than init's assert int(manager.c.spawn("true")) > 1 @manager_config def test_spawn_list(manager): # Spawn something with a pid greater than init's assert int(manager.c.spawn(["echo", "true"])) > 1 @manager_config def test_kill_window(manager): manager.test_window("one") window_info = manager.c.window.info() manager.c.window[window_info["id"]].kill() assert_window_died(manager.c, window_info) @manager_config def test_kill_other(manager): manager.c.group.setlayout("tile") one = manager.test_window("one") assert manager.c.window.info()["width"] == 798 window_one_info = manager.c.window.info() assert manager.c.window.info()["height"] == 578 two = manager.test_window("two") assert manager.c.window.info()["name"] == "two" assert manager.c.window.info()["width"] == 398 assert manager.c.window.info()["height"] == 578 assert len(manager.c.windows()) == 2 manager.kill_window(one) assert_window_died(manager.c, window_one_info) assert manager.c.window.info()["name"] == "two" assert manager.c.window.info()["width"] == 798 assert manager.c.window.info()["height"] == 578 manager.kill_window(two) @manager_config def test_regression_groupswitch(manager): manager.c.group["c"].toscreen() manager.c.group["d"].toscreen() assert manager.c.get_groups()["c"]["screen"] is None @manager_config def test_next_layout(manager): manager.test_window("one") manager.test_window("two") assert len(manager.c.layout.info()["stacks"]) == 1 manager.c.next_layout() assert len(manager.c.layout.info()["stacks"]) == 2 manager.c.next_layout() manager.c.next_layout() manager.c.next_layout() assert len(manager.c.layout.info()["stacks"]) == 1 @manager_config def test_setlayout(manager): assert not manager.c.layout.info()["name"] == "max" manager.c.group.setlayout("max") assert manager.c.layout.info()["name"] == "max" @manager_config def test_to_layout_index(manager): manager.c.to_layout_index(-1) assert manager.c.layout.info()["name"] == "max" manager.c.to_layout_index(-4) assert manager.c.layout.info()["name"] == "stack" with pytest.raises(SelectError): manager.c.to_layout.index(-5) manager.c.to_layout_index(-2) assert manager.c.layout.info()["name"] == "tile" @manager_config def test_adddelgroup(manager): manager.test_window("one") manager.c.addgroup("dummygroup") manager.c.addgroup("testgroup") assert "testgroup" in manager.c.get_groups().keys() manager.c.window.togroup("testgroup") manager.c.delgroup("testgroup") assert "testgroup" not in manager.c.get_groups().keys() # Assert that the test window is still a member of some group. assert sum(len(i["windows"]) for i in manager.c.get_groups().values()) for i in list(manager.c.get_groups().keys())[:-1]: manager.c.delgroup(i) with pytest.raises(CommandException): manager.c.delgroup(list(manager.c.get_groups().keys())[0]) # Assert that setting layout via addgroup works manager.c.addgroup("testgroup2", layout="max") assert manager.c.get_groups()["testgroup2"]["layout"] == "max" @manager_config def test_addgroupat(manager): manager.test_window("one") group_count = len(manager.c.get_groups()) manager.c.addgroup("aa", index=1) assert len(manager.c.get_groups()) == group_count + 1 assert list(manager.c.get_groups())[1] == "aa" @manager_config def test_delgroup(manager): manager.test_window("one") for i in ["a", "d", "c"]: manager.c.delgroup(i) with pytest.raises(CommandException): manager.c.delgroup("b") @manager_config def test_nextprevgroup(manager): start = manager.c.group.info()["name"] ret = manager.c.screen.next_group() assert manager.c.group.info()["name"] != start assert manager.c.group.info()["name"] == ret ret = manager.c.screen.prev_group() assert manager.c.group.info()["name"] == start def test_nextprevgroup_reload(manager_nospawn): manager_nospawn.start(lambda: BareConfig(file_path=configs_dir / "reloading.py")) # Current group will become unmanaged after reloading manager_nospawn.c.eval("self.old_group = self.current_group") manager_nospawn.c.reload_config() # Check that group has become unmanaged manager_nospawn.c.eval("self.new_group = self.current_group") assert "True" == manager_nospawn.c.eval("self.old_group != self.new_group")[1] # Unmanaged group should not change the group in the screen success, message = manager_nospawn.c.eval("self.old_group.screen.next_group()") assert "True" == manager_nospawn.c.eval("self.new_group == self.current_group")[1] assert success, message success, message = manager_nospawn.c.eval("self.old_group.screen.prev_group()") assert "True" == manager_nospawn.c.eval("self.new_group == self.current_group")[1] assert success, message @manager_config def test_toggle_group(manager): manager.c.group["a"].toscreen() manager.c.group["b"].toscreen() manager.c.screen.toggle_group("c") assert manager.c.group.info()["name"] == "c" manager.c.screen.toggle_group("c") assert manager.c.group.info()["name"] == "b" manager.c.screen.toggle_group() assert manager.c.group.info()["name"] == "c" @manager_config def test_static(manager): manager.test_window("one") manager.test_window("two") manager.c.window[manager.c.window.info()["id"]].static( screen=0, x=10, y=10, width=10, height=10, ) info = manager.c.window.info() assert info["name"] == "one" manager.c.window.kill() assert_window_died(manager.c, info) with pytest.raises(CommandError): manager.c.window.info() info = manager.c.windows()[0] assert info["name"] == "two" assert (info["x"], info["y"], info["width"], info["height"]) == (10, 10, 10, 10) @manager_config def test_match(manager): manager.test_window("one") assert manager.c.window.info()["name"] == "one" assert not manager.c.window.info()["name"] == "nonexistent" @manager_config def test_default_float(manager): # change to 2 col stack manager.c.next_layout() assert len(manager.c.layout.info()["stacks"]) == 2 manager.test_window("float") assert manager.c.group.info()["focus"] == "float" assert manager.c.window.info()["width"] == 100 assert manager.c.window.info()["height"] == 100 assert manager.c.window.info()["x"] == 350 assert manager.c.window.info()["y"] == 240 assert manager.c.window.info()["floating"] is True manager.c.window.move_floating(10, 20) assert manager.c.window.info()["width"] == 100 assert manager.c.window.info()["height"] == 100 assert manager.c.window.info()["x"] == 360 assert manager.c.window.info()["y"] == 260 assert manager.c.window.info()["floating"] is True manager.c.window.set_position_floating(10, 20) assert manager.c.window.info()["width"] == 100 assert manager.c.window.info()["height"] == 100 assert manager.c.window.info()["x"] == 10 assert manager.c.window.info()["y"] == 20 assert manager.c.window.info()["floating"] is True @manager_config def test_last_float_size(manager): """ When you re-float something it would be preferable to have it use the previous float size """ manager.test_window("one") assert manager.c.window.info()["name"] == "one" assert manager.c.window.info()["width"] == 798 assert manager.c.window.info()["height"] == 578 # float and it moves manager.c.window.toggle_floating() assert manager.c.window.info()["width"] == 100 assert manager.c.window.info()["height"] == 100 # resize manager.c.window.set_size_floating(50, 90) assert manager.c.window.info()["width"] == 50 assert manager.c.window.info()["height"] == 90 # back to not floating manager.c.window.toggle_floating() assert manager.c.window.info()["width"] == 798 assert manager.c.window.info()["height"] == 578 # float again, should use last float size manager.c.window.toggle_floating() assert manager.c.window.info()["width"] == 50 assert manager.c.window.info()["height"] == 90 # make sure it works through min and max manager.c.window.toggle_maximize() manager.c.window.toggle_minimize() manager.c.window.toggle_minimize() manager.c.window.toggle_floating() assert manager.c.window.info()["width"] == 50 assert manager.c.window.info()["height"] == 90 @manager_config def test_float_max_min_combo(manager): # change to 2 col stack manager.c.next_layout() assert len(manager.c.layout.info()["stacks"]) == 2 manager.test_window("two") manager.test_window("one") assert manager.c.group.info()["focus"] == "one" assert manager.c.window.info()["width"] == 398 assert manager.c.window.info()["height"] == 578 assert manager.c.window.info()["x"] == 400 assert manager.c.window.info()["y"] == 0 assert manager.c.window.info()["floating"] is False manager.c.window.toggle_maximize() assert manager.c.window.info()["floating"] is True assert manager.c.window.info()["maximized"] is True assert manager.c.window.info()["width"] == 800 assert manager.c.window.info()["height"] == 580 assert manager.c.window.info()["x"] == 0 assert manager.c.window.info()["y"] == 0 manager.c.window.toggle_minimize() assert manager.c.group.info()["focus"] == "one" assert manager.c.window.info()["floating"] is True assert manager.c.window.info()["minimized"] is True assert manager.c.window.info()["width"] == 800 assert manager.c.window.info()["height"] == 580 assert manager.c.window.info()["x"] == 0 assert manager.c.window.info()["y"] == 0 manager.c.window.toggle_floating() assert manager.c.group.info()["focus"] == "one" assert manager.c.window.info()["floating"] is False assert manager.c.window.info()["minimized"] is False assert manager.c.window.info()["maximized"] is False assert manager.c.window.info()["width"] == 398 assert manager.c.window.info()["height"] == 578 assert manager.c.window.info()["x"] == 400 assert manager.c.window.info()["y"] == 0 @manager_config def test_toggle_fullscreen(manager): # change to 2 col stack manager.c.next_layout() assert len(manager.c.layout.info()["stacks"]) == 2 manager.test_window("two") manager.test_window("one") assert manager.c.group.info()["focus"] == "one" assert manager.c.window.info()["width"] == 398 assert manager.c.window.info()["height"] == 578 assert manager.c.window.info()["float_info"] == { "y": 0, "x": 400, "width": 100, "height": 100, } assert manager.c.window.info()["x"] == 400 assert manager.c.window.info()["y"] == 0 manager.c.window.toggle_fullscreen() assert manager.c.window.info()["floating"] is True assert manager.c.window.info()["maximized"] is False assert manager.c.window.info()["fullscreen"] is True assert manager.c.window.info()["width"] == 800 assert manager.c.window.info()["height"] == 600 assert manager.c.window.info()["x"] == 0 assert manager.c.window.info()["y"] == 0 manager.c.window.toggle_fullscreen() assert manager.c.window.info()["floating"] is False assert manager.c.window.info()["maximized"] is False assert manager.c.window.info()["fullscreen"] is False assert manager.c.window.info()["width"] == 398 assert manager.c.window.info()["height"] == 578 assert manager.c.window.info()["x"] == 400 assert manager.c.window.info()["y"] == 0 @manager_config def test_toggle_max(manager): # change to 2 col stack manager.c.next_layout() assert len(manager.c.layout.info()["stacks"]) == 2 manager.test_window("two") manager.test_window("one") assert manager.c.group.info()["focus"] == "one" assert manager.c.window.info()["width"] == 398 assert manager.c.window.info()["height"] == 578 assert manager.c.window.info()["float_info"] == { "y": 0, "x": 400, "width": 100, "height": 100, } assert manager.c.window.info()["x"] == 400 assert manager.c.window.info()["y"] == 0 manager.c.window.toggle_maximize() assert manager.c.window.info()["floating"] is True assert manager.c.window.info()["maximized"] is True assert manager.c.window.info()["width"] == 800 assert manager.c.window.info()["height"] == 580 assert manager.c.window.info()["x"] == 0 assert manager.c.window.info()["y"] == 0 manager.c.window.toggle_maximize() assert manager.c.window.info()["floating"] is False assert manager.c.window.info()["maximized"] is False assert manager.c.window.info()["width"] == 398 assert manager.c.window.info()["height"] == 578 assert manager.c.window.info()["x"] == 400 assert manager.c.window.info()["y"] == 0 @manager_config def test_toggle_min(manager): # change to 2 col stack manager.c.next_layout() assert len(manager.c.layout.info()["stacks"]) == 2 manager.test_window("two") manager.test_window("one") assert manager.c.group.info()["focus"] == "one" assert manager.c.window.info()["width"] == 398 assert manager.c.window.info()["height"] == 578 assert manager.c.window.info()["float_info"] == { "y": 0, "x": 400, "width": 100, "height": 100, } assert manager.c.window.info()["x"] == 400 assert manager.c.window.info()["y"] == 0 manager.c.window.toggle_minimize() assert manager.c.group.info()["focus"] == "one" assert manager.c.window.info()["floating"] is True assert manager.c.window.info()["minimized"] is True assert manager.c.window.info()["width"] == 398 assert manager.c.window.info()["height"] == 578 assert manager.c.window.info()["x"] == 400 assert manager.c.window.info()["y"] == 0 manager.c.window.toggle_minimize() assert manager.c.group.info()["focus"] == "one" assert manager.c.window.info()["floating"] is False assert manager.c.window.info()["minimized"] is False assert manager.c.window.info()["width"] == 398 assert manager.c.window.info()["height"] == 578 assert manager.c.window.info()["x"] == 400 assert manager.c.window.info()["y"] == 0 @manager_config def test_toggle_floating(manager): manager.test_window("one") assert manager.c.window.info()["floating"] is False manager.c.window.toggle_floating() assert manager.c.window.info()["floating"] is True manager.c.window.toggle_floating() assert manager.c.window.info()["floating"] is False manager.c.window.toggle_floating() assert manager.c.window.info()["floating"] is True # change layout (should still be floating) manager.c.next_layout() assert manager.c.window.info()["floating"] is True @manager_config def test_floating_focus(manager): # change to 2 col stack manager.c.next_layout() assert len(manager.c.layout.info()["stacks"]) == 2 manager.test_window("two") manager.test_window("one") assert manager.c.window.info()["width"] == 398 assert manager.c.window.info()["height"] == 578 manager.c.window.toggle_floating() manager.c.window.move_floating(10, 20) assert manager.c.window.info()["name"] == "one" assert manager.c.group.info()["focus"] == "one" # check what stack thinks is focus assert [x["current"] for x in manager.c.layout.info()["stacks"]] == [0, 0] # change focus to "one" manager.c.group.next_window() assert manager.c.window.info()["width"] == 398 assert manager.c.window.info()["height"] == 578 assert manager.c.window.info()["name"] != "one" assert manager.c.group.info()["focus"] != "one" # check what stack thinks is focus # check what stack thinks is focus assert [x["current"] for x in manager.c.layout.info()["stacks"]] == [0, 0] # focus back to one manager.c.group.next_window() assert manager.c.window.info()["name"] == "one" # check what stack thinks is focus assert [x["current"] for x in manager.c.layout.info()["stacks"]] == [0, 0] # now focusing via layout is borked (won't go to float) manager.c.layout.up() assert manager.c.window.info()["name"] != "one" manager.c.layout.up() assert manager.c.window.info()["name"] != "one" # check what stack thinks is focus assert [x["current"] for x in manager.c.layout.info()["stacks"]] == [0, 0] # focus back to one manager.c.group.next_window() assert manager.c.window.info()["name"] == "one" # check what stack thinks is focus assert [x["current"] for x in manager.c.layout.info()["stacks"]] == [0, 0] @manager_config def test_move_floating(manager): manager.test_window("one") # manager.test_window("one") assert manager.c.window.info()["width"] == 798 assert manager.c.window.info()["height"] == 578 assert manager.c.window.info()["x"] == 0 assert manager.c.window.info()["y"] == 0 manager.c.window.toggle_floating() assert manager.c.window.info()["floating"] is True manager.c.window.move_floating(10, 20) assert manager.c.window.info()["width"] == 100 assert manager.c.window.info()["height"] == 100 assert manager.c.window.info()["x"] == 10 assert manager.c.window.info()["y"] == 20 manager.c.window.set_size_floating(50, 90) assert manager.c.window.info()["width"] == 50 assert manager.c.window.info()["height"] == 90 assert manager.c.window.info()["x"] == 10 assert manager.c.window.info()["y"] == 20 manager.c.window.resize_floating(10, 20) assert manager.c.window.info()["width"] == 60 assert manager.c.window.info()["height"] == 110 assert manager.c.window.info()["x"] == 10 assert manager.c.window.info()["y"] == 20 manager.c.window.set_size_floating(10, 20) assert manager.c.window.info()["width"] == 10 assert manager.c.window.info()["height"] == 20 assert manager.c.window.info()["x"] == 10 assert manager.c.window.info()["y"] == 20 # change layout (x, y should be same) manager.c.next_layout() assert manager.c.window.info()["width"] == 10 assert manager.c.window.info()["height"] == 20 assert manager.c.window.info()["x"] == 10 assert manager.c.window.info()["y"] == 20 @manager_config def test_one_screen(manager): assert len(manager.c.get_screens()) == 1 @dualmonitor @manager_config def test_two_screens(manager): assert len(manager.c.get_screens()) == 2 @manager_config def test_focus_stays_on_layout_switch(manager): manager.test_window("one") manager.test_window("two") # switch to a double stack layout manager.c.next_layout() # focus on a different window than the default manager.c.layout.next() # toggle the layout manager.c.next_layout() manager.c.prev_layout() assert manager.c.window.info()["name"] == "one" @pytest.mark.parametrize("manager", [BareConfig, ManagerConfig], indirect=True) def test_map_request(manager): manager.test_window("one") info = manager.c.get_groups()["a"] assert "one" in info["windows"] assert info["focus"] == "one" manager.test_window("two") info = manager.c.get_groups()["a"] assert "two" in info["windows"] assert info["focus"] == "two" @pytest.mark.parametrize("manager", [BareConfig, ManagerConfig], indirect=True) def test_unmap(manager): one = manager.test_window("one") two = manager.test_window("two") three = manager.test_window("three") info = manager.c.get_groups()["a"] assert info["focus"] == "three" assert len(manager.c.windows()) == 3 manager.kill_window(three) assert len(manager.c.windows()) == 2 info = manager.c.get_groups()["a"] assert info["focus"] == "two" manager.kill_window(two) assert len(manager.c.windows()) == 1 info = manager.c.get_groups()["a"] assert info["focus"] == "one" manager.kill_window(one) assert len(manager.c.windows()) == 0 info = manager.c.get_groups()["a"] assert info["focus"] is None @pytest.mark.parametrize("manager", [BareConfig, ManagerConfig], indirect=True) @multimonitor def test_setgroup(manager): manager.test_window("one") manager.c.group["b"].toscreen() manager.groupconsistency() if len(manager.c.get_screens()) == 1: assert manager.c.get_groups()["a"]["screen"] is None else: assert manager.c.get_groups()["a"]["screen"] == 1 assert manager.c.get_groups()["b"]["screen"] == 0 manager.c.group["c"].toscreen() manager.groupconsistency() assert manager.c.get_groups()["c"]["screen"] == 0 # Setting the current group once again switches back to the previous group manager.c.group["c"].toscreen(toggle=True) manager.groupconsistency() assert manager.c.group.info()["name"] == "b" @pytest.mark.parametrize("manager", [BareConfig, ManagerConfig], indirect=True) @multimonitor def test_unmap_noscreen(manager): manager.test_window("one") pid = manager.test_window("two") assert len(manager.c.windows()) == 2 manager.c.group["c"].toscreen() manager.groupconsistency() manager.c.status() assert len(manager.c.windows()) == 2 manager.kill_window(pid) assert len(manager.c.windows()) == 1 assert manager.c.get_groups()["a"]["focus"] == "one" class TScreen(libqtile.config.Screen): group = _Group("") def set_group(self, x, save_prev=True): pass def test_dx(): s = TScreen(left=libqtile.bar.Gap(10)) s._configure(None, 0, 0, 0, 100, 100, None) assert s.dx == 10 def test_dwidth(): s = TScreen(left=libqtile.bar.Gap(10)) s._configure(None, 0, 0, 0, 100, 100, None) assert s.dwidth == 90 s.right = libqtile.bar.Gap(10) assert s.dwidth == 80 def test_dy(): s = TScreen(top=libqtile.bar.Gap(10)) s._configure(None, 0, 0, 0, 100, 100, None) assert s.dy == 10 def test_dheight(): s = TScreen(top=libqtile.bar.Gap(10)) s._configure(None, 0, 0, 0, 100, 100, None) assert s.dheight == 90 s.bottom = libqtile.bar.Gap(10) assert s.dheight == 80 @manager_config def test_labelgroup(manager): manager.c.group["a"].toscreen() assert manager.c.group["a"].info()["label"] == "a" manager.c.labelgroup() manager.c.widget["prompt"].fake_keypress("b") manager.c.widget["prompt"].fake_keypress("Return") assert manager.c.group["a"].info()["label"] == "b" manager.c.labelgroup() manager.c.widget["prompt"].fake_keypress("Return") assert manager.c.group["a"].info()["label"] == "a" @manager_config def test_change_loglevel(manager): assert manager.c.loglevel() == logging.INFO assert manager.c.loglevelname() == "INFO" manager.c.debug() assert manager.c.loglevel() == logging.DEBUG assert manager.c.loglevelname() == "DEBUG" manager.c.info() assert manager.c.loglevel() == logging.INFO assert manager.c.loglevelname() == "INFO" manager.c.warning() assert manager.c.loglevel() == logging.WARNING assert manager.c.loglevelname() == "WARNING" manager.c.error() assert manager.c.loglevel() == logging.ERROR assert manager.c.loglevelname() == "ERROR" manager.c.critical() assert manager.c.loglevel() == logging.CRITICAL assert manager.c.loglevelname() == "CRITICAL" def test_switch_groups_cursor_warp(manager_nospawn): class SwitchGroupsCursorWarpConfig(ManagerConfig): cursor_warp = True layouts = [libqtile.layout.Stack(num_stacks=2), libqtile.layout.Max()] groups = [libqtile.config.Group("a"), libqtile.config.Group("b", layout="max")] manager_nospawn.start(SwitchGroupsCursorWarpConfig) manager_nospawn.test_window("one") manager_nospawn.test_window("two") manager_nospawn.c.layout.previous() assert_focused(manager_nospawn, "one") assert manager_nospawn.c.group.info()["name"] == "a" assert manager_nospawn.c.layout.info()["name"] == "stack" manager_nospawn.c.group["b"].toscreen() manager_nospawn.test_window("three") assert_focused(manager_nospawn, "three") assert manager_nospawn.c.group.info()["name"] == "b" assert manager_nospawn.c.layout.info()["name"] == "max" # do a fast switch to trigger races in focus behavior; unfortunately we # need the window in layout 'b' to map quite slowly (e.g. like firefox or # something), which it does not here most of the time. manager_nospawn.c.group["a"].toscreen() manager_nospawn.c.group["b"].toscreen() manager_nospawn.c.group["a"].toscreen() # make sure the right things are still focused assert_focused(manager_nospawn, "one") assert manager_nospawn.c.group.info()["name"] == "a" assert manager_nospawn.c.layout.info()["name"] == "stack" manager_nospawn.c.group["b"].toscreen() assert_focused(manager_nospawn, "three") assert manager_nospawn.c.group.info()["name"] == "b" assert manager_nospawn.c.layout.info()["name"] == "max" def test_reload_config(manager_nospawn): # The test config uses presence of Qtile.test_data to change config values # Here we just want to check configurables are being updated within the live Qtile manager_nospawn.start(lambda: BareConfig(file_path=configs_dir / "reloading.py")) @Retry(ignore_exceptions=(AssertionError,)) def assert_dd_appeared(): assert "dd" in manager_nospawn.c.group.info()["windows"] # Original config assert manager_nospawn.c.eval("len(self.keys_map)") == (True, "1") assert manager_nospawn.c.eval("len(self._mouse_map)") == (True, "1") assert "".join(manager_nospawn.c.get_groups().keys()) == "12345S" assert len(manager_nospawn.c.group.info()["layouts"]) == 1 assert manager_nospawn.c.widget["clock"].eval("self.background") == (True, "None") screens = manager_nospawn.c.get_screens()[0] assert screens["gaps"]["bottom"][3] == 24 and not screens["gaps"]["top"] assert len(manager_nospawn.c.internal_windows()) == 1 assert manager_nospawn.c.eval("self.dgroups.key_binder") == (True, "None") assert manager_nospawn.c.eval("len(self.dgroups.rules)") == (True, "6") manager_nospawn.test_window("one") assert manager_nospawn.c.window.info()["floating"] is True manager_nospawn.c.window.kill() if manager_nospawn.backend.name == "x11": assert manager_nospawn.c.eval("self.core.wmname") == (True, "LG3D") manager_nospawn.c.group["S"].dropdown_toggle("dropdown1") # Spawn dropdown assert_dd_appeared() manager_nospawn.c.group["S"].dropdown_toggle("dropdown1") # Send it to ScratchPad # Reload #1 - with libqtile.qtile.test_data manager_nospawn.c.eval("self.test_data = 1") manager_nospawn.c.eval("self.test_data_config_evaluations = 0") manager_nospawn.c.reload_config() # should be readed twice (check+read), but no more assert manager_nospawn.c.eval("self.test_data_config_evaluations") == (True, "2") assert manager_nospawn.c.eval("len(self.keys_map)") == (True, "2") assert manager_nospawn.c.eval("len(self._mouse_map)") == (True, "2") assert "".join(manager_nospawn.c.get_groups().keys()) == "123456789S" assert len(manager_nospawn.c.group.info()["layouts"]) == 2 assert manager_nospawn.c.widget["currentlayout"].eval("self.background") == (True, "#ff0000") screens = manager_nospawn.c.get_screens()[0] assert screens["gaps"]["top"][3] == 32 and not screens["gaps"]["bottom"] assert len(manager_nospawn.c.internal_windows()) == 1 _, binder = manager_nospawn.c.eval("self.dgroups.key_binder") assert "function simple_key_binder" in binder assert manager_nospawn.c.eval("len(self.dgroups.rules)") == (True, "11") manager_nospawn.test_window("one") assert manager_nospawn.c.window.info()["floating"] is False manager_nospawn.c.window.kill() if manager_nospawn.backend.name == "x11": assert manager_nospawn.c.eval("self.core.wmname") == (True, "TEST") manager_nospawn.c.group["S"].dropdown_toggle("dropdown2") # Spawn second dropdown assert_dd_appeared() manager_nospawn.c.group["S"].dropdown_toggle("dropdown1") # Send it to ScratchPad assert "dd" in manager_nospawn.c.get_groups()["S"]["windows"] assert "dd" in manager_nospawn.c.get_groups()["S"]["windows"] # Reload #2 - back to without libqtile.qtile.test_data manager_nospawn.c.eval("del self.test_data") manager_nospawn.c.eval("del self.test_data_config_evaluations") manager_nospawn.c.reload_config() assert manager_nospawn.c.eval("len(self.keys_map)") == (True, "1") assert manager_nospawn.c.eval("len(self._mouse_map)") == (True, "1") # The last four groups persist within QtileState assert "".join(manager_nospawn.c.get_groups().keys()) == "12345S" assert len(manager_nospawn.c.group.info()["layouts"]) == 1 assert manager_nospawn.c.widget["clock"].eval("self.background") == (True, "None") screens = manager_nospawn.c.get_screens()[0] assert screens["gaps"]["bottom"][3] == 24 and not screens["gaps"]["top"] assert len(manager_nospawn.c.internal_windows()) == 1 assert manager_nospawn.c.eval("self.dgroups.key_binder") == (True, "None") assert manager_nospawn.c.eval("len(self.dgroups.rules)") == (True, "6") manager_nospawn.test_window("one") assert manager_nospawn.c.window.info()["floating"] is True manager_nospawn.c.window.kill() if manager_nospawn.backend.name == "x11": assert manager_nospawn.c.eval("self.core.wmname") == (True, "LG3D") assert "dd" in manager_nospawn.c.get_groups()["S"]["windows"] # First dropdown persists assert "dd" in manager_nospawn.c.get_groups()["1"]["windows"] # Second orphans to group class CommandsConfig(Config): screens = [ libqtile.config.Screen( bottom=libqtile.bar.Bar([libqtile.widget.Systray()], 20), ) ] @pytest.mark.parametrize("manager", [CommandsConfig], indirect=True) def test_windows_from_commands(manager): manager.test_window("one") assert len(manager.c.items("window")) == 2 # This command returns windows including bars windows = manager.c.windows() # Whereas this one is just regular windows assert len(windows) == 1 # And the Systray is absent assert "TestWindow" in windows[0]["wm_class"] class DuplicateWidgetsConfig(ManagerConfig): screens = [ libqtile.config.Screen( bottom=libqtile.bar.Bar( [ libqtile.widget.Prompt(), libqtile.widget.Prompt(), libqtile.widget.Prompt(), libqtile.widget.Prompt(name="foo"), libqtile.widget.GroupBox(), libqtile.widget.GroupBox(), libqtile.widget.GroupBox(), libqtile.widget.GroupBox(name="foo"), ], 20, ), ) ] duplicate_widgets_config = pytest.mark.parametrize( "manager", [DuplicateWidgetsConfig], indirect=True ) @duplicate_widgets_config def test_widget_duplicate_names(manager): # Verify every widget is in widgets_map _, result = manager.c.eval("len(self.widgets_map)") assert int(result) == len(DuplicateWidgetsConfig.screens[0].bottom.widgets) # Verify renaming in qtile.widgets_map assert manager.c.widget["prompt"] assert manager.c.widget["prompt_1"] assert manager.c.widget["prompt_2"] assert manager.c.widget["groupbox"] assert manager.c.widget["groupbox_1"] assert manager.c.widget["groupbox_2"] assert manager.c.widget["foo"] assert manager.c.widget["foo_1"] # No renaming of actual widgets assert manager.c.bar["bottom"].info()["widgets"][0]["name"] == "prompt" assert manager.c.bar["bottom"].info()["widgets"][1]["name"] == "prompt" assert manager.c.bar["bottom"].info()["widgets"][2]["name"] == "prompt" assert manager.c.bar["bottom"].info()["widgets"][3]["name"] == "foo" assert manager.c.bar["bottom"].info()["widgets"][4]["name"] == "groupbox" assert manager.c.bar["bottom"].info()["widgets"][5]["name"] == "groupbox" assert manager.c.bar["bottom"].info()["widgets"][6]["name"] == "groupbox" assert manager.c.bar["bottom"].info()["widgets"][7]["name"] == "foo" @duplicate_widgets_config def test_widget_duplicate_warnings(manager): records = manager.get_log_buffer().splitlines() # We need to filter out other potential log messages here records = [r for r in records if "The following widgets" in r] assert len(records) == 1 for w in ["prompt_1", "prompt_2", "groupbox_1", "groupbox_2", "foo_1"]: assert w in records[0] # Check this message level was info assert all([r.startswith("INFO") for r in records]) qtile-0.31.0/test/test_images2.py0000664000175000017500000001052014762660347016553 0ustar epsilonepsilon""" test_images2.py tests libqtile.images.Img for rendering quality by comparing known good and bad images to images rendered using Img(). Image similarity / distance is calculated using imagemagick's convert utility. """ import subprocess as sp from collections import namedtuple from glob import glob from os import path import cairocffi import pytest from libqtile import images def get_imagemagick_version(): "Get the installed imagemagick version from the convert utility" try: p = sp.Popen(["convert", "-version"], stdout=sp.PIPE, stderr=sp.PIPE) stdout, stderr = p.communicate() lines = stdout.decode().splitlines() ver_line = [x for x in lines if x.startswith("Version:")] assert len(ver_line) == 1 version = ver_line[0].split()[2] version = version.replace("-", ".") vals = version.split(".") return [int(x) for x in vals] except FileNotFoundError: # we don't have the `convert` binary return (0, 0) def should_skip(): "Check if tests should be skipped due to old imagemagick version." min_version = (6, 8) # minimum imagemagick version try: actual_version = get_imagemagick_version() except AssertionError: return True actual_version = tuple(actual_version[:2]) return actual_version < min_version pytestmark = pytest.mark.skipif(should_skip(), reason="recent version of imagemagick not found") TEST_DIR = path.dirname(path.abspath(__file__)) DATA_DIR = path.join(TEST_DIR, "data") SVGS = glob(path.join(DATA_DIR, "*", "*.svg")) metrics = ("AE", "FUZZ", "MAE", "MEPP", "MSE", "PAE", "PHASH", "RMSE") ImgDistortion = namedtuple("ImgDistortion", metrics) def compare_images(test_img, reference_img, metric="MAE"): """Compare images at paths test_img and reference_img Use imagemagick to calculate distortion using the given metric. You can view the available metrics with 'convert -list metric'. """ cmd = [ "convert", test_img, reference_img, "-metric", metric, "-compare", "-format", "%[distortion]\n", "info:", ] p = sp.Popen(cmd, stdout=sp.PIPE, stderr=sp.PIPE) stdout, stderr = p.communicate() print("stdout", stdout.decode()) print("stderr", stderr.decode()) print("cmd", cmd) return float(stdout.decode().strip()) def compare_images_all_metrics(test_img, reference_img): """Compare images at paths test_img and reference_img Use imagemagick to calculate distortion using all metrics listed as fields in ImgDistortion. """ vals = [] for metric in ImgDistortion._fields: vals.append(compare_images(test_img, reference_img, metric)) return ImgDistortion._make(vals) @pytest.fixture(scope="function", params=SVGS) def svg_img(request): "svg_img returns an instance of libqtile.images.Img()" fpath = request.param return images.Img.from_path(fpath) @pytest.fixture(scope="function") def comparison_images(svg_img): "Return a tuple of paths to the bad and good comparison images, respectively." name = svg_img.name path_good = path.join(DATA_DIR, "comparison_images", name + "_good.png") path_bad = path.join(DATA_DIR, "comparison_images", name + "_bad.png") return path_bad, path_good @pytest.fixture(scope="function") def distortion_bad(svg_img, comparison_images): path_bad, path_good = comparison_images print("comparing:", path_bad, path_good) return compare_images_all_metrics(path_bad, path_good) def assert_distortion_less_than(distortion, bad_distortion, factor=0.3): for test_val, bad_val in zip(distortion, bad_distortion): assert test_val < (bad_val * factor) def test_svg_scaling(svg_img, distortion_bad, comparison_images, tmpdir): path_bad, path_good = comparison_images dpath = tmpdir.dirpath name = svg_img.name svg_img.scale(width_factor=20, lock_aspect_ratio=True) surf = cairocffi.SVGSurface(str(dpath(name + ".svg")), svg_img.width, svg_img.height) ctx = cairocffi.Context(surf) ctx.save() ctx.set_source(svg_img.pattern) ctx.paint() ctx.restore() test_png_path = str(dpath(name + ".png")) surf.write_to_png(test_png_path) surf.finish() distortion = compare_images_all_metrics(test_png_path, path_good) assert_distortion_less_than(distortion, distortion_bad) qtile-0.31.0/test/test_bar.py0000664000175000017500000006315514762660347016004 0ustar epsilonepsilon# Copyright (c) 2011 Florian Mounier # Copyright (c) 2012-2013 Craig Barnes # Copyright (c) 2012 roger # Copyright (c) 2012, 2014-2015 Tycho Andersen # Copyright (c) 2014 Sean Vig # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import os import sys import tempfile from pathlib import Path import pytest import libqtile.bar import libqtile.config import libqtile.confreader import libqtile.layout import libqtile.widget from libqtile.command.base import CommandError from test.conftest import dualmonitor from test.helpers import BareConfig, Retry from test.layouts.layout_utils import assert_focused, assert_unfocused class GBConfig(libqtile.confreader.Config): auto_fullscreen = True keys = [] mouse = [] groups = [ libqtile.config.Group("a"), libqtile.config.Group("bb"), libqtile.config.Group("ccc"), libqtile.config.Group("dddd"), libqtile.config.Group("Pppy"), ] layouts = [libqtile.layout.stack.Stack(num_stacks=1)] floating_layout = libqtile.resources.default_config.floating_layout screens = [ libqtile.config.Screen( top=libqtile.bar.Bar( [ libqtile.widget.CPUGraph( width=libqtile.bar.STRETCH, type="linefill", border_width=20, margin_x=1, margin_y=1, ), libqtile.widget.MemoryGraph(type="line"), libqtile.widget.SwapGraph(type="box"), libqtile.widget.TextBox(name="text", background="333333"), ], 50, ), bottom=libqtile.bar.Bar( [ libqtile.widget.GroupBox(), libqtile.widget.AGroupBox(), libqtile.widget.Prompt(), libqtile.widget.WindowName(), libqtile.widget.Sep(), libqtile.widget.Clock(), ], 50, ), # TODO: Add vertical bars and test widgets that support them ) ] gb_config = pytest.mark.parametrize("manager", [GBConfig], indirect=True) def test_completion(): c = libqtile.widget.prompt.CommandCompleter(None, True) c.reset() c.lookup = [ ("a", "x/a"), ("aa", "x/aa"), ] assert c.complete("a") == "a" assert c.actual() == "x/a" assert c.complete("a") == "aa" assert c.complete("a") == "a" c = libqtile.widget.prompt.CommandCompleter(None) r = c.complete("l") assert c.actual().endswith(r) c.reset() assert c.complete("/et") == "/etc/" c.reset() assert c.complete("/etc") != "/etc/" c.reset() home_dir = os.path.expanduser("~") with tempfile.TemporaryDirectory(prefix="qtile_test_", dir=home_dir) as absolute_tmp_path: tmp_dirname = absolute_tmp_path[len(home_dir + os.sep) :] user_input = os.path.join("~", tmp_dirname) assert c.complete(user_input) == user_input c.reset() test_bin_dir = os.path.join(absolute_tmp_path, "qtile-test-bin") os.mkdir(test_bin_dir) assert c.complete(user_input) == os.path.join(user_input, "qtile-test-bin") + os.sep c.reset() s = "thisisatotallynonexistantpathforsure" assert c.complete(s) == s assert c.actual() == s c.reset() assert c.complete("z", aliases={"z": "a"}) == "z" assert c.actual() == "a" c.reset() assert c.complete("/et", aliases={"z": "a"}) == "/etc/" assert c.actual() == "/etc" c.reset() @gb_config def test_draw(manager): manager.test_window("one") b = manager.c.bar["bottom"].info() assert b["widgets"][0]["name"] == "groupbox" @gb_config def test_prompt(manager, monkeypatch): manager.test_window("one") assert_focused(manager, "one") assert manager.c.widget["prompt"].info()["width"] == 0 manager.c.spawncmd(":") manager.c.widget["prompt"].fake_keypress("a") manager.c.widget["prompt"].fake_keypress("Tab") manager.c.spawncmd(":") manager.c.widget["prompt"].fake_keypress("slash") manager.c.widget["prompt"].fake_keypress("Tab") script = Path(__file__).parent / "scripts" / "window.py" manager.c.spawncmd(":", aliases={"w": f"{sys.executable} {script.as_posix()}"}) manager.c.widget["prompt"].fake_keypress("w") manager.test_window("two") assert_unfocused(manager, "two") manager.c.widget["prompt"].fake_keypress("Return") assert_focused(manager, "one") @Retry(ignore_exceptions=(CommandError,)) def is_spawned(): return manager.c.window.info() is_spawned() @gb_config def test_event(manager): manager.c.group["bb"].toscreen() @gb_config def test_textbox(manager): assert "text" in manager.c.list_widgets() s = "some text" manager.c.widget["text"].update(s) assert manager.c.widget["text"].get() == s s = "Aye, much longer string than the initial one" manager.c.widget["text"].update(s) assert manager.c.widget["text"].get() == s manager.c.group["Pppy"].toscreen() manager.c.widget["text"].set_font(fontsize=12) @gb_config def test_textbox_errors(manager): manager.c.widget["text"].update(None) manager.c.widget["text"].update("".join(chr(i) for i in range(255))) manager.c.widget["text"].update("V\xe2r\xe2na\xe7\xee") manager.c.widget["text"].update("\ua000") @gb_config def test_groupbox_button_press(manager): manager.c.group["ccc"].toscreen() assert manager.c.get_groups()["a"]["screen"] is None manager.c.bar["bottom"].fake_button_press(10, 10, 1) assert manager.c.get_groups()["a"]["screen"] == 0 class GeomConf(libqtile.confreader.Config): auto_fullscreen = False keys = [] mouse = [] groups = [ libqtile.config.Group("a"), libqtile.config.Group("b"), libqtile.config.Group("c"), libqtile.config.Group("d"), ] layouts = [libqtile.layout.stack.Stack(num_stacks=1)] floating_layout = libqtile.resources.default_config.floating_layout screens = [ libqtile.config.Screen( top=libqtile.bar.Bar([], 10), bottom=libqtile.bar.Bar([], 10), left=libqtile.bar.Bar([], 10), right=libqtile.bar.Bar([], 10), ) ] geom_config = pytest.mark.parametrize("manager", [GeomConf], indirect=True) class DBarH(libqtile.bar.Bar): def __init__(self, widgets, size): libqtile.bar.Bar.__init__(self, widgets, size) self.horizontal = True class DBarV(libqtile.bar.Bar): def __init__(self, widgets, size): libqtile.bar.Bar.__init__(self, widgets, size) self.horizontal = False class DWidget: def __init__(self, length, length_type): self.length, self.length_type = length, length_type @geom_config def test_geometry(manager): manager.test_window("one") g = manager.c.get_screens()[0]["gaps"] assert g["top"] == (0, 0, 800, 10) assert g["bottom"] == (0, 590, 800, 10) assert g["left"] == (0, 10, 10, 580) assert g["right"] == (790, 10, 10, 580) assert len(manager.c.windows()) == 1 geom = manager.c.windows()[0] assert geom["x"] == 10 assert geom["y"] == 10 assert geom["width"] == 778 assert geom["height"] == 578 internal = manager.c.internal_windows() assert len(internal) == 4 @geom_config def test_resize(manager): def wd(dwidget_list): return [i.length for i in dwidget_list] def offx(dwidget_list): return [i.offsetx for i in dwidget_list] def offy(dwidget_list): return [i.offsety for i in dwidget_list] for DBar, off in ((DBarH, offx), (DBarV, offy)): # noqa: N806 b = DBar([], 100) dwidget_list = [ DWidget(10, libqtile.bar.CALCULATED), DWidget(None, libqtile.bar.STRETCH), DWidget(None, libqtile.bar.STRETCH), DWidget(10, libqtile.bar.CALCULATED), ] b._resize(100, dwidget_list) assert wd(dwidget_list) == [10, 40, 40, 10] assert off(dwidget_list) == [0, 10, 50, 90] b._resize(101, dwidget_list) assert wd(dwidget_list) == [10, 41, 40, 10] assert off(dwidget_list) == [0, 10, 51, 91] dwidget_list = [DWidget(10, libqtile.bar.CALCULATED)] b._resize(100, dwidget_list) assert wd(dwidget_list) == [10] assert off(dwidget_list) == [0] dwidget_list = [DWidget(10, libqtile.bar.CALCULATED), DWidget(None, libqtile.bar.STRETCH)] b._resize(100, dwidget_list) assert wd(dwidget_list) == [10, 90] assert off(dwidget_list) == [0, 10] dwidget_list = [ DWidget(None, libqtile.bar.STRETCH), DWidget(10, libqtile.bar.CALCULATED), ] b._resize(100, dwidget_list) assert wd(dwidget_list) == [90, 10] assert off(dwidget_list) == [0, 90] dwidget_list = [ DWidget(10, libqtile.bar.CALCULATED), DWidget(None, libqtile.bar.STRETCH), DWidget(10, libqtile.bar.CALCULATED), ] b._resize(100, dwidget_list) assert wd(dwidget_list) == [10, 80, 10] assert off(dwidget_list) == [0, 10, 90] class ExampleWidget(libqtile.widget.base._Widget): orientations = libqtile.widget.base.ORIENTATION_HORIZONTAL def __init__(self): libqtile.widget.base._Widget.__init__(self, 10) def draw(self): pass class BrokenWidget(libqtile.widget.base._Widget): def __init__(self, exception_class, **config): libqtile.widget.base._Widget.__init__(self, 10, **config) self.exception_class = exception_class def _configure(self, qtile, bar): raise self.exception_class def test_basic(manager_nospawn): config = GeomConf config.screens = [ libqtile.config.Screen( bottom=libqtile.bar.Bar( [ ExampleWidget(), libqtile.widget.Spacer(libqtile.bar.STRETCH), ExampleWidget(), libqtile.widget.Spacer(libqtile.bar.STRETCH), ExampleWidget(), libqtile.widget.Spacer(libqtile.bar.STRETCH), ExampleWidget(), ], 10, ) ) ] manager_nospawn.start(config) i = manager_nospawn.c.bar["bottom"].info() assert i["widgets"][0]["offset"] == 0 assert i["widgets"][1]["offset"] == 10 assert i["widgets"][1]["width"] == 252 assert i["widgets"][2]["offset"] == 262 assert i["widgets"][3]["offset"] == 272 assert i["widgets"][3]["width"] == 256 assert i["widgets"][4]["offset"] == 528 assert i["widgets"][5]["offset"] == 538 assert i["widgets"][5]["width"] == 252 assert i["widgets"][6]["offset"] == 790 libqtile.hook.clear() def test_singlespacer(manager_nospawn): config = GeomConf config.screens = [ libqtile.config.Screen( bottom=libqtile.bar.Bar( [ libqtile.widget.Spacer(libqtile.bar.STRETCH), ], 10, ) ) ] manager_nospawn.start(config) i = manager_nospawn.c.bar["bottom"].info() assert i["widgets"][0]["offset"] == 0 assert i["widgets"][0]["width"] == 800 libqtile.hook.clear() def test_nospacer(manager_nospawn): config = GeomConf config.screens = [ libqtile.config.Screen(bottom=libqtile.bar.Bar([ExampleWidget(), ExampleWidget()], 10)) ] manager_nospawn.start(config) i = manager_nospawn.c.bar["bottom"].info() assert i["widgets"][0]["offset"] == 0 assert i["widgets"][1]["offset"] == 10 libqtile.hook.clear() def test_consecutive_spacer(manager_nospawn): config = GeomConf config.screens = [ libqtile.config.Screen( bottom=libqtile.bar.Bar( [ ExampleWidget(), # Left libqtile.widget.Spacer(libqtile.bar.STRETCH), libqtile.widget.Spacer(libqtile.bar.STRETCH), ExampleWidget(), # Centre ExampleWidget(), libqtile.widget.Spacer(libqtile.bar.STRETCH), ExampleWidget(), # Right ], 10, ) ) ] manager_nospawn.start(config) i = manager_nospawn.c.bar["bottom"].info() assert i["widgets"][0]["offset"] == 0 assert i["widgets"][0]["width"] == 10 assert i["widgets"][1]["offset"] == 10 assert i["widgets"][1]["width"] == 190 assert i["widgets"][2]["offset"] == 200 assert i["widgets"][2]["width"] == 190 assert i["widgets"][3]["offset"] == 390 assert i["widgets"][3]["width"] == 10 assert i["widgets"][4]["offset"] == 400 assert i["widgets"][4]["width"] == 10 assert i["widgets"][5]["offset"] == 410 assert i["widgets"][5]["width"] == 380 assert i["widgets"][6]["offset"] == 790 assert i["widgets"][6]["width"] == 10 libqtile.hook.clear() def test_configure_broken_widgets(manager_nospawn): config = GeomConf widget_list = [ BrokenWidget(ValueError), BrokenWidget(IndexError), BrokenWidget(IndentationError), BrokenWidget(TypeError), BrokenWidget(NameError), BrokenWidget(ImportError), libqtile.widget.Spacer(libqtile.bar.STRETCH), ] config.screens = [libqtile.config.Screen(bottom=libqtile.bar.Bar(widget_list, 10))] manager_nospawn.start(config) i = manager_nospawn.c.bar["bottom"].info() # Check that we have same number of widgets assert len(i["widgets"]) == len(widget_list) # Check each broken widget is replaced for index, widget in enumerate(widget_list): if isinstance(widget, BrokenWidget): assert i["widgets"][index]["name"] == "configerrorwidget" def test_bar_hide_show_with_margin(manager_nospawn): """Check : - the height of a horizontal bar with its margins, - the ordinate of a unique window. after 3 successive actions : - creation - hidding the bar - unhidding the bar """ config = GeomConf config.screens = [libqtile.config.Screen(top=libqtile.bar.Bar([], 12, margin=[5, 5, 5, 5]))] manager_nospawn.start(config) manager_nospawn.test_window("w") assert manager_nospawn.c.bar["top"].info().get("size") == 22 assert manager_nospawn.c.windows()[0]["y"] == 22 manager_nospawn.c.hide_show_bar("top") assert manager_nospawn.c.bar["top"].info().get("size") == 0 assert manager_nospawn.c.windows()[0]["y"] == 0 manager_nospawn.c.hide_show_bar("top") assert manager_nospawn.c.bar["top"].info().get("size") == 22 assert manager_nospawn.c.windows()[0]["y"] == 22 @pytest.mark.parametrize( "position,dimensions", [ ("all", (0, 0, 800, 600)), ("top", (10, 0, 800 - (2 * 10), 600 - 10)), ("bottom", (10, 10, 800 - (2 * 10), 600 - 10)), ("left", (0, 10, 800 - 10, 600 - (2 * 10))), ("right", (10, 10, 800 - 10, 600 - (2 * 10))), ], ) def test_bar_hide_show_single_screen(manager_nospawn, position, dimensions): conf = GeomConf conf.layouts = [libqtile.layout.Max()] conf.screens = [ libqtile.config.Screen( top=libqtile.bar.Bar([], 10), bottom=libqtile.bar.Bar([], 10), left=libqtile.bar.Bar([], 10), right=libqtile.bar.Bar([], 10), ) ] manager_nospawn.start(conf) # Dimensions of window with all 4 bars visible default_dimensions = (10, 10, 800 - 2 * 10, 600 - 2 * 10) def assert_dimensions(d=default_dimensions): win_info = manager_nospawn.c.window.info() win_x = win_info["x"] win_y = win_info["y"] win_w = win_info["width"] win_h = win_info["height"] assert (win_x, win_y, win_w, win_h) == d manager_nospawn.test_window("one") assert_dimensions() # Hide bar manager_nospawn.c.hide_show_bar(position=position) assert_dimensions(dimensions) # Show bar manager_nospawn.c.hide_show_bar(position=position) assert_dimensions() @dualmonitor @pytest.mark.parametrize( "position,dimensions", [ ("all", (0, 0, 800, 600)), ("top", (10, 0, 800 - (2 * 10), 600 - 10)), ("bottom", (10, 10, 800 - (2 * 10), 600 - 10)), ("left", (0, 10, 800 - 10, 600 - (2 * 10))), ("right", (10, 10, 800 - 10, 600 - (2 * 10))), ], ) def test_bar_hide_show_dual_screen(manager_nospawn, position, dimensions): conf = GeomConf conf.layouts = [libqtile.layout.Max()] conf.screens = [ libqtile.config.Screen( top=libqtile.bar.Bar([], 10), bottom=libqtile.bar.Bar([], 10), left=libqtile.bar.Bar([], 10), right=libqtile.bar.Bar([], 10), ), libqtile.config.Screen( top=libqtile.bar.Bar([], 10), bottom=libqtile.bar.Bar([], 10), left=libqtile.bar.Bar([], 10), right=libqtile.bar.Bar([], 10), ), ] manager_nospawn.start(conf) # Dimensions of window with all 4 bars visible default_dimensions = (10, 10, 800 - 2 * 10, 600 - 2 * 10) def assert_dimensions(screen=0, d=default_dimensions): win_info = manager_nospawn.c.screen[screen].window.info() win_x = win_info["x"] win_y = win_info["y"] win_w = win_info["width"] win_h = win_info["height"] # Second screen is 600x480 @ x=800,y=0 # Adjust dimensions for this if screen == 1: d = (d[0] + 800, d[1], d[2] - (800 - 640), d[3] - (600 - 480)) assert (win_x, win_y, win_w, win_h) == d manager_nospawn.test_window("one") manager_nospawn.c.to_screen(1) manager_nospawn.test_window("two") assert_dimensions(screen=0) assert_dimensions(screen=1) # Test current screen # Screen 0 - hidden, Screen 1 - shown manager_nospawn.c.to_screen(0) manager_nospawn.c.hide_show_bar(position=position) assert_dimensions(screen=0, d=dimensions) assert_dimensions(screen=1) # Screen 0 - hidden, Screen 1 - hidden manager_nospawn.c.to_screen(1) manager_nospawn.c.hide_show_bar(position=position) assert_dimensions(screen=0, d=dimensions) assert_dimensions(screen=1, d=dimensions) # Screen 0 - hidden, Screen 1 - shown manager_nospawn.c.hide_show_bar(position=position) assert_dimensions(screen=0, d=dimensions) assert_dimensions(screen=1) # Screen 0 - shown, Screen 1 - shown manager_nospawn.c.to_screen(0) manager_nospawn.c.hide_show_bar(position=position) assert_dimensions(screen=0) assert_dimensions(screen=1) # Test all screens # Screen 0 - hidden, Screen 1 - hidden manager_nospawn.c.hide_show_bar(position=position, screen="all") assert_dimensions(screen=0, d=dimensions) assert_dimensions(screen=1, d=dimensions) # Screen 0 - shown, Screen 1 - shown manager_nospawn.c.hide_show_bar(position=position, screen="all") assert_dimensions(screen=0) assert_dimensions(screen=1) def test_bar_border_horizontal(manager_nospawn): config = GeomConf config.screens = [ libqtile.config.Screen( top=libqtile.bar.Bar( [libqtile.widget.Spacer()], 12, margin=5, border_width=5, ), bottom=libqtile.bar.Bar( [libqtile.widget.Spacer()], 12, margin=5, border_width=0, ), ) ] manager_nospawn.start(config) top_info = manager_nospawn.c.bar["top"].info bottom_info = manager_nospawn.c.bar["bottom"].info # Screen is 800px wide so: # -top bar should have width of 800 - 5 - 5 - 5 - 5 = 780 (margin and border) # -bottom bar should have width of 800 - 5 - 5 = 790 (margin and no border) assert top_info()["width"] == 780 assert bottom_info()["width"] == 790 # Bar "height" should still be the value set in the config but "size" is # adjusted for margin and border: # -top bar should have size of 12 + 5 + 5 + 5 + 5 = 32 (margin and border) # -bottom bar should have size of 12 + 5 + 5 = 22 (margin and border) assert top_info()["height"] == 12 assert top_info()["size"] == 32 assert bottom_info()["height"] == 12 assert bottom_info()["size"] == 22 # Test widget offsets # Where there is a border, widget should be offset by that amount _, xoffset = manager_nospawn.c.bar["top"].eval("self.widgets[0].offsetx") assert xoffset == "5" _, yoffset = manager_nospawn.c.bar["top"].eval("self.widgets[0].offsety") assert xoffset == "5" # Where there is no border, this should be 0 _, xoffset = manager_nospawn.c.bar["bottom"].eval("self.widgets[0].offsetx") assert xoffset == "0" _, yoffset = manager_nospawn.c.bar["bottom"].eval("self.widgets[0].offsety") assert xoffset == "0" def test_bar_border_vertical(manager_nospawn): config = GeomConf config.screens = [ libqtile.config.Screen( left=libqtile.bar.Bar( [libqtile.widget.Spacer()], 12, margin=5, border_width=5, ), right=libqtile.bar.Bar( [libqtile.widget.Spacer()], 12, margin=5, border_width=0, ), ) ] manager_nospawn.start(config) left_info = manager_nospawn.c.bar["left"].info right_info = manager_nospawn.c.bar["right"].info # Screen is 600px tall so: # -left bar should have height of 600 - 5 - 5 - 5 - 5 = 580 (margin and border) # -right bar should have height of 600 - 5 - 5 = 590 (margin and no border) assert left_info()["height"] == 580 assert right_info()["height"] == 590 # Bar "width" should still be the value set in the config but "size" is # adjusted for margin and border: # -left bar should have size of 12 + 5 + 5 + 5 + 5 = 32 (margin and border) # -right bar should have size of 12 + 5 + 5 = 22 (margin and border) assert left_info()["width"] == 12 assert left_info()["size"] == 32 assert right_info()["width"] == 12 assert right_info()["size"] == 22 # Test widget offsets # Where there is a border, widget should be offset by that amount _, xoffset = manager_nospawn.c.bar["left"].eval("self.widgets[0].offsetx") assert xoffset == "5" _, yoffset = manager_nospawn.c.bar["left"].eval("self.widgets[0].offsety") assert xoffset == "5" # Where there is no border, this should be 0 _, xoffset = manager_nospawn.c.bar["right"].eval("self.widgets[0].offsetx") assert xoffset == "0" _, yoffset = manager_nospawn.c.bar["right"].eval("self.widgets[0].offsety") assert xoffset == "0" def test_unsupported_widget(manager_nospawn): """Widgets on unsupported backends should be removed silently from the bar.""" class UnsupportedWidget(libqtile.widget.TextBox): if manager_nospawn.backend.name == "x11": supported_backends = {"wayland"} elif manager_nospawn.backend.name == "wayland": supported_backends = {"x11"} else: pytest.skip("Unknown backend") class UnsupportedConfig(BareConfig): screens = [libqtile.config.Screen(top=libqtile.bar.Bar([UnsupportedWidget()], 20))] manager_nospawn.start(UnsupportedConfig) assert len(manager_nospawn.c.bar["top"].info()["widgets"]) == 0 @pytest.fixture def no_reserve_manager(manager_nospawn, request): position = getattr(request, "param", "top") class DontReserveBarConfig(GBConfig): screens = [ libqtile.config.Screen( **{position: libqtile.bar.Bar([libqtile.widget.Spacer()], 50, reserve=False)}, ) ] layouts = [libqtile.layout.max.Max()] manager_nospawn.start(DontReserveBarConfig) manager_nospawn.bar_position = position yield manager_nospawn @pytest.mark.parametrize( "no_reserve_manager,bar_x,bar_y,bar_w,bar_h", [ ("top", 0, 0, 800, 50), ("bottom", 0, 550, 800, 50), ("left", 0, 0, 50, 600), ("right", 750, 0, 50, 600), ], indirect=["no_reserve_manager"], ) def test_dont_reserve_bar(no_reserve_manager, bar_x, bar_y, bar_w, bar_h): """Bar is drawn over tiled windows.""" manager = no_reserve_manager manager.test_window("Window") info = manager.c.window.info() # Window should fill entire screen assert info["x"] == 0 assert info["y"] == 0 assert info["width"] == 800 assert info["height"] == 600 bar = manager.c.bar[manager.bar_position] bar_info = bar.info() _, x = bar.eval("self.x") _, y = bar.eval("self.y") assert bar_x == int(x) assert bar_y == int(y) assert bar_w == bar_info["width"] assert bar_h == bar_info["height"] qtile-0.31.0/test/test_command.py0000664000175000017500000003542214762660347016652 0ustar epsilonepsilon# Copyright (c) 2011 Florian Mounier # Copyright (c) 2012, 2014 Tycho Andersen # Copyright (c) 2013 Craig Barnes # Copyright (c) 2014 Sean Vig # Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import asyncio import pytest import libqtile.bar import libqtile.config import libqtile.confreader import libqtile.layout import libqtile.log_utils import libqtile.widget from libqtile.command.base import CommandObject, expose_command from libqtile.command.client import CommandClient from libqtile.command.interface import CommandError, IPCCommandInterface from libqtile.confreader import Config from libqtile.ipc import Client, IPCError from libqtile.lazy import lazy from test.conftest import dualmonitor from test.helpers import Retry class CallConfig(Config): keys = [ libqtile.config.Key( ["control"], "j", lazy.layout.down(), ), libqtile.config.Key( ["control"], "k", lazy.layout.up(), ), ] mouse = [] groups = [ libqtile.config.Group("a"), libqtile.config.Group("b"), ] layouts = [ libqtile.layout.Stack(num_stacks=1), libqtile.layout.Max(), ] floating_layout = libqtile.resources.default_config.floating_layout screens = [ libqtile.config.Screen( bottom=libqtile.bar.Bar( [ libqtile.widget.GroupBox(), libqtile.widget.TextBox(), ], 20, ), ) ] auto_fullscreen = True call_config = pytest.mark.parametrize("manager", [CallConfig], indirect=True) @call_config def test_layout_filter(manager): manager.test_window("one") manager.test_window("two") assert manager.c.get_groups()["a"]["focus"] == "two" manager.c.simulate_keypress(["control"], "j") assert manager.c.get_groups()["a"]["focus"] == "one" manager.c.simulate_keypress(["control"], "k") assert manager.c.get_groups()["a"]["focus"] == "two" @call_config def test_param_hoisting(manager): manager.test_window("two") client = Client(manager.sockfile) command = IPCCommandInterface(client) cmd_client = CommandClient(command) # 'zomg' is not a valid warp command with pytest.raises(IPCError): cmd_client.navigate("window", None).call("focus", warp="zomg", lifted=True) cmd_client.navigate("window", None).call("focus", warp=False, lifted=True) # 'zomg' is not a valid bar position with pytest.raises(IPCError): cmd_client.call("hide_show_bar", position="zomg", lifted=True) cmd_client.call("hide_show_bar", position="top", lifted=True) # 'zomg' is not a valid font size with pytest.raises(IPCError): cmd_client.navigate("widget", "textbox").call("set_font", fontsize="zomg", lifted=True) cmd_client.navigate("widget", "textbox").call("set_font", fontsize=12, lifted=True) class FakeCommandObject(CommandObject): @staticmethod @expose_command() def one(): pass @expose_command() def one_self(self): pass @expose_command() def two(self, a): pass @expose_command() def three(self, a, b=99): pass def _items(self, name): return None def _select(self, name, sel): return None def test_doc(): c = FakeCommandObject() assert "one()" in c.doc("one") assert "one_self()" in c.doc("one_self") assert "two(a)" in c.doc("two") assert "three(a, b=99)" in c.doc("three") def test_commands(): c = FakeCommandObject() assert len(c.commands()) == 9 def test_command(): c = FakeCommandObject() assert c.command("one") assert not c.command("nonexistent") class DecoratedTextBox(libqtile.widget.TextBox): @expose_command("mapped") def exposed(self): return "OK" class ServerConfig(Config): auto_fullscreen = True keys = [] mouse = [] groups = [ libqtile.config.Group("a"), libqtile.config.Group("b"), libqtile.config.Group("c"), ] layouts = [ libqtile.layout.Stack(num_stacks=1), libqtile.layout.Stack(num_stacks=2), libqtile.layout.Stack(num_stacks=3), ] floating_layout = libqtile.resources.default_config.floating_layout screens = [ libqtile.config.Screen( bottom=libqtile.bar.Bar( [ libqtile.widget.TextBox(name="one"), ], 20, ), ), libqtile.config.Screen( bottom=libqtile.bar.Bar( [ DecoratedTextBox(name="two"), ], 20, ), ), ] server_config = pytest.mark.parametrize("manager", [ServerConfig], indirect=True) @server_config def test_cmd_commands(manager): assert manager.c.commands() assert manager.c.layout.commands() assert manager.c.screen.bar["bottom"].commands() @server_config def test_cmd_eval_namespace(manager): assert manager.c.eval("__name__") == (True, "libqtile.core.manager") @server_config def test_call_unknown(manager): with pytest.raises(libqtile.command.client.SelectError, match="Not valid child or command"): manager.c.nonexistent manager.c.layout with pytest.raises(libqtile.command.client.SelectError, match="Not valid child or command"): manager.c.layout.nonexistent @dualmonitor @server_config def test_items_qtile(manager): v = manager.c.items("group") assert v[0] assert sorted(v[1]) == ["a", "b", "c"] assert manager.c.items("layout") == (True, [0, 1, 2]) v = manager.c.items("widget") assert not v[0] assert sorted(v[1]) == ["one", "two"] assert manager.c.items("bar") == (False, ["bottom"]) t, lst = manager.c.items("window") assert t assert len(lst) == 2 assert manager.c.window[lst[0]] assert manager.c.items("screen") == (True, [0, 1]) @dualmonitor @server_config def test_select_qtile(manager): assert manager.c.layout.info()["group"] == "a" assert len(manager.c.layout.info()["stacks"]) == 1 assert len(manager.c.layout[2].info()["stacks"]) == 3 with pytest.raises(libqtile.command.client.SelectError, match="Item not available in object"): manager.c.layout[99] assert manager.c.group.info()["name"] == "a" assert manager.c.group["c"].info()["name"] == "c" with pytest.raises(libqtile.command.client.SelectError, match="Item not available in object"): manager.c.group["nonexistent"] assert manager.c.widget["one"].info()["name"] == "one" with pytest.raises(CommandError, match="No object widget"): manager.c.widget.info() assert manager.c.bar["bottom"].info()["position"] == "bottom" manager.test_window("one") wid = manager.c.window.info()["id"] assert manager.c.window[wid].info()["id"] == wid assert manager.c.screen.info()["index"] == 0 assert manager.c.screen[1].info()["index"] == 1 with pytest.raises(libqtile.command.client.SelectError, match="Item not available in object"): manager.c.screen[22] @server_config def test_items_group(manager): group = manager.c.group manager.test_window("test") wid = manager.c.window.info()["id"] assert group.items("window") == (True, [wid]) assert group.items("layout") == (True, [0, 1, 2]) assert group.items("screen") == (True, []) @dualmonitor @server_config def test_select_group(manager): group = manager.c.group assert group.layout.info()["group"] == "a" assert len(group.layout.info()["stacks"]) == 1 assert len(group.layout[2].info()["stacks"]) == 3 with pytest.raises(CommandError): manager.c.group.window.info() manager.test_window("test") wid = manager.c.window.info()["id"] assert group.window.info()["id"] == wid assert group.window[wid].info()["id"] == wid with pytest.raises(libqtile.command.client.SelectError, match="Item not available in object"): group.window["foo"] assert group.screen.info()["index"] == 0 assert group["b"].screen.info()["index"] == 1 with pytest.raises(libqtile.command.client.SelectError, match="Item not available in object"): group.screen[0] @server_config def test_items_screen(manager): s = manager.c.screen assert s.items("layout") == (True, [0, 1, 2]) manager.test_window("test") wid = manager.c.window.info()["id"] assert s.items("window") == (True, [wid]) assert s.items("bar") == (False, ["bottom"]) @server_config def test_select_screen(manager): screen = manager.c.screen assert screen.layout.info()["group"] == "a" assert len(screen.layout.info()["stacks"]) == 1 assert len(screen.layout[2].info()["stacks"]) == 3 with pytest.raises(CommandError): manager.c.window.info() manager.test_window("test") wid = manager.c.window.info()["id"] assert screen.window.info()["id"] == wid assert screen.window[wid].info()["id"] == wid with pytest.raises(CommandError, match="No object"): screen.bar.info() with pytest.raises(libqtile.command.client.SelectError, match="Item not available in object"): screen.bar["top"] assert screen.bar["bottom"].info()["position"] == "bottom" @server_config def test_items_bar(manager): assert manager.c.bar["bottom"].items("screen") == (True, []) @dualmonitor @server_config def test_select_bar(manager): assert manager.c.screen[1].bar["bottom"].screen.info()["index"] == 1 b = manager.c.bar assert b["bottom"].screen.info()["index"] == 0 with pytest.raises(CommandError): b.screen.info() @server_config def test_items_layout(manager): assert manager.c.layout.items("screen") == (True, []) assert manager.c.layout.items("group") == (True, []) @server_config def test_select_layout(manager): layout = manager.c.layout assert layout.screen.info()["index"] == 0 with pytest.raises(libqtile.command.client.SelectError, match="Item not available in object"): layout.screen[0] assert layout.group.info()["name"] == "a" with pytest.raises(libqtile.command.client.SelectError, match="Item not available in object"): layout.group["a"] @dualmonitor @server_config def test_items_window(manager): manager.test_window("test") window = manager.c.window window.info()["id"] assert window.items("group") == (True, []) assert window.items("layout") == (True, [0, 1, 2]) assert window.items("screen") == (True, []) @dualmonitor @server_config def test_select_window(manager): manager.test_window("test") window = manager.c.window window.info()["id"] assert window.group.info()["name"] == "a" with pytest.raises(libqtile.command.client.SelectError, match="Item not available in object"): window.group["a"] assert len(window.layout.info()["stacks"]) == 1 assert len(window.layout[1].info()["stacks"]) == 2 assert window.screen.info()["index"] == 0 with pytest.raises(libqtile.command.client.SelectError, match="Item not available in object"): window.screen[0] @server_config def test_items_widget(manager): assert manager.c.widget["one"].items("bar") == (True, []) @server_config def test_select_widget(manager): widget = manager.c.widget["one"] assert widget.bar.info()["position"] == "bottom" with pytest.raises(libqtile.command.client.SelectError, match="Item not available in object"): widget.bar["bottom"] def test_core_node(manager, backend_name): assert manager.c.core.info()["backend"] == backend_name def test_lazy_arguments(manager_nospawn): # Decorated function to be bound to key presses @lazy.function def test_func(qtile, value, multiplier=1): qtile.test_func_output = value * multiplier config = ServerConfig config.keys = [ libqtile.config.Key( ["control"], "j", test_func(10), ), libqtile.config.Key(["control"], "k", test_func(5, multiplier=100)), ] manager_nospawn.start(config) manager_nospawn.c.simulate_keypress(["control"], "j") _, val = manager_nospawn.c.eval("self.test_func_output") assert val == "10" manager_nospawn.c.simulate_keypress(["control"], "k") _, val = manager_nospawn.c.eval("self.test_func_output") assert val == "500" def test_lazy_function_coroutine(manager_nospawn): """Test that lazy.function accepts coroutines.""" @Retry(ignore_exceptions=(AssertionError,)) def assert_func_text(manager, value): _, text = manager.c.eval("self.test_func_output") assert text == value @lazy.function async def test_async_func(qtile, value): await asyncio.sleep(0.1) qtile.test_func_output = value config = ServerConfig config.keys = [libqtile.config.Key(["control"], "k", test_async_func("qtile"))] manager_nospawn.start(config) manager_nospawn.c.simulate_keypress(["control"], "k") assert_func_text(manager_nospawn, "qtile") def test_decorators_direct_call(): widget = DecoratedTextBox() undecorated = libqtile.widget.TextBox() cmds = ["exposed", "mapped"] for cmd in cmds: # Check new command is exposed assert cmd in widget.commands() # Check commands are not in widget with same parent class assert cmd not in undecorated.commands() assert widget.exposed() == "OK" assert widget.mapped() == "OK" def test_decorators_deprecated_direct_call(): widget = DecoratedTextBox() assert widget.cmd_exposed() == "OK" def test_decorators_deprecated_method(): class CmdWidget(libqtile.widget.TextBox): def cmd_exposed(self): pass assert "exposed" in CmdWidget().commands() @dualmonitor @server_config def test_decorators_manager_call(manager): widget = manager.c.widget["two"] assert widget.exposed() == "OK" assert widget.mapped() == "OK" qtile-0.31.0/test/test_scratchpad.py0000664000175000017500000003141014762660347017341 0ustar epsilonepsilon# Copyright (c) 2017 Dirk Hartmann # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import sys from pathlib import Path import pytest import libqtile.config import libqtile.layout import libqtile.widget from libqtile.confreader import Config from test.helpers import Retry from test.layouts.layout_utils import assert_focus_path, assert_focused def spawn_cmd(title): script = Path(__file__).parent / "scripts" / "window.py" cmd = f"{sys.executable} {script.as_posix()} --name TestWindow {title} normal" return cmd class ScratchPadBaseConfic(Config): auto_fullscreen = True screens = [] groups = [ libqtile.config.ScratchPad( "SCRATCHPAD", dropdowns=[ libqtile.config.DropDown("dd-a", spawn_cmd("dd-a"), on_focus_lost_hide=False), libqtile.config.DropDown("dd-b", spawn_cmd("dd-b"), on_focus_lost_hide=False), libqtile.config.DropDown("dd-c", spawn_cmd("dd-c"), on_focus_lost_hide=True), libqtile.config.DropDown("dd-d", spawn_cmd("dd-d"), on_focus_lost_hide=True), libqtile.config.DropDown( "dd-e", spawn_cmd("dd-e"), match=libqtile.config.Match(title="dd-e"), on_focus_lost_hide=False, ), ], ), libqtile.config.ScratchPad( "SINGLE_SCRATCHPAD", dropdowns=[ libqtile.config.DropDown("dd-e", spawn_cmd("dd-e"), on_focus_lost_hide=False), libqtile.config.DropDown("dd-f", spawn_cmd("dd-f"), on_focus_lost_hide=False), ], single=True, ), libqtile.config.Group("a"), libqtile.config.Group("b"), ] layouts = [libqtile.layout.max.Max()] floating_layout = libqtile.resources.default_config.floating_layout keys = [] mouse = [] scratchpad_config = pytest.mark.parametrize("manager", [ScratchPadBaseConfic], indirect=True) @Retry(ignore_exceptions=(KeyError,)) def is_spawned(manager, name, scratch_group="SCRATCHPAD"): manager.c.group[scratch_group].dropdown_info(name)["window"] return True @Retry(ignore_exceptions=(ValueError,)) def is_killed(manager, name): if "window" not in manager.c.group["SCRATCHPAD"].dropdown_info(name): return True raise ValueError("not yet killed") @scratchpad_config def test_sratchpad_with_matcher(manager): # adjust command for current display manager.c.group["SCRATCHPAD"].dropdown_reconfigure("dd-e") manager.test_window("one") assert manager.c.group["a"].info()["windows"] == ["one"] # First toggling: wait for window manager.c.group["SCRATCHPAD"].dropdown_toggle("dd-e") is_spawned(manager, "dd-e") # assert window in current group "a" assert sorted(manager.c.group["a"].info()["windows"]) == ["dd-e", "one"] assert_focused(manager, "dd-e") # toggle again --> "hide" dd-e manager.c.group["SCRATCHPAD"].dropdown_toggle("dd-e") assert manager.c.group["a"].info()["windows"] == ["one"] assert_focused(manager, "one") assert manager.c.group["SCRATCHPAD"].info()["windows"] == ["dd-e"] # toggle again --> show again manager.c.group["SCRATCHPAD"].dropdown_toggle("dd-e") assert sorted(manager.c.group["a"].info()["windows"]) == ["dd-e", "one"] assert_focused(manager, "dd-e") assert manager.c.group["SCRATCHPAD"].info()["windows"] == [] @scratchpad_config def test_toggling_single(manager): # adjust command for current display manager.c.group["SINGLE_SCRATCHPAD"].dropdown_reconfigure("dd-e") manager.c.group["SINGLE_SCRATCHPAD"].dropdown_reconfigure("dd-f") manager.c.group["SINGLE_SCRATCHPAD"].dropdown_reconfigure("dd-g") manager.c.group["SINGLE_SCRATCHPAD"].dropdown_reconfigure("dd-h") manager.test_window("one") assert manager.c.group["a"].info()["windows"] == ["one"] # First toggling: wait for window manager.c.group["SINGLE_SCRATCHPAD"].dropdown_toggle("dd-e") is_spawned(manager, "dd-e", "SINGLE_SCRATCHPAD") # assert window in current group "a" assert sorted(manager.c.group["a"].info()["windows"]) == ["dd-e", "one"] assert_focused(manager, "dd-e") # toggle another window, this should hide the previous one. manager.c.group["SINGLE_SCRATCHPAD"].dropdown_toggle("dd-f") is_spawned(manager, "dd-f", "SINGLE_SCRATCHPAD") assert sorted(manager.c.group["a"].info()["windows"]) == ["dd-f", "one"] assert_focused(manager, "dd-f") assert manager.c.group["SINGLE_SCRATCHPAD"].info()["windows"] == ["dd-e"] # toggle the scratchpad that is now visible. manager.c.group["SINGLE_SCRATCHPAD"].dropdown_toggle("dd-f") assert sorted(manager.c.group["a"].info()["windows"]) == ["one"] assert_focused(manager, "one") assert sorted(manager.c.group["SINGLE_SCRATCHPAD"].info()["windows"]) == ["dd-e", "dd-f"] @scratchpad_config def test_toggling(manager): manager.c.group["SCRATCHPAD"].dropdown_reconfigure("dd-a") manager.test_window("one") assert manager.c.group["a"].info()["windows"] == ["one"] # First toggling: wait for window manager.c.group["SCRATCHPAD"].dropdown_toggle("dd-a") is_spawned(manager, "dd-a") # assert window in current group "a" assert sorted(manager.c.group["a"].info()["windows"]) == ["dd-a", "one"] assert_focused(manager, "dd-a") # toggle again --> "hide" window in scratchpad group manager.c.group["SCRATCHPAD"].dropdown_toggle("dd-a") assert manager.c.group["a"].info()["windows"] == ["one"] assert_focused(manager, "one") assert manager.c.group["SCRATCHPAD"].info()["windows"] == ["dd-a"] # toggle again --> show again manager.c.group["SCRATCHPAD"].dropdown_toggle("dd-a") assert sorted(manager.c.group["a"].info()["windows"]) == ["dd-a", "one"] assert_focused(manager, "dd-a") assert manager.c.group["SCRATCHPAD"].info()["windows"] == [] @scratchpad_config def test_focus_cycle(manager): manager.c.group["SCRATCHPAD"].dropdown_reconfigure("dd-a") manager.c.group["SCRATCHPAD"].dropdown_reconfigure("dd-b") manager.test_window("one") # spawn dd-a by toggling assert_focused(manager, "one") manager.c.group["SCRATCHPAD"].dropdown_toggle("dd-a") is_spawned(manager, "dd-a") assert_focused(manager, "dd-a") manager.test_window("two") assert_focused(manager, "two") # spawn dd-b by toggling manager.c.group["SCRATCHPAD"].dropdown_toggle("dd-b") is_spawned(manager, "dd-b") assert_focused(manager, "dd-b") # check all windows assert sorted(manager.c.group["a"].info()["windows"]) == ["dd-a", "dd-b", "one", "two"] assert_focus_path(manager, "one", "two", "dd-a", "dd-b") @scratchpad_config def test_focus_lost_hide(manager): manager.c.group["SCRATCHPAD"].dropdown_reconfigure("dd-c") manager.c.group["SCRATCHPAD"].dropdown_reconfigure("dd-d") manager.test_window("one") assert_focused(manager, "one") # spawn dd-c by toggling manager.c.group["SCRATCHPAD"].dropdown_toggle("dd-c") is_spawned(manager, "dd-c") assert_focused(manager, "dd-c") assert sorted(manager.c.group["a"].info()["windows"]) == ["dd-c", "one"] # New Window with Focus --> hide current DropDown manager.test_window("two") assert_focused(manager, "two") assert sorted(manager.c.group["a"].info()["windows"]) == ["one", "two"] assert sorted(manager.c.group["SCRATCHPAD"].info()["windows"]) == ["dd-c"] # spawn dd-b by toggling manager.c.group["SCRATCHPAD"].dropdown_toggle("dd-d") is_spawned(manager, "dd-d") assert_focused(manager, "dd-d") assert sorted(manager.c.group["a"].info()["windows"]) == ["dd-d", "one", "two"] assert sorted(manager.c.group["SCRATCHPAD"].info()["windows"]) == ["dd-c"] # focus next, is the first tiled window --> "hide" dd-d manager.c.group.next_window() assert_focused(manager, "one") assert sorted(manager.c.group["a"].info()["windows"]) == ["one", "two"] assert sorted(manager.c.group["SCRATCHPAD"].info()["windows"]) == ["dd-c", "dd-d"] # Bring dd-c to front manager.c.group["SCRATCHPAD"].dropdown_toggle("dd-c") assert_focused(manager, "dd-c") assert sorted(manager.c.group["a"].info()["windows"]) == ["dd-c", "one", "two"] assert sorted(manager.c.group["SCRATCHPAD"].info()["windows"]) == ["dd-d"] # Bring dd-d to front --> "hide dd-c manager.c.group["SCRATCHPAD"].dropdown_toggle("dd-d") assert_focused(manager, "dd-d") assert sorted(manager.c.group["a"].info()["windows"]) == ["dd-d", "one", "two"] assert sorted(manager.c.group["SCRATCHPAD"].info()["windows"]) == ["dd-c"] # change current group to "b" hids DropDowns manager.c.group["b"].toscreen() assert sorted(manager.c.group["a"].info()["windows"]) == ["one", "two"] assert sorted(manager.c.group["SCRATCHPAD"].info()["windows"]) == ["dd-c", "dd-d"] @scratchpad_config def test_kill(manager): manager.c.group["SCRATCHPAD"].dropdown_reconfigure("dd-a") manager.test_window("one") assert_focused(manager, "one") # dd-a has no window associated yet assert "window" not in manager.c.group["SCRATCHPAD"].dropdown_info("dd-a") # First toggling: wait for window manager.c.group["SCRATCHPAD"].dropdown_toggle("dd-a") is_spawned(manager, "dd-a") assert_focused(manager, "dd-a") assert manager.c.group["SCRATCHPAD"].dropdown_info("dd-a")["window"]["name"] == "dd-a" # kill current window "dd-a" manager.c.window.kill() manager.c.sync() is_killed(manager, "dd-a") assert_focused(manager, "one") assert "window" not in manager.c.group["SCRATCHPAD"].dropdown_info("dd-a") @scratchpad_config def test_floating_toggle(manager): manager.c.group["SCRATCHPAD"].dropdown_reconfigure("dd-a") manager.test_window("one") assert_focused(manager, "one") # dd-a has no window associated yet assert "window" not in manager.c.group["SCRATCHPAD"].dropdown_info("dd-a") # First toggling: wait for window manager.c.group["SCRATCHPAD"].dropdown_toggle("dd-a") is_spawned(manager, "dd-a") assert_focused(manager, "dd-a") assert "window" in manager.c.group["SCRATCHPAD"].dropdown_info("dd-a") assert sorted(manager.c.group["a"].info()["windows"]) == ["dd-a", "one"] manager.c.window.toggle_floating() # dd-a has no window associated any more, but is still in group assert "window" not in manager.c.group["SCRATCHPAD"].dropdown_info("dd-a") assert sorted(manager.c.group["a"].info()["windows"]) == ["dd-a", "one"] manager.c.group["SCRATCHPAD"].dropdown_toggle("dd-a") is_spawned(manager, "dd-a") assert sorted(manager.c.group["a"].info()["windows"]) == ["dd-a", "dd-a", "one"] @scratchpad_config def test_stepping_between_groups_should_skip_scratchpads(manager): # we are on a group manager.c.screen.next_group() # we are on b group manager.c.screen.next_group() # we should be on a group assert manager.c.group.info()["name"] == "a" manager.c.screen.prev_group() # we should be on b group assert manager.c.group.info()["name"] == "b" @scratchpad_config def test_skip_taskbar(manager): manager.c.group["SCRATCHPAD"].dropdown_reconfigure("dd-a") manager.test_window("one") assert_focused(manager, "one") # dd-a has no window associated yet assert "window" not in manager.c.group["SCRATCHPAD"].dropdown_info("dd-a") # First toggling: wait for window manager.c.group["SCRATCHPAD"].dropdown_toggle("dd-a") is_spawned(manager, "dd-a") assert_focused(manager, "dd-a") assert manager.c.group["SCRATCHPAD"].dropdown_info("dd-a")["window"]["name"] == "dd-a" if manager.c.core.info()["backend"] == "x11": # check that window's _NET_WM_STATE contains _NET_WM_STATE_SKIP_TASKBAR net_wm_state = manager.c.window.eval("self.window.get_net_wm_state()")[1] assert "_NET_WM_STATE_SKIP_TASKBAR" in net_wm_state qtile-0.31.0/test/test_hook.py0000664000175000017500000005233614762660347016177 0ustar epsilonepsilon# Copyright (c) 2009 Aldo Cortesi # Copyright (c) 2011 Florian Mounier # Copyright (c) 2011 Anshuman Bhaduri # Copyright (c) 2012 Tycho Andersen # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import asyncio from multiprocessing import Value import pytest import libqtile.log_utils import libqtile.utils from libqtile import config, hook, layout from libqtile.config import Match from libqtile.resources import default_config from test.conftest import BareConfig, dualmonitor from test.helpers import Retry class Call: def __init__(self, val): self.val = val def __call__(self, val): self.val = val class NoArgCall(Call): def __call__(self): self.val += 1 @pytest.fixture def hook_fixture(): libqtile.log_utils.init_log() yield hook.clear() def test_cannot_fire_unknown_event(): with pytest.raises(libqtile.utils.QtileError): hook.fire("unknown") @pytest.mark.usefixtures("hook_fixture") def test_hook_calls_subscriber(): test = Call(0) hook.subscribe.group_window_add(test) hook.fire("group_window_add", 8) assert test.val == 8 @pytest.mark.usefixtures("hook_fixture") def test_hook_calls_subscriber_async(): val = 0 async def co(new_val): nonlocal val val = new_val hook.subscribe.group_window_add(co) hook.fire("group_window_add", 8) assert val == 8 @pytest.mark.usefixtures("hook_fixture") def test_hook_calls_subscriber_async_co(): val = 0 async def co(new_val): nonlocal val val = new_val hook.subscribe.group_window_add(co(8)) hook.fire("group_window_add") assert val == 8 @pytest.mark.usefixtures("hook_fixture") def test_hook_calls_subscriber_async_in_existing_loop(): async def t(): val = 0 async def co(new_val): nonlocal val val = new_val hook.subscribe.group_window_add(co(8)) hook.fire("group_window_add") await asyncio.sleep(0) assert val == 8 asyncio.run(t()) @pytest.mark.usefixtures("hook_fixture") def test_subscribers_can_be_added_removed(): test = Call(0) hook.subscribe.group_window_add(test) assert hook.subscriptions hook.clear() assert not hook.subscriptions @pytest.mark.usefixtures("hook_fixture") def test_can_unsubscribe_from_hook(): test = Call(0) hook.subscribe.group_window_add(test) hook.fire("group_window_add", 3) assert test.val == 3 hook.unsubscribe.group_window_add(test) hook.fire("group_window_add", 4) assert test.val == 3 def test_can_subscribe_to_startup_hooks(manager_nospawn): config = BareConfig for attr in dir(default_config): if not hasattr(config, attr): setattr(config, attr, getattr(default_config, attr)) manager = manager_nospawn manager.startup_once_calls = Value("i", 0) manager.startup_calls = Value("i", 0) manager.startup_complete_calls = Value("i", 0) def inc_startup_once_calls(): manager.startup_once_calls.value += 1 def inc_startup_calls(): manager.startup_calls.value += 1 def inc_startup_complete_calls(): manager.startup_complete_calls.value += 1 hook.subscribe.startup_once(inc_startup_once_calls) hook.subscribe.startup(inc_startup_calls) hook.subscribe.startup_complete(inc_startup_complete_calls) manager.start(config) assert manager.startup_once_calls.value == 1 assert manager.startup_calls.value == 1 assert manager.startup_complete_calls.value == 1 # Restart and check that startup_once doesn't fire again manager.terminate() manager.start(config, no_spawn=True) assert manager.startup_once_calls.value == 1 assert manager.startup_calls.value == 2 assert manager.startup_complete_calls.value == 2 @pytest.mark.usefixtures("hook_fixture") def test_can_update_by_selection_change(manager): test = Call(0) hook.subscribe.selection_change(test) hook.fire("selection_change", "hello") assert test.val == "hello" @pytest.mark.usefixtures("hook_fixture") def test_can_call_by_selection_notify(manager): test = Call(0) hook.subscribe.selection_notify(test) hook.fire("selection_notify", "hello") assert test.val == "hello" @pytest.mark.usefixtures("hook_fixture") def test_resume_hook(manager): test = NoArgCall(0) hook.subscribe.resume(test) hook.fire("resume") assert test.val == 1 @pytest.mark.usefixtures("hook_fixture") def test_suspend_hook(manager): test = NoArgCall(0) hook.subscribe.suspend(test) hook.fire("suspend") assert test.val == 1 @pytest.mark.usefixtures("hook_fixture") def test_custom_hook_registry(): """Tests ability to create custom hook registries""" test = NoArgCall(0) custom = hook.Registry("test") custom.register_hook(hook.Hook("test_hook")) custom.subscribe.test_hook(test) assert test.val == 0 # Test ability to fire third party hooks custom.fire("test_hook") assert test.val == 1 # Check core hooks are not included in custom registry with pytest.raises(libqtile.utils.QtileError): custom.fire("client_managed") # Check custom hooks are not in core registry with pytest.raises(libqtile.utils.QtileError): hook.fire("test_hook") @pytest.mark.usefixtures("hook_fixture") def test_user_hook(manager_nospawn): config = BareConfig for attr in dir(default_config): if not hasattr(config, attr): setattr(config, attr, getattr(default_config, attr)) manager = manager_nospawn manager.custom_no_arg_text = Value("u", "A") manager.custom_text = Value("u", "A") # Define two functions: first takes no args, second takes a single arg def predefined_text(): with manager.custom_no_arg_text.get_lock(): manager.custom_no_arg_text.value = "B" def defined_text(text): with manager.custom_text.get_lock(): manager.custom_text.value = text hook.subscribe.user("set_text")(predefined_text) hook.subscribe.user("define_text")(defined_text) # Check values are as initialised manager.start(config) assert manager.custom_no_arg_text.value == "A" assert manager.custom_text.value == "A" # Check hooked function with no args manager.c.fire_user_hook("set_text") assert manager.custom_no_arg_text.value == "B" # Check hooked function with a single arg manager.c.fire_user_hook("define_text", "C") assert manager.custom_text.value == "C" def test_shutdown(manager_nospawn): def inc_shutdown_calls(): manager_nospawn.shutdown_calls.value += 1 manager_nospawn.shutdown_calls = Value("i", 0) hook.subscribe.shutdown(inc_shutdown_calls) manager_nospawn.start(BareConfig) manager_nospawn.c.shutdown() assert manager_nospawn.shutdown_calls.value == 1 @dualmonitor def test_setgroup(manager_nospawn): @Retry(ignore_exceptions=(AssertionError)) def assert_inc_calls(num: int): assert manager_nospawn.setgroup_calls.value == num def inc_setgroup_calls(): manager_nospawn.setgroup_calls.value += 1 manager_nospawn.setgroup_calls = Value("i", 0) hook.subscribe.setgroup(inc_setgroup_calls) # Starts with two because of the dual screen manager_nospawn.start(BareConfig) assert_inc_calls(2) manager_nospawn.c.switch_groups("a", "b") assert_inc_calls(3) manager_nospawn.c.to_screen(1) assert_inc_calls(4) manager_nospawn.c.to_screen(1) assert_inc_calls(4) manager_nospawn.c.next_screen() assert_inc_calls(5) manager_nospawn.c.prev_screen() assert_inc_calls(6) manager_nospawn.c.group.switch_groups("b") assert_inc_calls(7) class CallGroupname: def __init__(self): self.groupname = "" def __call__(self, groupname): self.groupname = groupname @Retry(ignore_exceptions=(AssertionError)) def assert_groupname(mgr_nospawn, groupname): _, _groupname = mgr_nospawn.c.eval("self.config.test.groupname") assert _groupname == groupname @pytest.mark.usefixtures("hook_fixture") def test_addgroup(manager_nospawn): class AddgroupConfig(BareConfig): test = CallGroupname() hook.subscribe.addgroup(test) manager_nospawn.start(AddgroupConfig) assert_groupname(manager_nospawn, "d") manager_nospawn.c.addgroup("e") assert_groupname(manager_nospawn, "e") @pytest.mark.usefixtures("hook_fixture") def test_delgroup(manager_nospawn): class DelgroupConfig(BareConfig): test = CallGroupname() hook.subscribe.delgroup(test) manager_nospawn.start(DelgroupConfig) manager_nospawn.c.delgroup("e") assert_groupname(manager_nospawn, "") manager_nospawn.c.delgroup("d") assert_groupname(manager_nospawn, "d") def test_changegroup(manager_nospawn): @Retry(ignore_exceptions=(AssertionError)) def assert_inc_calls(num: int): assert manager_nospawn.changegroup_calls.value == num def inc_changegroup_calls(): manager_nospawn.changegroup_calls.value += 1 manager_nospawn.changegroup_calls = Value("i", 0) hook.subscribe.changegroup(inc_changegroup_calls) # Starts with four beacuase of four groups in BareConfig manager_nospawn.start(BareConfig) assert_inc_calls(4) manager_nospawn.c.group.set_label("Test") assert_inc_calls(5) manager_nospawn.c.addgroup("e") assert_inc_calls(6) manager_nospawn.c.addgroup("e") assert_inc_calls(6) manager_nospawn.c.delgroup("e") assert_inc_calls(7) manager_nospawn.c.delgroup("e") assert_inc_calls(7) def test_focus_change(manager_nospawn): @Retry(ignore_exceptions=(AssertionError)) def assert_inc_calls(num: int): assert manager_nospawn.focus_change_calls.value == num def inc_focus_change_calls(): manager_nospawn.focus_change_calls.value += 1 manager_nospawn.focus_change_calls = Value("i", 0) hook.subscribe.focus_change(inc_focus_change_calls) manager_nospawn.start(BareConfig) assert_inc_calls(1) manager_nospawn.test_window("Test Window") assert_inc_calls(2) manager_nospawn.c.group.focus_by_index(0) assert_inc_calls(3) manager_nospawn.c.group.focus_by_index(1) assert_inc_calls(3) manager_nospawn.test_window("Test Focus Change") assert_inc_calls(4) manager_nospawn.c.group.focus_back() assert_inc_calls(5) manager_nospawn.c.group.focus_by_name("Test Focus Change") assert_inc_calls(6) manager_nospawn.c.group.focus_by_name("Test Focus") assert_inc_calls(6) manager_nospawn.c.group.next_window() assert_inc_calls(7) manager_nospawn.c.group.prev_window() assert_inc_calls(8) manager_nospawn.c.window.kill() assert_inc_calls(9) def test_float_change(manager_nospawn): @Retry(ignore_exceptions=(AssertionError)) def assert_inc_calls(num: int): assert manager_nospawn.float_change_calls.value == num def inc_float_change_calls(): manager_nospawn.float_change_calls.value += 1 manager_nospawn.float_change_calls = Value("i", 0) hook.subscribe.float_change(inc_float_change_calls) manager_nospawn.start(BareConfig) manager_nospawn.test_window("Test Window") manager_nospawn.c.window.enable_floating() assert_inc_calls(1) manager_nospawn.c.window.enable_floating() assert_inc_calls(1) manager_nospawn.c.window.disable_floating() assert_inc_calls(2) manager_nospawn.c.window.disable_floating() assert_inc_calls(2) manager_nospawn.c.window.toggle_floating() assert_inc_calls(3) manager_nospawn.c.window.toggle_floating() manager_nospawn.c.window.move_floating(0, 0) assert_inc_calls(5) manager_nospawn.c.window.toggle_floating() manager_nospawn.c.window.resize_floating(10, 10) assert_inc_calls(7) manager_nospawn.c.window.toggle_floating() manager_nospawn.c.window.set_position_floating(0, 0) assert_inc_calls(9) manager_nospawn.c.window.toggle_floating() manager_nospawn.c.window.set_size_floating(100, 100) assert_inc_calls(11) class CallGroupWindow: def __init__(self): self.window = "" self.group = "" def __call__(self, group, win): self.group = group.name self.window = win.name @Retry(ignore_exceptions=(AssertionError)) def assert_group_window(mgr_nospawn, group, window): _, _group = mgr_nospawn.c.eval("self.config.test.group") _, _window = mgr_nospawn.c.eval("self.config.test.window") assert _group == group assert _window == window @pytest.mark.usefixtures("hook_fixture") def test_group_window_add(manager_nospawn): class AddGroupWindowConfig(BareConfig): test = CallGroupWindow() hook.subscribe.group_window_add(test) manager_nospawn.start(AddGroupWindowConfig) manager_nospawn.test_window("Test Window") assert_group_window(manager_nospawn, "a", "Test Window") @pytest.mark.usefixtures("hook_fixture") def test_group_window_remove(manager_nospawn): class RemoveGroupWindowConfig(BareConfig): test = CallGroupWindow() hook.subscribe.group_window_remove(test) manager_nospawn.start(RemoveGroupWindowConfig) manager_nospawn.test_window("Test Window") manager_nospawn.c.window.kill() assert_group_window(manager_nospawn, "a", "Test Window") class CallWindow: def __init__(self): self.window = "" def __call__(self, window): self.window = window.name @Retry(ignore_exceptions=(AssertionError)) def assert_window(mgr_nospawn, window): _, _window = mgr_nospawn.c.eval("self.config.test.window") assert _window == window @pytest.mark.usefixtures("hook_fixture") def test_client_new(manager_nospawn): class ClientNewConfig(BareConfig): test = CallWindow() hook.subscribe.client_new(test) manager_nospawn.start(ClientNewConfig) manager_nospawn.test_window("Test Client") assert_window(manager_nospawn, "Test Client") @pytest.mark.usefixtures("hook_fixture") def test_client_managed(manager_nospawn): class ClientManagedConfig(BareConfig): test = CallWindow() hook.subscribe.client_managed(test) manager_nospawn.start(ClientManagedConfig) manager_nospawn.test_window("Test Client") assert_window(manager_nospawn, "Test Client") manager_nospawn.test_window("Test Static") manager_nospawn.c.group.focus_back() manager_nospawn.c.window.static() assert_window(manager_nospawn, "Test Client") @pytest.mark.usefixtures("hook_fixture") def test_client_killed(manager_nospawn): class ClientKilledConfig(BareConfig): test = CallWindow() hook.subscribe.client_killed(test) manager_nospawn.start(ClientKilledConfig) manager_nospawn.test_window("Test Client") manager_nospawn.c.window.kill() assert_window(manager_nospawn, "Test Client") @pytest.mark.usefixtures("hook_fixture") def test_client_focus(manager_nospawn): class ClientFocusConfig(BareConfig): test = CallWindow() hook.subscribe.client_focus(test) manager_nospawn.start(ClientFocusConfig) manager_nospawn.test_window("Test Client") assert_window(manager_nospawn, "Test Client") manager_nospawn.test_window("Test Focus") manager_nospawn.c.group.focus_back() assert_window(manager_nospawn, "Test Client") @pytest.mark.usefixtures("hook_fixture") def test_client_mouse_enter(manager_nospawn): class ClientMouseEnterConfig(BareConfig): test = CallWindow() hook.subscribe.client_mouse_enter(test) manager_nospawn.start(ClientMouseEnterConfig) manager_nospawn.test_window("Test Client") manager_nospawn.backend.fake_click(0, 0) assert_window(manager_nospawn, "Test Client") @pytest.mark.usefixtures("hook_fixture") def test_client_name_updated(manager_nospawn): class ClientNameUpdatedConfig(BareConfig): test = CallWindow() hook.subscribe.client_name_updated(test) manager_nospawn.start(ClientNameUpdatedConfig) manager_nospawn.test_window("Test Client", new_title="Test NameUpdated") assert_window(manager_nospawn, "Test NameUpdated") @pytest.mark.usefixtures("hook_fixture") def test_client_urgent_hint_changed(manager_nospawn, backend_name): if backend_name == "wayland": pytest.skip("Core not listening to XDG request_activate_event ?") class ClientUrgentHintChangedConfig(BareConfig): groups = [ config.Group("a"), config.Group("b", matches=[Match(title="Test Client")]), ] focus_on_window_activation = "urgent" test = CallWindow() hook.subscribe.client_urgent_hint_changed(test) manager_nospawn.start(ClientUrgentHintChangedConfig) manager_nospawn.test_window("Test Client", urgent_hint=True) assert_window(manager_nospawn, "Test Client") class CallLayoutGroup: def __init__(self): self.layout = "" self.group = "" def __call__(self, layout, group): self.layout = layout.name self.group = group.name @Retry(ignore_exceptions=(AssertionError)) def assert_layout_group(mgr_nospawn, layout, group): _, _layout = mgr_nospawn.c.eval("self.config.test.layout") assert _layout == layout _, _group = mgr_nospawn.c.eval("self.config.test.group") assert _group == group @pytest.mark.usefixtures("hook_fixture") def test_layout_change(manager_nospawn): class ClientLayoutChange(BareConfig): layouts = [layout.stack.Stack(), layout.columns.Columns()] test = CallLayoutGroup() hook.subscribe.layout_change(test) manager_nospawn.start(ClientLayoutChange) assert_layout_group(manager_nospawn, "stack", "a") manager_nospawn.c.group.setlayout("columns") assert_layout_group(manager_nospawn, "columns", "a") manager_nospawn.c.screen.next_group() assert_layout_group(manager_nospawn, "stack", "b") manager_nospawn.c.screen.prev_group() assert_layout_group(manager_nospawn, "columns", "a") manager_nospawn.c.screen.toggle_group() assert_layout_group(manager_nospawn, "stack", "b") manager_nospawn.c.next_layout() assert_layout_group(manager_nospawn, "columns", "b") manager_nospawn.c.prev_layout() assert_layout_group(manager_nospawn, "stack", "b") @pytest.mark.usefixtures("hook_fixture") def test_net_wm_icon_change(manager_nospawn, backend_name): if backend_name == "wayland": pytest.skip("X11 only.") class ClientNewConfig(BareConfig): test = CallWindow() hook.subscribe.net_wm_icon_change(test) manager_nospawn.start(ClientNewConfig) manager_nospawn.test_window("Test Client") assert_window(manager_nospawn, "Test Client") @pytest.mark.usefixtures("hook_fixture") def test_screen_change(manager_nospawn): @Retry(ignore_exceptions=(AssertionError)) def assert_inc_calls(num: int): assert manager_nospawn.screen_change_calls.value == num def inc_screen_change_calls(event): manager_nospawn.screen_change_calls.value += 1 manager_nospawn.screen_change_calls = Value("i", 0) hook.subscribe.screen_change(inc_screen_change_calls) manager_nospawn.start(BareConfig) assert_inc_calls(1) @pytest.mark.usefixtures("hook_fixture") def test_screens_reconfigured(manager_nospawn): @Retry(ignore_exceptions=(AssertionError)) def assert_inc_calls(num: int): assert manager_nospawn.screens_reconfigured_calls.value == num def inc_screens_reconfigured_calls(): manager_nospawn.screens_reconfigured_calls.value += 1 manager_nospawn.screens_reconfigured_calls = Value("i", 0) hook.subscribe.screens_reconfigured(inc_screens_reconfigured_calls) manager_nospawn.start(BareConfig) manager_nospawn.c.reconfigure_screens() assert_inc_calls(1) @dualmonitor @pytest.mark.usefixtures("hook_fixture") def test_current_screen_change(manager_nospawn): @Retry(ignore_exceptions=(AssertionError)) def assert_inc_calls(num: int): assert manager_nospawn.current_screen_change_calls.value == num def inc_current_screen_change_calls(): manager_nospawn.current_screen_change_calls.value += 1 manager_nospawn.current_screen_change_calls = Value("i", 0) hook.subscribe.current_screen_change(inc_current_screen_change_calls) manager_nospawn.start(BareConfig) manager_nospawn.c.to_screen(1) assert_inc_calls(1) manager_nospawn.c.to_screen(1) assert_inc_calls(1) manager_nospawn.c.next_screen() assert_inc_calls(2) manager_nospawn.c.prev_screen() assert_inc_calls(3) qtile-0.31.0/test/__init__.py0000664000175000017500000000000014762660347015714 0ustar epsilonepsilonqtile-0.31.0/test/test_dgroups.py0000664000175000017500000000465114762660347016717 0ustar epsilonepsilon# Copyright (c) 2024 Thomas Krug # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import time import pytest import libqtile class DGroupsConfig(libqtile.confreader.Config): auto_fullscreen = True groups = [ libqtile.config.Group("a"), libqtile.config.Group("b"), ] layouts = [libqtile.layout.MonadTall()] floating_layout = libqtile.resources.default_config.floating_layout keys = [] mouse = [] screens = [] dgroups_config = pytest.mark.parametrize("manager", [DGroupsConfig], indirect=True) @dgroups_config def test_dgroup_persist(manager): # create dgroup gname = "c" manager.c.addgroup(gname, persist=True) # switch to dgroup manager.c.group[gname].toscreen() # start window one = manager.test_window("test1") # close window manager.kill_window(one) # wait for window to close and group to NOT be destroyed time.sleep(2) # check if dgroup still exists assert len(manager.c.get_groups()) == 3 @dgroups_config def test_dgroup_nonpersist(manager): # create dgroup gname = "c" manager.c.addgroup(gname) # switch to dgroup manager.c.group[gname].toscreen() # start window one = manager.test_window("test1") # close window manager.kill_window(one) # wait for window to close and group to be destroyed time.sleep(2) # check if dgroup does not exist anymore assert len(manager.c.get_groups()) == 2 qtile-0.31.0/test/test_restart.py0000664000175000017500000001372414762660347016721 0ustar epsilonepsilon# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pickle import shutil import textwrap from multiprocessing import Value import pytest import libqtile.bar import libqtile.config import libqtile.layout from libqtile import config, hook, layout from libqtile.confreader import Config from libqtile.ipc import IPCError from libqtile.lazy import lazy from libqtile.resources import default_config from libqtile.widget import TextBox from test.helpers import TestManager as BareManager class TwoScreenConfig(Config): auto_fullscreen = True groups = [config.Group("a"), config.Group("b"), config.Group("c"), config.Group("d")] layouts = [layout.stack.Stack(num_stacks=1), layout.stack.Stack(num_stacks=2)] floating_layout = default_config.floating_layout keys = [ config.Key( ["control"], "k", lazy.layout.up(), ), config.Key( ["control"], "j", lazy.layout.down(), ), ] mouse = [] follow_mouse_focus = False reconfigure_screens = False screens = [] fake_screens = [ libqtile.config.Screen( top=libqtile.bar.Bar([TextBox("Qtile Test")], 10), x=0, y=0, width=400, height=600 ), libqtile.config.Screen( top=libqtile.bar.Bar([TextBox("Qtile Test")], 10), x=400, y=0, width=400, height=600 ), ] def test_restart_hook_and_state(manager_nospawn, request, backend, backend_name): if backend_name == "wayland": pytest.skip("Skipping test on Wayland.") manager = manager_nospawn # This injection allows us to capture the lifecycle state filepath before # restarting Qtile inject = textwrap.dedent( """ from libqtile.core.lifecycle import lifecycle def no_op(*args, **kwargs): pass self.lifecycle = lifecycle self._do_stop = self._stop self._stop = no_op """ ) # Set up test for restart hook. # Use a counter in manager and increment when hook is fired def inc_restart_call(): manager.restart_calls.value += 1 manager.restart_calls = Value("i", 0) hook.subscribe.restart(inc_restart_call) manager.start(TwoScreenConfig) # Check that hook hasn't been fired yet. assert manager.restart_calls.value == 0 manager.c.group["c"].toscreen(0) manager.c.group["d"].toscreen(1) manager.test_window("one") manager.test_window("two") wins = {w["name"]: w["id"] for w in manager.c.windows()} manager.c.window[wins["one"]].togroup("c") manager.c.window[wins["two"]].togroup("d") # Inject the code and start the restart manager.c.eval(inject) manager.c.restart() # Check hook fired assert manager.restart_calls.value == 1 # Get the path to the state file _, state_file = manager.c.eval("self.lifecycle.state_file") assert state_file # We need a copy of this as the next file will probably overwrite it original_state = f"{state_file}-original" shutil.copy(state_file, original_state) # Stop the manager manager.c.eval("self._do_stop()") # Manager should have shutdown now so trying to access it will raise an error with pytest.raises((IPCError, ConnectionResetError)): assert manager.c.status() # Set up a new manager which takes our state file with BareManager(backend, request.config.getoption("--debuglog")) as restarted_manager: restarted_manager.start(TwoScreenConfig, state=state_file) # Test 1: # Check that groups are shown on correct screens screen0_info = restarted_manager.c.screen[0].group.info() assert screen0_info["name"] == "c" assert screen0_info["screen"] == 0 screen1_info = restarted_manager.c.screen[1].group.info() assert screen1_info["name"] == "d" assert screen1_info["screen"] == 1 # Test 2: # Check that clients are returned to the correct groups assert len(restarted_manager.c.windows()) == 2 name_to_group = {w["name"]: w["group"] for w in restarted_manager.c.windows()} assert name_to_group["one"] == "c" assert name_to_group["two"] == "d" # Test 3: # Check that state file is the same # As before, inject code, restart and get state file restarted_manager.c.eval(inject) restarted_manager.c.restart() _, restarted_state = restarted_manager.c.eval("self.lifecycle.state_file") assert restarted_state restarted_manager.c.eval("self._do_stop()") # Load the two QtileState objects with open(original_state, "rb") as f: original = pickle.load(f) with open(restarted_state, "rb") as f: restarted = pickle.load(f) # Confirm that they're the same assert original.groups == restarted.groups assert original.screens == restarted.screens assert original.current_screen == restarted.current_screen assert original.scratchpads == restarted.scratchpads qtile-0.31.0/test/data/0000775000175000017500000000000014762660347014526 5ustar epsilonepsilonqtile-0.31.0/test/data/svg/0000775000175000017500000000000014762660347015325 5ustar epsilonepsilonqtile-0.31.0/test/data/svg/audio-volume-muted.svg0000664000175000017500000000101314762660347021563 0ustar epsilonepsilon qtile-0.31.0/test/data/comparison_images/0000775000175000017500000000000014762660347020225 5ustar epsilonepsilonqtile-0.31.0/test/data/comparison_images/audio-volume-muted_bad.png0000664000175000017500000001542314762660347025270 0ustar epsilonepsilonPNG  IHDR}ԾbKGDIDATxOoIJV"U@ .07 |i'|ؾ>x24h-EJXXiz34YH@dD/"˹ZˈXED^^^NuqqqqqqK> lZk_GikNrݻ?w s7G-K)q#kX,,*A3899(LTn;6 7MiSIm"=}w=Nzuuv?_V˩vGXRbǚZ&O0n{?^P-~zZ:kDΆao??jMPkQJ)Y,ֺ*L~Xk-0uY}B!v]eǢ֚MX,b?'O |,G2e\#Z-\k?F^'Cv{{۳gZOk7````````Ů2WkfCOfC'_=~zq.oX,3"b.83snak-_xqVJ6! W:֗/_Fӈ8 J)z9_}U{],I;c,bvYk9 Cܬ.//za^׈=s3"e^OjeYX:>1vttvuXL5c)ujsaߟR.">k˵Bf `f `f `f `f `f `f `<^a>35qmxuֲn(ggg:;!Ϟ=WWW|ׯ_rвxQD,#bR~v+"23ZkG[ơ)82?G?dDl6_n5""zRZa۟g2L'ojWݮ CnR'"b~7"E(lVקƫv~~~ x8ׯx\.c N2"NE)%oaV 盈z=Eկ~ocvft8ZWVk/^eGDZk1ح/Zktsqq|-w=tZwCf̬:u'=˄yyYsn߆H8fD>|zz f `f `f `f `f `f `f `f LٳO~zK?ZKf>`QFē̌e??jT/?63^mkWWWֺarnY2O"M #"ֶŢv(T2s^kt3<B5"bUkNW"(n2sRxR|g:lnm;??}?RZv||\,?es'q!jYkDo߾f{^Q8N1bqܣP)%v]Rj?הR~p}D[kݻm)%&zYoc^Z~Aއ{mbbSkݖR&mZy{lٓ棲nZk O*QJϟ?ӲS'|'nqs,d?b)~B4 @ 0@ 0@ 0@ 0@ 0@ 0@ 0@ VkmSZ2q2sZd椷EY2sӫa'Zk"bED/삁 dj9w!`f `f `f `f `f `f `f ` J鳦,8r_5dϟ?3kGgϞoZbhVkm=^} `>&z'q&"DěbJRJZߎxГVk!ONNr}}=з~ێ֒ژa|@֚=23s2. uZkD C0u71qǺ9::/O>}3svqqq;·ߓ宮V祔'O&M.2Z;/>|pRGk-2'OԩnooKfFƈhw)0&3'27/_YכM{02:yw[vRʤI ؾ73=^JD^{ɣ!hߪ}NbZ⏴{!tYpRjk:f `f `f `f `f `f `f `fp]/>n?8 xUJy...jkv? _kXӸ솹yiEDLz32EMD%"nqzRZkZ*&N*uZZrZL~:Ϟ=WWWZP5ou*3[uRZkxMY.cAEAu}}ggg{\ ?ߤwߕDZW>^RJmOfӡe f `f `f `f `f `f `f `fpÙ-"g 1Mkq<33{<2 1?mZk1{=A#wD8\!"~{www9S2"[k~3"N<ȏfY~Wm}'YMunm ??RJkW8Ffxң}{֚=vUn6qdW}H[kŋRJvpЋ^^z՞|;2`CF{׫mDgՈXx?dQt/_n6o;VUdfv}P#\n{7W=8aBf[VíڎcjX,nO-Gvȡ[{خ^}ֺNhxDljv||\{ef?D.ߺJ)|I8U`L8>/=;GH 0@ 0@ 0@ 0@ 0@ 0@ 0yd3։(Es;5Zz?G\,y~~ީu2^Zk\V}`hݮRnjZzj{݇n171m&x`jjunK)MQo_z>^}ZJZkRJٞfmLO. ho~0 xyyyv||#,&p)Zߌ>{(ZkoR󫯾jQ 9;.,8d `qt$ȣ!3;owUU#Y:?=no՞nvs9`K|dU{v4MV{`_ 3&%`_L* BIC|0)/y Hi`%@&/;9 @i`!H&/0:F$T%j7yvftYnOw$\d*"bZg (J|9' \w!!PsA|"3^߅eo>WBdž^׈Bw.DX90]rє!" M9-27=w!ƒm|FxR.DsbP|߅wj]!I*")C|)߅w]GO.D8R]nFO.D8].EO.D8Z]DO]p5]-27ƵEf!!4߅7.zp ͧ.D1wS/27]p#Z27D]peY!i 'a|"\I.G8Д!$B E27Bw5© ͧ.Dz"c JO]E27=5Ln~@ߜ:bڃdl?|˽wZv/֞2]oW#yj:=G4Mo&|s^,<onwy"u 9<NNN[{ˌAk <۶6oDoF"}a"76'w "<6c "w4"<&eMI"QC߱7n"fţ;وdD\D|dzL߆tm^]=`ߏM{p(m͈p"|Dxķ! #^ocnO [ 0 7"`.@E|&_"\_6NaD& p"\K|`8.@!0 !00x7!2DA#,I 02MӏkCi޺{מw=zm4Mo6OB[Ơ]pB#T(F|< Nf %e3D8@|sp-!\#-C|Džo{-"0@|F9pK[ފ7A[2w/D13ܐ!{% qķ =nC $eApl-C|n"-C|p-C|Jr!Up:\@|&pa;!Mx8!MBzx!M|x!)9![$0<-C|Sp!]=s!]=r!l?|˃w~W߯=Hv#\ xqzzzjRym+Յԧ>"go"ܧtݻ?+ߵg7Oʋw\V:nF|sᾤ|?ߊ ;L|s~tvM<,Ե^8Ӈ"uz??EW#Pvin]>{⦅}饗0oGįFDDWyPaչnfD|O=izfD|<1+.DxL;gIhwL"'Dx ;6c SDoKg1%9"'i"ܾ.*ᾈ/ᶍpYyrn pnUpF\Bz'ܹGD\{i~r"z~GiNA;99y[>;;{w^c9.#?g{g>;;{i_ ΧL`!ta="- p>EX| %!\@ [Xgos 'qŷ D8ʲE8{|#+|EX|%=7B'Aŷ1GF0$4amT) p> FX|Z{osC"OC$Zp`nAi LD8ߤjEF0{ T&W:½7BΧ`ŷ"DX|ߟ6qv7#wkgyvX허w"%TvXX`ۡRА "eZY}Cƻ >o#Qwޛy[$ډQ5[ {32>veνxu,xAcwDHb: m( ÀTB0` ;U4:;@yqh`ȈaccZ\18#0Ξ}pbb01qZk!Ew=OaއA>#ovWh_q01no'}J)ؚx͔REfΊX(,-[1); b4k #( ,y*0FuA>$u@0;{"ޡ `~ë(J%T\/) ǁ$W6SAׁGFa+TWPA&ɅQ,Q4~ڑǺm& ҊN?~|s |u/L+7ss?M\1c f>9,kرMuΝr6H<oGȺD/et,z4əLԟǏvٲ8]~flQ-r18UњmW~?Tϋ\ DJ\!خA`cʁC*xShOObY%tEXtdate:create2009-11-23T15:57:19+01:005%tEXtdate:modify2009-11-23T15:57:19+01:00h:tEXtSoftwarewww.inkscape.org<IENDB`qtile-0.31.0/test/data/png/battery-caution-charging.png0000664000175000017500000000244314762660347022715 0ustar epsilonepsilonPNG  IHDRw=sBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org< tEXtTitleBatteryUD?PtEXtAuthorLapo Calamandreiߑ*dIDATH[lUgfV텭@iIaP* ,Z"!<@E}   hbcEB6HV6Ks=>vAD dΙ/wϜ#@xP2h3 ڜR]ow>(9L L}<'O7oE"'4`=\׵EBU48u{ 8c{']uUu\׵|Ɇq1R`Yֽ l_yWrH ٶm<)i¶M\8 s IXy뺮iwcY~s@>x(_UcSG-*Vɝc&<PTUeY%KzW;y=Url`$;i=NLY O,YUU0M77 |m嬾OVc7Hz&5۶=)ii0MWoUNӕy+?4Eu-_:nSŋ@Zi[%]uHRȷ`pg^ϓJKנ:`H!S9KhmAʰuj*CHr;!D.F/_.uyٙEEE1 Xa{{aqaԄ~ H455 έ+){ޣ7Ѩ@" 7v0A&B@RBڐi~?0 q!#RJc".;.9ΑnWU}bb&`J)ʹX ٖ?En 9/RRtdIENDB`qtile-0.31.0/test/test_images.py0000664000175000017500000001610214762660347016473 0ustar epsilonepsilon""" test_images.py contains unittests for libqtile.images.Img and its supporting code. """ import os from glob import glob from os import path import cairocffi import cairocffi.pixbuf import pytest from libqtile import images TEST_DIR = path.dirname(os.path.abspath(__file__)) DATA_DIR = path.join(TEST_DIR, "data") PNGS = glob(path.join(DATA_DIR, "*", "*.png")) SVGS = glob(path.join(DATA_DIR, "*", "*.svg")) ALL_IMAGES = glob(path.join(DATA_DIR, "*", "*")) @pytest.fixture( scope="function", params=ALL_IMAGES, ) def path_n_bytes_image(request): fpath = request.param with open(fpath, "rb") as fobj: bobj = fobj.read() return fpath, bobj @pytest.fixture( scope="function", params=PNGS, ) def path_n_bytes_image_pngs(request): fpath = request.param with open(fpath, "rb") as fobj: bobj = fobj.read() return fpath, bobj @pytest.fixture(scope="function") def png_img(): return images.Img.from_path(PNGS[0]) def test_get_cairo_surface(path_n_bytes_image): path, bytes_image = path_n_bytes_image surf_info = images.get_cairo_surface(bytes_image) assert isinstance(surf_info.surface, cairocffi.ImageSurface) assert path.split(".")[-1].lower() == surf_info.file_type def test_get_cairo_surface_bad_input(): with pytest.raises(cairocffi.pixbuf.ImageLoadingError): images.get_cairo_surface(b"asdfasfdi3") def assert_approx_equal(vec0, vec1): approx = pytest.approx for val0, val1 in zip(vec0, vec1): assert val0 == approx(val1) class TestImg: def test_init(self, path_n_bytes_image): path, bytes_image = path_n_bytes_image img = images.Img(bytes_image) assert isinstance(img.surface, cairocffi.ImageSurface) del img.surface assert isinstance(img.surface, cairocffi.ImageSurface) def test_from_path(self, path_n_bytes_image): path, bytes_image = path_n_bytes_image img = images.Img(bytes_image) assert isinstance(img.surface, cairocffi.ImageSurface) img2 = img.from_path(path) assert img == img2 img2.theta = 90.0 assert img != img2 img2.theta = 0.0 assert img == img2 def test_setting(self, png_img): img = png_img width0, height0 = img.default_size pat0 = img.pattern img.width = width0 + 3 assert pat0 != img.pattern assert img.width == (width0 + 3) pat1 = img.pattern img.height = height0 + 7 assert img.height == (height0 + 7) assert img.pattern != pat0 assert img.pattern != pat1 pat2 = img.pattern img.theta = -35.0 assert img.pattern != pat0 assert img.pattern != pat1 assert img.pattern != pat2 assert img.theta == pytest.approx(-35.0) def test_equality(self, png_img): width0, height0 = png_img.default_size png_img2 = images.Img.from_path(png_img.path) assert png_img == png_img2 png_img.width = width0 * 2 png_img2.height = width0 * 2 assert png_img != png_img2 def test_setting_negative_size(self, png_img): png_img.width = -90 assert png_img.width == 1 png_img.height = 0 assert png_img.height == 1 def test_pattern(self, path_n_bytes_image): path, bytes_image = path_n_bytes_image img = images.Img(bytes_image) assert isinstance(img.pattern, cairocffi.SurfacePattern) def test_surface_resize(self, path_n_bytes_image_pngs): path, bytes_image = path_n_bytes_image_pngs img = images.Img.from_path(path) original_width = img.width original_height = img.height img.width = 2.0 * img.default_size.width assert img.surface.get_width() == 2 * original_width img.height = 3.0 * img.default_size.height assert img.surface.get_height() == 3 * original_height def test_pattern_rotate(self, path_n_bytes_image): path, bytes_image = path_n_bytes_image img = images.Img(bytes_image) img.theta = 90.0 assert img.theta == 90.0 t_matrix = img.pattern.get_matrix().as_tuple() assert_approx_equal(t_matrix, (0.0, 1.0, -1.0, 0.0)) img.theta = 45.0 t_matrix = img.pattern.get_matrix().as_tuple() from math import sqrt s2o2 = sqrt(2) / 2.0 assert_approx_equal(t_matrix, (s2o2, s2o2, -s2o2, s2o2)) del img.theta assert img.theta == pytest.approx(0.0) class TestImgScale: def test_scale(self, png_img): size = png_img.default_size png_img.scale(2, 3) assert png_img.width == 2 * size.width assert png_img.height == 3 * size.height def test_scale_rounding(self, png_img): size = png_img.default_size png_img.scale(1.99999, 2.99999) assert png_img.width == 2 * size.width assert png_img.height == 3 * size.height def test_scale_width_lock(self, png_img): size = png_img.default_size png_img.scale(width_factor=10, lock_aspect_ratio=True) assert png_img.width == 10 * size.width assert png_img.height == 10 * size.height def test_scale_height_lock(self, png_img): size = png_img.default_size png_img.scale(height_factor=11, lock_aspect_ratio=True) assert png_img.height == 11 * size.height assert png_img.width == 11 * size.width def test_scale_fail_lock(self, png_img): with pytest.raises(ValueError): png_img.scale(0.5, 4.0, lock_aspect_ratio=True) def test_scale_fail(self, png_img): with pytest.raises(ValueError): png_img.scale() class TestImgResize: def test_resize(self, png_img): png_img.resize(100, 100) assert png_img.width == 100 assert png_img.height == 100 def test_resize_width(self, png_img): size = png_img.default_size ratio = size.width / size.height png_img.resize(width=40) assert png_img.width == 40 assert (png_img.width / png_img.height) == pytest.approx(ratio) def test_resize_height(self, png_img): size = png_img.default_size ratio = size.width / size.height png_img.resize(height=10) assert png_img.height == 10 assert (png_img.width / png_img.height) == pytest.approx(ratio) class TestLoader: @pytest.fixture(scope="function") def loader(self): png_dir = path.join(DATA_DIR, "png") svg_dir = path.join(DATA_DIR, "svg") return images.Loader(svg_dir, png_dir) def test_audio_volume_muted(self, loader): name = "audio-volume-muted" result = loader(name) assert isinstance(result[name], images.Img) assert result[name].path.endswith(".svg") def test_audio_volume_muted_png(self, loader): name = "audio-volume-muted.png" result = loader(name) assert isinstance(result[name], images.Img) assert result[name].path.endswith(".png") def test_load_file_missing(self, loader): names = ("audio-asdlfjasdvolume-muted", "audio-volume-muted") with pytest.raises(images.LoadingError): loader(*names) qtile-0.31.0/test/extension/0000775000175000017500000000000014762660347015631 5ustar epsilonepsilonqtile-0.31.0/test/extension/test_base.py0000664000175000017500000000541414762660347020160 0ustar epsilonepsilon# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest from libqtile.extension.base import RunCommand, _Extension parameters = [ ("#000", "#000"), ("#000000", "#000000"), ("000", "#000"), ("000000", "#000000"), ("#0000", None), ("0000", None), (0, None), ] @pytest.mark.parametrize("value,expected", parameters) def test_valid_colours(value, expected): extension = _Extension(foreground=value) extension._configure(None) assert extension.foreground == expected def test_valid_colours_extension_defaults(monkeypatch): defaults = { "foreground": "00ff00", "background": "000000", "selected_foreground": "000000", "selected_background": "00ff00", } extension = _Extension(foreground="0000ff") # Set defaults after widget is created to mimic behaviour of extension being # initialised in config. monkeypatch.setattr(_Extension, "global_defaults", defaults) extension._configure(None) assert extension.foreground == "#0000ff" assert extension.background == "#000000" assert extension.selected_foreground == "#000000" assert extension.selected_background == "#00ff00" def test_base_methods(): class FakeQtile: pass qtile = FakeQtile() extension = _Extension() extension._configure(qtile) assert extension.qtile is qtile with pytest.raises(NotImplementedError): extension.run() def test_run_command(monkeypatch): def fake_popen(cmd, *args, **kwargs): return cmd monkeypatch.setattr("libqtile.extension.base.Popen", fake_popen) extension = RunCommand(command="command --arg1 --arg2") assert extension.command == "command --arg1 --arg2" assert extension.run() == "command --arg1 --arg2" qtile-0.31.0/test/extension/test_dmenu.py0000664000175000017500000001104514762660347020353 0ustar epsilonepsilon# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from libqtile.extension.base import _Extension from libqtile.extension.dmenu import Dmenu, DmenuRun, J4DmenuDesktop BLACK = "#000000" def test_dmenu_configuration_options(): """ Test that configuration options are correctly translated into command options for dmenu. """ _Extension.global_defaults = {} opts = [ ({}, ["dmenu"]), ({"dmenu_command": "testdmenu --test-option"}, ["testdmenu", "--test-option"]), ({"dmenu_command": ["testdmenu", "--test-option"]}, ["testdmenu", "--test-option"]), ({}, ["-fn", "sans"]), ({"dmenu_font": "testfont"}, ["-fn", "testfont"]), ({"font": "testfont"}, ["-fn", "testfont"]), ({"font": "testfont", "fontsize": 12}, ["-fn", "testfont-12"]), ({"dmenu_bottom": True}, ["-b"]), ({"dmenu_ignorecase": True}, ["-i"]), ({"dmenu_lines": 5}, ["-l", "5"]), ({"dmenu_prompt": "testprompt"}, ["-p", "testprompt"]), ({"background": BLACK}, ["-nb", BLACK]), ({"foreground": BLACK}, ["-nf", BLACK]), ({"selected_background": BLACK}, ["-sb", BLACK]), ({"selected_foreground": BLACK}, ["-sf", BLACK]), ({"dmenu_height": 100}, ["-h", "100"]), ] # Loop over options, create an instance of Dmenu with the provided "config" # find the index of the first item in "output" and check any following items # match the expected output for config, output in opts: extension = Dmenu(**config) extension._configure(None) index = extension.configured_command.index(output[0]) assert output == extension.configured_command[index : index + len(output)] def test_dmenu_run(monkeypatch): def fake_popen(cmd, *args, **kwargs): class PopenObj: def communicate(self, value_in, *args): return [value_in, None] return PopenObj() monkeypatch.setattr("libqtile.extension.base.Popen", fake_popen) # dmenu_lines is set to the lower of config value and len(items) so set a high value now extension = Dmenu(dmenu_lines=5) extension._configure(None) items = ["test1", "test2"] assert extension.run(items) == "test1\ntest2\n" # dmenu_lines should be length of items assert extension.configured_command[-2:] == ["-l", "2"] def test_dmenurun_extension(): extension = DmenuRun() assert extension.dmenu_command == "dmenu_run" def test_j4dmenu_configuration_options(): """ Test that configuration options are correctly translated into command options for dmenu. """ _Extension.global_defaults = {} opts = [ ({}, ["j4-dmenu-desktop", "--dmenu"]), ({"font": "testfont"}, ["dmenu -fn testfont"]), # Dmenu settings are applied too ({"j4dmenu_use_xdg_de": True}, ["--use-xdg-de"]), ({"j4dmenu_display_binary": True}, ["--display-binary"]), ({"j4dmenu_generic": False}, ["--no-generic"]), ({"j4dmenu_terminal": "testterminal"}, ["--term", "testterminal"]), ({"j4dmenu_usage_log": "testlog"}, ["--usage-log", "testlog"]), ] # Loop over options, create an instance of J4DmenuDesktop with the provided "config" # find the index of the first item in "output" and check any following items # match the expected output for config, output in opts: extension = J4DmenuDesktop(**config) extension._configure(None) index = extension.configured_command.index(output[0]) assert output == extension.configured_command[index : index + len(output)] qtile-0.31.0/test/extension/__init__.py0000664000175000017500000000000014762660347017730 0ustar epsilonepsilonqtile-0.31.0/test/extension/test_command_set.py0000664000175000017500000001144214762660347021535 0ustar epsilonepsilon# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import logging import pytest from libqtile.extension.command_set import CommandSet from libqtile.log_utils import init_log, logger @pytest.fixture def fake_qtile(): class FakeQtile: def spawn(self, value): logger.warning(value) yield FakeQtile() @pytest.fixture def log_extension_output(monkeypatch): init_log() def fake_popen(cmd, *args, **kwargs): class PopenObj: def communicate(self, value_in, *args): if value_in.startswith(b"missing"): return [b"something_else", None] else: return [value_in, None] return PopenObj() monkeypatch.setattr("libqtile.extension.base.Popen", fake_popen) yield @pytest.mark.usefixtures("log_extension_output") def test_command_set_valid_command(caplog, fake_qtile): """Extension should run pre-commands and selected command.""" extension = CommandSet(pre_commands=["run pre-command"], commands={"key": "run testcommand"}) extension._configure(fake_qtile) extension.run() assert caplog.record_tuples == [ ("libqtile", logging.WARNING, "run pre-command"), ("libqtile", logging.WARNING, "run testcommand"), ] @pytest.mark.usefixtures("log_extension_output") def test_command_set_invalid_command(caplog, fake_qtile): """Where the key is not in "commands", no command will be run.""" extension = CommandSet( pre_commands=["run pre-command"], commands={"missing": "run testcommand"} ) extension._configure(fake_qtile) extension.run() assert caplog.record_tuples == [("libqtile", logging.WARNING, "run pre-command")] @pytest.mark.usefixtures("log_extension_output") def test_command_set_inside_command_set_valid_command(caplog, fake_qtile): """Extension should run pre-commands and selected command.""" inside_command = CommandSet( pre_commands=["run inner pre-command"], commands={"key": "run testcommand"}, ) inside_command._configure(fake_qtile) extension = CommandSet( pre_commands=["run pre-command"], commands={"key": inside_command}, ) extension._configure(fake_qtile) extension.run() assert caplog.record_tuples == [ ("libqtile", logging.WARNING, "run pre-command"), ( "libqtile", logging.WARNING, "run inner pre-command", ), # pre-command of the inside_command ("libqtile", logging.WARNING, "run testcommand"), ] @pytest.mark.usefixtures("log_extension_output") def test_command_set_inside_command_set_invalid_command(caplog, fake_qtile): """Where the key is not in "commands", no command will be run.""" inside_command = CommandSet( pre_commands=["run inner pre-command"], commands={"key": "run testcommand"}, # doesn't really matter what command ) inside_command._configure(fake_qtile) extension = CommandSet(pre_commands=["run pre-command"], commands={"missing": inside_command}) extension._configure(fake_qtile) extension.run() assert caplog.record_tuples == [("libqtile", logging.WARNING, "run pre-command")] caplog.clear() inside_command = CommandSet( pre_commands=["run inner pre-command"], commands={"missing": "run testcommand"}, ) inside_command._configure(fake_qtile) extension = CommandSet( pre_commands=["run pre-command"], commands={"key": inside_command}, ) extension._configure(fake_qtile) extension.run() assert caplog.record_tuples == [ ("libqtile", logging.WARNING, "run pre-command"), ( "libqtile", logging.WARNING, "run inner pre-command", ), # pre-command of the inside_command ] qtile-0.31.0/test/extension/test_window_list.py0000664000175000017500000000554614762660347021616 0ustar epsilonepsilon# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest import libqtile.bar import libqtile.config import libqtile.layout from libqtile.confreader import Config from libqtile.extension.window_list import WindowList from libqtile.lazy import lazy @pytest.fixture def extension_manager(monkeypatch, manager_nospawn): extension = WindowList() # We want the value returned immediately def fake_popen(cmd, *args, **kwargs): class PopenObj: def communicate(self, value_in, *args): return [value_in, None] return PopenObj() monkeypatch.setattr("libqtile.extension.base.Popen", fake_popen) class ManagerConfig(Config): groups = [ libqtile.config.Group("a"), libqtile.config.Group("b"), ] layouts = [libqtile.layout.max.Max()] keys = [ libqtile.config.Key(["control"], "k", lazy.run_extension(extension)), ] screens = [ libqtile.config.Screen( bottom=libqtile.bar.Bar([], 20), ) ] manager_nospawn.start(ManagerConfig) yield manager_nospawn def test_window_list(extension_manager): """Test WindowList extension switches group.""" # Launch a window and verify it's on the current group extension_manager.test_window("one") assert len(extension_manager.c.group.info()["windows"]) == 1 # Switch group and verify no windows in group extension_manager.c.group["b"].toscreen() assert len(extension_manager.c.group.info()["windows"]) == 0 # Toggle extension (which is patched to return immediately) # Check that window is visible on original group extension_manager.c.simulate_keypress(["control"], "k") assert len(extension_manager.c.group.info()["windows"]) == 1 assert extension_manager.c.group.info()["label"] == "a" qtile-0.31.0/test/test_qtile_cmd.py0000664000175000017500000001311414762660347017167 0ustar epsilonepsilon# Copyright (c) 2020 Guangwang Huang # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import os import re import subprocess import pytest import libqtile.bar import libqtile.config import libqtile.layout import libqtile.widget from libqtile.confreader import Config from libqtile.lazy import lazy class ServerConfig(Config): auto_fullscreen = True keys = [ libqtile.config.Key(["mod4"], "Return", lazy.spawn("xterm")), libqtile.config.Key(["mod4"], "t", lazy.spawn("xterm"), desc="dummy description"), libqtile.config.Key([], "y", desc="noop"), libqtile.config.KeyChord( ["mod4"], "q", [ libqtile.config.KeyChord( [], "q", [ libqtile.config.Key([], "a", lazy.togroup("a")), ], ), # unnamed libqtile.config.Key([], "b", lazy.togroup("b")), ], mode="named", ), ] mouse = [] groups = [ libqtile.config.Group("a"), libqtile.config.Group("b"), libqtile.config.Group("c"), ] layouts = [ libqtile.layout.Stack(num_stacks=1), libqtile.layout.Stack(num_stacks=2), libqtile.layout.Stack(num_stacks=3), ] floating_layout = libqtile.resources.default_config.floating_layout screens = [ libqtile.config.Screen( bottom=libqtile.bar.Bar( [ libqtile.widget.TextBox(name="one"), ], 20, ), ), libqtile.config.Screen( bottom=libqtile.bar.Bar( [ libqtile.widget.TextBox(name="two"), ], 20, ), ), ] server_config = pytest.mark.parametrize("manager", [ServerConfig], indirect=True) def run_qtile_cmd(args, no_eval=False): cmd = os.path.join(os.path.dirname(__file__), "..", "bin", "qtile") argv = [cmd, "cmd-obj"] argv.extend(args.split()) pipe = subprocess.Popen(argv, stdout=subprocess.PIPE) output, _ = pipe.communicate() output = output.decode() if no_eval: return output return eval(output) # as returned by pprint.pprint @server_config def test_qtile_cmd(manager): manager.test_window("foo") wid = manager.c.window.info()["id"] for obj in ["window", "group", "screen"]: assert run_qtile_cmd(f"-s {manager.sockfile} -o {obj} -f info") layout = run_qtile_cmd(f"-s {manager.sockfile} -o layout -f info") assert layout["name"] == "stack" assert layout["group"] == "a" window = run_qtile_cmd(f"-s {manager.sockfile} -o window {wid} -f info") assert window["id"] == wid assert window["name"] == "foo" assert window["group"] == "a" group = run_qtile_cmd("-s {} -o group {} -f info".format(manager.sockfile, "a")) assert group["name"] == "a" assert group["screen"] == 0 assert group["layouts"] == ["stack", "stack", "stack"] assert group["focus"] == "foo" assert run_qtile_cmd(f"-s {manager.sockfile} -o screen {0} -f info") == { "height": 600, "index": 0, "width": 800, "x": 0, "y": 0, } bar = run_qtile_cmd("-s {} -o bar {} -f info".format(manager.sockfile, "bottom")) assert bar["height"] == 20 assert bar["width"] == 800 assert bar["size"] == 20 assert bar["position"] == "bottom" @server_config def test_display_kb(manager): from pprint import pprint cmd = f"-s {manager.sockfile} -o root -f display_kb" table = run_qtile_cmd(cmd) print(table) pprint(table) assert table.count("\n") >= 2 assert re.match(r"(?m)^Mode\s{3,}KeySym\s{3,}Mod\s{3,}Command\s{3,}Desc\s*$", table) assert re.search(r"(?m)^\s{3,}Return\s{3,}mod4\s{3,}spawn\('xterm'\)\s*$", table) assert re.search( r"(?m)^\s{3,}t\s{3,}mod4\s{3,}spawn\('xterm'\)\s{3,}dummy description\s*$", table ) assert re.search(r"(?m)^\s{3,}q\s{3,}mod4\s{13,}Enter named mode\s*$", table) assert re.search(r"(?m)^named\s{3,}q\s{13,}Enter mode\s*$", table) assert re.search(r"(?m)^named\s{3,}b\s{9,}togroup\('b'\)\s*$", table) assert re.search(r"(?m)^named>_\s{3,}a\s{9,}togroup\('a'\)\s*$", table) assert re.search(r"(?m)^\s{3,}y\s{9,}\s*$", table) is None @server_config def test_cmd_obj_root_node(manager): base = f"-s {manager.sockfile} -f ok" cmd_no_root = base cmd_with_root = f"{base} -o root" assert run_qtile_cmd(cmd_no_root, no_eval=True) == run_qtile_cmd(cmd_with_root, no_eval=True) qtile-0.31.0/test/test_qtile_help.py0000664000175000017500000000333714762660347017362 0ustar epsilonepsilon# # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import os import subprocess def run_qtile(args): cmd = os.path.join(os.path.dirname(__file__), "..", "bin", "qtile") argv = [cmd] argv.extend(args) proc = subprocess.Popen(argv, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = proc.communicate() assert proc.returncode == 0 stdout = stdout.decode() stderr = stderr.decode() return (stdout, stderr) def test_cmd_help_subcommand(): args = ["help"] stdout, stderr = run_qtile(args) assert "usage: qtile" in stdout assert stderr == "" def test_cmd_help_param(): args = ["--help"] stdout, stderr = run_qtile(args) assert "usage: qtile" in stdout assert stderr == "" qtile-0.31.0/test/test_configurable.py0000664000175000017500000000462314762660347017673 0ustar epsilonepsilon# Copyright (c) 2015 Michael Killough # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from libqtile import configurable class ConfigurableWithFallback(configurable.Configurable): defaults = [ ("foo", 3, ""), ] bar = configurable.ExtraFallback("bar", "foo") def __init__(self, **config): configurable.Configurable.__init__(self, **config) self.add_defaults(ConfigurableWithFallback.defaults) def test_use_fallback(): c = ConfigurableWithFallback() assert c.foo == c.bar == 3 c = ConfigurableWithFallback(foo=5) assert c.foo == c.bar == 5 def test_use_fallback_if_set_to_none(): # Even if it is explicitly set to None, we should still # use the fallback. Could be useful if widget_defaults # were to set bar= and we wanted to specify that an # individual widget should fall back to using foo. c = ConfigurableWithFallback(foo=7, bar=None) assert c.foo == c.bar == 7 c = ConfigurableWithFallback(foo=9) c.bar = None assert c.foo == c.bar == 9 def test_dont_use_fallback_if_set(): c = ConfigurableWithFallback(bar=5) assert c.foo == 3 assert c.bar == 5 c = ConfigurableWithFallback(bar=0) assert c.foo == 3 assert c.bar == 0 c = ConfigurableWithFallback(foo=1, bar=2) assert c.foo == 1 assert c.bar == 2 c = ConfigurableWithFallback(foo=1) c.bar = 3 assert c.foo == 1 assert c.bar == 3 qtile-0.31.0/test/backend/0000775000175000017500000000000014762660347015204 5ustar epsilonepsilonqtile-0.31.0/test/backend/wayland/0000775000175000017500000000000014762660347016643 5ustar epsilonepsilonqtile-0.31.0/test/backend/wayland/conftest.py0000664000175000017500000000527514762660347021053 0ustar epsilonepsilonimport contextlib import os import textwrap import pytest from test.helpers import BareConfig, TestManager try: from libqtile.backend.wayland.core import Core has_wayland = True except ImportError: has_wayland = False from test.helpers import Backend wlr_env = { "WLR_BACKENDS": "headless", "WLR_LIBINPUT_NO_DEVICES": "1", "WLR_RENDERER_ALLOW_SOFTWARE": "1", "WLR_RENDERER": "pixman", "XDG_RUNTIME_DIR": "/tmp", } @contextlib.contextmanager def wayland_environment(outputs): """This backend just needs some environmental variables set""" env = wlr_env.copy() env["WLR_HEADLESS_OUTPUTS"] = str(outputs) yield env @pytest.fixture(scope="function") def wmanager(request, wayland_session): """ This replicates the `manager` fixture except that the Wayland backend is hard-coded. We cannot parametrize the `backend_name` fixture module-wide because it gets parametrized by `pytest_generate_tests` in test/conftest.py and only one of these parametrize calls can be used. """ config = getattr(request, "param", BareConfig) backend = WaylandBackend(wayland_session) with TestManager(backend, request.config.getoption("--debuglog")) as manager: manager.start(config) yield manager class WaylandBackend(Backend): name = "wayland" def __init__(self, env, args=()): self.env = env self.args = args self.core = Core self.manager = None def create(self): """This is used to instantiate the Core""" os.environ.update(self.env) return self.core(*self.args) def configure(self, manager): """This backend needs to get WAYLAND_DISPLAY variable.""" success, display = manager.c.eval("self.core.display_name") assert success self.env["WAYLAND_DISPLAY"] = display def fake_click(self, x, y): """Click at the specified coordinates""" # Currently only restacks windows, and does not trigger bindings self.manager.c.eval( textwrap.dedent( f""" self.core.warp_pointer({x}, {y}) self.core._focus_by_click() """ ) ) def get_all_windows(self): """Get a list of all windows in ascending order of Z position""" return self.manager.c.core.query_tree() def new_xdg_client(wmanager, name="xdg"): """Helper to create 'regular' windows in the XDG shell""" pid = wmanager.test_window(name) wmanager.c.sync() return pid def new_layer_client(wmanager, name="layer"): """Helper to create layer shell windows, which are always static""" pid = wmanager.test_notification(name) wmanager.c.sync() return pid qtile-0.31.0/test/backend/wayland/test_window.py0000664000175000017500000000236014762660347021564 0ustar epsilonepsilonimport pytest from test.backend.wayland.conftest import new_layer_client, new_xdg_client from test.conftest import BareConfig try: # Check to see if we should skip layer shell tests import gi gi.require_version("Gtk", "3.0") gi.require_version("GtkLayerShell", "0.1") from gi.repository import GtkLayerShell # noqa: F401 has_layer_shell = True except (ImportError, ValueError): has_layer_shell = False pytestmark = pytest.mark.skipif(not has_layer_shell, reason="GtkLayerShell not available") bare_config = pytest.mark.parametrize("wmanager", [BareConfig], indirect=True) @bare_config def test_info(wmanager): """ Check windows are providing some Wayland-specific info. """ # Regular window via XDG shell pid = new_xdg_client(wmanager) assert wmanager.c.window.info()["shell"] == "XDG" # Regular XDG shell window converted to Static wmanager.c.window.static() wid = wmanager.c.windows()[0]["id"] assert wmanager.c.window[wid].info()["shell"] == "XDG" wmanager.kill_window(pid) # Static window via layer shell pid = new_layer_client(wmanager) wid = wmanager.c.windows()[0]["id"] assert wmanager.c.window[wid].info()["shell"] == "layer" wmanager.kill_window(pid) qtile-0.31.0/test/backend/x11/0000775000175000017500000000000014762660347015615 5ustar epsilonepsilonqtile-0.31.0/test/backend/x11/conftest.py0000664000175000017500000001425414762660347020022 0ustar epsilonepsilonimport contextlib import os import subprocess import pytest import xcffib import xcffib.testing import xcffib.xproto import xcffib.xtest from libqtile.backend.x11.core import Core from libqtile.backend.x11.xcbq import Connection from test.helpers import ( HEIGHT, SECOND_HEIGHT, SECOND_WIDTH, WIDTH, Backend, BareConfig, Retry, TestManager, ) @Retry(ignore_exceptions=(xcffib.ConnectionException,), return_on_fail=True) def can_connect_x11(disp=":0", *, ok=None): if ok is not None and not ok(): raise AssertionError() conn = xcffib.connect(display=disp) conn.disconnect() return True @contextlib.contextmanager def xvfb(): with xcffib.testing.XvfbTest(): display = os.environ["DISPLAY"] if not can_connect_x11(display): raise OSError("Xvfb did not come up") yield @pytest.fixture(scope="session") def display(): # noqa: F841 with xvfb(): yield os.environ["DISPLAY"] class Xephyr: """Spawn Xephyr instance Set-up a Xephyr instance with the given parameters. The Xephyr instance must be started, and then stopped. """ def __init__(self, outputs, xoffset=None): self.outputs = outputs if xoffset is None: self.xoffset = WIDTH else: self.xoffset = xoffset self.proc = None # Handle to Xephyr instance, subprocess.Popen object self.display = None self.display_file = None def __enter__(self): try: self.start_xephyr() except: # noqa: E722 self.stop_xephyr() raise return self def __exit__(self, _exc_type, _exc_val, _exc_tb): self.stop_xephyr() def start_xephyr(self): """Start Xephyr instance Starts the Xephyr instance and sets the `self.display` to the display which is used to setup the instance. """ # get a new display display, self.display_file = xcffib.testing.find_display() self.display = f":{display}" # build up arguments args = [ "Xephyr", "-name", "qtile_test", self.display, "-ac", "-screen", f"{WIDTH}x{HEIGHT}", ] if self.outputs == 2: args.extend( [ "-origin", f"{self.xoffset},0", "-screen", f"{SECOND_WIDTH}x{SECOND_HEIGHT}", ] ) args.extend(["+xinerama"]) self.proc = subprocess.Popen(args) if can_connect_x11(self.display, ok=lambda: self.proc.poll() is None): return # we weren't able to get a display up if self.proc.poll() is None: raise AssertionError("Unable to connect to running Xephyr") else: raise AssertionError( "Unable to start Xephyr, quit with return code " f"{self.proc.returncode}" ) def stop_xephyr(self): """Stop the Xephyr instance""" # Xephyr must be started first if self.proc is None: return # Kill xephyr only if it is running if self.proc.poll() is None: # We should always be able to kill xephyr nicely self.proc.terminate() self.proc.wait() self.proc = None # clean up the lock file for the display we allocated try: self.display_file.close() os.remove(xcffib.testing.lock_path(int(self.display[1:]))) except OSError: pass @contextlib.contextmanager def x11_environment(outputs, **kwargs): """This backend needs a Xephyr instance running""" with xvfb(): with Xephyr(outputs, **kwargs) as x: yield x @pytest.fixture(scope="function") def xmanager(request, xephyr): """ This replicates the `manager` fixture except that the x11 backend is hard-coded. We cannot simply parametrize the `backend_name` fixture module-wide because it gets parametrized by `pytest_generate_tests` in test/conftest.py and only one of these parametrize calls can be used. """ config = getattr(request, "param", BareConfig) backend = XBackend({"DISPLAY": xephyr.display}, args=[xephyr.display]) with TestManager(backend, request.config.getoption("--debuglog")) as manager: manager.display = xephyr.display manager.start(config) yield manager @pytest.fixture(scope="function") def xmanager_nospawn(request, xephyr): """ This replicates the `manager` fixture except that the x11 backend is hard-coded. We cannot simply parametrize the `backend_name` fixture module-wide because it gets parametrized by `pytest_generate_tests` in test/conftest.py and only one of these parametrize calls can be used. """ backend = XBackend({"DISPLAY": xephyr.display}, args=[xephyr.display]) with TestManager(backend, request.config.getoption("--debuglog")) as manager: manager.display = xephyr.display yield manager @pytest.fixture(scope="function") def conn(xmanager): conn = Connection(xmanager.display) yield conn conn.finalize() class XBackend(Backend): name = "x11" def __init__(self, env, args=()): self.env = env self.args = args self.core = Core self.manager = None def fake_click(self, x, y): """Click at the specified coordinates""" conn = Connection(self.env["DISPLAY"]) root = conn.default_screen.root.wid xtest = conn.conn(xcffib.xtest.key) xtest.FakeInput(6, 0, xcffib.xproto.Time.CurrentTime, root, x, y, 0) xtest.FakeInput(4, 1, xcffib.xproto.Time.CurrentTime, root, 0, 0, 0) xtest.FakeInput(5, 1, xcffib.xproto.Time.CurrentTime, root, 0, 0, 0) conn.conn.flush() self.manager.c.sync() conn.finalize() def get_all_windows(self): """Get a list of all windows in ascending order of Z position""" conn = Connection(self.env["DISPLAY"]) root = conn.default_screen.root.wid q = conn.conn.core.QueryTree(root).reply() wins = list(q.children) conn.finalize() return wins qtile-0.31.0/test/backend/x11/test_window.py0000664000175000017500000010376514762660347020551 0ustar epsilonepsilonimport os import shutil import subprocess import tempfile from multiprocessing import Value import pytest import xcffib.xproto import xcffib.xtest import libqtile.config from libqtile import hook, layout, utils from libqtile.backend.x11 import window, xcbq from libqtile.backend.x11.xcbq import Connection from test.conftest import dualmonitor from test.helpers import ( HEIGHT, SECOND_HEIGHT, SECOND_WIDTH, WIDTH, BareConfig, assert_window_died, ) from test.test_images2 import should_skip from test.test_manager import ManagerConfig bare_config = pytest.mark.parametrize("xmanager", [BareConfig], indirect=True) manager_config = pytest.mark.parametrize("xmanager", [ManagerConfig], indirect=True) @manager_config def test_kill_via_message(xmanager, conn): xmanager.test_window("one") window_info = xmanager.c.window.info() data = xcffib.xproto.ClientMessageData.synthetic([0, 0, 0, 0, 0], "IIIII") ev = xcffib.xproto.ClientMessageEvent.synthetic( 32, window_info["id"], conn.atoms["_NET_CLOSE_WINDOW"], data ) conn.default_screen.root.send_event(ev, mask=xcffib.xproto.EventMask.SubstructureRedirect) conn.xsync() assert_window_died(xmanager.c, window_info) @manager_config def test_change_state_via_message(xmanager, conn): xmanager.test_window("one") window_info = xmanager.c.window.info() data = xcffib.xproto.ClientMessageData.synthetic([window.IconicState, 0, 0, 0, 0], "IIIII") ev = xcffib.xproto.ClientMessageEvent.synthetic( 32, window_info["id"], conn.atoms["WM_CHANGE_STATE"], data ) conn.default_screen.root.send_event(ev, mask=xcffib.xproto.EventMask.SubstructureRedirect) conn.xsync() assert xmanager.c.window.info()["minimized"] data = xcffib.xproto.ClientMessageData.synthetic([window.NormalState, 0, 0, 0, 0], "IIIII") ev = xcffib.xproto.ClientMessageEvent.synthetic( 32, window_info["id"], conn.atoms["WM_CHANGE_STATE"], data ) conn.default_screen.root.send_event(ev, mask=xcffib.xproto.EventMask.SubstructureRedirect) conn.xsync() assert not xmanager.c.window.info()["minimized"] def set_urgent(w): w.urgent = True hook.fire("client_urgent_hint_changed", w) return False class UrgentConfig(BareConfig): focus_on_window_activation = "urgent" class SmartConfig(BareConfig): focus_on_window_activation = "smart" class FuncConfig(BareConfig): # must be a static method here because otherwise it gets turned into a MethodType (we need a FunctionType) # this is only an issue in this test and not the real config file focus_on_window_activation = staticmethod(set_urgent) @dualmonitor def test_urgent_hook_fire(xmanager_nospawn): xmanager_nospawn.display = xmanager_nospawn.backend.env["DISPLAY"] conn = Connection(xmanager_nospawn.display) xmanager_nospawn.hook_fired = Value("i", 0) def _hook_test(val): xmanager_nospawn.hook_fired.value += 1 hook.subscribe.client_urgent_hint_changed(_hook_test) xmanager_nospawn.start(UrgentConfig) xmanager_nospawn.test_window("one") window_info = xmanager_nospawn.c.window.info() # send activate window message data = xcffib.xproto.ClientMessageData.synthetic([0, 0, 0, 0, 0], "IIIII") ev = xcffib.xproto.ClientMessageEvent.synthetic( 32, window_info["id"], conn.atoms["_NET_ACTIVE_WINDOW"], data ) conn.default_screen.root.send_event(ev, mask=xcffib.xproto.EventMask.SubstructureRedirect) conn.xsync() xmanager_nospawn.terminate() assert xmanager_nospawn.hook_fired.value == 1 # test that focus_on_window_activation = "smart" also fires the hook xmanager_nospawn.start(SmartConfig, no_spawn=True) xmanager_nospawn.test_window("one") window_info = xmanager_nospawn.c.window.info() xmanager_nospawn.c.window.toscreen(1) # send activate window message ev = xcffib.xproto.ClientMessageEvent.synthetic( 32, window_info["id"], conn.atoms["_NET_ACTIVE_WINDOW"], data ) conn.default_screen.root.send_event(ev, mask=xcffib.xproto.EventMask.SubstructureRedirect) conn.xsync() xmanager_nospawn.terminate() assert xmanager_nospawn.hook_fired.value == 2 # test that a custom function also fires the hook xmanager_nospawn.start(FuncConfig, no_spawn=True) xmanager_nospawn.test_window("one") window_info = xmanager_nospawn.c.window.info() xmanager_nospawn.c.window.toscreen(1) # send activate window message ev = xcffib.xproto.ClientMessageEvent.synthetic( 32, window_info["id"], conn.atoms["_NET_ACTIVE_WINDOW"], data ) conn.default_screen.root.send_event(ev, mask=xcffib.xproto.EventMask.SubstructureRedirect) conn.xsync() xmanager_nospawn.terminate() assert xmanager_nospawn.hook_fired.value == 3 @manager_config def test_default_float_hints(xmanager, conn): xmanager.c.next_layout() w = None def size_hints(): nonlocal w w = conn.create_window(5, 5, 10, 10) # set the size hints hints = [0] * 18 hints[0] = xcbq.NormalHintsFlags["PMinSize"] | xcbq.NormalHintsFlags["PMaxSize"] hints[5] = hints[6] = hints[7] = hints[8] = 10 w.set_property("WM_NORMAL_HINTS", hints, type="WM_SIZE_HINTS", format=32) w.map() conn.xsync() try: xmanager.create_window(size_hints) assert xmanager.c.window.info()["floating"] is True finally: w.kill_client() w = None conn = xcbq.Connection(xmanager.display) def size_hints(): nonlocal w w = conn.create_window(5, 5, 10, 10) # set the aspect hints hints = [0] * 18 hints[0] = xcbq.NormalHintsFlags["PAspect"] hints[11] = hints[12] = hints[13] = hints[14] = 1 w.set_property("WM_NORMAL_HINTS", hints, type="WM_SIZE_HINTS", format=32) w.map() conn.conn.flush() try: xmanager.create_window(size_hints) assert xmanager.c.window.info()["floating"] is True info = xmanager.c.window.info() assert info["width"] == 10 assert info["height"] == 10 xmanager.c.window.toggle_floating() assert xmanager.c.window.info()["floating"] is False info = xmanager.c.window.info() assert info["width"] == 398 assert info["height"] == 578 xmanager.c.window.toggle_fullscreen() info = xmanager.c.window.info() assert info["width"] == 800 assert info["height"] == 600 finally: w.kill_client() conn.finalize() @manager_config def test_user_position(xmanager, conn): w = None def user_position_window(): nonlocal w w = conn.create_window(5, 5, 10, 10) # xmanager config automatically floats "float" w.set_property("WM_CLASS", "float", type="STRING", format=8) # set the user specified position flag hints = [0] * 18 hints[0] = xcbq.NormalHintsFlags["USPosition"] w.set_property("WM_NORMAL_HINTS", hints, type="WM_SIZE_HINTS", format=32) w.map() conn.conn.flush() try: xmanager.create_window(user_position_window) assert xmanager.c.window.info()["floating"] is True assert xmanager.c.window.info()["x"] == 5 assert xmanager.c.window.info()["y"] == 5 assert xmanager.c.window.info()["width"] == 10 assert xmanager.c.window.info()["height"] == 10 finally: w.kill_client() def wait_for_focus_events(conn): got_take_focus = False got_focus_in = False while True: event = conn.conn.poll_for_event() if not event: break if ( isinstance(event, xcffib.xproto.ClientMessageEvent) and event.type != conn.atoms["WM_TAKE_FOCUS"] ): got_take_focus = True if isinstance(event, xcffib.xproto.FocusInEvent): got_focus_in = True return got_take_focus, got_focus_in @manager_config def test_only_one_focus(xmanager, conn): w = None def both_wm_take_focus_and_input_hint(): nonlocal w w = conn.create_window(5, 5, 10, 10) w.set_attribute(eventmask=xcffib.xproto.EventMask.FocusChange) # xmanager config automatically floats "float" w.set_property("WM_CLASS", "float", type="STRING", format=8) # set both the input hit hints = [0] * 14 hints[0] = xcbq.HintsFlags["InputHint"] hints[1] = 1 # set hints to 1, i.e. we want them w.set_property("WM_HINTS", hints, type="WM_HINTS", format=32) # and add the WM_PROTOCOLS protocol WM_TAKE_FOCUS conn.conn.core.ChangePropertyChecked( xcffib.xproto.PropMode.Append, w.wid, conn.atoms["WM_PROTOCOLS"], conn.atoms["ATOM"], 32, 1, [conn.atoms["WM_TAKE_FOCUS"]], ).check() w.map() conn.conn.flush() try: xmanager.create_window(both_wm_take_focus_and_input_hint) assert xmanager.c.window.info()["floating"] is True got_take_focus, got_focus_in = wait_for_focus_events(conn) assert not got_take_focus assert got_focus_in finally: w.kill_client() @manager_config def test_only_wm_protocols_focus(xmanager, conn): w = None def only_wm_protocols_focus(): nonlocal w w = conn.create_window(5, 5, 10, 10) w.set_attribute(eventmask=xcffib.xproto.EventMask.FocusChange) # xmanager config automatically floats "float" w.set_property("WM_CLASS", "float", type="STRING", format=8) hints = [0] * 14 hints[0] = xcbq.HintsFlags["InputHint"] hints[1] = 0 # set hints to 0, i.e. we don't want them w.set_property("WM_HINTS", hints, type="WM_HINTS", format=32) # add the WM_PROTOCOLS protocol WM_TAKE_FOCUS conn.conn.core.ChangePropertyChecked( xcffib.xproto.PropMode.Append, w.wid, conn.atoms["WM_PROTOCOLS"], conn.atoms["ATOM"], 32, 1, [conn.atoms["WM_TAKE_FOCUS"]], ).check() w.map() conn.conn.flush() try: xmanager.create_window(only_wm_protocols_focus) assert xmanager.c.window.info()["floating"] is True got_take_focus, got_focus_in = wait_for_focus_events(conn) assert got_take_focus assert not got_focus_in finally: w.kill_client() @manager_config def test_only_input_hint_focus(xmanager, conn): w = None def only_input_hint(): nonlocal w w = conn.create_window(5, 5, 10, 10) w.set_attribute(eventmask=xcffib.xproto.EventMask.FocusChange) # xmanager config automatically floats "float" w.set_property("WM_CLASS", "float", type="STRING", format=8) # set the input hint hints = [0] * 14 hints[0] = xcbq.HintsFlags["InputHint"] hints[1] = 1 # set hints to 1, i.e. we want them w.set_property("WM_HINTS", hints, type="WM_HINTS", format=32) w.map() conn.conn.flush() try: xmanager.create_window(only_input_hint) assert xmanager.c.window.info()["floating"] is True got_take_focus, got_focus_in = wait_for_focus_events(conn) assert not got_take_focus assert got_focus_in finally: w.kill_client() @manager_config def test_no_focus(xmanager, conn): w = None def no_focus(): nonlocal w w = conn.create_window(5, 5, 10, 10) w.set_attribute(eventmask=xcffib.xproto.EventMask.FocusChange) # xmanager config automatically floats "float" w.set_property("WM_CLASS", "float", type="STRING", format=8) hints = [0] * 14 hints[0] = xcbq.HintsFlags["InputHint"] w.set_property("WM_HINTS", hints, type="WM_HINTS", format=32) w.map() conn.conn.flush() try: xmanager.create_window(no_focus) assert xmanager.c.window.info()["floating"] is True got_take_focus, got_focus_in = wait_for_focus_events(conn) assert not got_take_focus assert not got_focus_in finally: w.kill_client() @manager_config def test_hints_setting_unsetting(xmanager, conn): w = None def no_input_hint(): nonlocal w w = conn.create_window(5, 5, 10, 10) w.map() conn.conn.flush() try: xmanager.create_window(no_input_hint) # We default the input hint to true since some non-trivial number of # windows don't set it, and most of them want focus. The spec allows # WMs to assume "convenient" values. assert xmanager.c.window.get_hints()["input"] # now try to "update" it, but don't really set an update (i.e. the # InputHint bit is 0, so the WM should not derive a new hint from the # content of the message at the input hint's offset) hints = [0] * 14 w.set_property("WM_HINTS", hints, type="WM_HINTS", format=32) conn.flush() # should still have the hint assert xmanager.c.window.get_hints()["input"] # now do an update: turn it off hints[0] = xcbq.HintsFlags["InputHint"] hints[1] = 0 w.set_property("WM_HINTS", hints, type="WM_HINTS", format=32) conn.flush() assert not xmanager.c.window.get_hints()["input"] # turn it back on hints[0] = xcbq.HintsFlags["InputHint"] hints[1] = 1 w.set_property("WM_HINTS", hints, type="WM_HINTS", format=32) conn.flush() assert xmanager.c.window.get_hints()["input"] finally: w.kill_client() @dualmonitor @manager_config def test_strut_handling(xmanager, conn): w = [] def has_struts(): nonlocal w w.append(conn.create_window(0, 0, 10, 10)) w[-1].set_property("_NET_WM_STRUT_PARTIAL", [0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 800]) w[-1].map() conn.conn.flush() def with_gaps_left(): nonlocal w w.append(conn.create_window(800, 0, 10, 10)) w[-1].set_property("_NET_WM_STRUT_PARTIAL", [820, 0, 0, 0, 0, 480, 0, 0, 0, 0, 0, 0]) w[-1].map() conn.conn.flush() def with_gaps_bottom(): nonlocal w w.append(conn.create_window(800, 0, 10, 10)) w[-1].set_property("_NET_WM_STRUT_PARTIAL", [0, 0, 0, 130, 0, 0, 0, 0, 0, 0, 800, 1440]) w[-1].map() conn.conn.flush() def test_initial_state(): while xmanager.c.screen.info()["index"] != 0: xmanager.c.next_screen() assert xmanager.c.window.info()["width"] == 798 assert xmanager.c.window.info()["height"] == 578 assert xmanager.c.window.info()["x"] == 0 assert xmanager.c.window.info()["y"] == 0 bar_id = xmanager.c.bar["bottom"].info()["window"] bar = xmanager.c.window[bar_id].info() assert bar["height"] == 20 assert bar["y"] == 580 xmanager.c.next_screen() assert xmanager.c.window.info()["width"] == 638 assert xmanager.c.window.info()["height"] == 478 assert xmanager.c.window.info()["x"] == 800 assert xmanager.c.window.info()["y"] == 0 xmanager.test_window("one") xmanager.c.next_screen() xmanager.test_window("two") test_initial_state() try: while xmanager.c.screen.info()["index"] != 0: xmanager.c.next_screen() xmanager.create_window(has_struts) xmanager.c.window.static(None, None, None, None, None) assert xmanager.c.window.info()["width"] == 798 assert xmanager.c.window.info()["height"] == 568 assert xmanager.c.window.info()["x"] == 0 assert xmanager.c.window.info()["y"] == 0 bar_id = xmanager.c.bar["bottom"].info()["window"] bar = xmanager.c.window[bar_id].info() assert bar["height"] == 20 assert bar["y"] == 570 xmanager.c.next_screen() xmanager.create_window(with_gaps_bottom) xmanager.c.window.static(None, None, None, None, None) xmanager.create_window(with_gaps_left) xmanager.c.window.static(None, None, None, None, None) assert xmanager.c.window.info()["width"] == 618 assert xmanager.c.window.info()["height"] == 468 assert xmanager.c.window.info()["x"] == 820 assert xmanager.c.window.info()["y"] == 0 finally: for win in w: win.kill_client() conn.conn.flush() test_initial_state() class CursorWarpConfig(ManagerConfig): cursor_warp = "floating_only" screens = [ libqtile.config.Screen( bottom=libqtile.bar.Bar( [ libqtile.widget.GroupBox(), ], 20, ), ), libqtile.config.Screen( bottom=libqtile.bar.Bar( [ libqtile.widget.GroupBox(), ], 20, ), ), ] @dualmonitor @pytest.mark.parametrize( "xmanager", [CursorWarpConfig], indirect=True, ) def test_cursor_warp(xmanager, conn): root = conn.default_screen.root.wid assert xmanager.c.screen.info()["index"] == 0 xmanager.test_window("one") xmanager.c.window.set_position_floating(50, 50) xmanager.c.window.set_size_floating(50, 50) xmanager.c.to_screen(1) assert xmanager.c.screen.info()["index"] == 1 p = conn.conn.core.QueryPointer(root).reply() # Here pointer should warp to the second screen as there are no windows # there. assert p.root_x == WIDTH + SECOND_WIDTH // 2 # Reduce the bar height from the screen height. assert p.root_y == (SECOND_HEIGHT - 20) // 2 xmanager.c.to_screen(0) assert xmanager.c.window.info()["name"] == "one" p = conn.conn.core.QueryPointer(xmanager.c.window.info()["id"]).reply() # Here pointer should warp to the window. assert p.win_x == 25 assert p.win_y == 25 assert p.same_screen @dualmonitor def test_click_focus_screen(xmanager): screen1 = (WIDTH // 2, HEIGHT // 2) screen2 = (WIDTH + SECOND_WIDTH // 2, SECOND_HEIGHT // 2) xmanager.c.eval(f"self.core.warp_pointer{screen1}") assert xmanager.c.screen.info()["index"] == 0 # Warping alone shouldn't change the current screen xmanager.c.eval(f"self.core.warp_pointer{screen2}") assert xmanager.c.screen.info()["index"] == 0 # Clicking should xmanager.backend.fake_click(*screen2) assert xmanager.c.screen.info()["index"] == 1 xmanager.c.eval(f"self.core.warp_pointer{screen1}") assert xmanager.c.screen.info()["index"] == 1 xmanager.backend.fake_click(*screen1) assert xmanager.c.screen.info()["index"] == 0 @bare_config def test_min_size_hint(xmanager, conn): w = None def size_hints(): nonlocal w w = conn.create_window(0, 0, 100, 100) # set the size hints hints = [0] * 18 hints[0] = xcbq.NormalHintsFlags["PMinSize"] hints[5] = hints[6] = 100 w.set_property("WM_NORMAL_HINTS", hints, type="WM_SIZE_HINTS", format=32) w.map() conn.conn.flush() try: xmanager.create_window(size_hints) xmanager.c.window.enable_floating() assert xmanager.c.window.info()["width"] == 100 assert xmanager.c.window.info()["height"] == 100 xmanager.c.window.set_size_floating(50, 50) assert xmanager.c.window.info()["width"] == 100 assert xmanager.c.window.info()["height"] == 100 xmanager.c.window.set_size_floating(200, 200) assert xmanager.c.window.info()["width"] == 200 assert xmanager.c.window.info()["height"] == 200 finally: w.kill_client() @bare_config def test_min_size_hint_no_flag(xmanager, conn): w = None def size_hints(): nonlocal w w = conn.create_window(0, 0, 100, 100) # set the size hints hints = [0] * 18 hints[5] = hints[6] = 100 w.set_property("WM_NORMAL_HINTS", hints, type="WM_SIZE_HINTS", format=32) w.map() conn.conn.flush() try: xmanager.create_window(size_hints) xmanager.c.window.enable_floating() assert xmanager.c.window.info()["width"] == 100 assert xmanager.c.window.info()["height"] == 100 xmanager.c.window.set_size_floating(50, 50) assert xmanager.c.window.info()["width"] == 50 assert xmanager.c.window.info()["height"] == 50 xmanager.c.window.set_size_floating(200, 200) assert xmanager.c.window.info()["width"] == 200 assert xmanager.c.window.info()["height"] == 200 finally: w.kill_client() @bare_config def test_max_size_hint(xmanager, conn): w = None def size_hints(): nonlocal w w = conn.create_window(0, 0, 100, 100) # set the size hints hints = [0] * 18 hints[0] = xcbq.NormalHintsFlags["PMaxSize"] hints[7] = hints[8] = 100 w.set_property("WM_NORMAL_HINTS", hints, type="WM_SIZE_HINTS", format=32) w.map() conn.conn.flush() try: xmanager.create_window(size_hints) xmanager.c.window.enable_floating() assert xmanager.c.window.info()["width"] == 100 assert xmanager.c.window.info()["height"] == 100 xmanager.c.window.set_size_floating(50, 50) assert xmanager.c.window.info()["width"] == 50 assert xmanager.c.window.info()["height"] == 50 xmanager.c.window.set_size_floating(200, 200) assert xmanager.c.window.info()["width"] == 100 assert xmanager.c.window.info()["height"] == 100 finally: w.kill_client() @bare_config def test_max_size_hint_no_flag(xmanager, conn): w = None def size_hints(): nonlocal w w = conn.create_window(0, 0, 100, 100) # set the size hints hints = [0] * 18 hints[7] = hints[8] = 100 w.set_property("WM_NORMAL_HINTS", hints, type="WM_SIZE_HINTS", format=32) w.map() conn.conn.flush() try: xmanager.create_window(size_hints) xmanager.c.window.enable_floating() assert xmanager.c.window.info()["width"] == 100 assert xmanager.c.window.info()["height"] == 100 xmanager.c.window.set_size_floating(50, 50) assert xmanager.c.window.info()["width"] == 50 assert xmanager.c.window.info()["height"] == 50 xmanager.c.window.set_size_floating(200, 200) assert xmanager.c.window.info()["width"] == 200 assert xmanager.c.window.info()["height"] == 200 finally: w.kill_client() @bare_config def test_both_size_hints(xmanager, conn): w = None def size_hints(): nonlocal w w = conn.create_window(0, 0, 100, 100) # set the size hints hints = [0] * 18 hints[0] = xcbq.NormalHintsFlags["PMinSize"] | xcbq.NormalHintsFlags["PMaxSize"] hints[5] = hints[6] = hints[7] = hints[8] = 100 w.set_property("WM_NORMAL_HINTS", hints, type="WM_SIZE_HINTS", format=32) w.map() conn.conn.flush() try: xmanager.create_window(size_hints) xmanager.c.window.enable_floating() assert xmanager.c.window.info()["width"] == 100 assert xmanager.c.window.info()["height"] == 100 xmanager.c.window.set_size_floating(50, 50) assert xmanager.c.window.info()["width"] == 100 assert xmanager.c.window.info()["height"] == 100 xmanager.c.window.set_size_floating(200, 200) assert xmanager.c.window.info()["width"] == 100 assert xmanager.c.window.info()["height"] == 100 finally: w.kill_client() @manager_config def test_inspect_window(xmanager): xmanager.test_window("one") assert xmanager.c.window.inspect()["wm_class"] class MultipleBordersConfig(BareConfig): layouts = [ layout.Stack( border_focus=["#000000", "#111111", "#222222", "#333333", "#444444"], border_width=5, ) ] @pytest.mark.skipif(should_skip(), reason="recent version of imagemagick not found") @pytest.mark.parametrize("xmanager", [MultipleBordersConfig], indirect=True) def test_multiple_borders(xmanager): xmanager.test_window("one") wid = xmanager.c.window.info()["id"] name = os.path.join(tempfile.gettempdir(), "test_multiple_borders.txt") cmd = [ shutil.which("import"), "-border", "-window", str(wid), "-crop", "5x1+0+4", "-depth", "8", name, ] subprocess.run(cmd, env={"DISPLAY": xmanager.display}) with open(name) as f: data = f.readlines() os.unlink(name) # just test that each of the 5 borders is lighter than the last as the screenshot is # not pixel-perfect avg = -1 for i in range(5): color = utils.rgb(data[i + 1].split()[2]) next_avg = sum(color) / 3 assert avg < next_avg avg = next_avg class NetFrameExtentsConfig(BareConfig): layouts = [ layout.Columns(border_width=2, border_on_single=True), layout.Columns(border_width=4, border_on_single=True), ] floating_layout = layout.Floating(border_width=6) @pytest.mark.parametrize("xmanager", [NetFrameExtentsConfig], indirect=True) def test_net_frame_extents(xmanager, conn): def assert_frame(wid, frame): r = conn.conn.core.GetProperty( False, wid, conn.atoms["_NET_FRAME_EXTENTS"], conn.atoms["CARDINAL"], 0, (2**32) - 1 ).reply() assert r.value.to_atoms() == frame pid = xmanager.test_window("one") wid = xmanager.c.window.info()["id"] assert_frame(wid, (2, 2, 2, 2)) xmanager.c.next_layout() assert_frame(wid, (4, 4, 4, 4)) xmanager.c.window.enable_floating() assert_frame(wid, (6, 6, 6, 6)) xmanager.kill_window(pid) def test_net_wm_state_focused(xmanager, conn): atom = conn.atoms["_NET_WM_STATE_FOCUSED"] def assert_state_focused(wid, has_state): r = conn.conn.core.GetProperty( False, wid, conn.atoms["_NET_WM_STATE"], conn.atoms["ATOM"], 0, (2**32) - 1 ).reply() assert (atom in r.value.to_atoms()) == has_state one = xmanager.test_window("one") wid1 = xmanager.c.window.info()["id"] assert_state_focused(wid1, True) two = xmanager.test_window("two") wid2 = xmanager.c.window.info()["id"] assert_state_focused(wid1, False) assert_state_focused(wid2, True) xmanager.kill_window(two) assert_state_focused(wid1, True) xmanager.kill_window(one) @manager_config def test_window_stacking_order(xmanager): """Test basic window stacking controls.""" conn = xcbq.Connection(xmanager.display) def _wnd(name): return xmanager.c.window[{w["name"]: w["id"] for w in xmanager.c.windows()}[name]] def _clients(): root = conn.default_screen.root.wid q = conn.conn.core.QueryTree(root).reply() stack = list(q.children) wins = [(w["name"], stack.index(w["id"])) for w in xmanager.c.windows()] wins.sort(key=lambda x: x[1]) return [x[0] for x in wins] xmanager.test_window("one") xmanager.test_window("two") xmanager.test_window("three") xmanager.test_window("four") xmanager.test_window("five") # We're testing 3 "layers" # BELOW, 'everything else', ABOVE # New windows added on top of each other assert _clients() == ["one", "two", "three", "four", "five"] # Moving above/below moves above/below next client in the layer _wnd("one").move_up() assert _clients() == ["two", "one", "three", "four", "five"] _wnd("four").move_up() assert _clients() == ["two", "one", "three", "five", "four"] _wnd("one").move_down() assert _clients() == ["one", "two", "three", "five", "four"] # Keeping above/below moves client to ABOVE/BELOW layer # When moving to ABOVE, client will be placed at top of that layer # When moving to BELOW, client will be placed at bottom of layer # BELOW: None, ABOVE: two _wnd("two").keep_above() assert _clients() == ["one", "three", "five", "four", "two"] _wnd("five").move_up() assert _clients() == ["one", "three", "four", "five", "two"] # BELOW: three, ABOVE: two _wnd("three").keep_below() assert _clients() == ["three", "one", "four", "five", "two"] _wnd("four").move_down() assert _clients() == ["three", "four", "one", "five", "two"] # BELOW: four, three, ABOVE: two _wnd("four").keep_below() assert _clients() == ["four", "three", "one", "five", "two"] # BELOW: four, three, ABOVE: two, one _wnd("one").keep_above() assert _clients() == ["four", "three", "five", "two", "one"] _wnd("five").move_up() assert _clients() == ["four", "three", "five", "two", "one"] _wnd("five").move_down() assert _clients() == ["four", "three", "five", "two", "one"] # BELOW: two, four, three, ABOVE: one _wnd("two").keep_below() assert _clients() == ["two", "four", "three", "five", "one"] # BELOW: two, three, ABOVE: one, four _wnd("four").keep_above() assert _clients() == ["two", "three", "five", "one", "four"] # BELOW: two, three, ABOVE: one _wnd("four").keep_above() assert _clients() == ["two", "three", "five", "four", "one"] _wnd("five").move_up() assert _clients() == ["two", "three", "four", "five", "one"] # BELOW: two, three, ABOVE: None _wnd("one").keep_above() assert _clients() == ["two", "three", "four", "five", "one"] # BELOW: two, ABOVE: None _wnd("three").keep_below() assert _clients() == ["two", "three", "four", "five", "one"] _wnd("one").move_down() assert _clients() == ["two", "three", "four", "one", "five"] # BELOW: None ABOVE: None _wnd("two").keep_below() assert _clients() == ["two", "three", "four", "one", "five"] # BELOW: three, ABOVE: None _wnd("three").keep_below() assert _clients() == ["three", "two", "four", "one", "five"] _wnd("two").move_down() assert _clients() == ["three", "two", "four", "one", "five"] _wnd("one").move_down() assert _clients() == ["three", "two", "one", "four", "five"] _wnd("two").move_to_top() assert _clients() == ["three", "one", "four", "five", "two"] # three is kept_below so moving to bottom is still above that _wnd("five").move_to_bottom() assert _clients() == ["three", "five", "one", "four", "two"] # three is the only window kept_below so this will have no effect _wnd("three").move_to_top() assert _clients() == ["three", "five", "one", "four", "two"] # Keep three above everything else _wnd("three").keep_above() assert _clients() == ["five", "one", "four", "two", "three"] # This should have no effect as it's the only window kept_above _wnd("three").move_to_bottom() assert _clients() == ["five", "one", "four", "two", "three"] @manager_config def test_floats_kept_above(xmanager): """Test config option to pin floats to a higher level.""" conn = xcbq.Connection(xmanager.display) def _wnd(name): return xmanager.c.window[{w["name"]: w["id"] for w in xmanager.c.windows()}[name]] def _clients(): root = conn.default_screen.root.wid q = conn.conn.core.QueryTree(root).reply() stack = list(q.children) wins = [(w["name"], stack.index(w["id"])) for w in xmanager.c.windows()] wins.sort(key=lambda x: x[1]) return [x[0] for x in wins] xmanager.test_window("one", floating=True) xmanager.test_window("two") # Confirm floating window is above window that was opened later assert _clients() == ["two", "one"] # Open a different floating window. This should be above the first floating one. xmanager.test_window("three", floating=True) assert _clients() == ["two", "one", "three"] @manager_config def test_fullscreen_on_top(xmanager): """Test fullscreen, focused windows are on top.""" conn = xcbq.Connection(xmanager.display) def _wnd(name): return xmanager.c.window[{w["name"]: w["id"] for w in xmanager.c.windows()}[name]] def _clients(): root = conn.default_screen.root.wid q = conn.conn.core.QueryTree(root).reply() stack = list(q.children) wins = [(w["name"], stack.index(w["id"])) for w in xmanager.c.windows()] wins.sort(key=lambda x: x[1]) return [x[0] for x in wins] xmanager.test_window("one", floating=True) xmanager.test_window("two") # window "one" is kept_above, "two" is norm assert _clients() == ["two", "one"] # A fullscreen, focused window should display above windows that are "kept above" _wnd("two").enable_fullscreen() _wnd("two").focus() assert _clients() == ["one", "two"] # Focusing the other window should cause the fullscreen window to drop from the highest layer _wnd("one").focus() assert _clients() == ["two", "one"] # Disabling fullscreen will put the window below the "kept above" window, even if it has focus _wnd("two").focus() _wnd("two").toggle_fullscreen() assert _clients() == ["two", "one"] class UnpinFloatsConfig(ManagerConfig): # New floating windows not set to "keep_above" floats_kept_above = False # Floating windows should be moved above tiled windows when first floated, regardless # of whether `floats_kept_above` is True @pytest.mark.parametrize("xmanager", [ManagerConfig, UnpinFloatsConfig], indirect=True) def test_move_float_above_tiled(xmanager): conn = xcbq.Connection(xmanager.display) def _wnd(name): return xmanager.c.window[{w["name"]: w["id"] for w in xmanager.c.windows()}[name]] def _clients(): root = conn.default_screen.root.wid q = conn.conn.core.QueryTree(root).reply() stack = list(q.children) wins = [(w["name"], stack.index(w["id"])) for w in xmanager.c.windows()] wins.sort(key=lambda x: x[1]) return [x[0] for x in wins] xmanager.test_window("one") xmanager.test_window("two") xmanager.test_window("three") assert _clients() == ["one", "two", "three"] _wnd("two").toggle_floating() assert _clients() == ["one", "three", "two"] def test_multiple_wm_types(xmanager): conn = xcbq.Connection(xmanager.display) w = conn.create_window(50, 50, 50, 50) normal = conn.atoms["_NET_WM_WINDOW_TYPE_NORMAL"] kde_override = conn.atoms["_KDE_NET_WM_WINDOW_TYPE_OVERRIDE"] w.set_property("_NET_WM_WINDOW_TYPE", [kde_override, normal]) assert w.get_wm_type() == "normal" qtile-0.31.0/test/backend/x11/__init__.py0000664000175000017500000000000014762660347017714 0ustar epsilonepsilonqtile-0.31.0/test/backend/x11/test_xcbq.py0000664000175000017500000000150314762660347020162 0ustar epsilonepsilonimport pytest import xcffib import xcffib.testing from libqtile.backend.x11 import window, xcbq def test_new_window(conn): win = conn.create_window(1, 2, 640, 480) assert isinstance(win, window.XWindow) geom = win.get_geometry() assert geom.x == 1 assert geom.y == 2 assert geom.width == 640 assert geom.height == 480 win.kill_client() with pytest.raises(xcffib.ConnectionException): win.get_geometry() def test_masks(): cfgmasks = xcbq.ConfigureMasks d = {"x": 1, "y": 2, "width": 640, "height": 480} mask, vals = cfgmasks(**d) assert set(vals) == set(d.values()) with pytest.raises(ValueError): mask, vals = cfgmasks(asdf=32, **d) def test_translate_masks(): assert xcbq.translate_masks(["shift", "control"]) assert xcbq.translate_masks([]) == 0 qtile-0.31.0/test/backend/x11/test_xcore.py0000664000175000017500000000321314762660347020345 0ustar epsilonepsilonimport pytest from libqtile.backend import get_core from libqtile.backend.x11 import core from test.test_manager import ManagerConfig def test_get_core_x11(display): get_core("x11", display).finalize() def test_keys(display): assert "a" in core.get_keys() assert "shift" in core.get_modifiers() def test_no_two_qtiles(xmanager): with pytest.raises(core.ExistingWMException): core.Core(xmanager.display).finalize() def test_color_pixel(xmanager): (success, e) = xmanager.c.eval('self.core.conn.color_pixel("ffffff")') assert success, e @pytest.mark.parametrize("xmanager", [ManagerConfig], indirect=True) def test_net_client_list(xmanager, conn): def assert_clients(number): clients = conn.default_screen.root.get_property("_NET_CLIENT_LIST", unpack=int) assert len(clients) == number # ManagerConfig has a Bar, which should not appear in _NET_CLIENT_LIST xmanager.c.eval("self.core.update_client_lists()") assert_clients(0) one = xmanager.test_window("one") assert_clients(1) two = xmanager.test_window("two") xmanager.c.window.toggle_minimize() three = xmanager.test_window("three") xmanager.c.screen.next_group() assert_clients(3) # Minimized windows and windows on other groups are included xmanager.kill_window(one) xmanager.c.screen.next_group() assert_clients(2) xmanager.kill_window(three) assert_clients(1) xmanager.c.screen.next_group() one = xmanager.test_window("one") assert_clients(2) xmanager.c.window.static() # Static windows are not included assert_clients(1) xmanager.kill_window(two) assert_clients(0) qtile-0.31.0/test/backend/test_backend.py0000664000175000017500000000030414762660347020201 0ustar epsilonepsilonimport pytest from libqtile.backend import get_core from libqtile.utils import QtileError def test_get_core_bad(): with pytest.raises(QtileError): get_core("NonBackend").finalize() qtile-0.31.0/test/scripts/0000775000175000017500000000000014762660347015304 5ustar epsilonepsilonqtile-0.31.0/test/scripts/qtile_icon.rgba0000664000175000017500000001000014762660347020256 0ustar epsilonepsilonT*))))0e>))))))))))U)))))))))))))3))))0ra*))),*)))oO)))2?)))_)))e)))nB)))V))/)))+))m;))M)))dEj))-)))e))>))))))F)))>))))))>)))>))50))[>)))>g)))?)))<)))L>)))>V)))_>)))>3)))@=)))U,))))5`z=))3))))))))Ui)))))))S0)):qtile-0.31.0/test/scripts/qtile-logo-blue.svg0000664000175000017500000000557014762660347021035 0ustar epsilonepsilon qtile-0.31.0/test/scripts/window.py0000775000175000017500000001472514762660347017201 0ustar epsilonepsilon#!/usr/bin/env python3 """ This creates a minimal window using GTK that works the same in both X11 or Wayland. GTK sets the window class via `--name `, and then we manually set the window title and type. Therefore this is intended to be called as: python window.py --name <type> <new_title> where <type> is "normal" or "notification" The window will close itself if it receives any key or button press events. """ # flake8: noqa # This is needed otherwise the window will use any Wayland session it can find even if # WAYLAND_DISPLAY is not set. import os if os.environ.get("WAYLAND_DISPLAY"): os.environ["GDK_BACKEND"] = "wayland" else: os.environ["GDK_BACKEND"] = "x11" # Disable GTK ATK bridge, which appears to trigger errors with e.g. test_strut_handling # https://wiki.gnome.org/Accessibility/Documentation/GNOME2/Mechanics os.environ["NO_AT_BRIDGE"] = "1" import sys from pathlib import Path import gi gi.require_version("Gdk", "3.0") gi.require_version("Gtk", "3.0") from dbus_fast import Message from dbus_fast.auth import Authenticator from dbus_fast.constants import MessageType, PropertyAccess from dbus_fast.glib.message_bus import MessageBus, _AuthLineSource from dbus_fast.service import ServiceInterface, dbus_property, method, signal from gi.repository import Gdk, GLib, Gtk ICON = Path(__file__).parent / "qtile_icon.rgba" # This patch is needed to address the issue described here: # https://github.com/altdesktop/python-dbus-next/issues/113 # Once dbus_fast incorporates this patch. class PatchedMessageBus(MessageBus): def _authenticate(self, authenticate_notify): self._stream.write(b"\0") first_line = self._auth._authentication_start() if first_line is not None: if type(first_line) is not str: raise AuthError("authenticator gave response not type str") self._stream.write(f"{first_line}\r\n".encode()) self._stream.flush() def line_notify(line): try: resp = self._auth._receive_line(line) self._stream.write(Authenticator._format_line(resp)) self._stream.flush() if resp == "BEGIN": self._readline_source.destroy() authenticate_notify(None) return True except Exception as e: authenticate_notify(e) return True readline_source = _AuthLineSource(self._stream) readline_source.set_callback(line_notify) readline_source.add_unix_fd(self._fd, GLib.IO_IN) readline_source.attach(self._main_context) self._readline_source = readline_source class SNItem(ServiceInterface): """ Simplified StatusNotifierItem interface. Only exports methods, properties and signals required by StatusNotifier widget. """ def __init__(self, window, *args): ServiceInterface.__init__(self, *args) self.window = window self.fullscreen = False with open(ICON, "rb") as f: self.icon = f.read() arr = bytearray(self.icon) for i in range(0, len(arr), 4): r, g, b, a = arr[i : i + 4] arr[i] = a arr[i + 1 : i + 4] = bytearray([r, g, b]) self.icon = bytes(arr) @method() def Activate(self, x: "i", y: "i"): if self.fullscreen: self.window.unfullscreen() else: self.window.fullscreen() self.fullscreen = not self.fullscreen @dbus_property(PropertyAccess.READ) def IconName(self) -> "s": return "" @dbus_property(PropertyAccess.READ) def IconPixmap(self) -> "a(iiay)": return [[32, 32, self.icon]] @dbus_property(PropertyAccess.READ) def AttentionIconPixmap(self) -> "a(iiay)": return [] @dbus_property(PropertyAccess.READ) def OverlayIconPixmap(self) -> "a(iiay)": return [] @signal() def NewIcon(self): pass @signal() def NewAttentionIcon(self): pass @signal() def NewOverlayIcon(self): pass if __name__ == "__main__": # GTK consumes the `--name <class>` args if len(sys.argv) > 1: title = sys.argv[1] else: title = "TestWindow" if len(sys.argv) > 2: window_type = sys.argv[2] else: window_type = "normal" # Check if we want to export a StatusNotifierItem interface sni = "export_sni_interface" in sys.argv win = Gtk.Window(title=title) win.set_default_size(100, 100) if len(sys.argv) > 3 and sys.argv[3]: def gtk_set_title(*args): win.set_title(sys.argv[3]) # Time before renaming title GLib.timeout_add(500, gtk_set_title) if "urgent_hint" in sys.argv: def gtk_set_urgency_hint(*args): win.set_urgency_hint(True) # Time before changing urgency GLib.timeout_add(500, gtk_set_urgency_hint) icon = os.path.abspath(os.path.join(os.path.dirname(__file__), "../..", "logo.png")) if os.path.isfile(icon): win.set_icon_from_file(icon) if window_type == "notification": if os.environ["GDK_BACKEND"] == "wayland": try: gi.require_version("GtkLayerShell", "0.1") from gi.repository import GtkLayerShell except ValueError: sys.exit(1) win.add(Gtk.Label(label="This is a test notification")) GtkLayerShell.init_for_window(win) else: win.set_type_hint(Gdk.WindowTypeHint.NOTIFICATION) elif window_type == "normal": win.set_type_hint(Gdk.WindowTypeHint.NORMAL) if sni: bus = PatchedMessageBus().connect_sync() item = SNItem(win, "org.kde.StatusNotifierItem") # Export interfaces on the bus bus.export("/StatusNotifierItem", item) # Request the service name bus.request_name_sync(f"test.qtile.window-{title.replace(' ','-')}") msg = bus.call_sync( Message( message_type=MessageType.METHOD_CALL, destination="org.freedesktop.StatusNotifierWatcher", interface="org.freedesktop.StatusNotifierWatcher", path="/StatusNotifierWatcher", member="RegisterStatusNotifierItem", signature="s", body=[bus.unique_name], ) ) win.connect("destroy", Gtk.main_quit) win.connect("key-press-event", Gtk.main_quit) win.show_all() Gtk.main() �������������������������������������������qtile-0.31.0/test/test_group.py���������������������������������������������������������������������0000664�0001750�0001750�00000011450�14762660347�016363� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2011 Florian Mounier # Copyright (c) 2012, 2014 Tycho Andersen # Copyright (c) 2013 Craig Barnes # Copyright (c) 2014 Sean Vig # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import random import pytest import libqtile.config from libqtile import layout from libqtile.confreader import Config from test.helpers import Retry class GroupConfig(Config): auto_fullscreen = True groups = [ libqtile.config.Group("a"), libqtile.config.Group("b"), ] layouts = [layout.MonadTall()] floating_layout = libqtile.resources.default_config.floating_layout keys = [] mouse = [] screens = [] group_config = pytest.mark.parametrize("manager", [GroupConfig], indirect=True) @group_config def test_window_order(manager): # windows to add windows_name = ["one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten"] windows = {} # Add windows one by one for win in windows_name: windows[win] = manager.test_window(win) # Windows must be sorted in the same order as they were created assert windows_name == manager.c.group.info()["windows"] # Randomly remove 5 windows and see if orders remains persistant for i in range(5): win_to_remove = random.choice(windows_name) windows_name.remove(win_to_remove) manager.kill_window(windows[win_to_remove]) del windows[win_to_remove] assert windows_name == manager.c.group.info()["windows"] @group_config def test_focus_by_index(manager): manager.c.group["a"].toscreen() manager.test_window("one") manager.test_window("two") info = manager.c.group.info() assert info.get("focus") == "two" manager.c.group.focus_by_index(1) info = manager.c.group.info() assert info.get("focus") == "two" manager.c.group.focus_by_index(3) info = manager.c.group.info() assert info.get("focus") == "two" manager.c.group.focus_by_index(0) info = manager.c.group.info() assert info.get("focus") == "one" @group_config def test_toscreen_toggle(manager): assert manager.c.group.info()["name"] == "a" # Start on "a" manager.c.group["b"].toscreen() assert manager.c.group.info()["name"] == "b" # Switch to "b" manager.c.group["b"].toscreen() assert manager.c.group.info()["name"] == "b" # Does not toggle by default manager.c.group["b"].toscreen(toggle=True) assert manager.c.group.info()["name"] == "a" # Explicitly toggling moves to "a" manager.c.group["b"].toscreen(toggle=True) manager.c.group["b"].toscreen(toggle=True) assert manager.c.group.info()["name"] == "a" # Toggling twice roundtrips between the two class NoPersistGroupConfig(GroupConfig): groups = [ libqtile.config.Group("a"), libqtile.config.Group("b", exclusive=True), libqtile.config.Group("c", persist=False), ] @pytest.mark.parametrize("manager", [NoPersistGroupConfig], indirect=True) def test_non_persistent_groups(manager): @Retry(ignore_exceptions=(AssertionError,)) def wait_for_removed(group_name): assert group_name not in manager.c.get_groups() window_name = "no_match" manager.c.group["b"].toscreen() manager.test_window(window_name) assert window_name not in manager.c.group.info()["windows"] # Window was moved to a new group group_name = "TestWindow" # The new group is named after the window's `wm_class` property assert group_name in manager.c.get_groups() manager.c.group[group_name].toscreen() assert manager.c.window.info()["name"] == window_name manager.c.window.togroup("a") wait_for_removed(group_name) window_name = "bar" manager.c.group["c"].toscreen() manager.test_window(window_name) assert manager.c.window.info()["name"] == window_name manager.c.window.togroup("a") wait_for_removed(group_name) ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/migrate/��������������������������������������������������������������������������0000775�0001750�0001750�00000000000�14762660347�015245� 5����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/migrate/test_migrations.py��������������������������������������������������������0000664�0001750�0001750�00000004053�14762660347�021034� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2023, elParaguayo. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE import pytest from libqtile.scripts.migrations import MIGRATIONS, load_migrations migration_tests = [] # We store a list of test IDs so that tests can be filtered during development # e.g. pytest -k MigrationID migration_ids = [] load_migrations() for m in MIGRATIONS: tests = [] for i, test in enumerate(m.TESTS): tests.append((m.ID, test)) migration_ids.append(f"{m.ID}-{i}") if not tests: tests.append((m.ID, None)) migration_ids.append(f"{m.ID}-no-tests") migration_tests.extend(tests) @pytest.mark.parametrize("migration_tester", migration_tests, indirect=True, ids=migration_ids) def test_all_migrations(migration_tester): # If the migration is expected to result in a change then it should also provide linting if migration_tester.test.needs_lint: migration_tester.assert_lint(), "No linting provided" # Tests whether the expected output is returned by the migration migration_tester.assert_migrate() �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/migrate/conftest.py���������������������������������������������������������������0000664�0001750�0001750�00000006701�14762660347�017450� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2023, elParaguayo. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE import subprocess import sys import tempfile import textwrap from pathlib import Path import pytest from libqtile.scripts.migrations._base import NoChange from test.test_check import run_qtile_check class SourceCode: def __init__(self, test): self.source = textwrap.dedent(test.input) self.expected = textwrap.dedent(test.output) self.dir = tempfile.TemporaryDirectory() self.source_path = Path(self.dir.name) / "config.py" self.needs_lint = not isinstance(test, NoChange) def __enter__(self): self.source_path.write_text(self.source) return self def __exit__(self, type, value, traceback): self.dir.cleanup() del self.dir class MigrationTester: def __init__(self, test_id, test): self.test_id = test_id self.test = test self.cmd = Path(__file__).parent / ".." / ".." / "bin" / "qtile" def assert_migrate(self): if not self.test.source: assert False, f"{self.test_id} has no tests." argv = [ sys.executable, self.cmd, "migrate", "--yes", "-r", self.test_id, "-c", self.test.source_path, ] try: subprocess.check_call(argv) except subprocess.CalledProcessError: assert False updated = Path(f"{self.test.source_path}").read_text() assert updated == self.test.expected def assert_lint(self): argv = [ sys.executable, self.cmd, "migrate", "--lint", "-r", self.test_id, "-c", self.test.source_path, ] try: output = subprocess.run(argv, capture_output=True, check=True) except subprocess.CalledProcessError: assert False assert any(self.test_id in line for line in output.stdout.decode().splitlines()) def assert_check(self): if not self.test.source: assert False, f"{self.test_id} has no Check test." self.assert_migrate() assert run_qtile_check(self.test.source_path) @pytest.fixture def migration_tester(request): test_id, test = getattr(request, "param", (None, None)) if test is None: test = NoChange("") with SourceCode(test) as sc: yield MigrationTester(test_id, sc) ���������������������������������������������������������������qtile-0.31.0/test/migrate/__init__.py���������������������������������������������������������������0000664�0001750�0001750�00000000000�14762660347�017344� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/migrate/test_check_migrations.py��������������������������������������������������0000664�0001750�0001750�00000003637�14762660347�022200� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2023, elParaguayo. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE import pytest from libqtile.scripts.migrations import MIGRATIONS, load_migrations from test.test_check import have_mypy, is_cpython pytestmark = pytest.mark.skipif(not is_cpython() or not have_mypy(), reason="needs mypy") migration_tests = [] # We store a list of test IDs so that tests can be filtered during development # e.g. pytest -k MigrationID migration_ids = [] load_migrations() for m in MIGRATIONS: tests = [] for i, test in enumerate(m.TESTS): if not test.check: continue tests.append((m.ID, test)) migration_ids.append(f"{m.ID}-{i}") if not tests: continue migration_tests.extend(tests) @pytest.mark.parametrize("migration_tester", migration_tests, indirect=True, ids=migration_ids) def test_check_all_migrations(migration_tester): migration_tester.assert_check() �������������������������������������������������������������������������������������������������qtile-0.31.0/test/test_config.py��������������������������������������������������������������������0000664�0001750�0001750�00000006232�14762660347�016476� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2011 Florian Mounier # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Tycho Andersen # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from pathlib import Path import pytest from libqtile import config, confreader, utils configs_dir = Path(__file__).resolve().parent / "configs" def load_config(name): f = confreader.Config(configs_dir / name) f.load() return f def test_validate(): # bad key f = load_config("basic.py") f.keys[0].key = "nonexistent" with pytest.raises(confreader.ConfigError): f.validate() # bad modifier f = load_config("basic.py") f.keys[0].modifiers = ["nonexistent"] with pytest.raises(confreader.ConfigError): f.validate() def test_basic(): f = load_config("basic.py") assert f.keys def test_syntaxerr(): with pytest.raises(SyntaxError): load_config("syntaxerr.py") def test_falls_back(): f = load_config("basic.py") # We just care that it has a default, we don't actually care what the # default is; don't assert anything at all about the default in case # someone changes it down the road. assert hasattr(f, "follow_mouse_focus") def cmd(x): return None def test_ezkey(): key = config.EzKey("M-A-S-a", cmd, cmd) modkey, altkey = (config.EzConfig.modifier_keys[i] for i in "MA") assert key.modifiers == [modkey, altkey, "shift"] assert key.key == "a" assert key.commands == (cmd, cmd) key = config.EzKey("M-<Tab>", cmd) assert key.modifiers == [modkey] assert key.key == "Tab" assert key.commands == (cmd,) with pytest.raises(utils.QtileError): config.EzKey("M--", cmd) with pytest.raises(utils.QtileError): config.EzKey("Z-Z-z", cmd) with pytest.raises(utils.QtileError): config.EzKey("asdf", cmd) with pytest.raises(utils.QtileError): config.EzKey("M-a-A", cmd) def test_ezclick_ezdrag(): btn = config.EzClick("M-1", cmd) assert btn.button == "Button1" assert btn.modifiers == [config.EzClick.modifier_keys["M"]] btn = config.EzDrag("A-2", cmd) assert btn.button == "Button2" assert btn.modifiers == [config.EzClick.modifier_keys["A"]] ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/test_ipc.py�����������������������������������������������������������������������0000664�0001750�0001750�00000003566�14762660347�016013� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2022 Jaakko Sirén # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest from libqtile.ipc import _IPC def test_ipc_json_encoder_supports_sets(): serialized = _IPC.pack({"foo": set()}, is_json=True) assert serialized == b'{"foo": []}' def test_ipc_json_throws_error_on_unsupported_field(): class NonSerializableType: ... with pytest.raises( ValueError, match=( "Tried to JSON serialize unsupported type <class '" "test.test_ipc.test_ipc_json_throws_error_on_unsupported_field.<locals>.NonSerializableType" "'>.*" ), ): _IPC.pack({"foo": NonSerializableType()}, is_json=True) def test_ipc_marshall_error_on_unsupported_field(): class NonSerializableType: ... with pytest.raises(ValueError, match="unmarshallable object"): _IPC.pack({"foo": NonSerializableType()}) ������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/core/�����������������������������������������������������������������������������0000775�0001750�0001750�00000000000�14762660347�014545� 5����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/core/__init__.py������������������������������������������������������������������0000664�0001750�0001750�00000000000�14762660347�016644� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/core/test_exitcode.py�������������������������������������������������������������0000664�0001750�0001750�00000005671�14762660347�017773� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2024 Thomas Krug # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import multiprocessing import os import subprocess import tempfile import time import test repo_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../")) socket_path = tempfile.NamedTemporaryFile().name def run_qtile(backend): cmd = os.path.join(repo_path, "bin/qtile") args = [cmd, "start", "-s", socket_path] args.extend(["-b", backend.name]) env = os.environ.copy() if backend.name == "wayland": env.update(backend.env) proc = subprocess.Popen(args, env=env, stdout=subprocess.PIPE) out, err = proc.communicate(timeout=10) exitcode = proc.returncode return exitcode def stop_qtile(code): cmd = os.path.join(repo_path, "bin/qtile") args = [cmd, "cmd-obj", "-o", "cmd", "-s", socket_path, "-f", "shutdown"] if code: args.extend(["-a", str(code)]) proc = subprocess.Popen(args, stdout=subprocess.PIPE) proc.communicate(timeout=10) exitcode = proc.returncode return exitcode def deferred_stop(code=0): # wait for qtile process to start wait = 10 while not test.helpers.can_connect_qtile(socket_path): time.sleep(1) if not wait: raise Exception("timeout waiting for qtile process") wait -= 1 stop_qtile(code) # in these testcases there are two qtile instances # one started by the helpers.TestManager, # which is used to evaluate code coverage # and a second instance started by run_qtile, # which is used to test the actual exit code behavior def test_exitcode_default(backend): proc = multiprocessing.Process(target=deferred_stop) proc.start() exitcode = run_qtile(backend) proc.join() assert exitcode == 0 def test_exitcode_explicit(backend): code = 23 proc = multiprocessing.Process(target=deferred_stop, args=(code,)) proc.start() exitcode = run_qtile(backend) proc.join() assert exitcode == code �����������������������������������������������������������������������qtile-0.31.0/test/core/test_lifecycle.py������������������������������������������������������������0000664�0001750�0001750�00000004757�14762660347�020132� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest from libqtile.core.lifecycle import Behavior, LifeCycle from libqtile.log_utils import init_log def fake_os_execv(executable, args): assert executable == "python" assert args == [ "python", "arg1", "arg2", "--no-spawn", "--with-state=/tmp/test/fake_statefile", ] def no_op(*args, **kwargs): pass @pytest.fixture def patched_lifecycle(monkeypatch): init_log() monkeypatch.setattr("libqtile.core.lifecycle.sys.executable", "python") monkeypatch.setattr("libqtile.core.lifecycle.sys.argv", ["arg1", "arg2"]) monkeypatch.setattr("libqtile.core.lifecycle.atexit.register", no_op) monkeypatch.setattr("libqtile.core.lifecycle.os.execv", fake_os_execv) yield LifeCycle() def test_restart_behaviour(patched_lifecycle, caplog): patched_lifecycle.behavior = Behavior.RESTART patched_lifecycle.state_file = "/tmp/test/fake_statefile" patched_lifecycle._atexit() assert caplog.record_tuples == [("libqtile", 30, "Restarting Qtile with os.execv(...)")] def test_terminate_behavior(patched_lifecycle, caplog): patched_lifecycle.behavior = Behavior.TERMINATE patched_lifecycle._atexit() assert caplog.record_tuples == [("libqtile", 30, "Qtile will now terminate")] def test_none_behavior(patched_lifecycle, caplog): patched_lifecycle.behavior = Behavior.NONE patched_lifecycle._atexit() assert caplog.record_tuples == [] �����������������qtile-0.31.0/test/test_popup.py���������������������������������������������������������������������0000664�0001750�0001750�00000003472�14762660347�016377� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2020-1 Matt Colligan # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import textwrap def test_popup_focus(manager): manager.test_window("one") start_wins = len(manager.backend.get_all_windows()) success, msg = manager.c.eval( textwrap.dedent( """ from libqtile.popup import Popup popup = Popup(self, x=0, y=0, width=self.current_screen.width, height=self.current_screen.height, ) popup.place() popup.unhide() """ ) ) assert success, msg end_wins = len(manager.backend.get_all_windows()) assert end_wins == start_wins + 1 assert manager.c.group.info()["focus"] == "one" assert manager.c.group.info()["windows"] == ["one"] assert len(manager.c.windows()) == 1 ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/��������������������������������������������������������������������������0000775�0001750�0001750�00000000000�14762660347�015263� 5����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_check_updates.py�����������������������������������������������������0000664�0001750�0001750�00000013124�14762660347�021477� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������import libqtile.config from libqtile.widget.check_updates import CheckUpdates, Popen # noqa: F401 from test.widgets.conftest import FakeBar wrong_distro = "Barch" good_distro = "Arch" cmd_0_line = "export toto" # quick "monkeypatch" simulating 0 output, ie 0 update cmd_1_line = "echo toto" # quick "monkeypatch" simulating 1 output, ie 1 update cmd_error = "false" nus = "No Update Available" # This class returns None when first polled (to simulate that the task is still running) # and then 0 on the second call. class MockPopen: def __init__(self, *args, **kwargs): self.call_count = 0 def poll(self): if self.call_count == 0: self.call_count += 1 return None return 0 # Bit of an ugly hack to replicate the above functionality but for a method. class MockSpawn: call_count = 0 @classmethod def call_process(cls, *args, **kwargs): if cls.call_count == 0: cls.call_count += 1 return "Updates" return "" def test_unknown_distro(): """test an unknown distribution""" cu = CheckUpdates(distro=wrong_distro) text = cu.poll() assert text == "N/A" def test_update_available(fake_qtile, fake_window): """test output with update (check number of updates and color)""" cu2 = CheckUpdates( distro=good_distro, custom_command=cmd_1_line, colour_have_updates="#123456" ) fakebar = FakeBar([cu2], window=fake_window) cu2._configure(fake_qtile, fakebar) text = cu2.poll() assert text == "Updates: 1" assert cu2.layout.colour == cu2.colour_have_updates def test_no_update_available_without_no_update_string(fake_qtile, fake_window): """test output with no update (without dedicated string nor color)""" cu3 = CheckUpdates(distro=good_distro, custom_command=cmd_0_line) fakebar = FakeBar([cu3], window=fake_window) cu3._configure(fake_qtile, fakebar) text = cu3.poll() assert text == "" def test_no_update_available_with_no_update_string_and_color_no_updates(fake_qtile, fake_window): """test output with no update (with dedicated string and color)""" cu4 = CheckUpdates( distro=good_distro, custom_command=cmd_0_line, no_update_string=nus, colour_no_updates="#654321", ) fakebar = FakeBar([cu4], window=fake_window) cu4._configure(fake_qtile, fakebar) text = cu4.poll() assert text == nus assert cu4.layout.colour == cu4.colour_no_updates def test_update_available_with_restart_indicator(monkeypatch, fake_qtile, fake_window): """test output with no indicator where restart needed""" cu5 = CheckUpdates( distro=good_distro, custom_command=cmd_1_line, restart_indicator="*", ) monkeypatch.setattr("os.path.exists", lambda x: True) fakebar = FakeBar([cu5], window=fake_window) cu5._configure(fake_qtile, fakebar) text = cu5.poll() assert text == "Updates: 1*" def test_update_available_with_execute(manager_nospawn, minimal_conf_noscreen, monkeypatch): """test polling after executing command""" # Use monkeypatching to patch both Popen (for execute command) and call_process # This class returns None when first polled (to simulate that the task is still running) # and then 0 on the second call. class MockPopen: def __init__(self, *args, **kwargs): self.call_count = 0 def poll(self): if self.call_count == 0: self.call_count += 1 return None return 0 # Bit of an ugly hack to replicate the above functionality but for a method. class MockSpawn: call_count = 0 @classmethod def call_process(cls, *args, **kwargs): if cls.call_count == 0: cls.call_count += 1 return "Updates" return "" cu6 = CheckUpdates( distro=good_distro, custom_command="dummy", execute="dummy", no_update_string=nus, ) # Patch the necessary object monkeypatch.setattr(cu6, "call_process", MockSpawn.call_process) monkeypatch.setattr("libqtile.widget.check_updates.Popen", MockPopen) config = minimal_conf_noscreen config.screens = [libqtile.config.Screen(top=libqtile.bar.Bar([cu6], 10))] manager_nospawn.start(config) topbar = manager_nospawn.c.bar["top"] assert topbar.info()["widgets"][0]["text"] == "Updates: 1" # Clicking the widget triggers the execute command topbar.fake_button_press(0, 0, button=1) # The second time we poll the widget, the update process is complete # and there are no more updates _, result = manager_nospawn.c.widget["checkupdates"].eval("self.poll()") assert result == nus def test_update_process_error(fake_qtile, fake_window): """test output where update check gives error""" cu7 = CheckUpdates( distro=good_distro, custom_command=cmd_error, no_update_string="ERROR", ) fakebar = FakeBar([cu7], window=fake_window) cu7._configure(fake_qtile, fakebar) text = cu7.poll() assert text == "ERROR" def test_line_truncations(fake_qtile, monkeypatch, fake_window): """test update count is reduced""" # Mock output to return 5 lines of text def mock_process(*args, **kwargs): return "1\n2\n3\n4\n5\n" # Fedora is set up to remove 1 from line count cu8 = CheckUpdates(distro="Fedora") monkeypatch.setattr(cu8, "call_process", mock_process) fakebar = FakeBar([cu8], window=fake_window) cu8._configure(fake_qtile, fakebar) text = cu8.poll() # Should have 4 updates assert text == "Updates: 4" ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_chord.py�������������������������������������������������������������0000664�0001750�0001750�00000013547�14762660347�020005� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2020 Tycho Andersen # Copyright (c) 2022 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest import libqtile.confreader from libqtile import hook from libqtile.config import Key, KeyChord from libqtile.lazy import lazy from libqtile.widget import Chord, base from test.widgets.conftest import FakeBar RED = "#FF0000" BLUE = "#00FF00" textbox = base._TextBox("") BASE_BACKGROUND = textbox.background BASE_FOREGROUND = textbox.foreground def no_op(*args): pass class ChordConf(libqtile.confreader.Config): auto_fullscreen = False keys = [ KeyChord([], "a", [Key([], "b", lazy.function(no_op))], mode="persistent_chord"), KeyChord( [], "z", [ Key([], "b", lazy.function(no_op)), ], name="temporary_name", ), KeyChord( [], "y", [ Key([], "b", lazy.function(no_op)), ], name="mode_true", mode=True, ), ] mouse = [] groups = [libqtile.config.Group("a"), libqtile.config.Group("b")] layouts = [libqtile.layout.stack.Stack(num_stacks=1)] floating_layout = libqtile.resources.default_config.floating_layout screens = [libqtile.config.Screen(top=libqtile.bar.Bar([Chord()], 10))] chord_config = pytest.mark.parametrize("manager", [ChordConf], indirect=True) def test_chord_widget(fake_window, fake_qtile): chord = Chord(chords_colors={"testcolor": (RED, BLUE)}) fakebar = FakeBar([chord], window=fake_window) chord._configure(fake_qtile, fakebar) # Text is blank at start assert chord.text == "" # Fire hook for testcolor chord hook.fire("enter_chord", "testcolor") # Chord is in chords_colors so check colours assert chord.background == RED assert chord.foreground == BLUE assert chord.text == "testcolor" # New chord, not in dictionary so should be default colours hook.fire("enter_chord", "test") assert chord.text == "test" assert chord.background == BASE_BACKGROUND assert chord.foreground == BASE_FOREGROUND # Unnamed chord so no text hook.fire("enter_chord", "") assert chord.text == "" assert chord.background == BASE_BACKGROUND assert chord.foreground == BASE_FOREGROUND # Back into testcolor and custom colours hook.fire("enter_chord", "testcolor") assert chord.background == RED assert chord.foreground == BLUE assert chord.text == "testcolor" # Colours shoud reset when leaving chord hook.fire("leave_chord") assert chord.text == "" assert chord.background == BASE_BACKGROUND assert chord.foreground == BASE_FOREGROUND # Finalize the widget to prevent segfault (the drawer needs to be finalised) # We clear the _futures attribute as there are no real timers in it and calls # to `cancel()` them will fail. chord._futures = [] chord.finalize() @chord_config def test_chord_persistence(manager): widget = manager.c.widget["chord"] assert widget.info()["text"] == "" # Test 1: Test persistent chord mode name # Old style where mode contains text. # Enter the chord manager.c.simulate_keypress([], "a") assert widget.info()["text"] == "persistent_chord" # Chord has finished but mode should still be in place manager.c.simulate_keypress([], "b") assert widget.info()["text"] == "persistent_chord" # Escape to leave chord manager.c.simulate_keypress([], "Escape") assert widget.info()["text"] == "" # Test 2: Test persistent chord mode name # New style - mode = True # Enter the chord manager.c.simulate_keypress([], "y") assert widget.info()["text"] == "mode_true" # Chord has finished but mode should still be in place manager.c.simulate_keypress([], "b") assert widget.info()["text"] == "mode_true" # Escape to leave chord manager.c.simulate_keypress([], "Escape") assert widget.info()["text"] == "" # Test 3: Test temporary chord name # Enter the chord manager.c.simulate_keypress([], "z") assert widget.info()["text"] == "temporary_name" # Chord has finished and should exit manager.c.simulate_keypress([], "b") assert widget.info()["text"] == "" # Enter the chord manager.c.simulate_keypress([], "z") assert widget.info()["text"] == "temporary_name" # Escape to cancel chord manager.c.simulate_keypress([], "Escape") assert widget.info()["text"] == "" def test_chord_mode_name_deprecation(caplog): chord = KeyChord([], "a", [Key([], "b", lazy.function(no_op))], mode="persistent_chord") assert caplog.records log = caplog.records[0] assert log.levelname == "WARNING" assert "name='persistent_chord'" in log.message # Mode should be set to True and name set to the mode name assert chord.mode is True assert chord.name == "persistent_chord" ���������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_crypto_ticker.py�����������������������������������������������������0000664�0001750�0001750�00000003453�14762660347�021562� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # This widget is based on GenPollUrl which has a separate. # We just need to test parsing here. from libqtile import widget from test.widgets.conftest import FakeBar RESPONSE = {"data": {"base": "BTC", "currency": "GBP", "amount": "29625.02"}} def test_set_defaults(): crypto = widget.CryptoTicker(currency="", symbol="") assert crypto.currency == "USD" assert crypto.symbol == "$" def test_parse(fake_qtile, fake_window): crypto = widget.CryptoTicker(currency="GBP", symbol="£", crypto="BTC") fake_bar = FakeBar([crypto], window=fake_window) crypto._configure(fake_qtile, fake_bar) assert crypto.url == "https://api.coinbase.com/v2/prices/BTC-GBP/spot" assert crypto.parse(RESPONSE) == "BTC: £29625.02" ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_crashme.py�����������������������������������������������������������0000664�0001750�0001750�00000004414�14762660347�020321� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # Widget specific tests import pytest import libqtile.bar import libqtile.config import libqtile.confreader import libqtile.layout from libqtile.command.interface import CommandException from libqtile.widget.crashme import _CrashMe def test_crashme_init(manager_nospawn, minimal_conf_noscreen): crash = _CrashMe() config = minimal_conf_noscreen config.screens = [libqtile.config.Screen(top=libqtile.bar.Bar([crash], 10))] manager_nospawn.start(config) topbar = manager_nospawn.c.bar["top"] w = topbar.info()["widgets"][0] # Check that BadWidget has been replaced by ConfigErrorWidget assert w["name"] == "_crashme" assert w["text"] == "Crash me !" # Testing errors. Exceptions are wrapped in CommandException # so we catch that and match for the intended exception. # Left click generates ZeroDivisionError with pytest.raises(CommandException) as e_info: topbar.fake_button_press(0, 0, button=1) assert e_info.match("ZeroDivisionError") # Simulate right click to trigger parse_markup error with pytest.raises(CommandException) as e_info: topbar.fake_button_press(0, 0, button=3) assert e_info.match("parse_markup[(][)] failed") ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_configerror.py�������������������������������������������������������0000664�0001750�0001750�00000004325�14762660347�021217� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # Widget specific tests import pytest import libqtile.bar import libqtile.config from libqtile.widget.base import _Widget # This widget needs to crash during _configure class BadWidget(_Widget): def _configure(self, qtile, bar): _Widget._configure(qtile, bar) 1 / 0 def draw(self): pass @pytest.mark.parametrize("position", ["top", "bottom", "left", "right"]) def test_configerrorwidget(manager_nospawn, minimal_conf_noscreen, position): """ConfigError widget should show in any bar orientation.""" widget = BadWidget(length=10) config = minimal_conf_noscreen config.screens = [libqtile.config.Screen(**{position: libqtile.bar.Bar([widget], 10)})] manager_nospawn.start(config) testbar = manager_nospawn.c.bar[position] w = testbar.info()["widgets"][0] # Check that BadWidget has been replaced by ConfigErrorWidget assert w["name"] == "configerrorwidget" assert w["text"] == "Widget crashed: BadWidget (click to hide)" # Clicking on widget hides it so let's check it works testbar.fake_button_press(0, 0, button=1) w = testbar.info()["widgets"][0] assert w["text"] == "" �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/conftest.py���������������������������������������������������������������0000664�0001750�0001750�00000007250�14762660347�017466� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������import os import shutil import signal import subprocess import pytest import libqtile.config import libqtile.confreader import libqtile.layout from libqtile.bar import Bar from libqtile.widget.base import ORIENTATION_HORIZONTAL @pytest.fixture(scope="function") def fake_bar(): return FakeBar([]) class FakeBar(Bar): def __init__(self, widgets, size=24, width=100, window=None, **config): Bar.__init__(self, widgets, size, **config) self.height = size self.width = width self.window = window self.horizontal = ORIENTATION_HORIZONTAL def draw(self): pass TEST_DIR = os.path.dirname(os.path.abspath(__file__)) DATA_DIR = os.path.join(os.path.dirname(TEST_DIR), "data") @pytest.fixture(scope="module") def svg_img_as_pypath(): "Return the py.path object of a svg image" import py audio_volume_muted = os.path.join( DATA_DIR, "svg", "audio-volume-muted.svg", ) audio_volume_muted = py.path.local(audio_volume_muted) return audio_volume_muted @pytest.fixture(scope="module") def fake_qtile(): import asyncio def no_op(*args, **kwargs): pass class FakeQtile: def __init__(self): self.register_widget = no_op # Widgets call call_soon(asyncio.create_task, self._config_async) # at _configure. The coroutine needs to be run in a loop to suppress # warnings def call_soon(self, func, *args): coroutines = [arg for arg in args if asyncio.iscoroutine(arg)] if coroutines: loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) for func in coroutines: loop.run_until_complete(func) loop.close() return FakeQtile() # Fixture that defines a minimal configurations for testing widgets. # When used in a test, the function needs to receive a list of screens # (including bar and widgets) as an argument. This config can then be # passed to the manager to start. @pytest.fixture(scope="function") def minimal_conf_noscreen(): class MinimalConf(libqtile.confreader.Config): auto_fullscreen = False keys = [] mouse = [] groups = [libqtile.config.Group("a"), libqtile.config.Group("b")] layouts = [libqtile.layout.stack.Stack(num_stacks=1)] floating_layout = libqtile.resources.default_config.floating_layout screens = [] return MinimalConf @pytest.fixture(scope="function") def dbus(monkeypatch): # for Github CI/Ubuntu, dbus-launch is provided by "dbus-x11" package launcher = shutil.which("dbus-launch") # If dbus-launch can't be found then tests will fail so we # need to skip if launcher is None: pytest.skip("dbus-launch must be installed") # dbus-launch prints two lines which should be set as # environmental variables result = subprocess.run(launcher, capture_output=True) pid = None for line in result.stdout.decode().splitlines(): # dbus server addresses can have multiple "=" so # we use partition to split by the first one onle var, _, val = line.partition("=") # Use monkeypatch to set these variables so they are # removed at end of test. monkeypatch.setitem(os.environ, var, val) # We want the pid so we can kill the process when the # test is finished if var == "DBUS_SESSION_BUS_PID": try: pid = int(val) except ValueError: pass # Environment is set and dbus server should be running yield # Test is over so kill dbus session if pid: os.kill(pid, signal.SIGTERM) ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_base.py��������������������������������������������������������������0000664�0001750�0001750�00000023142�14762660347�017610� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021-22 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest import libqtile.bar import libqtile.config from libqtile.command.base import expose_command from libqtile.widget import Spacer, TextBox from libqtile.widget.base import ThreadPoolText, _Widget from test.helpers import BareConfig, Retry class TimerWidget(_Widget): @expose_command() def set_timer1(self): self.timer1 = self.timeout_add(10, self.set_timer1) @expose_command() def cancel_timer1(self): self.timer1.cancel() @expose_command() def set_timer2(self): self.timer2 = self.timeout_add(10, self.set_timer2) @expose_command() def cancel_timer2(self): self.timer2.cancel() @expose_command() def get_active_timers(self): active = [x for x in self._futures if getattr(x, "_scheduled", False)] return len(active) class PollingWidget(ThreadPoolText): poll_count = 0 def poll(self): self.poll_count += 1 return f"Poll count: {self.poll_count}" def test_multiple_timers(minimal_conf_noscreen, manager_nospawn): config = minimal_conf_noscreen config.screens = [libqtile.config.Screen(top=libqtile.bar.Bar([TimerWidget(10)], 10))] # Start manager and check no active timers manager_nospawn.start(config) assert manager_nospawn.c.widget["timerwidget"].get_active_timers() == 0 # Start both timers and confirm both are active manager_nospawn.c.widget["timerwidget"].set_timer1() manager_nospawn.c.widget["timerwidget"].set_timer2() assert manager_nospawn.c.widget["timerwidget"].get_active_timers() == 2 # Cancel timer1 manager_nospawn.c.widget["timerwidget"].cancel_timer1() assert manager_nospawn.c.widget["timerwidget"].get_active_timers() == 1 # Cancel timer2 manager_nospawn.c.widget["timerwidget"].cancel_timer2() assert manager_nospawn.c.widget["timerwidget"].get_active_timers() == 0 # Restart both timers manager_nospawn.c.widget["timerwidget"].set_timer1() manager_nospawn.c.widget["timerwidget"].set_timer2() assert manager_nospawn.c.widget["timerwidget"].get_active_timers() == 2 # Verify that `finalize()` cancels all timers. manager_nospawn.c.widget["timerwidget"].eval("self.finalize()") assert manager_nospawn.c.widget["timerwidget"].get_active_timers() == 0 def test_mirrors_same_bar(minimal_conf_noscreen, manager_nospawn): """Verify that mirror created when widget reused in same bar.""" config = minimal_conf_noscreen tbox = TextBox("Testing Mirrors") config.screens = [libqtile.config.Screen(top=libqtile.bar.Bar([tbox, tbox], 10))] manager_nospawn.start(config) info = manager_nospawn.c.bar["top"].info()["widgets"] # First instance is retained, second is replaced by mirror assert len(info) == 2 assert [w["name"] for w in info] == ["textbox", "mirror"] def test_mirrors_different_bar(minimal_conf_noscreen, manager_nospawn): """Verify that mirror created when widget reused in different bar.""" config = minimal_conf_noscreen tbox = TextBox("Testing Mirrors") config.fake_screens = [ libqtile.config.Screen(top=libqtile.bar.Bar([tbox], 10), x=0, y=0, width=400, height=600), libqtile.config.Screen( top=libqtile.bar.Bar([tbox], 10), x=400, y=0, width=400, height=600 ), ] manager_nospawn.start(config) screen0 = manager_nospawn.c.screen[0].bar["top"].info()["widgets"] screen1 = manager_nospawn.c.screen[1].bar["top"].info()["widgets"] # Original widget should be in the first screen assert len(screen0) == 1 assert [w["name"] for w in screen0] == ["textbox"] # Widget is replaced with a mirror on the second screen assert len(screen1) == 1 assert [w["name"] for w in screen1] == ["mirror"] def test_mirrors_stretch(minimal_conf_noscreen, manager_nospawn): """Verify that mirror widgets stretch according to their own bar""" config = minimal_conf_noscreen tbox = TextBox("Testing Mirrors") stretch = Spacer() config.fake_screens = [ libqtile.config.Screen( top=libqtile.bar.Bar([stretch, tbox], 10), x=0, y=0, width=600, height=600 ), libqtile.config.Screen( top=libqtile.bar.Bar([stretch, tbox], 10), x=600, y=0, width=200, height=600 ), ] manager_nospawn.start(config) screen0 = manager_nospawn.c.screen[0].bar["top"].info()["widgets"] screen1 = manager_nospawn.c.screen[1].bar["top"].info()["widgets"] # Spacer is the first widget in each bar. This should be stretched according to its own bar # so check its length is equal to the bar length minus the length of the text box. assert screen0[0]["length"] == 600 - screen0[1]["length"] assert screen1[0]["length"] == 200 - screen1[1]["length"] def test_threadpolltext_force_update(minimal_conf_noscreen, manager_nospawn): """Check that widget can be polled instantly via command interface.""" config = minimal_conf_noscreen tpoll = PollingWidget("Not polled") config.screens = [libqtile.config.Screen(top=libqtile.bar.Bar([tpoll], 10))] manager_nospawn.start(config) widget = manager_nospawn.c.widget["pollingwidget"] # Widget is polled immediately when configured assert widget.info()["text"] == "Poll count: 1" # Default update_interval is 600 seconds so the widget won't poll during test unless forced widget.force_update() assert widget.info()["text"] == "Poll count: 2" def test_threadpolltext_update_interval_none(minimal_conf_noscreen, manager_nospawn): """Check that widget will be polled only once if update_interval == None""" config = minimal_conf_noscreen tpoll = PollingWidget("Not polled", update_interval=None) config.screens = [libqtile.config.Screen(top=libqtile.bar.Bar([tpoll], 10))] manager_nospawn.start(config) widget = manager_nospawn.c.widget["pollingwidget"] # Widget is polled immediately when configured assert widget.info()["text"] == "Poll count: 1" class ScrollingTextConfig(BareConfig): screens = [ libqtile.config.Screen( top=libqtile.bar.Bar( [ TextBox("NoWidth", name="no_width", scroll=True), TextBox("ShortText", name="short_text", width=100, scroll=True), TextBox("Longer text " * 5, name="longer_text", width=100, scroll=True), TextBox( "ShortFixedWidth", name="fixed_width", width=200, scroll=True, scroll_fixed_width=True, ), ], 32, ) ) ] scrolling_text_config = pytest.mark.parametrize("manager", [ScrollingTextConfig], indirect=True) @scrolling_text_config def test_text_scroll_no_width(manager): """ Scrolling text needs a fixed width. If this is not set a warning is provided and scrolling is disabled. """ logs = manager.get_log_buffer() assert "WARNING - no_width: You must specify a width when enabling scrolling." in logs _, output = manager.c.widget["no_width"].eval("self.scroll") assert output == "False" @scrolling_text_config def test_text_scroll_short_text(manager): """ When scrolling is enabled, width is a "max_width" setting. Shorter text will reslult in widget shrinking. """ widget = manager.c.widget["short_text"] # Width is shorter than max width assert widget.info()["width"] < 100 # Scrolling is still enabled (but won't do anything) _, output = widget.eval("self.scroll") assert output == "True" _, output = widget.eval("self._should_scroll") assert output == "False" @scrolling_text_config def test_text_scroll_long_text(manager): """ Longer text scrolls by incrementing an offset counter. """ @Retry(ignore_exceptions=(AssertionError,)) def wait_for_scroll(widget): _, scroll_count = widget.eval("self._scroll_offset") assert int(scroll_count) > 5 widget = manager.c.widget["longer_text"] # Width is fixed at set width assert widget.info()["width"] == 100 # Scrolling is still enabled _, output = widget.eval("self.scroll") assert output == "True" _, output = widget.eval("self._should_scroll") assert output == "True" # Check actually scrolling wait_for_scroll(widget) @scrolling_text_config def test_scroll_fixed_width(manager): widget = manager.c.widget["fixed_width"] _, layout = widget.eval("self.layout.width") assert int(layout) < 200 # Widget width is fixed at set width assert widget.info()["width"] == 200 ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_statusnotifier.py����������������������������������������������������0000664�0001750�0001750�00000014673�14762660347�021772� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE import pytest import libqtile.bar import libqtile.config import libqtile.confreader import libqtile.layout import libqtile.widget from test.helpers import Retry # noqa: I001 pytest.skip("StatusNotifier tests are currently broken", allow_module_level=True) @Retry(ignore_exceptions=(AssertionError,)) def wait_for_icon(widget, hidden=True, prop="width"): width = widget.info()[prop] if hidden: assert width == 0 else: assert width > 0 @Retry(ignore_exceptions=(AssertionError,)) def check_fullscreen(windows, fullscreen=True): full = windows()[0]["fullscreen"] assert full is fullscreen @pytest.fixture(scope="function") def sni_config(request, manager_nospawn): """ Fixture provides a manager instance with StatusNotifier in the bar. Widget can be customised via parameterize. """ class SNIConfig(libqtile.confreader.Config): """Config for the test.""" auto_fullscreen = True keys = [] mouse = [] groups = [ libqtile.config.Group("a"), ] layouts = [libqtile.layout.Max()] floating_layout = libqtile.resources.default_config.floating_layout screens = [ libqtile.config.Screen( top=libqtile.bar.Bar( [libqtile.widget.StatusNotifier(**getattr(request, "param", dict()))], 50, ), ) ] yield SNIConfig @pytest.mark.usefixtures("dbus") def test_statusnotifier_defaults(manager_nospawn, sni_config): """Check that widget displays and removes icon.""" manager_nospawn.start(sni_config) widget = manager_nospawn.c.widget["statusnotifier"] assert widget.info()["width"] == 0 win = manager_nospawn.test_window("TestSNI", export_sni=True) wait_for_icon(widget, hidden=False) # Kill it and icon disappears manager_nospawn.kill_window(win) wait_for_icon(widget, hidden=True) @pytest.mark.usefixtures("dbus") def test_statusnotifier_defaults_vertical_bar(manager_nospawn, sni_config): """Check that widget displays and removes icon.""" screen = sni_config.screens[0] screen.left = screen.top screen.top = None manager_nospawn.start(sni_config) widget = manager_nospawn.c.widget["statusnotifier"] assert widget.info()["height"] == 0 win = manager_nospawn.test_window("TestSNI", export_sni=True) wait_for_icon(widget, hidden=False, prop="height") # Kill it and icon disappears manager_nospawn.kill_window(win) wait_for_icon(widget, hidden=True, prop="height") @pytest.mark.parametrize("sni_config", [{"icon_size": 35}], indirect=True) @pytest.mark.usefixtures("dbus") def test_statusnotifier_icon_size(manager_nospawn, sni_config): """Check that widget displays and removes icon.""" manager_nospawn.start(sni_config) widget = manager_nospawn.c.widget["statusnotifier"] assert widget.info()["width"] == 0 win = manager_nospawn.test_window("TestSNI", export_sni=True) wait_for_icon(widget, hidden=False) # Width should be icon_size (35) + 2 * padding (3) = 41 assert widget.info()["width"] == 41 manager_nospawn.kill_window(win) @pytest.mark.usefixtures("dbus") def test_statusnotifier_left_click(manager_nospawn, sni_config): """Check `activate` method when left-clicking widget.""" manager_nospawn.start(sni_config) widget = manager_nospawn.c.widget["statusnotifier"] windows = manager_nospawn.c.windows assert widget.info()["width"] == 0 try: win = manager_nospawn.test_window("TestSNILeftClick", export_sni=True) wait_for_icon(widget, hidden=False) # Check we have window and that it's not fullscreen assert len(windows()) == 1 check_fullscreen(windows, False) # Left click will toggle fullscreen manager_nospawn.c.bar["top"].fake_button_press(10, 0, 1) check_fullscreen(windows, True) # Left click again will restore window manager_nospawn.c.bar["top"].fake_button_press(10, 0, 1) check_fullscreen(windows, False) manager_nospawn.kill_window(win) assert not windows() except Exception: pytest.xfail("Unsure why test fails, but let's accept a failure for now.") @pytest.mark.usefixtures("dbus") def test_statusnotifier_left_click_vertical_bar(manager_nospawn, sni_config): """Check `activate` method when left-clicking widget in vertical bar.""" screen = sni_config.screens[0] screen.left = screen.top screen.top = None manager_nospawn.start(sni_config) widget = manager_nospawn.c.widget["statusnotifier"] windows = manager_nospawn.c.windows assert widget.info()["height"] == 0 try: win = manager_nospawn.test_window("TestSNILeftClick", export_sni=True) wait_for_icon(widget, hidden=False, prop="height") # Check we have window and that it's not fullscreen assert len(windows()) == 1 check_fullscreen(windows, False) # Left click will toggle fullscreen manager_nospawn.c.bar["left"].fake_button_press(0, 10, 1) check_fullscreen(windows, True) # Left click again will restore window manager_nospawn.c.bar["left"].fake_button_press(0, 10, 1) check_fullscreen(windows, False) manager_nospawn.kill_window(win) assert not windows() except Exception: pytest.xfail("Unsure why test fails, but let's accept a failure for now.") ���������������������������������������������������������������������qtile-0.31.0/test/widgets/test_memory.py������������������������������������������������������������0000664�0001750�0001750�00000006311�14762660347�020205� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # Widget specific tests import sys from importlib import reload from types import ModuleType import pytest import libqtile.bar import libqtile.config def no_op(*args, **kwargs): pass class FakePsutil(ModuleType): class virtual_memory: # noqa: N801 def __init__(self): self.used = 2534260736 # 2,474,864K, 2,417M, 2.36G self.total = 8180686848 # 7,988,952K, 7,802M, 7.72G self.free = 2354114560 self.available = 5646426112 self.percent = 39.4 self.buffers = 346394624 self.active = 1132359680 self.inactive = 3862183936 self.shared = 516395008 class swap_memory: # noqa: N801 def __init__(self): self.total = 8429498368 self.used = 0 self.free = 8429498368 self.percent = 0.0 @pytest.fixture() def patched_memory( monkeypatch, ): monkeypatch.setitem(sys.modules, "psutil", FakePsutil("psutil")) from libqtile.widget import memory reload(memory) return memory def test_memory_defaults(manager_nospawn, minimal_conf_noscreen, patched_memory): """Test no text when free space over threshold""" widget = patched_memory.Memory() config = minimal_conf_noscreen config.screens = [libqtile.config.Screen(top=libqtile.bar.Bar([widget], 10))] manager_nospawn.start(config) assert manager_nospawn.c.widget["memory"].info()["text"] == " 2417M/ 7802M" @pytest.mark.parametrize( "unit,expects", [ ("G", " 2G/ 8G"), ("M", " 2417M/ 7802M"), ("K", " 2474864K/ 7988952K"), ("B", " 2534260736B/ 8180686848B"), ], ) def test_memory_units(manager_nospawn, minimal_conf_noscreen, patched_memory, unit, expects): """Test no text when free space over threshold""" widget = patched_memory.Memory(measure_mem=unit) config = minimal_conf_noscreen config.screens = [libqtile.config.Screen(top=libqtile.bar.Bar([widget], 10))] manager_nospawn.start(config) manager_nospawn.c.widget["memory"].eval("self.update(self.poll())") assert manager_nospawn.c.widget["memory"].info()["text"] == expects �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_generic_poll_text.py�������������������������������������������������0000664�0001750�0001750�00000011313�14762660347�022401� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # Widget specific tests import sys from importlib import reload from types import ModuleType import pytest from libqtile.widget import generic_poll_text class Mockxml(ModuleType): @classmethod def parse(cls, value): return {"test": value} class MockRequest: return_value = None def __init__(self, *args, **kwargs): pass class Mockurlopen: def __init__(self, request): self.request = request class headers: # noqa: N801 @classmethod def get_content_charset(cls): return "utf-8" def read(self): return self.request.return_value def test_gen_poll_text(): gpt_no_func = generic_poll_text.GenPollText() assert gpt_no_func.poll() == "You need a poll function" gpt_with_func = generic_poll_text.GenPollText(func=lambda: "Has function") assert gpt_with_func.poll() == "Has function" def test_gen_poll_url_not_configured(): gpurl = generic_poll_text.GenPollUrl() assert gpurl.poll() == "Invalid config" def test_gen_poll_url_no_json(): gpurl = generic_poll_text.GenPollUrl(json=False) assert "Content-Type" not in gpurl.headers def test_gen_poll_url_headers_and_json(): gpurl = generic_poll_text.GenPollUrl( headers={"fake-header": "fake-value"}, data={"argument": "data value"}, user_agent="qtile test", ) assert gpurl.headers["User-agent"] == "qtile test" assert gpurl.headers["fake-header"] == "fake-value" assert gpurl.headers["Content-Type"] == "application/json" assert gpurl.data.decode() == '{"argument": "data value"}' def test_gen_poll_url_text(monkeypatch): gpurl = generic_poll_text.GenPollUrl(json=False, parse=lambda x: x, url="testing") monkeypatch.setattr(generic_poll_text, "Request", MockRequest) monkeypatch.setattr(generic_poll_text, "urlopen", Mockurlopen) generic_poll_text.Request.return_value = b"OK" assert gpurl.poll() == "OK" def test_gen_poll_url_json(monkeypatch): gpurl = generic_poll_text.GenPollUrl(parse=lambda x: x, data=[1, 2, 3], url="testing") monkeypatch.setattr(generic_poll_text, "Request", MockRequest) monkeypatch.setattr(generic_poll_text, "urlopen", Mockurlopen) generic_poll_text.Request.return_value = b'{"test": "OK"}' assert gpurl.poll()["test"] == "OK" def test_gen_poll_url_xml_no_xmltodict(monkeypatch): gpurl = generic_poll_text.GenPollUrl(json=False, xml=True, parse=lambda x: x, url="testing") monkeypatch.setattr(generic_poll_text, "Request", MockRequest) monkeypatch.setattr(generic_poll_text, "urlopen", Mockurlopen) generic_poll_text.Request.return_value = b"OK" with pytest.raises(Exception): gpurl.poll() def test_gen_poll_url_xml_has_xmltodict(monkeypatch): # injected fake xmltodict module but we have to reload the widget module # as the ImportError test is only run once when the module is loaded. monkeypatch.setitem(sys.modules, "xmltodict", Mockxml("xmltodict")) reload(generic_poll_text) gpurl = generic_poll_text.GenPollUrl(json=False, xml=True, parse=lambda x: x, url="testing") monkeypatch.setattr(generic_poll_text, "Request", MockRequest) monkeypatch.setattr(generic_poll_text, "urlopen", Mockurlopen) generic_poll_text.Request.return_value = b"OK" assert gpurl.poll()["test"] == "OK" def test_gen_poll_url_broken_parse(monkeypatch): gpurl = generic_poll_text.GenPollUrl(json=False, parse=lambda x: x.foo, url="testing") monkeypatch.setattr(generic_poll_text, "Request", MockRequest) monkeypatch.setattr(generic_poll_text, "urlopen", Mockurlopen) generic_poll_text.Request.return_value = b"OK" assert gpurl.poll() == "Can't parse" ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_windowtabs.py��������������������������������������������������������0000664�0001750�0001750�00000011062�14762660347�021055� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # Widget specific tests import pytest import libqtile.config from libqtile import bar, layout, widget from libqtile.config import Screen from libqtile.confreader import Config def custom_text_parser(name): return f"TEST-{name}-TEST" class WindowTabsConfig(Config): auto_fullscreen = True groups = [libqtile.config.Group("a"), libqtile.config.Group("b")] layouts = [layout.Stack()] floating_layout = libqtile.resources.default_config.floating_layout keys = [] mouse = [] fake_screens = [ Screen( top=bar.Bar( [ widget.WindowTabs(), widget.WindowTabs(name="customparse", parse_text=custom_text_parser), ], 24, ), bottom=bar.Bar( [ widget.WindowTabs(selected="!!"), ], 24, ), x=0, y=0, width=900, height=960, ), ] screens = [] windowtabs_config = pytest.mark.parametrize("manager", [WindowTabsConfig], indirect=True) @windowtabs_config def test_single_window_states(manager): def widget_text(): return manager.c.bar["top"].info()["widgets"][0]["text"] # When no windows are spawned the text should be "" # Initially TextBox has " " but the Config.set_group function already # calls focus_change hook, so the text should be updated to "" assert widget_text() == "" # Load a window proc = manager.test_window("one") assert widget_text() == "<b>one</b>" # Maximize window manager.c.window.toggle_maximize() assert widget_text() == "<b>[] one</b>" # Minimize window manager.c.window.toggle_minimize() assert widget_text() == "<b>_ one</b>" # Float window manager.c.window.toggle_minimize() manager.c.window.toggle_floating() assert widget_text() == "<b>V one</b>" # Kill the window and check empty string again manager.kill_window(proc) assert widget_text() == "" @windowtabs_config def test_multiple_windows(manager): def widget_text(): return manager.c.bar["top"].info()["widgets"][0]["text"] window_one = manager.test_window("one") assert widget_text() == "<b>one</b>" window_two = manager.test_window("two") assert widget_text() in ["<b>two</b> | one", "one | <b>two</b>"] manager.c.layout.next() assert widget_text() in ["<b>one</b> | two", "two | <b>one</b>"] manager.kill_window(window_one) assert widget_text() == "<b>two</b>" manager.kill_window(window_two) assert widget_text() == "" @windowtabs_config def test_selected(manager): # Bottom bar widget has custom "selected" indicator def widget_text(): return manager.c.bar["bottom"].info()["widgets"][0]["text"] window_one = manager.test_window("one") assert widget_text() == "!!one!!" manager.kill_window(window_one) assert widget_text() == "" @windowtabs_config def test_escaping_text(manager): """ Ampersands can cause a crash if not escaped before passing to pangocffi.parse_markup. Test that the widget can parse text safely. """ manager.test_window("Text & Text") assert manager.c.widget["windowtabs"].info()["text"] == "<b>Text & Text</b>" @windowtabs_config def test_custom_text_parser(manager): """Test the custom text parser function.""" manager.test_window("one") assert manager.c.widget["customparse"].info()["text"] == "<b>TEST-one-TEST</b>" ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_hdd.py���������������������������������������������������������������0000664�0001750�0001750�00000005447�14762660347�017445� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2024 Florian G. Hechler # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # Widget specific tests import sys import tempfile from importlib import reload from types import ModuleType import pytest import libqtile.config import libqtile.widget from libqtile.bar import Bar class MockPsutil(ModuleType): pass # Set ticks of fake stat file def set_io_ticks(temp_file, io_ticks): temp_file.truncate(0) temp_file.write( f"13850 7547 1201386 14536 12612 60213 1838656 73847 0 {io_ticks} 112672 0 0 0 0 2116 24287" ) temp_file.flush() @pytest.fixture def hdd_manager(monkeypatch, manager_nospawn, minimal_conf_noscreen): monkeypatch.setitem(sys.modules, "psutil", MockPsutil("psutil")) from libqtile.widget import hdd reload(hdd) config = minimal_conf_noscreen config.screens = [libqtile.config.Screen(top=Bar([hdd.HDD()], 10))] manager_nospawn.start(config) yield manager_nospawn def test_hdd(hdd_manager): # Create a fake stat file fake_stat_file = tempfile.NamedTemporaryFile(mode="w+", delete=False) widget = hdd_manager.c.widget["hdd"] widget.eval(f"self.path = '{fake_stat_file.name}'") set_io_ticks(fake_stat_file, 0) widget.eval("self.update(self.poll())") assert widget.info()["text"] == "HDD 0.0%" set_io_ticks(fake_stat_file, 300000) widget.eval("self.update(self.poll())") assert widget.info()["text"] == "HDD 50.0%" set_io_ticks(fake_stat_file, 900000) widget.eval("self.update(self.poll())") assert widget.info()["text"] == "HDD 100.0%" set_io_ticks(fake_stat_file, 2000000) widget.eval("self.update(self.poll())") assert widget.info()["text"] == "HDD 100.0%" set_io_ticks(fake_stat_file, 0) widget.eval("self.update(self.poll())") assert widget.info()["text"] == "HDD 0.0%" �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_mouse_callback.py����������������������������������������������������0000664�0001750�0001750�00000003534�14762660347�021645� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import libqtile.bar import libqtile.config import libqtile.confreader import libqtile.layout from libqtile import widget from libqtile.lazy import lazy def test_lazy_callback(manager_nospawn, minimal_conf_noscreen): """Test widgets accept lazy calls""" textbox = widget.TextBox( text="Testing", mouse_callbacks={ "Button1": lazy.widget["textbox"].update("LazyCall"), }, ) config = minimal_conf_noscreen config.screens = [libqtile.config.Screen(top=libqtile.bar.Bar([textbox], 10))] manager_nospawn.start(config) topbar = manager_nospawn.c.bar["top"] assert topbar.widget["textbox"].info()["text"] == "Testing" topbar.fake_button_press(0, 0, button=1) assert topbar.widget["textbox"].info()["text"] == "LazyCall" ��������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_keyboardkbdd.py������������������������������������������������������0000664�0001750�0001750�00000010536�14762660347�021326� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # Widget specific tests # The test_widget_init_config will cover the scenario where `kbdd` is not # running and will also run the asyncio _config_async method. # This test file covers the remaining widget code from importlib import reload import pytest from test.widgets.conftest import FakeBar async def mock_signal_receiver(*args, **kwargs): return True class MockSpawn: call_count = 0 @classmethod def call_process(cls, *args, **kwargs): if cls.call_count == 0: cls.call_count += 1 return "" return "kbdd" class MockMessage: def __init__(self, is_signal=True, body=0): self.message_type = 1 if is_signal else 0 self.body = [body] @pytest.fixture def patched_widget(monkeypatch): from libqtile.widget import keyboardkbdd reload(keyboardkbdd) # The next line shouldn't be necessary but I got occasional failures without it when testing locally monkeypatch.setattr( "libqtile.widget.keyboardkbdd.KeyboardKbdd.call_process", MockSpawn.call_process ) monkeypatch.setattr("libqtile.widget.keyboardkbdd.add_signal_receiver", mock_signal_receiver) return keyboardkbdd def test_keyboardkbdd_process_running(fake_qtile, patched_widget, fake_window): MockSpawn.call_count = 1 kbd = patched_widget.KeyboardKbdd(configured_keyboards=["gb", "us"]) fakebar = FakeBar([kbd], window=fake_window) kbd._configure(fake_qtile, fakebar) assert kbd.is_kbdd_running assert kbd.keyboard == "gb" # Create a message with the index of the active keyboard message = MockMessage(body=1) kbd._signal_received(message) assert kbd.keyboard == "us" def test_keyboardkbdd_process_not_running(fake_qtile, patched_widget, fake_window): MockSpawn.call_count = 0 kbd = patched_widget.KeyboardKbdd(configured_keyboards=["gb", "us"]) fakebar = FakeBar([kbd], window=fake_window) kbd._configure(fake_qtile, fakebar) assert not kbd.is_kbdd_running assert kbd.keyboard == "N/A" # Second call of _check_kbdd will confirm process running # so widget should now show layout kbd.poll() assert kbd.keyboard == "gb" # Custom colours are not set until a signal is received # TO DO: This should be fixed so the colour is set on __init__ def test_keyboard_kbdd_colours(fake_qtile, patched_widget, fake_window): MockSpawn.call_count = 1 kbd = patched_widget.KeyboardKbdd( configured_keyboards=["gb", "us"], colours=["#ff0000", "#00ff00"] ) fakebar = FakeBar([kbd], window=fake_window) kbd._configure(fake_qtile, fakebar) # Create a message with the index of the active keyboard message = MockMessage(body=0) kbd._signal_received(message) assert kbd.layout.colour == "#ff0000" # Create a message with the index of the active keyboard message = MockMessage(body=1) kbd._signal_received(message) assert kbd.layout.colour == "#00ff00" # No change where self.colours is a string kbd.colours = "#ffff00" kbd._set_colour(1) assert kbd.layout.colour == "#00ff00" # Colours list is shorter than length of layouts kbd.colours = ["#ff00ff"] # Should pick second item in colours list but it doesn't exit # so widget looks for previous item kbd._set_colour(1) assert kbd.layout.colour == "#ff00ff" ������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_do_not_disturb.py����������������������������������������������������0000664�0001750�0001750�00000007477�14762660347�021731� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2024 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest from libqtile.config import Bar, Screen from libqtile.confreader import Config from libqtile.widget import do_not_disturb as dnd class DunstStatus: PAUSED = False @classmethod def toggle(cls): cls.PAUSED = not cls.PAUSED @classmethod def toggle_and_status(cls): cls.toggle() return cls.PAUSED def mock_check_output(args): status = str(DunstStatus.PAUSED).lower() return status.encode() @pytest.fixture(scope="function") def patched_dnd(monkeypatch): monkeypatch.setattr("libqtile.widget.do_not_disturb.check_output", mock_check_output) class PatchedDND(dnd.DoNotDisturb): def __init__(self, **config): dnd.DoNotDisturb.__init__(self, **config) DunstStatus.PAUSED = False self.mouse_callbacks = {"Button1": lambda: DunstStatus.toggle()} self.name = "donotdisturb" yield PatchedDND @pytest.fixture(scope="function") def dnd_manager(manager_nospawn, request, patched_dnd): class GroupConfig(Config): screens = [ Screen( top=Bar( [patched_dnd(update_interval=10, **getattr(request, "param", dict()))], 30 ) ) ] manager_nospawn.start(GroupConfig) yield manager_nospawn def config(**kwargs): return pytest.mark.parametrize("dnd_manager", [kwargs], indirect=True) def test_dnd(dnd_manager): widget = dnd_manager.c.widget["donotdisturb"] assert widget.info()["text"] == "O" dnd_manager.c.bar["top"].fake_button_press(0, 0, 1) widget.eval("self.update(self.poll())") assert widget.info()["text"] == "X" dnd_manager.c.bar["top"].fake_button_press(0, 0, 1) widget.eval("self.update(self.poll())") assert widget.info()["text"] == "O" dnd_manager.c.bar["top"].fake_button_press(0, 0, 1) widget.eval("self.update(self.poll())") assert widget.info()["text"] == "X" @config(poll_function=DunstStatus.toggle_and_status) def test_dnd_custom_func(dnd_manager): widget = dnd_manager.c.widget["donotdisturb"] # Status is reversed here as the custom func toggles the status # every time it's polled assert widget.info()["text"] == "X" widget.eval("self.update(self.poll())") assert widget.info()["text"] == "O" widget.eval("self.update(self.poll())") assert widget.info()["text"] == "X" widget.eval("self.update(self.poll())") assert widget.info()["text"] == "O" @config(enabled_icon="-", disabled_icon="+") def test_dnd_custom_icons(dnd_manager): widget = dnd_manager.c.widget["donotdisturb"] assert widget.info()["text"] == "+" dnd_manager.c.bar["top"].fake_button_press(0, 0, 1) widget.eval("self.update(self.poll())") assert widget.info()["text"] == "-" �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_plasma.py������������������������������������������������������������0000664�0001750�0001750�00000007117�14762660347�020157� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2024 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest from libqtile import layout from libqtile.config import Bar, Screen from libqtile.confreader import Config from libqtile.widget import plasma @pytest.fixture(scope="function") def plasma_manager(manager_nospawn, request): class PlasmaConfig(Config): layouts = [layout.Plasma()] screens = [Screen(top=Bar([plasma.Plasma(**getattr(request, "param", dict()))], 30))] manager_nospawn.start(PlasmaConfig) yield manager_nospawn def config(**kwargs): return pytest.mark.parametrize("plasma_manager", [kwargs], indirect=True) def test_plasma_defaults(plasma_manager): def text(): return plasma_manager.c.widget["plasma"].info()["text"] layout = plasma_manager.c.layout assert text() == " H" layout.mode_vertical() assert text() == " V" layout.mode_horizontal_split() assert text() == "HS" layout.mode_vertical_split() assert text() == "VS" @config(horizontal="-", vertical="|", horizontal_split="=", vertical_split="||", format="{mode}") def test_custom_text(plasma_manager): def text(): return plasma_manager.c.widget["plasma"].info()["text"] layout = plasma_manager.c.layout assert text() == "-" layout.mode_vertical() assert text() == "|" layout.mode_horizontal_split() assert text() == "=" layout.mode_vertical_split() assert text() == "||" @config(format="{mode}") def test_window_focus_change(plasma_manager): def text(): return plasma_manager.c.widget["plasma"].info()["text"] def win(name): idx = [w["id"] for w in plasma_manager.c.windows() if w["name"] == name] assert idx return plasma_manager.c.window[idx[0]] layout = plasma_manager.c.layout assert text() == "H" plasma_manager.test_window("one") plasma_manager.test_window("two") layout.mode_vertical() plasma_manager.test_window("three") assert text() == "H" win("one").focus() assert text() == "V" win("three").focus() assert text() == "H" win("two").focus() assert text() == "H" win("one").focus() assert text() == "V" @config(format="{mode}") def test_mode_change(plasma_manager): def text(): return plasma_manager.c.widget["plasma"].info()["text"] assert text() == "H" for mode in ["HS", "V", "VS", "H"]: plasma_manager.c.widget["plasma"].next_mode() assert text() == mode for mode in ["HS", "V", "VS", "H"]: plasma_manager.c.bar["top"].fake_button_press(0, 0, 1) assert text() == mode �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_hide_crash.py��������������������������������������������������������0000664�0001750�0001750�00000003637�14762660347�020776� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2024 Sean Vig # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest from libqtile import bar from libqtile.config import Screen from libqtile.confreader import Config from libqtile.widget.base import _Widget class BadWidget(_Widget): def __init__(self, **config): _Widget.__init__(self, bar.CALCULATED, **config) def _configure(self, qtile, bar): _Widget._configure(self, qtile, bar) # Crash! 1 / 0 class CrashConfig(Config): screens = [Screen(top=bar.Bar([BadWidget(), BadWidget(hide_crash=True)], 20))] crash_config = pytest.mark.parametrize("manager", [CrashConfig], indirect=True) @crash_config def test_hide_crashed_widget(manager): widgets = manager.c.bar["top"].items("widget")[1] # There should only be one widget in the bar assert len(widgets) == 1 # That widget should be a ConfigErrorWidget assert widgets[0] == "configerrorwidget" �������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_pomodoro.py����������������������������������������������������������0000664�0001750�0001750�00000010760�14762660347�020536� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from datetime import datetime, timedelta from importlib import reload import pytest from libqtile.widget import pomodoro from test.widgets.conftest import FakeBar COLOR_INACTIVE = "123456" COLOR_ACTIVE = "654321" COLOR_BREAK = "AABBCC" PREFIX_INACTIVE = "TESTING POMODORO" PREFIX_ACTIVE = "ACTIVE" PREFIX_BREAK = "BREAK" PREFIX_LONG_BREAK = "LONG BREAK" PREFIX_PAUSED = "PAUSING" # Mock Datetime object that returns a set datetime but can # be adjusted via the '_adjustment' property class MockDatetime(datetime): _adjustment = timedelta(0) @classmethod def now(cls, *args, **kwargs): return cls(2021, 1, 1, 12, 00, 0) + cls._adjustment @pytest.fixture def patched_widget(monkeypatch): reload(pomodoro) monkeypatch.setattr("libqtile.widget.pomodoro.datetime", MockDatetime) yield pomodoro @pytest.mark.usefixtures("patched_widget") def test_pomodoro(fake_qtile, fake_window): widget = pomodoro.Pomodoro( update_interval=100, color_active=COLOR_ACTIVE, color_inactive=COLOR_INACTIVE, color_break=COLOR_BREAK, num_pomodori=2, length_pomodori=15, length_short_break=5, length_long_break=10, notification_on=False, prefix_inactive=PREFIX_INACTIVE, prefix_active=PREFIX_ACTIVE, prefix_break=PREFIX_BREAK, prefix_long_break=PREFIX_LONG_BREAK, prefix_paused=PREFIX_PAUSED, ) fakebar = FakeBar([widget], window=fake_window) widget._configure(fake_qtile, fakebar) # When we start, widget is inactive assert widget.poll() == PREFIX_INACTIVE assert widget.layout.colour == COLOR_INACTIVE # Left clicking toggles state widget.toggle_break() assert widget.poll() == f"{PREFIX_ACTIVE}0:15:00" assert widget.layout.colour == COLOR_ACTIVE # Another left click should pause widget.toggle_break() assert widget.poll() == PREFIX_PAUSED assert widget.layout.colour == COLOR_INACTIVE widget.toggle_break() # Add 5 mins should take 5 mins off our timer MockDatetime._adjustment += timedelta(minutes=5) assert widget.poll() == f"{PREFIX_ACTIVE}0:10:00" assert widget.layout.colour == COLOR_ACTIVE # Add 10 mins should take us to end of first pomodoro # So we get a short break between pomodori MockDatetime._adjustment += timedelta(minutes=10) assert widget.poll() == f"{PREFIX_BREAK}0:05:00" assert widget.layout.colour == COLOR_BREAK # Add 5 mins should take us to start of second pomodoro MockDatetime._adjustment += timedelta(minutes=5) assert widget.poll() == f"{PREFIX_ACTIVE}0:15:00" assert widget.layout.colour == COLOR_ACTIVE # Add 15 mins should take us to end of second pomodoro # and start of long break (as there are only two pomodori) MockDatetime._adjustment += timedelta(minutes=15) assert widget.poll() == f"{PREFIX_LONG_BREAK}0:10:00" assert widget.layout.colour == COLOR_BREAK # Move forward so we're at start of next pomodoro MockDatetime._adjustment += timedelta(minutes=10) assert widget.poll() == f"{PREFIX_ACTIVE}0:15:00" # Advance into pomodoro MockDatetime._adjustment += timedelta(minutes=10) assert widget.poll() == f"{PREFIX_ACTIVE}0:05:00" # Right-click toggles active state widget.toggle_active() assert widget.poll() == PREFIX_INACTIVE # Right-click again resets status widget.toggle_active() assert widget.poll() == f"{PREFIX_ACTIVE}0:15:00" ����������������qtile-0.31.0/test/widgets/__init__.py���������������������������������������������������������������0000664�0001750�0001750�00000000000�14762660347�017362� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_misc.py��������������������������������������������������������������0000664�0001750�0001750�00000004055�14762660347�017633� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2015 Tycho Andersen # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # Widget specific tests import pytest from libqtile.bar import Bar from libqtile.command.base import expose_command from libqtile.config import Screen from libqtile.widget import TextBox from test.conftest import BareConfig class ColorChanger(TextBox): count = 0 @expose_command() def update(self, text): self.count += 1 if self.count % 2 == 0: self.foreground = "ff0000" else: self.foreground = "0000ff" self.text = text class WidgetTestConf(BareConfig): screens = [Screen(bottom=Bar([ColorChanger(name="colorchanger")], 20))] widget_conf = pytest.mark.parametrize("manager", [WidgetTestConf], indirect=True) @widget_conf def test_textbox_color_change(manager): manager.c.widget["colorchanger"].update("f") assert manager.c.widget["colorchanger"].info()["foreground"] == "0000ff" manager.c.widget["colorchanger"].update("f") assert manager.c.widget["colorchanger"].info()["foreground"] == "ff0000" �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_clock.py�������������������������������������������������������������0000664�0001750�0001750�00000022532�14762660347�017773� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # Widget specific tests import datetime import sys from importlib import reload import pytest import libqtile.config from libqtile.widget import clock from test.widgets.conftest import FakeBar def no_op(*args, **kwargs): pass # Mock Datetime object that returns a set datetime and also # has a simplified timezone method to check functionality of # the widget. class MockDatetime(datetime.datetime): @classmethod def now(cls, *args, **kwargs): return cls(2021, 1, 1, 10, 20, 30) def astimezone(self, tzone=None): if tzone is None: return self return self + tzone.utcoffset(None) @pytest.fixture def patched_clock(monkeypatch): # Stop system importing these modules in case they exist on environment monkeypatch.setitem(sys.modules, "pytz", None) monkeypatch.setitem(sys.modules, "dateutil", None) monkeypatch.setitem(sys.modules, "dateutil.tz", None) # Reload module to force ImportErrors reload(clock) # Override datetime. # This is key for testing as we can fix time. monkeypatch.setattr("libqtile.widget.clock.datetime", MockDatetime) def test_clock(fake_qtile, monkeypatch, fake_window): """test clock output with default settings""" monkeypatch.setattr("libqtile.widget.clock.datetime", MockDatetime) clk1 = clock.Clock() fakebar = FakeBar([clk1], window=fake_window) clk1._configure(fake_qtile, fakebar) text = clk1.poll() assert text == "10:20" @pytest.mark.usefixtures("patched_clock") def test_clock_invalid_timezone(fake_qtile, monkeypatch, fake_window, caplog): """test clock widget with invalid timezone (and no pytz or dateutil modules)""" class FakeDateutilTZ: @classmethod def tz(cls): return cls @classmethod def gettz(cls, val): return None # pytz and dateutil must not be in the sys.modules dict... monkeypatch.delitem(sys.modules, "pytz") monkeypatch.delitem(sys.modules, "dateutil") # Set up references to pytz and dateutil so we know these aren't being used # If they're called, the widget would try to run None(self.timezone) which # would raise an exception clock.pytz = None clock.dateutil = FakeDateutilTZ # Fake datetime module just adds the timezone value to the time clk2 = clock.Clock(timezone="1") fakebar = FakeBar([clk2], window=fake_window) clk2._configure(fake_qtile, fakebar) # An invalid timezone results in a log message assert ( "Clock widget can not infer its timezone from a string without pytz or dateutil." in caplog.text ) @pytest.mark.usefixtures("patched_clock") def test_clock_datetime_timezone(fake_qtile, monkeypatch, fake_window): """test clock with datetime timezone""" class FakeDateutilTZ: class TZ: @classmethod def gettz(cls, val): None tz = TZ # Set up references to pytz and dateutil so we know these aren't being used # If they're called, the widget would try to run None(self.timezone) which # would raise an exception clock.pytz = None clock.dateutil = FakeDateutilTZ # Fake datetime module just adds the timezone value to the time tz = datetime.timezone(datetime.timedelta(hours=1)) clk3 = clock.Clock(timezone=tz) fakebar = FakeBar([clk3], window=fake_window) clk3._configure(fake_qtile, fakebar) text = clk3.poll() # Default time is 10:20 and we add 1 hour for the timezone assert text == "11:20" @pytest.mark.usefixtures("patched_clock") def test_clock_pytz_timezone(fake_qtile, monkeypatch, fake_window): """test clock with pytz timezone""" class FakeDateutilTZ: class TZ: @classmethod def gettz(cls, val): None tz = TZ class FakePytz: # pytz timezone is a string so convert it to an int and add 1 # to show that this code is being run @classmethod def timezone(cls, value): hours = int(value) + 1 return datetime.timezone(datetime.timedelta(hours=hours)) # We need pytz in the sys.modules dict monkeypatch.setitem(sys.modules, "pytz", True) # Set up references to pytz and dateutil so we know these aren't being used # If they're called, the widget would try to run None(self.timezone) which # would raise an exception clock.pytz = FakePytz clock.dateutil = FakeDateutilTZ # Pytz timezone must be a string clk4 = clock.Clock(timezone="1") fakebar = FakeBar([clk4], window=fake_window) clk4._configure(fake_qtile, fakebar) text = clk4.poll() # Default time is 10:20 and we add 1 hour for the timezone plus and extra # 1 for the pytz function assert text == "12:20" @pytest.mark.usefixtures("patched_clock") def test_clock_dateutil_timezone(fake_qtile, monkeypatch, fake_window): """test clock with dateutil timezone""" class FakeDateutilTZ: class TZ: @classmethod def gettz(cls, val): hours = int(val) + 2 return datetime.timezone(datetime.timedelta(hours=hours)) tz = TZ # pytz must not be in the sys.modules dict... monkeypatch.delitem(sys.modules, "pytz") # ...but dateutil must be monkeypatch.setitem(sys.modules, "dateutil", True) # Set up references to pytz and dateutil so we know these aren't being used # If they're called, the widget would try to run None(self.timezone) which # would raise an exception clock.pytz = None clock.dateutil = FakeDateutilTZ # Pytz timezone must be a string clk5 = clock.Clock(timezone="1") fakebar = FakeBar([clk5], window=fake_window) clk5._configure(fake_qtile, fakebar) text = clk5.poll() # Default time is 10:20 and we add 1 hour for the timezone plus and extra # 1 for the pytz function assert text == "13:20" @pytest.mark.usefixtures("patched_clock") def test_clock_tick(manager_nospawn, minimal_conf_noscreen, monkeypatch): """Test clock ticks""" class FakeDateutilTZ: class TZ: @classmethod def gettz(cls, val): return int(val) + 2 tz = TZ class TickingDateTime(datetime.datetime): offset = 0 @classmethod def now(cls, *args, **kwargs): return cls(2021, 1, 1, 10, 20, 30) # This will return 10:20 on first call and 10:21 on all # subsequent calls def astimezone(self, tzone=None): extra = datetime.timedelta(minutes=TickingDateTime.offset) if TickingDateTime.offset < 1: TickingDateTime.offset += 1 if tzone is None: return self + extra return self + datetime.timedelta(hours=tzone) + extra # pytz must not be in the sys.modules dict... monkeypatch.delitem(sys.modules, "pytz") # ...but dateutil must be monkeypatch.setitem(sys.modules, "dateutil", True) # Override datetime monkeypatch.setattr("libqtile.widget.clock.datetime", TickingDateTime) # Set up references to pytz and dateutil so we know these aren't being used # If they're called, the widget would try to run None(self.timezone) which # would raise an exception clock.pytz = None clock.dateutil = FakeDateutilTZ # set a long update interval as we'll tick manually clk6 = clock.Clock(update_interval=100) config = minimal_conf_noscreen config.screens = [libqtile.config.Screen(top=libqtile.bar.Bar([clk6], 10))] manager_nospawn.start(config) topbar = manager_nospawn.c.bar["top"] manager_nospawn.c.widget["clock"].eval("self.tick()") assert topbar.info()["widgets"][0]["text"] == "10:21" @pytest.mark.usefixtures("patched_clock") def test_clock_change_timezones(fake_qtile, monkeypatch, fake_window): """test commands to change timezones""" tz1 = datetime.timezone(datetime.timedelta(hours=1)) tz2 = datetime.timezone(-datetime.timedelta(hours=1)) # Pytz timezone must be a string clk4 = clock.Clock(timezone=tz1) fakebar = FakeBar([clk4], window=fake_window) clk4._configure(fake_qtile, fakebar) text = clk4.poll() assert text == "11:20" clk4.update_timezone(tz2) text = clk4.poll() assert text == "09:20" clk4.use_system_timezone() text = clk4.poll() assert text == "10:20" ����������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/���������������������������������������������������������0000775�0001750�0001750�00000000000�14762660347�020633� 5����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_cmus.py�����������������������������������������������0000664�0001750�0001750�00000003434�14762660347�022665� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import subprocess import pytest import libqtile.widget.cmus from test.widgets.test_cmus import MockCmusRemoteProcess @pytest.fixture def widget(monkeypatch): MockCmusRemoteProcess.reset() monkeypatch.setattr("libqtile.widget.cmus.subprocess", MockCmusRemoteProcess) monkeypatch.setattr( "libqtile.widget.cmus.subprocess.CalledProcessError", subprocess.CalledProcessError, ) monkeypatch.setattr( "libqtile.widget.cmus.base.ThreadPoolText.call_process", MockCmusRemoteProcess.call_process, ) yield libqtile.widget.cmus.Cmus @pytest.mark.parametrize("screenshot_manager", [{}], indirect=True) def ss_cmus(screenshot_manager): screenshot_manager.take_screenshot() ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_battery.py��������������������������������������������0000664�0001750�0001750�00000003464�14762660347�023373� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest import libqtile.widget import libqtile.widget.battery from libqtile.widget.battery import BatteryState, BatteryStatus from test.widgets.test_battery import dummy_load_battery @pytest.fixture def widget(monkeypatch): loaded_bat = BatteryStatus( state=BatteryState.DISCHARGING, percent=0.5, power=15.0, time=1729, charge_start_threshold=0, charge_end_threshold=100, ) monkeypatch.setattr("libqtile.widget.battery.load_battery", dummy_load_battery(loaded_bat)) yield libqtile.widget.battery.Battery @pytest.mark.parametrize( "screenshot_manager", [ {}, ], indirect=True, ) def ss_battery(screenshot_manager): screenshot_manager.take_screenshot() ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_quickexit.py������������������������������������������0000664�0001750�0001750�00000002753�14762660347�023727� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest from libqtile.widget import QuickExit @pytest.fixture def widget(): yield QuickExit @pytest.mark.parametrize( "screenshot_manager", [{}, {"default_text": "[X]", "countdown_format": "[{}]"}], indirect=True ) def ss_quickexit(screenshot_manager): screenshot_manager.take_screenshot() screenshot_manager.c.bar["top"].fake_button_press(0, 0, button=1) screenshot_manager.take_screenshot() ���������������������qtile-0.31.0/test/widgets/docs_screenshots/conftest.py����������������������������������������������0000664�0001750�0001750�00000017201�14762660347�023033� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import json import os import shutil from functools import partial from pathlib import Path import cairocffi import pytest from libqtile.bar import Bar from libqtile.command.base import expose_command from libqtile.config import Group, Screen @pytest.fixture(scope="function") def vertical(request): yield getattr(request, "param", False) vertical_bar = pytest.mark.parametrize("vertical", [True], indirect=True) @pytest.fixture(scope="session") def target(): folder = Path(__file__).parent / "screenshots" docs_folder = ( Path(__file__).parent / ".." / ".." / ".." / "docs" / "_static" / "screenshots" / "widgets" ) log = os.path.join(docs_folder, "shots.json") if folder.is_dir(): shutil.rmtree(folder) folder.mkdir() key = {} def get_file_name(w_name, config): nonlocal key # Convert config into a string of key=value entry = ", ".join(f"{k}={repr(v)}" for k, v in config.items()) # Check if widget is in the key dict if w_name not in key: key[w_name] = {} # Increment the index number indexes = [int(x) for x in key[w_name]] index = max(indexes) + 1 if indexes else 1 # Record the config key[w_name][index] = entry # Define the target folder and check it exists shots_dir = os.path.join(folder, w_name) if not os.path.isdir(shots_dir): os.mkdir(shots_dir) # Returnt the path for the screenshot return os.path.join(shots_dir, f"{index}.png") yield get_file_name # We copy the screenshots from the test folder to the docs folder at the end # This prevents pytest deleting the files itself # Remove old screenshots if os.path.isdir(docs_folder): shutil.rmtree(docs_folder) # Copy to the docs folder shutil.copytree(folder, docs_folder) with open(log, "w") as f: json.dump(key, f) # Clear up the tests folder shutil.rmtree(folder) @pytest.fixture def screenshot_manager(widget, request, manager_nospawn, minimal_conf_noscreen, target, vertical): """ Create a manager instance for the screenshots. Individual "tests" should only call `screenshot_manager.take_screenshot()` but the destination path is also available in `screenshot_manager.target`. Widgets should create their own `widget` fixture in the relevant file (applying monkeypatching etc as necessary). Configs can then be passed by parametrizing "screenshot_manager". """ # Partials are used to hide some aspects of the config from being displayed in the # docs. We need to split these out into their constituent parts. if type(widget) is partial: widget_class = widget.func widget_config = widget.keywords else: widget_class = widget widget_config = {} class ScreenshotWidget(widget_class): def __init__(self, *args, **kwargs): widget_class.__init__(self, *args, **kwargs) # We need the widget's name to be the name of the inherited class self.name = widget_class.__name__.lower() def _configure(self, bar, screen): widget_class._configure(self, bar, screen) # By setting `has_mirrors` to True, the drawer will keep a copy of the latest # contents in a separate RecordingSurface which we can access for our screenshots. self.drawer.has_mirrors = True @expose_command() def take_screenshot(self, target): if not self.configured: return source = self.drawer.last_surface dest = cairocffi.ImageSurface(cairocffi.FORMAT_ARGB32, self.width, self.height) with cairocffi.Context(dest) as ctx: ctx.set_source_surface(source) ctx.paint() dest.write_to_png(target) class ScreenshotBar(Bar): def _configure(self, qtile, screen, **kwargs): Bar._configure(self, qtile, screen, **kwargs) # By setting `has_mirrors` to True, the drawer will keep a copy of the latest # contents in a separate RecordingSurface which we can access for our screenshots. self.drawer.has_mirrors = True @expose_command() def take_screenshot(self, target, x=0, y=0, width=None, height=None): """Takes a screenshot of the bar. The area can be selected.""" if not self._configured: return if width is None: width = self.drawer.width if height is None: height = self.drawer.height # Widgets aren't drawn to the bar's drawer so we first need to render them all to a single surface bar_copy = cairocffi.ImageSurface( cairocffi.FORMAT_ARGB32, self.drawer.width, self.drawer.height ) with cairocffi.Context(bar_copy) as ctx: ctx.set_source_surface(self.drawer.last_surface) ctx.paint() for i in self.widgets: ctx.set_source_surface(i.drawer.last_surface, i.offsetx, i.offsety) ctx.paint() # Then we copy the desired area to our destination surface dest = cairocffi.ImageSurface(cairocffi.FORMAT_ARGB32, width, height) with cairocffi.Context(dest) as ctx: ctx.set_source_surface(bar_copy, x=x, y=y) ctx.paint() dest.write_to_png(target) # Get the widget and config config = getattr(request, "param", dict()) wdgt = ScreenshotWidget(**{**widget_config, **config}) name = wdgt.name # Create a function to generate filename def filename(): return target(name, config) # define bars position = "left" if vertical else "top" bar1 = {position: ScreenshotBar([wdgt], 32)} bar2 = {position: ScreenshotBar([], 32)} # Add the widget to our config minimal_conf_noscreen.groups = [Group(i) for i in "123456789"] minimal_conf_noscreen.fake_screens = [ Screen(**bar1, x=0, y=0, width=300, height=300), Screen(**bar2, x=0, y=300, width=300, height=300), ] manager_nospawn.start(minimal_conf_noscreen) # Add some convenience attributes for taking screenshots manager_nospawn.target = filename ss_widget = manager_nospawn.c.widget[name] manager_nospawn.take_screenshot = lambda f=filename: ss_widget.take_screenshot(f()) yield manager_nospawn def widget_config(params): return pytest.mark.parametrize("screenshot_manager", params, indirect=True) �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_mpd2.py�����������������������������������������������0000664�0001750�0001750�00000003720�14762660347�022556� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import sys import pytest import libqtile.widget from test.widgets.test_mpd2widget import MockMPD @pytest.fixture def widget(monkeypatch): monkeypatch.setitem(sys.modules, "mpd", MockMPD("mpd")) yield libqtile.widget.Mpd2 @pytest.mark.parametrize( "screenshot_manager", [{}, {"status_format": "{play_status} {artist}/{title}"}], indirect=True, ) def ss_mpd2(screenshot_manager): screenshot_manager.take_screenshot() @pytest.mark.parametrize( "screenshot_manager", [ { "idle_format": "{play_status} {idle_message}", "idle_message": "MPD not playing", } ], indirect=True, ) def ss_mpd2_idle(screenshot_manager): widget = screenshot_manager.c.widget["mpd2"] widget.eval("self.client.force_idle()") widget.eval("self.update(self.poll())") widget.eval("self.bar.draw()") screenshot_manager.take_screenshot() ������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_mpris2.py���������������������������������������������0000664�0001750�0001750�00000004025�14762660347�023127� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest from test.widgets.test_mpris2widget import ( # noqa: F401 METADATA_PAUSED, METADATA_PLAYING, patched_module, ) @pytest.fixture def widget(monkeypatch, patched_module): # noqa: F811 patched_module.Mpris2.PLAYING = METADATA_PLAYING patched_module.Mpris2.PAUSED = METADATA_PAUSED return patched_module.Mpris2 @pytest.mark.parametrize( "screenshot_manager", [{}, {"scroll_chars": 45}, {"display_metadata": ["xesam:url"]}], indirect=True, ) def ss_mpris2(screenshot_manager): widget = screenshot_manager.c.widget["mpris2"] widget.eval("self.parse_message(*self.PLAYING.body)") screenshot_manager.take_screenshot() @pytest.mark.parametrize( "screenshot_manager", [{"stop_pause_text": "Player paused"}], indirect=True ) def ss_mpris2_paused(screenshot_manager): widget = screenshot_manager.c.widget["mpris2"] widget.eval("self.parse_message(*self.PAUSED.body)") screenshot_manager.take_screenshot() �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_currentscreen.py��������������������������������������0000664�0001750�0001750�00000002724�14762660347�024601� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest import libqtile.widget @pytest.fixture def widget(): yield libqtile.widget.CurrentScreen def ss_currentscreen(screenshot_manager): # First screenshot is active screen screenshot_manager.take_screenshot() # Change focus to second screen screenshot_manager.c.to_screen(1) # Widget now shows it's on an inactive screen screenshot_manager.take_screenshot() ��������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_wlan.py�����������������������������������������������0000664�0001750�0001750�00000003033�14762660347�022652� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2022 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import sys from importlib import reload import pytest from test.widgets.test_wlan import MockIwlib @pytest.fixture def widget(monkeypatch): monkeypatch.setitem(sys.modules, "iwlib", MockIwlib("iwlib")) from libqtile.widget import wlan reload(wlan) yield wlan.Wlan @pytest.mark.parametrize( "screenshot_manager", [{}, {"format": "{essid} {percent:2.0%}"}], indirect=True ) def ss_wlan(screenshot_manager): screenshot_manager.take_screenshot() �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_batteryicon.py����������������������������������������0000664�0001750�0001750�00000003475�14762660347�024246� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest import libqtile.widget import libqtile.widget.battery from libqtile.widget.battery import BatteryState, BatteryStatus from test.widgets.test_battery import dummy_load_battery @pytest.fixture def widget(monkeypatch): loaded_bat = BatteryStatus( state=BatteryState.DISCHARGING, percent=0.5, power=15.0, time=1729, charge_start_threshold=0, charge_end_threshold=100, ) monkeypatch.setattr("libqtile.widget.battery.load_battery", dummy_load_battery(loaded_bat)) yield libqtile.widget.battery.BatteryIcon @pytest.mark.parametrize( "screenshot_manager", [ {}, ], indirect=True, ) def ss_batteryicon(screenshot_manager): screenshot_manager.take_screenshot() ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_clock.py����������������������������������������������0000664�0001750�0001750�00000002522�14762660347�023006� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest from libqtile.widget import Clock @pytest.fixture def widget(): yield Clock @pytest.mark.parametrize("screenshot_manager", [{}, {"format": "%d/%m/%y %H:%M"}], indirect=True) def ss_clock(screenshot_manager): screenshot_manager.take_screenshot() ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_gmail_checker.py��������������������������������������0000664�0001750�0001750�00000004606�14762660347�024475� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import sys from importlib import reload import pytest from libqtile.widget import gmail_checker from test.widgets.test_gmail_checker import FakeIMAP @pytest.fixture def widget(monkeypatch): monkeypatch.setitem(sys.modules, "imaplib", FakeIMAP("imaplib")) reload(gmail_checker) yield gmail_checker.GmailChecker @pytest.mark.parametrize( "screenshot_manager", [ {"username": "qtile", "password": "qtile"}, { "username": "qtile", "password": "qtile", "display_fmt": "unseen[{0}]", "status_only_unseen": True, }, ], indirect=True, ) def ss_gmail_checker(screenshot_manager): screenshot_manager.take_screenshot() # # This test is only required because the widget is written # # inefficiently. display_fmt should use keys instead of indices. # def test_gmail_checker_only_unseen(fake_qtile, monkeypatch, fake_window): # monkeypatch.setitem(sys.modules, "imaplib", FakeIMAP("imaplib")) # reload(gmail_checker) # gmc = gmail_checker.GmailChecker( # display_fmt="unseen[{0}]", # status_only_unseen=True, # username="qtile", # password="test" # ) # fakebar = FakeBar([gmc], window=fake_window) # gmc._configure(fake_qtile, fakebar) # text = gmc.poll() # assert text == "unseen[2]" ��������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_netgraph.py�������������������������������������������0000664�0001750�0001750�00000004517�14762660347�023531� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2022 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import random import sys from importlib import reload from types import ModuleType import pytest values = [] for _ in range(100): odds = random.randint(0, 10) val = 0 if odds < 8 else random.randint(1000, 8000) values.append(val) class MockPsutil(ModuleType): up = 0 down = 0 @classmethod def net_io_counters(cls, pernic=False, _nowrap=True): class IOCounters: def __init__(self): self.bytes_sent = 100 self.bytes_recv = 1034 if pernic: return {"wlp58s0": IOCounters(), "lo": IOCounters()} return IOCounters() @pytest.fixture def widget(monkeypatch): monkeypatch.setitem(sys.modules, "psutil", MockPsutil("psutil")) from libqtile.widget import graph reload(graph) yield graph.NetGraph @pytest.mark.parametrize( "screenshot_manager", [ {}, {"type": "box"}, {"type": "line"}, {"type": "line", "line_width": 1}, {"start_pos": "top"}, ], indirect=True, ) def ss_netgraph(screenshot_manager): widget = screenshot_manager.c.widget["netgraph"] widget.eval(f"self.values={values}") widget.eval(f"self.maxvalue={max(values)}") widget.eval("self.draw()") screenshot_manager.take_screenshot() ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_load.py�����������������������������������������������0000664�0001750�0001750�00000003033�14762660347�022630� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2022 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import sys from importlib import reload import pytest from test.widgets.test_load import MockPsutil @pytest.fixture def widget(monkeypatch): monkeypatch.setitem(sys.modules, "psutil", MockPsutil("psutil")) from libqtile.widget import load reload(load) yield load.Load @pytest.mark.parametrize( "screenshot_manager", [{}, {"format": "{time}: {load:.1f}"}], indirect=True ) def ss_load(screenshot_manager): screenshot_manager.take_screenshot() �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_caps_lock_indicator.py��������������������������������0000664�0001750�0001750�00000003704�14762660347�025710� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import subprocess import pytest from libqtile.widget import CapsNumLockIndicator from test.widgets.test_caps_num_lock_indicator import MockCapsNumLockIndicator @pytest.fixture def widget(monkeypatch): MockCapsNumLockIndicator.reset() monkeypatch.setattr( "libqtile.widget.caps_num_lock_indicator.subprocess", MockCapsNumLockIndicator ) monkeypatch.setattr( "libqtile.widget.caps_num_lock_indicator.subprocess.CalledProcessError", subprocess.CalledProcessError, ) monkeypatch.setattr( "libqtile.widget.caps_num_lock_indicator.base.ThreadPoolText.call_process", MockCapsNumLockIndicator.call_process, ) return CapsNumLockIndicator @pytest.mark.parametrize( "screenshot_manager", [ {}, ], indirect=True, ) def ss_caps_num_lock_indicator(screenshot_manager): screenshot_manager.take_screenshot() ������������������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_screensplit.py����������������������������������������0000664�0001750�0001750�00000003550�14762660347�024250� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2022 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest import libqtile.config import libqtile.confreader import libqtile.layout import libqtile.resources.default_config from libqtile.widget import ScreenSplit @pytest.fixture def widget(): yield ScreenSplit # We need to override default minimal_conf so we can force the layout @pytest.fixture(scope="function") def minimal_conf_noscreen(): class MinimalConf(libqtile.confreader.Config): auto_fullscreen = False keys = [] mouse = [] groups = [libqtile.config.Group("a"), libqtile.config.Group("b")] layouts = [libqtile.layout.ScreenSplit()] floating_layout = libqtile.resources.default_config.floating_layout screens = [] return MinimalConf def ss_screensplit(screenshot_manager): screenshot_manager.take_screenshot() ��������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_cpu.py������������������������������������������������0000664�0001750�0001750�00000002651�14762660347�022505� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import sys from importlib import reload import pytest from test.widgets.test_cpu import MockPsutil @pytest.fixture def widget(monkeypatch): monkeypatch.setitem(sys.modules, "psutil", MockPsutil("psutil")) from libqtile.widget import cpu reload(cpu) yield cpu.CPU def ss_cpu(screenshot_manager): screenshot_manager.take_screenshot() ���������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_textbox.py��������������������������������������������0000664�0001750�0001750�00000002615�14762660347�023413� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from functools import partial import pytest from libqtile.widget import TextBox @pytest.fixture def widget(): yield partial(TextBox, "Testing Text Box") @pytest.mark.parametrize("screenshot_manager", [{}, {"foreground": "2980b9"}], indirect=True) def ss_text(screenshot_manager): screenshot_manager.take_screenshot() �������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_imapwidget.py�����������������������������������������0000664�0001750�0001750�00000003367�14762660347�024055� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import sys from importlib import reload import pytest from test.widgets.test_imapwidget import FakeIMAP, FakeKeyring @pytest.fixture def widget(monkeypatch): monkeypatch.delitem(sys.modules, "imaplib", raising=False) monkeypatch.delitem(sys.modules, "keyring", raising=False) monkeypatch.setitem(sys.modules, "imaplib", FakeIMAP("imaplib")) monkeypatch.setitem(sys.modules, "keyring", FakeKeyring("keyring")) from libqtile.widget import imapwidget reload(imapwidget) yield imapwidget.ImapWidget @pytest.mark.parametrize("screenshot_manager", [{"user": "qtile"}], indirect=True) def ss_imapwidget(screenshot_manager): screenshot_manager.take_screenshot() �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_window_count.py���������������������������������������0000664�0001750�0001750�00000002545�14762660347�024437� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2022 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest from libqtile.widget import window_count @pytest.fixture def widget(): yield window_count.WindowCount def ss_window_count(screenshot_manager): screenshot_manager.test_window("One") screenshot_manager.test_window("Two") screenshot_manager.take_screenshot() �����������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_hddgraph.py�������������������������������������������0000664�0001750�0001750�00000003547�14762660347�023504� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2022 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import sys from importlib import reload from types import ModuleType import pytest values = [100] * 100 class MockPsutil(ModuleType): pass @pytest.fixture def widget(monkeypatch): monkeypatch.setitem(sys.modules, "psutil", MockPsutil("psutil")) from libqtile.widget import graph reload(graph) yield graph.HDDGraph @pytest.mark.parametrize( "screenshot_manager", [ {}, {"type": "box"}, {"type": "line"}, {"type": "line", "line_width": 1}, {"start_pos": "top"}, ], indirect=True, ) def ss_hddgraph(screenshot_manager): widget = screenshot_manager.c.widget["hddgraph"] widget.eval(f"self.values={values}") widget.eval("self.maxvalue=400") widget.eval("self.draw()") screenshot_manager.take_screenshot() ���������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_moc.py������������������������������������������������0000664�0001750�0001750�00000003025�14762660347�022470� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest from libqtile.widget import moc from test.widgets.test_moc import MockMocpProcess @pytest.fixture def widget(fake_qtile, monkeypatch, fake_window): # mocwidget = moc.Moc MockMocpProcess.reset() monkeypatch.setattr(moc.Moc, "call_process", MockMocpProcess.run) monkeypatch.setattr("libqtile.widget.moc.subprocess.Popen", MockMocpProcess.run) yield moc.Moc def ss_moc(screenshot_manager): screenshot_manager.take_screenshot() �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_widgetbox.py������������������������������������������0000664�0001750�0001750�00000005022�14762660347�023705� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest import libqtile.widget @pytest.fixture def widget(monkeypatch): # We create a wrapper for the WidgetBox which makes sure all widgets # inside the box have "has_mirrors" set to True as this keeps a copy of # the contents in the drawer which we can use for the screenshot. class WidgetBox(libqtile.widget.WidgetBox): def _configure(self, bar, screen): libqtile.widget.WidgetBox._configure(self, bar, screen) for w in self.widgets: w.drawer.has_mirrors = True yield WidgetBox @pytest.mark.parametrize( "screenshot_manager", [ {"widgets": [libqtile.widget.TextBox("Widget inside box.")]}, ], indirect=True, ) def ss_widgetbox(screenshot_manager): bar = screenshot_manager.c.bar["top"] # We can't just take a picture of the widget. We also need the area of the bar # that is revealed when the box is open. # As there are no other widgets here, we can just add up the length of all widgets. def bar_width(): info = bar.info() widgets = info["widgets"] if not widgets: return 0 return sum(x["length"] for x in widgets) def take_screenshot(): target = screenshot_manager.target() bar.take_screenshot(target, width=bar_width()) # Box is closed to start with take_screenshot() # Open the box to show contents screenshot_manager.c.widget["widgetbox"].toggle() take_screenshot() ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_openweather.py����������������������������������������0000664�0001750�0001750�00000003513�14762660347�024235� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest import libqtile.widget.open_weather from test.widgets.test_openweather import mock_fetch @pytest.fixture def widget(monkeypatch): monkeypatch.setattr("libqtile.widget.generic_poll_text.GenPollUrl.fetch", mock_fetch) yield libqtile.widget.open_weather.OpenWeather @pytest.mark.parametrize( "screenshot_manager", [ {"location": "London"}, {"location": "London", "format": "{location_city}: {sunrise} {sunset}"}, { "location": "London", "format": "{location_city}: {wind_speed} {wind_deg} {wind_direction}", }, {"location": "London", "format": "{location_city}: {icon}"}, ], indirect=True, ) def ss_openweather(screenshot_manager): screenshot_manager.take_screenshot() �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_vertical_clock.py�������������������������������������0000664�0001750�0001750�00000002714�14762660347�024702� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2024 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest from libqtile.widget import VerticalClock from test.widgets.docs_screenshots.conftest import vertical_bar, widget_config @pytest.fixture def widget(): yield VerticalClock @vertical_bar @widget_config( [{}, dict(format=["%H", "%M", "", "%d", "%m", "%y"], fontsize=[12, 12, 10, 10, 10, 10])] ) def ss_clock(screenshot_manager): screenshot_manager.take_screenshot() ����������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_nvidia_sensors.py�������������������������������������0000664�0001750�0001750�00000003211�14762660347�024735� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest from libqtile.widget import nvidia_sensors from test.widgets.test_nvidia_sensors import MockNvidiaSMI @pytest.fixture def widget(monkeypatch): monkeypatch.setattr(MockNvidiaSMI, "temperature", "65") monkeypatch.setattr( nvidia_sensors.NvidiaSensors, "call_process", MockNvidiaSMI.get_temperature ) yield nvidia_sensors.NvidiaSensors @pytest.mark.parametrize( "screenshot_manager", [{}, {"threshold": 60, "foreground_alert": "ff6000"}], indirect=True ) def ss_nvidia_sensors(screenshot_manager): screenshot_manager.take_screenshot() ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_thermal_zone.py���������������������������������������0000664�0001750�0001750�00000003213�14762660347�024400� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2022 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import os import tempfile from functools import partial import pytest from libqtile.widget import thermal_zone @pytest.fixture def widget(): with tempfile.TemporaryDirectory() as zone_dir: zone_file = os.path.join(zone_dir, "temp") with open(zone_file, "w") as zone: zone.write("49000") yield partial(thermal_zone.ThermalZone, zone=zone_file) @pytest.mark.parametrize( "screenshot_manager", [{}, {"high": 45}, {"high": 40, "crit": 45}], indirect=True ) def ss_thermal_zone(screenshot_manager): screenshot_manager.take_screenshot() �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_keyboardkbdd.py���������������������������������������0000664�0001750�0001750�00000003355�14762660347�024345� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from importlib import reload import pytest from test.widgets.test_keyboardkbdd import MockSpawn, mock_signal_receiver @pytest.fixture def widget(monkeypatch): from libqtile.widget import keyboardkbdd reload(keyboardkbdd) monkeypatch.setattr( "libqtile.widget.keyboardkbdd.KeyboardKbdd.call_process", MockSpawn.call_process ) monkeypatch.setattr("libqtile.widget.keyboardkbdd.add_signal_receiver", mock_signal_receiver) return keyboardkbdd.KeyboardKbdd @pytest.mark.parametrize( "screenshot_manager", [{"configured_keyboards": ["gb", "us"]}], indirect=True ) def ss_keyboardkbdd(screenshot_manager): screenshot_manager.take_screenshot() �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_spacer.py���������������������������������������������0000664�0001750�0001750�00000002555�14762660347�023176� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest from libqtile.widget import Spacer @pytest.fixture def widget(): yield Spacer @pytest.mark.parametrize( "screenshot_manager", [ {}, {"length": 50}, ], indirect=True, ) def ss_spacer(screenshot_manager): screenshot_manager.take_screenshot() ���������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_image.py����������������������������������������������0000664�0001750�0001750�00000003173�14762660347�023000� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from os import path import pytest from libqtile.widget import Image TEST_DIR = path.dirname(path.abspath(__file__)) DATA_DIR = path.join(TEST_DIR, "..", "..", "scripts") IMAGE_FILE = path.join(DATA_DIR, "qtile-logo-blue.svg") @pytest.fixture def widget(): yield Image @pytest.mark.parametrize( "screenshot_manager", [ {"filename": IMAGE_FILE}, {"filename": IMAGE_FILE, "margin": 5}, {"filename": IMAGE_FILE, "rotate": 45}, ], indirect=True, ) def ss_image(screenshot_manager): screenshot_manager.take_screenshot() �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_df.py�������������������������������������������������0000664�0001750�0001750�00000002740�14762660347�022306� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest import libqtile.widget from test.widgets.test_df import FakeOS @pytest.fixture def widget(monkeypatch): monkeypatch.setattr("libqtile.widget.df.os", FakeOS("os")) yield libqtile.widget.DF @pytest.mark.parametrize( "screenshot_manager", [{"warn_space": 40}, {"visible_on_warn": False}], indirect=True, ) def ss_df(screenshot_manager): screenshot_manager.take_screenshot() ��������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_chord.py����������������������������������������������0000664�0001750�0001750�00000002700�14762660347�023010� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest from libqtile.widget import Chord @pytest.fixture def widget(): yield Chord @pytest.mark.parametrize( "screenshot_manager", [{}, {"chords_colors": {"vim mode": ("2980b9", "ffffff")}}], indirect=True, ) def ss_chord(screenshot_manager): screenshot_manager.c.eval("hook.fire('enter_chord', 'vim mode')") screenshot_manager.take_screenshot() ����������������������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_volume.py���������������������������������������������0000664�0001750�0001750�00000003731�14762660347�023225� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2022 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import os import shutil import pytest from libqtile.widget import Volume from test.widgets.conftest import DATA_DIR ICON = os.path.join(DATA_DIR, "svg", "audio-volume-muted.svg") TEMP_DIR = os.path.join(DATA_DIR, "ss_temp") @pytest.fixture def widget(): os.mkdir(TEMP_DIR) for i in ( "audio-volume-high.svg", "audio-volume-low.svg", "audio-volume-medium.svg", "audio-volume-muted.svg", ): shutil.copy(ICON, os.path.join(TEMP_DIR, i)) yield Volume shutil.rmtree(TEMP_DIR) @pytest.mark.parametrize( "screenshot_manager", [{"theme_path": TEMP_DIR}, {"emoji": True}, {"fmt": "Vol: {}"}], indirect=True, ) def ss_volume(screenshot_manager): widget = screenshot_manager.c.widget["volume"] widget.eval("self.volume=-1") widget.eval("self._update_drawer()") widget.eval("self.bar.draw()") screenshot_manager.take_screenshot() ���������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_groupbox.py�������������������������������������������0000664�0001750�0001750�00000003045�14762660347�023561� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest from libqtile.widget import GroupBox @pytest.fixture def widget(): yield GroupBox @pytest.mark.parametrize( "screenshot_manager", [ {}, {"highlight_method": "block"}, {"highlight_method": "text"}, {"highlight_method": "line"}, {"visible_groups": ["1", "5", "6"]}, ], indirect=True, ) def ss_groupbox(screenshot_manager): screenshot_manager.test_window("One") screenshot_manager.take_screenshot() �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_net.py������������������������������������������������0000664�0001750�0001750�00000003516�14762660347�022505� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import sys from importlib import reload import pytest from test.widgets.test_net import MockPsutil @pytest.fixture def widget(monkeypatch): monkeypatch.setitem(sys.modules, "psutil", MockPsutil("psutil")) from libqtile.widget import net reload(net) yield net.Net @pytest.mark.parametrize( "screenshot_manager", [ {}, {"format": "{interface}: U {up} D {down} T {total}"}, { "format": "{interface}: U {up}{up_suffix} D {down}{down_suffix} T {total}{total_suffix}" }, {"format": "{down:.0f}{down_suffix} \u2193\u2191 {up:.0f}{up_suffix}"}, {"interface": "wlp58s0"}, {"prefix": "M"}, ], indirect=True, ) def ss_net(screenshot_manager): screenshot_manager.take_screenshot() ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_notify.py���������������������������������������������0000664�0001750�0001750�00000003274�14762660347�023230� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import shutil import subprocess import pytest from libqtile.widget import Notify from test.widgets.test_notify import NS, notification _, NOTIFICATION = notification("Notification", "Message body.") @pytest.fixture def widget(): yield Notify @pytest.mark.parametrize( "screenshot_manager", [ {}, ], indirect=True, ) @pytest.mark.skipif(shutil.which("notify-send") is None, reason="notify-send not installed.") @pytest.mark.usefixtures("dbus") def ss_notify(screenshot_manager): notif_1 = [NS] notif_1.extend(NOTIFICATION) subprocess.run(notif_1) screenshot_manager.take_screenshot() ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_countdown.py������������������������������������������0000664�0001750�0001750�00000002677�14762660347�023746� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from datetime import datetime, timedelta import pytest import libqtile.widget td = timedelta(days=1, hours=2, minutes=34, seconds=56) @pytest.fixture def widget(): yield libqtile.widget.Countdown @pytest.mark.parametrize("screenshot_manager", [{"date": datetime.now() + td}], indirect=True) def ss_countdown(screenshot_manager): screenshot_manager.take_screenshot() �����������������������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_genpollurl.py�����������������������������������������0000664�0001750�0001750�00000003301�14762660347�024072� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest import libqtile.widget from test.widgets.test_generic_poll_text import MockRequest, Mockurlopen @pytest.fixture def widget(monkeypatch): MockRequest.return_value = b"Text from URL" monkeypatch.setattr("libqtile.widget.generic_poll_text.Request", MockRequest) monkeypatch.setattr("libqtile.widget.generic_poll_text.urlopen", Mockurlopen) yield libqtile.widget.GenPollUrl @pytest.mark.parametrize( "screenshot_manager", [{}, {"url": "http://test.qtile.org", "json": False, "parse": lambda x: x}], indirect=True, ) def ss_genpollurl(screenshot_manager): screenshot_manager.take_screenshot() �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_memorygraph.py����������������������������������������0000664�0001750�0001750�00000004227�14762660347�024251� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2022 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import random import sys from importlib import reload from types import ModuleType import pytest values = [] val = 2000 for _ in range(100): adjust = random.uniform(-2.0, 2.0) * 100 val += adjust values.append(val) class MockPsutil(ModuleType): @classmethod def virtual_memory(cls): class Memory: total = 8175788032 free = 2055852032 buffers = 315994112 cached = 2715344896 return Memory() @pytest.fixture def widget(monkeypatch): monkeypatch.setitem(sys.modules, "psutil", MockPsutil("psutil")) from libqtile.widget import graph reload(graph) yield graph.MemoryGraph @pytest.mark.parametrize( "screenshot_manager", [ {}, {"type": "box"}, {"type": "line"}, {"type": "line", "line_width": 1}, {"start_pos": "top"}, ], indirect=True, ) def ss_memorygraph(screenshot_manager): widget = screenshot_manager.c.widget["memorygraph"] widget.eval(f"self.values={values}") widget.eval("self.draw()") screenshot_manager.take_screenshot() �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_idlerpg.py��������������������������������������������0000664�0001750�0001750�00000003271�14762660347�023343� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest from libqtile.widget import idlerpg from test.widgets.test_idlerpg import online_response @pytest.fixture def widget(monkeypatch): def no_op(*args, **kwargs): return "" idler = idlerpg.IdleRPG idler.RESPONSE = online_response monkeypatch.setattr(idler, "fetch", no_op) yield idler @pytest.mark.parametrize( "screenshot_manager", [{"url": "http://idlerpg.qtile.org?player=elParaguayo"}], indirect=True, ) def ss_idlerpg(screenshot_manager): screenshot_manager.c.widget["idlerpg"].eval("self.update(self.parse(self.RESPONSE))") screenshot_manager.take_screenshot() ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_sep.py������������������������������������������������0000664�0001750�0001750�00000002551�14762660347�022504� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest from libqtile.widget import Sep @pytest.fixture def widget(): yield Sep @pytest.mark.parametrize( "screenshot_manager", [{}, {"padding": 10, "linewidth": 5, "size_percent": 50}], indirect=True ) def ss_sep(screenshot_manager): screenshot_manager.take_screenshot() �������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_wttr.py�����������������������������������������������0000664�0001750�0001750�00000002756�14762660347�022724� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2022 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest from libqtile.widget import wttr RESPONSE = "London: +17°C" @pytest.fixture def widget(monkeypatch): def result(self): return RESPONSE monkeypatch.setattr("libqtile.widget.wttr.Wttr.fetch", result) yield wttr.Wttr @pytest.mark.parametrize("screenshot_manager", [{"location": {"London": "Home"}}], indirect=True) def ss_wttr(screenshot_manager): screenshot_manager.take_screenshot() ������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_hdd.py������������������������������������������������0000664�0001750�0001750�00000003460�14762660347�022454� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2024 Florian G. Hechler # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import sys import tempfile from importlib import reload import pytest from test.widgets.test_hdd import MockPsutil, set_io_ticks @pytest.fixture def widget(monkeypatch): monkeypatch.setitem(sys.modules, "psutil", MockPsutil("psutil")) from libqtile.widget import hdd reload(hdd) yield hdd.HDD def ss_cpu(screenshot_manager): # Create a fake stat file temp_file = tempfile.NamedTemporaryFile(mode="w+", delete=False) widget = screenshot_manager.c.widget["hdd"] widget.eval(f"self.path = '{temp_file.name}'") set_io_ticks(temp_file, 0) widget.eval("self.update(self.poll())") set_io_ticks(temp_file, 123000) widget.eval("self.update(self.poll())") screenshot_manager.take_screenshot() ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_stock_ticker.py���������������������������������������0000664�0001750�0001750�00000005257�14762660347�024407� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2022 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest from libqtile.widget import stock_ticker RESPONSE = { "Meta Data": { "1. Information": "Intraday (1min) open, high, low, close prices and volume", "2. Symbol": "QTIL", "3. Last Refreshed": "2021-07-30 19:09:00", "4. Interval": "1min", "5. Output Size": "Compact", "6. Time Zone": "US/Eastern", }, "Time Series (1min)": { "2021-07-30 19:09:00": { "1. open": "140.9800", "2. high": "140.9800", "3. low": "140.9800", "4. close": "140.9800", "5. volume": "527", }, "2021-07-30 17:27:00": { "1. open": "141.1900", "2. high": "141.1900", "3. low": "141.1900", "4. close": "141.1900", "5. volume": "300", }, "2021-07-30 16:44:00": { "1. open": "141.0000", "2. high": "141.0000", "3. low": "141.0000", "4. close": "141.0000", "5. volume": "482", }, "2021-07-30 16:26:00": { "1. open": "141.0000", "2. high": "141.0000", "3. low": "141.0000", "4. close": "141.0000", "5. volume": "102", }, }, } @pytest.fixture def widget(monkeypatch): def result(self): return RESPONSE monkeypatch.setattr("libqtile.widget.stock_ticker.StockTicker.fetch", result) yield stock_ticker.StockTicker @pytest.mark.parametrize("screenshot_manager", [{"symbol": "QTIL"}], indirect=True) def ss_stock_ticker(screenshot_manager): screenshot_manager.take_screenshot() �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_statusnotifier.py�������������������������������������0000664�0001750�0001750�00000003207�14762660347�024777� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE import pytest from libqtile.widget import StatusNotifier from test.widgets.test_statusnotifier import wait_for_icon @pytest.fixture def widget(request, manager_nospawn): yield StatusNotifier @pytest.mark.parametrize("screenshot_manager", [{}, {"icon_size": 30}], indirect=True) @pytest.mark.usefixtures("dbus") def ss_statusnotifier(screenshot_manager): win = screenshot_manager.test_window("TestSNI", export_sni=True) wait_for_icon(screenshot_manager.c.widget["statusnotifier"], hidden=False) screenshot_manager.take_screenshot() screenshot_manager.kill_window(win) �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_checkupdates.py���������������������������������������0000664�0001750�0001750�00000003620�14762660347�024356� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest import libqtile.widget from test.widgets.test_check_updates import MockPopen, MockSpawn @pytest.fixture def widget(monkeypatch): monkeypatch.setattr("libqtile.widget.base.subprocess.check_output", MockSpawn.call_process) monkeypatch.setattr("libqtile.widget.check_updates.Popen", MockPopen) yield libqtile.widget.CheckUpdates @pytest.mark.parametrize( "screenshot_manager", [ {"no_update_string": "No updates"}, ], indirect=True, ) def ss_checkupdates(screenshot_manager): # First screenshot shows updates available screenshot_manager.take_screenshot() # Polling mocks updates being installed screenshot_manager.c.widget["checkupdates"].eval("self.update(self.poll())") # Second screenshot means there are no updates to install screenshot_manager.take_screenshot() ����������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_swapgraph.py������������������������������������������0000664�0001750�0001750�00000004112�14762660347�023704� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2022 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import random import sys from importlib import reload from types import ModuleType import pytest values = [] val = 1500 for _ in range(100): adjust = random.uniform(-2.0, 2.0) * 100 val += adjust values.append(val) class MockPsutil(ModuleType): @classmethod def swap_memory(cls): class Swap: total = 8175788032 free = 2055852032 return Swap() @pytest.fixture def widget(monkeypatch): monkeypatch.setitem(sys.modules, "psutil", MockPsutil("psutil")) from libqtile.widget import graph reload(graph) yield graph.SwapGraph @pytest.mark.parametrize( "screenshot_manager", [ {}, {"type": "box"}, {"type": "line"}, {"type": "line", "line_width": 1}, {"start_pos": "top"}, ], indirect=True, ) def ss_swapgraph(screenshot_manager): widget = screenshot_manager.c.widget["swapgraph"] widget.eval(f"self.values={values}") widget.eval("self.draw()") screenshot_manager.take_screenshot() ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_memory.py���������������������������������������������0000664�0001750�0001750�00000003202�14762660347�023217� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import sys from importlib import reload import pytest from test.widgets.test_memory import FakePsutil @pytest.fixture def widget(monkeypatch): monkeypatch.setitem(sys.modules, "psutil", FakePsutil("psutil")) from libqtile.widget import memory reload(memory) return memory.Memory @pytest.mark.parametrize( "screenshot_manager", [ {}, {"measure_mem": "G"}, {"format": "Swap: {SwapUsed: .0f}{ms}/{SwapTotal: .0f}{ms}"}, ], indirect=True, ) def ss_memory(screenshot_manager): screenshot_manager.take_screenshot() ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_bluetooth.py������������������������������������������0000664�0001750�0001750�00000003061�14762660347�023717� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2023 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest from test.widgets.test_bluetooth import fake_dbus_daemon, wait_for_text, widget # noqa: F401 @pytest.mark.parametrize( "screenshot_manager", [ {}, ], indirect=True, ) def ss_bluetooth(fake_dbus_daemon, screenshot_manager): # noqa: F811 w = screenshot_manager.c.widget["bluetooth"] wait_for_text(w, "BT Speaker") screenshot_manager.take_screenshot() for _ in range(4): w.scroll_up() screenshot_manager.take_screenshot() �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_cpugraph.py�������������������������������������������0000664�0001750�0001750�00000004136�14762660347�023527� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2022 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import random import sys from importlib import reload from types import ModuleType import pytest values = [] val = 25.0 for _ in range(100): adjust = random.uniform(-2.0, 2.0) val += adjust values.append(val) class MockPsutil(ModuleType): @classmethod def cpu_times(cls): class CPU: user = 0.5 nice = 0.5 system = 0.5 idle = 0.5 return CPU() @pytest.fixture def widget(monkeypatch): monkeypatch.setitem(sys.modules, "psutil", MockPsutil("psutil")) from libqtile.widget import graph reload(graph) yield graph.CPUGraph @pytest.mark.parametrize( "screenshot_manager", [ {}, {"type": "box"}, {"type": "line"}, {"type": "line", "line_width": 1}, {"start_pos": "top"}, ], indirect=True, ) def ss_cpugraph(screenshot_manager): widget = screenshot_manager.c.widget["cpugraph"] widget.eval(f"self.values={values}") widget.eval("self.draw()") screenshot_manager.take_screenshot() ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_windowname.py�����������������������������������������0000664�0001750�0001750�00000003172�14762660347�024065� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2022 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest from libqtile.widget import windowname @pytest.fixture def widget(): yield windowname.WindowName def ss_windowname(screenshot_manager): screenshot_manager.test_window("One") screenshot_manager.take_screenshot() screenshot_manager.c.window.toggle_maximize() screenshot_manager.take_screenshot() screenshot_manager.c.window.toggle_minimize() screenshot_manager.take_screenshot() screenshot_manager.c.window.toggle_minimize() screenshot_manager.c.window.toggle_floating() screenshot_manager.take_screenshot() ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_genpolltext.py����������������������������������������0000664�0001750�0001750�00000002603�14762660347�024260� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest import libqtile.widget @pytest.fixture def widget(): yield libqtile.widget.GenPollText @pytest.mark.parametrize( "screenshot_manager", [ {"func": lambda: "Function text."}, ], indirect=True, ) def ss_genpolltext(screenshot_manager): screenshot_manager.take_screenshot() �����������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_crypto_ticker.py��������������������������������������0000664�0001750�0001750�00000003073�14762660347�024576� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest import libqtile.widget from test.widgets.test_crypto_ticker import RESPONSE @pytest.fixture def widget(): ticker = libqtile.widget.CryptoTicker ticker.RESPONSE = RESPONSE yield ticker @pytest.mark.parametrize( "screenshot_manager", [{}, {"format": "{crypto}:{amount:,.2f}"}], indirect=True ) def ss_crypto_ticker(screenshot_manager): screenshot_manager.c.widget["cryptoticker"].eval("self.update(self.parse(self.RESPONSE))") screenshot_manager.take_screenshot() ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_pomodoro.py�������������������������������������������0000664�0001750�0001750�00000003712�14762660347�023553� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from datetime import timedelta import pytest from libqtile.widget import pomodoro from test.widgets.test_pomodoro import MockDatetime def increment_time(self, increment): MockDatetime._adjustment += timedelta(minutes=increment) @pytest.fixture def widget(monkeypatch): monkeypatch.setattr("libqtile.widget.pomodoro.datetime", MockDatetime) pomodoro.Pomodoro.adjust_time = increment_time yield pomodoro.Pomodoro def ss_pomodoro(screenshot_manager): bar = screenshot_manager.c.bar["top"] widget = screenshot_manager.c.widget["pomodoro"] # Inactive screenshot_manager.take_screenshot() bar.fake_button_press(0, 0, 3) widget.eval("self.update(self.poll())") # Active screenshot_manager.take_screenshot() widget.eval("self.adjust_time(25)") widget.eval("self.update(self.poll())") # Short break screenshot_manager.take_screenshot() ������������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_redshift.py�������������������������������������������0000664�0001750�0001750�00000003432�14762660347�023524� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2024 Saath Satheeshkumar (saths008) # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest import libqtile.widget from test.widgets.test_redshift import mock_run @pytest.fixture def widget(monkeypatch): monkeypatch.setattr("subprocess.run", mock_run) yield libqtile.widget.redshift.Redshift @pytest.mark.parametrize( "screenshot_manager", [ {}, ], indirect=True, ) def ss_redshift(screenshot_manager): def click(): screenshot_manager.c.bar["top"].fake_button_press(0, 0, 1) w = screenshot_manager.c.widget["redshift"] screenshot_manager.take_screenshot() click() # Enable so scrolling works number_of_items = 4 for _ in range(number_of_items): screenshot_manager.take_screenshot() w.scroll_up() ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_do_not_disturb.py�������������������������������������0000664�0001750�0001750�00000002561�14762660347�024734� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2024 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest from test.widgets.test_do_not_disturb import patched_dnd # noqa: F401 @pytest.fixture def widget(patched_dnd): # noqa: F811 class DoNotDisturb(patched_dnd): pass yield DoNotDisturb def ss_do_not_disturb(screenshot_manager): screenshot_manager.take_screenshot() �����������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_hddbusygraph.py���������������������������������������0000664�0001750�0001750�00000004010�14762660347�024371� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2022 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import random import sys from importlib import reload from types import ModuleType import pytest values = [] for _ in range(100): odds = random.randint(0, 10) val = 0 if odds < 6 else random.randint(100, 20000) values.append(val) class MockPsutil(ModuleType): pass @pytest.fixture def widget(monkeypatch): monkeypatch.setitem(sys.modules, "psutil", MockPsutil("psutil")) from libqtile.widget import graph reload(graph) yield graph.HDDBusyGraph @pytest.mark.parametrize( "screenshot_manager", [ {}, {"type": "box"}, {"type": "line"}, {"type": "line", "line_width": 1}, {"start_pos": "top"}, ], indirect=True, ) def ss_hddbusygraph(screenshot_manager): widget = screenshot_manager.c.widget["hddbusygraph"] widget.eval(f"self.values={values}") widget.eval(f"self.maxvalue={max(values)}") widget.eval("self.draw()") screenshot_manager.take_screenshot() ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_windowtabs.py�����������������������������������������0000664�0001750�0001750�00000002556�14762660347�024103� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2022 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest from libqtile.widget import windowtabs @pytest.fixture def widget(): yield windowtabs.WindowTabs def ss_window_count(screenshot_manager): screenshot_manager.test_window("Window One") screenshot_manager.test_window("Window Two") screenshot_manager.take_screenshot() ��������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/docs_screenshots/ss_sensors.py��������������������������������������������0000664�0001750�0001750�00000003165�14762660347�023413� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2022 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import sys from importlib import reload import pytest from test.widgets.test_sensors import MockPsutil @pytest.fixture def widget(monkeypatch): monkeypatch.setitem(sys.modules, "psutil", MockPsutil("psutil")) from libqtile.widget import sensors reload(sensors) yield sensors.ThermalSensor @pytest.mark.parametrize( "screenshot_manager", [{}, {"tag_sensor": "NVME"}, {"format": "{tag}: {temp:.0f}{unit}"}, {"threshold": 30.0}], indirect=True, ) def ss_thermal_sensor(screenshot_manager): screenshot_manager.take_screenshot() �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_image.py�������������������������������������������������������������0000664�0001750�0001750�00000007743�14762660347�017771� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # Widget specific tests from os import path import pytest import libqtile.bar import libqtile.config from libqtile import widget TEST_DIR = path.dirname(path.abspath(__file__)) DATA_DIR = path.join(TEST_DIR, "..", "data", "png") IMAGE_FILE = path.join(DATA_DIR, "audio-volume-muted.png") img = widget.Image(filename=IMAGE_FILE) parameters = [ (libqtile.config.Screen(top=libqtile.bar.Bar([img], 40)), "top", "height"), (libqtile.config.Screen(left=libqtile.bar.Bar([img], 40)), "left", "width"), ] @pytest.mark.parametrize("screen,location,attribute", parameters) def test_default_settings(manager_nospawn, minimal_conf_noscreen, screen, location, attribute): config = minimal_conf_noscreen config.screens = [screen] manager_nospawn.start(config) bar = manager_nospawn.c.bar[location] info = bar.info() for dimension in ["height", "width"]: assert info["widgets"][0][dimension] == info[attribute] no_img = widget.Image() parameters = [ (libqtile.config.Screen(top=libqtile.bar.Bar([no_img], 40)), "top", "width"), (libqtile.config.Screen(left=libqtile.bar.Bar([no_img], 40)), "left", "height"), ] @pytest.mark.parametrize("screen,location,attribute", parameters) def test_no_filename(manager_nospawn, minimal_conf_noscreen, screen, location, attribute): config = minimal_conf_noscreen config.screens = [screen] manager_nospawn.start(config) bar = manager_nospawn.c.bar[location] info = bar.info() assert info["widgets"][0][attribute] == 0 def test_missing_file(manager_nospawn, minimal_conf_noscreen): img2 = widget.Image(filename="/this/file/does/not/exist") config = minimal_conf_noscreen config.screens = [libqtile.config.Screen(top=libqtile.bar.Bar([img2], 40))] manager_nospawn.start(config) bar = manager_nospawn.c.bar["top"] info = bar.info() assert info["widgets"][0]["width"] == 0 def test_no_scale(manager_nospawn, minimal_conf_noscreen): img2 = widget.Image(filename=IMAGE_FILE, scale=False) config = minimal_conf_noscreen config.screens = [libqtile.config.Screen(top=libqtile.bar.Bar([img2], 40))] manager_nospawn.start(config) bar = manager_nospawn.c.bar["top"] info = bar.info() assert info["widgets"][0]["width"] == 24 def test_no_image(manager_nospawn, minimal_conf_noscreen): img = widget.Image() config = minimal_conf_noscreen config.screens = [libqtile.config.Screen(top=libqtile.bar.Bar([img], 40))] manager_nospawn.start(config) assert "Image filename not set!" in manager_nospawn.get_log_buffer() def test_invalid_path(manager_nospawn, minimal_conf_noscreen): filename = "/made/up/file.png" img = widget.Image(filename=filename) config = minimal_conf_noscreen config.screens = [libqtile.config.Screen(top=libqtile.bar.Bar([img], 40))] manager_nospawn.start(config) assert f"Image does not exist: {filename}" in manager_nospawn.get_log_buffer() �����������������������������qtile-0.31.0/test/widgets/test_countdown.py���������������������������������������������������������0000664�0001750�0001750�00000003005�14762660347�020712� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # Widget specific tests from datetime import datetime, timedelta from libqtile import widget td = timedelta(days=10, hours=10, minutes=10, seconds=10) def test_countdown_formatting(): # Create widget but hide seconds from formatting to allow for # timing differences in test environment countdown = widget.Countdown(date=datetime.now() + td, format="{D}d {H}h {M}m") output = countdown.poll() assert output == "10d 10h 10m" ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_stock_ticker.py������������������������������������������������������0000664�0001750�0001750�00000005307�14762660347�021365� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from libqtile import widget RESPONSE = { "Meta Data": { "1. Information": "Intraday (1min) open, high, low, close prices and volume", "2. Symbol": "QTIL", "3. Last Refreshed": "2021-07-30 19:09:00", "4. Interval": "1min", "5. Output Size": "Compact", "6. Time Zone": "US/Eastern", }, "Time Series (1min)": { "2021-07-30 19:09:00": { "1. open": "140.9800", "2. high": "140.9800", "3. low": "140.9800", "4. close": "140.9800", "5. volume": "527", }, "2021-07-30 17:27:00": { "1. open": "141.1900", "2. high": "141.1900", "3. low": "141.1900", "4. close": "141.1900", "5. volume": "300", }, "2021-07-30 16:44:00": { "1. open": "141.0000", "2. high": "141.0000", "3. low": "141.0000", "4. close": "141.0000", "5. volume": "482", }, "2021-07-30 16:26:00": { "1. open": "141.0000", "2. high": "141.0000", "3. low": "141.0000", "4. close": "141.0000", "5. volume": "102", }, }, } def test_stock_ticker_methods(): ticker = widget.StockTicker(symbol="QTIL") assert ticker.url == ( "https://www.alphavantage.co/query?interval=1min&outputsize=compact&" "function=TIME_SERIES_INTRADAY&symbol=QTIL" ) # We don't know what locale is on the testing system but we can just use # whatever the widget is using. assert ticker.parse(RESPONSE) == f"QTIL: {ticker.sign}140.98" �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_tuned_manager.py�����������������������������������������������������0000664�0001750�0001750�00000005347�14762660347�021516� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2025 Emma Nora Theuer # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # Widget specific tests from unittest.mock import MagicMock, patch from libqtile.widget.tuned_manager import TunedManager def test_find_mode(): # Mocking subprocess.run to return a specific output with patch("subprocess.run") as mock_run: mock_run.return_value.stdout = "Current active profile: balanced-battery\n" widget = TunedManager() mode = widget.find_mode() assert mode == "balanced-battery" assert mock_run.call_count == 2 # Called during init and explicitly def test_update_bar(): with ( patch("subprocess.run") as mock_run, patch.object(TunedManager, "bar", create=True) as mock_bar, ): mock_run.return_value.stdout = "Current active profile: powersave\n" mock_bar.draw = MagicMock() widget = TunedManager() widget.update_bar() assert widget.current_mode == "powersave" assert widget.text == "powersave" mock_bar.draw.assert_called_once() def test_next_mode(): with patch.object(TunedManager, "execute_command") as mock_execute_command: widget = TunedManager() widget.modes = ["powersave", "balanced-battery", "throughput-performance"] widget.current_mode = "powersave" widget.next_mode() mock_execute_command.assert_called_once_with(1) def test_previous_mode(): with patch.object(TunedManager, "execute_command") as mock_execute_command: widget = TunedManager() widget.modes = ["powersave", "balanced-battery", "throughput-performance"] widget.current_mode = "balanced-battery" widget.previous_mode() mock_execute_command.assert_called_once_with(0) �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_thermal_zone.py������������������������������������������������������0000664�0001750�0001750�00000001035�14762660347�021362� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������import os from libqtile import widget def test_thermal_zone_getting_value(): # Create temporary zone file tmp = "/var/tmp/qtile/test/widgets/thermal_zone" zone_file = tmp + "/sys/class/thermal/thermal_zone0/temp" os.makedirs(os.path.dirname(zone_file), exist_ok=True) class FakeLayout: pass with open(zone_file, "w") as f: f.write("22000") thermal_zone = widget.ThermalZone(zone=zone_file) thermal_zone.layout = FakeLayout() output = thermal_zone.poll() assert output == "22°C" ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_spacer.py������������������������������������������������������������0000664�0001750�0001750�00000004575�14762660347�020164� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # Widget specific tests import pytest import libqtile.bar import libqtile.config import libqtile.confreader import libqtile.layout from libqtile import widget space = widget.Spacer() parameters = [ (libqtile.config.Screen(top=libqtile.bar.Bar([space], 10)), "top", "width"), (libqtile.config.Screen(left=libqtile.bar.Bar([space], 10)), "left", "height"), ] @pytest.mark.parametrize("screen,location,attribute", parameters) def test_stretch(manager_nospawn, minimal_conf_noscreen, screen, location, attribute): config = minimal_conf_noscreen config.screens = [screen] manager_nospawn.start(config) bar = manager_nospawn.c.bar[location] info = bar.info() assert info["widgets"][0][attribute] == info[attribute] space = widget.Spacer(length=100) parameters = [ (libqtile.config.Screen(top=libqtile.bar.Bar([space], 10)), "top", "width"), (libqtile.config.Screen(left=libqtile.bar.Bar([space], 10)), "left", "height"), ] @pytest.mark.parametrize("screen,location,attribute", parameters) def test_fixed_size(manager_nospawn, minimal_conf_noscreen, screen, location, attribute): config = minimal_conf_noscreen config.screens = [screen] manager_nospawn.start(config) bar = manager_nospawn.c.bar[location] info = bar.info() assert info["widgets"][0][attribute] == 100 �����������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_clipboard.py���������������������������������������������������������0000664�0001750�0001750�00000013326�14762660347�020640� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import textwrap import pytest import libqtile.config import libqtile.widget.bluetooth from libqtile.bar import Bar from libqtile.config import Screen from test.helpers import Retry @Retry(ignore_exceptions=(AssertionError,)) def clipboard_cleared(widget): assert widget.info()["text"] == "" @pytest.fixture def clipboard_manager(request, minimal_conf_noscreen, manager_nospawn): widget = libqtile.widget.Clipboard(**getattr(request, "param", dict())) config = minimal_conf_noscreen config.screens = [Screen(top=Bar([widget], 10))] manager_nospawn.start(config) if manager_nospawn.backend.name != "x11": pytest.skip("Test only available on X11.") yield manager_nospawn def test_clipboard_display(clipboard_manager): widget = clipboard_manager.c.widget["clipboard"] assert widget.info()["text"] == "" fake_hook = textwrap.dedent( """ from libqtile import hook sel = {"owner": 12345, "selection": "Test Clipboard"} hook.fire("selection_change", "CLIPBOARD", sel) hook.fire("selection_notify", "CLIPBOARD", sel) """ ) clipboard_manager.c.eval(fake_hook) # Default setting is to limit display to 10 chars assert widget.info()["text"] == "Test Clipb..." @pytest.mark.parametrize( "clipboard_manager", [{"max_width": None, "blacklist": []}], indirect=True ) def test_clipboard_display_full_text(clipboard_manager): widget = clipboard_manager.c.widget["clipboard"] assert widget.info()["text"] == "" fake_hook = textwrap.dedent( """ from libqtile import hook sel = {"owner": 12345, "selection": "Test Clipboard"} hook.fire("selection_change", "CLIPBOARD", sel) hook.fire("selection_notify", "CLIPBOARD", sel) """ ) clipboard_manager.c.eval(fake_hook) # Widget should now show full text assert widget.info()["text"] == "Test Clipboard" @pytest.mark.parametrize("clipboard_manager", [{"blacklist": ["TestWindow"]}], indirect=True) def test_clipboard_blacklist(clipboard_manager): """Test widget hides selection from blacklisted windows.""" widget = clipboard_manager.c.widget["clipboard"] assert widget.info()["text"] == "" clipboard_manager.test_window("Blacklisted Window") windows = clipboard_manager.c.windows() window_id = [w for w in windows if w["name"] == "Blacklisted Window"][0]["id"] fake_hook = textwrap.dedent( f""" from libqtile import hook sel = {{"owner": {window_id}, "selection": "Test Clipboard"}} hook.fire("selection_change", "CLIPBOARD", sel) hook.fire("selection_notify", "CLIPBOARD", sel) """ ) clipboard_manager.c.eval(fake_hook) # Widget should now show full text assert widget.info()["text"] == "***********" def test_clipboard_ignore_different_selection(clipboard_manager): widget = clipboard_manager.c.widget["clipboard"] assert widget.info()["text"] == "" fake_hook = textwrap.dedent( """ from libqtile import hook sel = {"owner": 12345, "selection": "Test Clipboard"} hook.fire("selection_change", "PRIMARY", sel) hook.fire("selection_notify", "PRIMARY", sel) """ ) clipboard_manager.c.eval(fake_hook) assert widget.info()["text"] == "" @pytest.mark.parametrize("clipboard_manager", [{"timeout": 0.5}], indirect=True) def test_clipboard_display_clear(clipboard_manager): widget = clipboard_manager.c.widget["clipboard"] assert widget.info()["text"] == "" fake_hook = textwrap.dedent( """ from libqtile import hook sel = {"owner": 12345, "selection": "Test Clipboard"} hook.fire("selection_change", "CLIPBOARD", sel) hook.fire("selection_notify", "CLIPBOARD", sel) """ ) clipboard_manager.c.eval(fake_hook) # Default setting is to limit display to 10 chars assert widget.info()["text"] == "Test Clipb..." # Check cleared after timeout clipboard_cleared(widget) def test_clipboard_display_multiple_changes(clipboard_manager): """Just need this test to cover last lines in hook_change.""" widget = clipboard_manager.c.widget["clipboard"] assert widget.info()["text"] == "" fake_hook = textwrap.dedent( """ from libqtile import hook sel = {"owner": 12345, "selection": "Test Clipboard"} sel_b = {"owner": 12345, "selection": "Second Selection"} hook.fire("selection_change", "CLIPBOARD", sel) hook.fire("selection_change", "CLIPBOARD", sel_b) """ ) clipboard_manager.c.eval(fake_hook) # Default setting is to limit display to 10 chars assert widget.info()["text"] == "Second Sel..." ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_quickexit.py���������������������������������������������������������0000664�0001750�0001750�00000005334�14762660347�020707� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # Widget specific tests import pytest import libqtile.bar import libqtile.config import libqtile.confreader import libqtile.layout from libqtile import widget from libqtile.ipc import IPCError def test_trigger_and_cancel(manager_nospawn, minimal_conf_noscreen): # Set a long interval to allow for unanticipated delays in testing environment qewidget = widget.QuickExit(timer_interval=100) config = minimal_conf_noscreen config.screens = [libqtile.config.Screen(top=libqtile.bar.Bar([qewidget], 10))] manager_nospawn.start(config) topbar = manager_nospawn.c.bar["top"] # Default text w = topbar.info()["widgets"][0] assert w["text"] == "[ shutdown ]" # Click widget to start countdown topbar.fake_button_press(0, 0, button=1) w = topbar.info()["widgets"][0] assert w["text"] == "[ 4 seconds ]" # Click widget again to cancel countdown topbar.fake_button_press(0, 0, button=1) w = topbar.info()["widgets"][0] assert w["text"] == "[ shutdown ]" def test_exit(manager_nospawn, minimal_conf_noscreen): # Set a short interval and start so widget exits immediately qewidget = widget.QuickExit(timer_interval=0.001, countdown_start=1) config = minimal_conf_noscreen config.screens = [libqtile.config.Screen(top=libqtile.bar.Bar([qewidget], 10))] manager_nospawn.start(config) topbar = manager_nospawn.c.bar["top"] # Click widget to start countdown topbar.fake_button_press(0, 0, button=1) # Trying to access bar should now give IPCError or a ConnectionResetError # as qtile has shutdown with pytest.raises((IPCError, ConnectionResetError)): assert topbar.info() ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_widget_init_configure.py���������������������������������������������0000664�0001750�0001750�00000013554�14762660347�023253� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # Widget specific tests import pytest import libqtile.bar import libqtile.config import libqtile.confreader import libqtile.layout import libqtile.widget as widgets from libqtile.widget.base import ORIENTATION_BOTH, ORIENTATION_HORIZONTAL, ORIENTATION_VERTICAL from libqtile.widget.clock import Clock from libqtile.widget.crashme import _CrashMe from test.widgets.conftest import FakeBar # This file runs a very simple test to check that widgets can be initialised # and that keyword arguments are added to default values. # # This test is not meant to replace any widget specific tests but should catch # any mistakes that inadvertently breakag widgets. # # By default, the test runs on every widget that is listed in __init__.py # This is done by building a list called `parameters` which contains a tuple of # (widget class, kwargs). # # Adjustments to the tests can be made below. # Some widgets may require certain parameters to be set when initialising. # Widgets listed here will replace the default values. # This should be used as a last resort - any failure may indicate an # underlying issue in the widget that should be resolved. overrides = [] # Some widgets are not included in __init__.py # They can be included in the tests by adding their details here extras = [ (_CrashMe, {}), # Just used by devs but no harm checking it works ] # To skip a test entirely, list the widget class here no_test = [widgets.Mirror, widgets.PulseVolume] # Mirror requires a reflection object no_test += [widgets.ImapWidget] # Requires a configured username # To test a widget only under one backend, list the widget class here exclusive_backend = { widgets.Systray: "x11", widgets.Redshift: "x11", widgets.SwayNC: "wayland", } ################################################################################ # Do not edit below this line ################################################################################ # Build default list of all widgets and assign simple keyword argument parameters = [(getattr(widgets, w), {"dummy_parameter": 1}) for w in widgets.__all__] # Replace items in default list with overrides for ovr in overrides: parameters = [ovr if ovr[0] == w[0] else w for w in parameters] # Add the extra widgets parameters.extend(extras) # Remove items which need to be skipped for skipped in no_test: parameters = [w for w in parameters if w[0] != skipped] def no_op(*args, **kwargs): pass @pytest.mark.parametrize( "widget_class,kwargs", [ param for param in parameters if param[0]().orientations in [ORIENTATION_BOTH, ORIENTATION_HORIZONTAL] ], ) def test_widget_init_config(manager_nospawn, minimal_conf_noscreen, widget_class, kwargs): if widget_class in exclusive_backend: if exclusive_backend[widget_class] != manager_nospawn.backend.name: pytest.skip("Unsupported backend") widget = widget_class(**kwargs) widget.draw = no_op # If widget inits ok then kwargs will now be attributes for k, v in kwargs.items(): assert getattr(widget, k) == v # Test configuration config = minimal_conf_noscreen config.screens = [libqtile.config.Screen(top=libqtile.bar.Bar([widget], 10))] manager_nospawn.start(config) i = manager_nospawn.c.bar["top"].info() # Check widget is registered by checking names of widgets in bar assert i["widgets"][0]["name"] == widget.name @pytest.mark.parametrize( "widget_class,kwargs", [ param for param in parameters if param[0]().orientations in [ORIENTATION_BOTH, ORIENTATION_VERTICAL] ], ) def test_widget_init_config_vertical_bar( manager_nospawn, minimal_conf_noscreen, widget_class, kwargs ): if widget_class in exclusive_backend: if exclusive_backend[widget_class] != manager_nospawn.backend.name: pytest.skip("Unsupported backend") widget = widget_class(**kwargs) widget.draw = no_op # If widget inits ok then kwargs will now be attributes for k, v in kwargs.items(): assert getattr(widget, k) == v # Test configuration config = minimal_conf_noscreen config.screens = [libqtile.config.Screen(left=libqtile.bar.Bar([widget], 10))] manager_nospawn.start(config) i = manager_nospawn.c.bar["left"].info() # Check widget is registered by checking names of widgets in bar assert i["widgets"][0]["name"] == widget.name @pytest.mark.parametrize("widget_class,kwargs", parameters) def test_widget_init_config_set_width(widget_class, kwargs): widget = widget_class(width=50) assert widget def test_incompatible_orientation(fake_qtile, fake_window): clk1 = Clock() clk1.orientations = ORIENTATION_VERTICAL fakebar = FakeBar([clk1], window=fake_window) with pytest.raises(libqtile.confreader.ConfigError): clk1._configure(fake_qtile, fakebar) ����������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_groupbox.py����������������������������������������������������������0000664�0001750�0001750�00000003562�14762660347�020547� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2024 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest from libqtile import config, widget from libqtile.bar import Bar from test.helpers import BareConfig class GroupBoxConfig(BareConfig): screens = [ config.Screen( top=Bar([widget.GroupBox(), widget.GroupBox(name="has_markup", markup=True)], 24) ) ] groups = [config.Group("1", label="<sup>1</sup>")] groupbox_config = pytest.mark.parametrize("manager", [GroupBoxConfig], indirect=True) @groupbox_config def test_groupbox_markup(manager): """Group labels can support markup but this is disabled by default.""" no_markup = manager.c.widget["groupbox"] has_markup = manager.c.widget["has_markup"] # If markup is disabled, text will include markup tags so widget will be wider assert no_markup.info()["width"] > has_markup.info()["width"] ����������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_launchbar.py���������������������������������������������������������0000664�0001750�0001750�00000003241�14762660347�020633� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2022 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import sys from types import ModuleType from libqtile import widget class MockXDG(ModuleType): def getIconPath(*args, **kwargs): # noqa: N802 pass def test_deprecated_configuration(caplog, monkeypatch): monkeypatch.setitem(sys.modules, "xdg.IconTheme", MockXDG("xdg.IconTheme")) _ = widget.LaunchBar( [("thunderbird", "thunderbird -safe-mode", "launch thunderbird in safe mode")] ) records = [r for r in caplog.records if r.msg.startswith("The use of")] assert records assert "The use of a positional argument in LaunchBar is deprecated." in records[0].msg ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_cmus.py��������������������������������������������������������������0000664�0001750�0001750�00000021016�14762660347�017643� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # Widget specific tests import subprocess import pytest import libqtile.config from libqtile.widget import cmus class MockCmusRemoteProcess: CalledProcessError = None EXTRA = [ "set aaa_mode all", "set continue true", "set play_library true", "set play_sorted false", "set replaygain disabled", "set replaygain_limit true", "set replaygain_preamp 0.000000", "set repeat false", "set repeat_current false", "set shuffle false", "set softvol false", "set vol_left 100", "set vol_right 100", ] info = {} is_error = False index = 0 @classmethod def reset(cls): cls.info = [ [ "status playing", "file /playing/file/rickroll.mp3", "duration 222", "position 14", "tag artist Rick Astley", "tag album Whenever You Need Somebody", "tag title Never Gonna Give You Up", ], [ "status playing", "file http://playing/file/sweetcaroline.mp3", "duration -1", "position -9", "tag artist Neil Diamond", "tag album Greatest Hits", "tag title Sweet Caroline", ], [ "status stopped", "file http://streaming.source/tomjones.m3u", "duration -1", "position -9", "tag title It's Not Unusual", "stream tomjones", ], [ "status playing", "file /playing/file/always.mp3", "duration 222", "position 14", "tag artist Above & Beyond", "tag album Anjunabeats 14", "tag title Always - Tinlicker Extended Mix", ], [ "status playing", "file /playing/file/always.mp3", "duration 222", "position 14", ], ] cls.index = 0 cls.is_error = False @classmethod def call_process(cls, cmd): if cls.is_error: raise subprocess.CalledProcessError(-1, cmd=cmd, output="Couldn't connect to cmus.") if cmd[1:] == ["-C", "status"]: track = cls.info[cls.index] track.extend(cls.EXTRA) output = "\n".join(track) return output elif cmd[1] == "-p": cls.info[cls.index][0] = "status playing" elif cmd[1] == "-u": if cls.info[cls.index][0] == "status playing": cls.info[cls.index][0] = "status paused" elif cls.info[cls.index][0] == "status paused": cls.info[cls.index][0] = "status playing" elif cmd[1] == "-n": cls.index = (cls.index + 1) % len(cls.info) elif cmd[1] == "-r": cls.index = (cls.index - 1) % len(cls.info) @classmethod def Popen(cls, cmd): # noqa: N802 cls.call_process(cmd) @pytest.fixture def cmus_manager(manager_nospawn, monkeypatch, minimal_conf_noscreen, request): widget_config = getattr(request, "param", dict()) MockCmusRemoteProcess.reset() monkeypatch.setattr("libqtile.widget.cmus.subprocess", MockCmusRemoteProcess) monkeypatch.setattr( "libqtile.widget.cmus.subprocess.CalledProcessError", subprocess.CalledProcessError ) monkeypatch.setattr( "libqtile.widget.cmus.base.ThreadPoolText.call_process", MockCmusRemoteProcess.call_process, ) config = minimal_conf_noscreen config.screens = [ libqtile.config.Screen( top=libqtile.bar.Bar( [cmus.Cmus(**widget_config)], 10, ), ) ] manager_nospawn.start(config) yield manager_nospawn def test_cmus(cmus_manager): widget = cmus_manager.c.widget["cmus"] widget.eval("self.update(self.poll())") assert widget.info()["text"] == "♫ Rick Astley - Never Gonna Give You Up" assert widget.eval("self.layout.colour") == widget.eval("self.playing_color") widget.eval("self.play()") widget.eval("self.update(self.poll())") assert widget.info()["text"] == "♫ Rick Astley - Never Gonna Give You Up" assert widget.eval("self.layout.colour") == widget.eval("self.paused_color") def test_cmus_play_stopped(cmus_manager): widget = cmus_manager.c.widget["cmus"] # Set track to a stopped item widget.eval("subprocess.index = 2") widget.eval("self.update(self.poll())") # It's stopped so colour should reflect this assert widget.info()["text"] == "♫ tomjones" assert widget.eval("self.layout.colour") == widget.eval("self.stopped_color") widget.eval("self.play()") widget.eval("self.update(self.poll())") assert widget.info()["text"] == "♫ tomjones" assert widget.eval("self.layout.colour") == widget.eval("self.playing_color") @pytest.mark.parametrize( "cmus_manager", [{"format": "{position} {duration} {position_percent} {remaining} {remaining_percent}"}], indirect=True, ) def test_cmus_times(cmus_manager): widget = cmus_manager.c.widget["cmus"] # Check item with valid position and duration widget.eval("self.update(self.poll())") # Check that times are correct assert widget.info()["text"] == "00:14 03:42 6% 03:28 94%" # Set track to an item with invalid position and duration widget.eval("subprocess.index = 1") widget.eval("self.update(self.poll())") # Check that times are empty assert widget.info()["text"].strip() == "" def test_cmus_buttons(cmus_manager): topbar = cmus_manager.c.bar["top"] widget = cmus_manager.c.widget["cmus"] assert widget.info()["text"] == "♫ Rick Astley - Never Gonna Give You Up" # Play next track # Non-local file source topbar.fake_button_press(0, 0, button=4) widget.eval("self.update(self.poll())") assert widget.info()["text"] == "♫ Neil Diamond - Sweet Caroline" # Play next track # Stream source so widget just displays stream info topbar.fake_button_press(0, 0, button=4) widget.eval("self.update(self.poll())") assert widget.info()["text"] == "♫ tomjones" # Play previous track # Non-local file source topbar.fake_button_press(0, 0, button=5) widget.eval("self.update(self.poll())") assert widget.info()["text"] == "♫ Neil Diamond - Sweet Caroline" def test_cmus_error_handling(cmus_manager): widget = cmus_manager.c.widget["cmus"] widget.eval("subprocess.is_error = True") widget.eval("self.update(self.poll())") # Widget does nothing with error message so text is blank # TODO: update widget to show error? assert widget.info()["text"] == "" def test_escape_text(cmus_manager): widget = cmus_manager.c.widget["cmus"] # Set track to an item with a title which needs escaping widget.eval("subprocess.index = 3") widget.eval("self.update(self.poll())") # & should be escaped to & assert widget.info()["text"] == "♫ Above & Beyond - Always - Tinlicker Extended Mix" def test_missing_metadata(cmus_manager): widget = cmus_manager.c.widget["cmus"] # Set track to one that's missing Title and Artist metadata widget.eval("subprocess.index = 4") widget.eval("self.update(self.poll())") # Displayed text should default to the name of the file assert widget.info()["text"] == "♫ always.mp3" ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_mpd2widget.py��������������������������������������������������������0000664�0001750�0001750�00000016167�14762660347�020755� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import sys from types import ModuleType import pytest import libqtile.config from libqtile import widget class MockMPD(ModuleType): class ConnectionError(Exception): pass class CommandError(Exception): pass class MPDClient: tracks = [ {"title": "Never gonna give you up", "artist": "Rick Astley", "song": "0"}, {"title": "Sweet Caroline", "artist": "Neil Diamond"}, {"title": "Marea", "artist": "Fred Again.."}, {}, {"title": "Sweden", "performer": "C418"}, ] def __init__(self): self._index = 0 self._connected = False self._state_override = True self._status = {"state": "pause"} @property def _current_song(self): return self.tracks[self._index] def ping(self): if not self._connected: raise ConnectionError() return self._state_override def connect(self, host, port): return True def command_list_ok_begin(self): pass def status(self): return self._status def currentsong(self): return self._index + 1 def command_list_end(self): return (self.status(), self._current_song) def close(self): pass def disconnect(self): pass def pause(self): self._status["state"] = "pause" def play(self): self._status["state"] = "play" def stop(self): self._status["state"] = "stop" def next(self): self._index = (self._index + 1) % len(self.tracks) def previous(self): self._index = (self._index - 1) % len(self.tracks) def add_states(self): self._status.update( {"repeat": "1", "random": "1", "single": "1", "consume": "1", "updating_db": "1"} ) def force_idle(self): self._status["state"] = "stop" self._index = 3 @pytest.fixture def mpd2_manager(manager_nospawn, monkeypatch, minimal_conf_noscreen, request): monkeypatch.setitem(sys.modules, "mpd", MockMPD("mpd")) monkeypatch.setattr("libqtile.widget.mpd2widget.MPDClient", MockMPD.MPDClient) config = minimal_conf_noscreen config.screens = [ libqtile.config.Screen( top=libqtile.bar.Bar( [widget.Mpd2(**getattr(request, "param", dict()))], 50, ), ) ] manager_nospawn.start(config) yield manager_nospawn def test_mpd2_widget_display_and_actions(mpd2_manager): widget = mpd2_manager.c.widget["mpd2"] assert widget.info()["text"] == "⏸ Rick Astley/Never gonna give you up [-----]" # Button 1 toggles state mpd2_manager.c.bar["top"].fake_button_press(0, 0, 1) widget.eval("self.update(self.poll())") assert widget.info()["text"] == "▶ Rick Astley/Never gonna give you up [-----]" # Button 3 stops mpd2_manager.c.bar["top"].fake_button_press(0, 0, 3) widget.eval("self.update(self.poll())") assert widget.info()["text"] == "■ Rick Astley/Never gonna give you up [-----]" # Button 1 toggles state mpd2_manager.c.bar["top"].fake_button_press(0, 0, 1) widget.eval("self.update(self.poll())") assert widget.info()["text"] == "▶ Rick Astley/Never gonna give you up [-----]" mpd2_manager.c.bar["top"].fake_button_press(0, 0, 1) widget.eval("self.update(self.poll())") assert widget.info()["text"] == "⏸ Rick Astley/Never gonna give you up [-----]" # Button 5 is "next" mpd2_manager.c.bar["top"].fake_button_press(0, 0, 5) widget.eval("self.update(self.poll())") assert widget.info()["text"] == "⏸ Neil Diamond/Sweet Caroline [-----]" mpd2_manager.c.bar["top"].fake_button_press(0, 0, 5) widget.eval("self.update(self.poll())") assert widget.info()["text"] == "⏸ Fred Again../Marea [-----]" # Button 4 is previous mpd2_manager.c.bar["top"].fake_button_press(0, 0, 4) widget.eval("self.update(self.poll())") assert widget.info()["text"] == "⏸ Neil Diamond/Sweet Caroline [-----]" mpd2_manager.c.bar["top"].fake_button_press(0, 0, 4) widget.eval("self.update(self.poll())") assert widget.info()["text"] == "⏸ Rick Astley/Never gonna give you up [-----]" def test_mpd2_widget_extra_info(mpd2_manager): """Quick test to check extra info is displayed ok.""" widget = mpd2_manager.c.widget["mpd2"] # Inject everything to make test quicker widget.eval("self.client.add_states()") # Update widget and check text widget.eval("self.update(self.poll())") assert widget.info()["text"] == "⏸ Rick Astley/Never gonna give you up [rz1cU]" def test_mpd2_widget_idle_message(mpd2_manager): """Quick test to check idle message.""" widget = mpd2_manager.c.widget["mpd2"] # Inject everything to make test quicker widget.eval("self.client.force_idle()") # Update widget and check text widget.eval("self.update(self.poll())") assert widget.info()["text"] == "■ MPD IDLE[-----]" @pytest.mark.parametrize( "mpd2_manager", [{"status_format": "{currentsong}: {artist}/{title}"}], indirect=True ) def test_mpd2_widget_current_song(mpd2_manager): """Quick test to check currentsong info""" widget = mpd2_manager.c.widget["mpd2"] assert widget.info()["text"] == "1: Rick Astley/Never gonna give you up" @pytest.mark.parametrize( "mpd2_manager", [{"undefined_value": "Unknown", "status_format": "{title} ({year})"}], indirect=True, ) def test_mpd2_widget_custom_undefined_value(mpd2_manager): """Quick test to check undefined_value option""" widget = mpd2_manager.c.widget["mpd2"] assert widget.info()["text"] == "Never gonna give you up (Unknown)" def test_mpd2_widget_dynamic_artist_value(mpd2_manager): """Quick test to check dynamic artist value""" widget = mpd2_manager.c.widget["mpd2"] widget.eval("self.client._index = 4") widget.eval("self.update(self.poll())") assert widget.info()["text"] == "⏸ C418/Sweden [-----]" ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_screensplit.py�������������������������������������������������������0000664�0001750�0001750�00000005625�14762660347�021237� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2022 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest import libqtile.config from libqtile import bar, layout, widget from libqtile.confreader import Config class ScreenSplitConfig(Config): auto_fullscreen = True groups = [libqtile.config.Group("a")] layouts = [layout.Max(), layout.ScreenSplit()] floating_layout = libqtile.resources.default_config.floating_layout keys = [] mouse = [] screens = [ libqtile.config.Screen( top=bar.Bar( [widget.ScreenSplit(), widget.ScreenSplit(format="{layout} - {split_name}")], 40 ) ) ] follow_mouse_focus = False screensplit_config = pytest.mark.parametrize("manager", [ScreenSplitConfig], indirect=True) @screensplit_config def test_screensplit_text(manager): widget = manager.c.widget["screensplit"] assert widget.info()["text"] == "" manager.c.next_layout() assert widget.info()["text"] == "top (max)" manager.c.layout.next_split() assert widget.info()["text"] == "bottom (columns)" manager.c.next_layout() assert widget.info()["text"] == "" @screensplit_config def test_screensplit_scroll_actions(manager): widget = manager.c.widget["screensplit"] bar = manager.c.bar["top"] assert widget.info()["text"] == "" manager.c.next_layout() assert widget.info()["text"] == "top (max)" bar.fake_button_press(0, 0, 4) assert widget.info()["text"] == "bottom (columns)" bar.fake_button_press(0, 0, 4) assert widget.info()["text"] == "top (max)" bar.fake_button_press(0, 0, 5) assert widget.info()["text"] == "bottom (columns)" bar.fake_button_press(0, 0, 5) assert widget.info()["text"] == "top (max)" @screensplit_config def test_screensplit_text_format(manager): widget = manager.c.widget["screensplit_1"] manager.c.next_layout() assert widget.info()["text"] == "max - top" �����������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_window_count.py������������������������������������������������������0000664�0001750�0001750�00000005622�14762660347�021420� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������import pytest import libqtile from libqtile.confreader import Config from libqtile.widget import WindowCount class DifferentScreens(Config): groups = [ libqtile.config.Group("a"), libqtile.config.Group("b"), ] layouts = [ libqtile.layout.Stack(num_stacks=1), ] floating_layout = libqtile.resources.default_config.floating_layout fake_screens = [ libqtile.config.Screen( top=libqtile.bar.Bar( [ WindowCount(), ], 20, ), x=0, y=0, width=300, height=300, ), libqtile.config.Screen( top=libqtile.bar.Bar( [ WindowCount(), ], 20, ), x=0, y=300, width=300, height=300, ), ] auto_fullscreen = True different_screens = pytest.mark.parametrize("manager", [DifferentScreens], indirect=True) @different_screens def test_different_screens(manager): # Put one window on screen 0 manager.c.to_screen(0) manager.test_window("one") # Put two windows on screen 1 manager.c.to_screen(1) manager.test_window("two") manager.test_window("three") assert manager.c.screen[0].widget["windowcount"].get() == "1" assert manager.c.screen[1].widget["windowcount"].get() == "2" def test_window_count(manager_nospawn, minimal_conf_noscreen): config = minimal_conf_noscreen config.screens = [libqtile.config.Screen(top=libqtile.bar.Bar([WindowCount()], 10))] manager_nospawn.start(config) # No windows opened assert int(manager_nospawn.c.widget["windowcount"].get()) == 0 # Add a window and check count one = manager_nospawn.test_window("one") assert int(manager_nospawn.c.widget["windowcount"].get()) == 1 # Add a window and check text two = manager_nospawn.test_window("two") assert manager_nospawn.c.widget["windowcount"].get() == "2" # Change to empty group manager_nospawn.c.group["b"].toscreen() assert int(manager_nospawn.c.widget["windowcount"].get()) == 0 # Change back to group manager_nospawn.c.group["a"].toscreen() assert int(manager_nospawn.c.widget["windowcount"].get()) == 2 # Move a window and check text manager_nospawn.c.window.togroup("b") assert int(manager_nospawn.c.widget["windowcount"].get()) == 1 # Close all windows and check count is 0 and widget not displayed manager_nospawn.kill_window(one) manager_nospawn.kill_window(two) assert int(manager_nospawn.c.widget["windowcount"].get()) == 0 def test_attribute_errors(): def no_op(*args, **kwargs): pass wc = WindowCount() wc.update = no_op wc._count = 1 wc._wincount() assert wc._count == 0 wc._count = 1 wc._win_killed(None) assert wc._count == 0 ��������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_load.py��������������������������������������������������������������0000664�0001750�0001750�00000006332�14762660347�017617� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2022 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import sys from importlib import reload from types import ModuleType import pytest import libqtile.config import libqtile.widget from libqtile.bar import Bar class MockPsutil(ModuleType): @classmethod def getloadavg(cls): return (0.73046875, 0.77587890625, 0.9521484375) @pytest.fixture def load_manager(monkeypatch, manager_nospawn, minimal_conf_noscreen, request): widget_config = getattr(request, "param", dict()) monkeypatch.setitem(sys.modules, "psutil", MockPsutil("psutil")) from libqtile.widget import load reload(load) config = minimal_conf_noscreen config.screens = [libqtile.config.Screen(top=Bar([load.Load(**widget_config)], 10))] manager_nospawn.start(config) yield manager_nospawn def test_load_times_button_click(load_manager): """Test cycling of loads via button press""" widget = load_manager.c.widget["load"] assert widget.info()["text"] == "Load(1m):0.73" load_manager.c.bar["top"].fake_button_press(0, 0, button=1) assert widget.info()["text"] == "Load(5m):0.78" load_manager.c.bar["top"].fake_button_press(0, 0, button=1) assert widget.info()["text"] == "Load(15m):0.95" load_manager.c.bar["top"].fake_button_press(0, 0, button=1) assert widget.info()["text"] == "Load(1m):0.73" def test_load_times_command(load_manager): """Test cycling of loads via exposed command""" widget = load_manager.c.widget["load"] assert widget.info()["text"] == "Load(1m):0.73" widget.next_load() assert widget.info()["text"] == "Load(5m):0.78" widget.next_load() assert widget.info()["text"] == "Load(15m):0.95" widget.next_load() assert widget.info()["text"] == "Load(1m):0.73" @pytest.mark.parametrize("load_manager", [{"format": "{time}: {load:.1f}"}], indirect=True) def test_load_times_formatting(load_manager): """Test formatting of load times""" widget = load_manager.c.widget["load"] assert widget.info()["text"] == "1m: 0.7" widget.next_load() assert widget.info()["text"] == "5m: 0.8" widget.next_load() assert widget.info()["text"] == "15m: 1.0" widget.next_load() assert widget.info()["text"] == "1m: 0.7" ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_imapwidget.py��������������������������������������������������������0000664�0001750�0001750�00000007167�14762660347�021041� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # Widget specific tests import sys from importlib import reload from types import ModuleType import pytest from test.widgets.conftest import FakeBar class FakeIMAP(ModuleType): class IMAP4_SSL: # noqa: N801 def __init__(self, *args, **kwargs): pass def login(self, username, password): self.username = username self.password = password def status(self, path, *args, **kwargs): if not (self.username and self.password): return False, None return ("OK", [f'"{path}" (UNSEEN 2)'.encode()]) def logout(self): pass class FakeKeyring(ModuleType): valid = True error = True def get_password(self, _app, user): if self.valid: return "password" else: if self.error: return "Gnome Keyring Error" return None @pytest.fixture() def patched_imap(monkeypatch): monkeypatch.delitem(sys.modules, "imaplib", raising=False) monkeypatch.delitem(sys.modules, "keyring", raising=False) monkeypatch.setitem(sys.modules, "imaplib", FakeIMAP("imaplib")) monkeypatch.setitem(sys.modules, "keyring", FakeKeyring("keyring")) from libqtile.widget import imapwidget reload(imapwidget) yield imapwidget def test_imapwidget(fake_qtile, monkeypatch, fake_window, patched_imap): imap = patched_imap.ImapWidget(user="qtile") fakebar = FakeBar([imap], window=fake_window) imap._configure(fake_qtile, fakebar) text = imap.poll() assert text == "INBOX: 2" def test_imapwidget_keyring_error(fake_qtile, monkeypatch, fake_window, patched_imap): patched_imap.keyring.valid = False imap = patched_imap.ImapWidget(user="qtile") fakebar = FakeBar([imap], window=fake_window) imap._configure(fake_qtile, fakebar) text = imap.poll() assert text == "Gnome Keyring Error" # Widget does not handle "password = None" elegantly. # It logs a message but doesn't set self.password. # The widget will then fail when it looks up this attribute and then # fail again when it tries to return self.text. # TO DO: Fix widget's handling of this scenario. def test_imapwidget_password_none(fake_qtile, monkeypatch, fake_window, patched_imap): patched_imap.keyring.valid = False patched_imap.keyring.error = False imap = patched_imap.ImapWidget(user="qtile") fakebar = FakeBar([imap], window=fake_window) imap._configure(fake_qtile, fakebar) with pytest.raises(AttributeError): with pytest.raises(UnboundLocalError): imap.poll() ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_sensors.py�����������������������������������������������������������0000664�0001750�0001750�00000007532�14762660347�020377� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2022 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import sys from importlib import reload from types import ModuleType import pytest import libqtile.config import libqtile.widget from libqtile.bar import Bar class Temp: def __init__(self, label, temp, fahrenheit=False): self.label = label self.current = temp if fahrenheit: self.current = (self.current * 9 / 5) + 32 class MockPsutil(ModuleType): @classmethod def sensors_temperatures(cls, fahrenheit=False): return {"core": [Temp("CPU", 45.0, fahrenheit)], "nvme": [Temp("NVME", 56.3, fahrenheit)]} @pytest.fixture def sensors_manager(monkeypatch, manager_nospawn, minimal_conf_noscreen, request): params = getattr(request, "param", dict()) monkeypatch.setitem(sys.modules, "psutil", MockPsutil("psutil")) from libqtile.widget import sensors reload(sensors) config = minimal_conf_noscreen config.screens = [libqtile.config.Screen(top=Bar([sensors.ThermalSensor(**params)], 10))] if "set_defaults" in params: config.widget_defaults = {"foreground": "123456"} manager_nospawn.start(config) yield manager_nospawn def test_thermal_sensor_metric(sensors_manager): assert sensors_manager.c.widget["thermalsensor"].info()["text"] == "45.0°C" @pytest.mark.parametrize("sensors_manager", [{"metric": False}], indirect=True) def test_thermal_sensor_imperial(sensors_manager): assert sensors_manager.c.widget["thermalsensor"].info()["text"] == "113.0°F" @pytest.mark.parametrize("sensors_manager", [{"tag_sensor": "NVME"}], indirect=True) def test_thermal_sensor_tagged_sensor(sensors_manager): assert sensors_manager.c.widget["thermalsensor"].info()["text"] == "56.3°C" @pytest.mark.parametrize("sensors_manager", [{"tag_sensor": "does_not_exist"}], indirect=True) def test_thermal_sensor_unknown_sensor(sensors_manager): assert sensors_manager.c.widget["thermalsensor"].info()["text"] == "N/A" @pytest.mark.parametrize( "sensors_manager", [{"format": "{tag}: {temp:.0f}{unit}"}], indirect=True ) def test_thermal_sensor_format(sensors_manager): assert sensors_manager.c.widget["thermalsensor"].info()["text"] == "CPU: 45°C" def test_thermal_sensor_colour_normal(sensors_manager): _, temp = sensors_manager.c.widget["thermalsensor"].eval("self.layout.colour") assert temp == "ffffff" @pytest.mark.parametrize("sensors_manager", [{"threshold": 30}], indirect=True) def test_thermal_sensor_colour_alert(sensors_manager): _, temp = sensors_manager.c.widget["thermalsensor"].eval("self.layout.colour") assert temp == "ff0000" @pytest.mark.parametrize("sensors_manager", [{"set_defaults": True}], indirect=True) def test_thermal_sensor_widget_defaults(sensors_manager): _, temp = sensors_manager.c.widget["thermalsensor"].eval("self.layout.colour") assert temp == "123456" ����������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_net.py���������������������������������������������������������������0000664�0001750�0001750�00000010233�14762660347�017461� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # Widget specific tests import sys from importlib import reload from types import ModuleType import pytest from test.widgets.conftest import FakeBar # Net widget only needs bytes_recv/sent attributes # Widget displays increase since last poll therefore # we need to increment value each time this is called. class MockPsutil(ModuleType): up = 0 down = 0 @classmethod def net_io_counters(cls, pernic=False, _nowrap=True): class IOCounters: def __init__(self, up, down): self.bytes_sent = up self.bytes_recv = down cls.up += 40000 cls.down += 1200000 if pernic: return {"wlp58s0": IOCounters(cls.up, cls.down), "lo": IOCounters(cls.up, cls.down)} return IOCounters(cls.up, cls.down) # Patch the widget with our mock psutil module. # Wrap widget so tests can pass keyword arguments. @pytest.fixture def patch_net(fake_qtile, monkeypatch, fake_window): def build_widget(**kwargs): monkeypatch.setitem(sys.modules, "psutil", MockPsutil("psutil")) from libqtile.widget import net # Reload fixes cases where psutil may have been imported previously reload(net) widget = net.Net( format="{interface}: U {up}{up_suffix} {up_cumulative}{up_cumulative_suffix} D " "{down}{down_suffix} {down_cumulative}{down_cumulative_suffix} T {total}" "{total_suffix} {total_cumulative}{total_cumulative_suffix}", **kwargs, ) fakebar = FakeBar([widget], window=fake_window) widget._configure(fake_qtile, fakebar) return widget return build_widget def test_net_defaults(patch_net): """Default: widget shows `all` interfaces""" net1 = patch_net() assert net1.poll() == "all: U 40.0kB 80.0kB D 1.2MB 2.4MB T 1.24MB 2.48MB" def test_net_single_interface(patch_net): """Display single named interface""" net2 = patch_net(interface="wlp58s0") assert net2.poll() == "wlp58s0: U 40.0kB 160.0kB D 1.2MB 4.8MB T 1.24MB 4.96MB" def test_net_list_interface(patch_net): """Display multiple named interfaces""" net2 = patch_net(interface=["wlp58s0", "lo"]) assert net2.poll() == ( "wlp58s0: U 40.0kB 240.0kB D 1.2MB 7.2MB T 1.24MB 7.44MB lo: U 40.0kB " "240.0kB D 1.2MB 7.2MB T 1.24MB 7.44MB" ) def test_net_invalid_interface(patch_net): """Pass an invalid interface value""" with pytest.raises(AttributeError): _ = patch_net(interface=12) def test_net_use_bits(patch_net): """Display all interfaces in bits rather than bytes""" net4 = patch_net(use_bits=True) assert net4.poll() == "all: U 320.0kb 2.56Mb D 9.6Mb 76.8Mb T 9.92Mb 79.36Mb" def test_net_convert_zero_b(patch_net): """Zero bytes is a special case in `convert_b`""" net5 = patch_net() assert net5.convert_b(0.0) == (0.0, "B") def test_net_use_prefix(patch_net): """Tests `prefix` configurable option""" net6 = patch_net(prefix="M") assert net6.poll() == "all: U 0.04MB 440.0kB D 1.2MB 13.2MB T 1.24MB 13.64MB" # Untested: 128-129 - generic exception catching ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_gmail_checker.py�����������������������������������������������������0000664�0001750�0001750�00000005774�14762660347�021466� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # Widget specific tests import sys from importlib import reload from types import ModuleType from libqtile.widget import gmail_checker from test.widgets.conftest import FakeBar class FakeIMAP(ModuleType): class IMAP4_SSL: # noqa: N801 def __init__(self, *args, **kwargs): pass def login(self, username, password): self.username = username self.password = password def status(self, path, *args, **kwargs): if not (self.username and self.password): return False, None return ("OK", [f'("{path}" (MESSAGES 10 UNSEEN 2)'.encode()]) def test_gmail_checker_valid_response(fake_qtile, monkeypatch, fake_window): monkeypatch.setitem(sys.modules, "imaplib", FakeIMAP("imaplib")) reload(gmail_checker) gmc = gmail_checker.GmailChecker(username="qtile", password="test") fakebar = FakeBar([gmc], window=fake_window) gmc._configure(fake_qtile, fakebar) text = gmc.poll() assert text == "inbox[10],unseen[2]" def test_gmail_checker_invalid_response(fake_qtile, monkeypatch, fake_window): monkeypatch.setitem(sys.modules, "imaplib", FakeIMAP("imaplib")) reload(gmail_checker) gmc = gmail_checker.GmailChecker() fakebar = FakeBar([gmc], window=fake_window) gmc._configure(fake_qtile, fakebar) text = gmc.poll() assert text == "UNKNOWN ERROR" # This test is only required because the widget is written # inefficiently. display_fmt should use keys instead of indices. def test_gmail_checker_only_unseen(fake_qtile, monkeypatch, fake_window): monkeypatch.setitem(sys.modules, "imaplib", FakeIMAP("imaplib")) reload(gmail_checker) gmc = gmail_checker.GmailChecker( display_fmt="unseen[{0}]", status_only_unseen=True, username="qtile", password="test" ) fakebar = FakeBar([gmc], window=fake_window) gmc._configure(fake_qtile, fakebar) text = gmc.poll() assert text == "unseen[2]" ����qtile-0.31.0/test/widgets/test_caps_num_lock_indicator.py�������������������������������������������0000664�0001750�0001750�00000010021�14762660347�023537� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # Widget specific tests import subprocess import pytest from libqtile.widget import caps_num_lock_indicator from test.widgets.conftest import FakeBar class MockCapsNumLockIndicator: CalledProcessError = None info: list[list[str]] = [] is_error = False index = 0 @classmethod def reset(cls): cls.info = [ [ "Keyboard Control:", " auto repeat: on key click percent: 0 LED mask: 00000002", " XKB indicators:", " 00: Caps Lock: off 01: Num Lock: on 02: Scroll Lock: off", " 03: Compose: off 04: Kana: off 05: Sleep: off", ], [ "Keyboard Control:", " auto repeat: on key click percent: 0 LED mask: 00000002", " XKB indicators:", " 00: Caps Lock: on 01: Num Lock: on 02: Scroll Lock: off", " 03: Compose: off 04: Kana: off 05: Sleep: off", ], ] cls.index = 0 cls.is_error = False @classmethod def call_process(cls, cmd): if cls.is_error: raise subprocess.CalledProcessError(-1, cmd=cmd, output="Couldn't call xset.") if cmd[1:] == ["q"]: track = cls.info[cls.index] output = "\n".join(track) return output def no_op(*args, **kwargs): pass @pytest.fixture def patched_cnli(monkeypatch): MockCapsNumLockIndicator.reset() monkeypatch.setattr( "libqtile.widget.caps_num_lock_indicator.subprocess", MockCapsNumLockIndicator ) monkeypatch.setattr( "libqtile.widget.caps_num_lock_indicator.subprocess.CalledProcessError", subprocess.CalledProcessError, ) monkeypatch.setattr( "libqtile.widget.caps_num_lock_indicator.base.ThreadPoolText.call_process", MockCapsNumLockIndicator.call_process, ) return caps_num_lock_indicator def test_cnli(fake_qtile, patched_cnli, fake_window): widget = patched_cnli.CapsNumLockIndicator() fakebar = FakeBar([widget], window=fake_window) widget._configure(fake_qtile, fakebar) text = widget.poll() assert text == "Caps off Num on" def test_cnli_caps_on(fake_qtile, patched_cnli, fake_window): widget = patched_cnli.CapsNumLockIndicator() # Simulate Caps on MockCapsNumLockIndicator.index = 1 fakebar = FakeBar([widget], window=fake_window) widget._configure(fake_qtile, fakebar) text = widget.poll() assert text == "Caps on Num on" def test_cnli_error_handling(fake_qtile, patched_cnli, fake_window): widget = patched_cnli.CapsNumLockIndicator() # Simulate a CalledProcessError exception MockCapsNumLockIndicator.is_error = True fakebar = FakeBar([widget], window=fake_window) widget._configure(fake_qtile, fakebar) text = widget.poll() # Widget does nothing with error message so text is blank assert text == "" ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_currentscreen.py�����������������������������������������������������0000664�0001750�0001750�00000003704�14762660347�021562� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # Widget specific tests import libqtile.bar import libqtile.config import libqtile.confreader import libqtile.layout from libqtile import widget from test.conftest import dualmonitor ACTIVE = "#FF0000" INACTIVE = "#00FF00" @dualmonitor def test_change_screen(manager_nospawn, minimal_conf_noscreen): cswidget = widget.CurrentScreen(active_color=ACTIVE, inactive_color=INACTIVE) config = minimal_conf_noscreen config.screens = [ libqtile.config.Screen(top=libqtile.bar.Bar([cswidget], 10)), libqtile.config.Screen(), ] manager_nospawn.start(config) w = manager_nospawn.c.screen[0].bar["top"].info()["widgets"][0] assert w["text"] == "A" assert w["foreground"] == ACTIVE manager_nospawn.c.to_screen(1) w = manager_nospawn.c.screen[0].bar["top"].info()["widgets"][0] assert w["text"] == "I" assert w["foreground"] == INACTIVE ������������������������������������������������������������qtile-0.31.0/test/widgets/test_mpris2widget.py������������������������������������������������������0000664�0001750�0001750�00000016720�14762660347�021322� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import sys from importlib import reload from types import ModuleType import pytest from test.widgets.conftest import FakeBar def no_op(*args, **kwargs): pass async def mock_signal_receiver(*args, **kwargs): return True def fake_timer(interval, func, *args, **kwargs): class TimerObj: def cancel(self): pass @property def _scheduled(self): return False return TimerObj() class MockConstants(ModuleType): class MessageType: SIGNAL = 1 class MockMessage: def __init__(self, is_signal=True, body=None): self.message_type = 1 if is_signal else 0 self.body = body # dbus_fast message data is stored in variants. The widget extracts the # information via the `value` attribute so we just need to mock that here. class obj: # noqa: N801 def __init__(self, value): self.value = value # Creates a mock message body containing both metadata and playback status def metadata_and_status(status): return MockMessage( body=( "", { "Metadata": obj( { "mpris:trackid": obj(1), "xesam:url": obj("/path/to/rickroll.mp3"), "xesam:title": obj("Never Gonna Give You Up"), "xesam:artist": obj(["Rick Astley"]), "xesam:album": obj("Whenever You Need Somebody"), "mpris:length": obj(200000000), } ), "PlaybackStatus": obj(status), }, [], ) ) # Creates a mock message body containing just playback status def playback_status(status, signal=True): return MockMessage(is_signal=signal, body=("", {"PlaybackStatus": obj(status)}, [])) METADATA_PLAYING = metadata_and_status("Playing") METADATA_PAUSED = metadata_and_status("Paused") STATUS_PLAYING = playback_status("Playing") STATUS_PAUSED = playback_status("Paused") STATUS_STOPPED = playback_status("Stopped") NON_SIGNAL = playback_status("Paused", False) @pytest.fixture def patched_module(monkeypatch): # Remove dbus_fast.constants entry from modules. If it's not there, don't raise error monkeypatch.delitem(sys.modules, "dbus_fast.constants", raising=False) monkeypatch.setitem(sys.modules, "dbus_fast.constants", MockConstants("dbus_fast.constants")) from libqtile.widget import mpris2widget # Need to force reload of the module to ensure patched module is loaded # This may only be needed if dbus_fast is installed on testing system so helpful for # local tests. reload(mpris2widget) monkeypatch.setattr("libqtile.widget.mpris2widget.add_signal_receiver", mock_signal_receiver) return mpris2widget def test_mpris2_signal_handling(fake_qtile, patched_module, fake_window): mp = patched_module.Mpris2() fakebar = FakeBar([mp], window=fake_window) mp.timeout_add = fake_timer mp._configure(fake_qtile, fakebar) assert mp.displaytext == "" # No text will be displayed if widget is not configured mp.parse_message(*METADATA_PLAYING.body) assert mp.displaytext == "" # Set configured flag, create a message with the metadata and playback status mp.configured = True mp.parse_message(*METADATA_PLAYING.body) assert mp.text == "Never Gonna Give You Up - Whenever You Need Somebody - Rick Astley" # If widget receives "paused" signal it prefixes track with "Paused: " mp.parse_message(*STATUS_PAUSED.body) assert mp.text == "Paused: Never Gonna Give You Up - Whenever You Need Somebody - Rick Astley" # If widget receives "stopped" signal with no metadata then widget is blank mp.parse_message(*STATUS_STOPPED.body) assert mp.displaytext == "" # Reset to playing + metadata mp.parse_message(*METADATA_PLAYING.body) assert mp.text == "Never Gonna Give You Up - Whenever You Need Somebody - Rick Astley" # If widget receives "paused" signal with metadata then message is "Paused: {metadata}" mp.parse_message(*METADATA_PAUSED.body) assert mp.text == "Paused: Never Gonna Give You Up - Whenever You Need Somebody - Rick Astley" # If widget now receives "playing" signal with no metadata, "paused" word is removed mp.parse_message(*STATUS_PLAYING.body) assert mp.text == "Never Gonna Give You Up - Whenever You Need Somebody - Rick Astley" info = mp.info() assert info["text"] == "Never Gonna Give You Up - Whenever You Need Somebody - Rick Astley" assert info["isplaying"] def test_mpris2_custom_stop_text(fake_qtile, patched_module, fake_window): mp = patched_module.Mpris2(stop_pause_text="Test Paused") fakebar = FakeBar([mp], window=fake_window) mp.timeout_add = fake_timer mp._configure(fake_qtile, fakebar) mp.configured = True mp.parse_message(*METADATA_PLAYING.body) assert mp.text == "Never Gonna Give You Up - Whenever You Need Somebody - Rick Astley" # Check our custom paused wording is shown mp.parse_message(*STATUS_PAUSED.body) assert mp.text == "Test Paused" def test_mpris2_no_metadata(fake_qtile, patched_module, fake_window): mp = patched_module.Mpris2() fakebar = FakeBar([mp], window=fake_window) mp.timeout_add = fake_timer mp._configure(fake_qtile, fakebar) mp.configured = True mp.parse_message(*STATUS_PLAYING.body) assert mp.text == "No metadata for current track" def test_mpris2_no_scroll(fake_qtile, patched_module, fake_window): # If no scrolling, then the update function creates the text to display # and draws the bar. mp = patched_module.Mpris2(scroll_chars=None) fakebar = FakeBar([mp], window=fake_window) mp.timeout_add = fake_timer mp._configure(fake_qtile, fakebar) mp.configured = True mp.parse_message(*METADATA_PLAYING.body) assert mp.text == "Never Gonna Give You Up - Whenever You Need Somebody - Rick Astley" mp.parse_message(*METADATA_PAUSED.body) assert mp.text == "Paused: Never Gonna Give You Up - Whenever You Need Somebody - Rick Astley" def test_mpris2_deprecated_format(patched_module): """ Previously, metadata was displayed by using a list of fields. Now, we use a `format` string. The widget should create this when a user provides `display_metadata` in their config. """ mp = patched_module.Mpris2(display_metadata=["xesam:title", "xesam:artist"]) assert mp.format == "{xesam:title} - {xesam:artist}" ������������������������������������������������qtile-0.31.0/test/widgets/test_notify.py������������������������������������������������������������0000664�0001750�0001750�00000031204�14762660347�020204� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # NOTE: This test only tests the functionality of the widget and parts of the manager # The notification service (in libqtile/notify.py) is tested separately # TO DO: notification service test ;) import shutil import subprocess import textwrap import pytest import libqtile.config from libqtile.bar import Bar from libqtile.widget import notify # Bit of a hack... when we log a timer, save the delay in an attribute # We'll use this to check message timeouts are being honoured. def log_timeout(self, delay, func, method_args=None): self.delay = delay self.qtile.call_later(delay, func) def notification(subject, body, urgency=None, timeout=None): """Function to build notification text and command list""" cmds = [] urgs = {0: "low", 1: "normal", 2: "critical"} urg_level = urgs.get(urgency, "normal") if urg_level != "normal": cmds += ["-u", urg_level] if timeout: cmds += ["-t", f"{timeout}"] cmds += [subject, body] text = '<span weight="bold">' if urg_level != "normal": text += '<span color="{{colour}}">' text += "{subject}" if urg_level != "normal": text += "</span>" text += "</span> - {body}" text = text.format(subject=subject, body=body) return text, cmds # for Github CI/Ubuntu, "notify-send" is provided by libnotify-bin package NS = shutil.which("notify-send") BACKGROUND_NORMAL = "111111" BACKGROUND_URGENT = "222222" BACKGROUND_LOW = "333333" URGENT = "#ff00ff" LOW = "#cccccc" MESSAGE_1, NOTIFICATION_1 = notification("Message 1", "Test Message 1", timeout=5000) MESSAGE_2, NOTIFICATION_2 = notification( "Urgent Message", "This is not a test!", urgency=2, timeout=10000 ) MESSAGE_3, NOTIFICATION_3 = notification("Low priority", "Windows closed unexpectedly", urgency=0) DEFAULT_TIMEOUT_LOW = 15 DEFAULT_TIMEOUT_NORMAL = 30 DEFAULT_TIMEOUT_URGENT = 45 @pytest.mark.skipif(shutil.which("notify-send") is None, reason="notify-send not installed.") @pytest.mark.usefixtures("dbus") def test_notifications(manager_nospawn, minimal_conf_noscreen): def background(obj): _, bground = obj.eval("self.background") return bground notify.Notify.timeout_add = log_timeout widget = notify.Notify( foreground_urgent=URGENT, foreground_low=LOW, background=BACKGROUND_NORMAL, background_urgent=BACKGROUND_URGENT, background_low=BACKGROUND_LOW, ) config = minimal_conf_noscreen config.screens = [libqtile.config.Screen(top=Bar([widget], 10))] manager_nospawn.start(config) obj = manager_nospawn.c.widget["notify"] # Send first notification and check time and display time notif_1 = [NS] notif_1.extend(NOTIFICATION_1) subprocess.run(notif_1) assert obj.info()["text"] == MESSAGE_1 assert background(obj) == BACKGROUND_NORMAL _, timeout = obj.eval("self.delay") assert timeout == "5.0" # Send second notification and check time and display time notif_2 = [NS] notif_2.extend(NOTIFICATION_2) subprocess.run(notif_2) assert obj.info()["text"] == MESSAGE_2.format(colour=URGENT) assert background(obj) == BACKGROUND_URGENT _, timeout = obj.eval("self.delay") assert timeout == "10.0" # Send third notification notif_3 = [NS] notif_3.extend(NOTIFICATION_3) subprocess.run(notif_3) assert obj.info()["text"] == MESSAGE_3.format(colour=LOW) assert background(obj) == BACKGROUND_LOW # Navigation tests # Hitting next while on last message should not change display obj.next() assert obj.info()["text"] == MESSAGE_3.format(colour=LOW) assert background(obj) == BACKGROUND_LOW # Show previous obj.prev() assert obj.info()["text"] == MESSAGE_2.format(colour=URGENT) assert background(obj) == BACKGROUND_URGENT # Show previous obj.prev() assert obj.info()["text"] == MESSAGE_1 assert background(obj) == BACKGROUND_NORMAL # Show previous while on first message should stay on first obj.prev() assert obj.info()["text"] == MESSAGE_1 assert background(obj) == BACKGROUND_NORMAL # Show next obj.next() assert obj.info()["text"] == MESSAGE_2.format(colour=URGENT) assert background(obj) == BACKGROUND_URGENT # Toggle display (clear) obj.toggle() assert obj.info()["text"] == "" assert background(obj) == BACKGROUND_NORMAL # Toggle display - restoring display shows last notification obj.toggle() assert obj.info()["text"] == MESSAGE_3.format(colour=LOW) assert background(obj) == BACKGROUND_LOW # Clear the dispay obj.clear() assert obj.info()["text"] == "" assert background(obj) == BACKGROUND_NORMAL # Show the display obj.display() assert obj.info()["text"] == MESSAGE_3.format(colour=LOW) assert background(obj) == BACKGROUND_LOW def test_capabilities(): # Default capabilities are "body" and "actions" widget_with_actions = notify.Notify() assert widget_with_actions.capabilities == {"body", "actions"} # If the user chooses not to have actions, the capabilities # are adjusted accordingly widget_no_actions = notify.Notify(action=False) assert widget_no_actions.capabilities == {"body"} @pytest.mark.skipif(shutil.which("notify-send") is None, reason="notify-send not installed.") @pytest.mark.usefixtures("dbus") def test_invoke_and_clear(manager_nospawn, minimal_conf_noscreen): # We need to create an object to listen for signals from the qtile # notification server. This needs to be created within the manager # object so we rely on "eval" applying "exec". handler = textwrap.dedent( """ from libqtile.utils import add_signal_receiver, create_task class SignalListener: def __init__(self): self.action_invoked = None self.notification_closed = None global add_signal_receiver global create_task create_task( add_signal_receiver( self.on_notification_closed, session_bus=True, signal_name="NotificationClosed" ) ) create_task( add_signal_receiver( self.on_action_invoked, session_bus=True, signal_name="ActionInvoked" ) ) def on_action_invoked(self, msg): self.action_invoked = msg.body def on_notification_closed(self, msg): self.notification_closed = msg.body self.signal_listener = SignalListener() """ ) # Create and send a custom notification with a list of actions. # `utils.send_notfication` is not an option here as it does not # expose actions so we need a lower-level call notification_with_actions = textwrap.dedent( """ from dbus_fast import Variant from dbus_fast.constants import MessageType from libqtile.utils import _send_dbus_message, create_task notification = [ "qtile", 2, "", "Test", "Test with actions", ["default", "ok"], {"urgency": Variant("y", 1)}, 5000 ] create_task( _send_dbus_message( True, MessageType.METHOD_CALL, "org.freedesktop.Notifications", "org.freedesktop.Notifications", "/org/freedesktop/Notifications", "Notify", "susssasa{sv}i", notification ) ) """ ) notify.Notify.timeout_add = log_timeout widget = notify.Notify() config = minimal_conf_noscreen config.screens = [libqtile.config.Screen(top=Bar([widget], 10))] # Start the manager manager_nospawn.start(config) # Create our signal listener manager_nospawn.c.eval(handler) _, result = manager_nospawn.c.eval("self.signal_listener") # Send first notification and check time and display time notif_1 = [NS] notif_1.extend(NOTIFICATION_1) subprocess.run(notif_1) # Check that listener hasn't received any signals yet _, result = manager_nospawn.c.eval("self.signal_listener.action_invoked") assert result == "None" _, result = manager_nospawn.c.eval("self.signal_listener.notification_closed") assert result == "None" # Clicking on notification dismisses it manager_nospawn.c.bar["top"].fake_button_press(0, 0, button=1) # Signal listener should get the id and close reason # id is 1 and dismiss reason is ClosedReason.dismissed which is 2 _, result = manager_nospawn.c.eval("self.signal_listener.notification_closed") assert result == "[1, 2]" # Send a new notification with defined actions _, res = manager_nospawn.c.eval(notification_with_actions) # Right-clicking on notification invokes it manager_nospawn.c.bar["top"].fake_button_press(0, 0, button=3) # Signal listener should get the id and close reason # id is 2 (as it is the second notification) and action is "default" _, result = manager_nospawn.c.eval("self.signal_listener.action_invoked") assert result == "[2, 'default']" @pytest.mark.skipif(shutil.which("notify-send") is None, reason="notify-send not installed.") @pytest.mark.usefixtures("dbus") def test_parse_text(manager_nospawn, minimal_conf_noscreen): def test_parser(text): return f"TEST:{text}" widget = notify.Notify( foreground_urgent=URGENT, foreground_low=LOW, parse_text=test_parser, ) config = minimal_conf_noscreen config.screens = [libqtile.config.Screen(top=Bar([widget], 10))] manager_nospawn.start(config) obj = manager_nospawn.c.widget["notify"] # Send first notification and check time and display time notif_1 = [NS] notif_1.extend(NOTIFICATION_1) subprocess.run(notif_1) assert obj.info()["text"] == f"TEST:{MESSAGE_1}" @pytest.mark.usefixtures("dbus") def test_unregister(manager_nospawn, minimal_conf_noscreen): """Short test to check if notifier deregisters correctly.""" def notifier_has_callbacks(): _, out = manager_nospawn.c.widget["notify"].eval("notifier.callbacks") return out != "[]" widget = notify.Notify() config = minimal_conf_noscreen config.screens = [libqtile.config.Screen(top=Bar([widget], 10))] manager_nospawn.start(config) assert notifier_has_callbacks() _ = manager_nospawn.c.widget["notify"].eval("self.finalize()") assert not notifier_has_callbacks() @pytest.mark.parametrize( "urgency,timeout", [(0, DEFAULT_TIMEOUT_LOW), (1, DEFAULT_TIMEOUT_NORMAL), (2, DEFAULT_TIMEOUT_URGENT)], ) @pytest.mark.skipif(shutil.which("notify-send") is None, reason="notify-send not installed.") @pytest.mark.usefixtures("dbus") def test_notifications_default_timeouts(manager_nospawn, minimal_conf_noscreen, urgency, timeout): notify.Notify.timeout_add = log_timeout widget = notify.Notify( default_timeout_low=DEFAULT_TIMEOUT_LOW, default_timeout=DEFAULT_TIMEOUT_NORMAL, default_timeout_urgent=DEFAULT_TIMEOUT_URGENT, ) config = minimal_conf_noscreen config.screens = [libqtile.config.Screen(top=Bar([widget], 10))] manager_nospawn.start(config) obj = manager_nospawn.c.widget["notify"] # Send first notification and check time and display time notif = [NS] notif.extend(notification("test", "test", urgency=urgency)[1]) subprocess.run(notif) _, delay = obj.eval("self.delay") assert delay == str(timeout) ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_volume.py������������������������������������������������������������0000664�0001750�0001750�00000004704�14762660347�020210� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������import cairocffi import pytest from libqtile import bar, images from libqtile.widget import Volume from test.widgets.conftest import TEST_DIR, FakeBar def test_images_fail(): vol = Volume(theme_path=TEST_DIR) with pytest.raises(images.LoadingError): vol.setup_images() def test_images_good(tmpdir, fake_bar, svg_img_as_pypath): names = ( "audio-volume-high.svg", "audio-volume-low.svg", "audio-volume-medium.svg", "audio-volume-muted.svg", ) for name in names: target = tmpdir.join(name) svg_img_as_pypath.copy(target) vol = Volume(theme_path=str(tmpdir)) vol.bar = fake_bar vol.length_type = bar.STATIC vol.length = 0 vol.setup_images() assert len(vol.surfaces) == len(names) for name, surfpat in vol.surfaces.items(): assert isinstance(surfpat, cairocffi.SurfacePattern) def test_emoji(): vol = Volume(emoji=True) vol.volume = -1 vol._update_drawer() assert vol.text == "\U0001f507" vol.volume = 29 vol._update_drawer() assert vol.text == "\U0001f508" vol.volume = 79 vol._update_drawer() assert vol.text == "\U0001f509" vol.volume = 80 vol._update_drawer() assert vol.text == "\U0001f50a" vol.is_mute = True vol._update_drawer() assert vol.text == "\U0001f507" def test_text(): fmt = "Volume: {}" vol = Volume(fmt=fmt) vol.volume = -1 vol._update_drawer() assert vol.text == "M" vol.volume = 50 vol._update_drawer() assert vol.text == "50%" def test_formats(): unmute_format = "Volume: {volume}%" mute_format = "Volume: {volume}% M" vol = Volume(unmute_format=unmute_format, mute_format=mute_format) vol.volume = 50 vol._update_drawer() assert vol.text == "Volume: 50%" vol.is_mute = True vol._update_drawer() assert vol.text == "Volume: 50% M" def test_foregrounds(fake_qtile, fake_window): foreground = "#dddddd" mute_foreground = None vol = Volume(foreground=foreground, mute_foreground=mute_foreground) fakebar = FakeBar([vol], window=fake_window) vol._configure(fake_qtile, fakebar) vol.volume = 50 vol._update_drawer() assert vol.foreground == foreground vol.mute_foreground = mute_foreground = "#888888" vol.is_mute = False vol._update_drawer() assert vol.foreground == foreground vol.is_mute = True vol._update_drawer() assert vol.foreground == mute_foreground ������������������������������������������������������������qtile-0.31.0/test/widgets/test_tasklist.py����������������������������������������������������������0000664�0001750�0001750�00000022073�14762660347�020536� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2023 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest import libqtile.config from libqtile import bar, layout from libqtile.config import Screen from libqtile.confreader import Config from libqtile.widget.tasklist import TaskList from test.layouts.layout_utils import assert_focused from test.test_scratchpad import is_spawned, spawn_cmd class TaskListTestWidget(TaskList): def __init__(self, *args, **kwargs): TaskList.__init__(self, *args, **kwargs) self._text = "" def calc_box_widths(self): ret_val = TaskList.calc_box_widths(self) self._text = "|".join(self.get_taskname(w) for w in self.windows) return ret_val def info(self): info = TaskList.info(self) info["text"] = self._text return info @pytest.fixture def override_xdg(request): return getattr(request, "param", False) xdg = pytest.mark.parametrize("override_xdg", [True], indirect=True) no_xdg = pytest.mark.parametrize("override_xdg", [False], indirect=True) @pytest.fixture def tasklist_manager(request, manager_nospawn, override_xdg, monkeypatch): monkeypatch.setattr("libqtile.widget.tasklist.has_xdg", override_xdg) config = getattr(request, "param", dict()) class TasklistConfig(Config): auto_fullscreen = True groups = [ libqtile.config.ScratchPad( "SCRATCHPAD", dropdowns=[ libqtile.config.DropDown("dd-a", spawn_cmd("dd-a"), on_focus_lost_hide=False), ], ), libqtile.config.Group("a"), libqtile.config.Group("b"), ] layouts = [layout.Stack()] floating_layout = libqtile.resources.default_config.floating_layout keys = [] mouse = [] screens = [Screen(top=bar.Bar([TaskListTestWidget(name="tasklist", **config)], 28))] manager_nospawn.start(TasklistConfig) yield manager_nospawn def configure_tasklist(**config): """Decorator to pass configuration to widget.""" return pytest.mark.parametrize("tasklist_manager", [config], indirect=True) def test_tasklist_defaults(tasklist_manager): widget = tasklist_manager.c.widget["tasklist"] tasklist_manager.test_window("One") tasklist_manager.test_window("Two") assert widget.info()["text"] == "One|Two" # Test floating tasklist_manager.c.window.toggle_floating() assert widget.info()["text"] == "One|V Two" tasklist_manager.c.window.toggle_floating() assert widget.info()["text"] == "One|Two" # Test maximize tasklist_manager.c.window.toggle_maximize() assert widget.info()["text"] == "One|[] Two" tasklist_manager.c.window.toggle_maximize() assert widget.info()["text"] == "One|Two" # Test minimize tasklist_manager.c.window.toggle_minimize() assert widget.info()["text"] == "One|_ Two" tasklist_manager.c.window.toggle_minimize() assert widget.info()["text"] == "One|Two" def test_tasklist_skip_taskbar_defaults(tasklist_manager): widget = tasklist_manager.c.widget["tasklist"] tasklist_manager.c.group["SCRATCHPAD"].dropdown_reconfigure("dd-a") tasklist_manager.test_window("one") assert_focused(tasklist_manager, "one") # dd-a has no window associated yet assert "window" not in tasklist_manager.c.group["SCRATCHPAD"].dropdown_info("dd-a") # First toggling: wait for window tasklist_manager.c.group["SCRATCHPAD"].dropdown_toggle("dd-a") is_spawned(tasklist_manager, "dd-a") assert_focused(tasklist_manager, "dd-a") assert ( tasklist_manager.c.group["SCRATCHPAD"].dropdown_info("dd-a")["window"]["name"] == "dd-a" ) if tasklist_manager.c.core.info()["backend"] == "x11": # check that window's _NET_WM_STATE contains _NET_WM_STATE_SKIP_TASKBAR net_wm_state = tasklist_manager.c.window.eval( 'self.window.get_property("_NET_WM_STATE", "ATOM", unpack=int)' )[1] skip_taskbar = tasklist_manager.c.window.eval( 'self.qtile.core.conn.atoms["_NET_WM_STATE_SKIP_TASKBAR"]' )[1] assert skip_taskbar in net_wm_state assert tasklist_manager.c.window.eval("self.window.get_wm_type()")[1] == "normal" assert widget.info()["text"] == "one" @configure_tasklist(txt_minimized="(min) ", txt_maximized="(max) ", txt_floating="(float) ") def test_tasklist_custom_text(tasklist_manager): widget = tasklist_manager.c.widget["tasklist"] tasklist_manager.test_window("One") tasklist_manager.test_window("Two") assert widget.info()["text"] == "One|Two" # Test floating tasklist_manager.c.window.toggle_floating() assert widget.info()["text"] == "One|(float) Two" tasklist_manager.c.window.toggle_floating() assert widget.info()["text"] == "One|Two" # Test maximize tasklist_manager.c.window.toggle_maximize() assert widget.info()["text"] == "One|(max) Two" tasklist_manager.c.window.toggle_maximize() assert widget.info()["text"] == "One|Two" # Test minimize tasklist_manager.c.window.toggle_minimize() assert widget.info()["text"] == "One|(min) Two" tasklist_manager.c.window.toggle_minimize() assert widget.info()["text"] == "One|Two" @configure_tasklist(markup_minimized="_{}_", markup_maximized="[{}]", markup_floating="V{}V") def test_tasklist_custom_markup(tasklist_manager): """markup_* options override txt_*""" widget = tasklist_manager.c.widget["tasklist"] tasklist_manager.test_window("One") tasklist_manager.test_window("Two") assert widget.info()["text"] == "One|Two" # Test floating tasklist_manager.c.window.toggle_floating() assert widget.info()["text"] == "One|VTwoV" tasklist_manager.c.window.toggle_floating() assert widget.info()["text"] == "One|Two" # Test maximize tasklist_manager.c.window.toggle_maximize() assert widget.info()["text"] == "One|[Two]" tasklist_manager.c.window.toggle_maximize() assert widget.info()["text"] == "One|Two" # Test minimize tasklist_manager.c.window.toggle_minimize() assert widget.info()["text"] == "One|_Two_" tasklist_manager.c.window.toggle_minimize() assert widget.info()["text"] == "One|Two" @configure_tasklist(markup_focused="({})", markup_focused_floating="[{}]") def test_tasklist_focused_and_floating(tasklist_manager): widget = tasklist_manager.c.widget["tasklist"] tasklist_manager.test_window("One") tasklist_manager.test_window("Two") assert widget.info()["text"] == "One|(Two)" # Test floating tasklist_manager.c.window.toggle_floating() assert widget.info()["text"] == "One|[Two]" tasklist_manager.c.window.toggle_floating() assert widget.info()["text"] == "One|(Two)" @configure_tasklist(margin=0) def test_tasklist_click_task(tasklist_manager): tasklist_manager.test_window("One") tasklist_manager.test_window("Two") # Current focused window is "Two" assert tasklist_manager.c.window.info()["name"] == "Two" # Click in top left corner of bar means we click on "One" # which should focus the window # margin is set to 0 as value set by widget_defaults means text would otherwise # mean text does not start at x=0 tasklist_manager.c.bar["top"].fake_button_press(0, 0, 1) assert tasklist_manager.c.window.info()["name"] == "One" @xdg @configure_tasklist(theme_mode="non-existent-mode") @pytest.mark.xfail def test_tasklist_bad_theme_mode(tasklist_manager): msgs = tasklist_manager.get_log_buffer() assert "Unexpected theme_mode (non-existent-mode). Theme icons will be disabled." in msgs @no_xdg @configure_tasklist(theme_mode="non-existent-mode") @pytest.mark.xfail def test_tasklist_no_xdg(tasklist_manager): msgs = tasklist_manager.get_log_buffer() assert "You must install pyxdg to use theme icons." in msgs @configure_tasklist(stretch=False) def test_tasklist_no_stretch(tasklist_manager): widget = tasklist_manager.c.widget["tasklist"] tasklist_manager.test_window("One") width_one = widget.info()["width"] tasklist_manager.test_window("Two") width_two = widget.info()["width"] assert width_one != width_two ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_vertical_clock.py����������������������������������������������������0000664�0001750�0001750�00000007560�14762660347�021670� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2024 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import datetime import sys from importlib import reload import pytest from libqtile.config import Bar, Screen from libqtile.confreader import Config from libqtile.widget import vertical_clock # Mock Datetime object that returns a set datetime and also # has a simplified timezone method to check functionality of # the widget. class MockDatetime(datetime.datetime): @classmethod def now(cls, *args, **kwargs): return cls(2024, 1, 1, 10, 20, 30) def astimezone(self, tzone=None): if tzone is None: return self return self + tzone.utcoffset(None) @pytest.fixture def patched_clock(monkeypatch): # Stop system importing these modules in case they exist on environment monkeypatch.setitem(sys.modules, "pytz", None) monkeypatch.setitem(sys.modules, "dateutil", None) monkeypatch.setitem(sys.modules, "dateutil.tz", None) # Reload module to force ImportErrors reload(vertical_clock) # Override datetime. # This is key for testing as we can fix time. monkeypatch.setattr("libqtile.widget.vertical_clock.datetime", MockDatetime) class TestVerticalClock(vertical_clock.VerticalClock): def __init__(self, **config): vertical_clock.VerticalClock.__init__(self, **config) self.name = "verticalclock" def info(self): info = vertical_clock.VerticalClock.info(self) info["text"] = "|".join(layout.text for layout in self.layouts) return info yield TestVerticalClock @pytest.fixture(scope="function") def vclock_manager(manager_nospawn, request, patched_clock): class VClockConfig(Config): screens = [ Screen( left=Bar( [ patched_clock( **getattr(request, "param", dict()), ) ], 30, ) ) ] manager_nospawn.start(VClockConfig) yield manager_nospawn def config(**kwargs): return pytest.mark.parametrize("vclock_manager", [kwargs], indirect=True) def test_vclock_default(vclock_manager): assert vclock_manager.c.widget["verticalclock"].info()["text"] == "10|20" @config(format=["%H", "%M", "-", "%d", "%m", "%Y"]) def test_vclock_extra_lines(vclock_manager): assert vclock_manager.c.widget["verticalclock"].info()["text"] == "10|20|-|01|01|2024" @pytest.mark.parametrize( "vclock_manager", [ dict(fontsize=[10]), # too few dict(fontsize=[10, 20, 30, 40]), # too many dict(foreground=["fff"]), # too few dict(foreground=["fff"] * 4), # too many ], indirect=True, ) def test_vclock_invalid_configs(vclock_manager): assert vclock_manager.c.bar["left"].info()["widgets"][0]["name"] == "configerrorwidget" ������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_widgetbox.py���������������������������������������������������������0000664�0001750�0001750�00000015155�14762660347�020677� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021-22 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest import libqtile.config from libqtile.widget import Systray, TextBox, WidgetBox from test.widgets.conftest import FakeBar def test_widgetbox_widget(fake_qtile, fake_window): tb_one = TextBox(name="tb_one", text="TB ONE") tb_two = TextBox(name="tb_two", text="TB TWO") # Give widgetbox invalid value for button location widget_box = WidgetBox(widgets=[tb_one, tb_two], close_button_location="middle", fontsize=10) # Create a bar and set attributes needed to run widget fakebar = FakeBar([widget_box], window=fake_window) # Configure the widget box widget_box._configure(fake_qtile, fakebar) # Invalid value should be corrected to default assert widget_box.close_button_location == "left" # Check only widget in bar is widgetbox assert fakebar.widgets == [widget_box] # Open box widget_box.toggle() # Check it's open assert widget_box.box_is_open # Default text position is left assert fakebar.widgets == [widget_box, tb_one, tb_two] # Close box widget_box.toggle() # Check it's closed assert not widget_box.box_is_open # Check widgets have been removed assert fakebar.widgets == [widget_box] # Move button to right-hand side widget_box.close_button_location = "right" # Re-open box with new layout widget_box.toggle() # Now widgetbox is on the right assert fakebar.widgets == [tb_one, tb_two, widget_box] def test_widgetbox_start_opened(manager_nospawn, minimal_conf_noscreen): config = minimal_conf_noscreen tbox = TextBox(text="Text Box") widget_box = WidgetBox(widgets=[tbox], start_opened=True) config.screens = [libqtile.config.Screen(top=libqtile.bar.Bar([widget_box], 10))] manager_nospawn.start(config) topbar = manager_nospawn.c.bar["top"] widgets = [w["name"] for w in topbar.info()["widgets"]] assert widgets == ["widgetbox", "textbox"] def test_widgetbox_mirror(manager_nospawn, minimal_conf_noscreen): config = minimal_conf_noscreen tbox = TextBox(text="Text Box") config.screens = [ libqtile.config.Screen(top=libqtile.bar.Bar([tbox, WidgetBox(widgets=[tbox])], 10)) ] manager_nospawn.start(config) manager_nospawn.c.widget["widgetbox"].toggle() topbar = manager_nospawn.c.bar["top"] widgets = [w["name"] for w in topbar.info()["widgets"]] assert widgets == ["textbox", "widgetbox", "mirror"] def test_widgetbox_mouse_click(manager_nospawn, minimal_conf_noscreen): config = minimal_conf_noscreen tbox = TextBox(text="Text Box") config.screens = [ libqtile.config.Screen(top=libqtile.bar.Bar([WidgetBox(widgets=[tbox])], 10)) ] manager_nospawn.start(config) topbar = manager_nospawn.c.bar["top"] assert len(topbar.info()["widgets"]) == 1 topbar.fake_button_press(0, 0, button=1) assert len(topbar.info()["widgets"]) == 2 topbar.fake_button_press(0, 0, button=1) assert len(topbar.info()["widgets"]) == 1 def test_widgetbox_with_systray_reconfigure_screens_box_open( manager_nospawn, minimal_conf_noscreen, backend_name ): """Check that Systray does not crash when inside an open widgetbox.""" if backend_name == "wayland": pytest.skip("Skipping test on Wayland.") config = minimal_conf_noscreen config.screens = [ libqtile.config.Screen(top=libqtile.bar.Bar([WidgetBox(widgets=[Systray()])], 10)) ] manager_nospawn.start(config) topbar = manager_nospawn.c.bar["top"] assert len(topbar.info()["widgets"]) == 1 manager_nospawn.c.widget["widgetbox"].toggle() assert len(topbar.info()["widgets"]) == 2 manager_nospawn.c.reconfigure_screens() assert len(topbar.info()["widgets"]) == 2 names = [w["name"] for w in topbar.info()["widgets"]] assert names == ["widgetbox", "systray"] def test_widgetbox_with_systray_reconfigure_screens_box_closed( manager_nospawn, minimal_conf_noscreen, backend_name ): """Check that Systray does not crash when inside a closed widgetbox.""" if backend_name == "wayland": pytest.skip("Skipping test on Wayland.") config = minimal_conf_noscreen config.screens = [ libqtile.config.Screen(top=libqtile.bar.Bar([WidgetBox(widgets=[Systray()])], 10)) ] manager_nospawn.start(config) topbar = manager_nospawn.c.bar["top"] assert len(topbar.info()["widgets"]) == 1 manager_nospawn.c.reconfigure_screens() assert len(topbar.info()["widgets"]) == 1 # Check that we've still got a Systray widget in the box. _, name = manager_nospawn.c.widget["widgetbox"].eval("self.widgets[0].name") assert name == "systray" def test_deprecated_configuration(caplog): tray = Systray() box = WidgetBox([tray]) assert box.widgets == [tray] assert "The use of a positional argument in WidgetBox is deprecated." in caplog.text def test_widgetbox_open_close_commands(manager_nospawn, minimal_conf_noscreen): config = minimal_conf_noscreen tbox = TextBox(text="Text Box") widget_box = WidgetBox(widgets=[tbox]) config.screens = [libqtile.config.Screen(top=libqtile.bar.Bar([widget_box], 10))] manager_nospawn.start(config) topbar = manager_nospawn.c.bar["top"] widget = manager_nospawn.c.widget["widgetbox"] def count(): return len(topbar.info()["widgets"]) assert count() == 1 widget.open() assert count() == 2 widget.open() assert count() == 2 widget.close() assert count() == 1 widget.close() assert count() == 1 widget.toggle() assert count() == 2 widget.toggle() assert count() == 1 �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_windowname.py��������������������������������������������������������0000664�0001750�0001750�00000007262�14762660347�021053� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # Widget specific tests import pytest import libqtile.config from libqtile import bar, layout, widget from libqtile.config import Screen from libqtile.confreader import Config class WindowNameConfig(Config): auto_fullscreen = True groups = [libqtile.config.Group("a"), libqtile.config.Group("b")] layouts = [layout.Max()] floating_layout = libqtile.resources.default_config.floating_layout keys = [] mouse = [] fake_screens = [ Screen( top=bar.Bar( [ widget.WindowName(), ], 24, ), x=0, y=0, width=900, height=480, ), Screen( top=bar.Bar( [ widget.WindowName(for_current_screen=True), ], 24, ), x=0, y=480, width=900, height=480, ), ] screens = [] windowname_config = pytest.mark.parametrize("manager", [WindowNameConfig], indirect=True) @windowname_config def test_window_names(manager): def widget_text_on_screen(index): return manager.c.screen[index].bar["top"].info()["widgets"][0]["text"] # Screen 1's widget is set up with for_current_screen=True # This means that when screen 0 is active, screen 1's widget should show the same text assert widget_text_on_screen(0) == " " assert widget_text_on_screen(0) == widget_text_on_screen(1) # Load a window proc = manager.test_window("one") assert widget_text_on_screen(0) == "one" assert widget_text_on_screen(0) == widget_text_on_screen(1) # Maximize window manager.c.window.toggle_maximize() assert widget_text_on_screen(0) == "[] one" assert widget_text_on_screen(0) == widget_text_on_screen(1) # Minimize window manager.c.window.toggle_minimize() assert widget_text_on_screen(0) == "_ one" assert widget_text_on_screen(0) == widget_text_on_screen(1) # Float window manager.c.window.toggle_minimize() manager.c.window.toggle_floating() assert widget_text_on_screen(0) == "V one" assert widget_text_on_screen(0) == widget_text_on_screen(1) # Kill the window and check text again manager.kill_window(proc) assert widget_text_on_screen(0) == " " assert widget_text_on_screen(0) == widget_text_on_screen(1) # Quick test to check for_current_screen=False works manager.c.to_screen(1) proc = manager.test_window("one") assert widget_text_on_screen(0) == " " assert widget_text_on_screen(1) == "one" manager.kill_window(proc) ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_redshift.py����������������������������������������������������������0000664�0001750�0001750�00000017300�14762660347�020505� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2024 Saath Satheeshkumar (saths008) # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest from libqtile.config import Bar, Screen from libqtile.confreader import Config from libqtile.widget import redshift def mock_run(argv, check): pass @pytest.fixture(scope="function") def patched_redshift(monkeypatch): class PatchedRedshift(redshift.Redshift): def __init__(self, **config): monkeypatch.setattr("subprocess.run", mock_run) redshift.Redshift.__init__(self, **config) self.name = "redshift" yield PatchedRedshift @pytest.fixture(scope="function") def redshift_manager(manager_nospawn, request, patched_redshift): if manager_nospawn.backend.name == "wayland": pytest.skip("Skipping test on Wayland.") class GroupConfig(Config): screens = [ Screen( top=Bar( [ patched_redshift( update_interval=10, **getattr(request, "param", dict()), ) ], 30, ) ) ] manager_nospawn.start(GroupConfig) yield manager_nospawn def config(**kwargs): return pytest.mark.parametrize("redshift_manager", [kwargs], indirect=True) def test_defaults(redshift_manager): widget = redshift_manager.c.widget["redshift"] def text(): return widget.info()["text"] def click(): redshift_manager.c.bar["top"].fake_button_press(0, 0, 1) disabled_txt = "󱠃" enabled_txt = "󰛨" assert text() == disabled_txt scroll_vals = [ "Brightness: 1.0", "Temperature: 1700", "Gamma: 1.0:1.0:1.0", enabled_txt, ] click() for _, val in enumerate(scroll_vals): widget.scroll_up() assert text() == val click() for _ in range(len(scroll_vals)): widget.scroll_down() # Test that scroll only works when # widget is enabled assert text() == disabled_txt @config(disabled_txt="Redshift disabled", enabled_txt="Redshift enabled") def test_changed_default_txt_non_fmted(redshift_manager): widget = redshift_manager.c.widget["redshift"] def text(): return widget.info()["text"] def click(): redshift_manager.c.bar["top"].fake_button_press(0, 0, 1) disabled_txt = "Redshift disabled" enabled_txt = "Redshift enabled" assert text() == disabled_txt click() assert text() == enabled_txt scroll_vals = [ "Brightness: 1.0", "Temperature: 1700", "Gamma: 1.0:1.0:1.0", enabled_txt, ] for _, val in enumerate(scroll_vals): widget.scroll_up() assert text() == val # move enabled_txt to the first index scroll_vals.remove(enabled_txt) scroll_vals = [enabled_txt] + scroll_vals for _, val in enumerate(reversed(scroll_vals)): widget.scroll_down() assert text() == val @config( brightness=0.4, disabled_txt="brightness{brightness}, temp{temperature}, r{gamma_red}, g{gamma_green}, b{gamma_blue}, is_enabled{is_enabled}", enabled_txt="brightness{brightness}, temp{temperature}, r{gamma_red}, g{gamma_green}, b{gamma_blue}, is_enabled{is_enabled}", gamma_red=0.2, gamma_green=0.2, gamma_blue=0.2, temperature=1200, ) def test_changed_default_txt_fmted(redshift_manager): widget = redshift_manager.c.widget["redshift"] def text(): return widget.info()["text"] def click(): redshift_manager.c.bar["top"].fake_button_press(0, 0, 1) brightness = 0.4 gamma_red = 0.2 gamma_green = 0.2 gamma_blue = 0.2 gamma_val = redshift.GammaGroup(gamma_red, gamma_green, gamma_blue) temp = 1200 comm_str = f"brightness{brightness}, temp{temp}, r{gamma_red}, g{gamma_green}, b{gamma_blue}" disabled_txt = comm_str + f", is_enabled{False}" enabled_txt = comm_str + f", is_enabled{True}" assert text() == disabled_txt click() assert text() == enabled_txt scroll_vals = [ f"Brightness: {brightness}", f"Temperature: {temp}", f"Gamma: {gamma_val._redshift_fmt()}", enabled_txt, ] for _, val in enumerate(scroll_vals): widget.scroll_up() assert text() == val scroll_vals.remove(enabled_txt) scroll_vals = [enabled_txt] + scroll_vals for _, val in enumerate(reversed(scroll_vals)): widget.scroll_down() assert text() == val @config( disabled_txt="Disabled", enabled_txt="Enabled t:{temperature} b:{brightness}", brightness=0.5, brightness_step=0.2, temperature=1200, temperature_step=101, ) def test_increase_decrease_temp_brightness(redshift_manager): widget = redshift_manager.c.widget["redshift"] def text(): return widget.info()["text"] def click(): redshift_manager.c.bar["top"].fake_button_press(0, 0, 1) def right_click(): redshift_manager.c.bar["top"].fake_button_press(0, 0, 3) disabled_txt = "Disabled" enabled_txt = "Enabled t:1200 b:0.5" assert text() == disabled_txt click() assert text() == enabled_txt widget.scroll_up() # Test that the right click respects brightness boundaries brightness_vals = [0.5, 0.7, 0.9, 1.0, 1.0, 1.0] for _, val in enumerate(brightness_vals): assert text() == f"Brightness: {val}" right_click() # Test that the left click respects brightness boundaries brightness_vals = [1.0, 0.8, 0.6, 0.4, 0.2, 0.1, 0.1] for _, val in enumerate(brightness_vals): assert text() == f"Brightness: {val}" click() widget.scroll_down() # Enabled assert text() == "Enabled t:1200 b:0.1" widget.scroll_down() # Gamma widget.scroll_down() # Temperature # Test that the right click respects temperature boundaries temp_vals = [1200] temp_lower_bound = 1000 temp_upper_bound = 25000 while temp_vals[-1] < temp_upper_bound: new_temp_val = temp_vals[-1] + 101 new_temp_val = min(new_temp_val, temp_upper_bound) temp_vals.append(new_temp_val) for _, val in enumerate(temp_vals): assert text() == f"Temperature: {val}" right_click() # Test that the left click respects temperature boundaries temp_vals = [temp_upper_bound] while temp_vals[-1] > temp_lower_bound: new_temp_val = temp_vals[-1] - 101 new_temp_val = max(new_temp_val, temp_lower_bound) temp_vals.append(new_temp_val) for _, val in enumerate(temp_vals): assert text() == f"Temperature: {val}" click() widget.scroll_up() # Gamma widget.scroll_up() # Temperature assert text() == "Enabled t:1000 b:0.1" ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_textbox.py�����������������������������������������������������������0000664�0001750�0001750�00000004257�14762660347�020401� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # Widget specific tests import pytest import libqtile.bar import libqtile.config from libqtile import widget @pytest.mark.parametrize("position", ["top", "bottom", "left", "right"]) def test_text_box_bar_orientations(manager_nospawn, minimal_conf_noscreen, position): """Text boxes are available on any bar position.""" textbox = widget.TextBox(text="Testing") config = minimal_conf_noscreen config.screens = [libqtile.config.Screen(**{position: libqtile.bar.Bar([textbox], 10)})] manager_nospawn.start(config) tbox = manager_nospawn.c.widget["textbox"] assert tbox.info()["text"] == "Testing" tbox.update("Updated") assert tbox.info()["text"] == "Updated" def test_text_box_max_chars(manager_nospawn, minimal_conf_noscreen): """Text boxes are available on any bar position.""" textbox = widget.TextBox(text="Testing", max_chars=4) config = minimal_conf_noscreen config.screens = [libqtile.config.Screen(top=libqtile.bar.Bar([textbox], 10))] manager_nospawn.start(config) tbox = manager_nospawn.c.widget["textbox"] assert tbox.info()["text"] == "Test…" �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_wlan.py��������������������������������������������������������������0000664�0001750�0001750�00000007600�14762660347�017640� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # Widget specific tests import builtins import sys from importlib import reload from types import ModuleType import pytest import libqtile.config from libqtile.bar import Bar def no_op(*args, **kwargs): pass def mock_open(output): class MockOpen: def __init__(self, *args): self.output = output def __enter__(self): return self def __exit__(*exc): return None def read(self): return self.output return MockOpen class MockIwlib(ModuleType): DATA = { "wlan0": { "NWID": b"Auto", "Frequency": b"5.18 GHz", "Access Point": b"12:34:56:78:90:AB", "BitRate": b"650 Mb/s", "ESSID": b"QtileNet", "Mode": b"Managed", "stats": {"quality": 49, "level": 190, "noise": 0, "updated": 75}, }, "wlan1": { "ESSID": None, }, } @classmethod def get_iwconfig(cls, interface): return cls.DATA.get(interface, dict()) # Patch the widget with our mock iwlib module. @pytest.fixture def patched_wlan(monkeypatch): monkeypatch.setitem(sys.modules, "iwlib", MockIwlib("iwlib")) from libqtile.widget import wlan # Reload fixes cases where psutil may have been imported previously reload(wlan) yield wlan @pytest.mark.parametrize( "kwargs,expected", [ ({}, "QtileNet 49/70"), ({"format": "{essid} {percent:2.0%}"}, "QtileNet 70%"), ({"interface": "wlan1"}, "Disconnected"), ], ) def test_wlan_display(minimal_conf_noscreen, manager_nospawn, patched_wlan, kwargs, expected): widget = patched_wlan.Wlan(**kwargs) config = minimal_conf_noscreen config.screens = [libqtile.config.Screen(top=Bar([widget], 10))] manager_nospawn.start(config) text = manager_nospawn.c.bar["top"].info()["widgets"][0]["text"] assert text == expected def test_wlan_display_escape_essid( minimal_conf_noscreen, manager_nospawn, patched_wlan, monkeypatch ): """Test escaping of pango markup in ESSID""" monkeypatch.setitem(MockIwlib.DATA["wlan0"], "ESSID", b"A&B") widget = patched_wlan.Wlan(format="{essid}") assert widget.poll() == "A&B" @pytest.mark.parametrize( "kwargs,state,expected", [ ({"interface": "wlan1", "use_ethernet": True}, "up", "eth"), ({"interface": "wlan1", "use_ethernet": True}, "down", "Disconnected"), ( {"interface": "wlan1", "use_ethernet": True, "ethernet_message": "Wired"}, "up", "Wired", ), ], ) def test_ethernet( minimal_conf_noscreen, manager_nospawn, patched_wlan, kwargs, state, expected, monkeypatch ): monkeypatch.setattr(builtins, "open", mock_open(state)) widget = patched_wlan.Wlan(**kwargs) assert widget.poll() == expected ��������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_df.py����������������������������������������������������������������0000664�0001750�0001750�00000006137�14762660347�017274� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # Widget specific tests import sys from importlib import reload from types import ModuleType import pytest from libqtile.widget import df from test.widgets.conftest import FakeBar class FakeOS(ModuleType): class statvfs: # noqa: N801 def __init__(self, *args, **kwargs): pass @property def f_frsize(self): return 4096 @property def f_blocks(self): return 60000000 @property def f_bfree(self): return 15000000 @property def f_bavail(self): return 10000000 # Patches os.stavfs gives these values for df widget: # unit: G # size = 228 # free = 57 # user_free = 38 # ratio (user_free / size) = 83.3333% @pytest.fixture() def patched_df(monkeypatch): monkeypatch.setitem(sys.modules, "os", FakeOS("os")) reload(df) @pytest.mark.usefixtures("patched_df") def test_df_no_warning(fake_qtile, fake_window): """Test no text when free space over threshold""" df1 = df.DF() fakebar = FakeBar([df1], window=fake_window) df1._configure(fake_qtile, fakebar) text = df1.poll() assert text == "" df1.draw() assert df1.layout.colour == df1.foreground @pytest.mark.usefixtures("patched_df") def test_df_always_visible(fake_qtile, fake_window): """Test text is always displayed""" df2 = df.DF(visible_on_warn=False) fakebar = FakeBar([df2], window=fake_window) df2._configure(fake_qtile, fakebar) text = df2.poll() # See values above assert text == "/ (38G|83%)" df2.draw() assert df2.layout.colour == df2.foreground @pytest.mark.usefixtures("patched_df") def test_df_warn_space(fake_qtile, fake_window): """ Test text is visible and colour changes when space below threshold """ df3 = df.DF(warn_space=40) fakebar = FakeBar([df3], window=fake_window) df3._configure(fake_qtile, fakebar) text = df3.poll() # See values above assert text == "/ (38G|83%)" df3.draw() assert df3.layout.colour == df3.warn_color ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_openweather.py�������������������������������������������������������0000664�0001750�0001750�00000007771�14762660347�021231� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import time import pytest import libqtile.config import libqtile.widget.open_weather from libqtile.bar import Bar def mock_fetch(*args, **kwargs): return { "coord": {"lon": -0.13, "lat": 51.51}, "weather": [ { "id": 300, "main": "Drizzle", "description": "light intensity drizzle", "icon": "09d", } ], "base": "stations", "main": { "temp": 280.15 - 273.15, "pressure": 1012, "humidity": 81, "temp_min": 279.15 - 273.15, "temp_max": 281.15 - 273.15, }, "visibility": 10000, "wind": {"speed": 4.1, "deg": 80}, "clouds": {"all": 90}, "dt": 1485789600, "sys": { "type": 1, "id": 5091, "message": 0.0103, "country": "GB", "sunrise": 1485762037, "sunset": 1485794875, }, "id": 2643743, "name": "London", "cod": 200, } @pytest.fixture def patch_openweather(request, monkeypatch): monkeypatch.setattr("libqtile.widget.generic_poll_text.GenPollUrl.fetch", mock_fetch) monkeypatch.setattr("libqtile.widget.open_weather.time.localtime", time.gmtime) yield libqtile.widget.open_weather @pytest.mark.parametrize( "params,expected", [ ({"location": "London"}, "London: 7.0 °C 81% light intensity drizzle"), ( {"location": "London", "format": "{location_city}: {sunrise} {sunset}"}, "London: 07:40 16:47", ), ( { "location": "London", "format": "{location_city}: {wind_speed} {wind_deg} {wind_direction}", }, "London: 4.1 80 E", ), ({"location": "London", "format": "{location_city}: {icon}"}, "London: 🌧️"), ], ) def test_openweather_parse( patch_openweather, minimal_conf_noscreen, manager_nospawn, params, expected ): """Check widget parses output correctly for display.""" config = minimal_conf_noscreen config.screens = [ libqtile.config.Screen(top=Bar([patch_openweather.OpenWeather(**params)], 10)) ] manager_nospawn.start(config) manager_nospawn.c.widget["openweather"].eval("self.update(self.poll())") info = manager_nospawn.c.widget["openweather"].info()["text"] assert info == expected @pytest.mark.parametrize( "params,vals", [ ({"location": "London"}, ["q=London"]), ({"cityid": 2643743}, ["id=2643743"]), ({"zip": 90210}, ["zip=90210"]), ( {"coordinates": {"longitude": "77.22", "latitude": "28.67"}}, ["lat=28.67", "lon=77.22"], ), ], ) def test_url(patch_openweather, params, vals): """Test that url is created correctly.""" widget = patch_openweather.OpenWeather(**params) url = widget.url for val in vals: assert val in url �������qtile-0.31.0/test/widgets/test_sep.py���������������������������������������������������������������0000664�0001750�0001750�00000004366�14762660347�017474� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # Widget specific tests import pytest import libqtile.bar import libqtile.config import libqtile.confreader import libqtile.layout from libqtile import widget sep = widget.Sep() parameters = [ (libqtile.config.Screen(top=libqtile.bar.Bar([sep], 10)), "top", "width"), (libqtile.config.Screen(left=libqtile.bar.Bar([sep], 10)), "left", "height"), ] @pytest.mark.parametrize("screen,location,attribute", parameters) def test_orientations(manager_nospawn, minimal_conf_noscreen, screen, location, attribute): config = minimal_conf_noscreen config.screens = [screen] manager_nospawn.start(config) bar = manager_nospawn.c.bar[location] w = bar.info()["widgets"][0] assert w[attribute] == 3 def test_padding_and_width(manager_nospawn, minimal_conf_noscreen): sep = widget.Sep(padding=5, linewidth=7) config = minimal_conf_noscreen config.screens = [libqtile.config.Screen(top=libqtile.bar.Bar([sep], 10))] manager_nospawn.start(config) topbar = manager_nospawn.c.bar["top"] w = topbar.info()["widgets"][0] assert w["width"] == 12 def test_deprecated_config(): sep = widget.Sep(height_percent=80) assert sep.size_percent == 80 ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_cpu.py���������������������������������������������������������������0000664�0001750�0001750�00000004065�14762660347�017470� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # Widget specific tests import sys from importlib import reload from types import ModuleType import pytest import libqtile.config import libqtile.widget from libqtile.bar import Bar class MockPsutil(ModuleType): __version__ = "5.8.0" @classmethod def cpu_percent(cls): return 2.6 @classmethod def cpu_freq(cls): class Freq: def __init__(self): self.current = 500.067 self.min = 400.0 self.max = 2800.0 return Freq() @pytest.fixture def cpu_manager(monkeypatch, manager_nospawn, minimal_conf_noscreen): monkeypatch.setitem(sys.modules, "psutil", MockPsutil("psutil")) from libqtile.widget import cpu reload(cpu) config = minimal_conf_noscreen config.screens = [libqtile.config.Screen(top=Bar([cpu.CPU()], 10))] manager_nospawn.start(config) yield manager_nospawn def test_cpu(cpu_manager): assert cpu_manager.c.widget["cpu"].info()["text"] == "CPU 0.5GHz 2.6%" ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_bluetooth.py���������������������������������������������������������0000664�0001750�0001750�00000032046�14762660347�020706� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2023 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import asyncio import multiprocessing import os import shutil import signal import subprocess import time from enum import Enum import pytest from dbus_fast.aio import MessageBus from dbus_fast.constants import BusType, PropertyAccess from dbus_fast.service import ServiceInterface, dbus_property, method from libqtile.bar import Bar from libqtile.config import Screen from libqtile.widget.bluetooth import BLUEZ_ADAPTER, BLUEZ_BATTERY, BLUEZ_DEVICE, Bluetooth from test.conftest import BareConfig from test.helpers import Retry ADAPTER_PATH = "/org/bluez/hci0" ADAPTER_NAME = "qtile_bluez" BLUEZ_SERVICE = "test.qtile.bluez" class DeviceState(Enum): UNPAIRED = 1 PAIRED = 2 CONNECTED = 3 class ForceSessionBusType: SESSION = BusType.SESSION SYSTEM = BusType.SESSION class Device(ServiceInterface): def __init__(self, *args, alias, state, adapter, address, **kwargs): ServiceInterface.__init__(self, *args, **kwargs) self._state = state self._name = alias self._adapter = adapter self._address = ":".join([address] * 8) @method() def Pair(self): # noqa: F821, N802 self._state = DeviceState.PAIRED self.emit_properties_changed({"Paired": True, "Connected": False}) @method() def Connect(self): # noqa: F821, N802 self._state = DeviceState.CONNECTED self.emit_properties_changed({"Paired": True, "Connected": True}) @method() def Disconnect(self): # noqa: F821, N802 self._state = DeviceState.PAIRED self.emit_properties_changed({"Paired": True, "Connected": False}) @dbus_property(access=PropertyAccess.READ) def Name(self) -> "s": # noqa: F821, N802 return self._name @dbus_property(access=PropertyAccess.READ) def Address(self) -> "s": # noqa: F821, N802 return self._address @dbus_property(access=PropertyAccess.READ) def Adapter(self) -> "s": # noqa: F821, N802 return self._adapter @dbus_property(access=PropertyAccess.READ) def Connected(self) -> "b": # noqa: F821, N802 return self._state == DeviceState.CONNECTED @dbus_property(access=PropertyAccess.READ) def Paired(self) -> "b": # noqa: F821, N802 return self._state != DeviceState.UNPAIRED class Adapter(ServiceInterface): def __init__(self, *args, **kwargs): ServiceInterface.__init__(self, *args, **kwargs) self._name = ADAPTER_NAME self._powered = True self._discovering = False @dbus_property(access=PropertyAccess.READ) def Name(self) -> "s": # noqa: F821, N802 return self._name @dbus_property() def Powered(self) -> "b": # noqa: F821, N802 return self._powered @Powered.setter def Powered_setter(self, state: "b"): # noqa: F821, N802 self._powered = state self.emit_properties_changed({"Powered": state}) @dbus_property(access=PropertyAccess.READ) def Discovering(self) -> "b": # noqa: F821, N802 return self._discovering @method() def StartDiscovery(self): # noqa: F821, N802 self._discovering = True self.emit_properties_changed({"Discovering": self._discovering}) @method() def StopDiscovery(self): # noqa: F821, N802 self._discovering = False self.emit_properties_changed({"Discovering": self._discovering}) class Battery(ServiceInterface): @dbus_property(PropertyAccess.READ) def Percentage(self) -> "d": # noqa: F821, N802 return 75 class QtileRoot(ServiceInterface): def __init__(self): super().__init__("org.qtile.root") class Bluez: """Class that runs fake UPower interface.""" async def start_server(self): """Connects to the bus and publishes 3 interfaces.""" bus = await MessageBus().connect() root = QtileRoot() bus.export("/", root) unpaired_device = Device( BLUEZ_DEVICE, alias="Earbuds", state=DeviceState.UNPAIRED, address="11", adapter=ADAPTER_PATH, ) paired_device = Device( BLUEZ_DEVICE, alias="Headphones", state=DeviceState.PAIRED, address="22", adapter=ADAPTER_PATH, ) connected_device = Device( BLUEZ_DEVICE, alias="Speaker", state=DeviceState.CONNECTED, address="33", adapter=ADAPTER_PATH, ) battery = Battery(BLUEZ_BATTERY) for d in [unpaired_device, paired_device, connected_device]: path = f"{ADAPTER_PATH}/dev_{d._address.replace(':', '_')}" bus.export(path, d) if d is connected_device: bus.export(path, battery) adapter = Adapter(BLUEZ_ADAPTER) bus.export(ADAPTER_PATH, adapter) # Request the service name await bus.request_name(BLUEZ_SERVICE) await asyncio.get_event_loop().create_future() def run(self): loop = asyncio.new_event_loop() loop.run_until_complete(self.start_server()) @pytest.fixture() def fake_dbus_daemon(monkeypatch): """Start a thread which publishes a fake bluez interface on dbus.""" # for Github CI/Ubuntu, dbus-launch is provided by "dbus-x11" package launcher = shutil.which("dbus-launch") # If dbus-launch can't be found then tests will fail so we # need to skip if launcher is None: pytest.skip("dbus-launch must be installed") # dbus-launch prints two lines which should be set as # environmental variables result = subprocess.run(launcher, capture_output=True) pid = None for line in result.stdout.decode().splitlines(): # dbus server addresses can have multiple "=" so # we use partition to split by the first one onle var, _, val = line.partition("=") # Use monkeypatch to set these variables so they are # removed at end of test. monkeypatch.setitem(os.environ, var, val) # We want the pid so we can kill the process when the # test is finished if var == "DBUS_SESSION_BUS_PID": try: pid = int(val) except ValueError: pass p = multiprocessing.Process(target=Bluez().run) p.start() # Pause for the dbus interface to come up time.sleep(1) yield # Stop the bus if pid: os.kill(pid, signal.SIGTERM) p.kill() @pytest.fixture def widget(monkeypatch): """Patch the widget to use the fake dbus service.""" monkeypatch.setattr("libqtile.widget.bluetooth.BLUEZ_SERVICE", BLUEZ_SERVICE) # Make dbus_fast always return the session bus address even if system bus is requested monkeypatch.setattr("libqtile.widget.bluetooth.BusType", ForceSessionBusType) yield Bluetooth @pytest.fixture def bluetooth_manager(request, widget, fake_dbus_daemon, manager_nospawn): class BluetoothConfig(BareConfig): screens = [Screen(top=Bar([widget(**getattr(request, "param", dict()))], 20))] manager_nospawn.start(BluetoothConfig) yield manager_nospawn @Retry(ignore_exceptions=(AssertionError,)) def wait_for_text(widget, text): assert widget.info()["text"] == text def test_defaults(bluetooth_manager): widget = bluetooth_manager.c.widget["bluetooth"] def text(): return widget.info()["text"] def click(): bluetooth_manager.c.bar["top"].fake_button_press(0, 0, 1) # Show prefix plus list of connected devices (1 connected at startup) wait_for_text(widget, "BT Speaker") widget.scroll_up() assert text() == f"Adapter: {ADAPTER_NAME} [*]" widget.scroll_up() assert text() == "Device: Earbuds [?]" widget.scroll_up() assert text() == "Device: Headphones [-]" widget.scroll_up() assert text() == "Device: Speaker (75.0%) [*]" widget.scroll_up() assert text() == "BT Speaker" widget.scroll_down() widget.scroll_down() assert text() == "Device: Headphones [-]" def test_device_actions(bluetooth_manager): widget = bluetooth_manager.c.widget["bluetooth"] def text(): return widget.info()["text"] def click(): bluetooth_manager.c.bar["top"].fake_button_press(0, 0, 1) wait_for_text(widget, "BT Speaker") widget.scroll_down() widget.scroll_down() wait_for_text(widget, "Device: Headphones [-]") # Clicking a paired device connects it click() wait_for_text(widget, "Device: Headphones [*]") # Clicking a connected device disconnects it click() wait_for_text(widget, "Device: Headphones [-]") # We want it connected for the last check click() # Clicking an unpaired device pairs and connects it widget.scroll_down() click() wait_for_text(widget, "Device: Earbuds [*]") # Clicking this now disconnects it but it remains paired click() wait_for_text(widget, "Device: Earbuds [-]") widget.scroll_down() widget.scroll_down() # 2 devices are now connected assert text() == "BT Headphones, Speaker" def test_adapter_actions(bluetooth_manager): widget = bluetooth_manager.c.widget["bluetooth"] def text(): return widget.info()["text"] def click(): bluetooth_manager.c.bar["top"].fake_button_press(0, 0, 1) wait_for_text(widget, "BT Speaker") widget.scroll_up() assert text() == f"Adapter: {ADAPTER_NAME} [*]" click() wait_for_text(widget, "Turn power off") click() wait_for_text(widget, "Turn power on") widget.scroll_up() assert text() == "Turn discovery on" click() wait_for_text(widget, "Turn discovery off") click() wait_for_text(widget, "Turn discovery on") widget.scroll_up() assert text() == "Exit" click() # Adapter power is now off wait_for_text(widget, f"Adapter: {ADAPTER_NAME} [-]") @pytest.mark.parametrize( "bluetooth_manager", [ { "symbol_connected": "C", "symbol_paired": "P", "symbol_unknown": "U", "symbol_powered": ("ON", "OFF"), } ], indirect=True, ) def test_custom_symbols(bluetooth_manager): widget = bluetooth_manager.c.widget["bluetooth"] def text(): return widget.info()["text"] def click(): bluetooth_manager.c.bar["top"].fake_button_press(0, 0, 1) wait_for_text(widget, "BT Speaker") widget.scroll_up() assert text() == f"Adapter: {ADAPTER_NAME} [ON]" click() wait_for_text(widget, "Turn power off") click() widget.scroll_up() widget.scroll_up() click() wait_for_text(widget, f"Adapter: {ADAPTER_NAME} [OFF]") widget.scroll_up() assert text() == "Device: Earbuds [U]" widget.scroll_up() assert text() == "Device: Headphones [P]" widget.scroll_up() assert text() == "Device: Speaker (75.0%) [C]" @pytest.mark.parametrize("bluetooth_manager", [{"default_show_battery": True}], indirect=True) def test_default_show_battery(bluetooth_manager): widget = bluetooth_manager.c.widget["bluetooth"] wait_for_text(widget, "BT Speaker (75.0%)") @pytest.mark.parametrize( "bluetooth_manager", [{"adapter_paths": ["/org/bluez/hci1"]}], indirect=True ) def test_missing_adapter(bluetooth_manager): widget = bluetooth_manager.c.widget["bluetooth"] def text(): return widget.info()["text"] wait_for_text(widget, "BT ") # No adapter or devices should be listed widget.scroll_up() assert text() == "BT " @pytest.mark.parametrize( "bluetooth_manager", [ { "default_text": "BT {connected_devices} {num_connected_devices} {adapters} {num_adapters}" } ], indirect=True, ) def test_default_text(bluetooth_manager): widget = bluetooth_manager.c.widget["bluetooth"] wait_for_text(widget, "BT Speaker 1 qtile_bluez 1") @pytest.mark.parametrize( "bluetooth_manager", [ {"device": "/dev_22_22_22_22_22_22_22_22"}, ], indirect=True, ) def test_default_device(bluetooth_manager): widget = bluetooth_manager.c.widget["bluetooth"] wait_for_text(widget, "Device: Headphones [-]") ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_nvidia_sensors.py����������������������������������������������������0000664�0001750�0001750�00000003372�14762660347�021727� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������import pytest from libqtile.widget.nvidia_sensors import NvidiaSensors, _all_sensors_names_correct from test.widgets.conftest import FakeBar def test_nvidia_sensors_input_regex(): correct_sensors = NvidiaSensors( format="temp:{temp}°C,fan{fan_speed}asd,performance{perf}fds" )._parse_format_string() incorrect_sensors = {"tem", "fan_speed", "perf"} assert correct_sensors == {"temp", "fan_speed", "perf"} assert _all_sensors_names_correct(correct_sensors) assert not _all_sensors_names_correct(incorrect_sensors) class MockNvidiaSMI: # nvidia-smi --query-gpu=temperature.gpu --format=csv,noheader # outputs one number for temperature with one gpu. temperature = "20" @classmethod def get_temperature(cls, *args, **kwargs): return cls.temperature @pytest.fixture def fake_nvidia(fake_qtile, monkeypatch, fake_window): n = NvidiaSensors() # Replace internal call_process since we cant rely # on the test computer having the required hardware. monkeypatch.setattr(n, "call_process", MockNvidiaSMI.get_temperature) monkeypatch.setattr("libqtile.widget.moc.subprocess.Popen", MockNvidiaSMI.get_temperature) fakebar = FakeBar([n], window=fake_window) n._configure(fake_qtile, fakebar) return n def test_nvidia_sensors_foreground_colour(fake_nvidia): # Initial temperature fake_nvidia.poll() assert fake_nvidia.layout.colour == fake_nvidia.foreground_normal # Simulate GPU overheating MockNvidiaSMI.temperature = "90" fake_nvidia.poll() assert fake_nvidia.layout.colour == fake_nvidia.foreground_alert # And cooling back down MockNvidiaSMI.temperature = "20" fake_nvidia.poll() assert fake_nvidia.layout.colour == fake_nvidia.foreground_normal ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_systray.py�����������������������������������������������������������0000664�0001750�0001750�00000011572�14762660347�020420� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2022 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import shutil import pytest import libqtile.bar import libqtile.config from libqtile import widget from test.helpers import Retry def test_no_duplicates_multiple_instances(manager_nospawn, minimal_conf_noscreen, backend_name): """Check only one instance of Systray widget.""" if backend_name == "wayland": pytest.skip("Skipping test on Wayland.") assert not widget.Systray._instances config = minimal_conf_noscreen config.screens = [ libqtile.config.Screen(top=libqtile.bar.Bar([widget.Systray(), widget.Systray()], 10)) ] manager_nospawn.start(config) widgets = manager_nospawn.c.bar["top"].info()["widgets"] assert len(widgets) == 2 assert widgets[1]["name"] == "configerrorwidget" def test_no_duplicates_mirror(manager_nospawn, minimal_conf_noscreen, backend_name): """Check systray is not mirrored.""" if backend_name == "wayland": pytest.skip("Skipping test on Wayland.") assert not widget.Systray._instances systray = widget.Systray() config = minimal_conf_noscreen config.fake_screens = [ libqtile.config.Screen( top=libqtile.bar.Bar([systray], 10), x=0, y=0, width=300, height=300, ), libqtile.config.Screen( top=libqtile.bar.Bar([systray], 10), x=0, y=300, width=300, height=300, ), ] manager_nospawn.start(config) # Second screen has tried to mirror the Systray instance widgets = manager_nospawn.c.screen[1].bar["top"].info()["widgets"] assert len(widgets) == 1 assert widgets[0]["name"] == "configerrorwidget" def test_systray_reconfigure_screens(manager_nospawn, minimal_conf_noscreen, backend_name): """Check systray does not crash when reconfiguring screens.""" if backend_name == "wayland": pytest.skip("Skipping test on Wayland.") assert not widget.Systray._instances config = minimal_conf_noscreen config.screens = [libqtile.config.Screen(top=libqtile.bar.Bar([widget.Systray()], 10))] manager_nospawn.start(config) assert manager_nospawn.c.bar["top"].info()["widgets"][0]["name"] == "systray" manager_nospawn.c.reconfigure_screens() assert manager_nospawn.c.bar["top"].info()["widgets"][0]["name"] == "systray" def test_systray_icons(manager_nospawn, minimal_conf_noscreen, backend_name): """Check icons are placed correctly.""" @Retry(ignore_exceptions=(AssertionError)) def wait_for_icons(): _, icons = manager_nospawn.c.widget["systray"].eval("len(self.tray_icons)") assert int(icons) == 2 if backend_name == "wayland": pytest.skip("Skipping test on Wayland.") for prog in ("volumeicon", "vlc"): if shutil.which(prog) is None: pytest.skip(f"{prog} must be installed. Skipping test.") config = minimal_conf_noscreen config.screens = [libqtile.config.Screen(top=libqtile.bar.Bar([widget.Systray()], 40))] manager_nospawn.start(config) # No icons at this stage so length is 0 assert manager_nospawn.c.widget["systray"].info()["widget"]["length"] == 0 manager_nospawn.c.spawn("volumeicon") manager_nospawn.c.spawn("vlc") wait_for_icons() # We now have two icons so widget should expand to leave space in bar for icons assert manager_nospawn.c.widget["systray"].info()["widget"]["length"] > 0 # Check positioning of icon _, x = manager_nospawn.c.widget["systray"].eval("self.tray_icons[0].x") _, y = manager_nospawn.c.widget["systray"].eval("self.tray_icons[0].y") # Positions are relative to bar assert (int(x), int(y)) == (3, 10) # Icons should be in alphabetical order _, order = manager_nospawn.c.widget["systray"].eval("[i.name for i in self.tray_icons]") assert order == "['vlc', 'volumeicon']" ��������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_battery.py�����������������������������������������������������������0000664�0001750�0001750�00000023453�14762660347�020355� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������import pytest from libqtile import images from libqtile.widget import battery from libqtile.widget.battery import Battery, BatteryIcon, BatteryState, BatteryStatus from test.widgets.conftest import TEST_DIR, FakeBar class DummyBattery: def __init__(self, status): self._status = status def update_status(self): return self._status class DummyErrorBattery: def __init__(self, **config): pass def update_status(self): raise RuntimeError("err") def dummy_load_battery(bat): def load_battery(**config): return DummyBattery(bat) return load_battery def test_text_battery_charging(monkeypatch): loaded_bat = BatteryStatus( state=BatteryState.CHARGING, percent=0.5, power=15.0, time=1729, charge_start_threshold=0, charge_end_threshold=100, ) with monkeypatch.context() as manager: manager.setattr(battery, "load_battery", dummy_load_battery(loaded_bat)) batt = Battery() text = batt.poll() assert text == "^ 50% 0:28 15.00 W" def test_text_battery_discharging(monkeypatch): loaded_bat = BatteryStatus( state=BatteryState.DISCHARGING, percent=0.5, power=15.0, time=1729, charge_start_threshold=0, charge_end_threshold=100, ) with monkeypatch.context() as manager: manager.setattr(battery, "load_battery", dummy_load_battery(loaded_bat)) batt = Battery() text = batt.poll() assert text == "V 50% 0:28 15.00 W" def test_text_battery_full(monkeypatch): loaded_bat = BatteryStatus( state=BatteryState.FULL, percent=0.5, power=15.0, time=1729, charge_start_threshold=0, charge_end_threshold=100, ) with monkeypatch.context() as manager: manager.setattr(battery, "load_battery", dummy_load_battery(loaded_bat)) batt = Battery() text = batt.poll() assert text == "Full" full_short_text = "🔋" with monkeypatch.context() as manager: manager.setattr(battery, "load_battery", dummy_load_battery(loaded_bat)) batt = Battery(full_short_text=full_short_text) text = batt.poll() assert text == full_short_text with monkeypatch.context() as manager: manager.setattr(battery, "load_battery", dummy_load_battery(loaded_bat)) batt = Battery(show_short_text=False) text = batt.poll() assert text == "= 50% 0:28 15.00 W" def test_text_battery_empty(monkeypatch): loaded_bat = BatteryStatus( state=BatteryState.EMPTY, percent=0.5, power=15.0, time=1729, charge_start_threshold=0, charge_end_threshold=100, ) with monkeypatch.context() as manager: manager.setattr(battery, "load_battery", dummy_load_battery(loaded_bat)) batt = Battery() text = batt.poll() assert text == "Empty" empty_short_text = "🪫" with monkeypatch.context() as manager: manager.setattr(battery, "load_battery", dummy_load_battery(loaded_bat)) batt = Battery(empty_short_text=empty_short_text) text = batt.poll() assert text == empty_short_text with monkeypatch.context() as manager: manager.setattr(battery, "load_battery", dummy_load_battery(loaded_bat)) batt = Battery(show_short_text=False) text = batt.poll() assert text == "x 50% 0:28 15.00 W" loaded_bat = BatteryStatus( state=BatteryState.UNKNOWN, percent=0.0, power=15.0, time=1729, charge_start_threshold=0, charge_end_threshold=100, ) with monkeypatch.context() as manager: manager.setattr(battery, "load_battery", dummy_load_battery(loaded_bat)) batt = Battery() text = batt.poll() assert text == "Empty" def test_text_battery_not_charging(monkeypatch): loaded_bat = BatteryStatus( state=BatteryState.NOT_CHARGING, percent=0.5, power=15.0, time=1729, charge_start_threshold=0, charge_end_threshold=100, ) with monkeypatch.context() as manager: manager.setattr(battery, "load_battery", dummy_load_battery(loaded_bat)) batt = Battery() text = batt.poll() assert text == "* 50% 0:28 15.00 W" def test_text_battery_unknown(monkeypatch): loaded_bat = BatteryStatus( state=BatteryState.UNKNOWN, percent=0.5, power=15.0, time=1729, charge_start_threshold=0, charge_end_threshold=100, ) with monkeypatch.context() as manager: manager.setattr(battery, "load_battery", dummy_load_battery(loaded_bat)) batt = Battery() text = batt.poll() assert text == "? 50% 0:28 15.00 W" def test_text_battery_hidden(monkeypatch): loaded_bat = BatteryStatus( state=BatteryState.DISCHARGING, percent=0.5, power=15.0, time=1729, charge_start_threshold=0, charge_end_threshold=100, ) with monkeypatch.context() as manager: manager.setattr(battery, "load_battery", dummy_load_battery(loaded_bat)) batt = Battery(hide_threshold=0.6) text = batt.poll() assert text != "" with monkeypatch.context() as manager: manager.setattr(battery, "load_battery", dummy_load_battery(loaded_bat)) batt = Battery(hide_threshold=0.4) text = batt.poll() assert text == "" def test_text_battery_error(monkeypatch): with monkeypatch.context() as manager: manager.setattr(battery, "load_battery", DummyErrorBattery) batt = Battery() text = batt.poll() assert text == "Error: err" def test_images_fail(): """Test BatteryIcon() with a bad theme_path This theme path doesn't contain all of the required images. """ batt = BatteryIcon(theme_path=TEST_DIR) with pytest.raises(images.LoadingError): batt.setup_images() def test_images_good(tmpdir, fake_bar, svg_img_as_pypath): """Test BatteryIcon() with a good theme_path This theme path does contain all of the required images. """ for name in BatteryIcon.icon_names: target = tmpdir.join(name + ".svg") svg_img_as_pypath.copy(target) batt = BatteryIcon(theme_path=str(tmpdir)) batt.fontsize = 12 batt.bar = fake_bar batt.setup_images() assert len(batt.images) == len(BatteryIcon.icon_names) for name, img in batt.images.items(): assert isinstance(img, images.Img) def test_images_default(fake_bar): """Test BatteryIcon() with the default theme_path Ensure that the default images are successfully loaded. """ batt = BatteryIcon() batt.fontsize = 12 batt.bar = fake_bar batt.setup_images() assert len(batt.images) == len(BatteryIcon.icon_names) for name, img in batt.images.items(): assert isinstance(img, images.Img) def test_battery_background(fake_qtile, fake_window, monkeypatch): ok = BatteryStatus( state=BatteryState.DISCHARGING, percent=0.5, power=15.0, time=1729, charge_start_threshold=0, charge_end_threshold=100, ) low = BatteryStatus( state=BatteryState.DISCHARGING, percent=0.1, power=15.0, time=1729, charge_start_threshold=0, charge_end_threshold=100, ) low_background = "ff0000" background = "000000" with monkeypatch.context() as manager: manager.setattr(battery, "load_battery", dummy_load_battery(ok)) batt = Battery(low_percentage=0.2, low_background=low_background, background=background) fakebar = FakeBar([batt], window=fake_window) batt._configure(fake_qtile, fakebar) assert batt.background == background batt._battery._status = low batt.poll() assert batt.background == low_background batt._battery._status = ok batt.poll() assert batt.background == background def test_charge_control(fake_qtile, fake_window, monkeypatch): start = 0 end = 100 def save_battery_percentage(self, charge_start_threshold, charge_end_threshold): nonlocal start nonlocal end start = charge_start_threshold end = charge_end_threshold with monkeypatch.context() as manager: manager.setattr( battery._LinuxBattery, "set_battery_charge_thresholds", save_battery_percentage ) batt = Battery(charge_controller=lambda: (5, 10)) fakebar = FakeBar([batt], window=fake_window) batt._configure(fake_qtile, fakebar) batt.poll() assert start == 5 assert end == 10 def test_charge_control_disabled(fake_qtile, fake_window, monkeypatch): start = 4 end = 7 def save_battery_percentage(self, charge_start_threshold, charge_end_threshold): raise "should not be called" with monkeypatch.context() as manager: manager.setattr( battery._LinuxBattery, "set_battery_charge_thresholds", save_battery_percentage ) batt = Battery(charge_controller=None) fakebar = FakeBar([batt], window=fake_window) batt._configure(fake_qtile, fakebar) batt.poll() assert start == 4 assert end == 7 def test_charge_control_force_charge(fake_qtile, fake_window, monkeypatch): start = 4 end = 7 def save_battery_percentage(self, charge_start_threshold, charge_end_threshold): nonlocal start nonlocal end start = charge_start_threshold end = charge_end_threshold with monkeypatch.context() as manager: manager.setattr( battery._LinuxBattery, "set_battery_charge_thresholds", save_battery_percentage ) batt = Battery(charge_controller=lambda: (0, 90), force_charge=True) fakebar = FakeBar([batt], window=fake_window) batt._configure(fake_qtile, fakebar) batt.poll() assert start == 0 assert end == 100 ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_moc.py���������������������������������������������������������������0000664�0001750�0001750�00000014256�14762660347�017462� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # Widget specific tests import subprocess import pytest import libqtile.config from libqtile.widget import moc from test.widgets.conftest import FakeBar class MockMocpProcess: info = {} is_error = False index = 0 @classmethod def reset(cls): cls.info = [ { "State": "PLAY", "File": "/playing/file/rickroll.mp3", "SongTitle": "Never Gonna Give You Up", "Artist": "Rick Astley", "Album": "Whenever You Need Somebody", }, { "State": "PLAY", "File": "/playing/file/sweetcaroline.mp3", "SongTitle": "Sweet Caroline", "Artist": "Neil Diamond", "Album": "Greatest Hits", }, { "State": "STOP", "File": "/playing/file/itsnotunusual.mp3", "SongTitle": "It's Not Unusual", "Artist": "Tom Jones", "Album": "Along Came Jones", }, ] cls.index = 0 @classmethod def run(cls, cmd): if cls.is_error: raise subprocess.CalledProcessError(-1, cmd=cmd, output="Couldn't connect to moc.") arg = cmd[1] if arg == "-i": output = "\n".join(f"{k}: {v}" for k, v in cls.info[cls.index].items()) return output elif arg == "-p": cls.info[cls.index]["State"] = "PLAY" elif arg == "-G": if cls.info[cls.index]["State"] == "PLAY": cls.info[cls.index]["State"] = "PAUSE" elif cls.info[cls.index]["State"] == "PAUSE": cls.info[cls.index]["State"] = "PLAY" elif arg == "-f": cls.index = (cls.index + 1) % len(cls.info) elif arg == "-r": cls.index = (cls.index - 1) % len(cls.info) def no_op(*args, **kwargs): pass @pytest.fixture def patched_moc(fake_qtile, monkeypatch, fake_window): widget = moc.Moc() MockMocpProcess.reset() monkeypatch.setattr(widget, "call_process", MockMocpProcess.run) monkeypatch.setattr("libqtile.widget.moc.subprocess.Popen", MockMocpProcess.run) fakebar = FakeBar([widget], window=fake_window) widget._configure(fake_qtile, fakebar) return widget def test_moc_poll_string_formatting(patched_moc): # Both artist and song title assert patched_moc.poll() == "♫ Rick Astley - Never Gonna Give You Up" # No artist MockMocpProcess.info[0]["Artist"] = "" assert patched_moc.poll() == "♫ Never Gonna Give You Up" # No title MockMocpProcess.info[0]["SongTitle"] = "" assert patched_moc.poll() == "♫ rickroll" def test_moc_state_and_colours(patched_moc): # Initial poll - playing patched_moc.poll() assert patched_moc.layout.colour == patched_moc.play_color # Toggle pause patched_moc.play() patched_moc.poll() assert patched_moc.layout.colour == patched_moc.noplay_color # Toggle pause --> playing again patched_moc.play() patched_moc.poll() assert patched_moc.layout.colour == patched_moc.play_color def test_moc_button_presses(manager_nospawn, minimal_conf_noscreen, monkeypatch): # This needs to be patched before initialising the widgets as mouse callbacks # bind subprocess.Popen. monkeypatch.setattr("subprocess.Popen", MockMocpProcess.run) # Long interval as we don't need this polling on its own. mocwidget = moc.Moc(update_interval=30) MockMocpProcess.reset() monkeypatch.setattr(mocwidget, "call_process", MockMocpProcess.run) monkeypatch.setattr("libqtile.widget.moc.subprocess.Popen", MockMocpProcess.run) config = minimal_conf_noscreen config.screens = [libqtile.config.Screen(top=libqtile.bar.Bar([mocwidget], 10))] manager_nospawn.start(config) # When started, we have the first item playing topbar = manager_nospawn.c.bar["top"] info = manager_nospawn.c.widget["moc"].info assert info()["text"] == "♫ Rick Astley - Never Gonna Give You Up" # Trigger next item and wait for update poll topbar.fake_button_press(0, 0, button=4) manager_nospawn.c.widget["moc"].eval("self.update(self.poll())") assert info()["text"] == "♫ Neil Diamond - Sweet Caroline" # Trigger next item and wait for update poll # This item's state is set to "STOP" so there's no track title topbar.fake_button_press(0, 0, button=4) manager_nospawn.c.widget["moc"].eval("self.update(self.poll())") assert info()["text"] == "♫" # Click to play it and get the information topbar.fake_button_press(0, 0, button=1) manager_nospawn.c.widget["moc"].eval("self.update(self.poll())") assert info()["text"] == "♫ Tom Jones - It's Not Unusual" # Trigger previous item and wait for update poll topbar.fake_button_press(0, 0, button=5) manager_nospawn.c.widget["moc"].eval("self.update(self.poll())") assert info()["text"] == "♫ Neil Diamond - Sweet Caroline" def test_moc_error_handling(patched_moc): MockMocpProcess.is_error = True # Widget does nothing with error message so text is blank assert patched_moc.poll() == "" ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_idlerpg.py�����������������������������������������������������������0000664�0001750�0001750�00000003350�14762660347�020323� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # Widget specific tests from libqtile.widget import idlerpg # Set up some fake responses # Default widget displayes "ttl" and "online" values but can also show # any others keys from the response so we need to include an extra field online_response = {"player": {"ttl": 1000, "online": "1", "unused": "0"}} offline_response = {"player": {"ttl": 10300, "online": "0"}} # The GenPollURL widget has been tested separately so we just need to test parse # method of this widget def test_idlerpg(): idler = idlerpg.IdleRPG() assert idler.parse(online_response) == "IdleRPG: online TTL: 0:16:40" assert idler.parse(offline_response) == "IdleRPG: offline TTL: 2:51:40" ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/widgets/test_wttr.py��������������������������������������������������������������0000664�0001750�0001750�00000002704�14762660347�017677� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from libqtile import widget RESPONSE = "London: +17°C" def test_wttr_methods(): wttr = widget.Wttr(location={"London": "Home"}) assert wttr._get_url() == "https://wttr.in/London?m&format=3&lang=en" assert wttr.parse(RESPONSE) == "Home: +17°C" def test_wttr_no_location(): wttr = widget.Wttr() assert wttr._get_url() == "https://wttr.in/?m&format=3&lang=en" ������������������������������������������������������������qtile-0.31.0/test/widgets/test_import_error.py������������������������������������������������������0000664�0001750�0001750�00000003771�14762660347�021427� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 elParaguayo # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # Widget specific tests import pytest import libqtile.bar import libqtile.config from libqtile import widget def bad_importer(*args, **kwargs): raise ImportError() @pytest.mark.parametrize("position", ["top", "bottom", "left", "right"]) def test_importerrorwidget(monkeypatch, manager_nospawn, minimal_conf_noscreen, position): """Check we get an ImportError widget with missing import?""" monkeypatch.setattr("libqtile.utils.importlib.import_module", bad_importer) badwidget = widget.TextBox("I am a naughty widget.") config = minimal_conf_noscreen config.screens = [libqtile.config.Screen(**{position: libqtile.bar.Bar([badwidget], 10)})] manager_nospawn.start(config) testbar = manager_nospawn.c.bar[position] w = testbar.info()["widgets"][0] # Check that the widget has been replaced with an ImportError assert w["name"] == "importerrorwidget" assert w["text"] == "Import Error: TextBox" �������qtile-0.31.0/test/test_command_graph.py�������������������������������������������������������������0000664�0001750�0001750�00000003224�14762660347�020026� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������import pytest from libqtile.command.graph import CommandGraphCall, CommandGraphObject, CommandGraphRoot def test_root_path(): node = CommandGraphRoot() assert node.selectors == [] assert node.selector is None assert node.parent is None def test_resolve_nodes(): root_node = CommandGraphRoot() node_1 = root_node.navigate("layout", None).navigate("screen", None) assert node_1.selectors == [("layout", None), ("screen", None)] assert isinstance(node_1, CommandGraphObject) node_2 = node_1.navigate("layout", None).navigate("window", None).navigate("group", None) assert node_2.selectors == [ ("layout", None), ("screen", None), ("layout", None), ("window", None), ("group", None), ] assert isinstance(node_2, CommandGraphObject) with pytest.raises(KeyError, match="Given node is not an object"): node_1.navigate("root", None) def test_resolve_selections(): root_node = CommandGraphRoot() node_1 = root_node.navigate("layout", None).navigate("screen", "1") assert node_1.selectors == [("layout", None), ("screen", "1")] assert isinstance(node_1, CommandGraphObject) def test_resolve_command(): root_node = CommandGraphRoot() command_1 = root_node.call("cmd_name") assert command_1.selectors == [] assert command_1.name == "cmd_name" assert isinstance(command_1, CommandGraphCall) command_2 = root_node.navigate("layout", None).navigate("screen", None).call("cmd_name") assert command_2.name == "cmd_name" assert command_2.selectors == [("layout", None), ("screen", None)] assert isinstance(command_2, CommandGraphCall) ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/test_when.py����������������������������������������������������������������������0000664�0001750�0001750�00000011253�14762660347�016171� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2021 Jeroen Wijenbergh # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest from libqtile import config, layout from libqtile.confreader import Config from libqtile.lazy import lazy # Config with multiple keys and when checks class WhenConfig(Config): keys = [ config.Key(["control"], "k", lazy.window.toggle_floating()), config.Key(["control"], "p", lazy.window.toggle_floating().when(when_floating=True)), config.Key(["control"], "o", lazy.window.toggle_floating().when(when_floating=False)), config.Key( ["control"], "j", lazy.window.toggle_floating().when(focused=config.Match(wm_class="TestWindow")), ), config.Key( ["control"], "h", lazy.window.toggle_floating().when(focused=config.Match(wm_class="idonotexist")), ), config.Key( ["control"], "n", lazy.next_layout().when(focused=config.Match(wm_class="TestWindow")), ), config.Key( ["control"], "m", lazy.next_layout().when( focused=config.Match(wm_class="TestWindow"), if_no_focused=True ), ), config.Key(["control"], "t", lazy.next_layout().when(condition=1 + 1 == 2)), config.Key(["control"], "f", lazy.next_layout().when(condition=1 + 1 == 3)), config.Key(["control", "shift"], "t", lazy.next_layout().when(func=lambda: True)), config.Key(["control", "shift"], "f", lazy.next_layout().when(func=lambda: False)), ] layouts = [layout.MonadWide(), layout.MonadTall()] when_config = pytest.mark.parametrize("manager", [WhenConfig], indirect=True) @when_config def test_when(manager): # Check if the test window is alive and tiled one = manager.test_window("one") assert not manager.c.window.info()["floating"] # This sets the window to floating as there is no when manager.c.simulate_keypress(["control"], "k") assert manager.c.window.info()["floating"] # This keeps the window floating as the class doesn't match manager.c.simulate_keypress(["control"], "h") assert manager.c.window.info()["floating"] # This sets the window tiled as the class does match manager.c.simulate_keypress(["control"], "j") assert not manager.c.window.info()["floating"] # This keeps the window tiled as window is not floating manager.c.simulate_keypress(["control"], "p") assert not manager.c.window.info()["floating"] # This sets the window floating as window is not floating manager.c.simulate_keypress(["control"], "o") assert manager.c.window.info()["floating"] # Kill the window to create an empty group manager.kill_window(one) prev_layout_info = manager.c.layout.info() # This does not go to the next layout as empty is not matched manager.c.simulate_keypress(["control"], "n") assert manager.c.layout.info() == prev_layout_info # This does go to the next layout as empty is matched manager.c.simulate_keypress(["control"], "m") assert manager.c.layout.info() != prev_layout_info # Test boolean argument prev_layout_info = manager.c.layout.info() manager.c.simulate_keypress(["control"], "f") assert manager.c.layout.info() == prev_layout_info manager.c.simulate_keypress(["control"], "t") assert manager.c.layout.info() != prev_layout_info # Test function argument prev_layout_info = manager.c.layout.info() manager.c.simulate_keypress(["control", "shift"], "f") assert manager.c.layout.info() == prev_layout_info manager.c.simulate_keypress(["control", "shift"], "t") assert manager.c.layout.info() != prev_layout_info �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/test_check.py���������������������������������������������������������������������0000664�0001750�0001750�00000007432�14762660347�016311� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2020 Tycho Andersen # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import os import platform import shutil import subprocess import tempfile import textwrap import pytest def is_cpython(): # https://mypy.readthedocs.io/en/stable/faq.html#does-it-run-on-pypy return platform.python_implementation() == "CPython" def have_mypy(): return shutil.which("mypy") is not None pytestmark = pytest.mark.skipif(not is_cpython() or not have_mypy(), reason="needs mypy") def run_qtile_check(config): # make sure we have mypy so we can type check the config if shutil.which("mypy") is None: raise Exception("missing mypy in test environment") cmd = os.path.join(os.path.dirname(__file__), "..", "bin", "qtile") argv = [cmd, "check", "-c", config] try: newenv = os.environ.copy() old_pp = newenv.get("PYTHONPATH", "") newenv["PYTHONPATH"] = os.path.join(os.path.dirname(__file__), "..") + ":" + old_pp subprocess.check_call(argv, env=newenv) except subprocess.CalledProcessError: return False else: return True def test_check_default_config(): # the default config should always be ok default_config = os.path.join( os.path.dirname(__file__), "..", "libqtile", "resources", "default_config.py" ) assert run_qtile_check(default_config) def check_literal_config(config): with tempfile.TemporaryDirectory() as tempdir: with tempfile.NamedTemporaryFile(dir=tempdir, suffix=".py") as temp: temp.write(textwrap.dedent(config).encode("utf-8")) temp.flush() return run_qtile_check(temp.name) def test_check_bad_syntax(): assert not check_literal_config( """ meshuggah rocks! """ ) def test_check_bad_key_arg(): assert not check_literal_config( """ Key([1], 1) """ ) def test_check_good_key_arg(): assert check_literal_config( """ from libqtile.config import Key from libqtile.lazy import lazy keys = [Key(["mod4"], "x", lazy.kill())] cursor_warp = True """ ) def test_check_bad_config_type(): assert not check_literal_config( """ keys = "i am not a List[Key]" cursor_warp = "i am not a bool" """ ) def test_extra_vars_are_ok(): assert check_literal_config( """ this_is_an_extra_config_var = "yes it is" """ ) def test_extra_files_are_ok(): with tempfile.TemporaryDirectory() as tempdir: config_file = os.path.join(tempdir, "config.py") with open(config_file, "w") as config: config.write("from bar import foo\n") with open(os.path.join(tempdir, "bar.py"), "w") as config: config.write("foo = 42") assert run_qtile_check(config_file) ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/test_sh.py������������������������������������������������������������������������0000664�0001750�0001750�00000010566�14762660347�015650� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2011 Florian Mounier # Copyright (c) 2012 Tycho Andersen # Copyright (c) 2014 Sean Vig # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest from libqtile import config, ipc, resources from libqtile.command.interface import IPCCommandInterface from libqtile.confreader import Config from libqtile.layout import Max from libqtile.sh import QSh class ShConfig(Config): keys = [] mouse = [] groups = [ config.Group("a"), config.Group("b"), ] layouts = [ Max(), ] floating_layout = resources.default_config.floating_layout screens = [config.Screen()] sh_config = pytest.mark.parametrize("manager", [ShConfig], indirect=True) @sh_config def test_columnize(manager): client = ipc.Client(manager.sockfile) command = IPCCommandInterface(client) sh = QSh(command) assert sh.columnize(["one", "two"]) == "one two" sh.termwidth = 1 assert sh.columnize(["one", "two"], update_termwidth=False) == "one\ntwo" sh.termwidth = 15 v = sh.columnize(["one", "two", "three", "four", "five"], update_termwidth=False) assert v == "one two \nthree four \nfive " @sh_config def test_ls(manager): client = ipc.Client(manager.sockfile) command = IPCCommandInterface(client) sh = QSh(command) assert sh.do_ls(None) == "bar/ group/ layout/ screen/ widget/ window/ core/ " assert sh.do_ls("") == "bar/ group/ layout/ screen/ widget/ window/ core/ " assert sh.do_ls("layout") == "layout/group/ layout/window/ layout/screen/ layout[0]/ " assert sh.do_cd("layout") == "layout" assert sh.do_ls(None) == "group/ window/ screen/" assert ( sh.do_ls("screen") == "screen/layout/ screen/window/ screen/bar/ screen/widget/ screen/group/ " ) @sh_config def test_do_cd(manager): client = ipc.Client(manager.sockfile) command = IPCCommandInterface(client) sh = QSh(command) assert sh.do_cd("layout") == "layout" assert sh.do_cd("../layout/0") == "layout[0]" assert sh.do_cd("..") == "/" assert sh.do_cd("layout") == "layout" assert sh.do_cd("../layout0/wibble") == "No such path." assert sh.do_cd(None) == "/" @sh_config def test_call(manager): client = ipc.Client(manager.sockfile) command = IPCCommandInterface(client) sh = QSh(command) assert sh.process_line("status()") == "OK" v = sh.process_line("nonexistent()") assert v == "Command does not exist: nonexistent" v = sh.process_line("status(((") assert v == "Invalid command: status(((" v = sh.process_line("status(1)") assert v.startswith("Caught command exception") @sh_config def test_complete(manager): client = ipc.Client(manager.sockfile) command = IPCCommandInterface(client) sh = QSh(command) assert sh._complete("c", "c") == [ "cd", "change_window_order", "commands", "critical", ] assert sh._complete("cd l", "l") == ["layout/"] assert sh._complete("cd layout/", "layout/") == [ "layout/" + x for x in ["group", "window", "screen", "0"] ] assert sh._complete("cd layout/", "layout/g") == ["layout/group/"] @sh_config def test_help(manager): client = ipc.Client(manager.sockfile) command = IPCCommandInterface(client) sh = QSh(command) assert sh.do_help("nonexistent").startswith("No such command") assert sh.do_help("help") ������������������������������������������������������������������������������������������������������������������������������������������qtile-0.31.0/test/test_fakescreen.py����������������������������������������������������������������0000664�0001750�0001750�00000035622�14762660347�017344� 0����������������������������������������������������������������������������������������������������ustar �epsilon�������������������������epsilon����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2011 Florian Mounier # Copyright (c) 2012, 2014 Tycho Andersen # Copyright (c) 2013 Craig Barnes # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Adi Sieker # Copyright (c) 2014 Sebastien Blot # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest import libqtile.config from libqtile import bar, layout, widget from libqtile.config import Screen from libqtile.confreader import Config LEFT_ALT = "mod1" WINDOWS = "mod4" FONTSIZE = 13 CHAM1 = "8AE234" CHAM3 = "4E9A06" GRAPH_KW = dict( line_width=1, graph_color=CHAM3, fill_color=CHAM3 + ".3", border_width=1, border_color=CHAM3 ) # screens look like this # 500 300 # |-------------|-----| # | 340| |380 # | A | B | # |----------|--| | # | 220|--|-----| # | C | |220 # |----------| D | # 450 |--------| # 350 # # Notice there is a hole in the middle # also D goes down below the others class FakeScreenConfig(Config): auto_fullscreen = True groups = [ libqtile.config.Group("a"), libqtile.config.Group("b"), libqtile.config.Group("c"), libqtile.config.Group("d"), ] layouts = [ layout.Max(), layout.RatioTile(), layout.Tile(), ] floating_layout = libqtile.resources.default_config.floating_layout keys = [] mouse = [] fake_screens = [ Screen( bottom=bar.Bar( [ widget.GroupBox( this_screen_border=CHAM3, borderwidth=1, fontsize=FONTSIZE, padding=1, margin_x=1, margin_y=1, ), widget.AGroupBox(), widget.Prompt(), widget.Sep(), widget.WindowName(fontsize=FONTSIZE, margin_x=6), widget.Sep(), widget.CPUGraph(**GRAPH_KW), widget.MemoryGraph(**GRAPH_KW), widget.SwapGraph(foreground="20C020", **GRAPH_KW), widget.Sep(), widget.Clock(format="%H:%M:%S %d.%manager.%Y", fontsize=FONTSIZE, padding=6), ], 24, background="#555555", ), left=bar.Gap(16), right=bar.Gap(20), x=0, y=0, width=500, height=340, ), Screen( top=bar.Bar( [widget.GroupBox(), widget.WindowName(), widget.Clock()], 30, ), bottom=bar.Gap(24), left=bar.Gap(12), x=500, y=0, width=300, height=380, ), Screen( top=bar.Bar( [widget.GroupBox(), widget.WindowName(), widget.Clock()], 30, ), bottom=bar.Gap(16), right=bar.Gap(40), x=0, y=340, width=450, height=220, ), Screen( top=bar.Bar( [widget.GroupBox(), widget.WindowName(), widget.Clock()], 30, ), left=bar.Gap(20), right=bar.Gap(24), x=450, y=380, width=350, height=220, ), ] screens = [] fakescreen_config = pytest.mark.parametrize("manager", [FakeScreenConfig], indirect=True) @fakescreen_config def test_basic(manager): manager.test_window("zero") assert manager.c.layout.info()["clients"] == ["zero"] assert manager.c.screen.info() == {"y": 0, "x": 0, "index": 0, "width": 500, "height": 340} manager.c.to_screen(1) manager.test_window("one") assert manager.c.layout.info()["clients"] == ["one"] assert manager.c.screen.info() == {"y": 0, "x": 500, "index": 1, "width": 300, "height": 380} manager.c.to_screen(2) manager.test_window("two") assert manager.c.screen.info() == {"y": 340, "x": 0, "index": 2, "width": 450, "height": 220} manager.c.to_screen(3) manager.test_window("one") assert manager.c.screen.info() == { "y": 380, "x": 450, "index": 3, "width": 350, "height": 220, } @fakescreen_config def test_gaps(manager): g = manager.c.get_screens()[0]["gaps"] assert g["bottom"] == (0, 316, 500, 24) assert g["left"] == (0, 0, 16, 316) assert g["right"] == (480, 0, 20, 316) g = manager.c.get_screens()[1]["gaps"] assert g["top"] == (500, 0, 300, 30) assert g["bottom"] == (500, 356, 300, 24) assert g["left"] == (500, 30, 12, 326) g = manager.c.get_screens()[2]["gaps"] assert g["top"] == (0, 340, 450, 30) assert g["bottom"] == (0, 544, 450, 16) assert g["right"] == (410, 370, 40, 174) g = manager.c.get_screens()[3]["gaps"] assert g["top"] == (450, 380, 350, 30) assert g["left"] == (450, 410, 20, 190) assert g["right"] == (776, 410, 24, 190) @fakescreen_config def test_maximize_with_move_to_screen(manager): """Ensure that maximize respects bars""" manager.test_window("one") manager.c.window.toggle_maximize() assert manager.c.window.info()["width"] == 464 assert manager.c.window.info()["height"] == 316 assert manager.c.window.info()["x"] == 16 assert manager.c.window.info()["y"] == 0 assert manager.c.window.info()["group"] == "a" # go to second screen manager.c.to_screen(1) assert manager.c.screen.info() == {"y": 0, "x": 500, "index": 1, "width": 300, "height": 380} assert manager.c.group.info()["name"] == "b" manager.c.group["a"].toscreen() assert manager.c.window.info()["width"] == 288 assert manager.c.window.info()["height"] == 326 assert manager.c.window.info()["x"] == 512 assert manager.c.window.info()["y"] == 30 assert manager.c.window.info()["group"] == "a" @fakescreen_config def test_float_first_on_second_screen(manager): manager.c.to_screen(1) assert manager.c.screen.info() == {"y": 0, "x": 500, "index": 1, "width": 300, "height": 380} manager.test_window("one") # I don't know where y=30, x=12 comes from... assert manager.c.window.info()["float_info"] == { "y": 30, "x": 12, "width": 100, "height": 100, } manager.c.window.toggle_floating() assert manager.c.window.info()["width"] == 100 assert manager.c.window.info()["height"] == 100 assert manager.c.window.info()["x"] == 512 assert manager.c.window.info()["y"] == 30 assert manager.c.window.info()["group"] == "b" assert manager.c.window.info()["float_info"] == { "y": 30, "x": 12, "width": 100, "height": 100, } @fakescreen_config def test_float_change_screens(manager): # add one tiled and one floating window manager.test_window("tiled") manager.test_window("float") manager.c.window.toggle_floating() assert set(manager.c.group.info()["windows"]) == set(("tiled", "float")) assert manager.c.group.info()["floating_info"]["clients"] == ["float"] assert manager.c.window.info()["width"] == 100 assert manager.c.window.info()["height"] == 100 # 16 is given by the left gap width assert manager.c.window.info()["x"] == 16 assert manager.c.window.info()["y"] == 0 assert manager.c.window.info()["group"] == "a" # put on group b assert manager.c.screen.info() == {"y": 0, "x": 0, "index": 0, "width": 500, "height": 340} assert manager.c.group.info()["name"] == "a" manager.c.to_screen(1) assert manager.c.group.info()["name"] == "b" assert manager.c.screen.info() == {"y": 0, "x": 500, "index": 1, "width": 300, "height": 380} manager.c.group["a"].toscreen() assert manager.c.group.info()["name"] == "a" assert set(manager.c.group.info()["windows"]) == set(("tiled", "float")) assert manager.c.window.info()["name"] == "float" # width/height unchanged assert manager.c.window.info()["width"] == 100 assert manager.c.window.info()["height"] == 100 # x is shifted by 500, y is shifted by 0 assert manager.c.window.info()["x"] == 516 assert manager.c.window.info()["y"] == 0 assert manager.c.window.info()["group"] == "a" assert manager.c.group.info()["floating_info"]["clients"] == ["float"] # move to screen 3 manager.c.to_screen(2) assert manager.c.screen.info() == {"y": 340, "x": 0, "index": 2, "width": 450, "height": 220} assert manager.c.group.info()["name"] == "c" manager.c.group["a"].toscreen() assert manager.c.group.info()["name"] == "a" assert set(manager.c.group.info()["windows"]) == set(("tiled", "float")) assert manager.c.window.info()["name"] == "float" # width/height unchanged assert manager.c.window.info()["width"] == 100 assert manager.c.window.info()["height"] == 100 # x is shifted by 0, y is shifted by 340 assert manager.c.window.info()["x"] == 16 assert manager.c.window.info()["y"] == 340 # now screen 4 for fun manager.c.to_screen(3) assert manager.c.screen.info() == { "y": 380, "x": 450, "index": 3, "width": 350, "height": 220, } assert manager.c.group.info()["name"] == "d" manager.c.group["a"].toscreen() assert manager.c.group.info()["name"] == "a" assert set(manager.c.group.info()["windows"]) == set(("tiled", "float")) assert manager.c.window.info()["name"] == "float" # width/height unchanged assert manager.c.window.info()["width"] == 100 assert manager.c.window.info()["height"] == 100 # x is shifted by 450, y is shifted by 40 assert manager.c.window.info()["x"] == 466 assert manager.c.window.info()["y"] == 380 # and back to one manager.c.to_screen(0) assert manager.c.screen.info() == {"y": 0, "x": 0, "index": 0, "width": 500, "height": 340} assert manager.c.group.info()["name"] == "b" manager.c.group["a"].toscreen() assert manager.c.group.info()["name"] == "a" assert set(manager.c.group.info()["windows"]) == set(("tiled", "float")) assert manager.c.window.info()["name"] == "float" # back to the original location assert manager.c.window.info()["width"] == 100 assert manager.c.window.info()["height"] == 100 assert manager.c.window.info()["x"] == 16 assert manager.c.window.info()["y"] == 0 @fakescreen_config def test_float_outside_edges(manager): manager.test_window("one") manager.c.window.toggle_floating() assert manager.c.window.info()["width"] == 100 assert manager.c.window.info()["height"] == 100 # 16 is given by the left gap width assert manager.c.window.info()["x"] == 16 assert manager.c.window.info()["y"] == 0 # empty because window is floating assert manager.c.layout.info() == {"clients": [], "current": 0, "group": "a", "name": "max"} # move left, but some still on screen 0 manager.c.window.move_floating(-30, 20) assert manager.c.window.info()["width"] == 100 assert manager.c.window.info()["height"] == 100 assert manager.c.window.info()["x"] == -14 assert manager.c.window.info()["y"] == 20 assert manager.c.window.info()["group"] == "a" # move up, but some still on screen 0 manager.c.window.set_position_floating(-10, -20) assert manager.c.window.info()["width"] == 100 assert manager.c.window.info()["height"] == 100 assert manager.c.window.info()["x"] == -10 assert manager.c.window.info()["y"] == -20 assert manager.c.window.info()["group"] == "a" # move above a manager.c.window.set_position_floating(50, -20) assert manager.c.window.info()["width"] == 100 assert manager.c.window.info()["height"] == 100 assert manager.c.window.info()["x"] == 50 assert manager.c.window.info()["y"] == -20 assert manager.c.window.info()["group"] == "a" # move down so still left, but next to screen c manager.c.window.set_position_floating(-10, 360) assert manager.c.window.info()["height"] == 100 assert manager.c.window.info()["x"] == -10 assert manager.c.window.info()["y"] == 360 assert manager.c.window.info()["group"] == "c" # move above b manager.c.window.set_position_floating(700, -10) assert manager.c.window.info()["width"] == 100 assert manager.c.window.info()["height"] == 100 assert manager.c.window.info()["x"] == 700 assert manager.c.window.info()["y"] == -10 assert manager.c.window.info()["group"] == "b" @fakescreen_config def test_hammer_tile(manager): # change to tile layout manager.c.next_layout() manager.c.next_layout() for i in range(7): manager.test_window("one") for i in range(30): manager.c.to_screen((i + 1) % 4) manager.c.group["a"].toscreen() assert manager.c.group["a"].info()["windows"] == [ "one", "one", "one", "one", "one", "one", "one", ] @fakescreen_config def test_hammer_ratio_tile(manager): # change to ratio tile layout manager.c.next_layout() for i in range(7): manager.test_window("one") for i in range(30): manager.c.to_screen((i + 1) % 4) manager.c.group["a"].toscreen() assert manager.c.group["a"].info()["windows"] == [ "one", "one", "one", "one", "one", "one", "one", ] @fakescreen_config def test_ratio_to_fourth_screen(manager): # change to ratio tile layout manager.c.next_layout() for i in range(7): manager.test_window("one") manager.c.to_screen(1) manager.c.group["a"].toscreen() assert manager.c.group["a"].info()["windows"] == [ "one", "one", "one", "one", "one", "one", "one", ] # now move to 4th, fails... manager.c.to_screen(3) manager.c.group["a"].toscreen() assert manager.c.group["a"].info()["windows"] == [ "one", "one", "one", "one", "one", "one", "one", ] ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������