diff --git a/.github/release-nest-v3 b/.github/release-nest-v3 index 56a6051..00750ed 100644 --- a/.github/release-nest-v3 +++ b/.github/release-nest-v3 @@ -1 +1 @@ -1 \ No newline at end of file +3 diff --git a/debian/changelog b/debian/changelog deleted file mode 100644 index 6d8d068..0000000 --- a/debian/changelog +++ /dev/null @@ -1,5 +0,0 @@ -upstream-name (1.0-101pika1) pika; urgency=medium - - * Initial release. (Closes: #nnnn) - - -- ferreo Wed, 18 Jan 2023 21:48:14 +0000 diff --git a/debian/control b/debian/control deleted file mode 100644 index 0bcd8e0..0000000 --- a/debian/control +++ /dev/null @@ -1,19 +0,0 @@ -Source: upstream-name -Section: admin -Priority: optional -Maintainer: name -Standards-Version: 4.6.1 -Build-Depends: debhelper-compat (= 13) -Rules-Requires-Root: no - -Package: pkgname1 -Architecture: linux-any -# Delete any of these lines if un-used -Depends: ${misc:Depends}, depends -Recommends: high priority optdepends -Conflicts: conflicts -Suggests: low priority optdepends -Breaks: also conflicts!? -Provides: provides -# -Description: pkgdesc diff --git a/debian/rules b/debian/rules deleted file mode 100755 index 64a084a..0000000 --- a/debian/rules +++ /dev/null @@ -1,67 +0,0 @@ -#! /usr/bin/make -f - -## See debhelper(7) (uncomment to enable). -## Output every command that modifies files on the build system. -export DH_VERBOSE = 1 -export PIKA_BUILD_ARCH = $(shell cat ../pika-build-arch) - -## === the chain of command === -## debuild runs a chain of dh functions in the following order: -## dh_testdir -## dh_clean -## dh_auto_clean -## dh_update_autotools_config -## dh_autoreconf -## dh_auto_configure -## dh_prep -## dh_build -## dh_auto_build -## dh_install -## dh_auto_install -## dh_installdocs -## dh_installchangelogs -## dh_perl -## dh_link -## dh_strip_nondeterminism -## dh_compress -## dh_fixperms -## dh_missing -## dh_dwz -## dh_strip -## dh_makeshlibs -## dh_shlibdeps -## dh_installdeb -## dh_gencontrol -## but you are most likely to only need to override the following: -## dh_clean -## dh_auto_configure -## dh_build -## dh_install - -## === End end of region === - -## === overriding dh functions === -## by default all dh functions will run a specific command based on the build system selected by "dh $@" -## if you have a makefile that does everything you need this is fine, -## but most likely you have no MakeFile and you want to add your own commands -## Note : overrides must be places above %: -## So here's a few examples: - -## overriding dh_clean to make it not delete rust vendor files: -#override_dh_clean: -# echo "disabled" - -## overriding dh_auto_configure to add custom configs: -#override_dh_auto_configure: -# $(srcdir)/configure -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_DATADIR=/usr/share -DCMAKE_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu -DBUILD_PLUGIN=OFF - -## overriding dh_install to install files to a package: -#override_dh_auto_configure: -# mkdir -p debian/pikman/usr/bin -# cp pikman debian/pikman/usr/bin/ - -## === End end of region === - -## This here will start the build: -%: - dh $@ diff --git a/gnome-shell-extension-pop-shell/debian/changelog b/gnome-shell-extension-pop-shell/debian/changelog new file mode 100644 index 0000000..4092fbb --- /dev/null +++ b/gnome-shell-extension-pop-shell/debian/changelog @@ -0,0 +1,5 @@ +gnome-shell-extension-pop-shell (46.0-101pika1) pika; urgency=medium + + * Initial Creation + + -- Ward Nakchbandi Sat, 01 Oct 2022 14:50:00 +0200 diff --git a/gnome-shell-extension-pop-shell/debian/compat b/gnome-shell-extension-pop-shell/debian/compat new file mode 100644 index 0000000..f599e28 --- /dev/null +++ b/gnome-shell-extension-pop-shell/debian/compat @@ -0,0 +1 @@ +10 diff --git a/gnome-shell-extension-pop-shell/debian/control b/gnome-shell-extension-pop-shell/debian/control new file mode 100644 index 0000000..d27abd0 --- /dev/null +++ b/gnome-shell-extension-pop-shell/debian/control @@ -0,0 +1,27 @@ +Source: gnome-shell-extension-pop-shell +Section: gnome +Priority: optional +Maintainer: Marco Trevisan +Build-Depends: debhelper (>= 10), + eslint , + libglib2.0-bin, + node-chalk , + node-js-yaml , + node-strip-ansi , + libgettextpo-dev, + gettext, + sassc +Standards-Version: 4.1.1 + +Package: gnome-shell-extension-pop-shell +Architecture: all +Depends: + gnome-shell, + fd-find, + x11-utils, + ${misc:Depends}, + ${shlibs:Depends}, +Recommends: + pop-launcher, + pop-shell-shortcuts, +Description: Pop!_OS GNOME Shell diff --git a/gnome-shell-extension-pop-shell/debian/copyright b/gnome-shell-extension-pop-shell/debian/copyright new file mode 100644 index 0000000..e72bfdd --- /dev/null +++ b/gnome-shell-extension-pop-shell/debian/copyright @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. \ No newline at end of file diff --git a/gnome-shell-extension-pop-shell/debian/gnome-shell-extension-pop-shell.install b/gnome-shell-extension-pop-shell/debian/gnome-shell-extension-pop-shell.install new file mode 100644 index 0000000..09beaf7 --- /dev/null +++ b/gnome-shell-extension-pop-shell/debian/gnome-shell-extension-pop-shell.install @@ -0,0 +1 @@ +usr \ No newline at end of file diff --git a/gnome-shell-extension-pop-shell/debian/postinst b/gnome-shell-extension-pop-shell/debian/postinst new file mode 100755 index 0000000..eb57d96 --- /dev/null +++ b/gnome-shell-extension-pop-shell/debian/postinst @@ -0,0 +1,6 @@ +#!/bin/sh + +set -e + +glib-compile-schemas /usr/share/glib-2.0/schemas/ + diff --git a/gnome-shell-extension-pop-shell/debian/prerm b/gnome-shell-extension-pop-shell/debian/prerm new file mode 100755 index 0000000..15cb003 --- /dev/null +++ b/gnome-shell-extension-pop-shell/debian/prerm @@ -0,0 +1,7 @@ +#!/bin/sh + +set -e + +glib-compile-schemas /usr/share/glib-2.0/schemas/ + + diff --git a/gnome-shell-extension-pop-shell/debian/rules b/gnome-shell-extension-pop-shell/debian/rules new file mode 100755 index 0000000..6a60942 --- /dev/null +++ b/gnome-shell-extension-pop-shell/debian/rules @@ -0,0 +1,6 @@ +#!/usr/bin/make -f +export DH_VERBOSE = 1 +export DEB_BUILD_OPTIONS=nocheck + +%: + dh $@ \ No newline at end of file diff --git a/debian/source/format b/gnome-shell-extension-pop-shell/debian/source/format similarity index 100% rename from debian/source/format rename to gnome-shell-extension-pop-shell/debian/source/format diff --git a/gnome-shell-extension-pop-shell/usr/share/doc/gnome-shell-extension-pop-shell/README.md b/gnome-shell-extension-pop-shell/usr/share/doc/gnome-shell-extension-pop-shell/README.md new file mode 100644 index 0000000..8c9e409 --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/doc/gnome-shell-extension-pop-shell/README.md @@ -0,0 +1,217 @@ +# Pop Shell + +Pop Shell is a keyboard-driven layer for GNOME Shell which allows for quick and sensible navigation and management of windows. The core feature of Pop Shell is the addition of advanced tiling window management — a feature that has been highly sought within our community. For many — ourselves included — i3wm has become the leading competitor to the GNOME desktop. + +Tiling window management in GNOME is virtually nonexistent, which makes the desktop awkward to interact with when your needs exceed that of two windows at a given time. Luckily, GNOME Shell is an extensible desktop with the foundations that make it possible to implement a tiling window manager on top of the desktop. + +Therefore, we see an opportunity here to advance the usability of the GNOME desktop to better accommodate the needs of our community with Pop Shell. Advanced tiling window management is a must for the desktop, so we've merged i3-like tiling window management with the GNOME desktop for the best of both worlds. + +[![](./screenshot.webp)](https://raw.githubusercontent.com/pop-os/shell/master/screenshot.webp) + +--- + +## Table of Contents + +- [The Proposal](#the-proposal): Possible upstreaming into GNOME +- [The Problem](#the-problem): Why we need this in GNOME +- [Installation](#installation): For those wanting to install this on their distribution +- The Solution: + - [Shared Features](#shared-features): Behaviors shared between stacking and auto-tiling modes + - [Floating Mode](#floating-mode): Behaviors specific to the floating mode + - [Tiling Mode](#tiling-mode): Behaviors specific to the auto-tiling mode +- [Developers](#developers): Guide for getting started with development +--- + +## The Proposal + +A proposal for integration of the tiling window management features from Pop Shell into GNOME is currently under development. It will be created as a GitLab issue on GNOME Shell for future discussion, once we have invested our time into producing a functioning prototype, and learned what does and does not work in practice. + +Ideally, the features explored in Pop Shell will be available for any environment using Mutter — far extending the half-monitor tiling capability currently present. By starting out as a shell extension, anyone using GNOME Shell can install this onto their system, without having to install a Pop-specific fork of GNOME on their system. + +--- + +## The Problem + +So, why is this a problem for us, and why do so many of our users switch to i3wm? + +### Displays are large, and windows are many + +GNOME currently only supports half-tiling, which tiles one window to one side of the screen, and another window to the other side of the screen. If you have more than two windows, it is expected to place them on separate workspaces, monitors, or to alternate between windows with `Alt` + `Tab`. + +This tends to work fine if you only have a small handful of applications. If you need more than two windows at a time on a display, your only option is to manually drag windows into position, and resize them to fit alongside each other — a very time-consuming process that could easily be automated and streamlined. + +### Displays are large. Very, **very** large + +Suppose you are a lucky — or perhaps unlucky — owner of an ultra-wide display. A maximized window will have much of its preferences and controls dispersed across the far left and far right corners. The application may place a panel with buttons on the far left, while other buttons get shifted to either the distant center or far right. + +Half-tiling in this scenario means that each window will be as large as an entire 2560x1440 or 4K display. In either scenario, at such extreme sizes, the mouse becomes completely useless — and applications become unbearable to use — in practice. + +### Fighting the window manager is futile + +As you struggle with fighting the window manager, it quickly becomes clear that any attempt to manage windows in a traditional stacking manner — where you need to manually move windows into place, and then manually resize them — is futile. Humans are nowhere near as precise or as quick as algorithms at aligning windows alongside each other on a display. + +### Why not switch to i3wm? + +The GNOME desktop comes with many useful desktop integration features, which are lost when switching to an i3wm session. Although possible to connect various GNOME session services to an i3wm session, much of the GNOME desktop experience is still lost in the process. The application overview, the GNOME panel, and GNOME extensions. + +Even worse, many users are completely unfamiliar with tiling window managers, and may never feel comfortable switching "cold turkey" to one. By offering tiling window management as a feature that can be opted into, we can empower the user to ease into gaining greater control over their desktop, so that the idea of tiling window management suddenly becomes accessible. + +There are additionally those who do want the traditional stacking window management experience, but they also want to be able to opt into advanced tiling window management, too. So it should be possible to opt into tiling window management as necessary. Other operating systems have successfully combined tiling window management features with the traditional stacking window management experience, and we feel that we can do this with GNOME as well. + +--- + +## Installation + +To install this GNOME Shell extension, you MUST have the following: + +- GNOME Shell 3.36 +- TypeScript 3.8 +- GNU Make + +Proper functionality of the shell requires modifying GNOME's default keyboard shortcuts. For a local installation, run `make local-install`. + +The `master_mantic` git branch corresponds to Ubuntu 23.10 and supports GNOME 45+. For GNOME 3.36 through 44 support, use the `master_jammy` branch. + +If you want to uninstall the extension, you may invoke `make uninstall`, and then open the "Keyboard Shortcuts" panel in GNOME Settings to select the "Reset All.." button in the header bar. + +> Note that if you are packaging for your Linux distribution, many features in Pop Shell will not work out of the box because they require changes to GNOME's default keyboard shortcuts. A local install is necessary if you aren't packaging your GNOME session with these default keyboard shortcuts unset or changed. + +### Packaging status + +- [Fedora](https://src.fedoraproject.org/rpms/gnome-shell-extension-pop-shell/): `sudo dnf install gnome-shell-extension-pop-shell xprop` +- [Gentoo](https://packages.gentoo.org/packages/gnome-extra/gnome-shell-extension-pop-shell): `emerge gnome-shell-extension-pop-shell` +- [openSUSE Tumbleweed](https://build.opensuse.org/package/show/openSUSE:Factory/gnome-shell-extension-pop-shell): `sudo zypper install gnome-shell-extension-pop-shell` +- [Arch Linux](https://aur.archlinux.org/packages/?O=0&K=gnome-shell-extension-pop-shell) (Using Yay as AUR helper): + - `yay -S gnome-shell-extension-pop-shell` + - For precompiled binary version: `yay -S gnome-shell-extension-pop-shell-bin` + - For GitHub repository version: `yay -S gnome-shell-extension-pop-shell-git` + +--- + +## Shared Features + +Features that are shared between stacking and auto-tiling modes. + +### Directional Keys + +These are key to many of the shortcuts utilized by tiling window managers. This document will henceforth refer to these keys as ``, which default to the following keys: + +- `Left` or `h` +- `Down` or `j` +- `Up` or `k` +- `Right` or `l` + +### Overridden GNOME Shortcuts + +- `Super` + `q`: Close window +- `Super` + `m`: Maximize the focused window +- `Super` + `,`: Minimize the focused window +- `Super` + `Esc`: Lock screen +- `Super` + `f`: Files +- `Super` + `e`: Email +- `Super` + `b`: Web Browser +- `Super` + `t`: Terminal + +### Window Management Mode + +> This mode is activated with `Super` + `Return`. + +Window management mode activates additional keyboard control over the size and location of the currently-focused window. The behavior of this mode changes slightly based on whether you are in auto-tile mode, or in the default stacking mode. In the default mode, an overlay is displayed snapped to a grid, which represents a possible future location and size of your focused window. This behavior changes slightly in auto-tiling mode, where resizes are performed immediately and overlays are only shown when swapping windows. + +Activating this enables the following behaviors: + +- `` + - In default mode, this will move the displayed overlay around based on a grid + - In auto-tile mode, this will resize the window +- `Shift` + `` + - In default mode, this will resize the overlay + - In auto-tile mode, this will do nothing +- `Ctrl` + `` + - Selects a window in the given direction of the overlay + - When `Return` is pressed, window positions will be swapped +- `Shift` + `Ctrl` + `` + - In auto-tile mode, this resizes in the opposite direction +- `O`: Toggles between horizontal and vertical tiling in auto-tile mode +- `~`: Toggles between floating and tiling in auto-tile mode +- `Return`: Applies the changes that have been requested +- `Esc`: Cancels any changes that were requested + +### Window Focus Switching + +When not in window management mode, pressing `Super` + `` will shift window focus to a window in the given direction. This is calculated based on the distance between the center of the side of the focused window that the window is being shifted from, and the opposite side of windows surrounding it. + +Switching focus to the left will calculate from the center of the east side of the focused window to the center of the west side of all other windows. The window with the least distance is the window we pick. + +### Launcher + +Pop Shell provides an integrated launcher which interfaces directly with our [pop-launcher](https://github.com/pop-os/launcher) service. JSON IPC is used to communicate between the shell and the launcher in an asynchronous fashion. This functionality was separated from the shell due to performance and maintainability issues. The new launcher is written in Rust and fully async. The launcher has extensive features that would be useful for implementing desktop launchers beyond a shell extension. + +### Inner and Outer Gaps + +Gaps improve the aesthetics of tiled windows and make it easier to grab the edge of a specific window. We've decided to add support for inner and outer gaps, and made these settings configurable in the extension's popup menu. + +### Hiding Window Title Bars + +Windows with server-side decorations may have their title bars completely hidden, resulting in additional screen real estate for your applications, and a visually cleaner environment. This feature can be toggled in the extension's popup menu. Windows can be moved with the mouse by holding `Super` when clicking and dragging a window to another location, or using the keyboard shortcuts native to pop-shell. Windows may be closed by pressing `Super` + `Q`, and maximized with `Super` + `M`. + +--- + +## Floating Mode + +This is the default mode of Pop Shell, which combines traditional stacking window management, with optional tiling window management features. + +### Display Grid + +In this mode, displays are split into a grid of columns and rows. When entering tile mode, windows are snapped to this grid as they are placed. The number of rows and columns are configurable in the extension's popup menu in the panel. + +### Snap-to-Grid + +An optional feature to improve your tiling experience is the ability to snap windows to the grid when using your mouse to move and resize them. This provides the same precision as entering window management mode to position a window with your keyboard, but with the convenience and familiarity of a mouse. This feature can be enabled through the extension's popup menu. + +--- + +## Tiling Mode + +Disabled by default, this mode manages windows using a tree-based tiling window manager. Similar to i3, each node of the tree represents two branches. A branch may be a window, a fork containing more branches, or a stack that contains many windows. Each branch represents a rectangular area of space on the screen, and can be subdivided by creating more branches inside of a branch. As windows are created, they are assigned to the window or stack that is actively focused, which creates a new fork on a window, or attaches the window to the focused stack. As windows are destroyed, the opposite is performed to compress the tree and rearrange windows to their new dimensions. + +### Keyboard Shortcuts + +- `Super` + `O` + - Toggles the orientation of a fork's tiling orientation +- `Super` + `G` + - Toggles a window between floating and tiling. + - See [#customizing the window float list](#customizing-the-floating-window-list) + +### Customizing the Floating Window List +There is file `$XDG_CONFIG_HOME/pop-shell/config.json` where you can add the following structure: +``` +{ + class: "", + title: "" +} +``` +For example, doing `xprop` on GNOME Settings (or GNOME Control Center), the WM_CLASS values are `gnome-control-center` and `Gnome-control-center`. Use the second value (Gnome-control-center), which pop-shell will read. The `title` field is optional. + +After applying changes in `config.json`, you can reload the tiling if it doesn't work the first time. + +## Developers + +Due to the risky nature of plain JavaScript, this GNOME Shell extension is written in [TypeScript](https://www.typescriptlang.org/). In addition to supplying static type-checking and self-documenting classes and interfaces, it allows us to write modern JavaScript syntax whilst supporting the generation of code for older targets. + +Please install the following as dependencies when developing: + +- [`Node.js`](https://nodejs.org/en/) LTS+ (v12+) +- Latest `npm` (comes with NodeJS) +- `npm install typescript@latest` + +While working on the shell, you can recompile, reconfigure, reinstall, and restart GNOME Shell with logging with `make debug`. Note that this only works reliably in X11 sessions, since Wayland will exit to the login screen on restarting the shell. + +[Discussions welcome on Pop Chat](https://chat.pop-os.org/pop-os/channels/development) + +## License + +Licensed under the GNU General Public License, Version 3.0, ([LICENSE](LICENSE) or https://www.gnu.org/licenses/gpl-3.0.en.html) + +### Contribution + +Any contribution intentionally submitted for inclusion in the work by you shall be licensed under the GNU GPLv3. diff --git a/gnome-shell-extension-pop-shell/usr/share/glib-2.0/schemas/50_org.gnome.desktop.wm.keybindings.pop-shell.gschema.override b/gnome-shell-extension-pop-shell/usr/share/glib-2.0/schemas/50_org.gnome.desktop.wm.keybindings.pop-shell.gschema.override new file mode 100644 index 0000000..ca2303a --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/glib-2.0/schemas/50_org.gnome.desktop.wm.keybindings.pop-shell.gschema.override @@ -0,0 +1,18 @@ +[org.gnome.desktop.wm.keybindings] +close = ['F4', 'q'] +maximize = [] +minimize = [] +move-to-monitor-down = [] +move-to-monitor-left = [] +move-to-monitor-right = [] +move-to-monitor-up = [] +move-to-workspace-down = [] +move-to-workspace-left = [] +move-to-workspace-right = [] +move-to-workspace-up = [] +switch-to-workspace-down = ['Down', 'KP_Down', 'j'] +switch-to-workspace-left = [] +switch-to-workspace-right = [] +switch-to-workspace-up = ['Up', 'KP_Up', 'k'] +toggle-maximized = ['m'] +unmaximize = [] diff --git a/gnome-shell-extension-pop-shell/usr/share/glib-2.0/schemas/50_org.gnome.mutter.pop-shell.gschema.override b/gnome-shell-extension-pop-shell/usr/share/glib-2.0/schemas/50_org.gnome.mutter.pop-shell.gschema.override new file mode 100644 index 0000000..4238624 --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/glib-2.0/schemas/50_org.gnome.mutter.pop-shell.gschema.override @@ -0,0 +1,7 @@ +[org.gnome.mutter:GNOME] +attach-modal-dialogs = false +workspaces-only-on-primary = false + +[org.gnome.mutter.keybindings] +toggle-tiled-left = ['Left', 'KP_Left', 'h'] +toggle-tiled-right = ['Right', 'KP_Right', 'l'] diff --git a/gnome-shell-extension-pop-shell/usr/share/glib-2.0/schemas/50_org.gnome.mutter.wayland.pop-shell.gschema.override b/gnome-shell-extension-pop-shell/usr/share/glib-2.0/schemas/50_org.gnome.mutter.wayland.pop-shell.gschema.override new file mode 100644 index 0000000..18566ed --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/glib-2.0/schemas/50_org.gnome.mutter.wayland.pop-shell.gschema.override @@ -0,0 +1,2 @@ +[org.gnome.mutter.wayland.keybindings] +restore-shortcuts = [] diff --git a/gnome-shell-extension-pop-shell/usr/share/glib-2.0/schemas/50_org.gnome.settings-daemon.plugins.media-keys.pop-shell.gschema.override b/gnome-shell-extension-pop-shell/usr/share/glib-2.0/schemas/50_org.gnome.settings-daemon.plugins.media-keys.pop-shell.gschema.override new file mode 100644 index 0000000..bee7aa9 --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/glib-2.0/schemas/50_org.gnome.settings-daemon.plugins.media-keys.pop-shell.gschema.override @@ -0,0 +1,6 @@ +[org.gnome.settings-daemon.plugins.media-keys] +email = ['e'] +home = ['f'] +screensaver = ['Escape'] +www = ['b'] +rotate-video-lock-static = ['XF86RotationLockToggle'] diff --git a/gnome-shell-extension-pop-shell/usr/share/glib-2.0/schemas/50_org.gnome.shell.pop-shell.gschema.override b/gnome-shell-extension-pop-shell/usr/share/glib-2.0/schemas/50_org.gnome.shell.pop-shell.gschema.override new file mode 100644 index 0000000..03ba5c5 --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/glib-2.0/schemas/50_org.gnome.shell.pop-shell.gschema.override @@ -0,0 +1,18 @@ +[org.gnome.shell.keybindings] +open-application-menu = [] +shift-overview-down = [] +shift-overview-up = [] +switch-to-application-1 = [] +switch-to-application-2 = [] +switch-to-application-3 = [] +switch-to-application-4 = [] +switch-to-application-5 = [] +switch-to-application-6 = [] +switch-to-application-7 = [] +switch-to-application-8 = [] +switch-to-application-9 = [] +toggle-message-tray = ['v'] + +[org.gnome.shell.overrides] +attach-modal-dialogs = false +workspaces-only-on-primary = false diff --git a/gnome-shell-extension-pop-shell/usr/share/glib-2.0/schemas/org.gnome.shell.extensions.pop-shell.gschema.xml b/gnome-shell-extension-pop-shell/usr/share/glib-2.0/schemas/org.gnome.shell.extensions.pop-shell.gschema.xml new file mode 100644 index 0000000..fc766da --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/glib-2.0/schemas/org.gnome.shell.extensions.pop-shell.gschema.xml @@ -0,0 +1,288 @@ + + + + + + false + Show a hint around the active window + + + + 5 + + Border radius for active window hint, in pixels + + + + false + Allow showing launcher above fullscreen windows + + + + 2 + Gap between tiled windows + + + + 2 + Gap surrounding tiled windows + + + + true + Show title bars on windows with server-side decorations + + + + true + Handle minimized to tray windows + + + + true + Move cursor to active window when navigating with keyboard shortcuts or touchpad gestures + + + + 0 + The location the mouse cursor focuses when selecting a window + + + + + 64 + Size of a column in the display grid + + + + 64 + Size of a row in the display grid + + + + false + Hide the outer gap when a tree contains only one window + + + + false + Snaps windows to the tiling grid on drop + + + + false + Tile launched windows by default + + + + true + Allow for stacking windows as a result of dragging a window with mouse + + + + + Left','KP_Left','h']]]> + Focus left window + + + + Down','KP_Down','j']]]> + Focus down window + + + + Up','KP_Up','k']]]> + Focus up window + + + + Right','KP_Right','l']]]> + Focus right window + + + + + slash']]]> + Search key combo + + + + + + Toggle stacking mode inside management mode + + + + s']]]> + Toggle stacking mode outside management mode + + + + + Toggle tiling orientation + + + + Return','KP_Enter']]]> + Enter tiling mode + + + + + Accept tiling changes + + + + + Reject tiling changes + + + + g']]]> + Toggles a window between floating and tiling + + + + + y']]]> + Toggles auto-tiling on and off + + + + + + Move window left + + + + + Move window down + + + + + Move window up + + + + + Move window right + + + + + Move window left + + + + + Move window down + + + + + Move window up + + + + + Move window right + + + + o']]]> + Toggle tiling orientation + + + + + Left','KP_Left','h']]]> + Resize window left + + + + Down','KP_Down','j']]]> + Resize window down + + + + Up','KP_Up','k']]]> + Resize window up + + + + Right','KP_Right','l']]]> + Resize window right + + + + + Left','KP_Left','h']]]> + Swap window left + + + + Down','KP_Down','j']]]> + Swap window down + + + + Up','KP_Up','k']]]> + Swap window up + + + + Right','KP_Right','l']]]> + Swap window right + + + + + + Down','KP_Down','j']]]> + Move window to the lower workspace + + + + Up','KP_Up','k']]]> + Move window to the upper workspace + + + + Down','KP_Down','j']]]> + Move window to the lower monitor + + + + Up','KP_Up','k']]]> + Move window to the upper monitor + + + + Left','KP_Left','h']]]> + Move window to the leftward monitor + + + + Right','KP_Right','l']]]> + Move window to the rightward monitor + + + + 'rgba(251, 184, 108, 1)' + The current active-hint-color in RGBA + + + 0 + + Derive some log4j level/order + 0 - OFF + 1 - ERROR + 2 - WARN + 3 - INFO + 4 - DEBUG + + + + + diff --git a/gnome-shell-extension-pop-shell/usr/share/gnome-control-center/keybindings/10-pop-shell-move.xml b/gnome-shell-extension-pop-shell/usr/share/gnome-control-center/keybindings/10-pop-shell-move.xml new file mode 100644 index 0000000..79cb47c --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/gnome-control-center/keybindings/10-pop-shell-move.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gnome-shell-extension-pop-shell/usr/share/gnome-control-center/keybindings/10-pop-shell-navigate.xml b/gnome-shell-extension-pop-shell/usr/share/gnome-control-center/keybindings/10-pop-shell-navigate.xml new file mode 100644 index 0000000..bbf8417 --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/gnome-control-center/keybindings/10-pop-shell-navigate.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/gnome-shell-extension-pop-shell/usr/share/gnome-control-center/keybindings/10-pop-shell-tile.xml b/gnome-shell-extension-pop-shell/usr/share/gnome-control-center/keybindings/10-pop-shell-tile.xml new file mode 100644 index 0000000..eae38f4 --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/gnome-control-center/keybindings/10-pop-shell-tile.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/arena.js b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/arena.js new file mode 100644 index 0000000..916e10f --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/arena.js @@ -0,0 +1,40 @@ +export class Arena { + constructor() { + this.slots = new Array(); + this.unused = new Array(); + } + truncate(n) { + this.slots.splice(n); + this.unused.splice(n); + } + get(n) { + return this.slots[n]; + } + insert(v) { + let n; + const slot = this.unused.pop(); + if (slot !== undefined) { + n = slot; + this.slots[n] = v; + } + else { + n = this.slots.length; + this.slots.push(v); + } + return n; + } + remove(n) { + if (this.slots[n] === null) + return null; + const v = this.slots[n]; + this.slots[n] = null; + this.unused.push(n); + return v; + } + *values() { + for (const v of this.slots) { + if (v !== null) + yield v; + } + } +} diff --git a/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/auto_tiler.js b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/auto_tiler.js new file mode 100644 index 0000000..1b96828 --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/auto_tiler.js @@ -0,0 +1,606 @@ +import * as ecs from './ecs.js'; +import * as lib from './lib.js'; +import * as log from './log.js'; +import * as node from './node.js'; +import * as result from './result.js'; +import * as stack from './stack.js'; +import * as geom from './geom.js'; +import * as tiling from './tiling.js'; +const { Stack } = stack; +const { Ok, Err, ERR } = result; +const { NodeKind } = node; +import * as Tags from './tags.js'; +export class AutoTiler { + constructor(forest, attached) { + this.forest = forest; + this.attached = attached; + } + attach_swap(ext, a, b) { + const a_ent = this.attached.get(a), b_ent = this.attached.get(b); + let a_win = ext.windows.get(a), b_win = ext.windows.get(b); + if (!a_ent || !b_ent || !a_win || !b_win) + return; + const a_fork = this.forest.forks.get(a_ent), b_fork = this.forest.forks.get(b_ent); + if (!a_fork || !b_fork) + return; + const a_stack = a_win.stack, b_stack = b_win.stack; + if (ext.auto_tiler) { + if (a_win.stack !== null) { + const stack = ext.auto_tiler.forest.stacks.get(a_win.stack); + if (stack) { + a = stack.active; + a_win = ext.windows.get(a); + if (!a_win) + return; + stack.deactivate(a_win); + } + } + if (b_win.stack !== null) { + const stack = ext.auto_tiler.forest.stacks.get(b_win.stack); + if (stack) { + b = stack.active; + b_win = ext.windows.get(b); + if (!b_win) + return; + stack.deactivate(b_win); + } + } + } + const a_fn = a_fork.replace_window(ext, a_win, b_win); + this.forest.on_attach(a_ent, b); + const b_fn = b_fork.replace_window(ext, b_win, a_win); + this.forest.on_attach(b_ent, a); + if (a_fn) + a_fn(); + if (b_fn) + b_fn(); + a_win.stack = b_stack; + b_win.stack = a_stack; + a_win.meta.get_compositor_private()?.show(); + b_win.meta.get_compositor_private()?.show(); + this.tile(ext, a_fork, a_fork.area); + this.tile(ext, b_fork, b_fork.area); + } + update_toplevel(ext, fork, monitor, smart_gaps) { + let rect = ext.monitor_work_area(monitor); + fork.smart_gapped = smart_gaps && fork.right === null; + if (!fork.smart_gapped) { + rect.x += ext.gap_outer; + rect.y += ext.gap_outer; + rect.width -= ext.gap_outer * 2; + rect.height -= ext.gap_outer * 2; + } + if (fork.left.inner.kind === 2) { + const win = ext.windows.get(fork.left.inner.entity); + if (win) { + win.smart_gapped = fork.smart_gapped; + } + } + fork.area = fork.set_area(rect.clone()); + fork.length_left = Math.round(fork.prev_ratio * fork.length()); + this.tile(ext, fork, fork.area); + } + attach_to_monitor(ext, win, workspace_id, smart_gaps) { + let rect = ext.monitor_work_area(workspace_id[0]); + if (!smart_gaps) { + rect.x += ext.gap_outer; + rect.y += ext.gap_outer; + rect.width -= ext.gap_outer * 2; + rect.height -= ext.gap_outer * 2; + } + const [entity, fork] = this.forest.create_toplevel(win.entity, rect.clone(), workspace_id); + this.forest.on_attach(entity, win.entity); + fork.smart_gapped = smart_gaps; + win.smart_gapped = smart_gaps; + this.tile(ext, fork, rect); + } + attach_to_window(ext, attachee, attacher, move_by, stack_from_left = true) { + let attached = this.forest.attach_window(ext, attachee.entity, attacher.entity, move_by, stack_from_left); + if (attached) { + const [, fork] = attached; + const monitor = ext.monitors.get(attachee.entity); + if (monitor) { + if (fork.is_toplevel && fork.smart_gapped && fork.right) { + fork.smart_gapped = false; + let rect = ext.monitor_work_area(fork.monitor); + rect.x += ext.gap_outer; + rect.y += ext.gap_outer; + rect.width -= ext.gap_outer * 2; + rect.height -= ext.gap_outer * 2; + fork.set_area(rect); + } + this.tile(ext, fork, fork.area.clone()); + return true; + } + else { + log.error(`missing monitor association for Window(${attachee.entity})`); + } + } + return false; + } + attach_to_workspace(ext, win, id) { + if (ext.should_ignore_workspace(id[0])) { + id = [id[0], 0]; + } + const toplevel = this.forest.find_toplevel(id); + if (toplevel) { + const onto = this.forest.largest_window_on(ext, toplevel); + if (onto) { + if (this.attach_to_window(ext, onto, win, { auto: 0 })) { + return; + } + } + } + this.attach_to_monitor(ext, win, id, ext.settings.smart_gaps()); + } + auto_tile(ext, win, ignore_focus = false) { + const result = this.fetch_mode(ext, win, ignore_focus); + this.detach_window(ext, win.entity); + if (result.kind == ERR) { + log.debug(`attach to workspace: ${result.value}`); + this.attach_to_workspace(ext, win, ext.workspace_id(win)); + } + else { + log.debug(`attaching to window ${win.entity}`); + this.attach_to_window(ext, result.value, win, { auto: 0 }); + } + } + destroy(ext) { + for (const [, [fent]] of this.forest.toplevel) { + for (const node of this.forest.iter(fent)) { + if (node.inner.kind === 2) { + this.forest.on_detach(node.inner.entity); + } + else if (node.inner.kind === 3) { + for (const window of node.inner.entities) { + this.forest.on_detach(window); + } + } + } + } + for (const stack of this.forest.stacks.values()) + stack.destroy(); + for (const window of ext.windows.values()) { + window.stack = null; + } + this.forest.stacks.truncate(0); + ext.show_border_on_focused(); + } + detach_window(ext, win) { + this.attached.take_with(win, (prev_fork) => { + const reflow_fork = this.forest.detach(ext, prev_fork, win); + if (reflow_fork) { + const fork = reflow_fork[1]; + if (fork.is_toplevel && ext.settings.smart_gaps() && fork.right === null) { + let rect = ext.monitor_work_area(fork.monitor); + fork.set_area(rect); + fork.smart_gapped = true; + } + this.tile(ext, fork, fork.area); + } + ext.windows.with(win, (info) => (info.ignore_detach = false)); + }); + } + dropped_on_sibling(ext, win, swap = true) { + const fork_entity = this.attached.get(win); + if (fork_entity) { + const cursor = lib.cursor_rect(); + const fork = this.forest.forks.get(fork_entity); + if (fork) { + if (fork.left.inner.kind === 2 && fork.right && fork.right.inner.kind === 2) { + if (fork.left.is_window(win)) { + const sibling = ext.windows.get(fork.right.inner.entity); + if (sibling && sibling.rect().contains(cursor)) { + if (swap) { + fork.left.inner.entity = fork.right.inner.entity; + fork.right.inner.entity = win; + this.tile(ext, fork, fork.area); + } + return true; + } + } + else if (fork.right.is_window(win)) { + const sibling = ext.windows.get(fork.left.inner.entity); + if (sibling && sibling.rect().contains(cursor)) { + if (swap) { + fork.right.inner.entity = fork.left.inner.entity; + fork.left.inner.entity = win; + this.tile(ext, fork, fork.area); + } + return true; + } + } + } + } + } + return false; + } + find_stack(entity) { + const att = this.attached.get(entity); + if (att) { + const fork = this.forest.forks.get(att); + if (fork) { + if (fork.left.is_in_stack(entity)) { + return [fork, fork.left, true]; + } + else if (fork.right?.is_in_stack(entity)) { + return [fork, fork.right, false]; + } + } + } + return null; + } + get_parent_fork(window) { + const entity = this.attached.get(window); + if (entity === null) + return null; + const fork = this.forest.forks.get(entity); + return fork; + } + largest_on_workspace(ext, monitor, workspace) { + const workspace_id = [monitor, workspace]; + const toplevel = this.forest.find_toplevel(workspace_id); + if (toplevel) { + return this.forest.largest_window_on(ext, toplevel); + } + return null; + } + on_drop(ext, win, via_overview = false) { + const [cursor, monitor] = ext.cursor_status(); + const workspace = ext.active_workspace(); + if (win.rect().contains(cursor)) { + via_overview = false; + } + const attach_mon = () => { + const attach_to = this.largest_on_workspace(ext, monitor, workspace); + if (attach_to) { + this.attach_to_window(ext, attach_to, win, { auto: 0 }); + } + else { + this.attach_to_monitor(ext, win, [monitor, workspace], ext.settings.smart_gaps()); + } + }; + if (via_overview) { + this.detach_window(ext, win.entity); + attach_mon(); + return; + } + let attach_to = null; + for (const found of ext.windows_at_pointer(cursor, monitor, workspace)) { + if (found != win && this.attached.contains(found.entity)) { + attach_to = found; + break; + } + } + const fork = this.get_parent_fork(win.entity); + if (!fork) + return; + const windowless = this.largest_on_workspace(ext, monitor, workspace) === null; + if (attach_to === null) { + if (fork.left.inner.kind === 2 && fork.right?.inner.kind === 2) { + let attaching = fork.left.is_window(win.entity) ? fork.right.inner.entity : fork.left.inner.entity; + attach_to = ext.windows.get(attaching); + } + else if (!windowless) { + this.tile(ext, fork, fork.area); + return true; + } + } + if (windowless) { + this.detach_window(ext, win.entity); + this.attach_to_monitor(ext, win, [monitor, workspace], ext.settings.smart_gaps()); + } + else if (attach_to) { + this.place_or_stack(ext, win, attach_to, cursor); + } + else { + this.detach_window(ext, win.entity); + attach_mon(); + } + } + place_or_stack(ext, win, attach_to, cursor) { + const fork = this.get_parent_fork(attach_to.entity); + if (!fork) + return true; + const is_sibling = this.windows_are_siblings(win.entity, attach_to.entity); + const attach_area = (win.stack === null && attach_to.stack === null && is_sibling) || (win.stack === null && is_sibling) + ? fork.area + : attach_to.meta.get_frame_rect(); + let placement = cursor_placement(ext, attach_area, cursor); + const stack = ext.auto_tiler?.find_stack(attach_to.entity); + const matching_stack = win.stack !== null && win.stack === attach_to.stack; + const { Left, Up, Right, Down } = tiling.Direction; + const swap = (o, d) => { + fork.set_orientation(o); + const is_left = fork.left.is_window(win.entity); + const swap = (is_left && (d == Right || d == Down)) || (!is_left && (d == Left || d == Up)); + if (swap) { + fork.swap_branches(); + } + this.tile(ext, fork, fork.area); + }; + if (placement) { + const direction = placement.orientation === lib.Orientation.HORIZONTAL + ? placement.swap + ? Left + : Right + : placement.swap + ? Up + : Down; + if (stack) { + if (matching_stack) { + ext.tiler.move_from_stack(ext, stack, win, direction, true); + return true; + } + else if (attach_to.stack !== null) { + const onto_stack = ext.auto_tiler?.find_stack(attach_to.entity); + if (onto_stack) { + if (is_sibling && win.stack === null) { + swap(placement.orientation, direction); + return true; + } + else { + ext.tiler.move_alongside_stack(ext, onto_stack, win, direction); + } + return true; + } + } + } + else if (is_sibling && win.stack === null) { + swap(placement.orientation, direction); + return true; + } + else if (fork.is_toplevel && fork.right === null) { + this.detach_window(ext, win.entity); + this.attach_to_window(ext, attach_to, win, placement); + swap(placement.orientation, direction); + return true; + } + } + else if (matching_stack) { + this.tile(ext, fork, fork.area); + return true; + } + else { + if (attach_to.stack === null) + this.create_stack(ext, attach_to); + placement = { auto: 0 }; + } + this.detach_window(ext, win.entity); + return this.attach_to_window(ext, attach_to, win, placement); + } + reflow(ext, win) { + const fork_entity = this.attached.get(win); + if (!fork_entity) + return; + ext.register_fn(() => { + const fork = this.forest.forks.get(fork_entity); + if (fork) + this.tile(ext, fork, fork.area); + }); + } + tile(ext, fork, area) { + this.forest.tile(ext, fork, area); + } + toggle_floating(ext) { + const focused = ext.focus_window(); + if (!focused) + return; + let wm_class = focused.meta.get_wm_class(); + let wm_title = focused.meta.get_title(); + let float_except = false; + if (wm_class != null && wm_title != null) { + float_except = ext.conf.window_shall_float(wm_class, wm_title); + } + if (float_except) { + if (ext.contains_tag(focused.entity, Tags.ForceTile)) { + ext.delete_tag(focused.entity, Tags.ForceTile); + const fork_entity = this.attached.get(focused.entity); + if (fork_entity) { + this.detach_window(ext, focused.entity); + } + } + else { + ext.add_tag(focused.entity, Tags.ForceTile); + this.auto_tile(ext, focused, false); + } + } + else { + if (ext.contains_tag(focused.entity, Tags.Floating)) { + ext.delete_tag(focused.entity, Tags.Floating); + this.auto_tile(ext, focused, false); + } + else { + const fork_entity = this.attached.get(focused.entity); + if (fork_entity) { + this.detach_window(ext, focused.entity); + ext.add_tag(focused.entity, Tags.Floating); + } + } + } + ext.register_fn(() => focused.activate(true)); + } + toggle_orientation(ext, window) { + const result = this.toggle_orientation_(ext, window); + if (result.kind == ERR) { + log.warn(`toggle_orientation: ${result.value}`); + } + } + toggle_stacking(ext, window) { + const focused = window ?? ext.focus_window(); + if (!focused) + return; + if (ext.contains_tag(focused.entity, Tags.Floating)) { + ext.delete_tag(focused.entity, Tags.Floating); + this.auto_tile(ext, focused, false); + } + const fork_entity = this.attached.get(focused.entity); + if (fork_entity) { + const fork = this.forest.forks.get(fork_entity); + if (fork) { + this.unstack(ext, fork, focused, true); + } + } + } + unstack(ext, fork, win, toggled = false) { + const stack_toggle = (fork, branch) => { + const stack = branch.inner; + if (stack.entities.length === 1) { + win.stack = null; + this.forest.stacks.remove(stack.idx)?.destroy(); + fork.measure(this.forest, ext, fork.area, this.forest.on_record()); + return node.Node.window(win.entity); + } + return null; + }; + if (toggled && fork.left.is_window(win.entity)) { + win.stack = this.forest.stacks.insert(new Stack(ext, win.entity, fork.workspace, fork.monitor)); + fork.left = node.Node.stacked(win.entity, win.stack); + fork.measure(this.forest, ext, fork.area, this.forest.on_record()); + } + else if (fork.left.is_in_stack(win.entity)) { + const node = stack_toggle(fork, fork.left); + if (node) { + fork.left = node; + if (!fork.right) { + this.forest.reassign_to_parent(fork, node); + } + } + } + else if (toggled && fork.right?.is_window(win.entity)) { + win.stack = this.forest.stacks.insert(new Stack(ext, win.entity, fork.workspace, fork.monitor)); + fork.right = node.Node.stacked(win.entity, win.stack); + fork.measure(this.forest, ext, fork.area, this.forest.on_record()); + } + else if (fork.right?.is_in_stack(win.entity)) { + const node = stack_toggle(fork, fork.right); + if (node) + fork.right = node; + } + this.tile(ext, fork, fork.area); + } + stack_left(ext, fork, window) { + window.stack = this.forest.stacks.insert(new Stack(ext, window.entity, fork.workspace, fork.monitor)); + fork.left = node.Node.stacked(window.entity, window.stack); + fork.measure(this.forest, ext, fork.area, this.forest.on_record()); + } + stack_right(ext, fork, window) { + window.stack = this.forest.stacks.insert(new Stack(ext, window.entity, fork.workspace, fork.monitor)); + fork.right = node.Node.stacked(window.entity, window.stack); + fork.measure(this.forest, ext, fork.area, this.forest.on_record()); + } + create_stack(ext, window) { + const entity = this.attached.get(window.entity); + if (!entity) + return; + const fork = this.forest.forks.get(entity); + if (!fork) + return; + if (fork.left.is_window(window.entity)) { + this.stack_left(ext, fork, window); + } + else if (fork.right?.is_window(window.entity)) { + this.stack_right(ext, fork, window); + } + } + update_stack(ext, stack) { + if (stack.rect) { + const container = this.forest.stacks.get(stack.idx); + if (container) { + container.clear(); + for (const entity of stack.entities) { + const window = ext.windows.get(entity); + if (window) { + window.stack = stack.idx; + container.add(window); + } + } + container.update_positions(stack.rect); + container.auto_activate(); + } + } + else { + log.warn('stack rect was null'); + } + } + windows_are_siblings(a, b) { + const a_parent = this.attached.get(a); + const b_parent = this.attached.get(b); + if (a_parent !== null && null !== b_parent && ecs.entity_eq(a_parent, b_parent)) { + return a_parent; + } + return null; + } + fetch_mode(ext, win, ignore_focus = false) { + if (ignore_focus) { + return Err('ignoring focus'); + } + const prev = ext.previously_focused(win); + if (!prev) { + return Err('no window has been previously focused'); + } + let onto = ext.windows.get(prev); + if (!onto) { + return Err('no focus window'); + } + if (ecs.entity_eq(onto.entity, win.entity)) { + return Err('tiled window and attach window are the same window'); + } + if (!onto.is_tilable(ext)) { + return Err('focused window is not tilable'); + } + if (onto.meta.minimized) { + return Err('previous window was minimized'); + } + if (!this.attached.contains(onto.entity)) { + return Err('focused window is not attached'); + } + return onto.meta.get_monitor() == win.meta.get_monitor() && onto.workspace_id() == win.workspace_id() + ? Ok(onto) + : Err('window is not on the same monitor or workspace'); + } + toggle_orientation_(ext, focused) { + if (focused.meta.get_maximized()) { + return Err('cannot toggle maximized window'); + } + const fork_entity = this.attached.get(focused.entity); + if (!fork_entity) { + return Err(`window is not attached to the tree`); + } + const fork = this.forest.forks.get(fork_entity); + if (!fork) { + return Err("window's fork attachment does not exist"); + } + if (!fork.right) + return Ok(void 0); + fork.toggle_orientation(); + this.forest.measure(ext, fork, fork.area); + for (const child of this.forest.iter(fork_entity, NodeKind.FORK)) { + const child_fork = this.forest.forks.get(child.inner.entity); + if (child_fork) { + child_fork.rebalance_orientation(); + this.forest.measure(ext, child_fork, child_fork.area); + } + else { + log.error('toggle_orientation: Fork(${child.entity}) does not exist to have its orientation toggled'); + } + } + this.forest.arrange(ext, fork.workspace, true); + return Ok(void 0); + } +} +export function cursor_placement(ext, area, cursor) { + const { LEFT, RIGHT, TOP, BOTTOM } = geom.Side; + const { HORIZONTAL, VERTICAL } = lib.Orientation; + const [, side] = geom.nearest_side(ext, [cursor.x, cursor.y], area); + let res = side === LEFT + ? [HORIZONTAL, true] + : side === RIGHT + ? [HORIZONTAL, false] + : side === TOP + ? [VERTICAL, true] + : side === BOTTOM + ? [VERTICAL, false] + : null; + return res ? { orientation: res[0], swap: res[1] } : null; +} diff --git a/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/color_dialog/main.js b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/color_dialog/main.js new file mode 100644 index 0000000..0ed5b91 --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/color_dialog/main.js @@ -0,0 +1,65 @@ +#!/usr/bin/gjs --module +import Gio from 'gi://Gio'; +import GLib from 'gi://GLib'; +import Gtk from 'gi://Gtk?version=3.0'; +import Gdk from 'gi://Gdk'; +const EXT_PATH_DEFAULTS = [ + GLib.get_home_dir() + '/.local/share/gnome-shell/extensions/', + '/usr/share/gnome-shell/extensions/', +]; +const DEFAULT_HINT_COLOR = 'rgba(251, 184, 108, 1)'; +function getExtensionPath(uuid) { + let ext_path = null; + for (let i = 0; i < EXT_PATH_DEFAULTS.length; i++) { + let path = EXT_PATH_DEFAULTS[i]; + let file = Gio.File.new_for_path(path + uuid); + log(file.get_path()); + if (file.query_exists(null)) { + ext_path = file; + break; + } + } + return ext_path; +} +function getSettings(schema) { + let extensionPath = getExtensionPath('pop-shell@system76.com'); + if (!extensionPath) + throw new Error('getSettings() can only be called when extension is available'); + const GioSSS = Gio.SettingsSchemaSource; + const schemaDir = extensionPath.get_child('schemas'); + let schemaSource = schemaDir.query_exists(null) + ? GioSSS.new_from_directory(schemaDir.get_path(), GioSSS.get_default(), false) + : GioSSS.get_default(); + const schemaObj = schemaSource.lookup(schema, true); + if (!schemaObj) { + throw new Error('Schema ' + schema + ' could not be found for extension '); + } + return new Gio.Settings({ settings_schema: schemaObj }); +} +function launch_color_dialog() { + let popshell_settings = getSettings('org.gnome.shell.extensions.pop-shell'); + let color_dialog = new Gtk.ColorChooserDialog({ + title: 'Choose Color', + }); + color_dialog.show_editor = true; + color_dialog.show_all(); + let rgba = new Gdk.RGBA(); + if (rgba.parse(popshell_settings.get_string('hint-color-rgba'))) { + color_dialog.set_rgba(rgba); + } + else { + rgba.parse(DEFAULT_HINT_COLOR); + color_dialog.set_rgba(rgba); + } + let response = color_dialog.run(); + if (response === Gtk.ResponseType.CANCEL) { + color_dialog.destroy(); + } + else if (response === Gtk.ResponseType.OK) { + popshell_settings.set_string('hint-color-rgba', color_dialog.get_rgba().to_string()); + Gio.Settings.sync(); + color_dialog.destroy(); + } +} +Gtk.init(null); +launch_color_dialog(); diff --git a/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/config.js b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/config.js new file mode 100644 index 0000000..57e6bf2 --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/config.js @@ -0,0 +1,239 @@ +import GLib from 'gi://GLib'; +import Gio from 'gi://Gio'; +const CONF_DIR = GLib.get_user_config_dir() + '/pop-shell'; +export var CONF_FILE = CONF_DIR + '/config.json'; +export const DEFAULT_FLOAT_RULES = [ + { class: 'Authy Desktop' }, + { class: 'Com.github.amezin.ddterm' }, + { class: 'Com.github.donadigo.eddy' }, + { class: 'Conky' }, + { title: 'Discord Updater' }, + { class: 'Enpass', title: 'Enpass Assistant' }, + { class: 'Floating Window Exceptions' }, + { class: 'Gjs', title: 'Settings' }, + { class: 'Gnome-initial-setup' }, + { class: 'Gnome-terminal', title: 'Preferences – General' }, + { class: 'Guake' }, + { class: 'Io.elementary.sideload' }, + { title: 'JavaEmbeddedFrame' }, + { class: 'KotatogramDesktop', title: 'Media viewer' }, + { class: 'Mozilla VPN' }, + { class: 'update-manager', title: 'Software Updater' }, + { class: 'Solaar' }, + { class: 'Steam', title: '^((?!Steam).)*$' }, + { class: 'Steam', title: '^.*(Guard|Login).*' }, + { class: 'TelegramDesktop', title: 'Media viewer' }, + { class: 'Zotero', title: 'Quick Format Citation' }, + { class: 'firefox', title: '^(?!.*Mozilla Firefox).*$' }, + { class: 'gnome-screenshot' }, + { class: 'ibus-.*' }, + { class: 'jetbrains-toolbox' }, + { class: 'jetbrains-webstorm', title: 'Customize WebStorm' }, + { class: 'jetbrains-webstorm', title: 'License Activation' }, + { class: 'jetbrains-webstorm', title: 'Welcome to WebStorm' }, + { class: 'krunner' }, + { class: 'pritunl' }, + { class: 're.sonny.Junction' }, + { class: 'system76-driver' }, + { class: 'tilda' }, + { class: 'zoom' }, + { class: '^.*action=join.*$' }, + { class: 'gjs' }, +]; +export const SKIPTASKBAR_EXCEPTIONS = [ + { class: 'Conky' }, + { class: 'gjs' }, + { class: 'Guake' }, + { class: 'Com.github.amezin.ddterm' }, + { class: 'plank' }, +]; +export class Config { + constructor() { + this.float = []; + this.skiptaskbarhidden = []; + this.log_on_focus = false; + } + add_app_exception(wmclass) { + for (const r of this.float) { + if (r.class === wmclass && r.title === undefined) + return; + } + this.float.push({ class: wmclass }); + this.sync_to_disk(); + } + add_window_exception(wmclass, title) { + for (const r of this.float) { + if (r.class === wmclass && r.title === title) + return; + } + this.float.push({ class: wmclass, title }); + this.sync_to_disk(); + } + window_shall_float(wclass, title) { + for (const rule of this.float.concat(DEFAULT_FLOAT_RULES)) { + if (rule.class) { + if (!new RegExp(rule.class, 'i').test(wclass)) { + continue; + } + } + if (rule.title) { + if (!new RegExp(rule.title, 'i').test(title)) { + continue; + } + } + return rule.disabled ? false : true; + } + return false; + } + skiptaskbar_shall_hide(meta_window) { + let wmclass = meta_window.get_wm_class(); + let wmtitle = meta_window.get_title(); + if (!meta_window.is_skip_taskbar()) + return false; + for (const rule of this.skiptaskbarhidden.concat(SKIPTASKBAR_EXCEPTIONS)) { + if (rule.class) { + if (!new RegExp(rule.class, 'i').test(wmclass)) { + continue; + } + } + if (rule.title) { + if (!new RegExp(rule.title, 'i').test(wmtitle)) { + continue; + } + } + return rule.disabled ? false : true; + } + return false; + } + reload() { + const conf = Config.from_config(); + if (conf.tag === 0) { + let c = conf.value; + this.float = c.float; + this.log_on_focus = c.log_on_focus; + } + else { + log(`error loading conf: ${conf.why}`); + } + } + rule_disabled(rule) { + for (const value of this.float.values()) { + if (value.disabled && rule.class === value.class && value.title === rule.title) { + return true; + } + } + return false; + } + to_json() { + return JSON.stringify(this, set_to_json, 2); + } + toggle_system_exception(wmclass, wmtitle, disabled) { + if (disabled) { + for (const value of DEFAULT_FLOAT_RULES) { + if (value.class === wmclass && value.title === wmtitle) { + value.disabled = disabled; + this.float.push(value); + this.sync_to_disk(); + return; + } + } + } + let index = 0; + let found = false; + for (const value of this.float) { + if (value.class === wmclass && value.title === wmtitle) { + found = true; + break; + } + index += 1; + } + if (found) + swap_remove(this.float, index); + this.sync_to_disk(); + } + remove_user_exception(wmclass, wmtitle) { + let index = 0; + let found = new Array(); + for (const value of this.float.values()) { + if (value.class === wmclass && value.title === wmtitle) { + found.push(index); + } + index += 1; + } + if (found.length !== 0) { + for (const idx of found) + swap_remove(this.float, idx); + this.sync_to_disk(); + } + } + static from_json(json) { + try { + return JSON.parse(json); + } + catch (error) { + return new Config(); + } + } + static from_config() { + const stream = Config.read(); + if (stream.tag === 1) + return stream; + let value = Config.from_json(stream.value); + return { tag: 0, value }; + } + static gio_file() { + try { + const conf = Gio.File.new_for_path(CONF_FILE); + if (!conf.query_exists(null)) { + const dir = Gio.File.new_for_path(CONF_DIR); + if (!dir.query_exists(null) && !dir.make_directory(null)) { + return { tag: 1, why: 'failed to create pop-shell config directory' }; + } + const example = new Config(); + example.float.push({ class: 'pop-shell-example', title: 'pop-shell-example' }); + conf.create(Gio.FileCreateFlags.NONE, null).write_all(JSON.stringify(example, undefined, 2), null); + } + return { tag: 0, value: conf }; + } + catch (why) { + return { tag: 1, why: `Gio.File I/O error: ${why}` }; + } + } + static read() { + try { + const file = Config.gio_file(); + if (file.tag === 1) + return file; + const [, buffer] = file.value.load_contents(null); + return { tag: 0, value: imports.byteArray.toString(buffer) }; + } + catch (why) { + return { tag: 1, why: `failed to read pop-shell config: ${why}` }; + } + } + static write(data) { + try { + const file = Config.gio_file(); + if (file.tag === 1) + return file; + file.value.replace_contents(data, null, false, Gio.FileCreateFlags.NONE, null); + return { tag: 0, value: file.value }; + } + catch (why) { + return { tag: 1, why: `failed to write to config: ${why}` }; + } + } + sync_to_disk() { + Config.write(this.to_json()); + } +} +function set_to_json(_key, value) { + if (typeof value === 'object' && value instanceof Set) { + return [...value]; + } + return value; +} +function swap_remove(array, index) { + array[index] = array[array.length - 1]; + return array.pop(); +} diff --git a/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/context.js b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/context.js new file mode 100644 index 0000000..cf5557c --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/context.js @@ -0,0 +1,24 @@ +import St from 'gi://St'; +import * as Main from 'resource:///org/gnome/shell/ui/main.js'; +import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js'; +export function addMenu(widget, request) { + const menu = new PopupMenu.PopupMenu(widget, 0.0, St.Side.TOP, 0); + Main.uiGroup.add_child(menu.actor); + menu.actor.hide(); + menu.actor.add_style_class_name('panel-menu'); + widget.connect('button-press-event', (_, event) => { + if (event.get_button() === 3) { + request(menu); + } + }); + return menu; +} +export function addContext(menu, name, activate) { + const menu_item = appendMenuItem(menu, name); + menu_item.connect('activate', () => activate()); +} +function appendMenuItem(menu, label) { + let item = new PopupMenu.PopupMenuItem(label); + menu.addMenuItem(item); + return item; +} diff --git a/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/dark.css b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/dark.css new file mode 100644 index 0000000..732aefb --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/dark.css @@ -0,0 +1,57 @@ +.pop-shell-active-hint { + border-style: solid; + border-color: #FBB86C; + border-radius: var(--active-hint-border-radius, 5px); + box-shadow: inset 0 0 0 1px rgba(24, 23, 23, 0) +} + +.pop-shell-overlay { + background-color: rgba(53, 132, 228, 0.3); +} + +.pop-shell-border-normal { + border-width: 3px; +} + +.pop-shell-border-maximize { + border-width: 3px; +} + +.pop-shell-search-element:select{ + background: rgba(246, 246, 246, .2); + border-radius: 5px; + color: #EDEDED; +} + +.pop-shell-search-icon { + margin-right: 10px; +} + +.pop-shell-search-cat { + margin-right: 10px; +} + +.pop-shell-search-element { + padding-left: 10px; + padding-right: 2px; + padding-top: 6px; + padding-bottom: 6px; +} + +.pop-shell-tab { + border: 1px solid #333; + color: #000; + padding: 0 1em; +} + +.pop-shell-tab-active { + background: #FBB86C; +} + +.pop-shell-tab-inactive { + background: #9B8E8A; +} + +.pop-shell-tab-urgent { + background: #D00; +} diff --git a/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/dbus_service.js b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/dbus_service.js new file mode 100644 index 0000000..66056ab --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/dbus_service.js @@ -0,0 +1,44 @@ +import Gio from 'gi://Gio'; +const IFACE = ` + + + + + + + + + + + + + + + + + + + +`; +export class Service { + constructor() { + this.FocusLeft = () => { }; + this.FocusRight = () => { }; + this.FocusUp = () => { }; + this.FocusDown = () => { }; + this.Launcher = () => { }; + this.WindowFocus = () => { }; + this.WindowList = () => []; + this.WindowQuit = () => { }; + this.dbus = Gio.DBusExportedObject.wrapJSObject(IFACE, this); + const onBusAcquired = (conn) => { + this.dbus.export(conn, '/com/System76/PopShell'); + }; + function onNameAcquired() { } + function onNameLost() { } + this.id = Gio.bus_own_name(Gio.BusType.SESSION, 'com.System76.PopShell', Gio.BusNameOwnerFlags.NONE, onBusAcquired, onNameAcquired, onNameLost); + } + destroy() { + Gio.bus_unown_name(this.id); + } +} diff --git a/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/dialog_add_exception.js b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/dialog_add_exception.js new file mode 100644 index 0000000..34c5907 --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/dialog_add_exception.js @@ -0,0 +1,59 @@ +import * as Lib from './lib.js'; +import St from 'gi://St'; +import Clutter from 'gi://Clutter'; +import * as ModalDialog from 'resource:///org/gnome/shell/ui/modalDialog.js'; +export class AddExceptionDialog { + constructor(cancel, this_app, current_window, on_close) { + this.dialog = new ModalDialog.ModalDialog({ + styleClass: 'pop-shell-search modal-dialog', + destroyOnClose: false, + shellReactive: true, + shouldFadeIn: false, + shouldFadeOut: false, + }); + let title = St.Label.new('Add Floating Window Exception'); + title.set_x_align(Clutter.ActorAlign.CENTER); + title.set_style('font-weight: bold'); + let desc = St.Label.new('Float the selected window or all windows from the application.'); + desc.set_x_align(Clutter.ActorAlign.CENTER); + let l = this.dialog.contentLayout; + l.add_child(title); + l.add_child(desc); + this.dialog.contentLayout.width = Math.max(Lib.current_monitor().width / 4, 640); + this.dialog.addButton({ + label: 'Cancel', + action: () => { + cancel(); + on_close(); + this.close(); + }, + key: Clutter.KEY_Escape, + }); + this.dialog.addButton({ + label: "This App's Windows", + action: () => { + this_app(); + on_close(); + this.close(); + }, + }); + this.dialog.addButton({ + label: 'Current Window Only', + action: () => { + current_window(); + on_close(); + this.close(); + }, + }); + } + close() { + this.dialog.close(global.get_current_time()); + } + show() { + this.dialog.show(); + } + open() { + this.dialog.open(global.get_current_time(), false); + this.show(); + } +} diff --git a/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/ecs.js b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/ecs.js new file mode 100644 index 0000000..4e2eebb --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/ecs.js @@ -0,0 +1,174 @@ +var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { + if (kind === "m") throw new TypeError("Private method is not writable"); + if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); + if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); + return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; +}; +var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { + if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); + if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); + return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); +}; +var _System_executor; +export function entity_eq(a, b) { + return a[0] == b[0] && b[1] == b[1]; +} +export function entity_new(pos, gen) { + return [pos, gen]; +} +export class Storage { + constructor() { + this.store = new Array(); + } + *_iter() { + let idx = 0; + for (const slot of this.store) { + if (slot) + yield [idx, slot]; + idx += 1; + } + } + *iter() { + for (const [idx, [gen, value]] of this._iter()) { + yield [entity_new(idx, gen), value]; + } + } + *find(func) { + for (const [idx, [gen, value]] of this._iter()) { + if (func(value)) + yield entity_new(idx, gen); + } + } + *values() { + for (const [, [, value]] of this._iter()) { + yield value; + } + } + contains(entity) { + return this.get(entity) != null; + } + get(entity) { + let [id, gen] = entity; + const val = this.store[id]; + return val && val[0] == gen ? val[1] : null; + } + get_or(entity, init) { + let value = this.get(entity); + if (!value) { + value = init(); + this.insert(entity, value); + } + return value; + } + insert(entity, component) { + let [id, gen] = entity; + let length = this.store.length; + if (length >= id) { + this.store.fill(null, length, id); + } + this.store[id] = [gen, component]; + } + is_empty() { + for (const slot of this.store) + if (slot) + return false; + return true; + } + remove(entity) { + const comp = this.get(entity); + if (comp) { + this.store[entity[0]] = null; + } + return comp; + } + take_with(entity, func) { + const component = this.remove(entity); + return component ? func(component) : null; + } + with(entity, func) { + const component = this.get(entity); + return component ? func(component) : null; + } +} +export class World { + constructor() { + this.entities_ = new Array(); + this.storages = new Array(); + this.tags_ = new Array(); + this.free_slots = new Array(); + } + get capacity() { + return this.entities_.length; + } + get free() { + return this.free_slots.length; + } + get length() { + return this.capacity - this.free; + } + tags(entity) { + return this.tags_[entity[0]]; + } + *entities() { + for (const entity of this.entities_.values()) { + if (!(this.free_slots.indexOf(entity[0]) > -1)) + yield entity; + } + } + create_entity() { + let slot = this.free_slots.pop(); + if (slot) { + var entity = this.entities_[slot]; + entity[1] += 1; + } + else { + var entity = entity_new(this.capacity, 0); + this.entities_.push(entity); + this.tags_.push(new Set()); + } + return entity; + } + delete_entity(entity) { + this.tags(entity).clear(); + for (const storage of this.storages) { + storage.remove(entity); + } + this.free_slots.push(entity[0]); + } + add_tag(entity, tag) { + this.tags(entity).add(tag); + } + contains_tag(entity, tag) { + return this.tags(entity).has(tag); + } + delete_tag(entity, tag) { + this.tags(entity).delete(tag); + } + register_storage() { + let storage = new Storage(); + this.storages.push(storage); + return storage; + } + unregister_storage(storage) { + let matched = this.storages.indexOf(storage); + if (matched) { + swap_remove(this.storages, matched); + } + } +} +function swap_remove(array, index) { + array[index] = array[array.length - 1]; + return array.pop(); +} +export class System extends World { + constructor(executor) { + super(); + _System_executor.set(this, void 0); + __classPrivateFieldSet(this, _System_executor, executor, "f"); + } + register(event) { + __classPrivateFieldGet(this, _System_executor, "f").wake(this, event); + } + run(_event) { } +} +_System_executor = new WeakMap(); diff --git a/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/error.js b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/error.js new file mode 100644 index 0000000..8e5b6fd --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/error.js @@ -0,0 +1,26 @@ +export class Error { + constructor(reason) { + this.cause = null; + this.reason = reason; + } + context(why) { + let error = new Error(why); + error.cause = this; + return error; + } + *chain() { + let current = this; + while (current != null) { + yield current; + current = current.cause; + } + } + format() { + let causes = this.chain(); + let buffer = causes.next().value.reason; + for (const error of causes) { + buffer += `\n caused by: ` + error.reason; + } + return buffer + `\n`; + } +} diff --git a/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/events.js b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/events.js new file mode 100644 index 0000000..7d82ba2 --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/events.js @@ -0,0 +1,26 @@ +export var GlobalEvent; +(function (GlobalEvent) { + GlobalEvent[GlobalEvent["GtkShellChanged"] = 0] = "GtkShellChanged"; + GlobalEvent[GlobalEvent["GtkThemeChanged"] = 1] = "GtkThemeChanged"; + GlobalEvent[GlobalEvent["MonitorsChanged"] = 2] = "MonitorsChanged"; + GlobalEvent[GlobalEvent["OverviewShown"] = 3] = "OverviewShown"; + GlobalEvent[GlobalEvent["OverviewHidden"] = 4] = "OverviewHidden"; +})(GlobalEvent || (GlobalEvent = {})); +export var WindowEvent; +(function (WindowEvent) { + WindowEvent[WindowEvent["Size"] = 0] = "Size"; + WindowEvent[WindowEvent["Workspace"] = 1] = "Workspace"; + WindowEvent[WindowEvent["Minimize"] = 2] = "Minimize"; + WindowEvent[WindowEvent["Maximize"] = 3] = "Maximize"; + WindowEvent[WindowEvent["Fullscreen"] = 4] = "Fullscreen"; +})(WindowEvent || (WindowEvent = {})); +export function global(event) { + return { tag: 4, event }; +} +export function window_move(ext, window, rect) { + ext.movements.insert(window.entity, rect); + return { tag: 2, window, kind: { tag: 1 } }; +} +export function window_event(window, event) { + return { tag: 2, window, kind: { tag: 2, event } }; +} diff --git a/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/executor.js b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/executor.js new file mode 100644 index 0000000..ccf4a70 --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/executor.js @@ -0,0 +1,90 @@ +var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { + if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); + if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); + return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); +}; +var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { + if (kind === "m") throw new TypeError("Private method is not writable"); + if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); + if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); + return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; +}; +var _GLibExecutor_event_loop, _GLibExecutor_events, _OnceExecutor_iterable, _OnceExecutor_signal, _ChannelExecutor_channel, _ChannelExecutor_signal; +import GLib from 'gi://GLib'; +export class GLibExecutor { + constructor() { + _GLibExecutor_event_loop.set(this, null); + _GLibExecutor_events.set(this, new Array()); + } + wake(system, event) { + __classPrivateFieldGet(this, _GLibExecutor_events, "f").unshift(event); + if (__classPrivateFieldGet(this, _GLibExecutor_event_loop, "f")) + return; + __classPrivateFieldSet(this, _GLibExecutor_event_loop, GLib.idle_add(GLib.PRIORITY_DEFAULT, () => { + let event = __classPrivateFieldGet(this, _GLibExecutor_events, "f").pop(); + if (event) + system.run(event); + if (__classPrivateFieldGet(this, _GLibExecutor_events, "f").length === 0) { + __classPrivateFieldSet(this, _GLibExecutor_event_loop, null, "f"); + return false; + } + return true; + }), "f"); + } +} +_GLibExecutor_event_loop = new WeakMap(), _GLibExecutor_events = new WeakMap(); +export class OnceExecutor { + constructor(iterable) { + _OnceExecutor_iterable.set(this, void 0); + _OnceExecutor_signal.set(this, null); + __classPrivateFieldSet(this, _OnceExecutor_iterable, iterable, "f"); + } + start(delay, apply, then) { + this.stop(); + const iterator = __classPrivateFieldGet(this, _OnceExecutor_iterable, "f")[Symbol.iterator](); + __classPrivateFieldSet(this, _OnceExecutor_signal, GLib.timeout_add(GLib.PRIORITY_DEFAULT, delay, () => { + const next = iterator.next().value; + if (typeof next === 'undefined') { + if (then) + GLib.timeout_add(GLib.PRIORITY_DEFAULT, delay, () => { + then(); + return false; + }); + return false; + } + return apply(next); + }), "f"); + } + stop() { + if (__classPrivateFieldGet(this, _OnceExecutor_signal, "f") !== null) + GLib.source_remove(__classPrivateFieldGet(this, _OnceExecutor_signal, "f")); + } +} +_OnceExecutor_iterable = new WeakMap(), _OnceExecutor_signal = new WeakMap(); +export class ChannelExecutor { + constructor() { + _ChannelExecutor_channel.set(this, new Array()); + _ChannelExecutor_signal.set(this, null); + } + clear() { + __classPrivateFieldGet(this, _ChannelExecutor_channel, "f").splice(0); + } + get length() { + return __classPrivateFieldGet(this, _ChannelExecutor_channel, "f").length; + } + send(v) { + __classPrivateFieldGet(this, _ChannelExecutor_channel, "f").push(v); + } + start(delay, apply) { + this.stop(); + __classPrivateFieldSet(this, _ChannelExecutor_signal, GLib.timeout_add(GLib.PRIORITY_DEFAULT, delay, () => { + const e = __classPrivateFieldGet(this, _ChannelExecutor_channel, "f").shift(); + return typeof e === 'undefined' ? true : apply(e); + }), "f"); + } + stop() { + if (__classPrivateFieldGet(this, _ChannelExecutor_signal, "f") !== null) + GLib.source_remove(__classPrivateFieldGet(this, _ChannelExecutor_signal, "f")); + } +} +_ChannelExecutor_channel = new WeakMap(), _ChannelExecutor_signal = new WeakMap(); diff --git a/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/extension.js b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/extension.js new file mode 100644 index 0000000..f2fb809 --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/extension.js @@ -0,0 +1,2314 @@ +import * as Config from './config.js'; +import * as Forest from './forest.js'; +import * as Ecs from './ecs.js'; +import * as Events from './events.js'; +import * as Focus from './focus.js'; +import * as Geom from './geom.js'; +import * as GrabOp from './grab_op.js'; +import * as Keybindings from './keybindings.js'; +import * as Lib from './lib.js'; +import * as log from './log.js'; +import * as PanelSettings from './panel_settings.js'; +import * as Rect from './rectangle.js'; +import * as Settings from './settings.js'; +import * as Tiling from './tiling.js'; +import * as Window from './window.js'; +import * as launcher from './launcher.js'; +import * as auto_tiler from './auto_tiler.js'; +import * as node from './node.js'; +import * as utils from './utils.js'; +import * as Executor from './executor.js'; +import * as movement from './movement.js'; +import * as stack from './stack.js'; +import * as add_exception from './dialog_add_exception.js'; +import * as exec from './executor.js'; +import * as dbus_service from './dbus_service.js'; +import * as scheduler from './scheduler.js'; +import { Extension } from 'resource:///org/gnome/shell/extensions/extension.js'; +const display = global.display; +const wim = global.window_manager; +const wom = global.workspace_manager; +const Movement = movement.Movement; +import GLib from 'gi://GLib'; +import Gio from 'gi://Gio'; +import St from 'gi://St'; +import Shell from 'gi://Shell'; +import Meta from 'gi://Meta'; +const { GlobalEvent, WindowEvent } = Events; +const { cursor_rect, is_keyboard_op, is_resize_op, is_move_op } = Lib; +import * as Main from 'resource:///org/gnome/shell/ui/main.js'; +const { layoutManager, loadTheme, overview, panel, setThemeStylesheet, screenShield, sessionMode, windowAttentionHandler, } = Main; +import { ScreenShield } from 'resource:///org/gnome/shell/ui/screenShield.js'; +import { WindowSwitcherPopup, } from 'resource:///org/gnome/shell/ui/altTab.js'; +import { Workspace } from 'resource:///org/gnome/shell/ui/workspace.js'; +import { WorkspaceThumbnail } from 'resource:///org/gnome/shell/ui/workspaceThumbnail.js'; +import { WindowPreview } from 'resource:///org/gnome/shell/ui/windowPreview.js'; +import { PACKAGE_VERSION } from 'resource:///org/gnome/shell/misc/config.js'; +import * as Tags from './tags.js'; +import { get_current_path } from './paths.js'; +const STYLESHEET_PATHS = ['light', 'dark', 'highcontrast'].map(stylesheet_path); +const STYLESHEETS = STYLESHEET_PATHS.map((path) => Gio.File.new_for_path(path)); +const GNOME_VERSION = PACKAGE_VERSION; +var Style; +(function (Style) { + Style[Style["Light"] = 0] = "Light"; + Style[Style["Dark"] = 1] = "Dark"; + Style[Style["HighContrast"] = 2] = "HighContrast"; +})(Style || (Style = {})); +export class Ext extends Ecs.System { + constructor() { + super(new Executor.GLibExecutor()); + this.keybindings = new Keybindings.Keybindings(this); + this.settings = new Settings.ExtensionSettings(); + this.overlay = new St.BoxLayout({ style_class: 'pop-shell-overlay', visible: false }); + this.window_search = new launcher.Launcher(this); + this.dbus = new dbus_service.Service(); + this.animate_windows = true; + this.button = null; + this.button_gio_icon_auto_on = null; + this.button_gio_icon_auto_off = null; + this.conf = new Config.Config(); + this.conf_watch = null; + this.column_size = 32; + this.current_style = Style.Dark; + this.displays_updating = null; + this.row_size = 32; + this.displays = [global.display.get_primary_monitor(), new Map()]; + this.dpi = St.ThemeContext.get_for_stage(global.stage).scale_factor; + this.drag_signal = null; + this.exception_selecting = false; + this.gap_inner = 0; + this.gap_inner_half = 0; + this.gap_inner_prev = 0; + this.gap_outer = 0; + this.gap_outer_prev = 0; + this.grab_op = null; + this.ignore_display_update = false; + this.injections = new Array(); + this.prev_focused = [null, null]; + this.init = true; + this.was_locked = false; + this.moved_by_mouse = false; + this.workareas_update = null; + this.signals = new Map(); + this.size_requests = new Map(); + this.workspace_active = new Map(); + this.ids = this.register_storage(); + this.monitors = this.register_storage(); + this.movements = this.register_storage(); + this.names = this.register_storage(); + this.size_changed_signal = 0; + this.size_signals = this.register_storage(); + this.snapped = this.register_storage(); + this.windows = this.register_storage(); + this.window_signals = this.register_storage(); + this.auto_tiler = null; + this.focus_selector = new Focus.FocusSelector(); + this.tiler = new Tiling.Tiler(this); + this.load_settings(); + this.reload_theme(); + this.register_fn(() => load_theme(this.current_style)); + this.conf.reload(); + if (this.settings.int) { + this.settings.int.connect('changed::gtk-theme', () => { + this.register(Events.global(GlobalEvent.GtkThemeChanged)); + }); + } + if (this.settings.shell) { + this.settings.shell.connect('changed::name', () => { + this.register(Events.global(GlobalEvent.GtkShellChanged)); + }); + } + this.dbus.FocusUp = () => this.focus_up(); + this.dbus.FocusDown = () => this.focus_down(); + this.dbus.FocusLeft = () => this.focus_left(); + this.dbus.FocusRight = () => this.focus_right(); + this.dbus.Launcher = () => this.window_search.open(this); + this.dbus.WindowFocus = (window) => { + const target_window = this.windows.get(window); + if (target_window) { + target_window.activate(); + this.on_focused(target_window); + } + this.window_search.close(); + }; + this.dbus.WindowList = () => { + const wins = new Array(); + for (const window of this.tab_list(Meta.TabList.NORMAL, null)) { + const string = window.window_app.get_id(); + wins.push([window.entity, window.title(), window.name(this), string ? string : '']); + } + return wins; + }; + this.dbus.WindowQuit = (win) => { + this.windows.get(win)?.meta.delete(global.get_current_time()); + this.window_search.close(); + }; + } + register_fn(callback, name) { + this.register({ tag: 1, callback, name }); + } + run(event) { + switch (event.tag) { + case 1: + event.callback(); + break; + case 2: + let win = event.window; + if (!win.actor_exists()) + return; + if (event.kind.tag === 1) { + const { window } = event; + let movement = this.movements.remove(window.entity); + if (!movement) + return; + let actor = window.meta.get_compositor_private(); + if (!actor) { + this.auto_tiler?.detach_window(this, window.entity); + return; + } + actor.remove_all_transitions(); + const { x, y, width, height } = movement; + window.meta.move_resize_frame(true, x, y, width, height); + window.meta.move_frame(true, x, y); + this.monitors.insert(window.entity, [win.meta.get_monitor(), win.workspace_id()]); + if (win.activate_after_move) { + win.activate_after_move = false; + win.activate(); + } + return; + } + switch (event.kind.event) { + case WindowEvent.Maximize: + this.unset_grab_op(); + this.on_maximize(win); + break; + case WindowEvent.Minimize: + this.unset_grab_op(); + this.on_minimize(win); + break; + case WindowEvent.Size: + if (this.auto_tiler && !win.is_maximized() && !win.meta.is_fullscreen()) { + this.auto_tiler.reflow(this, win.entity); + } + break; + case WindowEvent.Workspace: + this.on_workspace_changed(win); + break; + case WindowEvent.Fullscreen: + if (this.auto_tiler) { + let attachment = this.auto_tiler.attached.get(win.entity); + if (attachment) { + if (!win.meta.is_fullscreen()) { + let fork = this.auto_tiler.forest.forks.get(win.entity); + if (fork) { + this.auto_tiler.reflow(this, win.entity); + } + if (win.stack !== null) { + this.auto_tiler.forest.stacks.get(win.stack)?.set_visible(true); + } + } + else { + if (win.stack !== null) { + this.auto_tiler.forest.stacks.get(win.stack)?.set_visible(false); + } + } + } + } + break; + } + break; + case 3: + let actor = event.window.get_compositor_private(); + if (!actor) + return; + this.on_window_create(event.window, actor); + break; + case 4: + switch (event.event) { + case GlobalEvent.GtkShellChanged: + this.on_gtk_shell_changed(); + break; + case GlobalEvent.GtkThemeChanged: + this.on_gtk_theme_change(); + break; + case GlobalEvent.MonitorsChanged: + this.update_display_configuration(false); + break; + case GlobalEvent.OverviewShown: + this.on_overview_shown(); + break; + } + break; + } + } + activate_window(window) { + if (window) { + window.activate(); + } + } + active_monitor() { + return display.get_current_monitor(); + } + active_window_list() { + let workspace = wom.get_active_workspace(); + return this.tab_list(Meta.TabList.NORMAL_ALL, workspace); + } + active_workspace() { + return wom.get_active_workspace_index(); + } + actor_of(entity) { + const window = this.windows.get(entity); + return window ? window.meta.get_compositor_private() : null; + } + connect(object, property, callback) { + const signal = object.connect(property, callback); + const entry = this.signals.get(object); + if (entry) { + entry.push(signal); + } + else { + this.signals.set(object, [signal]); + } + return signal; + } + connect_meta(win, signal, callback) { + const id = win.meta.connect(signal, () => { + if (win.actor_exists()) + callback(); + }); + this.window_signals.get_or(win.entity, () => new Array()).push(id); + return id; + } + connect_size_signal(win, signal, func) { + return this.connect_meta(win, signal, () => { + if (!this.contains_tag(win.entity, Tags.Blocked)) + func(); + }); + } + connect_window(win) { + const size_event = () => { + const old = this.size_requests.get(win.meta); + if (old) { + try { + GLib.source_remove(old); + } + catch (_) { } + } + const new_s = GLib.timeout_add(GLib.PRIORITY_LOW, 500, () => { + this.register(Events.window_event(win, WindowEvent.Size)); + this.size_requests.delete(win.meta); + return false; + }); + this.size_requests.set(win.meta, new_s); + }; + this.connect_meta(win, 'workspace-changed', () => { + this.register(Events.window_event(win, WindowEvent.Workspace)); + }); + this.size_signals.insert(win.entity, [ + this.connect_size_signal(win, 'size-changed', size_event), + this.connect_size_signal(win, 'position-changed', size_event), + this.connect_size_signal(win, 'notify::minimized', () => { + this.register(Events.window_event(win, WindowEvent.Minimize)); + }), + ]); + } + exception_add(win) { + this.exception_selecting = false; + let d = new add_exception.AddExceptionDialog(() => this.exception_dialog(), () => { + let wmclass = win.meta.get_wm_class(); + if (wmclass !== null && wmclass.length === 0) { + wmclass = win.name(this); + } + if (wmclass) + this.conf.add_app_exception(wmclass); + this.exception_dialog(); + }, () => { + let wmclass = win.meta.get_wm_class(); + if (wmclass) + this.conf.add_window_exception(wmclass, win.title()); + this.exception_dialog(); + }, () => { + this.conf.reload(); + this.tiling_config_reapply(); + }); + d.open(); + } + exception_dialog() { + let path = get_current_path() + '/floating_exceptions/main.js'; + const event_handler = (event) => { + switch (event) { + case 'MODIFIED': + this.register_fn(() => { + this.conf.reload(); + this.tiling_config_reapply(); + }); + break; + case 'SELECT': + this.register_fn(() => this.exception_select()); + return false; + } + return true; + }; + const ipc = utils.async_process_ipc(['gjs', '--module', path]); + if (ipc) { + const generator = (stdout, res) => { + try { + const [bytes] = stdout.read_line_finish(res); + if (bytes) { + if (event_handler(imports.byteArray.toString(bytes).trim())) { + ipc.stdout.read_line_async(0, ipc.cancellable, generator); + } + } + } + catch (why) { + log.error(`failed to read response from floating exceptions dialog: ${why}`); + } + }; + ipc.stdout.read_line_async(0, ipc.cancellable, generator); + } + } + exception_select() { + GLib.timeout_add(GLib.PRIORITY_LOW, 500, () => { + this.exception_selecting = true; + overview.show(); + return false; + }); + } + exit_modes() { + this.tiler.exit(this); + this.window_search.reset(); + this.window_search.close(); + this.overlay.visible = false; + } + find_monitor_to_retach(width, height) { + if (!this.settings.workspaces_only_on_primary()) { + for (const [index, display] of this.displays[1]) { + if (display.area.width == width && display.area.height == height) { + return [index, display]; + } + } + } + const primary = display.get_primary_monitor(); + return [primary, this.displays[1].get(primary)]; + } + find_unused_workspace(monitor) { + if (!this.auto_tiler) + return [0, wom.get_workspace_by_index(0)]; + let id = 0; + const tiled_windows = new Array(); + for (const [window] of this.auto_tiler.attached.iter()) { + if (!this.auto_tiler.attached.contains(window)) + continue; + const win = this.windows.get(window); + if (win && !win.reassignment && win.meta.get_monitor() === monitor) + tiled_windows.push(win); + } + cancel: while (true) { + for (const window of tiled_windows) { + if (window.workspace_id() === id) { + id += 1; + continue cancel; + } + } + break; + } + let new_work; + if (id + 1 === wom.get_n_workspaces()) { + id += 1; + new_work = wom.append_new_workspace(true, global.get_current_time()); + } + else { + new_work = wom.get_workspace_by_index(id); + } + return [id, new_work]; + } + focus_left() { + this.stack_select((id, stack) => (id === 0 ? null : stack.tabs[id - 1].entity), () => this.activate_window(this.focus_selector.left(this, null))); + } + focus_right() { + this.stack_select((id, stack) => (stack.tabs.length > id + 1 ? stack.tabs[id + 1].entity : null), () => this.activate_window(this.focus_selector.right(this, null))); + } + focus_down() { + this.activate_window(this.focus_selector.down(this, null)); + } + focus_up() { + this.activate_window(this.focus_selector.up(this, null)); + } + focus_window() { + return this.get_window(display.get_focus_window()); + } + stack_select(select, focus_shift) { + const switched = this.stack_switch((stack) => { + if (!stack) + return false; + const stack_con = this.auto_tiler?.forest.stacks.get(stack.idx); + if (stack_con) { + const id = stack_con.active_id; + if (id !== -1) { + const next = select(id, stack_con); + if (next) { + stack_con.activate(next); + const window = this.windows.get(next); + if (window) { + window.activate(); + return true; + } + } + } + } + return false; + }); + if (!switched) { + focus_shift(); + } + } + stack_switch(apply) { + const window = this.focus_window(); + if (window) { + if (this.auto_tiler) { + const node = this.auto_tiler.find_stack(window.entity); + return node ? apply(node[1].inner) : false; + } + } + } + get_window(meta) { + let entity = this.window_entity(meta); + return entity ? this.windows.get(entity) : null; + } + inject(object, method, func) { + const prev = object[method]; + this.injections.push({ object, method, func: prev }); + object[method] = func; + } + injections_add() { + const screen_unlock_fn = ScreenShield.prototype['deactivate']; + this.inject(ScreenShield.prototype, 'deactivate', (args) => { + screen_unlock_fn.apply(screenShield, [args]); + this.update_display_configuration(true); + }); + } + injections_remove() { + for (const { object, method, func } of this.injections.splice(0)) { + object[method] = func; + } + } + load_settings() { + this.set_gap_inner(this.settings.gap_inner()); + this.set_gap_outer(this.settings.gap_outer()); + this.gap_inner_prev = this.gap_inner; + this.gap_outer_prev = this.gap_outer; + this.column_size = this.settings.column_size() * this.dpi; + this.row_size = this.settings.row_size() * this.dpi; + } + monitor_work_area(monitor) { + const meta = wom.get_active_workspace().get_work_area_for_monitor(monitor); + return Rect.Rectangle.from_meta(meta); + } + monitor_area(monitor) { + const rect = global.display.get_monitor_geometry(monitor); + return rect ? Rect.Rectangle.from_meta(rect) : null; + } + on_active_workspace_changed() { + this.register_fn(() => { + this.exit_modes(); + this.restack(); + const activate_window = (window) => { + this.on_focused(window); + window.activate(true); + this.prev_focused = [null, window.entity]; + }; + const focused = this.focus_window(); + if (focused && focused.same_workspace()) { + activate_window(focused); + return; + } + const workspace_id = this.active_workspace(); + const active = this.workspace_active.get(workspace_id); + if (active) { + const window = this.windows.get(active); + if (window && window.meta.get_workspace().index() == workspace_id && !window.meta.minimized) { + activate_window(window); + return; + } + } + const workspace = wom.get_workspace_by_index(workspace_id); + if (workspace) { + for (const win of workspace.list_windows()) { + const window = this.get_window(win); + if (window && !window.meta.minimized) { + activate_window(window); + return; + } + } + } + }); + } + on_destroy(win) { + if (this.tiler.window !== null && win == this.tiler.window) + this.tiler.exit(this); + const [prev_a, prev_b] = this.prev_focused; + if (prev_a && Ecs.entity_eq(win, prev_a)) { + this.prev_focused[0] = null; + } + else if (prev_b && Ecs.entity_eq(win, prev_b)) { + this.prev_focused[1] = this.prev_focused[0]; + this.prev_focused[0] = null; + } + const window = this.windows.get(win); + if (!window) + return; + const stack = window.stack; + window.destroying = true; + this.window_signals.take_with(win, (signals) => { + for (const signal of signals) { + window.meta.disconnect(signal); + } + }); + if (this.auto_tiler) { + const entity = this.auto_tiler.attached.get(win); + if (entity) { + const fork = this.auto_tiler.forest.forks.get(entity); + if (fork?.right?.is_window(win)) { + const entity = fork.right.inner.kind === 3 ? fork.right.inner.entities[0] : fork.right.inner.entity; + this.windows.with(entity, (sibling) => sibling.activate()); + } + } + } + if (this.auto_tiler) + this.auto_tiler.detach_window(this, win); + if (this.auto_tiler && stack !== null) { + const stack_object = this.auto_tiler.forest.stacks.get(stack); + const prev = this.prev_focused[1]; + if (stack_object && prev) { + const prev_window = this.windows.get(prev); + if (prev_window) { + if (prev_window.stack !== stack) { + stack_object.auto_activate(); + this.prev_focused = [null, stack_object.active]; + this.windows.get(stack_object.active)?.activate(); + } + } + } + } + this.movements.remove(win); + this.windows.remove(win); + this.delete_entity(win); + } + on_display_move(_from_id, _to_id) { + if (!this.auto_tiler) + return; + } + on_focused(win) { + this.workspace_active.set(this.active_workspace(), win.entity); + scheduler.setForeground(win.meta); + this.size_signals_unblock(win); + if (this.exception_selecting) { + this.exception_add(win); + } + if (this.prev_focused[1] !== win.entity) { + this.prev_focused[0] = this.prev_focused[1]; + this.prev_focused[1] = win.entity; + } + if (null !== this.auto_tiler && null !== win.stack) { + ext?.auto_tiler?.forest.stacks.get(win.stack)?.activate(win.entity); + } + this.unmaximize_workspace(win); + this.show_border_on_focused(); + if (this.auto_tiler && win.is_tilable(this) && this.prev_focused[0] !== null) { + let prev = this.windows.get(this.prev_focused[0]); + let is_attached = this.auto_tiler.attached.contains(this.prev_focused[0]); + if (prev && + prev !== win && + is_attached && + prev.actor_exists() && + prev.name(this) !== win.name(this) && + prev.workspace_id() === win.workspace_id()) { + if (prev.rect().contains(win.rect())) { + if (prev.is_maximized()) { + prev.meta.unmaximize(Meta.MaximizeFlags.BOTH); + } + } + else if (prev.stack) { + prev.meta.unmaximize(Meta.MaximizeFlags.BOTH); + this.auto_tiler.forest.stacks.get(prev.stack)?.restack(); + } + } + } + if (this.conf.log_on_focus) { + let msg = `focused Window(${win.entity}) {\n` + + ` class: "${win.meta.get_wm_class()}",\n` + + ` cmdline: ${win.cmdline()},\n` + + ` monitor: ${win.meta.get_monitor()},\n` + + ` name: ${win.name(this)},\n` + + ` rect: ${win.rect().fmt()},\n` + + ` workspace: ${win.workspace_id()},\n` + + ` xid: ${win.xid()},\n` + + ` stack: ${win.stack},\n`; + if (this.auto_tiler) { + msg += ` fork: (${this.auto_tiler.attached.get(win.entity)}),\n`; + } + log.debug(msg + '}'); + } + } + on_tile_attach(entity, window) { + if (this.auto_tiler) { + if (!this.auto_tiler.attached.contains(window)) { + this.windows.with(window, (w) => { + if (w.prev_rect === null) { + w.prev_rect = w.meta.get_frame_rect(); + } + }); + } + this.auto_tiler.attached.insert(window, entity); + } + } + on_tile_detach(win) { + this.windows.with(win, (window) => { + if (window.prev_rect && !window.ignore_detach) { + this.register(Events.window_move(this, window, window.prev_rect)); + window.prev_rect = null; + } + }); + } + show_border_on_focused() { + this.hide_all_borders(); + const focus = this.focus_window(); + if (focus) + focus.show_border(); + } + hide_all_borders() { + for (const win of this.windows.values()) { + win.hide_border(); + } + } + maximized_on_active_display() { + const aws = this.workspace_id(); + for (const window of this.windows.values()) { + if (!window.actor_exists()) + continue; + const wws = this.workspace_id(window); + if (aws[0] === wws[0] && aws[1] === wws[1]) { + if (window.is_maximized()) + return true; + } + } + return false; + } + on_gap_inner() { + let current = this.settings.gap_inner(); + this.set_gap_inner(current); + let prev_gap = this.gap_inner_prev / 4 / this.dpi; + if (current != prev_gap) { + this.update_inner_gap(); + Gio.Settings.sync(); + } + } + update_inner_gap() { + if (this.auto_tiler) { + for (const [entity] of this.auto_tiler.forest.toplevel.values()) { + const fork = this.auto_tiler.forest.forks.get(entity); + if (fork) { + this.auto_tiler.tile(this, fork, fork.area); + } + } + } + else { + this.update_snapped(); + } + } + unmaximize_workspace(win) { + if (this.auto_tiler) { + let mon; + let work; + if (!win.is_tilable(this)) { + return; + } + mon = win.meta.get_monitor(); + work = win.meta.get_workspace().index(); + for (const [, compare] of this.windows.iter()) { + const is_same_space = compare.meta.get_monitor() === mon && compare.meta.get_workspace().index() === work; + if (is_same_space && + !this.contains_tag(compare.entity, Tags.Floating) && + compare.is_maximized() && + win.entity[0] !== compare.entity[0]) { + compare.meta.unmaximize(Meta.MaximizeFlags.BOTH); + } + } + } + } + on_gap_outer() { + let current = this.settings.gap_outer(); + this.set_gap_outer(current); + let prev_gap = this.gap_outer_prev / 4 / this.dpi; + let diff = current - prev_gap; + if (diff != 0) { + this.set_gap_outer(current); + this.update_outer_gap(diff); + Gio.Settings.sync(); + } + } + update_outer_gap(diff) { + if (this.auto_tiler) { + for (const [entity] of this.auto_tiler.forest.toplevel.values()) { + const fork = this.auto_tiler.forest.forks.get(entity); + if (fork) { + fork.area.array[0] += diff * 4; + fork.area.array[1] += diff * 4; + fork.area.array[2] -= diff * 8; + fork.area.array[3] -= diff * 8; + this.auto_tiler.tile(this, fork, fork.area); + } + } + } + else { + this.update_snapped(); + } + } + on_grab_end(meta, op) { + let win = this.get_window(meta); + if (win !== null) { + win.grab = false; + } + if (null === win || !win.is_tilable(this)) { + this.unset_grab_op(); + return; + } + this.on_grab_end_(win, op); + this.unset_grab_op(); + } + on_grab_end_(win, op) { + this.moved_by_mouse = true; + this.size_signals_unblock(win); + if (win.meta && win.meta.minimized) { + this.on_minimize(win); + return; + } + if (win.is_maximized()) { + return; + } + const grab_op = this.grab_op; + if (!win) { + log.error('an entity was dropped, but there is no window'); + return; + } + if (this.auto_tiler && op === undefined) { + let mon = this.monitors.get(win.entity); + if (mon) { + let rect = win.meta.get_work_area_for_monitor(mon[0]); + if (rect && Rect.Rectangle.from_meta(rect).contains(cursor_rect())) { + this.auto_tiler.reflow(this, win.entity); + } + else { + this.auto_tiler.on_drop(this, win, true); + } + } + return; + } + if (!(grab_op && Ecs.entity_eq(grab_op.entity, win.entity))) { + log.error(`grabbed entity is not the same as the one that was dropped`); + return; + } + if (this.auto_tiler) { + let crect = win.rect(); + const rect = grab_op.rect; + if (is_move_op(op)) { + const cmon = win.meta.get_monitor(); + const prev_mon = this.monitors.get(win.entity); + const mon_drop = prev_mon ? prev_mon[0] !== cmon : false; + this.monitors.insert(win.entity, [win.meta.get_monitor(), win.workspace_id()]); + if (rect.x != crect.x || rect.y != crect.y) { + if (rect.contains(cursor_rect())) { + if (this.auto_tiler.attached.contains(win.entity)) { + this.auto_tiler.on_drop(this, win, mon_drop); + } + else { + this.auto_tiler.reflow(this, win.entity); + } + } + else { + this.auto_tiler.on_drop(this, win, mon_drop); + } + } + } + else { + const fork_entity = this.auto_tiler.attached.get(win.entity); + if (fork_entity) { + const forest = this.auto_tiler.forest; + const fork = forest.forks.get(fork_entity); + if (fork) { + if (win.stack) { + const tab_dimension = this.dpi * stack.TAB_HEIGHT; + crect.height += tab_dimension; + crect.y -= tab_dimension; + } + let top_level = forest.find_toplevel(this.workspace_id()); + if (top_level) { + crect.clamp(forest.forks.get(top_level).area); + } + const movement = grab_op.operation(crect); + if (this.movement_is_valid(win, movement)) { + forest.resize(this, fork_entity, fork, win.entity, movement, crect); + forest.arrange(this, fork.workspace); + } + else { + forest.tile(this, fork, fork.area); + } + } + else { + log.error(`no fork component found`); + } + } + else { + log.error(`no fork entity found`); + } + } + } + else if (this.settings.snap_to_grid()) { + this.tiler.snap(this, win); + } + } + previously_focused(active) { + for (const id of [1, 0]) { + const prev = this.prev_focused[id]; + if (prev && !Ecs.entity_eq(active.entity, prev)) { + return prev; + } + } + return null; + } + movement_is_valid(win, movement) { + if ((movement & Movement.SHRINK) !== 0) { + if ((movement & Movement.DOWN) !== 0) { + const w = this.focus_selector.up(this, win); + if (!w) + return false; + const r = w.rect(); + if (r.y + r.height > win.rect().y) + return false; + } + else if ((movement & Movement.UP) !== 0) { + const w = this.focus_selector.down(this, win); + if (!w) + return false; + const r = w.rect(); + if (r.y + r.height < win.rect().y) + return false; + } + else if ((movement & Movement.LEFT) !== 0) { + const w = this.focus_selector.right(this, win); + if (!w) + return false; + const r = w.rect(); + if (r.x + r.width < win.rect().x) + return false; + } + else if ((movement & Movement.RIGHT) !== 0) { + const w = this.focus_selector.left(this, win); + if (!w) + return false; + const r = w.rect(); + if (r.x + r.width > win.rect().x) + return false; + } + } + return true; + } + workspace_window_move(win, prev_monitor, next_monitor) { + const prev_area = win.meta.get_work_area_for_monitor(prev_monitor); + const next_area = win.meta.get_work_area_for_monitor(next_monitor); + if (prev_area && next_area) { + let rect = win.rect(); + let h_ratio = 1; + let w_ratio = 1; + h_ratio = next_area.height / prev_area.height; + rect.height = rect.height * h_ratio; + w_ratio = next_area.width / prev_area.width; + rect.width = rect.width * w_ratio; + if (next_area.x < prev_area.x) { + rect.x = ((next_area.x + rect.x - prev_area.x) / prev_area.width) * next_area.width; + } + else if (next_area.x > prev_area.x) { + rect.x = (rect.x / prev_area.width) * next_area.width + next_area.x; + } + if (next_area.y < prev_area.y) { + rect.y = ((next_area.y + rect.y - prev_area.y) / prev_area.height) * next_area.height; + } + else if (next_area.y > prev_area.y) { + rect.y = (rect.y / prev_area.height) * next_area.height + next_area.y; + } + if (this.auto_tiler) { + if (this.is_floating(win)) { + win.meta.unmaximize(Meta.MaximizeFlags.HORIZONTAL); + win.meta.unmaximize(Meta.MaximizeFlags.VERTICAL); + win.meta.unmaximize(Meta.MaximizeFlags.BOTH); + } + this.register(Events.window_move(this, win, rect)); + } + else { + win.move(this, rect, () => { }); + if (rect.width == next_area.width && rect.height == next_area.height) { + win.meta.maximize(Meta.MaximizeFlags.BOTH); + } + } + } + } + move_monitor(direction) { + const win = this.focus_window(); + if (!win) + return; + const prev_monitor = win.meta.get_monitor(); + const next_monitor = Tiling.locate_monitor(win, direction); + if (next_monitor !== null) { + if (this.auto_tiler && !this.is_floating(win)) { + win.ignore_detach = true; + this.auto_tiler.detach_window(this, win.entity); + this.auto_tiler.attach_to_workspace(this, win, [next_monitor[0], win.workspace_id()]); + } + else { + this.workspace_window_move(win, prev_monitor, next_monitor[0]); + } + } + win.activate_after_move = true; + } + move_workspace(direction) { + const win = this.focus_window(); + if (!win) + return; + const workspace_move = (direction) => { + const ws = win.meta.get_workspace(); + let neighbor = ws.get_neighbor(direction); + const last_window = () => { + const last = wom.get_n_workspaces() - 2 === ws.index() && ws.n_windows === 1; + return last; + }; + const place_on_nearest_window = (auto_tiler, ws, monitor) => { + const src = win.meta.get_frame_rect(); + auto_tiler.detach_window(this, win.entity); + const index = ws.index(); + const coord = [src.x, src.y]; + let nearest_window = null; + let nearest_distance = null; + for (const [entity, window] of this.windows.iter()) { + const other_monitor = window.meta.get_monitor(); + const other_index = window.meta.get_workspace().index(); + if (!this.contains_tag(entity, Tags.Floating) && + other_monitor == monitor && + other_index === index && + !Ecs.entity_eq(win.entity, window.entity)) { + const other_rect = window.rect(); + const other_coord = [other_rect.x, other_rect.y]; + const distance = Geom.distance(coord, other_coord); + if (nearest_distance === null || nearest_distance > distance) { + nearest_window = window; + nearest_distance = distance; + } + } + } + if (nearest_window === null) { + auto_tiler.attach_to_workspace(this, win, [monitor, index]); + } + else { + auto_tiler.attach_to_window(this, nearest_window, win, { src }, false); + } + }; + const move_to_neighbor = (neighbor) => { + const monitor = win.meta.get_monitor(); + if (this.auto_tiler && win.is_tilable(this)) { + win.ignore_detach = true; + place_on_nearest_window(this.auto_tiler, neighbor, monitor); + if (win.meta.minimized) { + this.size_signals_block(win); + win.meta.change_workspace_by_index(neighbor.index(), false); + this.size_signals_unblock(win); + } + } + else { + this.workspace_window_move(win, monitor, monitor); + } + this.workspace_active.set(neighbor.index(), win.entity); + win.activate_after_move = true; + }; + if (neighbor && neighbor.index() !== ws.index()) { + move_to_neighbor(neighbor); + } + else if (direction === Meta.MotionDirection.DOWN && !last_window()) { + if (this.settings.dynamic_workspaces()) { + neighbor = wom.append_new_workspace(false, global.get_current_time()); + } + else { + return; + } + } + else if (direction === Meta.MotionDirection.UP && ws.index() === 0) { + if (this.settings.dynamic_workspaces()) { + wom.append_new_workspace(false, global.get_current_time()); + this.on_workspace_modify(() => true, (current) => current + 1, true); + neighbor = wom.get_workspace_by_index(0); + if (!neighbor) + return; + move_to_neighbor(neighbor); + } + else { + return; + } + } + else { + return; + } + this.size_signals_block(win); + win.meta.change_workspace_by_index(neighbor.index(), true); + neighbor.activate_with_focus(win.meta, global.get_current_time()); + this.size_signals_unblock(win); + }; + switch (direction) { + case Meta.DisplayDirection.DOWN: + workspace_move(Meta.MotionDirection.DOWN); + break; + case Meta.DisplayDirection.UP: + workspace_move(Meta.MotionDirection.UP); + break; + } + if (this.auto_tiler) + this.restack(); + } + on_grab_start(meta, op) { + if (!meta) + return; + let win = this.get_window(meta); + if (win) { + win.grab = true; + if (win.is_tilable(this)) { + let entity = win.entity; + let rect = win.rect(); + this.unset_grab_op(); + this.grab_op = new GrabOp.GrabOp(entity, rect); + this.size_signals_block(win); + if (overview.visible || !win || is_keyboard_op(op) || is_resize_op(op)) + return; + const workspace = this.active_workspace(); + this.drag_signal = GLib.timeout_add(GLib.PRIORITY_LOW, 200, () => { + this.overlay.visible = false; + if (!win || !this.auto_tiler || !this.grab_op || this.grab_op.entity !== entity) { + this.drag_signal = null; + return false; + } + const [cursor, monitor] = this.cursor_status(); + let attach_to = null; + for (const found of this.windows_at_pointer(cursor, monitor, workspace)) { + if (found != win && this.auto_tiler.attached.contains(found.entity)) { + attach_to = found; + break; + } + } + const fork = this.auto_tiler.get_parent_fork(entity); + if (!fork) + return true; + let windowless = this.auto_tiler.largest_on_workspace(this, monitor, workspace) === null; + if (attach_to === null) { + if (fork.left.inner.kind === 2 && fork.right?.inner.kind === 2) { + let attaching = fork.left.is_window(entity) + ? fork.right.inner.entity + : fork.left.inner.entity; + attach_to = this.windows.get(attaching); + } + } + let area, monitor_attachment; + if (windowless) { + [area, monitor_attachment] = [this.monitor_work_area(monitor), true]; + area.x += this.gap_outer; + area.y += this.gap_outer; + area.width -= this.gap_outer * 2; + area.height -= this.gap_outer * 2; + } + else if (attach_to) { + const is_sibling = this.auto_tiler.windows_are_siblings(entity, attach_to.entity); + [area, monitor_attachment] = + (win.stack === null && attach_to.stack === null && is_sibling) || + (win.stack === null && is_sibling) + ? [fork.area, false] + : [attach_to.meta.get_frame_rect(), false]; + } + else { + return true; + } + const result = monitor_attachment ? null : auto_tiler.cursor_placement(this, area, cursor); + if (!result) { + this.overlay.x = area.x; + this.overlay.y = area.y; + this.overlay.width = area.width; + this.overlay.height = area.height; + this.overlay.visible = true; + return true; + } + const { orientation, swap } = result; + const half_width = area.width / 2; + const half_height = area.height / 2; + let new_area = orientation === Lib.Orientation.HORIZONTAL + ? swap + ? [area.x, area.y, half_width, area.height] + : [area.x + half_width, area.y, half_width, area.height] + : swap + ? [area.x, area.y, area.width, half_height] + : [area.x, area.y + half_height, area.width, half_height]; + this.overlay.x = new_area[0]; + this.overlay.y = new_area[1]; + this.overlay.width = new_area[2]; + this.overlay.height = new_area[3]; + this.overlay.visible = true; + return true; + }); + } + } + } + on_gtk_shell_changed() { + this.reload_theme(); + load_theme(this.current_style); + } + on_gtk_theme_change() { + this.reload_theme(); + load_theme(this.current_style); + } + reload_theme() { + this.current_style = this.settings.is_dark() + ? Style.Dark + : this.settings.is_high_contrast() + ? Style.HighContrast + : Style.Light; + } + on_maximize(win) { + if (win.is_maximized()) { + const actor = win.meta.get_compositor_private(); + if (actor) + global.window_group.set_child_above_sibling(actor, null); + this.on_monitor_changed(win, (_cfrom, cto, workspace) => { + if (win) { + win.ignore_detach = true; + this.monitors.insert(win.entity, [cto, workspace]); + this.auto_tiler?.detach_window(this, win.entity); + } + }); + } + else { + this.register_fn(() => { + if (this.auto_tiler) { + let fork_ent = this.auto_tiler.attached.get(win.entity); + if (fork_ent) { + let fork = this.auto_tiler.forest.forks.get(fork_ent); + if (fork) + this.auto_tiler.tile(this, fork, fork.area); + } + } + }); + } + } + on_minimize(win) { + if (this.focus_window() == win && this.settings.active_hint()) { + if (win.meta.minimized) { + win.hide_border(); + } + else { + this.show_border_on_focused(); + } + } + if (this.auto_tiler) { + if (win.meta.minimized) { + const attached = this.auto_tiler.attached.get(win.entity); + if (!attached) + return; + const fork = this.auto_tiler.forest.forks.get(attached); + if (!fork) + return; + let attachment; + if (win.stack !== null) { + attachment = win.stack; + } + else { + attachment = fork.left.is_window(win.entity); + } + win.was_attached_to = [attached, attachment]; + this.auto_tiler.detach_window(this, win.entity); + } + else if (!this.contains_tag(win.entity, Tags.Floating)) { + if (win.was_attached_to) { + const [entity, attachment] = win.was_attached_to; + delete win.was_attached_to; + const tiler = this.auto_tiler; + const fork = tiler.forest.forks.get(entity); + if (fork) { + if (typeof attachment === 'boolean') { + tiler.forest.attach_fork(this, fork, win.entity, attachment); + tiler.tile(this, fork, fork.area); + return; + } + else { + const stack = tiler.forest.stacks.get(attachment); + if (stack) { + const stack_info = tiler.find_stack(stack.active); + if (stack_info) { + const node = stack_info[1].inner; + win.stack = attachment; + node.entities.push(win.entity); + tiler.update_stack(this, node); + tiler.forest.on_attach(fork.entity, win.entity); + stack.activate(win.entity); + tiler.tile(this, fork, fork.area); + return; + } + } + } + } + } + this.auto_tiler.auto_tile(this, win, false); + } + } + } + on_monitor_changed(win, func) { + const actual_monitor = win.meta.get_monitor(); + const actual_workspace = win.workspace_id(); + const monitor = this.monitors.get(win.entity); + if (monitor) { + const [expected_monitor, expected_workspace] = monitor; + if (expected_monitor != actual_monitor || actual_workspace != expected_workspace) { + func(expected_monitor, actual_monitor, actual_workspace); + } + } + else { + func(null, actual_monitor, actual_workspace); + } + } + on_overview_shown() { + this.exit_modes(); + this.unset_grab_op(); + } + on_show_window_titles() { + const show_title = this.settings.show_title(); + if (indicator) { + indicator.toggle_titles.setToggleState(show_title); + } + for (const window of this.windows.values()) { + if (window.meta.is_client_decorated()) + continue; + if (show_title) { + window.decoration_show(this); + } + else { + window.decoration_hide(this); + } + } + } + on_smart_gap() { + if (this.auto_tiler) { + const smart_gaps = this.settings.smart_gaps(); + for (const [entity, [mon]] of this.auto_tiler.forest.toplevel.values()) { + const node = this.auto_tiler.forest.forks.get(entity); + if (node?.right === null) { + this.auto_tiler.update_toplevel(this, node, mon, smart_gaps); + } + } + } + } + on_window_create(window, actor) { + let win = this.get_window(window); + if (win) { + const entity = win.entity; + actor.connect('destroy', () => { + if (win && win.border) { + win.border.destroy(); + win.border = null; + } + this.on_destroy(entity); + return false; + }); + if (win.is_tilable(this)) { + this.connect_window(win); + } + } + } + on_workspace_added(_number) { + this.ignore_display_update = true; + } + on_workspace_changed(win) { + if (this.auto_tiler && !this.contains_tag(win.entity, Tags.Floating)) { + const id = this.workspace_id(win); + const prev_id = this.monitors.get(win.entity); + if (!prev_id || id[0] != prev_id[0] || id[1] != prev_id[1]) { + win.ignore_detach = true; + this.monitors.insert(win.entity, id); + if (win.is_tilable(this)) { + this.auto_tiler.detach_window(this, win.entity); + this.auto_tiler.attach_to_workspace(this, win, id); + } + } + if (win.meta.minimized) { + this.size_signals_block(win); + win.meta.unminimize(); + this.size_signals_unblock(win); + } + } + } + on_workspace_index_changed(prev, next) { + this.on_workspace_modify((current) => current == prev, (_) => next); + } + on_workspace_modify(condition, modify, change_workspace = false) { + function window_move(ext, entity, ws) { + if (change_workspace) { + const window = ext.windows.get(entity); + if (!window || !window.actor_exists() || window.meta.is_on_all_workspaces()) + return; + ext.size_signals_block(window); + window.meta.change_workspace_by_index(ws, false); + ext.size_signals_unblock(window); + } + } + if (this.auto_tiler) { + for (const [entity, monitor] of this.auto_tiler.forest.toplevel.values()) { + if (condition(monitor[1])) { + const value = modify(monitor[1]); + monitor[1] = value; + let fork = this.auto_tiler.forest.forks.get(entity); + if (fork) { + fork.workspace = value; + for (const child of this.auto_tiler.forest.iter(entity)) { + if (child.inner.kind === 1) { + fork = this.auto_tiler.forest.forks.get(child.inner.entity); + if (fork) + fork.workspace = value; + } + else if (child.inner.kind === 2) { + window_move(this, child.inner.entity, value); + } + else if (child.inner.kind === 3) { + let stack = this.auto_tiler.forest.stacks.get(child.inner.idx); + if (stack) { + stack.workspace = value; + for (const entity of child.inner.entities) { + window_move(this, entity, value); + } + stack.restack(); + } + } + } + } + } + } + for (const window of this.windows.values()) { + if (!window.actor_exists()) + this.auto_tiler.detach_window(this, window.entity); + } + } + else { + let to_delete = new Array(); + for (const [entity, window] of this.windows.iter()) { + if (!window.actor_exists()) { + to_delete.push(entity); + continue; + } + const ws = window.workspace_id(); + if (condition(ws)) { + window_move(this, entity, modify(ws)); + } + } + for (const e of to_delete) + this.delete_entity(e); + } + } + on_workspace_removed(number) { + this.on_workspace_modify((current) => current > number, (prev) => prev - 1); + } + restack() { + let attempts = 0; + GLib.timeout_add(GLib.PRIORITY_LOW, 50, () => { + if (this.auto_tiler) { + for (const container of this.auto_tiler.forest.stacks.values()) { + container.restack(); + } + } + let x = attempts; + attempts += 1; + return x < 10; + }); + } + set_gap_inner(gap) { + this.gap_inner_prev = this.gap_inner; + this.gap_inner = gap * 4 * this.dpi; + this.gap_inner_half = this.gap_inner / 2; + } + set_gap_outer(gap) { + this.gap_outer_prev = this.gap_outer; + this.gap_outer = gap * 4 * this.dpi; + } + set_overlay(rect) { + this.overlay.x = rect.x; + this.overlay.y = rect.y; + this.overlay.width = rect.width; + this.overlay.height = rect.height; + } + signals_attach() { + this.tiler.queue.start(100, (movement) => { + movement(); + return true; + }); + const workspace_manager = wom; + for (const [, ws] of iter_workspaces(workspace_manager)) { + let index = ws.index(); + this.connect(ws, 'notify::workspace-index', () => { + if (ws !== null) { + let new_index = ws.index(); + this.on_workspace_index_changed(index, new_index); + index = new_index; + } + }); + } + this.connect(display, 'workareas-changed', () => { + this.update_display_configuration(true); + }); + this.size_changed_signal = this.connect(wim, 'size-change', (_, actor, event, _before, _after) => { + if (this.auto_tiler) { + let win = this.get_window(actor.get_meta_window()); + if (!win) + return; + if (event === Meta.SizeChange.MAXIMIZE || event === Meta.SizeChange.UNMAXIMIZE) { + this.register(Events.window_event(win, WindowEvent.Maximize)); + } + else { + this.register(Events.window_event(win, WindowEvent.Fullscreen)); + } + } + }); + this.connect(this.settings.ext, 'changed', (_s, key) => { + switch (key) { + case 'active-hint': + if (indicator) + indicator.toggle_active.setToggleState(this.settings.active_hint()); + this.show_border_on_focused(); + case 'gap-inner': + this.on_gap_inner(); + break; + case 'gap-outer': + this.on_gap_outer(); + break; + case 'show-title': + this.on_show_window_titles(); + break; + case 'smart-gaps': + this.on_smart_gap(); + this.show_border_on_focused(); + break; + case 'show-skip-taskbar': + if (this.settings.show_skiptaskbar()) { + _show_skip_taskbar_windows(this); + } + else { + _hide_skip_taskbar_windows(); + } + } + }); + if (this.settings.mutter) { + this.connect(this.settings.mutter, 'changed::workspaces-only-on-primary', () => { + this.register(Events.global(GlobalEvent.MonitorsChanged)); + }); + } + this.connect(layoutManager, 'monitors-changed', () => { + this.register(Events.global(GlobalEvent.MonitorsChanged)); + }); + this.connect(sessionMode, 'updated', () => { + if (indicator) { + indicator.button.visible = !sessionMode.isLocked; + } + if (sessionMode.isLocked) { + this.exit_modes(); + } + }); + this.connect(overview, 'showing', () => { + this.register(Events.global(GlobalEvent.OverviewShown)); + }); + this.connect(overview, 'hiding', () => { + const window = this.focus_window(); + if (window) { + this.on_focused(window); + } + this.register(Events.global(GlobalEvent.OverviewHidden)); + }); + this.register_fn(() => { + if (screenShield?.locked) + this.update_display_configuration(false); + this.connect(display, 'notify::focus-window', () => { + if (Main.modalCount !== 0) { + const { actor } = Main.modalActorFocusStack[0]; + if (actor.style_class !== 'switcher-popup') { + return; + } + } + const refocus_tiled_window = () => { + let window = null; + const [x, y] = this.prev_focused; + if (y) { + window = this.windows.get(y); + } + if (window === null && x) { + window = this.windows.get(x); + } + if (window && window.same_monitor() && window.same_workspace() && !window.meta.minimized) { + window.activate(false); + } + else { + this.hide_all_borders(); + } + }; + this.register_fn(() => { + let meta_window = global.display.get_focus_window(); + if (meta_window) { + const shell_window = this.get_window(meta_window); + if (shell_window) { + if (shell_window.entity !== this.prev_focused[1] && !shell_window.meta.minimized) { + this.on_focused(shell_window); + } + } + else if (!meta_window.is_override_redirect()) { + if (this.auto_tiler && meta_window.window_type === Meta.WindowType.DESKTOP) { + refocus_tiled_window(); + } + else { + meta_window.activate(global.get_current_time()); + } + } + } + else if (this.auto_tiler) { + refocus_tiled_window(); + } + }); + return false; + }); + const window = this.focus_window(); + if (window) { + this.on_focused(window); + } + return false; + }); + this.connect(display, 'window_created', (_, window) => { + this.register({ tag: 3, window }); + }); + if (GNOME_VERSION?.startsWith('3.')) { + this.connect(display, 'grab-op-begin', (_, _display, win, op) => { + this.on_grab_start(win, op); + }); + this.connect(display, 'grab-op-end', (_, _display, win, op) => { + this.register_fn(() => this.on_grab_end(win, op)); + }); + } + else { + this.connect(display, 'grab-op-begin', (_display, win, op) => { + this.on_grab_start(win, op); + }); + this.connect(display, 'grab-op-end', (_display, win, op) => { + this.register_fn(() => this.on_grab_end(win, op)); + }); + } + this.connect(overview, 'window-drag-begin', (_, win) => { + this.on_grab_start(win, 1); + }); + this.connect(overview, 'window-drag-end', (_, win) => { + this.register_fn(() => this.on_grab_end(win)); + }); + this.connect(overview, 'window-drag-cancelled', () => { + this.unset_grab_op(); + }); + this.connect(wim, 'switch-workspace', () => { + this.hide_all_borders(); + }); + this.connect(workspace_manager, 'active-workspace-changed', () => { + this.on_active_workspace_changed(); + }); + this.connect(workspace_manager, 'workspace-removed', (_, number) => { + this.on_workspace_removed(number); + }); + this.connect(workspace_manager, 'workspace-added', (_, number) => { + this.on_workspace_added(number); + }); + this.connect(workspace_manager, 'showing-desktop-changed', () => { + this.hide_all_borders(); + this.prev_focused = [null, null]; + }); + St.ThemeContext.get_for_stage(global.stage).connect('notify::scale-factor', () => this.update_scale()); + if (this.settings.tile_by_default() && !this.auto_tiler) { + this.auto_tiler = new auto_tiler.AutoTiler(new Forest.Forest() + .connect_on_attach(this.on_tile_attach.bind(this)) + .connect_on_detach(this.on_tile_detach.bind(this)), this.register_storage()); + } + if (this.init) { + for (const window of this.tab_list(Meta.TabList.NORMAL, null)) { + this.register({ tag: 3, window: window.meta }); + } + this.register_fn(() => (this.init = false)); + } + } + signals_remove() { + for (const [object, signals] of this.signals) { + for (const signal of signals) { + object.disconnect(signal); + } + } + if (this.conf_watch) { + this.conf_watch[0].disconnect(this.conf_watch[1]); + this.conf_watch = null; + } + this.tiler.queue.stop(); + this.signals.clear(); + } + size_changed_block() { + utils.block_signal(wim, this.size_changed_signal); + } + size_changed_unblock() { + utils.unblock_signal(wim, this.size_changed_signal); + } + size_signals_block(win) { + this.add_tag(win.entity, Tags.Blocked); + } + size_signals_unblock(win) { + this.delete_tag(win.entity, Tags.Blocked); + } + snap_windows() { + for (const window of this.windows.values()) { + if (window.is_tilable(this)) + this.tiler.snap(this, window); + } + } + switch_to_workspace(id) { + this.workspace_by_id(id)?.activate(global.get_current_time()); + } + stop_launcher_services() { + this.window_search.stop_services(this); + } + tab_list(tablist, workspace) { + const windows = display.get_tab_list(tablist, workspace); + const matched = new Array(); + for (const window of windows) { + const win = this.get_window(window); + if (win) + matched.push(win); + } + return matched; + } + *tiled_windows() { + for (const entity of this.entities()) { + if (this.contains_tag(entity, Tags.Tiled)) { + yield entity; + } + } + } + tiling_config_reapply() { + if (this.auto_tiler) { + const at = this.auto_tiler; + for (const [entity, window] of this.windows.iter()) { + const attachment = at.attached.get(entity); + if (window.is_tilable(this)) { + if (!attachment) { + at.auto_tile(this, window, this.init); + } + } + else if (attachment) { + at.detach_window(this, entity); + } + } + } + } + toggle_tiling() { + if (this.settings.tile_by_default()) { + this.auto_tile_off(); + } + else { + this.auto_tile_on(); + } + } + auto_tile_off() { + this.settings.set_edge_tiling(true); + this.hide_all_borders(); + if (this.auto_tiler) { + this.unregister_storage(this.auto_tiler.attached); + this.auto_tiler.destroy(this); + this.auto_tiler = null; + this.settings.set_tile_by_default(false); + if (indicator) + indicator.toggle_tiled.setToggleState(false); + this.button.icon.gicon = this.button_gio_icon_auto_off; + if (this.settings.active_hint()) { + this.show_border_on_focused(); + } + } + } + auto_tile_on() { + this.settings.set_edge_tiling(false); + this.hide_all_borders(); + if (indicator) + indicator.toggle_tiled.setToggleState(true); + const original = this.active_workspace(); + let tiler = new auto_tiler.AutoTiler(new Forest.Forest() + .connect_on_attach(this.on_tile_attach.bind(this)) + .connect_on_detach(this.on_tile_detach.bind(this)), this.register_storage()); + this.auto_tiler = tiler; + this.settings.set_tile_by_default(true); + this.button.icon.gicon = this.button_gio_icon_auto_on; + for (const window of this.windows.values()) { + if (window.is_tilable(this)) { + let actor = window.meta.get_compositor_private(); + if (actor) { + if (!window.meta.minimized) { + tiler.auto_tile(this, window, true); + } + } + } + } + this.register_fn(() => this.switch_to_workspace(original)); + } + schedule_idle(func) { + if (!this.movements.is_empty()) { + GLib.timeout_add(GLib.PRIORITY_DEFAULT, 100, () => { + if (!this.movements.is_empty()) + return true; + return func(); + }); + } + else { + func(); + } + return false; + } + should_ignore_workspace(monitor) { + return this.settings.workspaces_only_on_primary() && monitor !== global.display.get_primary_monitor(); + } + unset_grab_op() { + if (this.drag_signal !== null) { + this.overlay.visible = false; + GLib.source_remove(this.drag_signal); + this.drag_signal = null; + } + if (this.grab_op !== null) { + let window = this.windows.get(this.grab_op.entity); + if (window) + this.size_signals_unblock(window); + this.grab_op = null; + } + this.moved_by_mouse = false; + } + update_display_configuration_before() { } + update_display_configuration(workareas_only) { + if (!this.auto_tiler || sessionMode.isLocked) + return; + if (this.ignore_display_update) { + this.ignore_display_update = false; + return; + } + if (layoutManager.monitors.length === 0) + return; + const primary_display = global.display.get_primary_monitor(); + const primary_display_ready = (ext) => { + const area = global.display.get_monitor_geometry(primary_display); + const work_area = ext.monitor_work_area(primary_display); + if (!area || !work_area) + return false; + return !(area.width === work_area.width && area.height === work_area.height); + }; + function displays_ready() { + const monitors = global.display.get_n_monitors(); + if (monitors === 0) + return false; + for (let i = 0; i < monitors; i += 1) { + const display = global.display.get_monitor_geometry(i); + if (!display) + return false; + if (display.width < 1 || display.height < 1) + return false; + } + return true; + } + if (!displays_ready() || !primary_display_ready(this)) { + if (this.displays_updating !== null) + return; + if (this.workareas_update !== null) + GLib.source_remove(this.workareas_update); + this.workareas_update = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 500, () => { + this.register_fn(() => { + this.update_display_configuration(workareas_only); + }); + this.workareas_update = null; + return false; + }); + return; + } + const update_tiling = () => { + if (!this.auto_tiler) + return; + for (const f of this.auto_tiler.forest.forks.values()) { + if (!f.is_toplevel) + continue; + const display = this.monitor_work_area(f.monitor); + if (display) { + const area = new Rect.Rectangle([display.x, display.y, display.width, display.height]); + f.smart_gapped = false; + f.set_area(area.clone()); + this.auto_tiler.update_toplevel(this, f, f.monitor, this.settings.smart_gaps()); + } + } + }; + let migrations = new Array(); + const apply_migrations = (assigned_monitors) => { + if (!migrations) + return; + new exec.OnceExecutor(migrations).start(500, ([fork, new_monitor, workspace, find_workspace]) => { + let new_workspace; + if (find_workspace) { + if (assigned_monitors.has(new_monitor)) { + [new_workspace] = this.find_unused_workspace(new_monitor); + } + else { + assigned_monitors.add(new_monitor); + new_workspace = 0; + } + } + else { + new_workspace = fork.workspace; + } + fork.migrate(this, forest, workspace, new_monitor, new_workspace); + fork.set_ratio(fork.length() / 2); + return true; + }, () => update_tiling()); + }; + function mark_for_reassignment(ext, fork) { + for (const win of forest.iter(fork, node.NodeKind.WINDOW)) { + if (win.inner.kind === 2) { + const entity = win.inner.entity; + const window = ext.windows.get(entity); + if (window) + window.reassignment = true; + } + } + } + const [old_primary, old_displays] = this.displays; + const changes = new Map(); + for (const [entity, w] of this.windows.iter()) { + if (!w.actor_exists()) + continue; + this.monitors.with(entity, ([mon]) => { + const assignment = mon === old_primary ? primary_display : w.meta.get_monitor(); + changes.set(mon, assignment); + }); + } + const updated = new Map(); + for (const monitor of layoutManager.monitors) { + const mon = monitor; + const area = new Rect.Rectangle([mon.x, mon.y, mon.width, mon.height]); + const ws = this.monitor_work_area(mon.index); + updated.set(mon.index, { area, ws }); + } + const forest = this.auto_tiler.forest; + if (old_displays.size === updated.size) { + update_tiling(); + this.displays = [primary_display, updated]; + return; + } + this.displays = [primary_display, updated]; + if (utils.map_eq(old_displays, updated)) { + return; + } + if (this.displays_updating !== null) + GLib.source_remove(this.displays_updating); + if (this.workareas_update !== null) { + GLib.source_remove(this.workareas_update); + this.workareas_update = null; + } + this.displays_updating = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 2000, () => { + (() => { + if (!this.auto_tiler) + return; + const toplevels = new Array(); + const assigned_monitors = new Set(); + for (const [old_mon, new_mon] of changes) { + if (old_mon === new_mon) + assigned_monitors.add(new_mon); + } + for (const f of forest.forks.values()) { + if (f.is_toplevel) { + toplevels.push(f); + let migration = null; + const displays = this.displays[1]; + for (const [old_monitor, new_monitor] of changes) { + const display = displays.get(new_monitor); + if (!display) + continue; + if (f.monitor === old_monitor) { + f.monitor = new_monitor; + f.workspace = 0; + migration = [f, new_monitor, display.ws, true]; + } + } + if (!migration) { + const display = displays.get(f.monitor); + if (display) { + migration = [f, f.monitor, display.ws, false]; + } + } + if (migration) { + mark_for_reassignment(this, migration[0].entity); + migrations.push(migration); + } + } + } + apply_migrations(assigned_monitors); + return; + })(); + this.displays_updating = null; + return false; + }); + } + update_scale() { + const new_dpi = St.ThemeContext.get_for_stage(global.stage).scale_factor; + const diff = new_dpi / this.dpi; + this.dpi = new_dpi; + this.column_size *= diff; + this.row_size *= diff; + this.gap_inner_prev *= diff; + this.gap_inner *= diff; + this.gap_inner_half *= diff; + this.gap_outer_prev *= diff; + this.gap_outer *= diff; + this.update_inner_gap(); + this.update_outer_gap(diff); + } + update_snapped() { + for (const entity of this.snapped.find((val) => val)) { + const window = this.windows.get(entity); + if (window) + this.tiler.snap(this, window); + } + } + window_entity(meta) { + if (!meta) + return null; + let id; + try { + id = meta.get_stable_sequence(); + } + catch (e) { + return null; + } + let entity = this.ids.find((comp) => comp == id).next().value; + if (!entity) { + const actor = meta.get_compositor_private(); + if (!actor) + return null; + let window_app, name; + try { + window_app = Window.window_tracker.get_window_app(meta); + name = window_app.get_name().replace(/&/g, '&'); + } + catch (e) { + return null; + } + const window_type = meta.get_window_type(); + if (window_type !== 0 && window_type !== 3 && window_type !== 4) { + return null; + } + entity = this.create_entity(); + this.ids.insert(entity, id); + this.names.insert(entity, name); + let win = new Window.ShellWindow(entity, meta, window_app, this); + this.windows.insert(entity, win); + this.monitors.insert(entity, [win.meta.get_monitor(), win.workspace_id()]); + const grab_focus = () => { + this.schedule_idle(() => { + this.windows.with(entity, (window) => { + window.meta.raise(); + window.meta.unminimize(); + window.activate(false); + }); + return false; + }); + }; + if (this.auto_tiler && !win.meta.minimized && win.is_tilable(this)) { + let id = actor.connect('first-frame', () => { + this.auto_tiler?.auto_tile(this, win, this.init); + grab_focus(); + actor.disconnect(id); + }); + } + else { + grab_focus(); + } + } + return entity; + } + *windows_at_pointer(cursor, monitor, workspace) { + for (const entity of this.monitors.find((m) => m[0] == monitor && m[1] == workspace)) { + let window = this.windows.with(entity, (window) => { + return window.is_tilable(this) && window.rect().contains(cursor) ? window : null; + }); + if (window) + yield window; + } + } + cursor_status() { + const cursor = cursor_rect(); + const rect = new Meta.Rectangle({ x: cursor.x, y: cursor.y, width: 1, height: 1 }); + const monitor = display.get_monitor_index_for_rect(rect); + return [cursor, monitor]; + } + workspace_by_id(id) { + return wom.get_workspace_by_index(id); + } + workspace_id(window = null) { + let id = window + ? [window.meta.get_monitor(), window.workspace_id()] + : [this.active_monitor(), this.active_workspace()]; + id[0] = Math.max(0, id[0]); + id[1] = Math.max(0, id[1]); + return id; + } + is_floating(window) { + let shall_float = false; + let wm_class = window.meta.get_wm_class(); + let wm_title = window.meta.get_title(); + if (wm_class && wm_title) { + shall_float = this.conf.window_shall_float(wm_class, wm_title); + } + let floating_tagged = this.contains_tag(window.entity, Tags.Floating); + let force_tiled_tagged = this.contains_tag(window.entity, Tags.ForceTile); + return (floating_tagged && !force_tiled_tagged) || (shall_float && !force_tiled_tagged); + } +} +let ext = null; +let indicator = null; +export default class PopShellExtension extends Extension { + enable() { + globalThis.popShellExtension = this; + log.info('enable'); + if (!ext) { + ext = new Ext(); + ext.register_fn(() => { + if (ext?.auto_tiler) + ext.snap_windows(); + }); + } + if (ext.settings.show_skiptaskbar()) { + _show_skip_taskbar_windows(ext); + } + else { + _hide_skip_taskbar_windows(); + } + if (ext.was_locked) { + ext.was_locked = false; + return; + } + ext.injections_add(); + ext.signals_attach(); + disable_window_attention_handler(); + layoutManager.addChrome(ext.overlay); + if (!indicator) { + indicator = new PanelSettings.Indicator(ext); + panel.addToStatusArea('pop-shell', indicator.button); + } + ext.keybindings.enable(ext.keybindings.global).enable(ext.keybindings.window_focus); + if (ext.settings.tile_by_default()) { + ext.auto_tile_on(); + } + } + disable() { + log.info('disable'); + if (ext) { + if (sessionMode.isLocked) { + ext.was_locked = true; + return; + } + delete globalThis.popShellExtension; + ext.injections_remove(); + ext.signals_remove(); + ext.exit_modes(); + ext.stop_launcher_services(); + ext.hide_all_borders(); + ext.window_search.remove_injections(); + layoutManager.removeChrome(ext.overlay); + ext.keybindings.disable(ext.keybindings.global).disable(ext.keybindings.window_focus); + if (ext.auto_tiler) { + ext.auto_tiler.destroy(ext); + ext.auto_tiler = null; + } + _hide_skip_taskbar_windows(); + } + if (indicator) { + indicator.destroy(); + indicator = null; + } + enable_window_attention_handler(); + } +} +const handler = windowAttentionHandler; +function enable_window_attention_handler() { + if (handler && !handler._windowDemandsAttentionId) { + handler._windowDemandsAttentionId = global.display.connect('window-demands-attention', (display, window) => { + handler._onWindowDemandsAttention(display, window); + }); + } +} +function disable_window_attention_handler() { + if (handler && handler._windowDemandsAttentionId) { + global.display.disconnect(handler._windowDemandsAttentionId); + handler._windowDemandsAttentionId = null; + } +} +function stylesheet_path(name) { + return get_current_path() + '/' + name + '.css'; +} +function load_theme(style) { + let pop_stylesheet = Number(style); + try { + const theme_context = St.ThemeContext.get_for_stage(global.stage); + const existing_theme = theme_context.get_theme(); + const pop_stylesheet_path = STYLESHEET_PATHS[pop_stylesheet]; + if (existing_theme) { + for (const s of STYLESHEETS) { + existing_theme.unload_stylesheet(s); + } + existing_theme.load_stylesheet(STYLESHEETS[pop_stylesheet]); + theme_context.set_theme(existing_theme); + } + else { + setThemeStylesheet(pop_stylesheet_path); + loadTheme(); + } + return pop_stylesheet_path; + } + catch (e) { + log.error('failed to load stylesheet: ' + e); + return null; + } +} +function* iter_workspaces(manager) { + let idx = 0; + let ws = manager.get_workspace_by_index(idx); + while (ws !== null) { + yield [idx, ws]; + idx += 1; + ws = manager.get_workspace_by_index(idx); + } +} +let default_isoverviewwindow_ws; +let default_isoverviewwindow_ws_thumbnail; +let default_init_appswitcher; +let default_getwindowlist_windowswitcher; +let default_getcaption_windowpreview; +let default_getcaption_workspace; +function _show_skip_taskbar_windows(ext) { + if (!default_isoverviewwindow_ws) { + default_isoverviewwindow_ws = Workspace.prototype._isOverviewWindow; + Workspace.prototype._isOverviewWindow = function (win) { + let meta_win = win; + if (GNOME_VERSION?.startsWith('3.36')) + meta_win = win.get_meta_window(); + return is_valid_minimize_to_tray(meta_win, ext) || default_isoverviewwindow_ws(win); + }; + } + if (GNOME_VERSION?.startsWith('3.36')) { + if (!default_getcaption_workspace) { + default_getcaption_workspace = Workspace.prototype._getCaption; + Workspace.prototype._getCaption = function () { + let metaWindow = this._windowClone.metaWindow; + if (metaWindow.title) + return metaWindow.title; + let tracker = Shell.WindowTracker.get_default(); + let app = tracker.get_window_app(metaWindow); + return app ? app.get_name() : ''; + }; + } + } + else { + if (!default_getcaption_windowpreview) { + default_getcaption_windowpreview = WindowPreview.prototype._getCaption; + log.debug(`override workspace._getCaption`); + WindowPreview.prototype._getCaption = function () { + if (this.metaWindow.title) + return this.metaWindow.title; + let tracker = Shell.WindowTracker.get_default(); + let app = tracker.get_window_app(this.metaWindow); + return app ? app.get_name() : ''; + }; + } + } + if (!default_isoverviewwindow_ws_thumbnail) { + default_isoverviewwindow_ws_thumbnail = WorkspaceThumbnail.prototype._isOverviewWindow; + WorkspaceThumbnail.prototype._isOverviewWindow = function (win) { + let meta_win = win.get_meta_window(); + return is_valid_minimize_to_tray(meta_win, ext) || default_isoverviewwindow_ws_thumbnail(win); + }; + } + if (!default_getwindowlist_windowswitcher) { + default_getwindowlist_windowswitcher = WindowSwitcherPopup.prototype._getWindowList; + WindowSwitcherPopup.prototype._getWindowList = function () { + let workspace = null; + if (this._settings.get_boolean('current-workspace-only')) { + let workspaceManager = global.workspace_manager; + workspace = workspaceManager.get_active_workspace(); + } + let windows = global.display.get_tab_list(Meta.TabList.NORMAL_ALL, workspace); + return windows + .map((w) => { + let meta_win = w.is_attached_dialog() ? w.get_transient_for() : w; + if (meta_win) { + if (!meta_win.skip_taskbar || is_valid_minimize_to_tray(meta_win, ext)) { + return meta_win; + } + } + return null; + }) + .filter((w, i, a) => w != null && a.indexOf(w) == i); + }; + } +} +function _hide_skip_taskbar_windows() { + if (default_isoverviewwindow_ws) { + Workspace.prototype._isOverviewWindow = default_isoverviewwindow_ws; + default_isoverviewwindow_ws = null; + } + if (GNOME_VERSION?.startsWith('3.36')) { + if (default_getcaption_workspace) { + Workspace.prototype._getCaption = default_getcaption_workspace; + default_getcaption_workspace = null; + } + } + else { + if (default_getcaption_windowpreview) { + WindowPreview.prototype._getCaption = default_getcaption_windowpreview; + default_getcaption_windowpreview = null; + } + } + if (default_isoverviewwindow_ws_thumbnail) { + WorkspaceThumbnail.prototype._isOverviewWindow = default_isoverviewwindow_ws_thumbnail; + default_isoverviewwindow_ws_thumbnail = null; + } + if (default_init_appswitcher) { + default_init_appswitcher = null; + } + if (default_getwindowlist_windowswitcher) { + WindowSwitcherPopup.prototype._getWindowList = default_getwindowlist_windowswitcher; + default_getwindowlist_windowswitcher = null; + } +} +function is_valid_minimize_to_tray(meta_win, ext) { + let cfg = ext.conf; + let valid_min_to_tray = false; + switch (meta_win.window_type) { + case Meta.WindowType.NORMAL: + case Meta.WindowType.UTILITY: + valid_min_to_tray = !meta_win.is_override_redirect(); + break; + } + let gnome_shell_wm_class = meta_win.get_wm_class() === 'Gjs' || meta_win.get_wm_class() === 'Gnome-shell'; + let show_skiptb = !cfg.skiptaskbar_shall_hide(meta_win); + valid_min_to_tray = + valid_min_to_tray && + !meta_win.is_attached_dialog() && + show_skiptb && + meta_win.skip_taskbar && + meta_win.get_wm_class() !== null && + !gnome_shell_wm_class; + return valid_min_to_tray; +} diff --git a/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/floating_exceptions/config.js b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/floating_exceptions/config.js new file mode 100644 index 0000000..57e6bf2 --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/floating_exceptions/config.js @@ -0,0 +1,239 @@ +import GLib from 'gi://GLib'; +import Gio from 'gi://Gio'; +const CONF_DIR = GLib.get_user_config_dir() + '/pop-shell'; +export var CONF_FILE = CONF_DIR + '/config.json'; +export const DEFAULT_FLOAT_RULES = [ + { class: 'Authy Desktop' }, + { class: 'Com.github.amezin.ddterm' }, + { class: 'Com.github.donadigo.eddy' }, + { class: 'Conky' }, + { title: 'Discord Updater' }, + { class: 'Enpass', title: 'Enpass Assistant' }, + { class: 'Floating Window Exceptions' }, + { class: 'Gjs', title: 'Settings' }, + { class: 'Gnome-initial-setup' }, + { class: 'Gnome-terminal', title: 'Preferences – General' }, + { class: 'Guake' }, + { class: 'Io.elementary.sideload' }, + { title: 'JavaEmbeddedFrame' }, + { class: 'KotatogramDesktop', title: 'Media viewer' }, + { class: 'Mozilla VPN' }, + { class: 'update-manager', title: 'Software Updater' }, + { class: 'Solaar' }, + { class: 'Steam', title: '^((?!Steam).)*$' }, + { class: 'Steam', title: '^.*(Guard|Login).*' }, + { class: 'TelegramDesktop', title: 'Media viewer' }, + { class: 'Zotero', title: 'Quick Format Citation' }, + { class: 'firefox', title: '^(?!.*Mozilla Firefox).*$' }, + { class: 'gnome-screenshot' }, + { class: 'ibus-.*' }, + { class: 'jetbrains-toolbox' }, + { class: 'jetbrains-webstorm', title: 'Customize WebStorm' }, + { class: 'jetbrains-webstorm', title: 'License Activation' }, + { class: 'jetbrains-webstorm', title: 'Welcome to WebStorm' }, + { class: 'krunner' }, + { class: 'pritunl' }, + { class: 're.sonny.Junction' }, + { class: 'system76-driver' }, + { class: 'tilda' }, + { class: 'zoom' }, + { class: '^.*action=join.*$' }, + { class: 'gjs' }, +]; +export const SKIPTASKBAR_EXCEPTIONS = [ + { class: 'Conky' }, + { class: 'gjs' }, + { class: 'Guake' }, + { class: 'Com.github.amezin.ddterm' }, + { class: 'plank' }, +]; +export class Config { + constructor() { + this.float = []; + this.skiptaskbarhidden = []; + this.log_on_focus = false; + } + add_app_exception(wmclass) { + for (const r of this.float) { + if (r.class === wmclass && r.title === undefined) + return; + } + this.float.push({ class: wmclass }); + this.sync_to_disk(); + } + add_window_exception(wmclass, title) { + for (const r of this.float) { + if (r.class === wmclass && r.title === title) + return; + } + this.float.push({ class: wmclass, title }); + this.sync_to_disk(); + } + window_shall_float(wclass, title) { + for (const rule of this.float.concat(DEFAULT_FLOAT_RULES)) { + if (rule.class) { + if (!new RegExp(rule.class, 'i').test(wclass)) { + continue; + } + } + if (rule.title) { + if (!new RegExp(rule.title, 'i').test(title)) { + continue; + } + } + return rule.disabled ? false : true; + } + return false; + } + skiptaskbar_shall_hide(meta_window) { + let wmclass = meta_window.get_wm_class(); + let wmtitle = meta_window.get_title(); + if (!meta_window.is_skip_taskbar()) + return false; + for (const rule of this.skiptaskbarhidden.concat(SKIPTASKBAR_EXCEPTIONS)) { + if (rule.class) { + if (!new RegExp(rule.class, 'i').test(wmclass)) { + continue; + } + } + if (rule.title) { + if (!new RegExp(rule.title, 'i').test(wmtitle)) { + continue; + } + } + return rule.disabled ? false : true; + } + return false; + } + reload() { + const conf = Config.from_config(); + if (conf.tag === 0) { + let c = conf.value; + this.float = c.float; + this.log_on_focus = c.log_on_focus; + } + else { + log(`error loading conf: ${conf.why}`); + } + } + rule_disabled(rule) { + for (const value of this.float.values()) { + if (value.disabled && rule.class === value.class && value.title === rule.title) { + return true; + } + } + return false; + } + to_json() { + return JSON.stringify(this, set_to_json, 2); + } + toggle_system_exception(wmclass, wmtitle, disabled) { + if (disabled) { + for (const value of DEFAULT_FLOAT_RULES) { + if (value.class === wmclass && value.title === wmtitle) { + value.disabled = disabled; + this.float.push(value); + this.sync_to_disk(); + return; + } + } + } + let index = 0; + let found = false; + for (const value of this.float) { + if (value.class === wmclass && value.title === wmtitle) { + found = true; + break; + } + index += 1; + } + if (found) + swap_remove(this.float, index); + this.sync_to_disk(); + } + remove_user_exception(wmclass, wmtitle) { + let index = 0; + let found = new Array(); + for (const value of this.float.values()) { + if (value.class === wmclass && value.title === wmtitle) { + found.push(index); + } + index += 1; + } + if (found.length !== 0) { + for (const idx of found) + swap_remove(this.float, idx); + this.sync_to_disk(); + } + } + static from_json(json) { + try { + return JSON.parse(json); + } + catch (error) { + return new Config(); + } + } + static from_config() { + const stream = Config.read(); + if (stream.tag === 1) + return stream; + let value = Config.from_json(stream.value); + return { tag: 0, value }; + } + static gio_file() { + try { + const conf = Gio.File.new_for_path(CONF_FILE); + if (!conf.query_exists(null)) { + const dir = Gio.File.new_for_path(CONF_DIR); + if (!dir.query_exists(null) && !dir.make_directory(null)) { + return { tag: 1, why: 'failed to create pop-shell config directory' }; + } + const example = new Config(); + example.float.push({ class: 'pop-shell-example', title: 'pop-shell-example' }); + conf.create(Gio.FileCreateFlags.NONE, null).write_all(JSON.stringify(example, undefined, 2), null); + } + return { tag: 0, value: conf }; + } + catch (why) { + return { tag: 1, why: `Gio.File I/O error: ${why}` }; + } + } + static read() { + try { + const file = Config.gio_file(); + if (file.tag === 1) + return file; + const [, buffer] = file.value.load_contents(null); + return { tag: 0, value: imports.byteArray.toString(buffer) }; + } + catch (why) { + return { tag: 1, why: `failed to read pop-shell config: ${why}` }; + } + } + static write(data) { + try { + const file = Config.gio_file(); + if (file.tag === 1) + return file; + file.value.replace_contents(data, null, false, Gio.FileCreateFlags.NONE, null); + return { tag: 0, value: file.value }; + } + catch (why) { + return { tag: 1, why: `failed to write to config: ${why}` }; + } + } + sync_to_disk() { + Config.write(this.to_json()); + } +} +function set_to_json(_key, value) { + if (typeof value === 'object' && value instanceof Set) { + return [...value]; + } + return value; +} +function swap_remove(array, index) { + array[index] = array[array.length - 1]; + return array.pop(); +} diff --git a/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/floating_exceptions/main.js b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/floating_exceptions/main.js new file mode 100644 index 0000000..660c3de --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/floating_exceptions/main.js @@ -0,0 +1,222 @@ +#!/usr/bin/gjs --module +import Gio from 'gi://Gio'; +import GLib from 'gi://GLib'; +import Gtk from 'gi://Gtk?version=3.0'; +import Pango from 'gi://Pango'; +const SCRIPT_DIR = GLib.path_get_dirname(new Error().stack.split(':')[0].slice(1)); +imports.searchPath.push(SCRIPT_DIR); +import * as config from './config.js'; +const WM_CLASS_ID = 'pop-shell-exceptions'; +var ViewNum; +(function (ViewNum) { + ViewNum[ViewNum["MainView"] = 0] = "MainView"; + ViewNum[ViewNum["Exceptions"] = 1] = "Exceptions"; +})(ViewNum || (ViewNum = {})); +function exceptions_button() { + let title = Gtk.Label.new('System Exceptions'); + title.set_xalign(0); + let description = Gtk.Label.new('Updated based on validated user reports.'); + description.set_xalign(0); + description.get_style_context().add_class('dim-label'); + let icon = Gtk.Image.new_from_icon_name('go-next-symbolic', Gtk.IconSize.BUTTON); + icon.set_hexpand(true); + icon.set_halign(Gtk.Align.END); + let layout = Gtk.Grid.new(); + layout.set_row_spacing(4); + layout.set_border_width(12); + layout.attach(title, 0, 0, 1, 1); + layout.attach(description, 0, 1, 1, 1); + layout.attach(icon, 1, 0, 1, 2); + let button = Gtk.Button.new(); + button.relief = Gtk.ReliefStyle.NONE; + button.add(layout); + return button; +} +export class MainView { + constructor() { + this.callback = () => { }; + let select = Gtk.Button.new_with_label('Select'); + select.set_halign(Gtk.Align.CENTER); + select.connect('clicked', () => this.callback({ tag: 0 })); + select.set_margin_bottom(12); + let exceptions = exceptions_button(); + exceptions.connect('clicked', () => this.callback({ tag: 1, view: ViewNum.Exceptions })); + this.list = Gtk.ListBox.new(); + this.list.set_selection_mode(Gtk.SelectionMode.NONE); + this.list.set_header_func(list_header_func); + this.list.add(exceptions); + let scroller = new Gtk.ScrolledWindow(); + scroller.hscrollbar_policy = Gtk.PolicyType.NEVER; + scroller.set_propagate_natural_width(true); + scroller.set_propagate_natural_height(true); + scroller.add(this.list); + let list_frame = Gtk.Frame.new(null); + list_frame.add(scroller); + let desc = new Gtk.Label({ + label: 'Add exceptions by selecting currently running applications and windows.', + }); + desc.set_line_wrap(true); + desc.set_halign(Gtk.Align.CENTER); + desc.set_justify(Gtk.Justification.CENTER); + desc.set_max_width_chars(55); + desc.set_margin_top(12); + this.widget = Gtk.Box.new(Gtk.Orientation.VERTICAL, 24); + this.widget.add(desc); + this.widget.add(select); + this.widget.add(list_frame); + } + add_rule(wmclass, wmtitle) { + let label = Gtk.Label.new(wmtitle === undefined ? wmclass : `${wmclass} / ${wmtitle}`); + label.set_xalign(0); + label.set_hexpand(true); + label.set_ellipsize(Pango.EllipsizeMode.END); + let button = Gtk.Button.new_from_icon_name('edit-delete', Gtk.IconSize.BUTTON); + button.set_valign(Gtk.Align.CENTER); + let widget = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 24); + widget.add(label); + widget.add(button); + widget.set_border_width(12); + widget.set_margin_start(12); + widget.show_all(); + button.connect('clicked', () => { + widget.destroy(); + this.callback({ tag: 3, wmclass, wmtitle }); + }); + this.list.add(widget); + } +} +export class ExceptionsView { + constructor() { + this.callback = () => { }; + this.exceptions = Gtk.ListBox.new(); + let desc_title = Gtk.Label.new('System Exceptions'); + desc_title.set_use_markup(true); + desc_title.set_xalign(0); + let desc_desc = Gtk.Label.new('Updated based on validated user reports.'); + desc_desc.set_xalign(0); + desc_desc.get_style_context().add_class('dim-label'); + desc_desc.set_margin_bottom(6); + let scroller = new Gtk.ScrolledWindow(); + scroller.hscrollbar_policy = Gtk.PolicyType.NEVER; + scroller.set_propagate_natural_width(true); + scroller.set_propagate_natural_height(true); + scroller.add(this.exceptions); + let exceptions_frame = Gtk.Frame.new(null); + exceptions_frame.add(scroller); + this.exceptions.set_selection_mode(Gtk.SelectionMode.NONE); + this.exceptions.set_header_func(list_header_func); + this.widget = Gtk.Box.new(Gtk.Orientation.VERTICAL, 6); + this.widget.add(desc_title); + this.widget.add(desc_desc); + this.widget.add(exceptions_frame); + } + add_rule(wmclass, wmtitle, enabled) { + let label = Gtk.Label.new(wmtitle === undefined ? wmclass : `${wmclass} / ${wmtitle}`); + label.set_xalign(0); + label.set_hexpand(true); + label.set_ellipsize(Pango.EllipsizeMode.END); + let button = Gtk.Switch.new(); + button.set_valign(Gtk.Align.CENTER); + button.set_state(enabled); + button.connect('notify::state', () => { + this.callback({ tag: 2, wmclass, wmtitle, enable: button.get_state() }); + }); + let widget = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 24); + widget.add(label); + widget.add(button); + widget.show_all(); + widget.set_border_width(12); + this.exceptions.add(widget); + } +} +class App { + constructor() { + var _a, _b, _c, _d; + this.main_view = new MainView(); + this.exceptions_view = new ExceptionsView(); + this.stack = Gtk.Stack.new(); + this.config = new config.Config(); + this.stack.set_border_width(16); + this.stack.add(this.main_view.widget); + this.stack.add(this.exceptions_view.widget); + let back = Gtk.Button.new_from_icon_name('go-previous-symbolic', Gtk.IconSize.BUTTON); + const TITLE = 'Floating Window Exceptions'; + let win = new Gtk.Dialog({ use_header_bar: true }); + let headerbar = win.get_header_bar(); + headerbar.set_show_close_button(true); + headerbar.set_title(TITLE); + headerbar.pack_start(back); + Gtk.Window.set_default_icon_name('application-default'); + win.set_wmclass(WM_CLASS_ID, TITLE); + win.set_default_size(550, 700); + win.get_content_area().add(this.stack); + win.show_all(); + win.connect('delete-event', () => Gtk.main_quit()); + back.hide(); + this.config.reload(); + for (const value of config.DEFAULT_FLOAT_RULES.values()) { + let wmtitle = (_a = value.title) !== null && _a !== void 0 ? _a : undefined; + let wmclass = (_b = value.class) !== null && _b !== void 0 ? _b : undefined; + let disabled = this.config.rule_disabled({ class: wmclass, title: wmtitle }); + this.exceptions_view.add_rule(wmclass, wmtitle, !disabled); + } + for (const value of Array.from(this.config.float)) { + let wmtitle = (_c = value.title) !== null && _c !== void 0 ? _c : undefined; + let wmclass = (_d = value.class) !== null && _d !== void 0 ? _d : undefined; + if (!value.disabled) + this.main_view.add_rule(wmclass, wmtitle); + } + let event_handler = (event) => { + switch (event.tag) { + case 0: + println('SELECT'); + Gtk.main_quit(); + break; + case 1: + switch (event.view) { + case ViewNum.MainView: + this.stack.set_visible_child(this.main_view.widget); + back.hide(); + break; + case ViewNum.Exceptions: + this.stack.set_visible_child(this.exceptions_view.widget); + back.show(); + break; + } + break; + case 2: + log(`toggling exception ${event.enable}`); + this.config.toggle_system_exception(event.wmclass, event.wmtitle, !event.enable); + println('MODIFIED'); + break; + case 3: + log(`removing exception`); + this.config.remove_user_exception(event.wmclass, event.wmtitle); + println('MODIFIED'); + break; + } + }; + this.main_view.callback = event_handler; + this.exceptions_view.callback = event_handler; + back.connect('clicked', () => event_handler({ tag: 1, view: ViewNum.MainView })); + } +} +function list_header_func(row, before) { + if (before) { + row.set_header(Gtk.Separator.new(Gtk.Orientation.HORIZONTAL)); + } +} +const STDOUT = new Gio.DataOutputStream({ + base_stream: new Gio.UnixOutputStream({ fd: 1 }), +}); +function println(message) { + STDOUT.put_string(message + '\n', null); +} +function main() { + GLib.set_prgname(WM_CLASS_ID); + GLib.set_application_name('Pop Shell Floating Window Exceptions'); + Gtk.init(null); + new App(); + Gtk.main(); +} +main(); diff --git a/debian/copyright b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/floating_exceptions/utils.js similarity index 100% rename from debian/copyright rename to gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/floating_exceptions/utils.js diff --git a/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/focus.js b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/focus.js new file mode 100644 index 0000000..a42f605 --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/focus.js @@ -0,0 +1,55 @@ +import * as Geom from './geom.js'; +export var FocusPosition; +(function (FocusPosition) { + FocusPosition["TopLeft"] = "Top Left"; + FocusPosition["TopRight"] = "Top Right"; + FocusPosition["BottomLeft"] = "Bottom Left"; + FocusPosition["BottomRight"] = "Bottom Right"; + FocusPosition["Center"] = "Center"; +})(FocusPosition || (FocusPosition = {})); +export class FocusSelector { + select(ext, direction, window) { + window = window ?? ext.focus_window(); + if (window) { + let window_list = ext.active_window_list(); + return select(direction, window, window_list); + } + return null; + } + down(ext, window) { + return this.select(ext, window_down, window); + } + left(ext, window) { + return this.select(ext, window_left, window); + } + right(ext, window) { + return this.select(ext, window_right, window); + } + up(ext, window) { + return this.select(ext, window_up, window); + } +} +function select(windows, focused, window_list) { + const array = windows(focused, window_list); + return array.length > 0 ? array[0] : null; +} +function window_down(focused, windows) { + return windows + .filter((win) => !win.meta.minimized && win.meta.get_frame_rect().y > focused.meta.get_frame_rect().y) + .sort((a, b) => Geom.downward_distance(a.meta, focused.meta) - Geom.downward_distance(b.meta, focused.meta)); +} +function window_left(focused, windows) { + return windows + .filter((win) => !win.meta.minimized && win.meta.get_frame_rect().x < focused.meta.get_frame_rect().x) + .sort((a, b) => Geom.leftward_distance(a.meta, focused.meta) - Geom.leftward_distance(b.meta, focused.meta)); +} +function window_right(focused, windows) { + return windows + .filter((win) => !win.meta.minimized && win.meta.get_frame_rect().x > focused.meta.get_frame_rect().x) + .sort((a, b) => Geom.rightward_distance(a.meta, focused.meta) - Geom.rightward_distance(b.meta, focused.meta)); +} +function window_up(focused, windows) { + return windows + .filter((win) => !win.meta.minimized && win.meta.get_frame_rect().y < focused.meta.get_frame_rect().y) + .sort((a, b) => Geom.upward_distance(a.meta, focused.meta) - Geom.upward_distance(b.meta, focused.meta)); +} diff --git a/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/forest.js b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/forest.js new file mode 100644 index 0000000..ad48293 --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/forest.js @@ -0,0 +1,700 @@ +import * as arena from './arena.js'; +import * as Ecs from './ecs.js'; +import * as Lib from './lib.js'; +import * as log from './log.js'; +import * as movement from './movement.js'; +import * as Rect from './rectangle.js'; +import * as Node from './node.js'; +import * as Fork from './fork.js'; +import * as geom from './geom.js'; +const { Arena } = arena; +import Meta from 'gi://Meta'; +const { Movement } = movement; +const { DOWN, UP, LEFT, RIGHT } = Movement; +export class Forest extends Ecs.World { + constructor() { + super(); + this.toplevel = new Map(); + this.requested = new Map(); + this.stack_updates = new Array(); + this.forks = this.register_storage(); + this.parents = this.register_storage(); + this.string_reps = this.register_storage(); + this.stacks = new Arena(); + this.on_attach = () => { }; + this.on_detach = () => { }; + } + measure(ext, fork, area) { + fork.measure(this, ext, area, this.on_record()); + } + tile(ext, fork, area, ignore_reset = true) { + this.measure(ext, fork, area); + this.arrange(ext, fork.workspace, ignore_reset); + } + arrange(ext, _workspace, _ignore_reset = false) { + for (const [entity, r] of this.requested) { + const window = ext.windows.get(entity); + if (!window) + continue; + let on_complete = () => { + if (!window.actor_exists()) + return; + }; + if (ext.tiler.window) { + if (Ecs.entity_eq(ext.tiler.window, entity)) { + on_complete = () => { + ext.set_overlay(window.rect()); + if (!window.actor_exists()) + return; + }; + } + } + move_window(ext, window, r.rect, on_complete); + } + this.requested.clear(); + for (const [stack] of this.stack_updates.splice(0)) { + ext.auto_tiler?.update_stack(ext, stack); + } + } + attach_fork(ext, fork, window, is_left) { + const node = Node.Node.window(window); + if (is_left) { + if (fork.right) { + const new_fork = this.create_fork(fork.left, fork.right, fork.area_of_right(ext), fork.workspace, fork.monitor)[0]; + fork.right = Node.Node.fork(new_fork); + this.parents.insert(new_fork, fork.entity); + this.on_attach(new_fork, window); + } + else { + this.on_attach(fork.entity, window); + fork.right = fork.left; + } + fork.left = node; + } + else { + if (fork.right) { + const new_fork = this.create_fork(fork.left, fork.right, fork.area_of_left(ext), fork.workspace, fork.monitor)[0]; + fork.left = Node.Node.fork(new_fork); + this.parents.insert(new_fork, fork.entity); + this.on_attach(new_fork, window); + } + else { + this.on_attach(fork.entity, window); + } + fork.right = node; + } + this.on_attach(fork.entity, window); + } + attach_stack(ext, stack, fork, new_entity, stack_from_left) { + const container = this.stacks.get(stack.idx); + if (container) { + const window = ext.windows.get(new_entity); + if (window) { + window.stack = stack.idx; + if (stack_from_left) { + stack.entities.push(new_entity); + } + else { + stack.entities.unshift(new_entity); + } + this.on_attach(fork.entity, new_entity); + ext.auto_tiler?.update_stack(ext, stack); + if (window.meta.has_focus()) { + container.activate(new_entity); + } + return [fork.entity, fork]; + } + else { + log.warn('attempted to attach window to stack that does not exist'); + } + } + else { + log.warn('attempted to attach to stack that does not exist'); + } + return null; + } + attach_window(ext, onto_entity, new_entity, place_by, stack_from_left) { + function place_by_keyboard(fork, src, left, right) { + const from = [src.x + src.width / 2, src.y + src.height / 2]; + const lside = geom.shortest_side(from, left); + const rside = geom.shortest_side(from, right); + if (lside < rside) + fork.swap_branches(); + } + function place(place_by, fork, left, right) { + if ('swap' in place_by) { + const { orientation, swap } = place_by; + fork.set_orientation(orientation); + if (swap) + fork.swap_branches(); + } + else if ('src' in place_by) { + place_by_keyboard(fork, place_by.src, left, right); + } + } + function area_of_halves(fork) { + const { x, y, width, height } = fork.area; + const [left, right] = fork.is_horizontal() + ? [ + [x, y, width / 2, height], + [x + width / 2, y, width / 2, height], + ] + : [ + [x, y, width, height / 2], + [x, y + height / 2, width, height / 2], + ]; + return [new Rect.Rectangle(left), new Rect.Rectangle(right)]; + } + const fork_and_place_on_left = (entity, fork) => { + const area = fork.area_of_left(ext); + const [fork_entity, new_fork] = this.create_fork(fork.left, right_node, area, fork.workspace, fork.monitor); + fork.left = Node.Node.fork(fork_entity); + this.parents.insert(fork_entity, entity); + const [left, right] = area_of_halves(new_fork); + place(place_by, new_fork, left, right); + return this._attach(onto_entity, new_entity, this.on_attach, entity, fork, [fork_entity, new_fork]); + }; + const fork_and_place_on_right = (entity, fork, right_branch) => { + const area = fork.area_of_right(ext); + const [fork_entity, new_fork] = this.create_fork(right_branch, right_node, area, fork.workspace, fork.monitor); + fork.right = Node.Node.fork(fork_entity); + this.parents.insert(fork_entity, entity); + const [left, right] = area_of_halves(new_fork); + place(place_by, new_fork, left, right); + return this._attach(onto_entity, new_entity, this.on_attach, entity, fork, [fork_entity, new_fork]); + }; + const right_node = Node.Node.window(new_entity); + for (const [entity, fork] of this.forks.iter()) { + if (fork.left.is_window(onto_entity)) { + if (fork.right) { + return fork_and_place_on_left(entity, fork); + } + else { + fork.right = right_node; + fork.set_ratio(fork.length() / 2); + if ('src' in place_by) { + const [left, right] = area_of_halves(fork); + place_by_keyboard(fork, place_by.src, left, right); + } + return this._attach(onto_entity, new_entity, this.on_attach, entity, fork, null); + } + } + else if (fork.left.is_in_stack(onto_entity)) { + const stack = fork.left.inner; + return this.attach_stack(ext, stack, fork, new_entity, stack_from_left); + } + else if (fork.right) { + if (fork.right.is_window(onto_entity)) { + return fork_and_place_on_right(entity, fork, fork.right); + } + else if (fork.right.is_in_stack(onto_entity)) { + const stack = fork.right.inner; + return this.attach_stack(ext, stack, fork, new_entity, stack_from_left); + } + } + } + return null; + } + connect_on_attach(callback) { + this.on_attach = callback; + return this; + } + connect_on_detach(callback) { + this.on_detach = callback; + return this; + } + create_entity() { + const entity = super.create_entity(); + this.string_reps.insert(entity, `${entity}`); + return entity; + } + create_fork(left, right, area, workspace, monitor) { + const entity = this.create_entity(); + let orient = area.width > area.height ? Lib.Orientation.HORIZONTAL : Lib.Orientation.VERTICAL; + let fork = new Fork.Fork(entity, left, right, area, workspace, monitor, orient); + this.forks.insert(entity, fork); + return [entity, fork]; + } + create_toplevel(window, area, id) { + const [entity, fork] = this.create_fork(Node.Node.window(window), null, area, id[1], id[0]); + this.string_reps.with(entity, (sid) => { + fork.set_toplevel(this, entity, sid, id); + }); + return [entity, fork]; + } + delete_entity(entity) { + const fork = this.forks.remove(entity); + if (fork && fork.is_toplevel) { + const id = this.string_reps.get(entity); + if (id) + this.toplevel.delete(id); + } + super.delete_entity(entity); + } + detach(ext, fork_entity, window) { + const fork = this.forks.get(fork_entity); + if (!fork) + return null; + let reflow_fork = null, stack_detach = false; + const parent = this.parents.get(fork_entity); + if (fork.left.is_window(window)) { + if (parent && fork.right) { + const pfork = this.reassign_child_to_parent(fork_entity, parent, fork.right); + if (!pfork) + return null; + reflow_fork = [parent, pfork]; + } + else if (fork.right) { + reflow_fork = [fork_entity, fork]; + switch (fork.right.inner.kind) { + case 1: + this.reassign_children_to_parent(fork_entity, fork.right.inner.entity, fork); + break; + default: + const detached = fork.right; + fork.left = detached; + fork.right = null; + } + } + else { + this.delete_entity(fork_entity); + } + } + else if (fork.left.is_in_stack(window)) { + reflow_fork = [fork_entity, fork]; + stack_detach = true; + this.remove_from_stack(ext, fork.left.inner, window, () => { + if (fork.right) { + fork.left = fork.right; + fork.right = null; + if (parent) { + const pfork = this.reassign_child_to_parent(fork_entity, parent, fork.left); + if (!pfork) + return null; + reflow_fork = [parent, pfork]; + } + } + else { + this.delete_entity(fork.entity); + } + }); + } + else if (fork.right) { + if (fork.right.is_window(window)) { + if (parent) { + const pfork = this.reassign_child_to_parent(fork_entity, parent, fork.left); + if (!pfork) + return null; + reflow_fork = [parent, pfork]; + } + else { + reflow_fork = [fork_entity, fork]; + switch (fork.left.inner.kind) { + case 1: + this.reassign_children_to_parent(fork_entity, fork.left.inner.entity, fork); + break; + default: + fork.right = null; + break; + } + } + } + else if (fork.right.is_in_stack(window)) { + reflow_fork = [fork_entity, fork]; + stack_detach = true; + this.remove_from_stack(ext, fork.right.inner, window, () => { + fork.right = null; + this.reassign_to_parent(fork, fork.left); + }); + } + } + if (stack_detach) { + ext.windows.with(window, (w) => (w.stack = null)); + } + this.on_detach(window); + if (reflow_fork && !stack_detach) { + reflow_fork[1].rebalance_orientation(); + } + return reflow_fork; + } + fmt(ext) { + let fmt = ''; + for (const [entity] of this.toplevel.values()) { + const fork = this.forks.get(entity); + fmt += ' '; + if (fork) { + fmt += this.display_fork(ext, entity, fork, 1) + '\n'; + } + else { + fmt += `Fork(${entity}) Invalid\n`; + } + } + return fmt; + } + find_toplevel([src_mon, src_work]) { + for (const [entity, fork] of this.forks.iter()) { + if (!fork.is_toplevel) + continue; + const { monitor, workspace } = fork; + if (monitor == src_mon && workspace == src_work) { + return entity; + } + } + return null; + } + grow_sibling(ext, fork_e, fork_c, is_left, movement, crect) { + const resize_fork = () => this.resize_fork_(ext, fork_e, crect, movement, false); + if (fork_c.is_horizontal()) { + if ((movement & (DOWN | UP)) != 0) { + resize_fork(); + } + else if (is_left) { + if ((movement & RIGHT) != 0) { + this.readjust_fork_ratio_by_left(ext, crect.width, fork_c); + } + else { + resize_fork(); + } + } + else if ((movement & RIGHT) != 0) { + resize_fork(); + } + else { + this.readjust_fork_ratio_by_right(ext, crect.width, fork_c, fork_c.area.width); + } + } + else { + if ((movement & (LEFT | RIGHT)) != 0) { + resize_fork(); + } + else if (is_left) { + if ((movement & DOWN) != 0) { + this.readjust_fork_ratio_by_left(ext, crect.height, fork_c); + } + else { + resize_fork(); + } + } + else if ((movement & DOWN) != 0) { + resize_fork(); + } + else { + this.readjust_fork_ratio_by_right(ext, crect.height, fork_c, fork_c.area.height); + } + } + } + *iter(entity, kind = null) { + let fork = this.forks.get(entity); + let forks = new Array(2); + while (fork) { + if (fork.left.inner.kind === 1) { + forks.push(this.forks.get(fork.left.inner.entity)); + } + if (kind === null || fork.left.inner.kind === kind) { + yield fork.left; + } + if (fork.right) { + if (fork.right.inner.kind === 1) { + forks.push(this.forks.get(fork.right.inner.entity)); + } + if (kind === null || fork.right.inner.kind == kind) { + yield fork.right; + } + } + fork = forks.pop(); + } + } + largest_window_on(ext, entity) { + let largest_window = null; + let largest_size = 0; + let window_compare = (entity) => { + const window = ext.windows.get(entity); + if (window && window.is_tilable(ext)) { + const rect = window.rect(); + const size = rect.width * rect.height; + if (size > largest_size) { + largest_size = size; + largest_window = window; + } + } + }; + for (const node of this.iter(entity)) { + switch (node.inner.kind) { + case 2: + window_compare(node.inner.entity); + break; + case 3: + window_compare(node.inner.entities[0]); + } + } + return largest_window; + } + resize(ext, fork_e, fork_c, win_e, movement, crect) { + const is_left = fork_c.left.is_window(win_e) || fork_c.left.is_in_stack(win_e); + ((movement & Movement.SHRINK) != 0 ? this.shrink_sibling : this.grow_sibling).call(this, ext, fork_e, fork_c, is_left, movement, crect); + } + on_record() { + return (e, p, a) => this.record(e, p, a); + } + record(entity, parent, rect) { + this.requested.set(entity, { + parent: parent, + rect: rect, + }); + } + reassign_child_to_parent(child_entity, parent_entity, branch) { + const parent = this.forks.get(parent_entity); + if (parent) { + if (parent.left.is_fork(child_entity)) { + parent.left = branch; + } + else { + parent.right = branch; + } + this.reassign_sibling(branch, parent_entity); + this.delete_entity(child_entity); + } + return parent; + } + reassign_to_parent(child, reassign) { + const p = this.parents.get(child.entity); + if (p) { + const p_fork = this.forks.get(p); + if (p_fork) { + if (p_fork.left.is_fork(child.entity)) { + p_fork.left = reassign; + } + else { + p_fork.right = reassign; + } + const inner = reassign.inner; + switch (inner.kind) { + case 1: + this.parents.insert(inner.entity, p); + break; + case 2: + this.on_attach(p, inner.entity); + break; + case 3: + for (const entity of inner.entities) + this.on_attach(p, entity); + } + } + this.delete_entity(child.entity); + } + } + reassign_sibling(sibling, parent) { + switch (sibling.inner.kind) { + case 1: + this.parents.insert(sibling.inner.entity, parent); + break; + case 2: + this.on_attach(parent, sibling.inner.entity); + break; + case 3: + for (const entity of sibling.inner.entities) { + this.on_attach(parent, entity); + } + } + } + reassign_children_to_parent(parent_entity, child_entity, p_fork) { + const c_fork = this.forks.get(child_entity); + if (c_fork) { + p_fork.left = c_fork.left; + p_fork.right = c_fork.right; + this.reassign_sibling(p_fork.left, parent_entity); + if (p_fork.right) + this.reassign_sibling(p_fork.right, parent_entity); + this.delete_entity(child_entity); + } + else { + log.error(`Fork(${child_entity}) does not exist`); + } + } + readjust_fork_ratio_by_left(ext, left_length, fork) { + fork.set_ratio(left_length).measure(this, ext, fork.area, this.on_record()); + } + readjust_fork_ratio_by_right(ext, right_length, fork, fork_length) { + this.readjust_fork_ratio_by_left(ext, fork_length - right_length, fork); + } + remove_from_stack(ext, stack, window, on_last) { + if (stack.entities.length === 1) { + this.stacks.remove(stack.idx)?.destroy(); + on_last(); + } + else { + const s = this.stacks.get(stack.idx); + if (s) { + Node.stack_remove(this, stack, window); + } + } + const win = ext.windows.get(window); + if (win) { + win.stack = null; + } + } + resize_fork_(ext, child_e, crect, mov, shrunk) { + let parent = this.parents.get(child_e), child = this.forks.get(child_e); + if (!parent) { + child.measure(this, ext, child.area, this.on_record()); + return; + } + const src_node = this.forks.get(child_e); + if (!src_node) + return; + let is_left = child.left.is_fork(child_e), length; + while (parent !== null) { + child = this.forks.get(parent); + is_left = child.left.is_fork(child_e); + if (child.area.contains(crect)) { + if ((mov & UP) !== 0) { + if (shrunk) { + if (child.area.y + child.area.height > src_node.area.y + src_node.area.height) { + break; + } + } + else if (!child.is_horizontal() || !is_left) { + break; + } + } + else if ((mov & DOWN) !== 0) { + if (shrunk) { + if (child.area.y < src_node.area.y) { + break; + } + } + else if (child.is_horizontal() || is_left) { + break; + } + } + else if ((mov & LEFT) !== 0) { + if (shrunk) { + if (child.area.x + child.area.width > src_node.area.x + src_node.area.width) { + break; + } + } + else if (!child.is_horizontal() || !is_left) { + break; + } + } + else if ((mov & RIGHT) !== 0) { + if (shrunk) { + if (child.area.x < src_node.area.x) { + break; + } + } + else if (!child.is_horizontal() || is_left) { + break; + } + } + } + child_e = parent; + parent = this.parents.get(child_e); + } + if (child.is_horizontal()) { + length = is_left ? crect.x + crect.width - child.area.x : crect.x - child.area.x; + } + else { + length = is_left ? crect.y + crect.height - child.area.y : child.area.height - crect.height; + } + child.set_ratio(length); + child.measure(this, ext, child.area, this.on_record()); + } + shrink_sibling(ext, fork_e, fork_c, is_left, movement, crect) { + const resize_fork = () => this.resize_fork_(ext, fork_e, crect, movement, true); + if (fork_c.area) { + if (fork_c.is_horizontal()) { + if ((movement & (DOWN | UP)) != 0) { + resize_fork(); + } + else if (is_left) { + if ((movement & LEFT) != 0) { + this.readjust_fork_ratio_by_left(ext, crect.width, fork_c); + } + else { + resize_fork(); + } + } + else if ((movement & LEFT) != 0) { + resize_fork(); + } + else { + this.readjust_fork_ratio_by_right(ext, crect.width, fork_c, fork_c.area.array[2]); + } + } + else { + if ((movement & (LEFT | RIGHT)) != 0) { + resize_fork(); + } + else if (is_left) { + if ((movement & UP) != 0) { + this.readjust_fork_ratio_by_left(ext, crect.height, fork_c); + } + else { + resize_fork(); + } + } + else if ((movement & UP) != 0) { + resize_fork(); + } + else { + this.readjust_fork_ratio_by_right(ext, crect.height, fork_c, fork_c.area.array[3]); + } + } + } + } + _attach(onto_entity, new_entity, assoc, entity, fork, result) { + if (result) { + assoc(result[0], onto_entity); + assoc(result[0], new_entity); + } + else { + assoc(entity, new_entity); + } + return [entity, fork]; + } + display_branch(ext, branch, scope) { + switch (branch.inner.kind) { + case 1: + const fork = this.forks.get(branch.inner.entity); + return fork ? this.display_fork(ext, branch.inner.entity, fork, scope + 1) : 'Missing Fork'; + case 2: + const window = ext.windows.get(branch.inner.entity); + return `Window(${branch.inner.entity}) (${window ? window.rect().fmt() : 'unknown area'}; parent: ${ext.auto_tiler?.attached.get(branch.inner.entity)})`; + case 3: + let fmt = 'Stack('; + for (const entity of branch.inner.entities) { + const window = ext.windows.get(entity); + fmt += `Window(${entity}) (${window ? window.rect().fmt() : 'unknown area'}), `; + } + return fmt + ')'; + } + } + display_fork(ext, entity, fork, scope) { + let fmt = `Fork(${entity}) [${fork.area ? fork.area.array : 'unknown'}]: {\n`; + fmt += ' '.repeat((1 + scope) * 2) + `workspace: (${fork.workspace}),\n`; + fmt += ' '.repeat((1 + scope) * 2) + 'left: ' + this.display_branch(ext, fork.left, scope) + ',\n'; + fmt += ' '.repeat((1 + scope) * 2) + 'parent: ' + this.parents.get(fork.entity) + ',\n'; + if (fork.right) { + fmt += ' '.repeat((1 + scope) * 2) + 'right: ' + this.display_branch(ext, fork.right, scope) + ',\n'; + } + fmt += ' '.repeat(scope * 2) + '}'; + return fmt; + } +} +function move_window(ext, window, rect, on_complete) { + if (!(window.meta instanceof Meta.Window)) { + log.error(`attempting to a window entity in a tree which lacks a Meta.Window`); + return; + } + const actor = window.meta.get_compositor_private(); + if (!actor) { + log.warn(`Window(${window.entity}) does not have an actor, and therefore cannot be moved`); + return; + } + ext.size_signals_block(window); + window.move(ext, rect, () => { + on_complete(); + ext.size_signals_unblock(window); + }); +} diff --git a/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/fork.js b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/fork.js new file mode 100644 index 0000000..119f9d9 --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/fork.js @@ -0,0 +1,286 @@ +import * as Ecs from './ecs.js'; +import * as Lib from './lib.js'; +import * as node from './node.js'; +import * as Rect from './rectangle.js'; +const XPOS = 0; +const YPOS = 1; +const WIDTH = 2; +const HEIGHT = 3; +export class Fork { + constructor(entity, left, right, area, workspace, monitor, orient) { + this.prev_ratio = 0.5; + this.minimum_ratio = 0.1; + this.orientation = Lib.Orientation.HORIZONTAL; + this.orientation_changed = false; + this.is_toplevel = false; + this.smart_gapped = false; + this.n_toggled = 0; + this.on_primary_display = global.display.get_primary_monitor() === monitor; + this.area = area; + this.left = left; + this.right = right; + this.workspace = workspace; + this.length_left = orient === Lib.Orientation.HORIZONTAL ? this.area.width / 2 : this.area.height / 2; + this.prev_length_left = this.length_left; + this.entity = entity; + this.orientation = orient; + this.monitor = monitor; + } + area_of_left(ext) { + return new Rect.Rectangle(this.is_horizontal() + ? [this.area.x, this.area.y, this.length_left - ext.gap_inner_half, this.area.height] + : [this.area.x, this.area.y, this.area.width, this.length_left - ext.gap_inner_half]); + } + area_of_right(ext) { + let area; + if (this.is_horizontal()) { + const width = this.area.width - this.length_left + ext.gap_inner; + area = [width, this.area.y, this.area.width - width, this.area.height]; + } + else { + const height = this.area.height - this.length_left + ext.gap_inner; + area = [this.area.x, height, this.area.width, this.area.height - height]; + } + return new Rect.Rectangle(area); + } + depth() { + return this.is_horizontal() ? this.area.height : this.area.width; + } + find_branch(entity) { + const locate = (branch) => { + switch (branch.inner.kind) { + case 2: + if (Ecs.entity_eq(branch.inner.entity, entity)) { + return branch; + } + break; + case 3: + for (const e of branch.inner.entities) { + if (Ecs.entity_eq(e, entity)) { + return branch; + } + } + } + return null; + }; + const node = locate(this.left); + if (node) + return node; + return this.right ? locate(this.right) : null; + } + is_horizontal() { + return Lib.Orientation.HORIZONTAL == this.orientation; + } + length() { + return this.is_horizontal() ? this.area.width : this.area.height; + } + replace_window(ext, a, b) { + let closure = null; + let check_right = () => { + if (this.right) { + const inner = this.right.inner; + if (inner.kind === 2) { + closure = () => { + inner.entity = b.entity; + }; + } + else if (inner.kind === 3) { + const idx = node.stack_find(inner, a.entity); + if (idx === null) { + closure = null; + return; + } + closure = () => { + node.stack_replace(ext, inner, b); + inner.entities[idx] = b.entity; + }; + } + } + }; + switch (this.left.inner.kind) { + case 1: + check_right(); + break; + case 2: + const inner = this.left.inner; + if (Ecs.entity_eq(inner.entity, a.entity)) { + closure = () => { + inner.entity = b.entity; + }; + } + else { + check_right(); + } + break; + case 3: + const inner_s = this.left.inner; + let idx = node.stack_find(inner_s, a.entity); + if (idx !== null) { + const id = idx; + closure = () => { + node.stack_replace(ext, inner_s, b); + inner_s.entities[id] = b.entity; + }; + } + else { + check_right(); + } + } + return closure; + } + set_area(area) { + this.area = area; + return this.area; + } + set_ratio(left_length) { + const fork_len = this.is_horizontal() ? this.area.width : this.area.height; + const clamped = Math.round(Math.max(256, Math.min(fork_len - 256, left_length))); + this.prev_length_left = clamped; + this.length_left = clamped; + return this; + } + set_toplevel(tiler, entity, string, id) { + this.is_toplevel = true; + tiler.toplevel.set(string, [entity, id]); + return this; + } + measure(tiler, ext, area, record) { + let ratio = null; + let manually_moved = ext.grab_op !== null || ext.tiler.resizing_window; + if (!this.is_toplevel) { + if (this.orientation_changed) { + this.orientation_changed = false; + ratio = this.length_left / this.depth(); + } + else { + ratio = this.length_left / this.length(); + } + this.area = this.set_area(area.clone()); + } + else if (this.orientation_changed) { + this.orientation_changed = false; + ratio = this.length_left / this.depth(); + } + if (ratio) { + this.length_left = Math.round(ratio * this.length()); + if (manually_moved) + this.prev_ratio = ratio; + } + else if (manually_moved) { + this.prev_ratio = this.length_left / this.length(); + } + if (this.right) { + const [l, p, startpos] = this.is_horizontal() ? [WIDTH, XPOS, this.area.x] : [HEIGHT, YPOS, this.area.y]; + let region = this.area.clone(); + const half = this.area.array[l] / 2; + let length; + if (this.length_left > half - 32 && this.length_left < half + 32) { + length = half; + } + else { + const diff = (startpos + this.length_left) % 32; + length = this.length_left - diff + (diff > 16 ? 32 : 0); + if (length == 0) + length = 32; + } + region.array[l] = length - ext.gap_inner_half; + this.left.measure(tiler, ext, this.entity, region, record); + region.array[p] = region.array[p] + length + ext.gap_inner_half; + region.array[l] = this.area.array[l] - length - ext.gap_inner_half; + this.right.measure(tiler, ext, this.entity, region, record); + } + else { + this.left.measure(tiler, ext, this.entity, this.area, record); + } + } + migrate(ext, forest, area, monitor, workspace) { + if (ext.auto_tiler && this.is_toplevel) { + const primary = global.display.get_primary_monitor() === monitor; + this.monitor = monitor; + this.workspace = workspace; + this.on_primary_display = primary; + let blocked = new Array(); + forest.toplevel.set(forest.string_reps.get(this.entity), [this.entity, [monitor, workspace]]); + for (const child of forest.iter(this.entity)) { + switch (child.inner.kind) { + case 1: + const cfork = forest.forks.get(child.inner.entity); + if (!cfork) + continue; + cfork.workspace = workspace; + cfork.monitor = monitor; + cfork.on_primary_display = primary; + break; + case 2: + let window = ext.windows.get(child.inner.entity); + if (window) { + ext.size_signals_block(window); + window.reassignment = false; + window.known_workspace = workspace; + window.meta.change_workspace_by_index(workspace, true); + ext.monitors.insert(window.entity, [monitor, workspace]); + blocked.push(window); + } + break; + case 3: + for (const entity of child.inner.entities) { + let stack = ext.auto_tiler.forest.stacks.get(child.inner.idx); + if (stack) { + stack.workspace = workspace; + } + let window = ext.windows.get(entity); + if (window) { + ext.size_signals_block(window); + window.known_workspace = workspace; + window.meta.change_workspace_by_index(workspace, true); + ext.monitors.insert(window.entity, [monitor, workspace]); + blocked.push(window); + } + } + } + } + area.x += ext.gap_outer; + area.y += ext.gap_outer; + area.width -= ext.gap_outer * 2; + area.height -= ext.gap_outer * 2; + this.set_area(area.clone()); + this.measure(forest, ext, area, forest.on_record()); + forest.arrange(ext, workspace, true); + for (const window of blocked) { + ext.size_signals_unblock(window); + } + } + } + rebalance_orientation() { + this.set_orientation(this.area.height > this.area.width ? Lib.Orientation.VERTICAL : Lib.Orientation.HORIZONTAL); + } + set_orientation(o) { + if (o !== this.orientation) { + this.orientation = o; + this.orientation_changed = true; + } + } + swap_branches() { + if (this.right) { + const temp = this.left; + this.left = this.right; + this.right = temp; + } + } + toggle_orientation() { + this.orientation = + Lib.Orientation.HORIZONTAL === this.orientation ? Lib.Orientation.VERTICAL : Lib.Orientation.HORIZONTAL; + this.orientation_changed = true; + if (this.n_toggled === 1) { + if (this.right) { + const tmp = this.right; + this.right = this.left; + this.left = tmp; + } + this.n_toggled = 0; + } + else { + this.n_toggled += 1; + } + } +} diff --git a/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/geom.js b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/geom.js new file mode 100644 index 0000000..77a8e2c --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/geom.js @@ -0,0 +1,74 @@ +export var Side; +(function (Side) { + Side[Side["LEFT"] = 0] = "LEFT"; + Side[Side["TOP"] = 1] = "TOP"; + Side[Side["RIGHT"] = 2] = "RIGHT"; + Side[Side["BOTTOM"] = 3] = "BOTTOM"; + Side[Side["CENTER"] = 4] = "CENTER"; +})(Side || (Side = {})); +export function xend(rect) { + return rect.x + rect.width; +} +export function xcenter(rect) { + return rect.x + rect.width / 2; +} +export function yend(rect) { + return rect.y + rect.height; +} +export function ycenter(rect) { + return rect.y + rect.height / 2; +} +export function center(rect) { + return [xcenter(rect), ycenter(rect)]; +} +export function north(rect) { + return [xcenter(rect), rect.y]; +} +export function east(rect) { + return [xend(rect), ycenter(rect)]; +} +export function south(rect) { + return [xcenter(rect), yend(rect)]; +} +export function west(rect) { + return [rect.x, ycenter(rect)]; +} +export function distance([ax, ay], [bx, by]) { + return Math.sqrt(Math.pow(bx - ax, 2) + Math.pow(by - ay, 2)); +} +export function directional_distance(a, b, fn_a, fn_b) { + return distance(fn_a(a), fn_b(b)); +} +export function window_distance(win_a, win_b) { + return directional_distance(win_a.get_frame_rect(), win_b.get_frame_rect(), center, center); +} +export function upward_distance(win_a, win_b) { + return directional_distance(win_a.get_frame_rect(), win_b.get_frame_rect(), south, north); +} +export function rightward_distance(win_a, win_b) { + return directional_distance(win_a.get_frame_rect(), win_b.get_frame_rect(), west, east); +} +export function downward_distance(win_a, win_b) { + return directional_distance(win_a.get_frame_rect(), win_b.get_frame_rect(), north, south); +} +export function leftward_distance(win_a, win_b) { + return directional_distance(win_a.get_frame_rect(), win_b.get_frame_rect(), east, west); +} +export function nearest_side(ext, origin, rect) { + const left = west(rect), top = north(rect), right = east(rect), bottom = south(rect), ctr = center(rect); + const left_distance = distance(origin, left), top_distance = distance(origin, top), right_distance = distance(origin, right), bottom_distance = distance(origin, bottom), center_distance = distance(origin, ctr); + let nearest = left_distance < right_distance ? [left_distance, Side.LEFT] : [right_distance, Side.RIGHT]; + if (top_distance < nearest[0]) + nearest = [top_distance, Side.TOP]; + if (bottom_distance < nearest[0]) + nearest = [bottom_distance, Side.BOTTOM]; + if (ext.settings.stacking_with_mouse() && center_distance < nearest[0]) + nearest = [center_distance, Side.CENTER]; + return nearest; +} +export function shortest_side(origin, rect) { + let shortest = distance(origin, west(rect)); + shortest = Math.min(shortest, distance(origin, north(rect))); + shortest = Math.min(shortest, distance(origin, east(rect))); + return Math.min(shortest, distance(origin, south(rect))); +} diff --git a/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/grab_op.js b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/grab_op.js new file mode 100644 index 0000000..328d425 --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/grab_op.js @@ -0,0 +1,10 @@ +import * as Movement from './movement.js'; +export class GrabOp { + constructor(entity, rect) { + this.entity = entity; + this.rect = rect; + } + operation(change) { + return Movement.calculate(this.rect, change); + } +} diff --git a/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/highcontrast.css b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/highcontrast.css new file mode 100644 index 0000000..2cc85c2 --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/highcontrast.css @@ -0,0 +1,61 @@ +.pop-shell-active-hint { + border-style: solid; + border-color: #FBB86C; + border-radius: var(--active-hint-border-radius, 5px); + box-shadow: inset 0 0 0 1px rgba(24, 23, 23, 0) +} + +.pop-shell-overlay { + background-color: rgba(53, 132, 228, 0.3); +} + +.pop-shell-border-normal { + border-width: 3px; +} + +.pop-shell-border-maximize { + border-width: 3px; +} + +.pop-shell-search-element:select{ + background: #fff; + border-radius: 5px; + color: #000; +} + +.pop-shell-search-icon { + margin-right: 10px; +} + +.pop-shell-search-cat { + margin-right: 10px; +} + +.pop-shell-search-element { + padding-left: 10px; + padding-right: 2px; + padding-top: 6px; + padding-bottom: 6px; +} + +.pop-shell-tab { + border: 1px solid #333; + color: #000; + padding: 0 1em; +} + +.pop-shell-tab-active { + background: #FBB86C; +} + +.pop-shell-tab-inactive { + background: #9B8E8A; +} + +.pop-shell-tab-urgent { + background: #D00; +} + +.pop-shell-entry:indeterminate { + font-style: italic +} diff --git a/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/icons/pop-shell-auto-off-symbolic.svg b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/icons/pop-shell-auto-off-symbolic.svg new file mode 100644 index 0000000..ff4616b --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/icons/pop-shell-auto-off-symbolic.svg @@ -0,0 +1,163 @@ + + + + + + + + + image/svg+xml + + Pop Symbolic Icon Theme + + + + + + + + + + + + + Pop Symbolic Icon Theme + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/icons/pop-shell-auto-on-symbolic.svg b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/icons/pop-shell-auto-on-symbolic.svg new file mode 100644 index 0000000..d57bd0d --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/icons/pop-shell-auto-on-symbolic.svg @@ -0,0 +1,161 @@ + + + + + + + image/svg+xml + + Pop Symbolic Icon Theme + + + + + + + + + + + + + Pop Symbolic Icon Theme + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/keybindings.js b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/keybindings.js new file mode 100644 index 0000000..c601142 --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/keybindings.js @@ -0,0 +1,50 @@ +import { wm } from 'resource:///org/gnome/shell/ui/main.js'; +import Shell from 'gi://Shell'; +import Meta from 'gi://Meta'; +export class Keybindings { + constructor(ext) { + this.ext = ext; + this.global = { + 'activate-launcher': () => ext.window_search.open(ext), + 'tile-enter': () => ext.tiler.enter(ext), + }; + this.window_focus = { + 'focus-left': () => ext.focus_left(), + 'focus-down': () => ext.focus_down(), + 'focus-up': () => ext.focus_up(), + 'focus-right': () => ext.focus_right(), + 'tile-orientation': () => { + const win = ext.focus_window(); + if (win && ext.auto_tiler) { + ext.auto_tiler.toggle_orientation(ext, win); + ext.register_fn(() => win.activate(true)); + } + }, + 'toggle-floating': () => ext.auto_tiler?.toggle_floating(ext), + 'toggle-tiling': () => ext.toggle_tiling(), + 'toggle-stacking-global': () => ext.auto_tiler?.toggle_stacking(ext), + 'tile-move-left-global': () => ext.tiler.move_left(ext, ext.focus_window()?.entity), + 'tile-move-down-global': () => ext.tiler.move_down(ext, ext.focus_window()?.entity), + 'tile-move-up-global': () => ext.tiler.move_up(ext, ext.focus_window()?.entity), + 'tile-move-right-global': () => ext.tiler.move_right(ext, ext.focus_window()?.entity), + 'pop-monitor-left': () => ext.move_monitor(Meta.DisplayDirection.LEFT), + 'pop-monitor-right': () => ext.move_monitor(Meta.DisplayDirection.RIGHT), + 'pop-monitor-up': () => ext.move_monitor(Meta.DisplayDirection.UP), + 'pop-monitor-down': () => ext.move_monitor(Meta.DisplayDirection.DOWN), + 'pop-workspace-up': () => ext.move_workspace(Meta.DisplayDirection.UP), + 'pop-workspace-down': () => ext.move_workspace(Meta.DisplayDirection.DOWN), + }; + } + enable(keybindings) { + for (const name in keybindings) { + wm.addKeybinding(name, this.ext.settings.ext, Meta.KeyBindingFlags.NONE, Shell.ActionMode.NORMAL, keybindings[name]); + } + return this; + } + disable(keybindings) { + for (const name in keybindings) { + wm.removeKeybinding(name); + } + return this; + } +} diff --git a/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/launcher.js b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/launcher.js new file mode 100644 index 0000000..c89fafb --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/launcher.js @@ -0,0 +1,289 @@ +import * as search from './search.js'; +import * as utils from './utils.js'; +import * as arena from './arena.js'; +import * as log from './log.js'; +import * as service from './launcher_service.js'; +import * as context from './context.js'; +import Clutter from 'gi://Clutter'; +import GLib from 'gi://GLib'; +import Meta from 'gi://Meta'; +import Gio from 'gi://Gio'; +import Shell from 'gi://Shell'; +import St from 'gi://St'; +const app_sys = Shell.AppSystem.get_default(); +const Clipboard = St.Clipboard.get_default(); +const CLIPBOARD_TYPE = St.ClipboardType.CLIPBOARD; +export class Launcher extends search.Search { + constructor(ext) { + super(); + this.options = new Map(); + this.options_array = new Array(); + this.windows = new arena.Arena(); + this.service = null; + this.append_id = null; + this.active_menu = null; + this.opened = false; + this.ext = ext; + this.dialog.dialogLayout._dialog.y_align = Clutter.ActorAlign.START; + this.dialog.dialogLayout._dialog.x_align = Clutter.ActorAlign.START; + this.dialog.dialogLayout.y = 48; + this.cancel = () => { + ext.overlay.visible = false; + this.stop_services(ext); + this.opened = false; + }; + this.search = (pat) => { + if (this.service !== null) { + this.service.query(pat); + } + }; + this.select = (id) => { + ext.overlay.visible = false; + if (id >= this.options.size) + return; + const option = this.options_array[id]; + if (option && option.result.window) { + const win = this.ext.windows.get(option.result.window); + if (!win) + return; + if (win.workspace_id() == ext.active_workspace()) { + const { x, y, width, height } = win.rect(); + ext.overlay.x = x; + ext.overlay.y = y; + ext.overlay.width = width; + ext.overlay.height = height; + ext.overlay.visible = true; + } + } + }; + this.activate_id = (id) => { + ext.overlay.visible = false; + const selected = this.options_array[id]; + if (selected) { + this.service?.activate(selected.result.id); + } + }; + this.complete = () => { + const option = this.options_array[this.active_id]; + if (option) { + this.service?.complete(option.result.id); + } + }; + this.quit = (id) => { + const option = this.options_array[id]; + if (option) { + this.service?.quit(option.result.id); + } + }; + this.copy = (id) => { + const option = this.options_array[id]; + if (!option) + return; + if (option.result.description) { + Clipboard.set_text(CLIPBOARD_TYPE, option.result.description); + } + else if (option.result.name) { + Clipboard.set_text(CLIPBOARD_TYPE, option.result.name); + } + }; + } + on_response(response) { + if ('Close' === response) { + this.close(); + } + else if ('Update' in response) { + this.clear(); + if (this.append_id !== null) { + GLib.source_remove(this.append_id); + this.append_id = null; + } + if (response.Update.length === 0) { + this.cleanup(); + return; + } + this.append_id = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => { + const item = response.Update.shift(); + if (item) { + try { + const button = new search.SearchOption(item.name, item.description, item.category_icon ? item.category_icon : null, item.icon ? item.icon : null, this.icon_size(), null, null); + const menu = context.addMenu(button.widget, (menu) => { + if (this.active_menu) { + this.active_menu.actor.hide(); + } + this.active_menu = menu; + this.service?.context(item.id); + }); + this.append_search_option(button); + const result = { result: item, menu }; + this.options.set(item.id, result); + this.options_array.push(result); + } + catch (error) { + log.error(`failed to create SearchOption: ${error}`); + } + } + if (response.Update.length === 0) { + this.append_id = null; + return false; + } + return true; + }); + } + else if ('Fill' in response) { + this.set_text(response.Fill); + } + else if ('DesktopEntry' in response) { + this.launch_desktop_entry(response.DesktopEntry); + this.close(); + } + else if ('Context' in response) { + const { id, options } = response.Context; + const option = this.options.get(id); + if (option) { + option.menu.removeAll(); + for (const opt of options) { + context.addContext(option.menu, opt.name, () => { + this.service?.activate_context(id, opt.id); + }); + option.menu.toggle(); + } + } + else { + log.error(`did not find id: ${id}`); + } + } + else { + log.error(`unknown response: ${JSON.stringify(response)}`); + } + } + clear() { + this.options.clear(); + this.options_array.splice(0); + super.clear(); + } + launch_desktop_app(app, path) { + try { + app.launch([], null); + } + catch (why) { + log.error(`${path}: could not launch by app info: ${why}`); + } + } + launch_desktop_entry(entry) { + const basename = (name) => { + return name.substr(name.indexOf('/applications/') + 14).replace('/', '-'); + }; + const desktop_entry_id = basename(entry.path); + const gpuPref = entry.gpu_preference === 'Default' ? Shell.AppLaunchGpu.DEFAULT : Shell.AppLaunchGpu.DISCRETE; + log.debug(`launching desktop entry: ${desktop_entry_id}`); + let app = app_sys.lookup_desktop_wmclass(desktop_entry_id); + if (!app) { + app = app_sys.lookup_app(desktop_entry_id); + } + if (!app) { + log.error(`GNOME Shell cannot find desktop entry for ${desktop_entry_id}`); + log.error(`pop-launcher will use Gio.DesktopAppInfo instead`); + const dapp = Gio.DesktopAppInfo.new_from_filename(entry.path); + if (!dapp) { + log.error(`could not find desktop entry for ${entry.path}`); + return; + } + this.launch_desktop_app(dapp, entry.path); + return; + } + const info = app.get_app_info(); + if (!info) { + log.error(`cannot find app info for ${desktop_entry_id}`); + return; + } + try { + app.launch(0, -1, gpuPref); + } + catch (why) { + log.error(`failed to launch application: ${why}`); + return; + } + if (info.get_executable() === 'gnome-control-center') { + app = app_sys.lookup_app('gnome-control-center.desktop'); + if (!app) + return; + app.activate(); + const window = app.get_windows()[0]; + if (window) { + window.get_workspace().activate_with_focus(window, global.get_current_time()); + return; + } + } + } + list_workspace(ext) { + for (const window of ext.tab_list(Meta.TabList.NORMAL, null)) { + this.windows.insert(window); + } + } + load_desktop_files() { + log.warn('pop-shell: deprecated function called (launcher::load_desktop_files)'); + } + locate_by_app_info(info) { + const workspace = this.ext.active_workspace(); + const exec_info = info.get_string('Exec'); + const exec = exec_info?.split(' ').shift()?.split('/').pop(); + if (exec) { + for (const window of this.ext.tab_list(Meta.TabList.NORMAL, null)) { + if (window.meta.get_workspace().index() !== workspace) + continue; + const pid = window.meta.get_pid(); + if (pid !== -1) { + try { + let f = Gio.File.new_for_path(`/proc/${pid}/cmdline`); + const [, bytes] = f.load_contents(null); + const output = imports.byteArray.toString(bytes); + const cmd = output.split(' ').shift()?.split('/').pop(); + if (cmd === exec) + return window; + } + catch (_) { } + } + } + } + return null; + } + open(ext) { + ext.tiler.exit(ext); + if (this.opened) + return; + if (!ext.settings.fullscreen_launcher() && ext.focus_window()?.meta.is_fullscreen()) + return; + this.opened = true; + const active_monitor = ext.active_monitor(); + const mon_work_area = ext.monitor_work_area(active_monitor); + const mon_area = ext.monitor_area(active_monitor); + const mon_width = mon_area ? mon_area.width : mon_work_area.width; + super._open(global.get_current_time(), false); + if (!this.dialog.visible) { + this.clear(); + this.cancel(); + this.close(); + return; + } + super.cleanup(); + this.start_services(); + this.search(''); + this.dialog.dialogLayout.x = mon_width / 2 - this.dialog.dialogLayout.width / 2; + let height = mon_work_area.height >= 900 ? mon_work_area.height / 2 : mon_work_area.height / 3.5; + this.dialog.dialogLayout.y = height - this.dialog.dialogLayout.height / 2; + } + start_services() { + if (this.service === null) { + log.debug('starting pop-launcher service'); + const ipc = utils.async_process_ipc(['pop-launcher']); + this.service = ipc ? new service.LauncherService(ipc, (resp) => this.on_response(resp)) : null; + } + } + stop_services(_ext) { + if (this.service !== null) { + log.info(`stopping pop-launcher services`); + this.service.exit(); + this.service = null; + } + } +} diff --git a/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/launcher_service.js b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/launcher_service.js new file mode 100644 index 0000000..87d130a --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/launcher_service.js @@ -0,0 +1,76 @@ +import * as log from './log.js'; +import Gio from 'gi://Gio'; +import GLib from 'gi://GLib'; +const { byteArray } = imports; +export class LauncherService { + constructor(service, callback) { + this.service = service; + const generator = (stdout, res) => { + try { + const [bytes] = stdout.read_line_finish(res); + if (bytes) { + const string = byteArray.toString(bytes); + callback(JSON.parse(string)); + this.service.stdout.read_line_async(0, this.service.cancellable, generator); + } + } + catch (why) { + if (why.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) { + return; + } + log.error(`failed to read response from launcher service: ${why}`); + } + }; + this.service.stdout.read_line_async(0, this.service.cancellable, generator); + } + activate(id) { + this.send({ Activate: id }); + } + activate_context(id, context) { + this.send({ ActivateContext: { id, context } }); + } + complete(id) { + this.send({ Complete: id }); + } + context(id) { + this.send({ Context: id }); + } + exit() { + this.send('Exit'); + this.service.cancellable.cancel(); + const service = this.service; + GLib.timeout_add(GLib.PRIORITY_DEFAULT, 100, () => { + if (service.stdout.has_pending() || service.stdin.has_pending()) + return true; + const close_stream = (stream) => { + try { + stream.close(null); + } + catch (why) { + log.error(`failed to close pop-launcher stream: ${why}`); + } + }; + close_stream(service.stdin); + close_stream(service.stdin); + return false; + }); + } + query(search) { + this.send({ Search: search }); + } + quit(id) { + this.send({ Quit: id }); + } + select(id) { + this.send({ Select: id }); + } + send(object) { + const message = JSON.stringify(object); + try { + this.service.stdin.write_all(message + '\n', null); + } + catch (why) { + log.error(`failed to send request to pop-launcher: ${why}`); + } + } +} diff --git a/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/lib.js b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/lib.js new file mode 100644 index 0000000..b867822 --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/lib.js @@ -0,0 +1,91 @@ +import * as log from './log.js'; +import * as rectangle from './rectangle.js'; +import Meta from 'gi://Meta'; +import St from 'gi://St'; +export var Orientation; +(function (Orientation) { + Orientation[Orientation["HORIZONTAL"] = 0] = "HORIZONTAL"; + Orientation[Orientation["VERTICAL"] = 1] = "VERTICAL"; +})(Orientation || (Orientation = {})); +export function nth_rev(array, nth) { + return array[array.length - nth - 1]; +} +export function ok(input, func) { + return input ? func(input) : null; +} +export function ok_or_else(input, ok_func, or_func) { + return input ? ok_func(input) : or_func(); +} +export function or_else(input, func) { + return input ? input : func(); +} +export function bench(name, callback) { + const start = new Date().getMilliseconds(); + const value = callback(); + const end = new Date().getMilliseconds(); + log.info(`bench ${name}: ${end - start} ms elapsed`); + return value; +} +export function current_monitor() { + return rectangle.Rectangle.from_meta(global.display.get_monitor_geometry(global.display.get_current_monitor())); +} +export function cursor_rect() { + let [x, y] = global.get_pointer(); + return new rectangle.Rectangle([x, y, 1, 1]); +} +export function dbg(value) { + log.debug(String(value)); + return value; +} +export function* get_children(actor) { + let nth = 0; + let children = actor.get_n_children(); + while (nth < children) { + const child = actor.get_child_at_index(nth); + if (child) + yield child; + nth += 1; + } +} +export function join(iterator, next_func, between_func) { + ok(iterator.next().value, (first) => { + next_func(first); + for (const item of iterator) { + between_func(); + next_func(item); + } + }); +} +export function is_keyboard_op(op) { + const window_flag_keyboard = Meta.GrabOp.KEYBOARD_MOVING & ~Meta.GrabOp.WINDOW_BASE; + return (op & window_flag_keyboard) != 0; +} +export function is_resize_op(op) { + const window_dir_mask = (Meta.GrabOp.RESIZING_N | Meta.GrabOp.RESIZING_E | Meta.GrabOp.RESIZING_S | Meta.GrabOp.RESIZING_W) & + ~Meta.GrabOp.WINDOW_BASE; + return ((op & window_dir_mask) != 0 || + (op & Meta.GrabOp.KEYBOARD_RESIZING_UNKNOWN) == Meta.GrabOp.KEYBOARD_RESIZING_UNKNOWN); +} +export function is_move_op(op) { + return !is_resize_op(op); +} +export function orientation_as_str(value) { + return value == 0 ? 'Orientation::Horizontal' : 'Orientation::Vertical'; +} +export function recursive_remove_children(actor) { + for (const child of get_children(actor)) { + recursive_remove_children(child); + } + actor.remove_all_children(); +} +export function round_increment(value, increment) { + return Math.round(value / increment) * increment; +} +export function round_to(n, digits) { + let m = Math.pow(10, digits); + n = parseFloat((n * m).toFixed(11)); + return Math.round(n) / m; +} +export function separator() { + return new St.BoxLayout({ styleClass: 'pop-shell-separator', x_expand: true }); +} diff --git a/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/light.css b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/light.css new file mode 100644 index 0000000..7e95c48 --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/light.css @@ -0,0 +1,57 @@ +.pop-shell-active-hint { + border-style: solid; + border-color: #FFAD00; + border-radius: var(--active-hint-border-radius, 5px); + box-shadow: inset 0 0 0 1px rgba(200, 200, 200, 0); +} + +.pop-shell-overlay { + background-color: rgba(53, 132, 228, 0.3); +} + +.pop-shell-border-normal { + border-width: 3px; +} + +.pop-shell-border-maximize { + border-width: 3px; +} + +.pop-shell-search-element:select{ + background: rgba(0, 0, 0, .1); + border-radius: 5px; + color: #393634; +} + +.pop-shell-search-icon { + margin-right: 10px; +} + +.pop-shell-search-cat { + margin-right: 10px; +} + +.pop-shell-search-element { + padding-left: 10px; + padding-right: 2px; + padding-top: 6px; + padding-bottom: 6px; +} + +.pop-shell-tab { + border: 1px solid #333; + color: #000; + padding: 0 1em; +} + +.pop-shell-tab-active { + background: #FFAD00; +} + +.pop-shell-tab-inactive { + background: #9B8E8A; +} + +.pop-shell-tab-urgent { + background: #D00; +} diff --git a/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/log.js b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/log.js new file mode 100644 index 0000000..26bdeec --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/log.js @@ -0,0 +1,32 @@ +export var LOG_LEVELS; +(function (LOG_LEVELS) { + LOG_LEVELS[LOG_LEVELS["OFF"] = 0] = "OFF"; + LOG_LEVELS[LOG_LEVELS["ERROR"] = 1] = "ERROR"; + LOG_LEVELS[LOG_LEVELS["WARN"] = 2] = "WARN"; + LOG_LEVELS[LOG_LEVELS["INFO"] = 3] = "INFO"; + LOG_LEVELS[LOG_LEVELS["DEBUG"] = 4] = "DEBUG"; +})(LOG_LEVELS || (LOG_LEVELS = {})); +export function log_level() { + let settings = globalThis.popShellExtension.getSettings(); + let log_level = settings.get_uint('log-level'); + return log_level; +} +export function log(text) { + globalThis.log('pop-shell: ' + text); +} +export function error(text) { + if (log_level() > LOG_LEVELS.OFF) + log('[ERROR] ' + text); +} +export function warn(text) { + if (log_level() > LOG_LEVELS.ERROR) + log('[WARN] ' + text); +} +export function info(text) { + if (log_level() > LOG_LEVELS.WARN) + log('[INFO] ' + text); +} +export function debug(text) { + if (log_level() > LOG_LEVELS.INFO) + log('[DEBUG] ' + text); +} diff --git a/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/metadata.json b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/metadata.json new file mode 100644 index 0000000..c59146e --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/metadata.json @@ -0,0 +1,11 @@ +{ + "name": "Pop Shell", + "description": "Pop Shell", + "version": 2, + "uuid": "pop-shell@system76.com", + "settings-schema": "org.gnome.shell.extensions.pop-shell", + "shell-version": [ + "45", + "46" + ] +} diff --git a/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/movement.js b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/movement.js new file mode 100644 index 0000000..86f74bf --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/movement.js @@ -0,0 +1,53 @@ +export var Movement; +(function (Movement) { + Movement[Movement["NONE"] = 0] = "NONE"; + Movement[Movement["MOVED"] = 1] = "MOVED"; + Movement[Movement["GROW"] = 2] = "GROW"; + Movement[Movement["SHRINK"] = 4] = "SHRINK"; + Movement[Movement["LEFT"] = 8] = "LEFT"; + Movement[Movement["UP"] = 16] = "UP"; + Movement[Movement["RIGHT"] = 32] = "RIGHT"; + Movement[Movement["DOWN"] = 64] = "DOWN"; +})(Movement || (Movement = {})); +export function calculate(from, change) { + const xpos = from.x == change.x; + const ypos = from.y == change.y; + if (xpos && ypos) { + if (from.width == change.width) { + if (from.height == change.width) { + return Movement.NONE; + } + else if (from.height < change.height) { + return Movement.GROW | Movement.DOWN; + } + else { + return Movement.SHRINK | Movement.UP; + } + } + else if (from.width < change.width) { + return Movement.GROW | Movement.RIGHT; + } + else { + return Movement.SHRINK | Movement.LEFT; + } + } + else if (xpos) { + if (from.height < change.height) { + return Movement.GROW | Movement.UP; + } + else { + return Movement.SHRINK | Movement.DOWN; + } + } + else if (ypos) { + if (from.width < change.width) { + return Movement.GROW | Movement.LEFT; + } + else { + return Movement.SHRINK | Movement.RIGHT; + } + } + else { + return Movement.MOVED; + } +} diff --git a/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/node.js b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/node.js new file mode 100644 index 0000000..4e04b80 --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/node.js @@ -0,0 +1,165 @@ +import * as Ecs from './ecs.js'; +export var NodeKind; +(function (NodeKind) { + NodeKind[NodeKind["FORK"] = 1] = "FORK"; + NodeKind[NodeKind["WINDOW"] = 2] = "WINDOW"; + NodeKind[NodeKind["STACK"] = 3] = "STACK"; +})(NodeKind || (NodeKind = {})); +function node_variant_as_string(value) { + return value == NodeKind.FORK ? 'NodeVariant::Fork' : 'NodeVariant::Window'; +} +function stack_detach(node, stack, idx) { + node.entities.splice(idx, 1); + stack.remove_by_pos(idx); +} +export function stack_find(node, entity) { + let idx = 0; + while (idx < node.entities.length) { + if (Ecs.entity_eq(entity, node.entities[idx])) { + return idx; + } + idx += 1; + } + return null; +} +export function stack_move_left(ext, forest, node, entity) { + const stack = forest.stacks.get(node.idx); + if (!stack) + return false; + let idx = 0; + for (const cmp of node.entities) { + if (Ecs.entity_eq(cmp, entity)) { + if (idx === 0) { + stack_detach(node, stack, 0); + return false; + } + else { + stack_swap(node, idx - 1, idx); + stack.active_id -= 1; + ext.auto_tiler?.update_stack(ext, node); + return true; + } + } + idx += 1; + } + return false; +} +export function stack_move_right(ext, forest, node, entity) { + const stack = forest.stacks.get(node.idx); + if (!stack) + return false; + let moved = false; + let idx = 0; + const max = node.entities.length - 1; + for (const cmp of node.entities) { + if (Ecs.entity_eq(cmp, entity)) { + if (idx === max) { + stack_detach(node, stack, idx); + moved = false; + } + else { + stack_swap(node, idx + 1, idx); + stack.active_id += 1; + ext.auto_tiler?.update_stack(ext, node); + moved = true; + } + break; + } + idx += 1; + } + return moved; +} +export function stack_replace(ext, node, window) { + if (!ext.auto_tiler) + return; + const stack = ext.auto_tiler.forest.stacks.get(node.idx); + if (!stack) + return; + stack.replace(window); +} +export function stack_remove(forest, node, entity) { + const stack = forest.stacks.get(node.idx); + if (!stack) + return null; + const idx = stack.remove_tab(entity); + if (idx !== null) + node.entities.splice(idx, 1); + return idx; +} +function stack_swap(node, from, to) { + const tmp = node.entities[from]; + node.entities[from] = node.entities[to]; + node.entities[to] = tmp; +} +export class Node { + constructor(inner) { + this.inner = inner; + } + static fork(entity) { + return new Node({ kind: NodeKind.FORK, entity }); + } + static window(entity) { + return new Node({ kind: NodeKind.WINDOW, entity }); + } + static stacked(window, idx) { + const node = new Node({ + kind: NodeKind.STACK, + entities: [window], + idx, + rect: null, + }); + return node; + } + display(fmt) { + fmt += `{\n kind: ${node_variant_as_string(this.inner.kind)},\n `; + switch (this.inner.kind) { + case 1: + case 2: + fmt += `entity: (${this.inner.entity})\n }`; + return fmt; + case 3: + fmt += `entities: ${this.inner.entities}\n }`; + return fmt; + } + } + is_in_stack(entity) { + if (this.inner.kind === 3) { + for (const compare of this.inner.entities) { + if (Ecs.entity_eq(entity, compare)) + return true; + } + } + return false; + } + is_fork(entity) { + return this.inner.kind === 1 && Ecs.entity_eq(this.inner.entity, entity); + } + is_window(entity) { + return this.inner.kind === 2 && Ecs.entity_eq(this.inner.entity, entity); + } + measure(tiler, ext, parent, area, record) { + switch (this.inner.kind) { + case 1: + const fork = tiler.forks.get(this.inner.entity); + if (fork) { + record; + fork.measure(tiler, ext, area, record); + } + break; + case 2: + record(this.inner.entity, parent, area.clone()); + break; + case 3: + const size = ext.dpi * 4; + this.inner.rect = area.clone(); + this.inner.rect.y += size * 6; + this.inner.rect.height -= size * 6; + for (const entity of this.inner.entities) { + record(entity, parent, this.inner.rect); + } + if (ext.auto_tiler) { + ext.auto_tiler.forest.stack_updates.push([this.inner, parent]); + } + } + } +} diff --git a/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/once_cell.js b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/once_cell.js new file mode 100644 index 0000000..ba86b08 --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/once_cell.js @@ -0,0 +1,9 @@ +export class OnceCell { + constructor() { } + get_or_init(callback) { + if (this.value === undefined) { + this.value = callback(); + } + return this.value; + } +} diff --git a/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/panel_settings.js b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/panel_settings.js new file mode 100644 index 0000000..632b1f5 --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/panel_settings.js @@ -0,0 +1,258 @@ +import * as Utils from './utils.js'; +import Clutter from 'gi://Clutter'; +import Gio from 'gi://Gio'; +import St from 'gi://St'; +import { PopupBaseMenuItem, PopupMenuItem, PopupSwitchMenuItem, PopupSeparatorMenuItem, } from 'resource:///org/gnome/shell/ui/popupMenu.js'; +import { Button } from 'resource:///org/gnome/shell/ui/panelMenu.js'; +import GLib from 'gi://GLib'; +import { spawn } from 'resource:///org/gnome/shell/misc/util.js'; +import { get_current_path } from './paths.js'; +export class Indicator { + constructor(ext) { + this.button = new Button(0.0, _('Pop Shell Settings')); + const path = get_current_path(); + ext.button = this.button; + ext.button_gio_icon_auto_on = Gio.icon_new_for_string(`${path}/icons/pop-shell-auto-on-symbolic.svg`); + ext.button_gio_icon_auto_off = Gio.icon_new_for_string(`${path}/icons/pop-shell-auto-off-symbolic.svg`); + let button_icon_auto_on = new St.Icon({ + gicon: ext.button_gio_icon_auto_on, + style_class: 'system-status-icon', + }); + let button_icon_auto_off = new St.Icon({ + gicon: ext.button_gio_icon_auto_off, + style_class: 'system-status-icon', + }); + if (ext.settings.tile_by_default()) { + this.button.icon = button_icon_auto_on; + } + else { + this.button.icon = button_icon_auto_off; + } + this.button.add_child(this.button.icon); + let bm = this.button.menu; + this.toggle_tiled = tiled(ext); + this.toggle_active = toggle(_('Show Active Hint'), ext.settings.active_hint(), (toggle) => { + ext.settings.set_active_hint(toggle.state); + }); + this.entry_gaps = number_entry(_('Gaps'), ext.settings.gap_inner(), (value) => { + ext.settings.set_gap_inner(value); + ext.settings.set_gap_outer(value); + }); + this.border_radius = number_entry(_('Active Border Radius'), { + value: ext.settings.active_hint_border_radius(), + min: 0, + max: 30, + }, (value) => { + ext.settings.set_active_hint_border_radius(value); + }); + bm.addMenuItem(this.toggle_tiled); + bm.addMenuItem(floating_window_exceptions(ext, bm)); + bm.addMenuItem(menu_separator('')); + bm.addMenuItem(shortcuts(bm)); + bm.addMenuItem(settings_button(bm)); + bm.addMenuItem(menu_separator('')); + if (!Utils.is_wayland()) { + this.toggle_titles = show_title(ext); + bm.addMenuItem(this.toggle_titles); + } + bm.addMenuItem(this.toggle_active); + bm.addMenuItem(this.border_radius); + bm.addMenuItem(color_selector(ext, bm)); + bm.addMenuItem(this.entry_gaps); + } + destroy() { + this.button.destroy(); + } +} +function menu_separator(text) { + return new PopupSeparatorMenuItem(text); +} +function settings_button(menu) { + let item = new PopupMenuItem(_('View All')); + item.connect('activate', () => { + let path = GLib.find_program_in_path('pop-shell-shortcuts'); + if (path) { + spawn([path]); + } + else { + spawn(['xdg-open', 'https://support.system76.com/articles/pop-keyboard-shortcuts/']); + } + menu.close(); + }); + item.label.get_clutter_text().set_margin_left(12); + return item; +} +function floating_window_exceptions(ext, menu) { + let label = new St.Label({ text: 'Floating Window Exceptions' }); + label.set_x_expand(true); + let icon = new St.Icon({ icon_name: 'go-next-symbolic', icon_size: 16 }); + let widget = new St.BoxLayout({ vertical: false }); + widget.add_child(label); + widget.add_child(icon); + widget.set_x_expand(true); + let base = new PopupBaseMenuItem(); + base.add_child(widget); + base.connect('activate', () => { + ext.exception_dialog(); + GLib.timeout_add(GLib.PRIORITY_LOW, 300, () => { + menu.close(); + return false; + }); + }); + return base; +} +function shortcuts(menu) { + let layout_manager = new Clutter.GridLayout({ orientation: Clutter.Orientation.HORIZONTAL }); + let widget = new St.Widget({ layout_manager, x_expand: true }); + let item = new PopupBaseMenuItem(); + item.add_child(widget); + item.connect('activate', () => { + let path = GLib.find_program_in_path('pop-shell-shortcuts'); + if (path) { + spawn([path]); + } + else { + spawn(['xdg-open', 'https://support.system76.com/articles/pop-keyboard-shortcuts/']); + } + menu.close(); + }); + function create_label(text) { + return new St.Label({ text }); + } + function create_shortcut_label(text) { + let label = create_label(text); + label.set_x_align(Clutter.ActorAlign.END); + return label; + } + layout_manager.set_row_spacing(12); + layout_manager.set_column_spacing(30); + layout_manager.attach(create_label(_('Shortcuts')), 0, 0, 2, 1); + let launcher_shortcut = _('Super + /'); + [ + [_('Launcher'), launcher_shortcut], + [_('Navigate Windows'), _('Super + Arrow Keys')], + [_('Toggle Tiling'), _('Super + Y')], + ].forEach((section, idx) => { + let key = create_label(section[0]); + key.get_clutter_text().set_margin_left(12); + let val = create_shortcut_label(section[1]); + layout_manager.attach(key, 0, idx + 1, 1, 1); + layout_manager.attach(val, 1, idx + 1, 1, 1); + }); + return item; +} +function clamp(input, min = 0, max = 128) { + return Math.min(Math.max(min, input), max); +} +function number_entry(label, valueOrOptions, callback) { + let value = valueOrOptions, min, max; + if (typeof valueOrOptions !== 'number') + ({ value, min, max } = valueOrOptions); + const entry = new St.Entry({ + text: String(value), + input_purpose: Clutter.InputContentPurpose.NUMBER, + x_align: Clutter.ActorAlign.CENTER, + x_expand: false, + }); + entry.set_style('width: 5em'); + entry.connect('button-release-event', () => { + return true; + }); + const text = entry.clutter_text; + text.set_max_length(2); + entry.connect('key-release-event', (_, event) => { + const symbol = event.get_key_symbol(); + const number = symbol == 65293 + ? parse_number(text.text) + : symbol == 65361 + ? clamp(parse_number(text.text) - 1, min, max) + : symbol == 65363 + ? clamp(parse_number(text.text) + 1, min, max) + : null; + if (number !== null) { + text.set_text(String(number)); + } + }); + const create_icon = (icon_name) => { + return new St.Icon({ icon_name, icon_size: 16 }); + }; + entry.set_primary_icon(create_icon('value-decrease')); + entry.connect('primary-icon-clicked', () => { + text.set_text(String(clamp(parseInt(text.get_text()) - 1, min, max))); + }); + entry.set_secondary_icon(create_icon('value-increase')); + entry.connect('secondary-icon-clicked', () => { + text.set_text(String(clamp(parseInt(text.get_text()) + 1, min, max))); + }); + text.connect('text-changed', () => { + const input = text.get_text(); + let parsed = parseInt(input); + if (isNaN(parsed)) { + text.set_text(input.substr(0, input.length - 1)); + parsed = 0; + } + callback(parsed); + }); + const item = new PopupMenuItem(label); + item.label.get_clutter_text().set_x_expand(true); + item.label.set_y_align(Clutter.ActorAlign.CENTER); + item.add_child(entry); + return item; +} +function parse_number(text) { + let number = parseInt(text, 10); + if (isNaN(number)) { + number = 0; + } + return number; +} +function show_title(ext) { + const t = toggle(_('Show Window Titles'), ext.settings.show_title(), (toggle) => { + ext.settings.set_show_title(toggle.state); + }); + return t; +} +function toggle(desc, active, connect) { + let toggle = new PopupSwitchMenuItem(desc, active); + toggle.label.set_y_align(Clutter.ActorAlign.CENTER); + toggle.connect('toggled', () => { + connect(toggle); + return true; + }); + return toggle; +} +function tiled(ext) { + let t = toggle(_('Tile Windows'), null != ext.auto_tiler, () => ext.toggle_tiling()); + return t; +} +function color_selector(ext, menu) { + let color_selector_item = new PopupMenuItem('Active Hint Color'); + let color_button = new St.Button(); + let settings = ext.settings; + let selected_color = settings.hint_color_rgba(); + color_button.label = ' '; + color_button.set_style(`background-color: ${selected_color}; border: 2px solid lightgray; border-radius: 2px`); + settings.ext.connect('changed', (_, key) => { + if (key === 'hint-color-rgba') { + let color_value = settings.hint_color_rgba(); + color_button.set_style(`background-color: ${color_value}; border: 2px solid lightgray; border-radius: 2px`); + } + }); + color_button.set_x_align(Clutter.ActorAlign.END); + color_button.set_x_expand(false); + color_selector_item.label.get_clutter_text().set_x_expand(true); + color_selector_item.label.set_y_align(Clutter.ActorAlign.CENTER); + color_selector_item.add_child(color_button); + color_button.connect('button-press-event', () => { + let path = get_current_path() + '/color_dialog/main.js'; + let resp = GLib.spawn_command_line_async(`gjs --module ${path}`); + if (!resp) { + return null; + } + GLib.timeout_add(GLib.PRIORITY_LOW, 300, () => { + menu.close(); + return false; + }); + }); + return color_selector_item; +} diff --git a/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/paths.js b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/paths.js new file mode 100644 index 0000000..4bd5c93 --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/paths.js @@ -0,0 +1,3 @@ +export function get_current_path() { + return import.meta.url.split('://')[1].split('/').slice(0, -1).join('/'); +} diff --git a/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/prefs.js b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/prefs.js new file mode 100644 index 0000000..7be1458 --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/prefs.js @@ -0,0 +1,197 @@ +import Gtk from 'gi://Gtk'; +import Gio from 'gi://Gio'; +const Settings = Gio.Settings; +import { ExtensionPreferences } from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js'; +import * as settings from './settings.js'; +import * as log from './log.js'; +import * as focus from './focus.js'; +export default class PopShellPreferences extends ExtensionPreferences { + getPreferencesWidget() { + globalThis.popShellExtension = this; + let dialog = settings_dialog_new(); + if (dialog.show_all) { + dialog.show_all(); + } + else { + dialog.show(); + } + log.debug(JSON.stringify(dialog)); + return dialog; + } +} +function settings_dialog_new() { + let [app, grid] = settings_dialog_view(); + let ext = new settings.ExtensionSettings(); + app.window_titles.set_active(ext.show_title()); + app.window_titles.connect('state-set', (_widget, state) => { + ext.set_show_title(state); + Settings.sync(); + }); + app.snap_to_grid.set_active(ext.snap_to_grid()); + app.snap_to_grid.connect('state-set', (_widget, state) => { + ext.set_snap_to_grid(state); + Settings.sync(); + }); + app.smart_gaps.set_active(ext.smart_gaps()); + app.smart_gaps.connect('state-set', (_widget, state) => { + ext.set_smart_gaps(state); + Settings.sync(); + }); + app.outer_gap.set_text(String(ext.gap_outer())); + app.outer_gap.connect('activate', (widget) => { + let parsed = parseInt(widget.get_text().trim()); + if (!isNaN(parsed)) { + ext.set_gap_outer(parsed); + Settings.sync(); + } + }); + app.inner_gap.set_text(String(ext.gap_inner())); + app.inner_gap.connect('activate', (widget) => { + let parsed = parseInt(widget.get_text().trim()); + if (!isNaN(parsed)) { + ext.set_gap_inner(parsed); + Settings.sync(); + } + }); + app.log_level.set_active(ext.log_level()); + app.log_level.connect('changed', () => { + let active_id = app.log_level.get_active_id(); + ext.set_log_level(active_id); + }); + app.show_skip_taskbar.set_active(ext.show_skiptaskbar()); + app.show_skip_taskbar.connect('state-set', (_widget, state) => { + ext.set_show_skiptaskbar(state); + Settings.sync(); + }); + app.mouse_cursor_follows_active_window.set_active(ext.mouse_cursor_follows_active_window()); + app.mouse_cursor_follows_active_window.connect('state-set', (_widget, state) => { + ext.set_mouse_cursor_follows_active_window(state); + Settings.sync(); + }); + app.mouse_cursor_focus_position.set_active(ext.mouse_cursor_focus_location()); + app.mouse_cursor_focus_position.connect('changed', () => { + let active_id = app.mouse_cursor_focus_position.get_active_id(); + ext.set_mouse_cursor_focus_location(active_id); + }); + app.fullscreen_launcher.set_active(ext.fullscreen_launcher()); + app.fullscreen_launcher.connect('state-set', (_widget, state) => { + ext.set_fullscreen_launcher(state); + Settings.sync(); + }); + app.stacking_with_mouse.set_active(ext.stacking_with_mouse()); + app.stacking_with_mouse.connect('state-set', (_widget, state) => { + ext.set_stacking_with_mouse(state); + Settings.sync(); + }); + return grid; +} +function settings_dialog_view() { + const grid = new Gtk.Grid({ + column_spacing: 12, + row_spacing: 12, + margin_start: 10, + margin_end: 10, + margin_bottom: 10, + margin_top: 10, + }); + const win_label = new Gtk.Label({ + label: 'Show Window Titles', + xalign: 0.0, + hexpand: true, + }); + const snap_label = new Gtk.Label({ + label: 'Snap to Grid (Floating Mode)', + xalign: 0.0, + }); + const smart_label = new Gtk.Label({ + label: 'Smart Gaps', + xalign: 0.0, + }); + const show_skip_taskbar_label = new Gtk.Label({ + label: 'Show Minimize to Tray Windows', + xalign: 0.0, + }); + const mouse_cursor_follows_active_window_label = new Gtk.Label({ + label: 'Mouse Cursor Follows Active Window', + xalign: 0.0, + }); + const fullscreen_launcher_label = new Gtk.Label({ + label: 'Allow launcher over fullscreen window', + xalign: 0.0, + }); + const stacking_with_mouse = new Gtk.Label({ + label: 'Allow stacking with mouse', + xalign: 0.0, + }); + const [inner_gap, outer_gap] = gaps_section(grid, 9); + const settings = { + inner_gap, + outer_gap, + fullscreen_launcher: new Gtk.Switch({ halign: Gtk.Align.END }), + stacking_with_mouse: new Gtk.Switch({ halign: Gtk.Align.END }), + smart_gaps: new Gtk.Switch({ halign: Gtk.Align.END }), + snap_to_grid: new Gtk.Switch({ halign: Gtk.Align.END }), + window_titles: new Gtk.Switch({ halign: Gtk.Align.END }), + show_skip_taskbar: new Gtk.Switch({ halign: Gtk.Align.END }), + mouse_cursor_follows_active_window: new Gtk.Switch({ halign: Gtk.Align.END }), + mouse_cursor_focus_position: build_combo(grid, 7, focus.FocusPosition, 'Mouse Cursor Focus Position'), + log_level: build_combo(grid, 8, log.LOG_LEVELS, 'Log Level'), + }; + grid.attach(win_label, 0, 0, 1, 1); + grid.attach(settings.window_titles, 1, 0, 1, 1); + grid.attach(snap_label, 0, 1, 1, 1); + grid.attach(settings.snap_to_grid, 1, 1, 1, 1); + grid.attach(smart_label, 0, 2, 1, 1); + grid.attach(settings.smart_gaps, 1, 2, 1, 1); + grid.attach(fullscreen_launcher_label, 0, 3, 1, 1); + grid.attach(settings.fullscreen_launcher, 1, 3, 1, 1); + grid.attach(stacking_with_mouse, 0, 4, 1, 1); + grid.attach(settings.stacking_with_mouse, 1, 4, 1, 1); + grid.attach(show_skip_taskbar_label, 0, 5, 1, 1); + grid.attach(settings.show_skip_taskbar, 1, 5, 1, 1); + grid.attach(mouse_cursor_follows_active_window_label, 0, 6, 1, 1); + grid.attach(settings.mouse_cursor_follows_active_window, 1, 6, 1, 1); + return [settings, grid]; +} +function gaps_section(grid, top) { + let outer_label = new Gtk.Label({ + label: 'Outer', + xalign: 0.0, + margin_start: 24, + }); + let outer_entry = number_entry(); + let inner_label = new Gtk.Label({ + label: 'Inner', + xalign: 0.0, + margin_start: 24, + }); + let inner_entry = number_entry(); + let section_label = new Gtk.Label({ + label: 'Gaps', + xalign: 0.0, + }); + grid.attach(section_label, 0, top, 1, 1); + grid.attach(outer_label, 0, top + 1, 1, 1); + grid.attach(outer_entry, 1, top + 1, 1, 1); + grid.attach(inner_label, 0, top + 2, 1, 1); + grid.attach(inner_entry, 1, top + 2, 1, 1); + return [inner_entry, outer_entry]; +} +function number_entry() { + return new Gtk.Entry({ input_purpose: Gtk.InputPurpose.NUMBER }); +} +function build_combo(grid, top_index, iter_enum, label) { + let label_ = new Gtk.Label({ + label: label, + halign: Gtk.Align.START, + }); + grid.attach(label_, 0, top_index, 1, 1); + let combo = new Gtk.ComboBoxText(); + for (const [index, key] of Object.keys(iter_enum).entries()) { + if (typeof iter_enum[key] == 'string') { + combo.append(`${index}`, iter_enum[key]); + } + } + grid.attach(combo, 1, top_index, 1, 1); + return combo; +} diff --git a/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/rectangle.js b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/rectangle.js new file mode 100644 index 0000000..a775968 --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/rectangle.js @@ -0,0 +1,81 @@ +export class Rectangle { + constructor(array) { + this.array = array; + } + static from_meta(meta) { + return new Rectangle([meta.x, meta.y, meta.width, meta.height]); + } + get x() { + return this.array[0]; + } + set x(x) { + this.array[0] = x; + } + get y() { + return this.array[1]; + } + set y(y) { + this.array[1] = y; + } + get width() { + return this.array[2]; + } + set width(width) { + this.array[2] = width; + } + get height() { + return this.array[3]; + } + set height(height) { + this.array[3] = height; + } + apply(other) { + this.x += other.x; + this.y += other.y; + this.width += other.width; + this.height += other.height; + return this; + } + clamp(other) { + this.x = Math.max(other.x, this.x); + this.y = Math.max(other.y, this.y); + let tend = this.x + this.width, oend = other.x + other.width; + if (tend > oend) { + this.width = oend - this.x; + } + tend = this.y + this.height; + oend = other.y + other.height; + if (tend > oend) { + this.height = oend - this.y; + } + } + clone() { + return new Rectangle([this.array[0], this.array[1], this.array[2], this.array[3]]); + } + contains(other) { + return (this.x <= other.x && + this.y <= other.y && + this.x + this.width >= other.x + other.width && + this.y + this.height >= other.y + other.height); + } + diff(other) { + return new Rectangle([ + other.x - this.x, + other.y - this.y, + other.width - this.width, + other.height - this.height, + ]); + } + eq(other) { + return this.x == other.x && this.y == other.y && this.width == other.width && this.height == other.height; + } + fmt() { + return `Rect(${[this.x, this.y, this.width, this.height]})`; + } + intersects(other) { + return (this.x < other.x + other.width && + this.x + this.width > other.x && + this.y < other.y + other.height && + this.y + this.height > other.y); + } +} diff --git a/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/result.js b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/result.js new file mode 100644 index 0000000..8e74c51 --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/result.js @@ -0,0 +1,8 @@ +export const OK = 1; +export const ERR = 2; +export function Ok(value) { + return { kind: 1, value: value }; +} +export function Err(value) { + return { kind: 2, value: value }; +} diff --git a/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/scheduler.js b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/scheduler.js new file mode 100644 index 0000000..9295d7c --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/scheduler.js @@ -0,0 +1,36 @@ +import * as log from './log.js'; +import Gio from 'gi://Gio'; +const SchedulerInterface = '\ + \ + \ + \ + \ + \ +'; +const SchedulerProxy = Gio.DBusProxy.makeProxyWrapper(SchedulerInterface); +const SchedProxy = new SchedulerProxy(Gio.DBus.system, 'com.system76.Scheduler', '/com/system76/Scheduler'); +let foreground = 0; +let failed = false; +export function setForeground(win) { + if (failed) + return; + const pid = win.get_pid(); + if (pid) { + if (foreground === pid) + return; + foreground = pid; + try { + SchedProxy.SetForegroundProcessRemote(pid, (_result, error, _fds) => { + if (error !== null) + errorHandler(error); + }); + } + catch (error) { + errorHandler(error); + } + } +} +function errorHandler(error) { + log.warn(`system76-scheduler may not be installed and running: ${error}`); + failed = true; +} diff --git a/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/search.js b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/search.js new file mode 100644 index 0000000..a49a826 --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/search.js @@ -0,0 +1,400 @@ +import * as Lib from './lib.js'; +import * as rect from './rectangle.js'; +import GLib from 'gi://GLib'; +import Clutter from 'gi://Clutter'; +import Gio from 'gi://Gio'; +import Pango from 'gi://Pango'; +import Shell from 'gi://Shell'; +import St from 'gi://St'; +import Gdk from 'gi://Gdk'; +import * as Main from 'resource:///org/gnome/shell/ui/main.js'; +import { ModalDialog } from 'resource:///org/gnome/shell/ui/modalDialog.js'; +import * as Util from 'resource:///org/gnome/shell/misc/util.js'; +const { overview, wm } = Main; +import { Overview } from 'resource:///org/gnome/shell/ui/overview.js'; +let overview_toggle = null; +export class Search { + constructor() { + this.dialog = new ModalDialog({ + styleClass: 'pop-shell-search modal-dialog', + destroyOnClose: false, + shellReactive: true, + shouldFadeIn: false, + shouldFadeOut: false, + }); + this.children_to_abandon = null; + this.last_trigger = 0; + this.grab_handle = null; + this.activate_id = () => { }; + this.cancel = () => { }; + this.complete = () => { }; + this.search = () => { }; + this.select = () => { }; + this.quit = () => { }; + this.copy = () => { }; + this.active_id = 0; + this.widgets = []; + this.entry = new St.Entry({ + style_class: 'pop-shell-entry', + can_focus: true, + x_expand: true, + }); + this.entry.set_hint_text(" Type to search apps, or type '?' for more options."); + this.text = this.entry.get_clutter_text(); + this.text.set_use_markup(true); + this.dialog.setInitialKeyFocus(this.text); + let text_changed = null; + this.text.connect('activate', () => this.activate_id(this.active_id)); + this.text.connect('text-changed', (entry) => { + if (text_changed !== null) + GLib.source_remove(text_changed); + const text = entry.get_text().trim(); + const update = () => { + this.clear(); + this.search(text); + }; + if (text.length === 0) { + update(); + return; + } + text_changed = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 0, () => { + text_changed = null; + update(); + return false; + }); + }); + this.text.connect('key-press-event', (_, event) => { + const key = Gdk.keyval_name(Gdk.keyval_to_upper(event.get_key_symbol())); + const ctrlKey = Boolean(event.get_state() & Clutter.ModifierType.CONTROL_MASK); + const is_down = () => { + return key === 'Down' || (ctrlKey && key === 'J') || (ctrlKey && key === 'N'); + }; + const is_up = () => { + return key === 'Up' || key === 'ISO_Left_Tab' || (ctrlKey && key === 'K') || (ctrlKey && key === 'P'); + }; + const up_arrow = () => { + if (0 < this.active_id) { + this.select_id(this.active_id - 1); + } + else if (this.active_id == 0) { + this.select_id(this.widgets.length - 1); + } + }; + const down_arrow = () => { + if (this.active_id + 1 < this.widgets.length) { + this.select_id(this.active_id + 1); + } + else if (this.active_id + 1 == this.widgets.length) { + this.select_id(0); + } + }; + if (event.get_flags() != Clutter.EventFlags.NONE) { + const now = global.get_current_time(); + if (now - this.last_trigger < 100) { + return; + } + this.last_trigger = now; + if (is_up()) { + up_arrow(); + this.select(this.active_id); + } + else if (is_down()) { + down_arrow(); + this.select(this.active_id); + } + return; + } + this.last_trigger = global.get_current_time(); + if (key === 'Escape') { + this.reset(); + this.close(); + this.cancel(); + return; + } + else if (key === 'Tab') { + this.complete(); + return; + } + if (is_up()) { + up_arrow(); + } + else if (is_down()) { + down_arrow(); + } + else if (ctrlKey && key === '1') { + this.activate_id(0); + return; + } + else if (ctrlKey && key === '2') { + this.activate_id(1); + return; + } + else if (ctrlKey && key === '3') { + this.activate_id(2); + return; + } + else if (ctrlKey && key === '4') { + this.activate_id(3); + return; + } + else if (ctrlKey && key === '5') { + this.activate_id(4); + return; + } + else if (ctrlKey && key === '6') { + this.activate_id(5); + return; + } + else if (ctrlKey && key === '7') { + this.activate_id(6); + return; + } + else if (ctrlKey && key === '8') { + this.activate_id(7); + return; + } + else if (ctrlKey && key === '9') { + this.activate_id(8); + return; + } + else if (ctrlKey && key === 'Q') { + this.quit(this.active_id); + return; + } + else if (key === 'Copy' || (ctrlKey && (key === 'C' || key === 'Insert'))) { + if (this.text.get_selection()) { + return; + } + else { + this.copy(this.active_id); + this.close(); + this.cancel(); + } + } + this.select(this.active_id); + }); + this.list = new St.BoxLayout({ + styleClass: 'pop-shell-search-list', + vertical: true, + }); + const scroller = new St.ScrollView(); + scroller.add_child(this.list); + this.dialog.contentLayout.add_child(this.entry); + this.dialog.contentLayout.add_child(scroller); + this.scroller = scroller; + this.dialog.contentLayout.width = Math.max(Lib.current_monitor().width / 4, 640); + this.dialog.connect('event', (_actor, event) => { + const { width, height } = this.dialog.dialogLayout._dialog; + const { x, y } = this.dialog.dialogLayout; + const area = new rect.Rectangle([x, y, width, height]); + const close = this.dialog.visible && + event.type() == Clutter.EventType.BUTTON_PRESS && + !area.contains(Lib.cursor_rect()); + if (close) { + this.reset(); + this.close(); + this.cancel(); + } + return Clutter.EVENT_PROPAGATE; + }); + this.dialog.connect('closed', () => this.cancel()); + } + cleanup() { + if (this.children_to_abandon) { + for (const child of this.children_to_abandon) { + child.destroy(); + } + this.children_to_abandon = null; + } + } + clear() { + this.children_to_abandon = this.list.get_children(); + this.widgets = []; + this.active_id = 0; + } + close() { + try { + if (this.grab_handle !== null) { + Main.popModal(this.grab_handle); + this.grab_handle = null; + } + } + catch (error) { + } + this.reset(); + this.remove_injections(); + this.dialog.close(global.get_current_time()); + wm.allowKeybinding('overlay-key', Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW); + } + _open(timestamp, on_primary) { + this.grab_handle = Main.pushModal(this.dialog.dialogLayout); + this.dialog.open(timestamp, on_primary); + wm.allowKeybinding('overlay-key', Shell.ActionMode.ALL); + overview_toggle = Overview.prototype['toggle']; + Overview.prototype['toggle'] = () => { + if (this.dialog.is_visible()) { + this.reset(); + this.close(); + this.cancel(); + } + else { + this.remove_injections(); + overview.toggle(); + } + }; + } + get_text() { + return this.text.get_text(); + } + icon_size() { + return 34; + } + get list_max() { + return 7; + } + reset() { + this.clear(); + this.text.set_text(null); + } + show() { + this.dialog.show_all(); + this.clear(); + this.entry.grab_key_focus(); + } + highlight_selected() { + const widget = this.widgets[this.active_id]; + if (widget) { + widget.add_style_pseudo_class('select'); + try { + Util.ensureActorVisibleInScrollView(this.scroller, widget); + } + catch (_error) { } + } + } + select_id(id) { + this.unselect(); + this.active_id = id; + this.highlight_selected(); + } + unselect() { + this.widgets[this.active_id]?.remove_style_pseudo_class('select'); + } + append_search_option(option) { + const id = this.widgets.length; + if (id !== 0) { + this.list.add_child(Lib.separator()); + } + const { widget, shortcut } = option; + if (id < 9) { + shortcut.set_text(`Ctrl + ${id + 1}`); + shortcut.show(); + } + else { + shortcut.hide(); + } + let initial_cursor = Lib.cursor_rect(); + widget.connect('clicked', () => this.activate_id(id)); + widget.connect('notify::hover', () => { + const { x, y } = Lib.cursor_rect(); + if (x === initial_cursor.x && y === initial_cursor.y) + return; + this.select_id(id); + this.select(id); + }); + this.widgets.push(widget); + this.list.add_child(widget); + this.cleanup(); + this.list.show(); + const vscroll = this.scroller.get_vscroll_bar(); + if (this.scroller.vscrollbar_visible) { + vscroll.show(); + } + else { + vscroll.hide(); + } + if (id === 0) { + GLib.idle_add(GLib.PRIORITY_DEFAULT, () => { + this.highlight_selected(); + this.select(0); + return false; + }); + } + } + set_text(text) { + this.text.set_text(text); + } + remove_injections() { + if (overview_toggle !== null) { + Overview.prototype['toggle'] = overview_toggle; + overview_toggle = null; + } + } +} +export class SearchOption { + constructor(title, description, category_icon, icon, icon_size, exec, keywords) { + this.shortcut = new St.Label({ + text: '', + y_align: Clutter.ActorAlign.CENTER, + style: 'padding-left: 6px;padding-right: 6px', + }); + this.title = title; + this.description = description; + this.exec = exec; + this.keywords = keywords; + const layout = new St.BoxLayout({ x_expand: true }); + attach_icon(layout, category_icon, icon_size / 2); + const label = new St.Label({ text: title }); + label.clutter_text.set_ellipsize(Pango.EllipsizeMode.END); + attach_icon(layout, icon, icon_size); + const info_box = new St.BoxLayout({ + y_align: Clutter.ActorAlign.CENTER, + vertical: true, + x_expand: true, + }); + info_box.add_child(label); + if (description) { + info_box.add_child(new St.Label({ text: description, style: 'font-size: small' })); + } + layout.add_child(info_box); + layout.add_child(this.shortcut); + this.widget = new St.Button({ style_class: 'pop-shell-search-element' }); + this.widget.add_child(layout); + } +} +function attach_icon(layout, icon, icon_size) { + if (icon) { + const generated = generate_icon(icon, icon_size); + if (generated) { + generated.set_y_align(Clutter.ActorAlign.CENTER); + layout.add_child(generated); + } + } +} +function generate_icon(icon, icon_size) { + let app_icon = null; + if ('Name' in icon) { + const file = Gio.File.new_for_path(icon.Name); + if (file.query_exists(null)) { + app_icon = new St.Icon({ + gicon: Gio.icon_new_for_string(icon.Name), + icon_size, + }); + } + else { + app_icon = new St.Icon({ + icon_name: icon.Name, + icon_size, + }); + } + } + else if ('Mime' in icon) { + app_icon = new St.Icon({ + gicon: Gio.content_type_get_icon(icon.Mime), + icon_size, + }); + } + if (app_icon) { + app_icon.style_class = 'pop-shell-search-icon'; + } + return app_icon; +} diff --git a/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/settings.js b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/settings.js new file mode 100644 index 0000000..353dcf8 --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/settings.js @@ -0,0 +1,185 @@ +import Gio from 'gi://Gio'; +import Gdk from 'gi://Gdk'; +import { get_current_path } from './paths.js'; +const DARK = ['dark', 'adapta', 'plata', 'dracula']; +function settings_new_id(schema_id) { + try { + return new Gio.Settings({ schema_id }); + } + catch (why) { + if (schema_id !== 'org.gnome.shell.extensions.user-theme') { + } + return null; + } +} +function settings_new_schema(schema) { + const GioSSS = Gio.SettingsSchemaSource; + const schemaDir = Gio.File.new_for_path(get_current_path()).get_child('schemas'); + let schemaSource = schemaDir.query_exists(null) + ? GioSSS.new_from_directory(schemaDir.get_path(), GioSSS.get_default(), false) + : GioSSS.get_default(); + const schemaObj = schemaSource.lookup(schema, true); + if (!schemaObj) { + throw new Error('Schema ' + schema + ' could not be found for extension pop-shell' + '. Please check your installation.'); + } + return new Gio.Settings({ settings_schema: schemaObj }); +} +const ACTIVE_HINT = 'active-hint'; +const ACTIVE_HINT_BORDER_RADIUS = 'active-hint-border-radius'; +const STACKING_WITH_MOUSE = 'stacking-with-mouse'; +const COLUMN_SIZE = 'column-size'; +const EDGE_TILING = 'edge-tiling'; +const FULLSCREEN_LAUNCHER = 'fullscreen-launcher'; +const GAP_INNER = 'gap-inner'; +const GAP_OUTER = 'gap-outer'; +const ROW_SIZE = 'row-size'; +const SHOW_TITLE = 'show-title'; +const SMART_GAPS = 'smart-gaps'; +const SNAP_TO_GRID = 'snap-to-grid'; +const TILE_BY_DEFAULT = 'tile-by-default'; +const HINT_COLOR_RGBA = 'hint-color-rgba'; +const DEFAULT_RGBA_COLOR = 'rgba(251, 184, 108, 1)'; +const LOG_LEVEL = 'log-level'; +const SHOW_SKIPTASKBAR = 'show-skip-taskbar'; +const MOUSE_CURSOR_FOLLOWS_ACTIVE_WINDOW = 'mouse-cursor-follows-active-window'; +const MOUSE_CURSOR_FOCUS_LOCATION = 'mouse-cursor-focus-location'; +export class ExtensionSettings { + constructor() { + this.ext = settings_new_schema('org.gnome.shell.extensions.pop-shell'); + this.int = settings_new_id('org.gnome.desktop.interface'); + this.mutter = settings_new_id('org.gnome.mutter'); + this.shell = settings_new_id('org.gnome.shell.extensions.user-theme'); + } + active_hint() { + return this.ext.get_boolean(ACTIVE_HINT); + } + active_hint_border_radius() { + return this.ext.get_uint(ACTIVE_HINT_BORDER_RADIUS); + } + stacking_with_mouse() { + return this.ext.get_boolean(STACKING_WITH_MOUSE); + } + column_size() { + return this.ext.get_uint(COLUMN_SIZE); + } + dynamic_workspaces() { + return this.mutter ? this.mutter.get_boolean('dynamic-workspaces') : false; + } + fullscreen_launcher() { + return this.ext.get_boolean(FULLSCREEN_LAUNCHER); + } + gap_inner() { + return this.ext.get_uint(GAP_INNER); + } + gap_outer() { + return this.ext.get_uint(GAP_OUTER); + } + hint_color_rgba() { + let rgba = this.ext.get_string(HINT_COLOR_RGBA); + let valid_color = new Gdk.RGBA().parse(rgba); + if (!valid_color) { + return DEFAULT_RGBA_COLOR; + } + return rgba; + } + theme() { + return this.shell ? this.shell.get_string('name') : this.int ? this.int.get_string('gtk-theme') : 'Adwaita'; + } + is_dark() { + const theme = this.theme().toLowerCase(); + return DARK.some((dark) => theme.includes(dark)); + } + is_high_contrast() { + return this.theme().toLowerCase() === 'highcontrast'; + } + row_size() { + return this.ext.get_uint(ROW_SIZE); + } + show_title() { + return this.ext.get_boolean(SHOW_TITLE); + } + smart_gaps() { + return this.ext.get_boolean(SMART_GAPS); + } + snap_to_grid() { + return this.ext.get_boolean(SNAP_TO_GRID); + } + tile_by_default() { + return this.ext.get_boolean(TILE_BY_DEFAULT); + } + workspaces_only_on_primary() { + return this.mutter ? this.mutter.get_boolean('workspaces-only-on-primary') : false; + } + log_level() { + return this.ext.get_uint(LOG_LEVEL); + } + show_skiptaskbar() { + return this.ext.get_boolean(SHOW_SKIPTASKBAR); + } + mouse_cursor_follows_active_window() { + return this.ext.get_boolean(MOUSE_CURSOR_FOLLOWS_ACTIVE_WINDOW); + } + mouse_cursor_focus_location() { + return this.ext.get_uint(MOUSE_CURSOR_FOCUS_LOCATION); + } + set_active_hint(set) { + this.ext.set_boolean(ACTIVE_HINT, set); + } + set_active_hint_border_radius(set) { + this.ext.set_uint(ACTIVE_HINT_BORDER_RADIUS, set); + } + set_stacking_with_mouse(set) { + this.ext.set_boolean(STACKING_WITH_MOUSE, set); + } + set_column_size(size) { + this.ext.set_uint(COLUMN_SIZE, size); + } + set_edge_tiling(enable) { + this.mutter?.set_boolean(EDGE_TILING, enable); + } + set_fullscreen_launcher(enable) { + this.ext.set_boolean(FULLSCREEN_LAUNCHER, enable); + } + set_gap_inner(gap) { + this.ext.set_uint(GAP_INNER, gap); + } + set_gap_outer(gap) { + this.ext.set_uint(GAP_OUTER, gap); + } + set_hint_color_rgba(rgba) { + let valid_color = new Gdk.RGBA().parse(rgba); + if (valid_color) { + this.ext.set_string(HINT_COLOR_RGBA, rgba); + } + else { + this.ext.set_string(HINT_COLOR_RGBA, DEFAULT_RGBA_COLOR); + } + } + set_row_size(size) { + this.ext.set_uint(ROW_SIZE, size); + } + set_show_title(set) { + this.ext.set_boolean(SHOW_TITLE, set); + } + set_smart_gaps(set) { + this.ext.set_boolean(SMART_GAPS, set); + } + set_snap_to_grid(set) { + this.ext.set_boolean(SNAP_TO_GRID, set); + } + set_tile_by_default(set) { + this.ext.set_boolean(TILE_BY_DEFAULT, set); + } + set_log_level(set) { + this.ext.set_uint(LOG_LEVEL, set); + } + set_show_skiptaskbar(set) { + this.ext.set_boolean(SHOW_SKIPTASKBAR, set); + } + set_mouse_cursor_follows_active_window(set) { + this.ext.set_boolean(MOUSE_CURSOR_FOLLOWS_ACTIVE_WINDOW, set); + } + set_mouse_cursor_focus_location(set) { + this.ext.set_uint(MOUSE_CURSOR_FOCUS_LOCATION, set); + } +} diff --git a/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/shell.js b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/shell.js new file mode 100644 index 0000000..64118f0 --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/shell.js @@ -0,0 +1,4 @@ +export function monitor_neighbor_index(which, direction) { + const neighbor = global.display.get_monitor_neighbor_index(which, direction); + return neighbor < 0 ? null : neighbor; +} diff --git a/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/shortcut_overlay.js b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/shortcut_overlay.js new file mode 100644 index 0000000..7785c6e --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/shortcut_overlay.js @@ -0,0 +1,93 @@ +import GObject from 'gi://GObject'; +import St from 'gi://St'; +import * as Lib from './lib.js'; +const { separator } = Lib; +export class Shortcut { + constructor(description) { + this.description = description; + this.bindings = new Array(); + } + add(binding) { + this.bindings.push(binding); + return this; + } +} +export class Section { + constructor(header, shortcuts) { + this.header = header; + this.shortcuts = shortcuts; + } +} +export class Column { + constructor(sections) { + this.sections = sections; + } +} +export var ShortcutOverlay = GObject.registerClass(class ShortcutOverlay extends St.BoxLayout { + constructor() { + super(); + this.title = ''; + this.columns = new Array(); + } + _init(title, columns) { + super.init({ + styleClass: 'pop-shell-shortcuts', + destroyOnClose: false, + shellReactive: true, + shouldFadeIn: true, + shouldFadeOut: true, + }); + let columns_layout = new St.BoxLayout({ + styleClass: 'pop-shell-shortcuts-columns', + horizontal: true, + }); + for (const column of columns) { + let column_layout = new St.BoxLayout({ + styleClass: 'pop-shell-shortcuts-column', + }); + for (const section of column.sections) { + column_layout.add(this.gen_section(section)); + } + columns_layout.add(column_layout); + } + this.add(new St.Label({ + styleClass: 'pop-shell-shortcuts-title', + text: title, + })); + this.add(columns_layout); + } + gen_combination(combination) { + let layout = new St.BoxLayout({ + styleClass: 'pop-shell-binding', + horizontal: true, + }); + for (const key of combination) { + layout.add(St.Label({ text: key })); + } + return layout; + } + gen_section(section) { + let layout = new St.BoxLayout({ + styleclass: 'pop-shell-section', + }); + layout.add(new St.Label({ + styleClass: 'pop-shell-section-header', + text: section.header, + })); + for (const subsection of section.shortcuts) { + layout.add(separator()); + layout.add(this.gen_shortcut(subsection)); + } + return layout; + } + gen_shortcut(shortcut) { + let layout = new St.BoxLayout({ + styleClass: 'pop-shell-shortcut', + horizontal: true, + }); + layout.add(new St.Label({ + text: shortcut.description, + })); + return layout; + } +}); diff --git a/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/stack.js b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/stack.js new file mode 100644 index 0000000..2fbebd5 --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/stack.js @@ -0,0 +1,549 @@ +import * as Ecs from './ecs.js'; +import * as a from './arena.js'; +import * as utils from './utils.js'; +const Arena = a.Arena; +import Clutter from 'gi://Clutter'; +import GObject from 'gi://GObject'; +import St from 'gi://St'; +const ACTIVE_TAB = 'pop-shell-tab pop-shell-tab-active'; +const INACTIVE_TAB = 'pop-shell-tab pop-shell-tab-inactive'; +const URGENT_TAB = 'pop-shell-tab pop-shell-tab-urgent'; +const INACTIVE_TAB_STYLE = '#9B8E8A'; +export var TAB_HEIGHT = 24; +function stack_widgets_new() { + let tabs = new St.BoxLayout({ + style_class: 'pop-shell-stack', + x_expand: true, + }); + tabs.get_layout_manager()?.set_homogeneous(true); + return { tabs }; +} +const ContainerButton = GObject.registerClass({ + Signals: { activate: {} }, +}, class ImageButton extends St.Button { + _init(icon) { + super._init({ + child: icon, + x_expand: true, + y_expand: true, + }); + } +}); +const TabButton = GObject.registerClass({ + Signals: { activate: {} }, +}, class TabButton extends St.Button { + _init(window) { + const icon = window.icon(window.ext, 24); + icon.set_x_align(Clutter.ActorAlign.START); + const label = new St.Label({ + y_expand: true, + x_align: Clutter.ActorAlign.START, + y_align: Clutter.ActorAlign.CENTER, + style: 'padding-left: 8px', + }); + label.text = window.title(); + const container = new St.BoxLayout({ + y_expand: true, + y_align: Clutter.ActorAlign.CENTER, + }); + const close_button = new ContainerButton(new St.Icon({ + icon_name: 'window-close-symbolic', + icon_size: 24, + y_align: Clutter.ActorAlign.CENTER, + })); + close_button.connect('clicked', () => { + window.meta.delete(global.get_current_time()); + }); + close_button.set_x_align(Clutter.ActorAlign.END); + close_button.set_y_align(Clutter.ActorAlign.CENTER); + container.add_child(icon); + container.add_child(label); + container.add_child(close_button); + super._init({ + child: container, + x_expand: true, + y_expand: true, + y_align: Clutter.ActorAlign.CENTER, + }); + this._title = label; + } + set_title(text) { + if (this._title) { + this._title.text = text; + } + } +}); +export class Stack { + constructor(ext, active, workspace, monitor) { + this.widgets = null; + this.active_id = 0; + this.prev_active = null; + this.prev_active_id = 0; + this.tabs = new Array(); + this.buttons = new Arena(); + this.tabs_height = TAB_HEIGHT; + this.stack_rect = { width: 0, height: 0, x: 0, y: 0 }; + this.active_signals = null; + this.rect = { width: 0, height: 0, x: 0, y: 0 }; + this.restacker = global.display.connect('restacked', () => this.restack()); + this.ext = ext; + this.active = active; + this.monitor = monitor; + this.workspace = workspace; + this.tabs_height = TAB_HEIGHT * this.ext.dpi; + this.widgets = stack_widgets_new(); + global.window_group.add_child(this.widgets.tabs); + this.reposition(); + this.tabs_destroy = this.widgets.tabs.connect('destroy', () => this.recreate_widgets()); + } + add(window) { + if (!this.widgets) + return; + const entity = window.entity; + const active = Ecs.entity_eq(entity, this.active); + const button = new TabButton(window); + const id = this.buttons.insert(button); + let tab = { active, entity, signals: [], button: id, button_signal: null }; + let comp = this.tabs.length; + this.bind_hint_events(tab); + this.tabs.push(tab); + this.watch_signals(comp, id, window); + this.widgets.tabs.add_child(button); + } + auto_activate() { + if (this.tabs.length === 0) + return null; + if (this.tabs.length <= this.active_id) { + this.active_id = this.tabs.length - 1; + } + const c = this.tabs[this.active_id]; + this.activate(c.entity); + return c.entity; + } + activate_prev() { + if (this.prev_active) { + this.activate(this.prev_active); + } + } + activate(entity) { + const permitted = this.permitted_to_show(); + if (this.widgets) + this.widgets.tabs.visible = permitted; + this.reset_visibility(permitted); + const win = this.ext.windows.get(entity); + if (!win) + return; + if (!Ecs.entity_eq(entity, this.active)) { + this.prev_active = this.active; + this.prev_active_id = this.active_id; + } + this.active_connect(win.meta, entity); + let id = 0; + for (const [idx, component] of this.tabs.entries()) { + let name; + this.window_exec(id, component.entity, (window) => { + const actor = window.meta.get_compositor_private(); + if (Ecs.entity_eq(entity, component.entity)) { + this.active_id = id; + component.active = true; + name = ACTIVE_TAB; + if (actor) + actor.show(); + } + else { + component.active = false; + name = INACTIVE_TAB; + if (actor) + actor.hide(); + } + let button = this.buttons.get(component.button); + if (button) { + button.set_style_class_name(name); + let tab_color = ''; + if (component.active) { + let settings = this.ext.settings; + let color_value = settings.hint_color_rgba(); + tab_color = `${color_value}; color: ${utils.is_dark(color_value) ? 'white' : 'black'}`; + } + else { + tab_color = `${INACTIVE_TAB_STYLE}`; + } + const tab_border_radius = this.get_tab_border_radius(idx); + button.set_style(`background: ${tab_color}; border-radius: ${tab_border_radius};`); + } + }); + id += 1; + } + this.reset_visibility(permitted); + } + get_tab_border_radius(idx) { + let result = `0px 0px 0px 0px`; + let radius = Math.max(0, this.ext.settings.active_hint_border_radius() - 4); + radius = Math.min(radius, Math.trunc(this.tabs_height / 2)); + if (this.tabs.length === 1) + result = `${radius}px`; + else if (idx === 0) + result = `${radius}px 0px 0px ${radius}px`; + else if (idx === this.tabs.length - 1) + result = `0px ${radius}px ${radius}px 0px`; + return result; + } + active_connect(window, active) { + this.active_disconnect(); + this.active = active; + this.active_reconnect(window); + } + active_reconnect(window) { + const on_window_changed = () => this.on_grab(() => { + const window = this.ext.windows.get(this.active); + if (window) { + this.update_positions(window.meta.get_frame_rect()); + this.window_changed(); + } + else { + this.active_disconnect(); + } + }); + this.active_signals = [ + window.connect('size-changed', on_window_changed), + window.connect('position-changed', on_window_changed), + ]; + } + active_disconnect() { + const active_meta = this.active_meta(); + if (this.active_signals && active_meta) { + for (const s of this.active_signals) + active_meta.disconnect(s); + } + this.active_signals = null; + } + active_meta() { + return this.ext.windows.get(this.active)?.meta; + } + bind_hint_events(tab) { + let settings = this.ext.settings; + let button = this.buttons.get(tab.button); + if (button) { + let change_id = settings.ext.connect('changed', (_, key) => { + if (key === 'hint-color-rgba') { + this.change_tab_color(tab); + } + return false; + }); + button.connect('destroy', () => { + settings.ext.disconnect(change_id); + }); + } + this.change_tab_color(tab); + } + change_tab_color(tab) { + let settings = this.ext.settings; + let button = this.buttons.get(tab.button); + if (button) { + let tab_color = ''; + if (Ecs.entity_eq(tab.entity, this.active)) { + let color_value = settings.hint_color_rgba(); + tab_color = `background: ${color_value}; color: ${utils.is_dark(color_value) ? 'white' : 'black'}`; + } + else { + tab_color = `background: ${INACTIVE_TAB_STYLE}`; + } + button.set_style(tab_color); + } + } + clear() { + this.active_disconnect(); + for (const c of this.tabs.splice(0)) + this.tab_disconnect(c); + this.widgets?.tabs.destroy_all_children(); + this.buttons.truncate(0); + } + tab_disconnect(c) { + const window = this.ext.windows.get(c.entity); + if (window) { + for (const s of c.signals) + window.meta.disconnect(s); + if (this.workspace === this.ext.active_workspace()) + window.meta.get_compositor_private()?.show(); + } + c.signals = []; + if (c.button_signal) { + const b = this.buttons.get(c.button); + if (b) { + b.disconnect(c.button_signal); + c.button_signal = null; + } + } + } + deactivate(w) { + for (const c of this.tabs) + if (Ecs.entity_eq(c.entity, w.entity)) { + this.tab_disconnect(c); + } + if (this.active_signals && Ecs.entity_eq(this.active, w.entity)) { + this.active_disconnect(); + } + } + destroy() { + global.display.disconnect(this.restacker); + this.active_disconnect(); + for (const c of this.tabs) { + this.tab_disconnect(c); + if (this.workspace === this.ext.active_workspace()) { + const win = this.ext.windows.get(c.entity); + if (win) { + win.meta.get_compositor_private()?.show(); + win.stack = null; + } + } + } + for (const b of this.buttons.values()) { + try { + b.destroy(); + } + catch (e) { } + } + if (this.widgets) { + const tabs = this.widgets.tabs; + this.widgets = null; + tabs.destroy(); + } + } + on_grab(or) { + if (this.ext.grab_op !== null) { + if (Ecs.entity_eq(this.ext.grab_op.entity, this.active)) { + if (this.widgets) { + const parent = this.widgets.tabs.get_parent(); + const actor = this.active_meta()?.get_compositor_private(); + if (actor && parent) { + parent.set_child_below_sibling(this.widgets.tabs, actor); + } + } + return; + } + } + or(); + } + recreate_widgets() { + if (this.widgets !== null) { + this.widgets.tabs.disconnect(this.tabs_destroy); + this.widgets = stack_widgets_new(); + global.window_group.add_child(this.widgets.tabs); + this.tabs_destroy = this.widgets.tabs.connect('destroy', () => this.recreate_widgets()); + this.active_disconnect(); + for (const c of this.tabs.splice(0)) { + this.tab_disconnect(c); + const window = this.ext.windows.get(c.entity); + if (window) + this.add(window); + } + this.update_positions(this.rect); + this.restack(); + const window = this.ext.windows.get(this.active); + if (!window) + return; + this.active_reconnect(window.meta); + } + } + remove_by_pos(idx) { + const c = this.tabs[idx]; + if (c) + this.remove_tab_component(c, idx); + } + remove_tab_component(c, idx) { + if (!this.widgets) + return; + this.tab_disconnect(c); + const b = this.buttons.get(c.button); + if (b) { + this.widgets.tabs.remove_child(b); + b.destroy(); + this.buttons.remove(c.button); + } + this.tabs.splice(idx, 1); + } + remove_tab(entity) { + if (!this.widgets) + return null; + if (this.prev_active && Ecs.entity_eq(entity, this.prev_active)) { + this.prev_active = null; + this.prev_active_id = 0; + } + let idx = 0; + for (const c of this.tabs) { + if (Ecs.entity_eq(c.entity, entity)) { + this.remove_tab_component(c, idx); + if (this.active_id > idx) { + this.active_id -= 1; + } + return idx; + } + idx += 1; + } + return null; + } + replace(window) { + if (!this.widgets) + return; + const c = this.tabs[this.active_id], actor = window.meta.get_compositor_private(); + if (c && actor) { + this.tab_disconnect(c); + if (Ecs.entity_eq(window.entity, this.active)) { + this.active_connect(window.meta, window.entity); + actor.show(); + } + else { + actor.hide(); + } + this.watch_signals(this.active_id, c.button, window); + this.buttons.get(c.button)?.set_title(window.title()); + this.activate(window.entity); + } + } + reposition() { + if (!this.widgets) + return; + const window = this.ext.windows.get(this.active); + if (!window) + return; + const actor = window.meta.get_compositor_private(); + if (!actor) { + this.active_disconnect(); + return; + } + actor.show(); + const parent = actor.get_parent(); + if (!parent) { + return; + } + const stack_parent = this.widgets.tabs.get_parent(); + if (stack_parent) { + stack_parent.remove_child(this.widgets.tabs); + } + parent.add_child(this.widgets.tabs); + if (!window.meta.is_fullscreen() && !window.is_maximized() && !this.ext.maximized_on_active_display()) { + parent.set_child_above_sibling(this.widgets.tabs, actor); + } + else { + parent.set_child_below_sibling(this.widgets.tabs, actor); + } + } + permitted_to_show(workspace) { + const active_workspace = workspace ?? global.workspace_manager.get_active_workspace_index(); + const primary = global.display.get_primary_monitor(); + const only_primary = this.ext.settings.workspaces_only_on_primary(); + return active_workspace === this.workspace || (only_primary && this.monitor != primary); + } + reset_visibility(permitted) { + let idx = 0; + for (const c of this.tabs) { + this.actor_exec(idx, c.entity, (actor) => { + if (permitted && this.active_id === idx) { + actor.show(); + return; + } + actor.hide(); + }); + idx += 1; + } + } + restack() { + this.on_grab(() => { + if (!this.widgets) + return; + const permitted = this.permitted_to_show(); + this.widgets.tabs.visible = permitted; + if (permitted) + this.reposition(); + this.reset_visibility(permitted); + }); + } + set_visible(visible) { + if (!this.widgets) + return; + this.widgets.tabs.visible = visible; + if (visible) { + this.widgets.tabs.show(); + } + else { + this.widgets.tabs.hide(); + } + } + update_positions(rect) { + if (!this.widgets) + return; + this.rect = rect; + this.tabs_height = TAB_HEIGHT * this.ext.dpi; + this.stack_rect = { + x: rect.x, + y: rect.y - this.tabs_height, + width: rect.width, + height: this.tabs_height + rect.height, + }; + this.widgets.tabs.x = rect.x; + this.widgets.tabs.y = this.stack_rect.y; + this.widgets.tabs.height = this.tabs_height; + this.widgets.tabs.width = rect.width; + } + watch_signals(comp, button, window) { + const entity = window.entity; + const widget = this.buttons.get(button); + if (!widget) + return; + const c = this.tabs[comp]; + if (c.button_signal) + widget.disconnect(c.button_signal); + c.button_signal = widget.connect('clicked', () => { + this.activate(entity); + this.window_exec(comp, entity, (window) => { + const actor = window.meta.get_compositor_private(); + if (actor) { + actor.show(); + window.activate(false); + this.reposition(); + for (const comp of this.tabs) { + this.buttons.get(comp.button)?.set_style_class_name(INACTIVE_TAB); + } + widget.set_style_class_name(ACTIVE_TAB); + } + }); + }); + if (this.tabs[comp].signals) { + for (const c of this.tabs[comp].signals) + window.meta.disconnect(c); + } + this.tabs[comp].signals = [ + window.meta.connect('notify::title', () => { + this.window_exec(comp, entity, (window) => { + this.buttons.get(button)?.set_title(window.title()); + }); + }), + window.meta.connect('notify::urgent', () => { + this.window_exec(comp, entity, (window) => { + if (!window.meta.has_focus()) { + this.buttons.get(button)?.set_style_class_name(URGENT_TAB); + } + }); + }), + ]; + } + window_changed() { + this.ext.show_border_on_focused(); + } + actor_exec(comp, entity, func) { + this.window_exec(comp, entity, (window) => { + func(window.meta.get_compositor_private()); + }); + } + window_exec(comp, entity, func) { + const window = this.ext.windows.get(entity); + if (window && window.actor_exists()) { + func(window); + } + else { + const tab = this.tabs[comp]; + if (tab) + this.tab_disconnect(tab); + } + } +} diff --git a/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/tags.js b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/tags.js new file mode 100644 index 0000000..cf97c1c --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/tags.js @@ -0,0 +1,4 @@ +export var Tiled = 0; +export var Floating = 1; +export var Blocked = 2; +export var ForceTile = 3; diff --git a/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/tiling.js b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/tiling.js new file mode 100644 index 0000000..d86f75a --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/tiling.js @@ -0,0 +1,710 @@ +import * as GrabOp from './grab_op.js'; +import * as Lib from './lib.js'; +import * as Log from './log.js'; +import * as Node from './node.js'; +import * as Rect from './rectangle.js'; +import * as Tags from './tags.js'; +import * as window from './window.js'; +import * as geom from './geom.js'; +import * as exec from './executor.js'; +import Meta from 'gi://Meta'; +import * as Main from 'resource:///org/gnome/shell/ui/main.js'; +const { ShellWindow } = window; +export var Direction; +(function (Direction) { + Direction[Direction["Left"] = 0] = "Left"; + Direction[Direction["Up"] = 1] = "Up"; + Direction[Direction["Right"] = 2] = "Right"; + Direction[Direction["Down"] = 3] = "Down"; +})(Direction || (Direction = {})); +export class Tiler { + constructor(ext) { + this.window = null; + this.moving = false; + this.resizing_window = false; + this.swap_window = null; + this.queue = new exec.ChannelExecutor(); + this.keybindings = { + 'management-orientation': () => this.toggle_orientation(ext), + 'tile-move-left': () => this.move_left(ext), + 'tile-move-down': () => this.move_down(ext), + 'tile-move-up': () => this.move_up(ext), + 'tile-move-right': () => this.move_right(ext), + 'tile-resize-left': () => this.resize(ext, Direction.Left), + 'tile-resize-down': () => this.resize(ext, Direction.Down), + 'tile-resize-up': () => this.resize(ext, Direction.Up), + 'tile-resize-right': () => this.resize(ext, Direction.Right), + 'tile-swap-left': () => this.swap_left(ext), + 'tile-swap-down': () => this.swap_down(ext), + 'tile-swap-up': () => this.swap_up(ext), + 'tile-swap-right': () => this.swap_right(ext), + 'tile-accept': () => this.accept(ext), + 'tile-reject': () => this.exit(ext), + 'toggle-stacking': () => this.toggle_stacking(ext), + }; + } + toggle_orientation(ext) { + const window = ext.focus_window(); + if (window && ext.auto_tiler) { + ext.auto_tiler.toggle_orientation(ext, window); + ext.register_fn(() => window.activate(true)); + } + } + toggle_stacking(ext) { + ext.auto_tiler?.toggle_stacking(ext); + const win = ext.focus_window(); + if (win) + this.overlay_watch(ext, win); + } + rect(ext, monitor) { + if (!ext.overlay.visible) + return null; + const columns = Math.floor(monitor.width / ext.column_size); + const rows = Math.floor(monitor.height / ext.row_size); + return monitor_rect(monitor, columns, rows); + } + change(overlay, rect, dx, dy, dw, dh) { + let changed = new Rect.Rectangle([ + overlay.x + dx * rect.width, + overlay.y + dy * rect.height, + overlay.width + dw * rect.width, + overlay.height + dh * rect.height, + ]); + changed.x = Lib.round_increment(changed.x - rect.x, rect.width) + rect.x; + changed.y = Lib.round_increment(changed.y - rect.y, rect.height) + rect.y; + changed.width = Lib.round_increment(changed.width, rect.width); + changed.height = Lib.round_increment(changed.height, rect.height); + if (changed.width < rect.width) { + changed.width = rect.width; + } + if (changed.height < rect.height) { + changed.height = rect.height; + } + let monitors = tile_monitors(changed); + if (monitors.length == 0) + return this; + let min_x = null; + let min_y = null; + let max_x = null; + let max_y = null; + for (const monitor of monitors) { + if (min_x === null || monitor.x < min_x) { + min_x = monitor.x; + } + if (min_y === null || monitor.y < min_y) { + min_y = monitor.y; + } + if (max_x === null || monitor.x + monitor.width > max_x) { + max_x = monitor.x + monitor.width; + } + if (max_y === null || monitor.y + monitor.height < max_y) { + max_y = monitor.y + monitor.height; + } + } + if (min_x === null || + min_y === null || + max_x === null || + max_y === null || + changed.x < min_x || + changed.x + changed.width > max_x || + changed.y < min_y || + changed.y + changed.height > max_y) + return this; + overlay.x = changed.x; + overlay.y = changed.y; + overlay.width = changed.width; + overlay.height = changed.height; + return this; + } + unstack_from_fork(ext, stack, focused, fork, left, right, is_left) { + if (!ext.auto_tiler) + return null; + const forest = ext.auto_tiler.forest; + const new_fork = forest.create_fork(left, right, fork.area, fork.workspace, fork.monitor); + if (is_left) { + fork.left = Node.Node.fork(new_fork[0]); + } + else { + fork.right = Node.Node.fork(new_fork[0]); + } + ext.auto_tiler.forest.parents.insert(new_fork[0], fork.entity); + forest.on_attach(new_fork[0], focused.entity); + for (const e of stack.entities) { + forest.on_attach(new_fork[0], e); + } + return new_fork[1]; + } + move(ext, window, x, y, w, h, direction, focus) { + if (!window) + return; + const win = ext.windows.get(window); + if (!win) + return; + const place_pointer = () => { + ext.register_fn(() => win.activate(true)); + }; + if (ext.auto_tiler && win.is_tilable(ext)) { + if (this.queue.length === 2) + return; + this.queue.send(() => { + const focused = ext.focus_window(); + if (focused) { + const move_to = focus(); + this.moving = true; + if (ext.auto_tiler) { + const s = ext.auto_tiler.find_stack(focused.entity); + if (s) { + this.move_from_stack(ext, s, focused, direction); + this.moving = false; + place_pointer(); + return; + } + } + if (move_to !== null) + this.move_auto(ext, focused, move_to, direction === Direction.Left); + this.moving = false; + place_pointer(); + } + }); + } + else { + this.swap_window = null; + this.rect_by_active_area(ext, (_monitor, rect) => { + this.change(ext.overlay, rect, x, y, w, h).change(ext.overlay, rect, 0, 0, 0, 0); + }); + } + } + move_alongside_stack(ext, [fork, branch, is_left], focused, direction) { + let new_fork = null; + if (fork.is_toplevel && fork.smart_gapped) { + fork.smart_gapped = false; + let rect = ext.monitor_work_area(fork.monitor); + rect.x += ext.gap_outer; + rect.y += ext.gap_outer; + rect.width -= ext.gap_outer * 2; + rect.height -= ext.gap_outer * 2; + fork.set_area(rect); + } + let orientation, reverse; + const { HORIZONTAL, VERTICAL } = Lib.Orientation; + switch (direction) { + case Direction.Left: + orientation = HORIZONTAL; + reverse = false; + break; + case Direction.Right: + orientation = HORIZONTAL; + reverse = true; + break; + case Direction.Up: + orientation = VERTICAL; + reverse = false; + break; + default: + orientation = VERTICAL; + reverse = true; + } + if (!ext.auto_tiler) + return; + const inner = branch.inner; + Node.stack_remove(ext.auto_tiler.forest, inner, focused.entity); + ext.auto_tiler.detach_window(ext, focused.entity); + focused.stack = null; + if (fork.right) { + let left, right; + if (reverse) { + left = branch; + right = Node.Node.window(focused.entity); + } + else { + left = Node.Node.window(focused.entity); + right = branch; + } + const inner = branch.inner; + new_fork = this.unstack_from_fork(ext, inner, focused, fork, left, right, is_left); + } + else if (reverse) { + fork.right = Node.Node.window(focused.entity); + } + else { + fork.right = fork.left; + fork.left = Node.Node.window(focused.entity); + } + let modifier = new_fork ?? fork; + modifier.set_orientation(orientation); + ext.auto_tiler.forest.on_attach(modifier.entity, focused.entity); + ext.auto_tiler.tile(ext, fork, fork.area); + this.overlay_watch(ext, focused); + } + move_from_stack(ext, [fork, branch, is_left], focused, direction, force_detach = false) { + if (!ext.auto_tiler) + return; + const inner = branch.inner; + if (inner.entities.length === 1) { + ext.auto_tiler.toggle_stacking(ext); + this.overlay_watch(ext, focused); + return; + } + let new_fork = null; + if (fork.is_toplevel && fork.smart_gapped) { + fork.smart_gapped = false; + let rect = ext.monitor_work_area(fork.monitor); + rect.x += ext.gap_outer; + rect.y += ext.gap_outer; + rect.width -= ext.gap_outer * 2; + rect.height -= ext.gap_outer * 2; + fork.set_area(rect); + } + const forest = ext.auto_tiler.forest; + const fentity = focused.entity; + const detach = (orient, reverse) => { + if (!ext.auto_tiler) + return; + focused.stack = null; + if (fork.right) { + let left, right; + if (reverse) { + left = branch; + right = Node.Node.window(fentity); + } + else { + left = Node.Node.window(fentity); + right = branch; + } + new_fork = this.unstack_from_fork(ext, inner, focused, fork, left, right, is_left); + } + else if (reverse) { + fork.right = Node.Node.window(fentity); + } + else { + fork.right = fork.left; + fork.left = Node.Node.window(fentity); + } + let modifier = new_fork ?? fork; + modifier.set_orientation(orient); + forest.on_attach(modifier.entity, fentity); + ext.auto_tiler.tile(ext, fork, fork.area); + this.overlay_watch(ext, focused); + }; + switch (direction) { + case Direction.Left: + if (force_detach) { + Node.stack_remove(forest, inner, fentity); + detach(Lib.Orientation.HORIZONTAL, false); + } + else if (!Node.stack_move_left(ext, forest, inner, fentity)) { + detach(Lib.Orientation.HORIZONTAL, false); + } + ext.auto_tiler.update_stack(ext, inner); + break; + case Direction.Right: + if (force_detach) { + Node.stack_remove(forest, inner, fentity); + detach(Lib.Orientation.HORIZONTAL, true); + } + else if (!Node.stack_move_right(ext, forest, inner, fentity)) { + detach(Lib.Orientation.HORIZONTAL, true); + } + ext.auto_tiler.update_stack(ext, inner); + break; + case Direction.Up: + Node.stack_remove(forest, inner, fentity); + detach(Lib.Orientation.VERTICAL, false); + break; + case Direction.Down: + Node.stack_remove(forest, inner, fentity); + detach(Lib.Orientation.VERTICAL, true); + break; + } + } + move_auto_(ext, mov1, mov2, callback) { + if (ext.auto_tiler && this.window) { + const entity = ext.auto_tiler.attached.get(this.window); + if (entity) { + const fork = ext.auto_tiler.forest.forks.get(entity); + const window = ext.windows.get(this.window); + if (!fork || !window) + return; + const workspace_id = ext.workspace_id(window); + const toplevel = ext.auto_tiler.forest.find_toplevel(workspace_id); + if (!toplevel) + return; + const topfork = ext.auto_tiler.forest.forks.get(toplevel); + if (!topfork) + return; + const toparea = topfork.area; + const before = window.rect(); + const grab_op = new GrabOp.GrabOp(this.window, before); + let crect = grab_op.rect.clone(); + let resize = (mov, func) => { + if (func(toparea, crect, mov) || crect.eq(grab_op.rect)) + return; + ext.auto_tiler.forest.resize(ext, entity, fork, this.window, grab_op.operation(crect), crect); + grab_op.rect = crect.clone(); + }; + resize(mov1, callback); + resize(mov2, callback); + ext.auto_tiler.forest.arrange(ext, fork.workspace); + ext.register_fn(() => ext.set_overlay(window.rect())); + } + } + } + overlay_watch(ext, window) { + ext.register_fn(() => { + if (window) { + ext.set_overlay(window.rect()); + window.activate(false); + } + }); + } + rect_by_active_area(ext, callback) { + if (this.window) { + const monitor_id = ext.monitors.get(this.window); + if (monitor_id) { + const monitor = ext.monitor_work_area(monitor_id[0]); + let rect = this.rect(ext, monitor); + if (rect) { + callback(monitor, rect); + } + } + } + } + resize_auto(ext, direction) { + let mov1, mov2; + const hrow = 64; + const hcolumn = 64; + switch (direction) { + case Direction.Left: + mov1 = [hrow, 0, -hrow, 0]; + mov2 = [0, 0, -hrow, 0]; + break; + case Direction.Right: + mov1 = [0, 0, hrow, 0]; + mov2 = [-hrow, 0, hrow, 0]; + break; + case Direction.Up: + mov1 = [0, hcolumn, 0, -hcolumn]; + mov2 = [0, 0, 0, -hcolumn]; + break; + default: + mov1 = [0, 0, 0, hcolumn]; + mov2 = [0, -hcolumn, 0, hcolumn]; + } + this.move_auto_(ext, new Rect.Rectangle(mov1), new Rect.Rectangle(mov2), (work_area, crect, mov) => { + crect.apply(mov); + let before = crect.clone(); + crect.clamp(work_area); + const diff = before.diff(crect); + crect.apply(new Rect.Rectangle([0, 0, -diff.x, -diff.y])); + return false; + }); + } + move_auto(ext, focused, move_to, stack_from_left = true) { + let watching = null; + const at = ext.auto_tiler; + if (at) { + if (move_to instanceof ShellWindow) { + const stack_info = at.find_stack(move_to.entity); + if (stack_info) { + const [stack_fork, branch] = stack_info; + const stack = branch.inner; + const placement = { auto: 0 }; + focused.ignore_detach = true; + at.detach_window(ext, focused.entity); + at.forest.on_attach(stack_fork.entity, focused.entity); + at.update_stack(ext, stack); + at.tile(ext, stack_fork, stack_fork.area); + focused.ignore_detach = true; + at.detach_window(ext, focused.entity); + at.attach_to_window(ext, move_to, focused, placement, stack_from_left); + watching = focused; + } + else { + const parent = at.windows_are_siblings(focused.entity, move_to.entity); + if (parent) { + const fork = at.forest.forks.get(parent); + if (fork) { + if (!fork.right) { + Log.error('move_auto: detected as sibling, but fork lacks right branch'); + return; + } + if (fork.left.inner.kind === 3) { + Node.stack_remove(at.forest, fork.left.inner, focused.entity); + focused.stack = null; + } + else { + const temp = fork.right; + fork.right = fork.left; + fork.left = temp; + at.tile(ext, fork, fork.area); + watching = focused; + } + } + } + if (!watching) { + let movement = { src: focused.meta.get_frame_rect() }; + focused.ignore_detach = true; + at.detach_window(ext, focused.entity); + at.attach_to_window(ext, move_to, focused, movement, false); + watching = focused; + } + } + } + else { + focused.ignore_detach = true; + at.detach_window(ext, focused.entity); + at.attach_to_workspace(ext, focused, [move_to, ext.active_workspace()]); + watching = focused; + } + } + if (watching) { + this.overlay_watch(ext, watching); + } + else { + ext.set_overlay(focused.rect()); + } + } + move_left(ext, window) { + this.move(ext, window ?? this.window, -1, 0, 0, 0, Direction.Left, move_window_or_monitor(ext, ext.focus_selector.left, Meta.DisplayDirection.LEFT)); + } + move_down(ext, window) { + this.move(ext, window ?? this.window, 0, 1, 0, 0, Direction.Down, move_window_or_monitor(ext, ext.focus_selector.down, Meta.DisplayDirection.DOWN)); + } + move_up(ext, window) { + this.move(ext, window ?? this.window, 0, -1, 0, 0, Direction.Up, move_window_or_monitor(ext, ext.focus_selector.up, Meta.DisplayDirection.UP)); + } + move_right(ext, window) { + this.move(ext, window ?? this.window, 1, 0, 0, 0, Direction.Right, move_window_or_monitor(ext, ext.focus_selector.right, Meta.DisplayDirection.RIGHT)); + } + resize(ext, direction) { + if (!this.window) + return; + this.resizing_window = true; + if (ext.auto_tiler && !ext.contains_tag(this.window, Tags.Floating)) { + this.resize_auto(ext, direction); + } + else { + let array; + switch (direction) { + case Direction.Down: + array = [0, 0, 0, 1]; + break; + case Direction.Left: + array = [0, 0, -1, 0]; + break; + case Direction.Up: + array = [0, 0, 0, -1]; + break; + default: + array = [0, 0, 1, 0]; + } + const [x, y, w, h] = array; + this.swap_window = null; + this.rect_by_active_area(ext, (_monitor, rect) => { + this.change(ext.overlay, rect, x, y, w, h).change(ext.overlay, rect, 0, 0, 0, 0); + }); + } + this.resizing_window = false; + } + swap(ext, selector) { + if (selector) { + ext.set_overlay(selector.rect()); + this.swap_window = selector.entity; + } + } + swap_left(ext) { + if (this.swap_window) { + ext.windows.with(this.swap_window, (window) => { + this.swap(ext, ext.focus_selector.left(ext, window)); + }); + } + else { + this.swap(ext, ext.focus_selector.left(ext, null)); + } + } + swap_down(ext) { + if (this.swap_window) { + ext.windows.with(this.swap_window, (window) => { + this.swap(ext, ext.focus_selector.down(ext, window)); + }); + } + else { + this.swap(ext, ext.focus_selector.down(ext, null)); + } + } + swap_up(ext) { + if (this.swap_window) { + ext.windows.with(this.swap_window, (window) => { + this.swap(ext, ext.focus_selector.up(ext, window)); + }); + } + else { + this.swap(ext, ext.focus_selector.up(ext, null)); + } + } + swap_right(ext) { + if (this.swap_window) { + ext.windows.with(this.swap_window, (window) => { + this.swap(ext, ext.focus_selector.right(ext, window)); + }); + } + else { + this.swap(ext, ext.focus_selector.right(ext, null)); + } + } + enter(ext) { + if (!this.window) { + const win = ext.focus_window(); + if (!win) + return; + this.window = win.entity; + if (win.is_maximized()) { + win.meta.unmaximize(Meta.MaximizeFlags.BOTH); + } + ext.set_overlay(win.rect()); + ext.overlay.visible = true; + if (!ext.auto_tiler || ext.contains_tag(win.entity, Tags.Floating)) { + this.rect_by_active_area(ext, (_monitor, rect) => { + this.change(ext.overlay, rect, 0, 0, 0, 0); + }); + } + ext.keybindings.disable(ext.keybindings.window_focus).enable(this.keybindings); + } + } + accept(ext) { + if (this.window) { + const meta = ext.windows.get(this.window); + if (meta) { + let tree_swapped = false; + if (this.swap_window) { + const meta_swap = ext.windows.get(this.swap_window); + if (meta_swap) { + if (ext.auto_tiler) { + tree_swapped = true; + ext.auto_tiler.attach_swap(ext, this.swap_window, this.window); + } + else { + ext.size_signals_block(meta_swap); + meta_swap.move(ext, meta.rect(), () => { + ext.size_signals_unblock(meta_swap); + }); + } + ext.register_fn(() => meta.activate(true)); + } + } + if (!tree_swapped) { + ext.size_signals_block(meta); + const meta_entity = this.window; + meta.move(ext, ext.overlay, () => { + ext.size_signals_unblock(meta); + ext.add_tag(meta_entity, Tags.Tiled); + }); + } + } + } + this.swap_window = null; + this.exit(ext); + } + exit(ext) { + this.queue.clear(); + if (this.window) { + this.window = null; + ext.overlay.visible = false; + ext.keybindings.disable(this.keybindings).enable(ext.keybindings.window_focus); + } + } + snap(ext, win) { + let mon_geom = ext.monitor_work_area(win.meta.get_monitor()); + if (mon_geom) { + let rect = win.rect(); + const columns = Math.floor(mon_geom.width / ext.column_size); + const rows = Math.floor(mon_geom.height / ext.row_size); + this.change(rect, monitor_rect(mon_geom, columns, rows), 0, 0, 0, 0); + win.move(ext, rect); + ext.snapped.insert(win.entity, true); + } + } +} +export function locate_monitor(win, direction) { + if (!win.actor_exists()) + return null; + const from = win.meta.get_monitor(); + const ref = win.meta.get_work_area_for_monitor(from); + const n_monitors = global.display.get_n_monitors(); + const { UP, DOWN, LEFT } = Meta.DisplayDirection; + let origin; + let exclude; + if (direction === UP) { + origin = [ref.x + ref.width / 2, ref.y]; + exclude = (rect) => { + return rect.y > ref.y; + }; + } + else if (direction === DOWN) { + origin = [ref.x + ref.width / 2, ref.y + ref.height]; + exclude = (rect) => rect.y < ref.y; + } + else if (direction === LEFT) { + origin = [ref.x, ref.y + ref.height / 2]; + exclude = (rect) => rect.x > ref.x; + } + else { + origin = [ref.x + ref.width, ref.y + ref.height / 2]; + exclude = (rect) => rect.x < ref.x; + } + let next = null; + for (let mon = 0; mon < n_monitors; mon += 1) { + if (mon === from) + continue; + const work_area = win.meta.get_work_area_for_monitor(mon); + if (!work_area || exclude(work_area)) + continue; + const weight = geom.shortest_side(origin, work_area); + if (next === null || next[1] > weight) { + next = [mon, weight, work_area]; + } + } + return next ? [next[0], next[2]] : null; +} +function monitor_rect(monitor, columns, rows) { + let tile_width = monitor.width / columns; + let tile_height = monitor.height / rows; + if (monitor.width * 9 >= monitor.height * 21) { + tile_width /= 2; + } + if (monitor.height * 9 >= monitor.width * 21) { + tile_height /= 2; + } + return new Rect.Rectangle([monitor.x, monitor.y, tile_width, tile_height]); +} +function move_window_or_monitor(ext, method, direction) { + return () => { + let next_window = method.call(ext.focus_selector, ext, null); + next_window = next_window?.actor_exists() ? next_window : null; + const focus = ext.focus_window(); + if (focus) { + const next_monitor = locate_monitor(focus, direction); + if (!next_window) + return next_monitor ? next_monitor[0] : null; + if (!next_monitor || focus.meta.get_monitor() == next_window.meta.get_monitor()) + return next_window; + return Rect.Rectangle.from_meta(next_monitor[1]).contains(next_window.rect()) + ? next_window + : next_monitor[0]; + } + return next_window; + }; +} +function tile_monitors(rect) { + let total_size = (a, b) => a.width * a.height - b.width * b.height; + let workspace = global.workspace_manager.get_active_workspace(); + return Main.layoutManager.monitors + .map((_monitor, i) => workspace.get_work_area_for_monitor(i)) + .filter((monitor) => { + return (rect.x + rect.width > monitor.x && + rect.y + rect.height > monitor.y && + rect.x < monitor.x + monitor.width && + rect.y < monitor.y + monitor.height); + }) + .sort(total_size); +} diff --git a/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/utils.js b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/utils.js new file mode 100644 index 0000000..1b40212 --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/utils.js @@ -0,0 +1,144 @@ +import * as result from './result.js'; +import * as error from './error.js'; +import * as log from './log.js'; +import Gio from 'gi://Gio'; +import GLib from 'gi://GLib'; +import GObject from 'gi://GObject'; +import Meta from 'gi://Meta'; +const { Ok, Err } = result; +const { Error } = error; +export function is_wayland() { + return Meta.is_wayland_compositor(); +} +export function block_signal(object, signal) { + GObject.signal_handler_block(object, signal); +} +export function unblock_signal(object, signal) { + GObject.signal_handler_unblock(object, signal); +} +export function read_to_string(path) { + const file = Gio.File.new_for_path(path); + try { + const [ok, contents] = file.load_contents(null); + if (ok) { + return Ok(imports.byteArray.toString(contents)); + } + else { + return Err(new Error(`failed to load contents of ${path}`)); + } + } + catch (e) { + return Err(new Error(String(e)).context(`failed to load contents of ${path}`)); + } +} +export function source_remove(id) { + return GLib.source_remove(id); +} +export function exists(path) { + return Gio.File.new_for_path(path).query_exists(null); +} +export function is_dark(color) { + let color_val = ''; + let r = 255; + let g = 255; + let b = 255; + if (color.indexOf('rgb') >= 0) { + color = color.replace('rgba', 'rgb').replace('rgb(', '').replace(')', ''); + let colors = color.split(','); + r = parseInt(colors[0].trim()); + g = parseInt(colors[1].trim()); + b = parseInt(colors[2].trim()); + } + else if (color.charAt(0) === '#') { + color_val = color.substring(1, 7); + r = parseInt(color_val.substring(0, 2), 16); + g = parseInt(color_val.substring(2, 4), 16); + b = parseInt(color_val.substring(4, 6), 16); + } + let uicolors = [r / 255, g / 255, b / 255]; + let c = uicolors.map((col) => { + if (col <= 0.03928) { + return col / 12.92; + } + return Math.pow((col + 0.055) / 1.055, 2.4); + }); + let L = 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2]; + return L <= 0.179; +} +export function async_process(argv, input = null, cancellable = null) { + let flags = Gio.SubprocessFlags.STDOUT_PIPE; + if (input !== null) + flags |= Gio.SubprocessFlags.STDIN_PIPE; + let proc = new Gio.Subprocess({ argv, flags }); + proc.init(cancellable); + proc.wait_async(null, (source, res) => { + source.wait_finish(res); + if (cancellable !== null) { + cancellable.cancel(); + } + }); + return new Promise((resolve, reject) => { + proc.communicate_utf8_async(input, cancellable, (proc, res) => { + try { + let bytes = proc.communicate_utf8_finish(res)[1]; + resolve(bytes.toString()); + } + catch (e) { + reject(e); + } + }); + }); +} +export function async_process_ipc(argv) { + const { SubprocessLauncher, SubprocessFlags } = Gio; + const launcher = new SubprocessLauncher({ + flags: SubprocessFlags.STDIN_PIPE | SubprocessFlags.STDOUT_PIPE, + }); + let child; + let cancellable = new Gio.Cancellable(); + try { + child = launcher.spawnv(argv); + } + catch (why) { + log.error(`failed to spawn ${argv}: ${why}`); + return null; + } + let stdin = new Gio.DataOutputStream({ + base_stream: child.get_stdin_pipe(), + close_base_stream: true, + }); + let stdout = new Gio.DataInputStream({ + base_stream: child.get_stdout_pipe(), + close_base_stream: true, + }); + child.wait_async(null, (source, res) => { + source.wait_finish(res); + cancellable.cancel(); + }); + return { child, stdin, stdout, cancellable }; +} +export function map_eq(map1, map2) { + if (map1.size !== map2.size) { + return false; + } + let cmp; + for (let [key, val] of map1) { + cmp = map2.get(key); + if (cmp !== val || (cmp === undefined && !map2.has(key))) { + return false; + } + } + return true; +} +export function os_release() { + const [ok, bytes] = GLib.file_get_contents('/etc/os-release'); + if (!ok) + return null; + const contents = imports.byteArray.toString(bytes); + for (const line of contents.split('\n')) { + if (line.startsWith('VERSION_ID')) { + return line.split('"')[1]; + } + } + return null; +} diff --git a/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/window.js b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/window.js new file mode 100644 index 0000000..1dd5d73 --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/window.js @@ -0,0 +1,583 @@ +import * as lib from './lib.js'; +import * as log from './log.js'; +import * as once_cell from './once_cell.js'; +import * as Rect from './rectangle.js'; +import * as Tags from './tags.js'; +import * as utils from './utils.js'; +import * as xprop from './xprop.js'; +import * as scheduler from './scheduler.js'; +import * as focus from './focus.js'; +import Gdk from 'gi://Gdk'; +import Meta from 'gi://Meta'; +import Shell from 'gi://Shell'; +import St from 'gi://St'; +import GLib from 'gi://GLib'; +import * as Main from 'resource:///org/gnome/shell/ui/main.js'; +const { OnceCell } = once_cell; +export var window_tracker = Shell.WindowTracker.get_default(); +let SCHEDULED_RESTACK = null; +let ACTIVE_HINT_SHOW_ID = null; +const WM_TITLE_BLACKLIST = [ + 'Firefox', + 'Nightly', + 'Tor Browser', +]; +var RESTACK_STATE; +(function (RESTACK_STATE) { + RESTACK_STATE[RESTACK_STATE["RAISED"] = 0] = "RAISED"; + RESTACK_STATE[RESTACK_STATE["WORKSPACE_CHANGED"] = 1] = "WORKSPACE_CHANGED"; + RESTACK_STATE[RESTACK_STATE["NORMAL"] = 2] = "NORMAL"; +})(RESTACK_STATE || (RESTACK_STATE = {})); +var RESTACK_SPEED; +(function (RESTACK_SPEED) { + RESTACK_SPEED[RESTACK_SPEED["RAISED"] = 430] = "RAISED"; + RESTACK_SPEED[RESTACK_SPEED["WORKSPACE_CHANGED"] = 300] = "WORKSPACE_CHANGED"; + RESTACK_SPEED[RESTACK_SPEED["NORMAL"] = 200] = "NORMAL"; +})(RESTACK_SPEED || (RESTACK_SPEED = {})); +export class ShellWindow { + constructor(entity, window, window_app, ext) { + this.stack = null; + this.grab = false; + this.activate_after_move = false; + this.ignore_detach = false; + this.destroying = false; + this.reassignment = false; + this.smart_gapped = false; + this.border = new St.Bin({ + style_class: 'pop-shell-active-hint pop-shell-border-normal', + }); + this.prev_rect = null; + this.was_hidden = false; + this.extra = { + normal_hints: new OnceCell(), + wm_role_: new OnceCell(), + xid_: new OnceCell(), + }; + this.border_size = 0; + this.window_app = window_app; + this.entity = entity; + this.meta = window; + this.ext = ext; + this.known_workspace = this.workspace_id(); + if (this.meta.is_fullscreen()) { + ext.add_tag(entity, Tags.Floating); + } + if (this.may_decorate()) { + if (!window.is_client_decorated()) { + if (ext.settings.show_title()) { + this.decoration_show(ext); + } + else { + this.decoration_hide(ext); + } + } + } + this.bind_window_events(); + this.bind_hint_events(); + if (this.border) + global.window_group.add_child(this.border); + this.hide_border(); + this.restack(); + this.update_border_layout(); + if (this.meta.get_compositor_private()?.get_stage()) + this.on_style_changed(); + } + activate(move_mouse = true) { + activate(this.ext, move_mouse, this.meta); + } + actor_exists() { + return !this.destroying && this.meta.get_compositor_private() !== null; + } + bind_window_events() { + this.ext.window_signals + .get_or(this.entity, () => new Array()) + .push(this.meta.connect('size-changed', () => { + this.window_changed(); + }), this.meta.connect('position-changed', () => { + this.window_changed(); + }), this.meta.connect('workspace-changed', () => { + this.workspace_changed(); + }), this.meta.connect('notify::wm-class', () => { + this.wm_class_changed(); + }), this.meta.connect('raised', () => { + this.window_raised(); + })); + } + bind_hint_events() { + if (!this.border) + return; + let settings = this.ext.settings; + let change_id = settings.ext.connect('changed', (_, key) => { + if (this.border) { + if (key === 'hint-color-rgba') { + this.update_hint_colors(); + } + } + return false; + }); + this.border.connect('destroy', () => { + settings.ext.disconnect(change_id); + }); + this.border.connect('style-changed', () => { + this.on_style_changed(); + }); + this.update_hint_colors(); + } + update_hint_colors() { + let settings = this.ext.settings; + const color_value = settings.hint_color_rgba(); + if (this.ext.overlay) { + const gdk = new Gdk.RGBA(); + const overlay_alpha = 0.3; + const orig_overlay = 'rgba(53, 132, 228, 0.3)'; + gdk.parse(color_value); + if (utils.is_dark(gdk.to_string())) { + gdk.parse(orig_overlay); + } + gdk.alpha = overlay_alpha; + this.ext.overlay.set_style(`background: ${gdk.to_string()}`); + } + this.update_border_style(); + } + cmdline() { + let pid = this.meta.get_pid(), out = null; + if (-1 === pid) + return out; + const path = '/proc/' + pid + '/cmdline'; + if (!utils.exists(path)) + return out; + const result = utils.read_to_string(path); + if (result.kind == 1) { + out = result.value.trim(); + } + else { + log.error(`failed to fetch cmdline: ${result.value.format()}`); + } + return out; + } + decoration(_ext, callback) { + if (this.may_decorate()) { + const xid = this.xid(); + if (xid) + callback(xid); + } + } + decoration_hide(ext) { + if (this.ignore_decoration()) + return; + this.was_hidden = true; + this.decoration(ext, (xid) => xprop.set_hint(xid, xprop.MOTIF_HINTS, xprop.HIDE_FLAGS)); + } + decoration_show(ext) { + if (!this.was_hidden) + return; + this.decoration(ext, (xid) => xprop.set_hint(xid, xprop.MOTIF_HINTS, xprop.SHOW_FLAGS)); + } + icon(_ext, size) { + let icon = this.window_app.create_icon_texture(size); + if (!icon) { + icon = new St.Icon({ + icon_name: 'applications-other', + icon_type: St.IconType.FULLCOLOR, + icon_size: size, + }); + } + return icon; + } + ignore_decoration() { + const name = this.meta.get_wm_class(); + if (name === null) + return true; + return WM_TITLE_BLACKLIST.findIndex((n) => name.startsWith(n)) !== -1; + } + is_maximized() { + return this.meta.get_maximized() !== 0; + } + is_max_screen() { + return this.is_maximized() || this.ext.settings.gap_inner() === 0 || this.smart_gapped; + } + is_single_max_screen() { + const display = this.meta.get_display(); + if (display) { + let monitor_count = display.get_n_monitors(); + return (this.is_maximized() || this.smart_gapped) && monitor_count == 1; + } + return false; + } + is_snap_edge() { + return this.meta.get_maximized() == Meta.MaximizeFlags.VERTICAL; + } + is_tilable(ext) { + let tile_checks = () => { + let wm_class = this.meta.get_wm_class(); + if (wm_class !== null && wm_class.trim().length === 0) { + wm_class = this.name(ext); + } + const role = this.meta.get_role(); + if (role === 'quake') + return false; + if (this.meta.get_title() === 'Steam') { + const rect = this.rect(); + const is_dialog = rect.width < 400 && rect.height < 200; + const is_first_login = rect.width === 432 && rect.height === 438; + if (is_dialog || is_first_login) + return false; + } + if (wm_class !== null && ext.conf.window_shall_float(wm_class, this.title())) { + return ext.contains_tag(this.entity, Tags.ForceTile); + } + return (this.meta.window_type == Meta.WindowType.NORMAL && + !this.is_transient() && + wm_class !== null); + }; + return !ext.contains_tag(this.entity, Tags.Floating) && tile_checks(); + } + is_transient() { + return this.meta.get_transient_for() !== null; + } + may_decorate() { + const xid = this.xid(); + return xid ? xprop.may_decorate(xid) : false; + } + move(ext, rect, on_complete) { + if (!this.same_workspace() && this.is_maximized()) { + return; + } + this.hide_border(); + const clone = Rect.Rectangle.from_meta(rect); + const meta = this.meta; + const actor = meta.get_compositor_private(); + if (actor) { + meta.unmaximize(Meta.MaximizeFlags.HORIZONTAL); + meta.unmaximize(Meta.MaximizeFlags.VERTICAL); + meta.unmaximize(Meta.MaximizeFlags.HORIZONTAL | Meta.MaximizeFlags.VERTICAL); + actor.remove_all_transitions(); + ext.movements.insert(this.entity, clone); + ext.register({ tag: 2, window: this, kind: { tag: 1 } }); + if (on_complete) + ext.register_fn(on_complete); + if (meta.appears_focused) { + this.update_border_layout(); + ext.show_border_on_focused(); + } + } + } + name(ext) { + return ext.names.get_or(this.entity, () => 'unknown'); + } + on_style_changed() { + if (!this.border) + return; + this.border_size = this.border.get_theme_node().get_border_width(St.Side.TOP); + } + rect() { + return Rect.Rectangle.from_meta(this.meta.get_frame_rect()); + } + size_hint() { + return this.extra.normal_hints.get_or_init(() => { + const xid = this.xid(); + return xid ? xprop.get_size_hints(xid) : null; + }); + } + swap(ext, other) { + let ar = this.rect().clone(); + let br = other.rect().clone(); + other.move(ext, ar); + this.move(ext, br, () => place_pointer_on(this.ext, this.meta)); + } + title() { + const title = this.meta.get_title(); + return title ? title : this.name(this.ext); + } + wm_role() { + return this.extra.wm_role_.get_or_init(() => { + const xid = this.xid(); + return xid ? xprop.get_window_role(xid) : null; + }); + } + workspace_id() { + const workspace = this.meta.get_workspace(); + if (workspace) { + return workspace.index(); + } + else { + this.meta.change_workspace_by_index(0, false); + return 0; + } + } + xid() { + return this.extra.xid_.get_or_init(() => { + if (utils.is_wayland()) + return null; + return xprop.get_xid(this.meta); + }); + } + show_border() { + if (!this.border) + return; + this.restack(); + this.update_border_style(); + if (this.ext.settings.active_hint()) { + let border = this.border; + const permitted = () => { + return (this.actor_exists() && + this.ext.focus_window() == this && + !this.meta.is_fullscreen() && + (!this.is_single_max_screen() || this.is_snap_edge()) && + !this.meta.minimized); + }; + if (permitted()) { + if (this.meta.appears_focused) { + border.show(); + let applications = 0; + if (ACTIVE_HINT_SHOW_ID !== null) + GLib.source_remove(ACTIVE_HINT_SHOW_ID); + ACTIVE_HINT_SHOW_ID = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 600, () => { + if ((applications > 4 && !this.same_workspace()) || !permitted()) { + ACTIVE_HINT_SHOW_ID = null; + return GLib.SOURCE_REMOVE; + } + applications += 1; + border.show(); + return GLib.SOURCE_CONTINUE; + }); + } + } + } + } + same_workspace() { + const workspace = this.meta.get_workspace(); + if (workspace) { + let workspace_id = workspace.index(); + return workspace_id === global.workspace_manager.get_active_workspace_index(); + } + return false; + } + same_monitor() { + return this.meta.get_monitor() === global.display.get_current_monitor(); + } + restack(updateState = RESTACK_STATE.NORMAL) { + this.update_border_layout(); + if (this.meta.is_fullscreen() || (this.is_single_max_screen() && !this.is_snap_edge()) || this.meta.minimized) { + this.hide_border(); + } + let restackSpeed = RESTACK_SPEED.NORMAL; + switch (updateState) { + case RESTACK_STATE.NORMAL: + restackSpeed = RESTACK_SPEED.NORMAL; + break; + case RESTACK_STATE.RAISED: + restackSpeed = RESTACK_SPEED.RAISED; + break; + case RESTACK_STATE.WORKSPACE_CHANGED: + restackSpeed = RESTACK_SPEED.WORKSPACE_CHANGED; + break; + } + let restacks = 0; + const action = () => { + const count = restacks; + restacks += 1; + if (!this.actor_exists && count === 0) + return true; + if (count === 3) { + if (SCHEDULED_RESTACK !== null) + GLib.source_remove(SCHEDULED_RESTACK); + SCHEDULED_RESTACK = null; + } + const border = this.border; + const actor = this.meta.get_compositor_private(); + const win_group = global.window_group; + if (actor && border && win_group) { + this.update_border_layout(); + win_group.set_child_above_sibling(border, null); + if (this.always_top_windows.length > 0) { + for (const above_actor of this.always_top_windows) { + if (actor != above_actor) { + if (border.get_parent() === above_actor.get_parent()) { + win_group.set_child_below_sibling(border, above_actor); + } + } + } + if (border.get_parent() === actor.get_parent()) { + win_group.set_child_above_sibling(border, actor); + } + } + for (const window of this.ext.windows.values()) { + const parent = window.meta.get_transient_for(); + const window_actor = window.meta.get_compositor_private(); + if (!parent || !window_actor) + continue; + const parent_actor = parent.get_compositor_private(); + if (!parent_actor && parent_actor !== actor) + continue; + win_group.set_child_below_sibling(border, window_actor); + } + } + return true; + }; + if (SCHEDULED_RESTACK !== null) + GLib.source_remove(SCHEDULED_RESTACK); + SCHEDULED_RESTACK = GLib.timeout_add(GLib.PRIORITY_LOW, restackSpeed, action); + } + get always_top_windows() { + let above_windows = new Array(); + for (const actor of global.get_window_actors()) { + if (actor && actor.get_meta_window() && actor.get_meta_window().is_above()) + above_windows.push(actor); + } + return above_windows; + } + hide_border() { + let b = this.border; + if (b) + b.hide(); + } + update_border_layout() { + let { x, y, width, height } = this.meta.get_frame_rect(); + const border = this.border; + let borderSize = this.border_size; + if (border) { + if (!(this.is_max_screen() || this.is_snap_edge())) { + border.remove_style_class_name('pop-shell-border-maximize'); + } + else { + borderSize = 0; + border.add_style_class_name('pop-shell-border-maximize'); + } + const stack_number = this.stack; + let dimensions = null; + if (stack_number !== null) { + const stack = this.ext.auto_tiler?.forest.stacks.get(stack_number); + if (stack) { + let stack_tab_height = stack.tabs_height; + if (borderSize === 0 || this.grab) { + stack_tab_height = 0; + } + dimensions = [ + x - borderSize, + y - stack_tab_height - borderSize, + width + 2 * borderSize, + height + stack_tab_height + 2 * borderSize, + ]; + } + } + else { + dimensions = [x - borderSize, y - borderSize, width + 2 * borderSize, height + 2 * borderSize]; + } + if (dimensions) { + [x, y, width, height] = dimensions; + const workspace = this.meta.get_workspace(); + if (workspace === null) + return; + const screen = workspace.get_work_area_for_monitor(this.meta.get_monitor()); + if (screen) { + width = Math.min(width, screen.x + screen.width); + height = Math.min(height, screen.y + screen.height); + } + border.set_position(x, y); + border.set_size(width, height); + } + } + } + update_border_style() { + const { settings } = this.ext; + const color_value = settings.hint_color_rgba(); + const radius_value = settings.active_hint_border_radius(); + if (this.border) { + this.border.set_style(`border-color: ${color_value}; border-radius: ${radius_value}px;`); + } + } + wm_class_changed() { + if (this.is_tilable(this.ext)) { + this.ext.connect_window(this); + if (!this.meta.minimized) { + this.ext.auto_tiler?.auto_tile(this.ext, this, this.ext.init); + } + } + } + window_changed() { + this.update_border_layout(); + this.ext.show_border_on_focused(); + } + window_raised() { + this.restack(RESTACK_STATE.RAISED); + this.ext.show_border_on_focused(); + } + workspace_changed() { + this.restack(RESTACK_STATE.WORKSPACE_CHANGED); + } +} +export function activate(ext, move_mouse, win) { + try { + if (!win.get_compositor_private()) + return; + if (ext.get_window(win)?.destroying) + return; + if (win.is_override_redirect()) + return; + const workspace = win.get_workspace(); + if (!workspace) + return; + scheduler.setForeground(win); + win.unminimize(); + workspace.activate_with_focus(win, global.get_current_time()); + win.raise(); + const pointer_placement_permitted = move_mouse && + Main.modalCount === 0 && + ext.settings.mouse_cursor_follows_active_window() && + !pointer_already_on_window(win) && + pointer_in_work_area(); + if (pointer_placement_permitted) { + place_pointer_on(ext, win); + } + } + catch (error) { + log.error(`failed to activate window: ${error}`); + } +} +function pointer_in_work_area() { + const cursor = lib.cursor_rect(); + const indice = global.display.get_current_monitor(); + const mon = global.display.get_workspace_manager().get_active_workspace().get_work_area_for_monitor(indice); + return mon ? cursor.intersects(mon) : false; +} +function place_pointer_on(ext, win) { + const rect = win.get_frame_rect(); + let x = rect.x; + let y = rect.y; + let key = Object.keys(focus.FocusPosition)[ext.settings.mouse_cursor_focus_location()]; + let pointer_position_ = focus.FocusPosition[key]; + switch (pointer_position_) { + case focus.FocusPosition.TopLeft: + x += 8; + y += 8; + break; + case focus.FocusPosition.BottomLeft: + x += 8; + y += rect.height - 16; + break; + case focus.FocusPosition.TopRight: + x += rect.width - 16; + y += 8; + break; + case focus.FocusPosition.BottomRight: + x += rect.width - 16; + y += rect.height - 16; + break; + case focus.FocusPosition.Center: + x += rect.width / 2 + 8; + y += rect.height / 2 + 8; + break; + default: + x += 8; + y += 8; + } + const display = Gdk.DisplayManager.get().get_default_display(); + if (display) { + display.get_default_seat().get_pointer().warp(display.get_default_screen(), x, y); + } +} +function pointer_already_on_window(meta) { + const cursor = lib.cursor_rect(); + return cursor.intersects(meta.get_frame_rect()); +} diff --git a/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/xprop.js b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/xprop.js new file mode 100644 index 0000000..3ea849a --- /dev/null +++ b/gnome-shell-extension-pop-shell/usr/share/gnome-shell/extensions/pop-shell@system76.com/xprop.js @@ -0,0 +1,95 @@ +import * as lib from './lib.js'; +import GLib from 'gi://GLib'; +import { spawn } from 'resource:///org/gnome/shell/misc/util.js'; +export var MOTIF_HINTS = '_MOTIF_WM_HINTS'; +export var HIDE_FLAGS = ['0x2', '0x0', '0x0', '0x0', '0x0']; +export var SHOW_FLAGS = ['0x2', '0x0', '0x1', '0x0', '0x0']; +export function get_window_role(xid) { + let out = xprop_cmd(xid, 'WM_WINDOW_ROLE'); + if (!out) + return null; + return parse_string(out); +} +export function get_hint(xid, hint) { + let out = xprop_cmd(xid, hint); + if (!out) + return null; + const array = parse_cardinal(out); + return array ? array.map((value) => (value.startsWith('0x') ? value : '0x' + value)) : null; +} +function size_params(line) { + let fields = line.split(' '); + let x = lib.dbg(lib.nth_rev(fields, 2)); + let y = lib.dbg(lib.nth_rev(fields, 0)); + if (!x || !y) + return null; + let xn = parseInt(x, 10); + let yn = parseInt(y, 10); + return isNaN(xn) || isNaN(yn) ? null : [xn, yn]; +} +export function get_size_hints(xid) { + let out = xprop_cmd(xid, 'WM_NORMAL_HINTS'); + if (out) { + let lines = out.split('\n')[Symbol.iterator](); + lines.next(); + let minimum = lines.next().value; + let increment = lines.next().value; + let base = lines.next().value; + if (!minimum || !increment || !base) + return null; + let min_values = size_params(minimum); + let inc_values = size_params(increment); + let base_values = size_params(base); + if (!min_values || !inc_values || !base_values) + return null; + return { + minimum: min_values, + increment: inc_values, + base: base_values, + }; + } + return null; +} +export function get_xid(meta) { + const desc = meta.get_description(); + const match = desc && desc.match(/0x[0-9a-f]+/); + return match && match[0]; +} +export function may_decorate(xid) { + const hints = motif_hints(xid); + return hints ? hints[2] == '0x0' || hints[2] == '0x1' : true; +} +export function motif_hints(xid) { + return get_hint(xid, MOTIF_HINTS); +} +export function set_hint(xid, hint, value) { + spawn(['xprop', '-id', xid, '-f', hint, '32c', '-set', hint, value.join(', ')]); +} +function consume_key(string) { + const pos = string.indexOf('='); + return -1 == pos ? null : pos; +} +function parse_cardinal(string) { + const pos = consume_key(string); + return pos + ? string + .slice(pos + 1) + .trim() + .split(', ') + : null; +} +function parse_string(string) { + const pos = consume_key(string); + return pos + ? string + .slice(pos + 1) + .trim() + .slice(1, -1) + : null; +} +function xprop_cmd(xid, args) { + let xprops = GLib.spawn_command_line_sync(`xprop -id ${xid} ${args}`); + if (!xprops[0]) + return null; + return imports.byteArray.toString(xprops[1]); +} diff --git a/main.sh b/main.sh index d80ca48..cef88fc 100755 --- a/main.sh +++ b/main.sh @@ -6,15 +6,13 @@ set -e echo "$PIKA_BUILD_ARCH" > pika-build-arch -VERSION="1.0" +VERSION="46.0" # Clone Upstream -mkdir -p ./src-pkg-name -cp -rvf ./debian ./src-pkg-name/ -cd ./src-pkg-name/ +cd ./gnome-shell-extension-pop-shell/ # Get build deps -LOGNAME=root dh_make --createorig -y -l -p src-pkg-name_"$VERSION" || echo "dh-make: Ignoring Last Error" +LOGNAME=root dh_make --createorig -y -l -p gnome-shell-extension-pop-shell_"$VERSION" || echo "dh-make: Ignoring Last Error" apt-get build-dep ./ -y # Build package