├── .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 | 
5 | 
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;
--------------------------------------------------------------------------------