feat: add bottles-opener
This commit is contained in:
commit
c831d860f3
27 files changed
+5710
No files matched your search
@@ -0,0 +1,2 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
@@ -0,0 +1,15 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: Felitendo
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: felitendo
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
polar: # Replace with a single Polar username
|
||||
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
|
||||
thanks_dev: # Replace with a single thanks.dev username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
@@ -0,0 +1,28 @@
|
||||
name: check
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: syntax and shellcheck
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
# The Makefile skips whatever is missing rather than failing, so the
|
||||
# tools it looks for have to be here or the check checks less than it
|
||||
# appears to.
|
||||
- name: Install the tools the Makefile looks for
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
shellcheck gettext scdoc
|
||||
|
||||
- run: make check
|
||||
|
||||
- name: Build the catalogs and the man page
|
||||
run: make build
|
||||
@@ -0,0 +1,9 @@
|
||||
# build artifacts
|
||||
po/*.mo
|
||||
doc/*.1
|
||||
|
||||
# packages
|
||||
dist/
|
||||
|
||||
# python
|
||||
__pycache__/
|
||||
@@ -0,0 +1,23 @@
|
||||
# CLAUDE.md
|
||||
|
||||
## Style
|
||||
|
||||
Keep everything short — replies, explanations, comments, docs.
|
||||
|
||||
## Commits
|
||||
|
||||
- English only.
|
||||
- Conventional Commits prefix: `feat:`, `fix:`, `chore:`, `docs:`, `refactor:`, `ci:`, `build:`.
|
||||
- Subject as short as possible. Imperative, lowercase, no trailing period.
|
||||
- Body only when something genuinely cannot be inferred from the diff.
|
||||
- Never add `Co-Authored-By`, "Generated with" or any other AI attribution to commits or PR descriptions.
|
||||
|
||||
## Testing
|
||||
|
||||
Never test against the real setup: `enable` and `apply` rewrite the user's file associations.
|
||||
|
||||
- `scripts/sandbox.sh <command>` runs the checkout with every XDG dir in a throwaway sandbox. It
|
||||
reads the real bottles, writes nothing outside the sandbox and leaves the watcher off.
|
||||
- `BO_DRY_RUN=1 scripts/sandbox.sh open <launcher> <file>` prints the launch command instead of
|
||||
starting the program. Launcher ids are in the sandbox's `state/bottles-opener/launchers`.
|
||||
- `make check` after every change. New strings: `make pot`, then translate them in `po/de.po`.
|
||||
@@ -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,151 @@
|
||||
# bottles-opener - build and install
|
||||
#
|
||||
# Everything here is plain shell and one small Python script; "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 ?= 0.1.0
|
||||
|
||||
PREFIX ?= /usr
|
||||
DESTDIR ?=
|
||||
BINDIR ?= $(PREFIX)/bin
|
||||
DATADIR ?= $(PREFIX)/share
|
||||
LIBDIR ?= $(DATADIR)/bottles-opener/lib
|
||||
LOCALEDIR ?= $(DATADIR)/locale
|
||||
MANDIR ?= $(DATADIR)/man
|
||||
|
||||
# Where systemd looks for user units. For a normal install into /usr this is
|
||||
# asked of systemd itself, because the answer is not the same everywhere, and
|
||||
# only guessed at when there is no systemd installed to ask.
|
||||
#
|
||||
# A build with a prefix of its own keeps the units under that prefix instead,
|
||||
# and under share rather than lib: $XDG_DATA_HOME/systemd/user is a search path
|
||||
# and ~/.local/lib/systemd/user is not, so an install into ~/.local that put
|
||||
# the units in lib would leave the watcher impossible to enable.
|
||||
ifeq ($(PREFIX),/usr)
|
||||
USERUNITDIR ?= $(shell pkg-config --variable=systemduserunitdir systemd 2>/dev/null || echo /usr/lib/systemd/user)
|
||||
else
|
||||
USERUNITDIR ?= $(DATADIR)/systemd/user
|
||||
endif
|
||||
|
||||
LINGUAS := de
|
||||
MOFILES := $(patsubst %,po/%.mo,$(LINGUAS))
|
||||
MANPAGE := doc/bottles-opener.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 pot clean version
|
||||
|
||||
all: build
|
||||
|
||||
build: $(MOFILES) $(MANPAGE)
|
||||
|
||||
# The one place the version is written down, for everything that has to agree
|
||||
# with it.
|
||||
version:
|
||||
@echo $(VERSION)
|
||||
|
||||
po/%.mo: po/%.po
|
||||
ifdef MSGFMT
|
||||
$(MSGFMT) --check --output-file=$@ $<
|
||||
else
|
||||
@echo "msgfmt not found - skipping $@"
|
||||
endif
|
||||
|
||||
$(MANPAGE): doc/bottles-opener.1.scd
|
||||
ifdef SCDOC
|
||||
$(SCDOC) < $< > $@
|
||||
else
|
||||
@echo "scdoc not found - skipping $@"
|
||||
endif
|
||||
|
||||
# Regenerates the template from the sources. The setting labels live in an
|
||||
# array rather than in a call xgettext can see, so they are listed in
|
||||
# po/extra.pot and merged in.
|
||||
pot:
|
||||
xgettext --language=Shell --from-code=UTF-8 -k --keyword=bo_msg --keyword=bo_msg_into \
|
||||
--package-name=bottles-opener --package-version=$(VERSION) \
|
||||
--msgid-bugs-address=https://github.com/Felitendo/bottles-opener/issues \
|
||||
--add-comments=TRANSLATORS --no-location --sort-output \
|
||||
--output=po/bottles-opener.pot.tmp src/bottles-opener $(LIBS)
|
||||
msgcat --use-first --sort-output --output-file=po/bottles-opener.pot \
|
||||
po/bottles-opener.pot.tmp po/extra.pot
|
||||
rm -f po/bottles-opener.pot.tmp
|
||||
@for l in $(LINGUAS); do msgmerge --quiet --update --backup=none po/$$l.po po/bottles-opener.pot; done
|
||||
|
||||
# Syntax-check every shell file, and run shellcheck when it is available.
|
||||
check:
|
||||
@set -e; for f in src/bottles-opener $(LIBS); do \
|
||||
bash -n "$$f" && echo "ok $$f"; \
|
||||
done
|
||||
@for f in src/lib/*.py; do \
|
||||
python3 -c "import ast, sys; ast.parse(open(sys.argv[1]).read())" "$$f" && echo "ok $$f"; \
|
||||
done
|
||||
@if command -v shellcheck >/dev/null 2>&1; then \
|
||||
shellcheck -x -e SC1090,SC1091 src/bottles-opener $(LIBS); \
|
||||
echo "ok shellcheck"; \
|
||||
else \
|
||||
echo "shellcheck not found - skipped"; \
|
||||
fi
|
||||
@if command -v msgfmt >/dev/null 2>&1; then \
|
||||
for l in $(LINGUAS); do msgfmt --check --output-file=/dev/null po/$$l.po && echo "ok po/$$l.po"; done; \
|
||||
fi
|
||||
|
||||
install: build
|
||||
# executable
|
||||
install -Dm755 src/bottles-opener "$(DESTDIR)$(BINDIR)/bottles-opener"
|
||||
|
||||
# shell libraries and the scanner
|
||||
install -d "$(DESTDIR)$(LIBDIR)"
|
||||
install -Dm644 -t "$(DESTDIR)$(LIBDIR)" $(LIBS) src/lib/scan.py src/lib/icon.py
|
||||
|
||||
# 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)/bottles-opener" \
|
||||
"$(DESTDIR)$(LIBDIR)"/*.sh
|
||||
|
||||
# user units: the watcher, and the one-shot it starts
|
||||
install -Dm644 res/systemd/bottles-opener.service \
|
||||
"$(DESTDIR)$(USERUNITDIR)/bottles-opener.service"
|
||||
install -Dm644 res/systemd/bottles-opener.path \
|
||||
"$(DESTDIR)$(USERUNITDIR)/bottles-opener.path"
|
||||
sed -i -e 's|@BINDIR@|$(BINDIR)|g' \
|
||||
"$(DESTDIR)$(USERUNITDIR)/bottles-opener.service"
|
||||
|
||||
# translations
|
||||
@for l in $(LINGUAS); do \
|
||||
if [ -f "po/$$l.mo" ]; then \
|
||||
install -Dm644 "po/$$l.mo" \
|
||||
"$(DESTDIR)$(LOCALEDIR)/$$l/LC_MESSAGES/bottles-opener.mo"; \
|
||||
fi; \
|
||||
done
|
||||
|
||||
# documentation
|
||||
@if [ -f $(MANPAGE) ]; then \
|
||||
install -Dm644 $(MANPAGE) "$(DESTDIR)$(MANDIR)/man1/bottles-opener.1"; \
|
||||
fi
|
||||
install -Dm644 README.md \
|
||||
"$(DESTDIR)$(DATADIR)/doc/bottles-opener/README.md"
|
||||
|
||||
uninstall:
|
||||
rm -f "$(DESTDIR)$(BINDIR)/bottles-opener"
|
||||
rm -rf "$(DESTDIR)$(DATADIR)/bottles-opener"
|
||||
rm -f "$(DESTDIR)$(USERUNITDIR)/bottles-opener.service"
|
||||
rm -f "$(DESTDIR)$(USERUNITDIR)/bottles-opener.path"
|
||||
rm -f "$(DESTDIR)$(MANDIR)/man1/bottles-opener.1"
|
||||
rm -rf "$(DESTDIR)$(DATADIR)/doc/bottles-opener"
|
||||
@for l in $(LINGUAS); do \
|
||||
rm -f "$(DESTDIR)$(LOCALEDIR)/$$l/LC_MESSAGES/bottles-opener.mo"; \
|
||||
done
|
||||
|
||||
clean:
|
||||
rm -f po/*.mo $(MANPAGE)
|
||||
@@ -0,0 +1,107 @@
|
||||
# bottles-opener
|
||||
|
||||
bottles-opener is a CLI for linux that lets you double-click a file and have it open in its Windows program, inside its bottle.
|
||||
Works with FL Studio, Microsoft Office, Photoshop, Ableton and anything else you run through [Bottles](https://usebottles.com).
|
||||
|
||||
You install FL Studio in Bottles, double-click your `.flp` and... your file manager asks what an `.flp` is. This tool reads which files your Windows programs registered in their bottle - the same list Windows uses - teaches your system those types, gives them the program's icons and hands the file over the way Explorer would. One command and your files _✨just open✨_ (like on windows).
|
||||
|
||||
## How to use
|
||||
|
||||
Just run `bottles-opener`:
|
||||
|
||||
```
|
||||
Bottles Opener
|
||||
|
||||
File opening ON
|
||||
|
||||
Bottles package
|
||||
Programs found 5 in 2 bottles
|
||||
File types .doc .docx .flp .fsc .fst .pptx .xlsx +31
|
||||
New programs ON
|
||||
Last applied 2 minutes ago
|
||||
|
||||
Double-click one of these files and it opens in its program.
|
||||
|
||||
[1] Turn file opening on or off
|
||||
[2] Re-apply everything
|
||||
[3] File types
|
||||
[4] Programs
|
||||
[5] Settings
|
||||
[q] Quit
|
||||
|
||||
>
|
||||
```
|
||||
|
||||
Press `[1]` and you're done. `[3]` lists every file type and lets you pick what opens it:
|
||||
|
||||
```
|
||||
File types
|
||||
|
||||
▸ .docx Word · Office double-click
|
||||
.flp FL Studio · FL Studio double-click
|
||||
.psd Photoshop · Adobe Open With
|
||||
```
|
||||
|
||||
`double-click` opens it. `Open With` only lists it in the right-click menu - that's what types get that already have an app on your system (`.docx` with LibreOffice), so nothing you picked gets taken away. Space switches.
|
||||
|
||||
`[4]` lists your programs; Enter edits their file types, so you can add any extension you like. Or without the menu:
|
||||
|
||||
```bash
|
||||
bottles-opener add flp,fst "FL Studio"
|
||||
```
|
||||
|
||||
## How to install
|
||||
|
||||
**From source (for now)**
|
||||
|
||||
```bash
|
||||
make
|
||||
sudo make install
|
||||
bottles-opener enable
|
||||
```
|
||||
|
||||
No root? `make install PREFIX=~/.local` works too.
|
||||
|
||||
Needs `bash`, `python3` with PyYAML and icoextract (Bottles needs both itself) and `update-mime-database`.
|
||||
|
||||
## How it works
|
||||
|
||||
- **Which files:** read from the bottle's registry, like Windows does (`.flp` → `FL64.flp.26` → `FL64.exe "%1"`). Programs that registered nothing are matched against a built-in list: all of Office, FL Studio, Ableton, Cubase, Photoshop, Illustrator, Affinity, AutoCAD, SketchUp and more.
|
||||
- **Unknown extensions** (`.flp`, `.msg`, `.als`, ...) get a MIME type in `~/.local/share/mime/packages/bottles-opener.xml`, named like in Windows ("FL Studio project file"). Where Linux means something else by an extension (`.mpp` is Musepack, `.fst` a tracker module) the file's signature tells them apart.
|
||||
- **Icons:** the file gets the icon Windows shows, pulled out of the program. A type your system already knows keeps its icon until the program is its default.
|
||||
- **Opening:** the path becomes the Windows path (`Z:\home\you\song.flp`), with the switches from the registry (Outlook `/f`, PowerPoint shows `/s`), started through `bottles-cli` with all your bottle's settings.
|
||||
- **Flatpak Bottles:** the file goes through the document portal first, like `flatpak run --file-forwarding`.
|
||||
|
||||
Everything written is recorded, so `bottles-opener disable` puts back exactly what was there. New programs are picked up by a systemd path unit; `bottles-opener apply` does the same by hand.
|
||||
|
||||
## Why not just Bottles?
|
||||
|
||||
Bottles 67 has "File Associations" per program, but it rejects extensions your system doesn't know (so `.flp`, `.msg`, `.als` are "invalid"), passes Linux paths, doesn't know which program opens what and doesn't touch icons.
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | |
|
||||
|---|---|
|
||||
| `bottles-opener` | Interactive menu |
|
||||
| `… enable` | Turn on, apply, start watching |
|
||||
| `… disable` | Undo everything |
|
||||
| `… apply` | Pick up new programs |
|
||||
| `… apply --rebuild` | Redo from scratch |
|
||||
| `… status` | What's set up |
|
||||
| `… list` | All file types and what opens them |
|
||||
| `… programs` | All programs and their file types |
|
||||
| `… add <ext,...> <program> [bottle]` | Open file types with a program |
|
||||
| `… remove <ext,...> [program]` | Stop opening file types |
|
||||
|
||||
## Building from source
|
||||
|
||||
```bash
|
||||
make
|
||||
sudo make install
|
||||
```
|
||||
|
||||
Optionally needs `msgfmt` (gettext) and `scdoc`. Supports `PREFIX` and `DESTDIR`. `make check` runs syntax checks and shellcheck.
|
||||
|
||||
## License
|
||||
|
||||
GPL-3.0-or-later.
|
||||
@@ -0,0 +1,128 @@
|
||||
bottles-opener(1)
|
||||
|
||||
# NAME
|
||||
|
||||
bottles-opener - double-click a file and it opens in its Windows program, inside its bottle
|
||||
|
||||
# SYNOPSIS
|
||||
|
||||
*bottles-opener* [_command_]
|
||||
|
||||
# DESCRIPTION
|
||||
|
||||
Sets up file opening for programs installed with Bottles: which files each
|
||||
program opens, the MIME types the system is missing for them, their icons, and
|
||||
a launcher that hands a file over as the Windows path the program expects.
|
||||
|
||||
Without a command it shows an interactive menu. Nothing outside the user's home
|
||||
is written to; running as root is refused.
|
||||
|
||||
# COMMANDS
|
||||
|
||||
*enable*
|
||||
Turn on, set up every file type, start watching for new programs.
|
||||
|
||||
*disable*
|
||||
Turn off and put back everything that was changed, including the
|
||||
previous default application of each type.
|
||||
|
||||
*apply* [*--rebuild*]
|
||||
Set up what appeared since the last run. *--rebuild* takes everything
|
||||
back first and writes it again.
|
||||
|
||||
*status*
|
||||
What is set up.
|
||||
|
||||
*list*
|
||||
Every file type and what opens it.
|
||||
|
||||
*programs*
|
||||
Every program and the file types it opens.
|
||||
|
||||
*add* _ext_[,_ext_...] _program_ [_bottle_] [*--open-with*|*--default*]
|
||||
Open file types with a program, named as Bottles lists it or by id.
|
||||
|
||||
*remove* _ext_[,_ext_...] [_program_ [_bottle_]]
|
||||
Stop opening file types. A detected type is switched off rather than
|
||||
forgotten, so it does not come back.
|
||||
|
||||
*-h*, *--help*, *-V*, *--version*
|
||||
Help and version.
|
||||
|
||||
# FILE TYPES
|
||||
|
||||
Found in three places, later ones winning:
|
||||
|
||||
. The bottle's registry, as Windows reads it: extension, ProgID (following
|
||||
*CurVer*), the default verb's command, *DefaultIcon* and *FriendlyTypeName*.
|
||||
. A built-in list for programs that registered nothing: Microsoft Office,
|
||||
FL Studio, Ableton Live, Cubase, Photoshop, AutoCAD and others.
|
||||
. What was set by hand, in the menu or with *add* and *remove*.
|
||||
|
||||
Each type is *double-click* (the default application), *Open With* (only
|
||||
offered there) or *off*. A detected type the system does not know becomes the
|
||||
default; one it knows only gets *Open With*, since another application has it.
|
||||
If the default is changed in the desktop's settings later, that choice stays.
|
||||
|
||||
An extension the system does not know gets a MIME type in
|
||||
_~/.local/share/mime/packages/bottles-opener.xml_, described as in Windows.
|
||||
Where the system uses an extension for something else (*.mpp*, *.fst*, *.gp*,
|
||||
*.gpx*), the type is defined anyway, with the file's signature to tell the two
|
||||
apart.
|
||||
|
||||
# ICONS
|
||||
|
||||
A file gets the icon Windows shows for it: *DefaultIcon* extracted from the
|
||||
program with icoextract, or the icon Bottles saved for the program. It is
|
||||
installed into the hicolor theme under a name of this program's own.
|
||||
|
||||
A type the system knows keeps its icon until the program is its default. It is
|
||||
then copied whole into the package with the new icon, because Qt and GLib read
|
||||
a type's description from the first directory that defines it.
|
||||
|
||||
# OPENING
|
||||
|
||||
The launcher, _~/.local/share/applications/bottles-opener-_id_.desktop_, is
|
||||
hidden from menus and runs *bottles-opener open*. The file's path is mapped
|
||||
through the bottle's drive letters (*C:* inside the bottle, *Z:* for the rest)
|
||||
and passed with the registry's switches for its extension, e.g. Outlook's
|
||||
*/f* or PowerPoint's */s*. A command Windows completes over DDE gets only the
|
||||
file. The program is started with *bottles-cli run*.
|
||||
|
||||
With the Bottles Flatpak a file outside the bottle is exported through the
|
||||
document portal first. The program can write that file, but nothing next to it.
|
||||
|
||||
# NEW PROGRAMS
|
||||
|
||||
A systemd user path unit watches the bottles directories, each _bottle.yml_
|
||||
and the system's MIME packages. Without a systemd user session, run *apply*.
|
||||
|
||||
# FILES
|
||||
|
||||
_~/.config/bottles-opener/config_, _~/.config/bottles-opener/types_
|
||||
Settings and types changed by hand.
|
||||
|
||||
_~/.local/state/bottles-opener/_
|
||||
*ledger* (every change, for *disable*), *launchers* and *commands* (read
|
||||
by *open*).
|
||||
|
||||
_~/.cache/bottles-opener/_
|
||||
Parsed registries and extracted icons. Safe to delete.
|
||||
|
||||
# ENVIRONMENT
|
||||
|
||||
*NO_COLOR*
|
||||
Disables colour.
|
||||
|
||||
# REQUIREMENTS
|
||||
|
||||
Bash 4.2+, coreutils, Python 3 with PyYAML and icoextract (both Bottles
|
||||
dependencies), *update-mime-database*. Optional: *notify-send*.
|
||||
|
||||
# SEE ALSO
|
||||
|
||||
*bottles-cli*(1), *update-mime-database*(1), *flatpak-document-export*(1)
|
||||
|
||||
# AUTHORS
|
||||
|
||||
Felitendo. https://github.com/Felitendo/bottles-opener
|
||||
@@ -0,0 +1,314 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the bottles-opener package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: bottles-opener 0.1.0\n"
|
||||
"Report-Msgid-Bugs-To: https://github.com/Felitendo/bottles-opener/issues\n"
|
||||
"POT-Creation-Date: 2026-09-21 11:30+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"
|
||||
|
||||
#, sh-printf-format
|
||||
msgid "\"%s\" is not a file extension."
|
||||
msgstr ""
|
||||
|
||||
msgid "\"double-click\" opens it; \"Open With\" only lists the program there."
|
||||
msgstr ""
|
||||
|
||||
msgid "%d days ago"
|
||||
msgstr ""
|
||||
|
||||
msgid "%d hours ago"
|
||||
msgstr ""
|
||||
|
||||
#, sh-printf-format
|
||||
msgid "%d in %d bottles"
|
||||
msgstr ""
|
||||
|
||||
#, sh-printf-format
|
||||
msgid "%d in 1 bottle"
|
||||
msgstr ""
|
||||
|
||||
msgid "%d minutes ago"
|
||||
msgstr ""
|
||||
|
||||
msgid "1 day ago"
|
||||
msgstr ""
|
||||
|
||||
msgid "1 hour ago"
|
||||
msgstr ""
|
||||
|
||||
msgid "1 minute ago"
|
||||
msgstr ""
|
||||
|
||||
#, sh-printf-format
|
||||
msgid "Already up to date for %s."
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Bottles could not be read - is python3 with PyYAML installed? Nothing was "
|
||||
"changed."
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Bottles is not installed - neither the package nor the Flatpak was found."
|
||||
msgstr ""
|
||||
|
||||
msgid "Commands:"
|
||||
msgstr ""
|
||||
|
||||
#, sh-printf-format
|
||||
msgid "Could not hand %s to the Bottles Flatpak."
|
||||
msgstr ""
|
||||
|
||||
msgid "Could not save the setting."
|
||||
msgstr ""
|
||||
|
||||
msgid "Could not start the watcher for newly installed programs."
|
||||
msgstr ""
|
||||
|
||||
msgid "Double-click one of these files and it opens in its program."
|
||||
msgstr ""
|
||||
|
||||
msgid "File opening"
|
||||
msgstr ""
|
||||
|
||||
msgid "File opening is off - this is what it would do."
|
||||
msgstr ""
|
||||
|
||||
msgid "File opening is off and everything that was changed has been put back."
|
||||
msgstr ""
|
||||
|
||||
msgid "File opening is off. Turn it on first."
|
||||
msgstr ""
|
||||
|
||||
#, sh-printf-format
|
||||
msgid "File opening is on for %s."
|
||||
msgstr ""
|
||||
|
||||
msgid "File types"
|
||||
msgstr ""
|
||||
|
||||
#, sh-printf-format
|
||||
msgid "File types for %s, separated by spaces:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Find the file types of installed programs"
|
||||
msgstr ""
|
||||
|
||||
msgid "Give files the icon of their program"
|
||||
msgstr ""
|
||||
|
||||
msgid "Last applied"
|
||||
msgstr ""
|
||||
|
||||
msgid "List every file type and what opens it"
|
||||
msgstr ""
|
||||
|
||||
msgid "List every program and the file types it opens"
|
||||
msgstr ""
|
||||
|
||||
msgid "Looking at the bottles..."
|
||||
msgstr ""
|
||||
|
||||
#, sh-printf-format
|
||||
msgid "More than one program is called \"%s\" - name the bottle as well."
|
||||
msgstr ""
|
||||
|
||||
msgid "New programs"
|
||||
msgstr ""
|
||||
|
||||
#, sh-printf-format
|
||||
msgid "No drive in the bottle \"%s\" reaches %s."
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"No file types are set up yet. Add one with \"bottles-opener add\" or under "
|
||||
"File types in the menu."
|
||||
msgstr ""
|
||||
|
||||
msgid "No file types yet."
|
||||
msgstr ""
|
||||
|
||||
msgid "No file types yet. Press a to add one."
|
||||
msgstr ""
|
||||
|
||||
#, sh-printf-format
|
||||
msgid ""
|
||||
"No program called \"%s\" was found. \"bottles-opener programs\" lists them "
|
||||
"all."
|
||||
msgstr ""
|
||||
|
||||
msgid "No programs found in any bottle."
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"No systemd user session - newly installed programs will not be picked up on "
|
||||
"their own."
|
||||
msgstr ""
|
||||
|
||||
#, sh-printf-format
|
||||
msgid "Not overwriting %s - it was not written by this program."
|
||||
msgstr ""
|
||||
|
||||
#, sh-printf-format
|
||||
msgid "Nothing opens .%s."
|
||||
msgstr ""
|
||||
|
||||
msgid "OFF"
|
||||
msgstr ""
|
||||
|
||||
msgid "ON"
|
||||
msgstr ""
|
||||
|
||||
msgid "Open With"
|
||||
msgstr ""
|
||||
|
||||
msgid "Open a file type with a program"
|
||||
msgstr ""
|
||||
|
||||
#, sh-printf-format
|
||||
msgid "Opens files in %s from the bottle \"%s\""
|
||||
msgstr ""
|
||||
|
||||
msgid "Pick up newly installed programs"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pick up programs installed since the last run"
|
||||
msgstr ""
|
||||
|
||||
msgid "Press any key to continue..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Programs"
|
||||
msgstr ""
|
||||
|
||||
msgid "Programs found"
|
||||
msgstr ""
|
||||
|
||||
msgid "Quit"
|
||||
msgstr ""
|
||||
|
||||
msgid "Re-apply everything"
|
||||
msgstr ""
|
||||
|
||||
msgid "Saved."
|
||||
msgstr ""
|
||||
|
||||
msgid "Saved. Turn file opening on with \"bottles-opener enable\"."
|
||||
msgstr ""
|
||||
|
||||
msgid "Settings"
|
||||
msgstr ""
|
||||
|
||||
msgid "Show the version"
|
||||
msgstr ""
|
||||
|
||||
msgid "Show this help"
|
||||
msgstr ""
|
||||
|
||||
msgid "Show what is set up"
|
||||
msgstr ""
|
||||
|
||||
msgid "Stop opening a file type"
|
||||
msgstr ""
|
||||
|
||||
msgid "Take everything back and write it again"
|
||||
msgstr ""
|
||||
|
||||
#, sh-printf-format
|
||||
msgid "The file %s does not exist."
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"This launcher is out of date. Run \"bottles-opener apply\" and try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "Turn file opening on"
|
||||
msgstr ""
|
||||
|
||||
msgid "Turn file opening on or off"
|
||||
msgstr ""
|
||||
|
||||
msgid "Turn it off and put everything back"
|
||||
msgstr ""
|
||||
|
||||
#, sh-printf-format
|
||||
msgid "Unknown command: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Up/Down select - Enter edits its file types - q goes back"
|
||||
msgstr ""
|
||||
|
||||
msgid "Up/Down select - Enter picks - q cancels"
|
||||
msgstr ""
|
||||
|
||||
msgid "Up/Down select - Space changes - q goes back"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Up/Down select - Space switches - a adds one - Del removes - q goes back"
|
||||
msgstr ""
|
||||
|
||||
msgid "Usage: bottles-opener [command]"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Usage: bottles-opener add <extension> <program> [bottle] [--open-with|--"
|
||||
"default]"
|
||||
msgstr ""
|
||||
|
||||
msgid "Usage: bottles-opener open <launcher> [file...]"
|
||||
msgstr ""
|
||||
|
||||
msgid "Usage: bottles-opener remove <extension> [program] [bottle]"
|
||||
msgstr ""
|
||||
|
||||
msgid "Which file extensions? For example: flp fst"
|
||||
msgstr ""
|
||||
|
||||
#, sh-printf-format
|
||||
msgid "Which program opens %s?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Without a command an interactive menu is shown."
|
||||
msgstr ""
|
||||
|
||||
msgid "added"
|
||||
msgstr ""
|
||||
|
||||
msgid "could not be read - is python3 with PyYAML installed?"
|
||||
msgstr ""
|
||||
|
||||
msgid "double-click"
|
||||
msgstr ""
|
||||
|
||||
msgid "just now"
|
||||
msgstr ""
|
||||
|
||||
msgid "never"
|
||||
msgstr ""
|
||||
|
||||
msgid "none yet - add one under File types"
|
||||
msgstr ""
|
||||
|
||||
msgid "not found"
|
||||
msgstr ""
|
||||
|
||||
msgid "off"
|
||||
msgstr ""
|
||||
|
||||
msgid "package"
|
||||
msgstr ""
|
||||
|
||||
msgid "program not found"
|
||||
msgstr ""
|
||||
@@ -0,0 +1,333 @@
|
||||
# German translation for bottles-opener.
|
||||
# Copyright (C) 2026 Felitendo
|
||||
# This file is distributed under the same license as bottles-opener.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: bottles-opener 0.1.0\n"
|
||||
"Report-Msgid-Bugs-To: https://github.com/Felitendo/bottles-opener/issues\n"
|
||||
"POT-Creation-Date: 2026-09-21 11:30+0200\n"
|
||||
"PO-Revision-Date: 2026-09-21 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"
|
||||
|
||||
#, sh-printf-format
|
||||
msgid "\"%s\" is not a file extension."
|
||||
msgstr "„%s“ ist keine Dateiendung."
|
||||
|
||||
msgid "\"double-click\" opens it; \"Open With\" only lists the program there."
|
||||
msgstr ""
|
||||
"„Doppelklick“ öffnet die Datei; „Öffnen mit“ listet das Programm nur dort "
|
||||
"auf."
|
||||
|
||||
msgid "%d days ago"
|
||||
msgstr "vor %d Tagen"
|
||||
|
||||
msgid "%d hours ago"
|
||||
msgstr "vor %d Stunden"
|
||||
|
||||
#, sh-printf-format
|
||||
msgid "%d in %d bottles"
|
||||
msgstr "%d in %d Bottles"
|
||||
|
||||
#, sh-printf-format
|
||||
msgid "%d in 1 bottle"
|
||||
msgstr "%d in 1 Bottle"
|
||||
|
||||
msgid "%d minutes ago"
|
||||
msgstr "vor %d Minuten"
|
||||
|
||||
msgid "1 day ago"
|
||||
msgstr "vor 1 Tag"
|
||||
|
||||
msgid "1 hour ago"
|
||||
msgstr "vor 1 Stunde"
|
||||
|
||||
msgid "1 minute ago"
|
||||
msgstr "vor 1 Minute"
|
||||
|
||||
#, sh-printf-format
|
||||
msgid "Already up to date for %s."
|
||||
msgstr "Für %s ist schon alles eingerichtet."
|
||||
|
||||
msgid ""
|
||||
"Bottles could not be read - is python3 with PyYAML installed? Nothing was "
|
||||
"changed."
|
||||
msgstr ""
|
||||
"Bottles konnte nicht gelesen werden - ist python3 mit PyYAML installiert? Es "
|
||||
"wurde nichts geändert."
|
||||
|
||||
msgid ""
|
||||
"Bottles is not installed - neither the package nor the Flatpak was found."
|
||||
msgstr ""
|
||||
"Bottles ist nicht installiert - weder das Paket noch das Flatpak wurde "
|
||||
"gefunden."
|
||||
|
||||
msgid "Commands:"
|
||||
msgstr "Befehle:"
|
||||
|
||||
#, sh-printf-format
|
||||
msgid "Could not hand %s to the Bottles Flatpak."
|
||||
msgstr "%s konnte nicht an das Bottles-Flatpak übergeben werden."
|
||||
|
||||
msgid "Could not save the setting."
|
||||
msgstr "Die Einstellung konnte nicht gespeichert werden."
|
||||
|
||||
msgid "Could not start the watcher for newly installed programs."
|
||||
msgstr ""
|
||||
"Die Überwachung für neu installierte Programme konnte nicht gestartet werden."
|
||||
|
||||
msgid "Double-click one of these files and it opens in its program."
|
||||
msgstr "Ein Doppelklick auf eine dieser Dateien öffnet sie in ihrem Programm."
|
||||
|
||||
msgid "File opening"
|
||||
msgstr "Dateien öffnen"
|
||||
|
||||
msgid "File opening is off - this is what it would do."
|
||||
msgstr "Dateien öffnen ist aus - so würde es aussehen."
|
||||
|
||||
msgid "File opening is off and everything that was changed has been put back."
|
||||
msgstr ""
|
||||
"Dateien öffnen ist aus und alles, was geändert wurde, ist wiederhergestellt."
|
||||
|
||||
msgid "File opening is off. Turn it on first."
|
||||
msgstr "Dateien öffnen ist aus. Schalte es zuerst ein."
|
||||
|
||||
#, sh-printf-format
|
||||
msgid "File opening is on for %s."
|
||||
msgstr "Dateien öffnen ist an für %s."
|
||||
|
||||
msgid "File types"
|
||||
msgstr "Dateitypen"
|
||||
|
||||
#, sh-printf-format
|
||||
msgid "File types for %s, separated by spaces:"
|
||||
msgstr "Dateitypen für %s, durch Leerzeichen getrennt:"
|
||||
|
||||
msgid "Find the file types of installed programs"
|
||||
msgstr "Dateitypen installierter Programme erkennen"
|
||||
|
||||
msgid "Give files the icon of their program"
|
||||
msgstr "Dateien das Symbol ihres Programms geben"
|
||||
|
||||
msgid "Last applied"
|
||||
msgstr "Zuletzt angewendet"
|
||||
|
||||
msgid "List every file type and what opens it"
|
||||
msgstr "Alle Dateitypen auflisten und womit sie geöffnet werden"
|
||||
|
||||
msgid "List every program and the file types it opens"
|
||||
msgstr "Alle Programme und ihre Dateitypen auflisten"
|
||||
|
||||
msgid "Looking at the bottles..."
|
||||
msgstr "Bottles werden durchsucht..."
|
||||
|
||||
#, sh-printf-format
|
||||
msgid "More than one program is called \"%s\" - name the bottle as well."
|
||||
msgstr "Mehrere Programme heißen „%s“ - gib auch die Bottle an."
|
||||
|
||||
msgid "New programs"
|
||||
msgstr "Neue Programme"
|
||||
|
||||
#, sh-printf-format
|
||||
msgid "No drive in the bottle \"%s\" reaches %s."
|
||||
msgstr "Kein Laufwerk in der Bottle „%s“ erreicht %s."
|
||||
|
||||
msgid ""
|
||||
"No file types are set up yet. Add one with \"bottles-opener add\" or under "
|
||||
"File types in the menu."
|
||||
msgstr ""
|
||||
"Noch keine Dateitypen eingerichtet. Füge einen mit „bottles-opener add“ oder "
|
||||
"im Menü unter Dateitypen hinzu."
|
||||
|
||||
msgid "No file types yet."
|
||||
msgstr "Noch keine Dateitypen."
|
||||
|
||||
msgid "No file types yet. Press a to add one."
|
||||
msgstr "Noch keine Dateitypen. Drücke a, um einen hinzuzufügen."
|
||||
|
||||
#, sh-printf-format
|
||||
msgid ""
|
||||
"No program called \"%s\" was found. \"bottles-opener programs\" lists them "
|
||||
"all."
|
||||
msgstr ""
|
||||
"Kein Programm namens „%s“ gefunden. „bottles-opener programs“ listet alle "
|
||||
"auf."
|
||||
|
||||
msgid "No programs found in any bottle."
|
||||
msgstr "In keiner Bottle wurden Programme gefunden."
|
||||
|
||||
msgid ""
|
||||
"No systemd user session - newly installed programs will not be picked up on "
|
||||
"their own."
|
||||
msgstr ""
|
||||
"Keine systemd-Benutzersitzung - neu installierte Programme werden nicht von "
|
||||
"selbst erkannt."
|
||||
|
||||
#, sh-printf-format
|
||||
msgid "Not overwriting %s - it was not written by this program."
|
||||
msgstr ""
|
||||
"%s wird nicht überschrieben - die Datei stammt nicht von diesem Programm."
|
||||
|
||||
#, sh-printf-format
|
||||
msgid "Nothing opens .%s."
|
||||
msgstr "Nichts öffnet .%s."
|
||||
|
||||
msgid "OFF"
|
||||
msgstr "AUS"
|
||||
|
||||
msgid "ON"
|
||||
msgstr "AN"
|
||||
|
||||
msgid "Open With"
|
||||
msgstr "Öffnen mit"
|
||||
|
||||
msgid "Open a file type with a program"
|
||||
msgstr "Einen Dateityp mit einem Programm öffnen"
|
||||
|
||||
#, sh-printf-format
|
||||
msgid "Opens files in %s from the bottle \"%s\""
|
||||
msgstr "Öffnet Dateien in %s aus der Bottle „%s“"
|
||||
|
||||
msgid "Pick up newly installed programs"
|
||||
msgstr "Neu installierte Programme erkennen"
|
||||
|
||||
msgid "Pick up programs installed since the last run"
|
||||
msgstr "Seit dem letzten Lauf installierte Programme übernehmen"
|
||||
|
||||
msgid "Press any key to continue..."
|
||||
msgstr "Beliebige Taste drücken, um fortzufahren..."
|
||||
|
||||
msgid "Programs"
|
||||
msgstr "Programme"
|
||||
|
||||
msgid "Programs found"
|
||||
msgstr "Gefundene Programme"
|
||||
|
||||
msgid "Quit"
|
||||
msgstr "Beenden"
|
||||
|
||||
msgid "Re-apply everything"
|
||||
msgstr "Alles neu anwenden"
|
||||
|
||||
msgid "Saved."
|
||||
msgstr "Gespeichert."
|
||||
|
||||
msgid "Saved. Turn file opening on with \"bottles-opener enable\"."
|
||||
msgstr "Gespeichert. Schalte Dateien öffnen mit „bottles-opener enable“ ein."
|
||||
|
||||
msgid "Settings"
|
||||
msgstr "Einstellungen"
|
||||
|
||||
msgid "Show the version"
|
||||
msgstr "Version anzeigen"
|
||||
|
||||
msgid "Show this help"
|
||||
msgstr "Diese Hilfe anzeigen"
|
||||
|
||||
msgid "Show what is set up"
|
||||
msgstr "Anzeigen, was eingerichtet ist"
|
||||
|
||||
msgid "Stop opening a file type"
|
||||
msgstr "Einen Dateityp nicht mehr öffnen"
|
||||
|
||||
msgid "Take everything back and write it again"
|
||||
msgstr "Alles zurücknehmen und neu schreiben"
|
||||
|
||||
#, sh-printf-format
|
||||
msgid "The file %s does not exist."
|
||||
msgstr "Die Datei %s existiert nicht."
|
||||
|
||||
msgid ""
|
||||
"This launcher is out of date. Run \"bottles-opener apply\" and try again."
|
||||
msgstr ""
|
||||
"Dieser Starter ist veraltet. Führe „bottles-opener apply“ aus und versuche "
|
||||
"es noch einmal."
|
||||
|
||||
msgid "Turn file opening on"
|
||||
msgstr "Dateien öffnen einschalten"
|
||||
|
||||
msgid "Turn file opening on or off"
|
||||
msgstr "Dateien öffnen ein- oder ausschalten"
|
||||
|
||||
msgid "Turn it off and put everything back"
|
||||
msgstr "Ausschalten und alles wiederherstellen"
|
||||
|
||||
#, sh-printf-format
|
||||
msgid "Unknown command: %s"
|
||||
msgstr "Unbekannter Befehl: %s"
|
||||
|
||||
msgid "Up/Down select - Enter edits its file types - q goes back"
|
||||
msgstr "Hoch/Runter wählt - Enter bearbeitet die Dateitypen - q geht zurück"
|
||||
|
||||
msgid "Up/Down select - Enter picks - q cancels"
|
||||
msgstr "Hoch/Runter wählt - Enter übernimmt - q bricht ab"
|
||||
|
||||
msgid "Up/Down select - Space changes - q goes back"
|
||||
msgstr "Hoch/Runter wählt - Leertaste ändert - q geht zurück"
|
||||
|
||||
msgid ""
|
||||
"Up/Down select - Space switches - a adds one - Del removes - q goes back"
|
||||
msgstr ""
|
||||
"Hoch/Runter wählt - Leertaste schaltet um - a fügt hinzu - Entf entfernt - q "
|
||||
"geht zurück"
|
||||
|
||||
msgid "Usage: bottles-opener [command]"
|
||||
msgstr "Aufruf: bottles-opener [Befehl]"
|
||||
|
||||
msgid ""
|
||||
"Usage: bottles-opener add <extension> <program> [bottle] [--open-with|--"
|
||||
"default]"
|
||||
msgstr ""
|
||||
"Aufruf: bottles-opener add <Endung> <Programm> [Bottle] [--open-with|--"
|
||||
"default]"
|
||||
|
||||
msgid "Usage: bottles-opener open <launcher> [file...]"
|
||||
msgstr "Aufruf: bottles-opener open <Starter> [Datei...]"
|
||||
|
||||
msgid "Usage: bottles-opener remove <extension> [program] [bottle]"
|
||||
msgstr "Aufruf: bottles-opener remove <Endung> [Programm] [Bottle]"
|
||||
|
||||
msgid "Which file extensions? For example: flp fst"
|
||||
msgstr "Welche Dateiendungen? Zum Beispiel: flp fst"
|
||||
|
||||
#, sh-printf-format
|
||||
msgid "Which program opens %s?"
|
||||
msgstr "Welches Programm öffnet %s?"
|
||||
|
||||
msgid "Without a command an interactive menu is shown."
|
||||
msgstr "Ohne Befehl wird ein interaktives Menü angezeigt."
|
||||
|
||||
msgid "added"
|
||||
msgstr "hinzugefügt"
|
||||
|
||||
msgid "could not be read - is python3 with PyYAML installed?"
|
||||
msgstr "konnte nicht gelesen werden - ist python3 mit PyYAML installiert?"
|
||||
|
||||
msgid "double-click"
|
||||
msgstr "Doppelklick"
|
||||
|
||||
msgid "just now"
|
||||
msgstr "gerade eben"
|
||||
|
||||
msgid "never"
|
||||
msgstr "nie"
|
||||
|
||||
msgid "none yet - add one under File types"
|
||||
msgstr "noch keine - unter Dateitypen hinzufügen"
|
||||
|
||||
msgid "not found"
|
||||
msgstr "nicht gefunden"
|
||||
|
||||
msgid "off"
|
||||
msgstr "aus"
|
||||
|
||||
msgid "package"
|
||||
msgstr "Paket"
|
||||
|
||||
msgid "program not found"
|
||||
msgstr "Programm nicht gefunden"
|
||||
@@ -0,0 +1,39 @@
|
||||
# Messages xgettext cannot find on its own: the setting labels are kept in an
|
||||
# array (src/lib/menu.sh, BO_SETTINGS) and looked up by value, and the parser
|
||||
# loses track inside the (( )) conditions of bo_time_ago (src/lib/common.sh).
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
|
||||
msgid "Find the file types of installed programs"
|
||||
msgstr ""
|
||||
|
||||
msgid "Give files the icon of their program"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pick up newly installed programs"
|
||||
msgstr ""
|
||||
|
||||
msgid "never"
|
||||
msgstr ""
|
||||
|
||||
msgid "just now"
|
||||
msgstr ""
|
||||
|
||||
msgid "1 minute ago"
|
||||
msgstr ""
|
||||
|
||||
msgid "%d minutes ago"
|
||||
msgstr ""
|
||||
|
||||
msgid "1 hour ago"
|
||||
msgstr ""
|
||||
|
||||
msgid "%d hours ago"
|
||||
msgstr ""
|
||||
|
||||
msgid "1 day ago"
|
||||
msgstr ""
|
||||
|
||||
msgid "%d days ago"
|
||||
msgstr ""
|
||||
@@ -0,0 +1,26 @@
|
||||
[Unit]
|
||||
Description=Watch Bottles for newly installed programs
|
||||
Documentation=man:bottles-opener(1)
|
||||
|
||||
[Path]
|
||||
# A bottle that is created or deleted. Where the bottles are depends on how
|
||||
# Bottles was installed: a distribution package keeps them in the user's data
|
||||
# directory, the Flatpak in its own. A directory that does not exist on this
|
||||
# system costs nothing - systemd watches the nearest parent that does and picks
|
||||
# the path up when it appears.
|
||||
PathModified=%h/.local/share/bottles/bottles
|
||||
PathModified=%h/.var/app/com.usebottles.bottles/data/bottles/bottles
|
||||
|
||||
# A program installed into a bottle that already exists turns up in that
|
||||
# bottle's bottle.yml, and Bottles can be told to keep its bottles somewhere
|
||||
# else entirely. Neither can be written down here: `enable` lists them in a
|
||||
# drop-in, and every run of the service keeps that list current.
|
||||
|
||||
# A type the system defines is copied whole to change its icon; a
|
||||
# shared-mime-info update refreshes the copy.
|
||||
PathModified=/usr/share/mime/packages
|
||||
|
||||
Unit=bottles-opener.service
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -0,0 +1,12 @@
|
||||
[Unit]
|
||||
Description=Set up file opening for programs installed with Bottles
|
||||
Documentation=man:bottles-opener(1)
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=@BINDIR@/bottles-opener apply --quiet
|
||||
# Reading every bottle.yml is quick; one that takes longer than this has found
|
||||
# something pathological and should not be left running.
|
||||
TimeoutStartSec=120
|
||||
Nice=10
|
||||
IOSchedulingClass=idle
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
# Runs the checkout against a throwaway XDG tree. Reads the real bottles, writes nowhere else.
|
||||
set -euo pipefail
|
||||
|
||||
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
box="${BO_SANDBOX:-${TMPDIR:-/tmp}/bottles-opener-sandbox}"
|
||||
|
||||
# The real bottles, resolved before the XDG dirs are pointed away.
|
||||
export BO_NATIVE_DATA="${XDG_DATA_HOME:-$HOME/.local/share}"
|
||||
export BO_FLATPAK_DATA="$HOME/.var/app/com.usebottles.bottles/data"
|
||||
|
||||
export XDG_CONFIG_HOME="$box/config" XDG_DATA_HOME="$box/data"
|
||||
export XDG_STATE_HOME="$box/state" XDG_CACHE_HOME="$box/cache"
|
||||
export BO_LIBDIR="$root/src/lib" BO_NO_WATCH=1
|
||||
|
||||
# Translations, when built.
|
||||
if [[ -f $root/po/de.mo ]]; then
|
||||
mkdir -p "$box/locale/de/LC_MESSAGES"
|
||||
cp -f "$root/po/de.mo" "$box/locale/de/LC_MESSAGES/bottles-opener.mo"
|
||||
export BO_LOCALEDIR="$box/locale"
|
||||
fi
|
||||
|
||||
mkdir -p "$XDG_CONFIG_HOME" "$XDG_DATA_HOME" "$XDG_STATE_HOME" "$XDG_CACHE_HOME"
|
||||
exec "$root/src/bottles-opener" "$@"
|
||||
Executable
+372
@@ -0,0 +1,372 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# bottles-opener - double-click a file and it opens in its Windows program,
|
||||
# inside its bottle
|
||||
#
|
||||
# Copyright (C) 2026 Felitendo
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
BO_LIBDIR="${BO_LIBDIR:-@LIBDIR@}"
|
||||
|
||||
# What the launchers run; a desktop session's PATH may differ.
|
||||
BO_SELF="$(readlink -f -- "${BASH_SOURCE[0]}")"
|
||||
|
||||
for _mod in common config scan known mime assoc apply open menu; do
|
||||
# shellcheck source=/dev/null
|
||||
if ! source "$BO_LIBDIR/$_mod.sh"; then
|
||||
printf 'bottles-opener: cannot load %s/%s.sh\n' "$BO_LIBDIR" "$_mod" >&2
|
||||
exit 14
|
||||
fi
|
||||
done
|
||||
unset _mod
|
||||
|
||||
# As root this would set up root's file types, not the user's.
|
||||
if [[ $EUID -eq 0 && -z ${BO_ALLOW_ROOT:-} ]]; then
|
||||
printf 'bottles-opener: run this as your own user, not as root.\n' >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Commands
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_bo_need_bottles() {
|
||||
bo_installs_detect
|
||||
(( ${#BO_INSTALLS[@]} )) && return 0
|
||||
bo_bad "$(bo_msg "Bottles is not installed - neither the package nor the Flatpak was found.")"
|
||||
return 1
|
||||
}
|
||||
|
||||
_bo_report() {
|
||||
if (( BO_N_ACTIVE == 0 )); then
|
||||
bo_note "$(bo_msg "No file types are set up yet. Add one with \"bottles-opener add\" or under File types in the menu.")"
|
||||
else
|
||||
bo_ok "$(bo_msg "File opening is on for %s." "$(bo_assoc_active_exts 12)")"
|
||||
fi
|
||||
}
|
||||
|
||||
bo_do_enable() {
|
||||
_bo_need_bottles || return 1
|
||||
|
||||
bo_config_set Enabled yes || {
|
||||
bo_bad "$(bo_msg "Could not save the setting.")"
|
||||
return 1
|
||||
}
|
||||
bo_config_load
|
||||
|
||||
bo_apply || return 1
|
||||
_bo_report
|
||||
|
||||
if [[ $CFG_WATCH == yes ]]; then
|
||||
if bo_watch_available; then
|
||||
bo_watch_enable || bo_note "$(bo_msg "Could not start the watcher for newly installed programs.")"
|
||||
else
|
||||
bo_note "$(bo_msg "No systemd user session - newly installed programs will not be picked up on their own.")"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
bo_do_disable() {
|
||||
bo_config_set Enabled no || {
|
||||
bo_bad "$(bo_msg "Could not save the setting.")"
|
||||
return 1
|
||||
}
|
||||
|
||||
bo_revert
|
||||
bo_watch_disable
|
||||
|
||||
bo_ok "$(bo_msg "File opening is off and everything that was changed has been put back.")"
|
||||
}
|
||||
|
||||
bo_do_apply() {
|
||||
local rebuild=0 arg
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--rebuild) rebuild=1 ;;
|
||||
--quiet) BO_QUIET=1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
bo_config_load
|
||||
|
||||
if [[ $CFG_ENABLED != yes ]]; then
|
||||
# The watcher, while switched off.
|
||||
[[ -n $BO_QUIET ]] && return 0
|
||||
bo_note "$(bo_msg "File opening is off. Turn it on first.")"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# The repair: whatever drifted is written again.
|
||||
if (( rebuild )); then
|
||||
bo_revert
|
||||
fi
|
||||
|
||||
bo_apply || return 1
|
||||
|
||||
if (( rebuild || BO_CHANGES )); then
|
||||
_bo_report
|
||||
elif (( BO_N_ACTIVE )); then
|
||||
bo_ok "$(bo_msg "Already up to date for %s." "$(bo_assoc_active_exts 12)")"
|
||||
else
|
||||
_bo_report
|
||||
fi
|
||||
|
||||
if [[ $CFG_WATCH == yes ]] && bo_watch_available; then
|
||||
if bo_watch_enabled; then
|
||||
bo_watch_refresh
|
||||
else
|
||||
bo_watch_enable || true
|
||||
fi
|
||||
elif [[ $CFG_WATCH != yes ]] && bo_watch_enabled; then
|
||||
bo_watch_disable
|
||||
fi
|
||||
}
|
||||
|
||||
bo_do_status() {
|
||||
bo_config_load
|
||||
bo_head " $BO_PRETTY"
|
||||
bo_ui_status
|
||||
printf '\n'
|
||||
}
|
||||
|
||||
# bo_do_list
|
||||
# Every file type and what opens it.
|
||||
bo_do_list() {
|
||||
local k i label state pad l_default l_openwith l_off l_missing
|
||||
|
||||
bo_config_load
|
||||
bo_assoc_compute
|
||||
|
||||
if (( ${#BO_A_ORDER[@]} == 0 )); then
|
||||
bo_note "$(bo_msg "No file types yet.")"
|
||||
return 0
|
||||
fi
|
||||
|
||||
bo_msg_into "double-click"; l_default="$BO_MSG_RESULT"
|
||||
bo_msg_into "Open With"; l_openwith="$BO_MSG_RESULT"
|
||||
bo_msg_into "off"; l_off="$BO_MSG_RESULT"
|
||||
bo_msg_into "program not found"; l_missing="$BO_MSG_RESULT"
|
||||
|
||||
bo_head " $(bo_msg "File types")"
|
||||
|
||||
for k in "${BO_A_ORDER[@]}"; do
|
||||
i="${BO_A_PIDX[k]}"
|
||||
if (( i >= 0 )); then
|
||||
bo_program_label "$i"
|
||||
label="$BO_LABEL"
|
||||
case "${BO_A_STATE[k]}" in
|
||||
default) state="$l_default" ;;
|
||||
openwith) state="$l_openwith" ;;
|
||||
*) state="$l_off" ;;
|
||||
esac
|
||||
else
|
||||
label="${BO_A_BOTTLE[k]}"
|
||||
state="$l_missing"
|
||||
fi
|
||||
printf -v label '%-9s %s' ".${BO_A_EXT[k]}" "$label"
|
||||
pad=$(( 46 - ${#label} ))
|
||||
(( pad < 0 )) && pad=0
|
||||
printf ' %s%*s %s\n' "$label" "$pad" '' "$state"
|
||||
done
|
||||
printf '\n'
|
||||
}
|
||||
|
||||
# bo_do_programs
|
||||
# Every program in every bottle, with the file types it opens.
|
||||
bo_do_programs() {
|
||||
local i pad label
|
||||
|
||||
bo_config_load
|
||||
_bo_need_bottles || return 1
|
||||
bo_assoc_compute
|
||||
|
||||
if (( ${#BO_P_ID[@]} == 0 )); then
|
||||
bo_note "$(bo_msg "No programs found in any bottle.")"
|
||||
return 0
|
||||
fi
|
||||
|
||||
bo_head " $(bo_msg "Programs")"
|
||||
for i in "${!BO_P_ID[@]}"; do
|
||||
bo_program_label "$i"
|
||||
label="$BO_LABEL"
|
||||
pad=$(( 46 - ${#label} ))
|
||||
(( pad < 0 )) && pad=0
|
||||
printf ' %s%*s %s%s%s\n' "$label" "$pad" '' "$BO_C_DIM" "$(bo_program_exts "$i")" "$BO_C_RESET"
|
||||
done
|
||||
printf '\n'
|
||||
}
|
||||
|
||||
# _bo_find_program <program> [bottle]
|
||||
# By name or id, optionally within one bottle. Index in BO_FOUND.
|
||||
_bo_find_program() {
|
||||
local want="$1" bottle="${2:-}" i
|
||||
local -a hits=()
|
||||
|
||||
for i in "${!BO_P_ID[@]}"; do
|
||||
[[ -n $bottle && ${BO_P_BOTTLE[i]} != "$bottle" ]] && continue
|
||||
if [[ ${BO_P_ID[i]} == "$want" || ${BO_P_NAME[i],,} == "${want,,}" ]]; then
|
||||
hits+=("$i")
|
||||
fi
|
||||
done
|
||||
|
||||
if (( ${#hits[@]} == 1 )); then
|
||||
BO_FOUND="${hits[0]}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if (( ${#hits[@]} == 0 )); then
|
||||
bo_bad "$(bo_msg "No program called \"%s\" was found. \"bottles-opener programs\" lists them all." "$want")"
|
||||
else
|
||||
bo_bad "$(bo_msg "More than one program is called \"%s\" - name the bottle as well." "$want")"
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# bo_do_add <extensions> <program> [bottle] [--open-with|--default]
|
||||
# Extensions separated by commas: flp,fst.
|
||||
bo_do_add() {
|
||||
local state='' arg ext i
|
||||
local -a pos=()
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--open-with) state=openwith ;;
|
||||
--default) state=default ;;
|
||||
*) pos+=("$arg") ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if (( ${#pos[@]} < 2 )); then
|
||||
bo_bad "$(bo_msg "Usage: bottles-opener add <extension> <program> [bottle] [--open-with|--default]")"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! _bo_parse_exts "${pos[0]}" || (( ${#BO_EXTS[@]} == 0 )); then
|
||||
bo_bad "$(bo_msg "\"%s\" is not a file extension." "${BO_EXT:-${pos[0]}}")"
|
||||
return 1
|
||||
fi
|
||||
|
||||
bo_config_load
|
||||
_bo_need_bottles || return 1
|
||||
bo_scan_once
|
||||
_bo_find_program "${pos[1]}" "${pos[2]:-}" || return 1
|
||||
# Saved: bo_assoc_compute looks programs up through BO_FOUND as well.
|
||||
i=$BO_FOUND
|
||||
|
||||
bo_assoc_compute
|
||||
for ext in "${BO_EXTS[@]}"; do
|
||||
bo_assoc_add "$ext" "$i" "$state" || {
|
||||
bo_bad "$(bo_msg "Could not save the setting.")"
|
||||
return 1
|
||||
}
|
||||
done
|
||||
|
||||
if [[ $CFG_ENABLED == yes ]]; then
|
||||
bo_do_apply
|
||||
else
|
||||
bo_ok "$(bo_msg "Saved. Turn file opening on with \"bottles-opener enable\".")"
|
||||
fi
|
||||
}
|
||||
|
||||
# bo_do_remove <extensions> [program] [bottle]
|
||||
# With every program that opens them, or only the one named.
|
||||
bo_do_remove() {
|
||||
local k n=0 i ext
|
||||
|
||||
if (( $# < 1 )) || ! _bo_parse_exts "$1" || (( ${#BO_EXTS[@]} == 0 )); then
|
||||
bo_bad "$(bo_msg "Usage: bottles-opener remove <extension> [program] [bottle]")"
|
||||
return 1
|
||||
fi
|
||||
|
||||
bo_config_load
|
||||
bo_scan_once
|
||||
local only=-1
|
||||
if (( $# >= 2 )); then
|
||||
_bo_find_program "$2" "${3:-}" || return 1
|
||||
only=$BO_FOUND
|
||||
fi
|
||||
|
||||
bo_assoc_compute
|
||||
for ext in "${BO_EXTS[@]}"; do
|
||||
for k in "${!BO_A_EXT[@]}"; do
|
||||
[[ ${BO_A_EXT[k]} == "$ext" && ${BO_A_STATE[k]} != off ]] || continue
|
||||
i="${BO_A_PIDX[k]}"
|
||||
(( only < 0 || only == i )) || continue
|
||||
bo_assoc_remove "$k" && n=$(( n + 1 ))
|
||||
done
|
||||
done
|
||||
|
||||
if (( n == 0 )); then
|
||||
bo_note "$(bo_msg "Nothing opens .%s." "${BO_EXTS[*]}")"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ $CFG_ENABLED == yes ]]; then
|
||||
bo_do_apply
|
||||
else
|
||||
bo_ok "$(bo_msg "Saved.")"
|
||||
fi
|
||||
}
|
||||
|
||||
bo_do_help() {
|
||||
cat <<- EOF
|
||||
$BO_PRETTY $BO_VERSION
|
||||
|
||||
$(bo_msg "Usage: bottles-opener [command]")
|
||||
|
||||
$(bo_msg "Commands:")
|
||||
enable $(bo_msg "Turn file opening on")
|
||||
disable $(bo_msg "Turn it off and put everything back")
|
||||
apply $(bo_msg "Pick up programs installed since the last run")
|
||||
apply --rebuild $(bo_msg "Take everything back and write it again")
|
||||
status $(bo_msg "Show what is set up")
|
||||
list $(bo_msg "List every file type and what opens it")
|
||||
programs $(bo_msg "List every program and the file types it opens")
|
||||
add <ext> <program> [bottle]
|
||||
$(bo_msg "Open a file type with a program")
|
||||
remove <ext> [program] $(bo_msg "Stop opening a file type")
|
||||
-h, --help $(bo_msg "Show this help")
|
||||
-V, --version $(bo_msg "Show the version")
|
||||
|
||||
$(bo_msg "Without a command an interactive menu is shown.")
|
||||
EOF
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Dispatch
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
main() {
|
||||
local cmd="${1:-}"
|
||||
[[ $# -gt 0 ]] && shift
|
||||
|
||||
case "$cmd" in
|
||||
enable) bo_do_enable "$@" ;;
|
||||
disable) bo_do_disable "$@" ;;
|
||||
apply|refresh) bo_do_apply "$@" ;;
|
||||
status) bo_do_status "$@" ;;
|
||||
list) bo_do_list "$@" ;;
|
||||
programs) bo_do_programs "$@" ;;
|
||||
add) bo_do_add "$@" ;;
|
||||
remove) bo_do_remove "$@" ;;
|
||||
|
||||
# What the launchers run. Not in the help: nobody types it.
|
||||
open) bo_do_open "$@" ;;
|
||||
|
||||
-h|--help|help) bo_do_help ;;
|
||||
-V|--version) printf '%s %s\n' "$BO_NAME" "$BO_VERSION" ;;
|
||||
|
||||
'') bo_ui_menu ;;
|
||||
*)
|
||||
bo_bad "$(bo_msg "Unknown command: %s" "$cmd")"
|
||||
printf '\n'
|
||||
bo_do_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -0,0 +1,484 @@
|
||||
# shellcheck shell=bash
|
||||
#
|
||||
# Writing it all down, and taking it back.
|
||||
#
|
||||
# bo_apply converges: works out what should exist, writes what differs, takes
|
||||
# back what the ledger has and is no longer wanted. Idempotent, so the watcher
|
||||
# can call it any time. Undoing is converging on nothing.
|
||||
|
||||
BO_CHANGES=0
|
||||
|
||||
# Stable, unique per program, nothing to quote.
|
||||
bo_launcher_id() {
|
||||
printf '%s\t%s' "$1" "$2" | sha1sum | cut -c1-12
|
||||
}
|
||||
|
||||
# _bo_entry_escape <string>
|
||||
# A desktop entry value. The scan already removed newlines.
|
||||
_bo_entry_escape() {
|
||||
printf '%s' "${1//\\/\\\\}"
|
||||
}
|
||||
|
||||
# _bo_exec_arg <string>
|
||||
# One Exec argument, escaped for both layers: string value and Exec quoting.
|
||||
_bo_exec_arg() {
|
||||
local s="$1"
|
||||
|
||||
s="${s//%/%%}"
|
||||
if [[ $s =~ ^[A-Za-z0-9_./+%-]+$ ]]; then
|
||||
printf '%s' "$s"
|
||||
return
|
||||
fi
|
||||
|
||||
s="${s//\\/\\\\}"
|
||||
s="${s//\"/\\\"}"
|
||||
s="${s//\`/\\\`}"
|
||||
s="${s//\$/\\\$}"
|
||||
# and the string layer on top
|
||||
s="${s//\\/\\\\}"
|
||||
printf '"%s"' "$s"
|
||||
}
|
||||
|
||||
# bo_launcher_entry <program index> <MIME types; separated> <launcher id> <display name>
|
||||
bo_launcher_entry() {
|
||||
local i="$1" mimes="$2" id="$3" name="$4" icon exe wmclass comment
|
||||
|
||||
icon="${BO_P_ICON[i]}"
|
||||
if [[ $icon == /* && ! -f $icon ]] || [[ -z $icon ]]; then
|
||||
icon="com.usebottles.bottles"
|
||||
fi
|
||||
|
||||
exe="${BO_P_PATH[i]//\\//}"
|
||||
exe="${exe##*/}"
|
||||
wmclass="${exe,,}"
|
||||
|
||||
comment="$(bo_msg "Opens files in %s from the bottle \"%s\"" "${BO_P_NAME[i]}" "${BO_P_BOTTLE[i]}")"
|
||||
|
||||
printf '[Desktop Entry]\n'
|
||||
printf 'Type=Application\n'
|
||||
printf 'Name=%s\n' "$(_bo_entry_escape "$name")"
|
||||
printf 'Comment=%s\n' "$(_bo_entry_escape "$comment")"
|
||||
printf 'Icon=%s\n' "$(_bo_entry_escape "$icon")"
|
||||
printf 'Exec=%s open %s %%f\n' "$(_bo_exec_arg "$BO_SELF")" "$id"
|
||||
printf 'Terminal=false\n'
|
||||
# Hidden: Bottles has the menu entry. Still opens its types and shows under
|
||||
# "Open With", as the spec intends.
|
||||
printf 'NoDisplay=true\n'
|
||||
printf 'MimeType=%s\n' "$mimes"
|
||||
[[ -n $wmclass ]] && printf 'StartupWMClass=%s\n' "$(_bo_entry_escape "$wmclass")"
|
||||
printf 'X-Bottles-Opener-Id=%s\n' "$id"
|
||||
}
|
||||
|
||||
# _bo_owned <path>
|
||||
# Ours to overwrite: in the ledger, or carrying our mark (a lost ledger).
|
||||
# Icons have no mark.
|
||||
_bo_owned() {
|
||||
local path="$1"
|
||||
[[ -e $path ]] || return 0
|
||||
bo_ledger_has file "$path" && return 0
|
||||
case "$path" in
|
||||
*.desktop) grep -q '^X-Bottles-Opener-Id=' "$path" 2>/dev/null ;;
|
||||
*.xml) grep -q 'Written by bottles-opener' "$path" 2>/dev/null ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# _bo_file_put <path> <content>
|
||||
# Writes and records a file of ours. Never overwrites somebody else's.
|
||||
_bo_file_put() {
|
||||
local path="$1" content="$2" rc
|
||||
|
||||
if ! _bo_owned "$path"; then
|
||||
bo_bad "$(bo_msg "Not overwriting %s - it was not written by this program." "$path")"
|
||||
return 1
|
||||
fi
|
||||
|
||||
bo_write_if_changed "$path" "$content"
|
||||
rc=$?
|
||||
(( rc == 2 )) && return 1
|
||||
(( rc == 0 )) && _bo_mark_dirty "$path"
|
||||
bo_ledger_add file "$path"
|
||||
return 0
|
||||
}
|
||||
|
||||
# _bo_file_copy <path> <source>
|
||||
_bo_file_copy() {
|
||||
local path="$1" src="$2"
|
||||
|
||||
_bo_owned "$path" || return 1
|
||||
|
||||
if ! cmp -s -- "$src" "$path" 2>/dev/null; then
|
||||
mkdir -p "$(dirname "$path")" 2>/dev/null || return 1
|
||||
cp -f -- "$src" "$path" 2>/dev/null || return 1
|
||||
_bo_mark_dirty "$path"
|
||||
fi
|
||||
bo_ledger_add file "$path"
|
||||
}
|
||||
|
||||
# What changed, so only those caches are rebuilt. BO_CHANGES: any of it.
|
||||
BO_DIRTY_MIME=0
|
||||
BO_DIRTY_APPS=0
|
||||
BO_DIRTY_ICONS=0
|
||||
|
||||
_bo_mark_dirty() {
|
||||
BO_CHANGES=1
|
||||
case "$1" in
|
||||
"$BO_MIMEPKG") BO_DIRTY_MIME=1 ;;
|
||||
"$BO_APPDIR"/*) BO_DIRTY_APPS=1 ;;
|
||||
"$BO_ICONDIR"/*) BO_DIRTY_ICONS=1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# bo_icons_changed
|
||||
# KIconLoader's iconChanged signal.
|
||||
bo_icons_changed() {
|
||||
if bo_have dbus-send; then
|
||||
dbus-send --session --type=signal /KIconLoader \
|
||||
org.kde.KIconLoader.iconChanged int32:0 > /dev/null 2>&1 || true
|
||||
elif bo_have gdbus; then
|
||||
gdbus emit --session --object-path /KIconLoader \
|
||||
--signal org.kde.KIconLoader.iconChanged 0 > /dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
# bo_refresh
|
||||
# Rebuilds the desktop's caches. Every tool is optional.
|
||||
bo_refresh() {
|
||||
if (( BO_DIRTY_MIME )) && bo_have update-mime-database; then
|
||||
mkdir -p "$BO_MIMEDIR/packages" 2>/dev/null
|
||||
update-mime-database "$BO_MIMEDIR" > /dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
if (( BO_DIRTY_APPS )) && bo_have update-desktop-database; then
|
||||
update-desktop-database "$BO_APPDIR" > /dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
if (( BO_DIRTY_ICONS )) && [[ -d $BO_ICONDIR ]] && bo_have gtk-update-icon-cache; then
|
||||
# The user's hicolor has no index.theme of its own, hence -t.
|
||||
gtk-update-icon-cache -q -t -f "$BO_ICONDIR" > /dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
# Running KDE apps remember icons they did not find; this makes them look
|
||||
# again, as System Settings does after an icon theme change.
|
||||
if (( BO_DIRTY_ICONS )); then
|
||||
bo_icons_changed
|
||||
fi
|
||||
|
||||
# KDE's cache, rebuilt now rather than eventually.
|
||||
if (( BO_DIRTY_MIME || BO_DIRTY_APPS )); then
|
||||
local k
|
||||
for k in kbuildsycoca6 kbuildsycoca5; do
|
||||
if bo_have "$k"; then
|
||||
"$k" > /dev/null 2>&1 || true
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
BO_DIRTY_MIME=0; BO_DIRTY_APPS=0; BO_DIRTY_ICONS=0
|
||||
}
|
||||
|
||||
# bo_apply
|
||||
bo_apply() {
|
||||
local k i ext mime id path name src
|
||||
local -a want_files=() launcher_lines=() command_lines=()
|
||||
declare -A prog_mimes=() lid=() defaults=() default_prog=() names=() dup=()
|
||||
declare -A icon_src=() icon_idx=() icon_prog=() icon_rank=()
|
||||
declare -A BO_PKG_DESC=() BO_PKG_MAGIC=() BO_PKG_GICON=() BO_PKG_GLOBS=() BO_PKG_ICON=()
|
||||
BO_PKG_ORDER=()
|
||||
|
||||
BO_CHANGES=0
|
||||
bo_config_load
|
||||
bo_assoc_compute
|
||||
|
||||
# Unreadable looks like empty, and converging on empty removes everything.
|
||||
if [[ -n $BO_SCAN_ERROR ]]; then
|
||||
bo_bad "$(bo_msg "Bottles could not be read - is python3 with PyYAML installed? Nothing was changed.")"
|
||||
return 1
|
||||
fi
|
||||
|
||||
for k in "${BO_A_ORDER[@]}"; do
|
||||
i="${BO_A_PIDX[k]}"
|
||||
[[ ${BO_A_STATE[k]} != off && $i -ge 0 ]] || continue
|
||||
ext="${BO_A_EXT[k]}"
|
||||
|
||||
bo_mime_for "$ext"
|
||||
mime="$BO_MIME"
|
||||
|
||||
[[ -n ${lid[$i]:-} ]] || lid[$i]="$(bo_launcher_id "${BO_P_INSTALL[i]}" "${BO_P_ID[i]}")"
|
||||
[[ ";${prog_mimes[$i]:-}" == *";$mime;"* ]] || prog_mimes[$i]+="$mime;"
|
||||
|
||||
if [[ ${BO_A_STATE[k]} == default && -z ${default_prog[$mime]:-} ]]; then
|
||||
default_prog[$mime]="$i"
|
||||
fi
|
||||
|
||||
# How Windows starts the program for this extension.
|
||||
[[ -n ${BO_A_ARGS[k]} ]] \
|
||||
&& command_lines+=("${lid[$i]}"$'\t'"$ext"$'\t'"${BO_A_ARGS[k]}")
|
||||
|
||||
if (( BO_MIME_OWN )); then
|
||||
if [[ -z ${BO_PKG_DESC[$mime]+set} ]]; then
|
||||
bo_known_type "$ext"
|
||||
BO_PKG_ORDER+=("$mime")
|
||||
BO_PKG_DESC[$mime]="${BO_A_DESC[k]:-$BO_KT_DESC}"
|
||||
BO_PKG_MAGIC[$mime]="$BO_KT_MAGIC"
|
||||
BO_PKG_GICON[$mime]="$BO_KT_ICON"
|
||||
BO_PKG_GLOBS[$mime]=''
|
||||
fi
|
||||
[[ " ${BO_PKG_GLOBS[$mime]} " == *" $ext "* ]] \
|
||||
|| BO_PKG_GLOBS[$mime]+="${BO_PKG_GLOBS[$mime]:+ }$ext"
|
||||
fi
|
||||
|
||||
# As in Windows: the icon of the default program. A system type keeps
|
||||
# its own until then; one of ours takes the first program's.
|
||||
if [[ $CFG_ICONS == yes ]] && { (( BO_MIME_OWN )) || [[ ${BO_A_STATE[k]} == default ]]; }; then
|
||||
local rank=1
|
||||
[[ ${BO_A_STATE[k]} == default ]] && rank=2
|
||||
[[ -n ${BO_A_ICON[k]} ]] && rank=$(( rank + 2 ))
|
||||
if (( rank > ${icon_rank[$mime]:-0} )); then
|
||||
icon_rank[$mime]=$rank
|
||||
icon_prog[$mime]=$i
|
||||
if [[ -n ${BO_A_ICON[k]} ]]; then
|
||||
icon_src[$mime]="${BO_A_ICON[k]}"; icon_idx[$mime]="${BO_A_IDX[k]}"
|
||||
else
|
||||
icon_src[$mime]="${BO_P_ICON[i]}"; icon_idx[$mime]=0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# The same name twice in "Open With" gets the bottle added.
|
||||
for i in "${!prog_mimes[@]}"; do
|
||||
name="${BO_P_NAME[i]}"
|
||||
[[ -n ${names[$name]+set} ]] && dup[$name]=1
|
||||
names[$name]=1
|
||||
done
|
||||
|
||||
# Icons. A system type goes into the package just for its icon.
|
||||
for mime in "${!icon_src[@]}"; do
|
||||
i="${icon_prog[$mime]}"
|
||||
src="${icon_src[$mime]}"
|
||||
# No Windows icon to be had: the program's own, as Bottles saved it.
|
||||
if ! bo_icon_png "${BO_P_INSTALL[i]}" "$src" "${icon_idx[$mime]}"; then
|
||||
[[ $src != "${BO_P_ICON[i]}" ]] || continue
|
||||
bo_icon_png "${BO_P_INSTALL[i]}" "${BO_P_ICON[i]}" 0 || continue
|
||||
fi
|
||||
bo_icon_target "$mime" "$BO_ICON_PNG" || continue
|
||||
_bo_file_copy "$BO_ICON_TARGET" "$BO_ICON_PNG" || continue
|
||||
want_files+=("$BO_ICON_TARGET")
|
||||
BO_PKG_ICON[$mime]="$BO_ICON_NAME"
|
||||
[[ -n ${BO_PKG_DESC[$mime]+set} ]] || BO_PKG_ORDER+=("$mime")
|
||||
done
|
||||
|
||||
if (( ${#BO_PKG_ORDER[@]} )); then
|
||||
_bo_file_put "$BO_MIMEPKG" "$(bo_mime_package)" && want_files+=("$BO_MIMEPKG")
|
||||
fi
|
||||
|
||||
# The launchers.
|
||||
for i in "${!prog_mimes[@]}"; do
|
||||
id="${lid[$i]}"
|
||||
path="$BO_APPDIR/bottles-opener-$id.desktop"
|
||||
name="${BO_P_NAME[i]}"
|
||||
[[ -n ${dup[$name]+set} ]] && name="$name (${BO_P_BOTTLE[i]})"
|
||||
|
||||
_bo_file_put "$path" "$(bo_launcher_entry "$i" "${prog_mimes[$i]}" "$id" "$name")" \
|
||||
|| continue
|
||||
want_files+=("$path")
|
||||
|
||||
launcher_lines+=("$(printf '%s\t%s\t%s\t%s\t%s\t%s' "$id" "${BO_P_INSTALL[i]}" \
|
||||
"${BO_P_ID[i]}" "${BO_P_BOTTLE[i]}" "${BO_P_BDIR[i]}" "${BO_P_NAME[i]}")")
|
||||
|
||||
for mime in "${!default_prog[@]}"; do
|
||||
[[ ${default_prog[$mime]} == "$i" ]] && defaults[$mime]="bottles-opener-$id.desktop"
|
||||
done
|
||||
done
|
||||
|
||||
# What `open` reads.
|
||||
mkdir -p "$BO_STATEDIR" 2>/dev/null
|
||||
if (( ${#launcher_lines[@]} )); then
|
||||
printf '%s\n' "${launcher_lines[@]}" > "$BO_LAUNCHERS"
|
||||
else
|
||||
rm -f -- "$BO_LAUNCHERS"
|
||||
fi
|
||||
if (( ${#command_lines[@]} )); then
|
||||
printf '%s\n' "${command_lines[@]}" > "$BO_COMMANDS"
|
||||
else
|
||||
rm -f -- "$BO_COMMANDS"
|
||||
fi
|
||||
|
||||
_bo_prune_files "${want_files[@]}"
|
||||
|
||||
# Defaults, once the launchers they name exist.
|
||||
for mime in "${!defaults[@]}"; do
|
||||
_bo_default_set "$mime" "${defaults[$mime]};"
|
||||
done
|
||||
local m
|
||||
while IFS= read -r m; do
|
||||
[[ -n $m && -z ${defaults[$m]:-} ]] && _bo_default_restore "$m"
|
||||
done < <(bo_ledger_list default)
|
||||
|
||||
bo_refresh
|
||||
|
||||
bo_assoc_counts
|
||||
bo_state_write last_apply "$(date +%s)"
|
||||
return 0
|
||||
}
|
||||
|
||||
# _bo_prune_files <wanted path>...
|
||||
_bo_prune_files() {
|
||||
local f w keep
|
||||
while IFS= read -r f; do
|
||||
[[ -n $f ]] || continue
|
||||
keep=0
|
||||
for w in "$@"; do
|
||||
[[ $w == "$f" ]] && { keep=1; break; }
|
||||
done
|
||||
(( keep )) && continue
|
||||
|
||||
# Only the file; the directory may have been there before.
|
||||
if [[ -e $f ]]; then
|
||||
rm -f -- "$f" && _bo_mark_dirty "$f"
|
||||
fi
|
||||
bo_ledger_forget file "$f"
|
||||
done < <(bo_ledger_list file)
|
||||
}
|
||||
|
||||
# _bo_default_set <MIME type> <value>
|
||||
_bo_default_set() {
|
||||
local mime="$1" value="$2" current
|
||||
|
||||
current="$(bo_mimeapps_get "$mime")"
|
||||
[[ $current == "$value" ]] && { bo_ledger_add default "$mime" "-"; return 0; }
|
||||
|
||||
# Set here before, changed since in the desktop's settings: that choice
|
||||
# stands. See bo_default_claim.
|
||||
if [[ -n $current ]] && ! bo_is_ours "$current" && bo_ledger_has default "$mime"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# What was there before, unless one of ours.
|
||||
if bo_is_ours "$current" || [[ -z $current ]]; then
|
||||
bo_ledger_add default "$mime" "-"
|
||||
else
|
||||
bo_ledger_add default "$mime" "$current"
|
||||
fi
|
||||
|
||||
bo_mimeapps_set "$mime" "$value" && { BO_CHANGES=1; BO_DIRTY_APPS=1; }
|
||||
}
|
||||
|
||||
# bo_default_claim <extension>
|
||||
# Made the default by hand: take the type back even from a later choice, by
|
||||
# forgetting the record. The next apply records the current one instead.
|
||||
bo_default_claim() {
|
||||
local current
|
||||
|
||||
bo_mime_for "$1"
|
||||
current="$(bo_mimeapps_get "$BO_MIME")"
|
||||
# Still ours: keep the record of what came first.
|
||||
bo_is_ours "$current" && return 0
|
||||
bo_ledger_forget default "$BO_MIME"
|
||||
}
|
||||
|
||||
# _bo_default_restore <MIME type>
|
||||
# Puts back what was there, if the default is still ours.
|
||||
_bo_default_restore() {
|
||||
local mime="$1" prev current
|
||||
|
||||
prev="$(bo_ledger_detail default "$mime")"
|
||||
current="$(bo_mimeapps_get "$mime")"
|
||||
|
||||
if bo_is_ours "$current"; then
|
||||
[[ $prev == "-" ]] && prev=''
|
||||
bo_mimeapps_set "$mime" "$prev" && { BO_CHANGES=1; BO_DIRTY_APPS=1; }
|
||||
fi
|
||||
bo_ledger_forget default "$mime"
|
||||
}
|
||||
|
||||
# bo_revert
|
||||
# Takes back every recorded change.
|
||||
bo_revert() {
|
||||
local m
|
||||
|
||||
BO_CHANGES=0
|
||||
_bo_prune_files
|
||||
while IFS= read -r m; do
|
||||
[[ -n $m ]] && _bo_default_restore "$m"
|
||||
done < <(bo_ledger_list default)
|
||||
rm -f -- "$BO_LAUNCHERS" "$BO_COMMANDS"
|
||||
[[ -s $BO_LEDGER ]] || rm -f -- "$BO_LEDGER"
|
||||
|
||||
bo_refresh
|
||||
return 0
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The watcher
|
||||
# ---------------------------------------------------------------------------
|
||||
# A systemd path unit on the bottles directories and every bottle.yml. What the
|
||||
# shipped unit cannot know goes into a drop-in.
|
||||
|
||||
BO_UNIT_PATH="bottles-opener.path"
|
||||
BO_UNIT_SERVICE="bottles-opener.service"
|
||||
BO_UNIT_DROPIN="${BO_XDG_CONFIG}/systemd/user/${BO_UNIT_PATH}.d"
|
||||
|
||||
bo_watch_available() {
|
||||
[[ -z ${BO_NO_WATCH:-} ]] || return 1
|
||||
bo_have systemctl && [[ -n ${XDG_RUNTIME_DIR:-} || -S "/run/user/$(id -u)/bus" ]]
|
||||
}
|
||||
|
||||
bo_watch_enabled() {
|
||||
bo_watch_available || return 1
|
||||
systemctl --user is-enabled --quiet "$BO_UNIT_PATH" 2>/dev/null
|
||||
}
|
||||
|
||||
# _bo_watch_dropin_write
|
||||
# 0 when it changed and systemd needs a reload, 1 when not.
|
||||
_bo_watch_dropin_write() {
|
||||
local content="[Path]"$'\n' p
|
||||
|
||||
bo_scan_once
|
||||
for p in "${BO_ROOTS[@]}"; do
|
||||
# The unit watches the default places already.
|
||||
[[ $p == "$HOME/.local/share/bottles/bottles" ]] && continue
|
||||
[[ $p == "$HOME/.var/app/$BO_FLATPAK_ID/data/bottles/bottles" ]] && continue
|
||||
# % is a unit file specifier.
|
||||
content+="PathModified=${p//%/%%}"$'\n'
|
||||
done
|
||||
for p in "${BO_CONFS[@]}"; do
|
||||
content+="PathChanged=${p//%/%%}"$'\n'
|
||||
done
|
||||
|
||||
bo_write_if_changed "$BO_UNIT_DROPIN/bottles.conf" "$content"
|
||||
}
|
||||
|
||||
_bo_watch_dropin_remove() {
|
||||
[[ -e "$BO_UNIT_DROPIN/bottles.conf" ]] || return 1
|
||||
rm -f -- "$BO_UNIT_DROPIN/bottles.conf"
|
||||
rmdir "$BO_UNIT_DROPIN" 2>/dev/null || true
|
||||
return 0
|
||||
}
|
||||
|
||||
bo_watch_enable() {
|
||||
bo_watch_available || return 1
|
||||
_bo_watch_dropin_write
|
||||
systemctl --user daemon-reload > /dev/null 2>&1 || true
|
||||
systemctl --user enable --now "$BO_UNIT_PATH" > /dev/null 2>&1 || return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
# bo_watch_refresh
|
||||
# Keeps a running watcher in step with the bottles that exist now.
|
||||
bo_watch_refresh() {
|
||||
bo_watch_available || return 0
|
||||
_bo_watch_dropin_write || return 0
|
||||
systemctl --user daemon-reload > /dev/null 2>&1 || true
|
||||
systemctl --user restart "$BO_UNIT_PATH" > /dev/null 2>&1 || true
|
||||
return 0
|
||||
}
|
||||
|
||||
bo_watch_disable() {
|
||||
bo_watch_available || return 0
|
||||
systemctl --user disable --now "$BO_UNIT_PATH" > /dev/null 2>&1 || true
|
||||
_bo_watch_dropin_remove && systemctl --user daemon-reload > /dev/null 2>&1
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
# shellcheck shell=bash
|
||||
#
|
||||
# Which program opens which file type.
|
||||
#
|
||||
# Three sources, later ones winning: what each program registered in its
|
||||
# bottle's registry, the table in known.sh, and what was set by hand in the
|
||||
# types file.
|
||||
#
|
||||
# States:
|
||||
# default opens on a double click
|
||||
# openwith only offered under "Open With"
|
||||
# off left alone
|
||||
#
|
||||
# Used by bo_apply to write everything and by the screens to show it, so what
|
||||
# they show is what happens.
|
||||
|
||||
BO_A_EXT=() # extension, without the dot
|
||||
BO_A_STATE=() # default | openwith | off
|
||||
BO_A_PIDX=() # index into BO_P_*, -1 for a program that is gone
|
||||
BO_A_SRC=() # detected | added
|
||||
BO_A_INSTALL=()
|
||||
BO_A_PID=()
|
||||
BO_A_BOTTLE=()
|
||||
BO_A_ARGS=() # Windows argument template, empty for plain "%1"
|
||||
BO_A_ICON=() # file holding the type's Windows icon, empty for none
|
||||
BO_A_IDX=() # its DefaultIcon index
|
||||
BO_A_DESC=() # the type's name in Windows, empty for none
|
||||
|
||||
BO_A_ORDER=() # indices into BO_A_*, sorted by extension then program
|
||||
|
||||
# _bo_assoc_push <ext> <state> <program index> <source> <install> <id> <bottle>
|
||||
_bo_assoc_push() {
|
||||
local r="${BO_R_AT[$1$'\x1f'$5$'\x1f'$6]:-}"
|
||||
|
||||
BO_A_EXT+=("$1"); BO_A_STATE+=("$2"); BO_A_PIDX+=("$3"); BO_A_SRC+=("$4")
|
||||
BO_A_INSTALL+=("$5"); BO_A_PID+=("$6"); BO_A_BOTTLE+=("$7")
|
||||
|
||||
if [[ -n $r ]]; then
|
||||
BO_A_ARGS+=("${BO_R_ARGS[r]}"); BO_A_ICON+=("${BO_R_ICON[r]}")
|
||||
BO_A_IDX+=("${BO_R_IDX[r]}"); BO_A_DESC+=("${BO_R_DESC[r]}")
|
||||
else
|
||||
BO_A_ARGS+=("${BO_KNOWN_ARGS[$1]:-}"); BO_A_ICON+=(''); BO_A_IDX+=(0); BO_A_DESC+=('')
|
||||
fi
|
||||
}
|
||||
|
||||
bo_assoc_compute() {
|
||||
local i j k r ext key
|
||||
declare -A at=() has_default=()
|
||||
|
||||
BO_A_EXT=(); BO_A_STATE=(); BO_A_PIDX=(); BO_A_SRC=()
|
||||
BO_A_INSTALL=(); BO_A_PID=(); BO_A_BOTTLE=()
|
||||
BO_A_ARGS=(); BO_A_ICON=(); BO_A_IDX=(); BO_A_DESC=(); BO_A_ORDER=()
|
||||
|
||||
bo_scan_once
|
||||
|
||||
# Detected: registered in the bottle, or known from the table. "auto" is
|
||||
# settled below, once every default set by hand is known.
|
||||
if [[ $CFG_DETECT == yes ]]; then
|
||||
for r in "${!BO_R_EXT[@]}"; do
|
||||
bo_program_find "${BO_R_INSTALL[r]}" "${BO_R_PID[r]}" || continue
|
||||
i=$BO_FOUND
|
||||
key="${BO_R_EXT[r]}"$'\x1f'"${BO_R_INSTALL[r]}"$'\x1f'"${BO_R_PID[r]}"
|
||||
[[ -n ${at[$key]+set} ]] && continue
|
||||
at[$key]=${#BO_A_EXT[@]}
|
||||
BO_KNOWN_ARGS=()
|
||||
_bo_assoc_push "${BO_R_EXT[r]}" auto "$i" detected \
|
||||
"${BO_R_INSTALL[r]}" "${BO_R_PID[r]}" "${BO_P_BOTTLE[i]}"
|
||||
done
|
||||
|
||||
for i in "${!BO_P_ID[@]}"; do
|
||||
bo_known_exts "${BO_P_PATH[i]}" || continue
|
||||
for ext in $BO_KNOWN; do
|
||||
key="$ext"$'\x1f'"${BO_P_INSTALL[i]}"$'\x1f'"${BO_P_ID[i]}"
|
||||
[[ -n ${at[$key]+set} ]] && continue
|
||||
at[$key]=${#BO_A_EXT[@]}
|
||||
_bo_assoc_push "$ext" auto "$i" detected \
|
||||
"${BO_P_INSTALL[i]}" "${BO_P_ID[i]}" "${BO_P_BOTTLE[i]}"
|
||||
done
|
||||
done
|
||||
fi
|
||||
|
||||
# Set by hand.
|
||||
for j in "${!BO_T_EXT[@]}"; do
|
||||
key="${BO_T_EXT[j]}"$'\x1f'"${BO_T_INSTALL[j]}"$'\x1f'"${BO_T_PID[j]}"
|
||||
if [[ -n ${at[$key]+set} ]]; then
|
||||
BO_A_STATE[at[$key]]="${BO_T_STATE[j]}"
|
||||
continue
|
||||
fi
|
||||
|
||||
i=-1
|
||||
bo_program_find "${BO_T_INSTALL[j]}" "${BO_T_PID[j]}" && i=$BO_FOUND
|
||||
|
||||
# Switched off for a program nothing detects any more: nothing to say.
|
||||
[[ $i -lt 0 && ${BO_T_STATE[j]} == off ]] && continue
|
||||
|
||||
at[$key]=${#BO_A_EXT[@]}
|
||||
BO_KNOWN_ARGS=()
|
||||
(( i >= 0 )) && bo_known_exts "${BO_P_PATH[i]}"
|
||||
_bo_assoc_push "${BO_T_EXT[j]}" "${BO_T_STATE[j]}" "$i" added \
|
||||
"${BO_T_INSTALL[j]}" "${BO_T_PID[j]}" "${BO_T_BOTTLE[j]}"
|
||||
done
|
||||
|
||||
# One default per extension: the first one set by hand.
|
||||
for k in "${!BO_A_EXT[@]}"; do
|
||||
[[ ${BO_A_STATE[k]} == default && ${BO_A_PIDX[k]} -ge 0 ]] || continue
|
||||
ext="${BO_A_EXT[k]}"
|
||||
if [[ -n ${has_default[$ext]+set} ]]; then
|
||||
BO_A_STATE[k]=openwith
|
||||
else
|
||||
has_default[$ext]=1
|
||||
fi
|
||||
done
|
||||
|
||||
# A detected type is the default when this program teaches the system the
|
||||
# type - nothing else could open it. A type the system knows already
|
||||
# belongs to some application somebody chose, and only gets "Open With".
|
||||
for k in "${!BO_A_EXT[@]}"; do
|
||||
[[ ${BO_A_STATE[k]} == auto ]] || continue
|
||||
ext="${BO_A_EXT[k]}"
|
||||
bo_mime_for "$ext"
|
||||
if (( BO_MIME_OWN )) && [[ -z ${has_default[$ext]+set} ]]; then
|
||||
BO_A_STATE[k]=default
|
||||
has_default[$ext]=1
|
||||
else
|
||||
BO_A_STATE[k]=openwith
|
||||
fi
|
||||
done
|
||||
|
||||
# Sorted for display.
|
||||
if (( ${#BO_A_EXT[@]} )); then
|
||||
local label
|
||||
while IFS=$'\x1f' read -r _ _ k; do
|
||||
BO_A_ORDER+=("$k")
|
||||
done < <(
|
||||
for k in "${!BO_A_EXT[@]}"; do
|
||||
if (( BO_A_PIDX[k] >= 0 )); then
|
||||
label="${BO_P_NAME[BO_A_PIDX[k]]}"
|
||||
else
|
||||
label="~"
|
||||
fi
|
||||
printf '%s\x1f%s\x1f%s\n' "${BO_A_EXT[k]}" "${label,,}" "$k"
|
||||
done | LC_ALL=C sort -t $'\x1f' -k1,1 -k2,2
|
||||
)
|
||||
fi
|
||||
}
|
||||
|
||||
# bo_assoc_counts
|
||||
# BO_N_ACTIVE: types that are on, with their program present.
|
||||
BO_N_ACTIVE=0
|
||||
|
||||
bo_assoc_counts() {
|
||||
local k
|
||||
BO_N_ACTIVE=0
|
||||
for k in "${!BO_A_EXT[@]}"; do
|
||||
[[ ${BO_A_STATE[k]} != off && ${BO_A_PIDX[k]} -ge 0 ]] \
|
||||
&& BO_N_ACTIVE=$(( BO_N_ACTIVE + 1 ))
|
||||
done
|
||||
}
|
||||
|
||||
# bo_assoc_active_exts [max]
|
||||
# The extensions that are on, sorted, deduplicated. With a maximum, the rest
|
||||
# is counted: ".flp .fst .doc +12".
|
||||
bo_assoc_active_exts() {
|
||||
local max="${1:-0}" k n=0 out=''
|
||||
declare -A seen=()
|
||||
for k in "${BO_A_ORDER[@]}"; do
|
||||
[[ ${BO_A_STATE[k]} != off && ${BO_A_PIDX[k]} -ge 0 ]] || continue
|
||||
[[ -n ${seen[${BO_A_EXT[k]}]+set} ]] && continue
|
||||
seen[${BO_A_EXT[k]}]=1
|
||||
n=$(( n + 1 ))
|
||||
(( max > 0 && n > max )) && continue
|
||||
out+="${out:+ }.${BO_A_EXT[k]}"
|
||||
done
|
||||
(( max > 0 && n > max )) && out+=" +$(( n - max ))"
|
||||
printf '%s\n' "$out"
|
||||
}
|
||||
|
||||
# bo_program_exts <program index> [max]
|
||||
# The extensions that are on for one program, like bo_assoc_active_exts.
|
||||
bo_program_exts() {
|
||||
local i="$1" max="${2:-0}" k n=0 out=''
|
||||
for k in "${BO_A_ORDER[@]}"; do
|
||||
[[ ${BO_A_PIDX[k]} == "$i" && ${BO_A_STATE[k]} != off ]] || continue
|
||||
n=$(( n + 1 ))
|
||||
(( max > 0 && n > max )) && continue
|
||||
out+="${out:+ }.${BO_A_EXT[k]}"
|
||||
done
|
||||
(( max > 0 && n > max )) && out+=" +$(( n - max ))"
|
||||
printf '%s\n' "$out"
|
||||
}
|
||||
|
||||
# _bo_assoc_demote <extension> <install> <program id>
|
||||
# Every other program that is the default for the extension becomes an
|
||||
# "Open With" entry.
|
||||
_bo_assoc_demote() {
|
||||
local ext="$1" install="$2" pid="$3" o
|
||||
|
||||
for o in "${!BO_A_EXT[@]}"; do
|
||||
[[ ${BO_A_EXT[o]} == "$ext" && ${BO_A_STATE[o]} == default ]] || continue
|
||||
[[ ${BO_A_INSTALL[o]} == "$install" && ${BO_A_PID[o]} == "$pid" ]] && continue
|
||||
bo_types_set "$ext" openwith "${BO_A_INSTALL[o]}" \
|
||||
"${BO_A_PID[o]}" "${BO_A_BOTTLE[o]}" || return 1
|
||||
done
|
||||
}
|
||||
|
||||
# bo_assoc_set <index into BO_A_*> <state>
|
||||
bo_assoc_set() {
|
||||
local k="$1" state="$2"
|
||||
|
||||
if [[ $state == default ]]; then
|
||||
_bo_assoc_demote "${BO_A_EXT[k]}" "${BO_A_INSTALL[k]}" "${BO_A_PID[k]}" || return 1
|
||||
fi
|
||||
|
||||
bo_types_set "${BO_A_EXT[k]}" "$state" "${BO_A_INSTALL[k]}" \
|
||||
"${BO_A_PID[k]}" "${BO_A_BOTTLE[k]}" || return 1
|
||||
|
||||
[[ $state == default ]] && bo_default_claim "${BO_A_EXT[k]}"
|
||||
return 0
|
||||
}
|
||||
|
||||
# bo_assoc_remove <index into BO_A_*>
|
||||
# Added by hand: gone. Detected: switched off, or detection brings it back.
|
||||
bo_assoc_remove() {
|
||||
local k="$1" how=remove
|
||||
[[ ${BO_A_SRC[k]} == detected ]] && how=off
|
||||
bo_types_set "${BO_A_EXT[k]}" "$how" "${BO_A_INSTALL[k]}" \
|
||||
"${BO_A_PID[k]}" "${BO_A_BOTTLE[k]}"
|
||||
}
|
||||
|
||||
# bo_assoc_add <extension> <index into BO_P_*> [state]
|
||||
# Without a state: the default for a type only this program can open, unless
|
||||
# another program is already; "Open With" for one the system knows.
|
||||
bo_assoc_add() {
|
||||
local ext="$1" i="$2" state="${3:-}" k
|
||||
|
||||
if [[ -z $state ]]; then
|
||||
bo_mime_for "$ext"
|
||||
state=default
|
||||
(( BO_MIME_OWN )) || state=openwith
|
||||
for k in "${!BO_A_EXT[@]}"; do
|
||||
if [[ ${BO_A_EXT[k]} == "$ext" && ${BO_A_STATE[k]} == default \
|
||||
&& ${BO_A_PIDX[k]} -ge 0 && ${BO_A_PIDX[k]} -ne $i ]]; then
|
||||
state=openwith
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ $state == default ]]; then
|
||||
_bo_assoc_demote "$ext" "${BO_P_INSTALL[i]}" "${BO_P_ID[i]}" || return 1
|
||||
fi
|
||||
|
||||
bo_types_set "$ext" "$state" "${BO_P_INSTALL[i]}" "${BO_P_ID[i]}" "${BO_P_BOTTLE[i]}" \
|
||||
|| return 1
|
||||
|
||||
[[ $state == default ]] && bo_default_claim "$ext"
|
||||
return 0
|
||||
}
|
||||
|
||||
# bo_program_set_exts <program index> <extension>...
|
||||
# Makes exactly these the types the program opens: new ones are added, ones
|
||||
# left out are removed. Returns 1 when something could not be saved.
|
||||
bo_program_set_exts() {
|
||||
local i="$1" k ext
|
||||
shift
|
||||
local want=" $* "
|
||||
|
||||
for k in "${BO_A_ORDER[@]}"; do
|
||||
[[ ${BO_A_PIDX[k]} == "$i" && ${BO_A_STATE[k]} != off ]] || continue
|
||||
[[ $want == *" ${BO_A_EXT[k]} "* ]] && continue
|
||||
bo_assoc_remove "$k" || return 1
|
||||
done
|
||||
|
||||
for ext in "$@"; do
|
||||
for k in "${BO_A_ORDER[@]}"; do
|
||||
[[ ${BO_A_PIDX[k]} == "$i" && ${BO_A_EXT[k]} == "$ext" && ${BO_A_STATE[k]} != off ]] \
|
||||
&& continue 2
|
||||
done
|
||||
bo_assoc_add "$ext" "$i" || return 1
|
||||
done
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
# shellcheck shell=bash
|
||||
#
|
||||
# Paths, translations, output helpers and the change ledger.
|
||||
|
||||
BO_VERSION="@VERSION@"
|
||||
BO_NAME="bottles-opener"
|
||||
BO_PRETTY="Bottles Opener"
|
||||
|
||||
BO_LIBDIR="${BO_LIBDIR:-@LIBDIR@}"
|
||||
BO_LOCALEDIR="${BO_LOCALEDIR:-@LOCALEDIR@}"
|
||||
|
||||
BO_XDG_CONFIG="${XDG_CONFIG_HOME:-$HOME/.config}"
|
||||
BO_XDG_DATA="${XDG_DATA_HOME:-$HOME/.local/share}"
|
||||
BO_XDG_STATE="${XDG_STATE_HOME:-$HOME/.local/state}"
|
||||
|
||||
BO_CONFDIR="${BO_CONFDIR:-${BO_XDG_CONFIG}/${BO_NAME}}"
|
||||
BO_CONFIG="${BO_CONFIG:-${BO_CONFDIR}/config}"
|
||||
BO_TYPES="${BO_TYPES:-${BO_CONFDIR}/types}"
|
||||
BO_STATEDIR="${BO_STATEDIR:-${BO_XDG_STATE}/${BO_NAME}}"
|
||||
BO_CACHEDIR="${BO_CACHEDIR:-${XDG_CACHE_HOME:-$HOME/.cache}/${BO_NAME}}"
|
||||
|
||||
# Launchers, each named bottles-opener-<id>.desktop.
|
||||
BO_APPDIR="${BO_XDG_DATA}/applications"
|
||||
|
||||
# The user's MIME database; update-mime-database compiles packages/.
|
||||
BO_MIMEDIR="${BO_XDG_DATA}/mime"
|
||||
BO_MIMEPKG="${BO_MIMEDIR}/packages/${BO_NAME}.xml"
|
||||
|
||||
# hicolor: the theme every other theme falls back to.
|
||||
BO_ICONDIR="${BO_XDG_DATA}/icons/hicolor"
|
||||
|
||||
# Default applications. Every desktop reads this one.
|
||||
BO_MIMEAPPS="${BO_XDG_CONFIG}/mimeapps.list"
|
||||
|
||||
# Every change, so `disable` undoes exactly that. See bo_ledger_add.
|
||||
BO_LEDGER="${BO_STATEDIR}/ledger"
|
||||
|
||||
# Which launcher starts what, read by `open`. Exec lines carry only the id:
|
||||
# no bottle name to quote.
|
||||
BO_LAUNCHERS="${BO_STATEDIR}/launchers"
|
||||
|
||||
# The Windows arguments per launcher and extension, as the registry has them.
|
||||
BO_COMMANDS="${BO_STATEDIR}/commands"
|
||||
|
||||
BO_FLATPAK_ID="com.usebottles.bottles"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Translations
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
export TEXTDOMAIN="bottles-opener"
|
||||
export TEXTDOMAINDIR="${BO_LOCALEDIR}"
|
||||
|
||||
bo_ui_locale() {
|
||||
local l="${BO_UI_LOCALE:-}"
|
||||
|
||||
if [[ -z $l ]]; then
|
||||
l="${LC_ALL:-}"
|
||||
[[ -z $l ]] && l="${LC_MESSAGES:-}"
|
||||
[[ -z $l ]] && l="${LANG:-}"
|
||||
fi
|
||||
|
||||
# systemd's file, and Debian's.
|
||||
if [[ -z $l ]]; then
|
||||
local f
|
||||
for f in /etc/locale.conf /etc/default/locale; do
|
||||
[[ -r $f ]] || continue
|
||||
l="$(sed -n 's/^LANG=//p' "$f" | tr -d '"' | head -n1)"
|
||||
[[ -n $l ]] && break
|
||||
done
|
||||
fi
|
||||
|
||||
printf '%s\n' "${l:-C}"
|
||||
}
|
||||
|
||||
# Resolved once, and lookups memoized: each is a fork, and screens redraw per
|
||||
# keypress.
|
||||
BO_LOCALE=''
|
||||
|
||||
declare -A BO_MSG_CACHE=()
|
||||
|
||||
BO_MSG_RESULT=''
|
||||
|
||||
# bo_msg_into <msgid>
|
||||
# Lookup into BO_MSG_RESULT, without a fork.
|
||||
bo_msg_into() {
|
||||
local msgid="$1"
|
||||
|
||||
[[ -n $BO_LOCALE ]] || BO_LOCALE="$(bo_ui_locale)"
|
||||
|
||||
if [[ -n ${BO_MSG_CACHE[$msgid]+set} ]]; then
|
||||
BO_MSG_RESULT="${BO_MSG_CACHE[$msgid]}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
BO_MSG_RESULT="$(LC_ALL="$BO_LOCALE" LANGUAGE="${BO_LOCALE%%.*}" gettext -- "$msgid" 2>/dev/null)"
|
||||
[[ -n $BO_MSG_RESULT ]] || BO_MSG_RESULT="$msgid"
|
||||
BO_MSG_CACHE[$msgid]="$BO_MSG_RESULT"
|
||||
return 0
|
||||
}
|
||||
|
||||
# bo_msg <msgid> [printf args...]
|
||||
bo_msg() {
|
||||
local msgid="$1"
|
||||
shift
|
||||
|
||||
bo_msg_into "$msgid"
|
||||
|
||||
# Without arguments it is text, not a format: a % would break printf.
|
||||
if (( $# == 0 )); then
|
||||
printf '%s' "$BO_MSG_RESULT"
|
||||
return
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC2059 # the format string is the translated message
|
||||
printf -- "$BO_MSG_RESULT" "$@"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Output
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Decided once: inside $(...) stdout is always a pipe.
|
||||
BO_INTERACTIVE=''
|
||||
[[ -t 1 ]] && BO_INTERACTIVE=1
|
||||
|
||||
if [[ -n $BO_INTERACTIVE && -z ${NO_COLOR:-} ]]; then
|
||||
BO_C_RESET=$'\033[0m'
|
||||
BO_C_BOLD=$'\033[1m'
|
||||
BO_C_DIM=$'\033[2m'
|
||||
BO_C_BLUE=$'\033[38;2;23;147;209m'
|
||||
BO_C_GREEN=$'\033[32m'
|
||||
BO_C_YELLOW=$'\033[33m'
|
||||
BO_C_RED=$'\033[31m'
|
||||
else
|
||||
BO_C_RESET='' BO_C_BOLD='' BO_C_DIM='' BO_C_BLUE=''
|
||||
BO_C_GREEN='' BO_C_YELLOW='' BO_C_RED=''
|
||||
fi
|
||||
|
||||
# Set by bo_bad and bo_note: the menu waits for a key before redrawing.
|
||||
BO_UI_NEEDS_ACK=''
|
||||
|
||||
# --quiet, for the systemd unit. Errors still go to stderr.
|
||||
BO_QUIET=''
|
||||
|
||||
bo_say() { [[ -n $BO_QUIET ]] || printf '%s\n' "$*"; }
|
||||
bo_head() { printf '\n%s%s%s\n\n' "$BO_C_BOLD$BO_C_BLUE" "$*" "$BO_C_RESET"; }
|
||||
bo_ok() { [[ -n $BO_QUIET ]] || printf '%s✔%s %s\n' "$BO_C_GREEN" "$BO_C_RESET" "$*"; }
|
||||
bo_bad() { BO_UI_NEEDS_ACK=1; printf '%s✘%s %s\n' "$BO_C_RED" "$BO_C_RESET" "$*" >&2; }
|
||||
bo_note() { BO_UI_NEEDS_ACK=1; [[ -n $BO_QUIET ]] || printf '%s•%s %s\n' "$BO_C_DIM" "$BO_C_RESET" "$*"; }
|
||||
|
||||
bo_have() { command -v "$1" > /dev/null 2>&1; }
|
||||
|
||||
# "x minutes ago" for a unix timestamp, "never" for none.
|
||||
bo_time_ago() {
|
||||
local ts="$1" now delta
|
||||
|
||||
[[ $ts =~ ^[0-9]+$ ]] || { bo_msg "never"; printf '\n'; return; }
|
||||
|
||||
now="$(date +%s)"
|
||||
delta=$(( now - ts ))
|
||||
(( delta < 0 )) && delta=0
|
||||
|
||||
if (( delta < 60 )); then bo_msg "just now"
|
||||
elif (( delta < 120 )); then bo_msg "1 minute ago"
|
||||
elif (( delta < 3600 )); then bo_msg "%d minutes ago" "$(( delta / 60 ))"
|
||||
elif (( delta < 7200 )); then bo_msg "1 hour ago"
|
||||
elif (( delta < 86400 )); then bo_msg "%d hours ago" "$(( delta / 3600 ))"
|
||||
elif (( delta < 172800 )); then bo_msg "1 day ago"
|
||||
else bo_msg "%d days ago" "$(( delta / 86400 ))"
|
||||
fi
|
||||
printf '\n'
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# State
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
bo_state_read() {
|
||||
local key="$1" default="${2:-}"
|
||||
if [[ -r "$BO_STATEDIR/$key" ]]; then
|
||||
cat "$BO_STATEDIR/$key"
|
||||
else
|
||||
printf '%s\n' "$default"
|
||||
fi
|
||||
}
|
||||
|
||||
bo_state_write() {
|
||||
local key="$1"
|
||||
shift
|
||||
mkdir -p "$BO_STATEDIR" 2>/dev/null || return 1
|
||||
printf '%s\n' "$*" > "$BO_STATEDIR/$key"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The change ledger
|
||||
# ---------------------------------------------------------------------------
|
||||
# Every change, so undoing never touches what this did not write. A set, tab
|
||||
# separated:
|
||||
# file <path> - deleted on undo
|
||||
# default <MIME type> <previous> put back on undo, "-" for none
|
||||
|
||||
bo_ledger_add() {
|
||||
local kind="$1" key="$2" detail="${3:--}"
|
||||
mkdir -p "$BO_STATEDIR" 2>/dev/null || return 1
|
||||
|
||||
# Keep the value from before the first time.
|
||||
if [[ $kind == default ]] && bo_ledger_has default "$key"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
bo_ledger_forget "$kind" "$key"
|
||||
printf '%s\t%s\t%s\n' "$kind" "$key" "$detail" >> "$BO_LEDGER"
|
||||
}
|
||||
|
||||
# bo_ledger_has <kind> <key>
|
||||
bo_ledger_has() {
|
||||
[[ -f $BO_LEDGER ]] || return 1
|
||||
awk -F'\t' -v k="$1" -v p="$2" '$1 == k && $2 == p { found = 1 } END { exit !found }' \
|
||||
"$BO_LEDGER"
|
||||
}
|
||||
|
||||
# bo_ledger_detail <kind> <key>
|
||||
bo_ledger_detail() {
|
||||
[[ -f $BO_LEDGER ]] || return 1
|
||||
awk -F'\t' -v k="$1" -v p="$2" '$1 == k && $2 == p { d = $3; f = 1 } END { if (!f) exit 1; print d }' \
|
||||
"$BO_LEDGER"
|
||||
}
|
||||
|
||||
bo_ledger_forget() {
|
||||
local kind="$1" key="$2" tmp
|
||||
[[ -f $BO_LEDGER ]] || return 0
|
||||
|
||||
tmp="$(mktemp "${BO_LEDGER}.XXXXXX")" || return 1
|
||||
awk -F'\t' -v k="$kind" -v p="$key" '!($1 == k && $2 == p)' "$BO_LEDGER" > "$tmp" 2>/dev/null \
|
||||
&& mv -f "$tmp" "$BO_LEDGER" || rm -f "$tmp"
|
||||
return 0
|
||||
}
|
||||
|
||||
# bo_ledger_list <kind>
|
||||
# Every key of that kind, one per line.
|
||||
bo_ledger_list() {
|
||||
[[ -f $BO_LEDGER ]] || return 0
|
||||
awk -F'\t' -v k="$1" '$1 == k { print $2 }' "$BO_LEDGER"
|
||||
}
|
||||
|
||||
# bo_write_if_changed <path> <content>
|
||||
# 0 written, 1 already so, 2 failed. An unchanged file keeps its timestamp, so
|
||||
# desktops do not rebuild their caches for nothing.
|
||||
bo_write_if_changed() {
|
||||
local path="$1" content="$2" current=''
|
||||
|
||||
# $(...) drops the final newline.
|
||||
[[ $content == *$'\n' ]] || content+=$'\n'
|
||||
|
||||
# Byte for byte: $(< file) would drop trailing newlines.
|
||||
[[ -r $path ]] && IFS= read -rd '' current < "$path"
|
||||
|
||||
[[ $current == "$content" ]] && return 1
|
||||
|
||||
mkdir -p "$(dirname "$path")" 2>/dev/null || return 2
|
||||
printf '%s' "$content" > "$path" || return 2
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
# shellcheck shell=bash
|
||||
#
|
||||
# Reading and writing ~/.config/bottles-opener/config and .../types.
|
||||
# Written by the interface, never by hand. Parsed, not sourced.
|
||||
|
||||
BO_CONFIG_CACHE=''
|
||||
BO_CONFIG_CACHED=0
|
||||
|
||||
_bo_config_slurp() {
|
||||
(( BO_CONFIG_CACHED )) && return 0
|
||||
BO_CONFIG_CACHE=''
|
||||
[[ -r $BO_CONFIG ]] && BO_CONFIG_CACHE="$(< "$BO_CONFIG")"
|
||||
BO_CONFIG_CACHED=1
|
||||
return 0
|
||||
}
|
||||
|
||||
# _bo_config_lookup <Key> [default]
|
||||
# Into BO_CONFIG_VALUE, without a fork: the settings screen reads every key per
|
||||
# frame.
|
||||
BO_CONFIG_VALUE=''
|
||||
|
||||
_bo_config_lookup() {
|
||||
local key="$1" default="${2:-}" val='' line
|
||||
|
||||
BO_CONFIG_VALUE="$default"
|
||||
[[ -r $BO_CONFIG ]] || return 0
|
||||
_bo_config_slurp
|
||||
|
||||
while IFS= read -r line; do
|
||||
[[ $line == *"$key"* ]] || continue
|
||||
[[ $line =~ ^[[:space:]]*"$key"[[:space:]]*=(.*)$ ]] || continue
|
||||
val="${BASH_REMATCH[1]}"
|
||||
done <<< "$BO_CONFIG_CACHE"
|
||||
|
||||
val="${val%%#*}"
|
||||
val="${val#"${val%%[![:space:]]*}"}"
|
||||
val="${val%"${val##*[![:space:]]}"}"
|
||||
val="${val%\"}"
|
||||
val="${val#\"}"
|
||||
|
||||
[[ -n $val ]] && BO_CONFIG_VALUE="$val"
|
||||
return 0
|
||||
}
|
||||
|
||||
# bo_config_get <Key> [default]
|
||||
bo_config_get() {
|
||||
_bo_config_lookup "$@"
|
||||
printf '%s\n' "$BO_CONFIG_VALUE"
|
||||
}
|
||||
|
||||
_bo_is_true() {
|
||||
case "${1,,}" in
|
||||
yes|y|true|1|on|enabled) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# bo_config_bool <Key> <default: yes|no>
|
||||
bo_config_bool() {
|
||||
_bo_config_lookup "$1" "$2"
|
||||
_bo_is_true "$BO_CONFIG_VALUE"
|
||||
}
|
||||
|
||||
# bo_config_set <Key> <Value>
|
||||
bo_config_set() {
|
||||
local key="$1" value="$2" tmp
|
||||
|
||||
if [[ ! -e $BO_CONFIG ]]; then
|
||||
mkdir -p "$(dirname "$BO_CONFIG")" || return 1
|
||||
{
|
||||
printf '# %s\n' "$BO_PRETTY"
|
||||
printf '#\n'
|
||||
printf '# Written by `%s`. Nothing here needs editing by hand -\n' "$BO_NAME"
|
||||
printf '# every option is reachable from `%s` itself.\n' "$BO_NAME"
|
||||
} > "$BO_CONFIG" || return 1
|
||||
fi
|
||||
[[ -w $BO_CONFIG ]] || return 1
|
||||
|
||||
tmp="$(mktemp "${BO_CONFIG}.XXXXXX")" || return 1
|
||||
chmod --reference="$BO_CONFIG" "$tmp" 2>/dev/null || chmod 0644 "$tmp"
|
||||
|
||||
if grep -qE "^[[:space:]]*#?[[:space:]]*${key}[[:space:]]*=" "$BO_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 }
|
||||
' "$BO_CONFIG" > "$tmp" || { rm -f "$tmp"; return 1; }
|
||||
else
|
||||
cat "$BO_CONFIG" > "$tmp" || { rm -f "$tmp"; return 1; }
|
||||
printf '%s=%s\n' "$key" "$value" >> "$tmp"
|
||||
fi
|
||||
|
||||
mv -f "$tmp" "$BO_CONFIG"
|
||||
BO_CONFIG_CACHED=0
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Resolved settings
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
bo_config_load() {
|
||||
CFG_ENABLED=no; bo_config_bool Enabled no && CFG_ENABLED=yes
|
||||
CFG_DETECT=no; bo_config_bool DetectKnown yes && CFG_DETECT=yes
|
||||
CFG_ICONS=no; bo_config_bool FileIcons yes && CFG_ICONS=yes
|
||||
CFG_WATCH=no; bo_config_bool WatchNewPrograms yes && CFG_WATCH=yes
|
||||
bo_types_load
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The types file
|
||||
# ---------------------------------------------------------------------------
|
||||
# Types changed by hand. Detected ones have no line: they follow detection.
|
||||
# Tab separated, no field empty:
|
||||
# <extension> <default|openwith|off> <install> <program id> <bottle name>
|
||||
# The program is found by install and id; the bottle name is for reading.
|
||||
|
||||
BO_T_EXT=()
|
||||
BO_T_STATE=()
|
||||
BO_T_INSTALL=()
|
||||
BO_T_PID=()
|
||||
BO_T_BOTTLE=()
|
||||
|
||||
bo_types_load() {
|
||||
local ext state install pid bottle
|
||||
|
||||
BO_T_EXT=(); BO_T_STATE=(); BO_T_INSTALL=(); BO_T_PID=(); BO_T_BOTTLE=()
|
||||
[[ -r $BO_TYPES ]] || return 0
|
||||
|
||||
while IFS=$'\t' read -r ext state install pid bottle; do
|
||||
[[ -z $ext || $ext == '#'* || -z $pid ]] && continue
|
||||
case "$state" in default|openwith|off) ;; *) continue ;; esac
|
||||
BO_T_EXT+=("$ext"); BO_T_STATE+=("$state"); BO_T_INSTALL+=("$install")
|
||||
BO_T_PID+=("$pid"); BO_T_BOTTLE+=("$bottle")
|
||||
done < "$BO_TYPES"
|
||||
}
|
||||
|
||||
# bo_types_set <ext> <state|remove> <install> <program id> <bottle name>
|
||||
# Replaces the line for that extension and program, or takes it out.
|
||||
bo_types_set() {
|
||||
local ext="$1" state="$2" install="$3" pid="$4" bottle="$5" tmp
|
||||
|
||||
mkdir -p "$(dirname "$BO_TYPES")" || return 1
|
||||
if [[ ! -e $BO_TYPES ]]; then
|
||||
{
|
||||
printf '# %s - file types changed by hand\n' "$BO_PRETTY"
|
||||
printf '#\n'
|
||||
printf '# Written by `%s`. Nothing here needs editing by hand.\n' "$BO_NAME"
|
||||
} > "$BO_TYPES" || return 1
|
||||
fi
|
||||
|
||||
tmp="$(mktemp "${BO_TYPES}.XXXXXX")" || return 1
|
||||
awk -F'\t' -v e="$ext" -v i="$install" -v p="$pid" \
|
||||
'!($1 == e && $3 == i && $4 == p)' "$BO_TYPES" > "$tmp" \
|
||||
|| { rm -f "$tmp"; return 1; }
|
||||
|
||||
if [[ $state != remove ]]; then
|
||||
printf '%s\t%s\t%s\t%s\t%s\n' "$ext" "$state" "$install" "$pid" "$bottle" >> "$tmp"
|
||||
fi
|
||||
|
||||
mv -f "$tmp" "$BO_TYPES"
|
||||
bo_types_load
|
||||
}
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Writes one icon of a Windows program as PNG to stdout - the largest image of
|
||||
# it, the way Explorer would pick it for a big view.
|
||||
#
|
||||
# Usage: icon.py <file> [index]
|
||||
# <file> an .exe or .dll, an .ico, or a .png
|
||||
# [index] Windows' DefaultIcon index: n >= 0 is the n-th icon, -n the icon
|
||||
# with resource id n
|
||||
#
|
||||
# Needs icoextract for executables, which Bottles depends on. Exit codes:
|
||||
# 3 icoextract missing, 4 no such icon.
|
||||
#
|
||||
# Copyright (C) 2026 Felitendo
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import struct
|
||||
import sys
|
||||
import zlib
|
||||
|
||||
PNG_SIG = b"\x89PNG\r\n\x1a\n"
|
||||
|
||||
|
||||
def png_encode(width, height, rgba):
|
||||
def chunk(kind, data):
|
||||
return (
|
||||
struct.pack(">I", len(data)) + kind + data
|
||||
+ struct.pack(">I", zlib.crc32(kind + data) & 0xFFFFFFFF)
|
||||
)
|
||||
|
||||
stride = width * 4
|
||||
raw = b"".join(b"\0" + rgba[y * stride:(y + 1) * stride] for y in range(height))
|
||||
return (
|
||||
PNG_SIG
|
||||
+ chunk(b"IHDR", struct.pack(">IIBBBBB", width, height, 8, 6, 0, 0, 0))
|
||||
+ chunk(b"IDAT", zlib.compress(raw, 9))
|
||||
+ chunk(b"IEND", b"")
|
||||
)
|
||||
|
||||
|
||||
def dib_to_png(data, width, height):
|
||||
"""An icon's bitmap image: BITMAPINFOHEADER, palette, colour, AND mask."""
|
||||
(hdr, w, h2, _planes, bpp, compression) = struct.unpack_from("<IiiHHI", data, 0)
|
||||
if compression != 0 or bpp not in (1, 4, 8, 24, 32):
|
||||
raise ValueError("unsupported bitmap")
|
||||
width = width or w
|
||||
height = height or abs(h2) // 2
|
||||
|
||||
colours = struct.unpack_from("<I", data, 32)[0] or (1 << bpp if bpp <= 8 else 0)
|
||||
palette_at = hdr
|
||||
pixels_at = palette_at + colours * 4
|
||||
stride = ((width * bpp + 31) // 32) * 4
|
||||
mask_at = pixels_at + stride * height
|
||||
mask_stride = ((width + 31) // 32) * 4
|
||||
has_mask = len(data) >= mask_at + mask_stride * height
|
||||
|
||||
out = bytearray(width * height * 4)
|
||||
any_alpha = False
|
||||
|
||||
for y in range(height):
|
||||
row = pixels_at + (height - 1 - y) * stride
|
||||
for x in range(width):
|
||||
if bpp == 32:
|
||||
b, g, r, a = data[row + x * 4:row + x * 4 + 4]
|
||||
any_alpha = any_alpha or a != 0
|
||||
elif bpp == 24:
|
||||
b, g, r = data[row + x * 3:row + x * 3 + 3]
|
||||
a = 255
|
||||
else:
|
||||
bit = x * bpp
|
||||
byte = data[row + bit // 8]
|
||||
index = (byte >> (8 - bpp - bit % 8)) & ((1 << bpp) - 1)
|
||||
b, g, r = data[palette_at + index * 4:palette_at + index * 4 + 3]
|
||||
a = 255
|
||||
o = (y * width + x) * 4
|
||||
out[o:o + 4] = bytes((r, g, b, a))
|
||||
|
||||
# Without alpha of its own the AND mask says what is transparent.
|
||||
if has_mask and not (bpp == 32 and any_alpha):
|
||||
for y in range(height):
|
||||
row = mask_at + (height - 1 - y) * mask_stride
|
||||
for x in range(width):
|
||||
if data[row + x // 8] & (0x80 >> (x % 8)):
|
||||
out[(y * width + x) * 4 + 3] = 0
|
||||
|
||||
return png_encode(width, height, bytes(out))
|
||||
|
||||
|
||||
def ico_to_png(ico):
|
||||
reserved, kind, count = struct.unpack_from("<HHH", ico, 0)
|
||||
if reserved != 0 or kind != 1 or count == 0:
|
||||
raise ValueError("not an icon")
|
||||
|
||||
entries = []
|
||||
for i in range(count):
|
||||
w, h, _c, _r, _p, bpp, size, offset = struct.unpack_from("<BBBBHHII", ico, 6 + 16 * i)
|
||||
data = ico[offset:offset + size]
|
||||
if data[:8] == PNG_SIG:
|
||||
w = struct.unpack_from(">I", data, 16)[0]
|
||||
h = struct.unpack_from(">I", data, 20)[0]
|
||||
bpp = bpp or 32
|
||||
entries.append((w or 256, h or 256, bpp or 32, data))
|
||||
|
||||
# Largest first, then the most colours.
|
||||
for w, h, _bpp, data in sorted(entries, key=lambda e: (e[0], e[2]), reverse=True):
|
||||
try:
|
||||
return data if data[:8] == PNG_SIG else dib_to_png(data, w, h)
|
||||
except (ValueError, struct.error, IndexError):
|
||||
continue
|
||||
raise ValueError("no usable image")
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
return 2
|
||||
path = sys.argv[1]
|
||||
index = int(sys.argv[2]) if len(sys.argv) > 2 and sys.argv[2] else 0
|
||||
|
||||
try:
|
||||
with open(path, "rb") as f:
|
||||
head = f.read(8)
|
||||
except OSError:
|
||||
return 4
|
||||
|
||||
try:
|
||||
if head == PNG_SIG:
|
||||
with open(path, "rb") as f:
|
||||
png = f.read()
|
||||
elif head[:4] == b"\0\0\1\0":
|
||||
with open(path, "rb") as f:
|
||||
png = ico_to_png(f.read())
|
||||
else:
|
||||
try:
|
||||
import icoextract
|
||||
except ImportError:
|
||||
return 3
|
||||
extractor = icoextract.IconExtractor(path)
|
||||
if index < 0:
|
||||
ico = extractor.get_icon(resource_id=-index)
|
||||
else:
|
||||
ico = extractor.get_icon(num=index)
|
||||
png = ico_to_png(ico.getvalue())
|
||||
except Exception: # anything a broken or unusual file can throw
|
||||
return 4
|
||||
|
||||
sys.stdout.buffer.write(png)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,220 @@
|
||||
# shellcheck shell=bash
|
||||
#
|
||||
# Programs whose file types are known without asking.
|
||||
#
|
||||
# The bottle's registry is asked first (scan.py): whatever an installer
|
||||
# registered there is what Windows would open. This table is for programs
|
||||
# that registered nothing - portable installs, copied folders.
|
||||
|
||||
# Format: <pattern>|<ext>[=<switch>] ...
|
||||
#
|
||||
# The pattern matches the program's Windows path, lowercased, with / for \.
|
||||
# A switch goes in front of the file, as in `OUTLOOK.EXE /f "file.msg"`.
|
||||
BO_KNOWN_PROGRAMS=(
|
||||
# Microsoft Office
|
||||
"*/winword.exe|doc docx docm dot dotx dotm rtf odt"
|
||||
"*/excel.exe|xls xlsx xlsm xlsb xlt xltx xltm xlam xla csv ods"
|
||||
"*/powerpnt.exe|ppt pptx pptm pot potx potm pps=/s ppsx=/s ppsm=/s ppa ppam odp"
|
||||
"*/outlook.exe|msg=/f oft=/t eml=/eml vcf=/v ics=/ical vcs=/vcal"
|
||||
"*/msaccess.exe|accdb accde accdr accdt mdb mde"
|
||||
"*/mspub.exe|pub"
|
||||
"*/visio.exe|vsd vsdx vsdm vss vssx vssm vst vstx vstm"
|
||||
"*/winproj.exe|mpp mpt"
|
||||
"*/onenote.exe|one onepkg onetoc2"
|
||||
|
||||
# Music
|
||||
"*/fl64.exe|flp fsc fst"
|
||||
"*/fl.exe|flp fsc fst"
|
||||
"*/ableton live *.exe|als alc adg"
|
||||
"*/cubase*.exe|cpr"
|
||||
"*/nuendo*.exe|npr"
|
||||
"*/studio one.exe|song"
|
||||
"*/reaper.exe|rpp"
|
||||
"*/reason.exe|reason"
|
||||
"*/adobe audition.exe|sesx"
|
||||
"*/guitarpro*.exe|gp gpx gp5 gp4 gp3"
|
||||
"*/sibelius.exe|sib"
|
||||
"*/finale.exe|musx mus"
|
||||
"*/foobar2000.exe|mp3 flac ogg opus m4a wav ape wv"
|
||||
|
||||
# Images and design
|
||||
"*/photoshop.exe|psd psb"
|
||||
"*/photoshopelementseditor.exe|psd pse"
|
||||
"*/illustrator.exe|ai"
|
||||
"*/indesign.exe|indd indt idml"
|
||||
"*/lightroom.exe|lrcat"
|
||||
"*/animate.exe|fla"
|
||||
"*/affinity*/photo*.exe|afphoto"
|
||||
"*/affinity*/designer*.exe|afdesign"
|
||||
"*/affinity*/publisher*.exe|afpub"
|
||||
"*/clipstudiopaint.exe|clip"
|
||||
"*/coreldrw.exe|cdr"
|
||||
"*/paintdotnet.exe|pdn"
|
||||
"*/sai.exe|sai"
|
||||
"*/sai2.exe|sai2"
|
||||
|
||||
# 3D and CAD
|
||||
"*/sketchup.exe|skp"
|
||||
"*/acad.exe|dwg dxf dwt"
|
||||
"*/acadlt.exe|dwg dxf dwt"
|
||||
"*/rhino.exe|3dm"
|
||||
"*/3dsmax.exe|max"
|
||||
"*/sldworks.exe|sldprt sldasm slddrw"
|
||||
"*/x2.exe|pcbdoc schdoc prjpcb"
|
||||
|
||||
# Video
|
||||
"*/afterfx.exe|aep"
|
||||
"*/adobe premiere pro.exe|prproj"
|
||||
"*/vegas*.exe|veg"
|
||||
|
||||
# Everything else
|
||||
"*/acrobat.exe|pdf"
|
||||
"*/acrord32.exe|pdf"
|
||||
"*/notepad++.exe|txt log"
|
||||
"*/7zfm.exe|7z zip rar"
|
||||
"*/winrar.exe|rar zip 7z"
|
||||
)
|
||||
|
||||
# Types for extensions the system does not know.
|
||||
#
|
||||
# Format: <ext>|<MIME type>|<description>|<magic>|<generic icon>|<flags>
|
||||
#
|
||||
# <magic>: string:<text> or big32:<hex>, matched at offset 0.
|
||||
# <flags>: "own" defines the type even where the system has the extension
|
||||
# already, as something else - .mpp is Musepack audio there, not a Project
|
||||
# file. The magic tells the two apart.
|
||||
BO_KNOWN_TYPES=(
|
||||
# Office
|
||||
"ppa|application/x-ms-powerpoint-addin|PowerPoint add-in||x-office-presentation|"
|
||||
"msg|application/vnd.ms-outlook|Outlook message||x-office-document|"
|
||||
"oft|application/x-ms-outlook-template|Outlook template||x-office-document|"
|
||||
"accdb|application/x-ms-access-database|Access database||x-office-spreadsheet|"
|
||||
"accde|application/x-ms-access-database|Access database||x-office-spreadsheet|"
|
||||
"accdr|application/x-ms-access-database|Access database||x-office-spreadsheet|"
|
||||
"accdt|application/x-ms-access-template|Access template||x-office-spreadsheet|"
|
||||
"mde|application/x-ms-access-database|Access database||x-office-spreadsheet|"
|
||||
"mpp|application/vnd.ms-project|Project file|big32:0xd0cf11e0|x-office-spreadsheet|own"
|
||||
"mpt|application/x-ms-project-template|Project template||x-office-spreadsheet|"
|
||||
"one|application/onenote|OneNote section||x-office-document|"
|
||||
"onepkg|application/x-onenote-package|OneNote package||x-office-document|"
|
||||
"onetoc2|application/x-onenote-toc|OneNote table of contents||x-office-document|"
|
||||
|
||||
# Music
|
||||
"flp|application/x-flstudio-project|FL Studio project|string:FLhd|audio-x-generic|"
|
||||
"fsc|application/x-flstudio-score|FL Studio score||audio-x-generic|"
|
||||
"fst|application/x-flstudio-preset|FL Studio preset|string:FLhd|audio-x-generic|own"
|
||||
"als|application/x-ableton-live-set|Ableton Live set||audio-x-generic|"
|
||||
"alc|application/x-ableton-live-clip|Ableton Live clip||audio-x-generic|"
|
||||
"adg|application/x-ableton-device-group|Ableton device group||audio-x-generic|"
|
||||
"cpr|application/x-cubase-project|Cubase project||audio-x-generic|"
|
||||
"npr|application/x-nuendo-project|Nuendo project||audio-x-generic|"
|
||||
"song|application/x-studio-one-song|Studio One song||audio-x-generic|"
|
||||
"rpp|application/x-reaper-project|REAPER project|string:<REAPER_PROJECT|audio-x-generic|"
|
||||
"reason|application/x-reason-song|Reason song||audio-x-generic|"
|
||||
"sesx|application/x-audition-session|Audition session||audio-x-generic|"
|
||||
"gp|application/x-guitar-pro|Guitar Pro tablature|big32:0x504b0304|audio-x-generic|own"
|
||||
"gpx|application/x-guitar-pro|Guitar Pro tablature|string:BCFS|audio-x-generic|own"
|
||||
"gp5|application/x-guitar-pro|Guitar Pro tablature||audio-x-generic|"
|
||||
"gp4|application/x-guitar-pro|Guitar Pro tablature||audio-x-generic|"
|
||||
"gp3|application/x-guitar-pro|Guitar Pro tablature||audio-x-generic|"
|
||||
"sib|application/x-sibelius-score|Sibelius score||audio-x-generic|"
|
||||
"musx|application/x-finale-score|Finale score||audio-x-generic|"
|
||||
"mus|application/x-finale-score|Finale score||audio-x-generic|"
|
||||
|
||||
# Images and design
|
||||
"psb|application/x-photoshop-large-document|Photoshop large document||image-x-generic|"
|
||||
"pse|application/x-photoshop-elements|Photoshop Elements document||image-x-generic|"
|
||||
"indd|application/x-indesign-document|InDesign document||x-office-document|"
|
||||
"indt|application/x-indesign-template|InDesign template||x-office-document|"
|
||||
"idml|application/vnd.adobe.indesign-idml-package|InDesign markup||x-office-document|"
|
||||
"lrcat|application/x-lightroom-catalog|Lightroom catalog||image-x-generic|"
|
||||
"fla|application/x-adobe-animate|Animate document||video-x-generic|"
|
||||
"afphoto|application/x-affinity-photo|Affinity Photo document||image-x-generic|"
|
||||
"afdesign|application/x-affinity-designer|Affinity Designer document||image-x-generic|"
|
||||
"afpub|application/x-affinity-publisher|Affinity Publisher document||x-office-document|"
|
||||
"clip|application/x-clip-studio-paint|Clip Studio Paint file||image-x-generic|"
|
||||
"pdn|application/x-paintdotnet|Paint.NET image||image-x-generic|"
|
||||
"sai|application/x-paint-tool-sai|PaintTool SAI document||image-x-generic|"
|
||||
"sai2|application/x-paint-tool-sai2|PaintTool SAI document||image-x-generic|"
|
||||
|
||||
# 3D and CAD
|
||||
"skp|application/vnd.sketchup.skp|SketchUp model||x-office-drawing|"
|
||||
"dwt|application/x-autocad-template|AutoCAD template||x-office-drawing|"
|
||||
"3dm|application/x-rhino-model|Rhino model||x-office-drawing|"
|
||||
"max|application/x-3ds-max-scene|3ds Max scene||x-office-drawing|"
|
||||
"sldprt|application/x-solidworks-part|SolidWorks part||x-office-drawing|"
|
||||
"sldasm|application/x-solidworks-assembly|SolidWorks assembly||x-office-drawing|"
|
||||
"slddrw|application/x-solidworks-drawing|SolidWorks drawing||x-office-drawing|"
|
||||
"pcbdoc|application/x-altium-pcb|Altium PCB||x-office-drawing|"
|
||||
"schdoc|application/x-altium-schematic|Altium schematic||x-office-drawing|"
|
||||
"prjpcb|application/x-altium-project|Altium project||x-office-drawing|"
|
||||
|
||||
# Video
|
||||
"aep|application/x-after-effects-project|After Effects project||video-x-generic|"
|
||||
"prproj|application/x-premiere-project|Premiere Pro project||video-x-generic|"
|
||||
"veg|application/x-vegas-project|VEGAS project||video-x-generic|"
|
||||
)
|
||||
|
||||
# bo_known_exts <windows path>
|
||||
# BO_KNOWN: the extensions, space separated. BO_KNOWN_ARGS[ext]: the Windows
|
||||
# argument template for those that need a switch.
|
||||
BO_KNOWN=''
|
||||
declare -A BO_KNOWN_ARGS=()
|
||||
|
||||
bo_known_exts() {
|
||||
local path="${1,,}" rule pattern entry ext
|
||||
path="${path//\\//}"
|
||||
BO_KNOWN=''
|
||||
BO_KNOWN_ARGS=()
|
||||
|
||||
for rule in "${BO_KNOWN_PROGRAMS[@]}"; do
|
||||
pattern="${rule%%|*}"
|
||||
# shellcheck disable=SC2053 # the right-hand side is a pattern on purpose
|
||||
[[ $path == $pattern ]] || continue
|
||||
for entry in ${rule#*|}; do
|
||||
ext="${entry%%=*}"
|
||||
[[ " $BO_KNOWN " == *" $ext "* ]] && continue
|
||||
BO_KNOWN="${BO_KNOWN:+$BO_KNOWN }$ext"
|
||||
[[ $entry == *=* ]] && BO_KNOWN_ARGS[$ext]="${entry#*=} \"%1\""
|
||||
done
|
||||
done
|
||||
[[ -n $BO_KNOWN ]]
|
||||
}
|
||||
|
||||
# bo_known_type <extension>
|
||||
# BO_KT_MIME, _DESC, _MAGIC, _ICON and _OWN for an extension. Anything not in
|
||||
# the table gets a type named after the extension and this program, so it
|
||||
# cannot collide with a real one. Returns 1 for those.
|
||||
BO_KT_MIME=''
|
||||
BO_KT_DESC=''
|
||||
BO_KT_MAGIC=''
|
||||
BO_KT_ICON=''
|
||||
BO_KT_OWN=0
|
||||
|
||||
declare -A BO_KT_INDEX=()
|
||||
|
||||
bo_known_type() {
|
||||
local ext="$1" entry e mime desc magic icon flags
|
||||
|
||||
if (( ${#BO_KT_INDEX[@]} == 0 )); then
|
||||
for entry in "${BO_KNOWN_TYPES[@]}"; do
|
||||
BO_KT_INDEX[${entry%%|*}]="$entry"
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ -n ${BO_KT_INDEX[$ext]:-} ]]; then
|
||||
IFS='|' read -r e mime desc magic icon flags <<< "${BO_KT_INDEX[$ext]}"
|
||||
BO_KT_MIME="$mime"; BO_KT_DESC="$desc"
|
||||
BO_KT_MAGIC="$magic"; BO_KT_ICON="$icon"
|
||||
BO_KT_OWN=0
|
||||
[[ $flags == *own* ]] && BO_KT_OWN=1
|
||||
return 0
|
||||
fi
|
||||
|
||||
BO_KT_MIME="application/x-bottles-opener-${ext}"
|
||||
BO_KT_DESC="${ext^^} file"
|
||||
BO_KT_MAGIC=''
|
||||
BO_KT_ICON='application-x-generic'
|
||||
BO_KT_OWN=0
|
||||
return 1
|
||||
}
|
||||
+704
@@ -0,0 +1,704 @@
|
||||
# shellcheck shell=bash
|
||||
#
|
||||
# The interactive front end.
|
||||
|
||||
# Non-canonical mode for the whole interface: between two `read -sn1` bash
|
||||
# goes back to canonical mode, where a backspace gets eaten.
|
||||
BO_TERM_SAVED=''
|
||||
|
||||
bo_ui_term_raw() {
|
||||
bo_have stty || return 0
|
||||
[[ -t 0 ]] || return 0
|
||||
[[ -n $BO_TERM_SAVED ]] && return 0
|
||||
|
||||
BO_TERM_SAVED="$(stty -g 2>/dev/null)" || { BO_TERM_SAVED=''; return 0; }
|
||||
stty -icanon -echo min 1 time 0 2>/dev/null || true
|
||||
}
|
||||
|
||||
bo_ui_term_restore() {
|
||||
[[ -n $BO_TERM_SAVED ]] || return 0
|
||||
stty "$BO_TERM_SAVED" 2>/dev/null || true
|
||||
BO_TERM_SAVED=''
|
||||
}
|
||||
|
||||
# Runs an action in normal line mode.
|
||||
bo_ui_cooked() {
|
||||
bo_ui_term_restore
|
||||
"$@"
|
||||
local rc=$?
|
||||
bo_ui_term_raw
|
||||
return $rc
|
||||
}
|
||||
|
||||
# bo_read_key
|
||||
# One keypress as a name. Escape sequences are read whole.
|
||||
bo_read_key() {
|
||||
local k rest tail
|
||||
|
||||
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' ;;
|
||||
'[3')
|
||||
IFS= read -rsn1 -t 0.05 tail || true
|
||||
printf 'delete\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
|
||||
}
|
||||
|
||||
# bo_ui_read_line <initial>
|
||||
# A line editor into BO_LINE_RESULT. `read -r` would break the following
|
||||
# `read -sn1`.
|
||||
BO_LINE_RESULT=''
|
||||
|
||||
bo_ui_read_line() {
|
||||
local buf="${1:-}" key
|
||||
|
||||
BO_LINE_RESULT=''
|
||||
printf '%s' "$buf"
|
||||
|
||||
while true; do
|
||||
key="$(bo_read_key)" || { printf '\n'; return 1; }
|
||||
|
||||
case "$key" in
|
||||
enter)
|
||||
printf '\n'
|
||||
BO_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|delete) ;;
|
||||
*)
|
||||
[[ ${#key} -eq 1 ]] || continue
|
||||
buf+="$key"
|
||||
printf '%s' "$key"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
bo_pause() {
|
||||
printf '\n %s' "$(bo_msg "Press any key to continue...")"
|
||||
read -rsn1 _ || true
|
||||
printf '\n'
|
||||
}
|
||||
|
||||
# _bo_row <label> <value>
|
||||
# Padded by characters: printf pads by bytes, which "ü" gets wrong.
|
||||
_bo_row() {
|
||||
local label="$1" value="$2" pad
|
||||
pad=$(( 30 - ${#label} ))
|
||||
(( pad < 0 )) && pad=0
|
||||
printf ' %s%*s %s\n' "$label" "$pad" '' "$value"
|
||||
}
|
||||
|
||||
_bo_onoff() {
|
||||
if [[ $1 == yes ]]; then
|
||||
printf '%s%s%s' "$BO_C_GREEN" "$(bo_msg "ON")" "$BO_C_RESET"
|
||||
else
|
||||
printf '%s%s%s' "$BO_C_DIM" "$(bo_msg "OFF")" "$BO_C_RESET"
|
||||
fi
|
||||
}
|
||||
|
||||
BO_CLEARSEQ=''
|
||||
|
||||
_bo_clearseq() {
|
||||
[[ -n $BO_CLEARSEQ ]] && return 0
|
||||
BO_CLEARSEQ="$(clear 2>/dev/null)" || BO_CLEARSEQ=$'\033[H\033[2J'
|
||||
}
|
||||
|
||||
# _bo_term_rows [reserved]
|
||||
# How many list rows fit, in BO_ROWS.
|
||||
BO_ROWS=20
|
||||
|
||||
_bo_term_rows() {
|
||||
local size
|
||||
size="$(stty size 2>/dev/null)" || size='24 80'
|
||||
BO_ROWS=$(( ${size%% *} - ${1:-8} ))
|
||||
(( BO_ROWS < 5 )) && BO_ROWS=5
|
||||
}
|
||||
|
||||
# _bo_scroll <cursor> <top>
|
||||
# The first row to show so the cursor is in view, in BO_TOP.
|
||||
BO_TOP=0
|
||||
|
||||
_bo_scroll() {
|
||||
BO_TOP=$2
|
||||
(( $1 < BO_TOP )) && BO_TOP=$1
|
||||
(( $1 >= BO_TOP + BO_ROWS )) && BO_TOP=$(( $1 - BO_ROWS + 1 ))
|
||||
(( BO_TOP < 0 )) && BO_TOP=0
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Status
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# bo_ui_status
|
||||
# For `status` and the menu header, from a fresh scan.
|
||||
bo_ui_status() {
|
||||
local last exts nb=0 np install where=''
|
||||
|
||||
bo_assoc_compute
|
||||
bo_assoc_counts
|
||||
last="$(bo_state_read last_apply '')"
|
||||
|
||||
_bo_row "$(bo_msg "File opening")" "$(_bo_onoff "$CFG_ENABLED")"
|
||||
printf '\n'
|
||||
|
||||
for install in "${BO_INSTALLS[@]}"; do
|
||||
case "$install" in
|
||||
native) where+="${where:+, }$(bo_msg "package")" ;;
|
||||
flatpak) where+="${where:+, }Flatpak" ;;
|
||||
esac
|
||||
done
|
||||
if [[ -z $where ]]; then
|
||||
_bo_row "Bottles" "${BO_C_YELLOW}$(bo_msg "not found")${BO_C_RESET}"
|
||||
elif [[ -n $BO_SCAN_ERROR ]]; then
|
||||
_bo_row "Bottles" \
|
||||
"${BO_C_RED}$(bo_msg "could not be read - is python3 with PyYAML installed?")${BO_C_RESET}"
|
||||
else
|
||||
_bo_row "Bottles" "$where"
|
||||
fi
|
||||
|
||||
nb=${#BO_B_NAME[@]}
|
||||
np=${#BO_P_ID[@]}
|
||||
if (( nb == 1 )); then
|
||||
_bo_row "$(bo_msg "Programs found")" "$(bo_msg "%d in 1 bottle" "$np")"
|
||||
else
|
||||
_bo_row "$(bo_msg "Programs found")" "$(bo_msg "%d in %d bottles" "$np" "$nb")"
|
||||
fi
|
||||
|
||||
exts="$(bo_assoc_active_exts 10)"
|
||||
if (( BO_N_ACTIVE )); then
|
||||
_bo_row "$(bo_msg "File types")" "$exts"
|
||||
else
|
||||
_bo_row "$(bo_msg "File types")" "${BO_C_DIM}$(bo_msg "none yet - add one under File types")${BO_C_RESET}"
|
||||
fi
|
||||
|
||||
if bo_watch_available; then
|
||||
if bo_watch_enabled; then
|
||||
_bo_row "$(bo_msg "New programs")" "$(_bo_onoff yes)"
|
||||
else
|
||||
_bo_row "$(bo_msg "New programs")" "$(_bo_onoff no)"
|
||||
fi
|
||||
fi
|
||||
|
||||
_bo_row "$(bo_msg "Last applied")" "$(bo_time_ago "$last")"
|
||||
|
||||
if [[ $CFG_ENABLED == yes ]] && (( BO_N_ACTIVE )); then
|
||||
printf '\n %s%s%s\n' "$BO_C_DIM" \
|
||||
"$(bo_msg "Double-click one of these files and it opens in its program.")" \
|
||||
"$BO_C_RESET"
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Settings
|
||||
# ---------------------------------------------------------------------------
|
||||
# Format: Key|default|label-msgid
|
||||
BO_SETTINGS=(
|
||||
"DetectKnown|yes|Find the file types of installed programs"
|
||||
"FileIcons|yes|Give files the icon of their program"
|
||||
"WatchNewPrograms|yes|Pick up newly installed programs"
|
||||
)
|
||||
|
||||
# bo_ui_settings
|
||||
# Returns 0 when something changed.
|
||||
bo_ui_settings() {
|
||||
local count=${#BO_SETTINGS[@]}
|
||||
local -a names=() defaults=() labels=() values=()
|
||||
local spec name default label i key frame row pad dirty=1 cursor=0 touched=0
|
||||
local l_on l_off title hint shown
|
||||
|
||||
bo_msg_into "ON"; l_on="$BO_MSG_RESULT"
|
||||
bo_msg_into "OFF"; l_off="$BO_MSG_RESULT"
|
||||
|
||||
for spec in "${BO_SETTINGS[@]}"; do
|
||||
IFS='|' read -r name default label <<< "$spec"
|
||||
names+=("$name"); defaults+=("$default")
|
||||
bo_msg_into "$label"
|
||||
labels+=("$BO_MSG_RESULT")
|
||||
done
|
||||
|
||||
bo_msg_into "Settings"; title="$BO_MSG_RESULT"
|
||||
bo_msg_into "Up/Down select - Space changes - q goes back"
|
||||
hint="$BO_MSG_RESULT"
|
||||
|
||||
_bo_clearseq
|
||||
|
||||
while true; do
|
||||
if (( dirty )); then
|
||||
for i in "${!names[@]}"; do
|
||||
_bo_config_lookup "${names[i]}" "${defaults[i]}"
|
||||
values[i]="$BO_CONFIG_VALUE"
|
||||
done
|
||||
dirty=0
|
||||
fi
|
||||
|
||||
frame="$BO_CLEARSEQ"$'\n'"${BO_C_BOLD}${BO_C_BLUE} ${title}${BO_C_RESET}"$'\n\n'
|
||||
|
||||
local marker selected="${BO_C_BLUE}▸${BO_C_RESET} "
|
||||
for i in "${!names[@]}"; do
|
||||
if _bo_is_true "${values[i]}"; then
|
||||
shown="${BO_C_GREEN}${l_on}${BO_C_RESET}"
|
||||
else
|
||||
shown="${BO_C_DIM}${l_off}${BO_C_RESET}"
|
||||
fi
|
||||
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" '' "$shown"
|
||||
frame+="$row"$'\n'
|
||||
done
|
||||
|
||||
frame+=$'\n'" ${BO_C_DIM}${hint}${BO_C_RESET}"$'\n'
|
||||
printf '%s' "$frame"
|
||||
|
||||
key="$(bo_read_key)" || return "$(( ! touched ))"
|
||||
|
||||
case "$key" in
|
||||
up|k) cursor=$(( (cursor - 1 + count) % count )) ;;
|
||||
down|j) cursor=$(( (cursor + 1) % count )) ;;
|
||||
space|enter|right|l|left|h)
|
||||
if _bo_is_true "${values[cursor]}"; then
|
||||
bo_config_set "${names[cursor]}" no
|
||||
else
|
||||
bo_config_set "${names[cursor]}" yes
|
||||
fi || { bo_bad "$(bo_msg "Could not save the setting.")"; bo_pause; }
|
||||
touched=1
|
||||
dirty=1
|
||||
;;
|
||||
q|Q|escape) return "$(( ! touched ))" ;;
|
||||
*) ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Picking a program
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# bo_ui_pick_program <title>
|
||||
# The chosen program's index in BO_PICKED; 1 for none.
|
||||
BO_PICKED=-1
|
||||
|
||||
bo_ui_pick_program() {
|
||||
local title="$1" count=${#BO_P_ID[@]} cursor=0 i key frame row hint
|
||||
local -a labels=()
|
||||
|
||||
BO_PICKED=-1
|
||||
if (( count == 0 )); then
|
||||
printf '\n %s\n' "$(bo_msg "No programs found in any bottle.")"
|
||||
bo_pause
|
||||
return 1
|
||||
fi
|
||||
|
||||
for i in "${!BO_P_ID[@]}"; do
|
||||
bo_program_label "$i"
|
||||
labels+=("$BO_LABEL")
|
||||
done
|
||||
|
||||
bo_msg_into "Up/Down select - Enter picks - q cancels"
|
||||
hint="$BO_MSG_RESULT"
|
||||
_bo_clearseq
|
||||
_bo_term_rows 7
|
||||
local top=0
|
||||
|
||||
while true; do
|
||||
_bo_scroll "$cursor" "$top"; top=$BO_TOP
|
||||
frame="$BO_CLEARSEQ"$'\n'"${BO_C_BOLD}${BO_C_BLUE} ${title}${BO_C_RESET}"$'\n\n'
|
||||
for i in "${!labels[@]}"; do
|
||||
(( i < top || i >= top + BO_ROWS )) && continue
|
||||
if (( i == cursor )); then
|
||||
printf -v row ' %s %s' "${BO_C_BLUE}▸${BO_C_RESET}" "${labels[i]}"
|
||||
else
|
||||
printf -v row ' %s' "${labels[i]}"
|
||||
fi
|
||||
frame+="$row"$'\n'
|
||||
done
|
||||
frame+=$'\n'" ${BO_C_DIM}${hint}${BO_C_RESET}"$'\n'
|
||||
printf '%s' "$frame"
|
||||
|
||||
key="$(bo_read_key)" || return 1
|
||||
case "$key" in
|
||||
up|k) cursor=$(( (cursor - 1 + count) % count )) ;;
|
||||
down|j) cursor=$(( (cursor + 1) % count )) ;;
|
||||
enter|space|right|l)
|
||||
BO_PICKED=$cursor
|
||||
return 0
|
||||
;;
|
||||
q|Q|escape|left|h) return 1 ;;
|
||||
*) ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# _bo_parse_exts <text>
|
||||
# "flp, .FST *.fsc" -> BO_EXTS=(flp fst fsc). 1 with the bad one in BO_EXT.
|
||||
BO_EXTS=()
|
||||
|
||||
_bo_parse_exts() {
|
||||
local word
|
||||
BO_EXTS=()
|
||||
for word in ${1//,/ }; do
|
||||
bo_ext_normalize "$word" || return 1
|
||||
[[ " ${BO_EXTS[*]} " == *" $BO_EXT "* ]] || BO_EXTS+=("$BO_EXT")
|
||||
done
|
||||
}
|
||||
|
||||
# bo_ui_add_type
|
||||
# Extensions, then the program. 0 when something was added.
|
||||
bo_ui_add_type() {
|
||||
local ext
|
||||
|
||||
printf '\n %s\n' "$(bo_msg "Which file extensions? For example: flp fst")"
|
||||
printf ' > '
|
||||
bo_ui_read_line '' || return 1
|
||||
[[ -n ${BO_LINE_RESULT// /} ]] || return 1
|
||||
|
||||
if ! _bo_parse_exts "$BO_LINE_RESULT"; then
|
||||
bo_bad "$(bo_msg "\"%s\" is not a file extension." "$BO_EXT")"
|
||||
bo_pause
|
||||
return 1
|
||||
fi
|
||||
|
||||
bo_ui_pick_program "$(bo_msg "Which program opens %s?" ".${BO_EXTS[*]}")" || return 1
|
||||
bo_assoc_compute
|
||||
for ext in "${BO_EXTS[@]}"; do
|
||||
bo_assoc_add "$ext" "$BO_PICKED" || {
|
||||
bo_bad "$(bo_msg "Could not save the setting.")"
|
||||
bo_pause
|
||||
return 1
|
||||
}
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Programs
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# bo_ui_programs
|
||||
# Programs and their types; Enter edits the list, custom extensions included.
|
||||
# 0 when something changed.
|
||||
bo_ui_programs() {
|
||||
local key frame row pad i count cursor=0 top=0 dirty=1 touched=0 exts current
|
||||
local title hint empty
|
||||
local -a labels=() pexts=()
|
||||
|
||||
bo_msg_into "Programs"; title="$BO_MSG_RESULT"
|
||||
bo_msg_into "Up/Down select - Enter edits its file types - q goes back"
|
||||
hint="$BO_MSG_RESULT"
|
||||
bo_msg_into "No programs found in any bottle."; empty="$BO_MSG_RESULT"
|
||||
|
||||
_bo_clearseq
|
||||
printf '%s\n %s\n' "$BO_CLEARSEQ" "$(bo_msg "Looking at the bottles...")"
|
||||
bo_config_load
|
||||
bo_scan_once
|
||||
count=${#BO_P_ID[@]}
|
||||
for i in "${!BO_P_ID[@]}"; do
|
||||
bo_program_label "$i"
|
||||
labels+=("$BO_LABEL")
|
||||
done
|
||||
_bo_term_rows 7
|
||||
|
||||
while true; do
|
||||
if (( dirty )); then
|
||||
bo_config_load
|
||||
bo_assoc_compute
|
||||
pexts=()
|
||||
for i in "${!labels[@]}"; do
|
||||
pexts[i]="$(bo_program_exts "$i" 6)"
|
||||
done
|
||||
dirty=0
|
||||
fi
|
||||
|
||||
frame="$BO_CLEARSEQ"$'\n'"${BO_C_BOLD}${BO_C_BLUE} ${title}${BO_C_RESET}"$'\n\n'
|
||||
(( count == 0 )) && frame+=" ${BO_C_DIM}${empty}${BO_C_RESET}"$'\n'
|
||||
|
||||
_bo_scroll "$cursor" "$top"; top=$BO_TOP
|
||||
local marker selected="${BO_C_BLUE}▸${BO_C_RESET} "
|
||||
for i in "${!labels[@]}"; do
|
||||
(( i < top || i >= top + BO_ROWS )) && continue
|
||||
exts="${pexts[i]}"
|
||||
[[ -n $exts ]] || exts="${BO_C_DIM}-${BO_C_RESET}"
|
||||
pad=$(( 40 - ${#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" '' "$exts"
|
||||
frame+="$row"$'\n'
|
||||
done
|
||||
|
||||
frame+=$'\n'" ${BO_C_DIM}${hint}${BO_C_RESET}"$'\n'
|
||||
printf '%s' "$frame"
|
||||
|
||||
key="$(bo_read_key)" || return "$(( ! touched ))"
|
||||
|
||||
case "$key" in
|
||||
up|k) (( count )) && cursor=$(( (cursor - 1 + count) % count )) ;;
|
||||
down|j) (( count )) && cursor=$(( (cursor + 1) % count )) ;;
|
||||
enter|space|right|l)
|
||||
(( count )) || continue
|
||||
current="$(bo_program_exts "$cursor")"
|
||||
current="${current//./}"
|
||||
printf '\n %s\n' "$(bo_msg "File types for %s, separated by spaces:" "${BO_P_NAME[cursor]}")"
|
||||
printf ' > '
|
||||
bo_ui_read_line "$current" || continue
|
||||
if ! _bo_parse_exts "$BO_LINE_RESULT"; then
|
||||
bo_bad "$(bo_msg "\"%s\" is not a file extension." "$BO_EXT")"
|
||||
bo_pause
|
||||
continue
|
||||
fi
|
||||
bo_program_set_exts "$cursor" "${BO_EXTS[@]}" \
|
||||
|| { bo_bad "$(bo_msg "Could not save the setting.")"; bo_pause; }
|
||||
touched=1
|
||||
dirty=1
|
||||
;;
|
||||
q|Q|escape|left|h) return "$(( ! touched ))" ;;
|
||||
*) ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# File types
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# bo_ui_types
|
||||
# 0 when something changed.
|
||||
bo_ui_types() {
|
||||
local key frame row pad i k count cursor=0 dirty=1 touched=0 label shown
|
||||
local title hint legend warn empty
|
||||
local l_default l_openwith l_off l_missing l_added
|
||||
|
||||
bo_msg_into "File types"; title="$BO_MSG_RESULT"
|
||||
bo_msg_into "Up/Down select - Space switches - a adds one - Del removes - q goes back"
|
||||
hint="$BO_MSG_RESULT"
|
||||
bo_msg_into "\"double-click\" opens it; \"Open With\" only lists the program there."
|
||||
legend="$BO_MSG_RESULT"
|
||||
bo_msg_into "File opening is off - this is what it would do."
|
||||
warn="$BO_MSG_RESULT"
|
||||
bo_msg_into "No file types yet. Press a to add one."
|
||||
empty="$BO_MSG_RESULT"
|
||||
|
||||
# Resolved once: in the loop, a fork per row per keypress.
|
||||
bo_msg_into "double-click"; l_default="$BO_MSG_RESULT"
|
||||
bo_msg_into "Open With"; l_openwith="$BO_MSG_RESULT"
|
||||
bo_msg_into "off"; l_off="$BO_MSG_RESULT"
|
||||
bo_msg_into "program not found"; l_missing="$BO_MSG_RESULT"
|
||||
bo_msg_into "added"; l_added="$BO_MSG_RESULT"
|
||||
|
||||
local s_default="${BO_C_GREEN}${l_default}${BO_C_RESET}"
|
||||
local s_openwith="${BO_C_GREEN}${l_openwith}${BO_C_RESET}"
|
||||
local s_off="${BO_C_DIM}${l_off}${BO_C_RESET}"
|
||||
local s_missing="${BO_C_YELLOW}${l_missing}${BO_C_RESET}"
|
||||
|
||||
_bo_clearseq
|
||||
printf '%s\n %s\n' "$BO_CLEARSEQ" "$(bo_msg "Looking at the bottles...")"
|
||||
bo_config_load
|
||||
bo_scan_once
|
||||
_bo_term_rows 11
|
||||
local top=0
|
||||
|
||||
while true; do
|
||||
if (( dirty )); then
|
||||
bo_config_load
|
||||
bo_assoc_compute
|
||||
count=${#BO_A_ORDER[@]}
|
||||
(( cursor >= count )) && cursor=$(( count > 0 ? count - 1 : 0 ))
|
||||
dirty=0
|
||||
fi
|
||||
|
||||
frame="$BO_CLEARSEQ"$'\n'"${BO_C_BOLD}${BO_C_BLUE} ${title}${BO_C_RESET}"$'\n\n'
|
||||
|
||||
if (( count == 0 )); then
|
||||
frame+=" ${BO_C_DIM}${empty}${BO_C_RESET}"$'\n'
|
||||
fi
|
||||
|
||||
_bo_scroll "$cursor" "$top"; top=$BO_TOP
|
||||
(( top > 0 )) && frame+=" ${BO_C_DIM} ↑${BO_C_RESET}"$'\n'
|
||||
|
||||
local marker selected="${BO_C_BLUE}▸${BO_C_RESET} " n=-1
|
||||
for k in "${BO_A_ORDER[@]}"; do
|
||||
n=$(( n + 1 ))
|
||||
(( n < top || n >= top + BO_ROWS )) && continue
|
||||
i="${BO_A_PIDX[k]}"
|
||||
if (( i >= 0 )); then
|
||||
bo_program_label "$i"
|
||||
label="$BO_LABEL"
|
||||
else
|
||||
label="${BO_A_BOTTLE[k]}"
|
||||
fi
|
||||
|
||||
if (( i < 0 )); then
|
||||
shown="$s_missing"
|
||||
else
|
||||
case "${BO_A_STATE[k]}" in
|
||||
default) shown="$s_default" ;;
|
||||
openwith) shown="$s_openwith" ;;
|
||||
*) shown="$s_off" ;;
|
||||
esac
|
||||
fi
|
||||
[[ ${BO_A_SRC[k]} == added ]] && shown+=" ${BO_C_DIM}(${l_added})${BO_C_RESET}"
|
||||
|
||||
printf -v row '%-9s %s' ".${BO_A_EXT[k]}" "$label"
|
||||
pad=$(( 46 - ${#row} ))
|
||||
(( pad < 0 )) && pad=0
|
||||
if (( n == cursor )); then marker="$selected"; else marker=' '; fi
|
||||
printf -v row ' %s%s%*s %s' "$marker" "$row" "$pad" '' "$shown"
|
||||
frame+="$row"$'\n'
|
||||
done
|
||||
(( top + BO_ROWS < count )) && frame+=" ${BO_C_DIM} ↓${BO_C_RESET}"$'\n'
|
||||
|
||||
frame+=$'\n'
|
||||
# Otherwise it reads as what is on.
|
||||
if [[ $CFG_ENABLED != yes ]]; then
|
||||
frame+=" ${BO_C_YELLOW}${warn}${BO_C_RESET}"$'\n'
|
||||
fi
|
||||
frame+=" ${BO_C_DIM}${legend}${BO_C_RESET}"$'\n'
|
||||
frame+=" ${BO_C_DIM}${hint}${BO_C_RESET}"$'\n'
|
||||
printf '%s' "$frame"
|
||||
|
||||
key="$(bo_read_key)" || return "$(( ! touched ))"
|
||||
k="${BO_A_ORDER[cursor]:-}"
|
||||
|
||||
case "$key" in
|
||||
up|k) (( count )) && cursor=$(( (cursor - 1 + count) % count )) ;;
|
||||
down|j) (( count )) && cursor=$(( (cursor + 1) % count )) ;;
|
||||
space|enter|right|l|left|h)
|
||||
[[ -n $k ]] || continue
|
||||
(( BO_A_PIDX[k] >= 0 )) || continue
|
||||
case "${BO_A_STATE[k]}" in
|
||||
default) bo_assoc_set "$k" openwith ;;
|
||||
openwith) bo_assoc_set "$k" off ;;
|
||||
*) bo_assoc_set "$k" default ;;
|
||||
esac || { bo_bad "$(bo_msg "Could not save the setting.")"; bo_pause; }
|
||||
touched=1
|
||||
dirty=1
|
||||
;;
|
||||
a|A|+)
|
||||
bo_ui_add_type && touched=1
|
||||
dirty=1
|
||||
;;
|
||||
delete|backspace|d|D|x|-)
|
||||
[[ -n $k ]] || continue
|
||||
bo_assoc_remove "$k" || { bo_bad "$(bo_msg "Could not save the setting.")"; bo_pause; }
|
||||
touched=1
|
||||
dirty=1
|
||||
;;
|
||||
q|Q|escape) return "$(( ! touched ))" ;;
|
||||
*) ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The menu
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
bo_ui_menu() {
|
||||
local choice
|
||||
|
||||
bo_ui_term_raw
|
||||
trap 'bo_ui_term_restore' EXIT INT TERM
|
||||
|
||||
while true; do
|
||||
bo_config_load
|
||||
# Scan again on every visit.
|
||||
BO_SCANNED=0
|
||||
|
||||
clear 2>/dev/null || true
|
||||
bo_head " $BO_PRETTY"
|
||||
bo_ui_status
|
||||
printf '\n'
|
||||
printf ' [1] %s\n' "$(bo_msg "Turn file opening on or off")"
|
||||
printf ' [2] %s\n' "$(bo_msg "Re-apply everything")"
|
||||
printf ' [3] %s\n' "$(bo_msg "File types")"
|
||||
printf ' [4] %s\n' "$(bo_msg "Programs")"
|
||||
printf ' [5] %s\n' "$(bo_msg "Settings")"
|
||||
printf ' [q] %s\n' "$(bo_msg "Quit")"
|
||||
printf '\n > '
|
||||
|
||||
choice="$(bo_read_key)" || {
|
||||
printf '\n'; bo_ui_term_restore; trap - EXIT INT TERM; return 0
|
||||
}
|
||||
case "$choice" in
|
||||
enter|space|up|down|left|right|escape|delete|backspace) choice='' ;;
|
||||
esac
|
||||
printf '%s\n' "$choice"
|
||||
|
||||
case "$choice" in
|
||||
1)
|
||||
BO_UI_NEEDS_ACK=''
|
||||
if [[ $CFG_ENABLED == yes ]]; then
|
||||
bo_ui_cooked bo_do_disable
|
||||
else
|
||||
bo_ui_cooked bo_do_enable
|
||||
fi
|
||||
[[ -n $BO_UI_NEEDS_ACK ]] && bo_pause
|
||||
;;
|
||||
# The repair: everything taken back and written again.
|
||||
2)
|
||||
bo_ui_cooked bo_do_apply --rebuild
|
||||
bo_pause
|
||||
;;
|
||||
# Quietly: the front screen shows the result. Problems wait.
|
||||
3)
|
||||
BO_UI_NEEDS_ACK=''
|
||||
if bo_ui_types && [[ $CFG_ENABLED == yes ]]; then
|
||||
bo_ui_cooked bo_do_apply --quiet
|
||||
BO_QUIET=''
|
||||
fi
|
||||
[[ -n $BO_UI_NEEDS_ACK ]] && bo_pause
|
||||
;;
|
||||
4)
|
||||
BO_UI_NEEDS_ACK=''
|
||||
if bo_ui_programs && [[ $CFG_ENABLED == yes ]]; then
|
||||
bo_ui_cooked bo_do_apply --quiet
|
||||
BO_QUIET=''
|
||||
fi
|
||||
[[ -n $BO_UI_NEEDS_ACK ]] && bo_pause
|
||||
;;
|
||||
5)
|
||||
BO_UI_NEEDS_ACK=''
|
||||
if bo_ui_settings && [[ $CFG_ENABLED == yes ]]; then
|
||||
bo_ui_cooked bo_do_apply --quiet
|
||||
BO_QUIET=''
|
||||
fi
|
||||
[[ -n $BO_UI_NEEDS_ACK ]] && bo_pause
|
||||
;;
|
||||
q|Q) bo_ui_term_restore; trap - EXIT INT TERM; return 0 ;;
|
||||
# Redraw. Not Escape: a mistyped arrow key must not quit.
|
||||
*) ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
+316
@@ -0,0 +1,316 @@
|
||||
# shellcheck shell=bash
|
||||
#
|
||||
# File types: what the system already knows, what has to be taught to it, and
|
||||
# which application opens what.
|
||||
#
|
||||
# Launchers claim MIME types, not extensions. An unknown extension is
|
||||
# octet-stream like every unknown file, so it gets a type of its own here.
|
||||
|
||||
# Extension -> MIME type from the compiled globs2 files, minus our own types.
|
||||
declare -A BO_SYS_MIME=()
|
||||
BO_SYS_MIME_LOADED=0
|
||||
|
||||
bo_mime_system_load() {
|
||||
local own='' dirs dir files=() ext mime
|
||||
|
||||
(( BO_SYS_MIME_LOADED )) && return 0
|
||||
BO_SYS_MIME_LOADED=1
|
||||
BO_SYS_MIME=()
|
||||
|
||||
# The types defined in our own package, space separated.
|
||||
if [[ -r $BO_MIMEPKG ]]; then
|
||||
own=" $(sed -n 's/.*<mime-type type="\([^"]*\)".*/\1/p' "$BO_MIMEPKG" | tr '\n' ' ')"
|
||||
fi
|
||||
|
||||
[[ -r $BO_MIMEDIR/globs2 ]] && files+=("$BO_MIMEDIR/globs2")
|
||||
IFS=: read -ra dirs <<< "${XDG_DATA_DIRS:-/usr/local/share:/usr/share}"
|
||||
for dir in "${dirs[@]}"; do
|
||||
[[ -n $dir && -r $dir/mime/globs2 ]] && files+=("$dir/mime/globs2")
|
||||
done
|
||||
(( ${#files[@]} )) || return 0
|
||||
|
||||
# weight:type:glob[:flags]; plain "*.ext" only, heaviest wins.
|
||||
while IFS=$'\t' read -r ext mime; do
|
||||
BO_SYS_MIME[$ext]="$mime"
|
||||
# Ours can only be in the user's database.
|
||||
done < <(awk -F: -v own="$own" -v userfile="$BO_MIMEDIR/globs2" '
|
||||
/^#/ { next }
|
||||
{
|
||||
weight = $1 + 0; type = $2; glob = $3
|
||||
if (FILENAME == userfile && index(own, " " type " ")) next
|
||||
if (glob !~ /^\*\.[^*?[\]\/]+$/) next
|
||||
ext = tolower(substr(glob, 3))
|
||||
if (!(ext in best) || weight > best[ext]) { best[ext] = weight; seen[ext] = type }
|
||||
}
|
||||
END { for (e in seen) printf "%s\t%s\n", e, seen[e] }
|
||||
' "${files[@]}" 2>/dev/null)
|
||||
}
|
||||
|
||||
# bo_mime_for <extension>
|
||||
# BO_MIME: the type the extension is claimed as. BO_MIME_OWN: 1 when this
|
||||
# program defines that type.
|
||||
BO_MIME=''
|
||||
BO_MIME_OWN=0
|
||||
|
||||
bo_mime_for() {
|
||||
local ext="$1"
|
||||
|
||||
bo_mime_system_load
|
||||
|
||||
if bo_known_type "$ext" && (( BO_KT_OWN )); then
|
||||
BO_MIME="$BO_KT_MIME"
|
||||
BO_MIME_OWN=1
|
||||
elif [[ -n ${BO_SYS_MIME[$ext]:-} ]]; then
|
||||
BO_MIME="${BO_SYS_MIME[$ext]}"
|
||||
BO_MIME_OWN=0
|
||||
else
|
||||
bo_known_type "$ext"
|
||||
BO_MIME="$BO_KT_MIME"
|
||||
BO_MIME_OWN=1
|
||||
fi
|
||||
}
|
||||
|
||||
# bo_ext_valid <extension>
|
||||
# Bottles' own rule.
|
||||
bo_ext_valid() {
|
||||
[[ $1 =~ ^[a-z0-9][a-z0-9_+-]{0,31}$ ]]
|
||||
}
|
||||
|
||||
# bo_ext_normalize <input>
|
||||
# ".FLP", "*.flp" and "flp" all mean the same thing. Result in BO_EXT.
|
||||
BO_EXT=''
|
||||
|
||||
bo_ext_normalize() {
|
||||
local e="${1,,}"
|
||||
e="${e#\*}"
|
||||
e="${e#.}"
|
||||
BO_EXT="$e"
|
||||
bo_ext_valid "$e"
|
||||
}
|
||||
|
||||
_bo_xml() {
|
||||
local s="$1"
|
||||
s="${s//&/&}"
|
||||
s="${s//</<}"
|
||||
s="${s//>/>}"
|
||||
s="${s//\"/"}"
|
||||
printf '%s' "$s"
|
||||
}
|
||||
|
||||
# _bo_system_type_xml <MIME type>
|
||||
# The system's own definition of a type, as update-mime-database wrote it.
|
||||
_bo_system_type_xml() {
|
||||
local dir f
|
||||
local -a dirs
|
||||
IFS=: read -ra dirs <<< "${XDG_DATA_DIRS:-/usr/local/share:/usr/share}"
|
||||
for dir in "${dirs[@]}"; do
|
||||
for f in "$dir/mime/$1.xml" "$dir/mime/${1,,}.xml"; do
|
||||
[[ -r $f ]] && { printf '%s\n' "$f"; return 0; }
|
||||
done
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# _bo_magic_xml <spec>
|
||||
_bo_magic_xml() {
|
||||
local kind="${1%%:*}" value="${1#*:}"
|
||||
[[ -n $1 ]] || return 0
|
||||
printf ' <magic priority="50">\n'
|
||||
printf ' <match type="%s" offset="0" value="%s"/>\n' "$kind" "$(_bo_xml "$value")"
|
||||
printf ' </magic>\n'
|
||||
}
|
||||
|
||||
# bo_mime_package
|
||||
# Every type in BO_PKG_ORDER, from BO_PKG_* set by bo_apply. A system type is
|
||||
# copied whole with ours added: Qt and GLib read the description from the
|
||||
# first directory defining the type, and the user's comes first.
|
||||
bo_mime_package() {
|
||||
local mime ext extra sysxml
|
||||
|
||||
printf '<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||
printf '<!-- Written by bottles-opener. Taken back by `bottles-opener disable`. -->\n'
|
||||
printf '<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">\n'
|
||||
|
||||
for mime in "${BO_PKG_ORDER[@]}"; do
|
||||
extra=''
|
||||
for ext in ${BO_PKG_GLOBS[$mime]:-}; do
|
||||
extra+=" <glob pattern=\"*.$(_bo_xml "$ext")\"/>"$'\n'
|
||||
done
|
||||
extra+="$(_bo_magic_xml "${BO_PKG_MAGIC[$mime]:-}")"
|
||||
[[ -n ${BO_PKG_MAGIC[$mime]:-} ]] && extra+=$'\n'
|
||||
if [[ -n ${BO_PKG_ICON[$mime]:-} ]]; then
|
||||
extra+=" <icon name=\"$(_bo_xml "${BO_PKG_ICON[$mime]}")\"/>"$'\n'
|
||||
fi
|
||||
|
||||
if sysxml="$(_bo_system_type_xml "$mime")"; then
|
||||
awk -v extra="$extra" '
|
||||
/^<\?xml/ || /<!--.*-->/ || /^[[:space:]]*<icon[[:space:]]/ { next }
|
||||
/<\/mime-type>/ { printf "%s", extra; print " " $0; next }
|
||||
{ print " " $0 }
|
||||
' "$sysxml"
|
||||
else
|
||||
printf ' <mime-type type="%s">\n' "$(_bo_xml "$mime")"
|
||||
printf ' <comment>%s</comment>\n' "$(_bo_xml "${BO_PKG_DESC[$mime]}")"
|
||||
printf ' <generic-icon name="%s"/>\n' "$(_bo_xml "${BO_PKG_GICON[$mime]}")"
|
||||
printf '%s' "$extra"
|
||||
printf ' </mime-type>\n'
|
||||
fi
|
||||
done
|
||||
|
||||
printf '</mime-info>\n'
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Icons
|
||||
# ---------------------------------------------------------------------------
|
||||
# Files get the icon Windows shows for them, installed into hicolor.
|
||||
|
||||
# bo_png_size <file>
|
||||
# A PNG's width, from its header.
|
||||
bo_png_size() {
|
||||
local -a b
|
||||
read -ra b < <(od -An -tu1 -j0 -N24 -- "$1" 2>/dev/null | tr -s ' \n' ' ')
|
||||
(( ${#b[@]} >= 24 )) || return 1
|
||||
# \x89 P N G
|
||||
(( b[0] == 137 && b[1] == 80 && b[2] == 78 && b[3] == 71 )) || return 1
|
||||
printf '%d\n' $(( (b[16] << 24) | (b[17] << 16) | (b[18] << 8) | b[19] ))
|
||||
}
|
||||
|
||||
# bo_icon_target <MIME type> <png>
|
||||
# BO_ICON_NAME and BO_ICON_TARGET; 1 for an unusable png. Our own name: a theme
|
||||
# would win over hicolor for the type's standard one.
|
||||
BO_ICON_TARGET=''
|
||||
BO_ICON_NAME=''
|
||||
|
||||
bo_icon_target() {
|
||||
local mime="$1" src="$2" size s dir=''
|
||||
|
||||
BO_ICON_TARGET=''
|
||||
[[ $src == /* && -f $src ]] || return 1
|
||||
size="$(bo_png_size "$src")" || return 1
|
||||
|
||||
# The largest theme size the icon still fills.
|
||||
for s in 16 22 24 32 48 64 128 256 512; do
|
||||
(( size >= s )) && dir="${s}x${s}"
|
||||
done
|
||||
[[ -n $dir ]] || return 1
|
||||
|
||||
BO_ICON_NAME="bottles-opener-${mime//\//-}"
|
||||
BO_ICON_TARGET="$BO_ICONDIR/$dir/mimetypes/$BO_ICON_NAME.png"
|
||||
}
|
||||
|
||||
# bo_icon_png <install> <file> <index>
|
||||
# The icon as PNG in BO_ICON_PNG; extracted once, then cached.
|
||||
BO_ICON_PNG=''
|
||||
|
||||
bo_icon_png() {
|
||||
local install="$1" src="$2" idx="${3:-0}" key out rc
|
||||
|
||||
BO_ICON_PNG=''
|
||||
[[ -f $src ]] || return 1
|
||||
if [[ ${src,,} == *.png ]]; then
|
||||
BO_ICON_PNG="$src"
|
||||
return 0
|
||||
fi
|
||||
|
||||
key="$(printf '%s\t%s\t%s' "$src" "$idx" "$(stat -c '%s.%Y' -- "$src" 2>/dev/null)" \
|
||||
| sha1sum | cut -c1-16)"
|
||||
out="$BO_CACHEDIR/icons/$key.png"
|
||||
|
||||
[[ -s $out ]] && { BO_ICON_PNG="$out"; return 0; }
|
||||
[[ -e $out.none ]] && return 1
|
||||
mkdir -p "$BO_CACHEDIR/icons" 2>/dev/null || return 1
|
||||
|
||||
rc=127
|
||||
if bo_have python3; then
|
||||
python3 "$BO_LIBDIR/icon.py" "$src" "$idx" > "$out.tmp" 2>/dev/null
|
||||
rc=$?
|
||||
fi
|
||||
# The Flatpak surely has icoextract.
|
||||
if (( rc == 3 || rc == 127 )) && [[ $install == flatpak ]]; then
|
||||
flatpak run --command=python3 "$BO_FLATPAK_ID" - "$src" "$idx" \
|
||||
< "$BO_LIBDIR/icon.py" > "$out.tmp" 2>/dev/null
|
||||
rc=$?
|
||||
fi
|
||||
|
||||
if (( rc == 0 )) && [[ -s $out.tmp ]]; then
|
||||
mv -f "$out.tmp" "$out"
|
||||
BO_ICON_PNG="$out"
|
||||
return 0
|
||||
fi
|
||||
rm -f "$out.tmp"
|
||||
# Remember only a missing icon, not a missing icoextract.
|
||||
(( rc == 4 )) && : > "$out.none"
|
||||
return 1
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# mimeapps.list
|
||||
# ---------------------------------------------------------------------------
|
||||
# Edited line by line: the rest is somebody's settings.
|
||||
|
||||
# bo_mimeapps_get <MIME type>
|
||||
bo_mimeapps_get() {
|
||||
[[ -r $BO_MIMEAPPS ]] || return 0
|
||||
awk -v key="$1" '
|
||||
/^[[:space:]]*\[/ { group = $0; gsub(/^[[:space:]]+|[[:space:]]+$/, "", group); next }
|
||||
group == "[Default Applications]" {
|
||||
line = $0
|
||||
sub(/^[[:space:]]+/, "", line)
|
||||
if (index(line, key "=") == 1) { print substr(line, length(key) + 2); exit }
|
||||
}
|
||||
' "$BO_MIMEAPPS"
|
||||
}
|
||||
|
||||
# bo_mimeapps_set <MIME type> <value>
|
||||
# An empty value takes the line out.
|
||||
bo_mimeapps_set() {
|
||||
local key="$1" value="$2" tmp file
|
||||
|
||||
mkdir -p "$(dirname "$BO_MIMEAPPS")" || return 1
|
||||
[[ -e $BO_MIMEAPPS ]] || : > "$BO_MIMEAPPS" || return 1
|
||||
|
||||
# Keep a dotfiles symlink: replace its target, not the link.
|
||||
file="$(readlink -f -- "$BO_MIMEAPPS")" || return 1
|
||||
|
||||
tmp="$(mktemp "${file}.XXXXXX")" || return 1
|
||||
chmod --reference="$file" "$tmp" 2>/dev/null || chmod 0644 "$tmp"
|
||||
|
||||
awk -v key="$key" -v value="$value" '
|
||||
function flush() {
|
||||
if (ingroup && !done && value != "") { print key "=" value; done = 1 }
|
||||
}
|
||||
/^[[:space:]]*\[/ {
|
||||
flush()
|
||||
group = $0; gsub(/^[[:space:]]+|[[:space:]]+$/, "", group)
|
||||
ingroup = (group == "[Default Applications]")
|
||||
if (ingroup) seen = 1
|
||||
print; next
|
||||
}
|
||||
ingroup {
|
||||
line = $0
|
||||
sub(/^[[:space:]]+/, "", line)
|
||||
if (index(line, key "=") == 1) {
|
||||
if (!done && value != "") { print key "=" value; done = 1 }
|
||||
next
|
||||
}
|
||||
}
|
||||
{ print }
|
||||
END {
|
||||
flush()
|
||||
if (!seen && value != "") {
|
||||
print "[Default Applications]"
|
||||
print key "=" value
|
||||
}
|
||||
}
|
||||
' "$file" > "$tmp" || { rm -f "$tmp"; return 1; }
|
||||
|
||||
mv -f "$tmp" "$file"
|
||||
}
|
||||
|
||||
# bo_is_ours <mimeapps value>
|
||||
# Whether it names only one of our launchers.
|
||||
bo_is_ours() {
|
||||
local v="${1%;}"
|
||||
[[ $v == bottles-opener-*.desktop && $v != *';'* ]]
|
||||
}
|
||||
+237
@@ -0,0 +1,237 @@
|
||||
# shellcheck shell=bash
|
||||
#
|
||||
# Opening a file: what a launcher runs on a double click.
|
||||
#
|
||||
# The file is handed over as Explorer would: a Windows path (C:\ inside the
|
||||
# bottle, Z:\ for the rest), since "/home/..." reads like a switch to many
|
||||
# programs. The Flatpak gets it through the document portal first.
|
||||
|
||||
# bo_uri_to_path <argument>
|
||||
# file:// URI to path, in BO_PATH. Returns 1 for anything not local.
|
||||
BO_PATH=''
|
||||
|
||||
bo_uri_to_path() {
|
||||
local s="$1"
|
||||
|
||||
case "$s" in
|
||||
file://*)
|
||||
s="${s#file://}"
|
||||
# localhost is local; other hosts are not.
|
||||
if [[ $s == localhost/* ]]; then
|
||||
s="/${s#localhost/}"
|
||||
elif [[ $s != /* ]]; then
|
||||
BO_PATH="$1"
|
||||
return 1
|
||||
fi
|
||||
# Percent-decoding; backslashes doubled for printf %b.
|
||||
s="${s//\\/\\\\}"
|
||||
s="${s//%/\\x}"
|
||||
printf -v s '%b' "$s"
|
||||
;;
|
||||
esac
|
||||
|
||||
BO_PATH="$s"
|
||||
}
|
||||
|
||||
# bo_winpath <bottle directory> <absolute path>
|
||||
# The path through the drive with the longest matching target, in BO_WINPATH.
|
||||
BO_WINPATH=''
|
||||
|
||||
bo_winpath() {
|
||||
local bdir="$1" path="$2" dev target letter best='' best_target='' best_len=-1 len rest
|
||||
|
||||
BO_WINPATH=''
|
||||
|
||||
for dev in "$bdir"/dosdevices/?:; do
|
||||
[[ -e $dev ]] || continue
|
||||
target="$(readlink -f -- "$dev")" || continue
|
||||
|
||||
if [[ $target == / ]]; then
|
||||
len=0
|
||||
elif [[ $path == "$target" || $path == "$target"/* ]]; then
|
||||
len=${#target}
|
||||
else
|
||||
continue
|
||||
fi
|
||||
|
||||
if (( len > best_len )); then
|
||||
best_len=$len
|
||||
best="$dev"
|
||||
best_target="$target"
|
||||
fi
|
||||
done
|
||||
|
||||
[[ -n $best ]] || return 1
|
||||
|
||||
letter="${best##*/}"
|
||||
letter="${letter%:}"
|
||||
|
||||
if [[ $best_target == / ]]; then
|
||||
rest="$path"
|
||||
else
|
||||
rest="${path#"$best_target"}"
|
||||
fi
|
||||
rest="${rest//\//\\}"
|
||||
[[ $rest == \\* ]] || rest="\\$rest"
|
||||
|
||||
BO_WINPATH="${letter^^}:$rest"
|
||||
}
|
||||
|
||||
# bo_open_fail <message>
|
||||
# On the terminal, or as a notification when started from a file manager.
|
||||
bo_open_fail() {
|
||||
printf '%s: %s\n' "$BO_NAME" "$1" >&2
|
||||
if [[ ! -t 2 ]] && bo_have notify-send; then
|
||||
notify-send --app-name="$BO_PRETTY" --icon=dialog-error \
|
||||
"$BO_PRETTY" "$1" > /dev/null 2>&1 || true
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# bo_cli_has_program_id <install>
|
||||
# Whether bottles-cli knows --program-id (older ones only names). Read, not
|
||||
# run: running it costs a second and a half per double click.
|
||||
bo_cli_has_program_id() {
|
||||
local cli loc
|
||||
|
||||
case "$1" in
|
||||
native)
|
||||
cli="$(command -v bottles-cli)" || return 1
|
||||
;;
|
||||
flatpak)
|
||||
loc="$(flatpak info --show-location "$BO_FLATPAK_ID" 2>/dev/null)" || return 1
|
||||
cli="$loc/files/bin/bottles-cli"
|
||||
;;
|
||||
esac
|
||||
|
||||
[[ -r $cli ]] && grep -q -- '--program-id' "$cli" 2>/dev/null
|
||||
}
|
||||
|
||||
# _bo_win_args <template> <windows path>
|
||||
# The arguments Windows would pass, in BO_WARGS: the registry's template with
|
||||
# the file for %1. Office's /o "%u" and other placeholders are dropped. A
|
||||
# template without %1 relies on DDE (Excel's /dde): then only the file.
|
||||
BO_WARGS=()
|
||||
|
||||
_bo_win_args() {
|
||||
local t="$1" file="$2" c j cur='' have=0 quoted=0 placed=0 tok last
|
||||
local -a toks=()
|
||||
|
||||
for (( j = 0; j < ${#t}; j++ )); do
|
||||
c="${t:j:1}"
|
||||
if [[ $c == '"' ]]; then
|
||||
quoted=$(( ! quoted )); have=1
|
||||
elif [[ $c == [[:space:]] ]] && (( ! quoted )); then
|
||||
(( have )) && toks+=("$cur")
|
||||
cur=''; have=0
|
||||
else
|
||||
cur+="$c"; have=1
|
||||
fi
|
||||
done
|
||||
(( have )) && toks+=("$cur")
|
||||
|
||||
BO_WARGS=()
|
||||
for tok in "${toks[@]}"; do
|
||||
if [[ $tok == *%[1lLvV]* ]]; then
|
||||
tok="${tok//%1/"$file"}"; tok="${tok//%[lL]/"$file"}"; tok="${tok//%[vV]/"$file"}"
|
||||
BO_WARGS+=("$tok")
|
||||
placed=1
|
||||
elif [[ $tok == *%[uU]* ]]; then
|
||||
# ...and the switch in front of it.
|
||||
last=$(( ${#BO_WARGS[@]} - 1 ))
|
||||
if (( last >= 0 )) && [[ ${BO_WARGS[last]} == [/-]* && ${BO_WARGS[last]} != *%* ]]; then
|
||||
unset "BO_WARGS[last]"
|
||||
BO_WARGS=("${BO_WARGS[@]}")
|
||||
fi
|
||||
elif [[ $tok == *%[*0-9A-Za-z~]* ]]; then
|
||||
continue
|
||||
else
|
||||
BO_WARGS+=("$tok")
|
||||
fi
|
||||
done
|
||||
|
||||
(( placed )) || BO_WARGS=("$file")
|
||||
}
|
||||
|
||||
# bo_do_open <launcher id> [file...]
|
||||
bo_do_open() {
|
||||
local id="${1:-}" line install pid bottle bdir pname arg path ext template
|
||||
local -a winargs=() cmd=()
|
||||
shift || true
|
||||
|
||||
if [[ ! $id =~ ^[0-9a-f]{12}$ ]]; then
|
||||
bo_open_fail "$(bo_msg "Usage: bottles-opener open <launcher> [file...]")"
|
||||
return 1
|
||||
fi
|
||||
|
||||
line="$(awk -F'\t' -v id="$id" '$1 == id { print; exit }' "$BO_LAUNCHERS" 2>/dev/null)"
|
||||
if [[ -z $line ]]; then
|
||||
bo_open_fail "$(bo_msg "This launcher is out of date. Run \"bottles-opener apply\" and try again.")"
|
||||
return 1
|
||||
fi
|
||||
IFS=$'\t' read -r _ install pid bottle bdir pname <<< "$line"
|
||||
|
||||
for arg in "$@"; do
|
||||
# Not a local file: passed as it is.
|
||||
if ! bo_uri_to_path "$arg"; then
|
||||
winargs+=("$arg")
|
||||
continue
|
||||
fi
|
||||
path="$BO_PATH"
|
||||
[[ $path == /* ]] || path="$PWD/$path"
|
||||
|
||||
if [[ ! -e $path ]]; then
|
||||
bo_open_fail "$(bo_msg "The file %s does not exist." "$path")"
|
||||
return 1
|
||||
fi
|
||||
path="$(readlink -f -- "$path")"
|
||||
|
||||
if [[ $install == flatpak && $path != "$bdir"/* ]]; then
|
||||
# Writable, so the program can save it.
|
||||
if ! path="$(flatpak document-export --app="$BO_FLATPAK_ID" \
|
||||
--allow-read --allow-write -- "$path" 2>/dev/null)" || [[ -z $path ]]; then
|
||||
bo_open_fail "$(bo_msg "Could not hand %s to the Bottles Flatpak." "$arg")"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! bo_winpath "$bdir" "$path"; then
|
||||
bo_open_fail "$(bo_msg "No drive in the bottle \"%s\" reaches %s." "$bottle" "$path")"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# With the switches Windows would use for this extension.
|
||||
ext="${path##*/}"
|
||||
ext="${ext##*.}"
|
||||
template=''
|
||||
[[ $path == */*.* ]] && template="$(awk -F'\t' -v id="$id" -v e="${ext,,}" \
|
||||
'$1 == id && $2 == e { print $3; exit }' "$BO_COMMANDS" 2>/dev/null)"
|
||||
_bo_win_args "$template" "$BO_WINPATH"
|
||||
winargs+=("${BO_WARGS[@]}")
|
||||
done
|
||||
|
||||
case "$install" in
|
||||
native) cmd=(bottles-cli) ;;
|
||||
flatpak) cmd=(flatpak run --command=bottles-cli "$BO_FLATPAK_ID") ;;
|
||||
*)
|
||||
bo_open_fail "$(bo_msg "This launcher is out of date. Run \"bottles-opener apply\" and try again.")"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
cmd+=(run -b "$bottle")
|
||||
if bo_cli_has_program_id "$install"; then
|
||||
cmd+=(--program-id "$pid")
|
||||
else
|
||||
cmd+=(-p "$pname")
|
||||
fi
|
||||
cmd+=(-- "${winargs[@]}")
|
||||
|
||||
if [[ -n ${BO_DRY_RUN:-} ]]; then
|
||||
printf '%q ' "${cmd[@]}"
|
||||
printf '\n'
|
||||
return 0
|
||||
fi
|
||||
|
||||
exec "${cmd[@]}"
|
||||
}
|
||||
+452
@@ -0,0 +1,452 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Lists the bottles and programs of one Bottles installation, and the file
|
||||
# types each program registered in its bottle's registry - the way Windows
|
||||
# knows them: extension -> ProgID -> open command, icon and description.
|
||||
#
|
||||
# Runs on the host, or inside the Bottles Flatpak fed through stdin, so it only
|
||||
# needs what Bottles itself needs: Python and PyYAML.
|
||||
#
|
||||
# Usage: scan.py <data home> [cache dir]
|
||||
#
|
||||
# Output, one record per line, fields separated by \x1f:
|
||||
# R <bottles directory>
|
||||
# B <bottle name> <bottle directory> <bottle.yml>
|
||||
# P <bottle name> <program id> <program name> <windows path> <icon>
|
||||
# A <bottle name> <program id> <ext> <args> <icon file> <icon index> <description>
|
||||
#
|
||||
# Copyright (C) 2026 Felitendo
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
sys.exit(3)
|
||||
|
||||
LOADER = getattr(yaml, "CSafeLoader", yaml.SafeLoader)
|
||||
SEP = "\x1f"
|
||||
CACHE_VERSION = 2
|
||||
|
||||
EXT_RE = re.compile(r"^[a-z0-9][a-z0-9_+-]{0,31}$")
|
||||
|
||||
# Registered by half of all installers, and never a document.
|
||||
SKIP_EXTS = {
|
||||
"exe", "dll", "lnk", "bat", "cmd", "com", "msi", "msp", "scr", "pif",
|
||||
"url", "cpl", "sys", "ocx", "drv", "reg", "inf", "ini", "cab",
|
||||
}
|
||||
|
||||
|
||||
def load(path):
|
||||
try:
|
||||
with open(path, encoding="utf-8") as f:
|
||||
return yaml.load(f, Loader=LOADER)
|
||||
except (OSError, UnicodeDecodeError, yaml.YAMLError):
|
||||
return None
|
||||
|
||||
|
||||
def field(value):
|
||||
text = "" if value is None else str(value)
|
||||
for ch in ("\n", "\r", "\t", SEP):
|
||||
text = text.replace(ch, " ")
|
||||
return text
|
||||
|
||||
|
||||
def emit(*values):
|
||||
sys.stdout.write(SEP.join(field(v) for v in values) + "\n")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Registry
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _unescape(s):
|
||||
out, i = [], 0
|
||||
while i < len(s):
|
||||
c = s[i]
|
||||
if c == "\\" and i + 1 < len(s):
|
||||
n = s[i + 1]
|
||||
if n == "x":
|
||||
m = re.match(r"[0-9a-fA-F]{1,4}", s[i + 2:])
|
||||
if m:
|
||||
out.append(chr(int(m.group(0), 16)))
|
||||
i += 2 + len(m.group(0))
|
||||
continue
|
||||
out.append({"n": "\n", "r": "\r", "t": "\t", "0": "\0"}.get(n, n))
|
||||
i += 2
|
||||
continue
|
||||
out.append(c)
|
||||
i += 1
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def _string(raw):
|
||||
"""A value's text, for the string kinds; None for anything else."""
|
||||
if raw.startswith('"') and raw.endswith('"'):
|
||||
return _unescape(raw[1:-1])
|
||||
if raw.startswith('str(2):"') and raw.endswith('"'):
|
||||
return _unescape(raw[8:-1])
|
||||
if raw.startswith("hex(2):"):
|
||||
try:
|
||||
data = bytes(int(b, 16) for b in raw[7:].replace(" ", "").split(",") if b)
|
||||
return data.decode("utf-16-le").rstrip("\0")
|
||||
except ValueError:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def read_classes(path):
|
||||
"""Software\\Classes of one hive: {lowercase subkey: {lowercase name: text}}."""
|
||||
classes = {}
|
||||
prefix = "software\\classes\\"
|
||||
current = None
|
||||
pending = None
|
||||
|
||||
try:
|
||||
f = open(path, encoding="utf-8", errors="replace")
|
||||
except OSError:
|
||||
return classes
|
||||
|
||||
with f:
|
||||
for line in f:
|
||||
line = line.rstrip("\n")
|
||||
|
||||
# hex values run on over lines ending in a backslash
|
||||
if pending is not None:
|
||||
pending[1] += line.strip().rstrip("\\")
|
||||
if not line.endswith("\\"):
|
||||
name, raw = pending
|
||||
pending = None
|
||||
text = _string(raw)
|
||||
if current is not None and text is not None:
|
||||
classes.setdefault(current, {})[name] = text
|
||||
continue
|
||||
|
||||
if line.startswith("["):
|
||||
end = line.rfind("]")
|
||||
key = _unescape(line[1:end]).lower() if end > 0 else ""
|
||||
current = key[len(prefix):] if key.startswith(prefix) else None
|
||||
# A key exists even with no values, and so do its parents.
|
||||
if current:
|
||||
parts = current.split("\\")
|
||||
for n in range(1, len(parts) + 1):
|
||||
classes.setdefault("\\".join(parts[:n]), {})
|
||||
continue
|
||||
|
||||
if current is None or not line or line[0] not in '@"':
|
||||
continue
|
||||
|
||||
if line.startswith("@="):
|
||||
name, raw = "", line[2:]
|
||||
else:
|
||||
m = re.match(r'"((?:[^"\\]|\\.)*)"=(.*)$', line)
|
||||
if not m:
|
||||
continue
|
||||
name, raw = _unescape(m.group(1)).lower(), m.group(2)
|
||||
|
||||
if raw.startswith("hex(2):") and raw.endswith("\\"):
|
||||
pending = [name, raw.rstrip("\\")]
|
||||
continue
|
||||
|
||||
text = _string(raw)
|
||||
if text is not None:
|
||||
classes.setdefault(current, {})[name] = text
|
||||
|
||||
return classes
|
||||
|
||||
|
||||
def expand(text, users):
|
||||
"""The environment variables installers put into paths."""
|
||||
user = f"C:\\users\\{users}"
|
||||
table = {
|
||||
"programfiles": "C:\\Program Files",
|
||||
"programw6432": "C:\\Program Files",
|
||||
"programfiles(x86)": "C:\\Program Files (x86)",
|
||||
"commonprogramfiles": "C:\\Program Files\\Common Files",
|
||||
"commonprogramw6432": "C:\\Program Files\\Common Files",
|
||||
"commonprogramfiles(x86)": "C:\\Program Files (x86)\\Common Files",
|
||||
"systemroot": "C:\\windows",
|
||||
"windir": "C:\\windows",
|
||||
"systemdrive": "C:",
|
||||
"programdata": "C:\\ProgramData",
|
||||
"allusersprofile": "C:\\ProgramData",
|
||||
"public": "C:\\users\\Public",
|
||||
"userprofile": user,
|
||||
"appdata": user + "\\AppData\\Roaming",
|
||||
"localappdata": user + "\\AppData\\Local",
|
||||
}
|
||||
return re.sub(
|
||||
r"%([^%]+)%",
|
||||
lambda m: table.get(m.group(1).lower(), m.group(0)),
|
||||
text,
|
||||
)
|
||||
|
||||
|
||||
def split_command(cmd):
|
||||
"""(executable, rest) of a command line."""
|
||||
cmd = cmd.strip()
|
||||
if cmd.startswith('"'):
|
||||
end = cmd.find('"', 1)
|
||||
if end < 0:
|
||||
return cmd[1:], ""
|
||||
return cmd[1:end], cmd[end + 1:].strip()
|
||||
m = re.match(r"(.*?\.(?:exe|com|bat|cmd))(?:\s+(.*))?$", cmd, re.I)
|
||||
if m:
|
||||
return m.group(1), (m.group(2) or "").strip()
|
||||
exe, _, rest = cmd.partition(" ")
|
||||
return exe, rest.strip()
|
||||
|
||||
|
||||
def to_unix(bottle_dir, winpath):
|
||||
"""A Windows path inside the bottle, found case-insensitively like Windows would."""
|
||||
winpath = winpath.strip().strip('"')
|
||||
if winpath.startswith("/"):
|
||||
return winpath if os.path.exists(winpath) else None
|
||||
m = re.match(r"^([A-Za-z]):[\\/]*(.*)$", winpath)
|
||||
if not m:
|
||||
return None
|
||||
|
||||
path = os.path.realpath(os.path.join(bottle_dir, "dosdevices", m.group(1).lower() + ":"))
|
||||
for part in re.split(r"[\\/]+", m.group(2)):
|
||||
if not part:
|
||||
continue
|
||||
candidate = os.path.join(path, part)
|
||||
if not os.path.exists(candidate):
|
||||
try:
|
||||
lower = part.lower()
|
||||
match = next(e for e in os.listdir(path) if e.lower() == lower)
|
||||
except (OSError, StopIteration):
|
||||
return None
|
||||
candidate = os.path.join(path, match)
|
||||
path = candidate
|
||||
return path
|
||||
|
||||
|
||||
def registered_types(bottle_dir, users):
|
||||
"""{ext: (exe, args, icon file, icon index, description)} from both hives."""
|
||||
system = read_classes(os.path.join(bottle_dir, "system.reg"))
|
||||
user = read_classes(os.path.join(bottle_dir, "user.reg"))
|
||||
|
||||
def values(key):
|
||||
merged = dict(system.get(key, {}))
|
||||
merged.update(user.get(key, {}))
|
||||
return merged
|
||||
|
||||
def default(key):
|
||||
return values(key).get("", "").strip()
|
||||
|
||||
def has(key):
|
||||
return key in system or key in user
|
||||
|
||||
keys = set(system) | set(user)
|
||||
result = {}
|
||||
|
||||
for key in keys:
|
||||
if not key.startswith(".") or "\\" in key:
|
||||
continue
|
||||
ext = key[1:]
|
||||
if not EXT_RE.match(ext) or ext in SKIP_EXTS:
|
||||
continue
|
||||
|
||||
progid = default(key).lower()
|
||||
if not progid or not has(progid):
|
||||
continue
|
||||
curver = default(progid + "\\curver").lower()
|
||||
if curver and has(curver):
|
||||
progid = curver
|
||||
|
||||
# The default verb, else open, else any.
|
||||
verb = default(progid + "\\shell").split(",")[0].strip().lower()
|
||||
if not verb or not has(f"{progid}\\shell\\{verb}\\command"):
|
||||
verb = "open"
|
||||
command = default(f"{progid}\\shell\\{verb}\\command")
|
||||
if not command:
|
||||
verbs = sorted(
|
||||
k for k in keys
|
||||
if k.startswith(progid + "\\shell\\") and k.endswith("\\command")
|
||||
)
|
||||
command = default(verbs[0]) if verbs else ""
|
||||
if not command:
|
||||
continue
|
||||
|
||||
exe, args = split_command(expand(command, users))
|
||||
|
||||
icon_file, icon_index = "", "0"
|
||||
icon = expand(default(progid + "\\defaulticon"), users).strip()
|
||||
if icon and "%1" not in icon:
|
||||
icon = icon.strip('"')
|
||||
m = re.match(r'^"?(.*?)"?\s*,\s*(-?\d+)$', icon)
|
||||
if m:
|
||||
icon, icon_index = m.group(1), m.group(2)
|
||||
icon_file = to_unix(bottle_dir, icon) or ""
|
||||
|
||||
desc = ""
|
||||
for candidate in (values(progid).get("friendlytypename", ""), default(progid)):
|
||||
candidate = candidate.strip()
|
||||
if candidate and not candidate.startswith("@"):
|
||||
desc = candidate
|
||||
break
|
||||
|
||||
result[ext] = (exe, args, icon_file, icon_index, desc)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def cached_types(bottle_dir, users, cache_dir):
|
||||
"""registered_types, kept across runs until either hive changes."""
|
||||
stamp = []
|
||||
for hive in ("system.reg", "user.reg"):
|
||||
try:
|
||||
st = os.stat(os.path.join(bottle_dir, hive))
|
||||
stamp.append([st.st_size, st.st_mtime_ns])
|
||||
except OSError:
|
||||
stamp.append(None)
|
||||
|
||||
cache = None
|
||||
if cache_dir:
|
||||
name = hashlib.sha1(bottle_dir.encode()).hexdigest()[:16] + ".json"
|
||||
cache = os.path.join(cache_dir, name)
|
||||
try:
|
||||
with open(cache, encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
if data.get("version") == CACHE_VERSION and data.get("stamp") == stamp:
|
||||
return {k: tuple(v) for k, v in data["types"].items()}
|
||||
except (OSError, ValueError, KeyError, AttributeError):
|
||||
pass
|
||||
|
||||
types = registered_types(bottle_dir, users)
|
||||
|
||||
if cache:
|
||||
try:
|
||||
os.makedirs(cache_dir, exist_ok=True)
|
||||
tmp = cache + ".tmp"
|
||||
with open(tmp, "w", encoding="utf-8") as f:
|
||||
json.dump({"version": CACHE_VERSION, "stamp": stamp, "types": types}, f)
|
||||
os.replace(tmp, cache)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
return types
|
||||
|
||||
|
||||
def windows_user(bottle_dir):
|
||||
try:
|
||||
names = os.listdir(os.path.join(bottle_dir, "drive_c", "users"))
|
||||
except OSError:
|
||||
return "user"
|
||||
names = [n for n in names if n.lower() not in ("public", "default", "default user", "all users")]
|
||||
return sorted(names)[0] if names else "user"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Bottles
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
return 2
|
||||
|
||||
cache_dir = sys.argv[2] if len(sys.argv) > 2 else ""
|
||||
base = os.path.join(sys.argv[1], "bottles")
|
||||
root = os.path.join(base, "bottles")
|
||||
|
||||
data = load(os.path.join(base, "data.yml"))
|
||||
if isinstance(data, dict):
|
||||
custom = data.get("custom_bottles_path")
|
||||
if (
|
||||
isinstance(custom, str)
|
||||
and custom
|
||||
and not ("/run/user/" in custom and "/doc/" in custom)
|
||||
and os.path.isdir(custom)
|
||||
and os.access(custom, os.W_OK)
|
||||
):
|
||||
root = custom
|
||||
|
||||
emit("R", root)
|
||||
|
||||
try:
|
||||
entries = sorted(os.listdir(root))
|
||||
except OSError:
|
||||
return 0
|
||||
|
||||
for entry in entries:
|
||||
bottle_dir = os.path.join(root, entry)
|
||||
if not os.path.isdir(bottle_dir):
|
||||
continue
|
||||
|
||||
config_path = os.path.join(bottle_dir, "bottle.yml")
|
||||
placeholder = os.path.join(bottle_dir, "placeholder.yml")
|
||||
if os.path.exists(placeholder):
|
||||
target = load(placeholder)
|
||||
if not isinstance(target, dict) or not target.get("Path"):
|
||||
continue
|
||||
bottle_dir = str(target["Path"])
|
||||
config_path = os.path.join(bottle_dir, "bottle.yml")
|
||||
|
||||
config = load(config_path)
|
||||
if not isinstance(config, dict) or config.get("Environment") == "Steam":
|
||||
continue
|
||||
|
||||
name = config.get("Name") or entry
|
||||
emit("B", name, bottle_dir, config_path)
|
||||
|
||||
programs = config.get("External_Programs") or {}
|
||||
if not isinstance(programs, dict):
|
||||
continue
|
||||
|
||||
listed = []
|
||||
for key, program in programs.items():
|
||||
if not isinstance(program, dict) or program.get("removed"):
|
||||
continue
|
||||
path = program.get("path")
|
||||
if not path:
|
||||
continue
|
||||
listed.append((
|
||||
str(program.get("name") or program.get("executable") or key),
|
||||
str(program.get("id") or key),
|
||||
str(path),
|
||||
program.get("icon") or "",
|
||||
))
|
||||
listed.sort(key=lambda p: p[0].lower())
|
||||
|
||||
for prog_name, prog_id, path, icon in listed:
|
||||
emit("P", name, prog_id, prog_name, path, icon)
|
||||
|
||||
if not listed:
|
||||
continue
|
||||
|
||||
# Which program each registered type belongs to: by its path, as
|
||||
# Windows spells it or as it is on disk, and by file name when that
|
||||
# names exactly one program.
|
||||
by_path, by_real, by_base = {}, {}, {}
|
||||
for _, prog_id, path, _ in listed:
|
||||
by_path[path.lower().replace("/", "\\")] = prog_id
|
||||
real = to_unix(bottle_dir, path)
|
||||
if real:
|
||||
by_real[os.path.realpath(real)] = prog_id
|
||||
base_name = re.split(r"[\\/]", path)[-1].lower()
|
||||
by_base.setdefault(base_name, []).append(prog_id)
|
||||
|
||||
types = cached_types(bottle_dir, windows_user(bottle_dir), cache_dir)
|
||||
for ext in sorted(types):
|
||||
exe, args, icon_file, icon_index, desc = types[ext]
|
||||
prog_id = by_path.get(exe.lower().replace("/", "\\"))
|
||||
if not prog_id:
|
||||
real = to_unix(bottle_dir, exe)
|
||||
prog_id = by_real.get(os.path.realpath(real)) if real else None
|
||||
if not prog_id:
|
||||
hits = by_base.get(re.split(r"[\\/]", exe)[-1].lower(), [])
|
||||
prog_id = hits[0] if len(hits) == 1 else None
|
||||
if prog_id:
|
||||
emit("A", name, prog_id, ext, args, icon_file, icon_index, desc)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
# shellcheck shell=bash
|
||||
#
|
||||
# Finding Bottles, its bottles and the programs in them.
|
||||
#
|
||||
# Native (bottles-cli, ~/.local/share/bottles) and Flatpak (~/.var/app/...),
|
||||
# possibly both. Everything is tagged with its install: it decides how a
|
||||
# program is started.
|
||||
|
||||
# The installations present, "native" and/or "flatpak". Filled once per run.
|
||||
BO_INSTALLS=()
|
||||
BO_INSTALLS_DONE=0
|
||||
|
||||
bo_installs_detect() {
|
||||
(( BO_INSTALLS_DONE )) && return 0
|
||||
BO_INSTALLS_DONE=1
|
||||
BO_INSTALLS=()
|
||||
|
||||
# For testing against a fixed set; not documented anywhere else.
|
||||
if [[ -n ${BO_FORCE_INSTALLS+set} ]]; then
|
||||
read -ra BO_INSTALLS <<< "$BO_FORCE_INSTALLS"
|
||||
return 0
|
||||
fi
|
||||
|
||||
bo_have bottles-cli && BO_INSTALLS+=(native)
|
||||
if bo_have flatpak && flatpak info "$BO_FLATPAK_ID" > /dev/null 2>&1; then
|
||||
BO_INSTALLS+=(flatpak)
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
bo_install_present() {
|
||||
local i
|
||||
bo_installs_detect
|
||||
for i in "${BO_INSTALLS[@]}"; do
|
||||
[[ $i == "$1" ]] && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# bo_install_data <install>
|
||||
# The directory holding bottles/ for that installation.
|
||||
bo_install_data() {
|
||||
case "$1" in
|
||||
native) printf '%s\n' "${BO_NATIVE_DATA:-$BO_XDG_DATA}" ;;
|
||||
flatpak) printf '%s\n' "${BO_FLATPAK_DATA:-$HOME/.var/app/$BO_FLATPAK_ID/data}" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The scan
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
BO_ROOTS=() # every bottles directory, for the watcher
|
||||
BO_CONFS=() # every bottle.yml, for the watcher
|
||||
|
||||
BO_B_INSTALL=() # one entry per bottle
|
||||
BO_B_NAME=()
|
||||
|
||||
BO_P_INSTALL=() # one entry per program
|
||||
BO_P_BOTTLE=()
|
||||
BO_P_BDIR=()
|
||||
BO_P_ID=()
|
||||
BO_P_NAME=()
|
||||
BO_P_PATH=()
|
||||
BO_P_ICON=()
|
||||
|
||||
BO_R_INSTALL=() # one entry per type a program registered in its bottle
|
||||
BO_R_PID=()
|
||||
BO_R_EXT=()
|
||||
BO_R_ARGS=()
|
||||
BO_R_ICON=()
|
||||
BO_R_IDX=()
|
||||
BO_R_DESC=()
|
||||
declare -A BO_R_AT=() # ext \x1f install \x1f program id -> index into BO_R_*
|
||||
declare -A BO_P_AT=() # install \x1f program id -> index into BO_P_*
|
||||
|
||||
# An install that could not be read, rather than one that is empty.
|
||||
BO_SCAN_ERROR=''
|
||||
|
||||
BO_SCANNED=0
|
||||
|
||||
# _bo_scan_run <install> <data home>
|
||||
# The host's Python first; the Flatpak's, which surely has PyYAML, as fallback.
|
||||
_bo_scan_run() {
|
||||
local install="$1" data="$2" rc=127
|
||||
|
||||
if bo_have python3; then
|
||||
python3 "$BO_LIBDIR/scan.py" "$data" "$BO_CACHEDIR/registry"
|
||||
rc=$?
|
||||
fi
|
||||
|
||||
if (( rc == 3 || rc == 127 )) && [[ $install == flatpak ]]; then
|
||||
flatpak run --command=python3 "$BO_FLATPAK_ID" - "$data" \
|
||||
< "$BO_LIBDIR/scan.py" 2>/dev/null
|
||||
rc=$?
|
||||
fi
|
||||
|
||||
return "$rc"
|
||||
}
|
||||
|
||||
bo_scan() {
|
||||
local install data kind a b c d e f g out bdir
|
||||
|
||||
BO_ROOTS=(); BO_CONFS=()
|
||||
BO_B_INSTALL=(); BO_B_NAME=()
|
||||
BO_P_INSTALL=(); BO_P_BOTTLE=(); BO_P_BDIR=(); BO_P_ID=()
|
||||
BO_P_NAME=(); BO_P_PATH=(); BO_P_ICON=()
|
||||
BO_R_INSTALL=(); BO_R_PID=(); BO_R_EXT=(); BO_R_ARGS=()
|
||||
BO_R_ICON=(); BO_R_IDX=(); BO_R_DESC=(); BO_R_AT=(); BO_P_AT=()
|
||||
BO_SCAN_ERROR=''
|
||||
|
||||
bo_installs_detect
|
||||
|
||||
for install in "${BO_INSTALLS[@]}"; do
|
||||
data="$(bo_install_data "$install")"
|
||||
|
||||
if ! out="$(_bo_scan_run "$install" "$data")"; then
|
||||
BO_SCAN_ERROR="$install"
|
||||
continue
|
||||
fi
|
||||
|
||||
declare -A dirs=()
|
||||
while IFS=$'\x1f' read -r kind a b c d e f g; do
|
||||
case "$kind" in
|
||||
R)
|
||||
BO_ROOTS+=("$a")
|
||||
;;
|
||||
B)
|
||||
BO_B_INSTALL+=("$install"); BO_B_NAME+=("$a")
|
||||
BO_CONFS+=("$c")
|
||||
dirs[$a]="$b"
|
||||
;;
|
||||
P)
|
||||
bdir="${dirs[$a]:-}"
|
||||
[[ -n $bdir && -n $b ]] || continue
|
||||
BO_P_AT[$install$'\x1f'$b]=${#BO_P_ID[@]}
|
||||
BO_P_INSTALL+=("$install"); BO_P_BOTTLE+=("$a")
|
||||
BO_P_BDIR+=("$bdir"); BO_P_ID+=("$b")
|
||||
BO_P_NAME+=("$c"); BO_P_PATH+=("$d"); BO_P_ICON+=("$e")
|
||||
;;
|
||||
A)
|
||||
BO_R_AT[$c$'\x1f'$install$'\x1f'$b]=${#BO_R_EXT[@]}
|
||||
BO_R_INSTALL+=("$install"); BO_R_PID+=("$b"); BO_R_EXT+=("$c")
|
||||
BO_R_ARGS+=("$d"); BO_R_ICON+=("$e"); BO_R_IDX+=("$f")
|
||||
BO_R_DESC+=("$g")
|
||||
;;
|
||||
esac
|
||||
done <<< "$out"
|
||||
unset dirs
|
||||
done
|
||||
|
||||
BO_SCANNED=1
|
||||
return 0
|
||||
}
|
||||
|
||||
bo_scan_once() {
|
||||
(( BO_SCANNED )) || bo_scan
|
||||
}
|
||||
|
||||
# bo_program_find <install> <program id>
|
||||
# Index into the BO_P_* arrays, in BO_FOUND; returns 1 when there is none.
|
||||
BO_FOUND=-1
|
||||
|
||||
bo_program_find() {
|
||||
BO_FOUND="${BO_P_AT[$1$'\x1f'$2]:--1}"
|
||||
(( BO_FOUND >= 0 ))
|
||||
}
|
||||
|
||||
# bo_program_label <index>
|
||||
# "FL Studio · FL Studio", plus "(Flatpak)" when both installs exist.
|
||||
BO_LABEL=''
|
||||
|
||||
bo_program_label() {
|
||||
local i="$1"
|
||||
BO_LABEL="${BO_P_NAME[i]} · ${BO_P_BOTTLE[i]}"
|
||||
if [[ ${BO_P_INSTALL[i]} == flatpak ]] && bo_install_present native; then
|
||||
BO_LABEL+=" (Flatpak)"
|
||||
fi
|
||||
}
|
||||
Reference in new issue
Block a user