├── .gitignore ├── LICENSE.md ├── README.md ├── REUSE.toml ├── cli ├── _ldocr.sh ├── gen-icon.js ├── get-version.sh └── update-po.sh ├── jsconfig.json ├── meson.build ├── meson.options ├── po ├── LINGUAS ├── POTFILES.in ├── meson.build ├── nl.po └── zh_CN.po ├── res ├── data │ ├── dbus.xml.in │ ├── extension.gresource.xml.in │ ├── meson.build │ ├── metadata.json.in │ ├── prefs.gresource.xml.in │ ├── scalable │ │ └── status │ │ │ └── meson.build │ └── theme │ │ ├── meson.build │ │ └── style.scss ├── meson.build ├── schema │ ├── meson.build │ └── schemas.gschema.xml.in └── style │ ├── gnome-shell-sass │ ├── COPYING │ ├── README.md │ ├── _colors.scss │ ├── _default-colors.scss │ └── _palette.scss │ ├── meson.build │ ├── stylesheet-dark.scss │ ├── stylesheet-light.scss │ └── stylesheet.scss └── src ├── const.js ├── extension.js ├── fubar.js ├── ldocr.py ├── menu.js ├── prefs.js ├── ui.js └── util.js /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | po/*.pot 3 | 4 | node_modules 5 | package.json 6 | package-lock.json 7 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # GNU GENERAL PUBLIC LICENSE 2 | 3 | Version 3, 29 June 2007 4 | 5 | Copyright (C) 2007 Free Software Foundation, Inc. 6 | 7 | 8 | Everyone is permitted to copy and distribute verbatim copies of this 9 | license document, but changing it is not allowed. 10 | 11 | ## Preamble 12 | 13 | The GNU General Public License is a free, copyleft license for 14 | software and other kinds of works. 15 | 16 | The licenses for most software and other practical works are designed 17 | to take away your freedom to share and change the works. By contrast, 18 | the GNU General Public License is intended to guarantee your freedom 19 | to share and change all versions of a program--to make sure it remains 20 | free software for all its users. We, the Free Software Foundation, use 21 | the GNU General Public License for most of our software; it applies 22 | also to any other work released this way by its authors. You can apply 23 | it to your programs, too. 24 | 25 | When we speak of free software, we are referring to freedom, not 26 | price. Our General Public Licenses are designed to make sure that you 27 | have the freedom to distribute copies of free software (and charge for 28 | them if you wish), that you receive source code or can get it if you 29 | want it, that you can change the software or use pieces of it in new 30 | free programs, and that you know you can do these things. 31 | 32 | To protect your rights, we need to prevent others from denying you 33 | these rights or asking you to surrender the rights. Therefore, you 34 | have certain responsibilities if you distribute copies of the 35 | software, or if you modify it: responsibilities to respect the freedom 36 | of others. 37 | 38 | For example, if you distribute copies of such a program, whether 39 | gratis or for a fee, you must pass on to the recipients the same 40 | freedoms that you received. You must make sure that they, too, receive 41 | or can get the source code. And you must show them these terms so they 42 | know their rights. 43 | 44 | Developers that use the GNU GPL protect your rights with two steps: 45 | (1) assert copyright on the software, and (2) offer you this License 46 | giving you legal permission to copy, distribute and/or modify it. 47 | 48 | For the developers' and authors' protection, the GPL clearly explains 49 | that there is no warranty for this free software. For both users' and 50 | authors' sake, the GPL requires that modified versions be marked as 51 | changed, so that their problems will not be attributed erroneously to 52 | authors of previous versions. 53 | 54 | Some devices are designed to deny users access to install or run 55 | modified versions of the software inside them, although the 56 | manufacturer can do so. This is fundamentally incompatible with the 57 | aim of protecting users' freedom to change the software. The 58 | systematic pattern of such abuse occurs in the area of products for 59 | individuals to use, which is precisely where it is most unacceptable. 60 | Therefore, we have designed this version of the GPL to prohibit the 61 | practice for those products. If such problems arise substantially in 62 | other domains, we stand ready to extend this provision to those 63 | domains in future versions of the GPL, as needed to protect the 64 | freedom of users. 65 | 66 | Finally, every program is threatened constantly by software patents. 67 | States should not allow patents to restrict development and use of 68 | software on general-purpose computers, but in those that do, we wish 69 | to avoid the special danger that patents applied to a free program 70 | could make it effectively proprietary. To prevent this, the GPL 71 | assures that patents cannot be used to render the program non-free. 72 | 73 | The precise terms and conditions for copying, distribution and 74 | modification follow. 75 | 76 | ## TERMS AND CONDITIONS 77 | 78 | ### 0. Definitions. 79 | 80 | "This License" refers to version 3 of the GNU General Public License. 81 | 82 | "Copyright" also means copyright-like laws that apply to other kinds 83 | of works, such as semiconductor masks. 84 | 85 | "The Program" refers to any copyrightable work licensed under this 86 | License. Each licensee is addressed as "you". "Licensees" and 87 | "recipients" may be individuals or organizations. 88 | 89 | To "modify" a work means to copy from or adapt all or part of the work 90 | in a fashion requiring copyright permission, other than the making of 91 | an exact copy. The resulting work is called a "modified version" of 92 | the earlier work or a work "based on" the earlier work. 93 | 94 | A "covered work" means either the unmodified Program or a work based 95 | on the Program. 96 | 97 | To "propagate" a work means to do anything with it that, without 98 | permission, would make you directly or secondarily liable for 99 | infringement under applicable copyright law, except executing it on a 100 | computer or modifying a private copy. Propagation includes copying, 101 | distribution (with or without modification), making available to the 102 | public, and in some countries other activities as well. 103 | 104 | To "convey" a work means any kind of propagation that enables other 105 | parties to make or receive copies. Mere interaction with a user 106 | through a computer network, with no transfer of a copy, is not 107 | conveying. 108 | 109 | An interactive user interface displays "Appropriate Legal Notices" to 110 | the extent that it includes a convenient and prominently visible 111 | feature that (1) displays an appropriate copyright notice, and (2) 112 | tells the user that there is no warranty for the work (except to the 113 | extent that warranties are provided), that licensees may convey the 114 | work under this License, and how to view a copy of this License. If 115 | the interface presents a list of user commands or options, such as a 116 | menu, a prominent item in the list meets this criterion. 117 | 118 | ### 1. Source Code. 119 | 120 | The "source code" for a work means the preferred form of the work for 121 | making modifications to it. "Object code" means any non-source form of 122 | a work. 123 | 124 | A "Standard Interface" means an interface that either is an official 125 | standard defined by a recognized standards body, or, in the case of 126 | interfaces specified for a particular programming language, one that 127 | is widely used among developers working in that language. 128 | 129 | The "System Libraries" of an executable work include anything, other 130 | than the work as a whole, that (a) is included in the normal form of 131 | packaging a Major Component, but which is not part of that Major 132 | Component, and (b) serves only to enable use of the work with that 133 | Major Component, or to implement a Standard Interface for which an 134 | implementation is available to the public in source code form. A 135 | "Major Component", in this context, means a major essential component 136 | (kernel, window system, and so on) of the specific operating system 137 | (if any) on which the executable work runs, or a compiler used to 138 | produce the work, or an object code interpreter used to run it. 139 | 140 | The "Corresponding Source" for a work in object code form means all 141 | the source code needed to generate, install, and (for an executable 142 | work) run the object code and to modify the work, including scripts to 143 | control those activities. However, it does not include the work's 144 | System Libraries, or general-purpose tools or generally available free 145 | programs which are used unmodified in performing those activities but 146 | which are not part of the work. For example, Corresponding Source 147 | includes interface definition files associated with source files for 148 | the work, and the source code for shared libraries and dynamically 149 | linked subprograms that the work is specifically designed to require, 150 | such as by intimate data communication or control flow between those 151 | subprograms and other parts of the work. 152 | 153 | The Corresponding Source need not include anything that users can 154 | regenerate automatically from other parts of the Corresponding Source. 155 | 156 | The Corresponding Source for a work in source code form is that same 157 | work. 158 | 159 | ### 2. Basic Permissions. 160 | 161 | All rights granted under this License are granted for the term of 162 | copyright on the Program, and are irrevocable provided the stated 163 | conditions are met. This License explicitly affirms your unlimited 164 | permission to run the unmodified Program. The output from running a 165 | covered work is covered by this License only if the output, given its 166 | content, constitutes a covered work. This License acknowledges your 167 | rights of fair use or other equivalent, as provided by copyright law. 168 | 169 | You may make, run and propagate covered works that you do not convey, 170 | without conditions so long as your license otherwise remains in force. 171 | You may convey covered works to others for the sole purpose of having 172 | them make modifications exclusively for you, or provide you with 173 | facilities for running those works, provided that you comply with the 174 | terms of this License in conveying all material for which you do not 175 | control copyright. Those thus making or running the covered works for 176 | you must do so exclusively on your behalf, under your direction and 177 | control, on terms that prohibit them from making any copies of your 178 | copyrighted material outside their relationship with you. 179 | 180 | Conveying under any other circumstances is permitted solely under the 181 | conditions stated below. Sublicensing is not allowed; section 10 makes 182 | it unnecessary. 183 | 184 | ### 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 185 | 186 | No covered work shall be deemed part of an effective technological 187 | measure under any applicable law fulfilling obligations under article 188 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 189 | similar laws prohibiting or restricting circumvention of such 190 | measures. 191 | 192 | When you convey a covered work, you waive any legal power to forbid 193 | circumvention of technological measures to the extent such 194 | circumvention is effected by exercising rights under this License with 195 | respect to the covered work, and you disclaim any intention to limit 196 | operation or modification of the work as a means of enforcing, against 197 | the work's users, your or third parties' legal rights to forbid 198 | circumvention of technological measures. 199 | 200 | ### 4. Conveying Verbatim Copies. 201 | 202 | You may convey verbatim copies of the Program's source code as you 203 | receive it, in any medium, provided that you conspicuously and 204 | appropriately publish on each copy an appropriate copyright notice; 205 | keep intact all notices stating that this License and any 206 | non-permissive terms added in accord with section 7 apply to the code; 207 | keep intact all notices of the absence of any warranty; and give all 208 | recipients a copy of this License along with the Program. 209 | 210 | You may charge any price or no price for each copy that you convey, 211 | and you may offer support or warranty protection for a fee. 212 | 213 | ### 5. Conveying Modified Source Versions. 214 | 215 | You may convey a work based on the Program, or the modifications to 216 | produce it from the Program, in the form of source code under the 217 | terms of section 4, provided that you also meet all of these 218 | conditions: 219 | 220 | - a) The work must carry prominent notices stating that you modified 221 | it, and giving a relevant date. 222 | - b) The work must carry prominent notices stating that it is 223 | released under this License and any conditions added under 224 | section 7. This requirement modifies the requirement in section 4 225 | to "keep intact all notices". 226 | - c) You must license the entire work, as a whole, under this 227 | License to anyone who comes into possession of a copy. This 228 | License will therefore apply, along with any applicable section 7 229 | additional terms, to the whole of the work, and all its parts, 230 | regardless of how they are packaged. This License gives no 231 | permission to license the work in any other way, but it does not 232 | invalidate such permission if you have separately received it. 233 | - d) If the work has interactive user interfaces, each must display 234 | Appropriate Legal Notices; however, if the Program has interactive 235 | interfaces that do not display Appropriate Legal Notices, your 236 | work need not make them do so. 237 | 238 | A compilation of a covered work with other separate and independent 239 | works, which are not by their nature extensions of the covered work, 240 | and which are not combined with it such as to form a larger program, 241 | in or on a volume of a storage or distribution medium, is called an 242 | "aggregate" if the compilation and its resulting copyright are not 243 | used to limit the access or legal rights of the compilation's users 244 | beyond what the individual works permit. Inclusion of a covered work 245 | in an aggregate does not cause this License to apply to the other 246 | parts of the aggregate. 247 | 248 | ### 6. Conveying Non-Source Forms. 249 | 250 | You may convey a covered work in object code form under the terms of 251 | sections 4 and 5, provided that you also convey the machine-readable 252 | Corresponding Source under the terms of this License, in one of these 253 | ways: 254 | 255 | - a) Convey the object code in, or embodied in, a physical product 256 | (including a physical distribution medium), accompanied by the 257 | Corresponding Source fixed on a durable physical medium 258 | customarily used for software interchange. 259 | - b) Convey the object code in, or embodied in, a physical product 260 | (including a physical distribution medium), accompanied by a 261 | written offer, valid for at least three years and valid for as 262 | long as you offer spare parts or customer support for that product 263 | model, to give anyone who possesses the object code either (1) a 264 | copy of the Corresponding Source for all the software in the 265 | product that is covered by this License, on a durable physical 266 | medium customarily used for software interchange, for a price no 267 | more than your reasonable cost of physically performing this 268 | conveying of source, or (2) access to copy the Corresponding 269 | Source from a network server at no charge. 270 | - c) Convey individual copies of the object code with a copy of the 271 | written offer to provide the Corresponding Source. This 272 | alternative is allowed only occasionally and noncommercially, and 273 | only if you received the object code with such an offer, in accord 274 | with subsection 6b. 275 | - d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | - e) Convey the object code using peer-to-peer transmission, 288 | provided you inform other peers where the object code and 289 | Corresponding Source of the work are being offered to the general 290 | public at no charge under subsection 6d. 291 | 292 | A separable portion of the object code, whose source code is excluded 293 | from the Corresponding Source as a System Library, need not be 294 | included in conveying the object code work. 295 | 296 | A "User Product" is either (1) a "consumer product", which means any 297 | tangible personal property which is normally used for personal, 298 | family, or household purposes, or (2) anything designed or sold for 299 | incorporation into a dwelling. In determining whether a product is a 300 | consumer product, doubtful cases shall be resolved in favor of 301 | coverage. For a particular product received by a particular user, 302 | "normally used" refers to a typical or common use of that class of 303 | product, regardless of the status of the particular user or of the way 304 | in which the particular user actually uses, or expects or is expected 305 | to use, the product. A product is a consumer product regardless of 306 | whether the product has substantial commercial, industrial or 307 | non-consumer uses, unless such uses represent the only significant 308 | mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to 312 | install and execute modified versions of a covered work in that User 313 | Product from a modified version of its Corresponding Source. The 314 | information must suffice to ensure that the continued functioning of 315 | the modified object code is in no case prevented or interfered with 316 | solely because modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or 331 | updates for a work that has been modified or installed by the 332 | recipient, or for the User Product in which it has been modified or 333 | installed. Access to a network may be denied when the modification 334 | itself materially and adversely affects the operation of the network 335 | or violates the rules and protocols for communication across the 336 | network. 337 | 338 | Corresponding Source conveyed, and Installation Information provided, 339 | in accord with this section must be in a format that is publicly 340 | documented (and with an implementation available to the public in 341 | source code form), and must require no special password or key for 342 | unpacking, reading or copying. 343 | 344 | ### 7. Additional Terms. 345 | 346 | "Additional permissions" are terms that supplement the terms of this 347 | License by making exceptions from one or more of its conditions. 348 | Additional permissions that are applicable to the entire Program shall 349 | be treated as though they were included in this License, to the extent 350 | that they are valid under applicable law. If additional permissions 351 | apply only to part of the Program, that part may be used separately 352 | under those permissions, but the entire Program remains governed by 353 | this License without regard to the additional permissions. 354 | 355 | When you convey a copy of a covered work, you may at your option 356 | remove any additional permissions from that copy, or from any part of 357 | it. (Additional permissions may be written to require their own 358 | removal in certain cases when you modify the work.) You may place 359 | additional permissions on material, added by you to a covered work, 360 | for which you have or can give appropriate copyright permission. 361 | 362 | Notwithstanding any other provision of this License, for material you 363 | add to a covered work, you may (if authorized by the copyright holders 364 | of that material) supplement the terms of this License with terms: 365 | 366 | - a) Disclaiming warranty or limiting liability differently from the 367 | terms of sections 15 and 16 of this License; or 368 | - b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | - c) Prohibiting misrepresentation of the origin of that material, 372 | or requiring that modified versions of such material be marked in 373 | reasonable ways as different from the original version; or 374 | - d) Limiting the use for publicity purposes of names of licensors 375 | or authors of the material; or 376 | - e) Declining to grant rights under trademark law for use of some 377 | trade names, trademarks, or service marks; or 378 | - f) Requiring indemnification of licensors and authors of that 379 | material by anyone who conveys the material (or modified versions 380 | of it) with contractual assumptions of liability to the recipient, 381 | for any liability that these contractual assumptions directly 382 | impose on those licensors and authors. 383 | 384 | All other non-permissive additional terms are considered "further 385 | restrictions" within the meaning of section 10. If the Program as you 386 | received it, or any part of it, contains a notice stating that it is 387 | governed by this License along with a term that is a further 388 | restriction, you may remove that term. If a license document contains 389 | a further restriction but permits relicensing or conveying under this 390 | License, you may add to a covered work material governed by the terms 391 | of that license document, provided that the further restriction does 392 | not survive such relicensing or conveying. 393 | 394 | If you add terms to a covered work in accord with this section, you 395 | must place, in the relevant source files, a statement of the 396 | additional terms that apply to those files, or a notice indicating 397 | where to find the applicable terms. 398 | 399 | Additional terms, permissive or non-permissive, may be stated in the 400 | form of a separately written license, or stated as exceptions; the 401 | above requirements apply either way. 402 | 403 | ### 8. Termination. 404 | 405 | You may not propagate or modify a covered work except as expressly 406 | provided under this License. Any attempt otherwise to propagate or 407 | modify it is void, and will automatically terminate your rights under 408 | this License (including any patent licenses granted under the third 409 | paragraph of section 11). 410 | 411 | However, if you cease all violation of this License, then your license 412 | from a particular copyright holder is reinstated (a) provisionally, 413 | unless and until the copyright holder explicitly and finally 414 | terminates your license, and (b) permanently, if the copyright holder 415 | fails to notify you of the violation by some reasonable means prior to 416 | 60 days after the cessation. 417 | 418 | Moreover, your license from a particular copyright holder is 419 | reinstated permanently if the copyright holder notifies you of the 420 | violation by some reasonable means, this is the first time you have 421 | received notice of violation of this License (for any work) from that 422 | copyright holder, and you cure the violation prior to 30 days after 423 | your receipt of the notice. 424 | 425 | Termination of your rights under this section does not terminate the 426 | licenses of parties who have received copies or rights from you under 427 | this License. If your rights have been terminated and not permanently 428 | reinstated, you do not qualify to receive new licenses for the same 429 | material under section 10. 430 | 431 | ### 9. Acceptance Not Required for Having Copies. 432 | 433 | You are not required to accept this License in order to receive or run 434 | a copy of the Program. Ancillary propagation of a covered work 435 | occurring solely as a consequence of using peer-to-peer transmission 436 | to receive a copy likewise does not require acceptance. However, 437 | nothing other than this License grants you permission to propagate or 438 | modify any covered work. These actions infringe copyright if you do 439 | not accept this License. Therefore, by modifying or propagating a 440 | covered work, you indicate your acceptance of this License to do so. 441 | 442 | ### 10. Automatic Licensing of Downstream Recipients. 443 | 444 | Each time you convey a covered work, the recipient automatically 445 | receives a license from the original licensors, to run, modify and 446 | propagate that work, subject to this License. You are not responsible 447 | for enforcing compliance by third parties with this License. 448 | 449 | An "entity transaction" is a transaction transferring control of an 450 | organization, or substantially all assets of one, or subdividing an 451 | organization, or merging organizations. If propagation of a covered 452 | work results from an entity transaction, each party to that 453 | transaction who receives a copy of the work also receives whatever 454 | licenses to the work the party's predecessor in interest had or could 455 | give under the previous paragraph, plus a right to possession of the 456 | Corresponding Source of the work from the predecessor in interest, if 457 | the predecessor has it or can get it with reasonable efforts. 458 | 459 | You may not impose any further restrictions on the exercise of the 460 | rights granted or affirmed under this License. For example, you may 461 | not impose a license fee, royalty, or other charge for exercise of 462 | rights granted under this License, and you may not initiate litigation 463 | (including a cross-claim or counterclaim in a lawsuit) alleging that 464 | any patent claim is infringed by making, using, selling, offering for 465 | sale, or importing the Program or any portion of it. 466 | 467 | ### 11. Patents. 468 | 469 | A "contributor" is a copyright holder who authorizes use under this 470 | License of the Program or a work on which the Program is based. The 471 | work thus licensed is called the contributor's "contributor version". 472 | 473 | A contributor's "essential patent claims" are all patent claims owned 474 | or controlled by the contributor, whether already acquired or 475 | hereafter acquired, that would be infringed by some manner, permitted 476 | by this License, of making, using, or selling its contributor version, 477 | but do not include claims that would be infringed only as a 478 | consequence of further modification of the contributor version. For 479 | purposes of this definition, "control" includes the right to grant 480 | patent sublicenses in a manner consistent with the requirements of 481 | this License. 482 | 483 | Each contributor grants you a non-exclusive, worldwide, royalty-free 484 | patent license under the contributor's essential patent claims, to 485 | make, use, sell, offer for sale, import and otherwise run, modify and 486 | propagate the contents of its contributor version. 487 | 488 | In the following three paragraphs, a "patent license" is any express 489 | agreement or commitment, however denominated, not to enforce a patent 490 | (such as an express permission to practice a patent or covenant not to 491 | sue for patent infringement). To "grant" such a patent license to a 492 | party means to make such an agreement or commitment not to enforce a 493 | patent against the party. 494 | 495 | If you convey a covered work, knowingly relying on a patent license, 496 | and the Corresponding Source of the work is not available for anyone 497 | to copy, free of charge and under the terms of this License, through a 498 | publicly available network server or other readily accessible means, 499 | then you must either (1) cause the Corresponding Source to be so 500 | available, or (2) arrange to deprive yourself of the benefit of the 501 | patent license for this particular work, or (3) arrange, in a manner 502 | consistent with the requirements of this License, to extend the patent 503 | license to downstream recipients. "Knowingly relying" means you have 504 | actual knowledge that, but for the patent license, your conveying the 505 | covered work in a country, or your recipient's use of the covered work 506 | in a country, would infringe one or more identifiable patents in that 507 | country that you have reason to believe are valid. 508 | 509 | If, pursuant to or in connection with a single transaction or 510 | arrangement, you convey, or propagate by procuring conveyance of, a 511 | covered work, and grant a patent license to some of the parties 512 | receiving the covered work authorizing them to use, propagate, modify 513 | or convey a specific copy of the covered work, then the patent license 514 | you grant is automatically extended to all recipients of the covered 515 | work and works based on it. 516 | 517 | A patent license is "discriminatory" if it does not include within the 518 | scope of its coverage, prohibits the exercise of, or is conditioned on 519 | the non-exercise of one or more of the rights that are specifically 520 | granted under this License. You may not convey a covered work if you 521 | are a party to an arrangement with a third party that is in the 522 | business of distributing software, under which you make payment to the 523 | third party based on the extent of your activity of conveying the 524 | work, and under which the third party grants, to any of the parties 525 | who would receive the covered work from you, a discriminatory patent 526 | license (a) in connection with copies of the covered work conveyed by 527 | you (or copies made from those copies), or (b) primarily for and in 528 | connection with specific products or compilations that contain the 529 | covered work, unless you entered into that arrangement, or that patent 530 | license was granted, prior to 28 March 2007. 531 | 532 | Nothing in this License shall be construed as excluding or limiting 533 | any implied license or other defenses to infringement that may 534 | otherwise be available to you under applicable patent law. 535 | 536 | ### 12. No Surrender of Others' Freedom. 537 | 538 | If conditions are imposed on you (whether by court order, agreement or 539 | otherwise) that contradict the conditions of this License, they do not 540 | excuse you from the conditions of this License. If you cannot convey a 541 | covered work so as to satisfy simultaneously your obligations under 542 | this License and any other pertinent obligations, then as a 543 | consequence you may not convey it at all. For example, if you agree to 544 | terms that obligate you to collect a royalty for further conveying 545 | from those to whom you convey the Program, the only way you could 546 | satisfy both those terms and this License would be to refrain entirely 547 | from conveying the Program. 548 | 549 | ### 13. Use with the GNU Affero General Public License. 550 | 551 | Notwithstanding any other provision of this License, you have 552 | permission to link or combine any covered work with a work licensed 553 | under version 3 of the GNU Affero General Public License into a single 554 | combined work, and to convey the resulting work. The terms of this 555 | License will continue to apply to the part which is the covered work, 556 | but the special requirements of the GNU Affero General Public License, 557 | section 13, concerning interaction through a network will apply to the 558 | combination as such. 559 | 560 | ### 14. Revised Versions of this License. 561 | 562 | The Free Software Foundation may publish revised and/or new versions 563 | of the GNU General Public License from time to time. Such new versions 564 | will be similar in spirit to the present version, but may differ in 565 | detail to address new problems or concerns. 566 | 567 | Each version is given a distinguishing version number. If the Program 568 | specifies that a certain numbered version of the GNU General Public 569 | License "or any later version" applies to it, you have the option of 570 | following the terms and conditions either of that numbered version or 571 | of any later version published by the Free Software Foundation. If the 572 | Program does not specify a version number of the GNU General Public 573 | License, you may choose any version ever published by the Free 574 | Software Foundation. 575 | 576 | If the Program specifies that a proxy can decide which future versions 577 | of the GNU General Public License can be used, that proxy's public 578 | statement of acceptance of a version permanently authorizes you to 579 | choose that version for the Program. 580 | 581 | Later license versions may give you additional or different 582 | permissions. However, no additional obligations are imposed on any 583 | author or copyright holder as a result of your choosing to follow a 584 | later version. 585 | 586 | ### 15. Disclaimer of Warranty. 587 | 588 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 589 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 590 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT 591 | WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT 592 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 593 | A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND 594 | PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE 595 | DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR 596 | CORRECTION. 597 | 598 | ### 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR 602 | CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 603 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES 604 | ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT 605 | NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR 606 | LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM 607 | TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER 608 | PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 609 | 610 | ### 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | ## How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these 626 | terms. 627 | 628 | To do so, attach the following notices to the program. It is safest to 629 | attach them to the start of each source file to most effectively state 630 | the exclusion of warranty; and each file should have at least the 631 | "copyright" line and a pointer to where the full notice is found. 632 | 633 | 634 | Copyright (C) 635 | 636 | This program is free software: you can redistribute it and/or modify 637 | it under the terms of the GNU General Public License as published by 638 | the Free Software Foundation, either version 3 of the License, or 639 | (at your option) any later version. 640 | 641 | This program is distributed in the hope that it will be useful, 642 | but WITHOUT ANY WARRANTY; without even the implied warranty of 643 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 644 | GNU General Public License for more details. 645 | 646 | You should have received a copy of the GNU General Public License 647 | along with this program. If not, see . 648 | 649 | Also add information on how to contact you by electronic and paper 650 | mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands \`show w' and \`show c' should show the 661 | appropriate parts of the General Public License. Of course, your 662 | program's commands might be different; for a GUI interface, you would 663 | use an "about box". 664 | 665 | You should also get your employer (if you work as a programmer) or 666 | school, if any, to sign a "copyright disclaimer" for the program, if 667 | necessary. For more information on this, and how to apply and follow 668 | the GNU GPL, see . 669 | 670 | The GNU General Public License does not permit incorporating your 671 | program into proprietary programs. If your program is a subroutine 672 | library, you may consider it more useful to permit linking proprietary 673 | applications with the library. If this is what you want to do, use the 674 | GNU Lesser General Public License instead of this License. But first, 675 | please read . 676 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 5 | # Light Dict 6 | 7 | Lightweight extension for on-the-fly manipulation to primary selections, especially optimized for Dictionary lookups. 8 | 9 | >L, you know what? The Shinigami only eats apples. —— *Light Yagami*\ 10 | [![license]](/LICENSE.md) 11 | 12 | ![ld](https://user-images.githubusercontent.com/17917040/91119018-d33a1900-e6c4-11ea-9bf0-b1c1a742cfeb.gif) 13 | 14 | ## Installation 15 | 16 | ### Manual 17 | 18 | The latest and supported version should only work on the [current stable version](https://release.gnome.org/calendar/#branches) of GNOME Shell. 19 | 20 | ```bash 21 | git clone https://github.com/tuberry/light-dict.git && cd light-dict 22 | meson setup build && meson install -C build 23 | # meson setup build -Dtarget=system && meson install -C build # system-wide, default --prefix=/usr/local 24 | ``` 25 | 26 | For older versions, it's recommended to install via: 27 | 28 | ```bash 29 | gdbus call --session --dest org.gnome.Shell --object-path /org/gnome/Shell \ 30 | --method org.gnome.Shell.Extensions.InstallRemoteExtension 'light-dict@tuberry.github.io' 31 | ``` 32 | 33 | It's quite the same as installing from: 34 | 35 | ### E.G.O 36 | 37 | [Get it on GNOME Extensions][EGO] 38 | 39 | ## Features 40 | 41 | ### DBus 42 | 43 | For the [DBus] usage, refer to [_ldocr.sh](/cli/_ldocr.sh). 44 | 45 | #### Methods 46 | 47 | ```bash 48 | gdbus introspect --session --dest org.gnome.Shell --object-path /org/gnome/Shell/Extensions/LightDict 49 | ``` 50 | 51 | * The `Get` method is private for the built-in OCR [script](/src/ldocr.py). 52 | 53 | #### Arguments 54 | 55 | ##### OCR 56 | 57 | * args: `a string` (temporary OCR arguments) 58 | 59 | ##### Run 60 | 61 | * type: `'auto'` (follow the trigger) | `'^swift(:.+)?$'` | `'popup'` | `'print'` (directly show `'text'` & `'info'`) 62 | * text: `a string` | `''` (for primary selection) 63 | * info: `a string` (for the `'print'` type) | `''` (for the other types) 64 | * area: `[]` (default to the cursor) | `[x, y, width, height]` (the source area) 65 | 66 | ### OCR 67 | 68 | OCR here is subject to factors such as fonts, colors, and backgrounds, which says any unexpected results are expected, but usually the simpler the scenes the better the results. 69 | 70 | #### Dependencies 71 | 72 | * [opencv-python] 73 | * [pytesseract] 74 | 75 | ![ldpref](https://github.com/user-attachments/assets/c2edd859-75a1-4f94-b15e-94c26f6c6bd5) 76 | 77 | #### Screencast 78 | 79 | 80 | 81 | ### Command 82 | 83 | Use (env)var `LDAPPID` to get the focused app (most likely where the text from); 84 | 85 | ## Notes 86 | 87 | * By lightweight, I mean that it doesn't come with any dictionary sources. :) 88 | * For English-Chinese offline dictionaries, try [dict-ecdict] or [dict-cedict]. 89 | * To customize appearances of some [widgets](/res/style/stylesheet.scss), try [user-theme-x]. 90 | 91 | ## Contributions 92 | 93 | Feel free to open an issue or PR in the repo for any question or idea. 94 | 95 | ### Translations 96 | 97 | To initialize or update the po file from sources: 98 | 99 | ```bash 100 | bash ./cli/update-po.sh [your_lang_code] # like zh_CN, default to $LANG 101 | ``` 102 | 103 | ### Developments 104 | 105 | To install GJS TypeScript type [definitions](https://www.npmjs.com/package/@girs/gnome-shell): 106 | 107 | ```bash 108 | npm install @girs/gnome-shell --save-dev 109 | ``` 110 | 111 | ## Acknowledgements 112 | 113 | * [youdaodict]: the idea of popup 114 | * [swift-selection-search]: the stylesheet of iconbar 115 | * [capture2text]: the idea of bubble OCR (dialog OCR here) 116 | 117 | [opencv-python]:https://github.com/opencv/opencv-python 118 | [dict-cedict]:https://github.com/tuberry/dict-cedict 119 | [dict-ecdict]:https://github.com/tuberry/dict-ecdict 120 | [DBus]:https://www.freedesktop.org/wiki/Software/dbus/ 121 | [user-theme-x]:https://github.com/tuberry/user-theme-x 122 | [youdaodict]:https://github.com/HalfdogStudio/youdaodict 123 | [EGO]:https://extensions.gnome.org/extension/2959/light-dict/ 124 | [license]:https://img.shields.io/badge/license-GPLv3+-green.svg 125 | [swift-selection-search]:https://github.com/CanisLupus/swift-selection-search 126 | [pytesseract]:https://github.com/madmaze/pytesseract 127 | [capture2text]:https://capture2text.sourceforge.net/ 128 | -------------------------------------------------------------------------------- /REUSE.toml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: tuberry 2 | # SPDX-License-Identifier: CC-BY-SA-4.0 3 | 4 | version = 1 5 | 6 | [[annotations]] 7 | path = ["**.in", ".gitignore", "jsconfig.json"] 8 | SPDX-FileCopyrightText = "tuberry" 9 | SPDX-License-Identifier = "GPL-3.0-or-later" 10 | 11 | [[annotations]] 12 | path = ["po/LINGUAS", "po/POTFILES.in"] 13 | SPDX-FileCopyrightText = "tuberry" 14 | SPDX-License-Identifier = "CC-BY-SA-4.0" 15 | 16 | [[annotations]] 17 | path = ["po/**po"] 18 | SPDX-License-Identifier = "GPL-3.0-or-later" 19 | -------------------------------------------------------------------------------- /cli/_ldocr.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # SPDX-FileCopyrightText: tuberry 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | word=word 6 | 7 | gdbus call --session \ 8 | --dest org.gnome.Shell \ 9 | --object-path /org/gnome/Shell/Extensions/LightDict \ 10 | --method org.gnome.Shell.Extensions.LightDict.Run swift "$word" "" [] \ 11 | # --method org.gnome.Shell.Extensions.LightDict.Run print word 词 [] \ 12 | # --method org.gnome.Shell.Extensions.LightDict.OCR -- "-m area -s swift" \ 13 | # &>/dev/null 14 | -------------------------------------------------------------------------------- /cli/gen-icon.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: tuberry 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import Gio from 'gi://Gio'; 5 | 6 | const L = 16; // length (side) 7 | const M = 1 / 16; // margin 8 | const W = 1 - 2 * M; // width (content) 9 | const C = '#28282B'; // color 10 | const XFM = `fill="${C}" transform="translate(${M} ${M}) scale(${W} ${W})"`; 11 | const SVG = `viewBox="0 0 1 1" width="${L}" height="${L}" xmlns="http://www.w3.org/2000/svg"`; 12 | const save = (text, name) => Gio.File.new_for_path(ARGV.concat(name).join('/')) 13 | .replace_contents(text, null, false, Gio.FileCreateFlags.NONE, null); 14 | 15 | let a = 1 / 7, // gap 16 | b = (1 - a) / 2 / 2, // half squircle side length 17 | c = a + b * 2, 18 | d = 2 * a / (1 - a), // d / (4 + d) = a / 2; 19 | e = b * Math.SQRT1_2, // diamond length 20 | box = `M1 0 C0 0 0 0 0 1 S0 2 1 2 h${2 + d * 2} c1 0 1 0 1 -1 s0 -1 -1 -1 L${2 + d} ${1 + d}Z`; // swift box 21 | 22 | for(let x of ['swift', 'popup', 'disable']) { 23 | for(let y of ['passive', 'proactive']) { 24 | save(` 25 | 26 | 27 | 28 | 29 | 30 | 31 | `, `ld-${x}-${y}-symbolic.svg`); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /cli/get-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # SPDX-FileCopyrightText: tuberry 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | RET=$(curl -sSf https://extensions.gnome.org/extension/"$EGO"/ | grep data-svm | sed -e 's/.*: //; s/}}"//') # | xargs -I{} expr {} + 1) 6 | echo "${RET:?'ERROR: Failed to fetch version, build with -Dversion option to skip'}" 7 | -------------------------------------------------------------------------------- /cli/update-po.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # SPDX-FileCopyrightText: tuberry 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | set -e 6 | set -o pipefail 7 | 8 | if [[ -d build ]]; then 9 | meson setup build --wipe 10 | else 11 | meson setup build 12 | fi 13 | LC=${1:-${LANG%%.*}} 14 | # DM=$(gjs -c "print(JSON.parse('$(meson introspect build --projectinfo)').descriptive_name)") 15 | DM=$(meson introspect build --projectinfo | python -c 'import sys,json; print(json.loads(sys.stdin.read())["descriptive_name"])') 16 | meson compile "${DM:?got no pot}-pot" -C build 17 | grep -Fqx "${LC:?got no LC code}" po/LINGUAS || (echo "$LC" >> po/LINGUAS; msginit --no-translator -l "$LC".UTF-8 -i po/"$DM".pot -o po/"$LC".po) 18 | msgmerge --backup=off -q -U po/"$LC".po po/"$DM".pot 19 | printf "\npo/%s.po is ready!\n" "$LC" 20 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noUnusedLocals": true, 4 | "noImplicitAny": true, 5 | "noEmit": true, 6 | "target": "ESNext", 7 | "allowJs": true, 8 | "checkJs": false, 9 | "strict": true, 10 | "module": "ESNext", 11 | "moduleResolution": "Bundler", 12 | "types": ["@girs/gjs", "@girs/gnome-shell/ambient"] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: tuberry 2 | # SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | project( 5 | 'gnome-shell-extension-light-dict', 6 | license: 'GPL-3.0-or-later', 7 | version: '48.0', 8 | meson_version: '>= 1.4.0', 9 | ) 10 | 11 | fs = import('fs') 12 | i18n = import('i18n') 13 | gnome = import('gnome') 14 | 15 | _name = 'Light Dict' 16 | _version = get_option('version') 17 | _title = meson.project_name().replace('gnome-shell-extension-', '') 18 | target = get_option('target') 19 | 20 | metadata = { 21 | 'name': _name, 22 | 'gettext': meson.project_name(), 23 | 'uuid': _title + '@tuberry.github.io', 24 | 'url': 'https://github.com/tuberry' / _title, 25 | 'shell': meson.project_version().split('.')[0], 26 | 'rdnn': 'org.gnome.shell.extensions.' + _title, 27 | 'path': f'/org/gnome/shell/extensions/@_title@/', 28 | 'dbus': 'org.gnome.Shell.Extensions.' + _name.replace(' ', ''), 29 | 'version': _version != 0 ? _version : run_command('cli/get-version.sh', check: true, env: ['EGO=2959' / _title]).stdout().strip(), 30 | 'description': 'Lightweight extension for on-the-fly manipulation to primary selections, especially optimized for Dictionary lookups' 31 | + (target == 'zip' ? '\\n\\nFor support, please report issues via the Homepage link below rather than the review section below it' : '') 32 | } 33 | 34 | if(target == 'system') 35 | datadir = get_option('datadir') 36 | locale_dir = get_option('localedir') 37 | schema_dir = datadir / 'glib-2.0' / 'schemas' 38 | target_dir = datadir / 'gnome-shell' / 'extensions' / metadata['uuid'] 39 | else 40 | target_root = (target == 'local') ? fs.expanduser('~/.local/share/gnome-shell/extensions/') : meson.project_build_root() 41 | target_dir = target_root / metadata['uuid'] 42 | locale_dir = target_dir / 'locale' 43 | schema_dir = target_dir / 'schemas' 44 | endif 45 | 46 | if(target == 'zip') 47 | zip_dir = get_option('desktop') ? fs.expanduser('~/Desktop') : target_root 48 | meson.add_install_script( 49 | find_program('7z'), 50 | 'a', 51 | zip_dir / '@0@_v@1@.zip'.format(metadata['gettext'], metadata['version']), 52 | target_dir / '*' 53 | ) 54 | endif 55 | 56 | subdir('po') 57 | subdir('res') 58 | install_subdir('src', strip_directory: true, install_dir: target_dir) 59 | -------------------------------------------------------------------------------- /meson.options: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: tuberry 2 | # SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | option('target', type: 'combo', choices: ['zip', 'local', 'system'], value: 'local', description: 'build target') 5 | option('version', type: 'integer', min: 0, max: 999, value: 0, description: 'default to get-version.sh') 6 | option('desktop', type: 'boolean', value: false, description: 'save zip to user Desktop or not') 7 | -------------------------------------------------------------------------------- /po/LINGUAS: -------------------------------------------------------------------------------- 1 | nl 2 | zh_CN 3 | -------------------------------------------------------------------------------- /po/POTFILES.in: -------------------------------------------------------------------------------- 1 | src/ui.js 2 | src/ldocr.py 3 | src/extension.js 4 | src/prefs.js 5 | -------------------------------------------------------------------------------- /po/meson.build: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: tuberry 2 | # SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | i18n.gettext(metadata['gettext'], install_dir: locale_dir, preset: 'glib') 5 | -------------------------------------------------------------------------------- /po/nl.po: -------------------------------------------------------------------------------- 1 | # Dutch translations for gnome-shell-extension-light-dict package. 2 | # Copyright (C) 2021 THE gnome-shell-extension-light-dict'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the gnome-shell-extension-light-dict package. 4 | # Automatically generated, 2021. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: gnome-shell-extension-light-dict\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2023-02-12 10:37+0800\n" 11 | "PO-Revision-Date: 2021-09-15 20:14+0200\n" 12 | "Last-Translator: Heimen Stoffels \n" 13 | "Language-Team: none\n" 14 | "Language: nl\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | "X-Generator: Poedit 3.0\n" 20 | 21 | #: src/ui.js:32 22 | msgid "Clear" 23 | msgstr "" 24 | 25 | #: src/ui.js:266 26 | msgid "Click or press ENTER to commit changes" 27 | msgstr "" 28 | 29 | #: src/prefs.js:36 30 | msgid "Basic" 31 | msgstr "Eenvoudig" 32 | 33 | #: src/prefs.js:37 src/prefs.js:620 src/extension.js:647 34 | msgid "Swift" 35 | msgstr "Swift" 36 | 37 | #: src/prefs.js:38 src/prefs.js:620 src/prefs.js:649 38 | msgid "Popup" 39 | msgstr "Pop-up" 40 | 41 | #: src/prefs.js:39 42 | msgid "About" 43 | msgstr "Over" 44 | 45 | #: src/prefs.js:365 46 | msgid "Add" 47 | msgstr "" 48 | 49 | #: src/prefs.js:367 50 | msgid "Remove" 51 | msgstr "" 52 | 53 | #: src/prefs.js:372 54 | msgid "Copy" 55 | msgstr "" 56 | 57 | #: src/prefs.js:374 58 | msgid "Paste" 59 | msgstr "" 60 | 61 | #: src/prefs.js:378 62 | msgid "Paste content parsing failed" 63 | msgstr "" 64 | 65 | #: src/prefs.js:428 src/prefs.js:619 66 | msgid "Click the app icon to remove" 67 | msgstr "Klik op een toepassingspictogram om te verwijderen" 68 | 69 | #: src/prefs.js:428 src/prefs.js:613 70 | msgid "Allowlist" 71 | msgstr "Witte lijst" 72 | 73 | #: src/prefs.js:434 src/prefs.js:502 74 | msgid "Run command" 75 | msgstr "Opdracht uitvoeren" 76 | 77 | #: src/prefs.js:435 src/prefs.js:503 78 | msgid "Command type" 79 | msgstr "Soort opdracht" 80 | 81 | #: src/prefs.js:436 src/prefs.js:505 82 | msgid "Show result" 83 | msgstr "Resultaat tonen" 84 | 85 | #: src/prefs.js:437 src/prefs.js:506 86 | msgid "Copy result" 87 | msgstr "Resultaat kopiëren" 88 | 89 | #: src/prefs.js:438 src/prefs.js:507 90 | msgid "Select result" 91 | msgstr "Resultaat selecteren" 92 | 93 | #: src/prefs.js:439 src/prefs.js:508 94 | msgid "Commit result" 95 | msgstr "Resultaat vastleggen" 96 | 97 | #: src/prefs.js:440 src/prefs.js:509 src/prefs.js:633 98 | msgid "Application list" 99 | msgstr "Toepassingenlijst" 100 | 101 | #: src/prefs.js:441 src/prefs.js:510 102 | msgid "RegExp matcher" 103 | msgstr "RegExp-overeenkomst" 104 | 105 | #: src/prefs.js:504 106 | msgid "Icon name" 107 | msgstr "Pictogramnaam" 108 | 109 | #: src/prefs.js:511 110 | msgid "Icon tooltip" 111 | msgstr "Pictogram-hulpballon" 112 | 113 | #: src/prefs.js:558 114 | #, javascript-format 115 | msgid "Version %d" 116 | msgstr "Versie %d" 117 | 118 | #: src/prefs.js:559 119 | #, fuzzy 120 | msgid "" 121 | "Lightweight extension for on-the-fly manipulation to primary selections, " 122 | "especially optimized for Dictionary lookups." 123 | msgstr "" 124 | "Een lichte uitbreiding voor het uitvoeren van directie acties op selecties, " 125 | "gericht op woordenboekopzoekingen." 126 | 127 | #: src/prefs.js:571 128 | msgid "Leave RegExp/application list blank for no restriction" 129 | msgstr "Laat de lijsten leeg om geen beperkingen in te voeren" 130 | 131 | #: src/prefs.js:572 132 | msgid "Middle click the panel to copy the result to clipboard" 133 | msgstr "Middelklik op het paneel om het resultaat te kopiëren" 134 | 135 | #: src/prefs.js:573 136 | msgid "Substitute LDWORD for the selected text in the command" 137 | msgstr "Vervang LDWORD in de geselecteerde tekst in de opdracht" 138 | 139 | #: src/prefs.js:574 140 | msgid "Simulate keyboard input in JS statement: key(\"Control_L+c\")" 141 | msgstr "" 142 | "Simuleer toetsenbordinvoer in de JS-opdracht key(\"Control_L+c\")" 143 | 144 | #: src/prefs.js:577 145 | msgid "Tips" 146 | msgstr "Tips" 147 | 148 | #: src/prefs.js:612 149 | msgid "Proactive" 150 | msgstr "Pro-actief" 151 | 152 | #: src/prefs.js:612 153 | msgid "Passive" 154 | msgstr "Passief" 155 | 156 | #: src/prefs.js:613 157 | msgid "Blocklist" 158 | msgstr "Zwarte lijst" 159 | 160 | #: src/prefs.js:620 161 | msgid "Disable" 162 | msgstr "Uitschakelen" 163 | 164 | #: src/prefs.js:621 165 | msgid "Word" 166 | msgstr "Woord" 167 | 168 | #: src/prefs.js:621 169 | msgid "Paragraph" 170 | msgstr "Paragraaf" 171 | 172 | #: src/prefs.js:621 173 | msgid "Area" 174 | msgstr "Gebied" 175 | 176 | #: src/prefs.js:621 177 | msgid "Line" 178 | msgstr "Regel" 179 | 180 | #: src/prefs.js:622 src/extension.js:648 181 | msgid "OCR" 182 | msgstr "OCR" 183 | 184 | #: src/prefs.js:622 185 | #, fuzzy 186 | msgid "Depends on python-opencv and python-pytesseract" 187 | msgstr "" 188 | "Afhankelijkheden: python-opencv, python-pytesseract en python-googletrans " 189 | "(optioneel)" 190 | 191 | #: src/prefs.js:625 192 | msgid "Parameters" 193 | msgstr "Aanvullende opties" 194 | 195 | #: src/prefs.js:631 196 | msgid "Enable systray" 197 | msgstr "Systeemvakpictogram tonen" 198 | 199 | #: src/prefs.js:632 200 | msgid "Trigger style" 201 | msgstr "Aanroepingsstijl" 202 | 203 | #: src/prefs.js:632 204 | msgid "Passive means that pressing Alt to trigger" 205 | msgstr "" 206 | 207 | #: src/prefs.js:636 208 | msgid "Shortcut" 209 | msgstr "Sneltoets" 210 | 211 | #: src/prefs.js:637 src/extension.js:643 212 | msgid "Dwell OCR" 213 | msgstr "" 214 | 215 | #: src/prefs.js:638 216 | msgid "Work mode" 217 | msgstr "Werkmodus" 218 | 219 | #: src/prefs.js:641 220 | msgid "Other" 221 | msgstr "" 222 | 223 | #: src/prefs.js:642 224 | msgid "Trim blank lines" 225 | msgstr "Aantal witregels beperken" 226 | 227 | #: src/prefs.js:643 228 | msgid "Autohide interval" 229 | msgstr "Automatisch verbergen na" 230 | 231 | #: src/prefs.js:644 232 | msgid "RegExp filter" 233 | msgstr "RegExp-filter" 234 | 235 | #: src/prefs.js:645 236 | msgid "Panel" 237 | msgstr "Paneel" 238 | 239 | #: src/prefs.js:646 240 | msgid "Hide title" 241 | msgstr "Naam verbergen" 242 | 243 | #: src/prefs.js:647 244 | msgid "Right command" 245 | msgstr "Rechteropdracht" 246 | 247 | #: src/prefs.js:647 248 | msgid "Right click to run and hide panel" 249 | msgstr "Rechtsklikken om uit te voeren en paneel te verbergen" 250 | 251 | #: src/prefs.js:648 252 | msgid "Left command" 253 | msgstr "Linkeropdracht" 254 | 255 | #: src/prefs.js:648 256 | msgid "Left click to run" 257 | msgstr "Linksklikken om uit te voeren" 258 | 259 | #: src/prefs.js:650 260 | msgid "Enable tooltip" 261 | msgstr "Hulpballon tonen" 262 | 263 | #: src/prefs.js:651 264 | msgid "Page size" 265 | msgstr "Paginagrootte" 266 | 267 | #: src/prefs.js:783 268 | msgid "Content copied" 269 | msgstr "" 270 | 271 | #: src/extension.js:644 272 | msgid "Passive mode" 273 | msgstr "Passieve modus" 274 | 275 | #: src/extension.js:646 276 | #, fuzzy 277 | msgid "Trigger" 278 | msgstr "Aanroeping: " 279 | 280 | #: src/extension.js:650 281 | msgid "Settings" 282 | msgstr "Voorkeuren" 283 | 284 | #: src/extension.js:860 285 | #, javascript-format 286 | msgid "Switch to %s style" 287 | msgstr "Overschakelen naar %s" 288 | 289 | #: src/ldocr.py:35 290 | msgid "OCR process failed. (-_-;)" 291 | msgstr "" 292 | 293 | #: src/ldocr.py:56 294 | msgid "show this help message and exit" 295 | msgstr "" 296 | 297 | #: src/ldocr.py:57 298 | #, python-format 299 | msgid "specify work mode: [%(choices)s] (default: %(default)s)" 300 | msgstr "" 301 | 302 | #: src/ldocr.py:58 303 | #, python-format 304 | msgid "specify LD trigger style: [%(choices)s] (default: %(default)s)" 305 | msgstr "" 306 | 307 | #: src/ldocr.py:59 308 | #, python-format 309 | msgid "specify language(s) used by Tesseract OCR (default: %(default)s)" 310 | msgstr "" 311 | 312 | #: src/ldocr.py:60 313 | msgid "specify LD swift style name" 314 | msgstr "" 315 | 316 | #: src/ldocr.py:61 317 | msgid "invoke LD around the cursor" 318 | msgstr "" 319 | 320 | #: src/ldocr.py:62 321 | msgid "flash on the detected area" 322 | msgstr "" 323 | 324 | #: src/ldocr.py:63 325 | msgid "suppress error messages" 326 | msgstr "" 327 | 328 | #: src/ldocr.py:125 329 | msgid "Too marginal. (>_<)" 330 | msgstr "" 331 | 332 | #: src/ldocr.py:134 src/ldocr.py:160 333 | msgid "OCR preprocess failed. (~_~)" 334 | msgstr "" 335 | 336 | #~ msgid "Swift: " 337 | #~ msgstr "Swift: " 338 | 339 | #~ msgid "OCR: " 340 | #~ msgstr "OCR: " 341 | 342 | #~ msgid "Add/remove current app" 343 | #~ msgstr "Huidige toepassing toevoegen/verwijderen" 344 | 345 | #~ msgid "Only one item can be enabled in swift style.\n" 346 | #~ msgstr "In de swift-stijl kan er slechts één item worden ingeschakeld.\n" 347 | 348 | #~ msgid "The first one will be used by default if none is enabled.\n" 349 | #~ msgstr "" 350 | #~ "Als er niks is ingeschakeld, wordt de eerste op de lijst gebruikt.\n" 351 | 352 | #~ msgid "Double click a list item on the left to change the name." 353 | #~ msgstr "Dubbelklik op een lijstitem om de naam ervan te wijzigen." 354 | 355 | #~ msgid "Add the icon to ~/.local/share/icons/hicolor/symbolic/apps/" 356 | #~ msgstr "" 357 | #~ "Voeg het pictogram toe aan ~/.local/share/icons/hicolor/symbolic/apps/" 358 | #~ "" 359 | 360 | #~ msgid "" 361 | #~ "Hold Alt/Shift to function when highlighting in Passive mode" 362 | #~ msgstr "" 363 | #~ "Houd Alt/Shift ingedrukt om uit te voeren tijdens markeren in de " 364 | #~ "passieve modus" 365 | 366 | #~ msgid "Need modifier to trigger or not" 367 | #~ msgstr "Of er een sneltoets moet worden gebruikt bij deze aanroeping" 368 | 369 | #, fuzzy 370 | #~ msgid "Enable OCR" 371 | #~ msgstr "Inschakelen" 372 | 373 | #~ msgid "Common" 374 | #~ msgstr "Algemeen" 375 | -------------------------------------------------------------------------------- /po/zh_CN.po: -------------------------------------------------------------------------------- 1 | # Chinese translations for gnome-shell-extension-light-dict package 2 | # gnome-shell-extension-light-dict 软件包的简体中文翻译. 3 | # Copyright (C) 2021 THE gnome-shell-extension-light-dict'S COPYRIGHT HOLDER 4 | # This file is distributed under the same license as the gnome-shell-extension-light-dict package. 5 | # Automatically generated, 2021. 6 | # 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: gnome-shell-extension-light-dict 55\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2021-08-18 13:20+0800\n" 12 | "PO-Revision-Date: 2021-08-18 13:20+0800\n" 13 | "Last-Translator: Automatically generated\n" 14 | "Language-Team: none\n" 15 | "Language: zh_CN\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: src/ui.js:384 21 | msgid "Mismatched filetype" 22 | msgstr "文件类型不匹配" 23 | 24 | #: src/ui.js:516 25 | msgid "Click or press ENTER to apply changes" 26 | msgstr "单击或按回车应用更改" 27 | 28 | #: src/ldocr.py:39 src/ldocr.py:158 29 | msgid "OCR process failed. (-_-;)" 30 | msgstr "OCR处理失败。(-_-;)" 31 | 32 | #: src/ldocr.py:60 33 | msgid "show this help message and exit" 34 | msgstr "显示此帮助信息并退出" 35 | 36 | #: src/ldocr.py:61 37 | #, python-format 38 | msgid "specify work mode: [%(choices)s] (default: %(default)s)" 39 | msgstr "指定工作模式: [%(choices)s] (默认: %(default)s)" 40 | 41 | #: src/ldocr.py:62 42 | #, python-format 43 | msgid "specify LD trigger style: [%(choices)s] (default: %(default)s)" 44 | msgstr "指定LD触发方式: [%(choices)s] (默认: %(default)s)" 45 | 46 | #: src/ldocr.py:63 47 | #, python-format 48 | msgid "specify language(s) used by Tesseract OCR (default: %(default)s)" 49 | msgstr "指定 Tesseract OCR 所用语言(默认: %(default)s)" 50 | 51 | #: src/ldocr.py:64 52 | msgid "specify LD swift style name" 53 | msgstr "指定LD即时方式名称" 54 | 55 | #: src/ldocr.py:65 56 | msgid "invoke LD around the cursor" 57 | msgstr "在光标附近唤起LD" 58 | 59 | #: src/ldocr.py:66 60 | msgid "flash on the detected area" 61 | msgstr "在目标区域上闪烁" 62 | 63 | #: src/ldocr.py:67 64 | msgid "suppress error messages" 65 | msgstr "禁言报错消息" 66 | 67 | #: src/ldocr.py:142 68 | msgid "OCR preprocess failed. (~_~)" 69 | msgstr "OCR预处理失败。(~_~)" 70 | 71 | #: src/ldocr.py:147 72 | msgid "Too marginal. (>_<)" 73 | msgstr "太靠边了。(>_<)" 74 | 75 | #: src/extension.js:305 76 | msgid "Dwell OCR" 77 | msgstr "悬停取词" 78 | 79 | #: src/extension.js:306 src/prefs.js:234 80 | msgid "OCR" 81 | msgstr "取词" 82 | 83 | #: src/extension.js:321 84 | msgid "Passive mode" 85 | msgstr "被动模式" 86 | 87 | #: src/extension.js:323 88 | msgid "Trigger" 89 | msgstr "触发" 90 | 91 | #: src/extension.js:324 src/prefs.js:208 92 | msgid "Swift" 93 | msgstr "即时" 94 | 95 | #: src/extension.js:327 96 | msgid "Settings" 97 | msgstr "设置" 98 | 99 | #: src/prefs.js:44 100 | msgid "Click the app icon to remove" 101 | msgstr "点击应用图标移除" 102 | 103 | #: src/prefs.js:174 104 | msgid "S_how result" 105 | msgstr "显示结果(_H)" 106 | 107 | #: src/prefs.js:175 108 | msgid "Cop_y result" 109 | msgstr "复制结果(_Y)" 110 | 111 | #: src/prefs.js:176 112 | msgid "A_wait result" 113 | msgstr "等待结果(_W)" 114 | 115 | #: src/prefs.js:176 116 | msgid "Show a spinner when running" 117 | msgstr "等待时显示加载动画" 118 | 119 | #: src/prefs.js:177 120 | msgid "Se_lect result" 121 | msgstr "选取结果(_L)" 122 | 123 | #: src/prefs.js:178 124 | msgid "Co_mmit result" 125 | msgstr "提交结果(_M)" 126 | 127 | #: src/prefs.js:206 128 | msgid "Proactive" 129 | msgstr "主动" 130 | 131 | #: src/prefs.js:206 132 | msgid "Passive" 133 | msgstr "被动" 134 | 135 | #: src/prefs.js:207 src/prefs.js:298 136 | msgid "Whitelist" 137 | msgstr "白名单" 138 | 139 | #: src/prefs.js:207 140 | msgid "Blacklist" 141 | msgstr "黑名单" 142 | 143 | #: src/prefs.js:208 src/prefs.js:231 144 | msgid "Popup" 145 | msgstr "弹出" 146 | 147 | #: src/prefs.js:208 148 | msgid "Disable" 149 | msgstr "禁用" 150 | 151 | #: src/prefs.js:209 152 | msgid "Word" 153 | msgstr "单词" 154 | 155 | #: src/prefs.js:209 156 | msgid "Paragraph" 157 | msgstr "段落" 158 | 159 | #: src/prefs.js:209 160 | msgid "Area" 161 | msgstr "区域" 162 | 163 | #: src/prefs.js:209 164 | msgid "Line" 165 | msgstr "单行" 166 | 167 | #: src/prefs.js:209 168 | msgid "Dialog" 169 | msgstr "对话" 170 | 171 | #: src/prefs.js:210 src/prefs.js:211 172 | msgid "get captured text with the environment variable LDWORD" 173 | msgstr "通过(环境)变量LDWORD获取捕获的文本" 174 | 175 | #: src/prefs.js:221 176 | msgid "Enable s_ystray" 177 | msgstr "系统托盘(_Y)" 178 | 179 | #: src/prefs.js:221 180 | msgid "Scroll to toggle the trigger style" 181 | msgstr "滚动以切换触发方式" 182 | 183 | #: src/prefs.js:222 184 | msgid "_Trigger style" 185 | msgstr "触发方式(_T)" 186 | 187 | #: src/prefs.js:222 188 | msgid "Passive means pressing Alt to trigger" 189 | msgstr "被动即按住 Alt 触发" 190 | 191 | #: src/prefs.js:223 src/prefs.js:298 192 | msgid "_App list" 193 | msgstr "应用名单(_A)" 194 | 195 | #: src/prefs.js:224 196 | msgid "RegE_xp filter" 197 | msgstr "正则过滤(_X)" 198 | 199 | #: src/prefs.js:225 200 | msgid "Autohide inter_val" 201 | msgstr "隐藏延迟(_V)" 202 | 203 | #: src/prefs.js:225 204 | msgid "ms" 205 | msgstr "毫秒" 206 | 207 | #: src/prefs.js:226 208 | msgid "Sp_lice text" 209 | msgstr "拼接文本(_L)" 210 | 211 | #: src/prefs.js:226 212 | msgid "Try to replace redundant line breaks with spaces" 213 | msgstr "尝试以空格替换冗余换行符" 214 | 215 | #: src/prefs.js:227 216 | msgid "Panel" 217 | msgstr "气泡" 218 | 219 | #: src/prefs.js:227 220 | msgid "Middle click to copy the result" 221 | msgstr "中键复制结果" 222 | 223 | #: src/prefs.js:228 224 | msgid "_Enable title" 225 | msgstr "启用标题(_E)" 226 | 227 | #: src/prefs.js:229 228 | msgid "Ri_ght command" 229 | msgstr "右键命令(_G)" 230 | 231 | #: src/prefs.js:229 232 | msgid "Right click to run and hide panel" 233 | msgstr "右键运行并关闭气泡" 234 | 235 | #: src/prefs.js:230 236 | msgid "Le_ft command" 237 | msgstr "左键命令(_F)" 238 | 239 | #: src/prefs.js:230 240 | msgid "Left click to run" 241 | msgstr "左键运行" 242 | 243 | #: src/prefs.js:231 244 | msgid "Scroll to flip pages" 245 | msgstr "滚动翻页" 246 | 247 | #: src/prefs.js:232 248 | msgid "Enable toolt_ip" 249 | msgstr "启用提示(_I)" 250 | 251 | #: src/prefs.js:233 252 | msgid "Page si_ze" 253 | msgstr "页面容量(_Z)" 254 | 255 | #: src/prefs.js:234 256 | msgid "Depends on: " 257 | msgstr "依赖于: " 258 | 259 | #: src/prefs.js:235 260 | msgid "Sho_rtcut" 261 | msgstr "快捷键(_R)" 262 | 263 | #: src/prefs.js:236 264 | msgid "_Dwell OCR" 265 | msgstr "悬停取词(_D)" 266 | 267 | #: src/prefs.js:237 268 | msgid "_Work mode" 269 | msgstr "工作模式(_W)" 270 | 271 | #: src/prefs.js:238 272 | msgid "Other para_meters" 273 | msgstr "其它参数(_M)" 274 | 275 | #: src/prefs.js:284 276 | msgid "_Run command" 277 | msgstr "运行命令(_R)" 278 | 279 | #: src/prefs.js:285 280 | msgid "_Command type" 281 | msgstr "命令类型(_C)" 282 | 283 | #: src/prefs.js:286 284 | msgid "Bash environment variable" 285 | msgstr "Bash 环境变量" 286 | 287 | #: src/prefs.js:287 288 | msgid "the captured text" 289 | msgstr "捕获的文本" 290 | 291 | #: src/prefs.js:288 292 | msgid "the focused app" 293 | msgstr "聚集的应用" 294 | 295 | #: src/prefs.js:289 296 | msgid "JS script statement" 297 | msgstr "JS 脚本语句" 298 | 299 | #: src/prefs.js:290 300 | msgid "open URI with default app" 301 | msgstr "用默认应用打开 URI" 302 | 303 | #: src/prefs.js:291 304 | msgid "simulate keyboard input" 305 | msgstr "模拟键盘输入" 306 | 307 | #: src/prefs.js:292 308 | msgid "copy LDWORD to clipboard" 309 | msgstr "复制 LDWORD 到剪贴板" 310 | 311 | #: src/prefs.js:293 312 | msgid "search LDWORD in Overview" 313 | msgstr "在概览中搜索 LDWORD" 314 | 315 | #: src/prefs.js:294 316 | msgid "some native functions" 317 | msgstr "部分原生函数" 318 | 319 | #: src/prefs.js:296 320 | msgid "_Icon name" 321 | msgstr "图标名称(_I)" 322 | 323 | #: src/prefs.js:299 324 | msgid "RegE_xp matcher" 325 | msgstr "正则匹配(_X)" 326 | 327 | #: src/prefs.js:300 328 | msgid "Ic_on tooltip" 329 | msgstr "图标提示(_O)" 330 | 331 | #: src/prefs.js:320 332 | msgid "Add" 333 | msgstr "添加" 334 | 335 | #: src/prefs.js:321 336 | msgid "Remove" 337 | msgstr "移除" 338 | 339 | #: src/prefs.js:322 340 | msgid "Copy" 341 | msgstr "复制" 342 | 343 | #: src/prefs.js:323 344 | msgid "Paste" 345 | msgstr "粘贴" 346 | 347 | #: src/prefs.js:361 348 | #, javascript-format 349 | msgid "Removed %s command" 350 | msgstr "已移除 %s 命令" 351 | 352 | #: src/prefs.js:367 353 | #, javascript-format 354 | msgid "Copied %s command" 355 | msgstr "已复制 %s 命令" 356 | 357 | #: src/prefs.js:375 358 | msgid "Failed to parse pasted command" 359 | msgstr "无法解析所贴命令" 360 | 361 | #: src/prefs.js:439 362 | msgid "_Basic" 363 | msgstr "基本(_B)" 364 | 365 | #: src/prefs.js:440 366 | msgid "_Swift" 367 | msgstr "即时(_S)" 368 | 369 | #: src/prefs.js:441 370 | msgid "_Popup" 371 | msgstr "弹出(_P)" 372 | 373 | #~ msgid "All" 374 | #~ msgstr "全部" 375 | 376 | #~ msgid "Normal" 377 | #~ msgstr "常规" 378 | 379 | #~ msgid "Symbolic" 380 | #~ msgstr "符号" 381 | 382 | #~ msgid "Use env var LDWORD for the selected text" 383 | #~ msgstr "以环境变量 LDWORD 代替所选文本" 384 | 385 | #~ msgid "O_CR" 386 | #~ msgstr "取词(_C)" 387 | 388 | #~ msgid "" 389 | #~ "Depends on opencv-" 390 | #~ "python and pytesseract" 392 | #~ msgstr "" 393 | #~ "依赖于 opencv-pythonpytesseract" 395 | 396 | #~ msgid "_Other" 397 | #~ msgstr "其它(_O)" 398 | 399 | #~ msgid "Unit: millisecond" 400 | #~ msgstr "单位: 毫秒" 401 | 402 | #~ msgid "Pa_nel" 403 | #~ msgstr "气泡(_N)" 404 | 405 | #~ msgid "Pop_up" 406 | #~ msgstr "弹出(_U)" 407 | 408 | #~ msgid "" 409 | #~ "Bash\n" 410 | #~ "please scrutinize your code as in a terminal\n" 411 | #~ "JS\n" 412 | #~ "open('URI'): open URI with default app\n" 413 | #~ "key('super+a'): simulate keyboard input\n" 414 | #~ "copy(LDWORD): copy LDWORD to clipboard\n" 415 | #~ "search(LDWORD): search LDWORD in Overview\n" 416 | #~ "other: some native functions like LDWORD.trim()" 417 | #~ msgstr "" 418 | #~ "Bash\n" 419 | #~ "请像在终端中一样仔细检查您的代码\n" 420 | #~ "JS\n" 421 | #~ "open('URI'): 用默认应用打开 URI\n" 422 | #~ "key('super+a'): 模拟键盘输入\n" 423 | #~ "copy(LDWORD): 复制 LDWORD 到剪贴板\n" 424 | #~ "search(LDWORD): 在概览中搜索 LDWORD\n" 425 | #~ "其它: 部分原生函数例如 LDWORD.trim()" 426 | 427 | #~ msgid "Use (env) var LDWORD for the selected text" 428 | #~ msgstr "以(环境)变量 LDWORD 代替所选文本" 429 | 430 | #~ msgid "Clear" 431 | #~ msgstr "清除" 432 | 433 | #~ msgid "Allowlist" 434 | #~ msgstr "白名单" 435 | 436 | #~ msgid "Pasted command parsing failed" 437 | #~ msgstr "粘贴命令解析失败" 438 | 439 | #, javascript-format 440 | #~ msgid "Command %s has been removed" 441 | #~ msgstr "%s 命令已被移除" 442 | 443 | #~ msgid "Command copied" 444 | #~ msgstr "命令已复制" 445 | 446 | #~ msgid "Depends on python-opencv and python-pytesseract" 447 | #~ msgstr "依赖于 python-opencv 和 python-pytesseract" 448 | 449 | #~ msgid "T_rim blank lines" 450 | #~ msgstr "去除空行(_R)" 451 | 452 | #, javascript-format 453 | #~ msgid "Visit the homepage for help" 454 | #~ msgstr "访问主页获取帮助" 455 | 456 | #~ msgid "Leave it blank for no such restriction" 457 | #~ msgstr "留空则无此限制" 458 | 459 | #, javascript-format 460 | #~ msgid "Version %d" 461 | #~ msgstr "版本 %d" 462 | 463 | #~ msgid "" 464 | #~ "Lightweight extension for on-the-fly manipulation to primary selections, " 465 | #~ "especially optimized for Dictionary lookups." 466 | #~ msgstr "即时操作所选文本的轻量扩展,为查词而生。" 467 | 468 | #~ msgid "Simulate keyboard input in JS statement: key(\"ctrl+c\")" 469 | #~ msgstr "JS语句模拟键盘输入: key(\"ctrl+c\")" 470 | 471 | #~ msgid "Visit the website above for more information and support" 472 | #~ msgstr "访问上面的网站以获取更多信息与支持" 473 | 474 | #~ msgid "Tips" 475 | #~ msgstr "提示" 476 | 477 | #~ msgid "About" 478 | #~ msgstr "关于" 479 | 480 | #~ msgid "Content copied" 481 | #~ msgstr "内容已复制" 482 | 483 | #, javascript-format 484 | #~ msgid "Switch to %s style" 485 | #~ msgstr "切换至%s风格" 486 | 487 | #~ msgid "Substitute LDWORD for the selected text in the command" 488 | #~ msgstr "以 LDWORD 代替命令中所选文本" 489 | 490 | #~ msgid "Help" 491 | #~ msgstr "帮助" 492 | 493 | #~ msgid "LD DBus error. (~_~)" 494 | #~ msgstr "LD DBus 出错。(~_~)" 495 | 496 | #, python-format 497 | #~ msgid "report error messages (default: %(default)r)" 498 | #~ msgstr "报告错误消息(默认: %(default)r)" 499 | 500 | #~ msgid "Swift: " 501 | #~ msgstr "即时:" 502 | 503 | #~ msgid "OCR: " 504 | #~ msgstr "取词:" 505 | 506 | #~ msgid "Allow/block current app" 507 | #~ msgstr "允许/禁止当前应用" 508 | 509 | #~ msgid "Leave it empty to display the name" 510 | #~ msgstr "留空则显示名称" 511 | 512 | #~ msgid "Add the icon to ~/.local/share/icons/hicolor/symbolic/apps/" 513 | #~ msgstr "添加图标至 ~/.local/share/icons/hicolor/symbolic/apps/" 514 | 515 | #~ msgid "Only one item can be enabled in swift style.\n" 516 | #~ msgstr "即时方式下仅能启用一项。\n" 517 | 518 | #~ msgid "The first one will be used by default if none is enabled.\n" 519 | #~ msgstr "若未启用默认使用第一项。\n" 520 | 521 | #~ msgid "Double click a list item on the left to change the name." 522 | #~ msgstr "双击左侧列表项更改名称。" 523 | 524 | #~ msgid "" 525 | #~ "Hold Alt/Shift to function when highlighting in Passive mode" 526 | #~ msgstr "被动模式高亮文本时需按住 Alt/Shift 来触发" 527 | 528 | #~ msgid "Need modifier to trigger or not" 529 | #~ msgstr "触发需修饰键与否" 530 | 531 | #~ msgid "Enable OCR" 532 | #~ msgstr "启用取词" 533 | 534 | #~ msgid "Common" 535 | #~ msgstr "通用" 536 | -------------------------------------------------------------------------------- /res/data/dbus.xml.in: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /res/data/extension.gresource.xml.in: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @dbus@.xml 5 | 6 | 7 | @0@ 8 | @1@ 9 | @2@ 10 | @3@ 11 | @4@ 12 | @5@ 13 | 14 | 15 | -------------------------------------------------------------------------------- /res/data/meson.build: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: tuberry 2 | # SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | configure_file( 5 | input: 'metadata.json.in', 6 | output: 'metadata.json', 7 | configuration: metadata, 8 | install_dir: target_dir, 9 | ) 10 | 11 | subdir('scalable/status') # HACK: for https://github.com/mesonbuild/meson/issues/2320 12 | foreach name: ['dbus', 'path'] 13 | icon.set(name, metadata[name]) 14 | endforeach 15 | 16 | subdir('theme') 17 | icon.set('theme', theme) 18 | 19 | dbus = configure_file( 20 | input: 'dbus.xml.in', 21 | output: '@0@.xml'.format(metadata['dbus']), 22 | configuration: metadata, 23 | install_dir: (target == 'system') ? datadir / 'dbus-1/interfaces' : '', 24 | ) 25 | 26 | foreach name: ['extension', 'prefs'] 27 | gres = configure_file( 28 | input: f'@name@.gresource.xml.in', 29 | output: f'@name@.gresource.xml', 30 | configuration: icon, 31 | ) 32 | gnome.compile_resources( 33 | name, gres, 34 | source_dir: '@OUTDIR@', 35 | dependencies: [tray, scss, dbus], 36 | gresource_bundle: true, 37 | install: true, 38 | install_dir: target_dir / 'resource', 39 | ) 40 | endforeach 41 | -------------------------------------------------------------------------------- /res/data/metadata.json.in: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@name@", 3 | "description": "@description@", 4 | "gettext-domain": "@gettext@", 5 | "settings-schema": "@rdnn@", 6 | "uuid": "@uuid@", 7 | "shell-version": [ 8 | "@shell@" 9 | ], 10 | "url": "@url@", 11 | "version": @version@ 12 | } 13 | -------------------------------------------------------------------------------- /res/data/prefs.gresource.xml.in: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @theme@ 5 | 6 | 7 | @0@ 8 | @1@ 9 | 10 | 11 | -------------------------------------------------------------------------------- /res/data/scalable/status/meson.build: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: tuberry 2 | # SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | icon = configuration_data() 5 | icons = [] 6 | count = 0 7 | foreach p: ['passive', 'proactive'] 8 | foreach t: ['swift', 'popup', 'disable'] 9 | name = 'ld-' + t + '-' + p + '-symbolic.svg' 10 | icon.set(count.to_string(), 'scalable/status' / name) 11 | icons += name 12 | count += 1 13 | endforeach 14 | endforeach 15 | 16 | tray = custom_target( 17 | output: icons, 18 | build_by_default: true, 19 | build_always_stale: true, 20 | command: [ 21 | find_program('gjs'), 22 | '-m', 23 | '@SOURCE_ROOT@' / 'cli/gen-icon.js', 24 | '@OUTDIR@', 25 | ], 26 | ) 27 | -------------------------------------------------------------------------------- /res/data/theme/meson.build: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: tuberry 2 | # SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | theme = 'style.css' 5 | 6 | scss = custom_target( 7 | input: fs.replace_suffix(theme, '.scss'), 8 | output: theme, 9 | command: [ 10 | find_program('sassc'), 11 | '-t', 12 | 'compressed', 13 | '-a', 14 | '@INPUT@', 15 | '@OUTPUT@' 16 | ], 17 | ) 18 | -------------------------------------------------------------------------------- /res/data/theme/style.scss: -------------------------------------------------------------------------------- 1 | $ac: var(--accent-color); 2 | $abc: var(--accent-bg-color); 3 | $wbc: var(--window-bg-color); 4 | 5 | .ld-apps { 6 | outline: none; 7 | } 8 | 9 | .ld-popover { 10 | caret-color: $ac; 11 | } 12 | 13 | // Ref: https://gist.github.com/JMoerman/6f2fa1494847ce7b7044b99787ccc769 14 | .ld-dragging { 15 | color: $ac; 16 | border-radius: 0.25em; 17 | border: 0.08em dashed $ac; 18 | background: color-mix(in srgb, $wbc 65%, transparent); 19 | } 20 | 21 | .ld-drop-top { 22 | background: linear-gradient( 23 | to bottom, 24 | color-mix(in srgb, $abc 85%, transparent) 0%, 25 | color-mix(in srgb, $abc 0, transparent) 35% 26 | ); 27 | } 28 | 29 | .ld-drop-bottom { 30 | background: linear-gradient( 31 | to bottom, 32 | color-mix(in srgb, $abc 0, transparent) 65%, 33 | color-mix(in srgb, $abc 85%, transparent) 100% 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /res/meson.build: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: tuberry 2 | # SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | subdir('data') 5 | subdir('schema') 6 | 7 | if fs.is_dir('style') 8 | subdir('style') 9 | endif 10 | -------------------------------------------------------------------------------- /res/schema/meson.build: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: tuberry 2 | # SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | gscm = configure_file( 5 | input: 'schemas.gschema.xml.in', 6 | output: '@0@.gschema.xml'.format(metadata['rdnn']), 7 | configuration: metadata, 8 | install_dir: schema_dir, 9 | ) 10 | 11 | if(target == 'system') 12 | gnome.post_install(glib_compile_schemas: true) 13 | elif(target == 'local') 14 | custom_target( 15 | depend_files: [gscm], 16 | output: 'gschemas.compiled', 17 | build_by_default: true, 18 | build_always_stale: true, 19 | command: [ 20 | find_program('glib-compile-schemas'), 21 | '--strict', 22 | '--targetdir=@OUTDIR@', 23 | '@OUTDIR@', 24 | ], 25 | install: true, 26 | install_dir: schema_dir, 27 | ) 28 | endif 29 | -------------------------------------------------------------------------------- /res/schema/schemas.gschema.xml.in: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | false 6 | enable popup panel title 7 | 8 | 9 | false 10 | enable text concatenation 11 | 12 | 13 | false 14 | enable tooltip for popup icon bar 15 | 16 | 17 | true 18 | enable systray 19 | 20 | 21 | true 22 | enable ocr 23 | 24 | 25 | false 26 | enable dwell ocr 27 | 28 | 29 | 30 | 0 31 | need modifier to trigger(1) or not(0) 32 | 33 | 34 | 35 | 2500 36 | autohide popup interval 37 | 38 | 39 | 40 | 5 41 | popup icon bar page size 42 | 43 | 44 | 45 | 0 46 | trigger style: 0 - swift, 1 - popup 2 - disable 47 | 48 | 49 | 50 | 1 51 | global app list type: 0 - whitelist 1 - blacklist 52 | 53 | 54 | 55 | 0 56 | ocr work mode: 0 - word 1 - paragraph 2 - area 3 - line 4 - dialog 57 | 58 | 59 | '' 60 | text filter in proactive mode 61 | 62 | 63 | "" 64 | command executed when right-clicking panel 65 | 66 | 67 | [] 68 | global app whitelist/blacklist 69 | 70 | 71 | "" 72 | parameters passed to the ocr script 73 | 74 | 75 | "" 76 | command executed when left-clicking panel 77 | 78 | 79 | 0 80 | command index of swift style 81 | 82 | 83 | e']]]> 84 | shortcut to invoke the ocr script 85 | 86 | 87 | , 'command': <'echo echo "$LDWORD"'>, 'result': }>]]]> 88 | alternative swift commands 89 | 90 | 91 | , 'enable': , 'type': , 'command': <'search(LDWORD)'>, 'icon': <'system-search-symbolic'>}>]]]> 92 | commands of popup style 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /res/style/gnome-shell-sass/COPYING: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The licenses for most software are designed to take away your 11 | freedom to share and change it. By contrast, the GNU General Public 12 | License is intended to guarantee your freedom to share and change free 13 | software--to make sure the software is free for all its users. This 14 | General Public License applies to most of the Free Software 15 | Foundation's software and to any other program whose authors commit to 16 | using it. (Some other Free Software Foundation software is covered by 17 | the GNU Library General Public License instead.) You can apply it to 18 | your programs, too. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | this service if you wish), that you receive source code or can get it 24 | if you want it, that you can change the software or use pieces of it 25 | in new free programs; and that you know you can do these things. 26 | 27 | To protect your rights, we need to make restrictions that forbid 28 | anyone to deny you these rights or to ask you to surrender the rights. 29 | These restrictions translate to certain responsibilities for you if you 30 | distribute copies of the software, or if you modify it. 31 | 32 | For example, if you distribute copies of such a program, whether 33 | gratis or for a fee, you must give the recipients all the rights that 34 | you have. You must make sure that they, too, receive or can get the 35 | source code. And you must show them these terms so they know their 36 | rights. 37 | 38 | We protect your rights with two steps: (1) copyright the software, and 39 | (2) offer you this license which gives you legal permission to copy, 40 | distribute and/or modify the software. 41 | 42 | Also, for each author's protection and ours, we want to make certain 43 | that everyone understands that there is no warranty for this free 44 | software. If the software is modified by someone else and passed on, we 45 | want its recipients to know that what they have is not the original, so 46 | that any problems introduced by others will not reflect on the original 47 | authors' reputations. 48 | 49 | Finally, any free program is threatened constantly by software 50 | patents. We wish to avoid the danger that redistributors of a free 51 | program will individually obtain patent licenses, in effect making the 52 | program proprietary. To prevent this, we have made it clear that any 53 | patent must be licensed for everyone's free use or not licensed at all. 54 | 55 | The precise terms and conditions for copying, distribution and 56 | modification follow. 57 | 58 | GNU GENERAL PUBLIC LICENSE 59 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 60 | 61 | 0. This License applies to any program or other work which contains 62 | a notice placed by the copyright holder saying it may be distributed 63 | under the terms of this General Public License. The "Program", below, 64 | refers to any such program or work, and a "work based on the Program" 65 | means either the Program or any derivative work under copyright law: 66 | that is to say, a work containing the Program or a portion of it, 67 | either verbatim or with modifications and/or translated into another 68 | language. (Hereinafter, translation is included without limitation in 69 | the term "modification".) Each licensee is addressed as "you". 70 | 71 | Activities other than copying, distribution and modification are not 72 | covered by this License; they are outside its scope. The act of 73 | running the Program is not restricted, and the output from the Program 74 | is covered only if its contents constitute a work based on the 75 | Program (independent of having been made by running the Program). 76 | Whether that is true depends on what the Program does. 77 | 78 | 1. You may copy and distribute verbatim copies of the Program's 79 | source code as you receive it, in any medium, provided that you 80 | conspicuously and appropriately publish on each copy an appropriate 81 | copyright notice and disclaimer of warranty; keep intact all the 82 | notices that refer to this License and to the absence of any warranty; 83 | and give any other recipients of the Program a copy of this License 84 | along with the Program. 85 | 86 | You may charge a fee for the physical act of transferring a copy, and 87 | you may at your option offer warranty protection in exchange for a fee. 88 | 89 | 2. You may modify your copy or copies of the Program or any portion 90 | of it, thus forming a work based on the Program, and copy and 91 | distribute such modifications or work under the terms of Section 1 92 | above, provided that you also meet all of these conditions: 93 | 94 | a) You must cause the modified files to carry prominent notices 95 | stating that you changed the files and the date of any change. 96 | 97 | b) You must cause any work that you distribute or publish, that in 98 | whole or in part contains or is derived from the Program or any 99 | part thereof, to be licensed as a whole at no charge to all third 100 | parties under the terms of this License. 101 | 102 | c) If the modified program normally reads commands interactively 103 | when run, you must cause it, when started running for such 104 | interactive use in the most ordinary way, to print or display an 105 | announcement including an appropriate copyright notice and a 106 | notice that there is no warranty (or else, saying that you provide 107 | a warranty) and that users may redistribute the program under 108 | these conditions, and telling the user how to view a copy of this 109 | License. (Exception: if the Program itself is interactive but 110 | does not normally print such an announcement, your work based on 111 | the Program is not required to print an announcement.) 112 | 113 | These requirements apply to the modified work as a whole. If 114 | identifiable sections of that work are not derived from the Program, 115 | and can be reasonably considered independent and separate works in 116 | themselves, then this License, and its terms, do not apply to those 117 | sections when you distribute them as separate works. But when you 118 | distribute the same sections as part of a whole which is a work based 119 | on the Program, the distribution of the whole must be on the terms of 120 | this License, whose permissions for other licensees extend to the 121 | entire whole, and thus to each and every part regardless of who wrote it. 122 | 123 | Thus, it is not the intent of this section to claim rights or contest 124 | your rights to work written entirely by you; rather, the intent is to 125 | exercise the right to control the distribution of derivative or 126 | collective works based on the Program. 127 | 128 | In addition, mere aggregation of another work not based on the Program 129 | with the Program (or with a work based on the Program) on a volume of 130 | a storage or distribution medium does not bring the other work under 131 | the scope of this License. 132 | 133 | 3. You may copy and distribute the Program (or a work based on it, 134 | under Section 2) in object code or executable form under the terms of 135 | Sections 1 and 2 above provided that you also do one of the following: 136 | 137 | a) Accompany it with the complete corresponding machine-readable 138 | source code, which must be distributed under the terms of Sections 139 | 1 and 2 above on a medium customarily used for software interchange; or, 140 | 141 | b) Accompany it with a written offer, valid for at least three 142 | years, to give any third party, for a charge no more than your 143 | cost of physically performing source distribution, a complete 144 | machine-readable copy of the corresponding source code, to be 145 | distributed under the terms of Sections 1 and 2 above on a medium 146 | customarily used for software interchange; or, 147 | 148 | c) Accompany it with the information you received as to the offer 149 | to distribute corresponding source code. (This alternative is 150 | allowed only for noncommercial distribution and only if you 151 | received the program in object code or executable form with such 152 | an offer, in accord with Subsection b above.) 153 | 154 | The source code for a work means the preferred form of the work for 155 | making modifications to it. For an executable work, complete source 156 | code means all the source code for all modules it contains, plus any 157 | associated interface definition files, plus the scripts used to 158 | control compilation and installation of the executable. However, as a 159 | special exception, the source code distributed need not include 160 | anything that is normally distributed (in either source or binary 161 | form) with the major components (compiler, kernel, and so on) of the 162 | operating system on which the executable runs, unless that component 163 | itself accompanies the executable. 164 | 165 | If distribution of executable or object code is made by offering 166 | access to copy from a designated place, then offering equivalent 167 | access to copy the source code from the same place counts as 168 | distribution of the source code, even though third parties are not 169 | compelled to copy the source along with the object code. 170 | 171 | 4. You may not copy, modify, sublicense, or distribute the Program 172 | except as expressly provided under this License. Any attempt 173 | otherwise to copy, modify, sublicense or distribute the Program is 174 | void, and will automatically terminate your rights under this License. 175 | However, parties who have received copies, or rights, from you under 176 | this License will not have their licenses terminated so long as such 177 | parties remain in full compliance. 178 | 179 | 5. You are not required to accept this License, since you have not 180 | signed it. However, nothing else grants you permission to modify or 181 | distribute the Program or its derivative works. These actions are 182 | prohibited by law if you do not accept this License. Therefore, by 183 | modifying or distributing the Program (or any work based on the 184 | Program), you indicate your acceptance of this License to do so, and 185 | all its terms and conditions for copying, distributing or modifying 186 | the Program or works based on it. 187 | 188 | 6. Each time you redistribute the Program (or any work based on the 189 | Program), the recipient automatically receives a license from the 190 | original licensor to copy, distribute or modify the Program subject to 191 | these terms and conditions. You may not impose any further 192 | restrictions on the recipients' exercise of the rights granted herein. 193 | You are not responsible for enforcing compliance by third parties to 194 | this License. 195 | 196 | 7. If, as a consequence of a court judgment or allegation of patent 197 | infringement or for any other reason (not limited to patent issues), 198 | conditions are imposed on you (whether by court order, agreement or 199 | otherwise) that contradict the conditions of this License, they do not 200 | excuse you from the conditions of this License. If you cannot 201 | distribute so as to satisfy simultaneously your obligations under this 202 | License and any other pertinent obligations, then as a consequence you 203 | may not distribute the Program at all. For example, if a patent 204 | license would not permit royalty-free redistribution of the Program by 205 | all those who receive copies directly or indirectly through you, then 206 | the only way you could satisfy both it and this License would be to 207 | refrain entirely from distribution of the Program. 208 | 209 | If any portion of this section is held invalid or unenforceable under 210 | any particular circumstance, the balance of the section is intended to 211 | apply and the section as a whole is intended to apply in other 212 | circumstances. 213 | 214 | It is not the purpose of this section to induce you to infringe any 215 | patents or other property right claims or to contest validity of any 216 | such claims; this section has the sole purpose of protecting the 217 | integrity of the free software distribution system, which is 218 | implemented by public license practices. Many people have made 219 | generous contributions to the wide range of software distributed 220 | through that system in reliance on consistent application of that 221 | system; it is up to the author/donor to decide if he or she is willing 222 | to distribute software through any other system and a licensee cannot 223 | impose that choice. 224 | 225 | This section is intended to make thoroughly clear what is believed to 226 | be a consequence of the rest of this License. 227 | 228 | 8. If the distribution and/or use of the Program is restricted in 229 | certain countries either by patents or by copyrighted interfaces, the 230 | original copyright holder who places the Program under this License 231 | may add an explicit geographical distribution limitation excluding 232 | those countries, so that distribution is permitted only in or among 233 | countries not thus excluded. In such case, this License incorporates 234 | the limitation as if written in the body of this License. 235 | 236 | 9. The Free Software Foundation may publish revised and/or new versions 237 | of the General Public License from time to time. Such new versions will 238 | be similar in spirit to the present version, but may differ in detail to 239 | address new problems or concerns. 240 | 241 | Each version is given a distinguishing version number. If the Program 242 | specifies a version number of this License which applies to it and "any 243 | later version", you have the option of following the terms and conditions 244 | either of that version or of any later version published by the Free 245 | Software Foundation. If the Program does not specify a version number of 246 | this License, you may choose any version ever published by the Free Software 247 | Foundation. 248 | 249 | 10. If you wish to incorporate parts of the Program into other free 250 | programs whose distribution conditions are different, write to the author 251 | to ask for permission. For software which is copyrighted by the Free 252 | Software Foundation, write to the Free Software Foundation; we sometimes 253 | make exceptions for this. Our decision will be guided by the two goals 254 | of preserving the free status of all derivatives of our free software and 255 | of promoting the sharing and reuse of software generally. 256 | 257 | NO WARRANTY 258 | 259 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 260 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 261 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 262 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 263 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 264 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 265 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 266 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 267 | REPAIR OR CORRECTION. 268 | 269 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 270 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 271 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 272 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 273 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 274 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 275 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 276 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 277 | POSSIBILITY OF SUCH DAMAGES. 278 | 279 | END OF TERMS AND CONDITIONS 280 | 281 | How to Apply These Terms to Your New Programs 282 | 283 | If you develop a new program, and you want it to be of the greatest 284 | possible use to the public, the best way to achieve this is to make it 285 | free software which everyone can redistribute and change under these terms. 286 | 287 | To do so, attach the following notices to the program. It is safest 288 | to attach them to the start of each source file to most effectively 289 | convey the exclusion of warranty; and each file should have at least 290 | the "copyright" line and a pointer to where the full notice is found. 291 | 292 | 293 | Copyright (C) 294 | 295 | This program is free software; you can redistribute it and/or modify 296 | it under the terms of the GNU General Public License as published by 297 | the Free Software Foundation; either version 2 of the License, or 298 | (at your option) any later version. 299 | 300 | This program is distributed in the hope that it will be useful, 301 | but WITHOUT ANY WARRANTY; without even the implied warranty of 302 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 303 | GNU General Public License for more details. 304 | 305 | You should have received a copy of the GNU General Public License 306 | along with this program; if not, write to the Free Software 307 | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 308 | 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Library General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /res/style/gnome-shell-sass/README.md: -------------------------------------------------------------------------------- 1 | # GNOME Shell Sass 2 | GNOME Shell Sass is a project intended to allow the sharing of the 3 | theme sources in sass between gnome-shell and other projects like 4 | gnome-shell-extensions. 5 | 6 | Any changes should be done in the [GNOME Shell subtree][shell-subtree] 7 | and not the stand-alone [gnome-shell-sass repository][sass-repo]. They 8 | will then be synchronized periodically before releases. 9 | 10 | ## License 11 | GNOME Shell Sass is distributed under the terms of the GNU General Public 12 | License, version 2 or later. See the [COPYING][license] file for details. 13 | 14 | [shell-subtree]: https://gitlab.gnome.org/GNOME/gnome-shell/tree/HEAD/data/theme/gnome-shell-sass 15 | [sass-repo]: https://gitlab.gnome.org/GNOME/gnome-shell-sass 16 | [license]: COPYING 17 | -------------------------------------------------------------------------------- /res/style/gnome-shell-sass/_colors.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Main color definitions 3 | // 4 | // When color definition differs for dark and light variant, it gets @if-ed depending on $variant 5 | 6 | @import '_palette.scss'; 7 | @import '_default-colors.scss'; 8 | 9 | 10 | // global colors 11 | $base_color: if($variant == 'light', $light_1, $_base_color_dark); 12 | $bg_color: if($variant == 'light', $_base_color_light, #36363a); 13 | $fg_color: if($variant == 'light', $_base_color_dark, $light_1); 14 | 15 | // OSD elements 16 | $osd_fg_color: $light_1; 17 | $osd_bg_color: lighten($_base_color_dark, 5%); 18 | 19 | // system elements (e.g. the overview) that are always dark 20 | $system_base_color: $_base_color_dark; 21 | $system_fg_color: $_base_color_light; 22 | 23 | // panel colors 24 | $panel_bg_color: if($variant == 'light', $_base_color_light, $dark_5); 25 | $panel_fg_color: if($variant == 'light', $_base_color_dark, $light_1); 26 | 27 | // card elements 28 | $card_bg_color: if($variant == 'light', $light_1, lighten($bg_color, 7%)); 29 | $card_shadow_color: if($variant == 'light', transparentize($dark_5, .97), transparent); 30 | $card_shadow_border_color: if($variant == 'light', transparentize($dark_5, .91), transparent); 31 | 32 | // 33 | // Derived Colors 34 | // 35 | // colors based on the global defines above 36 | 37 | // borders 38 | $borders_color: transparentize($fg_color, $border_opacity); 39 | $outer_borders_color: if($variant == 'light', darken($bg_color, 7%), lighten($bg_color, 5%)); 40 | 41 | // osd colors 42 | $osd_borders_color: transparentize($osd_fg_color, 0.9); 43 | $osd_outer_borders_color: transparentize($osd_fg_color, 0.98); 44 | 45 | // system colors 46 | $system_bg_color: lighten($system_base_color, 5%); 47 | $system_borders_color: transparentize($system_fg_color, .9); 48 | $system_insensitive_fg_color: mix($system_fg_color, $system_bg_color, 50%); 49 | $system_overlay_bg_color: mix($system_base_color, $system_fg_color, 90%); // for non-transparent items, e.g. dash 50 | 51 | // insensitive state 52 | $insensitive_fg_color: if($variant == 'light', mix($fg_color, $bg_color, 60%), mix($fg_color, $bg_color, 50%)); 53 | $insensitive_bg_color: mix($bg_color, $base_color, 60%); 54 | $insensitive_borders_color: mix($borders_color, $base_color, 60%); 55 | 56 | // checked state 57 | $checked_bg_color: if($variant=='light', darken($bg_color, 7%), lighten($bg_color, 7%)); 58 | $checked_fg_color: if($variant=='light', darken($fg_color, 7%), lighten($fg_color, 7%)); 59 | 60 | // hover state 61 | $hover_bg_color: if($variant=='light', darken($bg_color,9%), lighten($bg_color, 10%)); 62 | $hover_fg_color: if($variant=='light', darken($fg_color,9%), lighten($fg_color, 10%)); 63 | 64 | // active state 65 | $active_bg_color: if($variant=='light', darken($bg_color, 11%), lighten($bg_color, 12%)); 66 | $active_fg_color: if($variant=='light', darken($fg_color, 11%), lighten($fg_color, 12%)); 67 | 68 | // accent colors 69 | $accent_borders_color: if($variant== 'light', st-darken(-st-accent-color, 20%), st-lighten(-st-accent-color, 30%)); 70 | -------------------------------------------------------------------------------- /res/style/gnome-shell-sass/_default-colors.scss: -------------------------------------------------------------------------------- 1 | // Named Colors 2 | 3 | // base colors 4 | $_base_color_dark: #222226; 5 | $_base_color_light: #fafafb; 6 | 7 | // accent colors 8 | $accent_color: if($variant== 'light', -st-accent-color, st-mix(-st-accent-color, $light_1, 60%)); 9 | 10 | // colors for destructive elements 11 | $destructive_bg_color: if($variant == 'light', $red_3, $red_4); 12 | $destructive_fg_color: $light_1; 13 | $destructive_color: $destructive_bg_color; 14 | 15 | // colors for levelbars, entries, labels and infobars 16 | $success_bg_color: if($variant == 'light', $green_4, $green_5); 17 | $success_fg_color: $light_1; 18 | $success_color: $success_bg_color; 19 | 20 | $warning_bg_color: if($variant == 'light', $yellow_5, #cd9309); // uses darker off-palette yellow 21 | $warning_fg_color: transparentize(black, .2); 22 | $warning_color: $warning_bg_color; 23 | 24 | $error_bg_color: if($variant == 'light', $red_3, $red_4); 25 | $error_fg_color: $light_1; 26 | $error_color: $error_bg_color; 27 | 28 | // link colors 29 | $link_color: if($variant == 'light', st-darken(-st-accent-color, 10%), st-lighten(-st-accent-color, 20%)); 30 | $link_visited_color: st-transparentize($link_color, .6); 31 | 32 | // special cased widget definitions 33 | $background_mix_factor: if($variant == 'light', 12%, 9%); // used to boost the color of backgrounds in different variants 34 | 35 | // shadows 36 | $shadow_color: if($variant == 'light', rgba(0,0,0,.05), rgba(0,0,0,0.2)); 37 | $text_shadow_color: if($variant == 'light', rgba(255,255,255,0.3), rgba(0,0,0,0.2)); 38 | 39 | // border opacities 40 | $border_opacity: if($variant == 'light', .85, .9); // change the border opacity in different variants 41 | $focus_border_opacity: .2; 42 | 43 | // High Contrast overrides 44 | @if $contrast == 'high' { 45 | // increase border opacity 46 | $border_opacity: .5; 47 | $focus_border_opacity: .1; 48 | // remove shadows 49 | $shadow_color: transparent; 50 | $text_shadow_color: transparent; 51 | } 52 | -------------------------------------------------------------------------------- /res/style/gnome-shell-sass/_palette.scss: -------------------------------------------------------------------------------- 1 | //GNOME Color Palette 2 | $blue_1: #99c1f1; 3 | $blue_2: #62a0ea; 4 | $blue_3: #3584e4; 5 | $blue_4: #1c71d8; 6 | $blue_5: #1a5fb4; 7 | $green_1: #8ff0a4; 8 | $green_2: #57e389; 9 | $green_3: #33d17a; 10 | $green_4: #2ec27e; 11 | $green_5: #26a269; 12 | $yellow_1: #f9f06b; 13 | $yellow_2: #f8e45c; 14 | $yellow_3: #f6d32d; 15 | $yellow_4: #f5c211; 16 | $yellow_5: #e5a50a; 17 | $orange_1: #ffbe6f; 18 | $orange_2: #ffa348; 19 | $orange_3: #ff7800; 20 | $orange_4: #e66100; 21 | $orange_5: #c64600; 22 | $red_1: #f66151; 23 | $red_2: #ed333b; 24 | $red_3: #e01b24; 25 | $red_4: #c01c28; 26 | $red_5: #a51d2d; 27 | $purple_1: #dc8add; 28 | $purple_2: #c061cb; 29 | $purple_3: #9141ac; 30 | $purple_4: #813d9c; 31 | $purple_5: #613583; 32 | $brown_1: #cdab8f; 33 | $brown_2: #b5835a; 34 | $brown_3: #986a44; 35 | $brown_4: #865e3c; 36 | $brown_5: #63452c; 37 | $light_1: #ffffff; 38 | $light_2: #f6f5f4; 39 | $light_3: #deddda; 40 | $light_4: #c0bfbc; 41 | $light_5: #9a9996; 42 | $dark_1: #77767b; 43 | $dark_2: #5e5c64; 44 | $dark_3: #3d3846; 45 | $dark_4: #241f31; 46 | $dark_5: #000000; -------------------------------------------------------------------------------- /res/style/meson.build: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: tuberry 2 | # SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | sheets = ['stylesheet-dark.css', 'stylesheet-light.css'] 5 | 6 | sassc = find_program('sassc', required: true) 7 | foreach sheet: sheets 8 | custom_target( 9 | input: fs.replace_suffix(sheet, '.scss'), 10 | output: sheet, 11 | command: [sassc, '-t', 'expanded', '-a', '@INPUT@', '@OUTPUT@'], 12 | install: true, 13 | install_dir: target_dir, 14 | ) 15 | endforeach 16 | -------------------------------------------------------------------------------- /res/style/stylesheet-dark.scss: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: tuberry 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | $variant: 'dark'; 5 | 6 | @import 'stylesheet.scss'; 7 | -------------------------------------------------------------------------------- /res/style/stylesheet-light.scss: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: tuberry 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | $variant: 'light'; 5 | 6 | @import 'stylesheet.scss'; 7 | -------------------------------------------------------------------------------- /res/style/stylesheet.scss: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: tuberry 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | $contrast: 'normal'; 5 | 6 | @import 'gnome-shell-sass/_colors'; 7 | 8 | $pfx: 'light-dict'; 9 | $radius: 0.4em; 10 | 11 | @function tone($color, $percent: 10%, $reverse: true){ 12 | @return if($variant==if($reverse, 'light', 'dark'), darken($color, $percent), lighten($color, $percent)) 13 | } 14 | 15 | @function st-tone($color, $percent: 10%, $reverse: true){ 16 | @return if($variant==if($reverse, 'light', 'dark'), st-darken($color, $percent), st-lighten($color, $percent)) 17 | } 18 | 19 | .#{$pfx}-box-boxpointer { 20 | // -arrow-rize: 1em; 21 | box-shadow: 0.05em 0.15em 0.25em 0 $shadow_color; 22 | } 23 | 24 | .#{$pfx}-systray:state-busy { 25 | color: st-tone(-st-accent-color, 10%); 26 | } 27 | 28 | .#{$pfx}-spinner { 29 | padding: 0.2em; 30 | border-radius: 0.8em; // percent value not supported 31 | background-color: tone($bg_color, 5%); 32 | box-shadow: 0 0 0.2em 0.2em $shadow_color; 33 | } 34 | 35 | .#{$pfx}-view { 36 | color: $fg_color; 37 | font-size: 1.25em; 38 | border-radius: $radius; 39 | max-width: 40em !important; /* for text line wrap */ 40 | max-height: 30em !important; /* min height for scroll */ 41 | background-color: tone($bg_color, 10%); 42 | border: 0.05em solid tone($outer_borders_color, 13%); 43 | &:state-error { border: 0.1em solid $error_color; } 44 | &:state-empty { border: 0.1em solid $warning_color; font-family: monospace; } 45 | } 46 | 47 | .#{$pfx}-content { 48 | padding: $radius; 49 | border-radius: $radius; 50 | } 51 | 52 | .#{$pfx}-text { 53 | border-width: 0; 54 | border-style: dashed; 55 | border-bottom-width: 0.1em; 56 | border-color: transparentize($bg_color, 0.45); 57 | } 58 | 59 | .#{$pfx}-iconbox { 60 | padding: 0; 61 | border-radius: 0.6em; 62 | & StIcon { 63 | icon-size: 2em; 64 | padding: 0 0.1em; 65 | } 66 | } 67 | 68 | .#{$pfx}-button { 69 | font-weight: bold; 70 | padding: 0.5em 0.3em; 71 | transition-duration: 50ms; 72 | &:hover { 73 | border-width: 0; 74 | border-style: double; 75 | border-bottom-width: 0.2em; 76 | border-color: -st-accent-color; 77 | padding: 0.3em 0.3em 0.5em 0.3em; 78 | } 79 | } 80 | 81 | // .#{$pfx}-tooltip { 82 | // } 83 | -------------------------------------------------------------------------------- /src/const.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: tuberry 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | export const Result = {SHOW: 1 << 0, COPY: 1 << 1, AWAIT: 1 << 2, SELECT: 1 << 3, COMMIT: 1 << 4}; 5 | 6 | export const Key = { 7 | APPS: 'app-list', 8 | APP: 'list-type', 9 | DOCR: 'dwell-ocr', 10 | OCR: 'enable-ocr', 11 | TFLT: 'text-filter', 12 | LCMD: 'left-command', 13 | PSV: 'passive-mode', 14 | HEAD: 'enable-title', 15 | OCRS: 'ocr-work-mode', 16 | PGSZ: 'icon-pagesize', 17 | RCMD: 'right-command', 18 | SCMD: 'swift-command', 19 | SPLC: 'enable-splice', 20 | TRG: 'trigger-style', 21 | OCRP: 'ocr-parameters', 22 | PCMDS: 'popup-commands', 23 | SCMDS: 'swift-commands', 24 | TIP: 'enable-tooltip', 25 | TRAY: 'enable-systray', 26 | WAIT: 'autohide-timeout', 27 | KEYS: 'light-dict-ocr-shortcut', 28 | }; 29 | -------------------------------------------------------------------------------- /src/extension.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: tuberry 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import St from 'gi://St'; 5 | import Gio from 'gi://Gio'; 6 | import GLib from 'gi://GLib'; 7 | import IBus from 'gi://IBus'; 8 | import Meta from 'gi://Meta'; 9 | import Pango from 'gi://Pango'; 10 | import Shell from 'gi://Shell'; 11 | import Clutter from 'gi://Clutter'; 12 | import GObject from 'gi://GObject'; 13 | import Graphene from 'gi://Graphene'; 14 | 15 | import * as Main from 'resource:///org/gnome/shell/ui/main.js'; 16 | import * as Animation from 'resource:///org/gnome/shell/ui/animation.js'; 17 | import * as BoxPointer from 'resource:///org/gnome/shell/ui/boxpointer.js'; 18 | import * as Keyboard from 'resource:///org/gnome/shell/ui/status/keyboard.js'; 19 | import * as PointerWatcher from 'resource:///org/gnome/shell/ui/pointerWatcher.js'; 20 | 21 | import * as T from './util.js'; 22 | import * as M from './menu.js'; 23 | import * as F from './fubar.js'; 24 | import {Key as K, Result} from './const.js'; 25 | 26 | const {_} = F; 27 | const DBusSSS = Main.shellDBusService._screenshotService._senderChecker; 28 | 29 | const Trigger = {SWIFT: 0, POPUP: 1, DISABLE: 2}; 30 | const OCRMode = {WORD: 0, PARAGRAPH: 1, AREA: 2, LINE: 3, DIALOG: 4}; 31 | const Triggers = T.omap(Trigger, ([k, v]) => [[v, k.toLowerCase()]]); 32 | const OCRModes = T.omap(OCRMode, ([k, v]) => [[v, k.toLowerCase()]]); 33 | const Kaomojis = ['_(:з」∠)_', '¯\\_(ツ)_/¯', 'o(T^T)o', 'Σ(ʘωʘノ)ノ', 'ヽ(ー_ー)ノ']; // placeholder 34 | const EvalMask = Object.getOwnPropertyNames(globalThis).filter(x => x !== 'eval').join(','); 35 | const Modifier = {ctrl: Clutter.KEY_Control_L, shift: Clutter.KEY_Shift_L, alt: Clutter.KEY_Alt_L, super: Clutter.KEY_Super_L}; 36 | 37 | const keyval = keysym => Modifier[keysym] ?? Clutter[`KEY_${keysym}`] ?? Clutter.KEY_VoidSymbol; 38 | const approx = (exp, str, nil = true) => T.essay(() => exp ? RegExp(exp, 'u').test(str) : nil, e => (logError(e, exp), nil)); // =~ 39 | const allowed = (cmd, app, str) => cmd ? (!cmd.apps?.length || cmd.apps.includes(app)) && approx(cmd.regexp, str) : false; 40 | const evaluate = (script, scope) => Function(Object.keys(scope).concat(EvalMask).join(','), 41 | `'use strict'; return eval(${JSON.stringify(script)})`)(...Object.values(scope)); 42 | 43 | class GB { 44 | static get ptr() { return global.get_pointer(); }; 45 | static get size() { return global.display.get_size(); } 46 | static get win() { return global.display.get_focus_window(); } 47 | static get csr() { return Meta.prefs_get_cursor_size(); } 48 | } 49 | 50 | class DictBtn extends M.Button { 51 | static { 52 | T.enrol(this); 53 | } 54 | 55 | constructor(click) { 56 | super({styleClass: 'light-dict-button candidate-box'}, () => click(this.$index), null); 57 | } 58 | 59 | setup({icon, name, tooltip}, index, tip) { 60 | if(icon) { 61 | this.set_label(''); 62 | this.set_icon_name(icon); 63 | } else { 64 | this.set_icon_name(''); 65 | this.set_label(name || 'Name'); 66 | } 67 | this.$index = index; 68 | this.setTip(tip ? tooltip : ''); 69 | } 70 | } 71 | 72 | class DictBar extends BoxPointer.BoxPointer { 73 | static { 74 | T.enrol(this, null, {Signals: {'dict-bar-clicked': {param_types: [GObject.TYPE_JSOBJECT]}}}); 75 | } 76 | 77 | constructor(set) { 78 | super(St.Side.BOTTOM); 79 | this.#buildWidgets(); 80 | this.#bindSettings(set); 81 | } 82 | 83 | #buildWidgets() { 84 | this.set({visible: false, styleClass: 'light-dict-bar-boxpointer'}); 85 | this.$src = F.Source.tie({hide: F.Source.newTimer(x => [() => this.dispel(), x])}, this); 86 | this.$box = T.hook({ 87 | 'scroll-event': (...xs) => this.#onScroll(...xs), 88 | 'notify::hover': ({hover}) => this.$src.hide.switch(!hover, this[K.WAIT] / 10), 89 | }, new St.BoxLayout({ 90 | reactive: true, trackHover: true, styleClass: 'light-dict-iconbox candidate-popup-content', 91 | })); 92 | this.bin.set_child(this.$box); 93 | } 94 | 95 | #bindSettings(set) { 96 | this.$set = set.tie([ 97 | K.WAIT, K.PGSZ, [K.TIP, x => this.#onTooltipSet(x)], 98 | [['cmds', K.PCMDS], x => this.#onCommandsSet(x)], 99 | ], this); 100 | } 101 | 102 | #onTooltipSet(tip) { 103 | if(T.xnor(this[K.TIP], tip)) return; 104 | let setup = tip ? (x, i) => x.setTip(this.cmds[i].tooltip) : x => x.setTip(); 105 | [...this.$box].forEach(setup); 106 | } 107 | 108 | #onCommandsSet(commands) { 109 | return T.seq(cmds => T.homolog(this.cmds, cmds, this[K.TIP] ? ['icon', 'name', 'tooltip'] : ['icon', 'name']) || 110 | M.upsert(this.$box, x => x.add_child(new DictBtn(y => { this.dispel(); this.emit('dict-bar-clicked', this.cmds[y]); })), 111 | cmds, (v, x, i) => x.setup(v, i, this[K.TIP]), x => [...x]), commands.filter(x => x.enable)); 112 | } 113 | 114 | #getPages() { 115 | let length = this.cmds.reduce((p, x) => x.$visible ? p + 1 : p, 0); 116 | return length && this[K.PGSZ] ? Math.ceil(length / this[K.PGSZ]) : 0; 117 | } 118 | 119 | #updatePages(pages) { 120 | let icons = [...this.$box].filter((x, i) => (x.visible = this.cmds[i].$visible)); 121 | if(pages < 2) return; 122 | this.$index = this.$index < 1 ? pages : this.$index > pages ? 1 : this.$index ?? 1; 123 | if(this.$index === pages && icons.length % this[K.PGSZ]) { 124 | let start = icons.length - this[K.PGSZ]; 125 | icons.forEach((x, i) => F.view(i >= start, x)); 126 | } else { 127 | let end = this.$index * this[K.PGSZ]; 128 | let start = (this.$index - 1) * this[K.PGSZ]; 129 | icons.forEach((x, i) => F.view(i >= start && i < end, x)); 130 | } 131 | } 132 | 133 | #onScroll(_a, event) { 134 | switch(event.get_scroll_direction()) { 135 | case Clutter.ScrollDirection.UP: this.$index--; break; 136 | case Clutter.ScrollDirection.DOWN: this.$index++; break; 137 | default: return; 138 | } 139 | this.#updatePages(this.#getPages()); 140 | } 141 | 142 | summon(app, str) { 143 | this.cmds.forEach(x => { x.$visible = allowed(x, app, str); }); 144 | let pages = this.#getPages(); 145 | if(pages < 1) return; 146 | if(F.offstage(this)) Main.layoutManager.addTopChrome(this); 147 | this.#updatePages(pages); 148 | this.open(BoxPointer.PopupAnimation.NONE); 149 | this.$src.hide.revive(this[K.WAIT]); 150 | } 151 | 152 | dispel() { 153 | if(F.offstage(this)) return; 154 | this.$src.hide.dispel(); 155 | this.close(BoxPointer.PopupAnimation.FADE); 156 | Main.layoutManager.removeChrome(this); // HACK: workaround for unexpected leave event on reappearing in entered prect 157 | } 158 | } 159 | 160 | class DictBox extends BoxPointer.BoxPointer { 161 | static { 162 | T.enrol(this); 163 | } 164 | 165 | constructor(set) { 166 | super(St.Side.TOP); 167 | this.#buildWidgets(); 168 | this.#bindSettings(set); 169 | } 170 | 171 | #buildWidgets() { 172 | this.set({visible: false, styleClass: 'light-dict-box-boxpointer'}); 173 | this.$src = F.Source.tie({hide: F.Source.newTimer(x => [() => this.dispel(), x])}, this); 174 | this.$view = T.hook({ 175 | 'button-press-event': (...xs) => this.#onClick(...xs), 176 | 'notify::hover': ({hover}) => this.$src.hide.switch(!hover, this[K.WAIT] / 10), 177 | }, new St.ScrollView({ 178 | child: new St.BoxLayout({orientation: Clutter.Orientation.VERTICAL, styleClass: 'light-dict-content'}), 179 | styleClass: 'light-dict-view', overlayScrollbars: true, reactive: true, trackHover: true, 180 | })); 181 | this.$info = this.#insertLabel('light-dict-info'); 182 | this.bin.set_child(this.$view); 183 | } 184 | 185 | #bindSettings(set) { 186 | this.$set = set.tie([ 187 | K.LCMD, K.RCMD, K.WAIT, 188 | [K.HEAD, x => { if(!T.xnor(x, this.$text)) x ? this.$text = this.#insertLabel() : F.omit(this, '$text'); }], 189 | ], this); 190 | } 191 | 192 | #insertLabel(styleClass = 'light-dict-text', index = 0) { 193 | let ret = new St.Label({styleClass}); 194 | ret.clutterText.set({lineWrap: true, ellipsize: Pango.EllipsizeMode.NONE, lineWrapMode: Pango.WrapMode.WORD_CHAR}); 195 | this.$view.child.insert_child_at_index(ret, index); 196 | return ret; 197 | } 198 | 199 | #updateScroll() { 200 | let [, , w, h] = this.get_preferred_size(), 201 | theme = this.$view.get_theme_node(), 202 | limit = theme.get_max_height(); 203 | if(limit <= 0) limit = GB.size.at(1) * 15 / 32; 204 | let scroll = h >= limit; 205 | let count = scroll ? w * limit / (Clutter.Settings.get_default().fontDpi / 1024 * theme.get_font().get_size() / 1024 / 72) ** 2 206 | : [...this.$info.get_text()].reduce((p, x) => p + (GLib.unichar_iswide(x) ? 2 : GLib.unichar_iszerowidth(x) ? 0 : 1), 0); 207 | this.$wait = Math.clamp(this[K.WAIT] * count / 36, 1000, 20000); 208 | this.$view.vscrollbarPolicy = scroll ? St.PolicyType.ALWAYS : St.PolicyType.NEVER; // HACK: workaround for trailing lines with default policy (AUTOMATIC) 209 | this.$view.vadjustment.set_value(0); 210 | } 211 | 212 | #onClick(_a, event) { 213 | switch(event.get_button()) { 214 | case Clutter.BUTTON_MIDDLE: F.copy(this.$info.get_text().slice(1)); break; // HACK: remove workaround ZWSP 215 | case Clutter.BUTTON_PRIMARY: if(this[K.LCMD]) T.execute(this[K.LCMD], {LDWORD: this.$txt}).catch(T.nop); break; 216 | case Clutter.BUTTON_SECONDARY: if(this[K.RCMD]) T.execute(this[K.RCMD], {LDWORD: this.$txt}).catch(T.nop); this.dispel(); break; 217 | } 218 | } 219 | 220 | #setState(error, info) { 221 | let state = error ? 'state-error' : info ? '' : 'state-empty'; 222 | if(this.$state === state) return; 223 | if(this.$state) this.$view.remove_style_pseudo_class(this.$state); 224 | if((this.$state = state)) this.$view.add_style_pseudo_class(this.$state); 225 | } 226 | 227 | summon(info, text, error) { 228 | this.$txt = text; 229 | this.#setState(error, info); 230 | if(F.offstage(this)) Main.layoutManager.addTopChrome(this); 231 | info ||= T.lot(Kaomojis); 232 | try { 233 | Pango.parse_markup(info, -1, ''); 234 | F.marks(this.$info, info); 235 | } catch(e) { 236 | this.$info.set_text(info); 237 | } 238 | this.$text?.set_text(text); 239 | this.#updateScroll(); 240 | this.open(BoxPointer.PopupAnimation.NONE); 241 | this.$src.hide.revive(this.$wait); 242 | } 243 | 244 | dispel() { 245 | if(F.offstage(this)) return; 246 | this.$src.hide.dispel(); 247 | this.prect = this.get_transformed_extents(); 248 | this.close(BoxPointer.PopupAnimation.FADE); 249 | Main.layoutManager.removeChrome(this); 250 | } 251 | } 252 | 253 | class DictAct extends F.Mortal { 254 | constructor(set) { 255 | super(); 256 | this.#bindSettings(set); 257 | this.#buildSources(); 258 | } 259 | 260 | #bindSettings(set) { 261 | this.$set = set.tie([ 262 | [K.TRG, null, x => this.tray?.$menu.trigger.choose(x)], 263 | [K.PSV, x => !!x, x => this.tray?.$menu.passive.setToggleState(x)], 264 | ], this, () => this.#onTrayIconSet(), () => this.tray?.$icon.set_icon_name(this.icon)).tie([ 265 | [['cmds', K.SCMDS], x => this.#onCommandsSet(x)], 266 | [K.TRAY, null, x => this.$src.tray.toggle(x)], 267 | [K.OCR, null, x => this.#onEnableOcrSet(x)], 268 | [K.SCMD, null, x => this.tray?.$menu.cmds.choose(x)], 269 | ], this); 270 | } 271 | 272 | #buildSources() { 273 | let cancel = F.Source.newCancel(), 274 | tty = new F.Source(() => new Gio.SubprocessLauncher({flags: T.PIPE}), x => x.close(), true), 275 | ocr = F.Source.new(() => this.#genOCR(tty.hub), this[K.OCR]), 276 | tray = F.Source.new(() => this.#genSystray(ocr.hub), this[K.TRAY]), 277 | kbd = new F.Source(() => Clutter.get_default_backend().get_default_seat().create_virtual_device(Clutter.InputDeviceType.KEYBOARD_DEVICE), 278 | x => x.run_dispose(), true), // run_dispose to release keys immediately 279 | stroke = new F.Source(x => x.split(/\s+/).map((y, i) => setTimeout(() => this.#stroke(y.split('+'), kbd.hub), i * 50)), 280 | x => x.splice(0).forEach(y => clearTimeout(y))); 281 | this.$src = F.Source.tie({cancel, ocr, tray, tty, stroke, kbd}, this); 282 | } 283 | 284 | get ocr() { 285 | return this.$src.ocr.hub; 286 | } 287 | 288 | get tray() { 289 | return this.$src.tray.hub; 290 | } 291 | 292 | #stroke(keys, kbd) { 293 | keys.forEach(k => kbd.notify_keyval(Clutter.get_current_event_time() * 1000, keyval(k), Clutter.KeyState.PRESSED)); 294 | keys.reverse().forEach(k => kbd.notify_keyval(Clutter.get_current_event_time() * 1000, keyval(k), Clutter.KeyState.RELEASED)); 295 | } 296 | 297 | #genOCR(tty) { 298 | let ret = new F.Mortal(); 299 | this.$set.tie([ 300 | K.OCRP, [K.OCRS, null, x => this.tray?.$menu.ocrMode.choose(x)], 301 | ], ret, () => { ret.cmd = `python ${T.ROOT}/ldocr.py -m ${OCRModes[ret[K.OCRS]]} ${ret[K.OCRP]}`; }).tie([ 302 | [K.KEYS, x => !!x.length, x => ret.$src.keys.toggle(x)], 303 | [K.DOCR, null, x => { ret.$src.dwell.toggle(x); this.tray?.setDwell(x); }], 304 | ], ret); 305 | ret.$genDwellItem = () => new M.SwitchItem(_('Dwell OCR'), ret[K.DOCR], x => this.$set.set(K.DOCR, x)); 306 | ret.$genModeItem = () => new M.RadioItem(_('OCR'), M.RadioItem.getopt(OCRMode), ret[K.OCRS], x => this.$set.set(K.OCRS, x)); 307 | let keys = F.Source.newKeys(this.$set.hub, K.KEYS, () => ret.invoke(), ret[K.KEYS]), 308 | emit = F.Source.newTimer(x => T.seq(() => { ret.ppt = ret.pt; ret.pt = x; }, [() => this.emit('dict-act-dwelled', GB.ptr[2], ret.ppt), 180])), // 180 = 170 + 10 309 | dwell = new F.Source(() => PointerWatcher.getPointerWatcher().addWatch(170, (...xs) => emit.revive(xs)), x => x.remove(), ret[K.DOCR]), 310 | spawn = F.Source.newInjector([tty, {spawnv: (a, f, xs) => T.seq(p => { ret.pid = parseInt(p.get_identifier()); }, f.call(a, ...xs))}, 311 | DBusSSS, [['_isSenderAllowed', async (a, f, xs) => ret.pid === (await Gio.DBus.session.call('org.freedesktop.DBus', '/', 'org.freedesktop.DBus', 312 | 'GetConnectionUnixProcessID', T.pickle(xs), null, Gio.DBusCallFlags.NONE, -1, null)).recursiveUnpack()[0]]]]); 313 | ret.invoke = x => spawn.active || spawn.invoke(() => this.execute(x ? `${ret.cmd} ${x}` : ret.cmd).catch(T.nop).finally(() => delete ret.pid)); 314 | ret.$src = F.Source.tie({spawn, dwell, emit, keys}, ret); 315 | return ret; 316 | } 317 | 318 | #genSystray(ocr) { 319 | let ret = new M.Systray({ 320 | dwell: ocr?.$genDwellItem(), 321 | passive: new M.SwitchItem(_('Passive mode'), this[K.PSV], x => this.$set.set(K.PSV, x ? 1 : 0)), 322 | sep0: new M.Separator(), 323 | trigger: new M.RadioItem(_('Trigger'), M.RadioItem.getopt(Trigger), this[K.TRG], x => this.$set.set(K.TRG, x)), 324 | cmds: new M.RadioItem(_('Swift'), this.cmds.map(x => x.name), this[K.SCMD], x => this.$set.set(K.SCMD, x)), 325 | ocrMode: ocr?.$genModeItem(), 326 | sep1: new M.Separator(), 327 | prefs: new M.Item(_('Settings'), () => F.me().openPreferences()), 328 | }, this.icon); 329 | ret.add_style_class_name('light-dict-systray'); 330 | ret.setDwell = T.thunk(x => { 331 | x ? ret.add_style_pseudo_class('state-busy') : ret.remove_style_pseudo_class('state-busy'); 332 | ret.$menu.dwell.setToggleState(x); 333 | }, ocr?.[K.DOCR]); 334 | ret.connect('scroll-event', (_a, event) => { 335 | switch(event.get_scroll_direction()) { 336 | case Clutter.ScrollDirection.UP: this.$set.set(K.TRG, (this[K.TRG] + 1) % 2); break; 337 | case Clutter.ScrollDirection.DOWN: this.$set.set(K.PSV, this[K.PSV] ? 0 : 1); break; 338 | } 339 | }); 340 | return ret; 341 | } 342 | 343 | #onTrayIconSet() { 344 | this.icon = `ld-${Triggers[this[K.TRG]]}-${this[K.PSV] ? 'passive' : 'proactive'}-symbolic`; 345 | } 346 | 347 | #onEnableOcrSet(enable) { 348 | this.$src.ocr.toggle(enable); 349 | M.record(enable, this.tray, () => this.ocr.$genDwellItem(), 'dwell', 'passive', () => this.ocr.$genModeItem(), 'ocrMode', 'sep1'); 350 | } 351 | 352 | #onCommandsSet(commands) { 353 | return T.seq(x => T.homolog(this.cmds, x, ['name']) || this.$src?.tray.hub?.$menu.cmds.setup(x.map(c => c.name)), commands); 354 | } 355 | 356 | getCommand(name) { 357 | return (name ? this.cmds.find(x => x.name === name) : this.cmds[this[K.SCMD]]) ?? this.cmds[0]; 358 | } 359 | 360 | OCR(args) { 361 | this.ocr?.invoke(args); 362 | } 363 | 364 | stroke(keys) { 365 | this.$src.stroke.revive(keys); 366 | } 367 | 368 | commit(string) { 369 | let mgr = Keyboard.getInputSourceManager(); 370 | if(mgr.currentSource.type !== Keyboard.INPUT_SOURCE_TYPE_IBUS) Main.inputMethod.commit(string); // TODO: not tested 371 | else mgr._ibusManager._panelService?.commit_text(IBus.Text.new_from_string(string)); 372 | } 373 | 374 | execute(cmd, env) { 375 | return T.execute(cmd, env, this.$src.cancel.reborn(), this.$src.tty.hub); 376 | } 377 | } 378 | 379 | class LightDict extends F.Mortal { 380 | constructor(gset) { 381 | super(); 382 | this.#bindSettings(gset); 383 | this.#buildSources(); 384 | this.#buildWidgets(); 385 | } 386 | 387 | #bindSettings(gset) { 388 | this.$set = new F.Setting(gset, [K.TFLT, K.APPS, K.APP, K.SPLC], this); 389 | } 390 | 391 | #buildSources() { 392 | let box = new DictBox(this.$set), 393 | csr = T.seq(x => Main.uiGroup.add_child(x), new Clutter.Actor({opacity: 0, x: 1, y: 1})), // HACK: init pos to avoid misplacing at the first occurrence 394 | act = T.hook({'dict-act-dwelled': (...xs) => this.#onDwell(...xs)}, new DictAct(this.$set)), 395 | bar = T.hook({'dict-bar-clicked': (_a, x) => { this.$lck.dwell[0] = true; this.runCmd(x); }}, new DictBar(this.$set)), 396 | dbus = F.Source.newDBus('org.gnome.Shell.Extensions.LightDict', '/org/gnome/Shell/Extensions/LightDict', this, true), 397 | poll = F.Source.newDefer(() => this.#postPoll(), () => !(GB.ptr.at(2) & Clutter.ModifierType.BUTTON1_MASK), 50), // debounce for GTK+ 398 | wait = new F.Source(() => this.#genSpinner()); 399 | this.$src = F.Source.tie({box, csr, act, bar, dbus, poll, wait}, this); 400 | } 401 | 402 | #buildWidgets() { 403 | this.$lck = {dwell: []}; 404 | F.connect(this, global.display.get_selection(), 'owner-changed', (...xs) => this.#onSelect(...xs), 405 | global.display, 'notify::focus-window', () => { this.dispelAll(); this.#syncApp(); }); 406 | this.#syncApp(); 407 | } 408 | 409 | #genSpinner() { 410 | let [x, y] = GB.ptr, 411 | size = GB.csr >>> 1, 412 | ret = new St.Bin({child: new Animation.Spinner(18), styleClass: 'light-dict-spinner'}); 413 | Main.layoutManager.addTopChrome(ret); 414 | ret.set_position(x + size, y + size); 415 | ret.child.play(); 416 | return ret; 417 | } 418 | 419 | #onSelect(_s, type, src) { 420 | if(type !== St.ClipboardType.PRIMARY || !src || src instanceof Meta.SelectionSourceMemory || 421 | this.#denyApp() || this.denyMdf() || this.$src.act[K.TRG] === Trigger.DISABLE) return; 422 | this.$src.poll.revive(); 423 | } 424 | 425 | #postPoll() { 426 | F.paste(true).then(x => (this.$src.act[K.PSV] || !approx(this[K.TFLT], x, false)) && this.run('auto', x)).catch(T.nop); 427 | } 428 | 429 | #setArea(area) { 430 | this.dispelAll(); 431 | let [x, y, w, h] = area && area[3] < GB.size.at(1) / 2 ? area 432 | : (s => (([a, b], c, d) => [a - c, b - c, d, d])(GB.ptr, s / 2, s * 1.15))(GB.csr); 433 | this.center = area && w > 250; 434 | this.$src.csr.set_position(x, y); 435 | this.$src.csr.set_size(w, h); 436 | } 437 | 438 | #syncApp() { 439 | this.app = (w => w ? Shell.WindowTracker.get_default().get_window_app(w)?.get_id() ?? '' : '')(GB.win); 440 | } 441 | 442 | #denyApp() { 443 | return this[K.APPS].length && T.xnor(this[K.APP], this[K.APPS].includes(this.app)); 444 | } 445 | 446 | denyMdf(mdf = GB.ptr.at(2)) { 447 | return this.$src.act[K.PSV] && !(mdf & Clutter.ModifierType.MOD1_MASK); 448 | } 449 | 450 | #onDwell(_a, mdf, [x, y]) { 451 | let {box, bar, act} = this.$src; 452 | if(this.$lck.dwell.pop() || box.prect?.contains_point(new Graphene.Point().init(x, y)) || act.ocrMode === OCRMode.AREA || 453 | (box.visible && box.$view.hover) || (bar.visible && bar.$box.hover) || this.denyMdf(mdf)) return; 454 | act.OCR('--quiet'); 455 | } 456 | 457 | #postRun(output, result) { 458 | if(result & Result.SHOW) this.print(output); 459 | if(result & Result.COPY) F.copy(output); 460 | if(result & Result.SELECT) F.copy(output, true); 461 | if(result & Result.COMMIT) this.$src.act.commit(output); 462 | } 463 | 464 | async #runSh({command: cmd, result}) { 465 | let env = {LDWORD: this.txt, LDAPPID: this.app}; 466 | if(result) { 467 | try { 468 | if(result & Result.AWAIT) this.#postRun(await this.$src.wait.invoke(() => this.$src.act.execute(cmd, env)), result); 469 | else this.#postRun(await this.$src.act.execute(cmd, env), result); 470 | } catch(e) { 471 | if(!F.Source.cancelled(e)) this.print(e.message, true); 472 | } 473 | } else { 474 | T.execute(cmd, env).catch(logError); 475 | } 476 | } 477 | 478 | #runJS({command, result}) { 479 | try { 480 | let output = evaluate(command, { 481 | open: F.open, 482 | copy: F.copy, 483 | LDWORD: this.txt, 484 | LDAPPID: this.app, 485 | key: x => this.$src.act.stroke(x), 486 | search: x => { Main.overview.show(); Main.overview.searchEntry.set_text(x); }, 487 | }); 488 | if(result) this.#postRun(String(output), result); 489 | } catch(e) { 490 | this.print(e.message, true); 491 | } 492 | } 493 | 494 | dispelAll() { 495 | ['box', 'bar'].forEach(x => this.$src[x].dispel()); 496 | } 497 | 498 | async runCmd(cmd) { 499 | cmd.type ? this.#runJS(cmd) : await this.#runSh(cmd); 500 | } 501 | 502 | async swift(name) { 503 | let cmd = this.$src.act.getCommand(name); 504 | if(allowed(cmd, this.app, this.txt)) await this.runCmd(cmd); 505 | } 506 | 507 | popup() { 508 | this.$src.bar.setPosition(this.$src.csr, 1 / 2); 509 | this.$src.bar.summon(this.app, this.txt); 510 | } 511 | 512 | print(info, error) { 513 | this.$src.box.setPosition(this.$src.csr, this.center ? 1 / 2 : 1 / 10); 514 | this.$src.box.summon(info, this.txt, error); 515 | } 516 | 517 | async run(type, text, info, area) { 518 | this.#setArea(area); 519 | let [kind, name] = type === 'auto' ? [Triggers[this.$src.act[K.TRG]]] : type.split(':'); 520 | this.txt = text || (kind === 'print' ? 'Oops' : await F.paste(true)); 521 | if(this[K.SPLC]) this.txt = this.txt.replace(/(? { 538 | switch(x) { 539 | case 'display': return GB.size; 540 | case 'pointer': return GB.ptr.slice(0, 2); 541 | case 'focused': return (r => r ? [r.x, r.y, r.width, r.height] : null)(GB.win?.get_frame_rect?.()); 542 | default: throw Error(`Unknown property: ${x}`); 543 | } 544 | })])); 545 | } catch(e) { 546 | if(e instanceof GLib.Error) invocation.return_gerror(e); 547 | else invocation.return_error_literal(Gio.DBusError, Gio.DBusError.FAILED, e.message); 548 | } 549 | } 550 | 551 | OCR(args) { 552 | this.dispelAll(); 553 | this.$src.act.OCR(args); 554 | } 555 | } 556 | 557 | export default class extends F.Extension { $klass = LightDict; } 558 | -------------------------------------------------------------------------------- /src/fubar.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: tuberry 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import St from 'gi://St'; 5 | import Gio from 'gi://Gio'; 6 | import GLib from 'gi://GLib'; 7 | import Meta from 'gi://Meta'; 8 | import Shell from 'gi://Shell'; 9 | import GObject from 'gi://GObject'; 10 | 11 | import * as Main from 'resource:///org/gnome/shell/ui/main.js'; 12 | import * as Signals from 'resource:///org/gnome/shell/misc/signals.js'; 13 | import * as FileUtils from 'resource:///org/gnome/shell/misc/fileUtils.js'; 14 | import * as Extensions from 'resource:///org/gnome/shell/extensions/extension.js'; 15 | import * as SignalTracker from 'resource:///org/gnome/shell/misc/signalTracker.js'; 16 | 17 | import * as T from './util.js'; 18 | 19 | const ruin = o => o.destroy(); 20 | const raise = x => { throw Error(x); }; // NOTE: https://github.com/tc39/proposal-throw-expressions#todo 21 | // NOTE: see https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/2542 22 | const onus = o => [o, o[hub]].find(x => GObject.type_is_a(x, GObject.Object) && GObject.signal_lookup('destroy', x)) ?? raise('undestroyable'); 23 | 24 | export const _ = Extensions.gettext; 25 | export const hub = Symbol('Hidden Unique Binder'); 26 | export const offstage = x => !Main.uiGroup.contains(x); 27 | export const me = () => Extension.lookupByURL(import.meta.url); // NOTE: https://github.com/tc39/proposal-json-modules 28 | export const debug = (...xs) => me().getLogger().debug(...xs); // FIXME: see https://gitlab.gnome.org/GNOME/gobject-introspection/-/issues/491 29 | export const theme = () => St.ThemeContext.get_for_stage(global.stage); 30 | export const marks = (x, m) => x.clutterText.set_markup(`\u{200b}${m}`); // HACK: workaround for https://gitlab.gnome.org/GNOME/mutter/-/issues/1324 31 | export const omit = (o, ...ks) => ks.forEach(k => { ruin(o[k]); delete o[k]; }); 32 | export const view = (v, ...ws) => ws.forEach(w => w && !T.xnor(v, w.visible) && (v ? w.show() : w.hide())); // NOTE: https://github.com/tc39/proposal-optional-chaining-assignment 33 | export const connect = (tracker, ...args) => (t => args.reduce((p, x) => (x.connectObject ? p.push([x]) : p.at(-1).push(x), p), []) 34 | .forEach(([emitter, ...xs]) => emitter.connectObject(...xs, t)))(onus(tracker)); 35 | export const disconnect = (tracker, ...args) => (t => args.forEach(emitter => emitter?.disconnectObject(t)))(onus(tracker)); 36 | export const open = uri => Gio.AppInfo.launch_default_for_uri(uri, global.create_app_launch_context(0, -1)); 37 | export const copy = (text, primary) => St.Clipboard.get_default().set_text(primary ? St.ClipboardType.PRIMARY : St.ClipboardType.CLIPBOARD, text); 38 | export const paste = primary => new Promise((resolve, reject) => St.Clipboard.get_default().get_text(primary ? St.ClipboardType.PRIMARY 39 | : St.ClipboardType.CLIPBOARD, (_c, x) => x ? resolve(x) : reject(Error('empty')))); 40 | 41 | export class DBusProxy extends Gio.DBusProxy { 42 | static { 43 | T.enrol(this); 44 | } 45 | 46 | [hub] = new SignalTracker.TransientSignalHolder(this); 47 | 48 | constructor(name, object, callback, hooks, signals, xml, cancel = null, bus = Gio.DBus.session, gFlags = Gio.DBusProxyFlags.NONE) { 49 | let info = Gio.DBusInterfaceInfo.new_for_xml(xml ?? FileUtils.loadInterfaceXML(name)); 50 | super({gConnection: bus, gName: name, gObjectPath: object, gInterfaceInfo: info, gFlags, gInterfaceName: info.name}); 51 | if(signals) T.each(xs => this.connectSignal(...xs), signals, 2); 52 | if(hooks) connect(this, this, ...hooks); 53 | this.init_async(GLib.PRIORITY_DEFAULT, cancel).then(() => callback(this, null)).catch(e => callback(null, e)); 54 | } 55 | 56 | destroy() { 57 | Signals.EventEmitter.prototype.disconnectAll.call(this); 58 | omit(this, hub); 59 | } 60 | } 61 | 62 | export class Mortal extends Signals.EventEmitter { 63 | [hub] = new SignalTracker.TransientSignalHolder(this); 64 | 65 | destroy() { 66 | this.emit('destroy'); 67 | this.disconnectAll(); 68 | omit(this, hub); 69 | } 70 | } 71 | 72 | export class Extension extends Extensions.Extension { 73 | constructor(...args) { 74 | T.load(`${T.ROOT}/resource/extension.gresource`); 75 | super(...args); 76 | } 77 | 78 | enable() { 79 | this[hub] = new this.$klass(this.getSettings()); 80 | } 81 | 82 | disable() { 83 | omit(this, hub); 84 | } 85 | } 86 | 87 | export class Source { 88 | /** @template T * @param {T} doom * @return {T} */ 89 | static tie = (doom, host) => (host.connect('destroy', () => omit(doom, ...Object.keys(doom))), doom); 90 | 91 | static cancelled = error => error.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED); 92 | static newCancel(...args) { 93 | return T.seq(x => { x.reborn = (...ys) => { x.revive(...ys); return x.hub; }; }, 94 | new Source(() => new Gio.Cancellable(), x => x.cancel(), ...args)); 95 | } 96 | 97 | static newDBus(name, path, host, ...args) { 98 | let impl = new Source(x => T.seq(y => y.export(x, path), Gio.DBusExportedObject.wrapJSObject(FileUtils.loadInterfaceXML(name), host)), x => x.unexport()); 99 | return new Source(() => Gio.DBus.own_name(Gio.BusType.SESSION, name, Gio.BusNameOwnerFlags.NONE, x => impl.summon(x), null, null), 100 | x => { Gio.bus_unown_name(x); impl.dispel(); }, ...args); 101 | } 102 | 103 | static newKeys(gset, key, callback, ...args) { 104 | return new Source(() => Main.wm.addKeybinding(key, gset, Meta.KeyBindingFlags.NONE, Shell.ActionMode.ALL, callback), 105 | () => Main.wm.removeKeybinding(key), ...args); 106 | } 107 | 108 | static newTimer(callback, remove = true, clear, ...args) { 109 | return remove ? new Source((...xs) => setTimeout(...callback(...xs)), clear ? x => clear(clearTimeout(x)) : clearTimeout, ...args) 110 | : new Source((...xs) => setInterval(...callback(...xs)), clear ? x => clear(clearInterval(x)) : clearInterval, ...args); 111 | } 112 | 113 | static newDefer(callback, until, interval, clear, ...args) { // polling until... 114 | return Source.new(() => T.seq(async (x, y, z = 0) => { while(!(y = await until(z++))) await new Promise(r => x.revive(r)); callback(y); }, 115 | Source.newTimer(x => [x, interval], true, clear)), ...args); 116 | } 117 | 118 | static newHandler(emitter, signal, callback, ...args) { 119 | return new Source(() => emitter.connect(signal, callback), x => emitter.disconnect(x), ...args); 120 | } 121 | 122 | static newMonitor(file, changed, ...args) { 123 | return new Source((cancel = null) => T.hook({changed}, T.fopen(file).monitor(Gio.FileMonitorFlags.NONE, cancel)), x => x.cancel(), ...args); 124 | } 125 | 126 | static newInjector(overrides, ...args) { 127 | let mgr = new Extensions.InjectionManager(); /* eslint-disable-next-line no-invalid-this */ 128 | return new Source(() => T.each(([p, m]) => T.unit(m, Object.entries).forEach(([n, f]) => mgr.overrideMethod(p, n, g => function (...xs) { return f(this, g, xs); })), overrides, 2), 129 | () => mgr.clear(), ...args); 130 | } 131 | 132 | static new(summon, ...args) { 133 | return new Source(summon, undefined, ...args); 134 | } 135 | 136 | constructor(summon, dispel = ruin, enable, ...args) { 137 | this.summon = (...xs) => { this[hub] = summon(...xs); }; 138 | this.dispel = () => { if(this.active) dispel(this[hub]), delete this[hub]; }; 139 | this.revive = (...xs) => { this.dispel(); this.summon(...xs); }; 140 | this.reload = (...xs) => { if(this.active) this.revive(...xs); }; 141 | this.switch = (b, ...xs) => { b ? this.revive(...xs) : this.dispel(); }; 142 | this.invoke = (f, ...xs) => { this.revive(...xs); return f().finally(() => this.dispel()); }; 143 | this.toggle = (b, ...xs) => { if(!T.xnor(b, this.active)) b ? this.summon(...xs) : this.dispel(); }; 144 | if(enable) this.summon(...args); 145 | } 146 | 147 | get hub() { 148 | return this[hub]; 149 | } 150 | 151 | get active() { 152 | return Object.hasOwn(this, hub); 153 | } 154 | 155 | destroy() { 156 | this.dispel(); 157 | this.despel = this.summon = T.nop; 158 | } 159 | } 160 | 161 | export class Setting { 162 | constructor(gset, ...args) { 163 | this[hub] = T.str(gset) ? new Gio.Settings({schema: gset}) : gset; 164 | this.tie(...args); 165 | } 166 | 167 | get hub() { 168 | return this[hub]; 169 | } 170 | 171 | set(field, value) { 172 | this[hub].set_value(field, new GLib.Variant(this[hub].get_value(field).get_type_string(), value)); 173 | } 174 | 175 | not(field) { 176 | this[hub].set_boolean(field, !this[hub].get_boolean(field)); 177 | } 178 | 179 | tie(ring, host, cast, post) { 180 | T.unit(ring, Object.values).forEach(args => { 181 | let [keys, turn, back, init] = T.unit(args); 182 | let [key, field = keys] = T.unit(keys); 183 | if(key in host) throw Error(`key conflict: ${field}`); 184 | let call = (f, x) => f(x, key) ?? x, 185 | pipe = (f, g) => f ? () => call(f, g()) : g, 186 | read = pipe(turn, () => this[hub].get_value(field).recursiveUnpack()), 187 | load = T.thunk(() => (host[key] = read())); 188 | if(init) return; 189 | let sync = [post, cast, back, load].reduceRight((p, x) => pipe(x, p)); 190 | connect(host, this[hub], `changed::${field}`, () => void sync()); 191 | }); 192 | cast?.(); 193 | return this; 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/ldocr.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # SPDX-FileCopyrightText: tuberry 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | # type: ignore 5 | 6 | import cv2 7 | import string 8 | import gettext 9 | import argparse 10 | import colorsys 11 | import numpy as np 12 | import pytesseract 13 | from pathlib import Path 14 | from gi.repository import Gio, GLib 15 | from tempfile import NamedTemporaryFile 16 | 17 | SCALE = 2 18 | DEBUG = False 19 | CONFIG = r'-c preserve_interword_spaces=1' # HACK: workaround for https://github.com/tesseract-ocr/tesseract/issues/991 20 | 21 | _ = gettext.gettext 22 | 23 | class Result: 24 | def __init__(self, text=None, area=None, error=None, cancel=None): 25 | self.text, self.area, self.error, self.cancel, self.style = text, area, error, cancel, 'swift' 26 | 27 | def set_style(self, style, name): 28 | self.style = style + ':' + name if name else style 29 | 30 | def set_quiet(self, quiet): 31 | if quiet and self.erroneous: self.cancel = True 32 | 33 | @property 34 | def erroneous(self): 35 | return self.error or self.text is None 36 | 37 | @property 38 | def param(self): 39 | style, text, info = ['print', '', self.error or _('OCR process failed. (-_-;)')] if self.erroneous else [self.style, self.text, ''] 40 | return ('Run', ('(sssai)', (style, text.strip(), info, self.area or []))) 41 | 42 | def main(): 43 | locale() 44 | arg = parser() 45 | ret = exe_mode(arg) 46 | if ret.cancel: exit(125) 47 | if arg.flash and ret.area: gs_dbus_call('FlashArea', ('(iiii)', (*ret.area,))) 48 | if arg.cursor: ret.area = None 49 | # ISSUE: https://gitlab.gnome.org/GNOME/mutter/-/issues/207 50 | gs_dbus_call(*ret.param, '', '/Extensions/LightDict', '.Extensions.LightDict') 51 | 52 | def locale(): 53 | domain = 'gnome-shell-extension-light-dict' 54 | locale = Path(__file__).absolute().parent / 'locale' 55 | gettext.bindtextdomain(domain, locale if locale.exists() else None) 56 | gettext.textdomain(domain) 57 | 58 | def parser(): 59 | parser = argparse.ArgumentParser(add_help=False) 60 | parser.add_argument('-h', '--help', help=_('show this help message and exit'), action='help') 61 | parser.add_argument('-m', '--mode', help=_('specify work mode: [%(choices)s] (default: %(default)s)'), default='word', choices=['word', 'paragraph', 'area', 'line', 'dialog']) 62 | parser.add_argument('-s', '--style', help=_('specify LD trigger style: [%(choices)s] (default: %(default)s)'), default='auto', choices=['auto', 'swift', 'popup']) 63 | parser.add_argument('-l', '--lang', help=_('specify language(s) used by Tesseract OCR (default: %(default)s)'), default='eng') 64 | parser.add_argument('-n', '--name', help=_('specify LD swift style name'), action='store', default='') 65 | parser.add_argument('-c', '--cursor', help=_('invoke LD around the cursor'), action=argparse.BooleanOptionalAction) 66 | parser.add_argument('-f', '--flash', help=_('flash on the detected area'), action=argparse.BooleanOptionalAction) 67 | parser.add_argument('-q', '--quiet', help=_('suppress error messages'), action=argparse.BooleanOptionalAction) 68 | return parser.parse_args() 69 | 70 | def gs_dbus_call(method_name, parameters, name='.Screenshot', object_path='/Screenshot', interface_name='.Screenshot'): 71 | proxy = Gio.DBusProxy.new_for_bus_sync(Gio.BusType.SESSION, Gio.DBusProxyFlags.NONE, None, 'org.gnome.Shell' + name, 72 | '/org/gnome/Shell' + object_path, 'org.gnome.Shell' + interface_name, None) 73 | return proxy.call_sync(method_name, parameters and GLib.Variant(*parameters), Gio.DBusCallFlags.NONE, -1, None).unpack() 74 | 75 | def point_in_rect(p, r): return p[0] > r[0] and p[0] < r[0] + r[2] and p[1] > r[1] and p[1] < r[1] + r[3] 76 | 77 | def point_to_rect(p, r): return sum([max(a - b, 0, b - a - c) ** 2 for (a, b, c) in zip(r[0:2], p, r[2:4])]) 78 | 79 | def find_rect(rects, point): return min(filter(lambda x: point_in_rect(point, x), rects), key=lambda x: x[4], default=None) \ 80 | or min(rects, key=lambda x: point_to_rect(point, x), default=None) 81 | 82 | def bincount_img(img, point): 83 | bgcolor = None # Ref: https://stackoverflow.com/a/50900143 ; detect if image bgcolor is dark or not 84 | if point is not None: 85 | bgcolor = img[*reversed(point)] # for dialog 86 | else: 87 | colors = np.ravel_multi_index(img.reshape(-1, img.shape[-1]).T, (256, 256, 256)) 88 | bgcolor = np.unravel_index(np.bincount(colors).argmax(), (256, 256, 256)) 89 | return colorsys.rgb_to_hls(*[x / 255 for x in bgcolor])[1] < 0.5 90 | 91 | def read_img(filename, point=None, trim=False): 92 | img = cv2.imread(filename) 93 | if trim: # HACK: workaround for https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/3143 94 | mock = cv2.imread(filename, cv2.IMREAD_UNCHANGED) 95 | edge = next((x for x in range(min(*mock.shape[:2])) if mock[x][x][3] == 255), 0) 96 | if edge > 0: img = img[edge:-edge, edge:-edge] 97 | return cv2.bitwise_not(img) if bincount_img(img, point) else img 98 | 99 | def dilate_img(image, kernel): # <- grey img 100 | binary = cv2.threshold(image, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1] 101 | return cv2.dilate(binary, cv2.getStructuringElement(cv2.MORPH_RECT, kernel), iterations=3) 102 | 103 | def dialog_img(filename, point): 104 | img = cv2.cvtColor(read_img(filename, point), cv2.COLOR_RGB2GRAY) 105 | h, w = img.shape 106 | dilate = dilate_img(img, (3, 3)) 107 | mask1 = cv2.floodFill(dilate, np.zeros((h + 2, w + 2), np.uint8), point, 0, flags=cv2.FLOODFILL_MASK_ONLY | (255 << 8) | 8)[2] 108 | mask2 = cv2.floodFill(np.zeros((h, w), np.uint8), mask1, (0, 0), 255)[1] 109 | return cv2.bitwise_or(img, cv2.bitwise_or(mask2, mask1[1:-1, 1:-1])) 110 | 111 | def debug_img(image, rects, point): 112 | for x in rects: cv2.rectangle(image, (x[0], x[1]), (x[0] + x[2], x[1] + x[3]), (40, 240, 80), 2) 113 | cv2.circle(image, point, 20, (240, 80, 40)) 114 | cv2.imshow('img', image) 115 | cv2.waitKey(0) 116 | cv2.destroyAllWindows() 117 | 118 | def crop_img(image, point, kernel): 119 | # Ref: https://stackoverflow.com/a/57262099 120 | if len(image.shape) > 2: image = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY) 121 | area = image.shape[0] * image.shape[1] 122 | dilate = dilate_img(image, kernel) 123 | contours = cv2.findContours(dilate, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)[0] 124 | rects = list(filter(lambda x: x[4] > 0.002 and x[4] < 0.95, [x + (x[2] * x[3] / area,) for x in map(cv2.boundingRect, contours)])) 125 | if DEBUG: debug_img(image, rects, point) # cv2.drawContours(img, cs, -1, (40, 240, 80), 2) 126 | return find_rect(rects, point) 127 | 128 | def scale_img(image, rect=None): 129 | img = image if rect is None else image[rect[1]: rect[1] + rect[3], rect[0]: rect[0] + rect[2]] 130 | return cv2.resize(img, None, fx=SCALE, fy=SCALE, interpolation=cv2.INTER_LINEAR) 131 | 132 | def ocr_auto(lang, mode='paragraph'): 133 | ptr, = gs_dbus_call('Get', ('(as)', (['pointer'],)), '', '/Extensions/LightDict', '.Extensions.LightDict')[0] 134 | with NamedTemporaryFile(suffix='.png') as f: 135 | ok, path = gs_dbus_call('Screenshot', ('(bbs)', (False, False, f.name))) 136 | # ok, path = gs_dbus_call('ScreenshotWindow', ('(bbbs)', (False, False, False, f.name))) 137 | if not ok: return Result(error=path) 138 | kernel = (6, 3) if mode == 'line' else (9, 7) if mode == 'paragraph' else (9, 9) 139 | image = dialog_img(path, ptr) if mode == 'dialog' else read_img(path) 140 | crop = crop_img(image, ptr, kernel) 141 | return Result(text=pytesseract.image_to_string(scale_img(image, crop), lang=lang, config=CONFIG).strip() or None, 142 | area=(crop[0], crop[1], crop[2], crop[3])) if crop else Result(error=_('OCR preprocess failed. (~_~)')) 143 | 144 | def ocr_word(lang, size=(250, 50)): 145 | ptr, display = gs_dbus_call('Get', ('(as)', (['pointer', 'display'],)), '', '/Extensions/LightDict', '.Extensions.LightDict')[0] 146 | w, h = [min(a, b - a, c) for (a, b, c) in zip(ptr, display, size)] 147 | if w < 5 or h < 5: return Result(error=_('Too marginal. (>_<)')) 148 | area = [ptr[0] - w, ptr[1] - h, w * 2, h * 2] 149 | with NamedTemporaryFile(suffix='.png') as f: 150 | ok, path = gs_dbus_call('ScreenshotArea', ('(iiiibs)', (*area, False, f.name))) 151 | if not ok: return Result(error=path) 152 | data = pytesseract.image_to_data(scale_img(read_img(path)), output_type=pytesseract.Output.DICT, lang=lang, config=CONFIG) 153 | bins = [[data[x][i] for x in ['left', 'top', 'width', 'height', 'text']] for i, x in enumerate(data['text']) if x] 154 | rect = find_rect(bins, (w * SCALE, h * SCALE)) 155 | if DEBUG: debug_img(scale_img(read_img(path)), bins, (w * SCALE, h * SCALE)) 156 | return Result(text=rect[-1].strip(string.punctuation + '“”‘’,。').strip() or None, 157 | area=(rect[0] / SCALE + area[0], rect[1] / SCALE + area[1], rect[2] / SCALE, rect[3] / SCALE + 5)) \ 158 | if rect else Result(error=_('OCR process failed. (-_-;)')) 159 | 160 | def ocr_area(lang): 161 | area = gs_dbus_call('SelectArea', None) 162 | with NamedTemporaryFile(suffix='.png') as f: 163 | ok, path = gs_dbus_call('ScreenshotArea', ('(iiiibs)', (*area, False, f.name))) 164 | return Result(text=pytesseract.image_to_string(scale_img(read_img(path)), lang=lang, config=CONFIG).strip() or None, 165 | area=area) if ok else Result(error=path) 166 | 167 | def exe_mode(args): 168 | try: 169 | ret = (lambda m: m[0](args.lang, *m[1]))({ 170 | 'word': (ocr_word, ()), 171 | 'area': (ocr_area, ()), 172 | 'paragraph': (ocr_auto, ()), 173 | 'line': (ocr_auto, ('line',)), 174 | 'dialog': (ocr_auto, ('dialog',)), 175 | }[args.mode]) 176 | ret.set_style(args.style, args.name) 177 | ret.set_quiet(args.quiet) 178 | return ret 179 | except GLib.Error as e: 180 | if e.matches(Gio.io_error_quark(), Gio.IOErrorEnum.CANCELLED): return Result(cancel=True) 181 | else: raise 182 | except Exception as e: 183 | return Result(error=str(e)) 184 | 185 | if __name__ == '__main__': 186 | main() 187 | -------------------------------------------------------------------------------- /src/menu.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: tuberry 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import St from 'gi://St'; 5 | import Clutter from 'gi://Clutter'; 6 | 7 | import * as Main from 'resource:///org/gnome/shell/ui/main.js'; 8 | import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js'; 9 | import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js'; 10 | import * as BoxPointer from 'resource:///org/gnome/shell/ui/boxpointer.js'; 11 | 12 | import * as T from './util.js'; 13 | import * as F from './fubar.js'; 14 | 15 | export const Separator = PopupMenu.PopupSeparatorMenuItem; 16 | 17 | export const itemize = (x, y) => Object.values(x).forEach(z => z && y.addMenuItem(z)); 18 | 19 | export function upsert(table, insert, list, update, spread = x => x._getMenuItems()) { 20 | let items = spread(table); 21 | let delta = list.length - items.length; 22 | if(delta > 0) for(let i = 0; i < delta; i++) insert(table); 23 | else if(delta < 0) do items.at(delta).destroy(); while(++delta < 0); 24 | spread(table).forEach((x, i, a) => update(list[i], x, i, a)); 25 | } // NOTE: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/forEach 26 | 27 | export function record(ok, tray, ...args) { 28 | if(!tray) return; 29 | let {menu, $menu} = tray; 30 | T.each(([gen, key, pos]) => { 31 | if(T.xnor(ok, $menu[key])) return; 32 | ok ? menu.addMenuItem($menu[key] = gen?.() ?? new Separator(), 33 | pos ? menu._getMenuItems().findIndex(x => x === $menu[pos]) : undefined) : F.omit($menu, key); 34 | }, args, 3); 35 | } 36 | 37 | export function altNum(key, event, item) { // Ref: https://gitlab.gnome.org/GNOME/mutter/-/blob/main/clutter/clutter/clutter-keysyms.h 38 | return T.seq(x => x && [...item].filter(y => y instanceof St.Button).at(key - Clutter.KEY_1)?.emit('clicked', Clutter.BUTTON_PRIMARY), 39 | event.get_state() & Clutter.ModifierType.MOD1_MASK && key >= Clutter.KEY_0 && key <= Clutter.KEY_9); 40 | } // NOTE: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/find 41 | 42 | export class Systray extends PanelMenu.Button { 43 | static { 44 | T.enrol(this); 45 | } 46 | 47 | constructor(menu, icon = '', pos, box, prop, text) { 48 | let {uuid, metadata: {name}} = F.me(); 49 | super(0.5, text ?? name, !menu); 50 | this.$box = new St.BoxLayout({styleClass: 'panel-status-indicators-box'}); 51 | this.add_child(this.$box); 52 | this.$icon = new St.Icon({iconName: icon, styleClass: 'system-status-icon'}); 53 | this.$box.add_child(this.$icon); 54 | Main.panel.addToStatusArea(uuid, this, pos, box); 55 | if(menu) itemize(this.$menu = menu, this.menu); 56 | this.set(prop); 57 | } 58 | } 59 | 60 | export class Button extends St.Button { 61 | static { 62 | T.enrol(this); 63 | } 64 | 65 | constructor(param, callback, icon = '', tip) { 66 | super({canFocus: true, ...param}); 67 | this.#buildSources(); 68 | this.$callback = callback; 69 | this.set_child(new St.Icon({styleClass: 'popup-menu-icon'})); 70 | this.connect('clicked', (...xs) => this.$callback(...xs)); 71 | if(icon !== null) this.setup(icon); 72 | this.setTip(tip); 73 | } 74 | 75 | #buildSources() { 76 | let tip = new F.Source((...xs) => this.#genTip(...xs)); 77 | let show = F.Source.newTimer(() => [() => this.#showTip(true), 250], true, () => this.#showTip(false)); 78 | this.$src = F.Source.tie({tip, show}, this); 79 | } 80 | 81 | #genTip(text) { 82 | let tip = new BoxPointer.BoxPointer(St.Side.TOP); 83 | tip.bin.set_child(new St.Label({styleClass: 'dash-label'})); 84 | tip.set({$text: text, visible: false, styleClass: 'popup-menu-boxpointer'}); 85 | F.connect(tip, this, 'notify::hover', x => this.$src.show.toggle(x.hover)); 86 | return tip; 87 | } 88 | 89 | #showTip(show) { 90 | if(!this.tip) return; 91 | if(show) { 92 | this.tip.setPosition(this, 0.1); 93 | if(F.offstage(this.tip)) Main.layoutManager.addTopChrome(this.tip); 94 | this.tip.open(BoxPointer.PopupAnimation.FULL); 95 | } else { 96 | if(F.offstage(this.tip)) return; 97 | this.tip.close(BoxPointer.PopupAnimation.FADE); 98 | Main.layoutManager.removeChrome(this.tip); 99 | } 100 | } 101 | 102 | setup(icon) { 103 | this.child.set_icon_name(icon); 104 | } 105 | 106 | get tip() { 107 | return this.$src.tip.hub; 108 | } 109 | 110 | $setTip() { 111 | this.tip?.bin.child.set_text(this.tip.$text); 112 | } 113 | 114 | setTip(tip) { 115 | this.$src.tip.toggle(tip, tip); 116 | this.$setTip(); 117 | } 118 | } 119 | 120 | export class StateButton extends Button { 121 | static { 122 | T.enrol(this); 123 | } 124 | 125 | $setTip() { 126 | this.tip?.bin.child.set_text(this.tip.$text[this.$state ? 0 : 1]); 127 | } 128 | 129 | setup(icon) { 130 | let [state, ...icons] = icon; 131 | this.$icon = icons; 132 | this.toggleState(state ?? this.$state); 133 | } 134 | 135 | toggleState(state = !this.$state) { 136 | if(state === this.$state) return; 137 | this.$state = state; 138 | this.child?.set_icon_name(this.$icon[this.$state ? 0 : 1]); 139 | this.$setTip(); 140 | } 141 | } 142 | 143 | export class Item extends PopupMenu.PopupMenuItem { 144 | static { 145 | T.enrol(this); 146 | } 147 | 148 | constructor(text = '', callback, param, prop) { 149 | super(text, param); 150 | this.$callback = callback; 151 | this.connect('activate', (...xs) => this.$callback(...xs)); 152 | this.set(prop); 153 | } 154 | 155 | setup(label, callback) { 156 | this.label.set_text(label); 157 | this.$callback = callback; 158 | } 159 | } 160 | 161 | export class ToolItem extends PopupMenu.PopupBaseMenuItem { 162 | static { 163 | T.enrol(this); 164 | } 165 | 166 | constructor(tool, param, prop) { 167 | super({activate: false, can_focus: false, ...param}); 168 | this.setup(tool); 169 | this.set(prop); 170 | } 171 | 172 | setup(tool) { 173 | if(this.$tool) F.omit(this, ...this.$tool); 174 | this.$tool = T.unit(tool, Object.entries).flatMap(([k, v]) => { 175 | if(k in this) throw Error(`key conflict: ${k}`); 176 | else return v ? [(this.add_child(this[k] ??= v), k)] : []; 177 | }); 178 | } 179 | } 180 | 181 | export class SwitchItem extends PopupMenu.PopupSwitchMenuItem { 182 | static { 183 | T.enrol(this); 184 | } 185 | 186 | constructor(text, active, callback, param, prop) { 187 | super(text, active, param); 188 | this.connect('toggled', (_a, state) => callback(state)); 189 | this.set(prop); 190 | } 191 | } 192 | 193 | export class RadioItem extends PopupMenu.PopupSubMenuMenuItem { 194 | static { 195 | T.enrol(this); 196 | } 197 | 198 | static getopt = o => T.omap(o, ([k, v]) => [[v, F._(T.upcase(k))]]); 199 | 200 | constructor(category, options, chosen, callback, prop) { 201 | super(''); 202 | this.$category = category; 203 | this.$callback = callback; 204 | this.setup(options, chosen); 205 | this.set(prop); 206 | } 207 | 208 | choose(chosen) { 209 | this.$chosen = chosen; 210 | this.label.set_text(`${this.$category}:${this.$options[chosen] ?? ''}`); 211 | this.menu._getMenuItems().forEach((x, i) => x.setOrnament(chosen === i ? PopupMenu.Ornament.DOT : PopupMenu.Ornament.NO_DOT)); 212 | } 213 | 214 | setup(options, chosen = this.$chosen) { 215 | this.$options = options; 216 | upsert(this.menu, x => x.addMenuItem(new Item()), Object.entries(options), ([k, v], x) => x.setup(v, () => this.$callback(k))); 217 | this.choose(chosen); 218 | } 219 | } 220 | 221 | export class DatumItemBase extends PopupMenu.PopupMenuItem { 222 | static { 223 | T.enrol(this); 224 | } 225 | 226 | constructor(label, icon, callback, datum) { 227 | super(''); 228 | this.set_can_focus(false); 229 | this.label.add_style_class_name(label); 230 | this.label.set({xExpand: true, canFocus: true}); 231 | this.add_child(this.$btn = new Button({styleClass: icon}, () => this.$onClick())); 232 | if(callback) this.$onActivate = callback; 233 | if(datum) this.setup(datum); 234 | } 235 | 236 | #click() { 237 | if(this.$btn.visible) this.$btn.emit('clicked', Clutter.BUTTON_PRIMARY); 238 | } 239 | 240 | vfunc_key_press_event(event) { 241 | if(event.get_key_symbol() === Clutter.KEY_Control_L) this.#click(); 242 | return super.vfunc_key_press_event(event); 243 | } 244 | 245 | activate(event) { 246 | switch(event.type()) { 247 | case Clutter.EventType.BUTTON_RELEASE: 248 | case Clutter.EventType.PAD_BUTTON_RELEASE: 249 | switch(event.get_button()) { 250 | case Clutter.BUTTON_SECONDARY: this.#click(); return; 251 | default: this.$onActivate(); break; 252 | } 253 | break; 254 | default: this.$onActivate(); break; 255 | } 256 | super.activate(event); 257 | } 258 | 259 | destroy() { // HACK: workaround for dangling ref & defocus on destroy & focus 260 | if(this.active) Object.defineProperty(this, 'active', {set: T.nop}); 261 | if(this.active || this.label.has_key_focus() || this.$btn.has_key_focus()) this._getTopMenu()?.actor.grab_key_focus(); 262 | super.destroy(); 263 | } 264 | } 265 | 266 | export class DatasetSection extends PopupMenu.PopupMenuSection { 267 | constructor(gen, dataset) { 268 | super(); 269 | this.$genItem = gen; 270 | if(dataset) this.setup(dataset); 271 | } 272 | 273 | setup(dataset) { 274 | upsert(this, x => x.addMenuItem(this.$genItem()), dataset, (d, x) => x.setup(d)); 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /src/prefs.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: tuberry 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import Adw from 'gi://Adw'; 5 | import Gdk from 'gi://Gdk'; 6 | import Gio from 'gi://Gio'; 7 | import Gtk from 'gi://Gtk'; 8 | import GLib from 'gi://GLib'; 9 | import GObject from 'gi://GObject'; 10 | 11 | import * as UI from './ui.js'; 12 | import * as T from './util.js'; 13 | import {Key as K, Result} from './const.js'; 14 | 15 | const {_, _G} = UI; 16 | const EXE = 'application/x-executable'; 17 | 18 | Gio._promisify(Gdk.Clipboard.prototype, 'read_text_async'); 19 | 20 | class AppItem extends GObject.Object { 21 | static { 22 | T.enrol(this, {chosen: false, app: Gio.DesktopAppInfo}); 23 | } 24 | 25 | constructor(app, callback) { 26 | super(); 27 | this.app = app; 28 | this.toggle = (x = !this.chosen) => { this.chosen = x; callback(); }; 29 | } 30 | } 31 | 32 | class Apps extends UI.DialogButtonBase { 33 | static { 34 | UI.enrol(this); 35 | } 36 | 37 | constructor(tip, param) { 38 | super(null, null, false, param); 39 | if(tip) this.$btn.set_tooltip_text(tip); 40 | this.$btn.set_icon_name('list-add-symbolic'); 41 | this.$getInitial = () => new Set(this.value); 42 | this.prepend(this.$bin = new Gtk.ScrolledWindow({vexpand: false, cssName: 'entry', cssClasses: ['ld-apps'], vscrollbarPolicy: Gtk.PolicyType.NEVER})); 43 | this.bind_property_full('value', this.$bin, 'child', GObject.BindingFlags.SYNC_CREATE, (_b, x) => [true, new UI.Box(x?.map(y => this.$genApp(y)), 44 | {hexpand: true, tooltipText: _('Click the app icon to remove')})], null); 45 | } 46 | 47 | $genDialog(opt) { 48 | return new UI.Dialog(dlg => { 49 | let list = new Gio.ListStore({itemType: AppItem}), 50 | title = T.hook({clicked: () => { [...list].forEach(x => x.toggle(false)); }}, 51 | new Gtk.Button({child: new UI.Sign('edit-clear-symbolic', true), cssClasses: ['flat']})), 52 | factory = T.hook({ 53 | setup: (_f, x) => x.set_child(T.seq(w => w.append(w.$check = new Gtk.Image({iconName: 'object-select-symbolic'})), 54 | new UI.Sign('application-x-executable-symbolic'))), 55 | bind: (_f, {child, item}) => { 56 | item.$bind = item.bind_property('chosen', child.$check, 'visible', GObject.BindingFlags.SYNC_CREATE); 57 | child.setup(item.app.get_icon(), item.app.get_display_name()); 58 | }, 59 | unbind: (_f, {item}) => item.$bind.unbind(), 60 | }, new Gtk.SignalListItemFactory()), 61 | filter = Gtk.CustomFilter.new(null), 62 | select = new Gtk.SingleSelection({model: new Gtk.FilterListModel({model: list, filter})}), 63 | content = T.hook({activate: () => select.get_selected_item().toggle()}, 64 | new Gtk.ListView({singleClickActivate: true, model: select, factory, vexpand: true})), 65 | id, count = () => { clearTimeout(id); id = setTimeout(() => title.child.setup(null, String([...list].filter(y => y.chosen).length)), 50); }; 66 | list.splice(0, 0, (x => opt?.noDisplay ? x : x.filter(y => y.should_show()))(Gio.AppInfo.get_all()).map(x => new AppItem(x, () => count()))); 67 | filter.set_search = s => filter.set_filter_func(s ? (a => x => a.has(x.app.get_id()))(new Set(Gio.DesktopAppInfo.search(s).flat())) : null); 68 | dlg.initChosen = s => [...list].forEach(x => x.toggle(s.has(x.app.get_id()))); // TODO: Iter 69 | dlg.getChosen = () => [...list].filter(x => x.chosen).map(x => x.app.get_id()); 70 | return {content, filter, title}; 71 | }); 72 | } 73 | 74 | $genApp(id) { 75 | let app = Gio.DesktopAppInfo.new(id); 76 | return T.hook({clicked: () => { this.value = this.value.filter(x => x !== id); }}, new Gtk.Button(app 77 | ? {child: new Gtk.Image({gicon: app.get_icon()}), tooltipText: app.get_display_name(), hasFrame: false} 78 | : {iconName: 'system-help-symbolic', tooltipText: id, hasFrame: false})); 79 | } 80 | } 81 | 82 | class SideItem extends GObject.Object { 83 | static { 84 | T.enrol(this, {cmd: null, enable: false}); 85 | } 86 | 87 | constructor(cmd, enable = false) { 88 | super(); 89 | this.cmd = cmd; 90 | this.enable = enable; 91 | } 92 | 93 | setup(key, value) { 94 | if(value) this.cmd[key] = value; 95 | else delete this.cmd[key]; 96 | } 97 | } 98 | 99 | class SideRow extends Gtk.ListBoxRow { 100 | static { 101 | T.enrol(this, null, { 102 | Signals: { 103 | dropped: {param_types: [GObject.TYPE_UINT, GObject.TYPE_UINT]}, 104 | changed: {param_types: [GObject.TYPE_UINT, GObject.TYPE_STRING]}, 105 | toggled: {param_types: [GObject.TYPE_UINT, GObject.TYPE_BOOLEAN]}, 106 | }, 107 | }); 108 | } 109 | 110 | constructor(item, group, param) { 111 | super({hexpand: false, ...param}); 112 | this.$grp = group; 113 | this.$btn = T.hook({toggled: () => this.emit('toggled', this.get_index(), this.$btn.active)}, 114 | new UI.Check({group: group ? new UI.Check() : null})); 115 | this.$txt = T.hook({changed: () => !this.$txt.editing && this.emit('changed', this.get_index(), this.$txt.text)}, 116 | new Gtk.EditableLabel({maxWidthChars: 9})); 117 | this.$img = new Gtk.Image({iconName: 'list-drag-handle-symbolic'}); 118 | this.set_child(new UI.Box([this.$btn, this.$txt, this.$img], {spacing: 5, marginEnd: 5})); 119 | this.$txt.get_delegate().connect('activate', () => this.emit('changed', this.get_index(), this.$txt.text)); 120 | item.bind_property_full('cmd', this.$txt, 'text', GObject.BindingFlags.SYNC_CREATE, (_b, v) => [true, v.name], null); 121 | if(group) item.bind_property('enable', this.$btn, 'active', GObject.BindingFlags.SYNC_CREATE); 122 | else item.bind_property_full('cmd', this.$btn, 'active', GObject.BindingFlags.SYNC_CREATE, (_b, v) => [true, !!v.enable], null); 123 | this.$buildDND(item, this.$img); 124 | } 125 | 126 | $buildDND(item, handle) { // Ref: https://blog.gtk.org/2017/06/01/drag-and-drop-in-lists-revisited/ 127 | handle.add_controller(T.hook({ 128 | prepare: () => Gdk.ContentProvider.new_for_value(this), 129 | drag_begin: (_s, drag) => { 130 | let {width: widthRequest, height: heightRequest} = this.get_allocation(); 131 | let row = new SideRow(item, this.$grp ? new UI.Check() : null, {widthRequest, heightRequest, cssClasses: ['ld-dragging']}); 132 | Gtk.DragIcon.get_for_drag(drag).set_child(row); 133 | drag.set_hotspot(widthRequest - this.$img.get_width() / 2, heightRequest - this.$img.get_height() / 2); 134 | }, 135 | }, new Gtk.DragSource({actions: Gdk.DragAction.MOVE}))); 136 | this.add_controller(T.hook({ 137 | motion: (_t, _x, y) => { 138 | let top = y < this.get_height() / 2; 139 | this.add_css_class(top ? 'ld-drop-top' : 'ld-drop-bottom'); 140 | this.remove_css_class(top ? 'ld-drop-bottom' : 'ld-drop-top'); 141 | return Gdk.DragAction.MOVE; 142 | }, 143 | drop: (_t, src, _x, y) => { 144 | this.#clearDropStyle(); 145 | if(src.$grp !== this.$grp) return false; 146 | let drag = src.get_index(), 147 | target = this.get_index() + (y > this.get_height() / 2), 148 | drop = target > drag ? target - 1 : target; 149 | return T.seq(x => x && this.emit('dropped', drag, drop), drag !== drop); 150 | }, 151 | leave: () => this.#clearDropStyle(), 152 | }, Gtk.DropTarget.new(SideRow, Gdk.DragAction.MOVE))); 153 | } 154 | 155 | #clearDropStyle() { 156 | this.remove_css_class('ld-drop-top'); 157 | this.remove_css_class('ld-drop-bottom'); 158 | } 159 | 160 | editName() { 161 | this.$txt.grab_focus(); 162 | this.$txt.start_editing(); 163 | } 164 | } 165 | 166 | class ResultRows extends GObject.Object { 167 | static { 168 | UI.enrol(this, ['uint', 0, GLib.MAXINT32, 0]); 169 | } 170 | 171 | addToPane(addRow) { 172 | this.addToPane = null; 173 | [ 174 | [Result.SHOW, [_('S_how result')], new UI.Switch()], 175 | [Result.COPY, [_('Cop_y result')], new UI.Switch()], 176 | [Result.AWAIT, [_('A_wait result'), _('Show a spinner when running')], new UI.Switch()], 177 | [Result.SELECT, [_('Se_lect result')], new UI.Switch()], 178 | [Result.COMMIT, [_('Co_mmit result')], new UI.Switch()], 179 | ].forEach(([mask, titles, widget]) => { 180 | addRow(titles, widget); 181 | this.bind_property_full('value', widget, 'active', T.BIND, (_b, v) => (x => [x ^ widget.active, x])(!!(v & mask)), 182 | (_b, v) => [!!(this.value & mask) ^ v, this.value ^ mask]); 183 | }); 184 | } 185 | } 186 | 187 | class PrefsBasic extends UI.Page { 188 | static { 189 | T.enrol(this); 190 | } 191 | 192 | $buildWidgets() { 193 | return [ 194 | [K.APPS, new Apps()], 195 | [K.KEYS, new UI.Keys()], 196 | [K.OCR, new UI.Switch()], 197 | [K.DOCR, new UI.Switch()], 198 | [K.HEAD, new UI.Switch()], 199 | [K.TRAY, new UI.Switch()], 200 | [K.TIP, new UI.Switch()], 201 | [K.SPLC, new UI.Switch()], 202 | [K.OCRP, new UI.Entry('-h')], 203 | [K.TFLT, new UI.Entry('\\W')], 204 | [K.PGSZ, new UI.Spin(1, 10, 1)], 205 | [K.WAIT, new UI.Spin(1000, 20000, 250)], 206 | [K.PSV, new UI.Drop([_('Proactive'), _('Passive')])], 207 | [K.APP, new UI.Drop([_('Whitelist'), _('Blacklist')])], 208 | [K.TRG, new UI.Drop([_('Swift'), _('Popup'), _('Disable')])], 209 | [K.OCRS, new UI.Drop([_('Word'), _('Paragraph'), _('Area'), _('Line'), _('Dialog')])], 210 | [K.LCMD, new UI.Entry('notify-send "$LDWORD"', [EXE], _('get captured text with the environment variable LDWORD'))], 211 | [K.RCMD, new UI.Entry('notify-send "$LDWORD"', [EXE], _('get captured text with the environment variable LDWORD'))], 212 | ]; 213 | } 214 | 215 | $buildUI() { 216 | let opencv = 'opencv-python', 217 | tesseract = 'pytesseract', 218 | ocr = T.seq(w => T.execute(`python ${T.ROOT}/ldocr.py -h`).then(x => w.setup(x, {selectable: true, cssClasses: ['ld-popover']})).catch(e => w.setup(e.message, null, true)), 219 | new UI.Help(null, null, {popover: T.hook({'notify::visible': w => w.child.select_region(-1, -1)}, new Gtk.Popover())})); // HACK: workaround for full selection on popup 220 | this.$add([null, [ 221 | [[_('Enable s_ystray'), _('Scroll to toggle the trigger style')], K.TRAY], 222 | [[_('_Trigger style'), _('Passive means pressing Alt to trigger')], K.PSV, K.TRG], 223 | [[_('_App list')], K.APPS, K.APP], 224 | [[_('RegE_xp filter')], K.TFLT], 225 | [[_('Autohide inter_val')], K.WAIT, UI.Spin.unit(_('ms'))], 226 | [[_('Sp_lice text'), _('Try to replace redundant line breaks with spaces')], K.SPLC], 227 | ]], [[[_('Panel'), _('Middle click to copy the result')]], [ 228 | [[_('_Enable title')], K.HEAD], 229 | [[_('Ri_ght command'), _('Right click to run and hide panel')], K.RCMD], 230 | [[_('Le_ft command'), _('Left click to run')], K.LCMD], 231 | ]], [[[_('Popup'), _('Scroll to flip pages')]], [ 232 | [[_('Enable toolt_ip')], K.TIP], 233 | [[_('Page si_ze')], K.PGSZ], 234 | ]], [[[_('OCR'), `${_('Depends on: ')} ${opencv} & ${tesseract}`], K.OCR], [ 235 | [[_('Sho_rtcut')], K.KEYS], 236 | [[_('_Dwell OCR')], K.DOCR], 237 | [[_('_Work mode')], K.OCRS], 238 | [[_('Other para_meters')], ocr, K.OCRP], 239 | ]]); 240 | } 241 | } 242 | 243 | class PrefsPopup extends UI.Page { 244 | static { 245 | UI.enrol(this); 246 | } 247 | 248 | constructor(gset, param, field) { 249 | super(gset, param); 250 | this.$tie([[field, this]]); 251 | this.$add([null, [new Gtk.Frame({child: new UI.Box([this.$genSide(this.value, field), this.$genPane()], {vexpand: false, cssName: 'list'})})]]); 252 | this.grabFocus(0); // init pane 253 | } 254 | 255 | $save(func, grab, name, pane) { 256 | func(this.$cmds); 257 | this.value = [...this.$cmds].map(x => x.cmd); 258 | if(grab >= 0) this.grabFocus(grab, name); 259 | if(pane) this.$updatePaneSensitive(this.$cmds.nItems > 0); 260 | } 261 | 262 | $genSide(cmds, field) { 263 | this.$cmds = new Gio.ListStore({itemType: SideItem}); 264 | this.$cmds.splice(0, 0, cmds.map(x => new SideItem(x))); 265 | this.$list = T.hook({'row-selected': (_w, row) => row && this.$onSelect(row.get_index())}, 266 | new Gtk.ListBox({selectionMode: Gtk.SelectionMode.SINGLE, vexpand: true})); 267 | this.$list.add_css_class('data-table'); 268 | this.$list.bind_model(this.$cmds, item => T.hook({ 269 | dropped: (_w, f, t) => this.$onDrop(f, t), 270 | changed: (_w, p, v) => this.$onChange(p, 'name', v), 271 | toggled: (_w, p, v) => this.$onChange(p, 'enable', v), 272 | }, new SideRow(item, field === K.SCMDS))); 273 | return UI.Box.newV([this.$genTools(), new Gtk.Separator(), new Gtk.ScrolledWindow({overlayScrolling: false, child: this.$list})]); 274 | } 275 | 276 | grabFocus(index, name) { 277 | let row = this.$list.get_row_at_index(index); 278 | this.$list.select_row(row); 279 | if(name) row.editName(); 280 | } 281 | 282 | $genPaneWidgets() { 283 | return { 284 | command: ['', [_('_Run command')], new UI.Entry('gio open "$LDWORD"', [EXE])], 285 | type: [0, [_('_Command type')], new UI.Drop(['Bash', 'JS']), new UI.Help(({h, d}) => 286 | [h(_('Bash environment variable')), d([ 287 | 'LDWORD', _('the captured text'), 288 | 'LDAPPID', _('the focused app'), 289 | ]), h(_('JS script statement')), d([ 290 | "open('URI')", _('open URI with default app'), 291 | "key('super+a')", _('simulate keyboard input'), 292 | 'copy(LDWORD)', _('copy LDWORD to clipboard'), 293 | 'search(LDWORD)', _('search LDWORD in Overview'), 294 | 'LDWORD.trim()', _('some native functions'), 295 | ])])], 296 | icon: ['', [_('_Icon name')], new UI.Icon()], 297 | result: [0, [], new ResultRows()], 298 | apps: [[], [_('_App list')], new Apps(_('Whitelist'))], 299 | regexp: ['', [_('RegE_xp matcher')], new UI.Entry('(https?|ftp|file)://.*')], 300 | tooltip: ['', [_('Ic_on tooltip')], new UI.Entry('Open URL')], 301 | }; 302 | } 303 | 304 | $genPane() { 305 | let ret = new Adw.PreferencesGroup({hexpand: true}); 306 | let addRow = ([title, subtitle = ''], widget, help) => ret.add(T.seq(w => [help, widget].forEach(x => x && w.add_suffix(x)), 307 | new Adw.ActionRow({title, subtitle, activatableWidget: widget, useUnderline: true}))); 308 | this.$updatePaneSensitive = x => { if(!x) this.$onSelect(); ret.set_sensitive(x); }; 309 | this.$pane = T.omap(this.$genPaneWidgets(), ([key, [fallback, titles, widget, help]]) => { 310 | widget instanceof ResultRows ? widget.addToPane(addRow) : addRow(titles, widget, help); 311 | widget.connect('notify::value', ({value}) => !this.$syncing && this.$select(p => this.$onChange(p, key, value))); 312 | widget.$fallback = fallback; 313 | return [[key, widget]]; 314 | }); 315 | return ret; 316 | } 317 | 318 | $genTools() { 319 | return new UI.Box([ 320 | ['list-add-symbolic', _('Add'), () => this.$onAdd()], 321 | ['list-remove-symbolic', _('Remove'), () => this.$select(p => this.$onRemove(p))], 322 | ['edit-copy-symbolic', _('Copy'), () => this.$select(p => this.$onCopy(p))], 323 | ['edit-paste-symbolic', _('Paste'), () => this.$onPaste()], 324 | ].map(([x, y, z]) => T.hook({clicked: z}, new Gtk.Button({iconName: x, tooltipText: y, hasFrame: false})))); 325 | } 326 | 327 | get selected() { 328 | return this.$list.get_selected_row()?.get_index() ?? -1; 329 | } 330 | 331 | $select(callback) { 332 | let pos = this.selected; 333 | if(pos >= 0) callback(pos); 334 | } 335 | 336 | $onSelect(pos = this.selected) { 337 | this.$syncing = true; 338 | let cmd = pos < 0 ? {} : this.$cmds.get_item(pos).cmd; 339 | for(let k in this.$pane) this.$pane[k].value = cmd[k] ?? this.$pane[k].$fallback; 340 | this.$syncing = false; 341 | } 342 | 343 | $onChange(pos, key, value) { 344 | this.$save(x => x.get_item(pos).setup(key, value), key === 'enable' ? pos : -1); 345 | } 346 | 347 | $onAdd(cmd = {name: 'Name'}, pos = this.selected + 1) { 348 | this.$save(x => x.insert(pos, new SideItem(cmd)), pos, true, true); 349 | } 350 | 351 | $onDrop(drag, drop) { 352 | this.$save(x => { let item = x.get_item(drag); x.remove(drag); x.insert(drop, item); }, drop); 353 | } 354 | 355 | $onRemove(pos) { 356 | this.$save(x => { let item = x.get_item(pos); x.remove(pos); this.$toastRemove(item); }, Math.min(pos, this.$cmds.nItems - 2), false, true); 357 | } 358 | 359 | $toastRemove(item) { 360 | this.get_root().add_toast(T.hook({'button-clicked': () => this.$save(x => x.append(item), this.$cmds.nItems, true, true)}, 361 | new Adw.Toast({title: _('Removed %s command').format(item.cmd.name ?? ''), buttonLabel: _G('_Undo')}))); 362 | } 363 | 364 | $onCopy(pos) { 365 | let {cmd} = this.$cmds.get_item(pos); 366 | this.get_clipboard().set(JSON.stringify(cmd)); 367 | this.$toast(_('Copied %s command').format(cmd.name ?? '')); 368 | } 369 | 370 | async $onPaste() { 371 | try { 372 | let cmd = JSON.parse(await this.get_clipboard().read_text_async(null)); 373 | this.$onAdd(T.omap(cmd, ([k, v]) => Object.hasOwn(this.$pane, k) || k === 'name' || k === 'enable' ? [[k, v]] : [])); 374 | } catch(e) { 375 | this.$toast(_('Failed to parse pasted command')); 376 | } 377 | } 378 | 379 | $toast(title) { 380 | this.get_root().add_toast(new Adw.Toast({title, timeout: 7})); 381 | } 382 | } 383 | 384 | class PrefsSwift extends PrefsPopup { 385 | static { 386 | T.enrol(this, {enabled: ['int', -1, GLib.MAXINT32, -1], value: null}); // HACK: workaround for the trait overwrite rather than extend the super 387 | } 388 | 389 | constructor(gset, param, field) { 390 | super(gset, param, field); 391 | this.connect('notify::enabled', () => [...this.$cmds].forEach((x, i) => { x.enable = i === this.enabled; })); 392 | this.$tie([[K.SCMD, this, 'enabled']]); 393 | } 394 | 395 | $genPaneWidgets() { 396 | let {icon: i_, tooltip: t_, ...ret} = super.$genPaneWidgets(); 397 | return ret; 398 | } 399 | 400 | $onChange(pos, key, value) { 401 | if(key === 'enable') { 402 | if(!value) return; 403 | this.enabled = pos; 404 | this.grabFocus(pos); 405 | } else { 406 | super.$onChange(pos, key, value); 407 | } 408 | } 409 | 410 | $onAdd(cmd = {name: 'Name'}, pos = this.selected + 1) { 411 | delete cmd.enable; 412 | super.$onAdd(cmd, pos); 413 | if(this.enabled > pos) this.enabled += 1; 414 | } 415 | 416 | $onRemove(pos = this.selected) { 417 | super.$onRemove(pos); 418 | if(this.enabled > pos) this.enabled -= 1; 419 | else if(this.enabled === pos) this.enabled = -1; 420 | } 421 | 422 | $onDrop(drag, drop) { 423 | super.$onDrop(drag, drop); 424 | if(this.enabled > Math.max(drag, drop) || this.enabled < Math.min(drag, drop)) return; 425 | if(this.enabled > drag) this.enabled -= 1; 426 | else if(this.enabled === drag) this.enabled = drop; 427 | else this.enabled += 1; 428 | } 429 | } 430 | 431 | export default class extends UI.Prefs { 432 | fillPreferencesWindow(win) { 433 | let path = '/org/gnome/shell/extensions/light-dict/'; 434 | Gtk.IconTheme.get_for_display(Gdk.Display.get_default()).add_resource_path(`${path}icons`); 435 | Gtk.StyleContext.add_provider_for_display(Gdk.Display.get_default(), T.seq(p => p.load_from_resource(`${path}theme/style.css`), 436 | new Gtk.CssProvider()), Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); // HACK: unable (too late) to win.set_resource_base_path after inited (promised) 437 | let gset = this.getSettings(); 438 | [ 439 | new PrefsBasic(gset, {title: _('_Basic'), iconName: 'applications-system-symbolic'}), 440 | new PrefsSwift(gset, {title: _('_Swift'), iconName: 'ld-swift-passive-symbolic'}, K.SCMDS), 441 | new PrefsPopup(gset, {title: _('_Popup'), iconName: 'ld-popup-passive-symbolic'}, K.PCMDS), 442 | ].forEach(x => win.add(x)); 443 | } 444 | } 445 | -------------------------------------------------------------------------------- /src/ui.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: tuberry 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import Adw from 'gi://Adw'; 5 | import Gdk from 'gi://Gdk'; 6 | import Gio from 'gi://Gio'; 7 | import Gtk from 'gi://Gtk'; 8 | import GLib from 'gi://GLib'; 9 | import Pango from 'gi://Pango'; 10 | import GObject from 'gi://GObject'; 11 | import * as Gettext from 'gettext'; 12 | import * as Extensions from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js'; 13 | 14 | import * as T from './util.js'; 15 | 16 | const {BIND} = T; 17 | 18 | Gio._promisify(Gtk.FileDialog.prototype, 'open'); 19 | Gio._promisify(Gtk.FileDialog.prototype, 'select_folder'); 20 | 21 | export const _ = Extensions.gettext; 22 | export const _G = (x, y = 'gtk40') => Gettext.domain(y).gettext(x); 23 | export const me = () => Extensions.ExtensionPreferences.lookupByURL(import.meta.url); 24 | 25 | export const setv = Symbol('Set Value'); 26 | export const getv = Symbol('Get Default Value'); 27 | export const esse = Symbol('Default Binding Key'); 28 | 29 | export const once = (o, f, s = 'notify::value') => { let id = o.connect(s, () => { o.disconnect(id); f(); }); }; 30 | export const gtype = (o, v) => o.constructor[GObject.properties]?.[v]?.value_type; 31 | export const enrol = (c, v) => T.enrol(c, {value: v ?? null}); 32 | 33 | export class Prefs extends Extensions.ExtensionPreferences { 34 | constructor(...args) { 35 | T.load(`${T.ROOT}/resource/prefs.gresource`); 36 | super(...args); 37 | } 38 | 39 | getPreferencesWidget() { 40 | if(this.$klass) return new this.$klass(this.getSettings()); 41 | } 42 | } 43 | 44 | export class Page extends Adw.PreferencesPage { 45 | static { 46 | T.enrol(this); 47 | } 48 | 49 | #bind(gset, key, gobj, prop) { 50 | prop ??= gobj[esse] ?? 'value'; 51 | gobj[getv] = gset.get_default_value(key).recursiveUnpack(); 52 | gobj[setv] = val => { gobj[prop] = val ?? gobj[getv]; }; 53 | if(gtype(gobj, prop) !== GObject.TYPE_JSOBJECT) { 54 | gset.bind(key, gobj, prop, Gio.SettingsBindFlags.DEFAULT); 55 | } else { // HACK: workaround for https://gitlab.gnome.org/GNOME/gjs/-/issues/397 56 | gobj[prop] = gset.get_value(key).recursiveUnpack(); 57 | gobj.connect(`notify::${prop}`, () => gset.set_value(key, gobj.$picklev?.() ?? T.pickle(gobj[prop], false))); 58 | } 59 | return gobj; 60 | } 61 | 62 | #tie = (a, s) => Object.fromEntries(a.map(([k, o, p]) => [k, this.#bind(s, k, o, p)])); 63 | 64 | constructor(gset, param) { 65 | super({useUnderline: true, ...param}); 66 | this.$tie = (x, s = gset) => { if(Array.isArray(x)) this.$blk = Object.assign(this.#tie(x, s), this.$blk); }; 67 | T.seq(x => x && this.$tie(x), this.$buildWidgets?.(gset)); 68 | T.seq(x => x && this.$add([null, x]), this.$buildUI?.()); 69 | } 70 | 71 | $add(...grps) { 72 | let sensitize = (a, b) => a.bind_property(a[esse], b, 'sensitive', GObject.BindingFlags.SYNC_CREATE); 73 | grps.forEach(grp => this.add(grp instanceof Adw.PreferencesGroup ? grp : T.str(grp) ? this.$blk[grp] : T.seq(g => { 74 | let [[[title = '', subtitle = ''], suffix = null], rows, param] = (grp[0] ??= [[]], grp); 75 | g.set({title, description: subtitle, headerSuffix: T.str(suffix) ? this.$blk[suffix] : suffix, ...param}); 76 | rows = rows.map(row => row instanceof Gtk.Widget ? row : T.str(row) ? this.$blk[row] : T.seq(r => { 77 | row = row.map(x => T.str(x) ? this.$blk[x] : x); 78 | let [prefix, [title_, subtitle_ = ''], ...suffix1] = (Array.isArray(row[0]) && row.unshift(null), row); 79 | r.set({title: title_, subtitle: subtitle_}); 80 | if(prefix) r.add_prefix(prefix); 81 | if(prefix instanceof Check) { 82 | r.set_activatable_widget(prefix); 83 | suffix1.forEach(x => { r.add_suffix(x); sensitize(prefix, x); }); 84 | } else if(suffix1.length) { 85 | r.set_activatable_widget(suffix1.find(x => !(x instanceof Help)) ?? null); 86 | suffix1.forEach(x => r.add_suffix(x)); 87 | } 88 | }, new Adw.ActionRow({useUnderline: true}))); 89 | if(g.headerSuffix instanceof Switch) rows.forEach(r => { g.add(r); sensitize(g.headerSuffix, r); }); 90 | else rows.forEach(r => g.add(r)); 91 | }, new Adw.PreferencesGroup()))); 92 | } 93 | } 94 | 95 | export class Box extends Gtk.Box { 96 | static { 97 | T.enrol(this); 98 | } 99 | 100 | static newV = (cs, p, ...xs) => new Box(cs, {orientation: Gtk.Orientation.VERTICAL, valign: Gtk.Align.FILL, ...p}, ...xs); 101 | 102 | constructor(children, param, linked = true) { 103 | super({valign: Gtk.Align.CENTER, ...param}); 104 | children?.forEach(x => x && this.append(x)); 105 | if(linked) this.add_css_class('linked'); 106 | } 107 | } 108 | 109 | export class Spin extends Gtk.SpinButton { 110 | static unit = x => new Gtk.Label({label: x, cssClasses: ['dimmed']}); // TODO: ? embed to Spin 111 | 112 | static { 113 | T.enrol(this); 114 | } 115 | 116 | constructor(lower, upper, stepIncrement, tooltipText = '', param) { 117 | super({tooltipText, valign: Gtk.Align.CENTER, adjustment: new Gtk.Adjustment({lower, upper, stepIncrement}), ...param}); 118 | } 119 | } 120 | 121 | export class Switch extends Gtk.Switch { 122 | static { 123 | T.enrol(this); 124 | } 125 | 126 | [esse] = 'active'; 127 | 128 | constructor(param) { 129 | super({valign: Gtk.Align.CENTER, ...param}); 130 | } 131 | } 132 | 133 | export class Check extends Gtk.CheckButton { 134 | static get pad() { return new Gtk.CheckButton({sensitive: false, opacity: 0}); } 135 | 136 | static { 137 | T.enrol(this); 138 | } 139 | 140 | [esse] = 'active'; 141 | } 142 | 143 | export class Drop extends Gtk.DropDown { 144 | static { 145 | T.enrol(this); 146 | } 147 | 148 | [esse] = 'selected'; 149 | 150 | constructor(strv, tooltipText = '', param) { 151 | super({model: Gtk.StringList.new(strv), valign: Gtk.Align.CENTER, tooltipText, ...param}); 152 | } 153 | } 154 | 155 | export class Font extends Gtk.FontDialogButton { 156 | static { 157 | enrol(this, ''); 158 | } 159 | 160 | constructor(param) { 161 | super({valign: Gtk.Align.CENTER, dialog: new Gtk.FontDialog(), ...param}); 162 | this.bind_property_full('font-desc', this, 'value', BIND, (_b, x) => [true, x.to_string()], (_b, x) => [true, Pango.FontDescription.from_string(x)]); 163 | } 164 | } 165 | 166 | export class Help extends Gtk.MenuButton { 167 | static { 168 | T.enrol(this); 169 | } 170 | 171 | static typeset(build, param) { 172 | let keys = x => new Gtk.ShortcutLabel({accelerator: x}), 173 | mark = (x, y, z) => new Gtk.Label({label: x, cssClasses: y ? T.unit(y) : [], useMarkup: true, halign: Gtk.Align.START, ...z}), 174 | dict = (a, n = 2) => T.array(Math.ceil(a.length / n), () => a.splice(0, n).map((x, i) => i % 2 ? x : mark(`${x}`, null, {selectable: true}))), 175 | head = (x, z) => mark(`${x}`, null, z), 176 | wrap = x => x instanceof Gtk.Widget ? x : mark(x); 177 | return Box.newV(build({k: keys, m: mark, d: dict, h: head}).map(x => T.seq(w => T.unit(x).forEach((y, i) => 178 | T.unit(y).forEach((z, j) => z && w.attach(wrap(z), j, i, 1, 1))), new Gtk.Grid({vexpand: true, rowSpacing: 4, columnSpacing: 12}))), 179 | {valign: Gtk.Align.START, spacing: 6, ...param}, false); 180 | } 181 | 182 | constructor(help, param, param1) { 183 | super({hasFrame: false, valign: Gtk.Align.CENTER, popover: new Gtk.Popover(), ...param1}); 184 | if(help) this.setup(help, param); 185 | } 186 | 187 | setup(help, param, error) { 188 | switch(T.type(help)) { 189 | case 'function': help = Help.typeset(help, param); break; 190 | case 'string': help = new Gtk.Label({label: help, ...param}); break; 191 | } 192 | this.popover.set_child(help); 193 | this.set_icon_name(error ? 'dialog-error-symbolic' : 'help-about-symbolic'); 194 | } 195 | } 196 | 197 | export class Sign extends Gtk.Box { 198 | static { 199 | T.enrol(this); 200 | } 201 | 202 | constructor(fallbackIcon, reverse, labelParam, iconParam) { 203 | super({spacing: 5}); 204 | this.$fallbackIcon = fallbackIcon; 205 | this.$icon = new Gtk.Image(iconParam); 206 | this.$label = new Gtk.Label(labelParam); 207 | if(reverse) [this.$label, this.$icon].forEach(x => this.append(x)); 208 | else [this.$icon, this.$label].forEach(x => this.append(x)); 209 | } 210 | 211 | setup(icon, label) { 212 | this.$label.set_label(label || _G('(None)')); 213 | if(icon instanceof Gio.Icon) this.$icon.set_from_gicon(icon); 214 | else this.$icon.iconName = icon || this.$fallbackIcon; 215 | } 216 | } 217 | 218 | export class Dialog extends Adw.Window { // FIXME: revert from Adw.Dialog since https://gitlab.gnome.org/GNOME/libadwaita/-/merge_requests/1415 breaks ECK on close 219 | static { 220 | T.enrol(this, null, {Signals: {chosen: {param_types: [GObject.TYPE_JSOBJECT]}}}); 221 | } 222 | 223 | constructor(build, param) { 224 | super({widthRequest: 360, heightRequest: 320, modal: true, hideOnClose: true, ...param}); 225 | this.connect('chosen', (_d, value) => this.$chosen?.resolve(value)); 226 | this.connect('close-request', () => this.$chosen?.reject(Error('cancelled'))); 227 | this.add_controller(T.hook({'key-pressed': (...xs) => this.$onKeyPress(...xs)}, new Gtk.EventControllerKey())); 228 | this.$buildContent(build); 229 | } 230 | 231 | $buildContent(build) { 232 | this.set_content(build instanceof Gtk.Widget ? T.seq(w => w.add_top_bar(new Adw.HeaderBar({showTitle: false})), 233 | new Adw.ToolbarView({content: build})) : this.$buildWidgets(build)); 234 | } 235 | 236 | $buildWidgets(build) { 237 | let {content, filter, title} = build(this), search, 238 | close = T.hook({clicked: () => this.close()}, Gtk.Button.new_with_mnemonic(_G('_Cancel'))), 239 | select = T.hook({clicked: () => this.$onChosen()}, Gtk.Button.new_with_mnemonic(_G('_OK'))), 240 | header = new Adw.HeaderBar({showEndTitleButtons: false, showStartTitleButtons: false, titleWidget: title || null}); 241 | select.add_css_class('suggested-action'); 242 | header.pack_start(close); 243 | header.pack_end(select); 244 | if(filter) { 245 | let button = new Gtk.ToggleButton({iconName: 'system-search-symbolic'}); 246 | let entry = T.hook({'search-changed': x => filter.set_search(x.get_text())}, new Gtk.SearchEntry({halign: Gtk.Align.CENTER})); 247 | search = new Gtk.SearchBar({showCloseButton: false, child: entry, keyCaptureWidget: this}); 248 | search.connect_entry(entry); 249 | button.bind_property('active', search, 'search-mode-enabled', BIND); 250 | this.connect('close-request', () => { button.set_active(false); content.scroll_to(0, Gtk.ListScrollFlags.FOCUS, null); }); 251 | header.pack_end(button); 252 | } 253 | return Box.newV([header, search, new Gtk.ScrolledWindow({child: content})], null, false); 254 | } 255 | 256 | $onKeyPress(_w, key) { 257 | switch(key) { 258 | case Gdk.KEY_Escape: this.close(); break; 259 | case Gdk.KEY_Return: 260 | case Gdk.KEY_KP_Enter: 261 | case Gdk.KEY_ISO_Enter: this.$onChosen(); break; 262 | } 263 | } 264 | 265 | $onChosen(chosen = this.getChosen?.()) { 266 | if(chosen !== undefined) this.emit('chosen', [chosen]); 267 | this.close(); 268 | } 269 | 270 | choose(root, initial) { 271 | this.$chosen = Promise.withResolvers(); 272 | if(this.transient_for !== root) this.set_transient_for(root); 273 | this.present(); 274 | this.initChosen?.(initial); 275 | return this.$chosen.promise; 276 | } 277 | } 278 | 279 | export class DialogButtonBase extends Box { 280 | static { 281 | enrol(this, ''); 282 | } 283 | 284 | constructor(opt, child, reset, param) { 285 | super(); 286 | this.$opt = opt; 287 | this[setv] = v => { this.value = v; }; 288 | if(reset) this.append(T.hook({clicked: () => this[setv]()}, new Gtk.Button({iconName: 'edit-clear-symbolic', tooltipText: _G('Reset')}))); 289 | this.prepend(this.$btn = T.hook({clicked: () => this.$onClick().then(x => this.$onSetv(x)).catch(T.nop)}, new Gtk.Button({child, ...param}))); 290 | this.$buildDND(gtype(this, 'gvalue')); 291 | } 292 | 293 | $onClick() { 294 | return this.dlg.choose(this.get_root(), this.$getInitial?.() ?? this.value); 295 | } 296 | 297 | $onSetv([value]) { 298 | value.constructor === this.value?.constructor ? this[setv](value) : this.gvalue = value; 299 | } 300 | 301 | $buildDND(gType) { 302 | if(!gType) return; 303 | this.$onDrop = (_t, v) => { this.gvalue = v; }; 304 | this.$onDrag = src => { T.seq(x => x && src.set_icon(x, 10, 10), this.$genSwatch?.()); return Gdk.ContentProvider.new_for_value(this.gvalue); }; 305 | this.$btn.add_controller(T.hook({drop: (...xs) => this.$onDrop(...xs)}, Gtk.DropTarget.new(gType, Gdk.DragAction.COPY))); 306 | this.$btn.add_controller(T.hook({prepare: (...xs) => this.$onDrag(...xs)}, new Gtk.DragSource({actions: Gdk.DragAction.COPY}))); 307 | this.$bindv = (to, from) => this.bind_property_full('value', this, 'gvalue', BIND, to, from); 308 | this.connect('notify::gvalue', () => this.$onGValueSet?.(this.gvalue)); 309 | } 310 | 311 | get dlg() { 312 | return (this.$dialog ??= this.$genDialog(this.$opt)); 313 | } 314 | 315 | vfunc_mnemonic_activate() { 316 | this.$btn.activate(); 317 | } 318 | } 319 | 320 | export class App extends DialogButtonBase { 321 | static { 322 | T.enrol(this, {gvalue: Gio.DesktopAppInfo}); 323 | } 324 | 325 | constructor(opt, param) { 326 | super(opt, new Sign('application-x-executable-symbolic'), true, param); 327 | this.$bindv((_b, x) => [true, Gio.DesktopAppInfo.new(x)], (_b, x) => [true, x?.get_id() ?? '']); 328 | this.$onGValueSet = v => this.$btn.child.setup(...v ? [v.get_icon(), v.get_display_name()] : []); 329 | } 330 | 331 | $genSwatch() { 332 | return Gtk.IconTheme.get_for_display(Gdk.Display.get_default()).lookup_by_gicon(this.gvalue.get_icon(), 32, 1, Gtk.TextDirection.NONE, Gtk.IconLookupFlags.FORCE_SVG); 333 | } 334 | 335 | $genDialog(opt) { 336 | return new Dialog(dlg => { 337 | let factory = T.hook({ 338 | setup: (_f, x) => x.set_child(new Sign('application-x-executable-symbolic')), 339 | bind: (_f, x) => x.get_child().setup(...(y => [y.get_icon() || '', y.get_display_name()])(x.get_item())), 340 | }, new Gtk.SignalListItemFactory()), 341 | filter = Gtk.CustomFilter.new(null), 342 | list = new Gio.ListStore({itemType: Gio.DesktopAppInfo}), 343 | select = new Gtk.SingleSelection({model: new Gtk.FilterListModel({model: list, filter})}), 344 | content = T.hook({activate: () => dlg.$onChosen()}, new Gtk.ListView({model: select, factory, vexpand: true})); 345 | list.splice(0, 0, opt?.noDisplay ? Gio.AppInfo.get_all() : Gio.AppInfo.get_all().filter(x => x.should_show())); 346 | filter.set_search = s => filter.set_filter_func(s ? (a => x => a.has(x.get_id()))(new Set(Gio.DesktopAppInfo.search(s).flat())) : null); 347 | dlg.getChosen = () => select.get_selected_item(); 348 | return {content, filter}; 349 | }, {title: _G('Select Application')}); 350 | } 351 | } 352 | 353 | export class File extends DialogButtonBase { 354 | static { 355 | T.enrol(this, {gvalue: Gio.File}); 356 | } 357 | 358 | constructor(opt, param, icon = 'document-open-symbolic') { 359 | super(opt, new Sign(icon), true, param); 360 | if(opt?.folder) opt.filter = {mimeTypes: ['inode/directory']}; 361 | if(opt?.filter) this.$filter = new Gtk.FileFilter(opt.filter); 362 | if(opt?.size) this.$btn.child.$label.set_use_markup(true); 363 | if(opt?.open) { 364 | this.insert_child_after(T.hook({clicked: () => Gtk.FileLauncher.new(this.gvalue).launch(this.get_root(), null, null)}, 365 | new Gtk.Button({iconName: 'document-open-symbolic'})), this.$btn); 366 | } 367 | this.$onSetv = x => { this.gvalue = x; }; 368 | this.$bindv((_b, x) => [true, T.fopen(x)], (_b, x) => [true, x.get_path()]); 369 | this.$onGValueSet = v => T.fquery(v, Gio.FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME, Gio.FILE_ATTRIBUTE_STANDARD_ICON) 370 | .then(x => this.setup(x.get_icon(), x.get_display_name())).catch(() => this.setup()); 371 | } 372 | 373 | $genDialog() { 374 | return new Gtk.FileDialog({modal: true, title: this.$opt?.title ?? null, defaultFilter: this.$filter ?? null}); 375 | } 376 | 377 | $onDrop(_t, file) { 378 | if(!this.$filter) { 379 | this.gvalue = file; 380 | } else { 381 | T.fquery(file, Gio.FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE).then(y => { 382 | if(this.$filter.match(y)) this.gvalue = file; else throw Error(); 383 | }).catch(() => { 384 | this.get_root().add_toast(new Adw.Toast({title: _('Mismatched filetype'), timeout: 7})); 385 | }); 386 | } 387 | } 388 | 389 | $onClick() { 390 | this.dlg.set_initial_file(this.gvalue); 391 | return this.$opt?.folder ? this.dlg.select_folder(this.get_root(), null) : this.dlg.open(this.get_root(), null); 392 | } 393 | 394 | setup(icon, text) { 395 | if(this.$opt.size) { 396 | let size = T.essay(() => GLib.format_size(this.gvalue.measure_disk_usage(Gio.FileMeasureFlags.NONE, null, null)[1]), () => ''); 397 | text = `${T.escape(text)}${size && ` ${size}`}`; 398 | } 399 | this.$btn.child.setup(icon, text); 400 | } 401 | } 402 | 403 | export class Icon extends DialogButtonBase { 404 | static { 405 | T.enrol(this, {gvalue: Gio.ThemedIcon}); 406 | } 407 | 408 | static Type = {ALL: 0, NORMAL: 1, SYMBOLIC: 2}; 409 | 410 | constructor(opt, param) { 411 | super(opt, new Sign('image-missing'), true, param); 412 | this.$bindv((_b, x) => [true, Gio.ThemedIcon.new(x)], (_b, x) => [true, x.to_string()]); 413 | this.$onGValueSet = v => v ? this.$btn.child.setup(this.gvalue, this.value.replace(/-symbolic$/, '')) : this.$btn.child.setup(); 414 | } 415 | 416 | $genSwatch() { 417 | return Gtk.IconTheme.get_for_display(Gdk.Display.get_default()).lookup_by_gicon(this.gvalue, 32, 1, Gtk.TextDirection.NONE, Gtk.IconLookupFlags.FORCE_SVG); 418 | } 419 | 420 | $genDialog(opt) { 421 | return new Dialog(dlg => { 422 | let factory = T.hook({ 423 | setup: (_f, x) => x.set_child(new Gtk.Image({iconSize: Gtk.IconSize.LARGE})), 424 | bind: (_f, {child, item}) => { child.iconName = child.tooltipText = item.string; }, 425 | }, new Gtk.SignalListItemFactory()), 426 | filter = T.seq(w => [new Gtk.StringFilter({expression: new Gtk.PropertyExpression(Gtk.StringObject, null, 'string')}), 427 | new Gtk.BoolFilter()].forEach(x => w.append(x)), new Gtk.EveryFilter()), 428 | title = T.seq(w => ['image-missing', 'image-x-generic', 'image-x-generic-symbolic'].forEach(x => 429 | w.add(new Adw.Toggle({iconName: x}))), new Adw.ToggleGroup()), 430 | model = Gtk.StringList.new(Gtk.IconTheme.get_for_display(Gdk.Display.get_default()).get_icon_names()), 431 | select = new Gtk.SingleSelection({model: new Gtk.FilterListModel({model, filter})}), 432 | content = T.hook({activate: () => dlg.$onChosen()}, new Gtk.GridView({model: select, factory, vexpand: true})); 433 | title.set_active(opt?.type ?? Icon.Type.SYMBOLIC); 434 | title.bind_property_full('active', filter.get_item(1), 'expression', GObject.BindingFlags.SYNC_CREATE, (_b, x) => { 435 | switch(x) { 436 | case Icon.Type.ALL: return [true, Gtk.ConstantExpression.new_for_value(true)]; 437 | case Icon.Type.NORMAL: return [true, new Gtk.ClosureExpression(GObject.TYPE_BOOLEAN, y => !y.string.endsWith('-symbolic'), null)]; 438 | case Icon.Type.SYMBOLIC: return [true, new Gtk.ClosureExpression(GObject.TYPE_BOOLEAN, y => y.string.endsWith('-symbolic'), null)]; 439 | } 440 | }, null); 441 | dlg.getChosen = () => select.get_selected_item().get_string(); 442 | return {content, title, filter: filter.get_item(0)}; 443 | }); 444 | } 445 | } 446 | 447 | export class Keys extends DialogButtonBase { 448 | static genStatusPage() { 449 | return new Adw.StatusPage({ 450 | iconName: 'preferences-desktop-keyboard-shortcuts-symbolic', title: _G('Enter the new shortcut', 'gnome-control-center-2.0'), 451 | description: _G('Press Esc to cancel or Backspace to disable the keyboard shortcut', 'gnome-control-center-2.0'), 452 | }); 453 | } 454 | 455 | static { 456 | enrol(this); 457 | } 458 | 459 | constructor(param) { 460 | super(null, new Gtk.ShortcutLabel({disabledText: _G('New accelerator…')}), false, {hasFrame: false, ...param}); 461 | this.connect('notify::value', () => this.$btn.child.set_accelerator(this.value?.[0] ?? '')); 462 | this.$picklev = () => new GLib.Variant('as', this.value); 463 | } 464 | 465 | $validate(mask, keyval, keycode) { // from: https://gitlab.gnome.org/GNOME/gnome-control-center/-/blob/main/panels/keyboard/keyboard-shortcuts.c 466 | return (Gtk.accelerator_valid(keyval, mask) || (keyval === Gdk.KEY_Tab && mask !== 0)) && 467 | !(mask === 0 || mask === Gdk.SHIFT_MASK && keycode !== 0 && 468 | ((keyval >= Gdk.KEY_a && keyval <= Gdk.KEY_z) || 469 | (keyval >= Gdk.KEY_A && keyval <= Gdk.KEY_Z) || 470 | (keyval >= Gdk.KEY_0 && keyval <= Gdk.KEY_9) || 471 | (keyval >= Gdk.KEY_kana_fullstop && keyval <= Gdk.KEY_semivoicedsound) || 472 | (keyval >= Gdk.KEY_Arabic_comma && keyval <= Gdk.KEY_Arabic_sukun) || 473 | (keyval >= Gdk.KEY_Serbian_dje && keyval <= Gdk.KEY_Cyrillic_HARDSIGN) || 474 | (keyval >= Gdk.KEY_Greek_ALPHAaccent && keyval <= Gdk.KEY_Greek_omega) || 475 | (keyval >= Gdk.KEY_hebrew_doublelowline && keyval <= Gdk.KEY_hebrew_taf) || 476 | (keyval >= Gdk.KEY_Thai_kokai && keyval <= Gdk.KEY_Thai_lekkao) || 477 | (keyval >= Gdk.KEY_Hangul_Kiyeog && keyval <= Gdk.KEY_Hangul_J_YeorinHieuh) || 478 | (keyval === Gdk.KEY_space && mask === 0) || [Gdk.KEY_Home, Gdk.KEY_Left, Gdk.KEY_Up, Gdk.KEY_Right, Gdk.KEY_Down, Gdk.KEY_Page_Up, 479 | Gdk.KEY_Page_Down, Gdk.KEY_End, Gdk.KEY_Tab, Gdk.KEY_KP_Enter, Gdk.KEY_Return, Gdk.KEY_Mode_switch].includes(keyval))); 480 | } 481 | 482 | $genDialog() { 483 | return T.seq(dlg => { 484 | dlg.$onKeyPress = (_w, keyval, keycode, state) => { 485 | let mask = state & Gtk.accelerator_get_default_mod_mask() & ~Gdk.ModifierType.LOCK_MASK; 486 | if(!mask && keyval === Gdk.KEY_Escape) return dlg.close(); 487 | if(keyval === Gdk.KEY_BackSpace) return dlg.$onChosen([]); 488 | if(this.$validate(mask, keyval, keycode)) dlg.$onChosen([Gtk.accelerator_name_with_keycode(null, keyval, keycode, mask)]); 489 | }; 490 | }, new Dialog(Keys.genStatusPage())); 491 | } 492 | } 493 | 494 | export class Entry extends Gtk.Stack { 495 | static { 496 | enrol(this, ''); 497 | } 498 | 499 | constructor(placeholder, mime, tooltip, param) { 500 | super({valign: Gtk.Align.CENTER, hhomogeneous: true, ...param}); 501 | this.$buildWidgets(placeholder, mime, tooltip); 502 | } 503 | 504 | $buildWidgets(placeholderText = '', mimeTypes, tooltipText = '') { 505 | let label = new Gtk.Entry({hexpand: true, sensitive: false, placeholderText}), 506 | apply = w => { label.set_text(w.text); this.set_visible_child(label.parent); }, 507 | entry = mimeTypes ? T.hook({ 508 | activate: w => apply(w), 509 | 'icon-press': w => new Gtk.FileDialog({modal: true, defaultFilter: new Gtk.FileFilter({mimeTypes})}) 510 | .open(this.get_root(), null).then(x => w.set_text(x.get_path())).catch(T.nop), 511 | }, new Gtk.Entry({hexpand: true, enableUndo: true, secondaryIconName: 'document-open-symbolic', placeholderText})) 512 | : T.hook({activate: w => apply(w)}, new Gtk.Entry({hexpand: true, enableUndo: true, placeholderText})), 513 | edit = T.hook({clicked: () => { entry.set_text(label.text); entry.grab_focus(); this.set_visible_child(entry.parent); }}, 514 | new Gtk.Button({iconName: 'document-edit-symbolic', tooltipText})), 515 | done = T.hook({clicked: () => apply(entry)}, new Gtk.Button({ 516 | cssClasses: ['suggested-action'], iconName: 'object-select-symbolic', tooltipText: _('Click or press ENTER to apply changes'), 517 | })); 518 | [[label, edit], [entry, done]].forEach(x => this.add_child(new Box(x, {hexpand: true}))); 519 | this.$toggle = () => this.get_visible_child() === edit.parent ? edit.activate() : done.activate(); 520 | this.bind_property('value', label, 'text', BIND); 521 | } 522 | 523 | vfunc_mnemonic_activate() { 524 | this.$toggle(); 525 | } 526 | } 527 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: tuberry 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import Gio from 'gi://Gio'; 5 | import GLib from 'gi://GLib'; 6 | import GObject from 'gi://GObject'; 7 | import Soup from 'gi://Soup/?version=3.0'; 8 | 9 | Gio._promisify(Gio.File.prototype, 'copy_async'); 10 | Gio._promisify(Gio.File.prototype, 'delete_async'); 11 | Gio._promisify(Gio.File.prototype, 'query_info_async'); 12 | Gio._promisify(Gio.File.prototype, 'load_contents_async'); 13 | Gio._promisify(Gio.File.prototype, 'replace_contents_async'); 14 | Gio._promisify(Gio.File.prototype, 'enumerate_children_async'); 15 | Gio._promisify(Gio.Subprocess.prototype, 'communicate_utf8_async'); 16 | 17 | export const ROOT = GLib.path_get_dirname(import.meta.url.slice(7)); 18 | export const PIPE = Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE; 19 | export const BIND = GObject.BindingFlags.BIDIRECTIONAL | GObject.BindingFlags.SYNC_CREATE; 20 | 21 | export const id = x => x; 22 | export const nop = () => {}; 23 | /** @template T * @param {T} x * @return {T} */ 24 | export const seq = (f, x) => (f(x), x); 25 | export const xnor = (x, y) => !x === !y; 26 | export const Y = f => (...xs) => f(Y(f))(...xs); // Y combinator 27 | export const thunk = (f, ...xs) => (f(...xs), f); 28 | export const str = x => x?.constructor === String; 29 | export const decode = x => new TextDecoder().decode(x); 30 | export const encode = x => new TextEncoder().encode(x); 31 | export const vmap = (o, f) => omap(o, ([k, v]) => [[k, f(v)]]); 32 | export const lot = x => x[Math.floor(Math.random() * x.length)]; 33 | export const escape = (x, i = -1) => GLib.markup_escape_text(x, i); 34 | export const unit = (x, f = y => [y]) => Array.isArray(x) ? x : f(x); 35 | export const array = (n, f = id) => Array.from({length: n}, (_x, i) => f(i)); 36 | export const omap = (o, f) => Object.fromEntries(Object.entries(o).flatMap(f)); 37 | export const each = (f, a, s) => { for(let i = 0, n = a.length; i < n;) f(a.slice(i, i += s)); }; 38 | export const upcase = (s, f = x => x.toLowerCase()) => s.charAt(0).toUpperCase() + f(s.slice(1)); 39 | export const type = x => Object.prototype.toString.call(x).replace(/\[object (\w+)\]/, (_m, p) => p.toLowerCase()); 40 | export const format = (x, f) => x.replace(/\{\{(\w+)\}\}|\{(\w+)\}/g, (m, a, b) => b ? f(b) ?? m : f(a) === undefined ? m : `{${a}}`); 41 | /** @template T * @param {T} x * @return {T} */ // NOTE: see https://github.com/tc39/proposal-type-annotations & https://github.com/jsdoc/jsdoc/issues/1986 42 | export const hook = (o, x) => (Object.entries(o).forEach(([k, v]) => x.connect(k, v)), x); 43 | export const essay = (f, g = nop) => { try { return f(); } catch(e) { return g(e); } }; // NOTE: https://github.com/arthurfiorette/proposal-safe-assignment-operator 44 | export const load = x => exist(x) && Gio.Resource.load(x)._register(); 45 | export const exist = x => GLib.file_test(x, GLib.FileTest.EXISTS); 46 | 47 | export const fquery = (x, ...ys) => fopen(x).query_info_async(ys.join(','), Gio.FileQueryInfoFlags.NONE, GLib.PRIORITY_DEFAULT, null); 48 | export const fwrite = (x, y, c = null) => fopen(x).replace_contents_async(encode(y), null, false, Gio.FileCreateFlags.NONE, c); 49 | export const fcopy = (x, y, c = null) => fopen(x).copy_async(fopen(y), Gio.FileCopyFlags.NONE, GLib.PRIORITY_DEFAULT, c, null); 50 | export const fopen = x => str(x) ? x ? Gio.File.new_for_commandline_arg(x) : Gio.File.new_for_path(x) : x; 51 | export const fdelete = (x, c = null) => fopen(x).delete_async(GLib.PRIORITY_DEFAULT, c); 52 | export const fread = (x, c = null) => fopen(x).load_contents_async(c); 53 | 54 | export async function readdir(dir, func, attr = Gio.FILE_ATTRIBUTE_STANDARD_NAME, cancel = null) { 55 | return Array.fromAsync(await fopen(dir).enumerate_children_async(attr, Gio.FileQueryInfoFlags.NONE, GLib.PRIORITY_DEFAULT, cancel), func); 56 | } 57 | 58 | export function search(needle, haystack) { // Ref: https://github.com/bevacqua/fuzzysearch 59 | let tmp, iter = haystack[Symbol.iterator](); // TODO: Iterator.from() 60 | out: for(let char of needle) { 61 | while(!(tmp = iter.next()).done) if(tmp.value === char) continue out; 62 | return false; 63 | } 64 | return true; 65 | } 66 | 67 | export function enrol(klass, pspec, param) { 68 | if(pspec) { 69 | let spec = (k, t, ...vs) => [[k, GObject.ParamSpec[t](k, null, null, GObject.ParamFlags.READWRITE, ...vs)]]; 70 | GObject.registerClass({ 71 | Properties: omap(pspec, ([key, value]) => (kind => { 72 | switch(kind) { 73 | case 'array': return spec(key, ...value); 74 | case 'null': return spec(key, 'jsobject'); 75 | case 'function': return spec(key, 'object', value); 76 | default: return spec(key, kind, value); 77 | } 78 | })(type(value))), ...param, 79 | }, klass); 80 | } else { 81 | param ? GObject.registerClass(param, klass) : GObject.registerClass(klass); 82 | } 83 | } 84 | 85 | export function homolog(cat, dog, keys, cmp = (x, y, _k) => x === y) { // cat, dog: JSON-compatible object 86 | let list = (f, x, y) => x.length === y.length && f(x), 87 | dict = keys ? f => f(keys) : (f, x, y) => list(f, Object.keys(x), Object.keys(y)), 88 | kind = (x, y) => (t => t === type(y) ? t : NaN)(type(x)); 89 | return Y(f => (a, b, k) => { 90 | switch(kind(a, b)) { 91 | case 'array': return list(() => a.every((x, i) => f(x, b[i])), a, b); 92 | case 'object': return dict(xs => xs.every(x => f(a[x], b[x])), a, b); 93 | default: return cmp(a, b, k); 94 | } 95 | })(cat, dog); 96 | } 97 | 98 | export function pickle(value, tuple = true, number = 'u') { // value: JSON-compatible 99 | let list = tuple ? x => GLib.Variant.new_tuple(x) : x => new GLib.Variant('av', x); 100 | return Y(f => v => { 101 | switch(type(v)) { 102 | case 'array': return list(v.map(f)); 103 | case 'object': return new GLib.Variant('a{sv}', vmap(v, f)); 104 | case 'string': return GLib.Variant.new_string(v); 105 | case 'number': return new GLib.Variant(number, v); 106 | case 'boolean': return GLib.Variant.new_boolean(v); 107 | case 'null': return new GLib.Variant('mv', v); 108 | default: return GLib.Variant.new_string(String(v)); 109 | } 110 | })(value); 111 | } 112 | 113 | export async function request(method, url, param, cancel = null, header = null, session = new Soup.Session()) { 114 | let msg = param ? Soup.Message.new_from_encoded_form(method, url, Soup.form_encode_hash(param)) : Soup.Message.new(method, url); 115 | if(header) Object.entries(header).forEach(([k, v]) => msg.request_headers.append(k, v)); 116 | let ans = await session.send_and_read_async(msg, GLib.PRIORITY_DEFAULT, cancel); 117 | if(msg.statusCode !== Soup.Status.OK) throw Error(msg.get_reason_phrase()); 118 | return decode(ans.get_data()); 119 | } 120 | 121 | export async function execute(cmd, env, cancel = null, tty = new Gio.SubprocessLauncher({flags: PIPE})) { 122 | if(env) Object.entries(env).forEach(([k, v]) => tty.setenv(k, v, true)); 123 | let proc = tty.spawnv(['bash', '-c', cmd]), 124 | [stdout, stderr] = await proc.communicate_utf8_async(null, cancel), 125 | status = proc.get_exit_status(); 126 | if(status) throw Error(stderr?.trimEnd() ?? '', {cause: {status, cmdline: cmd}}); 127 | return stdout?.trimEnd() ?? ''; 128 | } 129 | --------------------------------------------------------------------------------