├── .github └── workflows │ └── python-publish.yml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── completions ├── bash │ └── wpg └── zsh │ └── _wpg ├── pyproject.toml └── wpgtk ├── __init__.py ├── __main__.py ├── data ├── __init__.py ├── color.py ├── config.py ├── files.py ├── keywords.py ├── reload.py ├── sample.py ├── themer.py └── util.py ├── gui ├── __init__.py ├── color_grid.py ├── color_picker.py ├── keyword_dialog.py ├── keyword_grid.py ├── option_grid.py ├── template_grid.py ├── theme_picker.py └── util.py └── misc ├── .no_sample.sample.png ├── .nsampler.sample.png ├── wpg-install.sh ├── wpg.conf └── wpgtk.desktop /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | push: 13 | tags: 14 | - '*' 15 | release: 16 | types: [published] 17 | 18 | permissions: 19 | contents: read 20 | 21 | jobs: 22 | deploy: 23 | 24 | runs-on: ubuntu-latest 25 | 26 | steps: 27 | - uses: actions/checkout@v3 28 | - name: Set up Python 29 | uses: actions/setup-python@v3 30 | with: 31 | python-version: '3.x' 32 | - name: Install dependencies 33 | run: | 34 | python -m pip install --upgrade pip 35 | pip install build 36 | - name: Build package 37 | run: python -m build 38 | - name: Publish package 39 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 40 | with: 41 | user: __token__ 42 | password: ${{ secrets.PYPI_API_TOKEN }} 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *__ 2 | *.bin 3 | *.swp 4 | *.pyc 5 | venv/ 6 | requirements.txt 7 | tags 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include wpgtk/misc/* 2 | include completions/* 3 | include README.md 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # :flower_playing_cards: _wpgtk_ 3 | 4 | ![PyPI](https://img.shields.io/pypi/v/wpgtk.svg?style=flat-square) 5 | ![license](https://img.shields.io/badge/license-GPLv2-green.svg?style=flat-square) 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | `wpgtk` uses [pywal](https://github.com/dylanaraps/pywal) as its colorscheme generator, but builds upon it with a graphic user interface and other features such as the ability to edit the color-schemes generated and save them with their respective wallpapers, having light and dark themes for dynamic icons, hackable and fast GTK+ theme made specifically for `wpgtk`, custom keywords and values to replace in templates and auto color-scheme sorting to achieve more readable palettes. 14 | 15 | In short, `wpgtk` is a color-scheme manager with a template system which lets you create templates from any text file and will replace keywords on it when you change your theme, delivering high customizing power. 16 | 17 | And also, for those who are not into auto-generated color-schemes, you will be happy to know that `wpgtk` includes all the preset themes that `pywal` does, so that's around 200+ themes to play around with, that you can also _modify_ to get really readable and cool results! 18 | 19 | ## Wiki 20 | If you prefer written documentation and want to know all the neat things you can do with `wpgtk` you can always go to the `wiki` and checkout the documentation for any of the features, as a user you can always contribute to this wiki to make it better. 21 | 22 | ### [[Home](https://github.com/deviantfero/wpgtk/wiki)] [[Installation](https://github.com/deviantfero/wpgtk/wiki/Installation)] [[Colorschemes](https://github.com/deviantfero/wpgtk/wiki/Colorschemes)] [[Configuration](https://github.com/deviantfero/wpgtk/wiki/Configuration)] [[Custom Keywords](https://github.com/deviantfero/wpgtk/wiki/Custom-Keywords)] [[Templates](https://github.com/deviantfero/wpgtk/wiki/Templates)] 23 | 24 | **_Warning_**: Users updating from versions older than `6.0.0` will have to update their templates to the new format, check the Templates section on the wiki for more information. 25 | 26 |
27 | 28 | ## Usage and Useful Links 29 | 30 | - **Video tutorials:** 31 | * [Installation](https://www.youtube.com/watch?v=jmY5NEPI4RM) 32 | * [Advanced Features](https://www.youtube.com/watch?v=QXpMMP8fT0o) 33 | * [Command Line](https://www.youtube.com/watch?v=yjNipQZpOUc) 34 | * [Import/export Colorschemes](https://www.youtube.com/watch?v=P3D0jtG6G2s) 35 | * [Upgrade to 6.0.0 - feature overview](https://youtu.be/5V4Rb7ULEjM) 36 | 37 | - **Other Repos:** 38 | * [wpgtk.vim](https://github.com/deviantfero/wpgtk.vim) 39 | * [ wpgtk-templates ](https://github.com/deviantfero/wpgtk-templates) 40 | * [wpgtk-colorschemes](https://github.com/deviantfero/wpgtk-colorschemes) 41 | 42 | - **Gallery** 43 | * [User Gallery](https://imgur.com/a/EVIhGLj) 44 | * [Personal Gallery](https://imgur.com/a/0FFbz9F) 45 | 46 | These are just some of the desktops of users I've managed to catch in the wild. They're all pretty cool, I really love seeing these, but if anyone wants their desktop removed (or added) just send me an email or open an issue. 47 | 48 | 49 | 50 | ## Buy me a Coffee (or a soda) 51 | 52 | If you found this project helpful and would like to give back in some way, you can donate here 53 | 54 | 55 | 56 | ## License 57 | 58 | This project is licensed under the GPLv2 License - see the [LICENSE](LICENSE) file for details 59 | -------------------------------------------------------------------------------- /completions/bash/wpg: -------------------------------------------------------------------------------- 1 | function _wpg() { 2 | local optforcolorschemes 3 | _get_comp_words_by_ref cur 4 | # themes 5 | optforcolorschemes="-s -e -d -z -m -LA -A --brt --sat -R" 6 | # themes, pywal themes 7 | optforvariabletheme="-Ti" 8 | # themes, filenames 9 | optvariable="-i -o" 10 | # filenames 11 | optfordefault="--link -aL -La -a -ta -at --update" 12 | # templates 13 | optfortemplates="-td -dt" 14 | # pywal backends 15 | optforbackends="--backends" 16 | # pywal themes 17 | optforthemes="--theme" 18 | # pywal light themes 19 | optforlightthemes="-L --light" 20 | 21 | if [[ $COMP_CWORD = 1 ]]; then 22 | COMPREPLY=($(compgen -W "$(_parse_usage wpg)" -- "$cur")) 23 | elif [[ $COMP_CWORD < 4 || ${COMP_WORDS[1]} != "-s" ]]; then 24 | for opt in $optforcolorschemes; do 25 | if [[ $opt = ${COMP_WORDS[1]} ]]; then 26 | COMPREPLY=($(compgen -W "$(wpg -l)" -- "$cur")) 27 | fi 28 | done 29 | for opt in $optbackends; do 30 | if [[ $opt = ${COMP_WORDS[1]} ]]; then 31 | COMPREPLY=($(compgen -W "$(wpg --backends)" -- "$cur")) 32 | fi 33 | done 34 | for opt in $optforthemes; do 35 | if [[ $opt = ${COMP_WORDS[1]} ]]; then 36 | COMPREPLY=($(compgen -W "$(wpg --theme)" -- "$cur")) 37 | fi 38 | done 39 | for opt in $optvariable; do 40 | if [[ $opt = ${COMP_WORDS[1]} && $COMP_CWORD < 3 ]]; then 41 | COMPREPLY=($(compgen -W "$(wpg -l)" -- "$cur")) 42 | elif [[ $opt = ${COMP_WORDS[1]} ]]; then 43 | compopt -o default; COMPREPLY=() 44 | fi 45 | done 46 | for opt in $optforvariabletheme; do 47 | if [[ $opt = ${COMP_WORDS[1]} && $COMP_CWORD < 3 ]]; then 48 | COMPREPLY=($(compgen -W "$(wpg -l)" -- "$cur")) 49 | elif [[ $opt = ${COMP_WORDS[1]} ]]; then 50 | COMPREPLY=($(compgen -W "$(wpg --theme)" -- "$cur")) 51 | fi 52 | done 53 | for opt in $optfordefault; do 54 | if [[ $opt = ${COMP_WORDS[1]} ]]; then 55 | compopt -o default; COMPREPLY=() 56 | fi 57 | done 58 | for opt in $optfortemplates; do 59 | if [[ $opt = ${COMP_WORDS[1]} ]]; then 60 | COMPREPLY=($(compgen -W "$(wpg -tl)" -- "$cur")) 61 | fi 62 | done 63 | for opt in $optforlightthemes; do 64 | if [[ $opt = ${COMP_WORDS[1]} && "--theme" = ${COMP_WORDS[2]} ]]; then 65 | COMPREPLY=($(compgen -W "$(wpg -L --theme)" -- "$cur")) 66 | fi 67 | done 68 | fi 69 | } 70 | 71 | complete -F _wpg wpg 72 | -------------------------------------------------------------------------------- /completions/zsh/_wpg: -------------------------------------------------------------------------------- 1 | #compdef wpg 2 | 3 | _colorschemes() { 4 | local -a colorschemes 5 | colorschemes=(`wpg -l`) 6 | _describe 'colorschemes' colorschemes 7 | } 8 | 9 | _templates() { 10 | local -a templates 11 | templates=(`wpg -tl`) 12 | _describe 'templates' templates 13 | } 14 | 15 | _backends() { 16 | local -a backends 17 | backends=(`wpg --backend`) 18 | _describe 'backends' backends 19 | } 20 | 21 | _themes() { 22 | local -a themes 23 | themes=(`wpg --theme`) 24 | _describe 'themes' themes 25 | } 26 | 27 | _light_themes() { 28 | local -a light_themes 29 | light_themes=(`wpg -L --theme`) 30 | _describe 'light_themes' light_themes 31 | } 32 | 33 | _wpg() { 34 | local state optforcolorschemes 35 | # themes 36 | optforcolorschemes=(-s -e -d -z -m -A -LA --brt --sat -R) 37 | # themes, filenames 38 | optvariable=(-i -o) 39 | # themes, pywal themes 40 | optvariabletheme=(-Ti) 41 | # filenames 42 | optforgeneric=(--link -a -La -aL -ta -at --update) 43 | # templates 44 | optfortemplates=(-td -dt) 45 | # pywal backends 46 | optforbackends=(--backend) 47 | # pywal themes 48 | optforthemes=(--theme) 49 | # pywal light themes 50 | optforlightthemes=(-L --light) 51 | 52 | _arguments \ 53 | '1: :->generic' \ 54 | '*: :->listcolorschemes' 55 | 56 | case $state in 57 | (generic) 58 | _gnu_generic 59 | ;; 60 | (listcolorschemes) 61 | if [[ $CURRENT < 5 || ${words[2]} != "-s" ]]; then 62 | for opt in $optforcolorschemes; do 63 | if [[ $opt = ${words[2]} ]]; then 64 | _colorschemes 65 | fi 66 | done 67 | for opt in $optvariable; do 68 | if [[ $opt = ${words[2]} && $CURRENT < 4 ]]; then 69 | _colorschemes 70 | elif [[ $opt = ${words[2]} ]]; then 71 | _gnu_generic 72 | fi 73 | done 74 | for opt in $optvariabletheme; do 75 | if [[ $opt = ${words[2]} && $CURRENT < 4 ]]; then 76 | _colorschemes 77 | elif [[ $opt = ${words[2]} ]]; then 78 | _themes 79 | fi 80 | done 81 | for opt in $optforgeneric; do 82 | if [[ $opt = ${words[2]} ]]; then 83 | _gnu_generic 84 | fi 85 | done 86 | for opt in $optfortemplates; do 87 | if [[ $opt = ${words[2]} ]]; then 88 | _templates 89 | fi 90 | done 91 | for opt in $optforbackends; do 92 | if [[ $opt = ${words[2]} ]]; then 93 | _backends 94 | fi 95 | done 96 | for opt in $optforthemes; do 97 | if [[ $opt = ${words[2]} ]]; then 98 | _themes 99 | fi 100 | done 101 | for opt in $optforlightthemes; do 102 | if [[ $opt = ${words[2]} && "--theme" = ${words[3]} ]]; then 103 | _light_themes 104 | fi 105 | done 106 | fi 107 | ;; 108 | esac 109 | } 110 | 111 | _wpg "$@" 112 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools >= 61.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.setuptools.dynamic] 6 | version = {attr = "wpgtk.data.config.__version__"} 7 | 8 | [tool.setuptools.packages.find] 9 | exclude = ["completions"] 10 | 11 | [project] 12 | name = "wpgtk" 13 | dynamic = ["version"] 14 | requires-python = ">=3.5" 15 | dependencies = [ 16 | "Pillow>=4.2.1", 17 | "pywal>=3.3.0", 18 | ] 19 | readme = "README.md" 20 | description = "GTK+ theme/wallpaper manager which uses pywal as its core" 21 | authors = [ 22 | {name = "Fernando Vásquez", email = "fmorataya.04@gmail.com"}, 23 | ] 24 | classifiers = [ 25 | "Environment :: X11 Applications", 26 | "License :: OSI Approved :: GNU General Public License v2 (GPLv2)", 27 | "Operating System :: POSIX :: Linux", 28 | "Programming Language :: Python :: 3.5", 29 | "Programming Language :: Python :: 3.6", 30 | "Programming Language :: Python :: 3.7", 31 | "Programming Language :: Python :: 3.8", 32 | ] 33 | license = {text = "GPL2"} 34 | 35 | [project.urls] 36 | Homepage = "https://github.com/deviantfero/wpgtk" 37 | 38 | [project.scripts] 39 | wpg = "wpgtk.__main__:main" 40 | -------------------------------------------------------------------------------- /wpgtk/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | wpgtk: An easy to use, colorscheme generator and wallpaper manager. 3 | """ 4 | from .data.config import __version__ 5 | from . import data 6 | from . import gui 7 | from . import misc 8 | 9 | __all__ = [ 10 | "data", 11 | "gui", 12 | "misc", 13 | "__version__", 14 | ] 15 | -------------------------------------------------------------------------------- /wpgtk/__main__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import random 3 | import pywal 4 | import logging 5 | import argparse 6 | import glob 7 | from os import path 8 | from .data import files 9 | from .data import themer 10 | from .data import color 11 | from .data import util 12 | from .data import sample 13 | from .data.config import OPT_DIR, __version__ 14 | from .data.config import settings 15 | 16 | 17 | def read_args(args): 18 | parser = argparse.ArgumentParser() 19 | 20 | parser.add_argument("--version", 21 | help="print the current version", 22 | action="store_true") 23 | 24 | parser.add_argument("-a", 25 | help="add a wallpaper and generate a colorscheme", 26 | nargs="+") 27 | 28 | parser.add_argument("-d", 29 | help="delete the wallpaper(s) from wallpaper folder", 30 | nargs="+") 31 | 32 | parser.add_argument("-t", 33 | help="add, remove and list templates instead " 34 | "of themes", 35 | action="store_true") 36 | 37 | parser.add_argument("-s", 38 | help="set the wallpaper and/or colorscheme", 39 | nargs="+") 40 | 41 | parser.add_argument('-l', 42 | help="see which wallpapers are available", 43 | action="store_true") 44 | 45 | parser.add_argument("-n", 46 | help="avoid setting a wallpaper", 47 | action="store_true") 48 | 49 | parser.add_argument("-m", 50 | help="pick a random wallpaper/colorscheme", 51 | action="store_true") 52 | 53 | parser.add_argument("-c", 54 | help="shows the current wallpaper", 55 | action="store_true") 56 | 57 | parser.add_argument("-z", 58 | help="shuffles the given colorscheme(s)", 59 | nargs="+") 60 | 61 | parser.add_argument("-A", 62 | help="auto-adjusts the given colorscheme(s)", 63 | nargs="+") 64 | 65 | parser.add_argument("-r", 66 | help="restore the wallpaper and colorscheme", 67 | action="store_true") 68 | 69 | parser.add_argument("-L", "--light", 70 | help="temporarily enable light themes", 71 | action="store_true") 72 | 73 | parser.add_argument("--theme", 74 | help="list included pywal themes " 75 | "or replace your current colorscheme with a " 76 | "selection of your own", 77 | const="list", nargs="?") 78 | 79 | parser.add_argument("-T", 80 | help="assign a pywal theme to a specific wallpaper" 81 | " instead of a json file", 82 | action="store_true") 83 | 84 | parser.add_argument("-i", 85 | help="import a theme in json format and assign " 86 | "to a wallpaper [wallpaper, json]", 87 | nargs=2) 88 | 89 | parser.add_argument("-o", 90 | help="export a theme in json " 91 | "format [wallpaper, json]", 92 | nargs="+") 93 | 94 | parser.add_argument("-R", 95 | help="reset template(s) to their original colors", 96 | nargs="+") 97 | 98 | parser.add_argument("--link", 99 | help="link config file to template backup " 100 | "[.base, config]", 101 | nargs=2) 102 | 103 | parser.add_argument("--sat", 104 | help="add or subtract the saturation of a " 105 | "colorscheme [colorscheme, sat] (0, 1)", 106 | nargs=2) 107 | 108 | parser.add_argument("--brt", 109 | help="add or subtract the brightness of a " 110 | "colorscheme [colorscheme, brt] (0, 255)", 111 | nargs=2) 112 | 113 | parser.add_argument("--backend", 114 | help="select a temporary backend", 115 | const="list", nargs="?") 116 | 117 | parser.add_argument("--alpha", 118 | help="set a one time alpha value", 119 | nargs=1) 120 | 121 | parser.add_argument("--preview", 122 | help="preview your current colorscheme", 123 | action="store_true") 124 | 125 | parser.add_argument("--noreload", 126 | help="Skip reloading other software after " 127 | "applying colorscheme", 128 | action="store_true") 129 | 130 | parser.add_argument("--noterminal", 131 | help="Skip changing the terminal colorscheme " 132 | "using pywal Skip changing colors in terminals", 133 | action="store_true") 134 | 135 | return parser.parse_args() 136 | 137 | 138 | def process_arg_errors(args): 139 | if args.r and not args.s: 140 | logging.error("invalid combination of flags, use with -s") 141 | exit(1) 142 | 143 | if args.m and (args.s or args.R): 144 | logging.error("invalid combination of flags") 145 | exit(1) 146 | 147 | if args.sat and args.brt: 148 | logging.error("invalid combination of flags") 149 | exit(1) 150 | 151 | if args.s and len(args.s) > 2: 152 | logging.error("specify at most 2 filenames") 153 | exit(1) 154 | 155 | if args.o and (len(args.o) < 1 or len(args.o) > 2): 156 | logging.error("specify wallpaper and optionally an output path") 157 | exit(1) 158 | 159 | 160 | def process_args(args): 161 | if args.light: 162 | settings["light_theme"] = "true" 163 | 164 | if args.noterminal: 165 | settings["terminal"] = "false" 166 | 167 | if args.n: 168 | settings["set_wallpaper"] = "false" 169 | 170 | if args.alpha: 171 | settings["alpha"] = args.alpha[0] 172 | 173 | if args.backend and args.backend != "list": 174 | if args.backend in pywal.colors.list_backends(): 175 | settings['backend'] = args.backend 176 | else: 177 | logging.error("no such backend, please " 178 | "choose a valid backend") 179 | exit(1) 180 | 181 | if args.preview: 182 | pywal.colors.palette() 183 | exit(0) 184 | 185 | if args.m: 186 | file_list = files.get_file_list() 187 | if len(file_list) > 0: 188 | filename = random.choice(file_list) 189 | themer.set_theme(filename, filename, args.r) 190 | exit(0) 191 | else: 192 | logging.error("you have no themes") 193 | exit(1) 194 | 195 | if args.s: 196 | if len(args.s) == 1: 197 | themer.set_theme(args.s[0], args.s[0], args.r) 198 | elif len(args.s) == 2: 199 | themer.set_theme(args.s[0], args.s[1], args.r) 200 | exit(0) 201 | 202 | if args.l: 203 | if args.t: 204 | templates = files.get_file_list(OPT_DIR, r".*\.base$") 205 | any(print(t) for t in templates) 206 | else: 207 | print("\n".join(files.get_file_list())) 208 | exit(0) 209 | 210 | if args.version: 211 | print("current version: " + __version__) 212 | exit(0) 213 | 214 | if args.d: 215 | delete_action = files.delete_template if args.t \ 216 | else themer.delete_theme 217 | try: 218 | any(delete_action(x) for x in args.d) 219 | except IOError: 220 | logging.error("file not found") 221 | exit(1) 222 | 223 | exit(0) 224 | 225 | if args.a: 226 | add_action = files.add_template if args.t \ 227 | else themer.create_theme 228 | for pattern in args.a: 229 | for filename in glob.glob(pattern): 230 | if path.isfile(filename): 231 | add_action(filename) 232 | 233 | exit(0) 234 | 235 | if args.c: 236 | print(themer.get_current()) 237 | exit(0) 238 | 239 | if args.z or args.A: 240 | alter_action = color.shuffle_colors if args.z \ 241 | else color.auto_adjust 242 | arg_list = args.z if args.z else args.A 243 | 244 | for arg in arg_list: 245 | colors = color.get_color_list(arg) 246 | colors = alter_action(colors) 247 | color.write_colors(arg, colors) 248 | 249 | sample.create_sample(colors, files.get_sample_path(arg)) 250 | logging.info("shuffled %s" % arg) 251 | exit(0) 252 | 253 | if args.link: 254 | files.add_template(args.link[1], args.link[0]) 255 | exit(0) 256 | 257 | if args.i: 258 | themer.import_theme(args.i[0], args.i[1], args.T) 259 | exit(0) 260 | 261 | if args.o: 262 | themer.export_theme(*args.o) 263 | exit(0) 264 | 265 | if args.R: 266 | try: 267 | any(themer.reset_theme(arg) for arg in args.R) 268 | except IOError: 269 | logging.error("file not found") 270 | exit(1) 271 | exit(0) 272 | 273 | if args.theme == "list": 274 | dark = settings['light_theme'] != "true" 275 | name_dic = pywal.theme.list_themes(dark) 276 | name_list = [t.name.replace(".json", "") for t in name_dic] 277 | print("\n".join(name_list)) 278 | exit(0) 279 | 280 | if args.sat: 281 | cl = color.get_color_list(args.sat[0]) 282 | val = float(args.sat[1]) 283 | cl = [util.alter_brightness(x, 0, val) for x in cl] 284 | 285 | color.write_colors(args.sat[0], cl) 286 | sample.create_sample(cl, files.get_sample_path(args.sat[0])) 287 | exit(0) 288 | 289 | if args.brt: 290 | cl = color.get_color_list(args.brt[0]) 291 | val = float(args.brt[1]) 292 | cl = [util.alter_brightness(x, val, 0) for x in cl] 293 | 294 | color.write_colors(args.brt[0], cl) 295 | sample.create_sample(cl, files.get_sample_path(args.brt[0])) 296 | exit(0) 297 | 298 | if args.theme and args.theme != "list": 299 | light = settings['light_theme'] == "true" 300 | themer.set_pywal_theme(args.theme, light) 301 | exit(0) 302 | 303 | if args.backend == "list": 304 | print("\n".join(pywal.colors.list_backends())) 305 | exit(0) 306 | 307 | if args.noreload: 308 | settings["reload"] = "false" 309 | 310 | 311 | def main(): 312 | util.setup_log() 313 | args = read_args(sys.argv[1:]) 314 | process_arg_errors(args) 315 | process_args(args) 316 | 317 | try: 318 | _gui = __import__("wpgtk.gui.theme_picker", fromlist=['theme_picker']) 319 | _gui.run(args) 320 | exit(0) 321 | except NameError: 322 | logging.error("missing pygobject module, use cli") 323 | exit(1) 324 | 325 | 326 | if __name__ == "__main__": 327 | main() 328 | -------------------------------------------------------------------------------- /wpgtk/data/__init__.py: -------------------------------------------------------------------------------- 1 | from . import color 2 | from . import config 3 | from . import files 4 | from . import keywords 5 | from . import reload 6 | from . import sample 7 | from . import themer 8 | from . import util 9 | 10 | __all__ = [ 11 | "color", 12 | "config", 13 | "files", 14 | "keywords", 15 | "reload", 16 | "sample", 17 | "themer", 18 | "util" 19 | ] 20 | -------------------------------------------------------------------------------- /wpgtk/data/color.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import logging 3 | import pywal 4 | import os 5 | import re 6 | import threading 7 | from operator import itemgetter 8 | from subprocess import Popen 9 | from random import shuffle, randint 10 | 11 | from .config import settings 12 | from .config import WALL_DIR, WPG_DIR, FILE_DIC, OPT_DIR 13 | from . import keywords 14 | from . import files 15 | from . import util 16 | from . import sample 17 | 18 | 19 | def get_pywal_dict(wallpaper, is_file=False): 20 | """get the color dictionary of a given wallpaper""" 21 | light_theme = settings.getboolean("light_theme", False) 22 | pywal.util.Color.alpha_num = settings.get("alpha", "100") 23 | 24 | image = pywal.image.get(os.path.join(WALL_DIR, wallpaper)) 25 | 26 | return pywal.colors.get( 27 | image, 28 | light=(is_file and light_theme), 29 | backend=settings.get("backend", "wal"), 30 | cache_dir=WPG_DIR 31 | ) 32 | 33 | 34 | def get_color_list(filename, json=False): 35 | """extract a list with 16 colors from a json or a pywal dict""" 36 | is_new = not os.path.isfile(files.get_cache_path(filename)) 37 | is_auto_adjust = settings.getboolean("auto_adjust", True) 38 | is_light_theme = settings.getboolean("light_theme", False) 39 | 40 | if json: 41 | theme = pywal.util.read_file_json(filename) 42 | else: 43 | theme = get_pywal_dict(filename) 44 | 45 | if "color" in theme: 46 | color_list = theme["color"] 47 | else: 48 | color_list = list(theme["colors"].values()) 49 | 50 | if is_new and not json: 51 | if is_auto_adjust or is_light_theme: 52 | color_list = auto_adjust(color_list) 53 | sample.create_sample(color_list, files.get_sample_path(filename)) 54 | write_colors(filename, color_list) 55 | 56 | return color_list 57 | 58 | 59 | def is_dark_theme(color_list): 60 | """compare brightness values to see if a color-scheme 61 | is light or dark""" 62 | fg_brightness = util.get_hls_val(color_list[7], "light") 63 | bg_brightness = util.get_hls_val(color_list[0], "light") 64 | 65 | return fg_brightness > bg_brightness 66 | 67 | 68 | def shuffle_colors(colors): 69 | """shuffle a color list in groups of 8""" 70 | color_group = [[colors[i], colors[i + 8]] for i in range(1, 7)] 71 | shuffle(color_group) 72 | 73 | bg = [colors[0]] + [c[0] for c in color_group] + [colors[7]] 74 | fg = [colors[8]] + [c[1] for c in color_group] + [colors[15]] 75 | 76 | return bg + fg 77 | 78 | 79 | def write_colors(img, color_list): 80 | """write changes to a cache file to persist customizations""" 81 | full_path = os.path.join(WALL_DIR, img) 82 | color_dict = pywal.colors.colors_to_dict(color_list, full_path) 83 | cache_file = files.get_cache_path(img) 84 | 85 | pywal.export.color(color_dict, "json", cache_file) 86 | 87 | 88 | def change_colors(colors, which): 89 | opt = which 90 | 91 | if which in FILE_DIC: 92 | which = FILE_DIC[which] 93 | 94 | try: 95 | with open("%s.base" % which, "r") as tmp_file: 96 | first_line = tmp_file.readline() 97 | 98 | if "wpgtk-ignore" not in first_line: 99 | tmp_file.seek(0) 100 | tmp_data = tmp_file.read() 101 | tmp_data = tmp_data.format_map(colors) 102 | 103 | with open(which, "w") as target_file: 104 | target_file.write(tmp_data) 105 | logging.info("wrote: %s" % os.path.basename(opt)) 106 | 107 | except KeyError as e: 108 | logging.error("%s in %s - key does not exist" % (e, opt)) 109 | 110 | except IOError: 111 | logging.error("%s - base file does not exist" % opt) 112 | 113 | 114 | def smart_sort(colors): 115 | """automatically set the most look-alike colors to their 116 | corresponding place in the standard xterm colors""" 117 | 118 | sorted_colors = [None] * 8 119 | base_colors = [ 120 | "#000000", "#ff0000", "#00ff00", "#ffff00", 121 | "#0000ff", "#ff00ff", "#00ffff", "#ffffff" 122 | ] 123 | 124 | base_distances = {} 125 | for color in colors[:8]: 126 | color_to_base_distances = [ 127 | (i, util.get_distance(color, base)) 128 | for i, base in enumerate(base_colors) 129 | ] 130 | color_to_base_distances.sort(key=itemgetter(1)) 131 | base_distances[color] = color_to_base_distances 132 | 133 | for color in colors[:8]: 134 | while len(base_distances[color]) >= 1: 135 | position, candidate_distance = base_distances[color][0] 136 | 137 | if sorted_colors[position] is None: 138 | sorted_colors[position] = (color, candidate_distance) 139 | break 140 | elif sorted_colors[position][1] > candidate_distance: 141 | old_color = sorted_colors[position][0] 142 | sorted_colors[position] = (color, candidate_distance) 143 | color = old_color 144 | 145 | base_distances[color].pop(0) 146 | 147 | result = [item[0] for item in sorted_colors] 148 | 149 | return result * 2 150 | 151 | 152 | def auto_adjust(colors): 153 | """create a clear foreground and background set of colors""" 154 | light = settings.getboolean("light_theme", False) 155 | 156 | if settings.getboolean("smart_sort", True): 157 | colors = smart_sort(colors) 158 | 159 | alter_brightness = util.alter_brightness 160 | get_hls_val = util.get_hls_val 161 | 162 | added_sat = 0.25 if light else 0.1 163 | sign = -1 if light else 1 164 | 165 | if light == is_dark_theme(colors): 166 | colors[7], colors[0] = colors[0], colors[7] 167 | 168 | comment = [alter_brightness(colors[0], sign * 25)] 169 | fg = [alter_brightness(colors[7], sign * 60)] 170 | colors = colors[:8] + comment \ 171 | + [alter_brightness(x, sign * get_hls_val(x, "light") * 0.3, added_sat) 172 | for x in colors[1:7]] + fg 173 | 174 | return colors 175 | 176 | 177 | def change_templates(colors): 178 | """call change_colors on each custom template 179 | installed or defined by the user""" 180 | templates = files.get_file_list(OPT_DIR, r".*\.base$") 181 | 182 | try: 183 | for template in templates: 184 | original = template.split(".base").pop(0) 185 | args = (colors, os.path.join(OPT_DIR, original)) 186 | t = threading.Thread(target=change_colors, args=args) 187 | t.start() 188 | 189 | except Exception as e: 190 | logging.error(str(e)) 191 | logging.error("optional file " + original, file=sys.stderr) 192 | 193 | 194 | def add_icon_colors(colors): 195 | try: 196 | icon_dic = dict() 197 | entry = re.compile(r"(.*)=(.*)$") 198 | 199 | with open(FILE_DIC["icon-step1"], "r") as icon_file: 200 | for line in icon_file: 201 | match = entry.search(line) 202 | if match: 203 | icon_dic[match.group(1)] = match.group(2) 204 | 205 | icon_dic["oldglyph"] = icon_dic["newglyph"] 206 | icon_dic["oldfront"] = icon_dic["newfront"] 207 | icon_dic["oldback"] = icon_dic["newback"] 208 | 209 | return icon_dic 210 | 211 | except KeyError: 212 | logging.error("icons - badly formatted base file for icons") 213 | return dict() 214 | 215 | except IOError: 216 | logging.error("icons - base file does not exist") 217 | return dict() 218 | 219 | 220 | def keyword_colors(hexc, is_dark_theme=True): 221 | """extract active and inactive colors from a given 222 | hex color value""" 223 | brightness = util.get_hls_val(hexc, "light") 224 | 225 | active = util.alter_brightness(hexc, brightness * -0.20) \ 226 | if is_dark_theme else util.alter_brightness(hexc, brightness * 0.30) 227 | 228 | inactive = util.alter_brightness(hexc, brightness * -0.45) \ 229 | if is_dark_theme else hexc 230 | 231 | return { 232 | "active": active, 233 | "inactive": inactive, 234 | "newfront": active, 235 | "newback": inactive, 236 | "newglyph": util.alter_brightness(inactive, -15) 237 | } 238 | 239 | 240 | def get_color_dict(pywal_colors, colorscheme): 241 | """ensamble wpgtk color dictionary from pywal color dictionary""" 242 | keyword_set = settings.get('keywords', 'default') 243 | index = settings.getint("active") 244 | index = index if index > 0 else randint(9, 14) 245 | 246 | base_color = pywal_colors["colors"]["color%s" % index] 247 | color_list = list(pywal_colors["colors"].values()) 248 | keyword_dict = keywords.get_keywords_section(keyword_set) 249 | 250 | all_colors = { 251 | "wallpaper": pywal_colors["wallpaper"], 252 | "alpha": pywal_colors["alpha"], 253 | **pywal_colors["special"], 254 | **pywal_colors["colors"], 255 | **add_icon_colors(pywal_colors), 256 | **keyword_colors(base_color, is_dark_theme(color_list)) 257 | } 258 | 259 | all_colors = { 260 | k: pywal.util.Color(v) for k, v in all_colors.items() 261 | } 262 | 263 | try: 264 | user_words = { 265 | k: pywal.util.Color(v.format_map(all_colors)) 266 | for k, v in keyword_dict.items() 267 | } 268 | except KeyError as e: 269 | logging.error("%s - invalid, use double {{}} " 270 | "to escape curly braces" % e) 271 | 272 | return {**all_colors, **user_words} 273 | 274 | 275 | def apply_colorscheme(color_dict): 276 | """Receives a colorscheme dict ensambled by 277 | color.get_color_dict as argument and applies it 278 | system-wide.""" 279 | if os.path.isfile(FILE_DIC["icon-step2"]): 280 | change_colors(color_dict, "icon-step1") 281 | Popen(FILE_DIC["icon-step2"]) 282 | 283 | change_templates(color_dict) 284 | -------------------------------------------------------------------------------- /wpgtk/data/config.py: -------------------------------------------------------------------------------- 1 | import configparser 2 | import shutil 3 | import os 4 | import logging 5 | 6 | __version__ = "6.7.0" 7 | 8 | 9 | settings = None 10 | 11 | HOME = os.getenv("HOME", os.path.expanduser("~")) 12 | CACHE = os.getenv("XDG_CACHE_HOME", os.path.join(HOME, ".cache")) 13 | CONFIG = os.getenv("XDG_CONFIG_HOME", os.path.join(HOME, ".config")) 14 | LOCAL = os.getenv("XDG_DATA_HOME", os.path.join(HOME, ".local", "share")) 15 | 16 | WPG_DIR = os.path.join(CONFIG, "wpg") 17 | CONF_FILE = os.path.join(WPG_DIR, "wpg.conf") 18 | KEYWORD_FILE = os.path.join(WPG_DIR, "keywords.conf") 19 | MODULE_DIR = os.path.abspath(os.path.join(__file__, "../../")) 20 | CONF_BACKUP = os.path.join(MODULE_DIR, "misc/wpg.conf") 21 | WALL_DIR = os.path.join(WPG_DIR, "wallpapers") 22 | SAMPLE_DIR = os.path.join(WPG_DIR, "samples") 23 | SCHEME_DIR = os.path.join(WPG_DIR, "schemes") 24 | FORMAT_DIR = os.path.join(CACHE, "wal") 25 | OPT_DIR = os.path.join(WPG_DIR, "templates") 26 | FILE_DIC = { 27 | "icon-step1": os.path.join( 28 | LOCAL, "icons/flattrcolor/scripts" "/replace_folder_file.sh" 29 | ), 30 | "icon-step2": os.path.join( 31 | LOCAL, 32 | "icons/flattrcolor/scripts" "/replace_script.sh", 33 | ), 34 | } 35 | 36 | 37 | def write_conf(config_path=CONF_FILE): 38 | global config_parser 39 | 40 | with open(config_path, "w") as config_file: 41 | config_parser.write(config_file) 42 | 43 | 44 | def write_keywords(keywords_path=KEYWORD_FILE): 45 | global user_keywords 46 | 47 | with open(keywords_path, "w") as keywords_file: 48 | user_keywords.write(keywords_file) 49 | 50 | 51 | def load_settings(): 52 | """reads the sections of the config file""" 53 | global settings 54 | global user_keywords 55 | global config_parser 56 | 57 | config_parser = configparser.ConfigParser() 58 | config_parser.optionxform = str 59 | config_parser.read(CONF_FILE) 60 | settings = config_parser["settings"] 61 | 62 | 63 | def load_keywords(): 64 | global user_keywords 65 | 66 | if not os.path.exists(KEYWORD_FILE): 67 | open(KEYWORD_FILE, "a").close() 68 | 69 | user_keywords = configparser.ConfigParser() 70 | user_keywords.optionxform = str 71 | user_keywords.read(KEYWORD_FILE) 72 | 73 | if not user_keywords.has_section("default"): 74 | user_keywords.add_section("default") 75 | write_keywords() 76 | 77 | 78 | def init_config(): 79 | os.makedirs(WALL_DIR, exist_ok=True) 80 | os.makedirs(SAMPLE_DIR, exist_ok=True) 81 | os.makedirs(SCHEME_DIR, exist_ok=True) 82 | os.makedirs(FORMAT_DIR, exist_ok=True) 83 | os.makedirs(OPT_DIR, exist_ok=True) 84 | 85 | try: 86 | load_settings() 87 | load_keywords() 88 | except Exception: 89 | logging.error("not a valid config file") 90 | logging.info("copying default config file") 91 | 92 | shutil.copy(CONF_BACKUP, CONF_FILE) 93 | load_settings() 94 | load_keywords() 95 | 96 | 97 | init_config() 98 | -------------------------------------------------------------------------------- /wpgtk/data/files.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import re 4 | import logging 5 | from subprocess import Popen 6 | from pywal.colors import cache_fname, list_backends 7 | 8 | from os.path import join, basename 9 | from .config import ( 10 | settings, 11 | WALL_DIR, 12 | WPG_DIR, 13 | OPT_DIR, 14 | SAMPLE_DIR, 15 | ) 16 | 17 | 18 | def get_file_list(path=WALL_DIR, regex=None): 19 | """gets file names in a given directory, optional regex 20 | parameter to filter the list of files by.""" 21 | 22 | files = [] 23 | 24 | for _, _, filenames in os.walk(path): 25 | files.extend(filenames) 26 | break 27 | 28 | files.sort() 29 | 30 | if regex is not None: 31 | valid = re.compile(regex) 32 | return [elem for elem in files if valid.fullmatch(elem)] 33 | else: 34 | return files 35 | 36 | 37 | def write_script(wallpaper, colorscheme): 38 | """writes the script that should be called on startup 39 | to restore the theme.""" 40 | set_wall = settings.getboolean("set_wallpaper", True) 41 | light_theme = settings.getboolean("light_theme", True) 42 | 43 | flags = "-L" if light_theme else "-" 44 | flags += "rs" if set_wall else "nrs" 45 | 46 | with open(join(WPG_DIR, "wp_init.sh"), "w") as script: 47 | command = "wpg %s '%s' '%s'" % (flags, wallpaper, colorscheme) 48 | script.writelines(["#!/usr/bin/env bash\n", command]) 49 | Popen(["chmod", "+x", join(WPG_DIR, "wp_init.sh")]) 50 | 51 | 52 | def get_cache_path(wallpaper, backend=None): 53 | """get a colorscheme cache path using a wallpaper name""" 54 | if not backend: 55 | backend = settings.get("backend", "wal") 56 | 57 | filepath = join(WALL_DIR, wallpaper) 58 | filename = cache_fname(filepath, backend, False, WPG_DIR) 59 | 60 | return join(*filename) 61 | 62 | 63 | def get_sample_path(wallpaper, backend=None): 64 | """gets a wallpaper colorscheme sample's path""" 65 | if not backend: 66 | backend = settings.get("backend", "wal") 67 | 68 | sample_filename = "%s_%s_sample.png" % (wallpaper, backend) 69 | 70 | return join(SAMPLE_DIR, sample_filename) 71 | 72 | 73 | def add_template(cfile, bfile=None): 74 | """adds a new base file from a config file to wpgtk 75 | or re-establishes link with config file for a 76 | previously generated base file""" 77 | cfile = os.path.realpath(cfile) 78 | 79 | if bfile: 80 | template_name = basename(bfile) 81 | else: 82 | clean_atoms = [atom.lstrip(".") for atom in cfile.split("/")[-3::]] 83 | template_name = "_".join(clean_atoms) + ".base" 84 | 85 | try: 86 | shutil.copy2(cfile, cfile + ".bak") 87 | src_file = bfile if bfile else cfile 88 | 89 | shutil.copy2(src_file, join(OPT_DIR, template_name)) 90 | os.symlink(cfile, join(OPT_DIR, template_name.replace(".base", ""))) 91 | 92 | logging.info("created backup %s.bak" % cfile) 93 | logging.info("added %s @ %s" % (template_name, cfile)) 94 | except Exception as e: 95 | logging.error(str(e.strerror)) 96 | 97 | 98 | def delete_template(basefile): 99 | """delete a template in wpgtk with the given 100 | base file name""" 101 | base_file = join(OPT_DIR, basefile) 102 | conf_file = base_file.replace(".base", "") 103 | 104 | try: 105 | os.remove(base_file) 106 | if os.path.islink(conf_file): 107 | os.remove(conf_file) 108 | except Exception as e: 109 | logging.error(str(e.strerror)) 110 | 111 | 112 | def delete_colorschemes(colorscheme): 113 | """delete all files related to the given colorscheme""" 114 | for backend in list_backends(): 115 | try: 116 | os.remove(get_cache_path(colorscheme, backend)) 117 | os.remove(get_sample_path(colorscheme, backend)) 118 | except OSError: 119 | pass 120 | 121 | 122 | def change_current(filename): 123 | """update symlink to point to the current wallpaper""" 124 | os.symlink(join(WALL_DIR, filename), join(WPG_DIR, ".currentTmp")) 125 | os.rename(join(WPG_DIR, ".currentTmp"), join(WPG_DIR, ".current")) 126 | -------------------------------------------------------------------------------- /wpgtk/data/keywords.py: -------------------------------------------------------------------------------- 1 | from .config import user_keywords, write_keywords 2 | 3 | KEY_LENGTH = 5 4 | VAL_LENGTH = 2 5 | 6 | 7 | def delete_keywords_section(name): 8 | if name != 'default': 9 | user_keywords.remove_section(name) 10 | write_keywords() 11 | 12 | 13 | def create_keywords_section(name): 14 | user_keywords.add_section(name) 15 | write_keywords() 16 | 17 | 18 | def get_keywords_section(theme): 19 | """get keyword file configparser for current wallpaper 20 | or create one if it does not exist""" 21 | if not user_keywords.has_section(theme): 22 | create_keywords_section(theme) 23 | 24 | return user_keywords[theme] 25 | 26 | 27 | def update_key(old_keyword, new_keyword, theme=None): 28 | """validates and updates a keyword for a wallpaper""" 29 | if not new_keyword: 30 | raise Exception('Keyword must be longer than 5 characters') 31 | 32 | keywords = get_keywords_section(theme) 33 | keywords[new_keyword] = keywords[old_keyword] 34 | 35 | if (old_keyword != new_keyword): 36 | keywords.pop(old_keyword, None) 37 | 38 | write_keywords() 39 | 40 | 41 | def update_value(keyword, value, theme): 42 | """update the value to replace the user defined keyword with""" 43 | if not value: 44 | raise Exception('Value must exist') 45 | 46 | keywords = get_keywords_section(theme) 47 | keywords[keyword] = value 48 | 49 | write_keywords() 50 | 51 | 52 | def create_pair(keyword, value, theme): 53 | """create a key value pair for a wallpaper""" 54 | if not value: 55 | raise Exception('There must be a value') 56 | 57 | if not keyword: 58 | raise Exception('There must be a keyword') 59 | 60 | keywords = get_keywords_section(theme) 61 | keywords[keyword] = value 62 | 63 | write_keywords() 64 | 65 | 66 | def remove_pair(keyword, theme): 67 | """removes a pair of keyword value for a wallpaper""" 68 | keywords = get_keywords_section(theme) 69 | keywords.pop(keyword, None) 70 | 71 | write_keywords() 72 | -------------------------------------------------------------------------------- /wpgtk/data/reload.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | import subprocess 3 | import tempfile 4 | import os 5 | import logging 6 | from pywal import reload 7 | import configparser 8 | 9 | from . import util 10 | from .config import FORMAT_DIR, HOME, CONFIG, settings 11 | 12 | 13 | def xrdb(): 14 | """Merges both a user's .Xresources and pywal's.""" 15 | reload.xrdb( 16 | [ 17 | os.path.join(FORMAT_DIR, "colors.Xresources"), 18 | os.path.join(HOME, ".Xresources"), 19 | ] 20 | ) 21 | 22 | 23 | def tint2(): 24 | """Reloads tint2 configuration on the fly.""" 25 | if shutil.which("tint2") and util.get_pid("tint2"): 26 | subprocess.Popen(["pkill", "-SIGUSR1", "tint2"]) 27 | 28 | 29 | def polybar(): 30 | """Reloads polybar configuration on the fly.""" 31 | if shutil.which("polybar") and util.get_pid("polybar"): 32 | util.silent_Popen(["polybar-msg", "cmd", "restart"]) 33 | 34 | 35 | def waybar(): 36 | """Reloads waybar configuration on the fly.""" 37 | if shutil.which("waybar") and util.get_pid("waybar"): 38 | subprocess.Popen(["pkill", "-SIGUSR2", "waybar"]) 39 | 40 | 41 | def dunst(): 42 | """Kills dunst so that notify-send reloads it when called.""" 43 | if shutil.which("dunst") and util.get_pid("dunst"): 44 | subprocess.Popen(["killall", "-w", "dunst"]) 45 | subprocess.Popen(["dunst"]) 46 | 47 | 48 | def openbox(): 49 | """Reloads openbox configuration to reload theme""" 50 | if shutil.which("openbox") and util.get_pid("openbox"): 51 | subprocess.Popen(["openbox", "--reconfigure"]) 52 | 53 | 54 | def xsettingsd(theme): 55 | """Call xsettingsd with a tempfile to trigger a reload of the GTK3 theme""" 56 | fd, path = tempfile.mkstemp() 57 | 58 | try: 59 | with os.fdopen(fd, "w+") as tmp: 60 | tmp.write('Net/ThemeName "' + theme + '"\n') 61 | tmp.close() 62 | 63 | util.silent_call(["timeout", "0.2s", "xsettingsd", "-c", path]) 64 | logging.info("reloaded %s from settings.ini using xsettingsd" % theme) 65 | finally: 66 | os.remove(path) 67 | 68 | 69 | def gtk3(): 70 | settings_ini = os.path.join(CONFIG, "gtk-3.0", "settings.ini") 71 | 72 | refresh_gsettings = ( 73 | "gsettings set org.gnome.desktop.interface " 74 | "gtk-theme '' && sleep 0.1 && gsettings set " 75 | "org.gnome.desktop.interface gtk-theme '{}'" 76 | ) 77 | 78 | refresh_xfsettings = ( 79 | "xfconf-query -c xsettings -p /Net/ThemeName -s" 80 | " '' && sleep 0.1 && xfconf-query -c xsettings -p" 81 | " /Net/ThemeName -s '{}'" 82 | ) 83 | 84 | if shutil.which("gsettings"): 85 | cmd = ["gsettings", "get", "org.gnome.desktop.interface", "gtk-theme"] 86 | gsettings_theme = ( 87 | subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) 88 | .communicate()[0] 89 | .decode() 90 | .strip("' \n") 91 | ) 92 | 93 | xfsettings_theme = None 94 | if shutil.which("xfconf-query"): 95 | cmd = ["xfconf-query", "-c", "xsettings", "-p", "/Net/ThemeName"] 96 | xfsettings_theme = ( 97 | subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) 98 | .communicate()[0] 99 | .decode() 100 | .strip("' \n") 101 | ) 102 | 103 | if util.get_pid("gsd-settings") and gsettings_theme: 104 | subprocess.call(refresh_gsettings.format(gsettings_theme), shell=True) 105 | logging.info("Reloaded %s theme via gsd-settings" % gsettings_theme) 106 | 107 | elif util.get_pid("xfsettingsd") and xfsettings_theme: 108 | subprocess.call(refresh_xfsettings.format(xfsettings_theme), shell=True) 109 | logging.info("reloaded %s theme via xfsettingsd" % xfsettings_theme) 110 | 111 | # no settings daemon is running. 112 | # So GTK is getting theme info from gtkrc file 113 | # using xsettingd to set the same theme (parsing it from gtkrc) 114 | elif shutil.which("xsettingsd"): 115 | if os.path.isfile(settings_ini): 116 | gtkrc = configparser.ConfigParser() 117 | gtkrc.read(settings_ini) 118 | theme = ( 119 | gtkrc["Settings"].get("gtk-theme-name", "FlatColor") 120 | if "Settings" in gtkrc 121 | else "FlatColor" 122 | ) 123 | xsettingsd(theme) 124 | else: 125 | xsettingsd("FlatColor") 126 | 127 | # The system has no known settings daemon installed, 128 | # but dconf gtk-theme exists, just refreshing its theme 129 | # Because user might be using unknown settings daemon 130 | elif shutil.which("gsettings") and gsettings_theme: 131 | subprocess.Popen(refresh_gsettings.format(gsettings_theme), shell=True) 132 | logging.warning( 133 | "No settings daemon found, just refreshing %s theme from gsettings" 134 | % gsettings_theme 135 | ) 136 | 137 | 138 | def all(): 139 | """Calls all possible reload methods at once.""" 140 | xrdb() 141 | tint2() 142 | dunst() 143 | openbox() 144 | reload.i3() 145 | reload.kitty() 146 | reload.sway() 147 | polybar() 148 | waybar() 149 | 150 | if settings.getboolean("gtk", True): 151 | gtk3() 152 | -------------------------------------------------------------------------------- /wpgtk/data/sample.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pywal 3 | 4 | from .config import SAMPLE_DIR 5 | 6 | try: 7 | import Image 8 | except ImportError: 9 | from PIL import Image 10 | 11 | 12 | def create_sample(colors, f=os.path.join(SAMPLE_DIR, ".tmp.sample.png")): 13 | """Creates sample image from a pywal color dictionary""" 14 | im = Image.new("RGB", (480, 50), "white") 15 | pix = im.load() 16 | width_sample = im.size[0]//(len(colors)//2) 17 | 18 | for i, c in enumerate(colors[:8]): 19 | for j in range(width_sample*i, width_sample*i+width_sample): 20 | for k in range(0, 25): 21 | pix[j, k] = pywal.util.hex_to_rgb(c) 22 | 23 | for i, c in enumerate(colors[8:16]): 24 | for j in range(width_sample*i, width_sample*i+width_sample): 25 | for k in range(25, 50): 26 | pix[j, k] = pywal.util.hex_to_rgb(c) 27 | 28 | im.save(f) 29 | -------------------------------------------------------------------------------- /wpgtk/data/themer.py: -------------------------------------------------------------------------------- 1 | import pywal 2 | import shutil 3 | import logging 4 | from os import remove, path, symlink 5 | from subprocess import Popen 6 | 7 | from .config import WPG_DIR, WALL_DIR, FORMAT_DIR, settings, user_keywords 8 | from . import color 9 | from . import files 10 | from . import sample 11 | from . import reload 12 | 13 | 14 | def create_theme(filepath): 15 | """create a colors-scheme from a filepath""" 16 | filepath = path.realpath(filepath) 17 | filename = path.basename(filepath).replace(" ", "_") 18 | tmplink = path.join(WALL_DIR, ".tmp.link") 19 | 20 | symlink(filepath, tmplink) 21 | shutil.move(tmplink, path.join(WALL_DIR, filename)) 22 | 23 | try: 24 | return color.get_color_list(filename) 25 | except SystemExit: 26 | return set_fallback_theme(filename) 27 | 28 | 29 | def set_theme(wallpaper, colorscheme, restore=False): 30 | """apply a given wallpaper and a given colorscheme""" 31 | use_vte = settings.getboolean("vte", False) 32 | is_file = path.isdir(colorscheme) or path.isfile(colorscheme) 33 | target = colorscheme if is_file else path.join(WALL_DIR, colorscheme) 34 | 35 | set_wall = settings.getboolean("set_wallpaper", True) 36 | set_term = settings.getboolean("terminal", True) 37 | reload_all = settings.getboolean("reload", True) 38 | colors = color.get_pywal_dict(target, is_file) 39 | pywal.sequences.send(colors, WPG_DIR, to_send=set_term, vte_fix=use_vte) 40 | 41 | if not restore: 42 | pywal.export.every(colors, FORMAT_DIR) 43 | color.apply_colorscheme(color.get_color_dict(colors, colorscheme)) 44 | if reload_all: 45 | reload.all() 46 | else: 47 | reload.xrdb() 48 | 49 | if set_wall: 50 | filepath = path.join(WALL_DIR, wallpaper) 51 | imagepath = filepath if path.isfile(filepath) else colors["wallpaper"] 52 | pywal.wallpaper.change(imagepath) 53 | 54 | files.write_script(wallpaper, colorscheme) 55 | files.change_current(wallpaper) 56 | 57 | if settings.getboolean('execute_cmd', False) and not restore: 58 | Popen(settings['command'].split()) 59 | 60 | 61 | def delete_theme(filename): 62 | remove(path.join(WALL_DIR, filename)) 63 | files.delete_colorschemes(filename) 64 | user_keywords.remove_section(filename) 65 | 66 | 67 | def get_current(): 68 | image = path.basename(path.realpath(path.join(WPG_DIR, '.current'))) 69 | return image 70 | 71 | 72 | def reset_theme(theme_name): 73 | """restore a colorscheme to its original state by deleting 74 | and re adding the image""" 75 | files.delete_colorschemes(theme_name) 76 | 77 | try: 78 | return color.get_color_list(theme_name) 79 | except SystemExit: 80 | return set_fallback_theme(theme_name) 81 | 82 | 83 | def import_theme(wallpaper, json_file, theme=False): 84 | """import a colorscheme from a JSON file either in 85 | terminal.sexy or pywal format""" 86 | json_file = path.realpath(json_file) 87 | filename = path.basename(json_file) 88 | 89 | if theme: 90 | theme = pywal.theme.file(filename) 91 | color_list = list(theme["colors"].values()) 92 | else: 93 | try: 94 | color_list = color.get_color_list(json_file, True) 95 | except IOError: 96 | logging.error("file does not exist") 97 | return 98 | 99 | color.write_colors(wallpaper, color_list) 100 | sample.create_sample(color_list, files.get_sample_path(wallpaper)) 101 | logging.info("applied %s to %s" % (filename, wallpaper)) 102 | 103 | 104 | def set_fallback_theme(wallpaper): 105 | """fallback theme for when color generation fails""" 106 | theme = pywal.theme.file("random") 107 | 108 | color_list = list(theme["colors"].values()) 109 | color.write_colors(wallpaper, color_list) 110 | sample.create_sample(color_list, files.get_sample_path(wallpaper)) 111 | 112 | return color_list 113 | 114 | 115 | def set_pywal_theme(theme_name, light): 116 | """sets a pywal theme and applies it to wpgtk""" 117 | current = get_current() 118 | theme = pywal.theme.file(theme_name, light) 119 | 120 | color_list = list(theme["colors"].values()) 121 | color.write_colors(current, color_list) 122 | sample.create_sample(color_list, files.get_sample_path(current)) 123 | 124 | set_theme(current, current) 125 | 126 | 127 | def export_theme(wallpaper, json_path="."): 128 | """export a colorscheme to json format""" 129 | try: 130 | if(path.isdir(json_path)): 131 | json_path = path.join(json_path, wallpaper + ".json") 132 | 133 | shutil.copy2(path.join(files.get_cache_path(wallpaper)), json_path) 134 | logging.info("theme for %s successfully exported", wallpaper) 135 | except IOError as e: 136 | logging.error("file not available") 137 | logging.error(e.message) 138 | -------------------------------------------------------------------------------- /wpgtk/data/util.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | import subprocess 4 | from math import sqrt 5 | from colorsys import rgb_to_hls, hls_to_rgb 6 | from pywal.util import rgb_to_hex, hex_to_rgb 7 | 8 | 9 | def get_distance(hex_src, hex_tgt): 10 | """gets color distance between to hex values""" 11 | r1, g1, b1 = hex_to_rgb(hex_src) 12 | r2, g2, b2 = hex_to_rgb(hex_tgt) 13 | 14 | return sqrt((r2 - r1) ** 2 + (g2 - g1) ** 2 + (b2 - b1) ** 2) 15 | 16 | 17 | def get_hls_val(hexv, what): 18 | """gets a color in hue light and saturation format""" 19 | whatdict = {"hue": 0, "light": 1, "sat": 2} 20 | hls = hex_to_hls(hexv) 21 | 22 | return hls[whatdict[what]] 23 | 24 | 25 | def set_hls_val(hexv, what, val): 26 | """assign a value to a hls color and return a 27 | converted hex value""" 28 | whatdict = {"hue": 0, "light": 1, "sat": 2} 29 | hls = list(hex_to_hls(hexv)) 30 | 31 | hls[whatdict[what]] = val 32 | return hls_to_hex(hls) 33 | 34 | 35 | def hex_to_hls(hex_string): 36 | """convert a hex value to hls coordinates""" 37 | r, g, b = hex_to_rgb(hex_string) 38 | return rgb_to_hls(r, g, b) 39 | 40 | 41 | def hls_to_hex(hls): 42 | """convert a hls coordinate to hex code""" 43 | h, l, s = hls 44 | r, g, b = hls_to_rgb(h, l, s) 45 | rgb_int = [max(min(int(elem), 255), 0) for elem in [r, g, b]] 46 | 47 | return rgb_to_hex(rgb_int) 48 | 49 | 50 | def alter_brightness(hex_string, amount, sat=0): 51 | """alters amount of light and saturation in a color""" 52 | h, l, s = hex_to_hls(hex_string) 53 | l = max(min(l + amount, 255), 1) 54 | s = min(max(s - sat, -1), 0) 55 | 56 | return hls_to_hex([h, l, s]) 57 | 58 | 59 | def setup_log(): 60 | logging.basicConfig( 61 | format="[%(levelname)s]" " %(module)-13s %(message)s", 62 | level=logging.INFO, 63 | stream=sys.stdout, 64 | ) 65 | logging.addLevelName(logging.ERROR, "e") 66 | logging.addLevelName(logging.INFO, "i") 67 | logging.addLevelName(logging.WARNING, "w") 68 | 69 | 70 | def silent_call(cmd): 71 | """Call a system command and hide it's output""" 72 | subprocess.call(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) 73 | 74 | 75 | def silent_Popen(cmd): 76 | """Popen a system command and hide it's output""" 77 | subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) 78 | 79 | 80 | def get_pid(name): 81 | """Check if a process is running, borrowed from a newer pywal version""" 82 | try: 83 | subprocess.check_output(["pidof", "-s", name]) 84 | except subprocess.CalledProcessError: 85 | return False 86 | 87 | return True 88 | -------------------------------------------------------------------------------- /wpgtk/gui/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deviantfero/wpgtk/fd8af752726c38a2dba71df091f7d0b002f6d7d1/wpgtk/gui/__init__.py -------------------------------------------------------------------------------- /wpgtk/gui/color_grid.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import pywal 4 | 5 | from ..data.config import SAMPLE_DIR 6 | from ..data import color 7 | from ..data import util 8 | from ..data import files 9 | from ..data import sample 10 | from ..data import themer 11 | from . import util as gui_util 12 | 13 | from .color_picker import ColorDialog 14 | from gi import require_version 15 | 16 | require_version("Gtk", "3.0") 17 | from gi.repository import Gtk, Gdk, GdkPixbuf # noqa: E402 18 | 19 | # TODO: remove current_walls call, use simple list 20 | # TODO: use simple text combo 21 | # TODO: only update pixbuf if parent has same color scheme 22 | current_walls = files.get_file_list() 23 | PAD = 10 24 | 25 | 26 | class ColorGrid(Gtk.Grid): 27 | def __init__(self, parent): 28 | Gtk.Grid.__init__(self) 29 | self.parent = parent 30 | self.set_border_width(PAD) 31 | self.set_column_homogeneous(1) 32 | self.set_row_spacing(PAD) 33 | self.set_column_spacing(PAD) 34 | 35 | self.colorgrid = Gtk.Grid() 36 | self.colorgrid.set_border_width(PAD) 37 | self.colorgrid.set_column_homogeneous(1) 38 | self.colorgrid.set_row_spacing(PAD) 39 | self.colorgrid.set_column_spacing(PAD) 40 | 41 | self.sat_add = Gtk.Button("+") 42 | self.sat_add.set_sensitive(False) 43 | 44 | self.sat_red = Gtk.Button("-") 45 | self.sat_red.set_sensitive(False) 46 | 47 | self.sat_add.connect("pressed", self.hls_change, "sat", "add") 48 | self.sat_red.connect("pressed", self.hls_change, "sat", "red") 49 | self.sat_lbl = Gtk.Label("Saturation:") 50 | 51 | self.light_add = Gtk.Button("+") 52 | self.light_add.set_sensitive(False) 53 | 54 | self.light_red = Gtk.Button("-") 55 | self.light_red.set_sensitive(False) 56 | 57 | self.light_add.connect("pressed", self.hls_change, "light", "add") 58 | self.light_red.connect("pressed", self.hls_change, "light", "red") 59 | self.light_lbl = Gtk.Label("Brightness:") 60 | 61 | self.sat_light_grid = Gtk.Grid() 62 | self.sat_light_grid.set_column_homogeneous(1) 63 | self.sat_light_grid.set_column_spacing(PAD) 64 | self.sat_light_grid.set_row_spacing(PAD) 65 | 66 | self.button_grid = Gtk.Grid() 67 | self.button_grid.set_column_homogeneous(1) 68 | self.button_grid.set_column_spacing(PAD) 69 | self.button_grid.set_row_spacing(PAD) 70 | 71 | self.combo_grid = Gtk.Grid() 72 | self.combo_grid.set_column_homogeneous(1) 73 | self.combo_grid.set_column_spacing(PAD) 74 | self.combo_grid.set_row_spacing(PAD) 75 | 76 | self.color_list = ["000000"] * 16 77 | self.button_list = [Gtk.Button("000000") for x in range(16)] 78 | self.selected_file = "" 79 | for button in self.button_list: 80 | button.connect("pressed", self.on_color_click) 81 | button.set_sensitive(False) 82 | 83 | cont = 0 84 | for y in range(0, 8, 2): 85 | for x in range(0, 4): 86 | label = Gtk.Label(cont) 87 | self.colorgrid.attach(label, x, y, 1, 1) 88 | self.colorgrid.attach(self.button_list[cont], x, y + 1, 1, 1) 89 | cont += 1 90 | 91 | sample_name = os.path.join(SAMPLE_DIR, ".no_sample.sample.png") 92 | self.sample = Gtk.Image() 93 | 94 | pixbuf_sample = gui_util.get_sample_pixbuf(sample_name) 95 | if pixbuf_sample is not None: 96 | self.sample.set_from_pixbuf(self.pixbuf_sample) 97 | 98 | self.shuffle_button = Gtk.Button("Shuffle colors") 99 | self.shuffle_button.connect("pressed", self.on_shuffle_click) 100 | self.shuffle_button.set_sensitive(False) 101 | 102 | self.import_button = Gtk.Button("import") 103 | self.import_button.set_sensitive(False) 104 | self.import_button.connect("pressed", self.on_import_click) 105 | 106 | self.ok_button = Gtk.Button("Save") 107 | self.ok_button.connect("pressed", self.on_ok_click) 108 | self.ok_button.set_sensitive(False) 109 | 110 | self.auto_button = Gtk.Button("Auto-adjust") 111 | self.auto_button.connect("pressed", self.on_auto_click) 112 | self.auto_button.set_sensitive(False) 113 | 114 | self.reset_button = Gtk.Button("Reset") 115 | self.reset_button.set_sensitive(False) 116 | self.reset_button.connect("pressed", self.on_reset_click) 117 | 118 | self.done_lbl = Gtk.Label("") 119 | 120 | option_list = Gtk.ListStore(str) 121 | for elem in list(files.get_file_list()): 122 | option_list.append([elem]) 123 | 124 | self.option_combo = Gtk.ComboBox.new_with_model(option_list) 125 | self.renderer_text = Gtk.CellRendererText() 126 | self.option_combo.pack_start(self.renderer_text, True) 127 | self.option_combo.add_attribute(self.renderer_text, "text", 0) 128 | self.option_combo.set_entry_text_column(0) 129 | self.option_combo.connect("changed", self.combo_box_change) 130 | 131 | self.combo_grid.attach(self.option_combo, 0, 0, 3, 1) 132 | self.combo_grid.attach(self.reset_button, 3, 0, 1, 1) 133 | 134 | self.button_grid.attach(self.ok_button, 0, 0, 1, 1) 135 | self.button_grid.attach(self.auto_button, 1, 0, 1, 1) 136 | self.button_grid.attach(self.shuffle_button, 2, 0, 1, 1) 137 | self.button_grid.attach(self.import_button, 3, 0, 1, 1) 138 | 139 | self.sat_light_grid.attach(self.sat_lbl, 0, 0, 1, 1) 140 | self.sat_light_grid.attach(self.sat_red, 1, 0, 1, 1) 141 | self.sat_light_grid.attach(self.sat_add, 2, 0, 1, 1) 142 | 143 | self.sat_light_grid.attach(self.light_lbl, 3, 0, 1, 1) 144 | self.sat_light_grid.attach(self.light_red, 4, 0, 1, 1) 145 | self.sat_light_grid.attach(self.light_add, 5, 0, 1, 1) 146 | 147 | self.attach(self.combo_grid, 0, 0, 1, 1) 148 | self.attach(self.button_grid, 0, 1, 1, 1) 149 | self.attach(self.colorgrid, 0, 2, 1, 1) 150 | self.attach(self.sample, 0, 3, 1, 1) 151 | self.attach(self.sat_light_grid, 0, 4, 1, 1) 152 | self.attach(self.done_lbl, 0, 5, 1, 1) 153 | 154 | def render_buttons(self): 155 | for x, button in enumerate(self.button_list): 156 | gcolor = Gdk.color_parse(self.color_list[x]) 157 | if util.get_hls_val(self.color_list[x], "light") < 99: 158 | fgcolor = Gdk.color_parse("#FFFFFF") 159 | else: 160 | fgcolor = Gdk.color_parse("#000000") 161 | button.set_label(self.color_list[x]) 162 | button.set_sensitive(True) 163 | button.modify_bg(Gtk.StateType.NORMAL, gcolor) 164 | button.modify_fg(Gtk.StateType.NORMAL, fgcolor) 165 | 166 | def render_theme(self): 167 | sample_path = files.get_sample_path(self.selected_file) 168 | 169 | try: 170 | self.color_list = color.get_color_list(self.selected_file) 171 | except SystemExit: 172 | self.color_list = themer.set_fallback_theme(self.selected_file) 173 | self.render_buttons() 174 | 175 | pixbuf_sample = gui_util.get_sample_pixbuf(sample_path) 176 | if pixbuf_sample is None: 177 | sample.create_sample(self.color_list, sample_path) 178 | pixbuf_sample = gui_util.get_sample_pixbuf(sample_path) 179 | 180 | self.sample.set_from_pixbuf(pixbuf_sample) 181 | self.parent.sample.set_from_pixbuf(pixbuf_sample) 182 | 183 | def hls_change(self, widget, *gparam): 184 | if gparam[0] == "sat": 185 | val = 0.05 if gparam[1] == "add" else -0.05 186 | self.color_list = [ 187 | util.alter_brightness(x, 0, val) for x in self.color_list 188 | ] 189 | elif gparam[0] == "light": 190 | val = 10 if gparam[1] == "add" else -10 191 | self.color_list = [ 192 | util.alter_brightness(x, val, 0) for x in self.color_list 193 | ] 194 | self.render_buttons() 195 | self.render_sample() 196 | 197 | def render_sample(self): 198 | sample.create_sample(self.color_list) 199 | sample_path = os.path.join(SAMPLE_DIR, ".tmp.sample.png") 200 | self.pixbuf_sample = GdkPixbuf.Pixbuf.new_from_file_at_size( 201 | sample_path, width=500, height=300 202 | ) 203 | self.sample.set_from_pixbuf(self.pixbuf_sample) 204 | 205 | def on_ok_click(self, widget): 206 | color.write_colors(self.selected_file, self.color_list) 207 | tmpfile = os.path.join(SAMPLE_DIR, ".tmp.sample.png") 208 | 209 | if os.path.isfile(tmpfile): 210 | shutil.move( 211 | os.path.join(SAMPLE_DIR, ".tmp.sample.png"), 212 | files.get_sample_path(self.selected_file), 213 | ) 214 | 215 | self.done_lbl.set_text("Changes saved") 216 | sample_path = files.get_sample_path(self.selected_file) 217 | self.parent.pixbuf_sample = GdkPixbuf.Pixbuf.new_from_file_at_size( 218 | sample_path, width=500, height=300 219 | ) 220 | self.parent.sample.set_from_pixbuf(self.pixbuf_sample) 221 | 222 | def on_auto_click(self, widget): 223 | self.color_list = color.auto_adjust(self.color_list) 224 | self.render_buttons() 225 | self.render_sample() 226 | 227 | def on_reset_click(self, widget): 228 | themer.reset_theme(self.selected_file) 229 | self.render_theme() 230 | 231 | def on_import_click(self, widget): 232 | fcd = Gtk.FileChooserDialog( 233 | "Select a colorscheme", 234 | self.parent, 235 | Gtk.FileChooserAction.OPEN, 236 | ( 237 | Gtk.STOCK_CANCEL, 238 | Gtk.ResponseType.CANCEL, 239 | Gtk.STOCK_OPEN, 240 | Gtk.ResponseType.OK, 241 | ), 242 | ) 243 | 244 | filter = Gtk.FileFilter() 245 | filter.set_name("JSON colorscheme") 246 | filter.add_mime_type("application/json") 247 | fcd.add_filter(filter) 248 | response = fcd.run() 249 | 250 | if response == Gtk.ResponseType.OK: 251 | self.color_list = color.get_color_list(fcd.get_filename(), True) 252 | self.render_buttons() 253 | self.render_sample() 254 | fcd.destroy() 255 | 256 | def on_shuffle_click(self, widget): 257 | self.color_list = color.shuffle_colors(self.color_list) 258 | self.render_buttons() 259 | self.render_sample() 260 | 261 | def on_color_click(self, widget): 262 | self.done_lbl.set_text("") 263 | gcolor = Gdk.RGBA() 264 | gcolor.parse(widget.get_label()) 265 | dialog = ColorDialog(self.parent, self.selected_file, gcolor) 266 | response = dialog.run() 267 | 268 | if response == Gtk.ResponseType.OK: 269 | r, g, b, _ = dialog.colorchooser.get_rgba() 270 | rgb = list(map(lambda x: round(x * 100 * 2.55), [r, g, b])) 271 | hex_color = pywal.util.rgb_to_hex(rgb) 272 | widget.set_label(hex_color) 273 | 274 | gcolor = Gdk.color_parse(hex_color) 275 | if util.get_hls_val(hex_color, "light") < 100: 276 | fgcolor = Gdk.color_parse("#FFFFFF") 277 | else: 278 | fgcolor = Gdk.color_parse("#000000") 279 | 280 | widget.set_sensitive(True) 281 | widget.modify_bg(Gtk.StateType.NORMAL, gcolor) 282 | widget.modify_fg(Gtk.StateType.NORMAL, fgcolor) 283 | 284 | for i, c in enumerate(self.button_list): 285 | if c.get_label() != self.color_list[i]: 286 | self.color_list[i] = c.get_label() 287 | self.render_sample() 288 | dialog.destroy() 289 | 290 | def combo_box_change(self, widget): 291 | self.done_lbl.set_text("") 292 | x = self.option_combo.get_active() 293 | 294 | self.auto_button.set_sensitive(True) 295 | self.shuffle_button.set_sensitive(True) 296 | self.ok_button.set_sensitive(True) 297 | self.import_button.set_sensitive(True) 298 | self.light_add.set_sensitive(True) 299 | self.light_red.set_sensitive(True) 300 | self.reset_button.set_sensitive(True) 301 | self.sat_add.set_sensitive(True) 302 | self.sat_red.set_sensitive(True) 303 | 304 | current_walls = files.get_file_list() 305 | self.selected_file = current_walls[x] 306 | self.render_theme() 307 | -------------------------------------------------------------------------------- /wpgtk/gui/color_picker.py: -------------------------------------------------------------------------------- 1 | from ..data import util 2 | from gi import require_version 3 | require_version("Gtk", "3.0") 4 | require_version("Gdk", "3.0") 5 | from gi.repository import Gtk # noqa: E402 6 | from gi.repository import Gdk # noqa: E402 7 | 8 | 9 | class ColorDialog(Gtk.Dialog): 10 | 11 | def __init__(self, parent, current_file, gcolor): 12 | Gtk.Dialog.__init__(self, "Pick a Color", parent, 0, 13 | (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, 14 | Gtk.STOCK_OK, Gtk.ResponseType.OK)) 15 | 16 | self.set_default_size(150, 100) 17 | box = self.get_content_area() 18 | box.set_border_width(10) 19 | box.set_spacing(10) 20 | 21 | sat_box = Gtk.Box(spacing=10, orientation=Gtk.Orientation.HORIZONTAL) 22 | light_box = Gtk.Box(spacing=10, orientation=Gtk.Orientation.HORIZONTAL) 23 | 24 | self.colorchooser = Gtk.ColorChooserWidget(show_editor=True) 25 | self.colorchooser.set_use_alpha(False) 26 | self.colorchooser.set_rgba(gcolor) 27 | 28 | r, g, b, _ = list(map(lambda x: round(x*100*2.55), gcolor)) 29 | hue, light, sat = util.rgb_to_hls(r, g, b) 30 | 31 | self.sat_lbl = Gtk.Label('Saturation') 32 | self.light_lbl = Gtk.Label('Light ') 33 | 34 | sat_range = Gtk.Adjustment(0, 0, 1, 0.1, 0.1, 0) 35 | self.sat_slider = Gtk.Scale(orientation=Gtk.Orientation.HORIZONTAL, 36 | adjustment=sat_range) 37 | self.sat_slider.set_value(-sat) 38 | self.sat_slider.set_digits(2) 39 | self.sat_slider.connect('value-changed', self.slider_changed, 'sat') 40 | 41 | light_range = Gtk.Adjustment(5, 0, 255, 1, 10, 0) 42 | self.light_slider = Gtk.Scale(orientation=Gtk.Orientation.HORIZONTAL, 43 | adjustment=light_range) 44 | self.light_slider.set_value(light) 45 | self.light_slider.connect('value-changed', 46 | self.slider_changed, 'light') 47 | 48 | box.add(self.colorchooser) 49 | 50 | sat_box.pack_start(self.sat_lbl, True, True, 0) 51 | sat_box.pack_start(self.sat_slider, True, True, 0) 52 | 53 | light_box.pack_start(self.light_lbl, True, True, 0) 54 | light_box.pack_start(self.light_slider, True, True, 0) 55 | 56 | box.add(light_box) 57 | box.add(sat_box) 58 | 59 | self.show_all() 60 | 61 | def slider_changed(self, slider, *arg): 62 | newval = -slider.get_value() if arg[0] == 'sat' else slider.get_value() 63 | 64 | red, green, blue, _ = self.colorchooser.get_rgba() 65 | rgb = list(map(lambda x: round(x*100*2.55), [red, green, blue])) 66 | newhex = util.set_hls_val(util.rgb_to_hex(rgb), arg[0], newval) 67 | 68 | new_gcolor = Gdk.RGBA() 69 | new_gcolor.parse(newhex) 70 | self.colorchooser.set_rgba(new_gcolor) 71 | -------------------------------------------------------------------------------- /wpgtk/gui/keyword_dialog.py: -------------------------------------------------------------------------------- 1 | from gi import require_version 2 | require_version("Gtk", "3.0") 3 | from gi.repository import Gtk # noqa: E402 4 | 5 | 6 | class KeywordDialog(Gtk.Dialog): 7 | 8 | def __init__(self, parent): 9 | Gtk.Dialog.__init__(self, "Name you keyword/value set", parent, 0, 10 | (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, 11 | Gtk.STOCK_OK, Gtk.ResponseType.OK)) 12 | 13 | self.set_default_size(150, 100) 14 | self.name_text_input = Gtk.Entry() 15 | self.error_lbl = Gtk.Label() 16 | 17 | box = self.get_content_area() 18 | box.set_border_width(10) 19 | box.set_spacing(10) 20 | box.add(self.name_text_input) 21 | box.add(self.error_lbl) 22 | 23 | self.show_all() 24 | 25 | def get_section_name(self): 26 | if len(self.name_text_input.get_text()) <= 0: 27 | raise Exception('Empty name not allowed') 28 | 29 | return self.name_text_input.get_text() 30 | -------------------------------------------------------------------------------- /wpgtk/gui/keyword_grid.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from ..data import keywords 3 | from ..data.config import user_keywords, settings, write_conf 4 | from gi import require_version 5 | require_version("Gtk", "3.0") 6 | from .keyword_dialog import KeywordDialog # noqa: E402 7 | from gi.repository import Gtk # noqa: E402 8 | 9 | PAD = 10 10 | 11 | # TODO: if create section, select the new valid section 12 | 13 | 14 | class KeywordGrid(Gtk.Grid): 15 | def __init__(self, parent): 16 | Gtk.Grid.__init__(self) 17 | self.parent = parent 18 | 19 | self.set_border_width(PAD) 20 | self.set_column_homogeneous(1) 21 | self.set_row_spacing(PAD) 22 | self.set_column_spacing(PAD) 23 | 24 | self.liststore = Gtk.ListStore(str, str) 25 | 26 | self.remove_button = Gtk.Button('Remove Keyword') 27 | self.remove_button.connect('clicked', self.remove_keyword) 28 | 29 | self.add_button = Gtk.Button('Add Keyword') 30 | self.add_button.connect('clicked', self.append_new_keyword) 31 | 32 | self.choose_button = Gtk.Button('Choose Set') 33 | self.choose_button.connect('clicked', self.choose_keywords_section) 34 | 35 | self.create_button = Gtk.Button('Create Set') 36 | self.create_button.connect('clicked', self.create_keywords_section) 37 | 38 | self.delete_button = Gtk.Button('Delete Set') 39 | self.delete_button.connect('clicked', self.delete_keywords_section) 40 | 41 | self.sections_combo = Gtk.ComboBoxText() 42 | self.sections_combo.connect("changed", self.on_section_change) 43 | self.reload_section_list() 44 | 45 | self.selected_file = settings.get("keywords", "default") 46 | idx = list(user_keywords.sections()).index(self.selected_file) 47 | self.sections_combo.set_active(idx) 48 | self.delete_button.set_sensitive(self.selected_file != 'default') 49 | self.choose_button.set_sensitive(False) 50 | 51 | self.reload_keyword_list() 52 | 53 | self.status_lbl = Gtk.Label('') 54 | self.keyword_tree = Gtk.TreeView(model=self.liststore) 55 | 56 | scroll = Gtk.ScrolledWindow() 57 | scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) 58 | scroll.set_min_content_height(320) 59 | scroll.set_propagate_natural_height(True) 60 | scroll.add(self.keyword_tree) 61 | 62 | self.attach(self.sections_combo, 0, 0, 2, 1) 63 | self.attach(self.choose_button, 2, 0, 1, 1) 64 | self.attach(self.delete_button, 3, 0, 1, 1) 65 | self.attach(self.create_button, 0, 1, 4, 1) 66 | self.attach(scroll, 0, 2, 4, 1) 67 | self.attach(self.add_button, 0, 3, 2, 1) 68 | self.attach(self.remove_button, 2, 3, 2, 1) 69 | self.attach(self.status_lbl, 0, 4, 4, 1) 70 | 71 | key_renderer = Gtk.CellRendererText() 72 | key_renderer.set_property('editable', True) 73 | key_renderer.connect('edited', self.text_edited, 0) 74 | 75 | value_renderer = Gtk.CellRendererText() 76 | value_renderer.set_property('editable', True) 77 | value_renderer.connect('edited', self.text_edited, 1) 78 | 79 | keyword_text = Gtk.TreeViewColumn("Keyword", key_renderer, text=0) 80 | self.keyword_tree.append_column(keyword_text) 81 | 82 | value_text = Gtk.TreeViewColumn("Value", value_renderer, text=1) 83 | self.keyword_tree.append_column(value_text) 84 | 85 | def remove_keyword(self, widget): 86 | self.status_lbl.set_text('') 87 | (m, pathlist) = self.keyword_tree.get_selection().get_selected_rows() 88 | 89 | for path in pathlist: 90 | tree_iter = m.get_iter(path) 91 | value = m.get_value(tree_iter, 0) 92 | keywords.remove_pair(value, self.selected_file) 93 | self.reload_keyword_list() 94 | 95 | def text_edited(self, widget, path, text, col): 96 | self.status_lbl.set_text('') 97 | if (col == 0): 98 | try: 99 | keywords.update_key(self.liststore[path][col], text, 100 | self.selected_file) 101 | except Exception as e: 102 | self.status_lbl.set_text(str(e)) 103 | else: 104 | try: 105 | keywords.update_value(self.liststore[path][0], text, 106 | self.selected_file) 107 | except Exception as e: 108 | self.status_lbl.set_text(str(e)) 109 | self.reload_keyword_list() 110 | 111 | def reload_section_list(self, active='default'): 112 | sections = list(user_keywords.sections()) 113 | self.sections_combo.remove_all() 114 | 115 | for item in sections: 116 | self.sections_combo.append_text(item) 117 | 118 | self.sections_combo.set_active(sections.index(active)) 119 | 120 | def reload_keyword_list(self): 121 | keyword_section = keywords.get_keywords_section(self.selected_file) 122 | 123 | self.liststore.clear() 124 | for k, v in keyword_section.items(): 125 | self.liststore.append([k, v]) 126 | 127 | def on_section_change(self, widget): 128 | self.selected_file = widget.get_active_text() 129 | 130 | if self.selected_file is not None: 131 | self.reload_keyword_list() 132 | self.choose_button.set_sensitive( 133 | settings.get('keywords', 'default') != self.selected_file 134 | ) 135 | settings['keywords'] = self.selected_file 136 | self.delete_button.set_sensitive(self.selected_file != 'default') 137 | 138 | def append_new_keyword(self, widget): 139 | self.status_lbl.set_text('') 140 | keywords.create_pair( 141 | 'keyword' + str(len(self.liststore)), 142 | 'value', 143 | self.selected_file, 144 | ) 145 | self.reload_keyword_list() 146 | 147 | def delete_keywords_section(self, widget): 148 | if self.selected_file: 149 | keywords.delete_keywords_section(self.selected_file) 150 | self.reload_section_list() 151 | 152 | def choose_keywords_section(self, widget): 153 | write_conf() 154 | self.choose_button.set_sensitive(False) 155 | 156 | def create_keywords_section(self, widget): 157 | dialog = KeywordDialog(self.parent) 158 | response = dialog.run() 159 | 160 | if response == Gtk.ResponseType.OK: 161 | try: 162 | section = dialog.get_section_name() 163 | keywords.create_keywords_section(section) 164 | self.reload_section_list(section) 165 | except Exception as e: 166 | logging.error(str(e)) 167 | dialog.destroy() 168 | if response == Gtk.ResponseType.CANCEL: 169 | dialog.destroy() 170 | -------------------------------------------------------------------------------- /wpgtk/gui/option_grid.py: -------------------------------------------------------------------------------- 1 | from gi import require_version 2 | from ..data.config import settings, write_conf 3 | from pywal import colors 4 | require_version("Gtk", "3.0") 5 | from gi.repository import Gtk, Gdk # noqa: E402 6 | 7 | PAD = 10 8 | 9 | 10 | class OptionsGrid(Gtk.Grid): 11 | def __init__(self, parent): 12 | Gtk.Grid.__init__(self) 13 | self.parent = parent 14 | self.set_border_width(PAD) 15 | self.set_column_homogeneous(1) 16 | self.set_row_spacing(PAD) 17 | self.set_column_spacing(PAD) 18 | 19 | # Switch Grid 20 | self.switch_grid = Gtk.Grid() 21 | self.switch_grid.set_border_width(PAD) 22 | self.switch_grid.set_column_homogeneous(1) 23 | self.switch_grid.set_row_spacing(PAD) 24 | self.switch_grid.set_column_spacing(PAD) 25 | 26 | # Active Color Grid 27 | self.active_grid = Gtk.Grid() 28 | self.active_grid.set_border_width(PAD) 29 | self.active_grid.set_column_homogeneous(1) 30 | self.active_grid.set_row_spacing(PAD) 31 | self.active_grid.set_column_spacing(PAD) 32 | 33 | # Setting up ComboBox 34 | color_list = ['Random'] + [str(x) for x in range(1, 16)] 35 | self.color_combo = Gtk.ComboBoxText() 36 | for elem in list(color_list): 37 | self.color_combo.append_text(elem) 38 | self.color_combo.connect("changed", self.combo_box_change, "active") 39 | 40 | # Button 41 | self.color_button = Gtk.Button("Active/Inactive Color") 42 | self.save_button = Gtk.Button("Save") 43 | self.save_button.connect("pressed", self.on_save_button) 44 | 45 | # Backend Combo 46 | self.backend_lbl = Gtk.Label("Select your backend:") 47 | self.backend_combo = Gtk.ComboBoxText() 48 | self.backend_list = colors.list_backends() 49 | 50 | for elem in self.backend_list: 51 | self.backend_combo.append_text(elem) 52 | self.backend_combo.connect("changed", self.combo_box_change, "backend") 53 | 54 | # Switches 55 | self.gtk_switch = Gtk.Switch() 56 | self.gtk_switch.connect("notify::active", self.on_activate, "gtk") 57 | self.lbl_gtk = Gtk.Label("Reload GTK+") 58 | 59 | self.vte_switch = Gtk.Switch() 60 | self.vte_switch.connect( 61 | "notify::active", 62 | self.on_activate, 63 | "vte" 64 | ) 65 | self.lbl_vte = Gtk.Label("Use VTE Fix") 66 | 67 | self.light_theme_switch = Gtk.Switch() 68 | self.light_theme_switch.connect( 69 | "notify::active", 70 | self.on_activate, 71 | "light_theme" 72 | ) 73 | self.lbl_light_theme = Gtk.Label("Use light themes") 74 | 75 | self.wallpaper_switch = Gtk.Switch() 76 | self.wallpaper_switch.connect( 77 | "notify::active", 78 | self.on_activate, 79 | "set_wallpaper" 80 | ) 81 | self.lbl_wallpaper = Gtk.Label("Set wallpaper") 82 | 83 | self.smart_sort_switch = Gtk.Switch() 84 | self.smart_sort_switch.connect( 85 | "notify::active", 86 | self.on_activate, 87 | "smart_sort" 88 | ) 89 | self.lbl_smart_sort = Gtk.Label("Use smart sort") 90 | 91 | self.auto_adjust_switch = Gtk.Switch() 92 | self.auto_adjust_switch.connect( 93 | "notify::active", 94 | self.on_activate, 95 | "auto_adjust" 96 | ) 97 | self.lbl_auto_adjust = Gtk.Label("Always auto adjust") 98 | 99 | self.reload_switch = Gtk.Switch() 100 | self.reload_switch.connect( 101 | "notify::active", 102 | self.on_activate, 103 | "reload" 104 | ) 105 | self.lbl_reload = Gtk.Label("Reload other software") 106 | 107 | self.terminal_switch = Gtk.Switch() 108 | self.terminal_switch.connect( 109 | "notify::active", 110 | self.on_activate, 111 | "terminal" 112 | ) 113 | self.lbl_terminal = Gtk.Label("Change terminal colors") 114 | 115 | # edit cmd 116 | self.editor_lbl = Gtk.Label("Open optional files with:") 117 | self.editor_txt = Gtk.Entry() 118 | self.editor_txt.connect("changed", self.on_txt_change, "editor") 119 | 120 | # cmd 121 | self.command_lbl = Gtk.Label("Run command after") 122 | self.command_exe_lbl = Gtk.Label("Command: ") 123 | 124 | self.command_txt = Gtk.Entry() 125 | self.command_txt.connect("changed", self.on_txt_change, "command") 126 | 127 | self.command_switch = Gtk.Switch() 128 | self.command_switch.connect( 129 | "notify::active", 130 | self.on_activate, 131 | "execute_cmd" 132 | ) 133 | 134 | self.alpha_lbl = Gtk.Label('Alpha:') 135 | self.alpha_txt = Gtk.Entry() 136 | self.alpha_txt.connect("changed", self.on_txt_change, "alpha") 137 | self.load_opt_list() 138 | 139 | # Switch Grid attach 140 | self.switch_grid.attach(self.lbl_wallpaper, 1, 1, 3, 1) 141 | self.switch_grid.attach(self.wallpaper_switch, 4, 1, 1, 1) 142 | 143 | self.switch_grid.attach(self.lbl_gtk, 5, 1, 3, 1) 144 | self.switch_grid.attach(self.gtk_switch, 9, 1, 1, 1) 145 | 146 | self.switch_grid.attach(self.lbl_auto_adjust, 5, 2, 3, 1) 147 | self.switch_grid.attach(self.auto_adjust_switch, 9, 2, 1, 1) 148 | 149 | self.switch_grid.attach(self.command_lbl, 1, 2, 3, 1) 150 | self.switch_grid.attach(self.command_switch, 4, 2, 1, 1) 151 | 152 | self.switch_grid.attach(self.lbl_light_theme, 1, 3, 3, 1) 153 | self.switch_grid.attach(self.light_theme_switch, 4, 3, 1, 1) 154 | 155 | self.switch_grid.attach(self.lbl_smart_sort, 1, 4, 3, 1) 156 | self.switch_grid.attach(self.smart_sort_switch, 4, 4, 1, 1) 157 | 158 | self.switch_grid.attach(self.lbl_vte, 5, 3, 3, 1) 159 | self.switch_grid.attach(self.vte_switch, 9, 3, 1, 1) 160 | 161 | self.switch_grid.attach(self.lbl_reload, 5, 4, 3, 1) 162 | self.switch_grid.attach(self.reload_switch, 9, 4, 1, 1) 163 | 164 | self.switch_grid.attach(self.lbl_terminal, 1, 5, 3, 1) 165 | self.switch_grid.attach(self.terminal_switch, 4, 5, 1, 1) 166 | 167 | # Active Grid attach 168 | self.active_grid.attach(self.backend_lbl, 1, 1, 1, 1) 169 | self.active_grid.attach(self.backend_combo, 2, 1, 1, 1) 170 | self.active_grid.attach(self.color_button, 1, 2, 1, 1) 171 | self.active_grid.attach(self.color_combo, 2, 2, 1, 1) 172 | 173 | self.active_grid.attach(self.editor_lbl, 1, 3, 1, 1) 174 | self.active_grid.attach(self.editor_txt, 2, 3, 1, 1) 175 | 176 | self.active_grid.attach(self.command_exe_lbl, 1, 4, 1, 1) 177 | self.active_grid.attach(self.command_txt, 2, 4, 1, 1) 178 | 179 | self.active_grid.attach(self.alpha_lbl, 1, 5, 1, 1) 180 | self.active_grid.attach(self.alpha_txt, 2, 5, 1, 1) 181 | 182 | self.active_grid.attach(self.save_button, 1, 6, 2, 1) 183 | 184 | self.attach(self.switch_grid, 1, 1, 1, 1) 185 | self.attach(self.active_grid, 1, 2, 1, 1) 186 | 187 | self.save_button.set_sensitive(False) 188 | 189 | def on_activate(self, switch, *gparam): 190 | if(gparam[1] == 'execute_cmd'): 191 | self.command_txt.set_editable(switch.get_active()) 192 | settings[gparam[1]] = str(switch.get_active()).lower() 193 | self.save_button.set_sensitive(True) 194 | 195 | def load_opt_list(self): 196 | current_backend = settings.get("backend", "wal") 197 | idx = self.backend_list.index(current_backend) 198 | self.backend_combo.set_active(idx) 199 | 200 | self.color_combo\ 201 | .set_active(settings.getint("active", 0)) 202 | self.gtk_switch\ 203 | .set_active(settings.getboolean("gtk", True)) 204 | self.command_switch\ 205 | .set_active(settings.getboolean("execute_cmd", False)) 206 | self.light_theme_switch\ 207 | .set_active(settings.getboolean("light_theme", False)) 208 | self.vte_switch\ 209 | .set_active(settings.getboolean("vte", False)) 210 | self.wallpaper_switch\ 211 | .set_active(settings.getboolean("set_wallpaper", True)) 212 | self.smart_sort_switch\ 213 | .set_active(settings.getboolean("smart_sort", True)) 214 | self.auto_adjust_switch\ 215 | .set_active(settings.getboolean("auto_adjust", False)) 216 | self.reload_switch\ 217 | .set_active(settings.getboolean("reload", True)) 218 | self.terminal_switch\ 219 | .set_active(settings.getboolean("terminal", True)) 220 | 221 | self.editor_txt\ 222 | .set_text(settings.get("editor", "urxvt -e vim")) 223 | self.command_txt\ 224 | .set_text(settings.get("command", "yes hi")) 225 | self.command_txt\ 226 | .set_editable(settings.getboolean("execute_cmd", False)) 227 | self.alpha_txt\ 228 | .set_text(settings.get("alpha", "100")) 229 | 230 | def combo_box_change(self, combo, *gparam): 231 | x = combo.get_active() 232 | item = combo.get_active_text() 233 | 234 | if gparam[0] == "active": 235 | settings[gparam[0]] = str(x) 236 | color = Gdk.color_parse(self.parent.cpage.color_list[x]) 237 | self.color_button.modify_bg(Gtk.StateType.NORMAL, color) 238 | if gparam[0] == "backend": 239 | settings[gparam[0]] = item 240 | self.save_button.set_sensitive(True) 241 | 242 | def on_txt_change(self, gtk_entry, *gparam): 243 | settings[gparam[0]] = gtk_entry.get_text() 244 | self.save_button.set_sensitive(True) 245 | 246 | def on_save_button(self, button): 247 | write_conf() 248 | self.save_button.set_sensitive(False) 249 | -------------------------------------------------------------------------------- /wpgtk/gui/template_grid.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | from subprocess import Popen 5 | from ..data.config import OPT_DIR, settings 6 | from ..data import files 7 | 8 | from gi import require_version 9 | require_version("Gtk", "3.0") 10 | from gi.repository import Gtk # noqa: E402 11 | from gi.repository.GdkPixbuf import Pixbuf # noqa: E402 12 | 13 | PAD = 10 14 | icon = 'document-open' 15 | 16 | 17 | class TemplateGrid(Gtk.Grid): 18 | 19 | """A helper for choosing config files 20 | that will be modified with wpgtk's help""" 21 | 22 | def __init__(self, parent): 23 | Gtk.Grid.__init__(self) 24 | self.current = None 25 | self.sel_file = '' 26 | 27 | self.parent = parent 28 | self.set_border_width(PAD) 29 | self.set_column_homogeneous(1) 30 | self.set_row_spacing(PAD) 31 | self.set_column_spacing(PAD) 32 | 33 | self.grid_edit = Gtk.Grid() 34 | self.grid_edit.set_column_homogeneous(1) 35 | self.grid_edit.set_row_spacing(PAD) 36 | self.grid_edit.set_column_spacing(PAD) 37 | 38 | self.button_add = Gtk.Button('Add') 39 | self.button_add.connect('clicked', self.on_add_clicked) 40 | self.button_rm = Gtk.Button('Remove') 41 | self.button_rm.connect('clicked', self.on_rm_clicked) 42 | self.button_edit = Gtk.Button('Edit') 43 | self.button_edit.connect('clicked', self.on_open_clicked) 44 | 45 | self.liststore = Gtk.ListStore(Pixbuf, str) 46 | self.file_view = Gtk.IconView.new() 47 | self.file_view.set_model(self.liststore) 48 | self.file_view.set_activate_on_single_click(True) 49 | self.file_view.set_pixbuf_column(0) 50 | self.file_view.set_text_column(1) 51 | self.file_view.connect('item-activated', self.on_file_click) 52 | 53 | self.scroll = Gtk.ScrolledWindow() 54 | self.scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) 55 | self.scroll.set_min_content_height(400) 56 | self.scroll.add(self.file_view) 57 | 58 | self.item_names = files.get_file_list(OPT_DIR, r".*\.base$") 59 | 60 | for filen in self.item_names: 61 | pixbuf = Gtk.IconTheme.get_default().load_icon(icon, 64, 0) 62 | self.liststore.append([pixbuf, filen]) 63 | 64 | self.grid_edit.attach(self.button_add, 0, 0, 2, 1) 65 | self.grid_edit.attach(self.button_edit, 0, 1, 1, 1) 66 | self.grid_edit.attach(self.button_rm, 1, 1, 1, 1) 67 | self.grid_edit.attach(self.scroll, 0, 2, 2, 1) 68 | 69 | self.attach(self.grid_edit, 0, 0, 1, 1) 70 | 71 | def on_add_clicked(self, widget): 72 | filechooser = Gtk.FileChooserDialog("Select an Image", self.parent, 73 | Gtk.FileChooserAction.OPEN, 74 | (Gtk.STOCK_CANCEL, 75 | Gtk.ResponseType.CANCEL, 76 | Gtk.STOCK_OPEN, 77 | Gtk.ResponseType.OK)) 78 | filefilter = Gtk.FileFilter() 79 | filechooser.set_select_multiple(True) 80 | filefilter.set_name("Text") 81 | filefilter.add_mime_type("text/*") 82 | filechooser.add_filter(filefilter) 83 | response = filechooser.run() 84 | 85 | if response == Gtk.ResponseType.OK: 86 | for f in filechooser.get_filenames(): 87 | files.add_template(f) 88 | self.item_names = files.get_file_list(OPT_DIR, r".*\.base$") 89 | self.liststore = Gtk.ListStore(Pixbuf, str) 90 | for filen in self.item_names: 91 | pixbuf = Gtk.IconTheme.get_default().load_icon(icon, 64, 0) 92 | self.liststore.append([pixbuf, filen]) 93 | self.file_view.set_model(self.liststore) 94 | filechooser.destroy() 95 | self.file_view.unselect_all() 96 | 97 | def on_open_clicked(self, widget): 98 | if self.current is not None: 99 | item = self.item_names[self.current] 100 | args_list = settings['editor'].split(' ') 101 | args_list.append(os.path.join(OPT_DIR, item)) 102 | try: 103 | Popen(args_list) 104 | except Exception as e: 105 | logging.error("malformed editor command") 106 | self.current = None 107 | self.file_view.unselect_all() 108 | 109 | def on_rm_clicked(self, widget): 110 | if self.current is not None: 111 | item = self.item_names.pop(self.current) 112 | files.delete_template(item) 113 | self.liststore = Gtk.ListStore(Pixbuf, str) 114 | for filen in self.item_names: 115 | pixbuf = Gtk.IconTheme.get_default().load_icon(icon, 64, 0) 116 | self.liststore.append([pixbuf, filen]) 117 | self.file_view.set_model(self.liststore) 118 | self.current = None 119 | self.file_view.unselect_all() 120 | 121 | def on_file_click(self, widget, pos): 122 | self.current = int(str(pos)) 123 | self.sel_file = self.liststore[self.current][1] 124 | -------------------------------------------------------------------------------- /wpgtk/gui/theme_picker.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | from . import color_grid 5 | from . import template_grid 6 | from . import option_grid 7 | from . import keyword_grid 8 | from . import util 9 | from ..data import files 10 | from ..data import themer 11 | from ..data.config import WALL_DIR, WPG_DIR, __version__ 12 | 13 | from gi import require_version 14 | 15 | require_version("Gtk", "3.0") 16 | from gi.repository import Gtk # noqa: E402 17 | 18 | PAD = 10 19 | 20 | 21 | class mainWindow(Gtk.Window): 22 | def __init__(self, args): 23 | Gtk.Window.__init__(self, title="wpgtk " + __version__) 24 | 25 | image_name = os.path.join(WPG_DIR, ".current") 26 | image_name = os.path.realpath(image_name) 27 | self.set_default_size(200, 200) 28 | self.args = args 29 | 30 | # these variables are just to get the image 31 | # and preview of current wallpaper 32 | file_name = themer.get_current() 33 | logging.info("current wallpaper: " + file_name) 34 | sample_name = files.get_sample_path(file_name) 35 | self.notebook = Gtk.Notebook() 36 | self.add(self.notebook) 37 | 38 | self.wpage = Gtk.Grid() 39 | self.wpage.set_border_width(PAD) 40 | self.wpage.set_column_homogeneous(1) 41 | self.wpage.set_row_spacing(PAD) 42 | self.wpage.set_column_spacing(PAD) 43 | 44 | self.cpage = color_grid.ColorGrid(self) 45 | self.fpage = template_grid.TemplateGrid(self) 46 | self.optpage = option_grid.OptionsGrid(self) 47 | self.keypage = keyword_grid.KeywordGrid(self) 48 | 49 | self.notebook.append_page(self.wpage, Gtk.Label("Wallpapers")) 50 | self.notebook.append_page(self.cpage, Gtk.Label("Colors")) 51 | self.notebook.append_page(self.fpage, Gtk.Label("Templates")) 52 | self.notebook.append_page(self.keypage, Gtk.Label("Keywords")) 53 | self.notebook.append_page(self.optpage, Gtk.Label("Options")) 54 | 55 | option_list = Gtk.ListStore(str) 56 | current_idx = None 57 | 58 | for i, elem in enumerate(files.get_file_list()): 59 | if elem == themer.get_current(): 60 | current_idx = i 61 | 62 | option_list.append([elem]) 63 | self.option_combo = Gtk.ComboBox.new_with_model(option_list) 64 | self.renderer_text = Gtk.CellRendererText() 65 | self.option_combo.pack_start(self.renderer_text, True) 66 | self.option_combo.add_attribute(self.renderer_text, "text", 0) 67 | self.option_combo.set_entry_text_column(0) 68 | 69 | self.textbox = Gtk.Label() 70 | self.textbox.set_text("Select colorscheme") 71 | self.colorscheme = Gtk.ComboBox.new_with_model(option_list) 72 | self.colorscheme.pack_start(self.renderer_text, True) 73 | self.colorscheme.add_attribute(self.renderer_text, "text", 0) 74 | self.colorscheme.set_entry_text_column(0) 75 | 76 | self.set_border_width(10) 77 | self.preview = Gtk.Image() 78 | self.sample = Gtk.Image() 79 | 80 | self.get_image_preview(image_name, sample_name) 81 | 82 | self.add_button = Gtk.Button(label="Add") 83 | self.set_button = Gtk.Button(label="Set") 84 | self.rm_button = Gtk.Button(label="Remove") 85 | 86 | # adds to first cell in wpage 87 | self.wpage.attach(self.option_combo, 1, 1, 2, 1) 88 | self.wpage.attach(self.colorscheme, 1, 2, 2, 1) 89 | self.wpage.attach(self.set_button, 3, 1, 1, 1) 90 | self.wpage.attach(self.add_button, 3, 2, 2, 1) 91 | self.wpage.attach(self.rm_button, 4, 1, 1, 1) 92 | self.wpage.attach(self.preview, 1, 3, 4, 1) 93 | self.wpage.attach(self.sample, 1, 4, 4, 1) 94 | self.add_button.connect("clicked", self.on_add_clicked) 95 | self.set_button.connect("clicked", self.on_set_clicked) 96 | self.rm_button.connect("clicked", self.on_rm_clicked) 97 | self.option_combo.connect("changed", self.combo_box_change) 98 | self.colorscheme.connect("changed", self.colorscheme_box_change) 99 | self.entry = Gtk.Entry() 100 | self.current_walls = Gtk.ComboBox() 101 | 102 | if current_idx is not None: 103 | self.option_combo.set_active(current_idx) 104 | self.colorscheme.set_active(current_idx) 105 | self.cpage.option_combo.set_active(current_idx) 106 | self.set_button.set_sensitive(True) 107 | 108 | def on_add_clicked(self, widget): 109 | filechooser = Gtk.FileChooserDialog( 110 | "Select an Image", 111 | self, 112 | Gtk.FileChooserAction.OPEN, 113 | ( 114 | Gtk.STOCK_CANCEL, 115 | Gtk.ResponseType.CANCEL, 116 | Gtk.STOCK_OPEN, 117 | Gtk.ResponseType.OK, 118 | ), 119 | ) 120 | 121 | filechooser.set_select_multiple(True) 122 | filefilter = Gtk.FileFilter() 123 | filefilter.set_name("Images") 124 | filefilter.add_mime_type("image/png") 125 | filefilter.add_mime_type("image/jpg") 126 | filefilter.add_mime_type("image/gif") 127 | filefilter.add_mime_type("image/jpeg") 128 | filechooser.add_filter(filefilter) 129 | response = filechooser.run() 130 | 131 | if response == Gtk.ResponseType.OK: 132 | option_list = Gtk.ListStore(str) 133 | 134 | for f in filechooser.get_filenames(): 135 | themer.create_theme(f) 136 | 137 | for elem in list(files.get_file_list()): 138 | option_list.append([elem]) 139 | 140 | self.option_combo.set_model(option_list) 141 | self.option_combo.set_entry_text_column(0) 142 | self.colorscheme.set_model(option_list) 143 | self.colorscheme.set_entry_text_column(0) 144 | 145 | self.cpage.option_combo.set_model(option_list) 146 | 147 | filechooser.destroy() 148 | 149 | def on_set_clicked(self, widget): 150 | x = self.option_combo.get_active() 151 | y = self.colorscheme.get_active() 152 | current_walls = files.get_file_list() 153 | if current_walls: 154 | filename = current_walls[x] 155 | colorscheme_file = current_walls[y] 156 | themer.set_theme(filename, colorscheme_file) 157 | 158 | def on_rm_clicked(self, widget): 159 | x = self.option_combo.get_active() 160 | current_walls = files.get_file_list() 161 | if current_walls: 162 | filename = current_walls[x] 163 | themer.delete_theme(filename) 164 | option_list = Gtk.ListStore(str) 165 | for elem in list(files.get_file_list()): 166 | option_list.append([elem]) 167 | self.option_combo.set_model(option_list) 168 | self.option_combo.set_entry_text_column(0) 169 | self.colorscheme.set_model(option_list) 170 | 171 | self.cpage.option_combo.set_model(option_list) 172 | 173 | def combo_box_change(self, widget): 174 | self.set_button.set_sensitive(True) 175 | x = self.option_combo.get_active() 176 | self.colorscheme.set_active(x) 177 | selected_file = files.get_file_list()[x] 178 | filepath = os.path.join(WALL_DIR, selected_file) 179 | 180 | self.set_image_preview(filepath) 181 | 182 | def colorscheme_box_change(self, widget): 183 | x = self.colorscheme.get_active() 184 | self.cpage.option_combo.set_active(x) 185 | 186 | # called on opening to looad the current image 187 | def get_image_preview(self, image_name, sample_name): 188 | pixbuf_preview = util.get_preview_pixbuf(image_name) 189 | pixbuf_sample = util.get_sample_pixbuf(sample_name) 190 | 191 | if pixbuf_preview is not None: 192 | self.preview.set_from_pixbuf(pixbuf_preview) 193 | 194 | if pixbuf_sample is not None: 195 | self.sample.set_from_pixbuf(pixbuf_sample) 196 | 197 | # called when combo box changes the selected image 198 | def set_image_preview(self, filepath): 199 | pixbuf_preview = util.get_preview_pixbuf(filepath) 200 | 201 | if pixbuf_preview is not None: 202 | self.preview.set_from_pixbuf(pixbuf_preview) 203 | 204 | 205 | def run(args): 206 | win = mainWindow(args) 207 | win.connect("delete-event", Gtk.main_quit) 208 | win.show_all() 209 | Gtk.main() 210 | -------------------------------------------------------------------------------- /wpgtk/gui/util.py: -------------------------------------------------------------------------------- 1 | from gi import require_version 2 | import os 3 | import pathlib 4 | 5 | require_version("GdkPixbuf", "2.0") 6 | from gi.repository import GdkPixbuf # noqa: E402 7 | 8 | 9 | def get_preview_pixbuf(image_name): 10 | """ 11 | Get a GdkPixbuf preview for an image file. 12 | 13 | This function takes an image file name as input, checks if the file exists, 14 | and creates a GdkPixbuf preview for display. If the file is a GIF, 15 | it extracts the static image from the animation, scales it to 500x333 px 16 | using the nearest-neighbor interpolation. For other image formats, it 17 | scales the image to the same dimensions while preserving the aspect ratio. 18 | 19 | Parameters: 20 | - image_name (str): The path to the image file. 21 | 22 | Returns: 23 | GdkPixbuf.Pixbuf or None: The GdkPixbuf preview if successful, or None if 24 | the file does not exist. 25 | """ 26 | if os.path.isfile(image_name): 27 | if pathlib.Path(image_name).suffix == ".gif": 28 | pixbuf = GdkPixbuf.PixbufAnimation.new_from_file(image_name) 29 | pixbuf = GdkPixbuf.PixbufAnimation.get_static_image(pixbuf) 30 | pixbuf = GdkPixbuf.Pixbuf.scale_simple( 31 | pixbuf, 500, 333, GdkPixbuf.InterpType.NEAREST 32 | ) 33 | else: 34 | pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale( 35 | image_name, width=500, height=333, preserve_aspect_ratio=False 36 | ) 37 | 38 | return pixbuf 39 | else: 40 | return None 41 | 42 | 43 | def get_sample_pixbuf(sample_name): 44 | """ 45 | Get a GdkPixbuf sample for an image file. 46 | 47 | This function takes the name of an image file as input, checks if the file 48 | exists, and creates a GdkPixbuf sample for display. The image is scaled to 49 | 500x500 pixels. 50 | 51 | Parameters: 52 | - sample_name (str): The path to the image file. 53 | 54 | Returns: 55 | GdkPixbuf.Pixbuf or None: The GdkPixbuf sample if successful, or None if 56 | the file does not exist 57 | """ 58 | if os.path.isfile(sample_name): 59 | return GdkPixbuf.Pixbuf.new_from_file_at_size( 60 | sample_name, width=500, height=500 61 | ) 62 | else: 63 | return None 64 | -------------------------------------------------------------------------------- /wpgtk/misc/.no_sample.sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deviantfero/wpgtk/fd8af752726c38a2dba71df091f7d0b002f6d7d1/wpgtk/misc/.no_sample.sample.png -------------------------------------------------------------------------------- /wpgtk/misc/.nsampler.sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deviantfero/wpgtk/fd8af752726c38a2dba71df091f7d0b002f6d7d1/wpgtk/misc/.nsampler.sample.png -------------------------------------------------------------------------------- /wpgtk/misc/wpg-install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | __ScriptVersion="0.1.6"; 4 | 5 | if [ -n "${XDG_CONFIG_HOME}" ]; then 6 | CONFIG="${XDG_CONFIG_HOME}" 7 | else 8 | CONFIG="${HOME}/.config" 9 | fi 10 | 11 | if [ -n "${XDG_DATA_HOME}" ]; then 12 | LOCAL="${XDG_DATA_HOME}" 13 | else 14 | LOCAL="${HOME}/.local/share" 15 | fi 16 | 17 | THEMES_DIR="${HOME}/.themes"; 18 | SRC_DIR="${PWD}/wpgtk-templates"; 19 | TEMPLATE_DIR="${CONFIG}/wpg/templates"; 20 | 21 | #=== FUNCTION ================================================================ 22 | # NAME: wpg-install.sh 23 | # DESCRIPTION: Installs various wpgtk themes. 24 | #=============================================================================== 25 | usage() 26 | { 27 | echo "Usage : $0 [options] [--] 28 | 29 | Options: 30 | -h Display this message 31 | -v Display script version 32 | -o Install openbox templates 33 | -t Install tint2 template 34 | -g Install gtk template 35 | -G Install linea nord gtk template 36 | -i Install icon-set 37 | -r Install rofi template 38 | -I Install i3 template 39 | -p Install polybar template 40 | -b Install bspwm template 41 | -d Install dunst template 42 | -B Install bpytop template 43 | -q Install qtile template 44 | -H Specify hash of wpgtk-templates repository to use 45 | " 46 | } 47 | 48 | checkprogram() 49 | { 50 | command -v $1 >/dev/null 2>&1; 51 | if [[ $? -eq 1 ]]; then 52 | echo "Please install $1 before proceeding"; 53 | exit 1; 54 | fi 55 | } 56 | 57 | getfiles() 58 | { 59 | checkprogram 'git'; 60 | checkprogram 'wpg'; 61 | mkdir -p "${LOCAL}/themes/color_other"; 62 | mkdir -p "${LOCAL}/icons"; 63 | git clone https://github.com/deviantfero/wpgtk-templates "$SRC_DIR"; 64 | if [[ $? -eq 0 ]]; then 65 | cd "$SRC_DIR"; 66 | [[ ! -z "$commit" ]] && git checkout $commit; 67 | return 0; 68 | else 69 | exit 1; 70 | fi 71 | } 72 | 73 | install_tint2() 74 | { 75 | echo -n "This might override your tint2 config, Continue?[Y/n]: "; 76 | read -r response; 77 | if [[ ! "$response" == "n" ]]; then 78 | echo "Installing tint2 config"; 79 | echo ":: backing up current tint2 conf in tint2rc.old.bak"; 80 | cp "${CONFIG}/tint2/tint2rc" "${CONFIG}/tint2/tint2rc.old.bak" 2>/dev/null; 81 | cp --remove-destination ./tint2/tint2rc "${CONFIG}/tint2/tint2rc" && \ 82 | cp --remove-destination ./tint2/tint2rc.base "${TEMPLATE_DIR}" && \ 83 | ln -sf "${CONFIG}/tint2/tint2rc" "${TEMPLATE_DIR}/tint2rc" && \ 84 | echo ":: tint2 template install done." 85 | return 0; 86 | fi 87 | echo ":: tint2 template not installed"; 88 | } 89 | 90 | install_rofi() 91 | { 92 | echo "Installing rofi wpg theme"; 93 | cp --remove-destination ./rofi/wpg.rasi "${CONFIG}/rofi/wpg.rasi" && \ 94 | cp --remove-destination ./rofi/wpg.rasi.base "${TEMPLATE_DIR}" && \ 95 | ln -sf "${CONFIG}/rofi/wpg.rasi" "${TEMPLATE_DIR}/wpg.rasi" && \ 96 | echo ":: rofi wpg theme install done." && \ 97 | echo ':: add @theme "wpg" to your rofi config' 98 | return 0; 99 | } 100 | 101 | install_i3() 102 | { 103 | echo -n "This might override your i3 config, Continue?[Y/n]: "; 104 | read -r response; 105 | if [[ ! "$response" == "n" ]]; then 106 | echo "Installing i3 config"; 107 | echo ":: backing up current i3 conf in config.bak"; 108 | cp "${CONFIG}/i3/config" "${CONFIG}/i3/config.bak" 2>/dev/null; 109 | cp --remove-destination ./i3/config "${CONFIG}/i3/config" && \ 110 | cp --remove-destination ./i3/i3.base "${TEMPLATE_DIR}" && \ 111 | ln -sf "${CONFIG}/i3/config" "${TEMPLATE_DIR}/i3" && \ 112 | echo ":: i3 template install done." 113 | return 0; 114 | fi 115 | echo ":: i3 template not installed"; 116 | } 117 | 118 | install_polybar() 119 | { 120 | echo -n "This might override your polybar config, Continue?[Y/n]: "; 121 | read -r response; 122 | if [[ ! "$response" == "n" ]]; then 123 | echo "Installing polybar config"; 124 | echo ":: backing up current polybar conf in config.bak"; 125 | cp "${CONFIG}/polybar/config" "${CONFIG}/polybar/config.bak" 2>/dev/null; 126 | cp --remove-destination ./polybar/config "${CONFIG}/polybar/config" && \ 127 | cp --remove-destination ./polybar/polybar.base "${TEMPLATE_DIR}" && \ 128 | ln -sf "${CONFIG}/polybar/config" "${TEMPLATE_DIR}/polybar" && \ 129 | echo ":: polybar template install done." 130 | return 0; 131 | fi 132 | echo ":: polybar template not installed"; 133 | } 134 | 135 | install_gtk() 136 | { 137 | echo "Installing gtk themes"; 138 | cp -r ./FlatColor "${LOCAL}/themes/" && \ 139 | 140 | mkdir -p "${THEMES_DIR}" && \ 141 | 142 | cp --remove-destination ./FlatColor/gtk-2.0/gtkrc.base "${TEMPLATE_DIR}/gtk2.base" && \ 143 | ln -sf "${LOCAL}/themes/FlatColor/gtk-2.0/gtkrc" "${TEMPLATE_DIR}/gtk2" && \ 144 | ln -sf "${LOCAL}/themes/FlatColor" "${THEMES_DIR}/FlatColor" && \ 145 | echo ":: gtk2 theme done" "${TEMPLATE_DIR}/gtk2"; 146 | 147 | cp --remove-destination ./FlatColor/gtk-3.0/gtk.css.base "${TEMPLATE_DIR}/gtk3.0.base" && \ 148 | ln -sf "${LOCAL}/themes/FlatColor/gtk-3.0/gtk.css" "${TEMPLATE_DIR}/gtk3.0" && \ 149 | echo ":: gtk3.0 theme done" 150 | 151 | cp --remove-destination ./FlatColor/gtk-3.20/gtk.css.base "${TEMPLATE_DIR}/gtk3.20.base" && \ 152 | ln -sf "${LOCAL}/themes/FlatColor/gtk-3.20/gtk.css" "${TEMPLATE_DIR}/gtk3.20" && \ 153 | echo ":: gtk3.20 theme done" 154 | 155 | echo ":: FlatColor gtk themes install done." 156 | } 157 | 158 | install_alternative_gtk() 159 | { 160 | echo "Installing linea nord gtk themes"; 161 | cp -r ./linea-nord-color "${LOCAL}/themes/" && \ 162 | 163 | cp --remove-destination ./linea-nord-color/gtk-2.0/gtkrc.base "${TEMPLATE_DIR}/gtk2-nord.base" && \ 164 | ln -sf "${LOCAL}/themes/linea-nord-color/gtk-2.0/gtkrc" "${TEMPLATE_DIR}/gtk2-nord" && \ 165 | echo ":: gtk2 theme done" "${TEMPLATE_DIR}/gtk2-nord"; 166 | 167 | mkdir -p "${THEMES_DIR}" && \ 168 | cp --remove-destination ./linea-nord-color/dark.css.base "${TEMPLATE_DIR}/linea-nord.css.base" && \ 169 | ln -sf "${LOCAL}/themes/linea-nord-color/general/dark.css" "${TEMPLATE_DIR}/linea-nord.css" && \ 170 | ln -sf "${LOCAL}/themes/linea-nord-color" "${THEMES_DIR}/linea-nord-color" && \ 171 | 172 | echo ":: Linea Nord Color gtk themes install done." 173 | } 174 | 175 | install_icons() 176 | { 177 | echo "Installing icon pack"; 178 | cp -r flattrcolor "${LOCAL}/icons/" && \ 179 | cp -r flattrcolor-dark "${LOCAL}/icons/" && \ 180 | echo ":: flattr icons install done." 181 | } 182 | 183 | install_openbox() 184 | { 185 | echo "Installing openbox themes"; 186 | cp --remove-destination -r ./openbox/colorbamboo/* "${LOCAL}/themes/colorbamboo/openbox-3/" 187 | 188 | mkdir -p "${THEMES_DIR}" 189 | 190 | if [[ $? -eq 0 ]]; then 191 | mv "${LOCAL}/themes/colorbamboo/openbox-3/themerc.base" "${TEMPLATE_DIR}/ob_colorbamboo.base" && \ 192 | ln -sf "${LOCAL}/themes/colorbamboo/openbox-3/themerc" "${TEMPLATE_DIR}/ob_colorbamboo" && \ 193 | ln -sf "${LOCAL}/themes/colorbamboo" "${THEMES_DIR}/colorbamboo" && \ 194 | echo ":: colorbamboo openbox themes install done."; 195 | fi 196 | } 197 | 198 | install_bspwm() 199 | { 200 | echo "Installing bspwm colors"; 201 | mv "./bspwm/bspwm_colors.base" "${TEMPLATE_DIR}/bspwm_colors.base"; 202 | mv "./bspwm/bspwm_colors" "${TEMPLATE_DIR}/bspwm_colors"; 203 | ln -sf "${CONFIG}/bspwm/bspwm_colors.sh" "${TEMPLATE_DIR}/bspwm_colors" && \ 204 | printf 'bash %s/bspwm/bspwm_colors.sh &' ${CONFIG} >> "${CONFIG}/bspwm/bspwmrc"; 205 | echo ":: bspwm colors install done."; 206 | } 207 | 208 | install_dunst() 209 | { 210 | echo "Installing dunst colors"; 211 | echo ":: backing up current dunst conf in dunstrc.bak"; 212 | cp "${CONFIG}/dunst/dunstrc" "${CONFIG}/dunst/dunstrc.bak" 2>/dev/null; 213 | 214 | mv "./dunst/dunstrc.base" "${TEMPLATE_DIR}/dunstrc.base"; 215 | mv "./dunst/dunstrc" "${TEMPLATE_DIR}/dunstrc"; 216 | ln -sf "${CONFIG}/dunst/dunstrc" "${TEMPLATE_DIR}/dunstrc" && \ 217 | echo ":: dunst colors install done."; 218 | } 219 | 220 | install_bpytop() 221 | { 222 | echo "Installing bpytop theme"; 223 | echo ":: backing up current bpytop flatcolor theme in flatcolor.theme.bak"; 224 | cp "${CONFIG}/bpytop/themes/flatcolor.theme" "${CONFIG}/bpytop/themes/flatcolor.theme.bak" 2>/dev/null; 225 | mv "./bpytop/bpytop.base" "${TEMPLATE_DIR}/bpytop.base"; 226 | mv "./bpytop/bpytop" "${TEMPLATE_DIR}/bpytop"; 227 | ln -sf "${CONFIG}/bpytop/themes/flatcolor.theme" "${TEMPLATE_DIR}/bpytop" && \ 228 | echo ":: backing up current bpytop config to bpytop.conf.bak"; 229 | sed -i.bak "s/^color_theme=.*/color_theme=+flatcolor/" ${CONFIG}/bpytop/bpytop.conf && \ 230 | echo ":: bpytop theme install done, 'flatcolor' theme applied"; 231 | } 232 | 233 | install_qtile() 234 | { 235 | echo "Installing qtile colors"; 236 | echo ":: backing up current qtile config in config.py.bak"; 237 | cp "${CONFIG}/qtile/config.py" "${CONFIG}/qtile/config.py.bak" 2>/dev/null; 238 | mv "./qtile/qtilecolors.py.base" "${TEMPLATE_DIR}/qtilecolors.py.base"; 239 | mv "./qtile/qtilecolors.py" "${TEMPLATE_DIR}/qtilecolors.py"; 240 | ln -sf "${CONFIG}/qtile/qtilecolors.py" "${TEMPLATE_DIR}/qtilecolors.py" && \ 241 | if ! grep -q qtilecolors "${CONFIG}/qtile/config.py"; then 242 | echo ":: adding imports to qtile config" 243 | sed -i -e '2ifrom qtilecolors import colors # noqa\' "${CONFIG}/qtile/config.py" 244 | else 245 | echo ":: imports are already in place, skipping..." 246 | fi 247 | echo ":: qtile theme install done" && \ 248 | echo ":: generated colors are available using colors[0-15] list in place of hex values." &&\ 249 | echo ":: remember to edit your config.py colors to use the wpg color scheme where appropriate"; 250 | } 251 | 252 | 253 | clean_up() 254 | { 255 | rm -rf "$SRC_DIR"; 256 | } 257 | 258 | 259 | #----------------------------------------------------------------------- 260 | # Handle command line arguments 261 | #----------------------------------------------------------------------- 262 | 263 | getargs() 264 | { 265 | while getopts "H:bhvotgGiIprdBq" opt 266 | do 267 | case $opt in 268 | h) 269 | usage; 270 | exit 0 271 | ;; 272 | v) 273 | echo "$0 -- Version $__ScriptVersion"; 274 | exit 0; 275 | ;; 276 | o) openbox="true" ;; 277 | i) icons="true" ;; 278 | g) gtk="true" ;; 279 | G) gtk_alt="true" ;; 280 | t) tint2="true" ;; 281 | r) rofi="true" ;; 282 | I) i3="true" ;; 283 | p) polybar="true" ;; 284 | b) bspwm="true" ;; 285 | d) dunst="true" ;; 286 | B) bpytop="true" ;; 287 | q) qtile="true" ;; 288 | H) commit="${OPTARG}" ;; 289 | *) 290 | echo -e "\n Option does not exist : $OPTARG\n" 291 | usage; 292 | exit 1 293 | ;; 294 | 295 | esac 296 | done 297 | shift "$((OPTIND - 1))" 298 | } 299 | 300 | main() 301 | { 302 | getargs "$@"; 303 | getfiles; 304 | [[ "$openbox" == "true" ]] && install_openbox; 305 | [[ "$tint2" == "true" ]] && install_tint2; 306 | [[ "$rofi" == "true" ]] && install_rofi; 307 | [[ "$gtk" == "true" ]] && install_gtk; 308 | [[ "$gtk_alt" == "true" ]] && install_alternative_gtk; 309 | [[ "$icons" == "true" ]] && install_icons; 310 | [[ "$polybar" == "true" ]] && install_polybar; 311 | [[ "$i3" == "true" ]] && install_i3; 312 | [[ "$bspwm" == "true" ]] && install_bspwm; 313 | [[ "$dunst" == "true" ]] && install_dunst; 314 | [[ "$bpytop" == "true" ]] && install_bpytop; 315 | [[ "$qtile" == "true" ]] && install_qtile; 316 | clean_up; 317 | } 318 | 319 | main "$@" 320 | 321 | -------------------------------------------------------------------------------- /wpgtk/misc/wpg.conf: -------------------------------------------------------------------------------- 1 | [settings] 2 | set_wallpaper = true 3 | gtk = true 4 | active = 0 5 | light_theme = false 6 | editor = urxvt -e vim 7 | execute_cmd = false 8 | command = urxvt -e echo hi 9 | backend = wal 10 | alpha = 100 11 | smart_sort = true 12 | auto_adjust = false 13 | reload = true 14 | terminal = true 15 | 16 | [keywords] 17 | -------------------------------------------------------------------------------- /wpgtk/misc/wpgtk.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | Version=1.0 4 | Name=wpgtk 5 | Comment=A colorscheme, wallpaper and template manager for *nix 6 | Exec=/usr/bin/wpg 7 | Icon=wpgtk 8 | Terminal=false 9 | Categories=Utility;DesktopSettings; --------------------------------------------------------------------------------