add files
All checks were successful
PikaOS Package Build & Release (amd64-v3) / build (push) Successful in 48s

This commit is contained in:
Ward from fusion-voyager-3 2024-08-27 20:07:05 +03:00
parent 2e93334023
commit 54272a6252
74 changed files with 11250 additions and 97 deletions

View File

@ -1 +1 @@
1 3

5
debian/changelog vendored
View File

@ -1,5 +0,0 @@
upstream-name (1.0-101pika1) pika; urgency=medium
* Initial release. (Closes: #nnnn) <nnnn is the bug number of your ITP>
-- ferreo <harderthanfire@gmail.com> Wed, 18 Jan 2023 21:48:14 +0000

19
debian/control vendored
View File

@ -1,19 +0,0 @@
Source: upstream-name
Section: admin
Priority: optional
Maintainer: name <email>
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

67
debian/rules vendored
View File

@ -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 $@

View File

@ -0,0 +1,5 @@
gnome-shell-extension-pop-shell (46.0-101pika1) pika; urgency=medium
* Initial Creation
-- Ward Nakchbandi <hotrod.master@hotmail.com> Sat, 01 Oct 2022 14:50:00 +0200

View File

@ -0,0 +1 @@
10

View File

@ -0,0 +1,27 @@
Source: gnome-shell-extension-pop-shell
Section: gnome
Priority: optional
Maintainer: Marco Trevisan <marco@ubuntu.com>
Build-Depends: debhelper (>= 10),
eslint <!nocheck>,
libglib2.0-bin,
node-chalk <!nocheck>,
node-js-yaml <!nocheck>,
node-strip-ansi <!nocheck>,
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

View File

@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
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.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
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 <https://www.gnu.org/licenses/>.
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:
<program> Copyright (C) <year> <name of author>
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
<https://www.gnu.org/licenses/>.
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
<https://www.gnu.org/licenses/why-not-lgpl.html>.

View File

@ -0,0 +1,6 @@
#!/bin/sh
set -e
glib-compile-schemas /usr/share/glib-2.0/schemas/

View File

@ -0,0 +1,7 @@
#!/bin/sh
set -e
glib-compile-schemas /usr/share/glib-2.0/schemas/

View File

@ -0,0 +1,6 @@
#!/usr/bin/make -f
export DH_VERBOSE = 1
export DEB_BUILD_OPTIONS=nocheck
%:
dh $@

View File

@ -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 `<Direction>`, 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:
- `<Direction>`
- In default mode, this will move the displayed overlay around based on a grid
- In auto-tile mode, this will resize the window
- `Shift` + `<Direction>`
- In default mode, this will resize the overlay
- In auto-tile mode, this will do nothing
- `Ctrl` + `<Direction>`
- Selects a window in the given direction of the overlay
- When `Return` is pressed, window positions will be swapped
- `Shift` + `Ctrl` + `<Direction>`
- 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` + `<Direction>` 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: "<WM_CLASS String from xprop>",
title: "<Optional Window 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.

View File

@ -0,0 +1,18 @@
[org.gnome.desktop.wm.keybindings]
close = ['<Alt>F4', '<Super>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 = ['<Primary><Super>Down', '<Primary><Super>KP_Down', '<Primary><Super>j']
switch-to-workspace-left = []
switch-to-workspace-right = []
switch-to-workspace-up = ['<Primary><Super>Up', '<Primary><Super>KP_Up', '<Primary><Super>k']
toggle-maximized = ['<Super>m']
unmaximize = []

View File

@ -0,0 +1,7 @@
[org.gnome.mutter:GNOME]
attach-modal-dialogs = false
workspaces-only-on-primary = false
[org.gnome.mutter.keybindings]
toggle-tiled-left = ['<Primary><Super>Left', '<Primary><Super>KP_Left', '<Primary><Super>h']
toggle-tiled-right = ['<Primary><Super>Right', '<Primary><Super>KP_Right', '<Primary><Super>l']

View File

@ -0,0 +1,2 @@
[org.gnome.mutter.wayland.keybindings]
restore-shortcuts = []

View File

@ -0,0 +1,6 @@
[org.gnome.settings-daemon.plugins.media-keys]
email = ['<Super>e']
home = ['<Super>f']
screensaver = ['<Super>Escape']
www = ['<Super>b']
rotate-video-lock-static = ['XF86RotationLockToggle']

View File

@ -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 = ['<Super>v']
[org.gnome.shell.overrides]
attach-modal-dialogs = false
workspaces-only-on-primary = false

View File

@ -0,0 +1,288 @@
<?xml version="1.0" encoding="UTF-8"?>
<schemalist gettext-domain="pop-shell">
<schema id="org.gnome.shell.extensions.pop-shell" path="/org/gnome/shell/extensions/pop-shell/">
<!-- Appearance Options -->
<key type="b" name="active-hint">
<default>false</default>
<summary>Show a hint around the active window</summary>
</key>
<key type="u" name="active-hint-border-radius">
<default>5</default>
<range min="0" max="30"/>
<summary>Border radius for active window hint, in pixels</summary>
</key>
<key type="b" name="fullscreen-launcher">
<default>false</default>
<summary>Allow showing launcher above fullscreen windows</summary>
</key>
<key type="u" name="gap-inner">
<default>2</default>
<summary>Gap between tiled windows</summary>
</key>
<key type="u" name="gap-outer">
<default>2</default>
<summary>Gap surrounding tiled windows</summary>
</key>
<key type="b" name="show-title">
<default>true</default>
<summary>Show title bars on windows with server-side decorations</summary>
</key>
<key type="b" name="show-skip-taskbar">
<default>true</default>
<summary>Handle minimized to tray windows</summary>
</key>
<key type="b" name="mouse-cursor-follows-active-window">
<default>true</default>
<summary>Move cursor to active window when navigating with keyboard shortcuts or touchpad gestures</summary>
</key>
<key type="u" name="mouse-cursor-focus-location">
<default>0</default>
<summary>The location the mouse cursor focuses when selecting a window</summary>
</key>
<!-- Tiling Options -->
<key type="u" name="column-size">
<default>64</default>
<summary>Size of a column in the display grid</summary>
</key>
<key type="u" name="row-size">
<default>64</default>
<summary>Size of a row in the display grid</summary>
</key>
<key type="b" name="smart-gaps">
<default>false</default>
<summary>Hide the outer gap when a tree contains only one window</summary>
</key>
<key type="b" name="snap-to-grid">
<default>false</default>
<summary>Snaps windows to the tiling grid on drop</summary>
</key>
<key type="b" name="tile-by-default">
<default>false</default>
<summary>Tile launched windows by default</summary>
</key>
<key type="b" name="stacking-with-mouse">
<default>true</default>
<summary>Allow for stacking windows as a result of dragging a window with mouse</summary>
</key>
<!-- Focus Shifting -->
<key type="as" name="focus-left">
<default><![CDATA[['<Super>Left','<Super>KP_Left','<Super>h']]]></default>
<summary>Focus left window</summary>
</key>
<key type="as" name="focus-down">
<default><![CDATA[['<Super>Down','<Super>KP_Down','<Super>j']]]></default>
<summary>Focus down window</summary>
</key>
<key type="as" name="focus-up">
<default><![CDATA[['<Super>Up','<Super>KP_Up','<Super>k']]]></default>
<summary>Focus up window</summary>
</key>
<key type="as" name="focus-right">
<default><![CDATA[['<Super>Right','<Super>KP_Right','<Super>l']]]></default>
<summary>Focus right window</summary>
</key>
<!-- Launcher -->
<key type="as" name="activate-launcher">
<default><![CDATA[['<Super>slash']]]></default>
<summary>Search key combo</summary>
</key>
<!-- Window Management Keys -->
<key type="as" name="toggle-stacking">
<default><![CDATA[['s']]]></default>
<summary>Toggle stacking mode inside management mode</summary>
</key>
<key type="as" name="toggle-stacking-global">
<default><![CDATA[['<Super>s']]]></default>
<summary>Toggle stacking mode outside management mode</summary>
</key>
<key type="as" name="management-orientation">
<default><![CDATA[['o']]]></default>
<summary>Toggle tiling orientation</summary>
</key>
<key type="as" name="tile-enter">
<default><![CDATA[['<Super>Return','<Super>KP_Enter']]]></default>
<summary>Enter tiling mode</summary>
</key>
<key type="as" name="tile-accept">
<default><![CDATA[['Return','KP_Enter']]]></default>
<summary>Accept tiling changes</summary>
</key>
<key type="as" name="tile-reject">
<default><![CDATA[['Escape']]]></default>
<summary>Reject tiling changes</summary>
</key>
<key type="as" name="toggle-floating">
<default><![CDATA[['<Super>g']]]></default>
<summary>Toggles a window between floating and tiling</summary>
</key>
<!-- Tiling Mode -->
<key type="as" name="toggle-tiling">
<default><![CDATA[['<Super>y']]]></default>
<summary>Toggles auto-tiling on and off</summary>
</key>
<key type="as" name="tile-move-left">
<default><![CDATA[['Left','KP_Left','h']]]></default>
<summary>Move window left</summary>
</key>
<key type="as" name="tile-move-down">
<default><![CDATA[['Down','KP_Down','j']]]></default>
<summary>Move window down</summary>
</key>
<key type="as" name="tile-move-up">
<default><![CDATA[['Up','KP_Up','k']]]></default>
<summary>Move window up</summary>
</key>
<key type="as" name="tile-move-right">
<default><![CDATA[['Right','KP_Right','l']]]></default>
<summary>Move window right</summary>
</key>
<key type="as" name="tile-move-left-global">
<default><![CDATA[[]]]></default>
<summary>Move window left</summary>
</key>
<key type="as" name="tile-move-down-global">
<default><![CDATA[[]]]></default>
<summary>Move window down</summary>
</key>
<key type="as" name="tile-move-up-global">
<default><![CDATA[[]]]></default>
<summary>Move window up</summary>
</key>
<key type="as" name="tile-move-right-global">
<default><![CDATA[[]]]></default>
<summary>Move window right</summary>
</key>
<key type="as" name="tile-orientation">
<default><![CDATA[['<Super>o']]]></default>
<summary>Toggle tiling orientation</summary>
</key>
<!-- Resize in normal direction -->
<key type="as" name="tile-resize-left">
<default><![CDATA[['<Shift>Left','<Shift>KP_Left','<Shift>h']]]></default>
<summary>Resize window left</summary>
</key>
<key type="as" name="tile-resize-down">
<default><![CDATA[['<Shift>Down','<Shift>KP_Down','<Shift>j']]]></default>
<summary>Resize window down</summary>
</key>
<key type="as" name="tile-resize-up">
<default><![CDATA[['<Shift>Up','<Shift>KP_Up','<Shift>k']]]></default>
<summary>Resize window up</summary>
</key>
<key type="as" name="tile-resize-right">
<default><![CDATA[['<Shift>Right','<Shift>KP_Right','<Shift>l']]]></default>
<summary>Resize window right</summary>
</key>
<!-- Swap windows -->
<key type="as" name="tile-swap-left">
<default><![CDATA[['<Primary>Left','<Primary>KP_Left','<Primary>h']]]></default>
<summary>Swap window left</summary>
</key>
<key type="as" name="tile-swap-down">
<default><![CDATA[['<Primary>Down','<Primary>KP_Down','<Primary>j']]]></default>
<summary>Swap window down</summary>
</key>
<key type="as" name="tile-swap-up">
<default><![CDATA[['<Primary>Up','<Primary>KP_Up','<Primary>k']]]></default>
<summary>Swap window up</summary>
</key>
<key type="as" name="tile-swap-right">
<default><![CDATA[['<Primary>Right','<Primary>KP_Right','<Primary>l']]]></default>
<summary>Swap window right</summary>
</key>
<!-- Workspace Management -->
<key type="as" name="pop-workspace-down">
<default><![CDATA[['<Super><Shift>Down','<Super><Shift>KP_Down','<Super><Shift>j']]]></default>
<summary>Move window to the lower workspace</summary>
</key>
<key type="as" name="pop-workspace-up">
<default><![CDATA[['<Super><Shift>Up','<Super><Shift>KP_Up','<Super><Shift>k']]]></default>
<summary>Move window to the upper workspace</summary>
</key>
<key type="as" name="pop-monitor-down">
<default><![CDATA[['<Super><Shift><Primary>Down','<Super><Shift><Primary>KP_Down','<Super><Shift><Primary>j']]]></default>
<summary>Move window to the lower monitor</summary>
</key>
<key type="as" name="pop-monitor-up">
<default><![CDATA[['<Super><Shift><Primary>Up','<Super><Shift><Primary>KP_Up','<Super><Shift><Primary>k']]]></default>
<summary>Move window to the upper monitor</summary>
</key>
<key type="as" name="pop-monitor-left">
<default><![CDATA[['<Super><Shift>Left','<Super><Shift>KP_Left','<Super><Shift>h']]]></default>
<summary>Move window to the leftward monitor</summary>
</key>
<key type="as" name="pop-monitor-right">
<default><![CDATA[['<Super><Shift>Right','<Super><Shift>KP_Right','<Super><Shift>l']]]></default>
<summary>Move window to the rightward monitor</summary>
</key>
<key type="s" name="hint-color-rgba">
<default>'rgba(251, 184, 108, 1)'</default>
<summary>The current active-hint-color in RGBA</summary>
</key>
<key type="u" name="log-level">
<default>0</default>
<summary>
Derive some log4j level/order
0 - OFF
1 - ERROR
2 - WARN
3 - INFO
4 - DEBUG
</summary>
</key>
</schema>
</schemalist>

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<KeyListEntries group="system" schema="org.gnome.shell.extensions.pop-shell" name="Move, resize, and swap windows">
<KeyListEntry name="tile-accept" description="Apply changes"/>
<KeyListEntry name="tile-enter" description="Adjustment mode"/>
<KeyListEntry name="tile-move-down" description="Move window down"/>
<KeyListEntry name="tile-move-left" description="Move window left"/>
<KeyListEntry name="tile-move-right" description="Move window right"/>
<KeyListEntry name="tile-move-up" description="Move window up"/>
<KeyListEntry name="tile-move-down-global" description="Move window down outside management mode"/>
<KeyListEntry name="tile-move-left-global" description="Move window left outside management mode"/>
<KeyListEntry name="tile-move-right-global" description="Move window right outside management mode"/>
<KeyListEntry name="tile-move-up-global" description="Move window up outside management mode"/>
<KeyListEntry name="pop-workspace-down" description="Move window to lower workspace"/>
<KeyListEntry name="pop-workspace-up" description="Move window to upper workspace"/>
<KeyListEntry name="pop-monitor-down" description="Move window to lower monitor"/>
<KeyListEntry name="pop-monitor-left" description="Move window to leftward monitor"/>
<KeyListEntry name="pop-monitor-right" description="Move window to rightward monitor"/>
<KeyListEntry name="pop-monitor-up" description="Move window to upper monitor"/>
<KeyListEntry name="tile-reject" description="Cancel changes"/>
<KeyListEntry name="tile-resize-left" description="Resize window smaller"/>
<KeyListEntry name="tile-resize-right" description="Resize window larger"/>
<KeyListEntry name="tile-resize-up" description="Resize window shorter"/>
<KeyListEntry name="tile-resize-down" description="Resize window taller"/>
<KeyListEntry name="tile-swap-down" description="Swap window down"/>
<KeyListEntry name="tile-swap-left" description="Swap window left"/>
<KeyListEntry name="tile-swap-right" description="Swap window right"/>
<KeyListEntry name="tile-swap-up" description="Swap window up"/>
<KeyListEntry name="management-orientation" description="Change current window orientation"/>
</KeyListEntries>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<KeyListEntries group="system" schema="org.gnome.shell.extensions.pop-shell" name="Navigate applications and windows">
<KeyListEntry name="activate-launcher" description="Launch and switch applications"/>
<KeyListEntry name="focus-down" description="Switch focus to window down"/>
<KeyListEntry name="focus-left" description="Switch focus to window left"/>
<KeyListEntry name="focus-right" description="Switch focus to window right"/>
<KeyListEntry name="focus-up" description="Switch focus to window up"/>
</KeyListEntries>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<KeyListEntries group="system" schema="org.gnome.shell.extensions.pop-shell" name="Tiling">
<KeyListEntry name="tile-orientation" description="Change window orientation"/>
<KeyListEntry name="toggle-floating" description="Toggles a window between floating and tiling"/>
<KeyListEntry name="toggle-tiling" description="Toggles auto-tiling"/>
<KeyListEntry name="toggle-stacking" description="Toggle stacking mode inside management mode"/>
<KeyListEntry name="toggle-stacking-global" description="Toggle stacking mode outside management mode"/>
</KeyListEntries>

View File

@ -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;
}
}
}

View File

@ -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;
}

View File

@ -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();

View File

@ -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();
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -0,0 +1,44 @@
import Gio from 'gi://Gio';
const IFACE = `<node>
<interface name="com.System76.PopShell">
<method name="FocusLeft"/>
<method name="FocusRight"/>
<method name="FocusUp"/>
<method name="FocusDown"/>
<method name="Launcher"/>
<method name="WindowFocus">
<arg type="(uu)" direction="in" name="window"/>
</method>
<method name="WindowHighlight">
<arg type="(uu)" direction="in" name="window"/>
</method>
<method name="WindowList">
<arg type="a((uu)sss)" direction="out" name="args"/>
</method>
<method name="WindowQuit">
<arg type="(uu)" direction="in" name="window"/>
</method>
</interface>
</node>`;
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);
}
}

View File

@ -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();
}
}

View File

@ -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();

View File

@ -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`;
}
}

View File

@ -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 } };
}

View File

@ -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();

View File

@ -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();
}

View File

@ -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('<b>System Exceptions</b>');
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();

View File

@ -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));
}

View File

@ -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);
});
}

View File

@ -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;
}
}
}

View File

@ -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)));
}

View File

@ -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);
}
}

View File

@ -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
}

View File

@ -0,0 +1,163 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07, custom)"
sodipodi:docname="pop-shell-auto-off-symbolic.svg"
width="16.031464"
version="1.1"
style="enable-background:new"
id="svg7384"
height="16">
<sodipodi:namedview
inkscape:current-layer="svg7384"
inkscape:window-maximized="0"
inkscape:window-y="23"
inkscape:window-x="26"
inkscape:cy="7.8736667"
inkscape:cx="5.4863836"
inkscape:zoom="47.743848"
showgrid="true"
id="namedview26"
inkscape:window-height="1032"
inkscape:window-width="1904"
inkscape:pageshadow="2"
inkscape:pageopacity="0"
guidetolerance="10"
gridtolerance="10"
objecttolerance="10"
borderopacity="1"
bordercolor="#666666"
pagecolor="#ffffff"
inkscape:snap-intersection-paths="true"
inkscape:snap-smooth-nodes="true"
inkscape:document-rotation="0">
<inkscape:grid
type="xygrid"
id="grid834" />
</sodipodi:namedview>
<metadata
id="metadata90">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>Pop Symbolic Icon Theme</dc:title>
<cc:license
rdf:resource="http://creativecommons.org/licenses/by-sa/4.0/" />
</cc:Work>
<cc:License
rdf:about="http://creativecommons.org/licenses/by-sa/4.0/">
<cc:permits
rdf:resource="http://creativecommons.org/ns#Reproduction" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#Distribution" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Notice" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Attribution" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#ShareAlike" />
</cc:License>
</rdf:RDF>
</metadata>
<title
id="title8473">Pop Symbolic Icon Theme</title>
<defs
id="defs7386">
<linearGradient
osb:paint="solid"
id="linearGradient8297">
<stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop8295" />
</linearGradient>
<linearGradient
osb:paint="solid"
id="linearGradient6882">
<stop
style="stop-color:#555555;stop-opacity:1;"
offset="0"
id="stop6884" />
</linearGradient>
<linearGradient
osb:paint="solid"
id="linearGradient5606">
<stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop5608" />
</linearGradient>
<filter
style="color-interpolation-filters:sRGB"
id="filter7554">
<feBlend
mode="darken"
in2="BackgroundImage"
id="feBlend7556" />
</filter>
</defs>
<g
transform="translate(-1124.0002,555.55128)"
style="display:inline"
id="layer9" />
<g
transform="translate(-1124.0002,555.55128)"
style="display:inline;filter:url(#filter7554)"
id="layer10" />
<g
transform="translate(-883,-61.448718)"
style="display:inline"
id="layer1" />
<g
transform="translate(-1124.0002,555.55128)"
style="display:inline"
id="layer14" />
<g
transform="translate(-1124.0002,555.55128)"
style="display:inline"
id="layer15" />
<g
transform="translate(-1124.0002,555.55128)"
style="display:inline"
id="g71291" />
<g
transform="translate(-883,88.551282)"
style="display:inline"
id="layer2" />
<g
transform="translate(-1124.0002,555.55128)"
style="display:inline"
id="layer12" />
<path
style="fill:#4c5263;fill-opacity:0.988327;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 11,9.5512818 v 1.1992192 c 0,0.979903 -0.820878,1.800781 -1.800781,1.800781 H 4 v 1.199219 c 0,0.4432 0.3575812,0.800781 0.8007812,0.800781 H 13.199219 C 13.642419,14.551282 14,14.193701 14,13.750501 V 9.5512818 Z"
id="rect3162"
inkscape:connector-curvature="0"
sodipodi:nodetypes="csscsssscc" />
<path
style="fill:#4c5263;fill-opacity:0.988327;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 6.8007812,1.5512818 C 6.3575812,1.5512818 6,1.908863 6,2.352063 v 1.1992188 h 3.199219 c 0.979903,0 1.777249,0.8211611 1.800781,1.8007812 v 3.1992188 h 4.199219 C 15.642419,8.5512818 16,8.1937006 16,7.7505006 V 2.352063 c 0,-0.4432 -0.357581,-0.8007812 -0.800781,-0.8007812 z"
id="rect3160"
inkscape:connector-curvature="0"
sodipodi:nodetypes="sscsscsssss" />
<rect
height="7"
id="rect3158"
ry="0.80000001"
style="fill:#4c5263;fill-opacity:0.988327;stroke:none;stroke-width:113.386;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
width="10"
x="0"
y="4.5512819" />
</svg>

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@ -0,0 +1,161 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07, custom)"
sodipodi:docname="pop-shell-auto-on-symbolic.svg"
width="16.031464"
version="1.1"
style="enable-background:new"
id="svg7384"
height="16">
<sodipodi:namedview
inkscape:current-layer="svg7384"
inkscape:window-maximized="1"
inkscape:window-y="0"
inkscape:window-x="0"
inkscape:cy="8.1513747"
inkscape:cx="5.7085514"
inkscape:zoom="51.17"
showgrid="false"
id="namedview26"
inkscape:window-height="1048"
inkscape:window-width="1920"
inkscape:pageshadow="2"
inkscape:pageopacity="0"
guidetolerance="10"
gridtolerance="10"
objecttolerance="10"
borderopacity="1"
bordercolor="#666666"
pagecolor="#ffffff"
inkscape:document-rotation="0" />
<metadata
id="metadata90">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>Pop Symbolic Icon Theme</dc:title>
<cc:license
rdf:resource="http://creativecommons.org/licenses/by-sa/4.0/" />
</cc:Work>
<cc:License
rdf:about="http://creativecommons.org/licenses/by-sa/4.0/">
<cc:permits
rdf:resource="http://creativecommons.org/ns#Reproduction" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#Distribution" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Notice" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Attribution" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#ShareAlike" />
</cc:License>
</rdf:RDF>
</metadata>
<title
id="title8473">Pop Symbolic Icon Theme</title>
<defs
id="defs7386">
<linearGradient
osb:paint="solid"
id="linearGradient8297">
<stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop8295" />
</linearGradient>
<linearGradient
osb:paint="solid"
id="linearGradient6882">
<stop
style="stop-color:#555555;stop-opacity:1;"
offset="0"
id="stop6884" />
</linearGradient>
<linearGradient
osb:paint="solid"
id="linearGradient5606">
<stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop5608" />
</linearGradient>
<filter
style="color-interpolation-filters:sRGB"
id="filter7554">
<feBlend
mode="darken"
in2="BackgroundImage"
id="feBlend7556" />
</filter>
</defs>
<g
transform="translate(-1124.0002,555)"
style="display:inline"
id="layer9" />
<g
transform="translate(-1124.0002,555)"
style="display:inline;filter:url(#filter7554)"
id="layer10" />
<g
transform="translate(-883,-62)"
style="display:inline"
id="layer1" />
<g
transform="translate(-1124.0002,555)"
style="display:inline"
id="layer14" />
<g
transform="translate(-1124.0002,555)"
style="display:inline"
id="layer15" />
<g
transform="translate(-1124.0002,555)"
style="display:inline"
id="g71291" />
<g
transform="translate(-883,88)"
style="display:inline"
id="layer2" />
<rect
height="8.9525318"
id="rect3158"
ry="0.65109324"
style="fill:#4c5263;fill-opacity:0.988327;stroke:none;stroke-width:102.291;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
width="7"
x="0"
y="3.9999995" />
<rect
height="4.0693326"
id="rect3160"
ry="0.65109324"
style="fill:#4c5263;fill-opacity:0.988327;stroke:none;stroke-width:102.291;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
width="8"
x="8"
y="3.9999995" />
<rect
height="4.0693326"
id="rect3162"
ry="0.65109324"
style="fill:#4c5263;fill-opacity:0.988327;stroke:none;stroke-width:102.291;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
width="8"
x="8"
y="8.8831997" />
<g
transform="translate(-1124.0002,555)"
style="display:inline"
id="layer12" />
</svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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}`);
}
}
}

View File

@ -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 });
}

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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"
]
}

View File

@ -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;
}
}

View File

@ -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]);
}
}
}
}

View File

@ -0,0 +1,9 @@
export class OnceCell {
constructor() { }
get_or_init(callback) {
if (this.value === undefined) {
this.value = callback();
}
return this.value;
}
}

View File

@ -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;
}

View File

@ -0,0 +1,3 @@
export function get_current_path() {
return import.meta.url.split('://')[1].split('/').slice(0, -1).join('/');
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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 };
}

View File

@ -0,0 +1,36 @@
import * as log from './log.js';
import Gio from 'gi://Gio';
const SchedulerInterface = '<node>\
<interface name="com.system76.Scheduler"> \
<method name="SetForegroundProcess"> \
<arg name="pid" type="u" direction="in"/> \
</method> \
</interface> \
</node>';
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;
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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;
}
});

View File

@ -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);
}
}
}

View File

@ -0,0 +1,4 @@
export var Tiled = 0;
export var Floating = 1;
export var Blocked = 2;
export var ForceTile = 3;

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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());
}

View File

@ -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]);
}

View File

@ -6,15 +6,13 @@ set -e
echo "$PIKA_BUILD_ARCH" > pika-build-arch echo "$PIKA_BUILD_ARCH" > pika-build-arch
VERSION="1.0" VERSION="46.0"
# Clone Upstream # Clone Upstream
mkdir -p ./src-pkg-name cd ./gnome-shell-extension-pop-shell/
cp -rvf ./debian ./src-pkg-name/
cd ./src-pkg-name/
# Get build deps # 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 apt-get build-dep ./ -y
# Build package # Build package