├── .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 | 
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 | [
][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 | 
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(``, `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-python"
394 | #~ "a> 和 pytesseract"
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 |
--------------------------------------------------------------------------------