├── .gitignore ├── LICENSE ├── README.md ├── flake.lock ├── flake.nix ├── gjs ├── .gitignore ├── eslint.config.mjs ├── index.ts ├── package-lock.json ├── package.json ├── src │ ├── application.ts │ ├── astalify.ts │ ├── binding.ts │ ├── file.ts │ ├── imports.ts │ ├── jsx │ │ └── jsx-runtime.ts │ ├── process.ts │ ├── time.ts │ ├── variable.ts │ └── widgets.ts └── tsconfig.json ├── lua ├── astal-dev-1.rockspec ├── astal │ ├── application.lua │ ├── binding.lua │ ├── file.lua │ ├── init.lua │ ├── process.lua │ ├── time.lua │ ├── variable.lua │ └── widget.lua ├── stylua.toml └── test.lua ├── meson.build ├── meson_options.txt ├── src ├── astal.vala ├── cli.vala ├── config.vala.in ├── file.vala ├── meson.build ├── process.vala ├── time.vala ├── variable.vala └── widget │ ├── box.vala │ ├── button.vala │ ├── centerbox.vala │ ├── circularprogress.vala │ ├── eventbox.vala │ ├── icon.vala │ ├── label.vala │ ├── levelbar.vala │ ├── overlay.vala │ ├── scrollable.vala │ ├── slider.vala │ ├── widget.vala │ └── window.vala └── version /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | result 3 | .cache/ 4 | test.sh 5 | tmp/ 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 2.1, February 1999 3 | 4 | Copyright (C) 1991, 1999 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 | [This is the first released version of the Lesser GPL. It also counts 10 | as the successor of the GNU Library Public License, version 2, hence 11 | the version number 2.1.] 12 | 13 | Preamble 14 | 15 | The licenses for most software are designed to take away your 16 | freedom to share and change it. By contrast, the GNU General Public 17 | Licenses are intended to guarantee your freedom to share and change 18 | free software--to make sure the software is free for all its users. 19 | 20 | This license, the Lesser General Public License, applies to some 21 | specially designated software packages--typically libraries--of the 22 | Free Software Foundation and other authors who decide to use it. You 23 | can use it too, but we suggest you first think carefully about whether 24 | this license or the ordinary General Public License is the better 25 | strategy to use in any particular case, based on the explanations below. 26 | 27 | When we speak of free software, we are referring to freedom of use, 28 | not price. Our General Public Licenses are designed to make sure that 29 | you have the freedom to distribute copies of free software (and charge 30 | for this service if you wish); that you receive source code or can get 31 | it if you want it; that you can change the software and use pieces of 32 | it in new free programs; and that you are informed that you can do 33 | these things. 34 | 35 | To protect your rights, we need to make restrictions that forbid 36 | distributors to deny you these rights or to ask you to surrender these 37 | rights. These restrictions translate to certain responsibilities for 38 | you if you distribute copies of the library or if you modify it. 39 | 40 | For example, if you distribute copies of the library, whether gratis 41 | or for a fee, you must give the recipients all the rights that we gave 42 | you. You must make sure that they, too, receive or can get the source 43 | code. If you link other code with the library, you must provide 44 | complete object files to the recipients, so that they can relink them 45 | with the library after making changes to the library and recompiling 46 | it. And you must show them these terms so they know their rights. 47 | 48 | We protect your rights with a two-step method: (1) we copyright the 49 | library, and (2) we offer you this license, which gives you legal 50 | permission to copy, distribute and/or modify the library. 51 | 52 | To protect each distributor, we want to make it very clear that 53 | there is no warranty for the free library. Also, if the library is 54 | modified by someone else and passed on, the recipients should know 55 | that what they have is not the original version, so that the original 56 | author's reputation will not be affected by problems that might be 57 | introduced by others. 58 | 59 | Finally, software patents pose a constant threat to the existence of 60 | any free program. We wish to make sure that a company cannot 61 | effectively restrict the users of a free program by obtaining a 62 | restrictive license from a patent holder. Therefore, we insist that 63 | any patent license obtained for a version of the library must be 64 | consistent with the full freedom of use specified in this license. 65 | 66 | Most GNU software, including some libraries, is covered by the 67 | ordinary GNU General Public License. This license, the GNU Lesser 68 | General Public License, applies to certain designated libraries, and 69 | is quite different from the ordinary General Public License. We use 70 | this license for certain libraries in order to permit linking those 71 | libraries into non-free programs. 72 | 73 | When a program is linked with a library, whether statically or using 74 | a shared library, the combination of the two is legally speaking a 75 | combined work, a derivative of the original library. The ordinary 76 | General Public License therefore permits such linking only if the 77 | entire combination fits its criteria of freedom. The Lesser General 78 | Public License permits more lax criteria for linking other code with 79 | the library. 80 | 81 | We call this license the "Lesser" General Public License because it 82 | does Less to protect the user's freedom than the ordinary General 83 | Public License. It also provides other free software developers Less 84 | of an advantage over competing non-free programs. These disadvantages 85 | are the reason we use the ordinary General Public License for many 86 | libraries. However, the Lesser license provides advantages in certain 87 | special circumstances. 88 | 89 | For example, on rare occasions, there may be a special need to 90 | encourage the widest possible use of a certain library, so that it becomes 91 | a de-facto standard. To achieve this, non-free programs must be 92 | allowed to use the library. A more frequent case is that a free 93 | library does the same job as widely used non-free libraries. In this 94 | case, there is little to gain by limiting the free library to free 95 | software only, so we use the Lesser General Public License. 96 | 97 | In other cases, permission to use a particular library in non-free 98 | programs enables a greater number of people to use a large body of 99 | free software. For example, permission to use the GNU C Library in 100 | non-free programs enables many more people to use the whole GNU 101 | operating system, as well as its variant, the GNU/Linux operating 102 | system. 103 | 104 | Although the Lesser General Public License is Less protective of the 105 | users' freedom, it does ensure that the user of a program that is 106 | linked with the Library has the freedom and the wherewithal to run 107 | that program using a modified version of the Library. 108 | 109 | The precise terms and conditions for copying, distribution and 110 | modification follow. Pay close attention to the difference between a 111 | "work based on the library" and a "work that uses the library". The 112 | former contains code derived from the library, whereas the latter must 113 | be combined with the library in order to run. 114 | 115 | GNU LESSER GENERAL PUBLIC LICENSE 116 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 117 | 118 | 0. This License Agreement applies to any software library or other 119 | program which contains a notice placed by the copyright holder or 120 | other authorized party saying it may be distributed under the terms of 121 | this Lesser General Public License (also called "this License"). 122 | Each licensee is addressed as "you". 123 | 124 | A "library" means a collection of software functions and/or data 125 | prepared so as to be conveniently linked with application programs 126 | (which use some of those functions and data) to form executables. 127 | 128 | The "Library", below, refers to any such software library or work 129 | which has been distributed under these terms. A "work based on the 130 | Library" means either the Library or any derivative work under 131 | copyright law: that is to say, a work containing the Library or a 132 | portion of it, either verbatim or with modifications and/or translated 133 | straightforwardly into another language. (Hereinafter, translation is 134 | included without limitation in the term "modification".) 135 | 136 | "Source code" for a work means the preferred form of the work for 137 | making modifications to it. For a library, complete source code means 138 | all the source code for all modules it contains, plus any associated 139 | interface definition files, plus the scripts used to control compilation 140 | and installation of the library. 141 | 142 | Activities other than copying, distribution and modification are not 143 | covered by this License; they are outside its scope. The act of 144 | running a program using the Library is not restricted, and output from 145 | such a program is covered only if its contents constitute a work based 146 | on the Library (independent of the use of the Library in a tool for 147 | writing it). Whether that is true depends on what the Library does 148 | and what the program that uses the Library does. 149 | 150 | 1. You may copy and distribute verbatim copies of the Library's 151 | complete source code as you receive it, in any medium, provided that 152 | you conspicuously and appropriately publish on each copy an 153 | appropriate copyright notice and disclaimer of warranty; keep intact 154 | all the notices that refer to this License and to the absence of any 155 | warranty; and distribute a copy of this License along with the 156 | Library. 157 | 158 | You may charge a fee for the physical act of transferring a copy, 159 | and you may at your option offer warranty protection in exchange for a 160 | fee. 161 | 162 | 2. You may modify your copy or copies of the Library or any portion 163 | of it, thus forming a work based on the Library, and copy and 164 | distribute such modifications or work under the terms of Section 1 165 | above, provided that you also meet all of these conditions: 166 | 167 | a) The modified work must itself be a software library. 168 | 169 | b) You must cause the files modified to carry prominent notices 170 | stating that you changed the files and the date of any change. 171 | 172 | c) You must cause the whole of the work to be licensed at no 173 | charge to all third parties under the terms of this License. 174 | 175 | d) If a facility in the modified Library refers to a function or a 176 | table of data to be supplied by an application program that uses 177 | the facility, other than as an argument passed when the facility 178 | is invoked, then you must make a good faith effort to ensure that, 179 | in the event an application does not supply such function or 180 | table, the facility still operates, and performs whatever part of 181 | its purpose remains meaningful. 182 | 183 | (For example, a function in a library to compute square roots has 184 | a purpose that is entirely well-defined independent of the 185 | application. Therefore, Subsection 2d requires that any 186 | application-supplied function or table used by this function must 187 | be optional: if the application does not supply it, the square 188 | root function must still compute square roots.) 189 | 190 | These requirements apply to the modified work as a whole. If 191 | identifiable sections of that work are not derived from the Library, 192 | and can be reasonably considered independent and separate works in 193 | themselves, then this License, and its terms, do not apply to those 194 | sections when you distribute them as separate works. But when you 195 | distribute the same sections as part of a whole which is a work based 196 | on the Library, the distribution of the whole must be on the terms of 197 | this License, whose permissions for other licensees extend to the 198 | entire whole, and thus to each and every part regardless of who wrote 199 | it. 200 | 201 | Thus, it is not the intent of this section to claim rights or contest 202 | your rights to work written entirely by you; rather, the intent is to 203 | exercise the right to control the distribution of derivative or 204 | collective works based on the Library. 205 | 206 | In addition, mere aggregation of another work not based on the Library 207 | with the Library (or with a work based on the Library) on a volume of 208 | a storage or distribution medium does not bring the other work under 209 | the scope of this License. 210 | 211 | 3. You may opt to apply the terms of the ordinary GNU General Public 212 | License instead of this License to a given copy of the Library. To do 213 | this, you must alter all the notices that refer to this License, so 214 | that they refer to the ordinary GNU General Public License, version 2, 215 | instead of to this License. (If a newer version than version 2 of the 216 | ordinary GNU General Public License has appeared, then you can specify 217 | that version instead if you wish.) Do not make any other change in 218 | these notices. 219 | 220 | Once this change is made in a given copy, it is irreversible for 221 | that copy, so the ordinary GNU General Public License applies to all 222 | subsequent copies and derivative works made from that copy. 223 | 224 | This option is useful when you wish to copy part of the code of 225 | the Library into a program that is not a library. 226 | 227 | 4. You may copy and distribute the Library (or a portion or 228 | derivative of it, under Section 2) in object code or executable form 229 | under the terms of Sections 1 and 2 above provided that you accompany 230 | it with the complete corresponding machine-readable source code, which 231 | must be distributed under the terms of Sections 1 and 2 above on a 232 | medium customarily used for software interchange. 233 | 234 | If distribution of object code is made by offering access to copy 235 | from a designated place, then offering equivalent access to copy the 236 | source code from the same place satisfies the requirement to 237 | distribute the source code, even though third parties are not 238 | compelled to copy the source along with the object code. 239 | 240 | 5. A program that contains no derivative of any portion of the 241 | Library, but is designed to work with the Library by being compiled or 242 | linked with it, is called a "work that uses the Library". Such a 243 | work, in isolation, is not a derivative work of the Library, and 244 | therefore falls outside the scope of this License. 245 | 246 | However, linking a "work that uses the Library" with the Library 247 | creates an executable that is a derivative of the Library (because it 248 | contains portions of the Library), rather than a "work that uses the 249 | library". The executable is therefore covered by this License. 250 | Section 6 states terms for distribution of such executables. 251 | 252 | When a "work that uses the Library" uses material from a header file 253 | that is part of the Library, the object code for the work may be a 254 | derivative work of the Library even though the source code is not. 255 | Whether this is true is especially significant if the work can be 256 | linked without the Library, or if the work is itself a library. The 257 | threshold for this to be true is not precisely defined by law. 258 | 259 | If such an object file uses only numerical parameters, data 260 | structure layouts and accessors, and small macros and small inline 261 | functions (ten lines or less in length), then the use of the object 262 | file is unrestricted, regardless of whether it is legally a derivative 263 | work. (Executables containing this object code plus portions of the 264 | Library will still fall under Section 6.) 265 | 266 | Otherwise, if the work is a derivative of the Library, you may 267 | distribute the object code for the work under the terms of Section 6. 268 | Any executables containing that work also fall under Section 6, 269 | whether or not they are linked directly with the Library itself. 270 | 271 | 6. As an exception to the Sections above, you may also combine or 272 | link a "work that uses the Library" with the Library to produce a 273 | work containing portions of the Library, and distribute that work 274 | under terms of your choice, provided that the terms permit 275 | modification of the work for the customer's own use and reverse 276 | engineering for debugging such modifications. 277 | 278 | You must give prominent notice with each copy of the work that the 279 | Library is used in it and that the Library and its use are covered by 280 | this License. You must supply a copy of this License. If the work 281 | during execution displays copyright notices, you must include the 282 | copyright notice for the Library among them, as well as a reference 283 | directing the user to the copy of this License. Also, you must do one 284 | of these things: 285 | 286 | a) Accompany the work with the complete corresponding 287 | machine-readable source code for the Library including whatever 288 | changes were used in the work (which must be distributed under 289 | Sections 1 and 2 above); and, if the work is an executable linked 290 | with the Library, with the complete machine-readable "work that 291 | uses the Library", as object code and/or source code, so that the 292 | user can modify the Library and then relink to produce a modified 293 | executable containing the modified Library. (It is understood 294 | that the user who changes the contents of definitions files in the 295 | Library will not necessarily be able to recompile the application 296 | to use the modified definitions.) 297 | 298 | b) Use a suitable shared library mechanism for linking with the 299 | Library. A suitable mechanism is one that (1) uses at run time a 300 | copy of the library already present on the user's computer system, 301 | rather than copying library functions into the executable, and (2) 302 | will operate properly with a modified version of the library, if 303 | the user installs one, as long as the modified version is 304 | interface-compatible with the version that the work was made with. 305 | 306 | c) Accompany the work with a written offer, valid for at 307 | least three years, to give the same user the materials 308 | specified in Subsection 6a, above, for a charge no more 309 | than the cost of performing this distribution. 310 | 311 | d) If distribution of the work is made by offering access to copy 312 | from a designated place, offer equivalent access to copy the above 313 | specified materials from the same place. 314 | 315 | e) Verify that the user has already received a copy of these 316 | materials or that you have already sent this user a copy. 317 | 318 | For an executable, the required form of the "work that uses the 319 | Library" must include any data and utility programs needed for 320 | reproducing the executable from it. However, as a special exception, 321 | the materials to be distributed need not include anything that is 322 | normally distributed (in either source or binary form) with the major 323 | components (compiler, kernel, and so on) of the operating system on 324 | which the executable runs, unless that component itself accompanies 325 | the executable. 326 | 327 | It may happen that this requirement contradicts the license 328 | restrictions of other proprietary libraries that do not normally 329 | accompany the operating system. Such a contradiction means you cannot 330 | use both them and the Library together in an executable that you 331 | distribute. 332 | 333 | 7. You may place library facilities that are a work based on the 334 | Library side-by-side in a single library together with other library 335 | facilities not covered by this License, and distribute such a combined 336 | library, provided that the separate distribution of the work based on 337 | the Library and of the other library facilities is otherwise 338 | permitted, and provided that you do these two things: 339 | 340 | a) Accompany the combined library with a copy of the same work 341 | based on the Library, uncombined with any other library 342 | facilities. This must be distributed under the terms of the 343 | Sections above. 344 | 345 | b) Give prominent notice with the combined library of the fact 346 | that part of it is a work based on the Library, and explaining 347 | where to find the accompanying uncombined form of the same work. 348 | 349 | 8. You may not copy, modify, sublicense, link with, or distribute 350 | the Library except as expressly provided under this License. Any 351 | attempt otherwise to copy, modify, sublicense, link with, or 352 | distribute the Library is void, and will automatically terminate your 353 | rights under this License. However, parties who have received copies, 354 | or rights, from you under this License will not have their licenses 355 | terminated so long as such parties remain in full compliance. 356 | 357 | 9. You are not required to accept this License, since you have not 358 | signed it. However, nothing else grants you permission to modify or 359 | distribute the Library or its derivative works. These actions are 360 | prohibited by law if you do not accept this License. Therefore, by 361 | modifying or distributing the Library (or any work based on the 362 | Library), you indicate your acceptance of this License to do so, and 363 | all its terms and conditions for copying, distributing or modifying 364 | the Library or works based on it. 365 | 366 | 10. Each time you redistribute the Library (or any work based on the 367 | Library), the recipient automatically receives a license from the 368 | original licensor to copy, distribute, link with or modify the Library 369 | subject to these terms and conditions. You may not impose any further 370 | restrictions on the recipients' exercise of the rights granted herein. 371 | You are not responsible for enforcing compliance by third parties with 372 | this License. 373 | 374 | 11. If, as a consequence of a court judgment or allegation of patent 375 | infringement or for any other reason (not limited to patent issues), 376 | conditions are imposed on you (whether by court order, agreement or 377 | otherwise) that contradict the conditions of this License, they do not 378 | excuse you from the conditions of this License. If you cannot 379 | distribute so as to satisfy simultaneously your obligations under this 380 | License and any other pertinent obligations, then as a consequence you 381 | may not distribute the Library at all. For example, if a patent 382 | license would not permit royalty-free redistribution of the Library by 383 | all those who receive copies directly or indirectly through you, then 384 | the only way you could satisfy both it and this License would be to 385 | refrain entirely from distribution of the Library. 386 | 387 | If any portion of this section is held invalid or unenforceable under any 388 | particular circumstance, the balance of the section is intended to apply, 389 | and the section as a whole is intended to apply in other circumstances. 390 | 391 | It is not the purpose of this section to induce you to infringe any 392 | patents or other property right claims or to contest validity of any 393 | such claims; this section has the sole purpose of protecting the 394 | integrity of the free software distribution system which is 395 | implemented by public license practices. Many people have made 396 | generous contributions to the wide range of software distributed 397 | through that system in reliance on consistent application of that 398 | system; it is up to the author/donor to decide if he or she is willing 399 | to distribute software through any other system and a licensee cannot 400 | impose that choice. 401 | 402 | This section is intended to make thoroughly clear what is believed to 403 | be a consequence of the rest of this License. 404 | 405 | 12. If the distribution and/or use of the Library is restricted in 406 | certain countries either by patents or by copyrighted interfaces, the 407 | original copyright holder who places the Library under this License may add 408 | an explicit geographical distribution limitation excluding those countries, 409 | so that distribution is permitted only in or among countries not thus 410 | excluded. In such case, this License incorporates the limitation as if 411 | written in the body of this License. 412 | 413 | 13. The Free Software Foundation may publish revised and/or new 414 | versions of the Lesser General Public License from time to time. 415 | Such new versions will be similar in spirit to the present version, 416 | but may differ in detail to address new problems or concerns. 417 | 418 | Each version is given a distinguishing version number. If the Library 419 | specifies a version number of this License which applies to it and 420 | "any later version", you have the option of following the terms and 421 | conditions either of that version or of any later version published by 422 | the Free Software Foundation. If the Library does not specify a 423 | license version number, you may choose any version ever published by 424 | the Free Software Foundation. 425 | 426 | 14. If you wish to incorporate parts of the Library into other free 427 | programs whose distribution conditions are incompatible with these, 428 | write to the author to ask for permission. For software which is 429 | copyrighted by the Free Software Foundation, write to the Free 430 | Software Foundation; we sometimes make exceptions for this. Our 431 | decision will be guided by the two goals of preserving the free status 432 | of all derivatives of our free software and of promoting the sharing 433 | and reuse of software generally. 434 | 435 | NO WARRANTY 436 | 437 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO 438 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 439 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR 440 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY 441 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE 442 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 443 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 444 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME 445 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 446 | 447 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN 448 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY 449 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU 450 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR 451 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 452 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING 453 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 454 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF 455 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 456 | DAMAGES. 457 | 458 | END OF TERMS AND CONDITIONS 459 | 460 | How to Apply These Terms to Your New Libraries 461 | 462 | If you develop a new library, and you want it to be of the greatest 463 | possible use to the public, we recommend making it free software that 464 | everyone can redistribute and change. You can do so by permitting 465 | redistribution under these terms (or, alternatively, under the terms of the 466 | ordinary General Public License). 467 | 468 | To apply these terms, attach the following notices to the library. It is 469 | safest to attach them to the start of each source file to most effectively 470 | convey the exclusion of warranty; and each file should have at least the 471 | "copyright" line and a pointer to where the full notice is found. 472 | 473 | 474 | Copyright (C) 475 | 476 | This library is free software; you can redistribute it and/or 477 | modify it under the terms of the GNU Lesser General Public 478 | License as published by the Free Software Foundation; either 479 | version 2.1 of the License, or (at your option) any later version. 480 | 481 | This library is distributed in the hope that it will be useful, 482 | but WITHOUT ANY WARRANTY; without even the implied warranty of 483 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 484 | Lesser General Public License for more details. 485 | 486 | You should have received a copy of the GNU Lesser General Public 487 | License along with this library; if not, write to the Free Software 488 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 489 | 490 | Also add information on how to contact you by electronic and paper mail. 491 | 492 | You should also get your employer (if you work as a programmer) or your 493 | school, if any, to sign a "copyright disclaimer" for the library, if 494 | necessary. Here is a sample; alter the names: 495 | 496 | Yoyodyne, Inc., hereby disclaims all copyright interest in the 497 | library `Frob' (a library for tweaking knobs) written by James Random Hacker. 498 | 499 | , 1 April 1990 500 | Ty Coon, President of Vice 501 | 502 | That's all there is to it! 503 | 504 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # libastal 2 | 3 | > [!WARNING] 4 | > WIP: everything is subject to change 5 | 6 | The main goal of this project is to further abstract gtk bindings in higher level 7 | languages with custom state management mechanisms, namely in javascript (gjs, node), 8 | lua (lua-lgi). 9 | 10 | `libastal`, which is the library written in Vala, 11 | comes with a few widgets built on top of gtk3 and 12 | tools to execute external binaries and store their output. 13 | It also comes with a builtin cli client to send messages to the running 14 | processes through a socket. 15 | 16 | ## Developing 17 | 18 | first install libastal or enter nix shell 19 | 20 | ```bash 21 | # non nix 22 | meson setup build 23 | meson insall -C build 24 | ``` 25 | 26 | ```bash 27 | # nix 28 | nix develop .#astal 29 | ``` 30 | 31 | lua should be stright forward, just run the interpreter 32 | 33 | for javascript do 34 | 35 | ```bash 36 | cd gjs 37 | npm i 38 | npm run types 39 | npm run build 40 | ``` 41 | 42 | ## Gtk abstractions 43 | 44 | `Variable` and `Binding` objects and a function that turns widget constructors 45 | into ones that can take `Binding` objects as parameters are added on top 46 | of gtk bindings. This mechanism takes care of all state management one would need. 47 | 48 | This works the same in lua too, but demonstrated in js 49 | 50 | ```javascript 51 | // this example will work with Variable 52 | // but it can take any type of value 53 | const v = Variable("value") 54 | .poll(1000, "some-executable on $PATH") 55 | .poll(1000, ["some-executable", "with", "args"]) 56 | .poll(1000, () => "some-function") 57 | .watch("some-executable") 58 | .watch(["some-executable", "with", "args"]) 59 | .observe(someGObject, "signal", (...args) => "some output") 60 | .observe([[gobj1, "signal"], [gobj2, "signal"]], (...args) => "some output") 61 | .onError(console.error) // when the script fails 62 | .onDropped(() => "clean-up") // cleanup resources if needed on drop() or GC 63 | 64 | Button({ 65 | label: bind(v), 66 | label: bind(v).as(v => "transformed"), 67 | label: v(t => "transformed"), // shorthand for the above 68 | 69 | // in ags we have Service.bind("prop") 70 | // here we will do this, since gobject implementations 71 | // will come from Vala code and not js 72 | label: bind(anyGObject, "one-of-its-prop").as(prop => "transformed"), 73 | 74 | // event handlers 75 | on_signalname(self, ...args) { print(self, args) }, 76 | 77 | // setup prop is still here, but should be rarely needed 78 | setup(self) { 79 | self.hook(v, (self) => print(self)) 80 | self.hook(gobject, "signal", (self) => print(self)) 81 | } 82 | }) 83 | 84 | // some additional Variable and Binding methods 85 | v.stop_poll() 86 | v.start_poll() 87 | 88 | v.stop_watch() 89 | v.start_watch() 90 | 91 | v.get() 92 | v.set("new-value") 93 | 94 | const unsub = v.subscribe(value => console.log(value)) 95 | unsub() // to unsubscribe 96 | 97 | const b = bind(v) 98 | b.get() 99 | // note that its value cannot be set through a Binding 100 | // if you want to, you are doing something wrong 101 | 102 | // same subscribe mechanism 103 | const unsub = b.subscribe(value => console.log(value)) 104 | unsub() 105 | 106 | const derived = Variable.derive([v, b], (vval, bval) => { 107 | return "can take a list of Variable | Binding" 108 | }) 109 | 110 | v.drop() // dispose when no longer needed 111 | 112 | // handle cli client 113 | App.start({ 114 | instanceName: "my-instance", 115 | responseHandler(msg, response) { 116 | console.log("message from cli", msg) 117 | response("hi") 118 | } 119 | }) 120 | ``` 121 | 122 | after `App.start` is called, it will open a socket, which can be used 123 | with the cli client that comes with libastal 124 | 125 | ```bash 126 | astal --instance-name my-instance "message was sent from cli" 127 | ``` 128 | 129 | ## Lower level languages 130 | 131 | As said before, the main goal is to make js/lua DX better, but libastal 132 | can be used in **any** language that has bindings for glib/gtk. 133 | `Binding` is not implemented in Vala, but in each language, because 134 | they are language specific, and it doesn't make much sense for lower 135 | level languages as they usually don't have a way to declaratively build 136 | layouts. Subclassed widgets and `Variable` can still be used, but they will 137 | need to be hooked **imperatively**. For languages like rust/go/c++ 138 | you will mostly benefit from the other libraries (called `Service` in ags). 139 | I can also recommend using [blueprint](https://jwestman.pages.gitlab.gnome.org/blueprint-compiler/) 140 | which lets you define layouts declaratively and hook functionality in your 141 | preferred language. 142 | 143 | I am open to add support for any other language if it makes sense, 144 | but if using blueprint makes more sense, I would rather maintain 145 | templates and examples instead to get started with development. 146 | 147 | ## Goals 148 | 149 | - libastal 150 | - Variables 151 | - [x] poll (interval, string) 152 | - [x] pollv (interval, string[]) 153 | - [x] pollfn (interval, closure) 154 | - [x] watch (string) 155 | - [x] watchv (string[]) 156 | - ~~[ ] observe (object, signal, closure)~~ 157 | - Time 158 | - [x] interval 159 | - [x] timeout 160 | - [x] idle 161 | - [x] now signal 162 | - Process 163 | - [x] exec: string, error as Error 164 | - [x] execAsync: proc, stdout, stderr signal 165 | - [x] subprocess: proc, stdout, stderr signal 166 | - app instance with a socket: Application 167 | - [x] gtk settings as props 168 | - [x] window getters 169 | - [x] include cli client 170 | - few additional widgets 171 | - [x] window widget with gtk-layer-shell 172 | - [x] box with children prop 173 | - [x] button with abstract signals for button-event 174 | - [ ] ?custom calendar like gtk4 175 | - [x] centerbox 176 | - [ ] circularprogress 177 | - [x] eventbox 178 | - [x] icon 179 | - [x] overlay 180 | - [ ] scrollable/viewport 181 | - [x] slider 182 | - [ ] stack, shown, children setter 183 | - widgets with no additional behaviour only for the sake of it 184 | - [ ] ?drawingarea 185 | - [ ] ?entry 186 | - [ ] ?fixed 187 | - [ ] ?flowbox 188 | - [ ] ?label 189 | - [ ] ?levelbar 190 | - [ ] ?revealer 191 | - [ ] ?switch 192 | - widget prop setters 193 | - [x] css 194 | - [x] class-names 195 | - [x] cursor 196 | - [x] click-through 197 | 198 | - language bindings 199 | - Binding for Variable and any GObject `bind(gobject, property).as(transform)` 200 | - .hook() for widgets 201 | - setup prop for widgets 202 | - constructor overrides to take bindings 203 | - override default `visible` for widgets to true 204 | - wrap Variable in native object to make sure no GValue crashes 205 | - Variable.observe for signals 206 | - Variable.derive that takes either Variables or Bindings 207 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1716293225, 6 | "narHash": "sha256-pU9ViBVE3XYb70xZx+jK6SEVphvt7xMTbm6yDIF4xPs=", 7 | "owner": "nixos", 8 | "repo": "nixpkgs", 9 | "rev": "3eaeaeb6b1e08a016380c279f8846e0bd8808916", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "nixos", 14 | "ref": "nixos-unstable", 15 | "repo": "nixpkgs", 16 | "type": "github" 17 | } 18 | }, 19 | "root": { 20 | "inputs": { 21 | "nixpkgs": "nixpkgs" 22 | } 23 | } 24 | }, 25 | "root": "root", 26 | "version": 7 27 | } 28 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; 3 | 4 | outputs = { 5 | self, 6 | nixpkgs, 7 | }: let 8 | version = builtins.replaceStrings ["\n"] [""] (builtins.readFile ./version); 9 | system = "x86_64-linux"; 10 | pkgs = import nixpkgs {inherit system;}; 11 | 12 | nativeBuildInputs = with pkgs; [ 13 | wrapGAppsHook 14 | gobject-introspection 15 | meson 16 | pkg-config 17 | ninja 18 | vala 19 | ]; 20 | 21 | buildInputs = with pkgs; [ 22 | glib 23 | gtk3 24 | gtk-layer-shell 25 | ]; 26 | in { 27 | packages.${system} = rec { 28 | default = astal; 29 | astal = pkgs.stdenv.mkDerivation { 30 | inherit nativeBuildInputs buildInputs; 31 | pname = "astal"; 32 | version = version; 33 | src = ./.; 34 | outputs = ["out" "dev"]; 35 | }; 36 | }; 37 | 38 | devShells.${system} = let 39 | inputs = with pkgs; 40 | buildInputs 41 | ++ [ 42 | (lua.withPackages (ps: [ps.lgi])) 43 | gjs 44 | ]; 45 | in { 46 | default = pkgs.mkShell { 47 | inherit nativeBuildInputs; 48 | buildInputs = inputs; 49 | }; 50 | astal = pkgs.mkShell { 51 | inherit nativeBuildInputs; 52 | buildInputs = 53 | inputs 54 | ++ [ 55 | self.packages.${system}.astal 56 | ]; 57 | }; 58 | }; 59 | }; 60 | } 61 | -------------------------------------------------------------------------------- /gjs/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | result/ 3 | dist/ 4 | -------------------------------------------------------------------------------- /gjs/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import eslint from "@eslint/js" 2 | import tseslint from "typescript-eslint" 3 | import stylistic from "@stylistic/eslint-plugin" 4 | 5 | export default tseslint.config({ 6 | extends: [ 7 | eslint.configs.recommended, 8 | ...tseslint.configs.recommended, 9 | stylistic.configs.customize({ 10 | semi: false, 11 | indent: 4, 12 | quotes: "double", 13 | }), 14 | ], 15 | rules: { 16 | "@typescript-eslint/no-explicit-any": "off", 17 | }, 18 | }) 19 | -------------------------------------------------------------------------------- /gjs/index.ts: -------------------------------------------------------------------------------- 1 | import { Gtk } from "./src/imports.js" 2 | 3 | export * from "./src/imports.js" 4 | export * from "./src/process.js" 5 | export * from "./src/time.js" 6 | export * from "./src/file.js" 7 | export { bind, default as Binding } from "./src/binding.js" 8 | export { Variable } from "./src/variable.js" 9 | export * as Widget from "./src/widgets.js" 10 | export { default as App } from "./src/application.js" 11 | 12 | // gjs crashes if a widget is constructed before Gtk.init 13 | Gtk.init(null) 14 | -------------------------------------------------------------------------------- /gjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "astal", 3 | "version": "0.1.0", 4 | "description": "Building blocks for buildin linux desktop shell", 5 | "type": "module", 6 | "author": "Aylur", 7 | "license": "GPL", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/astal-sh/libastal.git", 11 | "directory": "gjs" 12 | }, 13 | "funding": { 14 | "type": "kofi", 15 | "url": "https://ko-fi.com/aylur" 16 | }, 17 | "exports": { 18 | ".": "./index.ts", 19 | "./app": "./src/application.ts", 20 | "./file": "./src/file.ts", 21 | "./process": "./src/process.ts", 22 | "./time": "./src/time.ts", 23 | "./variable": "./src/variable.ts", 24 | "./widgets": "./src/widgets.ts" 25 | }, 26 | "engines": { 27 | "gjs": ">=1.79.0" 28 | }, 29 | "os": [ 30 | "linux" 31 | ], 32 | "publishConfig": {}, 33 | "devDependencies": { 34 | "@eslint/js": "^9.7.0", 35 | "@stylistic/eslint-plugin": "latest", 36 | "@ts-for-gir/cli": "latest", 37 | "@types/eslint__js": "^8.42.3", 38 | "eslint": "^8.57.0", 39 | "typescript": "^5.5.3", 40 | "typescript-eslint": "^7.16.1" 41 | }, 42 | "scripts": { 43 | "lint": "eslint . --fix", 44 | "types": "ts-for-gir generate -o node_modules/@girs --package" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /gjs/src/application.ts: -------------------------------------------------------------------------------- 1 | import { Astal, GObject, Gio, GLib } from "./imports.js" 2 | 3 | type RequestHandler = { 4 | (request: string, res: (response: any) => void): void 5 | } 6 | 7 | type Config = Partial<{ 8 | icons: string 9 | instanceName: string 10 | gtkTheme: string 11 | iconTheme: string 12 | cursorTheme: string 13 | css: string 14 | requestHandler: RequestHandler 15 | main(...args: string[]): void 16 | client(message: (msg: string) => string, ...args: string[]): void 17 | hold: boolean 18 | }> 19 | 20 | // @ts-expect-error missing types 21 | // https://github.com/gjsify/ts-for-gir/issues/164 22 | import { setConsoleLogDomain } from "console" 23 | import { exit, programArgs } from "system" 24 | 25 | class AstalJS extends Astal.Application { 26 | static { GObject.registerClass(this) } 27 | 28 | eval(body: string): Promise { 29 | return new Promise((res, rej) => { 30 | try { 31 | const fn = Function(`return (async function() { 32 | ${body.includes(";") ? body : `return ${body};`} 33 | })`) 34 | fn()() 35 | .then(res) 36 | .catch(rej) 37 | } 38 | catch (error) { 39 | rej(error) 40 | } 41 | }) 42 | } 43 | 44 | requestHandler?: RequestHandler 45 | 46 | vfunc_request(msg: string, conn: Gio.SocketConnection): void { 47 | if (typeof this.requestHandler === "function") { 48 | this.requestHandler(msg, (response) => { 49 | Astal.write_sock(conn, String(response), (_, res) => 50 | Astal.write_sock_finish(res), 51 | ) 52 | }) 53 | } 54 | else { 55 | super.vfunc_request(msg, conn) 56 | } 57 | } 58 | 59 | apply_css(style: string, reset = false) { 60 | super.apply_css(style, reset) 61 | } 62 | 63 | quit(code?: number): void { 64 | super.quit() 65 | exit(code ?? 0) 66 | } 67 | 68 | start({ requestHandler, css, hold, main, client, icons, ...cfg }: Config = {}) { 69 | client ??= () => { 70 | print(`Astal instance "${this.instanceName}" already running`) 71 | exit(1) 72 | } 73 | 74 | Object.assign(this, cfg) 75 | setConsoleLogDomain(this.instanceName) 76 | 77 | this.requestHandler = requestHandler 78 | this.connect("activate", () => { 79 | const path: string[] = import.meta.url.split("/").slice(3) 80 | const file = path.at(-1)!.replace(".js", ".css") 81 | const css = `/${path.slice(0, -1).join("/")}/${file}` 82 | if (file.endsWith(".css") && GLib.file_test(css, GLib.FileTest.EXISTS)) 83 | this.apply_css(css, false) 84 | 85 | main?.(...programArgs) 86 | }) 87 | 88 | if (!this.acquire_socket()) 89 | return client(msg => Astal.Application.send_message(this.instanceName, msg)!, ...programArgs) 90 | 91 | if (css) 92 | this.apply_css(css, false) 93 | 94 | if (icons) 95 | this.add_icons(icons) 96 | 97 | hold ??= true 98 | if (hold) 99 | this.hold() 100 | 101 | this.runAsync([]) 102 | } 103 | } 104 | 105 | export default new AstalJS() 106 | -------------------------------------------------------------------------------- /gjs/src/astalify.ts: -------------------------------------------------------------------------------- 1 | import Binding, { kebabify, snakeify, type Connectable, type Subscribable } from "./binding.js" 2 | import { Astal, Gtk, Gdk } from "./imports.js" 3 | import { execAsync } from "./process.js" 4 | import Variable from "./variable.js" 5 | 6 | Object.defineProperty(Astal.Box.prototype, "children", { 7 | get() { return this.get_children() }, 8 | set(v) { this.set_children(v) }, 9 | }) 10 | 11 | function setChildren(parent: Gtk.Widget, children: Gtk.Widget[]) { 12 | children = children.flat(Infinity).map(ch => ch instanceof Gtk.Widget 13 | ? ch 14 | : new Gtk.Label({ visible: true, label: String(ch) })) 15 | 16 | // remove 17 | if (parent instanceof Gtk.Bin) { 18 | const ch = parent.get_child() 19 | if (ch) 20 | parent.remove(ch) 21 | } 22 | 23 | // FIXME: add rest of the edge cases like Stack 24 | if (parent instanceof Astal.Box) { 25 | parent.set_children(children) 26 | } 27 | 28 | else if (parent instanceof Astal.CenterBox) { 29 | parent.startWidget = children[0] 30 | parent.centerWidget = children[1] 31 | parent.endWidget = children[2] 32 | } 33 | 34 | else if (parent instanceof Astal.Overlay) { 35 | const [child, ...overlays] = children 36 | parent.set_child(child) 37 | parent.set_overlays(overlays) 38 | } 39 | 40 | else if (parent instanceof Gtk.Container) { 41 | for (const ch of children) 42 | parent.add(ch) 43 | } 44 | } 45 | 46 | function mergeBindings(array: any[]) { 47 | function getValues(...args: any[]) { 48 | let i = 0 49 | return array.map(value => value instanceof Binding 50 | ? args[i++] 51 | : value, 52 | ) 53 | } 54 | 55 | const bindings = array.filter(i => i instanceof Binding) 56 | 57 | if (bindings.length === 0) 58 | return array 59 | 60 | if (bindings.length === 1) 61 | return bindings[0].as(getValues) 62 | 63 | return Variable.derive(bindings, getValues)() 64 | } 65 | 66 | function setProp(obj: any, prop: string, value: any) { 67 | try { 68 | const setter = `set_${snakeify(prop)}` 69 | if (typeof obj[setter] === "function") 70 | return obj[setter](value) 71 | 72 | if (Object.hasOwn(obj, prop)) 73 | return (obj[prop] = value) 74 | } 75 | catch (error) { 76 | console.error(`could not set property "${prop}" on ${obj}:`, error) 77 | } 78 | 79 | console.error(`could not set property "${prop}" on ${obj}`) 80 | } 81 | 82 | export type Widget> = C & { 83 | className: string 84 | css: string 85 | cursor: Cursor 86 | clickThrough: boolean 87 | toggleClassName(name: string, on?: boolean): void 88 | hook( 89 | object: Connectable, 90 | signal: string, 91 | callback: (self: Widget, ...args: any[]) => void, 92 | ): Widget 93 | hook( 94 | object: Subscribable, 95 | callback: (self: Widget, ...args: any[]) => void, 96 | ): Widget 97 | } 98 | 99 | function hook( 100 | self: Gtk.Widget, 101 | object: Connectable | Subscribable, 102 | signalOrCallback: string | ((self: Gtk.Widget, ...args: any[]) => void), 103 | callback?: (self: Gtk.Widget, ...args: any[]) => void, 104 | ) { 105 | if (typeof object.connect === "function" && callback) { 106 | const id = object.connect(signalOrCallback, (_: any, ...args: unknown[]) => { 107 | callback(self, ...args) 108 | }) 109 | self.connect("destroy", () => { 110 | (object.disconnect as Connectable["disconnect"])(id) 111 | }) 112 | } 113 | 114 | else if (typeof object.subscribe === "function" && typeof signalOrCallback === "function") { 115 | const unsub = object.subscribe((...args: unknown[]) => { 116 | signalOrCallback(self, ...args) 117 | }) 118 | self.connect("destroy", unsub) 119 | } 120 | 121 | return self 122 | } 123 | 124 | function ctor(self: any, config: any = {}, children: any = []) { 125 | const { setup, ...props } = config 126 | props.visible ??= true 127 | 128 | const bindings = Object.keys(props).reduce((acc: any, prop) => { 129 | if (props[prop] instanceof Binding) { 130 | const binding = props[prop] 131 | setProp(self, prop, binding.get()) 132 | delete props[prop] 133 | return [...acc, [prop, binding]] 134 | } 135 | return acc 136 | }, []) 137 | 138 | const onHandlers = Object.keys(props).reduce((acc: any, key) => { 139 | if (key.startsWith("on")) { 140 | const sig = kebabify(key).split("-").slice(1).join("-") 141 | const handler = props[key] 142 | delete props[key] 143 | return [...acc, [sig, handler]] 144 | } 145 | return acc 146 | }, []) 147 | 148 | Object.assign(self, props) 149 | 150 | for (const [signal, callback] of onHandlers) { 151 | if (typeof callback === "function") { 152 | self.connect(signal, callback) 153 | } 154 | else { 155 | self.connect(signal, () => execAsync(callback) 156 | .then(print).catch(console.error)) 157 | } 158 | } 159 | 160 | for (const [prop, bind] of bindings) { 161 | if (prop === "child" || prop === "children") { 162 | self.connect("destroy", bind.subscribe((v: any) => { 163 | setChildren(self, v) 164 | })) 165 | } 166 | self.connect("destroy", bind.subscribe((v: any) => { 167 | setProp(self, prop, v) 168 | })) 169 | } 170 | 171 | children = mergeBindings(children.flat(Infinity)) 172 | if (children instanceof Binding) { 173 | setChildren(self, children.get()) 174 | self.connect("destroy", children.subscribe((v) => { 175 | setChildren(self, v) 176 | })) 177 | } 178 | else { 179 | if (children.length > 0) 180 | setChildren(self, children) 181 | } 182 | 183 | setup?.(self) 184 | return self 185 | } 186 | 187 | function proxify< 188 | C extends typeof Gtk.Widget, 189 | >(klass: C) { 190 | Object.defineProperty(klass.prototype, "className", { 191 | get() { return Astal.widget_get_class_names(this).join(" ") }, 192 | set(v) { Astal.widget_set_class_names(this, v.split(/\s+/)) }, 193 | }) 194 | 195 | Object.defineProperty(klass.prototype, "css", { 196 | get() { return Astal.widget_get_css(this) }, 197 | set(v) { Astal.widget_set_css(this, v) }, 198 | }) 199 | 200 | Object.defineProperty(klass.prototype, "cursor", { 201 | get() { return Astal.widget_get_cursor(this) }, 202 | set(v) { Astal.widget_set_cursor(this, v) }, 203 | }) 204 | 205 | Object.defineProperty(klass.prototype, "clickThrough", { 206 | get() { return Astal.widget_get_click_through(this) }, 207 | set(v) { Astal.widget_set_click_through(this, v) }, 208 | }) 209 | 210 | Object.assign(klass.prototype, { 211 | hook: function (obj: any, sig: any, callback: any) { 212 | return hook(this as InstanceType, obj, sig, callback) 213 | }, 214 | toggleClassName: function name(cn: string, cond = true) { 215 | Astal.widget_toggle_class_name(this as InstanceType, cn, cond) 216 | }, 217 | set_class_name: function (name: string) { 218 | // @ts-expect-error unknown key 219 | this.className = name 220 | }, 221 | set_css: function (css: string) { 222 | // @ts-expect-error unknown key 223 | this.css = css 224 | }, 225 | set_cursor: function (cursor: string) { 226 | // @ts-expect-error unknown key 227 | this.cursor = cursor 228 | }, 229 | set_click_through: function (clickThrough: boolean) { 230 | // @ts-expect-error unknown key 231 | this.clickThrough = clickThrough 232 | }, 233 | }) 234 | 235 | const proxy = new Proxy(klass, { 236 | construct(_, [conf, ...children]) { 237 | // @ts-expect-error abstract class 238 | return ctor(new klass(), conf, children) 239 | }, 240 | apply(_t, _a, [conf, ...children]) { 241 | // @ts-expect-error abstract class 242 | return ctor(new klass(), conf, children) 243 | }, 244 | }) 245 | 246 | return proxy 247 | } 248 | 249 | export default function astalify< 250 | C extends typeof Gtk.Widget, 251 | P extends Record, 252 | N extends string = "Widget", 253 | >(klass: C) { 254 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 255 | type Astal = Omit & { 256 | new(props?: P, ...children: Gtk.Widget[]): Widget> 257 | (props?: P, ...children: Gtk.Widget[]): Widget> 258 | } 259 | 260 | return proxify(klass) as unknown as Astal 261 | } 262 | 263 | type BindableProps = { 264 | [K in keyof T]: Binding | T[K]; 265 | } 266 | 267 | type SigHandler< 268 | W extends InstanceType, 269 | Args extends Array, 270 | > = ((self: Widget, ...args: Args) => unknown) | string | string[] 271 | 272 | export type ConstructProps< 273 | Self extends InstanceType, 274 | Props extends Gtk.Widget.ConstructorProps, 275 | Signals extends Record<`on${string}`, Array> = Record<`on${string}`, any[]>, 276 | > = Partial<{ 277 | // @ts-expect-error can't assign to unknown, but it works as expected though 278 | [S in keyof Signals]: SigHandler 279 | }> & Partial<{ 280 | [Key in `on${string}`]: SigHandler 281 | }> & BindableProps & { 282 | className?: string 283 | css?: string 284 | cursor?: string 285 | clickThrough?: boolean 286 | }> & { 287 | onDestroy?: (self: Widget) => unknown 288 | onDraw?: (self: Widget) => unknown 289 | onKeyPressEvent?: (self: Widget, event: Gdk.Event) => unknown 290 | onKeyReleaseEvent?: (self: Widget, event: Gdk.Event) => unknown 291 | onButtonPressEvent?: (self: Widget, event: Gdk.Event) => unknown 292 | onButtonReleaseEvent?: (self: Widget, event: Gdk.Event) => unknown 293 | onRealize?: (self: Widget) => unknown 294 | setup?: (self: Widget) => void 295 | } 296 | 297 | type Cursor = 298 | | "default" 299 | | "help" 300 | | "pointer" 301 | | "context-menu" 302 | | "progress" 303 | | "wait" 304 | | "cell" 305 | | "crosshair" 306 | | "text" 307 | | "vertical-text" 308 | | "alias" 309 | | "copy" 310 | | "no-drop" 311 | | "move" 312 | | "not-allowed" 313 | | "grab" 314 | | "grabbing" 315 | | "all-scroll" 316 | | "col-resize" 317 | | "row-resize" 318 | | "n-resize" 319 | | "e-resize" 320 | | "s-resize" 321 | | "w-resize" 322 | | "ne-resize" 323 | | "nw-resize" 324 | | "sw-resize" 325 | | "se-resize" 326 | | "ew-resize" 327 | | "ns-resize" 328 | | "nesw-resize" 329 | | "nwse-resize" 330 | | "zoom-in" 331 | | "zoom-out" 332 | -------------------------------------------------------------------------------- /gjs/src/binding.ts: -------------------------------------------------------------------------------- 1 | export const snakeify = (str: string) => str 2 | .replace(/([a-z])([A-Z])/g, "$1_$2") 3 | .replaceAll("-", "_") 4 | .toLowerCase() 5 | 6 | export const kebabify = (str: string) => str 7 | .replace(/([a-z])([A-Z])/g, "$1-$2") 8 | .replaceAll("_", "-") 9 | .toLowerCase() 10 | 11 | export interface Subscribable { 12 | subscribe(callback: (value: T) => void): () => void 13 | get(): T 14 | [key: string]: any 15 | } 16 | 17 | export interface Connectable { 18 | connect(signal: string, callback: (...args: any[]) => unknown): number 19 | disconnect(id: number): void 20 | [key: string]: any 21 | } 22 | 23 | export default class Binding { 24 | private emitter: Subscribable | Connectable 25 | private prop?: string 26 | private transformFn = (v: any) => v 27 | 28 | static bind< 29 | T extends Connectable, 30 | P extends keyof T, 31 | >(object: T, property: P): Binding 32 | 33 | static bind(object: Subscribable): Binding 34 | 35 | static bind(emitter: Connectable | Subscribable, prop?: string) { 36 | return new Binding(emitter, prop) 37 | } 38 | 39 | private constructor(emitter: Connectable | Subscribable, prop?: string) { 40 | this.emitter = emitter 41 | this.prop = prop && kebabify(prop) 42 | } 43 | 44 | toString() { 45 | return `Binding<${this.emitter}${this.prop ? `, "${this.prop}"` : ""}>` 46 | } 47 | 48 | as(fn: (v: Value) => T): Binding { 49 | const bind = new Binding(this.emitter, this.prop) 50 | bind.transformFn = (v: Value) => fn(this.transformFn(v)) 51 | return bind as unknown as Binding 52 | } 53 | 54 | get(): Value { 55 | if (typeof this.emitter.get === "function") 56 | return this.transformFn(this.emitter.get()) 57 | 58 | if (typeof this.prop === "string") { 59 | const getter = `get_${snakeify(this.prop)}` 60 | if (typeof this.emitter[getter] === "function") 61 | return this.transformFn(this.emitter[getter]()) 62 | 63 | return this.transformFn(this.emitter[this.prop]) 64 | } 65 | 66 | throw Error("can not get value of binding") 67 | } 68 | 69 | subscribe(callback: (value: Value) => void): () => void { 70 | if (typeof this.emitter.subscribe === "function") { 71 | return this.emitter.subscribe(() => { 72 | callback(this.get()) 73 | }) 74 | } 75 | else if (typeof this.emitter.connect === "function") { 76 | const signal = `notify::${this.prop}` 77 | const id = this.emitter.connect(signal, () => { 78 | callback(this.get()) 79 | }) 80 | return () => { 81 | (this.emitter.disconnect as Connectable["disconnect"])(id) 82 | } 83 | } 84 | throw Error(`${this.emitter} is not bindable`) 85 | } 86 | } 87 | 88 | export const { bind } = Binding 89 | -------------------------------------------------------------------------------- /gjs/src/file.ts: -------------------------------------------------------------------------------- 1 | import { Astal, Gio } from "./imports.js" 2 | 3 | export function readFile(path: string): string { 4 | return Astal.read_file(path) || "" 5 | } 6 | 7 | export function readFileAsync(path: string): Promise { 8 | return new Promise((resolve, reject) => { 9 | Astal.read_file_async(path, (_, res) => { 10 | try { 11 | resolve(Astal.read_file_finish(res) || "") 12 | } 13 | catch (error) { 14 | reject(error) 15 | } 16 | }) 17 | }) 18 | } 19 | 20 | export function writeFile(path: string, content: string): void { 21 | Astal.write_file(path, content) 22 | } 23 | 24 | export function writeFileAsync(path: string, content: string): Promise { 25 | return new Promise((resolve, reject) => { 26 | Astal.write_file_async(path, content, (_, res) => { 27 | try { 28 | resolve(Astal.write_file_finish(res)) 29 | } 30 | catch (error) { 31 | reject(error) 32 | } 33 | }) 34 | }) 35 | } 36 | 37 | export function monitorFile( 38 | path: string, 39 | callback: (file: string, event: Gio.FileMonitorEvent) => void, 40 | ): Gio.FileMonitor { 41 | return Astal.monitor_file(path, (file: string, event: Gio.FileMonitorEvent) => { 42 | callback(file, event) 43 | })! 44 | } 45 | -------------------------------------------------------------------------------- /gjs/src/imports.ts: -------------------------------------------------------------------------------- 1 | // this file's purpose is to have glib versions in one place 2 | // this is only really needed for Gtk/Astal because 3 | // ts-gir might generate gtk4 versions too 4 | 5 | export { default as Astal } from "gi://Astal?version=0.1" 6 | export { default as GObject } from "gi://GObject?version=2.0" 7 | export { default as Gio } from "gi://Gio?version=2.0" 8 | export { default as Gtk } from "gi://Gtk?version=3.0" 9 | export { default as Gdk } from "gi://Gdk?version=3.0" 10 | export { default as GLib } from "gi://GLib?version=2.0" 11 | -------------------------------------------------------------------------------- /gjs/src/jsx/jsx-runtime.ts: -------------------------------------------------------------------------------- 1 | import { Gtk } from "../imports.js" 2 | import * as Widget from "../widgets.js" 3 | 4 | function isArrowFunction(func: any): func is (args: any) => any { 5 | return !Object.hasOwn(func, "prototype") 6 | } 7 | 8 | export function jsx( 9 | ctor: keyof typeof ctors | typeof Gtk.Widget, 10 | { children, ...props }: any, 11 | ) { 12 | children ??= [] 13 | 14 | if (!Array.isArray(children)) 15 | children = [children] 16 | 17 | children = children.filter(Boolean) 18 | 19 | if (typeof ctor === "string") 20 | return (ctors as any)[ctor](props, children) 21 | 22 | if (children.length === 1) 23 | props.child = children[0] 24 | else if (children.length > 1) 25 | props.children = children 26 | 27 | if (isArrowFunction(ctor)) 28 | return ctor(props) 29 | 30 | // @ts-expect-error can be class or function 31 | return new ctor(props) 32 | } 33 | 34 | const ctors = { 35 | box: Widget.Box, 36 | button: Widget.Button, 37 | centerbox: Widget.CenterBox, 38 | // TODO: circularprogress 39 | drawingarea: Widget.DrawingArea, 40 | entry: Widget.Entry, 41 | eventbox: Widget.EventBox, 42 | // TODO: fixed 43 | // TODO: flowbox 44 | icon: Widget.Icon, 45 | label: Widget.Label, 46 | levelbar: Widget.LevelBar, 47 | // TODO: listbox 48 | overlay: Widget.Overlay, 49 | revealer: Widget.Revealer, 50 | scrollable: Widget.Scrollable, 51 | slider: Widget.Slider, 52 | // TODO: stack 53 | switch: Widget.Switch, 54 | window: Widget.Window, 55 | } 56 | 57 | declare global { 58 | // eslint-disable-next-line @typescript-eslint/no-namespace 59 | namespace JSX { 60 | type Element = Gtk.Widget 61 | type ElementClass = Gtk.Widget 62 | interface IntrinsicElements { 63 | box: Widget.BoxProps 64 | button: Widget.ButtonProps 65 | centerbox: Widget.CenterBoxProps 66 | // TODO: circularprogress 67 | drawingarea: Widget.DrawingAreaProps 68 | entry: Widget.EntryProps 69 | eventbox: Widget.EventBoxProps 70 | // TODO: fixed 71 | // TODO: flowbox 72 | icon: Widget.IconProps 73 | label: Widget.LabelProps 74 | levelbar: Widget.LevelBarProps 75 | // TODO: listbox 76 | overlay: Widget.OverlayProps 77 | revealer: Widget.RevealerProps 78 | scrollable: Widget.ScrollableProps 79 | slider: Widget.SliderProps 80 | // TODO: stack 81 | switch: Widget.SwitchProps 82 | window: Widget.WindowProps 83 | } 84 | } 85 | } 86 | 87 | export const jsxs = jsx 88 | -------------------------------------------------------------------------------- /gjs/src/process.ts: -------------------------------------------------------------------------------- 1 | import { Astal } from "./imports.js" 2 | 3 | type Args = { 4 | cmd: string | string[] 5 | out?: (stdout: string) => Out 6 | err?: (stderr: string) => Err 7 | } 8 | 9 | function args(argsOrCmd: Args | string | string[], onOut: O, onErr: E) { 10 | const params = Array.isArray(argsOrCmd) || typeof argsOrCmd === "string" 11 | return { 12 | cmd: params ? argsOrCmd : argsOrCmd.cmd, 13 | err: params ? onErr : argsOrCmd.err || onErr, 14 | out: params ? onOut : argsOrCmd.out || onOut, 15 | } 16 | } 17 | 18 | export function subprocess(args: Args): Astal.Process 19 | export function subprocess( 20 | cmd: string | string[], 21 | onOut?: (stdout: string) => void, 22 | onErr?: (stderr: string) => void, 23 | ): Astal.Process 24 | export function subprocess( 25 | argsOrCmd: Args | string | string[], 26 | onOut: (stdout: string) => void = print, 27 | onErr: (stderr: string) => void = printerr, 28 | ) { 29 | const { cmd, err, out } = args(argsOrCmd, onOut, onErr) 30 | const proc = Array.isArray(cmd) 31 | ? Astal.Process.subprocessv(cmd) 32 | : Astal.Process.subprocess(cmd) 33 | 34 | proc.connect("stdout", (_, stdout: string) => out(stdout)) 35 | proc.connect("stderr", (_, stderr: string) => err(stderr)) 36 | return proc 37 | } 38 | 39 | /** @throws {GLib.Error} Throws stderr */ 40 | export function exec(cmd: string | string[]) { 41 | return Array.isArray(cmd) 42 | ? Astal.Process.execv(cmd) 43 | : Astal.Process.exec(cmd) 44 | } 45 | 46 | export function execAsync(cmd: string | string[]): Promise { 47 | return new Promise((resolve, reject) => { 48 | if (Array.isArray(cmd)) { 49 | Astal.Process.exec_asyncv(cmd, (_, res) => { 50 | try { 51 | resolve(Astal.Process.exec_asyncv_finish(res)) 52 | } 53 | catch (error) { 54 | reject(error) 55 | } 56 | }) 57 | } 58 | else { 59 | Astal.Process.exec_async(cmd, (_, res) => { 60 | try { 61 | resolve(Astal.Process.exec_finish(res)) 62 | } 63 | catch (error) { 64 | reject(error) 65 | } 66 | }) 67 | } 68 | }) 69 | } 70 | -------------------------------------------------------------------------------- /gjs/src/time.ts: -------------------------------------------------------------------------------- 1 | import { Astal } from "./imports.js" 2 | 3 | export function interval(interval: number, callback?: () => void) { 4 | return Astal.Time.interval(interval, () => void callback?.()) 5 | } 6 | 7 | export function timeout(timeout: number, callback?: () => void) { 8 | return Astal.Time.timeout(timeout, () => void callback?.()) 9 | } 10 | 11 | export function idle(callback?: () => void) { 12 | return Astal.Time.idle(() => void callback?.()) 13 | } 14 | -------------------------------------------------------------------------------- /gjs/src/variable.ts: -------------------------------------------------------------------------------- 1 | import Binding, { type Connectable } from "./binding.js" 2 | import { Astal } from "./imports.js" 3 | import { interval } from "./time.js" 4 | import { execAsync, subprocess } from "./process.js" 5 | 6 | class VariableWrapper extends Function { 7 | private variable!: Astal.VariableBase 8 | private errHandler? = console.error 9 | 10 | private _value: T 11 | private _poll?: Astal.Time 12 | private _watch?: Astal.Process 13 | 14 | private pollInterval = 1000 15 | private pollExec?: string[] | string 16 | private pollTransform?: (stdout: string, prev: T) => T 17 | private pollFn?: (prev: T) => T | Promise 18 | 19 | private watchTransform?: (stdout: string, prev: T) => T 20 | private watchExec?: string[] | string 21 | 22 | constructor(init: T) { 23 | super() 24 | this._value = init 25 | this.variable = new Astal.VariableBase() 26 | this.variable.connect("dropped", () => { 27 | this.stopWatch() 28 | this.stopPoll() 29 | }) 30 | this.variable.connect("error", (_, err) => this.errHandler?.(err)) 31 | return new Proxy(this, { 32 | apply: (target, _, args) => target._call(args[0]), 33 | }) 34 | } 35 | 36 | private _call(transform?: (value: T) => R): Binding { 37 | const b = Binding.bind(this) 38 | return transform ? b.as(transform) : b as unknown as Binding 39 | } 40 | 41 | toString() { 42 | return String(`Variable<${this.get()}>`) 43 | } 44 | 45 | get(): T { return this._value } 46 | set(value: T) { 47 | if (value !== this._value) { 48 | this._value = value 49 | this.variable.emit("changed") 50 | } 51 | } 52 | 53 | startPoll() { 54 | if (this._poll) 55 | return 56 | 57 | if (this.pollFn) { 58 | this._poll = interval(this.pollInterval, () => { 59 | const v = this.pollFn!(this.get()) 60 | if (v instanceof Promise) { 61 | v.then(v => this.set(v)) 62 | .catch(err => this.variable.emit("error", err)) 63 | } 64 | else { 65 | this.set(v) 66 | } 67 | }) 68 | } 69 | else if (this.pollExec) { 70 | this._poll = interval(this.pollInterval, () => { 71 | execAsync(this.pollExec!) 72 | .then(v => this.set(this.pollTransform!(v, this.get()))) 73 | .catch(err => this.variable.emit("error", err)) 74 | }) 75 | } 76 | } 77 | 78 | startWatch() { 79 | if (this._watch) 80 | return 81 | 82 | this._watch = subprocess({ 83 | cmd: this.watchExec!, 84 | out: out => this.set(this.watchTransform!(out, this.get())), 85 | err: err => this.variable.emit("error", err), 86 | }) 87 | } 88 | 89 | stopPoll() { 90 | this._poll?.cancel() 91 | delete this._poll 92 | } 93 | 94 | stopWatch() { 95 | this._watch?.kill() 96 | delete this._watch 97 | } 98 | 99 | isPolling() { return !!this._poll } 100 | isWatching() { return !!this._watch } 101 | 102 | drop() { 103 | this.variable.emit("dropped") 104 | this.variable.run_dispose() 105 | } 106 | 107 | onDropped(callback: () => void) { 108 | this.variable.connect("dropped", callback) 109 | return this as unknown as Variable 110 | } 111 | 112 | onError(callback: (err: string) => void) { 113 | delete this.errHandler 114 | this.variable.connect("error", (_, err) => callback(err)) 115 | return this as unknown as Variable 116 | } 117 | 118 | subscribe(callback: (value: T) => void) { 119 | const id = this.variable.connect("changed", () => { 120 | callback(this.get()) 121 | }) 122 | return () => this.variable.disconnect(id) 123 | } 124 | 125 | poll( 126 | interval: number, 127 | exec: string | string[], 128 | transform?: (stdout: string, prev: T) => T 129 | ): Variable 130 | 131 | poll( 132 | interval: number, 133 | callback: (prev: T) => T | Promise 134 | ): Variable 135 | 136 | poll( 137 | interval: number, 138 | exec: string | string[] | ((prev: T) => T | Promise), 139 | transform: (stdout: string, prev: T) => T = out => out as T, 140 | ) { 141 | this.stopPoll() 142 | this.pollInterval = interval 143 | this.pollTransform = transform 144 | if (typeof exec === "function") { 145 | this.pollFn = exec 146 | delete this.pollExec 147 | } 148 | else { 149 | this.pollExec = exec 150 | delete this.pollFn 151 | } 152 | this.startPoll() 153 | return this as unknown as Variable 154 | } 155 | 156 | watch( 157 | exec: string | string[], 158 | transform: (stdout: string, prev: T) => T = out => out as T, 159 | ) { 160 | this.stopWatch() 161 | this.watchExec = exec 162 | this.watchTransform = transform 163 | this.startWatch() 164 | return this as unknown as Variable 165 | } 166 | 167 | observe( 168 | objs: Array<[obj: Connectable, signal: string]>, 169 | callback: (...args: any[]) => T): Variable 170 | 171 | observe( 172 | obj: Connectable, 173 | signal: string, 174 | callback: (...args: any[]) => T): Variable 175 | 176 | observe( 177 | objs: Connectable | Array<[obj: Connectable, signal: string]>, 178 | sigOrFn: string | ((obj: Connectable, ...args: any[]) => T), 179 | callback?: (obj: Connectable, ...args: any[]) => T, 180 | ) { 181 | const f = typeof sigOrFn === "function" ? sigOrFn : callback ?? (() => this.get()) 182 | const set = (obj: Connectable, ...args: any[]) => this.set(f(obj, ...args)) 183 | 184 | if (Array.isArray(objs)) { 185 | for (const obj of objs) { 186 | const [o, s] = obj 187 | o.connect(s, set) 188 | } 189 | } 190 | else { 191 | if (typeof sigOrFn === "string") 192 | objs.connect(sigOrFn, set) 193 | } 194 | 195 | return this as unknown as Variable 196 | } 197 | 198 | static derive< 199 | const Deps extends Array | Binding>, 200 | Args extends { 201 | [K in keyof Deps]: Deps[K] extends Variable 202 | ? T : Deps[K] extends Binding ? T : never 203 | }, 204 | V = Args, 205 | >(deps: Deps, fn: (...args: Args) => V = (...args) => args as unknown as V) { 206 | const update = () => fn(...deps.map(d => d.get()) as Args) 207 | const derived = new Variable(update()) 208 | const unsubs = deps.map(dep => dep.subscribe(() => derived.set(update()))) 209 | derived.onDropped(() => unsubs.map(unsub => unsub())) 210 | return derived 211 | } 212 | } 213 | 214 | export interface Variable extends Omit, "bind"> { 215 | (transform: (value: T) => R): Binding 216 | (): Binding 217 | } 218 | 219 | export const Variable = new Proxy(VariableWrapper as any, { 220 | apply: (_t, _a, args) => new VariableWrapper(args[0]), 221 | }) as { 222 | derive: typeof VariableWrapper["derive"] 223 | (init: T): Variable 224 | new(init: T): Variable 225 | } 226 | 227 | export default Variable 228 | -------------------------------------------------------------------------------- /gjs/src/widgets.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | import { Astal, Gtk } from "./imports.js" 3 | import astalify, { type ConstructProps, type Widget } from "./astalify.js" 4 | 5 | export { astalify, ConstructProps } 6 | 7 | // Box 8 | export type Box = Widget 9 | export const Box = astalify(Astal.Box) 10 | export type BoxProps = ConstructProps 11 | 12 | // Button 13 | export type Button = Widget 14 | export const Button = astalify(Astal.Button) 15 | export type ButtonProps = ConstructProps 23 | 24 | // CenterBox 25 | export type CenterBox = Widget 26 | export const CenterBox = astalify(Astal.CenterBox) 27 | export type CenterBoxProps = ConstructProps 28 | 29 | // TODO: CircularProgress 30 | 31 | // DrawingArea 32 | export type DrawingArea = Widget 33 | export const DrawingArea = astalify(Gtk.DrawingArea) 34 | export type DrawingAreaProps = ConstructProps 37 | 38 | // Entry 39 | export type Entry = Widget 40 | export const Entry = astalify(Gtk.Entry) 41 | export type EntryProps = ConstructProps 45 | 46 | // EventBox 47 | export type EventBox = Widget 48 | export const EventBox = astalify(Astal.EventBox) 49 | export type EventBoxProps = ConstructProps 56 | 57 | // TODO: Fixed 58 | // TODO: FlowBox 59 | 60 | // Icon 61 | export type Icon = Widget 62 | export const Icon = astalify(Astal.Icon) 63 | export type IconProps = ConstructProps 64 | 65 | // Label 66 | export type Label = Widget 67 | export const Label = astalify(Astal.Label) 68 | export type LabelProps = ConstructProps 69 | 70 | // LevelBar 71 | export type LevelBar = Widget 72 | export const LevelBar = astalify(Astal.LevelBar) 73 | export type LevelBarProps = ConstructProps 74 | 75 | // TODO: ListBox 76 | 77 | // Overlay 78 | export type Overlay = Widget 79 | export const Overlay = astalify(Astal.Overlay) 80 | export type OverlayProps = ConstructProps 81 | 82 | // Revealer 83 | export type Revealer = Widget 84 | export const Revealer = astalify(Gtk.Revealer) 85 | export type RevealerProps = ConstructProps 86 | 87 | // Scrollable 88 | export type Scrollable = Widget 89 | export const Scrollable = astalify(Astal.Scrollable) 90 | export type ScrollableProps = ConstructProps 91 | 92 | // Slider 93 | export type Slider = Widget 94 | export const Slider = astalify(Astal.Slider) 95 | export type SliderProps = ConstructProps 98 | 99 | // TODO: Stack 100 | 101 | // Switch 102 | export type Switch = Widget 103 | export const Switch = astalify(Gtk.Switch) 104 | export type SwitchProps = ConstructProps 105 | 106 | // Window 107 | export type Window = Widget 108 | export const Window = astalify(Astal.Window) 109 | export type WindowProps = ConstructProps 110 | -------------------------------------------------------------------------------- /gjs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "lib": [ 6 | "ESNext" 7 | ], 8 | "outDir": "dist", 9 | "declaration": true, 10 | "strict": true, 11 | "moduleResolution": "Bundler", 12 | "skipLibCheck": true, 13 | "checkJs": true, 14 | "allowJs": true, 15 | "jsx": "react-jsx", 16 | "jsxImportSource": "./src/jsx", 17 | }, 18 | "include": [ 19 | "./node_modules/@girs", 20 | "./src/**/*", 21 | "./index.ts", 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /lua/astal-dev-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "astal" 2 | version = "dev-1" 3 | 4 | source = { 5 | url = "git+https://github.com/astal-sh/libastal", 6 | } 7 | 8 | description = { 9 | summary = "lua bindings for libastal.", 10 | homepage = "https://github.com/astal-sh/libastal", 11 | license = "GPL-3", 12 | } 13 | 14 | dependencies = { 15 | "lua >= 5.1, < 5.4", 16 | "lgi >= 0.9.2", 17 | } 18 | 19 | build = { 20 | type = "builtin", 21 | modules = { 22 | ["astal.application"] = "astal/application.lua", 23 | ["astal.binding"] = "astal/binding.lua", 24 | ["astal.init"] = "astal/init.lua", 25 | ["astal.process"] = "astal/process.lua", 26 | ["astal.time"] = "astal/time.lua", 27 | ["astal.variable"] = "astal/variable.lua", 28 | ["astal.widget"] = "astal/widget.lua", 29 | ["astal.file"] = "astal/file.lua", 30 | }, 31 | } 32 | -------------------------------------------------------------------------------- /lua/astal/application.lua: -------------------------------------------------------------------------------- 1 | local lgi = require("lgi") 2 | local Astal = lgi.require("Astal", "0.1") 3 | 4 | local AstalLua = Astal.Application:derive("AstalLua") 5 | local request_handler 6 | 7 | function AstalLua:do_request(msg, conn) 8 | if type(request_handler) == "function" then 9 | request_handler(msg, function(response) 10 | Astal.write_sock(conn, tostring(response), function(_, res) 11 | Astal.write_sock_finish(res) 12 | end) 13 | end) 14 | else 15 | Astal.Application.do_request(self, msg, conn) 16 | end 17 | end 18 | 19 | function AstalLua:quit(code) 20 | Astal.Application.quit(self) 21 | os.exit(code) 22 | end 23 | 24 | local app = AstalLua() 25 | 26 | ---@class StartConfig 27 | ---@field icons? string 28 | ---@field instance_name? string 29 | ---@field gtk_theme? string 30 | ---@field icon_theme? string 31 | ---@field cursor_theme? string 32 | ---@field css? string 33 | ---@field hold? boolean 34 | ---@field request_handler? fun(msg: string, response: fun(res: any)) 35 | ---@field main? fun(...): unknown 36 | ---@field client? fun(message: fun(msg: string): string, ...): unknown 37 | 38 | ---@param config StartConfig | nil 39 | function Astal.Application:start(config) 40 | if config == nil then 41 | config = {} 42 | end 43 | 44 | if config.client == nil then 45 | config.client = function() 46 | print('Astal instance "' .. app.instance_name .. '" is already running') 47 | os.exit(1) 48 | end 49 | end 50 | 51 | if config.hold == nil then 52 | config.hold = true 53 | end 54 | 55 | request_handler = config.request_handler 56 | 57 | if config.css then 58 | self:apply_css(config.css) 59 | end 60 | if config.icons then 61 | self:add_icons(config.icons) 62 | end 63 | if config.instance_name then 64 | self.instance_name = config.instance_name 65 | end 66 | if config.gtk_theme then 67 | self.gtk_theme = config.gtk_theme 68 | end 69 | if config.icon_theme then 70 | self.icon_theme = config.icon_theme 71 | end 72 | if config.cursor_theme then 73 | self.cursor_theme = config.cursor_theme 74 | end 75 | 76 | app.on_activate = function() 77 | if type(config.main) == "function" then 78 | config.main(table.unpack(arg)) 79 | end 80 | if config.hold then 81 | self:hold() 82 | end 83 | end 84 | 85 | if not app:acquire_socket() then 86 | return config.client(function(msg) 87 | return Astal.Application.send_message(self.instance_name, msg) 88 | end, table.unpack(arg)) 89 | end 90 | 91 | self:run(nil) 92 | end 93 | 94 | return app 95 | -------------------------------------------------------------------------------- /lua/astal/binding.lua: -------------------------------------------------------------------------------- 1 | local lgi = require("lgi") 2 | local GObject = lgi.require("GObject", "2.0") 3 | 4 | ---@class Binding 5 | ---@field emitter table|Variable 6 | ---@field property? string 7 | ---@field transformFn function 8 | local Binding = {} 9 | 10 | ---@param emitter table 11 | ---@param property? string 12 | ---@return Binding 13 | function Binding.new(emitter, property) 14 | return setmetatable({ 15 | emitter = emitter, 16 | property = property, 17 | transformFn = function(v) 18 | return v 19 | end, 20 | }, Binding) 21 | end 22 | 23 | function Binding:__tostring() 24 | local str = "Binding<" .. tostring(self.emitter) 25 | if self.property ~= nil then 26 | str = str .. ", " .. self.property 27 | end 28 | return str .. ">" 29 | end 30 | 31 | function Binding:get() 32 | if type(self.emitter.get) == "function" then 33 | return self.transformFn(self.emitter:get()) 34 | end 35 | return self.transformFn(self.emitter[self.property]) 36 | end 37 | 38 | ---@param transform fun(value: any): any 39 | ---@return Binding 40 | function Binding:as(transform) 41 | local b = Binding.new(self.emitter, self.property) 42 | b.transformFn = function(v) 43 | return transform(self.transformFn(v)) 44 | end 45 | return b 46 | end 47 | 48 | ---@param callback fun(value: any) 49 | ---@return function 50 | function Binding:subscribe(callback) 51 | if type(self.emitter.subscribe) == "function" then 52 | return self.emitter:subscribe(function() 53 | callback(self:get()) 54 | end) 55 | end 56 | local id = self.emitter.on_notify:connect(function() 57 | callback(self:get()) 58 | end, self.property, false) 59 | return function() 60 | GObject.signal_handler_disconnect(self.emitter, id) 61 | end 62 | end 63 | 64 | Binding.__index = Binding 65 | return Binding 66 | -------------------------------------------------------------------------------- /lua/astal/file.lua: -------------------------------------------------------------------------------- 1 | local lgi = require("lgi") 2 | local Astal = lgi.require("Astal", "0.1") 3 | local GObject = lgi.require("GObject", "2.0") 4 | 5 | local M = {} 6 | 7 | ---@param path string 8 | ---@return string 9 | function M.read_file(path) 10 | return Astal.read_file(path) 11 | end 12 | 13 | ---@param path string 14 | ---@param callback fun(content: string, err: string): nil 15 | function M.read_file_async(path, callback) 16 | Astal.read_file_async(path, function(_, res) 17 | local content, err = Astal.read_file_finish(res) 18 | callback(content, err) 19 | end) 20 | end 21 | 22 | ---@param path string 23 | ---@param content string 24 | function M.write_file(path, content) 25 | Astal.write_file(path, content) 26 | end 27 | 28 | ---@param path string 29 | ---@param content string 30 | ---@param callback? fun(err: string): nil 31 | function M.write_file_async(path, content, callback) 32 | Astal.write_file_async(path, content, function(_, res) 33 | if type(callback) == "function" then 34 | callback(Astal.write_file_finish(res)) 35 | end 36 | end) 37 | end 38 | 39 | ---@param path string 40 | ---@param callback fun(file: string, event: integer): nil 41 | function M.monitor_file(path, callback) 42 | return Astal.monitor_file(path, GObject.Closure(callback)) 43 | end 44 | 45 | return M 46 | -------------------------------------------------------------------------------- /lua/astal/init.lua: -------------------------------------------------------------------------------- 1 | local lgi = require("lgi") 2 | local Astal = lgi.require("Astal", "0.1") 3 | local Gtk = lgi.require("Gtk", "3.0") 4 | local Gdk = lgi.require("Gdk", "3.0") 5 | local GObject = lgi.require("GObject", "2.0") 6 | local Widget = require("astal.widget") 7 | local Variable = require("astal.variable") 8 | local Binding = require("astal.binding") 9 | local App = require("astal.application") 10 | local Process = require("astal.process") 11 | local Time = require("astal.time") 12 | local File = require("astal.file") 13 | 14 | return { 15 | App = App, 16 | Variable = Variable, 17 | Widget = Widget, 18 | bind = Binding.new, 19 | 20 | interval = Time.interval, 21 | timeout = Time.timeout, 22 | idle = Time.idle, 23 | 24 | subprocess = Process.subprocess, 25 | exec = Process.exec, 26 | exec_async = Process.exec_async, 27 | 28 | read_file = File.read_file, 29 | read_file_async = File.read_file_async, 30 | write_file = File.write_file, 31 | write_file_async = File.write_file_async, 32 | monitor_file = File.monitor_file, 33 | 34 | Astal = Astal, 35 | Gtk = Gtk, 36 | Gdk = Gdk, 37 | GObject = GObject, 38 | GLib = lgi.require("GLib", "2.0"), 39 | Gio = lgi.require("Gio", "2.0"), 40 | require = lgi.require, 41 | } 42 | -------------------------------------------------------------------------------- /lua/astal/process.lua: -------------------------------------------------------------------------------- 1 | local lgi = require("lgi") 2 | local Astal = lgi.require("Astal", "0.1") 3 | 4 | local M = {} 5 | 6 | local defualt_proc_args = function(on_stdout, on_stderr) 7 | if on_stdout == nil then 8 | on_stdout = function(out) 9 | io.stdout:write(tostring(out) .. "\n") 10 | return tostring(out) 11 | end 12 | end 13 | 14 | if on_stderr == nil then 15 | on_stderr = function(err) 16 | io.stderr:write(tostring(err) .. "\n") 17 | return tostring(err) 18 | end 19 | end 20 | 21 | return on_stdout, on_stderr 22 | end 23 | 24 | ---@param commandline string | string[] 25 | ---@param on_stdout? fun(out: string): nil 26 | ---@param on_stderr? fun(err: string): nil 27 | ---@return { kill: function } | nil proc 28 | function M.subprocess(commandline, on_stdout, on_stderr) 29 | local out, err = defualt_proc_args(on_stdout, on_stderr) 30 | local proc, fail 31 | if type(commandline) == "table" then 32 | proc, fail = Astal.Process.subprocessv(commandline) 33 | else 34 | proc, fail = Astal.Process.subprocess(commandline) 35 | end 36 | if fail ~= nil then 37 | err(fail) 38 | return nil 39 | end 40 | proc.on_stdout = function(_, str) 41 | out(str) 42 | end 43 | proc.on_stderr = function(_, str) 44 | err(str) 45 | end 46 | return proc 47 | end 48 | 49 | ---@generic T 50 | ---@param commandline string | string[] 51 | ---@param on_stdout? fun(out: string): T 52 | ---@param on_stderr? fun(err: string): T 53 | ---@return T 54 | function M.exec(commandline, on_stdout, on_stderr) 55 | local out, err = defualt_proc_args(on_stdout, on_stderr) 56 | local stdout, stderr 57 | if type(commandline) == "table" then 58 | stdout, stderr = Astal.Process.execv(commandline) 59 | else 60 | stdout, stderr = Astal.Process.exec(commandline) 61 | end 62 | if stderr then 63 | return err(stderr) 64 | end 65 | return out(stdout) 66 | end 67 | 68 | ---@param commandline string | string[] 69 | ---@param on_stdout? fun(out: string): nil 70 | ---@param on_stderr? fun(err: string): nil 71 | function M.exec_async(commandline, on_stdout, on_stderr) 72 | local out, err = defualt_proc_args(on_stdout, on_stderr) 73 | if type(commandline) == "table" then 74 | Astal.Process.exec_asyncv(commandline, function(_, res) 75 | local stdout, fail = Astal.exec_asyncv_finish(res) 76 | if fail ~= nil then 77 | err(fail) 78 | else 79 | out(stdout) 80 | end 81 | end) 82 | else 83 | Astal.Process.exec_async(commandline, function(_, res) 84 | local stdout, fail = Astal.exec_finish(res) 85 | if fail ~= nil then 86 | err(fail) 87 | else 88 | out(stdout) 89 | end 90 | end) 91 | end 92 | end 93 | 94 | return M 95 | -------------------------------------------------------------------------------- /lua/astal/time.lua: -------------------------------------------------------------------------------- 1 | local lgi = require("lgi") 2 | local Astal = lgi.require("Astal", "0.1") 3 | local GObject = lgi.require("GObject", "2.0") 4 | 5 | local M = {} 6 | 7 | ---@param interval number 8 | ---@param fn function 9 | ---@return { cancel: function, on_now: function } 10 | function M.interval(interval, fn) 11 | return Astal.Time.interval(interval, GObject.Closure(fn)) 12 | end 13 | 14 | ---@param timeout number 15 | ---@param fn function 16 | ---@return { cancel: function, on_now: function } 17 | function M.timeout(timeout, fn) 18 | return Astal.Time.timeout(timeout, GObject.Closure(fn)) 19 | end 20 | 21 | ---@param fn function 22 | ---@return { cancel: function, on_now: function } 23 | function M.idle(fn) 24 | return Astal.Time.idle(GObject.Closure(fn)) 25 | end 26 | 27 | return M 28 | -------------------------------------------------------------------------------- /lua/astal/variable.lua: -------------------------------------------------------------------------------- 1 | local lgi = require("lgi") 2 | local Astal = lgi.require("Astal", "0.1") 3 | local GObject = lgi.require("GObject", "2.0") 4 | local Binding = require("astal.binding") 5 | local Time = require("astal.time") 6 | local Process = require("astal.process") 7 | 8 | ---@class Variable 9 | ---@field private variable table 10 | ---@field private err_handler? function 11 | ---@field private _value any 12 | ---@field private _poll? table 13 | ---@field private _watch? table 14 | ---@field private poll_interval number 15 | ---@field private poll_exec? string[] | string 16 | ---@field private poll_transform? fun(next: any, prev: any): any 17 | ---@field private poll_fn? function 18 | ---@field private watch_transform? fun(next: any, prev: any): any 19 | ---@field private watch_exec? string[] | string 20 | local Variable = {} 21 | Variable.__index = Variable 22 | 23 | ---@param value any 24 | ---@return Variable 25 | function Variable.new(value) 26 | local v = Astal.VariableBase() 27 | local variable = setmetatable({ 28 | variable = v, 29 | _value = value, 30 | }, Variable) 31 | v.on_dropped = function() 32 | variable:stop_watch() 33 | variable:stop_watch() 34 | end 35 | v.on_error = function(_, err) 36 | if variable.err_handler then 37 | variable.err_handler(err) 38 | end 39 | end 40 | return variable 41 | end 42 | 43 | ---@param transform function 44 | ---@return Binding 45 | function Variable:__call(transform) 46 | if transform == nil then 47 | transform = function(v) 48 | return v 49 | end 50 | return Binding.new(self) 51 | end 52 | return Binding.new(self):as(transform) 53 | end 54 | 55 | function Variable:__tostring() 56 | return "Variable<" .. tostring(self:get()) .. ">" 57 | end 58 | 59 | function Variable:get() 60 | return self._value or nil 61 | end 62 | 63 | function Variable:set(value) 64 | if value ~= self:get() then 65 | self._value = value 66 | self.variable:emit_changed() 67 | end 68 | end 69 | 70 | function Variable:start_poll() 71 | if self._poll ~= nil then 72 | return 73 | end 74 | 75 | if self.poll_fn then 76 | self._poll = Time.interval(self.poll_interval, function() 77 | self:set(self.poll_fn(self:get())) 78 | end) 79 | elseif self.poll_exec then 80 | self._poll = Time.interval(self.poll_interval, function() 81 | Process.exec_async(self.poll_exec, function(out) 82 | self:set(self.poll_transform(out, self:get())) 83 | end, function(err) 84 | self.variable.emit_error(err) 85 | end) 86 | end) 87 | end 88 | end 89 | 90 | function Variable:start_watch() 91 | if self._watch then 92 | return 93 | end 94 | 95 | self._watch = Process.subprocess(self.watch_exec, function(out) 96 | self:set(self.watch_transform(out, self:get())) 97 | end, function(err) 98 | self.variable.emit_error(err) 99 | end) 100 | end 101 | 102 | function Variable:stop_poll() 103 | if self._poll then 104 | self._poll.cancel() 105 | end 106 | self._poll = nil 107 | end 108 | 109 | function Variable:stop_watch() 110 | if self._watch then 111 | self._watch.kill() 112 | end 113 | self._watch = nil 114 | end 115 | 116 | function Variable:is_polling() 117 | return self._poll ~= nil 118 | end 119 | 120 | function Variable:is_watching() 121 | return self._watch ~= nil 122 | end 123 | 124 | function Variable:drop() 125 | self.variable.emit_dropped() 126 | self.variable.run_dispose() 127 | end 128 | 129 | ---@param callback function 130 | ---@return Variable 131 | function Variable:on_dropped(callback) 132 | self.variable.on_dropped = callback 133 | return self 134 | end 135 | 136 | ---@param callback function 137 | ---@return Variable 138 | function Variable:on_error(callback) 139 | self.err_handler = nil 140 | self.variable.on_eror = function(_, err) 141 | callback(err) 142 | end 143 | return self 144 | end 145 | 146 | ---@param callback fun(value: any) 147 | ---@return function 148 | function Variable:subscribe(callback) 149 | local id = self.variable.on_changed:connect(function() 150 | callback(self:get()) 151 | end) 152 | return function() 153 | GObject.signal_handler_disconnect(self.variable, id) 154 | end 155 | end 156 | 157 | ---@param interval number 158 | ---@param exec string | string[] | function 159 | ---@param transform? fun(next: any, prev: any): any 160 | function Variable:poll(interval, exec, transform) 161 | if transform == nil then 162 | transform = function(next) 163 | return next 164 | end 165 | end 166 | self:stop_poll() 167 | self.poll_interval = interval 168 | self.poll_transform = transform 169 | 170 | if type(exec) == "function" then 171 | self.poll_fn = exec 172 | self.poll_exec = nil 173 | else 174 | self.poll_exec = exec 175 | self.poll_fn = nil 176 | end 177 | self:start_poll() 178 | return self 179 | end 180 | 181 | ---@param exec string | string[] 182 | ---@param transform? fun(next: any, prev: any): any 183 | function Variable:watch(exec, transform) 184 | if transform == nil then 185 | transform = function(next) 186 | return next 187 | end 188 | end 189 | self:stop_poll() 190 | self.watch_exec = exec 191 | self.watch_transform = transform 192 | self:start_watch() 193 | return self 194 | end 195 | 196 | ---@param object table | table[] 197 | ---@param sigOrFn string | fun(...): any 198 | ---@param callback fun(...): any 199 | ---@return Variable 200 | function Variable:observe(object, sigOrFn, callback) 201 | local f 202 | if type(sigOrFn) == "function" then 203 | f = sigOrFn 204 | elseif type(callback) == "function" then 205 | f = callback 206 | else 207 | f = function() 208 | return self:get() 209 | end 210 | end 211 | local set = function(...) 212 | self:set(f(...)) 213 | end 214 | 215 | if type(sigOrFn) == "string" then 216 | object["on_" .. sigOrFn]:connect(set) 217 | else 218 | for _, obj in ipairs(object) do 219 | obj[1]["on_" .. obj[2]]:connect(set) 220 | end 221 | end 222 | return self 223 | end 224 | 225 | ---@param deps Variable | (Binding | Variable)[] 226 | ---@param transform? fun(...): any 227 | ---@return Variable 228 | function Variable.derive(deps, transform) 229 | if type(transform) == "nil" then 230 | transform = function(...) 231 | return { ... } 232 | end 233 | end 234 | 235 | if getmetatable(deps) == Variable then 236 | local var = Variable.new(transform(deps:get())) 237 | deps:subscribe(function(v) 238 | var:set(transform(v)) 239 | end) 240 | return var 241 | end 242 | 243 | for i, var in ipairs(deps) do 244 | if getmetatable(var) == Variable then 245 | deps[i] = Binding.new(var) 246 | end 247 | end 248 | 249 | local update = function() 250 | local params = {} 251 | for _, binding in ipairs(deps) do 252 | table.insert(params, binding:get()) 253 | end 254 | return transform(table.unpack(params)) 255 | end 256 | 257 | local var = Variable.new(update()) 258 | 259 | local unsubs = {} 260 | for _, b in ipairs(deps) do 261 | table.insert(unsubs, b:subscribe(update)) 262 | end 263 | 264 | var.variable.on_dropped = function() 265 | for _, unsub in ipairs(unsubs) do 266 | var:set(unsub()) 267 | end 268 | end 269 | return var 270 | end 271 | 272 | return setmetatable(Variable, { 273 | __call = function(_, v) 274 | return Variable.new(v) 275 | end, 276 | }) 277 | -------------------------------------------------------------------------------- /lua/astal/widget.lua: -------------------------------------------------------------------------------- 1 | local lgi = require("lgi") 2 | local Astal = lgi.require("Astal", "0.1") 3 | local Gtk = lgi.require("Gtk", "3.0") 4 | local GObject = lgi.require("GObject", "2.0") 5 | local Binding = require("astal.binding") 6 | local Variable = require("astal.variable") 7 | local exec_async = require("astal.process").exec_async 8 | 9 | local function filter(tbl, fn) 10 | local copy = {} 11 | for key, value in pairs(tbl) do 12 | if fn(value, key) then 13 | if type(key) == "number" then 14 | table.insert(copy, value) 15 | else 16 | copy[key] = value 17 | end 18 | end 19 | end 20 | return copy 21 | end 22 | 23 | local function map(tbl, fn) 24 | local copy = {} 25 | for key, value in pairs(tbl) do 26 | copy[key] = fn(value) 27 | end 28 | return copy 29 | end 30 | 31 | local flatten 32 | flatten = function(tbl) 33 | local copy = {} 34 | for _, value in pairs(tbl) do 35 | if type(value) == "table" and getmetatable(value) == nil then 36 | for _, inner in pairs(flatten(value)) do 37 | table.insert(copy, inner) 38 | end 39 | else 40 | table.insert(copy, value) 41 | end 42 | end 43 | return copy 44 | end 45 | 46 | local function set_children(parent, children) 47 | children = map(flatten(children), function(item) 48 | if Gtk.Widget:is_type_of(item) then 49 | return item 50 | end 51 | return Gtk.Label({ 52 | visible = true, 53 | label = tostring(item), 54 | }) 55 | end) 56 | 57 | -- remove 58 | if Gtk.Bin:is_type_of(parent) then 59 | local rm = parent:get_child() 60 | if rm ~= nil then 61 | parent:remove(rm) 62 | end 63 | end 64 | 65 | -- FIXME: add rest of the edge cases like Stack 66 | if Astal.Box:is_type_of(parent) then 67 | parent:set_children(children) 68 | elseif Astal.CenterBox:is_type_of(parent) then 69 | parent.start_widget = children[1] 70 | parent.center_widget = children[2] 71 | parent.end_widget = children[3] 72 | elseif Astal.Overlay:is_type_of(parent) then 73 | parent:set_child(children[1]) 74 | children[1] = nil 75 | parent:set_overlays(children) 76 | elseif Gtk.Container:is_type_of(parent) then 77 | for _, child in pairs(children) do 78 | if Gtk.Widget:is_type_of(child) then 79 | parent:add(child) 80 | end 81 | end 82 | end 83 | end 84 | 85 | local function merge_bindings(array) 86 | local function get_values(...) 87 | local args = { ... } 88 | local i = 0 89 | return map(array, function(value) 90 | if getmetatable(value) == Binding then 91 | i = i + 1 92 | return args[i] 93 | else 94 | return value 95 | end 96 | end) 97 | end 98 | 99 | local bindings = filter(array, function(v) 100 | return getmetatable(v) == Binding 101 | end) 102 | 103 | if #bindings == 0 then 104 | return array 105 | end 106 | 107 | if #bindings == 1 then 108 | return bindings[1]:as(get_values) 109 | end 110 | 111 | return Variable.derive(bindings, get_values)() 112 | end 113 | 114 | local function astalify(ctor) 115 | function ctor:hook(object, signalOrCallback, callback) 116 | if type(object.subscribe) == "function" then 117 | local unsub = object.subscribe(function(...) 118 | signalOrCallback(self, ...) 119 | end) 120 | self.on_destroy = unsub 121 | return 122 | end 123 | local id = object["on_" .. signalOrCallback](function(_, ...) 124 | callback(self, ...) 125 | end) 126 | self.on_destroy = function() 127 | GObject.signal_handler_disconnect(object, id) 128 | end 129 | end 130 | 131 | function ctor:toggle_class_name(name, on) 132 | Astal.toggle_class_name(self, name, on) 133 | end 134 | 135 | return function(tbl) 136 | if tbl == nil then 137 | tbl = {} 138 | end 139 | 140 | local bindings = {} 141 | local setup = tbl.setup 142 | 143 | -- collect children 144 | local children = merge_bindings(flatten(filter(tbl, function(_, key) 145 | return type(key) == "number" 146 | end))) 147 | 148 | -- default visible to true 149 | if type(tbl.visible) ~= "boolean" then 150 | tbl.visible = true 151 | end 152 | 153 | -- filter props 154 | local props = filter(tbl, function(_, key) 155 | return type(key) == "string" and key ~= "setup" 156 | end) 157 | 158 | -- handle on_ handlers that are strings 159 | for prop, value in pairs(props) do 160 | if string.sub(prop, 0, 2) == "on" and type(value) ~= "function" then 161 | props[prop] = function() 162 | exec_async(value, print, print) 163 | end 164 | end 165 | end 166 | 167 | -- handle bindings 168 | for prop, value in pairs(props) do 169 | if getmetatable(value) == Binding then 170 | bindings[prop] = value 171 | props[prop] = value:get() 172 | end 173 | end 174 | 175 | -- construct, attach bindings, add children 176 | local widget = ctor() 177 | 178 | for prop, value in pairs(props) do 179 | widget[prop] = value 180 | end 181 | 182 | for prop, binding in pairs(bindings) do 183 | widget.on_destroy = binding:subscribe(function(v) 184 | widget[prop] = v 185 | end) 186 | end 187 | 188 | if getmetatable(children) == Binding then 189 | set_children(widget, children:get()) 190 | widget.on_destroy = children:subscribe(function(v) 191 | set_children(widget, v) 192 | end) 193 | else 194 | if #children > 0 then 195 | set_children(widget, children) 196 | end 197 | end 198 | 199 | if type(setup) == "function" then 200 | setup(widget) 201 | end 202 | 203 | return widget 204 | end 205 | end 206 | 207 | local Widget = { 208 | astalify = astalify, 209 | Box = astalify(Astal.Box), 210 | Button = astalify(Astal.Button), 211 | CenterBox = astalify(Astal.CenterBox), 212 | -- TODO: CircularProgress 213 | DrawingArea = astalify(Gtk.DrawingArea), 214 | Entry = astalify(Gtk.Entry), 215 | EventBox = astalify(Astal.EventBox), 216 | -- TODO: Fixed 217 | -- TODO: FlowBox 218 | Icon = astalify(Astal.Icon), 219 | Label = astalify(Gtk.Label), 220 | LevelBar = astalify(Astal.LevelBar), 221 | -- TODO: ListBox 222 | Overlay = astalify(Astal.Overlay), 223 | Revealer = astalify(Gtk.Revealer), 224 | Scrollable = astalify(Astal.Scrollable), 225 | Slider = astalify(Astal.Slider), 226 | -- TODO: Stack 227 | Switch = astalify(Gtk.Switch), 228 | Window = astalify(Astal.Window), 229 | } 230 | 231 | Gtk.Widget._attribute.css = { 232 | get = Astal.widget_get_css, 233 | set = Astal.widget_set_css, 234 | } 235 | 236 | Gtk.Widget._attribute.class_name = { 237 | get = function(self) 238 | local result = "" 239 | local strings = Astal.widget_set_class_names(self) 240 | for i, str in ipairs(strings) do 241 | result = result .. str 242 | if i < #strings then 243 | result = result .. " " 244 | end 245 | end 246 | return result 247 | end, 248 | set = function(self, class_name) 249 | local names = {} 250 | for word in class_name:gmatch("%S+") do 251 | table.insert(names, word) 252 | end 253 | Astal.widget_set_class_names(self, names) 254 | end, 255 | } 256 | 257 | Gtk.Widget._attribute.cursor = { 258 | get = Astal.widget_get_cursor, 259 | set = Astal.widget_set_cursor, 260 | } 261 | 262 | Gtk.Widget._attribute.click_through = { 263 | get = Astal.widget_get_click_through, 264 | set = Astal.widget_set_click_through, 265 | } 266 | 267 | Astal.Box._attribute.children = { 268 | get = Astal.Box.get_children, 269 | set = Astal.Box.set_children, 270 | } 271 | 272 | return setmetatable(Widget, { 273 | __call = function(_, ctor) 274 | return astalify(ctor) 275 | end, 276 | }) 277 | -------------------------------------------------------------------------------- /lua/stylua.toml: -------------------------------------------------------------------------------- 1 | indent_type = "Spaces" 2 | indent_width = 4 3 | column_width = 100 4 | -------------------------------------------------------------------------------- /lua/test.lua: -------------------------------------------------------------------------------- 1 | local App = require("astal.application") 2 | 3 | App:start({ 4 | instance_name = "test", 5 | main = function() 6 | App:quit(1) 7 | end, 8 | }) 9 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project( 2 | 'astal', 3 | 'vala', 4 | 'c', 5 | version: run_command('cat', join_paths(meson.project_source_root(), 'version')).stdout().strip(), 6 | meson_version: '>= 0.62.0', 7 | default_options: [ 8 | 'warning_level=2', 9 | 'werror=false', 10 | 'c_std=gnu11', 11 | ], 12 | ) 13 | 14 | prefix = get_option('prefix') 15 | libdir = get_option('prefix') / get_option('libdir') 16 | pkgdatadir = prefix / get_option('datadir') / 'astal' 17 | 18 | # math 19 | add_project_arguments(['-X', '-lm'], language: 'vala') 20 | 21 | assert( 22 | get_option('lib') or get_option('cli'), 23 | 'Either lib or cli option must be set to true.', 24 | ) 25 | 26 | if get_option('gjs') 27 | install_subdir('gjs', install_dir: pkgdatadir) 28 | endif 29 | 30 | subdir('src') 31 | -------------------------------------------------------------------------------- /meson_options.txt: -------------------------------------------------------------------------------- 1 | option( 2 | 'lib', 3 | type: 'boolean', 4 | value: true, 5 | ) 6 | 7 | option( 8 | 'cli', 9 | type: 'boolean', 10 | value: true, 11 | ) 12 | 13 | option( 14 | 'gjs', 15 | type: 'boolean', 16 | value: true, 17 | ) 18 | -------------------------------------------------------------------------------- /src/astal.vala: -------------------------------------------------------------------------------- 1 | namespace Astal { 2 | [DBus (name="io.Astal.Application")] 3 | public class Application : Gtk.Application { 4 | private List css_providers = new List(); 5 | private SocketService service; 6 | private DBusConnection conn; 7 | private string _instance_name; 8 | 9 | public string socket_path { get; private set; } 10 | 11 | [DBus (visible=false)] 12 | public string instance_name { 13 | get { return _instance_name; } 14 | set { 15 | application_id = "io.Astal." + value; 16 | _instance_name = value; 17 | } 18 | } 19 | 20 | [DBus (visible=false)] 21 | public List windows { 22 | get { return get_windows(); } 23 | } 24 | 25 | [DBus (visible=false)] 26 | public Gtk.Settings settings { 27 | get { return Gtk.Settings.get_default(); } 28 | } 29 | 30 | [DBus (visible=false)] 31 | public Gdk.Screen screen { 32 | get { return Gdk.Screen.get_default(); } 33 | } 34 | 35 | [DBus (visible=false)] 36 | public string gtk_theme { 37 | owned get { return settings.gtk_theme_name; } 38 | set { settings.gtk_theme_name = value; } 39 | } 40 | 41 | [DBus (visible=false)] 42 | public string icon_theme { 43 | owned get { return settings.gtk_icon_theme_name; } 44 | set { settings.gtk_icon_theme_name = value; } 45 | } 46 | 47 | [DBus (visible=false)] 48 | public string cursor_theme { 49 | owned get { return settings.gtk_cursor_theme_name; } 50 | set { settings.gtk_cursor_theme_name = value; } 51 | } 52 | 53 | [DBus (visible=false)] 54 | public void reset_css() { 55 | foreach(var provider in css_providers) { 56 | Gtk.StyleContext.remove_provider_for_screen(screen, provider); 57 | } 58 | css_providers = new List(); 59 | } 60 | 61 | public void inspector() throws DBusError, IOError { 62 | Gtk.Window.set_interactive_debugging(true); 63 | } 64 | 65 | [DBus (visible=false)] 66 | public Gtk.Window? get_window(string name) { 67 | foreach(var win in windows) { 68 | if (win.name == name) 69 | return win; 70 | } 71 | 72 | critical("no window with name \"%s\"".printf(name)); 73 | return null; 74 | } 75 | 76 | public void toggle_window(string window) throws DBusError, IOError { 77 | var win = get_window(window); 78 | if (win != null) { 79 | win.visible = !win.visible; 80 | } else { 81 | throw new IOError.FAILED("window not found"); 82 | } 83 | } 84 | 85 | [DBus (visible=false)] 86 | public void apply_css(string style, bool reset = false) { 87 | var provider = new Gtk.CssProvider(); 88 | 89 | if (reset) 90 | reset_css(); 91 | 92 | try { 93 | if (FileUtils.test(style, FileTest.EXISTS)) 94 | provider.load_from_path(style); 95 | else 96 | provider.load_from_data(style); 97 | } catch (Error err) { 98 | critical(err.message); 99 | } 100 | 101 | Gtk.StyleContext.add_provider_for_screen( 102 | screen, provider, Gtk.STYLE_PROVIDER_PRIORITY_USER); 103 | 104 | css_providers.append(provider); 105 | } 106 | 107 | [DBus (visible=false)] 108 | public void add_icons(string? path) { 109 | if (path != null) 110 | Gtk.IconTheme.get_default().prepend_search_path(path); 111 | } 112 | 113 | private async void _socket_request(SocketConnection conn) { 114 | string message = yield read_sock(conn); 115 | request(message != null ? message.strip() : "", conn); 116 | } 117 | 118 | [DBus (visible=false)] 119 | public virtual void request(string msg, SocketConnection conn) { 120 | write_sock.begin(conn, @"missing response implementation on $application_id"); 121 | } 122 | 123 | /** 124 | * should be called before `run()` 125 | * the return value indicates if instance is already running 126 | */ 127 | [DBus (visible=false)] 128 | public bool acquire_socket() { 129 | foreach (var instance in get_instances()) { 130 | if (instance == instance_name) { 131 | return false; 132 | } 133 | } 134 | 135 | var rundir = GLib.Environment.get_user_runtime_dir(); 136 | socket_path = @"$rundir/$instance_name.sock"; 137 | 138 | if (FileUtils.test(socket_path, GLib.FileTest.EXISTS)) { 139 | try { 140 | File.new_for_path(socket_path).delete(null); 141 | } catch (Error err) { 142 | critical(err.message); 143 | } 144 | } 145 | 146 | try { 147 | service = new SocketService(); 148 | service.add_address( 149 | new UnixSocketAddress(socket_path), 150 | SocketType.STREAM, 151 | SocketProtocol.DEFAULT, 152 | null, 153 | null); 154 | 155 | service.incoming.connect((conn) => { 156 | _socket_request.begin(conn, (_, res) => _socket_request.end(res)); 157 | return false; 158 | }); 159 | 160 | Bus.own_name( 161 | BusType.SESSION, 162 | "io.Astal." + instance_name, 163 | BusNameOwnerFlags.NONE, 164 | (conn) => { 165 | try { 166 | this.conn = conn; 167 | conn.register_object("/io/Astal/Application", this); 168 | } catch (Error err) { 169 | critical(err.message); 170 | } 171 | }, 172 | () => {}, 173 | () => {}); 174 | 175 | info("socket acquired: %s\n", socket_path); 176 | return true; 177 | } catch (Error err) { 178 | critical("could not acquire socket %s\n", application_id); 179 | critical(err.message); 180 | return false; 181 | } 182 | } 183 | 184 | public string message(string? msg) throws DBusError, IOError { 185 | var rundir = GLib.Environment.get_user_runtime_dir(); 186 | var socket_path = @"$rundir/$instance_name.sock"; 187 | var client = new SocketClient(); 188 | 189 | if (msg == null) 190 | msg = ""; 191 | 192 | try { 193 | var conn = client.connect(new UnixSocketAddress(socket_path), null); 194 | conn.output_stream.write(msg.concat("\x04").data); 195 | 196 | var stream = new DataInputStream(conn.input_stream); 197 | return stream.read_upto("\x04", -1, null, null); 198 | } catch (Error err) { 199 | printerr(err.message); 200 | return ""; 201 | } 202 | } 203 | 204 | public new void quit() throws DBusError, IOError { 205 | if (service != null) { 206 | if (FileUtils.test(socket_path, GLib.FileTest.EXISTS)){ 207 | try { 208 | File.new_for_path(socket_path).delete(null); 209 | } catch (Error err) { 210 | warning(err.message); 211 | } 212 | } 213 | } 214 | 215 | base.quit(); 216 | } 217 | 218 | construct { 219 | if (instance_name == null) 220 | instance_name = "astal"; 221 | 222 | shutdown.connect(() => { try { quit(); } catch(Error err) {} }); 223 | Unix.signal_add(1, () => { try { quit(); } catch(Error err) {} }, Priority.HIGH); 224 | Unix.signal_add(2, () => { try { quit(); } catch(Error err) {} }, Priority.HIGH); 225 | Unix.signal_add(15, () => { try { quit(); } catch(Error err) {} }, Priority.HIGH); 226 | } 227 | 228 | public static List get_instances() { 229 | var list = new List(); 230 | var prefix = "io.Astal."; 231 | 232 | try { 233 | DBusImpl dbus = Bus.get_proxy_sync( 234 | BusType.SESSION, 235 | "org.freedesktop.DBus", 236 | "/org/freedesktop/DBus" 237 | ); 238 | 239 | foreach (var busname in dbus.list_names()) { 240 | if (busname.has_prefix(prefix)) 241 | list.append(busname.replace(prefix, "")); 242 | } 243 | } catch (Error err) { 244 | critical(err.message); 245 | } 246 | 247 | return list; 248 | } 249 | 250 | public static void quit_instance(string instance) { 251 | try { 252 | IApplication proxy = Bus.get_proxy_sync( 253 | BusType.SESSION, 254 | "io.Astal." + instance, 255 | "/io/Astal/Application" 256 | ); 257 | 258 | proxy.quit(); 259 | } catch (Error err) { 260 | critical(err.message); 261 | } 262 | } 263 | 264 | public static void open_inspector(string instance) { 265 | try { 266 | IApplication proxy = Bus.get_proxy_sync( 267 | BusType.SESSION, 268 | "io.Astal." + instance, 269 | "/io/Astal/Application" 270 | ); 271 | 272 | proxy.inspector(); 273 | } catch (Error err) { 274 | critical(err.message); 275 | } 276 | } 277 | 278 | public static void toggle_window_by_name(string instance, string window) { 279 | try { 280 | IApplication proxy = Bus.get_proxy_sync( 281 | BusType.SESSION, 282 | "io.Astal." + instance, 283 | "/io/Astal/Application" 284 | ); 285 | 286 | proxy.toggle_window(window); 287 | } catch (Error err) { 288 | critical(err.message); 289 | } 290 | } 291 | 292 | public static string send_message(string instance_name, string msg) { 293 | var rundir = GLib.Environment.get_user_runtime_dir(); 294 | var socket_path = @"$rundir/$instance_name.sock"; 295 | var client = new SocketClient(); 296 | 297 | try { 298 | var conn = client.connect(new UnixSocketAddress(socket_path), null); 299 | conn.output_stream.write(msg.concat("\x04").data); 300 | 301 | var stream = new DataInputStream(conn.input_stream); 302 | return stream.read_upto("\x04", -1, null, null); 303 | } catch (Error err) { 304 | printerr(err.message); 305 | return ""; 306 | } 307 | } 308 | } 309 | 310 | [DBus (name="org.freedesktop.DBus")] 311 | private interface DBusImpl : DBusProxy { 312 | public abstract string[] list_names() throws GLib.Error; 313 | } 314 | 315 | [DBus (name="io.Astal.Application")] 316 | private interface IApplication : DBusProxy { 317 | public abstract void quit() throws GLib.Error; 318 | public abstract void inspector() throws GLib.Error; 319 | public abstract void toggle_window(string window) throws GLib.Error; 320 | public abstract string message(string window) throws GLib.Error; 321 | } 322 | 323 | public async string read_sock(SocketConnection conn) { 324 | try { 325 | var stream = new DataInputStream(conn.input_stream); 326 | return yield stream.read_upto_async("\x04", -1, Priority.DEFAULT, null, null); 327 | } catch (Error err) { 328 | critical(err.message); 329 | return err.message; 330 | } 331 | } 332 | 333 | public async void write_sock(SocketConnection conn, string response) { 334 | try { 335 | yield conn.output_stream.write_async( 336 | response.concat("\x04").data, 337 | Priority.DEFAULT); 338 | } catch (Error err) { 339 | critical(err.message); 340 | } 341 | } 342 | } 343 | -------------------------------------------------------------------------------- /src/cli.vala: -------------------------------------------------------------------------------- 1 | private static bool version; 2 | private static bool help; 3 | private static bool list; 4 | private static bool quit; 5 | private static bool inspector; 6 | private static string? toggle_window; 7 | private static string? instance_name; 8 | 9 | private const GLib.OptionEntry[] options = { 10 | { "version", 'v', OptionFlags.NONE, OptionArg.NONE, ref version, null, null }, 11 | { "help", 'h', OptionFlags.NONE, OptionArg.NONE, ref help, null, null }, 12 | { "list", 'l', OptionFlags.NONE, OptionArg.NONE, ref list, null, null }, 13 | { "quit", 'q', OptionFlags.NONE, OptionArg.NONE, ref quit, null, null }, 14 | { "quit", 'q', OptionFlags.NONE, OptionArg.NONE, ref quit, null, null }, 15 | { "inspector", 'I', OptionFlags.NONE, OptionArg.NONE, ref inspector, null, null }, 16 | { "toggle-window", 't', OptionFlags.NONE, OptionArg.STRING, ref toggle_window, null, null }, 17 | { "instance", 'i', OptionFlags.NONE, OptionArg.STRING, ref instance_name, null, null }, 18 | { null }, 19 | }; 20 | 21 | int main(string[] argv) { 22 | try { 23 | var opts = new OptionContext(); 24 | opts.add_main_entries(options, null); 25 | opts.set_help_enabled(false); 26 | opts.set_ignore_unknown_options(false); 27 | opts.parse(ref argv); 28 | } catch (OptionError err) { 29 | printerr (err.message); 30 | return 1; 31 | } 32 | 33 | if (help) { 34 | print("Client for Astal.Application instances\n\n"); 35 | print("Usage:\n"); 36 | print(" %s [flags] message\n\n", argv[0]); 37 | print("Flags:\n"); 38 | print(" -h, --help Print this help and exit\n"); 39 | print(" -v, --version Print version number and exit\n"); 40 | print(" -l, --list List running Astal instances and exit\n"); 41 | print(" -q, --quit Quit an Astal.Application instance\n"); 42 | print(" -i, --instance Instance name of the Astal instance\n"); 43 | print(" -I, --inspector Open up Gtk debug tool\n"); 44 | print(" -t, --toggle-window Show or hide a window\n"); 45 | return 0; 46 | } 47 | 48 | if (version) { 49 | print(Astal.VERSION); 50 | return 0; 51 | } 52 | 53 | if (instance_name == null) 54 | instance_name = "astal"; 55 | 56 | if (list) { 57 | foreach (var name in Astal.Application.get_instances()) 58 | stdout.printf("%s\n", name); 59 | 60 | return 0; 61 | } 62 | 63 | if (quit) { 64 | Astal.Application.quit_instance(instance_name); 65 | return 0; 66 | } 67 | 68 | if (inspector) { 69 | Astal.Application.open_inspector(instance_name); 70 | return 0; 71 | } 72 | 73 | if (toggle_window != null) { 74 | Astal.Application.toggle_window_by_name(instance_name, toggle_window); 75 | return 0; 76 | } 77 | 78 | var request = ""; 79 | for (var i = 1; i < argv.length; ++i) { 80 | request = request.concat(" ", argv[i]); 81 | } 82 | 83 | var reply = Astal.Application.send_message(instance_name, request); 84 | print("%s\n", reply); 85 | 86 | return 0; 87 | } 88 | -------------------------------------------------------------------------------- /src/config.vala.in: -------------------------------------------------------------------------------- 1 | namespace Astal { 2 | public const int MAJOR_VERSION = @MAJOR_VERSION@; 3 | public const int MINOR_VERSION = @MINOR_VERSION@; 4 | public const int MICRO_VERSION = @MICRO_VERSION@; 5 | public const string VERSION = "@VERSION@"; 6 | } 7 | -------------------------------------------------------------------------------- /src/file.vala: -------------------------------------------------------------------------------- 1 | namespace Astal { 2 | public string read_file(string path) { 3 | var str = ""; 4 | try { 5 | FileUtils.get_contents(path, out str, null); 6 | } catch (Error error) { 7 | critical(error.message); 8 | } 9 | return str; 10 | } 11 | 12 | public async string read_file_async(string path) throws Error { 13 | uint8[] content; 14 | yield File.new_for_path(path).load_contents_async(null, out content, null); 15 | return (string)content; 16 | } 17 | 18 | public void write_file(string path, string content) { 19 | try { 20 | FileUtils.set_contents(path, content); 21 | } catch (Error error) { 22 | critical(error.message); 23 | } 24 | } 25 | 26 | public async void write_file_async(string path, string content) throws Error { 27 | yield File.new_for_path(path).replace_contents_async( 28 | content.data, 29 | null, 30 | false, 31 | GLib.FileCreateFlags.REPLACE_DESTINATION, 32 | null, 33 | null); 34 | } 35 | 36 | public FileMonitor? monitor_file(string path, Closure callback) { 37 | try { 38 | var file = File.new_for_path(path); 39 | var mon = file.monitor(GLib.FileMonitorFlags.NONE); 40 | 41 | mon.changed.connect((file, _file, event) => { 42 | var f = Value(Type.STRING); 43 | var e = Value(Type.INT); 44 | var ret = Value(Type.POINTER); 45 | 46 | f.set_string(file.get_path()); 47 | e.set_int(event); 48 | 49 | callback.invoke(ref ret, { f, e }); 50 | }); 51 | 52 | if (FileUtils.test(path, FileTest.IS_DIR)) { 53 | var enumerator = file.enumerate_children("standard::*", 54 | FileQueryInfoFlags.NONE, null); 55 | 56 | var i = enumerator.next_file(null); 57 | while (i != null) { 58 | if (i.get_file_type() == FileType.DIRECTORY) { 59 | var filepath = file.get_child(i.get_name()).get_path(); 60 | if (filepath != null) { 61 | var m = monitor_file(path, callback); 62 | mon.notify["cancelled"].connect(() => { 63 | m.cancel(); 64 | }); 65 | } 66 | } 67 | i = enumerator.next_file(null); 68 | } 69 | } 70 | 71 | mon.ref(); 72 | mon.notify["cancelled"].connect(() => { 73 | mon.unref(); 74 | }); 75 | return mon; 76 | } catch (Error error) { 77 | critical(error.message); 78 | return null; 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/meson.build: -------------------------------------------------------------------------------- 1 | version_split = meson.project_version().split('.') 2 | api_version = version_split[0] + '.' + version_split[1] 3 | gir = 'Astal-' + api_version + '.gir' 4 | typelib = 'Astal-' + api_version + '.typelib' 5 | so = 'libastal.so.' + meson.project_version() 6 | 7 | config = configure_file( 8 | input: 'config.vala.in', 9 | output: 'config.vala', 10 | configuration: { 11 | 'VERSION': meson.project_version(), 12 | 'MAJOR_VERSION': version_split[0], 13 | 'MINOR_VERSION': version_split[1], 14 | 'MICRO_VERSION': version_split[2], 15 | }, 16 | ) 17 | 18 | deps = [ 19 | dependency('glib-2.0'), 20 | dependency('gio-unix-2.0'), 21 | dependency('gobject-2.0'), 22 | dependency('gio-2.0'), 23 | dependency('gtk+-3.0'), 24 | dependency('gdk-pixbuf-2.0'), 25 | dependency('gtk-layer-shell-0'), 26 | ] 27 | 28 | sources = [ 29 | config, 30 | 'widget/box.vala', 31 | 'widget/button.vala', 32 | 'widget/centerbox.vala', 33 | # 'widget/circularprogress.vala', # TODO: math lib -X -lm 34 | 'widget/eventbox.vala', 35 | 'widget/icon.vala', 36 | 'widget/label.vala', 37 | 'widget/levelbar.vala', 38 | 'widget/overlay.vala', 39 | 'widget/scrollable.vala', 40 | 'widget/slider.vala', 41 | 'widget/widget.vala', 42 | 'widget/window.vala', 43 | 'astal.vala', 44 | 'file.vala', 45 | 'process.vala', 46 | 'time.vala', 47 | 'variable.vala', 48 | ] 49 | 50 | if get_option('lib') 51 | lib = library( 52 | meson.project_name(), 53 | sources, 54 | dependencies: deps, 55 | vala_header: meson.project_name() + '.h', 56 | vala_vapi: meson.project_name() + '-' + api_version + '.vapi', 57 | vala_gir: gir, 58 | version: meson.project_version(), 59 | install: true, 60 | install_dir: [true, true, true, true], 61 | ) 62 | 63 | import('pkgconfig').generate( 64 | lib, 65 | name: meson.project_name(), 66 | filebase: meson.project_name() + '-' + api_version, 67 | version: meson.project_version(), 68 | subdirs: meson.project_name(), 69 | requires: deps, 70 | install_dir: libdir / 'pkgconfig', 71 | variables: { 72 | 'gjs': pkgdatadir / 'gjs', 73 | }, 74 | ) 75 | 76 | custom_target( 77 | typelib, 78 | command: [ 79 | find_program('g-ir-compiler'), 80 | '--output', '@OUTPUT@', 81 | '--shared-library', libdir / '@PLAINNAME@', 82 | meson.current_build_dir() / gir, 83 | ], 84 | input: lib, 85 | output: typelib, 86 | depends: lib, 87 | install: true, 88 | install_dir: libdir / 'girepository-1.0', 89 | ) 90 | endif 91 | 92 | if get_option('cli') 93 | executable( 94 | meson.project_name(), 95 | ['cli.vala', sources], 96 | dependencies: deps, 97 | install: true, 98 | ) 99 | endif 100 | -------------------------------------------------------------------------------- /src/process.vala: -------------------------------------------------------------------------------- 1 | public class Astal.Process : Object { 2 | private void read_stream(DataInputStream stream, bool err) { 3 | stream.read_line_utf8_async.begin(Priority.DEFAULT, null, (_, res) => { 4 | try { 5 | var output = stream.read_line_utf8_async.end(res); 6 | if (output != null) { 7 | if (err) 8 | stdout(output.strip()); 9 | else 10 | stderr(output.strip()); 11 | 12 | read_stream(stream, err); 13 | } 14 | } catch (Error err) { 15 | printerr("%s\n", err.message); 16 | } 17 | }); 18 | } 19 | 20 | private DataInputStream out_stream; 21 | private DataInputStream err_stream; 22 | private DataOutputStream in_stream; 23 | private Subprocess process; 24 | public string[] argv { construct; get; } 25 | 26 | public signal void stdout (string out); 27 | public signal void stderr (string err); 28 | 29 | public void kill() { 30 | process.force_exit(); 31 | } 32 | 33 | public void signal(int signal_num) { 34 | process.send_signal(signal_num); 35 | } 36 | 37 | public void write(string in) throws Error { 38 | in_stream.put_string(in); 39 | } 40 | 41 | public void write_async(string in) { 42 | in_stream.write_all_async.begin( 43 | in.data, 44 | Priority.DEFAULT, null, (_, res) => { 45 | try { 46 | in_stream.write_all_async.end(res, null); 47 | } catch (Error err) { 48 | printerr("%s\n", err.message); 49 | } 50 | } 51 | ); 52 | } 53 | 54 | public Process.subprocessv(string[] cmd) throws Error { 55 | Object(argv: cmd); 56 | process = new Subprocess.newv(cmd, 57 | SubprocessFlags.STDIN_PIPE | 58 | SubprocessFlags.STDERR_PIPE | 59 | SubprocessFlags.STDOUT_PIPE 60 | ); 61 | out_stream = new DataInputStream(process.get_stdout_pipe()); 62 | err_stream = new DataInputStream(process.get_stderr_pipe()); 63 | in_stream = new DataOutputStream(process.get_stdin_pipe()); 64 | read_stream(out_stream, true); 65 | read_stream(err_stream, false); 66 | } 67 | 68 | public static Process subprocess(string cmd) throws Error { 69 | string[] argv; 70 | Shell.parse_argv(cmd, out argv); 71 | return new Process.subprocessv(argv); 72 | } 73 | 74 | public static string execv(string[] cmd) throws Error { 75 | var process = new Subprocess.newv( 76 | cmd, 77 | SubprocessFlags.STDERR_PIPE | 78 | SubprocessFlags.STDOUT_PIPE 79 | ); 80 | 81 | string err_str, out_str; 82 | process.communicate_utf8(null, null, out out_str, out err_str); 83 | var success = process.get_successful(); 84 | process.dispose(); 85 | if (success) 86 | return out_str.strip(); 87 | else 88 | throw new IOError.FAILED(err_str.strip()); 89 | } 90 | 91 | public static string exec(string cmd) throws Error { 92 | string[] argv; 93 | Shell.parse_argv(cmd, out argv); 94 | return Process.execv(argv); 95 | } 96 | 97 | public static async string exec_asyncv(string[] cmd) throws Error { 98 | var process = new Subprocess.newv( 99 | cmd, 100 | SubprocessFlags.STDERR_PIPE | 101 | SubprocessFlags.STDOUT_PIPE 102 | ); 103 | 104 | string err_str, out_str; 105 | yield process.communicate_utf8_async(null, null, out out_str, out err_str); 106 | var success = process.get_successful(); 107 | process.dispose(); 108 | if (success) 109 | return out_str.strip(); 110 | else 111 | throw new IOError.FAILED(err_str.strip()); 112 | } 113 | 114 | public static async string exec_async(string cmd) throws Error { 115 | string[] argv; 116 | Shell.parse_argv(cmd, out argv); 117 | return yield exec_asyncv(argv); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/time.vala: -------------------------------------------------------------------------------- 1 | namespace Astal { 2 | public class Time : Object { 3 | public signal void now (); 4 | public signal void cancelled (); 5 | private Cancellable cancellable; 6 | private uint timeout_id; 7 | private bool fulfilled = false; 8 | 9 | construct { 10 | cancellable = new Cancellable(); 11 | cancellable.cancelled.connect(() => { 12 | if (!fulfilled) { 13 | Source.remove(timeout_id); 14 | cancelled(); 15 | dispose(); 16 | } 17 | }); 18 | } 19 | 20 | private void connect_closure(Closure? closure) { 21 | if (closure == null) 22 | return; 23 | 24 | now.connect(() => { 25 | Value ret = Value(Type.POINTER); // void 26 | closure.invoke(ref ret, {}); 27 | }); 28 | } 29 | 30 | public Time.interval_prio(uint interval, int prio = Priority.DEFAULT, Closure? fn) { 31 | connect_closure(fn); 32 | Idle.add_once(() => now()); 33 | timeout_id = Timeout.add(interval, () => { 34 | now(); 35 | return Source.CONTINUE; 36 | }, prio); 37 | } 38 | 39 | public Time.timeout_prio(uint timeout, int prio = Priority.DEFAULT, Closure? fn) { 40 | connect_closure(fn); 41 | timeout_id = Timeout.add(timeout, () => { 42 | now(); 43 | fulfilled = true; 44 | return Source.REMOVE; 45 | }, prio); 46 | } 47 | 48 | public Time.idle_prio(int prio = Priority.DEFAULT_IDLE, Closure? fn) { 49 | connect_closure(fn); 50 | timeout_id = Idle.add(() => { 51 | now(); 52 | fulfilled = true; 53 | return Source.REMOVE; 54 | }, prio); 55 | } 56 | 57 | public static Time interval(uint interval, Closure? fn) { 58 | return new Time.interval_prio(interval, Priority.DEFAULT, fn); 59 | } 60 | 61 | public static Time timeout(uint timeout, Closure? fn) { 62 | return new Time.timeout_prio(timeout, Priority.DEFAULT, fn); 63 | } 64 | 65 | public static Time idle(Closure? fn) { 66 | return new Time.idle_prio(Priority.DEFAULT_IDLE, fn); 67 | } 68 | 69 | public void cancel() { 70 | cancellable.cancel(); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/variable.vala: -------------------------------------------------------------------------------- 1 | namespace Astal { 2 | public class VariableBase : Object { 3 | public signal void changed (); 4 | public signal void dropped (); 5 | public signal void error (string err); 6 | 7 | // lua-lgi crashes when using its emitting mechanism 8 | public void emit_changed() { changed(); } 9 | public void emit_dropped() { dropped(); } 10 | public void emit_error(string err) { this.error(err); } 11 | 12 | ~VariableBase() { 13 | dropped(); 14 | } 15 | } 16 | 17 | public class Variable : VariableBase { 18 | public Value value { owned get; set; } 19 | 20 | private uint poll_id = 0; 21 | private Process? watch_proc; 22 | 23 | private uint poll_interval { get; set; default = 1000; } 24 | private string[] poll_exec { get; set; } 25 | private Closure? poll_transform { get; set; } 26 | private Closure? poll_fn { get; set; } 27 | 28 | private Closure? watch_transform { get; set; } 29 | private string[] watch_exec { get; set; } 30 | 31 | public Variable(Value init) { 32 | Object(value: init); 33 | } 34 | 35 | public Variable poll( 36 | uint interval, 37 | string exec, 38 | Closure? transform 39 | ) throws Error { 40 | string[] argv; 41 | Shell.parse_argv(exec, out argv); 42 | return pollv(interval, argv, transform); 43 | } 44 | 45 | public Variable pollv( 46 | uint interval, 47 | string[] execv, 48 | Closure? transform 49 | ) throws Error { 50 | if (is_polling()) 51 | stop_poll(); 52 | 53 | poll_interval = interval; 54 | poll_exec = execv; 55 | poll_transform = transform; 56 | poll_fn = null; 57 | start_poll(); 58 | return this; 59 | } 60 | 61 | public Variable pollfn( 62 | uint interval, 63 | Closure fn 64 | ) throws Error { 65 | if (is_polling()) 66 | stop_poll(); 67 | 68 | poll_interval = interval; 69 | poll_fn = fn; 70 | poll_exec = null; 71 | start_poll(); 72 | return this; 73 | } 74 | 75 | public Variable watch( 76 | string exec, 77 | Closure? transform 78 | ) throws Error { 79 | string[] argv; 80 | Shell.parse_argv(exec, out argv); 81 | return watchv(argv, transform); 82 | } 83 | 84 | public Variable watchv( 85 | string[] execv, 86 | Closure? transform 87 | ) throws Error { 88 | if (is_watching()) 89 | stop_watch(); 90 | 91 | watch_exec = execv; 92 | watch_transform = transform; 93 | start_watch(); 94 | return this; 95 | } 96 | 97 | construct { 98 | notify["value"].connect(() => changed()); 99 | dropped.connect(() => { 100 | if (is_polling()) 101 | stop_poll(); 102 | 103 | if (is_watching()) 104 | stop_watch(); 105 | }); 106 | } 107 | 108 | private void set_closure(string val, Closure? transform) { 109 | if (transform != null) { 110 | var str = Value(typeof(string)); 111 | str.set_string(val); 112 | 113 | var ret_val = Value(this.value.type()); 114 | transform.invoke(ref ret_val, { str, this.value }); 115 | this.value = ret_val; 116 | } 117 | else { 118 | if (this.value.type() == Type.STRING && this.value.get_string() == val) 119 | return; 120 | 121 | var str = Value(typeof(string)); 122 | str.set_string(val); 123 | this.value = str; 124 | } 125 | } 126 | 127 | private void set_fn() { 128 | var ret_val = Value(this.value.type()); 129 | poll_fn.invoke(ref ret_val, { this.value }); 130 | this.value = ret_val; 131 | } 132 | 133 | public void start_poll() throws Error { 134 | return_if_fail(poll_id == 0); 135 | 136 | if (poll_fn != null) { 137 | set_fn(); 138 | poll_id = Timeout.add(poll_interval, () => { 139 | set_fn(); 140 | return Source.CONTINUE; 141 | }, Priority.DEFAULT); 142 | } 143 | if (poll_exec != null) { 144 | Process.exec_asyncv.begin(poll_exec, (_, res) => { 145 | try { 146 | var str = Process.exec_asyncv.end(res); 147 | set_closure(str, poll_transform); 148 | } catch (Error err) { 149 | this.error(err.message); 150 | } 151 | }); 152 | poll_id = Timeout.add(poll_interval, () => { 153 | Process.exec_asyncv.begin(poll_exec, (_, res) => { 154 | try { 155 | var str = Process.exec_asyncv.end(res); 156 | set_closure(str, poll_transform); 157 | } catch (Error err) { 158 | this.error(err.message); 159 | Source.remove(poll_id); 160 | poll_id = 0; 161 | } 162 | }); 163 | return Source.CONTINUE; 164 | }, Priority.DEFAULT); 165 | } 166 | } 167 | 168 | public void start_watch() throws Error { 169 | return_if_fail(watch_proc == null); 170 | return_if_fail(watch_exec != null); 171 | 172 | watch_proc = new Process.subprocessv(watch_exec); 173 | watch_proc.stdout.connect((str) => set_closure(str, watch_transform)); 174 | watch_proc.stderr.connect((str) => this.error(str)); 175 | } 176 | 177 | public void stop_poll() { 178 | return_if_fail(poll_id != 0); 179 | Source.remove(poll_id); 180 | poll_id = 0; 181 | } 182 | 183 | public void stop_watch() { 184 | return_if_fail(watch_proc != null); 185 | watch_proc.kill(); 186 | watch_proc = null; 187 | } 188 | 189 | public bool is_polling() { return poll_id > 0; } 190 | public bool is_watching() { return watch_proc != null; } 191 | 192 | ~Variable() { 193 | dropped(); 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/widget/box.vala: -------------------------------------------------------------------------------- 1 | namespace Astal { 2 | public class Box : Gtk.Box { 3 | [CCode (notify = false)] 4 | public bool vertical { 5 | get { return orientation == Gtk.Orientation.VERTICAL; } 6 | set { orientation = value ? Gtk.Orientation.VERTICAL : Gtk.Orientation.HORIZONTAL; } 7 | } 8 | 9 | /** 10 | * wether to implicity destroy previous children when setting them 11 | */ 12 | public bool implicit_destroy { get; set; default = true; } 13 | 14 | public List children { 15 | set { _set_children(value); } 16 | owned get { return get_children(); } 17 | } 18 | 19 | public new Gtk.Widget child { 20 | owned get { return _get_child(); } 21 | set { _set_child(value); } 22 | } 23 | 24 | construct { 25 | notify["orientation"].connect(() => { 26 | notify_property("vertical"); 27 | }); 28 | } 29 | 30 | private void _set_child(Gtk.Widget child) { 31 | var list = new List(); 32 | list.append(child); 33 | _set_children(list); 34 | } 35 | 36 | private Gtk.Widget? _get_child() { 37 | foreach(var child in get_children()) 38 | return child; 39 | 40 | return null; 41 | } 42 | 43 | private void _set_children(List arr) { 44 | foreach(var child in get_children()) { 45 | if (implicit_destroy && arr.find(child).length() == 0) 46 | child.destroy(); 47 | else 48 | remove(child); 49 | } 50 | 51 | foreach(var child in arr) 52 | add(child); 53 | } 54 | 55 | public Box(bool vertical, List children) { 56 | this.vertical = vertical; 57 | _set_children(children); 58 | } 59 | 60 | public Box.newh(List children) { 61 | this.vertical = false; 62 | _set_children(children); 63 | } 64 | 65 | public Box.newv(List children) { 66 | this.vertical = true; 67 | _set_children(children); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/widget/button.vala: -------------------------------------------------------------------------------- 1 | namespace Astal { 2 | public class Button : Gtk.Button { 3 | public signal void hover (HoverEvent event); 4 | public signal void hover_lost (HoverEvent event); 5 | public signal void click (ClickEvent event); 6 | public signal void click_release (ClickEvent event); 7 | public signal void scroll (ScrollEvent event); 8 | 9 | construct { 10 | add_events(Gdk.EventMask.SCROLL_MASK); 11 | add_events(Gdk.EventMask.SMOOTH_SCROLL_MASK); 12 | 13 | enter_notify_event.connect((self, event) => { 14 | hover(HoverEvent(event) { lost = false }); 15 | }); 16 | 17 | leave_notify_event.connect((self, event) => { 18 | hover_lost(HoverEvent(event) { lost = true }); 19 | }); 20 | 21 | button_press_event.connect((event) => { 22 | click(ClickEvent(event) { release = false }); 23 | }); 24 | 25 | button_release_event.connect((event) => { 26 | click_release(ClickEvent(event) { release = true }); 27 | }); 28 | 29 | scroll_event.connect((event) => { 30 | scroll(ScrollEvent(event)); 31 | }); 32 | } 33 | } 34 | 35 | public enum MouseButton { 36 | PRIMARY = 0, 37 | MIDDLE, 38 | SECONDARY, 39 | BACK, 40 | FORWARD, 41 | } 42 | 43 | // these structs are here because gjs converts every event 44 | // into a union Gdk.Event, which cannot be destructured 45 | // and are not as convinent to work with as a struct 46 | public struct ClickEvent { 47 | bool release; 48 | uint time; 49 | double x; 50 | double y; 51 | Gdk.ModifierType modifier; 52 | MouseButton button; 53 | 54 | public ClickEvent(Gdk.EventButton event) { 55 | this.time = event.time; 56 | this.x = event.x; 57 | this.y = event.y; 58 | this.button = (MouseButton)event.button; 59 | this.modifier = event.state; 60 | } 61 | } 62 | 63 | public struct HoverEvent { 64 | bool lost; 65 | uint time; 66 | double x; 67 | double y; 68 | Gdk.ModifierType modifier; 69 | Gdk.CrossingMode mode; 70 | Gdk.NotifyType detail; 71 | 72 | public HoverEvent(Gdk.EventCrossing event) { 73 | this.time = event.time; 74 | this.x = event.x; 75 | this.y = event.y; 76 | this.modifier = event.state; 77 | this.mode = event.mode; 78 | this.detail = event.detail; 79 | } 80 | } 81 | 82 | public struct ScrollEvent { 83 | uint time; 84 | double x; 85 | double y; 86 | Gdk.ModifierType modifier; 87 | Gdk.ScrollDirection direction; 88 | double delta_x; 89 | double delta_y; 90 | 91 | public ScrollEvent(Gdk.EventScroll event) { 92 | this.time = event.time; 93 | this.x = event.x; 94 | this.y = event.y; 95 | this.modifier = event.state; 96 | this.direction = event.direction; 97 | this.delta_x = event.delta_x; 98 | this.delta_y = event.delta_y; 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/widget/centerbox.vala: -------------------------------------------------------------------------------- 1 | namespace Astal { 2 | public class CenterBox : Gtk.Box { 3 | [CCode (notify = false)] 4 | public bool vertical { 5 | get { return orientation == Gtk.Orientation.VERTICAL; } 6 | set { orientation = value ? Gtk.Orientation.VERTICAL : Gtk.Orientation.HORIZONTAL; } 7 | } 8 | 9 | construct { 10 | notify["orientation"].connect(() => { 11 | notify_property("vertical"); 12 | }); 13 | } 14 | 15 | static construct { 16 | set_css_name("centerbox"); 17 | } 18 | 19 | private Gtk.Widget _start_widget; 20 | public Gtk.Widget start_widget { 21 | get { return _start_widget; } 22 | set { 23 | if (_start_widget != null) 24 | remove(_start_widget); 25 | 26 | if (value != null) 27 | pack_start(value, true, true, 0); 28 | } 29 | } 30 | 31 | private Gtk.Widget _end_widget; 32 | public Gtk.Widget end_widget { 33 | get { return _end_widget; } 34 | set { 35 | if (_end_widget != null) 36 | remove(_end_widget); 37 | 38 | if (value != null) 39 | pack_end(value, true, true, 0); 40 | } 41 | } 42 | 43 | public Gtk.Widget center_widget { 44 | get { return get_center_widget(); } 45 | set { 46 | if (center_widget != null) 47 | remove(center_widget); 48 | 49 | if (value != null) 50 | set_center_widget(value); 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/widget/circularprogress.vala: -------------------------------------------------------------------------------- 1 | namespace Astal { 2 | public class CircularProgress : Gtk.Bin { 3 | public new Gtk.Widget child { get; set; } 4 | public double start_at { get; set; } 5 | public double end_at { get; set; } 6 | public double value { get; set; } 7 | public bool inverted { get; set; } 8 | public bool rounded { get; set; } 9 | 10 | construct { 11 | notify["start-at"].connect(queue_draw); 12 | notify["end-at"].connect(queue_draw); 13 | notify["value"].connect(queue_draw); 14 | notify["inverted"].connect(queue_draw); 15 | notify["rounded"].connect(queue_draw); 16 | notify["child"].connect(queue_draw); 17 | } 18 | 19 | static construct { 20 | set_css_name("circular-progress"); 21 | } 22 | 23 | public new void get_preferred_height(out int minh, out int nath) { 24 | var val = get_style_context().get_property("min-height", Gtk.StateFlags.NORMAL); 25 | if (val.get_int() <= 0) { 26 | minh = 40; 27 | nath = 40; 28 | } 29 | 30 | minh = val.get_int(); 31 | nath = val.get_int(); 32 | } 33 | 34 | public new void get_preferred_width(out int minw, out int natw) { 35 | var val = get_style_context().get_property("min-width", Gtk.StateFlags.NORMAL); 36 | if (val.get_int() <= 0) { 37 | minw = 40; 38 | natw = 40; 39 | } 40 | 41 | minw = val.get_int(); 42 | natw = val.get_int(); 43 | } 44 | 45 | private double _to_radian(double percentage) { 46 | percentage = Math.floor(percentage * 100); 47 | return (percentage / 100) * (2 * Math.PI); 48 | } 49 | 50 | private bool _is_full_circle(double start, double end, double epsilon = 1e-10) { 51 | // Ensure that start and end are between 0 and 1 52 | start = (start % 1 + 1) % 1; 53 | end = (end % 1 + 1) % 1; 54 | 55 | // Check if the difference between start and end is close to 1 56 | return Math.fabs(start - end) <= epsilon; 57 | } 58 | 59 | private double _map_arc_value_to_range(double start, double end, double value) { 60 | // Ensure that start and end are between 0 and 1 61 | start = (start % 1 + 1) % 1; 62 | end = (end % 1 + 1) % 1; 63 | 64 | // Calculate the length of the arc 65 | var arcLength = end - start; 66 | if (arcLength < 0) 67 | arcLength += 1; // Adjust for circular representation 68 | 69 | // Calculate the position on the arc based on the percentage value 70 | var position = start + (arcLength * value); 71 | 72 | // Ensure the position is between 0 and 1 73 | position = (position % 1 + 1) % 1; 74 | 75 | return position; 76 | } 77 | 78 | private double _min(double[] arr) { 79 | double min = arr[0]; 80 | foreach(var i in arr) 81 | if (min > i) min = i; 82 | return min; 83 | } 84 | 85 | private double _max(double[] arr) { 86 | double max = arr[0]; 87 | foreach(var i in arr) 88 | if (max < i) max = i; 89 | return max; 90 | } 91 | 92 | public new bool draw(Cairo.Context cr) { 93 | Gtk.Allocation allocation; 94 | get_allocation(out allocation); 95 | 96 | var styles = get_style_context(); 97 | var width = allocation.width; 98 | var height = allocation.height; 99 | var thickness = styles.get_property("font-size", Gtk.StateFlags.NORMAL).get_double(); 100 | var margin = styles.get_margin(Gtk.StateFlags.NORMAL); 101 | var fg = styles.get_color(Gtk.StateFlags.NORMAL); 102 | var bg = styles.get_background_color(Gtk.StateFlags.NORMAL); 103 | 104 | var bg_stroke = thickness + _min({margin.bottom, margin.top, margin.left, margin.right}); 105 | var fg_stroke = thickness; 106 | var radius = _min({width, height}) / 2.0 - _max({bg_stroke, fg_stroke}) / 2.0; 107 | var center_x = width / 2; 108 | var center_y = height / 2; 109 | 110 | var start_background = _to_radian(this.start_at); 111 | var end_background = _to_radian(this.end_at); 112 | var ranged_value = this.value + this.start_at; 113 | 114 | var is_circle = _is_full_circle(this.start_at, this.end_at); 115 | 116 | if (is_circle) { 117 | // Redefine endDraw in radius to create an accurate full circle 118 | end_background = start_background + 2 * Math.PI; 119 | } else { 120 | // Range the value for the arc shape 121 | ranged_value = _map_arc_value_to_range( 122 | this.start_at, 123 | this.end_at, 124 | this.value 125 | ); 126 | } 127 | 128 | var to = _to_radian(ranged_value); 129 | double start_progress, end_progress; 130 | 131 | if (this.inverted) { 132 | start_progress = (2 * Math.PI - to) - start_background; 133 | end_progress = (2 * Math.PI - start_background) - start_background; 134 | } else { 135 | start_progress = start_background; 136 | end_progress = to; 137 | } 138 | 139 | // Draw background 140 | cr.set_source_rgba(bg.red, bg.green, bg.blue, bg.alpha); 141 | cr.arc(center_x, center_y, radius, start_background, end_background); 142 | 143 | cr.set_line_width(bg_stroke); 144 | cr.stroke(); 145 | 146 | // Draw progress 147 | cr.set_source_rgba(fg.red, fg.green, fg.blue, fg.alpha); 148 | cr.arc(center_x, center_y, radius, start_progress, end_progress); 149 | cr.set_line_width(fg_stroke); 150 | cr.stroke(); 151 | 152 | // Draw rounded ends 153 | if (this.rounded) { 154 | var start_x = center_x + Math.cos(start_background); 155 | var start_y = center_y + Math.cos(start_background); 156 | var end_x = center_x + Math.cos(to) * radius; 157 | var end_y = center_y + Math.cos(to) * radius; 158 | cr.set_line_width(0); 159 | cr.arc(start_x, start_y, fg_stroke / 2, 0, 0 - 0.01); 160 | cr.fill(); 161 | cr.arc(end_x, end_y, fg_stroke / 2, 0, 0 - 0.01); 162 | cr.fill(); 163 | } 164 | 165 | if (this.child != null) { 166 | this.child.size_allocate(allocation); 167 | this.propagate_draw(this.child, cr); 168 | } 169 | 170 | return true; 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/widget/eventbox.vala: -------------------------------------------------------------------------------- 1 | namespace Astal { 2 | public class EventBox : Gtk.EventBox { 3 | public signal void hover (HoverEvent event); 4 | public signal void hover_lost (HoverEvent event); 5 | public signal void click (ClickEvent event); 6 | public signal void click_release (ClickEvent event); 7 | public signal void scroll (ScrollEvent event); 8 | public signal void motion (MotionEvent event); 9 | 10 | static construct { 11 | set_css_name("eventbox"); 12 | } 13 | 14 | construct { 15 | add_events(Gdk.EventMask.SCROLL_MASK); 16 | add_events(Gdk.EventMask.SMOOTH_SCROLL_MASK); 17 | add_events(Gdk.EventMask.POINTER_MOTION_MASK); 18 | 19 | enter_notify_event.connect((self, event) => { 20 | if (event.window == self.get_window() && 21 | event.detail != Gdk.NotifyType.INFERIOR) { 22 | this.set_state_flags(Gtk.StateFlags.PRELIGHT, false); 23 | hover(HoverEvent(event) { lost = false }); 24 | } 25 | }); 26 | 27 | leave_notify_event.connect((self, event) => { 28 | if (event.window == self.get_window() && 29 | event.detail != Gdk.NotifyType.INFERIOR) { 30 | this.unset_state_flags(Gtk.StateFlags.PRELIGHT); 31 | hover_lost(HoverEvent(event) { lost = true }); 32 | } 33 | }); 34 | 35 | button_press_event.connect((event) => { 36 | click(ClickEvent(event) { release = false }); 37 | }); 38 | 39 | button_release_event.connect((event) => { 40 | click_release(ClickEvent(event) { release = true }); 41 | }); 42 | 43 | scroll_event.connect((event) => { 44 | scroll(ScrollEvent(event)); 45 | }); 46 | 47 | motion_notify_event.connect((event) => { 48 | motion(MotionEvent(event)); 49 | }); 50 | } 51 | } 52 | 53 | public struct MotionEvent { 54 | uint time; 55 | double x; 56 | double y; 57 | Gdk.ModifierType modifier; 58 | 59 | public MotionEvent(Gdk.EventMotion event) { 60 | this.time = event.time; 61 | this.x = event.x; 62 | this.y = event.y; 63 | this.modifier = event.state; 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/widget/icon.vala: -------------------------------------------------------------------------------- 1 | namespace Astal { 2 | public Gtk.IconInfo? lookup_icon(string icon) { 3 | var theme = Gtk.IconTheme.get_default(); 4 | return theme.lookup_icon(icon, 16, Gtk.IconLookupFlags.USE_BUILTIN); 5 | } 6 | 7 | public class Icon : Gtk.Image { 8 | private IconType type = IconType.NAMED; 9 | private double size { get; set; default = 14; } 10 | 11 | public new Gdk.Pixbuf pixbuf { get; set; } 12 | public string icon { get; set; default = ""; } 13 | 14 | private async void display_icon() { 15 | switch(type) { 16 | case IconType.NAMED: 17 | icon_name = icon; 18 | pixel_size = (int)size; 19 | break; 20 | case IconType.FILE: 21 | try { 22 | var file = File.new_for_path(icon); 23 | var stream = yield file.read_async(); 24 | var pb = yield new Gdk.Pixbuf.from_stream_at_scale_async( 25 | stream, 26 | (int)size * scale_factor, 27 | (int)size * scale_factor, 28 | true, 29 | null 30 | ); 31 | var cs = Gdk.cairo_surface_create_from_pixbuf(pb, 0, this.get_window()); 32 | set_from_surface(cs); 33 | } catch (Error err) { 34 | printerr(err.message); 35 | } 36 | break; 37 | case IconType.PIXBUF: 38 | var pb_scaled = pixbuf.scale_simple( 39 | (int)size * scale_factor, 40 | (int)size * scale_factor, 41 | Gdk.InterpType.BILINEAR 42 | ); 43 | if (pb_scaled != null) { 44 | var cs = Gdk.cairo_surface_create_from_pixbuf(pb_scaled, 0, this.get_window()); 45 | set_from_surface(cs); 46 | } 47 | break; 48 | } 49 | } 50 | 51 | static construct { 52 | set_css_name("icon"); 53 | } 54 | 55 | construct { 56 | notify["icon"].connect(() => { 57 | if(FileUtils.test(icon, GLib.FileTest.EXISTS)) 58 | type = IconType.FILE; 59 | else if (lookup_icon(icon) != null) 60 | type = IconType.NAMED; 61 | else { 62 | type = IconType.NAMED; 63 | warning("cannot assign %s as icon, "+ 64 | "it is not a file nor a named icon", icon); 65 | } 66 | display_icon.begin(); 67 | }); 68 | 69 | notify["pixbuf"].connect(() => { 70 | type = IconType.PIXBUF; 71 | display_icon.begin(); 72 | }); 73 | 74 | size_allocate.connect(() => { 75 | size = get_style_context() 76 | .get_property("font-size", Gtk.StateFlags.NORMAL).get_double(); 77 | 78 | display_icon.begin(); 79 | }); 80 | 81 | get_style_context().changed.connect(() => { 82 | size = get_style_context() 83 | .get_property("font-size", Gtk.StateFlags.NORMAL).get_double(); 84 | 85 | display_icon.begin(); 86 | }); 87 | } 88 | } 89 | 90 | private enum IconType { 91 | NAMED, 92 | FILE, 93 | PIXBUF, 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/widget/label.vala: -------------------------------------------------------------------------------- 1 | using Pango; 2 | 3 | public class Astal.Label : Gtk.Label { 4 | public bool truncate { 5 | set { ellipsize = value ? EllipsizeMode.END : EllipsizeMode.NONE; } 6 | get { return ellipsize == EllipsizeMode.END; } 7 | } 8 | 9 | public new bool justify_fill { 10 | set { justify = value ? Gtk.Justification.FILL : Gtk.Justification.LEFT; } 11 | get { return justify == Gtk.Justification.FILL; } 12 | } 13 | 14 | construct { 15 | notify["ellipsize"].connect(() => notify_property("truncate")); 16 | notify["justify"].connect(() => notify_property("justify_fill")); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/widget/levelbar.vala: -------------------------------------------------------------------------------- 1 | namespace Astal { 2 | public class LevelBar : Gtk.LevelBar { 3 | [CCode (notify = false)] 4 | public bool vertical { 5 | get { return orientation == Gtk.Orientation.VERTICAL; } 6 | set { orientation = value ? Gtk.Orientation.VERTICAL : Gtk.Orientation.HORIZONTAL; } 7 | } 8 | 9 | construct { 10 | notify["orientation"].connect(() => { 11 | notify_property("vertical"); 12 | }); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/widget/overlay.vala: -------------------------------------------------------------------------------- 1 | namespace Astal { 2 | public class Overlay : Gtk.Overlay { 3 | public bool pass_through { get; set; } 4 | 5 | public Gtk.Widget? overlay { 6 | get { return overlays.nth_data(0); } 7 | set { 8 | foreach (var ch in get_children()) { 9 | if (ch != child) 10 | remove(ch); 11 | } 12 | 13 | if (value != null) 14 | add_overlay(value); 15 | } 16 | } 17 | 18 | public List overlays { 19 | owned get { return get_children(); } 20 | set { 21 | foreach (var ch in get_children()) { 22 | if (ch != child) 23 | remove(ch); 24 | } 25 | 26 | foreach (var ch in value) 27 | add_overlay(ch); 28 | } 29 | } 30 | 31 | public new Gtk.Widget? child { 32 | get { return get_child(); } 33 | set { 34 | var ch = get_child(); 35 | if (ch != null) 36 | remove(ch); 37 | 38 | if (value != null) 39 | add(value); 40 | } 41 | } 42 | 43 | construct { 44 | notify["pass-through"].connect(() => { 45 | update_pass_through(); 46 | }); 47 | } 48 | 49 | private void update_pass_through() { 50 | foreach (var child in get_children()) 51 | set_overlay_pass_through(child, pass_through); 52 | } 53 | 54 | public new void add_overlay(Gtk.Widget widget) { 55 | base.add_overlay(widget); 56 | set_overlay_pass_through(widget, pass_through); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/widget/scrollable.vala: -------------------------------------------------------------------------------- 1 | namespace Astal { 2 | public class Scrollable : Gtk.ScrolledWindow { 3 | private Gtk.PolicyType _hscroll = Gtk.PolicyType.AUTOMATIC; 4 | private Gtk.PolicyType _vscroll = Gtk.PolicyType.AUTOMATIC; 5 | 6 | public Gtk.PolicyType hscroll { 7 | get { return _hscroll; } 8 | set { 9 | _hscroll = value; 10 | set_policy(value, vscroll); 11 | } 12 | } 13 | 14 | public Gtk.PolicyType vscroll { 15 | get { return _vscroll; } 16 | set { 17 | _vscroll = value; 18 | set_policy(hscroll, value); 19 | } 20 | } 21 | 22 | static construct { 23 | set_css_name("scrollable"); 24 | } 25 | 26 | construct { 27 | if (hadjustment != null) 28 | hadjustment = new Gtk.Adjustment(0,0,0,0,0,0); 29 | 30 | if (vadjustment != null) 31 | vadjustment = new Gtk.Adjustment(0,0,0,0,0,0); 32 | } 33 | 34 | public new Gtk.Widget get_child() { 35 | var ch = base.get_child(); 36 | if (ch is Gtk.Viewport) { 37 | return ch.get_child(); 38 | } 39 | return ch; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/widget/slider.vala: -------------------------------------------------------------------------------- 1 | namespace Astal { 2 | public class Slider : Gtk.Scale { 3 | [CCode (notify = false)] 4 | public bool vertical { 5 | get { return orientation == Gtk.Orientation.VERTICAL; } 6 | set { orientation = value ? Gtk.Orientation.VERTICAL : Gtk.Orientation.HORIZONTAL; } 7 | } 8 | 9 | // emitted when the user drags the slider 10 | public signal void dragged (); 11 | 12 | construct { 13 | if (adjustment == null) 14 | adjustment = new Gtk.Adjustment(0,0,0,0,0,0); 15 | 16 | if (max == 0 && min == 0) { 17 | max = 1; 18 | } 19 | 20 | if (step == 0) { 21 | step = 0.05; 22 | } 23 | 24 | notify["orientation"].connect(() => { 25 | notify_property("vertical"); 26 | }); 27 | 28 | button_press_event.connect(() => { dragging = true; }); 29 | key_press_event.connect(() => { dragging = true; }); 30 | button_release_event.connect(() => { dragging = false; }); 31 | key_release_event.connect(() => { dragging = false; }); 32 | scroll_event.connect((event) => { 33 | dragging = true; 34 | if (event.delta_y > 0) 35 | value -= step; 36 | else 37 | value += step; 38 | dragging = false; 39 | }); 40 | 41 | value_changed.connect(() => { 42 | if (dragging) 43 | dragged(); 44 | }); 45 | } 46 | 47 | public bool dragging { get; private set; } 48 | 49 | public double value { 50 | get { return adjustment.value; } 51 | set { if (!dragging) adjustment.value = value; } 52 | } 53 | 54 | public double min { 55 | get { return adjustment.lower; } 56 | set { adjustment.lower = value; } 57 | } 58 | 59 | public double max { 60 | get { return adjustment.upper; } 61 | set { adjustment.upper = value; } 62 | } 63 | 64 | public double step { 65 | get { return adjustment.step_increment; } 66 | set { adjustment.step_increment = value; } 67 | } 68 | 69 | // TODO: marks 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/widget/widget.vala: -------------------------------------------------------------------------------- 1 | namespace Astal { 2 | private class Css { 3 | private static HashTable _providers; 4 | public static HashTable providers { 5 | get { 6 | if (_providers == null) { 7 | _providers = new HashTable( 8 | (w) => (uint)w, 9 | (a, b) => a == b); 10 | } 11 | 12 | return _providers; 13 | } 14 | } 15 | } 16 | 17 | private void remove_provider(Gtk.Widget widget) { 18 | var providers = Css.providers; 19 | 20 | if (providers.contains(widget)) { 21 | var p = providers.get(widget); 22 | widget.get_style_context().remove_provider(p); 23 | providers.remove(widget); 24 | p.dispose(); 25 | } 26 | } 27 | 28 | public void widget_set_css(Gtk.Widget widget, string css) { 29 | var providers = Css.providers; 30 | 31 | if (providers.contains(widget)) { 32 | remove_provider(widget); 33 | } else { 34 | widget.destroy.connect(() => { 35 | remove_provider(widget); 36 | }); 37 | } 38 | 39 | var style = !css.contains("{") || !css.contains("}") 40 | ? "* { ".concat(css, "}") : css; 41 | 42 | var p = new Gtk.CssProvider(); 43 | widget.get_style_context() 44 | .add_provider(p, Gtk.STYLE_PROVIDER_PRIORITY_USER); 45 | 46 | try { 47 | p.load_from_data(style, style.length); 48 | providers.set(widget, p); 49 | } catch (Error err) { 50 | warning(err.message); 51 | } 52 | } 53 | 54 | public string widget_get_css(Gtk.Widget widget) { 55 | var providers = Css.providers; 56 | 57 | if (providers.contains(widget)) 58 | return providers.get(widget).to_string(); 59 | 60 | return ""; 61 | } 62 | 63 | public void widget_set_class_names(Gtk.Widget widget, string[] class_names) { 64 | foreach (var name in widget_get_class_names(widget)) 65 | widget_toggle_class_name(widget, name, false); 66 | 67 | foreach (var name in class_names) 68 | widget_toggle_class_name(widget, name, true); 69 | } 70 | 71 | public List widget_get_class_names(Gtk.Widget widget) { 72 | return widget.get_style_context().list_classes(); 73 | } 74 | 75 | public void widget_toggle_class_name( 76 | Gtk.Widget widget, 77 | string class_name, 78 | bool condition = true 79 | ) { 80 | var c = widget.get_style_context(); 81 | if (condition) 82 | c.add_class(class_name); 83 | else 84 | c.remove_class(class_name); 85 | } 86 | 87 | private class Cursor { 88 | private static HashTable _cursors; 89 | public static HashTable cursors { 90 | get { 91 | if (_cursors == null) { 92 | _cursors = new HashTable( 93 | (w) => (uint)w, 94 | (a, b) => a == b); 95 | } 96 | return _cursors; 97 | } 98 | } 99 | } 100 | 101 | private void widget_setup_cursor(Gtk.Widget widget) { 102 | widget.add_events(Gdk.EventMask.ENTER_NOTIFY_MASK); 103 | widget.add_events(Gdk.EventMask.LEAVE_NOTIFY_MASK); 104 | widget.enter_notify_event.connect(() => { 105 | widget.get_window().set_cursor( 106 | new Gdk.Cursor.from_name( 107 | Gdk.Display.get_default(), 108 | Cursor.cursors.get(widget))); 109 | return false; 110 | }); 111 | widget.leave_notify_event.connect(() => { 112 | widget.get_window().set_cursor( 113 | new Gdk.Cursor.from_name( 114 | Gdk.Display.get_default(), 115 | "default")); 116 | return false; 117 | }); 118 | widget.destroy.connect(() => { 119 | if (Cursor.cursors.contains(widget)) 120 | Cursor.cursors.remove(widget); 121 | }); 122 | } 123 | 124 | public void widget_set_cursor(Gtk.Widget widget, string cursor) { 125 | if (!Cursor.cursors.contains(widget)) 126 | widget_setup_cursor(widget); 127 | 128 | Cursor.cursors.set(widget, cursor); 129 | } 130 | 131 | public string widget_get_cursor(Gtk.Widget widget) { 132 | return Cursor.cursors.get(widget); 133 | } 134 | 135 | private class ClickThrough { 136 | private static HashTable _click_through; 137 | public static HashTable click_through { 138 | get { 139 | if (_click_through == null) { 140 | _click_through = new HashTable( 141 | (w) => (uint)w, 142 | (a, b) => a == b); 143 | } 144 | return _click_through; 145 | } 146 | } 147 | } 148 | 149 | public void widget_set_click_through(Gtk.Widget widget, bool click_through) { 150 | ClickThrough.click_through.set(widget, click_through); 151 | widget.input_shape_combine_region(click_through ? new Cairo.Region() : null); 152 | } 153 | 154 | public bool widget_get_click_through(Gtk.Widget widget) { 155 | return ClickThrough.click_through.get(widget); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/widget/window.vala: -------------------------------------------------------------------------------- 1 | using GtkLayerShell; 2 | 3 | namespace Astal { 4 | public enum WindowAnchor { 5 | NONE = 0, 6 | TOP = 1, 7 | RIGHT = 2, 8 | LEFT = 4, 9 | BOTTOM = 8, 10 | } 11 | 12 | public enum Exclusivity { 13 | NORMAL, 14 | EXCLUSIVE, 15 | IGNORE, 16 | } 17 | 18 | public enum Layer { 19 | BACKGROUND = 0, // GtkLayerShell.Layer.BACKGROUND 20 | BOTTOM = 1, // GtkLayerShell.Layer.BOTTOM 21 | TOP = 2, // GtkLayerShell.Layer.TOP 22 | OVERLAY = 3, // GtkLayerShell.Layer.OVERLAY 23 | } 24 | 25 | public enum Keymode { 26 | NONE = 0, // GtkLayerShell.KeyboardMode.NONE 27 | ON_DEMAND = 1, // GtkLayerShell.KeyboardMode.ON_DEMAND 28 | EXCLUSIVE = 2, // GtkLayerShell.KeyboardMode.EXCLUSIVE 29 | } 30 | 31 | public class Window : Gtk.Window { 32 | private static bool check(string action) { 33 | if (!is_supported()) { 34 | critical(@"can not $action on window: layer shell not supported"); 35 | print("tip: running from an xwayland terminal can cause this, for example VsCode"); 36 | return true; 37 | } 38 | return false; 39 | } 40 | 41 | construct { 42 | if (check("initialize layer shell")) 43 | return; 44 | 45 | height_request = 1; 46 | width_request = 1; 47 | init_for_window(this); 48 | } 49 | 50 | public string namespace { 51 | get { return get_namespace(this); } 52 | set { set_namespace(this, value); } 53 | } 54 | 55 | public int anchor { 56 | set { 57 | if (check("set anchor")) 58 | return; 59 | 60 | set_anchor(this, Edge.TOP, WindowAnchor.TOP in value); 61 | set_anchor(this, Edge.BOTTOM, WindowAnchor.BOTTOM in value); 62 | set_anchor(this, Edge.LEFT, WindowAnchor.LEFT in value); 63 | set_anchor(this, Edge.RIGHT, WindowAnchor.RIGHT in value); 64 | } 65 | get { 66 | var a = WindowAnchor.NONE; 67 | if (get_anchor(this, Edge.TOP)) 68 | a = a | WindowAnchor.TOP; 69 | 70 | if (get_anchor(this, Edge.RIGHT)) 71 | a = a | WindowAnchor.RIGHT; 72 | 73 | if (get_anchor(this, Edge.LEFT)) 74 | a = a | WindowAnchor.LEFT; 75 | 76 | if (get_anchor(this, Edge.BOTTOM)) 77 | a = a | WindowAnchor.BOTTOM; 78 | 79 | return a; 80 | } 81 | } 82 | 83 | public Exclusivity exclusivity { 84 | set { 85 | if (check("set exclusivity")) 86 | return; 87 | 88 | switch (value) { 89 | case Exclusivity.NORMAL: 90 | set_exclusive_zone(this, 0); 91 | break; 92 | case Exclusivity.EXCLUSIVE: 93 | auto_exclusive_zone_enable(this); 94 | break; 95 | case Exclusivity.IGNORE: 96 | set_exclusive_zone(this, -1); 97 | break; 98 | } 99 | } 100 | get { 101 | if (auto_exclusive_zone_is_enabled(this)) 102 | return Exclusivity.EXCLUSIVE; 103 | 104 | if (get_exclusive_zone(this) == -1) 105 | return Exclusivity.IGNORE; 106 | 107 | return Exclusivity.NORMAL; 108 | } 109 | } 110 | 111 | public Layer layer { 112 | get { return (Layer)get_layer(this); } 113 | set { 114 | if (check("set layer")) 115 | return; 116 | 117 | set_layer(this, (GtkLayerShell.Layer)value); 118 | } 119 | } 120 | 121 | public Keymode keymode { 122 | get { return (Keymode)get_keyboard_mode(this); } 123 | set { 124 | if (check("set keymode")) 125 | return; 126 | 127 | set_keyboard_mode(this, (GtkLayerShell.KeyboardMode)value); 128 | } 129 | } 130 | 131 | public Gdk.Monitor gdkmonitor { 132 | get { return get_monitor(this); } 133 | set { 134 | if (check("set gdkmonitor")) 135 | return; 136 | 137 | set_monitor (this, value); 138 | } 139 | } 140 | 141 | public new int margin_top { 142 | get { return GtkLayerShell.get_margin(this, Edge.TOP); } 143 | set { 144 | if (check("set margin_top")) 145 | return; 146 | 147 | GtkLayerShell.set_margin(this, Edge.TOP, value); 148 | } 149 | } 150 | 151 | public new int margin_bottom { 152 | get { return GtkLayerShell.get_margin(this, Edge.BOTTOM); } 153 | set { 154 | if (check("set margin_bottom")) 155 | return; 156 | 157 | GtkLayerShell.set_margin(this, Edge.BOTTOM, value); 158 | } 159 | } 160 | 161 | public new int margin_left { 162 | get { return GtkLayerShell.get_margin(this, Edge.LEFT); } 163 | set { 164 | if (check("set margin_left")) 165 | return; 166 | 167 | GtkLayerShell.set_margin(this, Edge.LEFT, value); 168 | } 169 | } 170 | 171 | public new int margin_right { 172 | get { return GtkLayerShell.get_margin(this, Edge.RIGHT); } 173 | set { 174 | if (check("set margin_right")) 175 | return; 176 | 177 | GtkLayerShell.set_margin(this, Edge.RIGHT, value); 178 | } 179 | } 180 | 181 | public new int margin { 182 | set { 183 | if (check("set margin")) 184 | return; 185 | 186 | margin_top = value; 187 | margin_right = value; 188 | margin_bottom = value; 189 | margin_left = value; 190 | } 191 | } 192 | 193 | /** 194 | * CAUTION: the id might not be the same mapped by the compositor 195 | * to reset and let the compositor map it pass a negative number 196 | */ 197 | public int monitor { 198 | set { 199 | if (check("set monitor")) 200 | return; 201 | 202 | if (value < 0) 203 | set_monitor(this, (Gdk.Monitor)null); 204 | 205 | var m = Gdk.Display.get_default().get_monitor(value); 206 | set_monitor(this, m); 207 | } 208 | get { 209 | var m = get_monitor(this); 210 | var d = Gdk.Display.get_default(); 211 | for (var i = 0; i < d.get_n_monitors(); ++i) { 212 | if (m == d.get_monitor(i)) 213 | return i; 214 | } 215 | 216 | return -1; 217 | } 218 | } 219 | } 220 | 221 | /** 222 | * CAUTION: the id might not be the same mapped by the compositor 223 | */ 224 | public uint get_num_monitors() { 225 | return Gdk.Display.get_default().get_n_monitors(); 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /version: -------------------------------------------------------------------------------- 1 | 0.1.0 2 | --------------------------------------------------------------------------------