middleclick-autoscroll 1.0.0
Middle-click autoscroll for every Chromium-based application on the system: Electron and CEF applications, Chromium-based browsers, Flatpaks, Spotify and Steam, plus anything installed later. The flag goes into an Arch wrapper's <name>-flags.conf where one exists, and into a shadowing desktop entry where it does not. Steam gets its web helper script patched and -noverifyfiles on its launcher, because it restores its own files otherwise. Every change is recorded so it can be taken back exactly.
This commit is contained in:
commit
d3c82fdff8
18 files changed
+4208
No files matched your search
@@ -0,0 +1,2 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
@@ -0,0 +1,3 @@
|
||||
# build artifacts
|
||||
po/*.mo
|
||||
doc/*.1
|
||||
@@ -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>.
|
||||
@@ -0,0 +1,114 @@
|
||||
# middleclick-autoscroll - build and install
|
||||
#
|
||||
# Everything here is plain shell; "building" only means compiling the gettext
|
||||
# catalogs and rendering the man page. Both targets degrade to a no-op when
|
||||
# msgfmt/scdoc are missing, so the tree stays usable for development without
|
||||
# the build dependencies installed.
|
||||
|
||||
# Overridable so a packager can pass the version it is actually building
|
||||
# (`make VERSION=$pkgver`). The literal below is the fallback for builds
|
||||
# straight from a checkout, and is what a release tag has to carry.
|
||||
VERSION ?= 1.0.0
|
||||
|
||||
PREFIX ?= /usr
|
||||
DESTDIR ?=
|
||||
BINDIR ?= $(PREFIX)/bin
|
||||
DATADIR ?= $(PREFIX)/share
|
||||
LIBDIR ?= $(DATADIR)/middleclick-autoscroll/lib
|
||||
LOCALEDIR ?= $(DATADIR)/locale
|
||||
MANDIR ?= $(DATADIR)/man
|
||||
USERUNITDIR ?= $(PREFIX)/lib/systemd/user
|
||||
|
||||
LINGUAS := de
|
||||
MOFILES := $(patsubst %,po/%.mo,$(LINGUAS))
|
||||
MANPAGE := doc/middleclick-autoscroll.1
|
||||
|
||||
LIBS := $(wildcard src/lib/*.sh)
|
||||
|
||||
MSGFMT := $(shell command -v msgfmt 2>/dev/null)
|
||||
SCDOC := $(shell command -v scdoc 2>/dev/null)
|
||||
|
||||
.PHONY: all build install uninstall check clean
|
||||
|
||||
all: build
|
||||
|
||||
build: $(MOFILES) $(MANPAGE)
|
||||
|
||||
po/%.mo: po/%.po
|
||||
ifdef MSGFMT
|
||||
$(MSGFMT) --check --output-file=$@ $<
|
||||
else
|
||||
@echo "msgfmt not found - skipping $@"
|
||||
endif
|
||||
|
||||
$(MANPAGE): doc/middleclick-autoscroll.1.scd
|
||||
ifdef SCDOC
|
||||
$(SCDOC) < $< > $@
|
||||
else
|
||||
@echo "scdoc not found - skipping $@"
|
||||
endif
|
||||
|
||||
# Syntax-check every shell file, and run shellcheck when it is available.
|
||||
check:
|
||||
@set -e; for f in src/middleclick-autoscroll $(LIBS); do \
|
||||
bash -n "$$f" && echo "ok $$f"; \
|
||||
done
|
||||
@if command -v shellcheck >/dev/null 2>&1; then \
|
||||
shellcheck -x -e SC1090,SC1091 src/middleclick-autoscroll $(LIBS); \
|
||||
echo "ok shellcheck"; \
|
||||
else \
|
||||
echo "shellcheck not found - skipped"; \
|
||||
fi
|
||||
@if command -v desktop-file-validate >/dev/null 2>&1; then \
|
||||
echo "ok desktop-file-validate (nothing to check)"; \
|
||||
fi
|
||||
|
||||
install: build
|
||||
# executable
|
||||
install -Dm755 src/middleclick-autoscroll \
|
||||
"$(DESTDIR)$(BINDIR)/middleclick-autoscroll"
|
||||
|
||||
# shell libraries
|
||||
install -d "$(DESTDIR)$(LIBDIR)"
|
||||
install -Dm644 -t "$(DESTDIR)$(LIBDIR)" $(LIBS)
|
||||
|
||||
# the version and the resolved paths are baked in at install time
|
||||
sed -i -e 's|@VERSION@|$(VERSION)|g' \
|
||||
-e 's|@LIBDIR@|$(LIBDIR)|g' \
|
||||
-e 's|@LOCALEDIR@|$(LOCALEDIR)|g' \
|
||||
"$(DESTDIR)$(BINDIR)/middleclick-autoscroll" \
|
||||
"$(DESTDIR)$(LIBDIR)"/*.sh
|
||||
|
||||
# user units: the watcher, and the one-shot it starts
|
||||
install -Dm644 res/systemd/middleclick-autoscroll.service \
|
||||
"$(DESTDIR)$(USERUNITDIR)/middleclick-autoscroll.service"
|
||||
install -Dm644 res/systemd/middleclick-autoscroll.path \
|
||||
"$(DESTDIR)$(USERUNITDIR)/middleclick-autoscroll.path"
|
||||
sed -i -e 's|@BINDIR@|$(BINDIR)|g' \
|
||||
"$(DESTDIR)$(USERUNITDIR)/middleclick-autoscroll.service"
|
||||
|
||||
# translations
|
||||
@for l in $(LINGUAS); do \
|
||||
if [ -f "po/$$l.mo" ]; then \
|
||||
install -Dm644 "po/$$l.mo" \
|
||||
"$(DESTDIR)$(LOCALEDIR)/$$l/LC_MESSAGES/middleclick-autoscroll.mo"; \
|
||||
fi; \
|
||||
done
|
||||
|
||||
# documentation
|
||||
@if [ -f $(MANPAGE) ]; then \
|
||||
install -Dm644 $(MANPAGE) "$(DESTDIR)$(MANDIR)/man1/middleclick-autoscroll.1"; \
|
||||
fi
|
||||
install -Dm644 README.md \
|
||||
"$(DESTDIR)$(DATADIR)/doc/middleclick-autoscroll/README.md"
|
||||
|
||||
uninstall:
|
||||
rm -f "$(DESTDIR)$(BINDIR)/middleclick-autoscroll"
|
||||
rm -rf "$(DESTDIR)$(DATADIR)/middleclick-autoscroll"
|
||||
rm -f "$(DESTDIR)$(USERUNITDIR)/middleclick-autoscroll.service"
|
||||
rm -f "$(DESTDIR)$(USERUNITDIR)/middleclick-autoscroll.path"
|
||||
rm -f "$(DESTDIR)$(MANDIR)/man1/middleclick-autoscroll.1"
|
||||
rm -rf "$(DESTDIR)$(DATADIR)/doc/middleclick-autoscroll"
|
||||
|
||||
clean:
|
||||
rm -f po/*.mo $(MANPAGE)
|
||||
@@ -0,0 +1,191 @@
|
||||
# middleclick-autoscroll
|
||||
|
||||
Middle-click autoscroll — hold the middle mouse button, move the pointer, the
|
||||
page scrolls — for every Chromium-based application on the system. Discord,
|
||||
Vesktop, Equibop, VS Code, Obsidian, Signal, Spotify, Steam, Chromium-based
|
||||
browsers, and whatever gets installed next week.
|
||||
|
||||
```bash
|
||||
paru -S middleclick-autoscroll
|
||||
middleclick-autoscroll enable
|
||||
```
|
||||
|
||||
That is the whole setup. Nothing else has to be configured, and no file has to
|
||||
be edited.
|
||||
|
||||
## Why this needs a program at all
|
||||
|
||||
Blink — the engine inside Chromium, Electron and CEF — has had Windows-style
|
||||
autoscroll for years. On Linux it is switched off, because middle click is
|
||||
already taken by primary-selection paste. One command line argument turns it
|
||||
back on:
|
||||
|
||||
```
|
||||
--enable-blink-features=MiddleClickAutoscroll
|
||||
```
|
||||
|
||||
Getting that argument into one application is a five-minute job. Getting it into
|
||||
all of them, in a way that survives the next package upgrade, is not:
|
||||
|
||||
- Some applications read a flag file, some don't.
|
||||
- Some ship their own copy of Electron, some use the system one.
|
||||
- Flatpaks see none of the host's configuration.
|
||||
- An application that starts itself at login uses a different entry than the one
|
||||
in the menu, and Discord launched at login used to behave differently from
|
||||
Discord launched by hand.
|
||||
- Steam takes no arguments for its interface at all, and puts back any file you
|
||||
change.
|
||||
- Every upgrade can undo the lot.
|
||||
|
||||
## The interface
|
||||
|
||||
`middleclick-autoscroll` on its own:
|
||||
|
||||
```
|
||||
Middle-Click Autoscroll
|
||||
|
||||
Autoscroll ON
|
||||
|
||||
Applications covered 14 of 15
|
||||
Not identified 1 - see the applications list
|
||||
Steam ON
|
||||
New applications ON
|
||||
Last applied 3 minutes ago
|
||||
|
||||
Applications pick this up the next time they are started.
|
||||
|
||||
[1] Turn autoscroll on or off
|
||||
[2] Apply now
|
||||
[3] Applications
|
||||
[4] Settings
|
||||
[q] Quit
|
||||
```
|
||||
|
||||
**[3] Applications** lists everything that was found, how each one is handled,
|
||||
and lets a single application be switched off — or an unrecognised one switched
|
||||
on — with the space bar:
|
||||
|
||||
```
|
||||
Applications
|
||||
|
||||
▸ Vesktop on (flag file)
|
||||
Discord on (flag file)
|
||||
Code - OSS on (flag file)
|
||||
Obsidian on (flag file)
|
||||
Signal on (flag file)
|
||||
Spotify on (launcher)
|
||||
Steam on (Steam)
|
||||
Cursor cannot tell
|
||||
```
|
||||
|
||||
**[4] Settings** has the categories — Electron and CEF applications, browsers,
|
||||
Flatpaks, autostart entries, Steam, Spotify, whether to watch for new
|
||||
applications, and a field for extra Chromium arguments if you want any.
|
||||
|
||||
There is a configuration file behind all of this. You are never asked to open
|
||||
it.
|
||||
|
||||
## Where the argument actually goes
|
||||
|
||||
Two routes, picked per application.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Flag file** | Arch's Electron and Chromium wrappers read extra arguments from `~/.config/<name>-flags.conf`. This is the good one: it is the supported way to pass arguments, it survives package upgrades untouched, and it applies to a launch from a terminal as much as one from the menu. |
|
||||
| **Desktop entry** | For applications that ship their own binary with no wrapper, and for Flatpaks, a copy of the entry with the argument appended goes into `~/.local/share/applications`, where it shadows the system one. |
|
||||
|
||||
Entries that already live in `~/.local/share/applications` — AppImages, web app
|
||||
shortcuts — are edited in place and the original is kept. So are the entries in
|
||||
`~/.config/autostart`, so an application that starts itself at login gets the
|
||||
same treatment as one started from the menu.
|
||||
|
||||
An `--enable-blink-features` that is already there is **extended**, never
|
||||
repeated. Chromium keeps only the last occurrence of that option, so a second
|
||||
one would silently switch off whatever the first one enabled.
|
||||
|
||||
## Steam
|
||||
|
||||
Steam's interface is CEF and supports the feature perfectly well, but Steam
|
||||
builds the command line for its web helper itself and offers no way to add to
|
||||
it. The only place an argument fits is the script that starts the helper, inside
|
||||
Steam's own installation:
|
||||
|
||||
```
|
||||
~/.local/share/Steam/ubuntu12_64/steamwebhelper_sniper_wrap.sh
|
||||
```
|
||||
|
||||
Steam checksums that script at every start and restores it when it differs, so
|
||||
its launcher entry also gets `-noverifyfiles`. **The trade-off is real**: with
|
||||
verification off, Steam no longer repairs a damaged installation by itself. That
|
||||
is why Steam is a switch of its own rather than part of the general handling —
|
||||
turn it off in the settings and Steam is left completely alone.
|
||||
|
||||
The script comes back on every client update. The watcher notices and puts the
|
||||
patch back.
|
||||
|
||||
Starting Steam from a terminal without `-noverifyfiles` undoes it for that one
|
||||
session; the next start from the menu has it again.
|
||||
|
||||
## Applications installed later
|
||||
|
||||
A systemd user path unit watches every directory a launcher can appear in —
|
||||
`/usr/share/applications`, the Flatpak exports, `~/.local/share/applications`,
|
||||
`~/.config/autostart` — plus Steam's helper script. Anything new is handled
|
||||
within a second of being installed, whether it came from pacman, the AUR,
|
||||
Flatpak or an AppImage manager. There is no hook to install per package manager.
|
||||
|
||||
## What it will not guess
|
||||
|
||||
An AppImage keeps its payload in a compressed filesystem, so there is no way to
|
||||
tell from the outside whether Chromium is in there. Those show up as **cannot
|
||||
tell** and are left alone until you switch them on from the applications screen.
|
||||
|
||||
Detection is deliberately conservative everywhere else too. A wrong "yes" would
|
||||
append an unknown argument to something that is not Chromium, and plenty of
|
||||
programs treat an unrecognised argument as a file name to open.
|
||||
|
||||
## Undoing it
|
||||
|
||||
```bash
|
||||
middleclick-autoscroll disable
|
||||
```
|
||||
|
||||
Every change is recorded in a ledger as it is made, and `disable` replays it
|
||||
backwards: generated entries are deleted, edited files are restored from their
|
||||
backups, flag files that only ever contained our line are removed, and a flag
|
||||
file that was merged into loses exactly the one feature that was added to it.
|
||||
Files that were not touched by this program are not touched by it now either.
|
||||
|
||||
Run this before uninstalling the package.
|
||||
|
||||
## Commands
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| `middleclick-autoscroll` | the menu above |
|
||||
| `… enable` | turn it on, apply, start watching |
|
||||
| `… disable` | turn it off and put everything back |
|
||||
| `… apply` | apply to anything new (this is what the watcher calls) |
|
||||
| `… status` | what is covered |
|
||||
| `… list` | every application that was found and how it is handled |
|
||||
|
||||
See `man middleclick-autoscroll` for the details.
|
||||
|
||||
## Requirements
|
||||
|
||||
Arch or an Arch derivative (CachyOS, EndeavourOS, Manjaro), bash, systemd for
|
||||
the watcher. Nothing outside your home directory is ever written to, and running
|
||||
it as root is refused.
|
||||
|
||||
## Building from source
|
||||
|
||||
```bash
|
||||
make
|
||||
sudo make install
|
||||
```
|
||||
|
||||
`make check` runs `bash -n` and, if installed, `shellcheck` over every script.
|
||||
|
||||
## License
|
||||
|
||||
GPL-3.0-or-later.
|
||||
@@ -0,0 +1,142 @@
|
||||
middleclick-autoscroll(1)
|
||||
|
||||
# NAME
|
||||
|
||||
middleclick-autoscroll - middle-click autoscroll for Chromium-based applications
|
||||
|
||||
# SYNOPSIS
|
||||
|
||||
*middleclick-autoscroll* [_command_]
|
||||
|
||||
# DESCRIPTION
|
||||
|
||||
Blink, the engine inside Chromium, Electron and CEF, implements Windows-style
|
||||
autoscroll: hold the middle mouse button and the page scrolls with the pointer.
|
||||
On Linux it is switched off, because middle click is taken by primary-selection
|
||||
paste. It can be turned back on with *--enable-blink-features=MiddleClickAutoscroll*
|
||||
on the command line.
|
||||
|
||||
*middleclick-autoscroll* finds every Chromium-based application on the system
|
||||
and puts that argument somewhere the application will actually read it, then
|
||||
keeps doing so for anything installed later.
|
||||
|
||||
Run without a command it shows an interactive menu. Everything it can be told
|
||||
is reachable from there; the configuration file behind it does not need to be
|
||||
edited by hand.
|
||||
|
||||
Nothing outside the user's home directory is written to, and running it as root
|
||||
is refused.
|
||||
|
||||
# COMMANDS
|
||||
|
||||
*enable*
|
||||
Turn autoscroll on, apply it to everything installed, and start watching
|
||||
for new applications.
|
||||
|
||||
*disable*
|
||||
Turn it off and put every file that was changed back the way it was.
|
||||
|
||||
*apply*
|
||||
Apply to anything that has appeared since the last run. This is what the
|
||||
watcher calls; running it by hand is only needed when the watcher is off.
|
||||
|
||||
*status*
|
||||
How many applications are covered, whether Steam is patched, and when the
|
||||
last run was.
|
||||
|
||||
*list*
|
||||
Every Chromium-based application that was found, and how each one is
|
||||
handled.
|
||||
|
||||
*-h*, *--help*
|
||||
Show a summary of the commands.
|
||||
|
||||
*-V*, *--version*
|
||||
Show the version.
|
||||
|
||||
# HOW THE ARGUMENT GETS IN
|
||||
|
||||
Two ways, chosen per application.
|
||||
|
||||
*Flag file*
|
||||
Arch's Electron and Chromium wrappers read extra arguments from
|
||||
_$XDG_CONFIG_HOME/<name>-flags.conf_. This is the preferred route: it is
|
||||
the supported way to pass arguments, it survives package upgrades, and it
|
||||
applies to a launch from a terminal as much as one from the menu. An
|
||||
*--enable-blink-features* line that is already in the file is extended
|
||||
rather than duplicated - Chromium keeps only the last occurrence of that
|
||||
option, so a second one would switch the first one off.
|
||||
|
||||
*Desktop entry*
|
||||
For applications that ship their own binary with no wrapper, and for
|
||||
Flatpaks, a copy of the desktop entry with the argument appended is written
|
||||
to _~/.local/share/applications_, where it shadows the system one. Entries
|
||||
that already live there - AppImages, web app shortcuts - are edited in
|
||||
place, with the original kept. Every generated entry is marked with
|
||||
*X-MCA-Generated*; an entry that is not marked is somebody else's and is
|
||||
left alone.
|
||||
|
||||
Programs that start themselves at login write their own entry into
|
||||
_~/.config/autostart_ pointing straight at their binary, bypassing the menu
|
||||
entry entirely. Those are patched in place as well.
|
||||
|
||||
# STEAM
|
||||
|
||||
Steam's interface is CEF and supports the feature, but Steam builds the command
|
||||
line for its web helper itself. The only way in is the script that starts the
|
||||
helper, inside Steam's own installation:
|
||||
|
||||
~/.local/share/Steam/ubuntu12_64/steamwebhelper_sniper_wrap.sh
|
||||
|
||||
Steam checksums that script at every start and restores it when it differs, so
|
||||
its launcher entry also gets *-noverifyfiles*. The trade-off is that Steam no
|
||||
longer repairs a damaged installation on its own; that is why Steam is a switch
|
||||
of its own in the settings.
|
||||
|
||||
Starting Steam from a terminal without *-noverifyfiles* undoes the patch for
|
||||
that session. The watcher notices and puts it back, so the next start has it
|
||||
again.
|
||||
|
||||
# SPOTIFY
|
||||
|
||||
The official client is CEF rather than Electron. Installed through
|
||||
*spotify-launcher*, it is started by a program that builds its own command line
|
||||
and has a configuration file with a slot for extra arguments; that slot is
|
||||
where the flag goes. Installed as a plain package, it is an ordinary desktop
|
||||
entry and needs nothing special.
|
||||
|
||||
# WHAT CANNOT BE DETECTED
|
||||
|
||||
An AppImage keeps its payload in a compressed filesystem, so there is no way to
|
||||
tell from the outside whether it contains Chromium. Those are listed as *cannot
|
||||
tell* and left alone until they are switched on from the applications screen.
|
||||
|
||||
# FILES
|
||||
|
||||
_~/.config/middleclick-autoscroll/config_
|
||||
Written by the program. Every option in it is reachable from the menu.
|
||||
|
||||
_~/.local/state/middleclick-autoscroll/ledger_
|
||||
One line per change, so *disable* can undo exactly what was done and
|
||||
nothing else.
|
||||
|
||||
_~/.local/state/middleclick-autoscroll/backup/_
|
||||
Copies of the files that are edited in place rather than shadowed.
|
||||
|
||||
_~/.cache/middleclick-autoscroll/detect_
|
||||
Which programs were found to be Chromium, keyed by size and modification
|
||||
time. Safe to delete.
|
||||
|
||||
# ENVIRONMENT
|
||||
|
||||
*NO_COLOR*
|
||||
Disables colour.
|
||||
|
||||
# SEE ALSO
|
||||
|
||||
*systemctl*(1), *flatpak*(1)
|
||||
|
||||
# AUTHORS
|
||||
|
||||
Felitendo. Source and issue tracker at
|
||||
https://github.com/Felitendo/middleclick-autoscroll
|
||||
@@ -0,0 +1,226 @@
|
||||
# Translation template for middleclick-autoscroll.
|
||||
# Copyright (C) 2026 Felitendo
|
||||
# This file is distributed under the same license as middleclick-autoscroll.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: middleclick-autoscroll 1.0.0\n"
|
||||
"Report-Msgid-Bugs-To: https://github.com/Felitendo/middleclick-autoscroll/issues\n"
|
||||
"POT-Creation-Date: 2026-08-16 00:00+0200\n"
|
||||
"PO-Revision-Date: 2026-08-16 00:00+0200\n"
|
||||
"Last-Translator: Felitendo\n"
|
||||
"Language-Team: German\n"
|
||||
"Language: de\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#. Status labels
|
||||
msgid "ON"
|
||||
msgstr "AN"
|
||||
|
||||
msgid "OFF"
|
||||
msgstr "AUS"
|
||||
|
||||
msgid "on"
|
||||
msgstr "an"
|
||||
|
||||
msgid "off"
|
||||
msgstr "aus"
|
||||
|
||||
msgid "(none)"
|
||||
msgstr "(keine)"
|
||||
|
||||
msgid "flag file"
|
||||
msgstr "Flag-Datei"
|
||||
|
||||
msgid "launcher"
|
||||
msgstr "Startereintrag"
|
||||
|
||||
msgid "cannot tell"
|
||||
msgstr "nicht erkennbar"
|
||||
|
||||
msgid "not patched yet"
|
||||
msgstr "noch nicht gepatcht"
|
||||
|
||||
|
||||
#. Status block
|
||||
msgid "Autoscroll"
|
||||
msgstr "Autoscroll"
|
||||
|
||||
msgid "Applications covered"
|
||||
msgstr "Abgedeckte Anwendungen"
|
||||
|
||||
msgid "%d of %d"
|
||||
msgstr "%d von %d"
|
||||
|
||||
msgid "Not identified"
|
||||
msgstr "Nicht erkannt"
|
||||
|
||||
msgid "%d - see the applications list"
|
||||
msgstr "%d – siehe Anwendungsliste"
|
||||
|
||||
msgid "Steam"
|
||||
msgstr "Steam"
|
||||
|
||||
msgid "New applications"
|
||||
msgstr "Neue Anwendungen"
|
||||
|
||||
msgid "Last applied"
|
||||
msgstr "Zuletzt angewendet"
|
||||
|
||||
msgid "Applications pick this up the next time they are started."
|
||||
msgstr "Anwendungen übernehmen das beim nächsten Start."
|
||||
|
||||
|
||||
#. Menu
|
||||
msgid "Turn autoscroll on or off"
|
||||
msgstr "Autoscroll ein- oder ausschalten"
|
||||
|
||||
msgid "Apply now"
|
||||
msgstr "Jetzt anwenden"
|
||||
|
||||
msgid "Applications"
|
||||
msgstr "Anwendungen"
|
||||
|
||||
msgid "Settings"
|
||||
msgstr "Einstellungen"
|
||||
|
||||
msgid "Quit"
|
||||
msgstr "Beenden"
|
||||
|
||||
msgid "Press any key to continue..."
|
||||
msgstr "Beliebige Taste drücken …"
|
||||
|
||||
|
||||
#. Settings
|
||||
msgid "Electron and CEF applications"
|
||||
msgstr "Electron- und CEF-Anwendungen"
|
||||
|
||||
msgid "Chromium-based browsers"
|
||||
msgstr "Chromium-basierte Browser"
|
||||
|
||||
msgid "Flatpak applications"
|
||||
msgstr "Flatpak-Anwendungen"
|
||||
|
||||
msgid "Programs that start themselves at login"
|
||||
msgstr "Programme, die sich beim Anmelden selbst starten"
|
||||
|
||||
msgid "Spotify"
|
||||
msgstr "Spotify"
|
||||
|
||||
msgid "Apply to newly installed applications"
|
||||
msgstr "Auf neu installierte Anwendungen anwenden"
|
||||
|
||||
msgid "Additional Chromium arguments"
|
||||
msgstr "Zusätzliche Chromium-Argumente"
|
||||
|
||||
msgid "Up/Down select - Space or Right changes - q goes back"
|
||||
msgstr "Hoch/Runter wählt – Leertaste oder Rechts ändert – q geht zurück"
|
||||
|
||||
msgid "Arguments separated by spaces, empty for none:"
|
||||
msgstr "Argumente mit Leerzeichen getrennt, leer für keine:"
|
||||
|
||||
msgid "Could not save the setting."
|
||||
msgstr "Einstellung konnte nicht gespeichert werden."
|
||||
|
||||
|
||||
#. Applications screen
|
||||
msgid "Looking at the installed applications..."
|
||||
msgstr "Installierte Anwendungen werden durchgesehen …"
|
||||
|
||||
msgid "No Chromium-based applications found."
|
||||
msgstr "Keine Chromium-basierten Anwendungen gefunden."
|
||||
|
||||
msgid "Anything not identified is left alone until it is turned on here."
|
||||
msgstr "Nicht erkannte Anwendungen bleiben unangetastet, bis sie hier eingeschaltet werden."
|
||||
|
||||
msgid "Autoscroll is off - this is what would be covered."
|
||||
msgstr "Autoscroll ist aus – das hier wäre abgedeckt."
|
||||
|
||||
msgid "Up/Down select - Space turns one on or off - q goes back"
|
||||
msgstr "Hoch/Runter wählt – Leertaste schaltet einzeln ein oder aus – q geht zurück"
|
||||
|
||||
|
||||
#. Results
|
||||
msgid "Autoscroll is on for %d applications."
|
||||
msgstr "Autoscroll ist für %d Anwendungen an."
|
||||
|
||||
msgid "Applications that are already running pick this up when they are restarted."
|
||||
msgstr "Bereits laufende Anwendungen übernehmen das nach einem Neustart."
|
||||
|
||||
msgid "Steam's launcher now skips its file check, which is what keeps the change from being undone at every start."
|
||||
msgstr "Steams Startereintrag überspringt jetzt die Dateiprüfung – nur so bleibt die Änderung bei jedem Start erhalten."
|
||||
|
||||
msgid "Could not start the watcher for newly installed applications."
|
||||
msgstr "Die Überwachung neu installierter Anwendungen konnte nicht gestartet werden."
|
||||
|
||||
msgid "No systemd user session - newly installed applications will not be picked up on their own."
|
||||
msgstr "Keine systemd-Benutzersitzung – neu installierte Anwendungen werden nicht von selbst erfasst."
|
||||
|
||||
msgid "Autoscroll is off and everything that was changed has been put back."
|
||||
msgstr "Autoscroll ist aus, alle Änderungen wurden zurückgenommen."
|
||||
|
||||
msgid "Autoscroll is off. Turn it on first."
|
||||
msgstr "Autoscroll ist aus. Erst einschalten."
|
||||
|
||||
msgid "Applied to %d applications."
|
||||
msgstr "Auf %d Anwendungen angewendet."
|
||||
|
||||
msgid "Already applied to %d applications."
|
||||
msgstr "Bereits auf %d Anwendungen angewendet."
|
||||
|
||||
msgid "Steam starts its interface in a way this version does not recognise; leaving it alone."
|
||||
msgstr "Steam startet seine Oberfläche auf eine Art, die diese Version nicht kennt; Steam bleibt unangetastet."
|
||||
|
||||
|
||||
#. Help
|
||||
msgid "Usage: middleclick-autoscroll [command]"
|
||||
msgstr "Aufruf: middleclick-autoscroll [Befehl]"
|
||||
|
||||
msgid "Commands:"
|
||||
msgstr "Befehle:"
|
||||
|
||||
msgid "Turn autoscroll on"
|
||||
msgstr "Autoscroll einschalten"
|
||||
|
||||
msgid "Turn it off and put everything back"
|
||||
msgstr "Ausschalten und alles zurücknehmen"
|
||||
|
||||
msgid "Apply to anything installed since the last run"
|
||||
msgstr "Auf alles anwenden, was seit dem letzten Lauf dazugekommen ist"
|
||||
|
||||
msgid "Show what is covered"
|
||||
msgstr "Anzeigen, was abgedeckt ist"
|
||||
|
||||
msgid "List every application that was found"
|
||||
msgstr "Alle gefundenen Anwendungen auflisten"
|
||||
|
||||
msgid "Show this help"
|
||||
msgstr "Diese Hilfe anzeigen"
|
||||
|
||||
msgid "Show the version"
|
||||
msgstr "Version anzeigen"
|
||||
|
||||
msgid "Without a command an interactive menu is shown."
|
||||
msgstr "Ohne Befehl wird ein interaktives Menü angezeigt."
|
||||
|
||||
msgid "Unknown command: %s"
|
||||
msgstr "Unbekannter Befehl: %s"
|
||||
|
||||
|
||||
#. Time
|
||||
msgid "never"
|
||||
msgstr "nie"
|
||||
|
||||
msgid "just now"
|
||||
msgstr "gerade eben"
|
||||
|
||||
msgid "%d minutes ago"
|
||||
msgstr "vor %d Minuten"
|
||||
|
||||
msgid "%d hours ago"
|
||||
msgstr "vor %d Stunden"
|
||||
|
||||
msgid "%d days ago"
|
||||
msgstr "vor %d Tagen"
|
||||
@@ -0,0 +1,226 @@
|
||||
# Translation template for middleclick-autoscroll.
|
||||
# Copyright (C) 2026 Felitendo
|
||||
# This file is distributed under the same license as middleclick-autoscroll.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: middleclick-autoscroll 1.0.0\n"
|
||||
"Report-Msgid-Bugs-To: https://github.com/Felitendo/middleclick-autoscroll/issues\n"
|
||||
"POT-Creation-Date: 2026-08-16 00:00+0200\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#. Status labels
|
||||
msgid "ON"
|
||||
msgstr ""
|
||||
|
||||
msgid "OFF"
|
||||
msgstr ""
|
||||
|
||||
msgid "on"
|
||||
msgstr ""
|
||||
|
||||
msgid "off"
|
||||
msgstr ""
|
||||
|
||||
msgid "(none)"
|
||||
msgstr ""
|
||||
|
||||
msgid "flag file"
|
||||
msgstr ""
|
||||
|
||||
msgid "launcher"
|
||||
msgstr ""
|
||||
|
||||
msgid "cannot tell"
|
||||
msgstr ""
|
||||
|
||||
msgid "not patched yet"
|
||||
msgstr ""
|
||||
|
||||
|
||||
#. Status block
|
||||
msgid "Autoscroll"
|
||||
msgstr ""
|
||||
|
||||
msgid "Applications covered"
|
||||
msgstr ""
|
||||
|
||||
msgid "%d of %d"
|
||||
msgstr ""
|
||||
|
||||
msgid "Not identified"
|
||||
msgstr ""
|
||||
|
||||
msgid "%d - see the applications list"
|
||||
msgstr ""
|
||||
|
||||
msgid "Steam"
|
||||
msgstr ""
|
||||
|
||||
msgid "New applications"
|
||||
msgstr ""
|
||||
|
||||
msgid "Last applied"
|
||||
msgstr ""
|
||||
|
||||
msgid "Applications pick this up the next time they are started."
|
||||
msgstr ""
|
||||
|
||||
|
||||
#. Menu
|
||||
msgid "Turn autoscroll on or off"
|
||||
msgstr ""
|
||||
|
||||
msgid "Apply now"
|
||||
msgstr ""
|
||||
|
||||
msgid "Applications"
|
||||
msgstr ""
|
||||
|
||||
msgid "Settings"
|
||||
msgstr ""
|
||||
|
||||
msgid "Quit"
|
||||
msgstr ""
|
||||
|
||||
msgid "Press any key to continue..."
|
||||
msgstr ""
|
||||
|
||||
|
||||
#. Settings
|
||||
msgid "Electron and CEF applications"
|
||||
msgstr ""
|
||||
|
||||
msgid "Chromium-based browsers"
|
||||
msgstr ""
|
||||
|
||||
msgid "Flatpak applications"
|
||||
msgstr ""
|
||||
|
||||
msgid "Programs that start themselves at login"
|
||||
msgstr ""
|
||||
|
||||
msgid "Spotify"
|
||||
msgstr ""
|
||||
|
||||
msgid "Apply to newly installed applications"
|
||||
msgstr ""
|
||||
|
||||
msgid "Additional Chromium arguments"
|
||||
msgstr ""
|
||||
|
||||
msgid "Up/Down select - Space or Right changes - q goes back"
|
||||
msgstr ""
|
||||
|
||||
msgid "Arguments separated by spaces, empty for none:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Could not save the setting."
|
||||
msgstr ""
|
||||
|
||||
|
||||
#. Applications screen
|
||||
msgid "Looking at the installed applications..."
|
||||
msgstr ""
|
||||
|
||||
msgid "No Chromium-based applications found."
|
||||
msgstr ""
|
||||
|
||||
msgid "Anything not identified is left alone until it is turned on here."
|
||||
msgstr ""
|
||||
|
||||
msgid "Autoscroll is off - this is what would be covered."
|
||||
msgstr ""
|
||||
|
||||
msgid "Up/Down select - Space turns one on or off - q goes back"
|
||||
msgstr ""
|
||||
|
||||
|
||||
#. Results
|
||||
msgid "Autoscroll is on for %d applications."
|
||||
msgstr ""
|
||||
|
||||
msgid "Applications that are already running pick this up when they are restarted."
|
||||
msgstr ""
|
||||
|
||||
msgid "Steam's launcher now skips its file check, which is what keeps the change from being undone at every start."
|
||||
msgstr ""
|
||||
|
||||
msgid "Could not start the watcher for newly installed applications."
|
||||
msgstr ""
|
||||
|
||||
msgid "No systemd user session - newly installed applications will not be picked up on their own."
|
||||
msgstr ""
|
||||
|
||||
msgid "Autoscroll is off and everything that was changed has been put back."
|
||||
msgstr ""
|
||||
|
||||
msgid "Autoscroll is off. Turn it on first."
|
||||
msgstr ""
|
||||
|
||||
msgid "Applied to %d applications."
|
||||
msgstr ""
|
||||
|
||||
msgid "Already applied to %d applications."
|
||||
msgstr ""
|
||||
|
||||
msgid "Steam starts its interface in a way this version does not recognise; leaving it alone."
|
||||
msgstr ""
|
||||
|
||||
|
||||
#. Help
|
||||
msgid "Usage: middleclick-autoscroll [command]"
|
||||
msgstr ""
|
||||
|
||||
msgid "Commands:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Turn autoscroll on"
|
||||
msgstr ""
|
||||
|
||||
msgid "Turn it off and put everything back"
|
||||
msgstr ""
|
||||
|
||||
msgid "Apply to anything installed since the last run"
|
||||
msgstr ""
|
||||
|
||||
msgid "Show what is covered"
|
||||
msgstr ""
|
||||
|
||||
msgid "List every application that was found"
|
||||
msgstr ""
|
||||
|
||||
msgid "Show this help"
|
||||
msgstr ""
|
||||
|
||||
msgid "Show the version"
|
||||
msgstr ""
|
||||
|
||||
msgid "Without a command an interactive menu is shown."
|
||||
msgstr ""
|
||||
|
||||
msgid "Unknown command: %s"
|
||||
msgstr ""
|
||||
|
||||
|
||||
#. Time
|
||||
msgid "never"
|
||||
msgstr ""
|
||||
|
||||
msgid "just now"
|
||||
msgstr ""
|
||||
|
||||
msgid "%d minutes ago"
|
||||
msgstr ""
|
||||
|
||||
msgid "%d hours ago"
|
||||
msgstr ""
|
||||
|
||||
msgid "%d days ago"
|
||||
msgstr ""
|
||||
@@ -0,0 +1,25 @@
|
||||
[Unit]
|
||||
Description=Watch for newly installed Chromium-based applications
|
||||
Documentation=man:middleclick-autoscroll(1)
|
||||
|
||||
[Path]
|
||||
# Every way an application can turn up: a package, a Flatpak, something
|
||||
# installed by hand. Watching where the launchers land means there is no hook
|
||||
# to write per package manager - pacman, flatpak and an AppImage manager all
|
||||
# end up here.
|
||||
PathModified=/usr/share/applications
|
||||
PathModified=/usr/local/share/applications
|
||||
PathModified=%h/.local/share/applications
|
||||
PathModified=%h/.config/autostart
|
||||
PathModified=/var/lib/flatpak/exports/share/applications
|
||||
PathModified=%h/.local/share/flatpak/exports/share/applications
|
||||
|
||||
# Steam replaces the script that starts its interface on every client update,
|
||||
# which takes the patch with it.
|
||||
PathChanged=%h/.local/share/Steam/ubuntu12_64/steamwebhelper_sniper_wrap.sh
|
||||
PathChanged=%h/.var/app/com.valvesoftware.Steam/.local/share/Steam/ubuntu12_64/steamwebhelper_sniper_wrap.sh
|
||||
|
||||
Unit=middleclick-autoscroll.service
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -0,0 +1,16 @@
|
||||
[Unit]
|
||||
Description=Apply middle-click autoscroll to Chromium-based applications
|
||||
Documentation=man:middleclick-autoscroll(1)
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=@BINDIR@/middleclick-autoscroll apply --quiet
|
||||
# Scanning binaries for their engine is the slow part, and it is cached; a run
|
||||
# that takes longer than this has found something pathological and should not
|
||||
# be left running at every login.
|
||||
TimeoutStartSec=180
|
||||
Nice=10
|
||||
IOSchedulingClass=idle
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -0,0 +1,211 @@
|
||||
# shellcheck shell=bash
|
||||
#
|
||||
# Deciding what happens to each application, and doing it.
|
||||
#
|
||||
# The routing lives in mca_route and is used twice: once by mca_apply to patch
|
||||
# and once by the applications screen to show what would happen. Keeping it in
|
||||
# one function is what makes that screen trustworthy - it is not a description
|
||||
# of the behaviour, it is the behaviour.
|
||||
|
||||
MCA_CHANGES=0
|
||||
MCA_TOUCHED=()
|
||||
MCA_ROUTES=()
|
||||
|
||||
# mca_route <kind> <id> <program>
|
||||
# What happens to one application, left in MCA_ROUTE:
|
||||
# flags - the launcher reads a flag file; write it there
|
||||
# desktop - no flag file; shadow or edit the desktop entry
|
||||
# steam - Steam's own two-part treatment
|
||||
# unknown - cannot tell what engine this is (an AppImage), so nothing is done
|
||||
# off - detected, but switched off
|
||||
#
|
||||
# Assigns rather than prints so the memo inside mca_has_flags_file survives:
|
||||
# from inside a command substitution it would be filled in a subshell and
|
||||
# thrown away, which is the whole point of having it.
|
||||
MCA_ROUTE=''
|
||||
|
||||
mca_route() {
|
||||
local kind="$1" id="$2" prog="$3"
|
||||
|
||||
if [[ $kind == steam ]]; then
|
||||
if mca_config_list_has Skip "$id" || [[ $CFG_STEAM != yes ]]; then
|
||||
MCA_ROUTE=off
|
||||
else
|
||||
MCA_ROUTE=steam
|
||||
fi
|
||||
return
|
||||
fi
|
||||
|
||||
if ! mca_kind_wanted "$kind" "$id"; then
|
||||
[[ $kind == unknown ]] && MCA_ROUTE=unknown || MCA_ROUTE=off
|
||||
return
|
||||
fi
|
||||
|
||||
# A Flatpak has its own copy of everything and none of the host's wrappers,
|
||||
# so the desktop entry is the only way in.
|
||||
if [[ $kind == flatpak ]]; then
|
||||
MCA_ROUTE=desktop
|
||||
return
|
||||
fi
|
||||
|
||||
if mca_has_flags_file "$prog"; then
|
||||
MCA_ROUTE=flags
|
||||
else
|
||||
MCA_ROUTE=desktop
|
||||
fi
|
||||
}
|
||||
|
||||
# mca_apply
|
||||
# Brings everything into line with the current settings. Safe to run as often
|
||||
# as it likes to be - it writes only what differs, which is what keeps the
|
||||
# watcher from chasing its own changes.
|
||||
mca_apply() {
|
||||
local i id file prog kind route steam_done=0
|
||||
|
||||
MCA_CHANGES=0
|
||||
MCA_TOUCHED=()
|
||||
MCA_ROUTES=()
|
||||
MCA_N_ON=0; MCA_N_OFF=0; MCA_N_UNKNOWN=0
|
||||
|
||||
mca_config_load
|
||||
mca_scan
|
||||
|
||||
for i in "${!MCA_IDS[@]}"; do
|
||||
id="${MCA_IDS[i]}"
|
||||
file="${MCA_FILES[i]}"
|
||||
prog="${MCA_PROGS[i]}"
|
||||
kind="${MCA_KINDS[i]}"
|
||||
|
||||
mca_route "$kind" "$id" "$prog"
|
||||
route="$MCA_ROUTE"
|
||||
MCA_ROUTES[i]="$route"
|
||||
|
||||
case "$route" in
|
||||
unknown) MCA_N_UNKNOWN=$(( MCA_N_UNKNOWN + 1 )) ;;
|
||||
off) MCA_N_OFF=$(( MCA_N_OFF + 1 )) ;;
|
||||
*) MCA_N_ON=$(( MCA_N_ON + 1 )) ;;
|
||||
esac
|
||||
|
||||
case "$route" in
|
||||
flags)
|
||||
mca_flags_apply "$prog" || mca_desktop_apply "$id" "$file"
|
||||
;;
|
||||
desktop)
|
||||
mca_desktop_apply "$id" "$file"
|
||||
;;
|
||||
steam)
|
||||
mca_steam_desktop_apply "$id" "$file"
|
||||
if (( ! steam_done )); then
|
||||
mca_steam_apply
|
||||
steam_done=1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Steam is worth patching even when its desktop entry is missing - a user
|
||||
# who starts it from a script or a game launcher still gets the interface.
|
||||
if [[ $CFG_STEAM == yes ]] && (( ! steam_done )) && mca_steam_installed; then
|
||||
mca_steam_apply
|
||||
fi
|
||||
|
||||
[[ $CFG_AUTOSTART == yes ]] && mca_autostart_apply
|
||||
[[ $CFG_SPOTIFY == yes ]] && mca_spotify_apply
|
||||
|
||||
if (( MCA_CHANGES )) && mca_have update-desktop-database; then
|
||||
update-desktop-database "$MCA_APPDIR" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
mca_state_write last_apply "$(date +%s)"
|
||||
mca_state_write covered "${#MCA_TOUCHED[@]}"
|
||||
return 0
|
||||
}
|
||||
|
||||
# mca_revert
|
||||
# Undoes every recorded change and forgets them.
|
||||
mca_revert() {
|
||||
MCA_CHANGES=0
|
||||
mca_revert_all
|
||||
|
||||
if (( MCA_CHANGES )) && mca_have update-desktop-database; then
|
||||
update-desktop-database "$MCA_APPDIR" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
mca_state_write covered 0
|
||||
return 0
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The watcher
|
||||
# ---------------------------------------------------------------------------
|
||||
# A systemd user path unit watching every directory a desktop entry can appear
|
||||
# in. That covers a package installed with pacman, a Flatpak, an AppImage
|
||||
# registered by hand and a Steam client update, without a hook per package
|
||||
# manager.
|
||||
|
||||
MCA_UNIT_PATH="middleclick-autoscroll.path"
|
||||
MCA_UNIT_SERVICE="middleclick-autoscroll.service"
|
||||
|
||||
mca_watch_available() {
|
||||
mca_have systemctl && [[ -n ${XDG_RUNTIME_DIR:-} || -S "/run/user/$(id -u)/bus" ]]
|
||||
}
|
||||
|
||||
mca_watch_enabled() {
|
||||
mca_watch_available || return 1
|
||||
systemctl --user is-enabled --quiet "$MCA_UNIT_PATH" 2>/dev/null
|
||||
}
|
||||
|
||||
mca_watch_enable() {
|
||||
mca_watch_available || return 1
|
||||
systemctl --user enable --now "$MCA_UNIT_PATH" > /dev/null 2>&1 || return 1
|
||||
systemctl --user enable "$MCA_UNIT_SERVICE" > /dev/null 2>&1 || true
|
||||
return 0
|
||||
}
|
||||
|
||||
mca_watch_disable() {
|
||||
mca_watch_available || return 0
|
||||
systemctl --user disable --now "$MCA_UNIT_PATH" > /dev/null 2>&1 || true
|
||||
systemctl --user disable "$MCA_UNIT_SERVICE" > /dev/null 2>&1 || true
|
||||
return 0
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Reporting
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# mca_count_routes
|
||||
# Fills MCA_N_ON / MCA_N_OFF / MCA_N_UNKNOWN from a scan, without writing
|
||||
# anything. Used by the status block and the menu header.
|
||||
MCA_N_ON=0
|
||||
MCA_N_OFF=0
|
||||
MCA_N_UNKNOWN=0
|
||||
|
||||
mca_count_routes() {
|
||||
local i route
|
||||
|
||||
MCA_N_ON=0; MCA_N_OFF=0; MCA_N_UNKNOWN=0
|
||||
MCA_ROUTES=()
|
||||
|
||||
for i in "${!MCA_IDS[@]}"; do
|
||||
mca_route "${MCA_KINDS[i]}" "${MCA_IDS[i]}" "${MCA_PROGS[i]}"
|
||||
route="$MCA_ROUTE"
|
||||
MCA_ROUTES[i]="$route"
|
||||
case "$route" in
|
||||
unknown) MCA_N_UNKNOWN=$(( MCA_N_UNKNOWN + 1 )) ;;
|
||||
off) MCA_N_OFF=$(( MCA_N_OFF + 1 )) ;;
|
||||
*) MCA_N_ON=$(( MCA_N_ON + 1 )) ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# mca_route_label <route>
|
||||
mca_route_label() {
|
||||
case "$1" in
|
||||
flags) mca_msg "flag file" ;;
|
||||
desktop) mca_msg "launcher" ;;
|
||||
steam) mca_msg "Steam" ;;
|
||||
unknown) mca_msg "cannot tell" ;;
|
||||
*) mca_msg "off" ;;
|
||||
esac
|
||||
printf '\n'
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
# shellcheck shell=bash
|
||||
#
|
||||
# Paths, translations, output helpers and the change ledger.
|
||||
#
|
||||
# Everything here runs unprivileged. This tool only ever writes inside the
|
||||
# user's own home - flag files, desktop entries, Steam's own scripts - so there
|
||||
# is no system state to guard and nothing that needs root.
|
||||
|
||||
MCA_VERSION="@VERSION@"
|
||||
MCA_NAME="middleclick-autoscroll"
|
||||
MCA_PRETTY="Middle-Click Autoscroll"
|
||||
|
||||
# The whole point of the package. Blink implements Windows-style autoscroll
|
||||
# behind a runtime flag that Chromium does not turn on for Linux, because
|
||||
# middle click is taken by primary-selection paste there.
|
||||
MCA_FLAG="--enable-blink-features=MiddleClickAutoscroll"
|
||||
|
||||
# The Blink feature name on its own, for merging into an --enable-blink-features
|
||||
# list that an app (or the user) already carries.
|
||||
MCA_FEATURE="MiddleClickAutoscroll"
|
||||
|
||||
MCA_LIBDIR="${MCA_LIBDIR:-@LIBDIR@}"
|
||||
MCA_LOCALEDIR="${MCA_LOCALEDIR:-@LOCALEDIR@}"
|
||||
|
||||
MCA_XDG_CONFIG="${XDG_CONFIG_HOME:-$HOME/.config}"
|
||||
MCA_XDG_DATA="${XDG_DATA_HOME:-$HOME/.local/share}"
|
||||
MCA_XDG_STATE="${XDG_STATE_HOME:-$HOME/.local/state}"
|
||||
MCA_XDG_CACHE="${XDG_CACHE_HOME:-$HOME/.cache}"
|
||||
|
||||
MCA_CONFDIR="${MCA_CONFDIR:-${MCA_XDG_CONFIG}/${MCA_NAME}}"
|
||||
MCA_CONFIG="${MCA_CONFIG:-${MCA_CONFDIR}/config}"
|
||||
MCA_STATEDIR="${MCA_STATEDIR:-${MCA_XDG_STATE}/${MCA_NAME}}"
|
||||
MCA_CACHEDIR="${MCA_CACHEDIR:-${MCA_XDG_CACHE}/${MCA_NAME}}"
|
||||
|
||||
# Where generated desktop entries go. A file here shadows the one with the same
|
||||
# name in /usr/share/applications, which is how an application gets extra
|
||||
# command line arguments without touching anything pacman owns.
|
||||
MCA_APPDIR="${MCA_XDG_DATA}/applications"
|
||||
|
||||
# Copies of every file that is edited in place rather than shadowed.
|
||||
MCA_BACKUPDIR="${MCA_STATEDIR}/backup"
|
||||
|
||||
# One line per change, so `revert` can undo exactly what was done and nothing
|
||||
# else. See mca_ledger_add.
|
||||
MCA_LEDGER="${MCA_STATEDIR}/ledger"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Translations
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
export TEXTDOMAIN="middleclick-autoscroll"
|
||||
export TEXTDOMAINDIR="${MCA_LOCALEDIR}"
|
||||
|
||||
mca_ui_locale() {
|
||||
local l="${MCA_UI_LOCALE:-}"
|
||||
|
||||
if [[ -z $l ]]; then
|
||||
l="${LC_ALL:-}"
|
||||
[[ -z $l ]] && l="${LC_MESSAGES:-}"
|
||||
[[ -z $l ]] && l="${LANG:-}"
|
||||
fi
|
||||
|
||||
if [[ -z $l && -r /etc/locale.conf ]]; then
|
||||
l="$(sed -n 's/^LANG=//p' /etc/locale.conf | tr -d '"' | head -n1)"
|
||||
fi
|
||||
|
||||
printf '%s\n' "${l:-C}"
|
||||
}
|
||||
|
||||
# Every gettext lookup is a fork and the settings screen redraws a screenful of
|
||||
# labels per keypress, so results are memoized.
|
||||
declare -A MCA_MSG_CACHE=()
|
||||
|
||||
MCA_MSG_RESULT=''
|
||||
|
||||
# mca_msg_into <locale> <msgid>
|
||||
# Plain lookup with the result in MCA_MSG_RESULT and no printf formatting, for
|
||||
# callers that would otherwise pay a fork per label per frame.
|
||||
mca_msg_into() {
|
||||
local locale="$1" msgid="$2" cachekey
|
||||
cachekey="${locale}"$'\x1f'"${msgid}"
|
||||
|
||||
if [[ -n ${MCA_MSG_CACHE[$cachekey]+set} ]]; then
|
||||
MCA_MSG_RESULT="${MCA_MSG_CACHE[$cachekey]}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
MCA_MSG_RESULT="$(LC_ALL="$locale" LANGUAGE="${locale%%.*}" gettext -- "$msgid" 2>/dev/null)"
|
||||
[[ -n $MCA_MSG_RESULT ]] || MCA_MSG_RESULT="$msgid"
|
||||
MCA_MSG_CACHE[$cachekey]="$MCA_MSG_RESULT"
|
||||
return 0
|
||||
}
|
||||
|
||||
# mca_msg_in <locale> <msgid> [printf args...]
|
||||
mca_msg_in() {
|
||||
local locale="$1" msgid="$2" translated cachekey
|
||||
shift 2
|
||||
|
||||
cachekey="${locale}"$'\x1f'"${msgid}"
|
||||
if [[ -n ${MCA_MSG_CACHE[$cachekey]+set} ]]; then
|
||||
translated="${MCA_MSG_CACHE[$cachekey]}"
|
||||
else
|
||||
translated="$(LC_ALL="$locale" LANGUAGE="${locale%%.*}" gettext -- "$msgid" 2>/dev/null)"
|
||||
[[ -n $translated ]] || translated="$msgid"
|
||||
MCA_MSG_CACHE[$cachekey]="$translated"
|
||||
fi
|
||||
|
||||
# With no arguments the message is plain text, not a format string. Feeding
|
||||
# it to printf anyway would turn a literal percent sign in a translation
|
||||
# into an invalid conversion.
|
||||
if (( $# == 0 )); then
|
||||
printf '%s' "$translated"
|
||||
return
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC2059 # the format string is the translated message
|
||||
printf -- "$translated" "$@"
|
||||
}
|
||||
|
||||
# mca_msg <msgid> [printf args...]
|
||||
mca_msg() { mca_msg_in "$(mca_ui_locale)" "$@"; }
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Output
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Decided once, while stdout is still whatever the process was started with:
|
||||
# testing -t 1 at the point of use is wrong for anything called through $(...),
|
||||
# which sees a pipe and would conclude nobody is watching.
|
||||
MCA_INTERACTIVE=''
|
||||
[[ -t 1 ]] && MCA_INTERACTIVE=1
|
||||
|
||||
if [[ -n $MCA_INTERACTIVE && -z ${NO_COLOR:-} ]]; then
|
||||
MCA_C_RESET=$'\033[0m'
|
||||
MCA_C_BOLD=$'\033[1m'
|
||||
MCA_C_DIM=$'\033[2m'
|
||||
MCA_C_BLUE=$'\033[38;2;23;147;209m'
|
||||
MCA_C_GREEN=$'\033[32m'
|
||||
MCA_C_YELLOW=$'\033[33m'
|
||||
MCA_C_RED=$'\033[31m'
|
||||
else
|
||||
MCA_C_RESET='' MCA_C_BOLD='' MCA_C_DIM='' MCA_C_BLUE=''
|
||||
MCA_C_GREEN='' MCA_C_YELLOW='' MCA_C_RED=''
|
||||
fi
|
||||
|
||||
# Set by mca_bad and mca_note. The menu redraws straight after an action, which
|
||||
# would wipe the screen; this marks that something was printed the user still
|
||||
# has to read. A plain success needs no acknowledgement - the status block at
|
||||
# the top of the menu already shows the new state.
|
||||
MCA_UI_NEEDS_ACK=''
|
||||
|
||||
# --quiet silences progress chatter; errors still go to stderr. Used by the
|
||||
# systemd unit, which has no terminal and logs to the journal anyway.
|
||||
MCA_QUIET=''
|
||||
|
||||
mca_say() { [[ -n $MCA_QUIET ]] || printf '%s\n' "$*"; }
|
||||
mca_head() { printf '\n%s%s%s\n\n' "$MCA_C_BOLD$MCA_C_BLUE" "$*" "$MCA_C_RESET"; }
|
||||
mca_ok() { [[ -n $MCA_QUIET ]] || printf '%s✔%s %s\n' "$MCA_C_GREEN" "$MCA_C_RESET" "$*"; }
|
||||
mca_bad() { MCA_UI_NEEDS_ACK=1; printf '%s✘%s %s\n' "$MCA_C_RED" "$MCA_C_RESET" "$*" >&2; }
|
||||
mca_note() { MCA_UI_NEEDS_ACK=1; [[ -n $MCA_QUIET ]] || printf '%s•%s %s\n' "$MCA_C_DIM" "$MCA_C_RESET" "$*"; }
|
||||
mca_debug() { [[ -n ${MCA_DEBUG:-} ]] && printf 'debug: %s\n' "$*" >&2; return 0; }
|
||||
|
||||
mca_have() { command -v "$1" > /dev/null 2>&1; }
|
||||
|
||||
# Human-readable "x minutes ago" for a unix timestamp. Empty input yields the
|
||||
# translated "never".
|
||||
mca_time_ago() {
|
||||
local ts="$1" now delta
|
||||
|
||||
[[ $ts =~ ^[0-9]+$ ]] || { mca_msg "never"; printf '\n'; return; }
|
||||
|
||||
now="$(date +%s)"
|
||||
delta=$(( now - ts ))
|
||||
(( delta < 0 )) && delta=0
|
||||
|
||||
if (( delta < 60 )); then mca_msg "just now"
|
||||
elif (( delta < 3600 )); then mca_msg "%d minutes ago" "$(( delta / 60 ))"
|
||||
elif (( delta < 86400 )); then mca_msg "%d hours ago" "$(( delta / 3600 ))"
|
||||
else mca_msg "%d days ago" "$(( delta / 86400 ))"
|
||||
fi
|
||||
printf '\n'
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# State
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
mca_state_read() {
|
||||
local key="$1" default="${2:-}"
|
||||
if [[ -r "$MCA_STATEDIR/$key" ]]; then
|
||||
cat "$MCA_STATEDIR/$key"
|
||||
else
|
||||
printf '%s\n' "$default"
|
||||
fi
|
||||
}
|
||||
|
||||
mca_state_write() {
|
||||
local key="$1"
|
||||
shift
|
||||
mkdir -p "$MCA_STATEDIR" 2>/dev/null || return 1
|
||||
printf '%s\n' "$*" > "$MCA_STATEDIR/$key"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The change ledger
|
||||
# ---------------------------------------------------------------------------
|
||||
# Reverting by pattern - "delete every desktop entry that mentions the flag" -
|
||||
# would also delete entries the user wrote by hand. So every change is recorded
|
||||
# instead, and `revert` replays the ledger backwards.
|
||||
#
|
||||
# Format, tab separated:
|
||||
# <kind> <path> <detail>
|
||||
#
|
||||
# shadow <generated desktop entry> <source entry it was generated from>
|
||||
# inplace <edited file> <basename of its backup copy>
|
||||
# flags <flag file> created | appended | merged
|
||||
#
|
||||
# `inplace` and `flags` differ in how they are undone: a backup is restored
|
||||
# wholesale, a flag file only loses the one line that was added to it.
|
||||
|
||||
mca_ledger_add() {
|
||||
local kind="$1" path="$2" detail="${3:-}"
|
||||
mkdir -p "$MCA_STATEDIR" 2>/dev/null || return 1
|
||||
|
||||
# Never record the same path twice: applying repeatedly is normal (the path
|
||||
# unit fires on every desktop file that appears) and the ledger has to stay
|
||||
# a set, not a log.
|
||||
mca_ledger_forget "$path"
|
||||
printf '%s\t%s\t%s\n' "$kind" "$path" "$detail" >> "$MCA_LEDGER"
|
||||
}
|
||||
|
||||
mca_ledger_forget() {
|
||||
local path="$1" tmp
|
||||
[[ -f $MCA_LEDGER ]] || return 0
|
||||
|
||||
tmp="$(mktemp "${MCA_LEDGER}.XXXXXX")" || return 1
|
||||
awk -F'\t' -v p="$path" '$2 != p' "$MCA_LEDGER" > "$tmp" 2>/dev/null \
|
||||
&& mv -f "$tmp" "$MCA_LEDGER" || rm -f "$tmp"
|
||||
return 0
|
||||
}
|
||||
|
||||
# mca_ledger_has <path>
|
||||
mca_ledger_has() {
|
||||
[[ -f $MCA_LEDGER ]] || return 1
|
||||
awk -F'\t' -v p="$1" '$2 == p { found = 1 } END { exit !found }' "$MCA_LEDGER"
|
||||
}
|
||||
|
||||
# mca_ledger_detail <path>
|
||||
mca_ledger_detail() {
|
||||
[[ -f $MCA_LEDGER ]] || return 1
|
||||
awk -F'\t' -v p="$1" '$2 == p { print $3; exit }' "$MCA_LEDGER"
|
||||
}
|
||||
|
||||
mca_ledger_count() {
|
||||
[[ -f $MCA_LEDGER ]] || { printf '0\n'; return; }
|
||||
grep -c . "$MCA_LEDGER" 2>/dev/null || printf '0\n'
|
||||
}
|
||||
|
||||
# mca_backup <file>
|
||||
# Copies a file aside before it is edited in place, and prints the name the
|
||||
# copy was stored under. Existing backups are never overwritten: the first copy
|
||||
# is the pristine one, and a second apply must not replace it with an already
|
||||
# patched version.
|
||||
mca_backup() {
|
||||
local file="$1" name
|
||||
name="$(printf '%s' "$file" | sed 's|/|%|g')"
|
||||
|
||||
mkdir -p "$MCA_BACKUPDIR" 2>/dev/null || return 1
|
||||
if [[ ! -e "$MCA_BACKUPDIR/$name" ]]; then
|
||||
cp -p -- "$file" "$MCA_BACKUPDIR/$name" 2>/dev/null || return 1
|
||||
fi
|
||||
printf '%s\n' "$name"
|
||||
}
|
||||
|
||||
# mca_write_if_changed <path> <content>
|
||||
# Writing a file that is already correct would touch its mtime, and the systemd
|
||||
# path unit watches these directories: an unconditional write would retrigger
|
||||
# the service, which would write again, forever.
|
||||
mca_write_if_changed() {
|
||||
local path="$1" content="$2" current=''
|
||||
|
||||
[[ -r $path ]] && current="$(< "$path")"
|
||||
|
||||
# $(< file) drops trailing newlines and callers pass content that ends in
|
||||
# one, so both sides are trimmed before comparing. Getting this wrong makes
|
||||
# every file look changed on every run, which is exactly the feedback loop
|
||||
# this function exists to prevent.
|
||||
[[ "${current%"${current##*[!$'\n']}"}" == "${content%"${content##*[!$'\n']}"}" ]] && return 1
|
||||
|
||||
mkdir -p "$(dirname "$path")" 2>/dev/null || return 2
|
||||
printf '%s' "$content" > "$path" || return 2
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
# shellcheck shell=bash
|
||||
#
|
||||
# Reading and writing ~/.config/middleclick-autoscroll/config.
|
||||
#
|
||||
# The file exists so the tool has somewhere to remember what it was told; it is
|
||||
# not meant to be edited. Every option it holds is reachable from the interface,
|
||||
# which is why there is no commented template shipped anywhere and no reference
|
||||
# to the path in the interface itself.
|
||||
#
|
||||
# It is parsed rather than sourced. One "Key=Value" per line, '#' comments.
|
||||
|
||||
MCA_CONFIG_CACHE=''
|
||||
MCA_CONFIG_CACHED=0
|
||||
|
||||
_mca_config_slurp() {
|
||||
(( MCA_CONFIG_CACHED )) && return 0
|
||||
MCA_CONFIG_CACHE=''
|
||||
[[ -r $MCA_CONFIG ]] && MCA_CONFIG_CACHE="$(< "$MCA_CONFIG")"
|
||||
MCA_CONFIG_CACHED=1
|
||||
return 0
|
||||
}
|
||||
|
||||
# _mca_config_lookup <Key> [default]
|
||||
# Result in MCA_CONFIG_VALUE. Assigning rather than printing matters on the
|
||||
# settings screen, which reads every key on every frame: a command substitution
|
||||
# there is a fork, and forks are the whole cost of a redraw.
|
||||
MCA_CONFIG_VALUE=''
|
||||
|
||||
_mca_config_lookup() {
|
||||
local key="$1" default="${2:-}" val='' line
|
||||
|
||||
MCA_CONFIG_VALUE="$default"
|
||||
[[ -r $MCA_CONFIG ]] || return 0
|
||||
_mca_config_slurp
|
||||
|
||||
while IFS= read -r line; do
|
||||
[[ $line == *"$key"* ]] || continue
|
||||
[[ $line =~ ^[[:space:]]*"$key"[[:space:]]*=(.*)$ ]] || continue
|
||||
val="${BASH_REMATCH[1]}"
|
||||
done <<< "$MCA_CONFIG_CACHE"
|
||||
|
||||
val="${val%%#*}"
|
||||
val="${val#"${val%%[![:space:]]*}"}"
|
||||
val="${val%"${val##*[![:space:]]}"}"
|
||||
val="${val%\"}"
|
||||
val="${val#\"}"
|
||||
|
||||
[[ -n $val ]] && MCA_CONFIG_VALUE="$val"
|
||||
return 0
|
||||
}
|
||||
|
||||
# mca_config_get <Key> [default]
|
||||
mca_config_get() {
|
||||
_mca_config_lookup "$@"
|
||||
printf '%s\n' "$MCA_CONFIG_VALUE"
|
||||
}
|
||||
|
||||
# mca_config_bool <Key> <default: yes|no>
|
||||
mca_config_bool() {
|
||||
local val
|
||||
val="$(mca_config_get "$1" "$2")"
|
||||
case "${val,,}" in
|
||||
yes|y|true|1|on|enabled) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# mca_config_set <Key> <Value>
|
||||
mca_config_set() {
|
||||
local key="$1" value="$2" tmp
|
||||
|
||||
if [[ ! -e $MCA_CONFIG ]]; then
|
||||
mkdir -p "$(dirname "$MCA_CONFIG")" || return 1
|
||||
{
|
||||
printf '# %s\n' "$MCA_PRETTY"
|
||||
printf '#\n'
|
||||
printf '# Written by `%s`. Nothing here needs editing by hand -\n' "$MCA_NAME"
|
||||
printf '# every option is reachable from `%s` itself.\n' "$MCA_NAME"
|
||||
} > "$MCA_CONFIG" || return 1
|
||||
fi
|
||||
[[ -w $MCA_CONFIG ]] || return 1
|
||||
|
||||
tmp="$(mktemp "${MCA_CONFIG}.XXXXXX")" || return 1
|
||||
chmod --reference="$MCA_CONFIG" "$tmp" 2>/dev/null || chmod 0644 "$tmp"
|
||||
|
||||
if grep -qE "^[[:space:]]*#?[[:space:]]*${key}[[:space:]]*=" "$MCA_CONFIG"; then
|
||||
awk -v key="$key" -v value="$value" '
|
||||
!done && $0 ~ "^[[:space:]]*#?[[:space:]]*" key "[[:space:]]*=" {
|
||||
print key "=" value; done = 1; next
|
||||
}
|
||||
# drop any further occurrences so the file cannot grow duplicates
|
||||
$0 ~ "^[[:space:]]*" key "[[:space:]]*=" { next }
|
||||
{ print }
|
||||
' "$MCA_CONFIG" > "$tmp" || { rm -f "$tmp"; return 1; }
|
||||
else
|
||||
cat "$MCA_CONFIG" > "$tmp" || { rm -f "$tmp"; return 1; }
|
||||
printf '%s=%s\n' "$key" "$value" >> "$tmp"
|
||||
fi
|
||||
|
||||
mv -f "$tmp" "$MCA_CONFIG"
|
||||
MCA_CONFIG_CACHED=0
|
||||
}
|
||||
|
||||
# mca_config_list_has <Key> <item>
|
||||
# The per-application overrides are stored as space separated lists.
|
||||
mca_config_list_has() {
|
||||
local list item="$2" entry
|
||||
list="$(mca_config_get "$1" '')"
|
||||
for entry in $list; do
|
||||
[[ $entry == "$item" ]] && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# mca_config_list_add <Key> <item> / mca_config_list_del <Key> <item>
|
||||
mca_config_list_add() {
|
||||
local key="$1" item="$2" list
|
||||
mca_config_list_has "$key" "$item" && return 0
|
||||
list="$(mca_config_get "$key" '')"
|
||||
mca_config_set "$key" "${list:+$list }$item"
|
||||
}
|
||||
|
||||
mca_config_list_del() {
|
||||
local key="$1" item="$2" list entry out=''
|
||||
list="$(mca_config_get "$key" '')"
|
||||
for entry in $list; do
|
||||
[[ $entry == "$item" ]] && continue
|
||||
out="${out:+$out }$entry"
|
||||
done
|
||||
mca_config_set "$key" "$out"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Resolved settings
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
mca_config_load() {
|
||||
CFG_ENABLED=no; mca_config_bool Enabled no && CFG_ENABLED=yes
|
||||
CFG_APPS=no; mca_config_bool PatchApps yes && CFG_APPS=yes
|
||||
CFG_BROWSERS=no; mca_config_bool PatchBrowsers yes && CFG_BROWSERS=yes
|
||||
CFG_FLATPAK=no; mca_config_bool PatchFlatpak yes && CFG_FLATPAK=yes
|
||||
CFG_AUTOSTART=no; mca_config_bool PatchAutostart yes && CFG_AUTOSTART=yes
|
||||
CFG_STEAM=no; mca_config_bool PatchSteam yes && CFG_STEAM=yes
|
||||
CFG_SPOTIFY=no; mca_config_bool PatchSpotify yes && CFG_SPOTIFY=yes
|
||||
CFG_WATCH=no; mca_config_bool WatchNewApps yes && CFG_WATCH=yes
|
||||
|
||||
CFG_EXTRA_FLAGS="$(mca_config_get ExtraFlags '')"
|
||||
|
||||
# Applications the user turned on or off by hand on the applications
|
||||
# screen, by desktop file id. Include wins over detection, Skip wins over
|
||||
# everything.
|
||||
CFG_INCLUDE="$(mca_config_get Include '')"
|
||||
CFG_SKIP="$(mca_config_get Skip '')"
|
||||
}
|
||||
|
||||
# The full argument string that gets injected. Kept in one place so the flag
|
||||
# file writer and the desktop entry writer cannot drift apart.
|
||||
mca_flags() {
|
||||
printf '%s' "$MCA_FLAG"
|
||||
[[ -n ${CFG_EXTRA_FLAGS:-} ]] && printf ' %s' "$CFG_EXTRA_FLAGS"
|
||||
printf '\n'
|
||||
}
|
||||
@@ -0,0 +1,571 @@
|
||||
# shellcheck shell=bash
|
||||
#
|
||||
# Finding the applications and deciding which of them are Chromium underneath.
|
||||
#
|
||||
# The detection is deliberately conservative. A wrong "yes" appends an unknown
|
||||
# argument to something that is not Chromium, and plenty of programs treat an
|
||||
# unrecognised argument as a file name to open - so every rule here is a
|
||||
# positive one, and anything that cannot be identified is reported as unknown
|
||||
# rather than guessed at.
|
||||
|
||||
# Files that only ever sit next to a Chromium or Electron binary. Any single one
|
||||
# is conclusive; together they cover both bundled Electron (app.asar, the
|
||||
# swiftshader libraries) and plain CEF (libcef).
|
||||
#
|
||||
# Deliberately not in here: libEGL.so and libffmpeg.so. Chromium ships both, but
|
||||
# so does the system - /usr/lib/libEGL.so exists on any machine with Mesa - and
|
||||
# a marker that can be somebody else's file is not a marker.
|
||||
MCA_MARKERS=(
|
||||
chrome_crashpad_handler chrome-sandbox chrome_100_percent.pak
|
||||
icudtl.dat v8_context_snapshot.bin snapshot_blob.bin resources.pak
|
||||
libvk_swiftshader.so LICENSES.chromium.html libcef.so
|
||||
)
|
||||
|
||||
# Shared directories, where a marker belongs to the system rather than to the
|
||||
# program that happens to live there. An application ships its payload in a
|
||||
# directory of its own; nothing unpacks Chromium straight into /usr/lib.
|
||||
MCA_SYSTEM_DIRS=(
|
||||
/ /bin /lib /lib32 /lib64 /sbin /usr /usr/bin /usr/lib /usr/lib32
|
||||
/usr/lib64 /usr/libexec /usr/sbin /usr/local /usr/local/bin
|
||||
/usr/local/lib /opt
|
||||
)
|
||||
|
||||
# Strings in a launcher script that mean it starts a Chromium or Electron
|
||||
# process, for the wrappers whose command line is assembled out of variables and
|
||||
# cannot be followed from the outside.
|
||||
#
|
||||
# Only strings that no other kind of program has a reason to contain. The word
|
||||
# "chromium" on its own is not one of them: /usr/bin/xdg-open lists every
|
||||
# browser it knows how to start, and that is not a browser.
|
||||
#
|
||||
# The flag file convention is an Arch packaging habit and says nothing about the
|
||||
# engine either, so it is not in here.
|
||||
MCA_SCRIPT_HINTS='ELECTRON_|app\.asar|chrome-sandbox|libcef|enable-blink-features|ozone-platform-hint'
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Desktop entries
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# The directories a desktop entry can come from, most specific first - which is
|
||||
# also XDG lookup order, so the first file found for an id is the one that is
|
||||
# actually used.
|
||||
mca_desktop_dirs() {
|
||||
local dirs="${XDG_DATA_DIRS:-/usr/local/share:/usr/share}" d
|
||||
printf '%s\n' "$MCA_APPDIR"
|
||||
while IFS= read -r -d: d; do
|
||||
[[ -n $d ]] && printf '%s/applications\n' "${d%/}"
|
||||
done <<< "${dirs}:"
|
||||
}
|
||||
|
||||
# mca_desktop_get <file> <key>
|
||||
# A single value from the [Desktop Entry] group. Desktop Action groups repeat
|
||||
# the same keys, and reading past the first group would pick up the wrong one.
|
||||
mca_desktop_get() {
|
||||
local file="$1" key="$2"
|
||||
awk -v key="$key" '
|
||||
/^[[:space:]]*\[/ { inentry = ($0 ~ /^[[:space:]]*\[Desktop Entry\][[:space:]]*$/); next }
|
||||
inentry && index($0, key "=") == 1 { print substr($0, length(key) + 2); exit }
|
||||
' "$file" 2>/dev/null
|
||||
}
|
||||
|
||||
# _mca_desktop_read <file>
|
||||
# Every key the scan needs, in one pass and without a single fork. There are a
|
||||
# couple of hundred desktop entries on an ordinary system, and doing this with
|
||||
# one awk per key per file is the difference between a menu that redraws and a
|
||||
# menu that pauses.
|
||||
DE_TYPE='' DE_HIDDEN='' DE_EXEC='' DE_NAME='' DE_CATEGORIES='' DE_MIME='' DE_OURS=''
|
||||
|
||||
_mca_desktop_read() {
|
||||
local file="$1" line ingroup=0
|
||||
|
||||
DE_TYPE=''; DE_HIDDEN=''; DE_EXEC=''; DE_NAME=''
|
||||
DE_CATEGORIES=''; DE_MIME=''; DE_OURS=''
|
||||
|
||||
while IFS= read -r line || [[ -n $line ]]; do
|
||||
case "$line" in
|
||||
'[Desktop Entry]'*) ingroup=1; continue ;;
|
||||
'['*) ingroup=0; continue ;;
|
||||
esac
|
||||
(( ingroup )) || continue
|
||||
|
||||
# Locale variants are Name[de]= and never match these patterns, which
|
||||
# is what we want: the untranslated key is the identifying one.
|
||||
case "$line" in
|
||||
Exec=*) [[ -n $DE_EXEC ]] || DE_EXEC="${line#Exec=}" ;;
|
||||
Name=*) [[ -n $DE_NAME ]] || DE_NAME="${line#Name=}" ;;
|
||||
Type=*) DE_TYPE="${line#Type=}" ;;
|
||||
Hidden=*) DE_HIDDEN="${line#Hidden=}" ;;
|
||||
Categories=*) DE_CATEGORIES="${line#Categories=}" ;;
|
||||
MimeType=*) DE_MIME="${line#MimeType=}" ;;
|
||||
X-MCA-Generated=*) DE_OURS=1 ;;
|
||||
esac
|
||||
done < "$file"
|
||||
}
|
||||
|
||||
# mca_exec_program <exec line>
|
||||
# The program a desktop entry actually starts: the first token that is not an
|
||||
# environment prefix, resolved to an absolute path. The result is left in
|
||||
# MCA_PROG rather than printed - the scan calls this for every desktop entry on
|
||||
# the system, and a command substitution each time is a fork each time.
|
||||
#
|
||||
# Fails for entries this tool has no safe way to rewrite: anything routed
|
||||
# through a shell, where the real program is inside a quoted string.
|
||||
MCA_PROG=''
|
||||
|
||||
mca_exec_program() {
|
||||
local line="$1" tok prog=''
|
||||
local -a tokens
|
||||
|
||||
MCA_PROG=''
|
||||
|
||||
# Field codes are placeholders, not arguments, and quotes only ever wrap
|
||||
# whole tokens here; splitting on whitespace is enough to find token one.
|
||||
read -r -a tokens <<< "$line"
|
||||
|
||||
for tok in "${tokens[@]}"; do
|
||||
tok="${tok%\"}"; tok="${tok#\"}"
|
||||
tok="${tok%\'}"; tok="${tok#\'}"
|
||||
[[ -z $tok ]] && continue
|
||||
[[ $tok == *=* && $tok != /* ]] && continue # VAR=value prefix
|
||||
[[ $tok == env ]] && continue
|
||||
prog="$tok"
|
||||
break
|
||||
done
|
||||
|
||||
[[ -n $prog ]] || return 1
|
||||
|
||||
case "${prog##*/}" in
|
||||
sh|bash|dash|zsh|fish) return 1 ;;
|
||||
esac
|
||||
|
||||
# PATH is searched here rather than with `command -v`, which is a builtin
|
||||
# but would have to be read back through a command substitution, and that
|
||||
# is a fork per desktop entry.
|
||||
if [[ $prog != /* ]]; then
|
||||
local d found=''
|
||||
local -a pathdirs
|
||||
IFS=: read -r -a pathdirs <<< "$PATH"
|
||||
for d in "${pathdirs[@]}"; do
|
||||
[[ -n $d ]] || continue
|
||||
if [[ -x "$d/$prog" && ! -d "$d/$prog" ]]; then found="$d/$prog"; break; fi
|
||||
done
|
||||
[[ -n $found ]] || return 1
|
||||
prog="$found"
|
||||
fi
|
||||
|
||||
MCA_PROG="$prog"
|
||||
}
|
||||
|
||||
# mca_exec_flatpak_id <exec line>
|
||||
# The application id out of a `flatpak run ...` command line, in MCA_PROG.
|
||||
mca_exec_flatpak_id() {
|
||||
local line="$1" tok seen_run=0
|
||||
local -a tokens
|
||||
read -r -a tokens <<< "$line"
|
||||
|
||||
MCA_PROG=''
|
||||
for tok in "${tokens[@]}"; do
|
||||
if (( ! seen_run )); then
|
||||
[[ $tok == run ]] && seen_run=1
|
||||
continue
|
||||
fi
|
||||
[[ $tok == -* || $tok == @@* || $tok == %* ]] && continue
|
||||
[[ $tok == *.*.* ]] || continue
|
||||
MCA_PROG="$tok"
|
||||
return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Is this Chromium?
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Answers are cached against size and mtime, because the systemd path unit can
|
||||
# fire several times in a row while a package installs and each miss costs a
|
||||
# scan of a 200 MB binary.
|
||||
#
|
||||
# The file is read once into memory rather than searched per lookup: a scan
|
||||
# asks about every program on the system, and an awk per question is most of
|
||||
# the time the scan takes.
|
||||
declare -A MCA_DETECT_MEMO=()
|
||||
declare -A MCA_DETECT_CACHE=()
|
||||
MCA_CACHE_LOADED=0
|
||||
MCA_CACHE_DIRTY=0
|
||||
|
||||
# Size and mtime for every program the scan is about to ask about, collected in
|
||||
# one call. Checking a cache entry is still stale needs a stat, and one stat per
|
||||
# program on the system was most of what a warm scan spent its time on.
|
||||
declare -A MCA_STAT=()
|
||||
|
||||
_mca_stat_batch() {
|
||||
local name st
|
||||
(( $# )) || return 0
|
||||
while IFS=$'\t' read -r name st; do
|
||||
[[ -n $name ]] && MCA_STAT["$name"]="$st"
|
||||
done < <(stat -Lc '%n %s:%Y' -- "$@" 2>/dev/null)
|
||||
return 0
|
||||
}
|
||||
|
||||
_mca_cache_load() {
|
||||
local path stamp verdict
|
||||
|
||||
(( MCA_CACHE_LOADED )) && return 0
|
||||
MCA_CACHE_LOADED=1
|
||||
|
||||
[[ -r "$MCA_CACHEDIR/detect" ]] || return 0
|
||||
while IFS=$'\t' read -r path stamp verdict; do
|
||||
[[ -n $path && -n $stamp ]] || continue
|
||||
MCA_DETECT_CACHE["$path"]="$stamp"$'\t'"$verdict"
|
||||
done < "$MCA_CACHEDIR/detect"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Written back once, at exit, instead of after every miss.
|
||||
mca_cache_flush() {
|
||||
local path entry tmp
|
||||
|
||||
(( MCA_CACHE_DIRTY )) || return 0
|
||||
mkdir -p "$MCA_CACHEDIR" 2>/dev/null || return 0
|
||||
|
||||
tmp="$(mktemp "$MCA_CACHEDIR/detect.XXXXXX")" || return 0
|
||||
for path in "${!MCA_DETECT_CACHE[@]}"; do
|
||||
entry="${MCA_DETECT_CACHE[$path]}"
|
||||
printf '%s\t%s\n' "$path" "$entry" >> "$tmp"
|
||||
done
|
||||
mv -f "$tmp" "$MCA_CACHEDIR/detect" 2>/dev/null || rm -f "$tmp"
|
||||
MCA_CACHE_DIRTY=0
|
||||
return 0
|
||||
}
|
||||
|
||||
# _mca_has_markers <directory>
|
||||
_mca_has_markers() {
|
||||
local dir="$1" m s
|
||||
|
||||
[[ -d $dir ]] || return 1
|
||||
dir="${dir%/}"
|
||||
for s in "${MCA_SYSTEM_DIRS[@]}"; do
|
||||
[[ $dir == "$s" ]] && return 1
|
||||
done
|
||||
|
||||
for m in "${MCA_MARKERS[@]}"; do
|
||||
[[ -e "$dir/$m" ]] && return 0
|
||||
done
|
||||
[[ -e "$dir/resources/app.asar" || -e "$dir/app.asar" ]] && return 0
|
||||
return 1
|
||||
}
|
||||
|
||||
# _mca_script_target <script>
|
||||
# The program a wrapper script hands over to, so a chain like
|
||||
# heroic -> electron43 -> /usr/lib/electron43/electron can be followed.
|
||||
_mca_script_target() {
|
||||
local script="$1" line tok
|
||||
local -a tokens
|
||||
|
||||
while IFS= read -r line; do
|
||||
[[ $line =~ ^[[:space:]]*exec[[:space:]]+(.*)$ ]] || continue
|
||||
read -r -a tokens <<< "${BASH_REMATCH[1]}"
|
||||
for tok in "${tokens[@]}"; do
|
||||
[[ $tok == env ]] && continue
|
||||
[[ $tok == *=* && $tok != /* ]] && continue
|
||||
[[ $tok == -* ]] && continue
|
||||
# Anything still carrying a shell variable cannot be resolved from
|
||||
# the outside; the hint scan has to answer for those.
|
||||
[[ $tok == *'$'* ]] && return 1
|
||||
if [[ $tok == /* ]]; then
|
||||
printf '%s\n' "$tok"
|
||||
else
|
||||
command -v "$tok" 2>/dev/null
|
||||
fi
|
||||
return $?
|
||||
done
|
||||
done < "$script"
|
||||
return 1
|
||||
}
|
||||
|
||||
# mca_flags_candidates <program>
|
||||
# The flag files a launcher reads, in the order it reads them. Arch's Electron
|
||||
# and Chromium wrappers all take extra arguments from
|
||||
# $XDG_CONFIG_HOME/<name>-flags.conf, which is a far better place to inject than
|
||||
# a desktop entry: it survives package updates and it applies to a launch from
|
||||
# the terminal too.
|
||||
mca_flags_candidates() {
|
||||
local prog="$1" depth="${2:-0}" target
|
||||
(( depth > 3 )) && return 0
|
||||
[[ -r $prog ]] || return 0
|
||||
head -c2 -- "$prog" 2>/dev/null | grep -q '#!' || return 0
|
||||
|
||||
grep -oE '[A-Za-z0-9_.+-]+-flags\.conf' -- "$prog" 2>/dev/null
|
||||
|
||||
# A wrapper that only execs another wrapper (heroic -> electron43) inherits
|
||||
# that one's flag files, plus the specific name it builds from a variable.
|
||||
if target="$(_mca_script_target "$prog")" && [[ -n $target ]]; then
|
||||
printf '%s-flags.conf\n' "${target##*/}"
|
||||
mca_flags_candidates "$target" $(( depth + 1 ))
|
||||
fi
|
||||
}
|
||||
|
||||
# mca_is_chromium <program>
|
||||
# Succeeds when the program is a Chromium, CEF or Electron process.
|
||||
mca_is_chromium() {
|
||||
local prog="$1" verdict real stamp cached
|
||||
|
||||
[[ -n $prog && -e $prog ]] || return 1
|
||||
|
||||
# Memoized under the path as given, so the same launcher named twice in a
|
||||
# scan costs nothing at all the second time.
|
||||
if [[ -n ${MCA_DETECT_MEMO[$prog]+set} ]]; then
|
||||
[[ ${MCA_DETECT_MEMO[$prog]} == yes ]]
|
||||
return $?
|
||||
fi
|
||||
|
||||
_mca_cache_load
|
||||
|
||||
if [[ -n ${MCA_STAT[$prog]+set} ]]; then
|
||||
stamp="${MCA_STAT[$prog]}"
|
||||
else
|
||||
stamp="$(stat -Lc '%s:%Y' -- "$prog" 2>/dev/null)" || stamp=''
|
||||
fi
|
||||
|
||||
if [[ -n $stamp && -n ${MCA_DETECT_CACHE[$prog]+set} ]]; then
|
||||
cached="${MCA_DETECT_CACHE[$prog]}"
|
||||
if [[ "${cached%%$'\t'*}" == "$stamp" ]]; then
|
||||
verdict="${cached#*$'\t'}"
|
||||
MCA_DETECT_MEMO[$prog]="$verdict"
|
||||
[[ $verdict == yes ]]
|
||||
return $?
|
||||
fi
|
||||
fi
|
||||
|
||||
real="$(readlink -f -- "$prog" 2>/dev/null)" || real="$prog"
|
||||
|
||||
verdict=no
|
||||
if _mca_detect_uncached "$real"; then verdict=yes; fi
|
||||
|
||||
MCA_DETECT_MEMO[$prog]="$verdict"
|
||||
if [[ -n $stamp ]]; then
|
||||
MCA_DETECT_CACHE["$prog"]="$stamp"$'\t'"$verdict"
|
||||
MCA_CACHE_DIRTY=1
|
||||
fi
|
||||
[[ $verdict == yes ]]
|
||||
}
|
||||
|
||||
_mca_detect_uncached() {
|
||||
local real="$1" depth="${2:-0}" dir target
|
||||
|
||||
(( depth > 3 )) && return 1
|
||||
[[ -r $real ]] || return 1
|
||||
|
||||
if head -c2 -- "$real" 2>/dev/null | grep -q '#!'; then
|
||||
# A launcher script. Following where it hands over is the reliable
|
||||
# answer; the hint scan catches the ones that build the command line
|
||||
# out of variables (vesktop, discord and most vendor wrappers).
|
||||
if target="$(_mca_script_target "$real")" && [[ -n $target ]]; then
|
||||
_mca_detect_uncached "$(readlink -f -- "$target" 2>/dev/null || printf '%s' "$target")" \
|
||||
$(( depth + 1 )) && return 0
|
||||
fi
|
||||
grep -qE "$MCA_SCRIPT_HINTS" -- "$real" 2>/dev/null && return 0
|
||||
return 1
|
||||
fi
|
||||
|
||||
# A binary. Everything Chromium ships is unpacked next to it, either in the
|
||||
# same directory or - for /opt/thing/bin/Thing layouts - one level up.
|
||||
dir="$(dirname -- "$real")"
|
||||
_mca_has_markers "$dir" && return 0
|
||||
[[ ${dir##*/} == bin ]] && _mca_has_markers "${dir%/*}" && return 0
|
||||
|
||||
# Last resort: Chromium's own argument table is in the binary. -m1 stops at
|
||||
# the first hit, so this reads far less than the file size suggests.
|
||||
#
|
||||
# AppImages are the one thing this cannot see through - their payload is a
|
||||
# compressed filesystem - which is why they come out as unknown and are
|
||||
# left to the applications screen.
|
||||
grep -qaFm1 -- 'enable-blink-features' "$real" 2>/dev/null && return 0
|
||||
grep -qaFm1 -- 'CHROME_VERSION_EXTRA' "$real" 2>/dev/null && return 0
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The scan
|
||||
# ---------------------------------------------------------------------------
|
||||
# Results land in parallel arrays rather than being printed, so the caller can
|
||||
# use them for both patching and the applications screen without scanning twice.
|
||||
|
||||
MCA_IDS=() # desktop file id, without the .desktop suffix
|
||||
MCA_FILES=() # the desktop entry that is in effect for that id
|
||||
MCA_NAMES=() # display name
|
||||
MCA_PROGS=() # resolved program, or a flatpak app id
|
||||
MCA_KINDS=() # app | browser | flatpak | steam | unknown | no
|
||||
|
||||
# A scan reads every desktop entry on the system, so the menu does it once and
|
||||
# then redraws from what it found. Anything that changes the answer - applying,
|
||||
# reverting - invalidates it explicitly.
|
||||
MCA_SCANNED=0
|
||||
|
||||
mca_scan_once() {
|
||||
(( MCA_SCANNED )) && return 0
|
||||
mca_scan
|
||||
}
|
||||
|
||||
mca_scan_invalidate() { MCA_SCANNED=0; }
|
||||
|
||||
mca_scan() {
|
||||
local dir file id name exec_line prog kind i
|
||||
local -A seen=()
|
||||
local -a c_ids=() c_files=() c_names=() c_progs=() c_browser=() c_stat=()
|
||||
|
||||
MCA_IDS=(); MCA_FILES=(); MCA_NAMES=(); MCA_PROGS=(); MCA_KINDS=()
|
||||
|
||||
# Pass one: read the entries and work out what each of them starts. No
|
||||
# detection yet - that needs a stat per program, and those are collected so
|
||||
# they can be asked for all at once.
|
||||
while IFS= read -r dir; do
|
||||
[[ -d $dir ]] || continue
|
||||
for file in "$dir"/*.desktop; do
|
||||
[[ -f $file ]] || continue
|
||||
|
||||
id="${file##*/}"; id="${id%.desktop}"
|
||||
[[ -n ${seen[$id]+set} ]] && continue
|
||||
seen[$id]=1
|
||||
|
||||
_mca_desktop_read "$file"
|
||||
|
||||
# One of our own generated entries. It describes the same
|
||||
# application as the system one it shadows, so it is skipped and
|
||||
# the id left free for the original further down the search path.
|
||||
[[ -n $DE_OURS ]] && { unset "seen[$id]"; continue; }
|
||||
|
||||
[[ $DE_TYPE == Application ]] || continue
|
||||
[[ $DE_HIDDEN == true ]] && continue
|
||||
|
||||
exec_line="$DE_EXEC"
|
||||
[[ -n $exec_line ]] || continue
|
||||
|
||||
name="$DE_NAME"
|
||||
[[ -n $name ]] || name="$id"
|
||||
|
||||
mca_exec_program "$exec_line" || continue
|
||||
prog="$MCA_PROG"
|
||||
|
||||
if [[ ${prog##*/} == flatpak ]]; then
|
||||
mca_exec_flatpak_id "$exec_line" || continue
|
||||
prog="flatpak:$MCA_PROG"
|
||||
fi
|
||||
|
||||
c_ids+=("$id"); c_files+=("$file"); c_names+=("$name")
|
||||
c_progs+=("$prog")
|
||||
if mca_desktop_is_browser; then c_browser+=(1); else c_browser+=(0); fi
|
||||
[[ $prog == /* ]] && c_stat+=("$prog")
|
||||
done
|
||||
done < <(mca_desktop_dirs)
|
||||
|
||||
MCA_STAT=()
|
||||
_mca_stat_batch "${c_stat[@]}"
|
||||
|
||||
# Pass two: decide what each one is.
|
||||
for i in "${!c_ids[@]}"; do
|
||||
prog="${c_progs[i]}"
|
||||
kind=no
|
||||
|
||||
if [[ $prog == flatpak:* ]]; then
|
||||
prog="${prog#flatpak:}"
|
||||
mca_flatpak_is_chromium "$prog" && kind=flatpak
|
||||
elif [[ ${prog##*/} == steam || ${prog##*/} == steam-runtime ]]; then
|
||||
# Steam is Chromium inside, but nothing about it can be changed
|
||||
# from a command line argument; it has its own module.
|
||||
kind=steam
|
||||
elif mca_is_chromium "$prog"; then
|
||||
(( c_browser[i] )) && kind=browser || kind=app
|
||||
elif [[ $prog == *.AppImage || $prog == *.appimage ]]; then
|
||||
kind=unknown
|
||||
fi
|
||||
|
||||
[[ $kind == no ]] && continue
|
||||
|
||||
MCA_IDS+=("${c_ids[i]}")
|
||||
MCA_FILES+=("${c_files[i]}")
|
||||
MCA_NAMES+=("${c_names[i]}")
|
||||
MCA_PROGS+=("$prog")
|
||||
MCA_KINDS+=("$kind")
|
||||
done
|
||||
|
||||
mca_cache_flush
|
||||
MCA_SCANNED=1
|
||||
}
|
||||
|
||||
# mca_has_flags_file <program>
|
||||
# Whether the program's launcher reads a flag file. Memoized: the status block
|
||||
# asks this for every application it lists, and answering it means reading the
|
||||
# launcher script.
|
||||
declare -A MCA_FLAGS_MEMO=()
|
||||
|
||||
mca_has_flags_file() {
|
||||
local prog="$1"
|
||||
|
||||
if [[ -z ${MCA_FLAGS_MEMO[$prog]+set} ]]; then
|
||||
if [[ -n "$(mca_flags_candidates "$prog")" ]]; then
|
||||
MCA_FLAGS_MEMO[$prog]=yes
|
||||
else
|
||||
MCA_FLAGS_MEMO[$prog]=no
|
||||
fi
|
||||
fi
|
||||
|
||||
[[ ${MCA_FLAGS_MEMO[$prog]} == yes ]]
|
||||
}
|
||||
|
||||
# A browser is anything that offers itself for http. That is the property that
|
||||
# matters here: those are the applications where middle click currently pastes
|
||||
# a URL, so a user may well want them left alone.
|
||||
#
|
||||
# Reads the keys _mca_desktop_read left behind, so it only makes sense straight
|
||||
# after that call.
|
||||
mca_desktop_is_browser() {
|
||||
[[ $DE_CATEGORIES == *WebBrowser* ]] && return 0
|
||||
[[ $DE_MIME == *x-scheme-handler/http* ]] && return 0
|
||||
return 1
|
||||
}
|
||||
|
||||
# mca_flatpak_is_chromium <app id>
|
||||
# Flatpak keeps every application in its own tree, so the marker check works the
|
||||
# same way once that tree has been located.
|
||||
mca_flatpak_is_chromium() {
|
||||
local id="$1" loc
|
||||
mca_have flatpak || return 1
|
||||
|
||||
if [[ -n ${MCA_DETECT_MEMO[flatpak:$id]+set} ]]; then
|
||||
[[ ${MCA_DETECT_MEMO[flatpak:$id]} == yes ]]
|
||||
return $?
|
||||
fi
|
||||
|
||||
loc="$(flatpak info --show-location "$id" 2>/dev/null)"
|
||||
if [[ -n $loc && -d "$loc/files" ]] \
|
||||
&& [[ -n "$(find "$loc/files" -maxdepth 4 \
|
||||
\( -name 'chrome_crashpad_handler' -o -name 'app.asar' \
|
||||
-o -name 'libcef.so' -o -name 'v8_context_snapshot.bin' \) \
|
||||
-print -quit 2>/dev/null)" ]]
|
||||
then
|
||||
MCA_DETECT_MEMO[flatpak:$id]=yes
|
||||
return 0
|
||||
fi
|
||||
|
||||
MCA_DETECT_MEMO[flatpak:$id]=no
|
||||
return 1
|
||||
}
|
||||
|
||||
# mca_kind_wanted <kind> <id>
|
||||
# Whether the current settings say this entry should be patched. Skip beats
|
||||
# everything, an explicit include beats detection, and detection beats nothing.
|
||||
mca_kind_wanted() {
|
||||
local kind="$1" id="$2"
|
||||
|
||||
mca_config_list_has Skip "$id" && return 1
|
||||
mca_config_list_has Include "$id" && return 0
|
||||
|
||||
case "$kind" in
|
||||
app) [[ $CFG_APPS == yes ]] ;;
|
||||
browser) [[ $CFG_BROWSERS == yes ]] ;;
|
||||
flatpak) [[ $CFG_FLATPAK == yes && $CFG_APPS == yes ]] ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
+523
@@ -0,0 +1,523 @@
|
||||
# shellcheck shell=bash
|
||||
#
|
||||
# The interactive front end.
|
||||
#
|
||||
# This is the whole configuration interface. There is a file behind it, but no
|
||||
# part of the program ever asks anyone to open it: one switch on the front
|
||||
# screen, a settings list for the categories, and a list of the applications
|
||||
# themselves for the cases where the automatic answer is not the wanted one.
|
||||
|
||||
# Terminal mode.
|
||||
#
|
||||
# bash flips the terminal into non-canonical mode for each `read -sn1` and back
|
||||
# out again in between. That gap matters: in canonical mode DEL is the ERASE
|
||||
# character, so the line discipline eats it instead of delivering it, and a
|
||||
# backspace typed while the interface was between reads simply vanishes.
|
||||
# Holding non-canonical mode for the whole interface removes the gap.
|
||||
MCA_TERM_SAVED=''
|
||||
|
||||
mca_ui_term_raw() {
|
||||
mca_have stty || return 0
|
||||
[[ -t 0 ]] || return 0
|
||||
[[ -n $MCA_TERM_SAVED ]] && return 0
|
||||
|
||||
MCA_TERM_SAVED="$(stty -g 2>/dev/null)" || { MCA_TERM_SAVED=''; return 0; }
|
||||
stty -icanon -echo min 1 time 0 2>/dev/null || true
|
||||
}
|
||||
|
||||
mca_ui_term_restore() {
|
||||
[[ -n $MCA_TERM_SAVED ]] || return 0
|
||||
stty "$MCA_TERM_SAVED" 2>/dev/null || true
|
||||
MCA_TERM_SAVED=''
|
||||
}
|
||||
|
||||
# Runs an action with the terminal handed back to normal line mode, so anything
|
||||
# it prints - or prompts for - behaves the way a program expects.
|
||||
mca_ui_cooked() {
|
||||
mca_ui_term_restore
|
||||
"$@"
|
||||
local rc=$?
|
||||
mca_ui_term_raw
|
||||
return $rc
|
||||
}
|
||||
|
||||
# mca_read_key
|
||||
# One keypress, resolved to a symbolic name. Arrow keys arrive as ESC [ A, so
|
||||
# the tail of the sequence is consumed here rather than being mistaken for
|
||||
# three separate presses.
|
||||
mca_read_key() {
|
||||
local k rest
|
||||
|
||||
IFS= read -rsn1 k || return 1
|
||||
|
||||
case "$k" in
|
||||
$'\e')
|
||||
if IFS= read -rsn2 -t 0.05 rest; then
|
||||
case "$rest" in
|
||||
'[A') printf 'up\n' ;;
|
||||
'[B') printf 'down\n' ;;
|
||||
'[C') printf 'right\n' ;;
|
||||
'[D') printf 'left\n' ;;
|
||||
*) printf 'escape\n' ;;
|
||||
esac
|
||||
else
|
||||
printf 'escape\n'
|
||||
fi
|
||||
;;
|
||||
''|$'\r') printf 'enter\n' ;;
|
||||
$'\x7f'|$'\b') printf 'backspace\n' ;;
|
||||
' ') printf 'space\n' ;;
|
||||
*) printf '%s\n' "$k" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# mca_ui_read_line <initial>
|
||||
# A minimal line editor built on mca_read_key, with the result in
|
||||
# MCA_LINE_RESULT. This exists instead of bash's own `read -r` because mixing
|
||||
# line mode into a single-key interface breaks it: after one cooked-mode read
|
||||
# the following `read -sn1` stops receiving keystrokes entirely.
|
||||
MCA_LINE_RESULT=''
|
||||
|
||||
mca_ui_read_line() {
|
||||
local buf="${1:-}" key
|
||||
|
||||
MCA_LINE_RESULT=''
|
||||
printf '%s' "$buf"
|
||||
|
||||
while true; do
|
||||
key="$(mca_read_key)" || { printf '\n'; return 1; }
|
||||
|
||||
case "$key" in
|
||||
enter)
|
||||
printf '\n'
|
||||
MCA_LINE_RESULT="$buf"
|
||||
return 0
|
||||
;;
|
||||
escape)
|
||||
printf '\n'
|
||||
return 1
|
||||
;;
|
||||
backspace)
|
||||
if [[ -n $buf ]]; then
|
||||
buf="${buf%?}"
|
||||
printf '\b \b'
|
||||
fi
|
||||
;;
|
||||
space)
|
||||
buf+=' '
|
||||
printf ' '
|
||||
;;
|
||||
up|down|left|right) ;;
|
||||
*)
|
||||
[[ ${#key} -eq 1 ]] || continue
|
||||
buf+="$key"
|
||||
printf '%s' "$key"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
mca_pause() {
|
||||
printf '\n %s' "$(mca_msg "Press any key to continue...")"
|
||||
read -rsn1 _ || true
|
||||
printf '\n'
|
||||
}
|
||||
|
||||
# _mca_row <label> <value>
|
||||
# printf's %-28s pads by bytes, so a label containing "ü" comes out one column
|
||||
# short. ${#s} counts characters in a UTF-8 locale, so the padding is computed
|
||||
# here instead - and applied inline, because command substitution would eat the
|
||||
# trailing spaces again.
|
||||
_mca_row() {
|
||||
local label="$1" value="$2" pad
|
||||
pad=$(( 30 - ${#label} ))
|
||||
(( pad < 0 )) && pad=0
|
||||
printf ' %s%*s %s\n' "$label" "$pad" '' "$value"
|
||||
}
|
||||
|
||||
_mca_onoff() {
|
||||
if [[ $1 == yes ]]; then
|
||||
printf '%s%s%s' "$MCA_C_GREEN" "$(mca_msg "ON")" "$MCA_C_RESET"
|
||||
else
|
||||
printf '%s%s%s' "$MCA_C_DIM" "$(mca_msg "OFF")" "$MCA_C_RESET"
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Status
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# mca_ui_status
|
||||
# Shared by the `status` subcommand and the menu header. Scans, so the numbers
|
||||
# describe what is on disk right now rather than what was true at the last run.
|
||||
mca_ui_status() {
|
||||
local last covered
|
||||
|
||||
mca_scan_once
|
||||
mca_count_routes
|
||||
|
||||
last="$(mca_state_read last_apply '')"
|
||||
|
||||
_mca_row "$(mca_msg "Autoscroll")" "$(_mca_onoff "$CFG_ENABLED")"
|
||||
printf '\n'
|
||||
|
||||
if [[ $CFG_ENABLED == yes ]]; then
|
||||
covered="$MCA_N_ON"
|
||||
else
|
||||
covered=0
|
||||
fi
|
||||
|
||||
_mca_row "$(mca_msg "Applications covered")" \
|
||||
"$(mca_msg "%d of %d" "$covered" "$(( MCA_N_ON + MCA_N_OFF + MCA_N_UNKNOWN ))")"
|
||||
|
||||
if (( MCA_N_UNKNOWN )); then
|
||||
_mca_row "$(mca_msg "Not identified")" \
|
||||
"${MCA_C_DIM}$(mca_msg "%d - see the applications list" "$MCA_N_UNKNOWN")${MCA_C_RESET}"
|
||||
fi
|
||||
|
||||
if mca_steam_installed; then
|
||||
if mca_steam_patched; then
|
||||
_mca_row "$(mca_msg "Steam")" "$(_mca_onoff yes)"
|
||||
elif [[ $CFG_STEAM == yes && $CFG_ENABLED == yes ]]; then
|
||||
_mca_row "$(mca_msg "Steam")" \
|
||||
"${MCA_C_YELLOW}$(mca_msg "not patched yet")${MCA_C_RESET}"
|
||||
else
|
||||
_mca_row "$(mca_msg "Steam")" "$(_mca_onoff no)"
|
||||
fi
|
||||
fi
|
||||
|
||||
if mca_watch_available; then
|
||||
if mca_watch_enabled; then
|
||||
_mca_row "$(mca_msg "New applications")" "$(_mca_onoff yes)"
|
||||
else
|
||||
_mca_row "$(mca_msg "New applications")" "$(_mca_onoff no)"
|
||||
fi
|
||||
fi
|
||||
|
||||
_mca_row "$(mca_msg "Last applied")" "$(mca_time_ago "$last")"
|
||||
|
||||
if [[ $CFG_ENABLED == yes ]]; then
|
||||
printf '\n %s%s%s\n' "$MCA_C_DIM" \
|
||||
"$(mca_msg "Applications pick this up the next time they are started.")" \
|
||||
"$MCA_C_RESET"
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Settings
|
||||
# ---------------------------------------------------------------------------
|
||||
# Format: Key|type|default|label-msgid
|
||||
# type is bool or text.
|
||||
MCA_SETTINGS=(
|
||||
"PatchApps|bool|yes|Electron and CEF applications"
|
||||
"PatchBrowsers|bool|yes|Chromium-based browsers"
|
||||
"PatchFlatpak|bool|yes|Flatpak applications"
|
||||
"PatchAutostart|bool|yes|Programs that start themselves at login"
|
||||
"PatchSteam|bool|yes|Steam"
|
||||
"PatchSpotify|bool|yes|Spotify"
|
||||
"WatchNewApps|bool|yes|Apply to newly installed applications"
|
||||
"ExtraFlags|text||Additional Chromium arguments"
|
||||
)
|
||||
|
||||
_mca_is_true() {
|
||||
case "${1,,}" in
|
||||
yes|y|true|1|on|enabled) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
MCA_SETTING_SHOWN=''
|
||||
MCA_LBL_ON=''
|
||||
MCA_LBL_OFF=''
|
||||
MCA_LBL_NONE=''
|
||||
|
||||
_mca_setting_display() {
|
||||
local type="$1" value="$2"
|
||||
|
||||
case "$type" in
|
||||
bool)
|
||||
if _mca_is_true "$value"; then
|
||||
MCA_SETTING_SHOWN="${MCA_C_GREEN}${MCA_LBL_ON}${MCA_C_RESET}"
|
||||
else
|
||||
MCA_SETTING_SHOWN="${MCA_C_DIM}${MCA_LBL_OFF}${MCA_C_RESET}"
|
||||
fi
|
||||
;;
|
||||
text)
|
||||
if [[ -n $value ]]; then
|
||||
MCA_SETTING_SHOWN="$value"
|
||||
else
|
||||
MCA_SETTING_SHOWN="${MCA_C_DIM}${MCA_LBL_NONE}${MCA_C_RESET}"
|
||||
fi
|
||||
;;
|
||||
*) MCA_SETTING_SHOWN="$value" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# mca_ui_settings
|
||||
# A cursor list rather than a numbered menu. The frame is assembled in memory
|
||||
# and written once, and everything constant is resolved before the loop:
|
||||
# drawing the naive way costs a command substitution per label per frame, which
|
||||
# is slow enough that arrow keys feel like the console is reloading.
|
||||
#
|
||||
# Returns 0 when something was changed, so the caller knows to re-apply.
|
||||
mca_ui_settings() {
|
||||
local count=${#MCA_SETTINGS[@]}
|
||||
local -a names=() types=() defaults=() labels=() values=()
|
||||
local spec name type default label locale i key frame row pad dirty=1 cursor=0
|
||||
local touched=0
|
||||
|
||||
locale="$(mca_ui_locale)"
|
||||
mca_msg_into "$locale" "ON"; MCA_LBL_ON="$MCA_MSG_RESULT"
|
||||
mca_msg_into "$locale" "OFF"; MCA_LBL_OFF="$MCA_MSG_RESULT"
|
||||
mca_msg_into "$locale" "(none)"; MCA_LBL_NONE="$MCA_MSG_RESULT"
|
||||
|
||||
for spec in "${MCA_SETTINGS[@]}"; do
|
||||
IFS='|' read -r name type default label <<< "$spec"
|
||||
names+=("$name"); types+=("$type"); defaults+=("$default")
|
||||
mca_msg_into "$locale" "$label"
|
||||
labels+=("$MCA_MSG_RESULT")
|
||||
done
|
||||
|
||||
local title hint
|
||||
mca_msg_into "$locale" "Settings"; title="$MCA_MSG_RESULT"
|
||||
mca_msg_into "$locale" "Up/Down select - Space or Right changes - q goes back"
|
||||
hint="$MCA_MSG_RESULT"
|
||||
|
||||
local clearseq
|
||||
clearseq="$(clear 2>/dev/null)" || clearseq=$'\033[H\033[2J'
|
||||
|
||||
while true; do
|
||||
if (( dirty )); then
|
||||
for i in "${!names[@]}"; do
|
||||
_mca_config_lookup "${names[i]}" "${defaults[i]}"
|
||||
values[i]="$MCA_CONFIG_VALUE"
|
||||
done
|
||||
dirty=0
|
||||
fi
|
||||
|
||||
frame="$clearseq"$'\n'"${MCA_C_BOLD}${MCA_C_BLUE} ${title}${MCA_C_RESET}"$'\n\n'
|
||||
|
||||
local marker selected="${MCA_C_BLUE}▸${MCA_C_RESET} "
|
||||
for i in "${!names[@]}"; do
|
||||
_mca_setting_display "${types[i]}" "${values[i]}"
|
||||
pad=$(( 46 - ${#labels[i]} ))
|
||||
(( pad < 0 )) && pad=0
|
||||
if (( i == cursor )); then marker="$selected"; else marker=' '; fi
|
||||
printf -v row ' %s%s%*s %s' \
|
||||
"$marker" "${labels[i]}" "$pad" '' "$MCA_SETTING_SHOWN"
|
||||
frame+="$row"$'\n'
|
||||
done
|
||||
|
||||
frame+=$'\n'" ${MCA_C_DIM}${hint}${MCA_C_RESET}"$'\n'
|
||||
printf '%s' "$frame"
|
||||
|
||||
key="$(mca_read_key)" || return "$(( ! touched ))"
|
||||
|
||||
type="${types[cursor]}"
|
||||
name="${names[cursor]}"
|
||||
|
||||
case "$key" in
|
||||
up|k) cursor=$(( (cursor - 1 + count) % count )) ;;
|
||||
down|j) cursor=$(( (cursor + 1) % count )) ;;
|
||||
space|enter|right|l|left|h)
|
||||
if [[ $type == text ]]; then
|
||||
[[ $key == left || $key == h ]] && continue
|
||||
mca_ui_edit_text "$name" "${values[cursor]}" && touched=1
|
||||
else
|
||||
if _mca_is_true "${values[cursor]}"; then
|
||||
mca_config_set "$name" no
|
||||
else
|
||||
mca_config_set "$name" yes
|
||||
fi || { mca_bad "$(mca_msg "Could not save the setting.")"; mca_pause; }
|
||||
touched=1
|
||||
fi
|
||||
dirty=1
|
||||
;;
|
||||
q|Q|escape) return "$(( ! touched ))" ;;
|
||||
*) ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# mca_ui_edit_text <key> <current>
|
||||
mca_ui_edit_text() {
|
||||
local name="$1" current="$2"
|
||||
|
||||
printf '\n %s\n' "$(mca_msg "Arguments separated by spaces, empty for none:")"
|
||||
printf ' > '
|
||||
|
||||
if mca_ui_read_line "$current"; then
|
||||
mca_config_set "$name" "$MCA_LINE_RESULT" \
|
||||
|| { mca_bad "$(mca_msg "Could not save the setting.")"; mca_pause; }
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Applications
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# mca_ui_apps
|
||||
# Every application that was found, what will happen to it, and a key to
|
||||
# change that. This is where an AppImage that could not be identified gets
|
||||
# turned on, and where a single application gets left out without having to
|
||||
# switch off its whole category.
|
||||
#
|
||||
# Returns 0 when something was changed.
|
||||
mca_ui_apps() {
|
||||
local count key frame row pad i cursor=0 dirty=1 touched=0 locale
|
||||
local -a labels=() states=()
|
||||
|
||||
locale="$(mca_ui_locale)"
|
||||
|
||||
local title hint legend
|
||||
mca_msg_into "$locale" "Applications"; title="$MCA_MSG_RESULT"
|
||||
mca_msg_into "$locale" "Up/Down select - Space turns one on or off - q goes back"
|
||||
hint="$MCA_MSG_RESULT"
|
||||
mca_msg_into "$locale" "Anything not identified is left alone until it is turned on here."
|
||||
legend="$MCA_MSG_RESULT"
|
||||
|
||||
local clearseq
|
||||
clearseq="$(clear 2>/dev/null)" || clearseq=$'\033[H\033[2J'
|
||||
|
||||
printf '%s\n %s\n' "$clearseq" "$(mca_msg "Looking at the installed applications...")"
|
||||
mca_config_load
|
||||
mca_scan_once
|
||||
count=${#MCA_IDS[@]}
|
||||
|
||||
if (( count == 0 )); then
|
||||
printf '\n %s\n' "$(mca_msg "No Chromium-based applications found.")"
|
||||
mca_pause
|
||||
return 1
|
||||
fi
|
||||
|
||||
while true; do
|
||||
if (( dirty )); then
|
||||
mca_config_load
|
||||
mca_count_routes
|
||||
labels=(); states=()
|
||||
for i in "${!MCA_IDS[@]}"; do
|
||||
labels+=("${MCA_NAMES[i]}")
|
||||
states+=("${MCA_ROUTES[i]}")
|
||||
done
|
||||
dirty=0
|
||||
fi
|
||||
|
||||
frame="$clearseq"$'\n'"${MCA_C_BOLD}${MCA_C_BLUE} ${title}${MCA_C_RESET}"$'\n\n'
|
||||
|
||||
local marker selected="${MCA_C_BLUE}▸${MCA_C_RESET} " shown
|
||||
for i in "${!labels[@]}"; do
|
||||
case "${states[i]}" in
|
||||
off) shown="${MCA_C_DIM}$(mca_msg "off")${MCA_C_RESET}" ;;
|
||||
unknown) shown="${MCA_C_DIM}$(mca_msg "cannot tell")${MCA_C_RESET}" ;;
|
||||
steam) shown="${MCA_C_GREEN}$(mca_msg "on")${MCA_C_RESET} ${MCA_C_DIM}($(mca_msg "Steam"))${MCA_C_RESET}" ;;
|
||||
flags) shown="${MCA_C_GREEN}$(mca_msg "on")${MCA_C_RESET} ${MCA_C_DIM}($(mca_msg "flag file"))${MCA_C_RESET}" ;;
|
||||
*) shown="${MCA_C_GREEN}$(mca_msg "on")${MCA_C_RESET} ${MCA_C_DIM}($(mca_msg "launcher"))${MCA_C_RESET}" ;;
|
||||
esac
|
||||
pad=$(( 34 - ${#labels[i]} ))
|
||||
(( pad < 0 )) && pad=0
|
||||
if (( i == cursor )); then marker="$selected"; else marker=' '; fi
|
||||
printf -v row ' %s%s%*s %s' "$marker" "${labels[i]}" "$pad" '' "$shown"
|
||||
frame+="$row"$'\n'
|
||||
done
|
||||
|
||||
frame+=$'\n'
|
||||
# Without this the list reads as a list of what is switched on, which
|
||||
# it is not while the whole thing is off.
|
||||
if [[ $CFG_ENABLED != yes ]]; then
|
||||
frame+=" ${MCA_C_YELLOW}$(mca_msg "Autoscroll is off - this is what would be covered.")${MCA_C_RESET}"$'\n'
|
||||
fi
|
||||
frame+=" ${MCA_C_DIM}${legend}${MCA_C_RESET}"$'\n'
|
||||
frame+=" ${MCA_C_DIM}${hint}${MCA_C_RESET}"$'\n'
|
||||
printf '%s' "$frame"
|
||||
|
||||
key="$(mca_read_key)" || return "$(( ! touched ))"
|
||||
|
||||
case "$key" in
|
||||
up|k) cursor=$(( (cursor - 1 + count) % count )) ;;
|
||||
down|j) cursor=$(( (cursor + 1) % count )) ;;
|
||||
space|enter|right|left|l|h)
|
||||
case "${states[cursor]}" in
|
||||
off|unknown)
|
||||
mca_config_list_del Skip "${MCA_IDS[cursor]}"
|
||||
mca_config_list_add Include "${MCA_IDS[cursor]}"
|
||||
;;
|
||||
*)
|
||||
mca_config_list_del Include "${MCA_IDS[cursor]}"
|
||||
mca_config_list_add Skip "${MCA_IDS[cursor]}"
|
||||
;;
|
||||
esac
|
||||
touched=1
|
||||
dirty=1
|
||||
;;
|
||||
q|Q|escape) return "$(( ! touched ))" ;;
|
||||
*) ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The menu
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
mca_ui_menu() {
|
||||
local choice
|
||||
|
||||
mca_ui_term_raw
|
||||
trap 'mca_ui_term_restore' EXIT INT TERM
|
||||
|
||||
while true; do
|
||||
mca_config_load
|
||||
|
||||
clear 2>/dev/null || true
|
||||
mca_head " $MCA_PRETTY"
|
||||
mca_ui_status
|
||||
printf '\n'
|
||||
printf ' [1] %s\n' "$(mca_msg "Turn autoscroll on or off")"
|
||||
printf ' [2] %s\n' "$(mca_msg "Apply now")"
|
||||
printf ' [3] %s\n' "$(mca_msg "Applications")"
|
||||
printf ' [4] %s\n' "$(mca_msg "Settings")"
|
||||
printf ' [q] %s\n' "$(mca_msg "Quit")"
|
||||
printf '\n > '
|
||||
|
||||
choice="$(mca_read_key)" || {
|
||||
printf '\n'; mca_ui_term_restore; trap - EXIT INT TERM; return 0
|
||||
}
|
||||
case "$choice" in
|
||||
enter|space|up|down|left|right|escape) choice='' ;;
|
||||
esac
|
||||
printf '%s\n' "$choice"
|
||||
|
||||
case "$choice" in
|
||||
1)
|
||||
MCA_UI_NEEDS_ACK=''
|
||||
if [[ $CFG_ENABLED == yes ]]; then
|
||||
mca_ui_cooked mca_do_disable
|
||||
else
|
||||
mca_ui_cooked mca_do_enable
|
||||
fi
|
||||
[[ -n $MCA_UI_NEEDS_ACK ]] && mca_pause
|
||||
;;
|
||||
2)
|
||||
mca_ui_cooked mca_do_apply
|
||||
mca_pause
|
||||
;;
|
||||
3)
|
||||
if mca_ui_apps; then
|
||||
[[ $CFG_ENABLED == yes ]] && mca_ui_cooked mca_do_apply --changed
|
||||
fi
|
||||
;;
|
||||
4)
|
||||
if mca_ui_settings; then
|
||||
[[ $CFG_ENABLED == yes ]] && mca_ui_cooked mca_do_apply --changed
|
||||
fi
|
||||
;;
|
||||
q|Q) mca_ui_term_restore; trap - EXIT INT TERM; return 0 ;;
|
||||
# Anything else - Enter, arrow keys, stray characters - just
|
||||
# redraws. Escape is deliberately not a quit key, so a mistyped
|
||||
# arrow key cannot close the menu.
|
||||
*) ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
@@ -0,0 +1,466 @@
|
||||
# shellcheck shell=bash
|
||||
#
|
||||
# Putting the flag where the application will read it, and taking it back out.
|
||||
#
|
||||
# There are two ways in, and the order matters:
|
||||
#
|
||||
# 1. A flag file. Arch's Electron and Chromium wrappers read extra arguments
|
||||
# from $XDG_CONFIG_HOME/<name>-flags.conf. This is the good one - it is the
|
||||
# supported way to pass arguments, it survives package upgrades untouched,
|
||||
# and it applies however the program is started, including from a terminal.
|
||||
#
|
||||
# 2. A desktop entry. For applications that ship their own binary with no
|
||||
# wrapper - most vendor packages, every Flatpak - there is nowhere else to
|
||||
# put an argument, so a copy of the entry with the flag appended goes into
|
||||
# ~/.local/share/applications, where it shadows the system one.
|
||||
#
|
||||
# Nothing outside $HOME is ever written to.
|
||||
|
||||
MCA_MARK_BEGIN='# >>> middleclick-autoscroll'
|
||||
MCA_MARK_END='# <<< middleclick-autoscroll'
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Command lines
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# _mca_exec_insert_at <exec line>
|
||||
# Where an argument has to go: after the program and its own arguments, but
|
||||
# before the first field code (%U and friends) or Flatpak's @@ markers, which
|
||||
# are placeholders the launcher expands rather than arguments.
|
||||
_mca_exec_insert_at() {
|
||||
local rest="$1" tok idx=0
|
||||
|
||||
while [[ -n $rest ]]; do
|
||||
while [[ $rest == ' '* ]]; do rest="${rest# }"; idx=$(( idx + 1 )); done
|
||||
[[ -n $rest ]] || break
|
||||
tok="${rest%% *}"
|
||||
case "$tok" in
|
||||
%[a-zA-Z]|@@|@@u) printf '%s\n' "$idx"; return 0 ;;
|
||||
esac
|
||||
rest="${rest:${#tok}}"
|
||||
idx=$(( idx + ${#tok} ))
|
||||
done
|
||||
|
||||
printf '%s\n' "$(( ${#1} ))"
|
||||
}
|
||||
|
||||
# _mca_exec_after_program <exec line>
|
||||
# The position just after the program name, for arguments that a program only
|
||||
# accepts before its positional ones.
|
||||
_mca_exec_after_program() {
|
||||
local rest="$1" idx=0 tok
|
||||
|
||||
while [[ $rest == ' '* ]]; do rest="${rest# }"; idx=$(( idx + 1 )); done
|
||||
tok="${rest%% *}"
|
||||
printf '%s\n' "$(( idx + ${#tok} ))"
|
||||
}
|
||||
|
||||
# mca_exec_inject <exec line> [flags] [position: before-fields|after-program]
|
||||
# The same line with the given flags added, defaulting to the configured ones.
|
||||
# Prints nothing and returns 1 when there was nothing to change, so callers can
|
||||
# tell a real edit from a no-op without diffing.
|
||||
#
|
||||
# An --enable-blink-features that is already there is extended rather than
|
||||
# repeated: Chromium keeps the last occurrence of the option and drops the
|
||||
# rest, so a second one would silently disable whatever the first turned on.
|
||||
mca_exec_inject() {
|
||||
local line="$1" flagstr="${2:-}" where="${3:-before-fields}"
|
||||
local flag value existing at changed=0
|
||||
local -a flags
|
||||
|
||||
[[ -n $flagstr ]] || flagstr="$(mca_flags)"
|
||||
read -r -a flags <<< "$flagstr"
|
||||
|
||||
for flag in "${flags[@]}"; do
|
||||
if [[ $flag == --enable-blink-features=* ]]; then
|
||||
value="${flag#--enable-blink-features=}"
|
||||
if [[ $line =~ --enable-blink-features=([A-Za-z0-9,_-]*) ]]; then
|
||||
existing="${BASH_REMATCH[1]}"
|
||||
[[ ",$existing," == *",$value,"* ]] && continue
|
||||
line="${line/--enable-blink-features=$existing/--enable-blink-features=${existing:+$existing,}$value}"
|
||||
changed=1
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
|
||||
[[ " $line " == *" $flag "* ]] && continue
|
||||
|
||||
if [[ $where == after-program ]]; then
|
||||
at="$(_mca_exec_after_program "$line")"
|
||||
line="${line:0:$at} $flag${line:$at}"
|
||||
else
|
||||
at="$(_mca_exec_insert_at "$line")"
|
||||
if (( at >= ${#line} )); then
|
||||
line="${line% } $flag"
|
||||
else
|
||||
line="${line:0:$at}$flag ${line:$at}"
|
||||
fi
|
||||
fi
|
||||
changed=1
|
||||
done
|
||||
|
||||
(( changed )) || return 1
|
||||
printf '%s\n' "$line"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Flag files
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# mca_flags_apply <program>
|
||||
# Writes the flags into the file the program's launcher reads. Returns 0 when
|
||||
# the program has such a file (whether or not anything needed changing), 1 when
|
||||
# it has none and the desktop entry route has to be used instead.
|
||||
mca_flags_apply() {
|
||||
local prog="$1" name target='' existing_target=''
|
||||
local -a candidates=()
|
||||
|
||||
mapfile -t candidates < <(mca_flags_candidates "$prog" | awk '!seen[$0]++')
|
||||
(( ${#candidates[@]} )) || return 1
|
||||
|
||||
# A wrapper that offers several files reads the first one that exists and
|
||||
# ignores the rest, so an existing file is always the right one to edit.
|
||||
for name in "${candidates[@]}"; do
|
||||
if [[ -f "$MCA_XDG_CONFIG/$name" ]]; then
|
||||
existing_target="$name"
|
||||
_mca_flags_write "$MCA_XDG_CONFIG/$name"
|
||||
fi
|
||||
done
|
||||
[[ -n $existing_target ]] && return 0
|
||||
|
||||
# None exists yet. The last candidate is the wrapper's own fallback - the
|
||||
# generic electron-flags.conf rather than electron43-flags.conf - so
|
||||
# creating that one also covers every other application using the same
|
||||
# shared Electron.
|
||||
target="${candidates[-1]}"
|
||||
_mca_flags_write "$MCA_XDG_CONFIG/$target"
|
||||
return 0
|
||||
}
|
||||
|
||||
# _mca_flags_write <file>
|
||||
# Adds the flags as a marked block, or merges into an --enable-blink-features
|
||||
# line that is already in the file.
|
||||
_mca_flags_write() {
|
||||
local file="$1" content='' line flag value existing merged=0 block=''
|
||||
local -a flags
|
||||
local created=0
|
||||
|
||||
read -r -a flags <<< "$(mca_flags)"
|
||||
|
||||
[[ -f $file ]] || created=1
|
||||
if (( ! created )); then
|
||||
content="$(< "$file")"
|
||||
# Already ours: the block is rewritten below, so drop the old one first.
|
||||
if [[ $content == *"$MCA_MARK_BEGIN"* ]]; then
|
||||
content="$(_mca_flags_drop_block "$file")"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Merge into a blink-features line the user (or a distribution) put there.
|
||||
if [[ $content == *--enable-blink-features=* ]]; then
|
||||
local out=''
|
||||
while IFS= read -r line; do
|
||||
if [[ $line =~ ^[[:space:]]*--enable-blink-features=([A-Za-z0-9,_-]*)[[:space:]]*$ ]]; then
|
||||
existing="${BASH_REMATCH[1]}"
|
||||
if [[ ",$existing," != *",$MCA_FEATURE,"* ]]; then
|
||||
line="--enable-blink-features=${existing:+$existing,}$MCA_FEATURE"
|
||||
fi
|
||||
merged=1
|
||||
fi
|
||||
out+="$line"$'\n'
|
||||
done <<< "$content"
|
||||
content="${out%$'\n'}"
|
||||
fi
|
||||
|
||||
for flag in "${flags[@]}"; do
|
||||
(( merged )) && [[ $flag == --enable-blink-features=* ]] && continue
|
||||
block+="$flag"$'\n'
|
||||
done
|
||||
|
||||
if [[ -n $block ]]; then
|
||||
content="${content:+$content$'\n'}$MCA_MARK_BEGIN"$'\n'"$block$MCA_MARK_END"
|
||||
fi
|
||||
|
||||
if mca_write_if_changed "$file" "$content"$'\n'; then
|
||||
MCA_CHANGES=$(( MCA_CHANGES + 1 ))
|
||||
fi
|
||||
|
||||
if (( created )); then
|
||||
mca_ledger_add flags "$file" created
|
||||
elif (( merged )) && [[ -z $block ]]; then
|
||||
mca_ledger_add flags "$file" merged
|
||||
elif (( merged )); then
|
||||
mca_ledger_add flags "$file" merged+block
|
||||
else
|
||||
mca_ledger_add flags "$file" block
|
||||
fi
|
||||
|
||||
# Every application whose flag file was touched counts as covered, so the
|
||||
# status line can say how many there are.
|
||||
MCA_TOUCHED+=("$file")
|
||||
return 0
|
||||
}
|
||||
|
||||
_mca_flags_drop_block() {
|
||||
awk -v b="$MCA_MARK_BEGIN" -v e="$MCA_MARK_END" '
|
||||
$0 == b { skip = 1; next }
|
||||
$0 == e { skip = 0; next }
|
||||
!skip
|
||||
' "$1" 2>/dev/null
|
||||
}
|
||||
|
||||
# mca_flags_revert <file> <detail>
|
||||
mca_flags_revert() {
|
||||
local file="$1" detail="$2" content line out='' existing new
|
||||
|
||||
[[ -f $file ]] || return 0
|
||||
|
||||
case "$detail" in
|
||||
created)
|
||||
# Only remove the file if nothing but our block ended up in it;
|
||||
# anything else means the user started using it in the meantime.
|
||||
content="$(_mca_flags_drop_block "$file")"
|
||||
if [[ -z ${content//[[:space:]]/} ]]; then
|
||||
rm -f -- "$file"
|
||||
return 0
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
content="$(_mca_flags_drop_block "$file")"
|
||||
|
||||
if [[ $detail == merged* ]]; then
|
||||
while IFS= read -r line; do
|
||||
if [[ $line =~ ^[[:space:]]*--enable-blink-features=([A-Za-z0-9,_-]*)[[:space:]]*$ ]]; then
|
||||
existing="${BASH_REMATCH[1]}"
|
||||
new="${existing//$MCA_FEATURE/}"
|
||||
new="${new//,,/,}"; new="${new#,}"; new="${new%,}"
|
||||
[[ -z $new ]] && continue
|
||||
line="--enable-blink-features=$new"
|
||||
fi
|
||||
out+="$line"$'\n'
|
||||
done <<< "$content"
|
||||
content="${out%$'\n'}"
|
||||
fi
|
||||
|
||||
if [[ -z ${content//[[:space:]]/} ]]; then
|
||||
rm -f -- "$file"
|
||||
else
|
||||
printf '%s\n' "$content" > "$file"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Desktop entries
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# _mca_desktop_transform <file> [flags] [position]
|
||||
# The whole file with every Exec line rewritten - the main one and the one in
|
||||
# each Desktop Action, because those are the right-click menu entries and a
|
||||
# user who starts Steam from "Library" expects the same behaviour there.
|
||||
_mca_desktop_transform() {
|
||||
local file="$1" flags="${2:-}" where="${3:-before-fields}"
|
||||
local line rest new marked=0
|
||||
|
||||
while IFS= read -r line || [[ -n $line ]]; do
|
||||
case "$line" in
|
||||
Exec=*)
|
||||
rest="${line#Exec=}"
|
||||
new="$(mca_exec_inject "$rest" "$flags" "$where")" && line="Exec=$new"
|
||||
;;
|
||||
# A D-Bus activated entry is started through its service file and
|
||||
# the Exec line is never used, so the flag would go nowhere.
|
||||
DBusActivatable=true)
|
||||
line='DBusActivatable=false'
|
||||
;;
|
||||
# A marker from an older version; the current one is written below.
|
||||
X-MCA-*)
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
|
||||
printf '%s\n' "$line"
|
||||
|
||||
if (( ! marked )) && [[ $line =~ ^[[:space:]]*\[Desktop\ Entry\][[:space:]]*$ ]]; then
|
||||
printf 'X-MCA-Generated=%s\n' "$MCA_VERSION"
|
||||
marked=1
|
||||
fi
|
||||
done < "$file"
|
||||
}
|
||||
|
||||
# mca_desktop_apply <id> <source entry> [flags] [position]
|
||||
# Puts a patched copy in ~/.local/share/applications, or patches the entry in
|
||||
# place when it already lives there.
|
||||
mca_desktop_apply() {
|
||||
local id="$1" src="$2" flags="${3:-}" where="${4:-before-fields}"
|
||||
local target="$MCA_APPDIR/$id.desktop" content backup
|
||||
|
||||
if [[ "$src" == "$target" ]]; then
|
||||
# The user's own entry - an AppImage, a web app shortcut, something
|
||||
# installed by hand. There is nowhere to shadow it from, so it is
|
||||
# edited directly and the original is kept.
|
||||
grep -q '^X-MCA-Generated=' "$src" 2>/dev/null && return 0
|
||||
|
||||
content="$(_mca_desktop_transform "$src" "$flags" "$where")"
|
||||
[[ -n $content ]] || return 1
|
||||
[[ "$content" == "$(< "$src")" ]] && return 0
|
||||
|
||||
backup="$(mca_backup "$src")" || return 1
|
||||
if mca_write_if_changed "$src" "$content"$'\n'; then
|
||||
MCA_CHANGES=$(( MCA_CHANGES + 1 ))
|
||||
fi
|
||||
mca_ledger_add inplace "$src" "$backup"
|
||||
MCA_TOUCHED+=("$src")
|
||||
return 0
|
||||
fi
|
||||
|
||||
content="$(_mca_desktop_transform "$src" "$flags" "$where")"
|
||||
[[ -n $content ]] || return 1
|
||||
|
||||
mkdir -p "$MCA_APPDIR" 2>/dev/null || return 1
|
||||
|
||||
# Something is already shadowing this entry. If it is not ours it is the
|
||||
# user's own override and is left exactly as it is.
|
||||
if [[ -e $target ]] && ! grep -q '^X-MCA-Generated=' "$target" 2>/dev/null; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if mca_write_if_changed "$target" "$content"$'\n'; then
|
||||
MCA_CHANGES=$(( MCA_CHANGES + 1 ))
|
||||
fi
|
||||
mca_ledger_add shadow "$target" "$src"
|
||||
MCA_TOUCHED+=("$target")
|
||||
return 0
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Autostart entries
|
||||
# ---------------------------------------------------------------------------
|
||||
# Applications that start themselves at login write their own entry into
|
||||
# ~/.config/autostart, pointing straight at their binary. Those bypass the
|
||||
# desktop entry in the menu completely, which is why Discord launched at login
|
||||
# used to behave differently from Discord launched from the menu.
|
||||
|
||||
mca_autostart_apply() {
|
||||
local dir="$MCA_XDG_CONFIG/autostart" file exec_line prog content backup
|
||||
|
||||
[[ -d $dir ]] || return 0
|
||||
|
||||
for file in "$dir"/*.desktop; do
|
||||
[[ -f $file ]] || continue
|
||||
grep -q '^X-MCA-Generated=' "$file" 2>/dev/null && continue
|
||||
|
||||
exec_line="$(mca_desktop_get "$file" Exec)"
|
||||
[[ -n $exec_line ]] || continue
|
||||
|
||||
mca_exec_program "$exec_line" || continue
|
||||
prog="$MCA_PROG"
|
||||
mca_is_chromium "$prog" || continue
|
||||
|
||||
content="$(_mca_desktop_transform "$file")"
|
||||
[[ -n $content ]] || continue
|
||||
[[ "$content" == "$(< "$file")" ]] && continue
|
||||
|
||||
backup="$(mca_backup "$file")" || continue
|
||||
if mca_write_if_changed "$file" "$content"$'\n'; then
|
||||
MCA_CHANGES=$(( MCA_CHANGES + 1 ))
|
||||
fi
|
||||
mca_ledger_add inplace "$file" "$backup"
|
||||
MCA_TOUCHED+=("$file")
|
||||
done
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Spotify
|
||||
# ---------------------------------------------------------------------------
|
||||
# The official client is CEF rather than Electron and, when installed through
|
||||
# spotify-launcher, is started by a program that builds its own command line.
|
||||
# That launcher has a configuration file with a slot for exactly this.
|
||||
|
||||
mca_spotify_apply() {
|
||||
local file="$MCA_XDG_CONFIG/spotify-launcher.conf" content line out='' backup
|
||||
local found_section=0 found_args=0
|
||||
|
||||
mca_have spotify-launcher || return 0
|
||||
|
||||
if [[ -f $file ]]; then
|
||||
content="$(< "$file")"
|
||||
while IFS= read -r line; do
|
||||
if [[ $line =~ ^[[:space:]]*\[spotify\][[:space:]]*$ ]]; then
|
||||
found_section=1
|
||||
elif [[ $line =~ ^[[:space:]]*extra_arguments[[:space:]]*=[[:space:]]*\[(.*)\][[:space:]]*$ ]]; then
|
||||
local args="${BASH_REMATCH[1]}"
|
||||
found_args=1
|
||||
if [[ $args != *"$MCA_FEATURE"* ]]; then
|
||||
line="extra_arguments = [${args:+$args, }\"$MCA_FLAG\"]"
|
||||
fi
|
||||
fi
|
||||
out+="$line"$'\n'
|
||||
done <<< "$content"
|
||||
content="${out%$'\n'}"
|
||||
fi
|
||||
|
||||
if (( ! found_args )); then
|
||||
(( found_section )) || content="${content:+$content$'\n'}[spotify]"
|
||||
content="${content:+$content$'\n'}extra_arguments = [\"$MCA_FLAG\"]"
|
||||
fi
|
||||
|
||||
[[ -f $file ]] && backup="$(mca_backup "$file")"
|
||||
|
||||
if mca_write_if_changed "$file" "$content"$'\n'; then
|
||||
MCA_CHANGES=$(( MCA_CHANGES + 1 ))
|
||||
fi
|
||||
mca_ledger_add inplace "$file" "${backup:-}"
|
||||
MCA_TOUCHED+=("$file")
|
||||
return 0
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Undo
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
mca_revert_all() {
|
||||
local kind path detail
|
||||
|
||||
[[ -f $MCA_LEDGER ]] || return 0
|
||||
|
||||
# Backwards, so a file that was recorded twice ends up at its oldest state.
|
||||
while IFS=$'\t' read -r kind path detail; do
|
||||
[[ -n $path ]] || continue
|
||||
case "$kind" in
|
||||
shadow)
|
||||
# Only remove what is still recognisably ours.
|
||||
if [[ -f $path ]] && grep -q '^X-MCA-Generated=' "$path" 2>/dev/null; then
|
||||
rm -f -- "$path"
|
||||
MCA_CHANGES=$(( MCA_CHANGES + 1 ))
|
||||
fi
|
||||
;;
|
||||
inplace)
|
||||
if [[ -n $detail && -f "$MCA_BACKUPDIR/$detail" ]]; then
|
||||
if [[ -e $path ]] || [[ -d "$(dirname -- "$path")" ]]; then
|
||||
cp -p -- "$MCA_BACKUPDIR/$detail" "$path" 2>/dev/null \
|
||||
&& MCA_CHANGES=$(( MCA_CHANGES + 1 ))
|
||||
fi
|
||||
rm -f -- "$MCA_BACKUPDIR/$detail"
|
||||
elif [[ -f $path ]]; then
|
||||
# No backup: the file did not exist before we wrote it.
|
||||
rm -f -- "$path"
|
||||
MCA_CHANGES=$(( MCA_CHANGES + 1 ))
|
||||
fi
|
||||
;;
|
||||
flags)
|
||||
mca_flags_revert "$path" "$detail" \
|
||||
&& MCA_CHANGES=$(( MCA_CHANGES + 1 ))
|
||||
;;
|
||||
steam)
|
||||
mca_steam_revert "$path" "$detail" \
|
||||
&& MCA_CHANGES=$(( MCA_CHANGES + 1 ))
|
||||
;;
|
||||
esac
|
||||
done < <(tac "$MCA_LEDGER" 2>/dev/null)
|
||||
|
||||
rm -f -- "$MCA_LEDGER"
|
||||
rmdir "$MCA_BACKUPDIR" 2>/dev/null || true
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
# shellcheck shell=bash
|
||||
#
|
||||
# Steam.
|
||||
#
|
||||
# Steam's interface is CEF, so the feature is there - but Steam builds the
|
||||
# command line for its web helper itself and offers no way to add to it. The
|
||||
# only place to get an argument in is the shell script that starts the helper,
|
||||
# which lives inside Steam's own installation:
|
||||
#
|
||||
# ~/.local/share/Steam/ubuntu12_64/steamwebhelper_sniper_wrap.sh
|
||||
#
|
||||
# Steam checksums that script at every start and restores it when it differs,
|
||||
# which is why the launcher gets -noverifyfiles as well. The trade-off is real
|
||||
# and belongs to the user, so this is a switch of its own rather than part of
|
||||
# the general application handling: with verification off, Steam no longer
|
||||
# repairs a damaged installation by itself.
|
||||
#
|
||||
# The file comes back on every client update, and the watcher re-applies the
|
||||
# patch when that happens.
|
||||
|
||||
MCA_STEAM_EXEC='exec ./steamwebhelper "$@"'
|
||||
MCA_STEAM_LAUNCH_FLAG='-noverifyfiles'
|
||||
|
||||
# Every place a Steam installation is known to live, resolved and de-duplicated
|
||||
# because ~/.steam/steam is normally a symlink into ~/.local/share.
|
||||
mca_steam_roots() {
|
||||
local candidates=(
|
||||
"$MCA_XDG_DATA/Steam"
|
||||
"$HOME/.steam/steam"
|
||||
"$HOME/.steam/root"
|
||||
"$HOME/.steam/debian-installation"
|
||||
"$HOME/.var/app/com.valvesoftware.Steam/.local/share/Steam"
|
||||
)
|
||||
local dir real
|
||||
local -A seen=()
|
||||
|
||||
for dir in "${candidates[@]}"; do
|
||||
[[ -d $dir ]] || continue
|
||||
real="$(readlink -f -- "$dir" 2>/dev/null)" || continue
|
||||
[[ -n ${seen[$real]+set} ]] && continue
|
||||
seen[$real]=1
|
||||
printf '%s\n' "$real"
|
||||
done
|
||||
}
|
||||
|
||||
# The script to patch inside a Steam installation. Newer clients start the
|
||||
# helper through a wrapper inside the container runtime; older ones exec it
|
||||
# from steamwebhelper.sh directly, and that one forwards its arguments too.
|
||||
mca_steam_script() {
|
||||
local root="$1" f
|
||||
for f in \
|
||||
"$root/ubuntu12_64/steamwebhelper_sniper_wrap.sh" \
|
||||
"$root/ubuntu12_64/steamwebhelper.sh"
|
||||
do
|
||||
[[ -f $f && -w $f ]] && { printf '%s\n' "$f"; return 0; }
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
mca_steam_installed() {
|
||||
local root
|
||||
while IFS= read -r root; do
|
||||
mca_steam_script "$root" > /dev/null && return 0
|
||||
done < <(mca_steam_roots)
|
||||
return 1
|
||||
}
|
||||
|
||||
# mca_steam_script_patched <script>
|
||||
mca_steam_script_patched() {
|
||||
grep -q -- "$MCA_FEATURE" "$1" 2>/dev/null
|
||||
}
|
||||
|
||||
mca_steam_patched() {
|
||||
local root script
|
||||
while IFS= read -r root; do
|
||||
script="$(mca_steam_script "$root")" || continue
|
||||
mca_steam_script_patched "$script" && return 0
|
||||
done < <(mca_steam_roots)
|
||||
return 1
|
||||
}
|
||||
|
||||
# _mca_steam_exec_line <script>
|
||||
# The line number of the command that starts the web helper: the last
|
||||
# uncommented line that both names steamwebhelper and forwards "$@". Both the
|
||||
# current layout (exec ./steamwebhelper "$@" inside the container wrapper) and
|
||||
# the older one (steamwebhelper.sh handing the arguments to the runtime entry
|
||||
# point) end in exactly that.
|
||||
_mca_steam_exec_line() {
|
||||
awk '
|
||||
/^[[:space:]]*#/ { next }
|
||||
/steamwebhelper/ && /"\$@"/ { n = NR }
|
||||
END { if (n) print n }
|
||||
' "$1" 2>/dev/null
|
||||
}
|
||||
|
||||
# mca_steam_apply
|
||||
# Appends the flags to the line that starts the web helper. Everything else in
|
||||
# the script - setting up the container runtime, deciding whether the sandbox
|
||||
# can be used - is left alone.
|
||||
mca_steam_apply() {
|
||||
local root script backup flags lineno found=0 out
|
||||
|
||||
flags="$(mca_flags)"
|
||||
|
||||
while IFS= read -r root; do
|
||||
script="$(mca_steam_script "$root")" || continue
|
||||
found=1
|
||||
|
||||
mca_steam_script_patched "$script" && { MCA_TOUCHED+=("$script"); continue; }
|
||||
|
||||
lineno="$(_mca_steam_exec_line "$script")"
|
||||
if [[ -z $lineno ]]; then
|
||||
# A client update changed how the helper is started. Leaving the
|
||||
# file alone is the only safe answer: this script is what starts
|
||||
# Steam's entire interface.
|
||||
mca_note "$(mca_msg "Steam starts its interface in a way this version does not recognise; leaving it alone.")"
|
||||
continue
|
||||
fi
|
||||
|
||||
out="$(awk -v n="$lineno" -v f=" $flags" 'NR == n { $0 = $0 f } { print }' "$script")" || continue
|
||||
|
||||
backup="$(mca_backup "$script")" || continue
|
||||
if mca_write_if_changed "$script" "$out"$'\n'; then
|
||||
MCA_CHANGES=$(( MCA_CHANGES + 1 ))
|
||||
fi
|
||||
chmod +x -- "$script" 2>/dev/null || true
|
||||
mca_ledger_add steam "$script" "$backup"
|
||||
MCA_TOUCHED+=("$script")
|
||||
done < <(mca_steam_roots)
|
||||
|
||||
(( found ))
|
||||
}
|
||||
|
||||
# mca_steam_revert <script> <backup name>
|
||||
mca_steam_revert() {
|
||||
local script="$1" backup="$2"
|
||||
|
||||
if [[ -n $backup && -f "$MCA_BACKUPDIR/$backup" ]]; then
|
||||
if [[ -e $script ]]; then
|
||||
cp -p -- "$MCA_BACKUPDIR/$backup" "$script" 2>/dev/null || true
|
||||
chmod +x -- "$script" 2>/dev/null || true
|
||||
fi
|
||||
rm -f -- "$MCA_BACKUPDIR/$backup"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# No backup to fall back on - take the flags back out of the exec line.
|
||||
[[ -f $script ]] || return 1
|
||||
local tmp
|
||||
tmp="$(mktemp "${script}.XXXXXX")" || return 1
|
||||
sed "s| $MCA_FLAG||g" "$script" > "$tmp" && mv -f "$tmp" "$script" || { rm -f "$tmp"; return 1; }
|
||||
chmod +x -- "$script" 2>/dev/null || true
|
||||
return 0
|
||||
}
|
||||
|
||||
# mca_steam_desktop_apply <id> <source entry>
|
||||
# Steam restores its own files at every start unless it is told not to verify
|
||||
# them, so the launcher entry carries that switch. It goes directly after the
|
||||
# program because Steam reads its options before the steam:// argument that the
|
||||
# right-click actions pass.
|
||||
mca_steam_desktop_apply() {
|
||||
mca_desktop_apply "$1" "$2" "$MCA_STEAM_LAUNCH_FLAG" after-program
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# middleclick-autoscroll - middle-click autoscroll for Chromium-based
|
||||
# applications on Linux
|
||||
#
|
||||
# Blink has had Windows-style autoscroll for years, behind a runtime flag that
|
||||
# is not turned on for Linux because middle click is taken by primary-selection
|
||||
# paste there. Turning it on is a matter of one command line argument - the
|
||||
# work is finding every application that needs it, putting the argument
|
||||
# somewhere it survives an update, and doing the same for the two programs that
|
||||
# do not take arguments at all.
|
||||
#
|
||||
# Copyright (C) 2026 Felitendo
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
MCA_LIBDIR="${MCA_LIBDIR:-@LIBDIR@}"
|
||||
|
||||
for _mod in common config detect patch steam apply menu; do
|
||||
# shellcheck source=/dev/null
|
||||
if ! source "$MCA_LIBDIR/$_mod.sh"; then
|
||||
printf 'middleclick-autoscroll: cannot load %s/%s.sh\n' "$MCA_LIBDIR" "$_mod" >&2
|
||||
exit 14
|
||||
fi
|
||||
done
|
||||
unset _mod
|
||||
|
||||
# Nothing here writes outside $HOME, so running it as root would patch root's
|
||||
# own applications and leave the actual user without any.
|
||||
if [[ $EUID -eq 0 && -z ${MCA_ALLOW_ROOT:-} ]]; then
|
||||
printf 'middleclick-autoscroll: run this as your own user, not as root.\n' >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Commands
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
mca_do_enable() {
|
||||
mca_config_set Enabled yes || {
|
||||
mca_bad "$(mca_msg "Could not save the setting.")"
|
||||
return 1
|
||||
}
|
||||
mca_config_load
|
||||
|
||||
mca_apply
|
||||
|
||||
if [[ $CFG_WATCH == yes ]]; then
|
||||
if mca_watch_available; then
|
||||
mca_watch_enable || mca_note "$(mca_msg "Could not start the watcher for newly installed applications.")"
|
||||
else
|
||||
mca_note "$(mca_msg "No systemd user session - newly installed applications will not be picked up on their own.")"
|
||||
fi
|
||||
fi
|
||||
|
||||
mca_ok "$(mca_msg "Autoscroll is on for %d applications." "$MCA_N_ON")"
|
||||
|
||||
if [[ $CFG_STEAM == yes ]] && mca_steam_installed; then
|
||||
mca_note "$(mca_msg "Steam's launcher now skips its file check, which is what keeps the change from being undone at every start.")"
|
||||
fi
|
||||
|
||||
mca_note "$(mca_msg "Applications that are already running pick this up when they are restarted.")"
|
||||
}
|
||||
|
||||
mca_do_disable() {
|
||||
mca_config_set Enabled no || {
|
||||
mca_bad "$(mca_msg "Could not save the setting.")"
|
||||
return 1
|
||||
}
|
||||
|
||||
mca_revert
|
||||
mca_watch_disable
|
||||
|
||||
mca_ok "$(mca_msg "Autoscroll is off and everything that was changed has been put back.")"
|
||||
}
|
||||
|
||||
mca_do_apply() {
|
||||
local changed=0 arg
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--changed) changed=1 ;;
|
||||
--quiet) MCA_QUIET=1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
mca_config_load
|
||||
|
||||
if [[ $CFG_ENABLED != yes ]]; then
|
||||
# Called by the watcher while switched off: there is nothing to do, and
|
||||
# nothing to complain about either.
|
||||
[[ -n $MCA_QUIET ]] && return 0
|
||||
mca_note "$(mca_msg "Autoscroll is off. Turn it on first.")"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# A settings change can take an application out of scope as well as bring
|
||||
# one in, and only a full undo can tell the difference, so that path starts
|
||||
# from a clean slate. The watcher never does this: rewriting files that are
|
||||
# already correct would make it trigger itself.
|
||||
if (( changed )); then
|
||||
mca_revert
|
||||
fi
|
||||
|
||||
mca_apply
|
||||
|
||||
if (( MCA_CHANGES )); then
|
||||
mca_ok "$(mca_msg "Applied to %d applications." "$MCA_N_ON")"
|
||||
mca_say " $(mca_msg "Applications that are already running pick this up when they are restarted.")"
|
||||
else
|
||||
mca_ok "$(mca_msg "Already applied to %d applications." "$MCA_N_ON")"
|
||||
fi
|
||||
|
||||
if [[ $CFG_WATCH == yes ]] && mca_watch_available && ! mca_watch_enabled; then
|
||||
mca_watch_enable || true
|
||||
elif [[ $CFG_WATCH != yes ]] && mca_watch_enabled; then
|
||||
mca_watch_disable
|
||||
fi
|
||||
}
|
||||
|
||||
mca_do_status() {
|
||||
mca_config_load
|
||||
mca_head " $MCA_PRETTY"
|
||||
mca_ui_status
|
||||
printf '\n'
|
||||
}
|
||||
|
||||
mca_do_list() {
|
||||
local i route label pad
|
||||
|
||||
mca_config_load
|
||||
mca_scan_once
|
||||
mca_count_routes
|
||||
|
||||
if (( ${#MCA_IDS[@]} == 0 )); then
|
||||
mca_note "$(mca_msg "No Chromium-based applications found.")"
|
||||
return 0
|
||||
fi
|
||||
|
||||
mca_head " $(mca_msg "Applications")"
|
||||
|
||||
for i in "${!MCA_IDS[@]}"; do
|
||||
route="${MCA_ROUTES[i]}"
|
||||
label="${MCA_NAMES[i]}"
|
||||
pad=$(( 34 - ${#label} ))
|
||||
(( pad < 0 )) && pad=0
|
||||
printf ' %s%*s %s\n' "$label" "$pad" '' "$(mca_route_label "$route")"
|
||||
done
|
||||
printf '\n'
|
||||
}
|
||||
|
||||
mca_do_help() {
|
||||
cat <<- EOF
|
||||
$MCA_PRETTY $MCA_VERSION
|
||||
|
||||
$(mca_msg "Usage: middleclick-autoscroll [command]")
|
||||
|
||||
$(mca_msg "Commands:")
|
||||
enable $(mca_msg "Turn autoscroll on")
|
||||
disable $(mca_msg "Turn it off and put everything back")
|
||||
apply $(mca_msg "Apply to anything installed since the last run")
|
||||
status $(mca_msg "Show what is covered")
|
||||
list $(mca_msg "List every application that was found")
|
||||
-h, --help $(mca_msg "Show this help")
|
||||
-V, --version $(mca_msg "Show the version")
|
||||
|
||||
$(mca_msg "Without a command an interactive menu is shown.")
|
||||
EOF
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Dispatch
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
main() {
|
||||
local cmd="${1:-}"
|
||||
[[ $# -gt 0 ]] && shift
|
||||
|
||||
case "$cmd" in
|
||||
enable) mca_do_enable "$@" ;;
|
||||
disable) mca_do_disable "$@" ;;
|
||||
apply|refresh) mca_do_apply "$@" ;;
|
||||
status) mca_do_status "$@" ;;
|
||||
list) mca_do_list "$@" ;;
|
||||
|
||||
-h|--help|help) mca_do_help ;;
|
||||
-V|--version) printf '%s %s\n' "$MCA_NAME" "$MCA_VERSION" ;;
|
||||
|
||||
'') mca_ui_menu ;;
|
||||
*)
|
||||
mca_bad "$(mca_msg "Unknown command: %s" "$cmd")"
|
||||
printf '\n'
|
||||
mca_do_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in new issue
Block a user