Compare commits
11
Commits
4ff0615bac
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bae0ffbdd1 | ||
|
|
71b27f5ab7 | ||
|
|
6079f6639b | ||
|
|
d1b2fbdf15 | ||
|
|
893fe34a11 | ||
|
|
89b1e380aa | ||
|
|
e078b21843 | ||
|
|
82af24522a | ||
|
|
0eaac74dfa | ||
|
|
9becc60ef8 | ||
|
|
4327b8ce0e |
No files matched your search
@@ -1,674 +0,0 @@
|
|||||||
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>.
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
class Echo:
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
print(*args, **kwargs)
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
from cli.echo import Echo
|
|
||||||
|
|
||||||
|
|
||||||
class CustomDialog:
|
|
||||||
def __init__(self, message, msg_type="", timeout=None):
|
|
||||||
self.message = message
|
|
||||||
self.msg_type = msg_type.lower()
|
|
||||||
self.timeout = timeout
|
|
||||||
|
|
||||||
def mainloop(self):
|
|
||||||
if self.msg_type == "success":
|
|
||||||
Echo("✅", self.message)
|
|
||||||
elif self.msg_type == "error":
|
|
||||||
Echo("❌", self.message)
|
|
||||||
elif self.msg_type == "warning":
|
|
||||||
Echo("⚠️", self.message)
|
|
||||||
elif self.msg_type == "info":
|
|
||||||
Echo("ℹ", self.message)
|
|
||||||
else:
|
|
||||||
Echo(self.message)
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
pass
|
|
||||||
@@ -1,214 +0,0 @@
|
|||||||
import getpass
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
|
|
||||||
from core.config import Config
|
|
||||||
from cli.info import CustomDialog
|
|
||||||
from core.constants import *
|
|
||||||
|
|
||||||
|
|
||||||
class LoginForm:
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
config: Config,
|
|
||||||
on_login_callback=None,
|
|
||||||
on_quit_callback=None,
|
|
||||||
):
|
|
||||||
self.config = config
|
|
||||||
self.on_login_callback = on_login_callback
|
|
||||||
self.on_quit_callback = on_quit_callback
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def remove_trailing_slash(entry):
|
|
||||||
return re.sub(r"/+$", "", entry)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def is_positive_integer(value):
|
|
||||||
if value.isdigit() and int(value) > 0:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def bool_to_str(value):
|
|
||||||
return "y" if value else "n"
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def str_to_bool(value):
|
|
||||||
return value.lower().strip() == "y"
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def is_positive_integer(value):
|
|
||||||
if value.isdigit() and int(value) > 0:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def mainloop(self):
|
|
||||||
# save data to config
|
|
||||||
self.config.data["username"] = (
|
|
||||||
input(f"username [{self.config.data['username']}]: ")
|
|
||||||
or self.config.data["username"]
|
|
||||||
)
|
|
||||||
|
|
||||||
self.config.data["password"] = getpass.getpass("password: ")
|
|
||||||
|
|
||||||
server_url = (
|
|
||||||
input(f"server url [{self.config.data['server_url']}]: ")
|
|
||||||
or self.config.data["server_url"]
|
|
||||||
)
|
|
||||||
self.config.data["server_url"] = LoginForm.remove_trailing_slash(
|
|
||||||
server_url.strip()
|
|
||||||
)
|
|
||||||
|
|
||||||
self.config.data["cipher_enabled"] = LoginForm.str_to_bool(
|
|
||||||
input(
|
|
||||||
f"enabled encryption(recommended) [{LoginForm.bool_to_str(self.config.data['cipher_enabled'])}]: "
|
|
||||||
)
|
|
||||||
or LoginForm.bool_to_str(self.config.data["cipher_enabled"])
|
|
||||||
)
|
|
||||||
|
|
||||||
self.config.data["notification"] = LoginForm.str_to_bool(
|
|
||||||
input(
|
|
||||||
f"enabled notification [{LoginForm.bool_to_str(self.config.data['notification'])}]: "
|
|
||||||
)
|
|
||||||
or LoginForm.bool_to_str(self.config.data["notification"])
|
|
||||||
)
|
|
||||||
|
|
||||||
# extra fields
|
|
||||||
show_extra_fields = input(f"show extra fields [n]: ") or "n"
|
|
||||||
if not LoginForm.str_to_bool(show_extra_fields):
|
|
||||||
CustomDialog("Logging in...").mainloop()
|
|
||||||
return
|
|
||||||
|
|
||||||
# Validate hash_rounds
|
|
||||||
while True:
|
|
||||||
hash_rounds = input(
|
|
||||||
f"hash rounds [{self.config.data['hash_rounds']}]: "
|
|
||||||
) or str(
|
|
||||||
""
|
|
||||||
if self.config.data["hash_rounds"] is None
|
|
||||||
else str(self.config.data["hash_rounds"])
|
|
||||||
)
|
|
||||||
hash_rounds = hash_rounds.strip()
|
|
||||||
|
|
||||||
if not LoginForm.is_positive_integer(hash_rounds):
|
|
||||||
CustomDialog(
|
|
||||||
"Invalid Input\nHash Rounds must be a positive integer.",
|
|
||||||
msg_type="error",
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
self.config.data["hash_rounds"] = int(hash_rounds)
|
|
||||||
break
|
|
||||||
|
|
||||||
self.config.data["salt"] = (
|
|
||||||
input(f"salt [{self.config.data['salt']}]: ") or self.config.data["salt"]
|
|
||||||
)
|
|
||||||
|
|
||||||
self.config.data["save_password"] = LoginForm.str_to_bool(
|
|
||||||
input(
|
|
||||||
f"store password locally(not recommended; only works if encryption is disabled) [{LoginForm.bool_to_str(self.config.data['save_password'])}]: "
|
|
||||||
)
|
|
||||||
or LoginForm.bool_to_str(self.config.data["save_password"])
|
|
||||||
)
|
|
||||||
|
|
||||||
# Validate max_clipboard_size_local_limit_bytes
|
|
||||||
while True:
|
|
||||||
saved_mcsll = str(
|
|
||||||
""
|
|
||||||
if self.config.data["max_clipboard_size_local_limit_bytes"] is None
|
|
||||||
else str(self.config.data["max_clipboard_size_local_limit_bytes"])
|
|
||||||
)
|
|
||||||
|
|
||||||
max_clipboard_size_local_limit_bytes = (
|
|
||||||
input(
|
|
||||||
f"maximum clipboard size local limit (in bytes) [{saved_mcsll}]: "
|
|
||||||
)
|
|
||||||
or saved_mcsll
|
|
||||||
)
|
|
||||||
max_clipboard_size_local_limit_bytes = (
|
|
||||||
max_clipboard_size_local_limit_bytes.strip()
|
|
||||||
)
|
|
||||||
if max_clipboard_size_local_limit_bytes == "":
|
|
||||||
self.config.data["max_clipboard_size_local_limit_bytes"] = None
|
|
||||||
break
|
|
||||||
|
|
||||||
if not LoginForm.is_positive_integer(max_clipboard_size_local_limit_bytes):
|
|
||||||
CustomDialog(
|
|
||||||
"Invalid Input\nMax Clipboard Size Local Limit must be a positive integer.",
|
|
||||||
msg_type="error",
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
self.config.data["max_clipboard_size_local_limit_bytes"] = int(
|
|
||||||
max_clipboard_size_local_limit_bytes
|
|
||||||
)
|
|
||||||
break
|
|
||||||
|
|
||||||
self.config.data["enable_image_sharing"] = LoginForm.str_to_bool(
|
|
||||||
input(
|
|
||||||
f"enable image sharing [{LoginForm.bool_to_str(self.config.data['enable_image_sharing'])}]: "
|
|
||||||
)
|
|
||||||
or LoginForm.bool_to_str(self.config.data["enable_image_sharing"])
|
|
||||||
)
|
|
||||||
|
|
||||||
self.config.data["enable_file_sharing"] = LoginForm.str_to_bool(
|
|
||||||
input(
|
|
||||||
f"enable file sharing [{LoginForm.bool_to_str(self.config.data['enable_file_sharing'])}]: "
|
|
||||||
)
|
|
||||||
or LoginForm.bool_to_str(self.config.data["enable_file_sharing"])
|
|
||||||
)
|
|
||||||
|
|
||||||
# Validate default file download location
|
|
||||||
while True:
|
|
||||||
default_file_download_location = (
|
|
||||||
input(
|
|
||||||
f"default file download location [{self.config.data['default_file_download_location']}]: "
|
|
||||||
)
|
|
||||||
or self.config.data["default_file_download_location"]
|
|
||||||
)
|
|
||||||
|
|
||||||
default_file_download_location = default_file_download_location.strip()
|
|
||||||
if default_file_download_location == "":
|
|
||||||
self.config.data["default_file_download_location"] = ""
|
|
||||||
break
|
|
||||||
|
|
||||||
if not os.path.isdir(default_file_download_location):
|
|
||||||
CustomDialog(
|
|
||||||
"Invalid directory path.",
|
|
||||||
msg_type="error",
|
|
||||||
).mainloop()
|
|
||||||
continue
|
|
||||||
|
|
||||||
self.config.data["default_file_download_location"] = (
|
|
||||||
default_file_download_location
|
|
||||||
)
|
|
||||||
break
|
|
||||||
|
|
||||||
while True:
|
|
||||||
ssl_ca_bundle = (
|
|
||||||
input(
|
|
||||||
f"ssl ca bundle path (optional) [{self.config.data.get('ssl_ca_bundle') or ''}]: "
|
|
||||||
)
|
|
||||||
or (self.config.data.get("ssl_ca_bundle") or "")
|
|
||||||
).strip()
|
|
||||||
if ssl_ca_bundle == "":
|
|
||||||
self.config.data["ssl_ca_bundle"] = ""
|
|
||||||
break
|
|
||||||
if not os.path.isfile(ssl_ca_bundle):
|
|
||||||
CustomDialog(
|
|
||||||
"Invalid Input\nSSL CA bundle path is not a file or does not exist.",
|
|
||||||
msg_type="error",
|
|
||||||
).mainloop()
|
|
||||||
continue
|
|
||||||
self.config.data["ssl_ca_bundle"] = ssl_ca_bundle
|
|
||||||
break
|
|
||||||
|
|
||||||
CustomDialog("Logging in...").mainloop()
|
|
||||||
|
|
||||||
def on_quit(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
pass
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
class MessageBox:
|
|
||||||
def askquestion(self, title, message):
|
|
||||||
response = input(f"{title}\n{message} (y/n): ").strip().lower()
|
|
||||||
return "yes" if (response.lower().strip() == "y") else "no"
|
|
||||||
@@ -1,367 +0,0 @@
|
|||||||
import math
|
|
||||||
import os
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
import webbrowser
|
|
||||||
|
|
||||||
|
|
||||||
from cli.echo import Echo
|
|
||||||
from cli.info import CustomDialog
|
|
||||||
from core.config import Config
|
|
||||||
from core.constants import *
|
|
||||||
from itertools import count
|
|
||||||
|
|
||||||
|
|
||||||
if PLATFORM != WINDOWS:
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
|
|
||||||
class TaskbarPanel:
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
on_connect_callback: callable = None,
|
|
||||||
on_disconnect_callback: callable = None,
|
|
||||||
on_logoff_callback: callable = None,
|
|
||||||
new_version_available: list = None,
|
|
||||||
github_url: str = GITHUB_URL,
|
|
||||||
donation_url: str = None,
|
|
||||||
ws_interface=None, # type= interfaces.ws_interface.WSInterface
|
|
||||||
config: Config = None,
|
|
||||||
):
|
|
||||||
self.on_connect_callback = on_connect_callback
|
|
||||||
self.on_disconnect_callback = on_disconnect_callback
|
|
||||||
self.on_logoff_callback = on_logoff_callback
|
|
||||||
self.new_version_available = new_version_available
|
|
||||||
self.github_url = github_url
|
|
||||||
self.donation_url = donation_url
|
|
||||||
self.ws_interface = ws_interface
|
|
||||||
self.config = config
|
|
||||||
|
|
||||||
self.is_disconnecting = False
|
|
||||||
self.disconnecting_items = None
|
|
||||||
self.is_file_download_enabled = False
|
|
||||||
self.file_download_items = None
|
|
||||||
self.previous_stats: str = ""
|
|
||||||
self.previous_stats_items = None
|
|
||||||
|
|
||||||
# Initial state: Connected
|
|
||||||
self.is_connected = True
|
|
||||||
|
|
||||||
self.menu_items = None
|
|
||||||
self.numbered_menu = None
|
|
||||||
self.loop_terminate = False
|
|
||||||
|
|
||||||
self.update_stats() # Start the stats update thread
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
Echo("-" * 53)
|
|
||||||
self.menu_items, self.numbered_menu = self.create_menu()
|
|
||||||
|
|
||||||
while not self.loop_terminate:
|
|
||||||
Echo("\n")
|
|
||||||
|
|
||||||
self.display_menu()
|
|
||||||
|
|
||||||
# input
|
|
||||||
choice = input("Choice: \n")
|
|
||||||
Echo("Please wait...")
|
|
||||||
if choice == "": # refresh
|
|
||||||
continue
|
|
||||||
|
|
||||||
if choice.isdigit():
|
|
||||||
choice = int(choice)
|
|
||||||
else:
|
|
||||||
Echo("Invalid choice. Please enter a number.")
|
|
||||||
|
|
||||||
if choice in self.numbered_menu:
|
|
||||||
fun = self.numbered_menu[choice][1]
|
|
||||||
if fun is not None:
|
|
||||||
fun()
|
|
||||||
else:
|
|
||||||
Echo("Invalid choice.")
|
|
||||||
|
|
||||||
def get_max_width(self):
|
|
||||||
"""Determine the maximum width of menu item texts."""
|
|
||||||
menu_texts = [
|
|
||||||
item[0] if isinstance(item, tuple) else "" for item in self.menu_items
|
|
||||||
]
|
|
||||||
return max(len(text) for text in menu_texts) + 1
|
|
||||||
|
|
||||||
def display_menu(self):
|
|
||||||
counter = count(1)
|
|
||||||
max_width = self.get_max_width()
|
|
||||||
|
|
||||||
total_width = max_width + 8
|
|
||||||
|
|
||||||
# Box Top Border
|
|
||||||
print("\n╔" + "═" * (max_width + 6) + "╗")
|
|
||||||
print(f"║{' MAIN MENU ':^{max_width + 6}}║")
|
|
||||||
print("╠" + "═" * (max_width + 6) + "╣")
|
|
||||||
|
|
||||||
# Menu Items
|
|
||||||
for item in self.menu_items:
|
|
||||||
if isinstance(item, tuple):
|
|
||||||
width = max_width
|
|
||||||
if "disconnect" in item[0].lower() or "logs" in item[0].lower():
|
|
||||||
if "disconnecting" in item[0].lower():
|
|
||||||
width -= 1
|
|
||||||
else:
|
|
||||||
width += 1
|
|
||||||
else:
|
|
||||||
width -= 1
|
|
||||||
print(f"║ {next(counter):<2}. {item[0]:<{width}} ║")
|
|
||||||
else:
|
|
||||||
print(f"║ {'─' * (max_width + 4)} ║")
|
|
||||||
|
|
||||||
# Box Bottom Border
|
|
||||||
print("╚" + "═" * (max_width + 6) + "╝")
|
|
||||||
|
|
||||||
def create_menu(self, item_: tuple = None):
|
|
||||||
# Menu items
|
|
||||||
menu_items = [
|
|
||||||
"", # Menu Separator
|
|
||||||
("🗒️ Open Logs", self._open_logs),
|
|
||||||
("📂 Program Files", self._open_program_location),
|
|
||||||
"", # Menu Separator
|
|
||||||
("🏠 Homepage", self._open_homepage),
|
|
||||||
("❓ Help", self._open_help),
|
|
||||||
("💟 Donate", self._open_donate),
|
|
||||||
("🌐 GitHub", self._open_github),
|
|
||||||
"", # Menu Separator
|
|
||||||
("🔒 Logoff and Quit", self._on_logoff),
|
|
||||||
("❌ Quit", self._on_quit),
|
|
||||||
]
|
|
||||||
|
|
||||||
# Add connect/disconnect option (top of the menu - 0 index)
|
|
||||||
if not self.is_connected:
|
|
||||||
menu_items.insert(0, ("🔗 Connect", self._on_connect))
|
|
||||||
else:
|
|
||||||
if self.is_disconnecting and self.disconnecting_items is not None:
|
|
||||||
menu_items.insert(
|
|
||||||
self.disconnecting_items[1],
|
|
||||||
(self.disconnecting_items[0], self.disconnecting_items[2]),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
menu_items.insert(0, ("⛓️💥 Disconnect", self._on_disconnect))
|
|
||||||
|
|
||||||
# Add update option (before the last 3 items)
|
|
||||||
if self.new_version_available is not None and self.new_version_available[0]:
|
|
||||||
menu_items.insert(
|
|
||||||
len(menu_items) - 3,
|
|
||||||
(
|
|
||||||
f"🔄 Update ({self.new_version_available[2]} ➞ {self.new_version_available[1]})",
|
|
||||||
self._on_update,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add files download option (top of the menu - 0 index)
|
|
||||||
if self.is_file_download_enabled and self.file_download_items is not None:
|
|
||||||
menu_items.insert(
|
|
||||||
self.file_download_items[1],
|
|
||||||
(self.file_download_items[0], self.file_download_items[2]),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add stats option (top of the menu - 0 index)
|
|
||||||
if self.previous_stats_items is not None:
|
|
||||||
menu_items.insert(
|
|
||||||
self.previous_stats_items[1],
|
|
||||||
(self.previous_stats_items[0], self.previous_stats_items[2]),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create numbered menu
|
|
||||||
counter = count(1)
|
|
||||||
numbered_menu = {
|
|
||||||
next(counter): item for item in menu_items if isinstance(item, tuple)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (menu_items, numbered_menu)
|
|
||||||
|
|
||||||
def update_menu(self, item_: tuple = None, no_dialog=False):
|
|
||||||
self.menu_items, self.numbered_menu = self.create_menu(item_=item_)
|
|
||||||
if not no_dialog:
|
|
||||||
CustomDialog(
|
|
||||||
"Menu Updated! Press Enter to Refresh. ⏎", msg_type="info"
|
|
||||||
).mainloop()
|
|
||||||
|
|
||||||
def update_stats(self):
|
|
||||||
threading.Thread(target=self._update_stats_thread, daemon=True).start()
|
|
||||||
|
|
||||||
def _update_stats_thread(self):
|
|
||||||
while True:
|
|
||||||
current_stats = self.ws_interface.get_stats()
|
|
||||||
if current_stats is not None and self.previous_stats != current_stats:
|
|
||||||
self.previous_stats = current_stats
|
|
||||||
self.previous_stats_items = (current_stats, 0, None)
|
|
||||||
self.update_menu(no_dialog=True)
|
|
||||||
time.sleep(1) # Sleep for 1 second
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def open_webbrowser(url):
|
|
||||||
try:
|
|
||||||
webbrowser.open(url)
|
|
||||||
except Exception as e:
|
|
||||||
CustomDialog(
|
|
||||||
f"Failed to open the browser. Here is the URL: {url}\nError: {e}",
|
|
||||||
msg_type="error",
|
|
||||||
).mainloop()
|
|
||||||
|
|
||||||
def _on_update(self):
|
|
||||||
TaskbarPanel.open_webbrowser(self.new_version_available[3])
|
|
||||||
|
|
||||||
def _on_connect(self):
|
|
||||||
if self.on_connect_callback:
|
|
||||||
try:
|
|
||||||
self.on_connect_callback()
|
|
||||||
except Exception as e:
|
|
||||||
pass
|
|
||||||
self.is_connected = True
|
|
||||||
self.update_menu(no_dialog=True)
|
|
||||||
|
|
||||||
def _on_disconnect(self):
|
|
||||||
if self.on_disconnect_callback:
|
|
||||||
self.on_disconnect_callback()
|
|
||||||
if self.ws_interface is not None and self.ws_interface.is_auto_reconnecting:
|
|
||||||
threading.Thread(target=self._wait_to_disconnect, daemon=True).start()
|
|
||||||
else:
|
|
||||||
self.is_connected = False
|
|
||||||
self.update_menu(no_dialog=True)
|
|
||||||
|
|
||||||
def _wait_to_disconnect(self):
|
|
||||||
"""
|
|
||||||
Wait for the auto-reconnect timer to expire before disconnecting.
|
|
||||||
"""
|
|
||||||
if self.ws_interface is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
timeout = math.ceil(self.ws_interface.get_total_timeout() / 1000)
|
|
||||||
while timeout > 0:
|
|
||||||
self.is_disconnecting = True
|
|
||||||
self.disconnecting_items = (
|
|
||||||
f"⏳ Disconnecting... ({timeout} sec)",
|
|
||||||
0,
|
|
||||||
None,
|
|
||||||
) # text, location, callback
|
|
||||||
self.update_menu(no_dialog=True)
|
|
||||||
time.sleep(1) # seconds
|
|
||||||
timeout -= 1
|
|
||||||
self.is_disconnecting = False
|
|
||||||
self.disconnecting_items = None
|
|
||||||
self.ws_interface.is_auto_reconnecting = False
|
|
||||||
self.is_connected = False
|
|
||||||
self.update_menu()
|
|
||||||
|
|
||||||
def _open_homepage(self):
|
|
||||||
TaskbarPanel.open_webbrowser(self.config.data["server_url"])
|
|
||||||
|
|
||||||
def _open_github(self):
|
|
||||||
TaskbarPanel.open_webbrowser(self.github_url)
|
|
||||||
|
|
||||||
def _open_help(self):
|
|
||||||
TaskbarPanel.open_webbrowser(HELP_URL)
|
|
||||||
|
|
||||||
def _open_donate(self):
|
|
||||||
if self.donation_url is not None:
|
|
||||||
TaskbarPanel.open_webbrowser(self.donation_url)
|
|
||||||
|
|
||||||
def open_location(self, path):
|
|
||||||
if PLATFORM == WINDOWS:
|
|
||||||
os.startfile(path)
|
|
||||||
elif PLATFORM == MACOS:
|
|
||||||
subprocess.run(["open", path])
|
|
||||||
elif PLATFORM.startswith(LINUX):
|
|
||||||
subprocess.run(["xdg-open", path])
|
|
||||||
|
|
||||||
def _open_logs(self):
|
|
||||||
log_file_path = os.path.join(get_program_files_directory(), LOG_FILE_NAME)
|
|
||||||
if os.path.exists(log_file_path):
|
|
||||||
try:
|
|
||||||
self.open_location(log_file_path)
|
|
||||||
except Exception as e:
|
|
||||||
CustomDialog(
|
|
||||||
f"Failed to open the log file '{log_file_path}'.\nError: {e}",
|
|
||||||
msg_type="error",
|
|
||||||
).mainloop()
|
|
||||||
else:
|
|
||||||
CustomDialog(
|
|
||||||
f"Log file not found at '{log_file_path}'.", msg_type="error"
|
|
||||||
).mainloop()
|
|
||||||
|
|
||||||
def _open_program_location(self):
|
|
||||||
try:
|
|
||||||
program_location = get_program_files_directory()
|
|
||||||
self.open_location(program_location)
|
|
||||||
except Exception as e:
|
|
||||||
CustomDialog(
|
|
||||||
f"Failed to open the program location '{program_location}'.\nError: {e}",
|
|
||||||
msg_type="error",
|
|
||||||
).mainloop()
|
|
||||||
|
|
||||||
def enable_files_download(self, files):
|
|
||||||
self.is_file_download_enabled = True
|
|
||||||
self.file_download_items = (
|
|
||||||
"📥 Download File(s)",
|
|
||||||
0,
|
|
||||||
lambda: self._on_download(files),
|
|
||||||
)
|
|
||||||
self.update_menu()
|
|
||||||
|
|
||||||
def disable_files_download(self):
|
|
||||||
self.is_file_download_enabled = False
|
|
||||||
self.file_download_items = None
|
|
||||||
self.update_menu()
|
|
||||||
|
|
||||||
def _on_download(self, files):
|
|
||||||
try:
|
|
||||||
if self.config.data["default_file_download_location"] != "":
|
|
||||||
target_directory = self.config.data["default_file_download_location"]
|
|
||||||
else:
|
|
||||||
target_directory = input("Enter the location to save file(s): ").strip()
|
|
||||||
if not target_directory:
|
|
||||||
CustomDialog(
|
|
||||||
"No input provided. Please enter a valid directory path.",
|
|
||||||
msg_type="error",
|
|
||||||
).mainloop()
|
|
||||||
return
|
|
||||||
|
|
||||||
if not os.path.isdir(target_directory):
|
|
||||||
CustomDialog("Invalid directory path.", msg_type="error").mainloop()
|
|
||||||
return
|
|
||||||
|
|
||||||
if not os.access(target_directory, os.W_OK):
|
|
||||||
CustomDialog(
|
|
||||||
"Insufficient permissions to write to the directory.",
|
|
||||||
msg_type="error",
|
|
||||||
).mainloop()
|
|
||||||
return
|
|
||||||
|
|
||||||
CustomDialog(
|
|
||||||
f"Saving files to: {target_directory}", msg_type="info"
|
|
||||||
).mainloop()
|
|
||||||
# Save each file to the chosen directory
|
|
||||||
for filename, file_obj in files.items():
|
|
||||||
file_path = os.path.join(target_directory, filename)
|
|
||||||
with open(file_path, "wb") as f:
|
|
||||||
f.write(file_obj.getvalue())
|
|
||||||
logging.debug(f"Saved: {file_path}")
|
|
||||||
CustomDialog("Done.", msg_type="success").mainloop()
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
msg = f"An error occurred while downloading files. Error: {e}"
|
|
||||||
logging.error(msg)
|
|
||||||
CustomDialog(
|
|
||||||
msg,
|
|
||||||
msg_type="error",
|
|
||||||
).mainloop()
|
|
||||||
|
|
||||||
def _on_logoff(self):
|
|
||||||
try:
|
|
||||||
if self.on_logoff_callback:
|
|
||||||
self.on_logoff_callback()
|
|
||||||
self._on_quit()
|
|
||||||
except Exception as e:
|
|
||||||
CustomDialog(
|
|
||||||
f"An error occurred while logging off: {e}", msg_type="error"
|
|
||||||
).mainloop()
|
|
||||||
|
|
||||||
def _on_quit(self):
|
|
||||||
self.loop_terminate = True
|
|
||||||
@@ -1,464 +0,0 @@
|
|||||||
import base64
|
|
||||||
import io
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
import xxhash
|
|
||||||
|
|
||||||
|
|
||||||
from PIL import Image
|
|
||||||
from core.constants import *
|
|
||||||
from core.config import Config
|
|
||||||
|
|
||||||
if PLATFORM.startswith(LINUX) and LINUX_USE_CLI_UI:
|
|
||||||
from cli.tray import TaskbarPanel
|
|
||||||
else:
|
|
||||||
from gui.tray import TaskbarPanel
|
|
||||||
|
|
||||||
if PLATFORM == WINDOWS or PLATFORM == MACOS:
|
|
||||||
import pyperclip
|
|
||||||
|
|
||||||
|
|
||||||
if PLATFORM == WINDOWS:
|
|
||||||
import win32clipboard
|
|
||||||
from clipboard import clipboard_monitor_win as clipboard_monitor
|
|
||||||
elif PLATFORM == MACOS:
|
|
||||||
import pasteboard
|
|
||||||
from clipboard import clipboard_monitor_mac as clipboard_monitor
|
|
||||||
elif PLATFORM.startswith(LINUX):
|
|
||||||
from clipboard import clipboard_monitor_linux as clipboard_monitor
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
|
|
||||||
class ClipboardManager:
|
|
||||||
def __init__(self, config: Config):
|
|
||||||
self.config = config
|
|
||||||
self.previous_clipboard_hash = 0
|
|
||||||
self.sys_tray: TaskbarPanel = None
|
|
||||||
self.is_files_download_enabled = False
|
|
||||||
|
|
||||||
if PLATFORM.startswith(LINUX) and XMODE:
|
|
||||||
self.is_x_clipboard_owner = clipboard_monitor.is_x_clipboard_owner()
|
|
||||||
|
|
||||||
def set_tray_ref(self, sys_tray: TaskbarPanel):
|
|
||||||
"""
|
|
||||||
Sets the system tray reference.
|
|
||||||
"""
|
|
||||||
self.sys_tray = sys_tray
|
|
||||||
|
|
||||||
def reset_files_download(self):
|
|
||||||
"""
|
|
||||||
Resets the files download flag and disables the download functionality in the system tray if enabled.
|
|
||||||
"""
|
|
||||||
if self.is_files_download_enabled:
|
|
||||||
self.is_files_download_enabled = False
|
|
||||||
if self.sys_tray:
|
|
||||||
self.sys_tray.disable_files_download()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def hash_clipboard(clipboard: str) -> int:
|
|
||||||
return xxhash.xxh64(clipboard).intdigest()
|
|
||||||
|
|
||||||
def is_clipboard_size_within_limit(
|
|
||||||
self, clipboard_content: any, type_: str = "text"
|
|
||||||
) -> bool:
|
|
||||||
if clipboard_content is None:
|
|
||||||
raise ValueError("Clipboard content cannot be None")
|
|
||||||
|
|
||||||
content_size_in_bytes = None
|
|
||||||
if type_ == "text":
|
|
||||||
content_size_in_bytes = len(clipboard_content.encode("utf-8"))
|
|
||||||
elif type_ == "image":
|
|
||||||
content_size_in_bytes = ClipboardManager.get_image_size(
|
|
||||||
img=clipboard_content
|
|
||||||
)
|
|
||||||
elif type_ == "files":
|
|
||||||
content_size_in_bytes = ClipboardManager.calculate_cumulative_file_size(
|
|
||||||
files=clipboard_content
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check if the content size exceeds the server limit
|
|
||||||
max_allowed_size = self.config.data["maxsize"]
|
|
||||||
if (
|
|
||||||
max_allowed_size is not None
|
|
||||||
and max_allowed_size >= 0
|
|
||||||
and content_size_in_bytes > max_allowed_size
|
|
||||||
):
|
|
||||||
logging.warning(
|
|
||||||
"Clipboard content size exceeds the maximum allowed limit. "
|
|
||||||
f"Allowed: {max_allowed_size} bytes, Found: {content_size_in_bytes} bytes."
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Check if the content size exceeds the local limit
|
|
||||||
local_clipboard_size_limit = self.config.data[
|
|
||||||
"max_clipboard_size_local_limit_bytes"
|
|
||||||
]
|
|
||||||
if local_clipboard_size_limit is not None and local_clipboard_size_limit >= 0:
|
|
||||||
if content_size_in_bytes > local_clipboard_size_limit:
|
|
||||||
logging.warning(
|
|
||||||
"Clipboard content size exceeds the local allowed limit. "
|
|
||||||
f"Allowed: {local_clipboard_size_limit} bytes, Found: {content_size_in_bytes} bytes."
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def has_clipboard_changed(self, payload: str) -> bool:
|
|
||||||
"""
|
|
||||||
Check if the clipboard content has changed by comparing the current hash
|
|
||||||
with the previous clipboard hash.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
- payload: The current clipboard content.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
- True if the clipboard content has changed, False otherwise.
|
|
||||||
"""
|
|
||||||
current_clipboard_hash = ClipboardManager.hash_clipboard(payload)
|
|
||||||
if current_clipboard_hash != self.previous_clipboard_hash:
|
|
||||||
self.previous_clipboard_hash = current_clipboard_hash
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def on_copy(self, copy_callback):
|
|
||||||
clipboard_monitor.on_update(
|
|
||||||
callback=lambda type_, content: self.clipboard_to_base64(
|
|
||||||
copy_callback, content, type_
|
|
||||||
),
|
|
||||||
enable_image_monitoring=self.config.data["enable_image_sharing"],
|
|
||||||
enable_file_monitoring=self.config.data["enable_file_sharing"],
|
|
||||||
)
|
|
||||||
|
|
||||||
def clipboard_to_base64(self, callback, content: any, type_: str = "text"):
|
|
||||||
try:
|
|
||||||
self.reset_files_download()
|
|
||||||
|
|
||||||
type_ = type_.lower()
|
|
||||||
if type_ == "text":
|
|
||||||
if self.is_clipboard_size_within_limit(content, type_):
|
|
||||||
callback(content, type_)
|
|
||||||
|
|
||||||
elif type_ == "image":
|
|
||||||
if isinstance(content, list):
|
|
||||||
if content is None or len(content) == 0:
|
|
||||||
raise ValueError(
|
|
||||||
"Clipboard image content cannot be None or empty"
|
|
||||||
)
|
|
||||||
|
|
||||||
content = Image.open(content[0])
|
|
||||||
if self.is_clipboard_size_within_limit(content, type_):
|
|
||||||
content_str = ClipboardManager.convert_image_to_base64(img=content)
|
|
||||||
callback(content_str, type_)
|
|
||||||
|
|
||||||
elif type_ == "files":
|
|
||||||
if PLATFORM.startswith(LINUX):
|
|
||||||
if content is not None and len(content) > 0:
|
|
||||||
temp = []
|
|
||||||
for path in content:
|
|
||||||
if path.startswith("file:"):
|
|
||||||
temp.append(path[5:])
|
|
||||||
else:
|
|
||||||
temp.append(path)
|
|
||||||
content = temp
|
|
||||||
|
|
||||||
if self.is_clipboard_size_within_limit(content, type_):
|
|
||||||
content_str = ClipboardManager.convert_files_to_base64(
|
|
||||||
file_paths=content
|
|
||||||
)
|
|
||||||
if content_str != "{}": # Check if the JSON string is empty
|
|
||||||
callback(content_str, type_)
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Failed to convert clipboard data to base64: {e}")
|
|
||||||
|
|
||||||
def base64_to_clipboard(self, base64_string: str, type_: str = "text"):
|
|
||||||
try:
|
|
||||||
if type_ == "text":
|
|
||||||
txt = base64_string
|
|
||||||
if self.is_clipboard_size_within_limit(txt, type_):
|
|
||||||
self.paste(txt, type_)
|
|
||||||
elif type_ == "image":
|
|
||||||
img = ClipboardManager.convert_base64_to_image(base64_img=base64_string)
|
|
||||||
if self.is_clipboard_size_within_limit(img, type_):
|
|
||||||
self.paste(img, type_)
|
|
||||||
elif type_ == "files":
|
|
||||||
file_objects = ClipboardManager.convert_base64_to_files(
|
|
||||||
base64_json=base64_string
|
|
||||||
)
|
|
||||||
if self.is_clipboard_size_within_limit(file_objects, type_):
|
|
||||||
self.paste(file_objects, type_)
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Failed to convert base64 data to clipboard: {e}")
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def execute_command(*args, input_data):
|
|
||||||
"""
|
|
||||||
Execute a command with input data.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
- *args: Positional arguments for the command.
|
|
||||||
- input_data: Input data to be passed to the command.
|
|
||||||
"""
|
|
||||||
if PLATFORM.startswith(LINUX):
|
|
||||||
try:
|
|
||||||
subprocess.run(
|
|
||||||
args,
|
|
||||||
input=input_data,
|
|
||||||
check=True,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Failed to execute command: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
def paste(self, payload: any, payload_type: str = "text"):
|
|
||||||
try:
|
|
||||||
self.reset_files_download()
|
|
||||||
|
|
||||||
if payload_type == "text":
|
|
||||||
if PLATFORM == WINDOWS or PLATFORM == MACOS:
|
|
||||||
pyperclip.copy(payload)
|
|
||||||
elif PLATFORM.startswith(LINUX):
|
|
||||||
if XMODE and self.is_x_clipboard_owner:
|
|
||||||
ClipboardManager.execute_command(
|
|
||||||
"xclip",
|
|
||||||
"-selection",
|
|
||||||
"clipboard",
|
|
||||||
input_data=payload.encode("utf-8"),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
ClipboardManager.execute_command(
|
|
||||||
"wl-copy",
|
|
||||||
input_data=payload.encode("utf-8"),
|
|
||||||
)
|
|
||||||
|
|
||||||
elif payload_type == "image":
|
|
||||||
if PLATFORM == WINDOWS:
|
|
||||||
# Save the image to a binary buffer in BMP format
|
|
||||||
with io.BytesIO() as output:
|
|
||||||
payload.convert("RGB").save(output, format="BMP")
|
|
||||||
bmp_data = output.getvalue()[14:] # Skip BMP header (14 bytes)
|
|
||||||
|
|
||||||
win32clipboard.OpenClipboard()
|
|
||||||
win32clipboard.EmptyClipboard()
|
|
||||||
clipboard_monitor.enable_block_image_once() # Block image copy to prevent deadlock
|
|
||||||
win32clipboard.SetClipboardData(win32clipboard.CF_DIB, bmp_data)
|
|
||||||
win32clipboard.CloseClipboard()
|
|
||||||
elif PLATFORM == MACOS:
|
|
||||||
# Save the image to a binary buffer in TIFF format
|
|
||||||
with io.BytesIO() as output:
|
|
||||||
payload.convert("RGB").save(output, format="TIFF")
|
|
||||||
tiff_data = output.getvalue()
|
|
||||||
|
|
||||||
clipboard_monitor.enable_block_image_once() # Block image copy to prevent deadlock
|
|
||||||
clipboard_monitor.write_to_pasteboard(tiff_data, pasteboard.TIFF)
|
|
||||||
elif PLATFORM.startswith(LINUX):
|
|
||||||
# Save the image to a binary buffer in PNG format
|
|
||||||
with io.BytesIO() as output:
|
|
||||||
payload.convert("RGB").save(output, format="PNG")
|
|
||||||
png_data = output.getvalue()
|
|
||||||
|
|
||||||
clipboard_monitor.enable_block_image_once() # Block image copy to prevent deadlock
|
|
||||||
if XMODE and self.is_x_clipboard_owner:
|
|
||||||
ClipboardManager.execute_command(
|
|
||||||
"xclip",
|
|
||||||
"-selection",
|
|
||||||
"clipboard",
|
|
||||||
"-t",
|
|
||||||
"image/png",
|
|
||||||
input_data=png_data,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
ClipboardManager.execute_command(
|
|
||||||
"wl-copy",
|
|
||||||
"--type",
|
|
||||||
"image/png",
|
|
||||||
input_data=png_data,
|
|
||||||
)
|
|
||||||
|
|
||||||
elif payload_type == "files":
|
|
||||||
if (
|
|
||||||
payload is not None
|
|
||||||
and isinstance(payload, dict)
|
|
||||||
and len(payload) > 0
|
|
||||||
):
|
|
||||||
if self.sys_tray is not None:
|
|
||||||
self.is_files_download_enabled = True
|
|
||||||
self.sys_tray.enable_files_download(files=payload)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Failed to copy data to clipboard: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
self.reset_files_download()
|
|
||||||
clipboard_monitor.stop()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def calculate_cumulative_file_size(files: tuple | list | dict) -> int:
|
|
||||||
"""
|
|
||||||
Calculate the cumulative size of a list of files.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
files (tuple or list): A tuple of file paths.
|
|
||||||
files (dict): A dictionary of files with file names as keys and file object as values.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
int: The cumulative size of the files in bytes.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
IOError: If a file cannot be read or processed.
|
|
||||||
"""
|
|
||||||
cumulative_size = 0
|
|
||||||
if isinstance(files, tuple | list):
|
|
||||||
for file_path in files:
|
|
||||||
try:
|
|
||||||
if os.path.isfile(file_path):
|
|
||||||
cumulative_size += os.path.getsize(file_path)
|
|
||||||
except Exception as e:
|
|
||||||
raise IOError(
|
|
||||||
f"Failed to calculate size for file '{file_path}' {e}."
|
|
||||||
) from e
|
|
||||||
|
|
||||||
if isinstance(files, dict):
|
|
||||||
for file_name, file_object in files.items():
|
|
||||||
try:
|
|
||||||
cumulative_size += file_object.getbuffer().nbytes
|
|
||||||
except Exception as e:
|
|
||||||
raise IOError(
|
|
||||||
f"Failed to calculate size for file '{file_name}' {e}."
|
|
||||||
) from e
|
|
||||||
|
|
||||||
return cumulative_size
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def convert_files_to_base64(file_paths: tuple | list) -> str:
|
|
||||||
"""
|
|
||||||
Converts a list of files to base64-encoded strings and returns them as a JSON string.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
file_paths (tuple or list): A tuple of file paths to be converted.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: A JSON string where file names are keys and base64-encoded content is values.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
IOError: If a file cannot be read or processed.
|
|
||||||
"""
|
|
||||||
base64_encoded_files = {}
|
|
||||||
for file_path in file_paths:
|
|
||||||
try:
|
|
||||||
if os.path.isfile(file_path):
|
|
||||||
file_name = os.path.basename(file_path)
|
|
||||||
with open(file_path, "rb") as file:
|
|
||||||
encoded_string = base64.b64encode(file.read()).decode("utf-8")
|
|
||||||
base64_encoded_files[file_name] = encoded_string
|
|
||||||
except Exception as e:
|
|
||||||
raise IOError(f"Failed to process file '{file_path}'. {e}") from e
|
|
||||||
|
|
||||||
return json.dumps(base64_encoded_files)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def convert_base64_to_files(base64_json: dict) -> dict:
|
|
||||||
"""
|
|
||||||
Converts a JSON string with base64-encoded file content to a dictionary of file-like objects.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
base64_json (str): A JSON string where file names are keys and base64-encoded content is values.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: A dictionary where keys are file names, and values are BytesIO objects containing the decoded file data.
|
|
||||||
"""
|
|
||||||
file_objects = {}
|
|
||||||
try:
|
|
||||||
base64_data = json.loads(base64_json)
|
|
||||||
for file_name, encoded_content in base64_data.items():
|
|
||||||
decoded_content = base64.b64decode(encoded_content)
|
|
||||||
file_objects[file_name] = io.BytesIO(decoded_content)
|
|
||||||
except Exception as e:
|
|
||||||
raise IOError(f"Error processing base64 JSON. {e}") from e
|
|
||||||
|
|
||||||
return file_objects
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_image_size(img: Image.Image | bytes) -> int:
|
|
||||||
"""
|
|
||||||
Calculate the size of an image in bytes.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
img (Image.Image | bytes): The image to calculate the size for.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
int: The size of the image in bytes.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
IOError: If the image cannot be processed or saved.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
if isinstance(img, bytes):
|
|
||||||
size_in_bytes = len(img)
|
|
||||||
if isinstance(img, Image.Image):
|
|
||||||
with io.BytesIO() as buffer:
|
|
||||||
img.save(buffer, format=img.format or "PNG")
|
|
||||||
size_in_bytes = buffer.tell()
|
|
||||||
|
|
||||||
return size_in_bytes
|
|
||||||
except Exception as e:
|
|
||||||
raise IOError(f"Failed to calculate the image size. {e}") from e
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def convert_image_to_base64(img: Image.Image | bytes) -> str:
|
|
||||||
"""
|
|
||||||
Converts an image to a base64-encoded string.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
img (Image.Image | bytes): The image to be converted.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: The base64-encoded string representation of the image.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
IOError: If the image cannot be processed or saved.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
if isinstance(img, bytes):
|
|
||||||
base64_string = base64.b64encode(img).decode("utf-8")
|
|
||||||
if isinstance(img, Image.Image):
|
|
||||||
with io.BytesIO() as buffered:
|
|
||||||
img.save(buffered, format=img.format or "PNG")
|
|
||||||
base64_string = base64.b64encode(buffered.getvalue()).decode(
|
|
||||||
"utf-8"
|
|
||||||
)
|
|
||||||
|
|
||||||
return base64_string
|
|
||||||
except Exception as e:
|
|
||||||
raise IOError(f"Failed to convert the image to base64. {e}") from e
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def convert_base64_to_image(base64_img: str) -> Image.Image:
|
|
||||||
"""
|
|
||||||
Converts a base64-encoded string to a PIL Image object.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
base64_img (str): The base64-encoded string to be converted.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Image.Image: A PIL Image object representing the decoded image.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If the base64 string is invalid.
|
|
||||||
IOError: If the image cannot be processed.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# Decode the base64 string
|
|
||||||
image_data = base64.b64decode(base64_img)
|
|
||||||
except base64.binascii.Error as e:
|
|
||||||
raise ValueError("Invalid base64 string.") from e
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Load the image from the decoded bytes
|
|
||||||
with io.BytesIO(image_data) as image_stream:
|
|
||||||
image = Image.open(image_stream)
|
|
||||||
image.load()
|
|
||||||
return image
|
|
||||||
except IOError as e:
|
|
||||||
raise IOError(f"Failed to process the image. {e}") from e
|
|
||||||
@@ -1,486 +0,0 @@
|
|||||||
import logging
|
|
||||||
import re
|
|
||||||
import subprocess
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
|
|
||||||
from core.constants import *
|
|
||||||
|
|
||||||
_callback_update = None
|
|
||||||
_clipboard_thread = None
|
|
||||||
|
|
||||||
_block_image_once = False
|
|
||||||
|
|
||||||
_is_gdk_running = False
|
|
||||||
_run_poll = threading.Event()
|
|
||||||
_wl_watch_proc = None
|
|
||||||
|
|
||||||
|
|
||||||
def _on_clipboard_changed(
|
|
||||||
clipboard, event=None, enable_image_monitoring=False, enable_file_monitoring=False
|
|
||||||
):
|
|
||||||
global _block_image_once
|
|
||||||
|
|
||||||
# Files
|
|
||||||
if enable_file_monitoring:
|
|
||||||
uris = clipboard.wait_for_uris()
|
|
||||||
if uris is not None and len(uris) > 0:
|
|
||||||
if _callback_update:
|
|
||||||
_callback_update("files", uris)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Text
|
|
||||||
text = clipboard.wait_for_text()
|
|
||||||
if text is not None and len(text) > 0:
|
|
||||||
if _callback_update:
|
|
||||||
_callback_update("text", text)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Image
|
|
||||||
if enable_image_monitoring:
|
|
||||||
pixbuf = clipboard.wait_for_image()
|
|
||||||
if pixbuf is not None:
|
|
||||||
if _block_image_once:
|
|
||||||
_block_image_once = False
|
|
||||||
return
|
|
||||||
if _callback_update:
|
|
||||||
success, buffer = pixbuf.save_to_bufferv("png")
|
|
||||||
if success:
|
|
||||||
_callback_update("image", bytes(buffer))
|
|
||||||
else:
|
|
||||||
logging.error("Failed to convert image(pixbuf) to buffer")
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
def _monitor_x_wl_clipboard(
|
|
||||||
x_mode: bool,
|
|
||||||
enable_image_monitoring=False,
|
|
||||||
enable_file_monitoring=False,
|
|
||||||
):
|
|
||||||
global _block_image_once
|
|
||||||
last_error = None
|
|
||||||
previous_clipboard: str | bytes | None = None
|
|
||||||
ignore_patterns = [
|
|
||||||
r"target .+ not available", # xclip pattern
|
|
||||||
r"no suitable type of content copied", # wl-clipboard pattern
|
|
||||||
]
|
|
||||||
|
|
||||||
if LINUX_CLIPBOARD_POLL_INTERVAL_SEC is not None:
|
|
||||||
timeout = LINUX_CLIPBOARD_POLL_INTERVAL_SEC
|
|
||||||
logging.info(f"Clipboard polling interval (--polling): {timeout}s")
|
|
||||||
elif x_mode:
|
|
||||||
timeout = 0.3 # xclip seconds
|
|
||||||
else:
|
|
||||||
timeout = 3 # wl-clipboard seconds
|
|
||||||
|
|
||||||
while _run_poll.is_set():
|
|
||||||
if x_mode:
|
|
||||||
success, mime_list = execute_command(
|
|
||||||
"xclip", "-selection", "clipboard", "-t", "TARGETS", "-o"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
success, mime_list = execute_command("wl-paste", "-l")
|
|
||||||
if not success:
|
|
||||||
error_msg = f"Failed to retrieve MIME types: {mime_list}"
|
|
||||||
if error_msg != last_error:
|
|
||||||
logging.error(error_msg)
|
|
||||||
last_error = error_msg
|
|
||||||
time.sleep(timeout)
|
|
||||||
continue
|
|
||||||
|
|
||||||
mime_list = mime_list.decode("utf-8")
|
|
||||||
mime_list = mime_list.replace("\r\n", "\n").replace("\r", "\n").split("\n")
|
|
||||||
mime_list = [m.strip() for m in mime_list if len(m.strip()) > 0]
|
|
||||||
type_ = convert_mime_to_generic_type(mime_list)
|
|
||||||
|
|
||||||
# Text
|
|
||||||
if type_ == "text":
|
|
||||||
if x_mode:
|
|
||||||
success, text = execute_command(
|
|
||||||
"xclip", "-selection", "clipboard", "-o"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
success, text = execute_command("wl-paste", "-n")
|
|
||||||
if success:
|
|
||||||
text = text.decode("utf-8")
|
|
||||||
if len(text) > 0 and text != previous_clipboard:
|
|
||||||
previous_clipboard = text
|
|
||||||
if _callback_update:
|
|
||||||
_callback_update("text", text)
|
|
||||||
else:
|
|
||||||
error_msg = f"Failed to retrieve text content from clipboard. {text}"
|
|
||||||
if error_msg != last_error:
|
|
||||||
if not any(
|
|
||||||
re.search(pattern, error_msg.lower())
|
|
||||||
for pattern in ignore_patterns
|
|
||||||
):
|
|
||||||
logging.error(error_msg)
|
|
||||||
last_error = error_msg
|
|
||||||
|
|
||||||
# Image
|
|
||||||
if type_ == "image" and enable_image_monitoring:
|
|
||||||
if x_mode:
|
|
||||||
success, image = execute_command(
|
|
||||||
"xclip",
|
|
||||||
"-selection",
|
|
||||||
"clipboard",
|
|
||||||
"-t",
|
|
||||||
"image/png",
|
|
||||||
"-o",
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
success, image = execute_command("wl-paste", "-t", "image/png")
|
|
||||||
if success:
|
|
||||||
if image != previous_clipboard:
|
|
||||||
previous_clipboard = image
|
|
||||||
if _callback_update and not _block_image_once:
|
|
||||||
_callback_update("image", image)
|
|
||||||
else:
|
|
||||||
_block_image_once = False
|
|
||||||
else:
|
|
||||||
error_msg = f"Failed to retrieve image content from clipboard. {image}"
|
|
||||||
if error_msg != last_error:
|
|
||||||
if not any(
|
|
||||||
re.search(pattern, error_msg.lower())
|
|
||||||
for pattern in ignore_patterns
|
|
||||||
):
|
|
||||||
logging.error(error_msg)
|
|
||||||
last_error = error_msg
|
|
||||||
|
|
||||||
# Files
|
|
||||||
if type_ == "files" and enable_file_monitoring:
|
|
||||||
if x_mode:
|
|
||||||
success, files = execute_command(
|
|
||||||
"xclip",
|
|
||||||
"-selection",
|
|
||||||
"clipboard",
|
|
||||||
"-t",
|
|
||||||
"text/uri-list",
|
|
||||||
"-o",
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
success, files = execute_command(
|
|
||||||
"wl-paste", "-t", "text/uri-list", "-n"
|
|
||||||
)
|
|
||||||
if success:
|
|
||||||
files = files.decode("utf-8")
|
|
||||||
files = files.replace("\r\n", "\n").replace("\r", "\n").split("\n")
|
|
||||||
files = [f.strip() for f in files if len(f.strip()) > 0]
|
|
||||||
if files != previous_clipboard:
|
|
||||||
previous_clipboard = files
|
|
||||||
if _callback_update:
|
|
||||||
_callback_update("files", files)
|
|
||||||
else:
|
|
||||||
error_msg = f"Failed to retrieve files content from clipboard. {files}"
|
|
||||||
if error_msg != last_error:
|
|
||||||
if not any(
|
|
||||||
re.search(pattern, error_msg.lower())
|
|
||||||
for pattern in ignore_patterns
|
|
||||||
):
|
|
||||||
logging.error(error_msg)
|
|
||||||
last_error = error_msg
|
|
||||||
|
|
||||||
time.sleep(timeout)
|
|
||||||
|
|
||||||
|
|
||||||
def _monitor_wl_watch(enable_image_monitoring=False, enable_file_monitoring=False):
|
|
||||||
"""Event-driven Wayland clipboard monitoring using wl-paste --watch.
|
|
||||||
Uses the wlr-data-control-v1 protocol which does not create visible
|
|
||||||
surfaces or steal focus. Supported by wlroots-based compositors
|
|
||||||
(Sway, Hyprland, etc.) and KDE Plasma on Wayland.
|
|
||||||
Returns True if watch mode ran successfully, False to fall back to polling."""
|
|
||||||
global _block_image_once, _wl_watch_proc
|
|
||||||
|
|
||||||
last_error = None
|
|
||||||
previous_clipboard = None
|
|
||||||
ignore_patterns = [
|
|
||||||
r"target .+ not available",
|
|
||||||
r"no suitable type of content copied",
|
|
||||||
]
|
|
||||||
|
|
||||||
try:
|
|
||||||
_wl_watch_proc = subprocess.Popen(
|
|
||||||
["wl-paste", "--watch", "echo"],
|
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
stderr=subprocess.PIPE,
|
|
||||||
)
|
|
||||||
|
|
||||||
time.sleep(0.5)
|
|
||||||
if _wl_watch_proc.poll() is not None:
|
|
||||||
stderr_out = _wl_watch_proc.stderr.read().decode("utf-8", errors="ignore")
|
|
||||||
logging.warning(
|
|
||||||
f"wl-paste --watch exited immediately: {stderr_out.strip()}"
|
|
||||||
)
|
|
||||||
_wl_watch_proc = None
|
|
||||||
return False
|
|
||||||
|
|
||||||
logging.info(
|
|
||||||
"Using wl-paste --watch for clipboard monitoring (no focus stealing)"
|
|
||||||
)
|
|
||||||
|
|
||||||
while _run_poll.is_set():
|
|
||||||
line = _wl_watch_proc.stdout.readline()
|
|
||||||
if not line:
|
|
||||||
break
|
|
||||||
if not _run_poll.is_set():
|
|
||||||
break
|
|
||||||
|
|
||||||
success, mime_output = execute_command("wl-paste", "-l")
|
|
||||||
if not success:
|
|
||||||
error_msg = f"Failed to retrieve MIME types: {mime_output}"
|
|
||||||
if error_msg != last_error:
|
|
||||||
logging.error(error_msg)
|
|
||||||
last_error = error_msg
|
|
||||||
continue
|
|
||||||
|
|
||||||
mime_list = mime_output.decode("utf-8")
|
|
||||||
mime_list = mime_list.replace("\r\n", "\n").replace("\r", "\n").split("\n")
|
|
||||||
mime_list = [m.strip() for m in mime_list if len(m.strip()) > 0]
|
|
||||||
type_ = convert_mime_to_generic_type(mime_list)
|
|
||||||
|
|
||||||
# Text
|
|
||||||
if type_ == "text":
|
|
||||||
success, text = execute_command("wl-paste", "-n")
|
|
||||||
if success:
|
|
||||||
text = text.decode("utf-8")
|
|
||||||
if len(text) > 0 and text != previous_clipboard:
|
|
||||||
previous_clipboard = text
|
|
||||||
if _callback_update:
|
|
||||||
_callback_update("text", text)
|
|
||||||
else:
|
|
||||||
error_msg = (
|
|
||||||
f"Failed to retrieve text content from clipboard. {text}"
|
|
||||||
)
|
|
||||||
if error_msg != last_error:
|
|
||||||
if not any(
|
|
||||||
re.search(pattern, error_msg.lower())
|
|
||||||
for pattern in ignore_patterns
|
|
||||||
):
|
|
||||||
logging.error(error_msg)
|
|
||||||
last_error = error_msg
|
|
||||||
|
|
||||||
# Image
|
|
||||||
elif type_ == "image" and enable_image_monitoring:
|
|
||||||
success, image = execute_command("wl-paste", "-t", "image/png")
|
|
||||||
if success:
|
|
||||||
if image != previous_clipboard:
|
|
||||||
previous_clipboard = image
|
|
||||||
if _callback_update and not _block_image_once:
|
|
||||||
_callback_update("image", image)
|
|
||||||
else:
|
|
||||||
_block_image_once = False
|
|
||||||
else:
|
|
||||||
error_msg = (
|
|
||||||
f"Failed to retrieve image content from clipboard. {image}"
|
|
||||||
)
|
|
||||||
if error_msg != last_error:
|
|
||||||
if not any(
|
|
||||||
re.search(pattern, error_msg.lower())
|
|
||||||
for pattern in ignore_patterns
|
|
||||||
):
|
|
||||||
logging.error(error_msg)
|
|
||||||
last_error = error_msg
|
|
||||||
|
|
||||||
# Files
|
|
||||||
elif type_ == "files" and enable_file_monitoring:
|
|
||||||
success, files = execute_command(
|
|
||||||
"wl-paste", "-t", "text/uri-list", "-n"
|
|
||||||
)
|
|
||||||
if success:
|
|
||||||
files = files.decode("utf-8")
|
|
||||||
files = (
|
|
||||||
files.replace("\r\n", "\n").replace("\r", "\n").split("\n")
|
|
||||||
)
|
|
||||||
files = [f.strip() for f in files if len(f.strip()) > 0]
|
|
||||||
if files != previous_clipboard:
|
|
||||||
previous_clipboard = files
|
|
||||||
if _callback_update:
|
|
||||||
_callback_update("files", files)
|
|
||||||
else:
|
|
||||||
error_msg = (
|
|
||||||
f"Failed to retrieve files content from clipboard. {files}"
|
|
||||||
)
|
|
||||||
if error_msg != last_error:
|
|
||||||
if not any(
|
|
||||||
re.search(pattern, error_msg.lower())
|
|
||||||
for pattern in ignore_patterns
|
|
||||||
):
|
|
||||||
logging.error(error_msg)
|
|
||||||
last_error = error_msg
|
|
||||||
|
|
||||||
return True
|
|
||||||
except FileNotFoundError:
|
|
||||||
logging.warning("wl-paste not found, cannot use --watch mode")
|
|
||||||
return False
|
|
||||||
except Exception as e:
|
|
||||||
logging.warning(f"wl-paste --watch failed: {e}")
|
|
||||||
return False
|
|
||||||
finally:
|
|
||||||
if _wl_watch_proc is not None:
|
|
||||||
_wl_watch_proc.terminate()
|
|
||||||
try:
|
|
||||||
_wl_watch_proc.wait(timeout=2)
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
_wl_watch_proc.kill()
|
|
||||||
_wl_watch_proc = None
|
|
||||||
|
|
||||||
|
|
||||||
def convert_mime_to_generic_type(mime_list):
|
|
||||||
if "text/uri-list" in mime_list:
|
|
||||||
return "files"
|
|
||||||
|
|
||||||
if any(mime.startswith("image/") for mime in mime_list):
|
|
||||||
return "image"
|
|
||||||
|
|
||||||
text_mime = [
|
|
||||||
"text/plain",
|
|
||||||
"text/plain;charset=utf-8",
|
|
||||||
"STRING",
|
|
||||||
"TEXT",
|
|
||||||
"COMPOUND_TEXT",
|
|
||||||
"UTF8_STRING",
|
|
||||||
]
|
|
||||||
if any(t_mime in mime_list for t_mime in text_mime):
|
|
||||||
return "text"
|
|
||||||
|
|
||||||
return "unknown"
|
|
||||||
|
|
||||||
|
|
||||||
def execute_command(*args) -> tuple:
|
|
||||||
"""
|
|
||||||
Executes a command with the given arguments and returns the output or error.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
*args: Variable-length argument list to be passed as the command and arguments.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
tuple: (success: bool, result: str)
|
|
||||||
success is True if the command executed successfully, False otherwise.
|
|
||||||
result is the output or error of the command.
|
|
||||||
"""
|
|
||||||
process = subprocess.Popen(
|
|
||||||
args,
|
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
stderr=subprocess.PIPE,
|
|
||||||
)
|
|
||||||
output, error = process.communicate()
|
|
||||||
if process.returncode == 0: # Success
|
|
||||||
return (True, output)
|
|
||||||
else: # Failure
|
|
||||||
return (False, error.decode())
|
|
||||||
|
|
||||||
|
|
||||||
def is_x_clipboard_owner():
|
|
||||||
# Check if the X clipboard is owned by the current user
|
|
||||||
return execute_command("xclip", "-selection", "clipboard", "-t", "TARGETS", "-o")[0]
|
|
||||||
|
|
||||||
|
|
||||||
def _start_clipboard_polling(enable_image_monitoring, enable_file_monitoring):
|
|
||||||
if XMODE:
|
|
||||||
x_clipboard_owner = is_x_clipboard_owner()
|
|
||||||
if not x_clipboard_owner:
|
|
||||||
logging.warning(
|
|
||||||
"x-clip is not owned by the current user. Switching to wl-clipboard."
|
|
||||||
)
|
|
||||||
_monitor_x_wl_clipboard(
|
|
||||||
x_mode=x_clipboard_owner,
|
|
||||||
enable_image_monitoring=enable_image_monitoring,
|
|
||||||
enable_file_monitoring=enable_file_monitoring,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
if not _monitor_wl_watch(
|
|
||||||
enable_image_monitoring=enable_image_monitoring,
|
|
||||||
enable_file_monitoring=enable_file_monitoring,
|
|
||||||
):
|
|
||||||
logging.info(
|
|
||||||
"Falling back to wl-paste polling mode for clipboard monitoring"
|
|
||||||
)
|
|
||||||
_monitor_x_wl_clipboard(
|
|
||||||
x_mode=False,
|
|
||||||
enable_image_monitoring=enable_image_monitoring,
|
|
||||||
enable_file_monitoring=enable_file_monitoring,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _runner(enable_image_monitoring=False, enable_file_monitoring=False):
|
|
||||||
global _is_gdk_running, _run_poll
|
|
||||||
logging.info(f"XMODE: {XMODE}")
|
|
||||||
try:
|
|
||||||
_run_poll.set()
|
|
||||||
import gi
|
|
||||||
|
|
||||||
gi.require_version("Gtk", "3.0")
|
|
||||||
gi.require_version("Gdk", "3.0")
|
|
||||||
from gi.repository import Gtk, Gdk
|
|
||||||
|
|
||||||
if "x11" in str(type(Gdk.Display.get_default())).lower(): # X11
|
|
||||||
logging.info("Starting GTK clipboard monitoring for X11 display server.")
|
|
||||||
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
|
|
||||||
clipboard.connect(
|
|
||||||
"owner-change",
|
|
||||||
lambda clip, event: _on_clipboard_changed(
|
|
||||||
clip, event, enable_image_monitoring, enable_file_monitoring
|
|
||||||
),
|
|
||||||
)
|
|
||||||
_is_gdk_running = True
|
|
||||||
Gtk.main()
|
|
||||||
else:
|
|
||||||
logging.warning(
|
|
||||||
f"Unsupported display server detected ${str(type(Gdk.Display.get_default())).lower()}. Starting polling mode for {detect_linux_display_server()} server as fallback."
|
|
||||||
)
|
|
||||||
_start_clipboard_polling(enable_image_monitoring, enable_file_monitoring)
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(
|
|
||||||
f"Failed to start clipboard monitor: Error {e}\nStarting polling mode for {detect_linux_display_server()} server as fallback."
|
|
||||||
)
|
|
||||||
_start_clipboard_polling(enable_image_monitoring, enable_file_monitoring)
|
|
||||||
|
|
||||||
|
|
||||||
def _start(enable_image_monitoring=False, enable_file_monitoring=False):
|
|
||||||
global _clipboard_thread
|
|
||||||
if not _clipboard_thread:
|
|
||||||
_clipboard_thread = threading.Thread(
|
|
||||||
target=_runner,
|
|
||||||
args=(enable_image_monitoring, enable_file_monitoring),
|
|
||||||
daemon=True,
|
|
||||||
)
|
|
||||||
_clipboard_thread.start()
|
|
||||||
|
|
||||||
|
|
||||||
def stop():
|
|
||||||
global _clipboard_thread, _callback_update, _block_image_once, _run_poll, _is_gdk_running, _wl_watch_proc
|
|
||||||
if _clipboard_thread:
|
|
||||||
if _is_gdk_running:
|
|
||||||
import gi
|
|
||||||
|
|
||||||
gi.require_version("Gtk", "3.0")
|
|
||||||
from gi.repository import Gtk
|
|
||||||
|
|
||||||
Gtk.main_quit()
|
|
||||||
_is_gdk_running = False
|
|
||||||
_run_poll.clear()
|
|
||||||
if _wl_watch_proc is not None:
|
|
||||||
_wl_watch_proc.terminate()
|
|
||||||
_clipboard_thread.join() # Wait for the thread to finish
|
|
||||||
_clipboard_thread = None
|
|
||||||
_callback_update = None
|
|
||||||
_block_image_once = False
|
|
||||||
_wl_watch_proc = None
|
|
||||||
logging.info("Clipboard monitor stopped")
|
|
||||||
|
|
||||||
|
|
||||||
def wait():
|
|
||||||
global _clipboard_thread
|
|
||||||
if _clipboard_thread:
|
|
||||||
_clipboard_thread.join()
|
|
||||||
|
|
||||||
|
|
||||||
def enable_block_image_once():
|
|
||||||
global _block_image_once
|
|
||||||
_block_image_once = True
|
|
||||||
|
|
||||||
|
|
||||||
def on_update(callback, enable_image_monitoring=False, enable_file_monitoring=False):
|
|
||||||
global _callback_update
|
|
||||||
_callback_update = callback
|
|
||||||
_start(enable_image_monitoring, enable_file_monitoring)
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
from io import BytesIO
|
|
||||||
import logging
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
import pasteboard
|
|
||||||
|
|
||||||
_clipboard_thread = None
|
|
||||||
_callback_update = None
|
|
||||||
_run = False
|
|
||||||
_first_run = False
|
|
||||||
_block_image_once = False
|
|
||||||
_pasteboard_lock = threading.Lock()
|
|
||||||
_pb_writer = None
|
|
||||||
|
|
||||||
|
|
||||||
def write_to_pasteboard(data, pb_type):
|
|
||||||
global _pb_writer
|
|
||||||
with _pasteboard_lock:
|
|
||||||
if _pb_writer is None:
|
|
||||||
_pb_writer = pasteboard.Pasteboard()
|
|
||||||
_pb_writer.set_contents(data, pb_type)
|
|
||||||
|
|
||||||
|
|
||||||
def _runner(enable_image_monitoring=False, enable_file_monitoring=False):
|
|
||||||
global _first_run, _block_image_once
|
|
||||||
try:
|
|
||||||
pb_text = pasteboard.Pasteboard()
|
|
||||||
if enable_image_monitoring:
|
|
||||||
pb_image_png = pasteboard.Pasteboard()
|
|
||||||
pb_image_tiff = pasteboard.Pasteboard()
|
|
||||||
if enable_file_monitoring:
|
|
||||||
pb_files = pasteboard.Pasteboard()
|
|
||||||
image_processed = False
|
|
||||||
files_processed = False
|
|
||||||
while _run:
|
|
||||||
# don't change the execution order (files,text,image or files,image,text)
|
|
||||||
|
|
||||||
if enable_file_monitoring:
|
|
||||||
# Files
|
|
||||||
with _pasteboard_lock:
|
|
||||||
clipboard_files = pb_files.get_file_urls(diff=True)
|
|
||||||
if (
|
|
||||||
clipboard_files is not None
|
|
||||||
and type(clipboard_files) is tuple
|
|
||||||
and len(clipboard_files) > 0
|
|
||||||
):
|
|
||||||
if _callback_update and not _first_run:
|
|
||||||
files_processed = True
|
|
||||||
_callback_update("files", clipboard_files)
|
|
||||||
|
|
||||||
# Text
|
|
||||||
with _pasteboard_lock:
|
|
||||||
clipboard_text = pb_text.get_contents(
|
|
||||||
type=pasteboard.String, diff=True
|
|
||||||
) # If True, retrieves and returns the content only if it has changed since the last call.
|
|
||||||
# This approach is efficient even in cases of frequent polling.
|
|
||||||
if (
|
|
||||||
clipboard_text is not None
|
|
||||||
and type(clipboard_text) is str
|
|
||||||
and len(clipboard_text) > 0
|
|
||||||
):
|
|
||||||
if _callback_update and not _first_run and not files_processed:
|
|
||||||
_callback_update("text", clipboard_text)
|
|
||||||
|
|
||||||
if enable_image_monitoring:
|
|
||||||
# Image (PNG)
|
|
||||||
with _pasteboard_lock:
|
|
||||||
clipboard_image_png = pb_image_png.get_contents(
|
|
||||||
type=pasteboard.PNG, diff=True
|
|
||||||
)
|
|
||||||
if (
|
|
||||||
clipboard_image_png is not None
|
|
||||||
and type(clipboard_image_png) is bytes
|
|
||||||
):
|
|
||||||
if _callback_update and not _first_run and not image_processed:
|
|
||||||
image_processed = True
|
|
||||||
if _block_image_once:
|
|
||||||
_block_image_once = False
|
|
||||||
else:
|
|
||||||
if not files_processed:
|
|
||||||
_callback_update("image", clipboard_image_png)
|
|
||||||
|
|
||||||
# Image (TIFF)
|
|
||||||
with _pasteboard_lock:
|
|
||||||
clipboard_image_tiff = pb_image_tiff.get_contents(
|
|
||||||
type=pasteboard.TIFF, diff=True
|
|
||||||
)
|
|
||||||
if (
|
|
||||||
clipboard_image_tiff is not None
|
|
||||||
and type(clipboard_image_tiff) is bytes
|
|
||||||
):
|
|
||||||
if _callback_update and not _first_run and not image_processed:
|
|
||||||
image_processed = True
|
|
||||||
if _block_image_once:
|
|
||||||
_block_image_once = False
|
|
||||||
else:
|
|
||||||
if not files_processed:
|
|
||||||
_callback_update("image", clipboard_image_tiff)
|
|
||||||
|
|
||||||
files_processed = False
|
|
||||||
image_processed = False
|
|
||||||
_first_run = False
|
|
||||||
time.sleep(0.3) # seconds
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Error processing clipboard update: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
def _start(enable_image_monitoring=False, enable_file_monitoring=False):
|
|
||||||
global _clipboard_thread, _run, _first_run
|
|
||||||
if not _clipboard_thread:
|
|
||||||
_run = True
|
|
||||||
_first_run = True
|
|
||||||
_clipboard_thread = threading.Thread(
|
|
||||||
target=_runner,
|
|
||||||
args=(enable_image_monitoring, enable_file_monitoring),
|
|
||||||
daemon=True,
|
|
||||||
)
|
|
||||||
_clipboard_thread.start()
|
|
||||||
|
|
||||||
|
|
||||||
def stop():
|
|
||||||
global _clipboard_thread, _callback_update, _run, _first_run, _block_image_once, _pb_writer
|
|
||||||
if _clipboard_thread:
|
|
||||||
_run = False
|
|
||||||
_clipboard_thread.join() # Wait for the thread to finish
|
|
||||||
_first_run = False
|
|
||||||
_clipboard_thread = None
|
|
||||||
_callback_update = None
|
|
||||||
_block_image_once = False
|
|
||||||
_pb_writer = None
|
|
||||||
logging.info("Clipboard monitor stopped")
|
|
||||||
|
|
||||||
|
|
||||||
def wait():
|
|
||||||
global _clipboard_thread
|
|
||||||
if _clipboard_thread:
|
|
||||||
_clipboard_thread.join()
|
|
||||||
|
|
||||||
|
|
||||||
def enable_block_image_once():
|
|
||||||
global _block_image_once
|
|
||||||
_block_image_once = True
|
|
||||||
|
|
||||||
|
|
||||||
def on_update(callback, enable_image_monitoring=False, enable_file_monitoring=False):
|
|
||||||
global _callback_update
|
|
||||||
_callback_update = callback
|
|
||||||
_start(enable_image_monitoring, enable_file_monitoring)
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
import logging
|
|
||||||
import win32gui
|
|
||||||
import win32api
|
|
||||||
import win32con
|
|
||||||
import win32clipboard
|
|
||||||
import threading
|
|
||||||
import ctypes
|
|
||||||
import time
|
|
||||||
from PIL import ImageGrab
|
|
||||||
|
|
||||||
|
|
||||||
_clipboard_thread = None
|
|
||||||
_hwnd = None # Store the window handle
|
|
||||||
_callback_update = None
|
|
||||||
_block_image_once = False
|
|
||||||
|
|
||||||
|
|
||||||
def _get_clipboard_content(enable_image_monitoring=False, enable_file_monitoring=False):
|
|
||||||
"""
|
|
||||||
Get the content of the clipboard.
|
|
||||||
|
|
||||||
Image:
|
|
||||||
PNG -> PngImagePlugin.PngImageFile
|
|
||||||
DIB -> BmpImagePlugin.DibImageFile
|
|
||||||
PNG, DIB, JPG, etc. -> [file_path1, file_path2, ...]
|
|
||||||
|
|
||||||
Text:
|
|
||||||
CF_UNICODETEXT, CF_TEXT -> str
|
|
||||||
|
|
||||||
Files:
|
|
||||||
CF_HDROP -> (file_path1, file_path2, ...)
|
|
||||||
"""
|
|
||||||
# sleep 0.5 to avoid clipboard not ready for read
|
|
||||||
time.sleep(0.5)
|
|
||||||
clipboard_type = None
|
|
||||||
clipboard_content = None
|
|
||||||
|
|
||||||
if enable_image_monitoring and win32clipboard.IsClipboardFormatAvailable(
|
|
||||||
win32con.CF_BITMAP
|
|
||||||
):
|
|
||||||
clipboard_type = "image"
|
|
||||||
clipboard_content = ImageGrab.grabclipboard()
|
|
||||||
else:
|
|
||||||
win32clipboard.OpenClipboard()
|
|
||||||
try:
|
|
||||||
if win32clipboard.IsClipboardFormatAvailable(win32con.CF_UNICODETEXT):
|
|
||||||
text = win32clipboard.GetClipboardData(win32con.CF_UNICODETEXT)
|
|
||||||
clipboard_type = "text"
|
|
||||||
clipboard_content = text
|
|
||||||
elif win32clipboard.IsClipboardFormatAvailable(win32con.CF_TEXT):
|
|
||||||
text_bytes = win32clipboard.GetClipboardData(win32con.CF_TEXT)
|
|
||||||
text = text_bytes.decode()
|
|
||||||
clipboard_type = "text"
|
|
||||||
clipboard_content = text
|
|
||||||
elif enable_file_monitoring and win32clipboard.IsClipboardFormatAvailable(
|
|
||||||
win32con.CF_HDROP
|
|
||||||
):
|
|
||||||
files = win32clipboard.GetClipboardData(win32con.CF_HDROP)
|
|
||||||
clipboard_type = "files"
|
|
||||||
clipboard_content = files
|
|
||||||
finally:
|
|
||||||
win32clipboard.CloseClipboard()
|
|
||||||
|
|
||||||
return (clipboard_type, clipboard_content)
|
|
||||||
|
|
||||||
|
|
||||||
def _process_message(
|
|
||||||
hwnd: int,
|
|
||||||
msg: int,
|
|
||||||
wparam: int,
|
|
||||||
lparam: int,
|
|
||||||
enable_image_monitoring=False,
|
|
||||||
enable_file_monitoring=False,
|
|
||||||
):
|
|
||||||
global _block_image_once
|
|
||||||
WM_CLIPBOARDUPDATE = 0x031D
|
|
||||||
if msg == WM_CLIPBOARDUPDATE:
|
|
||||||
clip = _get_clipboard_content(enable_image_monitoring, enable_file_monitoring)
|
|
||||||
|
|
||||||
try:
|
|
||||||
if clip[0] == "text" and _callback_update:
|
|
||||||
_callback_update(clip[0], clip[1])
|
|
||||||
|
|
||||||
if enable_image_monitoring and clip[0] == "image" and _callback_update:
|
|
||||||
if _block_image_once:
|
|
||||||
_block_image_once = False
|
|
||||||
else:
|
|
||||||
_callback_update(clip[0], clip[1])
|
|
||||||
|
|
||||||
if enable_file_monitoring and clip[0] == "files" and _callback_update:
|
|
||||||
_callback_update(clip[0], clip[1])
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Error processing clipboard update: {e}")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
def _create_window(enable_image_monitoring=False, enable_file_monitoring=False):
|
|
||||||
global _hwnd
|
|
||||||
className = "ClipboardHook"
|
|
||||||
wc = win32gui.WNDCLASS()
|
|
||||||
wc.lpfnWndProc = lambda hwnd, msg, wparam, lparam: _process_message(
|
|
||||||
hwnd, msg, wparam, lparam, enable_image_monitoring, enable_file_monitoring
|
|
||||||
)
|
|
||||||
wc.lpszClassName = className
|
|
||||||
wc.hInstance = win32api.GetModuleHandle(None)
|
|
||||||
class_atom = win32gui.RegisterClass(wc)
|
|
||||||
_hwnd = win32gui.CreateWindow(
|
|
||||||
class_atom, className, 0, 0, 0, 0, 0, 0, 0, wc.hInstance, None
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _runner(enable_image_monitoring=False, enable_file_monitoring=False):
|
|
||||||
global _hwnd
|
|
||||||
_create_window(enable_image_monitoring, enable_file_monitoring)
|
|
||||||
ctypes.windll.user32.AddClipboardFormatListener(_hwnd)
|
|
||||||
try:
|
|
||||||
win32gui.PumpMessages()
|
|
||||||
finally:
|
|
||||||
ctypes.windll.user32.RemoveClipboardFormatListener(_hwnd)
|
|
||||||
win32gui.DestroyWindow(_hwnd)
|
|
||||||
win32gui.UnregisterClass("ClipboardHook", win32api.GetModuleHandle(None))
|
|
||||||
|
|
||||||
|
|
||||||
def _start(enable_image_monitoring=False, enable_file_monitoring=False):
|
|
||||||
global _clipboard_thread
|
|
||||||
if not _clipboard_thread:
|
|
||||||
_clipboard_thread = threading.Thread(
|
|
||||||
target=_runner,
|
|
||||||
args=(enable_image_monitoring, enable_file_monitoring),
|
|
||||||
daemon=True,
|
|
||||||
)
|
|
||||||
_clipboard_thread.start()
|
|
||||||
|
|
||||||
|
|
||||||
def stop():
|
|
||||||
global _clipboard_thread, _hwnd, _callback_update, _block_image_once
|
|
||||||
if _clipboard_thread and _hwnd:
|
|
||||||
win32gui.PostMessage(
|
|
||||||
_hwnd, win32con.WM_QUIT, 0, 0
|
|
||||||
) # Send WM_QUIT to the window
|
|
||||||
_clipboard_thread.join() # Wait for the thread to finish
|
|
||||||
_clipboard_thread = None
|
|
||||||
_hwnd = None
|
|
||||||
_callback_update = None
|
|
||||||
_block_image_once = False
|
|
||||||
logging.info("Clipboard monitor stopped")
|
|
||||||
|
|
||||||
|
|
||||||
def wait():
|
|
||||||
global _clipboard_thread
|
|
||||||
if _clipboard_thread:
|
|
||||||
_clipboard_thread.join()
|
|
||||||
|
|
||||||
|
|
||||||
def enable_block_image_once():
|
|
||||||
global _block_image_once
|
|
||||||
_block_image_once = True
|
|
||||||
|
|
||||||
|
|
||||||
def on_update(callback, enable_image_monitoring=False, enable_file_monitoring=False):
|
|
||||||
global _callback_update
|
|
||||||
_callback_update = callback
|
|
||||||
_start(enable_image_monitoring, enable_file_monitoring)
|
|
||||||
@@ -1,309 +0,0 @@
|
|||||||
import logging
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
from core.constants import *
|
|
||||||
|
|
||||||
from core.config import Config
|
|
||||||
from utils.request_manager import RequestManager
|
|
||||||
from utils.cipher_manager import CipherManager
|
|
||||||
from stomp_ws.stomp_manager import STOMPManager
|
|
||||||
from p2p.p2p_manager import P2PManager
|
|
||||||
|
|
||||||
if PLATFORM == WINDOWS:
|
|
||||||
import ctypes
|
|
||||||
elif PLATFORM == MACOS or PLATFORM.startswith(LINUX):
|
|
||||||
import fcntl
|
|
||||||
|
|
||||||
|
|
||||||
if PLATFORM.startswith(LINUX) and LINUX_USE_CLI_UI:
|
|
||||||
import pyfiglet
|
|
||||||
from cli.login import LoginForm
|
|
||||||
from cli.info import CustomDialog
|
|
||||||
from cli.tray import TaskbarPanel
|
|
||||||
from cli.message_box import MessageBox
|
|
||||||
from cli.echo import Echo
|
|
||||||
else:
|
|
||||||
from gui.login import LoginForm
|
|
||||||
from gui.info import CustomDialog
|
|
||||||
from gui.tray import TaskbarPanel
|
|
||||||
from gui.message_box import MessageBox
|
|
||||||
|
|
||||||
|
|
||||||
class Application:
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
log_file_path=LOG_FILE_NAME,
|
|
||||||
data_file_path=DATA_FILE_NAME,
|
|
||||||
mutex_identifier=MUTEX_NAME,
|
|
||||||
):
|
|
||||||
try:
|
|
||||||
self.log_file_path = os.path.join(
|
|
||||||
get_program_files_directory(), log_file_path
|
|
||||||
)
|
|
||||||
self.data_file_path = os.path.join(
|
|
||||||
get_program_files_directory(), data_file_path
|
|
||||||
)
|
|
||||||
self.mutex_identifier = mutex_identifier
|
|
||||||
|
|
||||||
if PLATFORM == MACOS or PLATFORM.startswith(LINUX):
|
|
||||||
self.lock_file = None # File(lock) object
|
|
||||||
self.mutex_identifier = os.path.join(
|
|
||||||
get_program_files_directory(), self.mutex_identifier
|
|
||||||
)
|
|
||||||
|
|
||||||
self.config = Config(
|
|
||||||
file_name=self.data_file_path
|
|
||||||
) # Maintain a single configuration instance for the entire application lifecycle.
|
|
||||||
|
|
||||||
self.request_manager = RequestManager(self.config)
|
|
||||||
self.stomp_manager = STOMPManager(self.config)
|
|
||||||
self.p2p_manager = P2PManager(self.config)
|
|
||||||
self.cipher_manager = CipherManager(self.config)
|
|
||||||
except Exception as e:
|
|
||||||
CustomDialog(
|
|
||||||
f"An error occurred during application initialization: {e}",
|
|
||||||
msg_type="error",
|
|
||||||
).mainloop()
|
|
||||||
|
|
||||||
def setup_logging(self):
|
|
||||||
LOG_FORMAT = "%(asctime)s - %(levelname)s - %(message)s"
|
|
||||||
logging.basicConfig(
|
|
||||||
level=LOG_LEVEL,
|
|
||||||
format=LOG_FORMAT,
|
|
||||||
filename=self.log_file_path,
|
|
||||||
filemode="w",
|
|
||||||
)
|
|
||||||
|
|
||||||
def ensure_single_instance(self):
|
|
||||||
if PLATFORM == WINDOWS:
|
|
||||||
ctypes.windll.kernel32.CreateMutexW(None, False, self.mutex_identifier)
|
|
||||||
if ctypes.windll.kernel32.GetLastError() == 183: # ERROR_ALREADY_EXISTS
|
|
||||||
CustomDialog(
|
|
||||||
"Another instance of ClipCascade is already running.",
|
|
||||||
msg_type="warning",
|
|
||||||
).mainloop()
|
|
||||||
sys.exit(0)
|
|
||||||
elif PLATFORM == MACOS or PLATFORM.startswith(LINUX):
|
|
||||||
if PLATFORM == MACOS:
|
|
||||||
app_dir = get_program_files_directory()
|
|
||||||
if not os.path.exists(app_dir):
|
|
||||||
try:
|
|
||||||
os.makedirs(app_dir)
|
|
||||||
except Exception as e:
|
|
||||||
CustomDialog(
|
|
||||||
f"An error occurred while creating the directory '{app_dir}'. Error: {e}",
|
|
||||||
msg_type="error",
|
|
||||||
).mainloop()
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# Create the lock file
|
|
||||||
try:
|
|
||||||
self.create_lock_file()
|
|
||||||
except IOError:
|
|
||||||
run_anyway = MessageBox().askquestion(
|
|
||||||
"ClipCascade",
|
|
||||||
"Another instance of ClipCascade is already running. Do you want to run anyway?",
|
|
||||||
)
|
|
||||||
if run_anyway == "yes":
|
|
||||||
os.remove(self.mutex_identifier)
|
|
||||||
self.create_lock_file()
|
|
||||||
else:
|
|
||||||
self.lock_file = None
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
def create_lock_file(self, path=None):
|
|
||||||
if path is None:
|
|
||||||
path = self.mutex_identifier
|
|
||||||
|
|
||||||
if PLATFORM == MACOS or PLATFORM.startswith(LINUX):
|
|
||||||
self.lock_file = open(path, "w")
|
|
||||||
fcntl.flock(self.lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
||||||
|
|
||||||
def authenticate_and_connect(self):
|
|
||||||
# Attempt to connect with existing cookie
|
|
||||||
if self.config.data.get("cookie"):
|
|
||||||
ws_conn_successful, msg = self._get_ws_manager().connect()
|
|
||||||
if ws_conn_successful:
|
|
||||||
self._get_ws_manager().is_login_phase = False
|
|
||||||
return
|
|
||||||
|
|
||||||
# enable login form
|
|
||||||
used_saved_credentials = False
|
|
||||||
display_login_success_dialog = False
|
|
||||||
if PLATFORM.startswith(LINUX) and LINUX_USE_CLI_UI:
|
|
||||||
Echo("═" * 14 + "\n║ LOGIN FORM ║\n" + "═" * 14)
|
|
||||||
while True:
|
|
||||||
if (
|
|
||||||
self.config.data.get("cookie") is not None
|
|
||||||
and self.config.data["save_password"]
|
|
||||||
and self.config.data["cipher_enabled"] == False
|
|
||||||
and not used_saved_credentials
|
|
||||||
):
|
|
||||||
# Attempt to connect with password when using saved credentials
|
|
||||||
used_saved_credentials = True
|
|
||||||
else:
|
|
||||||
display_login_success_dialog = True
|
|
||||||
self.config.data["password"] = "" # Clear the password
|
|
||||||
login_form = LoginForm(
|
|
||||||
self.config,
|
|
||||||
on_quit_callback=(
|
|
||||||
None
|
|
||||||
if (PLATFORM.startswith(LINUX) and LINUX_USE_CLI_UI)
|
|
||||||
else lambda: sys.exit(0)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
login_form.mainloop() # wait until login form is closed
|
|
||||||
raw_password = self.config.data[
|
|
||||||
"password"
|
|
||||||
] # Store the raw password temporarily for hashing
|
|
||||||
self.config.data["password"] = (
|
|
||||||
CipherManager.string_to_sha3_512_lowercase_hex(raw_password)
|
|
||||||
) # Hash the password
|
|
||||||
|
|
||||||
login_successful, msg_login, self.config.data["cookie"] = (
|
|
||||||
self.request_manager.login()
|
|
||||||
)
|
|
||||||
if login_successful:
|
|
||||||
self.config.data["csrf_token"] = self.request_manager.get_csrf_token()
|
|
||||||
self.config.data["server_mode"] = self.request_manager.get_server_mode()
|
|
||||||
if self.config.data["server_mode"] == "P2P":
|
|
||||||
self.config.data["stun_url"] = self.request_manager.get_stun_url()
|
|
||||||
self.config.data["maxsize"] = -1
|
|
||||||
self.config.data["websocket_url"] = Config.convert_to_websocket_url(
|
|
||||||
self.config.data["server_url"], WEBSOCKET_ENDPOINT_P2P
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.config.data["stun_url"] = ""
|
|
||||||
self.config.data["maxsize"] = self.request_manager.maxsize()
|
|
||||||
self.config.data["websocket_url"] = Config.convert_to_websocket_url(
|
|
||||||
self.config.data["server_url"], WEBSOCKET_ENDPOINT
|
|
||||||
)
|
|
||||||
ws_conn_successful, msg = self._get_ws_manager().connect()
|
|
||||||
if ws_conn_successful:
|
|
||||||
self._get_ws_manager().is_login_phase = False
|
|
||||||
if self.config.data["cipher_enabled"]:
|
|
||||||
self.config.data["hashed_password"] = (
|
|
||||||
self.cipher_manager.hash_password(raw_password)
|
|
||||||
)
|
|
||||||
if not self.config.data["save_password"]:
|
|
||||||
self.config.data["password"] = ""
|
|
||||||
if display_login_success_dialog:
|
|
||||||
CustomDialog(
|
|
||||||
"Success! ClipCascade will now run in the task bar/menu bar.",
|
|
||||||
msg_type="success",
|
|
||||||
timeout=5000,
|
|
||||||
).mainloop()
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
CustomDialog(
|
|
||||||
"Login successful but websocket connection failed. \nPlease check websocket-url\n"
|
|
||||||
+ msg,
|
|
||||||
msg_type="error",
|
|
||||||
).mainloop()
|
|
||||||
else:
|
|
||||||
CustomDialog("Login Failed\n" + msg_login, msg_type="error").mainloop()
|
|
||||||
|
|
||||||
raw_password = None # Clear the raw password
|
|
||||||
if PLATFORM.startswith(LINUX) and LINUX_USE_CLI_UI:
|
|
||||||
Echo("-" * 53)
|
|
||||||
|
|
||||||
def _get_ws_manager(self):
|
|
||||||
if self.config.data["server_mode"] == "P2P":
|
|
||||||
return self.p2p_manager
|
|
||||||
else:
|
|
||||||
return self.stomp_manager
|
|
||||||
|
|
||||||
def get_version_update_status(self) -> list:
|
|
||||||
"""
|
|
||||||
Checks for a new version of the application by comparing the current version
|
|
||||||
with the one available in a remote JSON file.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list: [bool, str, str, str] - [Is new version available, latest version, current version, release URL]
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
response = RequestManager.get(VERSION_URL)
|
|
||||||
response_data = response.json()
|
|
||||||
if PLATFORM == WINDOWS:
|
|
||||||
key = "windows"
|
|
||||||
elif PLATFORM == MACOS:
|
|
||||||
key = "macos"
|
|
||||||
elif PLATFORM.startswith(LINUX):
|
|
||||||
if not LINUX_USE_CLI_UI:
|
|
||||||
key = "linux_gui"
|
|
||||||
else:
|
|
||||||
key = "linux_non_gui"
|
|
||||||
|
|
||||||
if response_data[key] != APP_VERSION:
|
|
||||||
return [True, response_data[key], APP_VERSION, RELEASE_URL]
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Error checking for new version: {e}")
|
|
||||||
return [False, "", APP_VERSION, RELEASE_URL]
|
|
||||||
|
|
||||||
def get_donation_url(self) -> str:
|
|
||||||
try:
|
|
||||||
metadata = self.request_manager.get_metadata()
|
|
||||||
if metadata is not None:
|
|
||||||
return metadata.get("funding", None)
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Error fetching metadata: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def logoff_and_exit(self):
|
|
||||||
try:
|
|
||||||
self._get_ws_manager().disconnect()
|
|
||||||
self.request_manager.logout()
|
|
||||||
self.config.data["hashed_password"] = None
|
|
||||||
self.config.data["cookie"] = None
|
|
||||||
self.config.data["maxsize"] = None
|
|
||||||
self.config.data["password"] = ""
|
|
||||||
self.config.data["csrf_token"] = ""
|
|
||||||
self.config.save()
|
|
||||||
except Exception as e:
|
|
||||||
raise Exception(f"Error during logging off: {e}")
|
|
||||||
|
|
||||||
def banner(self):
|
|
||||||
if PLATFORM.startswith(LINUX) and LINUX_USE_CLI_UI:
|
|
||||||
Echo(pyfiglet.figlet_format(APP_NAME))
|
|
||||||
Echo("*" * 53)
|
|
||||||
Echo("Real-Time Clipboard Syncing".center(53))
|
|
||||||
Echo(GITHUB_URL.center(53))
|
|
||||||
Echo("*" * 53)
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
try:
|
|
||||||
self.banner()
|
|
||||||
self.setup_logging()
|
|
||||||
self.ensure_single_instance()
|
|
||||||
self.config.load()
|
|
||||||
self.authenticate_and_connect()
|
|
||||||
self.config.save()
|
|
||||||
update_available = self.get_version_update_status()
|
|
||||||
donation_url = self.get_donation_url()
|
|
||||||
|
|
||||||
sys_tray = TaskbarPanel(
|
|
||||||
on_connect_callback=self._get_ws_manager().manual_reconnect,
|
|
||||||
on_disconnect_callback=self._get_ws_manager().disconnect,
|
|
||||||
on_logoff_callback=self.logoff_and_exit,
|
|
||||||
new_version_available=update_available,
|
|
||||||
github_url=GITHUB_URL,
|
|
||||||
donation_url=donation_url,
|
|
||||||
ws_interface=self._get_ws_manager(),
|
|
||||||
config=self.config,
|
|
||||||
)
|
|
||||||
self._get_ws_manager().set_tray_ref(sys_tray)
|
|
||||||
sys_tray.run()
|
|
||||||
except Exception as e:
|
|
||||||
msg = f"An unexpected error has occurred: {e}"
|
|
||||||
logging.error(msg)
|
|
||||||
CustomDialog(
|
|
||||||
msg + "\nCheck logs in project directory", msg_type="error"
|
|
||||||
).mainloop()
|
|
||||||
finally:
|
|
||||||
self._get_ws_manager().disconnect()
|
|
||||||
if PLATFORM == MACOS or PLATFORM.startswith(LINUX):
|
|
||||||
if self.lock_file is not None:
|
|
||||||
fcntl.flock(self.lock_file, fcntl.LOCK_UN)
|
|
||||||
self.lock_file.close()
|
|
||||||
os.remove(self.mutex_identifier)
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
import base64
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
from core.constants import *
|
|
||||||
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
def __init__(self, file_name=DATA_FILE_NAME):
|
|
||||||
self.file_name = file_name
|
|
||||||
self.data = {
|
|
||||||
"cipher_enabled": True,
|
|
||||||
"server_url": "http://localhost:8080",
|
|
||||||
"websocket_url": "",
|
|
||||||
"username": "",
|
|
||||||
"hashed_password": None,
|
|
||||||
"cookie": None,
|
|
||||||
"maxsize": None,
|
|
||||||
"hash_rounds": 664937,
|
|
||||||
"salt": "",
|
|
||||||
"csrf_token": "",
|
|
||||||
"notification": True,
|
|
||||||
"save_password": False,
|
|
||||||
"password": "",
|
|
||||||
"max_clipboard_size_local_limit_bytes": None,
|
|
||||||
"enable_image_sharing": True,
|
|
||||||
"enable_file_sharing": True,
|
|
||||||
"default_file_download_location": "",
|
|
||||||
"server_mode": "P2S",
|
|
||||||
"stun_url": "",
|
|
||||||
"ssl_ca_bundle": "",
|
|
||||||
}
|
|
||||||
|
|
||||||
def save(self):
|
|
||||||
"""
|
|
||||||
Save data to file
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
temp = self.data.copy()
|
|
||||||
if self.data.get("cipher_enabled") and self.data.get("hashed_password"):
|
|
||||||
temp["hashed_password"] = base64.b64encode(
|
|
||||||
temp["hashed_password"]
|
|
||||||
).decode("utf-8")
|
|
||||||
with open(self.file_name, "w") as f:
|
|
||||||
json.dump(temp, f, indent=4)
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Failed to save data: {e}")
|
|
||||||
|
|
||||||
def load(self):
|
|
||||||
"""
|
|
||||||
Load data from file
|
|
||||||
"""
|
|
||||||
if os.path.isfile(self.file_name):
|
|
||||||
try:
|
|
||||||
with open(self.file_name, "r") as f:
|
|
||||||
file_data = json.load(f)
|
|
||||||
self.data.update(file_data)
|
|
||||||
# Decode hashed_password if present
|
|
||||||
if self.data.get("hashed_password"):
|
|
||||||
self.data["hashed_password"] = base64.b64decode(
|
|
||||||
self.data["hashed_password"]
|
|
||||||
)
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Failed to load data: {e}")
|
|
||||||
logging.error(
|
|
||||||
"Try deleting DATA file in the program directory, and re-run the program again"
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def convert_to_websocket_url(input_url: str, endpoint: str = None) -> str:
|
|
||||||
if not input_url or not isinstance(input_url, str):
|
|
||||||
raise ValueError("Invalid URL provided")
|
|
||||||
|
|
||||||
# Trim whitespace, remove trailing slashes, and convert to lowercase
|
|
||||||
input_url = re.sub(r"/+$", "", input_url.strip()).lower()
|
|
||||||
|
|
||||||
# Determine protocol and convert
|
|
||||||
if input_url.startswith("https://"):
|
|
||||||
ws_url = input_url.replace("https://", "wss://", 1)
|
|
||||||
elif input_url.startswith("http://"):
|
|
||||||
ws_url = input_url.replace("http://", "ws://", 1)
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unsupported protocol in URL: {input_url}")
|
|
||||||
|
|
||||||
if endpoint is not None:
|
|
||||||
# Append the WebSocket endpoint and remove any trailing slash
|
|
||||||
ws_url += endpoint
|
|
||||||
ws_url = re.sub(r"/+$", "", ws_url)
|
|
||||||
|
|
||||||
return ws_url
|
|
||||||
@@ -1,262 +0,0 @@
|
|||||||
import logging
|
|
||||||
import platform
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
# platform constants
|
|
||||||
WINDOWS = "Windows"
|
|
||||||
MACOS = "macOS"
|
|
||||||
LINUX = "Linux"
|
|
||||||
LINUX_X11 = f"{LINUX}_X11"
|
|
||||||
LINUX_WAYLAND = f"{LINUX}_Wayland"
|
|
||||||
LINUX_HEADLESS = f"{LINUX}_Headless"
|
|
||||||
|
|
||||||
|
|
||||||
# OS detection
|
|
||||||
def get_os_and_display_server():
|
|
||||||
system = platform.system().lower().strip()
|
|
||||||
if system == "windows":
|
|
||||||
return WINDOWS
|
|
||||||
elif system == "darwin":
|
|
||||||
return MACOS
|
|
||||||
elif system == "linux":
|
|
||||||
session_type = os.environ.get("XDG_SESSION_TYPE", "").lower().strip()
|
|
||||||
if session_type == "wayland":
|
|
||||||
return LINUX_WAYLAND
|
|
||||||
elif session_type == "x11":
|
|
||||||
return LINUX_X11
|
|
||||||
else:
|
|
||||||
# Could be a headless session or another type of session
|
|
||||||
return LINUX_HEADLESS
|
|
||||||
else:
|
|
||||||
return "Unknown OS"
|
|
||||||
|
|
||||||
|
|
||||||
def detect_linux_display_server():
|
|
||||||
session_type = os.environ.get("XDG_SESSION_TYPE", "").lower().strip()
|
|
||||||
session_desktop = os.environ.get("XDG_SESSION_DESKTOP", "").lower().strip()
|
|
||||||
wayland_display = os.environ.get("WAYLAND_DISPLAY")
|
|
||||||
x_display = os.environ.get("DISPLAY")
|
|
||||||
|
|
||||||
# Priority detection order: X11 > XWayland > Hyprland > Wayland > Unknown
|
|
||||||
|
|
||||||
if session_type == "x11":
|
|
||||||
return "X11"
|
|
||||||
|
|
||||||
if session_type == "wayland" and x_display and wayland_display:
|
|
||||||
return "XWayland"
|
|
||||||
|
|
||||||
if session_type == "wayland" and session_desktop == "hyprland":
|
|
||||||
return "Hyprland"
|
|
||||||
|
|
||||||
if session_type == "wayland":
|
|
||||||
return "Wayland"
|
|
||||||
|
|
||||||
return "Unknown"
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_linux_bool_flag_token(raw):
|
|
||||||
if raw is None or not str(raw).strip():
|
|
||||||
raise ValueError("empty value")
|
|
||||||
s = str(raw).strip().lower()
|
|
||||||
if s == "true":
|
|
||||||
return True
|
|
||||||
if s == "false":
|
|
||||||
return False
|
|
||||||
raise ValueError(raw)
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_linux_positive_float(raw):
|
|
||||||
if raw is None or not str(raw).strip():
|
|
||||||
raise ValueError("empty value")
|
|
||||||
try:
|
|
||||||
value = float(str(raw).strip())
|
|
||||||
except ValueError:
|
|
||||||
raise ValueError(f"not a number: {raw!r}")
|
|
||||||
if value <= 0:
|
|
||||||
raise ValueError("must be a positive number")
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
def _strip_linux_cli_overrides(argv):
|
|
||||||
"""
|
|
||||||
Linux-only: parse --gui / --xmode (true|false), --polling (seconds), remove them from argv.
|
|
||||||
Last occurrence wins if a flag is repeated.
|
|
||||||
"""
|
|
||||||
gui_override = None
|
|
||||||
xmode_override = None
|
|
||||||
polling_override = None
|
|
||||||
i = 1
|
|
||||||
kept = [argv[0]] if argv else []
|
|
||||||
while i < len(argv):
|
|
||||||
item = argv[i]
|
|
||||||
if item.startswith("--gui="):
|
|
||||||
gui_override = _parse_linux_bool_flag_token(item.split("=", 1)[1])
|
|
||||||
i += 1
|
|
||||||
continue
|
|
||||||
if item == "--gui":
|
|
||||||
if i + 1 >= len(argv):
|
|
||||||
raise ValueError("--gui requires true or false")
|
|
||||||
gui_override = _parse_linux_bool_flag_token(argv[i + 1])
|
|
||||||
i += 2
|
|
||||||
continue
|
|
||||||
if item.startswith("--xmode="):
|
|
||||||
xmode_override = _parse_linux_bool_flag_token(item.split("=", 1)[1])
|
|
||||||
i += 1
|
|
||||||
continue
|
|
||||||
if item == "--xmode":
|
|
||||||
if i + 1 >= len(argv):
|
|
||||||
raise ValueError("--xmode requires true or false")
|
|
||||||
xmode_override = _parse_linux_bool_flag_token(argv[i + 1])
|
|
||||||
i += 2
|
|
||||||
continue
|
|
||||||
if item.startswith("--polling="):
|
|
||||||
polling_override = _parse_linux_positive_float(item.split("=", 1)[1])
|
|
||||||
i += 1
|
|
||||||
continue
|
|
||||||
if item == "--polling":
|
|
||||||
if i + 1 >= len(argv):
|
|
||||||
raise ValueError("--polling requires a positive number (seconds)")
|
|
||||||
polling_override = _parse_linux_positive_float(argv[i + 1])
|
|
||||||
i += 2
|
|
||||||
continue
|
|
||||||
kept.append(item)
|
|
||||||
i += 1
|
|
||||||
argv[:] = kept
|
|
||||||
return gui_override, xmode_override, polling_override
|
|
||||||
|
|
||||||
|
|
||||||
PLATFORM = get_os_and_display_server()
|
|
||||||
|
|
||||||
# Linux: CLI UI vs GTK tray — False on other platforms (unused except behind LINUX checks).
|
|
||||||
LINUX_USE_CLI_UI = False
|
|
||||||
# Linux: optional override for xclip/wl-paste polling sleep (seconds); None = use built-ins (0.3 / 3).
|
|
||||||
LINUX_CLIPBOARD_POLL_INTERVAL_SEC = None
|
|
||||||
|
|
||||||
if PLATFORM.startswith(LINUX):
|
|
||||||
if (
|
|
||||||
detect_linux_display_server() == "X11"
|
|
||||||
or detect_linux_display_server() == "XWayland"
|
|
||||||
or detect_linux_display_server() == "Unknown"
|
|
||||||
):
|
|
||||||
XMODE = True
|
|
||||||
else:
|
|
||||||
XMODE = False
|
|
||||||
|
|
||||||
try:
|
|
||||||
gui_override, xmode_override, polling_override = _strip_linux_cli_overrides(
|
|
||||||
sys.argv
|
|
||||||
)
|
|
||||||
except ValueError as e:
|
|
||||||
print(f"clipcascade (Linux CLI): {e}", file=sys.stderr)
|
|
||||||
sys.exit(2)
|
|
||||||
|
|
||||||
if xmode_override is not None:
|
|
||||||
XMODE = xmode_override
|
|
||||||
|
|
||||||
if gui_override is not None:
|
|
||||||
LINUX_USE_CLI_UI = not gui_override
|
|
||||||
else:
|
|
||||||
LINUX_USE_CLI_UI = not XMODE
|
|
||||||
|
|
||||||
if polling_override is not None:
|
|
||||||
LINUX_CLIPBOARD_POLL_INTERVAL_SEC = polling_override
|
|
||||||
|
|
||||||
|
|
||||||
# App version
|
|
||||||
if PLATFORM == WINDOWS:
|
|
||||||
APP_VERSION = "3.2.0"
|
|
||||||
elif PLATFORM == MACOS:
|
|
||||||
APP_VERSION = "3.2.0"
|
|
||||||
elif PLATFORM.startswith(LINUX):
|
|
||||||
if XMODE:
|
|
||||||
APP_VERSION = "3.2.0" # gui version
|
|
||||||
else:
|
|
||||||
APP_VERSION = "3.2.0" # non-gui(cli) version
|
|
||||||
|
|
||||||
|
|
||||||
# core constants
|
|
||||||
RECONNECT_WS_TIMER = 10 # seconds
|
|
||||||
WEBSOCKET_TIMEOUT = 3000 # milliseconds
|
|
||||||
|
|
||||||
# P2P signaling WebSocket keepalive (RFC 6455 ping/pong).
|
|
||||||
P2P_WS_PING_INTERVAL_SEC = 25
|
|
||||||
P2P_WS_PING_TIMEOUT_SEC = 20
|
|
||||||
# Data-channel keepalive (JSON envelope with _cc_keepalive); helps idle sessions and mobile radios.
|
|
||||||
P2P_DC_HEARTBEAT_INTERVAL_SEC = 20
|
|
||||||
# After sleep, aiortc RTCPeerConnection.close() can block; cap wait so the asyncio
|
|
||||||
# thread does not stall (which would also block processing ASSIGNED_ID / PEER_LIST).
|
|
||||||
P2P_PC_CLOSE_TIMEOUT_SEC = 5.0
|
|
||||||
|
|
||||||
LOG_FILE_NAME = "clipcascade_log.log"
|
|
||||||
LOG_LEVEL = logging.INFO # Use valid levels: DEBUG, INFO, WARNING, ERROR, CRITICAL
|
|
||||||
DATA_FILE_NAME = "DATA"
|
|
||||||
MAX_SIZE = 1048576 # 1 MiB
|
|
||||||
FRAGMENT_SIZE = 15360 # 15 KiB
|
|
||||||
SUBSCRIPTION_DESTINATION = "/user/queue/cliptext"
|
|
||||||
SEND_DESTINATION = "/app/cliptext"
|
|
||||||
LOGIN_URL = "/login"
|
|
||||||
LOGOUT_URL = "/logout"
|
|
||||||
MAXSIZE_URL = "/max-size"
|
|
||||||
CSRF_URL = "/csrf-token"
|
|
||||||
SERVER_MODE_URL = "/server-mode"
|
|
||||||
WEBSOCKET_ENDPOINT = "/clipsocket"
|
|
||||||
WEBSOCKET_ENDPOINT_P2P = "/p2psignaling"
|
|
||||||
STUN_URL = "/stun-url"
|
|
||||||
|
|
||||||
VERSION_URL = "https://raw.githubusercontent.com/Sathvik-Rao/ClipCascade/main/version.json"
|
|
||||||
RELEASE_URL = "https://github.com/Sathvik-Rao/ClipCascade/releases/latest"
|
|
||||||
GITHUB_URL = "https://github.com/Sathvik-Rao/ClipCascade"
|
|
||||||
APP_NAME = "ClipCascade"
|
|
||||||
HELP_URL = f"{GITHUB_URL}/blob/main/README.md"
|
|
||||||
METADATA_URL = "https://raw.githubusercontent.com/Sathvik-Rao/ClipCascade/main/metadata.json"
|
|
||||||
|
|
||||||
if PLATFORM == WINDOWS:
|
|
||||||
MUTEX_NAME = "Global\\ClipCascade_Mutex_PSSR"
|
|
||||||
elif PLATFORM == MACOS or PLATFORM.startswith(LINUX):
|
|
||||||
MUTEX_NAME = "clipcascade.lock"
|
|
||||||
|
|
||||||
|
|
||||||
# helper functions
|
|
||||||
def get_user_home_directory():
|
|
||||||
"""
|
|
||||||
Get the user's home directory in a cross-platform manner.
|
|
||||||
On Windows, this resolves to something like C:\\Users\\<Username>
|
|
||||||
On macOS and Linux, this typically resolves to /Users/<Username> or /home/<Username>, respectively.
|
|
||||||
"""
|
|
||||||
return os.path.expanduser("~")
|
|
||||||
|
|
||||||
|
|
||||||
def get_program_files_directory():
|
|
||||||
"""
|
|
||||||
Get the directory containing the program files.
|
|
||||||
"""
|
|
||||||
if PLATFORM == MACOS:
|
|
||||||
app_dir = os.path.join(
|
|
||||||
get_user_home_directory(), "Library", "Application Support", "ClipCascade"
|
|
||||||
)
|
|
||||||
if not os.path.exists(app_dir):
|
|
||||||
try:
|
|
||||||
os.makedirs(app_dir)
|
|
||||||
except Exception as e:
|
|
||||||
raise e
|
|
||||||
|
|
||||||
return app_dir
|
|
||||||
else:
|
|
||||||
if getattr(sys, "frozen", False): # Running as a PyInstaller executable
|
|
||||||
return os.path.dirname(sys.executable)
|
|
||||||
else: # Running as a regular Python script
|
|
||||||
running_dir = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
parent_dir = os.path.dirname(running_dir) # Go one folder up
|
|
||||||
return parent_dir
|
|
||||||
|
|
||||||
|
|
||||||
def get_downloads_folder():
|
|
||||||
"""
|
|
||||||
Get the path to the user's Downloads folder in a cross-platform manner.
|
|
||||||
By default, the Downloads directory is commonly located as:
|
|
||||||
- Windows: C:\\Users\\<Username>\\Downloads
|
|
||||||
- macOS: /Users/<Username>/Downloads
|
|
||||||
- Linux: /home/<Username>/Downloads
|
|
||||||
"""
|
|
||||||
return os.path.join(get_user_home_directory(), "Downloads")
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
import tkinter as tk
|
|
||||||
from tkinter import ttk
|
|
||||||
from tkinter import scrolledtext
|
|
||||||
import gc
|
|
||||||
import time
|
|
||||||
|
|
||||||
from utils.window_manager import center_window
|
|
||||||
from core.constants import *
|
|
||||||
|
|
||||||
|
|
||||||
class CustomDialog(tk.Tk):
|
|
||||||
def __init__(self, message, msg_type="info", timeout=None):
|
|
||||||
super().__init__()
|
|
||||||
self.message = message
|
|
||||||
self.msg_type = msg_type.lower()
|
|
||||||
self.timeout = timeout
|
|
||||||
self.after_id = None
|
|
||||||
|
|
||||||
self.style = ttk.Style(self)
|
|
||||||
self.style.configure("TFrame", background="#f2f2f2")
|
|
||||||
self.style.configure(
|
|
||||||
"TLabel", background="#f2f2f2", foreground="#333333", font=("Arial", 11)
|
|
||||||
)
|
|
||||||
self.style.configure("Header.TLabel", font=("Arial", 14, "bold"))
|
|
||||||
self.style.configure("TButton", font=("Arial", 11))
|
|
||||||
|
|
||||||
self._configure_window()
|
|
||||||
self._show_dialog()
|
|
||||||
|
|
||||||
if self.timeout:
|
|
||||||
self.after_id = self.after(self.timeout, self.close)
|
|
||||||
|
|
||||||
# Allow closing with keyboard shortcuts
|
|
||||||
self.bind("<Return>", lambda e: self.close())
|
|
||||||
self.bind("<Escape>", lambda e: self.close())
|
|
||||||
|
|
||||||
def _configure_window(self):
|
|
||||||
"""Configure the dialog window properties."""
|
|
||||||
self.title("ClipCascade")
|
|
||||||
self.geometry(
|
|
||||||
"600x300+{}+{}".format(
|
|
||||||
(self.winfo_screenwidth() - 600) // 2,
|
|
||||||
(self.winfo_screenheight() - 300) // 2,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
self.update_idletasks()
|
|
||||||
center_window(self)
|
|
||||||
self.attributes("-topmost", True)
|
|
||||||
self.resizable(False, False)
|
|
||||||
self.focus_force()
|
|
||||||
|
|
||||||
if PLATFORM == MACOS:
|
|
||||||
self.after(100, self._macos_restore_focus)
|
|
||||||
|
|
||||||
def _show_dialog(self):
|
|
||||||
"""Display the appropriate dialog based on the message type."""
|
|
||||||
type_config = {
|
|
||||||
"success": ("Success", "green", "✔"),
|
|
||||||
"error": ("Error", "red", "✖"),
|
|
||||||
"warning": ("Warning", "orange", "⚠"),
|
|
||||||
"info": ("Information", "blue", "ℹ"),
|
|
||||||
}
|
|
||||||
title, color, symbol = type_config.get(self.msg_type, ("Message", "black", "•"))
|
|
||||||
|
|
||||||
# Create a main frame
|
|
||||||
main_frame = ttk.Frame(self, padding="20 20 20 20")
|
|
||||||
main_frame.pack(fill="both", expand=True)
|
|
||||||
|
|
||||||
# Title frame with symbol and title
|
|
||||||
title_frame = ttk.Frame(main_frame)
|
|
||||||
title_frame.pack(pady=(0, 10), fill="x")
|
|
||||||
|
|
||||||
symbol_label = ttk.Label(
|
|
||||||
title_frame, text=symbol, font=("Arial", 18, "bold"), foreground=color
|
|
||||||
)
|
|
||||||
symbol_label.pack(side="left", padx=(0, 10))
|
|
||||||
|
|
||||||
title_label = ttk.Label(title_frame, text=title, style="Header.TLabel", foreground=color)
|
|
||||||
title_label.pack(side="left")
|
|
||||||
|
|
||||||
# Message frame for scrolled text
|
|
||||||
message_frame = ttk.Frame(main_frame)
|
|
||||||
message_frame.pack(fill="both", expand=True, pady=10)
|
|
||||||
|
|
||||||
# ScrolledText widget for the message
|
|
||||||
self.text_widget = scrolledtext.ScrolledText(
|
|
||||||
message_frame, wrap="word", font=("Arial", 11), width=70, height=8
|
|
||||||
)
|
|
||||||
self.text_widget.pack(fill="both", expand=True)
|
|
||||||
|
|
||||||
# Insert the message and disable editing
|
|
||||||
self.text_widget.insert("1.0", self.message)
|
|
||||||
self.text_widget.config(state="disabled")
|
|
||||||
|
|
||||||
# Button frame
|
|
||||||
button_frame = ttk.Frame(main_frame)
|
|
||||||
button_frame.pack(pady=(10, 0))
|
|
||||||
|
|
||||||
ok_button = ttk.Button(button_frame, text="OK", command=self.close, width=10)
|
|
||||||
ok_button.pack()
|
|
||||||
ok_button.focus_set()
|
|
||||||
|
|
||||||
def _macos_restore_focus(self):
|
|
||||||
"""Force macOS to properly activate and focus the dialog."""
|
|
||||||
try:
|
|
||||||
from AppKit import NSApplication
|
|
||||||
app = NSApplication.sharedApplication()
|
|
||||||
app.activateIgnoringOtherApps_(True)
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
self.lift()
|
|
||||||
self.focus_force()
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
if self.after_id:
|
|
||||||
self.after_cancel(self.after_id) # Cancel the after event if it exists
|
|
||||||
self.after_id = None
|
|
||||||
self.destroy()
|
|
||||||
gc.collect()
|
|
||||||
time.sleep(0.5)
|
|
||||||
@@ -1,648 +0,0 @@
|
|||||||
import re
|
|
||||||
import tkinter as tk
|
|
||||||
from tkinter import ttk, font
|
|
||||||
import gc
|
|
||||||
import time
|
|
||||||
import os
|
|
||||||
|
|
||||||
from utils.window_manager import center_window
|
|
||||||
from core.config import Config
|
|
||||||
from gui.info import CustomDialog
|
|
||||||
from core.constants import *
|
|
||||||
|
|
||||||
|
|
||||||
class HoverTooltip:
|
|
||||||
"""Simple hover tooltip for Tk widgets."""
|
|
||||||
|
|
||||||
def __init__(self, widget, text, wraplength=520):
|
|
||||||
self.widget = widget
|
|
||||||
self.text = text
|
|
||||||
self.wraplength = wraplength
|
|
||||||
self.tooltip_window = None
|
|
||||||
self.widget.bind("<Enter>", self._show, add="+")
|
|
||||||
self.widget.bind("<Leave>", self._hide, add="+")
|
|
||||||
self.widget.bind("<ButtonPress>", self._hide, add="+")
|
|
||||||
|
|
||||||
def _show(self, _event=None):
|
|
||||||
if self.tooltip_window or not self.text.strip():
|
|
||||||
return
|
|
||||||
|
|
||||||
# Position tooltip near the widget with an offset.
|
|
||||||
x = self.widget.winfo_rootx() + 18
|
|
||||||
y = self.widget.winfo_rooty() + self.widget.winfo_height() + 8
|
|
||||||
|
|
||||||
self.tooltip_window = tw = tk.Toplevel(self.widget)
|
|
||||||
tw.wm_overrideredirect(True)
|
|
||||||
tw.wm_geometry(f"+{x}+{y}")
|
|
||||||
|
|
||||||
label = tk.Label(
|
|
||||||
tw,
|
|
||||||
text=self.text,
|
|
||||||
justify=tk.LEFT,
|
|
||||||
relief=tk.SOLID,
|
|
||||||
borderwidth=1,
|
|
||||||
background="#ffffe0",
|
|
||||||
foreground="#202020",
|
|
||||||
font=("Helvetica", 10),
|
|
||||||
wraplength=self.wraplength,
|
|
||||||
padx=8,
|
|
||||||
pady=6,
|
|
||||||
)
|
|
||||||
label.pack()
|
|
||||||
|
|
||||||
def _hide(self, _event=None):
|
|
||||||
if self.tooltip_window:
|
|
||||||
self.tooltip_window.destroy()
|
|
||||||
self.tooltip_window = None
|
|
||||||
|
|
||||||
|
|
||||||
class LoginForm(tk.Tk):
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
config: Config,
|
|
||||||
on_login_callback=None,
|
|
||||||
on_quit_callback=None,
|
|
||||||
):
|
|
||||||
super().__init__()
|
|
||||||
self.config = config
|
|
||||||
self.on_login_callback = on_login_callback
|
|
||||||
self.on_quit_callback = on_quit_callback
|
|
||||||
|
|
||||||
if PLATFORM == MACOS or PLATFORM.startswith(LINUX):
|
|
||||||
# Set an initial size to avoid 0 size issue
|
|
||||||
# self.geometry("800x450")
|
|
||||||
self.update_idletasks()
|
|
||||||
|
|
||||||
self.withdraw() # Hide the root window
|
|
||||||
self.title("ClipCascade")
|
|
||||||
self._tooltips = []
|
|
||||||
|
|
||||||
# Make the window appear on top and grab focus
|
|
||||||
self.attributes("-topmost", True)
|
|
||||||
self.focus_force()
|
|
||||||
|
|
||||||
# Configure styles
|
|
||||||
style = ttk.Style(self)
|
|
||||||
# Use default theme (may vary by OS); consider 'clam', 'alt', or 'default'
|
|
||||||
style.configure("TLabel", font=("Helvetica", 13), padding=5)
|
|
||||||
style.configure("TEntry", font=("Helvetica", 13), padding=5)
|
|
||||||
style.configure("TButton", font=("Helvetica", 13), padding=5)
|
|
||||||
style.configure("TCheckbutton", font=("Helvetica", 13), padding=5)
|
|
||||||
|
|
||||||
# Main frame with padding
|
|
||||||
main_frame = ttk.Frame(self, padding=20)
|
|
||||||
main_frame.pack(fill=tk.BOTH, expand=True)
|
|
||||||
|
|
||||||
# Title Label
|
|
||||||
title_label = ttk.Label(
|
|
||||||
main_frame,
|
|
||||||
text="Please Log In",
|
|
||||||
font=("Helvetica", 16, "bold"),
|
|
||||||
takefocus=False,
|
|
||||||
)
|
|
||||||
title_label.pack(pady=(0, 10))
|
|
||||||
|
|
||||||
# Create a frame for the fields
|
|
||||||
self.field_frame = ttk.Frame(main_frame)
|
|
||||||
self.field_frame.pack(padx=10, pady=10, fill=tk.X)
|
|
||||||
|
|
||||||
# Username
|
|
||||||
user_label = ttk.Label(self.field_frame, text="Username:")
|
|
||||||
user_label.grid(row=0, column=0, padx=(0, 10), pady=5, sticky=tk.W)
|
|
||||||
self.username_entry = ttk.Entry(self.field_frame, width=50, font=("Helvetica", 13))
|
|
||||||
self.username_entry.insert(0, self.config.data["username"])
|
|
||||||
self.username_entry.grid(row=0, column=1, padx=10, pady=5, sticky=tk.W + tk.E)
|
|
||||||
self._add_tooltip(
|
|
||||||
[user_label, self.username_entry],
|
|
||||||
"Username used to log in to your ClipCascade account.\n\n"
|
|
||||||
"Use the same account name on all devices that should share clipboard data.",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Password
|
|
||||||
pass_label = ttk.Label(self.field_frame, text="Password:")
|
|
||||||
pass_label.grid(row=1, column=0, padx=(0, 10), pady=5, sticky=tk.W)
|
|
||||||
self.password_frame = ttk.Frame(self.field_frame)
|
|
||||||
self.password_frame.grid(row=1, column=1, padx=10, pady=5, sticky=tk.W + tk.E)
|
|
||||||
self.password_entry = ttk.Entry(
|
|
||||||
self.password_frame, show="*", width=47, font=("Helvetica", 13)
|
|
||||||
)
|
|
||||||
self.password_entry.insert(0, self.config.data["password"])
|
|
||||||
self.password_entry.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
|
||||||
|
|
||||||
self.eye_icon = ttk.Label(
|
|
||||||
self.password_frame, text="🙈", cursor="hand2", takefocus=False
|
|
||||||
)
|
|
||||||
self.eye_icon.pack(side=tk.RIGHT, padx=(5, 0))
|
|
||||||
self.eye_icon.bind("<Button-1>", self._toggle_password)
|
|
||||||
self._add_tooltip(
|
|
||||||
[pass_label, self.password_entry, self.eye_icon],
|
|
||||||
"Account password used for server authentication.\n\n"
|
|
||||||
"If encryption is enabled, this password is also used to derive your encryption key.\n"
|
|
||||||
"Use the same password on all devices for successful decrypt/sync.",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Server URL
|
|
||||||
server_label = ttk.Label(self.field_frame, text="Server URL:")
|
|
||||||
server_label.grid(row=2, column=0, padx=(0, 10), pady=5, sticky=tk.W)
|
|
||||||
self.server_url_entry = ttk.Entry(self.field_frame, width=50, font=("Helvetica", 13))
|
|
||||||
self.server_url_entry.insert(0, self.config.data["server_url"])
|
|
||||||
self.server_url_entry.grid(row=2, column=1, padx=10, pady=5, sticky=tk.W + tk.E)
|
|
||||||
self._add_tooltip(
|
|
||||||
[server_label, self.server_url_entry],
|
|
||||||
"Address of your ClipCascade server.\n\n"
|
|
||||||
"Examples:\n"
|
|
||||||
"- Local server: http://localhost:8080\n"
|
|
||||||
"- LAN server: http://192.168.1.50:8080\n"
|
|
||||||
"- Reverse proxy/domain: https://clipcascade.example.com\n\n"
|
|
||||||
"Include protocol (http/https). Do not add /login or /clipsocket.",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Configure grid weights to ensure entries expand
|
|
||||||
self.field_frame.columnconfigure(1, weight=1)
|
|
||||||
|
|
||||||
# Checkboxes frame
|
|
||||||
checks_frame = ttk.Frame(main_frame)
|
|
||||||
checks_frame.pack(pady=(5, 10), fill=tk.X)
|
|
||||||
|
|
||||||
# Encryption Checkbox
|
|
||||||
self.cipher_var = tk.BooleanVar(value=self.config.data["cipher_enabled"])
|
|
||||||
self.cipher_checkbox = ttk.Checkbutton(
|
|
||||||
checks_frame,
|
|
||||||
text="Enable Encryption (recommended)",
|
|
||||||
variable=self.cipher_var,
|
|
||||||
takefocus=False,
|
|
||||||
)
|
|
||||||
self.cipher_checkbox.pack(pady=3, anchor=tk.W)
|
|
||||||
self._add_tooltip(
|
|
||||||
[self.cipher_checkbox],
|
|
||||||
"Enables end-to-end encryption for clipboard content (recommended).\n\n"
|
|
||||||
"When enabled, clipboard data is encrypted on-device before sending.\n"
|
|
||||||
"All devices in the same account must use the same encryption settings "
|
|
||||||
"(password, salt, and hash rounds) to decrypt data correctly.",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Notification Checkbox
|
|
||||||
self.notification_var = tk.BooleanVar(value=self.config.data["notification"])
|
|
||||||
self.notification_checkbox = ttk.Checkbutton(
|
|
||||||
checks_frame,
|
|
||||||
text="Enable Notification",
|
|
||||||
variable=self.notification_var,
|
|
||||||
takefocus=False,
|
|
||||||
)
|
|
||||||
self.notification_checkbox.pack(pady=3, anchor=tk.W)
|
|
||||||
self._add_tooltip(
|
|
||||||
[self.notification_checkbox],
|
|
||||||
"Shows local notifications for connection status events.\n\n"
|
|
||||||
"Useful to know when WebSocket disconnects/reconnects happen.\n"
|
|
||||||
"Turn this off if you prefer a quieter experience with fewer popups.",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Button frame
|
|
||||||
button_frame = ttk.Frame(main_frame)
|
|
||||||
button_frame.pack(pady=(10, 0))
|
|
||||||
|
|
||||||
# Login Button
|
|
||||||
self.login_button = ttk.Button(button_frame, text="Login", command=self.on_login)
|
|
||||||
self.login_button.pack()
|
|
||||||
|
|
||||||
# Extra Configurations Toggle Label
|
|
||||||
# macOS: show advanced fields by default
|
|
||||||
self.show_extra = PLATFORM == MACOS
|
|
||||||
self.toggle_normal_color = "#007acc" # neutral blue with strong contrast
|
|
||||||
self.toggle_hover_color = "#005a9e" # slightly darker for hover feedback
|
|
||||||
self.toggle_label = tk.Label(
|
|
||||||
main_frame,
|
|
||||||
text="Hide Extra Config" if PLATFORM == MACOS else "Enable Extra Config",
|
|
||||||
fg=self.toggle_normal_color,
|
|
||||||
cursor="hand2",
|
|
||||||
font=font.Font(family="Helvetica", size=13, underline=True),
|
|
||||||
takefocus=False,
|
|
||||||
)
|
|
||||||
self.toggle_label.pack(pady=(5, 10))
|
|
||||||
self.toggle_label.bind("<Button-1>", self._toggle_extra_config)
|
|
||||||
self._add_tooltip(
|
|
||||||
[self.toggle_label],
|
|
||||||
"Shows advanced settings for encryption and clipboard behavior.\n\n"
|
|
||||||
"Most users can keep defaults. Open this section only if you need custom tuning.",
|
|
||||||
)
|
|
||||||
|
|
||||||
# hover effects for the toggle label
|
|
||||||
self.toggle_label.bind("<Enter>", self._on_hover)
|
|
||||||
self.toggle_label.bind("<Leave>", self._on_leave)
|
|
||||||
|
|
||||||
# Extra Configuration Frame with Scrollbar
|
|
||||||
self.extra_frame_container = ttk.Frame(main_frame)
|
|
||||||
if PLATFORM == MACOS:
|
|
||||||
self.extra_frame_container.pack(fill=tk.BOTH, expand=True)
|
|
||||||
else:
|
|
||||||
self.extra_frame_container.pack_forget()
|
|
||||||
|
|
||||||
# Create a canvas inside the extra_frame_container
|
|
||||||
self.extra_canvas = tk.Canvas(
|
|
||||||
self.extra_frame_container,
|
|
||||||
borderwidth=0,
|
|
||||||
background="#f0f0f0",
|
|
||||||
takefocus=False,
|
|
||||||
)
|
|
||||||
self.extra_canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
|
||||||
|
|
||||||
# Add a vertical scrollbar to the canvas
|
|
||||||
self.scrollbar = ttk.Scrollbar(
|
|
||||||
self.extra_frame_container,
|
|
||||||
orient="vertical",
|
|
||||||
command=self.extra_canvas.yview,
|
|
||||||
takefocus=False,
|
|
||||||
)
|
|
||||||
self.scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
|
||||||
|
|
||||||
# Configure the canvas to use the scrollbar
|
|
||||||
self.extra_canvas.configure(yscrollcommand=self.scrollbar.set)
|
|
||||||
|
|
||||||
# Create a frame inside the canvas to hold the extra configuration widgets
|
|
||||||
self.extra_frame = ttk.Frame(self.extra_canvas, padding=10)
|
|
||||||
self.extra_frame_window = self.extra_canvas.create_window(
|
|
||||||
(0, 0), window=self.extra_frame, anchor="nw"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Bind the configure event to update the scrollregion
|
|
||||||
self.extra_frame.bind(
|
|
||||||
"<Configure>",
|
|
||||||
lambda event: self.extra_canvas.configure(scrollregion=self.extra_canvas.bbox("all")),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Bind a configure event to the container to adjust the width
|
|
||||||
self.extra_frame_container.bind("<Configure>", self._on_extra_container_configure)
|
|
||||||
|
|
||||||
# Hash Rounds
|
|
||||||
hash_rounds_label = ttk.Label(self.extra_frame, text="Hash Rounds:")
|
|
||||||
hash_rounds_label.grid(row=0, column=0, padx=(0, 10), pady=5, sticky=tk.W)
|
|
||||||
self.hash_rounds_entry = ttk.Entry(self.extra_frame, width=50, font=("Helvetica", 13))
|
|
||||||
self.hash_rounds_entry.insert(0, str(self.config.data["hash_rounds"]))
|
|
||||||
self.hash_rounds_entry.grid(row=0, column=1, padx=10, pady=5, sticky=tk.W + tk.E)
|
|
||||||
self._add_tooltip(
|
|
||||||
[hash_rounds_label, self.hash_rounds_entry],
|
|
||||||
"Number of hash iterations used for encryption key derivation.\n\n"
|
|
||||||
"Higher value = stronger brute-force resistance, but slower processing.\n"
|
|
||||||
"Default is tuned for balance. Keep default unless you know why to change.\n"
|
|
||||||
"Must be a positive integer and must match on every device.",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Salt
|
|
||||||
salt_label = ttk.Label(self.extra_frame, text="Salt:")
|
|
||||||
salt_label.grid(row=1, column=0, padx=(0, 10), pady=5, sticky=tk.W)
|
|
||||||
self.salt_entry = ttk.Entry(self.extra_frame, width=50, font=("Helvetica", 13))
|
|
||||||
self.salt_entry.insert(0, self.config.data["salt"])
|
|
||||||
self.salt_entry.grid(row=1, column=1, padx=10, pady=5, sticky=tk.W + tk.E)
|
|
||||||
self._add_tooltip(
|
|
||||||
[salt_label, self.salt_entry],
|
|
||||||
"Optional extra text mixed into encryption key generation.\n\n"
|
|
||||||
"It can be any combination of letters and numbers (you can also include symbols/spaces if desired).\n"
|
|
||||||
"Use the same exact salt on all devices if encryption is enabled.\n"
|
|
||||||
"If left blank, encryption still works using password + hash rounds.\n"
|
|
||||||
"Changing salt later will prevent old devices from decrypting new clipboard data.",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Save Password Locally Checkbox
|
|
||||||
save_password_label = ttk.Label(
|
|
||||||
self.extra_frame,
|
|
||||||
text="Store Password Locally\n(not recommended; \nonly works if encryption is disabled):",
|
|
||||||
)
|
|
||||||
save_password_label.grid(row=2, column=0, padx=(0, 10), pady=5, sticky=tk.W)
|
|
||||||
self.save_password_var = tk.BooleanVar(value=self.config.data["save_password"])
|
|
||||||
self.save_password_checkbox = ttk.Checkbutton(
|
|
||||||
self.extra_frame,
|
|
||||||
variable=self.save_password_var,
|
|
||||||
takefocus=False,
|
|
||||||
)
|
|
||||||
self.save_password_checkbox.grid(row=2, column=1, padx=10, pady=5, sticky=tk.W)
|
|
||||||
self._add_tooltip(
|
|
||||||
[save_password_label, self.save_password_checkbox],
|
|
||||||
"Stores your plain password locally for automatic login convenience.\n\n"
|
|
||||||
"Security trade-off: avoid this on shared or untrusted machines.\n"
|
|
||||||
"This only works when encryption is disabled.",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Maximum Clipboard Size - Local Limit
|
|
||||||
local_clipboard_size_label = ttk.Label(
|
|
||||||
self.extra_frame, text="Maximum Clipboard Size\nLocal Limit (in bytes):"
|
|
||||||
)
|
|
||||||
local_clipboard_size_label.grid(row=3, column=0, padx=(0, 10), pady=5, sticky=tk.W)
|
|
||||||
self.local_clipboard_size_entry = ttk.Entry(
|
|
||||||
self.extra_frame, width=50, font=("Helvetica", 13)
|
|
||||||
)
|
|
||||||
self.local_clipboard_size_entry.insert(
|
|
||||||
0, str(self.config.data["max_clipboard_size_local_limit_bytes"] or "")
|
|
||||||
)
|
|
||||||
self.local_clipboard_size_entry.grid(row=3, column=1, padx=10, pady=5, sticky=tk.W + tk.E)
|
|
||||||
self._add_tooltip(
|
|
||||||
[local_clipboard_size_label, self.local_clipboard_size_entry],
|
|
||||||
"Optional local safety limit for clipboard payload size (incoming and outgoing) in bytes.\n\n"
|
|
||||||
"Example: 1 MiB = 1048576 bytes.\n"
|
|
||||||
"Leave empty for no local limit.\n"
|
|
||||||
"Use a lower value if your device struggles with very large clipboard data "
|
|
||||||
"(large text/images/files). Must be a positive integer.",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Enable Image Sharing Checkbox
|
|
||||||
enable_image_sharing_label = ttk.Label(self.extra_frame, text="Enable Image Sharing:")
|
|
||||||
enable_image_sharing_label.grid(row=4, column=0, padx=(0, 10), pady=5, sticky=tk.W)
|
|
||||||
self.enable_image_sharing_var = tk.BooleanVar(
|
|
||||||
value=self.config.data["enable_image_sharing"]
|
|
||||||
)
|
|
||||||
self.enable_image_sharing_checkbox = ttk.Checkbutton(
|
|
||||||
self.extra_frame,
|
|
||||||
variable=self.enable_image_sharing_var,
|
|
||||||
takefocus=False,
|
|
||||||
)
|
|
||||||
self.enable_image_sharing_checkbox.grid(row=4, column=1, padx=10, pady=5, sticky=tk.W)
|
|
||||||
self._add_tooltip(
|
|
||||||
[enable_image_sharing_label, self.enable_image_sharing_checkbox],
|
|
||||||
"Controls whether this device sends copied images to other devices.\n\n"
|
|
||||||
"If disabled, this device can still receive images sent from other devices.",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Enable File Sharing Checkbox
|
|
||||||
enable_file_sharing_label = ttk.Label(self.extra_frame, text="Enable File Sharing:")
|
|
||||||
enable_file_sharing_label.grid(row=5, column=0, padx=(0, 10), pady=5, sticky=tk.W)
|
|
||||||
self.enable_file_sharing_var = tk.BooleanVar(value=self.config.data["enable_file_sharing"])
|
|
||||||
self.enable_file_sharing_checkbox = ttk.Checkbutton(
|
|
||||||
self.extra_frame,
|
|
||||||
variable=self.enable_file_sharing_var,
|
|
||||||
takefocus=False,
|
|
||||||
)
|
|
||||||
self.enable_file_sharing_checkbox.grid(row=5, column=1, padx=10, pady=5, sticky=tk.W)
|
|
||||||
self._add_tooltip(
|
|
||||||
[enable_file_sharing_label, self.enable_file_sharing_checkbox],
|
|
||||||
"Controls whether this device sends copied/shared files to other devices.\n\n"
|
|
||||||
"If disabled, this device can still receive files sent from other devices.",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Default File Download Location
|
|
||||||
default_file_download_location_label = ttk.Label(
|
|
||||||
self.extra_frame, text="Default File Download Location:"
|
|
||||||
)
|
|
||||||
default_file_download_location_label.grid(
|
|
||||||
row=6, column=0, padx=(0, 10), pady=5, sticky=tk.W
|
|
||||||
)
|
|
||||||
self.default_file_download_location_entry = ttk.Entry(
|
|
||||||
self.extra_frame, width=50, font=("Helvetica", 13)
|
|
||||||
)
|
|
||||||
self.default_file_download_location_entry.insert(
|
|
||||||
0, self.config.data["default_file_download_location"]
|
|
||||||
)
|
|
||||||
self.default_file_download_location_entry.grid(
|
|
||||||
row=6, column=1, padx=10, pady=5, sticky=tk.W + tk.E
|
|
||||||
)
|
|
||||||
self._add_tooltip(
|
|
||||||
[default_file_download_location_label, self.default_file_download_location_entry],
|
|
||||||
"Optional default folder to save incoming files automatically.\n\n"
|
|
||||||
"Examples:\n"
|
|
||||||
"- Windows: C:\\Users\\YourName\\Downloads\n"
|
|
||||||
"- macOS/Linux: /Users/yourname/Downloads\n\n"
|
|
||||||
"Leave blank to choose a location manually when downloading files.\n"
|
|
||||||
"Path must already exist on this device.",
|
|
||||||
)
|
|
||||||
|
|
||||||
# SSL CA bundle (private / corporate PKI)
|
|
||||||
ssl_ca_label = ttk.Label(self.extra_frame, text="SSL CA bundle (optional):")
|
|
||||||
ssl_ca_label.grid(row=7, column=0, padx=(0, 10), pady=5, sticky=tk.W)
|
|
||||||
self.ssl_ca_bundle_entry = ttk.Entry(
|
|
||||||
self.extra_frame, width=50, font=("Helvetica", 13)
|
|
||||||
)
|
|
||||||
self.ssl_ca_bundle_entry.insert(
|
|
||||||
0, self.config.data.get("ssl_ca_bundle") or ""
|
|
||||||
)
|
|
||||||
self.ssl_ca_bundle_entry.grid(row=7, column=1, padx=10, pady=5, sticky=tk.W + tk.E)
|
|
||||||
self._add_tooltip(
|
|
||||||
[ssl_ca_label, self.ssl_ca_bundle_entry],
|
|
||||||
"Path to a PEM file that contains your root CA (or full chain) for HTTPS/WSS.\n\n"
|
|
||||||
"Leave empty for default verification (public CAs / OS defaults).\n"
|
|
||||||
"Use this when your server uses a certificate from a private or internal CA.\n\n"
|
|
||||||
"Example: /etc/ssl/corp-root.pem or C:\\\\certs\\\\corp-root.pem",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Configure grid weights for extra_frame
|
|
||||||
self.extra_frame.columnconfigure(1, weight=1)
|
|
||||||
|
|
||||||
# Bind mouse wheel events for scrolling
|
|
||||||
self._bind_mousewheel()
|
|
||||||
|
|
||||||
# Handle window close
|
|
||||||
self.protocol("WM_DELETE_WINDOW", self.on_quit)
|
|
||||||
|
|
||||||
# Update idle tasks to calculate widget sizes
|
|
||||||
self.update_idletasks()
|
|
||||||
|
|
||||||
# Set a maximum height for the window
|
|
||||||
self.max_window_height = 600
|
|
||||||
|
|
||||||
# on press enter key
|
|
||||||
self.bind("<Return>", lambda event: self.on_login())
|
|
||||||
|
|
||||||
self._sync_login_tab_order()
|
|
||||||
|
|
||||||
self.deiconify() # Show window
|
|
||||||
self.update_idletasks()
|
|
||||||
center_window(self, self.max_window_height)
|
|
||||||
|
|
||||||
# Drop always-on-top after showing
|
|
||||||
self.after(300, lambda: self.attributes("-topmost", False))
|
|
||||||
|
|
||||||
if PLATFORM == MACOS:
|
|
||||||
self.after(400, self._macos_restore_focus)
|
|
||||||
|
|
||||||
def _sync_login_tab_order(self):
|
|
||||||
"""Tab cycles username → password → server → Login → [extra entries if shown] → repeat.
|
|
||||||
Checkboxes and chrome are excluded from keyboard focus."""
|
|
||||||
extra_entries = (
|
|
||||||
self.hash_rounds_entry,
|
|
||||||
self.salt_entry,
|
|
||||||
self.local_clipboard_size_entry,
|
|
||||||
self.default_file_download_location_entry,
|
|
||||||
self.ssl_ca_bundle_entry,
|
|
||||||
)
|
|
||||||
for w in extra_entries:
|
|
||||||
w.configure(takefocus=bool(self.show_extra))
|
|
||||||
|
|
||||||
def _add_tooltip(self, widgets, text):
|
|
||||||
for widget in widgets:
|
|
||||||
self._tooltips.append(HoverTooltip(widget, text))
|
|
||||||
|
|
||||||
def _macos_restore_focus(self):
|
|
||||||
"""Force macOS to properly activate and focus the window.
|
|
||||||
Needed after sequential tk.Tk() creation/destruction cycles."""
|
|
||||||
try:
|
|
||||||
from AppKit import NSApplication
|
|
||||||
app = NSApplication.sharedApplication()
|
|
||||||
app.activateIgnoringOtherApps_(True)
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
self.lift()
|
|
||||||
self.focus_force()
|
|
||||||
self.username_entry.focus_set()
|
|
||||||
|
|
||||||
def _toggle_password(self, event=None):
|
|
||||||
if self.password_entry.cget("show") == "*":
|
|
||||||
self.password_entry.config(show="")
|
|
||||||
self.eye_icon.config(text="🐵")
|
|
||||||
else:
|
|
||||||
self.password_entry.config(show="*")
|
|
||||||
self.eye_icon.config(text="🙈")
|
|
||||||
|
|
||||||
def _toggle_extra_config(self, event=None):
|
|
||||||
"""Toggle the visibility of the extra configuration fields."""
|
|
||||||
extra_entries = (
|
|
||||||
self.hash_rounds_entry,
|
|
||||||
self.salt_entry,
|
|
||||||
self.local_clipboard_size_entry,
|
|
||||||
self.default_file_download_location_entry,
|
|
||||||
self.ssl_ca_bundle_entry,
|
|
||||||
)
|
|
||||||
if self.show_extra:
|
|
||||||
focused = self.focus_get()
|
|
||||||
if focused in extra_entries:
|
|
||||||
self.login_button.focus_set()
|
|
||||||
self.extra_frame_container.pack_forget()
|
|
||||||
self.toggle_label.config(text="Enable Extra Config")
|
|
||||||
else:
|
|
||||||
self.extra_frame_container.pack(fill=tk.BOTH, expand=True)
|
|
||||||
self.toggle_label.config(text="Hide Extra Config")
|
|
||||||
self.show_extra = not self.show_extra
|
|
||||||
self._sync_login_tab_order()
|
|
||||||
|
|
||||||
self.update_idletasks()
|
|
||||||
self.geometry("")
|
|
||||||
self.update_idletasks()
|
|
||||||
center_window(self)
|
|
||||||
|
|
||||||
def _on_hover(self, event=None):
|
|
||||||
"""Change text color on hover."""
|
|
||||||
self.toggle_label.config(fg=self.toggle_hover_color)
|
|
||||||
|
|
||||||
def _on_leave(self, event=None):
|
|
||||||
"""Revert text color when not hovered."""
|
|
||||||
self.toggle_label.config(fg=self.toggle_normal_color)
|
|
||||||
|
|
||||||
def _bind_mousewheel(self):
|
|
||||||
"""Bind mouse wheel events to the canvas for scrolling."""
|
|
||||||
if PLATFORM == WINDOWS:
|
|
||||||
self.extra_canvas.bind_all("<MouseWheel>", self._on_mousewheel_windows)
|
|
||||||
elif PLATFORM == MACOS:
|
|
||||||
self.extra_canvas.bind_all("<MouseWheel>", self._on_mousewheel_mac)
|
|
||||||
elif PLATFORM.startswith(LINUX):
|
|
||||||
# Linux uses Button-4 and Button-5 for scroll up/down
|
|
||||||
self.extra_canvas.bind_all("<Button-4>", self._on_mousewheel_linux)
|
|
||||||
self.extra_canvas.bind_all("<Button-5>", self._on_mousewheel_linux)
|
|
||||||
|
|
||||||
def _on_mousewheel_windows(self, event):
|
|
||||||
"""Handle mouse wheel scrolling on Windows."""
|
|
||||||
self.extra_canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
|
|
||||||
|
|
||||||
def _on_mousewheel_mac(self, event):
|
|
||||||
"""Handle mouse wheel scrolling on macOS."""
|
|
||||||
self.extra_canvas.yview_scroll(int(-1 * event.delta), "units")
|
|
||||||
|
|
||||||
def _on_mousewheel_linux(self, event):
|
|
||||||
"""Handle mouse wheel scrolling on Linux."""
|
|
||||||
if event.num == 4:
|
|
||||||
self.extra_canvas.yview_scroll(-1, "units")
|
|
||||||
elif event.num == 5:
|
|
||||||
self.extra_canvas.yview_scroll(1, "units")
|
|
||||||
|
|
||||||
def _on_extra_container_configure(self, event):
|
|
||||||
"""Adjust the width of the extra_frame as the container is resized."""
|
|
||||||
canvas_width = event.width - self.scrollbar.winfo_width()
|
|
||||||
self.extra_canvas.itemconfigure(self.extra_frame_window, width=canvas_width)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def remove_trailing_slash(entry):
|
|
||||||
return re.sub(r"/+$", "", entry)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def is_positive_integer(value):
|
|
||||||
if value.isdigit() and int(value) > 0:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def on_login(self):
|
|
||||||
# save data to config
|
|
||||||
self.config.data["username"] = self.username_entry.get()
|
|
||||||
self.config.data["password"] = self.password_entry.get()
|
|
||||||
self.config.data["server_url"] = LoginForm.remove_trailing_slash(
|
|
||||||
self.server_url_entry.get().strip()
|
|
||||||
)
|
|
||||||
self.config.data["cipher_enabled"] = self.cipher_var.get()
|
|
||||||
self.config.data["notification"] = self.notification_var.get()
|
|
||||||
|
|
||||||
# extra fields
|
|
||||||
|
|
||||||
# Validate hash_rounds
|
|
||||||
hash_rounds_input = self.hash_rounds_entry.get().strip()
|
|
||||||
if not LoginForm.is_positive_integer(hash_rounds_input):
|
|
||||||
CustomDialog(
|
|
||||||
"Invalid Input\nHash Rounds must be a positive integer.",
|
|
||||||
msg_type="error",
|
|
||||||
).mainloop()
|
|
||||||
return # retry login
|
|
||||||
self.config.data["hash_rounds"] = int(hash_rounds_input)
|
|
||||||
|
|
||||||
self.config.data["salt"] = self.salt_entry.get()
|
|
||||||
self.config.data["save_password"] = self.save_password_var.get()
|
|
||||||
|
|
||||||
# Validate max_clipboard_size_local_limit_bytes
|
|
||||||
max_clipboard_size_local_limit_bytes_input = self.local_clipboard_size_entry.get().strip()
|
|
||||||
if max_clipboard_size_local_limit_bytes_input == "":
|
|
||||||
self.config.data["max_clipboard_size_local_limit_bytes"] = None
|
|
||||||
else:
|
|
||||||
if not LoginForm.is_positive_integer(max_clipboard_size_local_limit_bytes_input):
|
|
||||||
CustomDialog(
|
|
||||||
"Invalid Input\nMax Clipboard Size Local Limit must be a positive integer.",
|
|
||||||
msg_type="error",
|
|
||||||
).mainloop()
|
|
||||||
return # retry login
|
|
||||||
self.config.data["max_clipboard_size_local_limit_bytes"] = int(
|
|
||||||
max_clipboard_size_local_limit_bytes_input
|
|
||||||
)
|
|
||||||
|
|
||||||
self.config.data["enable_image_sharing"] = self.enable_image_sharing_var.get()
|
|
||||||
self.config.data["enable_file_sharing"] = self.enable_file_sharing_var.get()
|
|
||||||
|
|
||||||
# Validate file download location
|
|
||||||
default_file_download_location = self.default_file_download_location_entry.get().strip()
|
|
||||||
if default_file_download_location == "":
|
|
||||||
self.config.data["default_file_download_location"] = ""
|
|
||||||
else:
|
|
||||||
if not os.path.exists(default_file_download_location):
|
|
||||||
CustomDialog(
|
|
||||||
"Invalid Input\nDefault File Download Location does not exist.",
|
|
||||||
msg_type="error",
|
|
||||||
).mainloop()
|
|
||||||
return # retry login
|
|
||||||
self.config.data["default_file_download_location"] = default_file_download_location
|
|
||||||
|
|
||||||
ssl_ca_bundle = self.ssl_ca_bundle_entry.get().strip()
|
|
||||||
if ssl_ca_bundle:
|
|
||||||
if not os.path.isfile(ssl_ca_bundle):
|
|
||||||
CustomDialog(
|
|
||||||
"Invalid Input\nSSL CA bundle path is not a file or does not exist.",
|
|
||||||
msg_type="error",
|
|
||||||
).mainloop()
|
|
||||||
return
|
|
||||||
self.config.data["ssl_ca_bundle"] = ssl_ca_bundle
|
|
||||||
|
|
||||||
# call login callback
|
|
||||||
if self.on_login_callback:
|
|
||||||
self.on_login_callback()
|
|
||||||
|
|
||||||
self.close()
|
|
||||||
|
|
||||||
def on_quit(self):
|
|
||||||
if self.on_quit_callback:
|
|
||||||
self.on_quit_callback()
|
|
||||||
self.close()
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
self.destroy()
|
|
||||||
gc.collect()
|
|
||||||
time.sleep(0.5)
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
from tkinter import messagebox
|
|
||||||
|
|
||||||
|
|
||||||
class MessageBox:
|
|
||||||
def askquestion(self, title, message):
|
|
||||||
return messagebox.askquestion(title, message)
|
|
||||||
@@ -1,397 +0,0 @@
|
|||||||
import math
|
|
||||||
import os
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
import tkinter as tk
|
|
||||||
from tkinter import filedialog
|
|
||||||
import webbrowser
|
|
||||||
from pystray import Icon, MenuItem as item, Menu
|
|
||||||
from PIL import Image, ImageDraw
|
|
||||||
|
|
||||||
from core.config import Config
|
|
||||||
from gui.info import CustomDialog
|
|
||||||
from core.constants import *
|
|
||||||
|
|
||||||
if PLATFORM != WINDOWS:
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
|
|
||||||
class TaskbarPanel:
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
on_connect_callback: callable = None,
|
|
||||||
on_disconnect_callback: callable = None,
|
|
||||||
on_logoff_callback: callable = None,
|
|
||||||
new_version_available: list = None,
|
|
||||||
github_url: str = GITHUB_URL,
|
|
||||||
donation_url: str = None,
|
|
||||||
ws_interface=None, # type= interfaces.ws_interface.WSInterface
|
|
||||||
config: Config = None,
|
|
||||||
):
|
|
||||||
self.on_connect_callback = on_connect_callback
|
|
||||||
self.on_disconnect_callback = on_disconnect_callback
|
|
||||||
self.on_logoff_callback = on_logoff_callback
|
|
||||||
self.new_version_available = new_version_available
|
|
||||||
self.github_url = github_url
|
|
||||||
self.donation_url = donation_url
|
|
||||||
self.ws_interface = ws_interface
|
|
||||||
self.config = config
|
|
||||||
|
|
||||||
self.is_disconnecting = False
|
|
||||||
self.disconnecting_items = None
|
|
||||||
self.is_file_download_enabled = False
|
|
||||||
self.file_download_items = None
|
|
||||||
self.previous_stats: str = ""
|
|
||||||
self.previous_stats_items = None
|
|
||||||
|
|
||||||
self.root = tk.Tk()
|
|
||||||
self.root.withdraw() # Hide the root window
|
|
||||||
|
|
||||||
# Hide dock icon on macOS after creating tkinter window
|
|
||||||
if PLATFORM == MACOS:
|
|
||||||
try:
|
|
||||||
from AppKit import NSApplication, NSApplicationActivationPolicyAccessory
|
|
||||||
NSApplication.sharedApplication().setActivationPolicy_(NSApplicationActivationPolicyAccessory)
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Initial state: Connected
|
|
||||||
self.is_connected = True
|
|
||||||
|
|
||||||
# Create the tray icon
|
|
||||||
self.icon = Icon(
|
|
||||||
"ClipCascade", self.create_clipboard_icon(), menu=self.create_menu()
|
|
||||||
)
|
|
||||||
|
|
||||||
self.icon.title = "ClipCascade"
|
|
||||||
|
|
||||||
self.update_stats() # Start the stats update thread
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
self.icon.run()
|
|
||||||
|
|
||||||
def _create_clipboard_base_image(self):
|
|
||||||
"""Shared clipboard artwork for normal tray icon and file-download badge variant."""
|
|
||||||
width, height = 64, 64
|
|
||||||
image = Image.new("RGBA", (width, height), (0, 0, 0, 0))
|
|
||||||
draw = ImageDraw.Draw(image)
|
|
||||||
|
|
||||||
fill_color = (220, 220, 220)
|
|
||||||
outline_color = (255, 255, 255)
|
|
||||||
|
|
||||||
board_coords = (12, 12, 52, 57)
|
|
||||||
try:
|
|
||||||
draw.rounded_rectangle(
|
|
||||||
board_coords, radius=5, fill=None, outline=outline_color, width=3
|
|
||||||
)
|
|
||||||
except (AttributeError, TypeError):
|
|
||||||
draw.rectangle(board_coords, fill=None, outline=outline_color)
|
|
||||||
|
|
||||||
clip_coords = (22, 7, 42, 17)
|
|
||||||
try:
|
|
||||||
draw.rounded_rectangle(
|
|
||||||
clip_coords, radius=3, fill=fill_color, outline=outline_color, width=3
|
|
||||||
)
|
|
||||||
except (AttributeError, TypeError):
|
|
||||||
draw.rectangle(clip_coords, fill=fill_color, outline=outline_color)
|
|
||||||
|
|
||||||
return image
|
|
||||||
|
|
||||||
def create_clipboard_icon(self):
|
|
||||||
return self._create_clipboard_base_image()
|
|
||||||
|
|
||||||
def create_clipboard_icon_with_dot(self):
|
|
||||||
"""Same clipboard as normal state, plus a blue notification dot for pending file download."""
|
|
||||||
image = self._create_clipboard_base_image().copy()
|
|
||||||
draw = ImageDraw.Draw(image)
|
|
||||||
width, height = 64, 64
|
|
||||||
|
|
||||||
dot_radius = 8
|
|
||||||
dot_center_x = width - dot_radius - 5
|
|
||||||
dot_center_y = dot_radius + 5
|
|
||||||
dot_bbox = [
|
|
||||||
dot_center_x - dot_radius,
|
|
||||||
dot_center_y - dot_radius,
|
|
||||||
dot_center_x + dot_radius,
|
|
||||||
dot_center_y + dot_radius,
|
|
||||||
]
|
|
||||||
draw.ellipse(dot_bbox, fill=(0, 128, 255, 255))
|
|
||||||
|
|
||||||
highlight_radius = dot_radius // 2
|
|
||||||
highlight_center_x = dot_center_x - highlight_radius // 2
|
|
||||||
highlight_center_y = dot_center_y - highlight_radius // 2
|
|
||||||
highlight_bbox = [
|
|
||||||
highlight_center_x - highlight_radius,
|
|
||||||
highlight_center_y - highlight_radius,
|
|
||||||
highlight_center_x + highlight_radius,
|
|
||||||
highlight_center_y + highlight_radius,
|
|
||||||
]
|
|
||||||
draw.ellipse(highlight_bbox, fill=(255, 255, 255, 180))
|
|
||||||
|
|
||||||
return image
|
|
||||||
|
|
||||||
def create_menu(self, item_: tuple = None):
|
|
||||||
"""Create the menu for the tray icon.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
item_ (tuple, optional): The item that triggered the menu. Defaults to None.
|
|
||||||
item_ = (text, location, callback)
|
|
||||||
Returns:
|
|
||||||
Menu: The menu for the tray icon.
|
|
||||||
"""
|
|
||||||
# Menu items
|
|
||||||
menu_items = [
|
|
||||||
Menu.SEPARATOR,
|
|
||||||
item("🗒️ Open Logs", self._open_logs),
|
|
||||||
item("📂 Program Files", self._open_program_location),
|
|
||||||
Menu.SEPARATOR,
|
|
||||||
item("🏠 Homepage", self._open_homepage),
|
|
||||||
item("❓ Help", self._open_help),
|
|
||||||
item("💟 Donate", self._open_donate),
|
|
||||||
item("🌐 GitHub", self._open_github),
|
|
||||||
Menu.SEPARATOR,
|
|
||||||
item("🔒 Logoff and Quit", self._on_logoff),
|
|
||||||
item("❌ Quit", self._on_quit),
|
|
||||||
]
|
|
||||||
|
|
||||||
# Add connect/disconnect option (top of the menu - 0 index)
|
|
||||||
if not self.is_connected:
|
|
||||||
menu_items.insert(0, item("🔗 Connect", self._on_connect, default=True))
|
|
||||||
else:
|
|
||||||
if self.is_disconnecting and self.disconnecting_items is not None:
|
|
||||||
menu_items.insert(
|
|
||||||
self.disconnecting_items[1],
|
|
||||||
item(self.disconnecting_items[0], self.disconnecting_items[2]),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
menu_items.insert(0, item("⛓️💥 Disconnect", self._on_disconnect))
|
|
||||||
|
|
||||||
# Add update option (before the last 3 items)
|
|
||||||
if self.new_version_available is not None and self.new_version_available[0]:
|
|
||||||
menu_items.insert(
|
|
||||||
len(menu_items) - 3,
|
|
||||||
item(
|
|
||||||
f"🔄 Update ({self.new_version_available[2]} ➞ {self.new_version_available[1]})",
|
|
||||||
self._on_update,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add files download option (top of the menu - 0 index)
|
|
||||||
if self.is_file_download_enabled and self.file_download_items is not None:
|
|
||||||
menu_items.insert(
|
|
||||||
self.file_download_items[1],
|
|
||||||
item(
|
|
||||||
self.file_download_items[0],
|
|
||||||
self.file_download_items[2],
|
|
||||||
default=True,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add stats option (top of the menu - 0 index)
|
|
||||||
if self.previous_stats_items is not None:
|
|
||||||
menu_items.insert(
|
|
||||||
self.previous_stats_items[1],
|
|
||||||
item(
|
|
||||||
self.previous_stats_items[0],
|
|
||||||
self.previous_stats_items[2],
|
|
||||||
enabled=self.previous_stats_items[2] is not None,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
return Menu(*menu_items)
|
|
||||||
|
|
||||||
def update_menu(self, item_: tuple = None):
|
|
||||||
self.icon.menu = self.create_menu(item_=item_)
|
|
||||||
|
|
||||||
def update_stats(self):
|
|
||||||
threading.Thread(target=self._update_stats_thread, daemon=True).start()
|
|
||||||
|
|
||||||
def _update_stats_thread(self):
|
|
||||||
while True:
|
|
||||||
current_stats = self.ws_interface.get_stats()
|
|
||||||
if current_stats is not None and self.previous_stats != current_stats:
|
|
||||||
self.previous_stats = current_stats
|
|
||||||
self.previous_stats_items = (current_stats, 0, None)
|
|
||||||
self.update_menu()
|
|
||||||
time.sleep(1) # Sleep for 1 second
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def open_webbrowser(url):
|
|
||||||
try:
|
|
||||||
webbrowser.open(url)
|
|
||||||
except Exception as e:
|
|
||||||
CustomDialog(
|
|
||||||
f"Failed to open the browser. Here is the URL: {url}\nError: {e}",
|
|
||||||
msg_type="error",
|
|
||||||
).mainloop()
|
|
||||||
|
|
||||||
def _on_update(self, icon, item):
|
|
||||||
TaskbarPanel.open_webbrowser(self.new_version_available[3])
|
|
||||||
|
|
||||||
def _on_connect(self, icon, item):
|
|
||||||
if self.on_connect_callback:
|
|
||||||
try:
|
|
||||||
self.on_connect_callback()
|
|
||||||
except Exception as e:
|
|
||||||
pass
|
|
||||||
self.is_connected = True
|
|
||||||
self.update_menu()
|
|
||||||
|
|
||||||
def _on_disconnect(self, icon, item):
|
|
||||||
if self.on_disconnect_callback:
|
|
||||||
self.on_disconnect_callback()
|
|
||||||
if self.ws_interface is not None and self.ws_interface.is_auto_reconnecting:
|
|
||||||
threading.Thread(
|
|
||||||
target=self._wait_to_disconnect, args=(icon, item), daemon=True
|
|
||||||
).start()
|
|
||||||
else:
|
|
||||||
self.is_connected = False
|
|
||||||
self.update_menu()
|
|
||||||
|
|
||||||
def _wait_to_disconnect(self, icon, item):
|
|
||||||
"""
|
|
||||||
Wait for the auto-reconnect timer to expire before disconnecting.
|
|
||||||
"""
|
|
||||||
if self.ws_interface is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
timeout = math.ceil(self.ws_interface.get_total_timeout() / 1000)
|
|
||||||
while timeout > 0:
|
|
||||||
self.is_disconnecting = True
|
|
||||||
self.disconnecting_items = (
|
|
||||||
f"⏳ Disconnecting... ({timeout} sec)",
|
|
||||||
0,
|
|
||||||
None,
|
|
||||||
) # text, location, callback
|
|
||||||
self.update_menu()
|
|
||||||
time.sleep(1) # seconds
|
|
||||||
timeout -= 1
|
|
||||||
self.is_disconnecting = False
|
|
||||||
self.disconnecting_items = None
|
|
||||||
self.ws_interface.is_auto_reconnecting = False
|
|
||||||
self.is_connected = False
|
|
||||||
self.update_menu()
|
|
||||||
|
|
||||||
def _open_homepage(self, icon, item):
|
|
||||||
TaskbarPanel.open_webbrowser(self.config.data["server_url"])
|
|
||||||
|
|
||||||
def _open_github(self, icon, item):
|
|
||||||
TaskbarPanel.open_webbrowser(self.github_url)
|
|
||||||
|
|
||||||
def _open_help(self, icon, item):
|
|
||||||
TaskbarPanel.open_webbrowser(HELP_URL)
|
|
||||||
|
|
||||||
def _open_donate(self, icon, item):
|
|
||||||
if self.donation_url is not None:
|
|
||||||
TaskbarPanel.open_webbrowser(self.donation_url)
|
|
||||||
|
|
||||||
def open_location(self, path):
|
|
||||||
if PLATFORM == WINDOWS:
|
|
||||||
os.startfile(path)
|
|
||||||
elif PLATFORM == MACOS:
|
|
||||||
subprocess.run(["open", path])
|
|
||||||
elif PLATFORM.startswith(LINUX):
|
|
||||||
subprocess.run(["xdg-open", path])
|
|
||||||
|
|
||||||
def _open_logs(self, icon, item):
|
|
||||||
log_file_path = os.path.join(get_program_files_directory(), LOG_FILE_NAME)
|
|
||||||
if os.path.exists(log_file_path):
|
|
||||||
try:
|
|
||||||
self.open_location(log_file_path)
|
|
||||||
except Exception as e:
|
|
||||||
CustomDialog(
|
|
||||||
f"Failed to open the log file '{log_file_path}'.\nError: {e}",
|
|
||||||
msg_type="error",
|
|
||||||
).mainloop()
|
|
||||||
else:
|
|
||||||
CustomDialog(
|
|
||||||
f"Log file not found at '{log_file_path}'.", msg_type="error"
|
|
||||||
).mainloop()
|
|
||||||
|
|
||||||
def _open_program_location(self, icon, item):
|
|
||||||
try:
|
|
||||||
program_location = get_program_files_directory()
|
|
||||||
self.open_location(program_location)
|
|
||||||
except Exception as e:
|
|
||||||
CustomDialog(
|
|
||||||
f"Failed to open the program location '{program_location}'.\nError: {e}",
|
|
||||||
msg_type="error",
|
|
||||||
).mainloop()
|
|
||||||
|
|
||||||
def enable_files_download(self, files):
|
|
||||||
self.is_file_download_enabled = True
|
|
||||||
self.icon.icon = self.create_clipboard_icon_with_dot()
|
|
||||||
self.file_download_items = (
|
|
||||||
"📥 Download File(s)",
|
|
||||||
0,
|
|
||||||
lambda icon, item: self._on_download(icon, item, files),
|
|
||||||
)
|
|
||||||
self.update_menu()
|
|
||||||
|
|
||||||
def disable_files_download(self):
|
|
||||||
self.is_file_download_enabled = False
|
|
||||||
self.file_download_items = None
|
|
||||||
self.icon.icon = self.create_clipboard_icon()
|
|
||||||
self.update_menu()
|
|
||||||
|
|
||||||
def _on_download(self, icon, item, files):
|
|
||||||
"""Download the files to the user's Downloads folder."""
|
|
||||||
try:
|
|
||||||
try:
|
|
||||||
if self.config.data["default_file_download_location"] != "":
|
|
||||||
target_directory = self.config.data[
|
|
||||||
"default_file_download_location"
|
|
||||||
]
|
|
||||||
else:
|
|
||||||
target_directory = filedialog.askdirectory(
|
|
||||||
title="Select location to Save File(s)",
|
|
||||||
initialdir=get_downloads_folder(),
|
|
||||||
)
|
|
||||||
if not target_directory:
|
|
||||||
logging.debug("No directory selected. Exiting.")
|
|
||||||
return
|
|
||||||
except RuntimeError as re:
|
|
||||||
target_directory = os.path.join(
|
|
||||||
get_program_files_directory(), "downloads"
|
|
||||||
)
|
|
||||||
if not os.path.exists(target_directory):
|
|
||||||
os.makedirs(target_directory)
|
|
||||||
logging.error(
|
|
||||||
f"A runtime error occurred while starting filedialog to select a directory. Error: {re}.\n"
|
|
||||||
+ f"Setting the default location to the program directory '{target_directory}'."
|
|
||||||
)
|
|
||||||
CustomDialog(
|
|
||||||
f"ClipCascade 📥: Saving files to the program directory '{target_directory}'.",
|
|
||||||
msg_type="info",
|
|
||||||
timeout=5000,
|
|
||||||
).mainloop()
|
|
||||||
|
|
||||||
# Save each file to the chosen directory
|
|
||||||
for filename, file_obj in files.items():
|
|
||||||
file_path = os.path.join(target_directory, filename)
|
|
||||||
with open(file_path, "wb") as f:
|
|
||||||
f.write(file_obj.getvalue())
|
|
||||||
logging.debug(f"Saved: {file_path}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
msg = f"An error occurred while downloading files. Error: {e}"
|
|
||||||
logging.error(msg)
|
|
||||||
CustomDialog(
|
|
||||||
msg,
|
|
||||||
msg_type="error",
|
|
||||||
).mainloop()
|
|
||||||
|
|
||||||
def _on_logoff(self, icon, item):
|
|
||||||
try:
|
|
||||||
if self.on_logoff_callback:
|
|
||||||
self.on_logoff_callback()
|
|
||||||
self.icon.stop()
|
|
||||||
self.root.quit()
|
|
||||||
except Exception as e:
|
|
||||||
CustomDialog(
|
|
||||||
f"An error occurred while logging off: {e}", msg_type="error"
|
|
||||||
).mainloop()
|
|
||||||
|
|
||||||
def _on_quit(self, icon, item):
|
|
||||||
self.icon.stop()
|
|
||||||
self.root.quit()
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
https://github.com/Sathvik-Rao/ClipCascade
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
from abc import ABC, abstractmethod
|
|
||||||
|
|
||||||
from cli.tray import TaskbarPanel
|
|
||||||
from core.config import Config
|
|
||||||
|
|
||||||
|
|
||||||
class WSInterface(ABC):
|
|
||||||
############################
|
|
||||||
# ABSTRACT VARIABLES #
|
|
||||||
############################
|
|
||||||
is_auto_reconnecting: bool = False
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def __init__(self, config: Config, is_login_phase=True):
|
|
||||||
"""This method must be implemented in subclasses."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_total_timeout(self):
|
|
||||||
"""This method must be implemented in subclasses."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_stats(self):
|
|
||||||
"""This method must be implemented in subclasses."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def connect(self):
|
|
||||||
"""This method must be implemented in subclasses."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def set_tray_ref(self, sys_tray: TaskbarPanel):
|
|
||||||
"""This method must be implemented in subclasses."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def manual_reconnect(self):
|
|
||||||
"""This method must be implemented in subclasses."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def disconnect(self):
|
|
||||||
"""This method must be implemented in subclasses."""
|
|
||||||
pass
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
# ClipCascade - A seamless clipboard syncing utility
|
|
||||||
# Repository: https://github.com/Sathvik-Rao/ClipCascade
|
|
||||||
#
|
|
||||||
# Author: Sathvik Rao Poladi
|
|
||||||
# License: GPL-3.0
|
|
||||||
#
|
|
||||||
# This script serves as the entry point for the ClipCascade application,
|
|
||||||
# initializing and running the core application logic.
|
|
||||||
|
|
||||||
from core.application import Application
|
|
||||||
|
|
||||||
|
|
||||||
class Main:
|
|
||||||
def __init__(self):
|
|
||||||
Application().run()
|
|
||||||
|
|
||||||
def main():
|
|
||||||
Main()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -1,84 +0,0 @@
|
|||||||
[build-system]
|
|
||||||
requires = ["setuptools>=65.0", "wheel"]
|
|
||||||
build-backend = "setuptools.build_meta"
|
|
||||||
|
|
||||||
[project]
|
|
||||||
name = "ClipCascade"
|
|
||||||
version = "3.2.0"
|
|
||||||
description = "ClipCascade is a lightweight utility that automatically syncs the clipboard across devices, no key press required."
|
|
||||||
authors = [{ name = "Sathvik Rao", email = "sathvik.poladi@gmail.com" }]
|
|
||||||
license = { file = "LICENSE" }
|
|
||||||
requires-python = ">=3.8"
|
|
||||||
keywords = [
|
|
||||||
"clipboard",
|
|
||||||
"sync",
|
|
||||||
"cross-platform",
|
|
||||||
"open-source",
|
|
||||||
"utility",
|
|
||||||
"encryption",
|
|
||||||
]
|
|
||||||
classifiers = [
|
|
||||||
"Development Status :: 3 - Alpha",
|
|
||||||
"Intended Audience :: End Users/Desktop",
|
|
||||||
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
|
||||||
"Operating System :: OS Independent",
|
|
||||||
"Programming Language :: Python :: 3",
|
|
||||||
"Programming Language :: Python :: 3.8",
|
|
||||||
"Programming Language :: Python :: 3.9",
|
|
||||||
"Programming Language :: Python :: 3.10",
|
|
||||||
"Programming Language :: Python :: 3.11",
|
|
||||||
"Programming Language :: Python :: 3.12",
|
|
||||||
"Topic :: System :: Monitoring",
|
|
||||||
]
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
"Pillow>=10.4.0",
|
|
||||||
"plyer==2.1.0",
|
|
||||||
"pycryptodome==3.20.0",
|
|
||||||
"pystray==0.19.5",
|
|
||||||
"Requests==2.32.3",
|
|
||||||
"websocket_client==1.8.0",
|
|
||||||
"xxhash==3.5.0",
|
|
||||||
"beautifulsoup4==4.12.3",
|
|
||||||
"aiortc==1.10.0",
|
|
||||||
# Platform-specific dependencies
|
|
||||||
"pyfiglet==1.0.2; sys_platform == 'linux'",
|
|
||||||
"pyperclip==1.8.2; sys_platform == 'win32' or sys_platform == 'darwin'",
|
|
||||||
"pywin32==306; sys_platform == 'win32'",
|
|
||||||
"certifi; sys_platform == 'darwin'",
|
|
||||||
"pyobjus==1.2.3; sys_platform == 'darwin'",
|
|
||||||
"pasteboard==0.4.0; sys_platform == 'darwin'",
|
|
||||||
]
|
|
||||||
|
|
||||||
[project.scripts]
|
|
||||||
clipcascade = "main:main"
|
|
||||||
|
|
||||||
[tool.setuptools]
|
|
||||||
py-modules = ["main"]
|
|
||||||
packages = [
|
|
||||||
"cli",
|
|
||||||
"clipboard",
|
|
||||||
"core",
|
|
||||||
"gui",
|
|
||||||
"interfaces",
|
|
||||||
"p2p",
|
|
||||||
"stomp_ws",
|
|
||||||
"utils",
|
|
||||||
]
|
|
||||||
|
|
||||||
[tool.black]
|
|
||||||
line-length = 100
|
|
||||||
target-version = ["py38", "py39", "py310", "py311", "py312"]
|
|
||||||
include = "\\.pyi?$"
|
|
||||||
|
|
||||||
[tool.isort]
|
|
||||||
profile = "black"
|
|
||||||
line_length = 100
|
|
||||||
multi_line_mode = 3
|
|
||||||
|
|
||||||
[project.urls]
|
|
||||||
Homepage = "https://clipcascade.sathvik.dev/"
|
|
||||||
Repository = "https://github.com/Sathvik-Rao/ClipCascade"
|
|
||||||
Documentation = "https://github.com/Sathvik-Rao/ClipCascade#readme"
|
|
||||||
Issues = "https://github.com/Sathvik-Rao/ClipCascade/issues"
|
|
||||||
Discussions = "https://github.com/Sathvik-Rao/ClipCascade/discussions"
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
Pillow==10.4.0
|
|
||||||
plyer==2.1.0
|
|
||||||
pycryptodome==3.20.0
|
|
||||||
pystray==0.19.5
|
|
||||||
Requests==2.32.3
|
|
||||||
websocket_client==1.8.0
|
|
||||||
xxhash==3.5.0
|
|
||||||
pyfiglet==1.0.2
|
|
||||||
beautifulsoup4==4.12.3
|
|
||||||
aiortc==1.10.0
|
|
||||||
@@ -1,206 +0,0 @@
|
|||||||
import time
|
|
||||||
from threading import Thread
|
|
||||||
|
|
||||||
from .frame import Frame
|
|
||||||
import websocket
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from core.constants import *
|
|
||||||
|
|
||||||
VERSIONS = "1.0,1.1"
|
|
||||||
|
|
||||||
|
|
||||||
class Client:
|
|
||||||
|
|
||||||
def __init__(self, url, headers={}, on_close_callback=None, sslopt=None):
|
|
||||||
|
|
||||||
self.url = url
|
|
||||||
self._ws_sslopt = sslopt
|
|
||||||
self.ws = websocket.WebSocketApp(self.url, headers)
|
|
||||||
self.ws.on_open = self._on_open
|
|
||||||
self.ws.on_message = self._on_message
|
|
||||||
self.ws.on_error = self._on_error
|
|
||||||
self.ws.on_close = self._on_close
|
|
||||||
self.on_close_callback = on_close_callback
|
|
||||||
|
|
||||||
self.opened = False
|
|
||||||
|
|
||||||
self.connected = False
|
|
||||||
|
|
||||||
self.counter = 0
|
|
||||||
self.subscriptions = {}
|
|
||||||
|
|
||||||
self._connectCallback = None
|
|
||||||
self.errorCallback = None
|
|
||||||
|
|
||||||
def _connect(self, timeout=0):
|
|
||||||
if self._ws_sslopt:
|
|
||||||
thread = Thread(
|
|
||||||
target=lambda: self.ws.run_forever(sslopt=self._ws_sslopt)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
thread = Thread(target=self.ws.run_forever)
|
|
||||||
thread.daemon = True
|
|
||||||
thread.start()
|
|
||||||
|
|
||||||
total_ms = 0
|
|
||||||
while self.opened is False:
|
|
||||||
time.sleep(0.25)
|
|
||||||
total_ms += 250
|
|
||||||
if 0 < timeout < total_ms:
|
|
||||||
raise TimeoutError(f"Connection to {self.url} timed out")
|
|
||||||
|
|
||||||
def _on_open(self, ws_app, *args):
|
|
||||||
self.opened = True
|
|
||||||
|
|
||||||
def _on_close(self, ws_app, *args):
|
|
||||||
self.connected = False
|
|
||||||
logging.debug("Whoops! Lost connection to " + self.ws.url)
|
|
||||||
if self.on_close_callback is not None:
|
|
||||||
self.on_close_callback()
|
|
||||||
self._clean_up()
|
|
||||||
|
|
||||||
def _on_error(self, ws_app, error, *args):
|
|
||||||
logging.debug(error)
|
|
||||||
|
|
||||||
def _on_message(self, ws_app, message, *args):
|
|
||||||
if message == "\n": # If message is a newline, it's a heartbeat frame
|
|
||||||
logging.debug("Received heartbeat frame")
|
|
||||||
self.ws.send("\n") # Send a heartbeat back to the server
|
|
||||||
logging.debug("Sent heartbeat frame")
|
|
||||||
return
|
|
||||||
|
|
||||||
logging.debug("\n<<< " + str(message))
|
|
||||||
frame = Frame.unmarshall_single(message)
|
|
||||||
_results = []
|
|
||||||
if frame.command == "CONNECTED":
|
|
||||||
self.connected = True
|
|
||||||
logging.debug("connected to server " + self.url)
|
|
||||||
if self._connectCallback is not None:
|
|
||||||
_results.append(self._connectCallback(frame))
|
|
||||||
elif frame.command == "MESSAGE":
|
|
||||||
|
|
||||||
subscription = frame.headers["subscription"]
|
|
||||||
|
|
||||||
if subscription in self.subscriptions:
|
|
||||||
onreceive = self.subscriptions[subscription]
|
|
||||||
messageID = frame.headers["message-id"]
|
|
||||||
|
|
||||||
def ack(headers):
|
|
||||||
if headers is None:
|
|
||||||
headers = {}
|
|
||||||
return self.ack(messageID, subscription, headers)
|
|
||||||
|
|
||||||
def nack(headers):
|
|
||||||
if headers is None:
|
|
||||||
headers = {}
|
|
||||||
return self.nack(messageID, subscription, headers)
|
|
||||||
|
|
||||||
frame.ack = ack
|
|
||||||
frame.nack = nack
|
|
||||||
|
|
||||||
_results.append(onreceive(frame))
|
|
||||||
else:
|
|
||||||
info = "Unhandled received MESSAGE: " + str(frame)
|
|
||||||
logging.debug(info)
|
|
||||||
_results.append(info)
|
|
||||||
elif frame.command == "RECEIPT":
|
|
||||||
pass
|
|
||||||
elif frame.command == "ERROR":
|
|
||||||
if self.errorCallback is not None:
|
|
||||||
_results.append(self.errorCallback(frame))
|
|
||||||
else:
|
|
||||||
info = "Unhandled received MESSAGE: " + frame.command
|
|
||||||
logging.debug(info)
|
|
||||||
_results.append(info)
|
|
||||||
|
|
||||||
return _results
|
|
||||||
|
|
||||||
def _transmit(self, command, headers, body=None):
|
|
||||||
out = Frame.marshall(command, headers, body)
|
|
||||||
logging.debug("\n>>> " + out)
|
|
||||||
self.ws.send(out)
|
|
||||||
|
|
||||||
def connect(
|
|
||||||
self,
|
|
||||||
login=None,
|
|
||||||
passcode=None,
|
|
||||||
headers=None,
|
|
||||||
connectCallback=None,
|
|
||||||
errorCallback=None,
|
|
||||||
timeout=0,
|
|
||||||
):
|
|
||||||
|
|
||||||
logging.debug("Opening web socket...")
|
|
||||||
self._connect(timeout)
|
|
||||||
|
|
||||||
headers = headers if headers is not None else {}
|
|
||||||
headers["host"] = self.url
|
|
||||||
headers["accept-version"] = VERSIONS
|
|
||||||
headers["heart-beat"] = "0,20000"
|
|
||||||
|
|
||||||
if login is not None:
|
|
||||||
headers["login"] = login
|
|
||||||
if passcode is not None:
|
|
||||||
headers["passcode"] = passcode
|
|
||||||
|
|
||||||
self._connectCallback = connectCallback
|
|
||||||
self.errorCallback = errorCallback
|
|
||||||
|
|
||||||
self._transmit("CONNECT", headers)
|
|
||||||
|
|
||||||
def disconnect(self, disconnectCallback=None, headers=None):
|
|
||||||
if headers is None:
|
|
||||||
headers = {}
|
|
||||||
|
|
||||||
# self._transmit("DISCONNECT", headers) # comment this
|
|
||||||
self.ws.on_close = None
|
|
||||||
self.ws.close()
|
|
||||||
self._clean_up()
|
|
||||||
|
|
||||||
if disconnectCallback is not None:
|
|
||||||
disconnectCallback()
|
|
||||||
|
|
||||||
def _clean_up(self):
|
|
||||||
self.connected = False
|
|
||||||
|
|
||||||
def send(self, destination, headers=None, body=None):
|
|
||||||
if headers is None:
|
|
||||||
headers = {}
|
|
||||||
if body is None:
|
|
||||||
body = ""
|
|
||||||
headers["destination"] = destination
|
|
||||||
return self._transmit("SEND", headers, body)
|
|
||||||
|
|
||||||
def subscribe(self, destination, callback=None, headers=None):
|
|
||||||
if headers is None:
|
|
||||||
headers = {}
|
|
||||||
if "id" not in headers:
|
|
||||||
headers["id"] = "sub-" + str(self.counter)
|
|
||||||
self.counter += 1
|
|
||||||
headers["destination"] = destination
|
|
||||||
self.subscriptions[headers["id"]] = callback
|
|
||||||
self._transmit("SUBSCRIBE", headers)
|
|
||||||
|
|
||||||
def unsubscribe():
|
|
||||||
self.unsubscribe(headers["id"])
|
|
||||||
|
|
||||||
return headers["id"], unsubscribe
|
|
||||||
|
|
||||||
def unsubscribe(self, id):
|
|
||||||
del self.subscriptions[id]
|
|
||||||
return self._transmit("UNSUBSCRIBE", {"id": id})
|
|
||||||
|
|
||||||
def ack(self, message_id, subscription, headers):
|
|
||||||
if headers is None:
|
|
||||||
headers = {}
|
|
||||||
headers["message-id"] = message_id
|
|
||||||
headers["subscription"] = subscription
|
|
||||||
return self._transmit("ACK", headers)
|
|
||||||
|
|
||||||
def nack(self, message_id, subscription, headers):
|
|
||||||
if headers is None:
|
|
||||||
headers = {}
|
|
||||||
headers["message-id"] = message_id
|
|
||||||
headers["subscription"] = subscription
|
|
||||||
return self._transmit("NACK", headers)
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
from urllib.parse import urlparse
|
|
||||||
|
|
||||||
Byte = {"LF": "\x0A", "NULL": "\x00"}
|
|
||||||
|
|
||||||
|
|
||||||
class Frame:
|
|
||||||
|
|
||||||
def __init__(self, command, headers, body):
|
|
||||||
self.command = command
|
|
||||||
self.headers = headers
|
|
||||||
self.body = "" if body is None else body
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
lines = [self.command]
|
|
||||||
skipContentLength = "content-length" in self.headers
|
|
||||||
if skipContentLength:
|
|
||||||
del self.headers["content-length"]
|
|
||||||
|
|
||||||
for name in self.headers:
|
|
||||||
value = self.headers[name]
|
|
||||||
lines.append("" + name + ":" + value)
|
|
||||||
|
|
||||||
if self.body is not None and not skipContentLength:
|
|
||||||
lines.append("content-length:" + str(len(self.body)))
|
|
||||||
|
|
||||||
lines.append(Byte["LF"] + self.body)
|
|
||||||
return Byte["LF"].join(lines)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def unmarshall_single(data):
|
|
||||||
lines = data.split(Byte["LF"])
|
|
||||||
|
|
||||||
command = lines[0].strip()
|
|
||||||
headers = {}
|
|
||||||
|
|
||||||
# get all headers
|
|
||||||
i = 1
|
|
||||||
while lines[i] != "":
|
|
||||||
# get key, value from raw header
|
|
||||||
(key, value) = lines[i].split(":", 1)
|
|
||||||
headers[key] = value
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
# set body to None if there is no body
|
|
||||||
body = None if lines[i + 1] == Byte["NULL"] else lines[i + 1][:-1]
|
|
||||||
|
|
||||||
return Frame(command, headers, body)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def marshall(command, headers, body):
|
|
||||||
return str(Frame(command, headers, body)) + Byte["NULL"]
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
import json
|
|
||||||
import logging
|
|
||||||
import time
|
|
||||||
|
|
||||||
|
|
||||||
from interfaces.ws_interface import WSInterface
|
|
||||||
from stomp_ws.client import Client
|
|
||||||
from core.config import Config
|
|
||||||
from utils.cipher_manager import CipherManager
|
|
||||||
from clipboard.clipboard_manager import ClipboardManager
|
|
||||||
from utils.notification_manager import NotificationManager
|
|
||||||
from utils.request_manager import RequestManager
|
|
||||||
from utils.ssl_helper import websocket_sslopt_for_config
|
|
||||||
from core.constants import *
|
|
||||||
|
|
||||||
if PLATFORM.startswith(LINUX) and LINUX_USE_CLI_UI:
|
|
||||||
from cli.tray import TaskbarPanel
|
|
||||||
else:
|
|
||||||
from gui.tray import TaskbarPanel
|
|
||||||
|
|
||||||
|
|
||||||
class STOMPManager(WSInterface):
|
|
||||||
def __init__(self, config: Config, is_login_phase=True):
|
|
||||||
self.config = config
|
|
||||||
self.clipboard_manager = ClipboardManager(self.config)
|
|
||||||
self.cipher_manager = CipherManager(self.config)
|
|
||||||
self.notification_manager = NotificationManager(self.config)
|
|
||||||
self.sys_tray: TaskbarPanel = None
|
|
||||||
self.first_conn_lost = True
|
|
||||||
self.is_login_phase = is_login_phase
|
|
||||||
self.client = None
|
|
||||||
self.is_connected = False
|
|
||||||
self.disconnected = False
|
|
||||||
self.is_auto_reconnecting = False
|
|
||||||
|
|
||||||
def set_tray_ref(self, sys_tray: TaskbarPanel):
|
|
||||||
"""
|
|
||||||
Sets the system tray reference.
|
|
||||||
"""
|
|
||||||
self.sys_tray = sys_tray
|
|
||||||
self.clipboard_manager.set_tray_ref(sys_tray)
|
|
||||||
|
|
||||||
def get_total_timeout(self):
|
|
||||||
"""
|
|
||||||
Returns the total timeout value in milliseconds."""
|
|
||||||
return (RECONNECT_WS_TIMER * 1000) + WEBSOCKET_TIMEOUT
|
|
||||||
|
|
||||||
def get_stats(self):
|
|
||||||
return None
|
|
||||||
|
|
||||||
def connect(self) -> tuple[bool, str]:
|
|
||||||
try:
|
|
||||||
if self.is_connected:
|
|
||||||
return True, ""
|
|
||||||
self.client = Client(
|
|
||||||
self.config.data["websocket_url"],
|
|
||||||
headers={
|
|
||||||
"Cookie": RequestManager.format_cookie(
|
|
||||||
self.config.data["cookie"]
|
|
||||||
)
|
|
||||||
},
|
|
||||||
on_close_callback=self._on_close,
|
|
||||||
sslopt=websocket_sslopt_for_config(self.config),
|
|
||||||
)
|
|
||||||
self.client.connect(
|
|
||||||
timeout=WEBSOCKET_TIMEOUT,
|
|
||||||
connectCallback=lambda _: self.client.subscribe( # receive event
|
|
||||||
destination=SUBSCRIPTION_DESTINATION,
|
|
||||||
callback=self._receive,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
if self.disconnected:
|
|
||||||
self.disconnect()
|
|
||||||
return False, "Websocket disconnected"
|
|
||||||
|
|
||||||
# logging.info("Websocket connected")
|
|
||||||
self.is_connected = True
|
|
||||||
self.is_auto_reconnecting = False
|
|
||||||
if not self.first_conn_lost:
|
|
||||||
self.first_conn_lost = True
|
|
||||||
self.notification_manager.notify(
|
|
||||||
title=f"{APP_NAME}: WebSocket Connection Restored 🔗",
|
|
||||||
message="Connection re-established",
|
|
||||||
)
|
|
||||||
|
|
||||||
# send event
|
|
||||||
self.clipboard_manager.on_copy(self.send)
|
|
||||||
return True, "Websocket connected"
|
|
||||||
except Exception as e:
|
|
||||||
msg = f"Failed to connect websocket: {e}"
|
|
||||||
logging.error(msg)
|
|
||||||
return False, msg
|
|
||||||
|
|
||||||
def _on_close(self):
|
|
||||||
self.is_connected = False
|
|
||||||
# Auto Reconnect
|
|
||||||
if not self.is_login_phase and not self.disconnected:
|
|
||||||
self.is_auto_reconnecting = True
|
|
||||||
if self.first_conn_lost:
|
|
||||||
self.notification_manager.notify(
|
|
||||||
title=f"{APP_NAME}: WebSocket Connection Lost ⛓️💥",
|
|
||||||
message="Check your internet connection. Retrying...",
|
|
||||||
)
|
|
||||||
self.first_conn_lost = False
|
|
||||||
time.sleep(RECONNECT_WS_TIMER) # seconds
|
|
||||||
self.connect()
|
|
||||||
|
|
||||||
def send(self, payload: str, payload_type: str = "text"):
|
|
||||||
try:
|
|
||||||
if self.is_connected:
|
|
||||||
if self.clipboard_manager.has_clipboard_changed(payload):
|
|
||||||
if self.config.data["cipher_enabled"]:
|
|
||||||
payload = CipherManager.encode_to_json_string(
|
|
||||||
**self.cipher_manager.encrypt(payload)
|
|
||||||
)
|
|
||||||
body = json.dumps({"payload": payload, "type": payload_type})
|
|
||||||
self.client.send(destination=SEND_DESTINATION, body=body)
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Failed to send data: {e}")
|
|
||||||
|
|
||||||
def _receive(self, frame: any) -> str:
|
|
||||||
try:
|
|
||||||
if self.is_connected:
|
|
||||||
body = json.loads(frame.body)
|
|
||||||
payload = body["payload"]
|
|
||||||
payload_type = body.get("type", "text")
|
|
||||||
if self.config.data["cipher_enabled"]:
|
|
||||||
payload = self.cipher_manager.decrypt(
|
|
||||||
**CipherManager.decode_from_json_string(payload)
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.clipboard_manager.has_clipboard_changed(payload):
|
|
||||||
self.clipboard_manager.base64_to_clipboard(
|
|
||||||
base64_string=payload, type_=payload_type
|
|
||||||
)
|
|
||||||
except json.decoder.JSONDecodeError:
|
|
||||||
logging.error(
|
|
||||||
"If cipher is enabled, please make sure it is enabled on all devices"
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Failed to receive data: {e}")
|
|
||||||
|
|
||||||
def manual_reconnect(self):
|
|
||||||
if not self.is_auto_reconnecting:
|
|
||||||
self.disconnected = False
|
|
||||||
self.connect()
|
|
||||||
|
|
||||||
def disconnect(self):
|
|
||||||
try:
|
|
||||||
self.clipboard_manager.previous_clipboard_hash = 0
|
|
||||||
self.disconnected = True
|
|
||||||
self.first_conn_lost = True
|
|
||||||
try:
|
|
||||||
self.client.disconnect()
|
|
||||||
self.is_connected = False
|
|
||||||
logging.info("Websocket disconnected")
|
|
||||||
except Exception as e:
|
|
||||||
pass # silent catch
|
|
||||||
self.clipboard_manager.stop()
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Failed to disconnect websocket: {e}")
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
import base64
|
|
||||||
import json
|
|
||||||
import hashlib
|
|
||||||
|
|
||||||
from Crypto.Cipher import AES
|
|
||||||
from core.constants import *
|
|
||||||
from core.config import Config
|
|
||||||
|
|
||||||
|
|
||||||
class CipherManager:
|
|
||||||
def __init__(self, config: Config):
|
|
||||||
self.config = config
|
|
||||||
|
|
||||||
# hash
|
|
||||||
self.hash_name = "sha256"
|
|
||||||
self.dklen = 32 # 256 bits for AES-256
|
|
||||||
|
|
||||||
# encryption
|
|
||||||
self.mode = AES.MODE_GCM
|
|
||||||
|
|
||||||
def hash_password(self, password: str) -> bytes:
|
|
||||||
return hashlib.pbkdf2_hmac(
|
|
||||||
hash_name=self.hash_name,
|
|
||||||
password=password.encode(),
|
|
||||||
salt=(
|
|
||||||
self.config.data["username"] + password + self.config.data["salt"]
|
|
||||||
).encode("utf-8"),
|
|
||||||
iterations=self.config.data["hash_rounds"],
|
|
||||||
dklen=self.dklen,
|
|
||||||
)
|
|
||||||
|
|
||||||
def encrypt(self, plaintext: str) -> dict:
|
|
||||||
key = self.config.data["hashed_password"]
|
|
||||||
plaintext_bytes = plaintext.encode("utf-8")
|
|
||||||
cipher = AES.new(key, self.mode)
|
|
||||||
ciphertext, tag = cipher.encrypt_and_digest(plaintext_bytes)
|
|
||||||
return {"nonce": cipher.nonce, "ciphertext": ciphertext, "tag": tag}
|
|
||||||
|
|
||||||
def decrypt(self, nonce: bytes, ciphertext: bytes, tag: bytes) -> str:
|
|
||||||
key = self.config.data["hashed_password"]
|
|
||||||
cipher = AES.new(key, self.mode, nonce=nonce)
|
|
||||||
return cipher.decrypt_and_verify(ciphertext, tag).decode()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def encode_to_json_string(**kwargs: bytes) -> str:
|
|
||||||
"""
|
|
||||||
Convert bytes values to Base64 and create a JSON string.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
**kwargs: Key-value pairs where values must be of type `bytes`.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: A JSON string with all `bytes` values Base64-encoded.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If a value is not of type `bytes`.
|
|
||||||
"""
|
|
||||||
json_data = {}
|
|
||||||
for key, value in kwargs.items():
|
|
||||||
if isinstance(value, bytes):
|
|
||||||
json_data[key] = base64.b64encode(value).decode("utf-8")
|
|
||||||
else:
|
|
||||||
raise ValueError(
|
|
||||||
f"Unsupported value type for key '{key}': {type(value)}. "
|
|
||||||
f"This method only supports 'bytes'."
|
|
||||||
)
|
|
||||||
return json.dumps(json_data)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def decode_from_json_string(json_string: str) -> dict:
|
|
||||||
"""
|
|
||||||
Decode a JSON string where all values are Base64-encoded back to their original bytes.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
json_string (str): A JSON string with Base64-encoded values.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: A dictionary with the original keys and `bytes` values decoded from Base64.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If the JSON string is not valid or if decoding fails.
|
|
||||||
"""
|
|
||||||
# Parse the JSON string into a dictionary
|
|
||||||
json_data = json.loads(json_string)
|
|
||||||
decoded_data = {}
|
|
||||||
|
|
||||||
# Decode each Base64-encoded value back to bytes
|
|
||||||
for key, value in json_data.items():
|
|
||||||
if isinstance(value, str):
|
|
||||||
decoded_data[key] = base64.b64decode(value)
|
|
||||||
else:
|
|
||||||
raise ValueError(
|
|
||||||
f"Unsupported value type for key '{key}': {type(value)}. "
|
|
||||||
+ f"Expected 'str' for Base64 decoding."
|
|
||||||
)
|
|
||||||
return decoded_data
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def string_to_sha3_512_lowercase_hex(input_string: str) -> str:
|
|
||||||
"""
|
|
||||||
Convert a string to its lowercase hexadecimal SHA3-512 hash.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
input_string (str): The input string to hash.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: The lowercase hexadecimal representation of the SHA3-512 hash.
|
|
||||||
"""
|
|
||||||
return hashlib.sha3_512(input_string.encode("utf-8")).hexdigest()
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
from core.constants import *
|
|
||||||
from core.config import Config
|
|
||||||
|
|
||||||
if PLATFORM == WINDOWS or (
|
|
||||||
PLATFORM.startswith(LINUX) and not LINUX_USE_CLI_UI
|
|
||||||
):
|
|
||||||
# WINDOWS: When creating the executable with pyinstaller, add --hidden-import plyer.platforms.win.notification
|
|
||||||
from plyer import notification
|
|
||||||
elif PLATFORM.startswith(LINUX) and LINUX_USE_CLI_UI:
|
|
||||||
from cli.info import CustomDialog
|
|
||||||
elif PLATFORM == MACOS:
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationManager:
|
|
||||||
def __init__(self, config: Config):
|
|
||||||
self.config = config
|
|
||||||
|
|
||||||
def notify(self, title: str, message: str, app_name: str = APP_NAME, timeout=10):
|
|
||||||
if self.config.data["notification"]: # Check if notifications are enabled
|
|
||||||
try:
|
|
||||||
if PLATFORM == WINDOWS or (
|
|
||||||
PLATFORM.startswith(LINUX) and not LINUX_USE_CLI_UI
|
|
||||||
):
|
|
||||||
notification.notify(
|
|
||||||
title=title,
|
|
||||||
message=message,
|
|
||||||
app_name=app_name,
|
|
||||||
timeout=timeout, # seconds
|
|
||||||
)
|
|
||||||
elif PLATFORM.startswith(LINUX) and LINUX_USE_CLI_UI:
|
|
||||||
CustomDialog(f"{title} : {message}").mainloop()
|
|
||||||
elif PLATFORM == MACOS:
|
|
||||||
subprocess.run(
|
|
||||||
[
|
|
||||||
"osascript",
|
|
||||||
"-e",
|
|
||||||
f'display notification "{message}" with title "{title}"',
|
|
||||||
]
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Failed to handle notification: {e}")
|
|
||||||
@@ -1,201 +0,0 @@
|
|||||||
import json
|
|
||||||
import logging
|
|
||||||
import requests
|
|
||||||
from core.constants import *
|
|
||||||
from core.config import Config
|
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
from utils.ssl_helper import requests_verify_arg
|
|
||||||
|
|
||||||
|
|
||||||
class RequestManager:
|
|
||||||
def __init__(self, config: Config):
|
|
||||||
self.config = config
|
|
||||||
|
|
||||||
def _verify(self):
|
|
||||||
return requests_verify_arg(self.config)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def format_cookie(cookie: dict) -> str:
|
|
||||||
"""
|
|
||||||
Format the cookie string for headers.
|
|
||||||
"""
|
|
||||||
return f"JSESSIONID={cookie.get('JSESSIONID', '')};"
|
|
||||||
|
|
||||||
def login(self) -> tuple[bool, str, dict]:
|
|
||||||
try:
|
|
||||||
session = requests.Session()
|
|
||||||
|
|
||||||
# Fetch the login page to get the CSRF token
|
|
||||||
response = session.get(
|
|
||||||
self.config.data["server_url"] + LOGIN_URL,
|
|
||||||
verify=self._verify(),
|
|
||||||
)
|
|
||||||
|
|
||||||
if response.status_code != 200:
|
|
||||||
msg = f"Failed to fetch login page: {response.status_code}"
|
|
||||||
logging.error(msg)
|
|
||||||
return False, msg, None
|
|
||||||
|
|
||||||
soup = BeautifulSoup(response.text, "html.parser")
|
|
||||||
csrf_token = soup.find("input", {"name": "_csrf"})["value"]
|
|
||||||
|
|
||||||
# Login with the credentials
|
|
||||||
form_data = {
|
|
||||||
"username": self.config.data["username"],
|
|
||||||
"password": self.config.data["password"],
|
|
||||||
"_csrf": csrf_token,
|
|
||||||
}
|
|
||||||
response = session.post(
|
|
||||||
self.config.data["server_url"] + LOGIN_URL,
|
|
||||||
data=form_data,
|
|
||||||
verify=self._verify(),
|
|
||||||
)
|
|
||||||
if (
|
|
||||||
response.status_code == 200
|
|
||||||
and "bad credentials" not in response.text.lower()
|
|
||||||
):
|
|
||||||
# login successful
|
|
||||||
cookie = session.cookies.get_dict()
|
|
||||||
logging.info(f"Login successful: {response.status_code}")
|
|
||||||
return True, "Login successful", cookie
|
|
||||||
else:
|
|
||||||
# login failed
|
|
||||||
msg = f"Login failed: {response.status_code}"
|
|
||||||
logging.error(msg)
|
|
||||||
return False, msg, None
|
|
||||||
except Exception as e:
|
|
||||||
msg = f"An error occurred during login: {e}"
|
|
||||||
logging.error(msg)
|
|
||||||
return False, msg, None
|
|
||||||
|
|
||||||
def maxsize(self) -> int:
|
|
||||||
try:
|
|
||||||
response = RequestManager.get(
|
|
||||||
url=self.config.data["server_url"] + MAXSIZE_URL,
|
|
||||||
headers={
|
|
||||||
"Cookie": RequestManager.format_cookie(self.config.data["cookie"])
|
|
||||||
},
|
|
||||||
verify=self._verify(),
|
|
||||||
)
|
|
||||||
if response.status_code == 200:
|
|
||||||
# maxsize request successful
|
|
||||||
maxsize = response.json().get("maxsize", MAX_SIZE)
|
|
||||||
logging.info(f"Max size: {maxsize}")
|
|
||||||
return maxsize
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(
|
|
||||||
f"Error fetching max size: {e}, defaulting to {MAX_SIZE} Bytes"
|
|
||||||
)
|
|
||||||
return MAX_SIZE
|
|
||||||
|
|
||||||
def get_server_mode(self) -> str:
|
|
||||||
try:
|
|
||||||
response = RequestManager.get(
|
|
||||||
url=self.config.data["server_url"] + SERVER_MODE_URL,
|
|
||||||
headers={
|
|
||||||
"Cookie": RequestManager.format_cookie(self.config.data["cookie"])
|
|
||||||
},
|
|
||||||
verify=self._verify(),
|
|
||||||
)
|
|
||||||
if response.status_code == 200:
|
|
||||||
# server mode request successful
|
|
||||||
server_mode = response.json().get("mode")
|
|
||||||
logging.info(f"Server mode: {server_mode}")
|
|
||||||
return server_mode
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Error fetching server mode: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
def get_stun_url(self) -> str:
|
|
||||||
try:
|
|
||||||
response = RequestManager.get(
|
|
||||||
url=self.config.data["server_url"] + STUN_URL,
|
|
||||||
headers={
|
|
||||||
"Cookie": RequestManager.format_cookie(self.config.data["cookie"])
|
|
||||||
},
|
|
||||||
verify=self._verify(),
|
|
||||||
)
|
|
||||||
if response.status_code == 200:
|
|
||||||
# stun url request successful
|
|
||||||
stun_url = response.json().get("url")
|
|
||||||
logging.info(f"STUN URL: {stun_url}")
|
|
||||||
return stun_url
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Error fetching STUN URL: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
def get_metadata(self) -> dict:
|
|
||||||
try:
|
|
||||||
response = RequestManager.get(
|
|
||||||
url=METADATA_URL,
|
|
||||||
headers={
|
|
||||||
"Cookie": RequestManager.format_cookie(self.config.data["cookie"])
|
|
||||||
},
|
|
||||||
verify=True,
|
|
||||||
)
|
|
||||||
if response.status_code == 200:
|
|
||||||
# metadata request successful
|
|
||||||
return response.json()
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Error fetching metadata: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
def logout(self):
|
|
||||||
try:
|
|
||||||
response = RequestManager.post(
|
|
||||||
url=self.config.data["server_url"] + LOGOUT_URL,
|
|
||||||
data={"_csrf": self.config.data["csrf_token"]},
|
|
||||||
headers={
|
|
||||||
"Cookie": RequestManager.format_cookie(self.config.data["cookie"])
|
|
||||||
},
|
|
||||||
verify=self._verify(),
|
|
||||||
)
|
|
||||||
if response.status_code == 204:
|
|
||||||
logging.info(f"Logout successful: {response.status_code}")
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Error during logout: {e}")
|
|
||||||
|
|
||||||
def get_csrf_token(self) -> str:
|
|
||||||
try:
|
|
||||||
response = RequestManager.get(
|
|
||||||
url=self.config.data["server_url"] + CSRF_URL,
|
|
||||||
headers={
|
|
||||||
"Cookie": RequestManager.format_cookie(self.config.data["cookie"])
|
|
||||||
},
|
|
||||||
verify=self._verify(),
|
|
||||||
)
|
|
||||||
|
|
||||||
if response.status_code == 200:
|
|
||||||
# CSRF token request successful
|
|
||||||
return json.loads(response.text).get("token", "")
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Error fetching CSRF token: {e}")
|
|
||||||
return ""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get(url: str, headers: dict = None, verify=True) -> requests.Response:
|
|
||||||
"""
|
|
||||||
A generic GET mapper for handling GET requests.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
response = requests.get(url, headers=headers, verify=verify)
|
|
||||||
response.raise_for_status() # Will raise an HTTPError if the HTTP request returned an unsuccessful status code
|
|
||||||
return response
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Error during GET request to {url}: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def post(
|
|
||||||
url: str, data: dict, headers: dict = None, verify=True
|
|
||||||
) -> requests.Response:
|
|
||||||
"""
|
|
||||||
A generic POST mapper for handling POST requests.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
response = requests.post(url, data=data, headers=headers, verify=verify)
|
|
||||||
response.raise_for_status() # Will raise an HTTPError if the HTTP request returned an unsuccessful status code
|
|
||||||
return response
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Error during POST request to {url}: {e}")
|
|
||||||
raise
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import os
|
|
||||||
import ssl
|
|
||||||
from typing import Any, Dict, Optional, Union
|
|
||||||
|
|
||||||
from core.constants import MACOS, PLATFORM
|
|
||||||
|
|
||||||
if PLATFORM == MACOS:
|
|
||||||
import certifi
|
|
||||||
|
|
||||||
|
|
||||||
def requests_verify_arg(config) -> Union[bool, str]:
|
|
||||||
"""Argument for requests' verify= when calling the user's server."""
|
|
||||||
path = (config.data.get("ssl_ca_bundle") or "").strip()
|
|
||||||
if not path:
|
|
||||||
return True
|
|
||||||
if not os.path.isfile(path):
|
|
||||||
raise FileNotFoundError(f"SSL CA bundle file not found: {path}")
|
|
||||||
return path
|
|
||||||
|
|
||||||
|
|
||||||
def websocket_sslopt_for_config(config) -> Optional[Dict[str, Any]]:
|
|
||||||
"""sslopt for websocket_client run_forever; None uses the library/OS default."""
|
|
||||||
path = (config.data.get("ssl_ca_bundle") or "").strip()
|
|
||||||
if path:
|
|
||||||
if not os.path.isfile(path):
|
|
||||||
raise FileNotFoundError(f"SSL CA bundle file not found: {path}")
|
|
||||||
ctx = ssl.create_default_context(cafile=path)
|
|
||||||
return {"context": ctx}
|
|
||||||
if PLATFORM == MACOS:
|
|
||||||
ctx = ssl.create_default_context(cafile=certifi.where())
|
|
||||||
return {"context": ctx}
|
|
||||||
return None
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import logging
|
|
||||||
import re
|
|
||||||
from core.constants import *
|
|
||||||
|
|
||||||
|
|
||||||
def center_window(win, max_height=None):
|
|
||||||
"""Center a Tk/Toplevel window on the primary monitor."""
|
|
||||||
logging.debug(f"Centering window with max_height={max_height}")
|
|
||||||
win.update_idletasks()
|
|
||||||
window_width = win.winfo_width()
|
|
||||||
window_height = win.winfo_height()
|
|
||||||
if max_height:
|
|
||||||
window_height = min(window_height, max_height)
|
|
||||||
logging.debug(f"Window dimensions: {window_width}x{window_height}")
|
|
||||||
primary_x, primary_y = 0, 0
|
|
||||||
primary_w, primary_h = win.winfo_screenwidth(), win.winfo_screenheight()
|
|
||||||
try:
|
|
||||||
if PLATFORM.startswith(LINUX):
|
|
||||||
logging.debug("Detecting monitors on Linux platform")
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
result = subprocess.run(
|
|
||||||
["xrandr", "--query"], capture_output=True, text=True, timeout=2
|
|
||||||
)
|
|
||||||
for line in result.stdout.splitlines():
|
|
||||||
if " connected primary" in line:
|
|
||||||
m = re.search(r"(\d+)x(\d+)\+(\d+)\+(\d+)", line)
|
|
||||||
if m:
|
|
||||||
primary_w = int(m.group(1))
|
|
||||||
primary_h = int(m.group(2))
|
|
||||||
primary_x = int(m.group(3))
|
|
||||||
primary_y = int(m.group(4))
|
|
||||||
logging.debug(
|
|
||||||
f"Primary monitor: position=({primary_x}, {primary_y}), size={primary_w}x{primary_h}"
|
|
||||||
)
|
|
||||||
break
|
|
||||||
except Exception as e:
|
|
||||||
logging.warning(f"Failed to detect primary monitor: {e}. Using fallback dimensions.")
|
|
||||||
x = primary_x + (primary_w - window_width) // 2
|
|
||||||
y = primary_y + (primary_h - window_height) // 2
|
|
||||||
x = max(primary_x, x)
|
|
||||||
y = max(primary_y, y)
|
|
||||||
logging.debug(f"Calculated window position: ({x}, {y})")
|
|
||||||
win.geometry(f"{window_width}x{window_height}+{x}+{y}")
|
|
||||||
logging.debug(f"Applied geometry: {window_width}x{window_height}+{x}+{y}")
|
|
||||||
+30
-15
@@ -1,25 +1,28 @@
|
|||||||
# Agent Guidelines: Aerc Configuration Repository
|
# Agent Guidelines: Aerc Configuration Repository
|
||||||
|
|
||||||
This repository contains the configuration and customization for `aerc`, a terminal-based email client. It is optimized for a workflow involving `notmuch` for indexing, `mbsync` for synchronization, `abook` for address management, `khal` for calendar, and `taskwarrior` for tasks.
|
This repository contains the configuration and customization for `aerc`, a terminal-based email client. It is optimized for a workflow involving `notmuch` for indexing, `mbsync` for synchronization, and `abook` for address management (with a fuzzy recipient picker inside Neovim).
|
||||||
|
|
||||||
## 1. Build, Lint, and Test Commands
|
## 1. Build, Lint, and Test Commands
|
||||||
|
|
||||||
### Sync and Maintenance
|
### Sync and Maintenance
|
||||||
- **Global Sync**: Run `mailsync` (bound to `u` in `aerc`) to perform:
|
- **Global Sync**: Run `mailsync` (`~/.local/bin/mailsync`, bound to `u` in `aerc`) to perform:
|
||||||
1. `mbsync -a` (Mail sync)
|
1. `mbsync -c ~/.config/isync/mksyncrc <channel>` for channels `liphlink-phil`, `liphlink-spam`, `proton` (per-channel failure tolerated; Proton Bridge check before the proton channel)
|
||||||
2. `vdirsyncer sync` (Calendar sync)
|
2. `NOTMUCH_CONFIG=~/.config/notmuch/<acct>/config notmuch new` for `phil`, `proton`, `spam` (per-account databases)
|
||||||
3. `caldawarrior sync` (Task sync)
|
3. abook contact rebuild: every unique sender from the `phil` and `proton` accounts (spam account excluded, junk/trash/deleted filtered, own addresses and MAILER-DAEMON dropped), merged with existing manual abook entries, written atomically to `~/.local/share/abook/addressbook`
|
||||||
4. `notmuch new` for all account databases (Indexing)
|
- **Scheduling**: systemd user timers run `mailsync` every day at 08:00, 12:00, 17:00 (`mailsync.timer`, `Persistent=true`) plus 2 minutes after login (`mailsync-login.timer`). A `flock` lock prevents concurrent runs.
|
||||||
5. `notmuch-abook-sync` (Address book population)
|
- **Logs**: per-run logs rotated in `~/.local/share/aerc/logs/` (last 7 kept: `mailsync.log` is newest, `.1`–`.7` older runs).
|
||||||
|
- **Safety**: the abook datafile is backed up to `addressbook.pre-mailsync` before the first rebuild; the generated file is validated with `abook --convert` before replacing the datafile.
|
||||||
|
|
||||||
### Linting
|
### Linting
|
||||||
- **Permissions**: `accounts.conf` must be `0600`.
|
- **Permissions**: `accounts.conf` must be `0600`.
|
||||||
- **Dependencies**: `w3m`, `bat`, `yazi`, `notmuch`, `abook`, `pass`, `isync`, `khal`, `vdirsyncer`, `task`, `taskwarrior-tui`, `caldawarrior`.
|
- **Dependencies**: `w3m`, `bat`, `less` (pager scrolling), `yazi`, `notmuch`, `abook`, `pass`, `isync` (mbsync), `fzf`, `flock`, `chafa` (inline images), `urlscan` (links), `pandoc` (HTML send), `mpv` (video parts), `zathura` (PDF).
|
||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
- **Filters**: `cat email.html | w3m -T text/html -dump`
|
- **Filters**: `cat email.html | w3m -T text/html -dump`
|
||||||
- **Calendar**: `khal list`
|
- **Contacts**: `abook --mutt-query "<term>"` (must output tab-delimited `email\tname`)
|
||||||
- **Tasks**: `task list`
|
- **Sync**: run `mailsync` and inspect `~/.local/share/aerc/logs/mailsync.log`
|
||||||
|
- **HTML send**: review screen `y` uses `~/.local/bin/aerc-md-convert` (pandoc) — must produce HTML on stdout
|
||||||
|
- **Links**: `,l` = fuzzy fzf link picker (`:menu -lc fzf :open-link`), `,lu` = urlscan; OSC8 links are auto-detected from the host terminal (`[general].inherit-host-tty-features`, default on — do NOT set `enable-osc8`, deprecated in aerc 0.22)
|
||||||
|
|
||||||
## 2. Code Style Guidelines
|
## 2. Code Style Guidelines
|
||||||
|
|
||||||
@@ -32,12 +35,24 @@ This repository contains the configuration and customization for `aerc`, a termi
|
|||||||
- **Leader**: `,` (comma).
|
- **Leader**: `,` (comma).
|
||||||
- **Tab switching**: `Alt+n` (next) and `Alt+p` (prev).
|
- **Tab switching**: `Alt+n` (next) and `Alt+p` (prev).
|
||||||
- **Contacts**: `,ab` opens `abook`.
|
- **Contacts**: `,ab` opens `abook`.
|
||||||
- **Calendar**: `,ca` opens unified `khal`.
|
- **Compose with picker**: `M` (works in every context) = `:menu -t Compose -c 'mail-compose' :compose` — `mail-compose` runs in a popover (TTY) with labelled, skippable fzf passes: [1/5] account (switches tab via IPC `:change-tab`), [2/5] from/alias (parsed from `accounts.conf`), [3/5] To, [4/5] Cc, [5/5] Bcc. Marking uses **Tab or Ctrl+Space** (Ctrl+A = mark all). Its output is ONE line (`-H "From: ..." -H "To: ..." …`) — `:menu` executes each stdout line as a separate command, so all headers must share one line. Nothing picked → empty composer. Plain empty compose stays on `C`/`m`. `M` is explicitly cleared in `[terminal]` so it passes through to embedded terminal apps.
|
||||||
- **Tasks**: `,ta` opens `taskwarrior-tui`.
|
- **Attachments (compose)**: while typing in the editor (`edit-headers=true` sends keys to nvim), use **`,a` in normal mode** or **`<Alt+a>` in insert mode** — `after/ftplugin/mail.lua` opens yazi in a terminal split and attaches every selected file to the draft via IPC (`aerc :attach <path>`, one call per file — `:attach` rejects multiple paths per call). aerc-side fallbacks: **`<F4>`** (editor context) and **`,a`** (`:menu -t Attach -c 'yazi --chooser-file=%f ~/Downloads' :attach`, compose-fields + review contexts). `<C-k>`/`<C-a>` = `:attach<space>` for raw path input; `d` on review detaches. (`file-picker-cmd` is deliberately unset: aerc runs it without a TTY, so TUI pickers must use `:menu`.)
|
||||||
|
- **Compose header order**: `[compose].header-layout=To,From,Cc,Bcc,Subject` so Cc/Bcc appear above Subject in the editor.
|
||||||
|
- **Attachments (viewer)**: `n`/`p` navigate parts; `s` saves the current part to `~/Downloads`; `S = :save<space>` prompts (relative to `[general].default-save-path=~/Downloads`); `,sa` saves all attachments.
|
||||||
|
|
||||||
### Terminal Tabs (Khal/Taskwarrior)
|
### Helper scripts (`~/.local/bin`)
|
||||||
- **Escape**: Press `Ctrl+x` to release terminal focus and use `aerc` commands.
|
- **`mailsync`** — see the Sync and Maintenance section.
|
||||||
- **Tab Passthrough**: `Tab` and `Backtab` are passed to terminal apps for internal navigation.
|
- **`mail-compose`** — `:menu` picker driving `:compose` natively (bound to `M`); dispatches `:change-tab` over IPC for account selection; prints `-H` + quoted header lines.
|
||||||
|
- **`aerc-md-convert`** — pandoc markdown→HTML converter used by `:multipart text/html` (review `y`).
|
||||||
|
- **Backup layout**: all live above via symlinks into this repo — scripts in `~/dotfiles/scripts/scripts/`, systemd units in `~/dotfiles/systemd/.config/systemd/user/`, aerc config in `aerc/.config/aerc/` (`~/.config/aerc` is a symlink). Commit + push to back up.
|
||||||
|
|
||||||
|
### Neovim compose behavior (`after/ftplugin/mail.lua`)
|
||||||
|
- **Fresh messages**: cursor auto-lands on the `Subject:` line (insert mode at end); replies are left as-is (quote detection: `On ... wrote:` / `>` prefix / `In-Reply-To`).
|
||||||
|
|
||||||
|
### Markdown sending
|
||||||
|
- Drafts are written in **markdown** (`ft=mail.markdown`, spell-checked).
|
||||||
|
- Review screen `y` = `:multipart text/html` → `aerc-md-convert` (pandoc + minimal inline CSS) → sends **multipart/alternative** (plain markdown + rendered HTML). Recipients in GUI clients see rendered HTML.
|
||||||
|
- PDF is not an email body format; attach PDFs with `,a` if needed.
|
||||||
|
|
||||||
### Neovim integration (`after/ftplugin/mail.lua`)
|
### Neovim integration (`after/ftplugin/mail.lua`)
|
||||||
- **Completion**: Triggered by `Tab` on header lines using `abook --mutt-query`.
|
- **Completion**: Triggered by `Tab` on header lines using `abook --mutt-query`.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[phil]
|
[phil]
|
||||||
source = notmuch:///home/liph/Mail/phil
|
source = notmuch://phil
|
||||||
query-map = ~/.config/aerc/phil.queries
|
query-map = ~/.config/aerc/phil.queries
|
||||||
outgoing = smtp://phil%40liphlink.xyz@mail.liphlink.xyz:587
|
outgoing = smtp://phil%40liphlink.xyz@mail.liphlink.xyz:587
|
||||||
outgoing-cred-cmd = pass show mail/phil@liphlink.xyz
|
outgoing-cred-cmd = pass show mail/phil@liphlink.xyz
|
||||||
@@ -10,7 +10,7 @@ postpone = Drafts
|
|||||||
aliases = pw@liphlink.xyz,p.waibel@liphlink.xyz,philipp.waibel@liphlink.xyz
|
aliases = pw@liphlink.xyz,p.waibel@liphlink.xyz,philipp.waibel@liphlink.xyz
|
||||||
|
|
||||||
[spam]
|
[spam]
|
||||||
source = notmuch:///home/liph/Mail/spam
|
source = notmuch://spam
|
||||||
query-map = ~/.config/aerc/notmuch-queries.conf
|
query-map = ~/.config/aerc/notmuch-queries.conf
|
||||||
outgoing = smtp://spam%40liphlink.xyz@mail.liphlink.xyz:587
|
outgoing = smtp://spam%40liphlink.xyz@mail.liphlink.xyz:587
|
||||||
outgoing-cred-cmd = pass show mail/spam@liphlink.xyz
|
outgoing-cred-cmd = pass show mail/spam@liphlink.xyz
|
||||||
@@ -20,7 +20,7 @@ default = Inbox
|
|||||||
aliases = blue@liphlink.xyz,red@liphlink.xyz
|
aliases = blue@liphlink.xyz,red@liphlink.xyz
|
||||||
|
|
||||||
[proton]
|
[proton]
|
||||||
source = notmuch:///home/liph/Mail/proton
|
source = notmuch://proton
|
||||||
query-map = ~/.config/aerc/notmuch-queries.conf
|
query-map = ~/.config/aerc/notmuch-queries.conf
|
||||||
outgoing = smtp+insecure://liiph%40protonmail.com@127.0.0.1:1025
|
outgoing = smtp+insecure://liiph%40protonmail.com@127.0.0.1:1025
|
||||||
outgoing-cred-cmd = pass show mail/liiph@protonmail.com
|
outgoing-cred-cmd = pass show mail/liiph@protonmail.com
|
||||||
|
|||||||
+11
-10
@@ -5,7 +5,7 @@
|
|||||||
# Used as a default path for save operations if no other path is specified.
|
# Used as a default path for save operations if no other path is specified.
|
||||||
# ~ is expanded to the current user home dir.
|
# ~ is expanded to the current user home dir.
|
||||||
#
|
#
|
||||||
#default-save-path=
|
default-save-path=~/Downloads
|
||||||
|
|
||||||
# If set to "gpg", aerc will use system gpg binary and keystore for all crypto
|
# If set to "gpg", aerc will use system gpg binary and keystore for all crypto
|
||||||
# operations. If set to "internal", the internal openpgp keyring will be used.
|
# operations. If set to "internal", the internal openpgp keyring will be used.
|
||||||
@@ -22,7 +22,6 @@
|
|||||||
#
|
#
|
||||||
# Default: false
|
# Default: false
|
||||||
unsafe-accounts-conf=false
|
unsafe-accounts-conf=false
|
||||||
index-format=notmuch://~/.local/share/mail
|
|
||||||
# Output log messages to specified file. A path starting with ~/ is expanded to
|
# Output log messages to specified file. A path starting with ~/ is expanded to
|
||||||
# the user home dir. When redirecting aerc's output to a file using > shell
|
# the user home dir. When redirecting aerc's output to a file using > shell
|
||||||
# redirection, this setting is ignored and log messages are printed to stdout.
|
# redirection, this setting is ignored and log messages are printed to stdout.
|
||||||
@@ -58,8 +57,6 @@ log-level=warn
|
|||||||
# Display OSC8 strings in the embedded terminal
|
# Display OSC8 strings in the embedded terminal
|
||||||
#
|
#
|
||||||
# Default: false
|
# Default: false
|
||||||
#enable-osc8=false
|
|
||||||
|
|
||||||
# Default shell command to use for :menu. This will be executed with sh -c and
|
# Default shell command to use for :menu. This will be executed with sh -c and
|
||||||
# will run in an popover dialog.
|
# will run in an popover dialog.
|
||||||
#
|
#
|
||||||
@@ -282,6 +279,9 @@ dirlist-collapse=1
|
|||||||
#
|
#
|
||||||
sort = -r date
|
sort = -r date
|
||||||
|
|
||||||
|
# Keep 2 lines of context visible when moving through the message list
|
||||||
|
msglist-scroll-offset=2
|
||||||
|
|
||||||
# Moves to next message when the current message is deleted
|
# Moves to next message when the current message is deleted
|
||||||
#
|
#
|
||||||
# Default: true
|
# Default: true
|
||||||
@@ -576,7 +576,7 @@ pager= bat --style=plain --paging=always
|
|||||||
# html-unsafe-images=false
|
# html-unsafe-images=false
|
||||||
# html-filter=dante
|
# html-filter=dante
|
||||||
image/*=chafa -f kitty -s ${width}x${height}
|
image/*=chafa -f kitty -s ${width}x${height}
|
||||||
text/html=w3m -T text/html -dump -o display_link_number=1
|
text/html=/usr/lib/aerc/filters/html
|
||||||
application/pdf=zathura
|
application/pdf=zathura
|
||||||
video/*=mpv
|
video/*=mpv
|
||||||
header-layout=From,To,Subject,Date
|
header-layout=From,To,Subject,Date
|
||||||
@@ -640,6 +640,9 @@ editor=nvim +'set ft=mail.markdown'
|
|||||||
# Address book command
|
# Address book command
|
||||||
address-book-cmd=abook --mutt-query '%s'
|
address-book-cmd=abook --mutt-query '%s'
|
||||||
|
|
||||||
|
# Order of headers in the compose editor (edit-headers=true: one per line, no | merging)
|
||||||
|
header-layout=To,From,Cc,Bcc,Subject
|
||||||
|
|
||||||
# Auto-complete addresses
|
# Auto-complete addresses
|
||||||
# address-book-cmd-match='%s'
|
# address-book-cmd-match='%s'
|
||||||
# address-book-cmd=abook --mutt-query '%s'
|
# address-book-cmd=abook --mutt-query '%s'
|
||||||
@@ -700,6 +703,8 @@ edit-headers=true
|
|||||||
# standard output, one file per line. If it is present, then aerc does not
|
# standard output, one file per line. If it is present, then aerc does not
|
||||||
# capture the standard output and instead reads the files from the temporary
|
# capture the standard output and instead reads the files from the temporary
|
||||||
# file which should have the same format.
|
# file which should have the same format.
|
||||||
|
# file-picker-cmd: aerc runs it without a TTY, so TUI pickers (yazi) cannot run here.
|
||||||
|
# Attachments use :menu with yazi instead (see binds.conf [compose] `a`).
|
||||||
#file-picker-cmd=/home/liph/.local/bin/yazi-picker %f
|
#file-picker-cmd=/home/liph/.local/bin/yazi-picker %f
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -740,8 +745,6 @@ text/html=/home/liph/.local/bin/aerc-md-convert
|
|||||||
[filters]
|
[filters]
|
||||||
#
|
#
|
||||||
text/plain=sed 's/^$//'
|
text/plain=sed 's/^$//'
|
||||||
|
|
||||||
# Filters allow you to pipe an email body through a shell command to render
|
|
||||||
# certain emails differently, e.g. highlighting them with ANSI escape codes.
|
# certain emails differently, e.g. highlighting them with ANSI escape codes.
|
||||||
#
|
#
|
||||||
# The commands are invoked with sh -c. The following folders are prepended to
|
# The commands are invoked with sh -c. The following folders are prepended to
|
||||||
@@ -773,11 +776,10 @@ text/plain=sed 's/^$//'
|
|||||||
# against (non-case-sensitive) and a comma, e.g. subject,text will match a
|
# against (non-case-sensitive) and a comma, e.g. subject,text will match a
|
||||||
# subject which contains "text". Use header,~regex to match against a regex.
|
# subject which contains "text". Use header,~regex to match against a regex.
|
||||||
#
|
#
|
||||||
text/plain=colorize
|
|
||||||
text/calendar=calendar
|
text/calendar=calendar
|
||||||
message/delivery-status=colorize
|
message/delivery-status=colorize
|
||||||
message/rfc822=colorize
|
message/rfc822=colorize
|
||||||
text/html=w3m -T text/html -dump -o display_link_number=1
|
text/html=/usr/lib/aerc/filters/html
|
||||||
#text/*=bat -fP --file-name="$AERC_FILENAME"
|
#text/*=bat -fP --file-name="$AERC_FILENAME"
|
||||||
#application/x-sh=bat -fP -l sh
|
#application/x-sh=bat -fP -l sh
|
||||||
#image/*=catimg -w $(tput cols) -
|
#image/*=catimg -w $(tput cols) -
|
||||||
@@ -814,7 +816,6 @@ text/html=w3m -T text/html -dump -o display_link_number=1
|
|||||||
text/html=w3m
|
text/html=w3m
|
||||||
x-scheme-handler/http=librewolf
|
x-scheme-handler/http=librewolf
|
||||||
x-scheme-handler/https=librewolf
|
x-scheme-handler/https=librewolf
|
||||||
text/calendar=khal import --batch
|
|
||||||
|
|
||||||
[hooks]
|
[hooks]
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -0,0 +1,886 @@
|
|||||||
|
#
|
||||||
|
# aerc main configuration
|
||||||
|
|
||||||
|
[general]
|
||||||
|
# Used as a default path for save operations if no other path is specified.
|
||||||
|
# ~ is expanded to the current user home dir.
|
||||||
|
#
|
||||||
|
#default-save-path=
|
||||||
|
|
||||||
|
# If set to "gpg", aerc will use system gpg binary and keystore for all crypto
|
||||||
|
# operations. If set to "internal", the internal openpgp keyring will be used.
|
||||||
|
# If set to "auto", the system gpg will be preferred unless the internal
|
||||||
|
# keyring already exists, in which case the latter will be used.
|
||||||
|
#
|
||||||
|
# Default: auto
|
||||||
|
#pgp-provider=auto
|
||||||
|
|
||||||
|
# By default, the file permissions of accounts.conf must be restrictive and
|
||||||
|
# only allow reading by the file owner (0600). Set this option to true to
|
||||||
|
# ignore this permission check. Use this with care as it may expose your
|
||||||
|
# credentials.
|
||||||
|
#
|
||||||
|
# Default: false
|
||||||
|
unsafe-accounts-conf=false
|
||||||
|
index-format=notmuch://~/.local/share/mail
|
||||||
|
# Output log messages to specified file. A path starting with ~/ is expanded to
|
||||||
|
# the user home dir. When redirecting aerc's output to a file using > shell
|
||||||
|
# redirection, this setting is ignored and log messages are printed to stdout.
|
||||||
|
#
|
||||||
|
log-file=~/.local/share/aerc/aerc.log
|
||||||
|
log-level=warn
|
||||||
|
|
||||||
|
|
||||||
|
# Disable IPC entirely. Don't run commands (including mailto:... and mbox:...)
|
||||||
|
# in an existing aerc instance, and don't start an IPC server to allow
|
||||||
|
# subsequent aerc instances to run commands in the current one.
|
||||||
|
#
|
||||||
|
# Default: false
|
||||||
|
#disable-ipc=false
|
||||||
|
|
||||||
|
# Don't run mailto:... commands over IPC; start a new aerc instance with the
|
||||||
|
# composer instead.
|
||||||
|
#
|
||||||
|
# Default: false
|
||||||
|
#disable-ipc-mailto=false
|
||||||
|
#
|
||||||
|
# Don't run mbox:... commands over IPC; start a new aerc instance with the mbox
|
||||||
|
# file instead.
|
||||||
|
#
|
||||||
|
# Default: false
|
||||||
|
#disable-ipc-mbox=false
|
||||||
|
|
||||||
|
# Set the $TERM environment variable used for the embedded terminal.
|
||||||
|
#
|
||||||
|
# Default: xterm-256color
|
||||||
|
#term=xterm-256color
|
||||||
|
|
||||||
|
# Display OSC8 strings in the embedded terminal
|
||||||
|
#
|
||||||
|
# Default: false
|
||||||
|
#enable-osc8=false
|
||||||
|
|
||||||
|
# Default shell command to use for :menu. This will be executed with sh -c and
|
||||||
|
# will run in an popover dialog.
|
||||||
|
#
|
||||||
|
# Any occurrence of %f will be replaced by a temporary file path where the
|
||||||
|
# command is expected to write output lines to be consumed by :menu. Otherwise,
|
||||||
|
# the lines will be read from the command's standard output.
|
||||||
|
#
|
||||||
|
# Examples:
|
||||||
|
# default-menu-cmd=fzf
|
||||||
|
# default-menu-cmd=fzf --multi
|
||||||
|
# default-menu-cmd=dmenu -l 20
|
||||||
|
# default-menu-cmd=ranger --choosefiles=%f
|
||||||
|
#
|
||||||
|
#default-menu-cmd=
|
||||||
|
|
||||||
|
[ui]
|
||||||
|
#
|
||||||
|
# Describes the format for each row in a mailbox view. This is a comma
|
||||||
|
# separated list of column names with an optional align and width suffix. After
|
||||||
|
# the column name, one of the '<' (left), ':' (center) or '>' (right) alignment
|
||||||
|
# characters can be added (by default, left) followed by an optional width
|
||||||
|
# specifier. The width is either an integer representing a fixed number of
|
||||||
|
# characters, or a percentage between 1% and 99% representing a fraction of the
|
||||||
|
# terminal width. It can also be one of the '*' (auto) or '=' (fit) special
|
||||||
|
# width specifiers. Auto width columns will be equally attributed the remaining
|
||||||
|
# terminal width. Fit width columns take the width of their contents. If no
|
||||||
|
# width specifier is set, '*' is used by default.
|
||||||
|
#
|
||||||
|
# Default: flags:4,name<20%,subject,date>=
|
||||||
|
index-columns=flags:4,name<35%,subject,date>=
|
||||||
|
column-name={{if match .Folder "(?i)Sent|Gesendet"}}{{index (.To | persons) 0}}{{else}}{{index (.From | persons) 0}}{{end}}
|
||||||
|
border-char-vertical="│"
|
||||||
|
border-char-horizontal="─"
|
||||||
|
styleset-name="rose-pine-moon"
|
||||||
|
threading-enabled=true
|
||||||
|
|
||||||
|
#
|
||||||
|
# Each name in index-columns must have a corresponding column-$name setting.
|
||||||
|
# All column-$name settings accept golang text/template syntax. See
|
||||||
|
# aerc-templates(7) for available template attributes and functions.
|
||||||
|
#
|
||||||
|
# Here are some examples to show the To field instead of the From field for
|
||||||
|
# an email (modifying column-name):
|
||||||
|
#
|
||||||
|
# 1. a generic one
|
||||||
|
# column-name={{ .Peer | names | join ", " }}
|
||||||
|
# 2. based upon the selected folder
|
||||||
|
# column-name={{if match .Folder "^(Gesendet|Sent)$"}}{{index (.To | names) 0}}{{else}}{{index (.From | names) 0}}{{end}}
|
||||||
|
#
|
||||||
|
# Default settings
|
||||||
|
#column-flags={{.Flags | join ""}}
|
||||||
|
#column-name={{index (.From | names) 0}}
|
||||||
|
#column-subject={{.ThreadPrefix}}{{.Subject}}
|
||||||
|
#column-date={{.DateAutoFormat .Date.Local}}
|
||||||
|
|
||||||
|
#
|
||||||
|
# String separator inserted between columns. When the column width specifier is
|
||||||
|
# an exact number of characters, the separator is added to it (i.e. the exact
|
||||||
|
# width will be fully available for the column contents).
|
||||||
|
#
|
||||||
|
# Default: " "
|
||||||
|
#column-separator=" "
|
||||||
|
|
||||||
|
#
|
||||||
|
# See time.Time#Format at https://godoc.org/time#Time.Format
|
||||||
|
#
|
||||||
|
# Default: 2006 Jan 02
|
||||||
|
#timestamp-format=2006 Jan 02
|
||||||
|
|
||||||
|
#
|
||||||
|
# Index-only time format for messages that were received/sent today.
|
||||||
|
# If this is empty, timestamp-format is used instead.
|
||||||
|
#
|
||||||
|
# Default: 15:04
|
||||||
|
#this-day-time-format=15:04
|
||||||
|
|
||||||
|
#
|
||||||
|
# Index-only time format for messages that were received/sent within the last
|
||||||
|
# 7 days. If this is empty, timestamp-format is used instead.
|
||||||
|
#
|
||||||
|
# Default: Jan 02
|
||||||
|
#this-week-time-format=Jan 02
|
||||||
|
|
||||||
|
#
|
||||||
|
# Index-only time format for messages that were received/sent this year.
|
||||||
|
# If this is empty, timestamp-format is used instead.
|
||||||
|
#
|
||||||
|
#Default: Jan 02
|
||||||
|
#this-year-time-format=Jan 02
|
||||||
|
|
||||||
|
#
|
||||||
|
# Overrides timestamp-format for the message view.
|
||||||
|
#
|
||||||
|
# Default: 2006 Jan 02, 15:04 GMT-0700
|
||||||
|
#message-view-timestamp-format=2006 Jan 02, 15:04 GMT-0700
|
||||||
|
|
||||||
|
#
|
||||||
|
# If set, overrides timestamp-format in the message view for messages
|
||||||
|
# that were received/sent today.
|
||||||
|
#
|
||||||
|
#message-view-this-day-time-format=
|
||||||
|
|
||||||
|
# If set, overrides timestamp-format in the message view for messages
|
||||||
|
# that were received/sent within the last 7 days.
|
||||||
|
#
|
||||||
|
#message-view-this-week-time-format=
|
||||||
|
|
||||||
|
#
|
||||||
|
# If set, overrides *timestamp-format* in the message view for messages
|
||||||
|
# that were received/sent this year.
|
||||||
|
#
|
||||||
|
#message-view-this-year-time-format=
|
||||||
|
|
||||||
|
#
|
||||||
|
# Width of the sidebar, including the border.
|
||||||
|
#
|
||||||
|
# Default: 22
|
||||||
|
sidebar-width=34
|
||||||
|
|
||||||
|
#
|
||||||
|
# Default split layout for message list tabs. The syntax is:
|
||||||
|
#
|
||||||
|
# [<direction>] <size>
|
||||||
|
#
|
||||||
|
# <direction> is optional and defaults to horizontal. It can take one
|
||||||
|
# of the following values: h, horiz, horizontal, v, vert, vertical.
|
||||||
|
#
|
||||||
|
# <size> is a positive integer representing the size (in terminal cells)
|
||||||
|
# of the message list window.
|
||||||
|
#
|
||||||
|
#message-list-split=
|
||||||
|
|
||||||
|
#
|
||||||
|
# Message to display when viewing an empty folder.
|
||||||
|
#
|
||||||
|
# Default: (no messages)
|
||||||
|
#empty-message=(no messages)
|
||||||
|
|
||||||
|
# Message to display when no folders exists or are all filtered
|
||||||
|
#
|
||||||
|
# Default: (no folders)
|
||||||
|
#empty-dirlist=(no folders)
|
||||||
|
#
|
||||||
|
# Value to set {{.Subject}} template to when subject is empty.
|
||||||
|
#
|
||||||
|
# Default: (no subject)
|
||||||
|
#empty-subject=(no subject)
|
||||||
|
|
||||||
|
# Enable mouse events in the ui, e.g. clicking and scrolling with the mousewheel
|
||||||
|
#
|
||||||
|
# Default: false
|
||||||
|
mouse-enabled=false
|
||||||
|
|
||||||
|
#
|
||||||
|
# Ring the bell when new messages are received
|
||||||
|
#
|
||||||
|
# Default: true
|
||||||
|
#new-message-bell=true
|
||||||
|
|
||||||
|
#
|
||||||
|
# Template to use for Account tab titles
|
||||||
|
#
|
||||||
|
# Default: {{.Account}}
|
||||||
|
#tab-title-account={{.Account}}
|
||||||
|
|
||||||
|
#
|
||||||
|
# Template to use for Composer tab titles
|
||||||
|
#
|
||||||
|
# Default: {{if .To}}to:{{index (.To | shortmboxes) 0}} {{end}}{{.SubjectBase}}
|
||||||
|
#tab-title-composer={{if .To}}to:{{index (.To | shortmboxes) 0}} {{end}}{{.SubjectBase}}
|
||||||
|
|
||||||
|
#
|
||||||
|
# Template to use for Message Viewer tab titles
|
||||||
|
#
|
||||||
|
# Default: {{.Subject}}
|
||||||
|
#tab-title-viewer={{.Subject}}
|
||||||
|
|
||||||
|
|
||||||
|
# Marker to show before a pinned tab's name.
|
||||||
|
#
|
||||||
|
# Default: `
|
||||||
|
#pinned-tab-marker='`'
|
||||||
|
|
||||||
|
# Template for the left side of the directory list.
|
||||||
|
# See aerc-templates(7) for all available fields and functions.
|
||||||
|
#
|
||||||
|
# Default: {{.Folder}}
|
||||||
|
dirlist-left={{if match .Folder "/"}}{{.Style (index (.Folder | split "/" | tail 1) 0) "folder_sub"}}{{else}}{{.Folder}}{{end}}
|
||||||
|
|
||||||
|
# Template for the right side of the directory list.
|
||||||
|
# See aerc-templates(7) for all available fields and functions.
|
||||||
|
#
|
||||||
|
# Default: {{if .Unread}}{{humanReadable .Unread}}{{end}}
|
||||||
|
dirlist-right={{if .Unread}}{{humanReadable .Unread}} / {{end}}{{humanReadable .Exists}}
|
||||||
|
|
||||||
|
# Delay after which the messages are actually listed when entering a directory.
|
||||||
|
# This avoids loading messages when skipping over folders and makes the UI more
|
||||||
|
# responsive. If you do not want that, set it to 0s.
|
||||||
|
#
|
||||||
|
# Default: 200ms
|
||||||
|
#dirlist-delay=200ms
|
||||||
|
|
||||||
|
# Display the directory list as a foldable tree that allows to collapse and
|
||||||
|
# expand the folders.
|
||||||
|
#
|
||||||
|
# Default: false
|
||||||
|
dirlist-tree=true
|
||||||
|
|
||||||
|
# If dirlist-tree is enabled, set level at which folders are collapsed by
|
||||||
|
# default. Set to 0 to disable.
|
||||||
|
#
|
||||||
|
# Default: 0
|
||||||
|
dirlist-collapse=1
|
||||||
|
|
||||||
|
# List of space-separated criteria to sort the messages by, see *sort*
|
||||||
|
# command in *aerc*(1) for reference. Prefixing a criterion with "-r "
|
||||||
|
# reverses that criterion.
|
||||||
|
#
|
||||||
|
# Example: "from -r date"
|
||||||
|
#
|
||||||
|
sort = -r date
|
||||||
|
|
||||||
|
# Moves to next message when the current message is deleted
|
||||||
|
#
|
||||||
|
# Default: true
|
||||||
|
#next-message-on-delete=true
|
||||||
|
|
||||||
|
# Automatically set the "seen" flag when a message is opened in the message
|
||||||
|
# viewer.
|
||||||
|
#
|
||||||
|
# Default: true
|
||||||
|
#auto-mark-read=true
|
||||||
|
|
||||||
|
# The directories where the stylesets are stored. It takes a colon-separated
|
||||||
|
# list of directories. If this is unset or if a styleset cannot be found, the
|
||||||
|
# following paths will be used as a fallback in that order:
|
||||||
|
#
|
||||||
|
# ${XDG_CONFIG_HOME:-~/.config}/aerc/stylesets
|
||||||
|
# ${XDG_DATA_HOME:-~/.local/share}/aerc/stylesets
|
||||||
|
# /usr/local/share/aerc/stylesets
|
||||||
|
# /usr/share/aerc/stylesets
|
||||||
|
#
|
||||||
|
#stylesets-dirs=
|
||||||
|
|
||||||
|
# Uncomment to use box-drawing characters for vertical and horizontal borders.
|
||||||
|
#
|
||||||
|
# Default: "│" and "─"
|
||||||
|
#border-char-vertical="│"
|
||||||
|
#border-char-horizontal="─"
|
||||||
|
|
||||||
|
# Sets the styleset to use for the aerc ui elements.
|
||||||
|
#
|
||||||
|
# Default: default
|
||||||
|
#styleset-name=default
|
||||||
|
|
||||||
|
# Activates fuzzy search in commands and their arguments: the typed string is
|
||||||
|
# searched in the command or option in any position, and need not be
|
||||||
|
# consecutive characters in the command or option.
|
||||||
|
#
|
||||||
|
# Default: false
|
||||||
|
#fuzzy-complete=false
|
||||||
|
|
||||||
|
# How long to wait after the last input before auto-completion is triggered.
|
||||||
|
#
|
||||||
|
# Default: 250ms
|
||||||
|
#completion-delay=250ms
|
||||||
|
|
||||||
|
# The minimum required characters to allow auto-completion to be triggered after
|
||||||
|
# completion-delay.
|
||||||
|
#
|
||||||
|
# Setting this to "manual" disables automatic completion, leaving only the
|
||||||
|
# manually triggered completion with the $complete key (see aerc-binds(5) for
|
||||||
|
# more details).
|
||||||
|
#
|
||||||
|
# Default: 1
|
||||||
|
#completion-min-chars=1
|
||||||
|
|
||||||
|
#
|
||||||
|
# Global switch for completion popovers
|
||||||
|
#
|
||||||
|
# Default: true
|
||||||
|
#completion-popovers=true
|
||||||
|
|
||||||
|
# Uncomment to use UTF-8 symbols to indicate PGP status of messages
|
||||||
|
#
|
||||||
|
# Default: ASCII
|
||||||
|
#icon-unencrypted=
|
||||||
|
#icon-encrypted=✔
|
||||||
|
#icon-signed=✔
|
||||||
|
#icon-signed-encrypted=✔
|
||||||
|
#icon-unknown=✘
|
||||||
|
#icon-invalid=⚠
|
||||||
|
|
||||||
|
# Reverses the order of the message list. By default, the message list is
|
||||||
|
# ordered with the newest (highest UID) message on top. Reversing the order
|
||||||
|
# will put the oldest (lowest UID) message on top. This can be useful in cases
|
||||||
|
# where the backend does not support sorting.
|
||||||
|
#
|
||||||
|
# Default: false
|
||||||
|
#reverse-msglist-order = false
|
||||||
|
|
||||||
|
# Reverse display of the message threads. Default order is the initial
|
||||||
|
# message is on the top with all the replies being displayed below. The
|
||||||
|
# reverse option will put the initial message at the bottom with the
|
||||||
|
# replies on top.
|
||||||
|
#
|
||||||
|
# Default: false
|
||||||
|
#reverse-thread-order=false
|
||||||
|
|
||||||
|
# Positions the cursor on the last message in the message list (at the
|
||||||
|
# bottom of the view) when opening a new folder.
|
||||||
|
#
|
||||||
|
# Default: false
|
||||||
|
#select-last-message=false
|
||||||
|
|
||||||
|
# Sort the thread siblings according to the sort criteria for the messages. If
|
||||||
|
# sort-thread-siblings is false, the thread siblings will be sorted based on
|
||||||
|
# the message UID in ascending order. This option is only applicable for
|
||||||
|
# client-side threading with a backend that enables sorting. Note that there's
|
||||||
|
# a performance impact when sorting is activated.
|
||||||
|
#
|
||||||
|
# Default: false
|
||||||
|
#sort-thread-siblings=false
|
||||||
|
|
||||||
|
# Set the scroll offset in number of lines from the top and bottom of the
|
||||||
|
# message list.
|
||||||
|
#
|
||||||
|
# Default: 0
|
||||||
|
#msglist-scroll-offset = 0
|
||||||
|
|
||||||
|
#
|
||||||
|
# Enable a threaded view of messages. If this is not supported by the backend
|
||||||
|
# (IMAP server or notmuch), threads will be built by the client.
|
||||||
|
#
|
||||||
|
# Default: false
|
||||||
|
#threading-enabled=false
|
||||||
|
|
||||||
|
# Force client-side thread building
|
||||||
|
#
|
||||||
|
# Default: false
|
||||||
|
#force-client-threads=false
|
||||||
|
|
||||||
|
# If no References nor In-Reply-To headers can be matched to build client side
|
||||||
|
# threads, fallback to similar subjects.
|
||||||
|
#
|
||||||
|
# Default: false
|
||||||
|
#threading-by-subject=false
|
||||||
|
|
||||||
|
# Show thread context enables messages which do not match the current query (or
|
||||||
|
# belong to the current mailbox) to be shown for context. These messages can be
|
||||||
|
# styled separately using "msglist_thread_context" in a styleset. This feature
|
||||||
|
# is not supported by all backends
|
||||||
|
#
|
||||||
|
# Default: false
|
||||||
|
#show-thread-context=false
|
||||||
|
|
||||||
|
# Debounce client-side thread building
|
||||||
|
#
|
||||||
|
# Default: 50ms
|
||||||
|
#client-threads-delay=50ms
|
||||||
|
|
||||||
|
#
|
||||||
|
# Thread prefix customization:
|
||||||
|
|
||||||
|
#
|
||||||
|
# Customize the thread prefix appearance by selecting the arrow head.
|
||||||
|
#
|
||||||
|
# Default: ">"
|
||||||
|
#thread-prefix-tip = ">"
|
||||||
|
|
||||||
|
#
|
||||||
|
# Customize the thread prefix appearance by selecting the arrow indentation.
|
||||||
|
#
|
||||||
|
# Default: " "
|
||||||
|
#thread-prefix-indent = " "
|
||||||
|
|
||||||
|
#
|
||||||
|
# Customize the thread prefix appearance by selecting the vertical extension of
|
||||||
|
# the arrow.
|
||||||
|
#
|
||||||
|
# Default: "│"
|
||||||
|
#thread-prefix-stem = "│"
|
||||||
|
|
||||||
|
#
|
||||||
|
# Customize the thread prefix appearance by selecting the horizontal extension
|
||||||
|
# of the arrow.
|
||||||
|
#
|
||||||
|
# Default: ""
|
||||||
|
#thread-prefix-limb = ""
|
||||||
|
|
||||||
|
#
|
||||||
|
# Customize the thread prefix appearance by selecting the folded thread
|
||||||
|
# indicator.
|
||||||
|
#
|
||||||
|
# Default: "+"
|
||||||
|
#thread-prefix-folded = "+"
|
||||||
|
|
||||||
|
#
|
||||||
|
# Customize the thread prefix appearance by selecting the unfolded thread
|
||||||
|
# indicator.
|
||||||
|
#
|
||||||
|
# Default: ""
|
||||||
|
#thread-prefix-unfolded = ""
|
||||||
|
|
||||||
|
#
|
||||||
|
# Customize the thread prefix appearance by selecting the first child connector.
|
||||||
|
#
|
||||||
|
# Default: ""
|
||||||
|
#thread-prefix-first-child = ""
|
||||||
|
|
||||||
|
#
|
||||||
|
# Customize the thread prefix appearance by selecting the connector used if
|
||||||
|
# the message has siblings.
|
||||||
|
#
|
||||||
|
# Default: "├─"
|
||||||
|
#thread-prefix-has-siblings = "├─"
|
||||||
|
|
||||||
|
#
|
||||||
|
# Customize the thread prefix appearance by selecting the connector used if the
|
||||||
|
# message has no parents and no children.
|
||||||
|
#
|
||||||
|
# Default: ""
|
||||||
|
#thread-prefix-lone = ""
|
||||||
|
|
||||||
|
#
|
||||||
|
# Customize the thread prefix appearance by selecting the connector used if the
|
||||||
|
# message has no parents and has children.
|
||||||
|
#
|
||||||
|
# Default: ""
|
||||||
|
#thread-prefix-orphan = ""
|
||||||
|
|
||||||
|
#
|
||||||
|
# Customize the thread prefix appearance by selecting the connector for the last
|
||||||
|
# sibling.
|
||||||
|
#
|
||||||
|
# Default: "└─"
|
||||||
|
#thread-prefix-last-sibling = "└─"
|
||||||
|
|
||||||
|
#
|
||||||
|
# Customize the reversed thread prefix appearance by selecting the connector for
|
||||||
|
# the last sibling.
|
||||||
|
#
|
||||||
|
# Default: "┌─"
|
||||||
|
#thread-prefix-last-sibling-reverse = "┌─"
|
||||||
|
|
||||||
|
#
|
||||||
|
# Customize the thread prefix appearance by selecting the connector for dummy
|
||||||
|
# thread.
|
||||||
|
#
|
||||||
|
# Default: "┬─"
|
||||||
|
#thread-prefix-dummy = "┬─"
|
||||||
|
|
||||||
|
#
|
||||||
|
# Customize the reversed thread prefix appearance by selecting the connector for
|
||||||
|
# dummy thread.
|
||||||
|
#
|
||||||
|
# Default: "┴─"
|
||||||
|
#thread-prefix-dummy-reverse = "┴─"
|
||||||
|
|
||||||
|
#
|
||||||
|
# Customize the reversed thread prefix appearance by selecting the first child
|
||||||
|
# connector.
|
||||||
|
#
|
||||||
|
# Default: ""
|
||||||
|
#thread-prefix-first-child-reverse = ""
|
||||||
|
|
||||||
|
#
|
||||||
|
# Customize the reversed thread prefix appearance by selecting the connector
|
||||||
|
# used if the message has no parents and has children.
|
||||||
|
#
|
||||||
|
# Default: ""
|
||||||
|
#thread-prefix-orphan-reverse = ""
|
||||||
|
|
||||||
|
[statusline]
|
||||||
|
#
|
||||||
|
# Describes the format for the status line. This is a comma separated list of
|
||||||
|
# column names with an optional align and width suffix. See [ui].index-columns
|
||||||
|
# for more details. To completely mute the status line except for push
|
||||||
|
# notifications, explicitly set status-columns to an empty string.
|
||||||
|
#
|
||||||
|
# Default: left<*,center:=,right>*
|
||||||
|
#status-columns=left<*,center:=,right>*
|
||||||
|
|
||||||
|
#
|
||||||
|
# Each name in status-columns must have a corresponding column-$name setting.
|
||||||
|
# All column-$name settings accept golang text/template syntax. See
|
||||||
|
# aerc-templates(7) for available template attributes and functions.
|
||||||
|
#
|
||||||
|
# Default settings
|
||||||
|
#column-left=[{{.Account}}] {{.StatusInfo}}
|
||||||
|
#column-center={{.PendingKeys}}
|
||||||
|
#column-right={{.TrayInfo}} | {{cwd}}
|
||||||
|
|
||||||
|
#
|
||||||
|
# String separator inserted between columns.
|
||||||
|
# See [ui].column-separator for more details.
|
||||||
|
#
|
||||||
|
#column-separator=" "
|
||||||
|
|
||||||
|
# Specifies the separator between grouped statusline elements.
|
||||||
|
#
|
||||||
|
# Default: " | "
|
||||||
|
#separator=" | "
|
||||||
|
|
||||||
|
# Defines the mode for displaying the status elements.
|
||||||
|
# Options: text, icon
|
||||||
|
#
|
||||||
|
# Default: text
|
||||||
|
#display-mode=text
|
||||||
|
|
||||||
|
[viewer]
|
||||||
|
# pager=less -Rc
|
||||||
|
pager= bat --style=plain --paging=always
|
||||||
|
# html-unsafe-images=false
|
||||||
|
# html-filter=dante
|
||||||
|
image/*=chafa -f kitty -s ${width}x${height}
|
||||||
|
text/html=w3m -T text/html -dump -o display_link_number=1
|
||||||
|
application/pdf=zathura
|
||||||
|
video/*=mpv
|
||||||
|
header-layout=From,To,Subject,Date
|
||||||
|
|
||||||
|
|
||||||
|
#w3m -dump -T text/html -o display_link_number=1
|
||||||
|
#
|
||||||
|
# Specifies the pager to use when displaying emails. Note that some filters
|
||||||
|
# may add ANSI codes to add color to rendered emails, so you may want to use a
|
||||||
|
# pager which supports ANSI codes.
|
||||||
|
#
|
||||||
|
# Default: less -Rc
|
||||||
|
|
||||||
|
#
|
||||||
|
# If an email offers several versions (multipart), you can configure which
|
||||||
|
# mimetype to prefer. For example, this can be used to prefer plaintext over
|
||||||
|
# html emails.
|
||||||
|
#
|
||||||
|
# Default: text/plain,text/html
|
||||||
|
#alternatives=text/plain,text/html
|
||||||
|
|
||||||
|
#
|
||||||
|
# Default setting to determine whether to show full headers or only parsed
|
||||||
|
# ones in message viewer.
|
||||||
|
#
|
||||||
|
# Default: false
|
||||||
|
#show-headers=false
|
||||||
|
|
||||||
|
#
|
||||||
|
# Layout of headers when viewing a message. To display multiple headers in the
|
||||||
|
# same row, separate them with a pipe, e.g. "From|To". Rows will be hidden if
|
||||||
|
# none of their specified headers are present in the message.
|
||||||
|
#
|
||||||
|
# Default: From|To,Cc|Bcc,Date,Subject
|
||||||
|
#header-layout=From|To,Cc|Bcc,Date,Subject
|
||||||
|
|
||||||
|
# Whether to always show the mimetype of an email, even when it is just a single part
|
||||||
|
#
|
||||||
|
# Default: false
|
||||||
|
#always-show-mime=false
|
||||||
|
|
||||||
|
# Define the maximum height of the mimetype switcher before a scrollbar is used.
|
||||||
|
# The height of the mimetype switcher is restricted to half of the display
|
||||||
|
# height. If the provided value for the height is zero, the number of parts will
|
||||||
|
# be used as the height of the type switcher.
|
||||||
|
#
|
||||||
|
# Default: 0
|
||||||
|
#max-mime-height = 0
|
||||||
|
|
||||||
|
# Parses and extracts http links when viewing a message. Links can then be
|
||||||
|
# accessed with the open-link command.
|
||||||
|
#
|
||||||
|
# Default: true
|
||||||
|
#parse-http-links=true
|
||||||
|
|
||||||
|
[compose]
|
||||||
|
editor=nvim +'set ft=mail.markdown'
|
||||||
|
|
||||||
|
# editor=nvim +setf\ mail
|
||||||
|
# editor=nvim # or your preferred editor
|
||||||
|
# Address book command
|
||||||
|
address-book-cmd=abook --mutt-query '%s'
|
||||||
|
|
||||||
|
# Auto-complete addresses
|
||||||
|
# address-book-cmd-match='%s'
|
||||||
|
# address-book-cmd=abook --mutt-query '%s'
|
||||||
|
# terminal, though it may also launch a graphical window if the environment
|
||||||
|
# supports it. Defaults to $EDITOR, or vi.
|
||||||
|
#editor=
|
||||||
|
|
||||||
|
#
|
||||||
|
# When set, aerc will create and read .eml files for composing that have
|
||||||
|
# non-standard \n linebreaks. This is only relevant if the used editor does not
|
||||||
|
# support CRLF linebreaks.
|
||||||
|
#
|
||||||
|
#lf-editor=false
|
||||||
|
|
||||||
|
#
|
||||||
|
# Default header fields to display when composing a message. To display
|
||||||
|
# multiple headers in the same row, separate them with a pipe, e.g. "To|From".
|
||||||
|
#
|
||||||
|
# Default: To|From,Subject
|
||||||
|
#header-layout=To|From,Subject
|
||||||
|
|
||||||
|
#
|
||||||
|
# Edit headers into the text editor instead than separate fields.
|
||||||
|
#
|
||||||
|
# When this is true, address-book-cmd is not supported and address completion
|
||||||
|
# is left to the editor itself. Also, displaying multiple headers on the same
|
||||||
|
# line is not possible.
|
||||||
|
#
|
||||||
|
# Default: false
|
||||||
|
edit-headers=true
|
||||||
|
|
||||||
|
#
|
||||||
|
# Sets focus to the email body when the composer window opens.
|
||||||
|
#
|
||||||
|
# Default: false
|
||||||
|
#focus-body=false
|
||||||
|
|
||||||
|
#
|
||||||
|
# Specifies the command to be used to tab-complete email addresses. Any
|
||||||
|
# occurrence of "%s" in the address-book-cmd will be replaced with what the
|
||||||
|
# user has typed so far.
|
||||||
|
#
|
||||||
|
# The command must output the completions to standard output, one completion
|
||||||
|
# per line. Each line must be tab-delimited, with an email address occurring as
|
||||||
|
# the first field. Only the email address field is required. The second field,
|
||||||
|
# if present, will be treated as the contact name. Additional fields are
|
||||||
|
# ignored.
|
||||||
|
#
|
||||||
|
# This parameter can also be set per account in accounts.conf.
|
||||||
|
#address-book-cmd=
|
||||||
|
|
||||||
|
# Specifies the command to be used to select attachments. Any occurrence of
|
||||||
|
# '%s' in the file-picker-cmd will be replaced with the argument <arg>
|
||||||
|
# to :attach -m <arg>. Any occurrence of '%f' will be replaced by the
|
||||||
|
# location of a temporary file, from which aerc will read the selected files.
|
||||||
|
#
|
||||||
|
# If '%f' is not present, the command must output the selected files to
|
||||||
|
# standard output, one file per line. If it is present, then aerc does not
|
||||||
|
# capture the standard output and instead reads the files from the temporary
|
||||||
|
# file which should have the same format.
|
||||||
|
#file-picker-cmd=/home/liph/.local/bin/yazi-picker %f
|
||||||
|
|
||||||
|
#
|
||||||
|
# Allow to address yourself when replying
|
||||||
|
#
|
||||||
|
# Default: true
|
||||||
|
#reply-to-self=true
|
||||||
|
|
||||||
|
# Warn before sending an email with an empty subject.
|
||||||
|
#
|
||||||
|
# Default: false
|
||||||
|
#empty-subject-warning=false
|
||||||
|
|
||||||
|
#
|
||||||
|
# Warn before sending an email that matches the specified regexp but does not
|
||||||
|
# have any attachments. Leave empty to disable this feature.
|
||||||
|
#
|
||||||
|
# Uses Go's regexp syntax, documented at https://golang.org/s/re2syntax. The
|
||||||
|
# "(?im)" flags are set by default (case-insensitive and multi-line).
|
||||||
|
#
|
||||||
|
# Example:
|
||||||
|
# no-attachment-warning=^[^>]*attach(ed|ment)
|
||||||
|
#
|
||||||
|
#no-attachment-warning=
|
||||||
|
|
||||||
|
#
|
||||||
|
# When set, aerc will generate "format=flowed" bodies with a content type of
|
||||||
|
# "text/plain; format=flowed" as described in RFC3676. This format is easier to
|
||||||
|
# handle for some mailing software, and generally just looks like ordinary
|
||||||
|
# text. To actually make use of this format's features, you'll need support in
|
||||||
|
# your editor.
|
||||||
|
#
|
||||||
|
format-flowed=false
|
||||||
|
|
||||||
|
[multipart-converters]
|
||||||
|
text/html=/home/liph/.local/bin/aerc-md-convert
|
||||||
|
|
||||||
|
[filters]
|
||||||
|
#
|
||||||
|
text/plain=sed 's/^$//'
|
||||||
|
|
||||||
|
# Filters allow you to pipe an email body through a shell command to render
|
||||||
|
# certain emails differently, e.g. highlighting them with ANSI escape codes.
|
||||||
|
#
|
||||||
|
# The commands are invoked with sh -c. The following folders are prepended to
|
||||||
|
# the system $PATH to allow referencing filters from their name only:
|
||||||
|
#
|
||||||
|
# ${XDG_CONFIG_HOME:-~/.config}/aerc/filters
|
||||||
|
# ~/.local/libexec/aerc/filters
|
||||||
|
# ${XDG_DATA_HOME:-~/.local/share}/aerc/filters
|
||||||
|
# $PREFIX/libexec/aerc/filters
|
||||||
|
# $PREFIX/share/aerc/filters
|
||||||
|
# /usr/libexec/aerc/filters
|
||||||
|
# /usr/share/aerc/filters
|
||||||
|
#
|
||||||
|
# If you want to run a program in your default $PATH which has the same name
|
||||||
|
# as a builtin filter (e.g. /usr/bin/colorize), use its absolute path.
|
||||||
|
#
|
||||||
|
# The following variables are defined in the filter command environment:
|
||||||
|
#
|
||||||
|
# AERC_MIME_TYPE the part MIME type/subtype
|
||||||
|
# AERC_FORMAT the part content type format= parameter
|
||||||
|
# AERC_FILENAME the attachment filename (if any)
|
||||||
|
# AERC_SUBJECT the message Subject header value
|
||||||
|
# AERC_FROM the message From header value
|
||||||
|
#
|
||||||
|
# The first filter which matches the email's mimetype will be used, so order
|
||||||
|
# them from most to least specific.
|
||||||
|
#
|
||||||
|
# You can also match on non-mimetypes, by prefixing with the header to match
|
||||||
|
# against (non-case-sensitive) and a comma, e.g. subject,text will match a
|
||||||
|
# subject which contains "text". Use header,~regex to match against a regex.
|
||||||
|
#
|
||||||
|
text/plain=colorize
|
||||||
|
text/calendar=calendar
|
||||||
|
message/delivery-status=colorize
|
||||||
|
message/rfc822=colorize
|
||||||
|
text/html=w3m -T text/html -dump -o display_link_number=1
|
||||||
|
#text/*=bat -fP --file-name="$AERC_FILENAME"
|
||||||
|
#application/x-sh=bat -fP -l sh
|
||||||
|
#image/*=catimg -w $(tput cols) -
|
||||||
|
#subject,~Git(hub|lab)=lolcat -f
|
||||||
|
#from,thatguywhodoesnothardwraphismessages=wrap -w 100 | colorize
|
||||||
|
|
||||||
|
# This special filter is only used to post-process email headers when
|
||||||
|
# [viewer].show-headers=true
|
||||||
|
# By default, headers are piped directly into the pager.
|
||||||
|
#
|
||||||
|
.headers=colorize
|
||||||
|
|
||||||
|
[openers]
|
||||||
|
#
|
||||||
|
# Openers allow you to specify the command to use for the :open and :open-link
|
||||||
|
# actions on a per-MIME-type basis. The :open-link URL scheme is used to
|
||||||
|
# determine the MIME type as follows: x-scheme-handler/<scheme>.
|
||||||
|
#
|
||||||
|
# {} is expanded as the temporary filename or URL to be opened with proper
|
||||||
|
# shell quoting. If it is not encountered in the command, the filename/URL will
|
||||||
|
# be appended to the end of the command. The command will then be executed with
|
||||||
|
# `sh -c`.
|
||||||
|
#
|
||||||
|
# Like [filters], openers support basic shell globbing. The first opener which
|
||||||
|
# matches the part's MIME type (or URL scheme handler MIME type) will be used,
|
||||||
|
# so order them from most to least specific.
|
||||||
|
#
|
||||||
|
# Examples:
|
||||||
|
# x-scheme-handler/irc=hexchat
|
||||||
|
# x-scheme-handler/http*=printf '%s' {} | wl-copy
|
||||||
|
# text/html=surf -dfgms
|
||||||
|
# text/plain=gvim {} +125
|
||||||
|
# message/rfc822=thunderbird
|
||||||
|
text/html=w3m
|
||||||
|
x-scheme-handler/http=librewolf
|
||||||
|
x-scheme-handler/https=librewolf
|
||||||
|
text/calendar=khal import --batch
|
||||||
|
|
||||||
|
[hooks]
|
||||||
|
#
|
||||||
|
# Hooks are triggered whenever the associated event occurs.
|
||||||
|
|
||||||
|
#
|
||||||
|
# Executed when a new email arrives in the selected folder
|
||||||
|
#mail-received=notify-send "[$AERC_ACCOUNT/$AERC_FOLDER] New mail from $AERC_FROM_NAME" "$AERC_SUBJECT"
|
||||||
|
|
||||||
|
#
|
||||||
|
# Executed when mail is deleted from a folder
|
||||||
|
#mail-deleted=mbsync "$AERC_ACCOUNT:$AERC_FOLDER" &
|
||||||
|
|
||||||
|
#
|
||||||
|
# Executed when aerc adds mail to a folder
|
||||||
|
#mail-added=mbsync "$AERC_ACCOUNT:$AERC_FOLDER" &
|
||||||
|
|
||||||
|
#
|
||||||
|
# Executed when aerc starts
|
||||||
|
#aerc-startup=aerc :terminal calcurse && aerc :next-tab
|
||||||
|
|
||||||
|
#
|
||||||
|
# Executed when aerc shuts down.
|
||||||
|
#aerc-shutdown=
|
||||||
|
|
||||||
|
#
|
||||||
|
# Executed when notmuch tags are modified.
|
||||||
|
#tag-modified=
|
||||||
|
|
||||||
|
#
|
||||||
|
# Executed when flags are changed on a message.
|
||||||
|
#flag-changed=mbsync "$AERC_ACCOUNT:$AERC_FOLDER" &
|
||||||
|
|
||||||
|
[templates]
|
||||||
|
# Templates are used to populate email bodies automatically.
|
||||||
|
|
||||||
|
###
|
||||||
|
new-message=new-msg
|
||||||
|
quoted-reply=reply-quoted
|
||||||
|
|
||||||
|
|
||||||
|
# The directories where the templates are stored. It takes a colon-separated
|
||||||
|
# list of directories. If this is unset or if a template cannot be found, the
|
||||||
|
# following paths will be used as a fallback in that order:
|
||||||
|
#
|
||||||
|
# ${XDG_CONFIG_HOME:-~/.config}/aerc/templates
|
||||||
|
# ${XDG_DATA_HOME:-~/.local/share}/aerc/templates
|
||||||
|
# /usr/local/share/aerc/templates
|
||||||
|
# /usr/share/aerc/templates
|
||||||
|
#
|
||||||
|
template-dirs=${XDG_CONFIG_HOME:-~/.config}/aerc/templates
|
||||||
|
|
||||||
|
# The default template to be used for new messages.
|
||||||
|
#
|
||||||
|
# default: new_message
|
||||||
|
#new-message=new_message
|
||||||
|
|
||||||
|
# The default template to be used for quoted replies.
|
||||||
|
#
|
||||||
|
# default: quoted_reply
|
||||||
|
#quoted-reply=quoted_reply
|
||||||
|
|
||||||
|
# The default template to be used for forward as body.
|
||||||
|
#
|
||||||
|
# default: forward_as_body
|
||||||
|
#forwards=forward_as_body
|
||||||
|
|
||||||
|
[terminal]
|
||||||
|
passthrough=false
|
||||||
@@ -0,0 +1,891 @@
|
|||||||
|
#
|
||||||
|
# aerc main configuration
|
||||||
|
|
||||||
|
[general]
|
||||||
|
# Used as a default path for save operations if no other path is specified.
|
||||||
|
# ~ is expanded to the current user home dir.
|
||||||
|
#
|
||||||
|
default-save-path=~/Downloads
|
||||||
|
|
||||||
|
# If set to "gpg", aerc will use system gpg binary and keystore for all crypto
|
||||||
|
# operations. If set to "internal", the internal openpgp keyring will be used.
|
||||||
|
# If set to "auto", the system gpg will be preferred unless the internal
|
||||||
|
# keyring already exists, in which case the latter will be used.
|
||||||
|
#
|
||||||
|
# Default: auto
|
||||||
|
#pgp-provider=auto
|
||||||
|
|
||||||
|
# By default, the file permissions of accounts.conf must be restrictive and
|
||||||
|
# only allow reading by the file owner (0600). Set this option to true to
|
||||||
|
# ignore this permission check. Use this with care as it may expose your
|
||||||
|
# credentials.
|
||||||
|
#
|
||||||
|
# Default: false
|
||||||
|
unsafe-accounts-conf=false
|
||||||
|
index-format=notmuch://~/.local/share/mail
|
||||||
|
# Output log messages to specified file. A path starting with ~/ is expanded to
|
||||||
|
# the user home dir. When redirecting aerc's output to a file using > shell
|
||||||
|
# redirection, this setting is ignored and log messages are printed to stdout.
|
||||||
|
#
|
||||||
|
log-file=~/.local/share/aerc/aerc.log
|
||||||
|
log-level=warn
|
||||||
|
|
||||||
|
|
||||||
|
# Disable IPC entirely. Don't run commands (including mailto:... and mbox:...)
|
||||||
|
# in an existing aerc instance, and don't start an IPC server to allow
|
||||||
|
# subsequent aerc instances to run commands in the current one.
|
||||||
|
#
|
||||||
|
# Default: false
|
||||||
|
#disable-ipc=false
|
||||||
|
|
||||||
|
# Don't run mailto:... commands over IPC; start a new aerc instance with the
|
||||||
|
# composer instead.
|
||||||
|
#
|
||||||
|
# Default: false
|
||||||
|
#disable-ipc-mailto=false
|
||||||
|
#
|
||||||
|
# Don't run mbox:... commands over IPC; start a new aerc instance with the mbox
|
||||||
|
# file instead.
|
||||||
|
#
|
||||||
|
# Default: false
|
||||||
|
#disable-ipc-mbox=false
|
||||||
|
|
||||||
|
# Set the $TERM environment variable used for the embedded terminal.
|
||||||
|
#
|
||||||
|
# Default: xterm-256color
|
||||||
|
#term=xterm-256color
|
||||||
|
|
||||||
|
# Display OSC8 strings in the embedded terminal
|
||||||
|
#
|
||||||
|
# Default: false
|
||||||
|
#enable-osc8=false
|
||||||
|
|
||||||
|
# Default shell command to use for :menu. This will be executed with sh -c and
|
||||||
|
# will run in an popover dialog.
|
||||||
|
#
|
||||||
|
# Any occurrence of %f will be replaced by a temporary file path where the
|
||||||
|
# command is expected to write output lines to be consumed by :menu. Otherwise,
|
||||||
|
# the lines will be read from the command's standard output.
|
||||||
|
#
|
||||||
|
# Examples:
|
||||||
|
# default-menu-cmd=fzf
|
||||||
|
# default-menu-cmd=fzf --multi
|
||||||
|
# default-menu-cmd=dmenu -l 20
|
||||||
|
# default-menu-cmd=ranger --choosefiles=%f
|
||||||
|
#
|
||||||
|
#default-menu-cmd=
|
||||||
|
|
||||||
|
[ui]
|
||||||
|
#
|
||||||
|
# Describes the format for each row in a mailbox view. This is a comma
|
||||||
|
# separated list of column names with an optional align and width suffix. After
|
||||||
|
# the column name, one of the '<' (left), ':' (center) or '>' (right) alignment
|
||||||
|
# characters can be added (by default, left) followed by an optional width
|
||||||
|
# specifier. The width is either an integer representing a fixed number of
|
||||||
|
# characters, or a percentage between 1% and 99% representing a fraction of the
|
||||||
|
# terminal width. It can also be one of the '*' (auto) or '=' (fit) special
|
||||||
|
# width specifiers. Auto width columns will be equally attributed the remaining
|
||||||
|
# terminal width. Fit width columns take the width of their contents. If no
|
||||||
|
# width specifier is set, '*' is used by default.
|
||||||
|
#
|
||||||
|
# Default: flags:4,name<20%,subject,date>=
|
||||||
|
index-columns=flags:4,name<35%,subject,date>=
|
||||||
|
column-name={{if match .Folder "(?i)Sent|Gesendet"}}{{index (.To | persons) 0}}{{else}}{{index (.From | persons) 0}}{{end}}
|
||||||
|
border-char-vertical="│"
|
||||||
|
border-char-horizontal="─"
|
||||||
|
styleset-name="rose-pine-moon"
|
||||||
|
threading-enabled=true
|
||||||
|
|
||||||
|
#
|
||||||
|
# Each name in index-columns must have a corresponding column-$name setting.
|
||||||
|
# All column-$name settings accept golang text/template syntax. See
|
||||||
|
# aerc-templates(7) for available template attributes and functions.
|
||||||
|
#
|
||||||
|
# Here are some examples to show the To field instead of the From field for
|
||||||
|
# an email (modifying column-name):
|
||||||
|
#
|
||||||
|
# 1. a generic one
|
||||||
|
# column-name={{ .Peer | names | join ", " }}
|
||||||
|
# 2. based upon the selected folder
|
||||||
|
# column-name={{if match .Folder "^(Gesendet|Sent)$"}}{{index (.To | names) 0}}{{else}}{{index (.From | names) 0}}{{end}}
|
||||||
|
#
|
||||||
|
# Default settings
|
||||||
|
#column-flags={{.Flags | join ""}}
|
||||||
|
#column-name={{index (.From | names) 0}}
|
||||||
|
#column-subject={{.ThreadPrefix}}{{.Subject}}
|
||||||
|
#column-date={{.DateAutoFormat .Date.Local}}
|
||||||
|
|
||||||
|
#
|
||||||
|
# String separator inserted between columns. When the column width specifier is
|
||||||
|
# an exact number of characters, the separator is added to it (i.e. the exact
|
||||||
|
# width will be fully available for the column contents).
|
||||||
|
#
|
||||||
|
# Default: " "
|
||||||
|
#column-separator=" "
|
||||||
|
|
||||||
|
#
|
||||||
|
# See time.Time#Format at https://godoc.org/time#Time.Format
|
||||||
|
#
|
||||||
|
# Default: 2006 Jan 02
|
||||||
|
#timestamp-format=2006 Jan 02
|
||||||
|
|
||||||
|
#
|
||||||
|
# Index-only time format for messages that were received/sent today.
|
||||||
|
# If this is empty, timestamp-format is used instead.
|
||||||
|
#
|
||||||
|
# Default: 15:04
|
||||||
|
#this-day-time-format=15:04
|
||||||
|
|
||||||
|
#
|
||||||
|
# Index-only time format for messages that were received/sent within the last
|
||||||
|
# 7 days. If this is empty, timestamp-format is used instead.
|
||||||
|
#
|
||||||
|
# Default: Jan 02
|
||||||
|
#this-week-time-format=Jan 02
|
||||||
|
|
||||||
|
#
|
||||||
|
# Index-only time format for messages that were received/sent this year.
|
||||||
|
# If this is empty, timestamp-format is used instead.
|
||||||
|
#
|
||||||
|
#Default: Jan 02
|
||||||
|
#this-year-time-format=Jan 02
|
||||||
|
|
||||||
|
#
|
||||||
|
# Overrides timestamp-format for the message view.
|
||||||
|
#
|
||||||
|
# Default: 2006 Jan 02, 15:04 GMT-0700
|
||||||
|
#message-view-timestamp-format=2006 Jan 02, 15:04 GMT-0700
|
||||||
|
|
||||||
|
#
|
||||||
|
# If set, overrides timestamp-format in the message view for messages
|
||||||
|
# that were received/sent today.
|
||||||
|
#
|
||||||
|
#message-view-this-day-time-format=
|
||||||
|
|
||||||
|
# If set, overrides timestamp-format in the message view for messages
|
||||||
|
# that were received/sent within the last 7 days.
|
||||||
|
#
|
||||||
|
#message-view-this-week-time-format=
|
||||||
|
|
||||||
|
#
|
||||||
|
# If set, overrides *timestamp-format* in the message view for messages
|
||||||
|
# that were received/sent this year.
|
||||||
|
#
|
||||||
|
#message-view-this-year-time-format=
|
||||||
|
|
||||||
|
#
|
||||||
|
# Width of the sidebar, including the border.
|
||||||
|
#
|
||||||
|
# Default: 22
|
||||||
|
sidebar-width=34
|
||||||
|
|
||||||
|
#
|
||||||
|
# Default split layout for message list tabs. The syntax is:
|
||||||
|
#
|
||||||
|
# [<direction>] <size>
|
||||||
|
#
|
||||||
|
# <direction> is optional and defaults to horizontal. It can take one
|
||||||
|
# of the following values: h, horiz, horizontal, v, vert, vertical.
|
||||||
|
#
|
||||||
|
# <size> is a positive integer representing the size (in terminal cells)
|
||||||
|
# of the message list window.
|
||||||
|
#
|
||||||
|
#message-list-split=
|
||||||
|
|
||||||
|
#
|
||||||
|
# Message to display when viewing an empty folder.
|
||||||
|
#
|
||||||
|
# Default: (no messages)
|
||||||
|
#empty-message=(no messages)
|
||||||
|
|
||||||
|
# Message to display when no folders exists or are all filtered
|
||||||
|
#
|
||||||
|
# Default: (no folders)
|
||||||
|
#empty-dirlist=(no folders)
|
||||||
|
#
|
||||||
|
# Value to set {{.Subject}} template to when subject is empty.
|
||||||
|
#
|
||||||
|
# Default: (no subject)
|
||||||
|
#empty-subject=(no subject)
|
||||||
|
|
||||||
|
# Enable mouse events in the ui, e.g. clicking and scrolling with the mousewheel
|
||||||
|
#
|
||||||
|
# Default: false
|
||||||
|
mouse-enabled=false
|
||||||
|
|
||||||
|
#
|
||||||
|
# Ring the bell when new messages are received
|
||||||
|
#
|
||||||
|
# Default: true
|
||||||
|
#new-message-bell=true
|
||||||
|
|
||||||
|
#
|
||||||
|
# Template to use for Account tab titles
|
||||||
|
#
|
||||||
|
# Default: {{.Account}}
|
||||||
|
#tab-title-account={{.Account}}
|
||||||
|
|
||||||
|
#
|
||||||
|
# Template to use for Composer tab titles
|
||||||
|
#
|
||||||
|
# Default: {{if .To}}to:{{index (.To | shortmboxes) 0}} {{end}}{{.SubjectBase}}
|
||||||
|
#tab-title-composer={{if .To}}to:{{index (.To | shortmboxes) 0}} {{end}}{{.SubjectBase}}
|
||||||
|
|
||||||
|
#
|
||||||
|
# Template to use for Message Viewer tab titles
|
||||||
|
#
|
||||||
|
# Default: {{.Subject}}
|
||||||
|
#tab-title-viewer={{.Subject}}
|
||||||
|
|
||||||
|
|
||||||
|
# Marker to show before a pinned tab's name.
|
||||||
|
#
|
||||||
|
# Default: `
|
||||||
|
#pinned-tab-marker='`'
|
||||||
|
|
||||||
|
# Template for the left side of the directory list.
|
||||||
|
# See aerc-templates(7) for all available fields and functions.
|
||||||
|
#
|
||||||
|
# Default: {{.Folder}}
|
||||||
|
dirlist-left={{if match .Folder "/"}}{{.Style (index (.Folder | split "/" | tail 1) 0) "folder_sub"}}{{else}}{{.Folder}}{{end}}
|
||||||
|
|
||||||
|
# Template for the right side of the directory list.
|
||||||
|
# See aerc-templates(7) for all available fields and functions.
|
||||||
|
#
|
||||||
|
# Default: {{if .Unread}}{{humanReadable .Unread}}{{end}}
|
||||||
|
dirlist-right={{if .Unread}}{{humanReadable .Unread}} / {{end}}{{humanReadable .Exists}}
|
||||||
|
|
||||||
|
# Delay after which the messages are actually listed when entering a directory.
|
||||||
|
# This avoids loading messages when skipping over folders and makes the UI more
|
||||||
|
# responsive. If you do not want that, set it to 0s.
|
||||||
|
#
|
||||||
|
# Default: 200ms
|
||||||
|
#dirlist-delay=200ms
|
||||||
|
|
||||||
|
# Display the directory list as a foldable tree that allows to collapse and
|
||||||
|
# expand the folders.
|
||||||
|
#
|
||||||
|
# Default: false
|
||||||
|
dirlist-tree=true
|
||||||
|
|
||||||
|
# If dirlist-tree is enabled, set level at which folders are collapsed by
|
||||||
|
# default. Set to 0 to disable.
|
||||||
|
#
|
||||||
|
# Default: 0
|
||||||
|
dirlist-collapse=1
|
||||||
|
|
||||||
|
# List of space-separated criteria to sort the messages by, see *sort*
|
||||||
|
# command in *aerc*(1) for reference. Prefixing a criterion with "-r "
|
||||||
|
# reverses that criterion.
|
||||||
|
#
|
||||||
|
# Example: "from -r date"
|
||||||
|
#
|
||||||
|
sort = -r date
|
||||||
|
|
||||||
|
# Moves to next message when the current message is deleted
|
||||||
|
#
|
||||||
|
# Default: true
|
||||||
|
#next-message-on-delete=true
|
||||||
|
|
||||||
|
# Automatically set the "seen" flag when a message is opened in the message
|
||||||
|
# viewer.
|
||||||
|
#
|
||||||
|
# Default: true
|
||||||
|
#auto-mark-read=true
|
||||||
|
|
||||||
|
# The directories where the stylesets are stored. It takes a colon-separated
|
||||||
|
# list of directories. If this is unset or if a styleset cannot be found, the
|
||||||
|
# following paths will be used as a fallback in that order:
|
||||||
|
#
|
||||||
|
# ${XDG_CONFIG_HOME:-~/.config}/aerc/stylesets
|
||||||
|
# ${XDG_DATA_HOME:-~/.local/share}/aerc/stylesets
|
||||||
|
# /usr/local/share/aerc/stylesets
|
||||||
|
# /usr/share/aerc/stylesets
|
||||||
|
#
|
||||||
|
#stylesets-dirs=
|
||||||
|
|
||||||
|
# Uncomment to use box-drawing characters for vertical and horizontal borders.
|
||||||
|
#
|
||||||
|
# Default: "│" and "─"
|
||||||
|
#border-char-vertical="│"
|
||||||
|
#border-char-horizontal="─"
|
||||||
|
|
||||||
|
# Sets the styleset to use for the aerc ui elements.
|
||||||
|
#
|
||||||
|
# Default: default
|
||||||
|
#styleset-name=default
|
||||||
|
|
||||||
|
# Activates fuzzy search in commands and their arguments: the typed string is
|
||||||
|
# searched in the command or option in any position, and need not be
|
||||||
|
# consecutive characters in the command or option.
|
||||||
|
#
|
||||||
|
# Default: false
|
||||||
|
#fuzzy-complete=false
|
||||||
|
|
||||||
|
# How long to wait after the last input before auto-completion is triggered.
|
||||||
|
#
|
||||||
|
# Default: 250ms
|
||||||
|
#completion-delay=250ms
|
||||||
|
|
||||||
|
# The minimum required characters to allow auto-completion to be triggered after
|
||||||
|
# completion-delay.
|
||||||
|
#
|
||||||
|
# Setting this to "manual" disables automatic completion, leaving only the
|
||||||
|
# manually triggered completion with the $complete key (see aerc-binds(5) for
|
||||||
|
# more details).
|
||||||
|
#
|
||||||
|
# Default: 1
|
||||||
|
#completion-min-chars=1
|
||||||
|
|
||||||
|
#
|
||||||
|
# Global switch for completion popovers
|
||||||
|
#
|
||||||
|
# Default: true
|
||||||
|
#completion-popovers=true
|
||||||
|
|
||||||
|
# Uncomment to use UTF-8 symbols to indicate PGP status of messages
|
||||||
|
#
|
||||||
|
# Default: ASCII
|
||||||
|
#icon-unencrypted=
|
||||||
|
#icon-encrypted=✔
|
||||||
|
#icon-signed=✔
|
||||||
|
#icon-signed-encrypted=✔
|
||||||
|
#icon-unknown=✘
|
||||||
|
#icon-invalid=⚠
|
||||||
|
|
||||||
|
# Reverses the order of the message list. By default, the message list is
|
||||||
|
# ordered with the newest (highest UID) message on top. Reversing the order
|
||||||
|
# will put the oldest (lowest UID) message on top. This can be useful in cases
|
||||||
|
# where the backend does not support sorting.
|
||||||
|
#
|
||||||
|
# Default: false
|
||||||
|
#reverse-msglist-order = false
|
||||||
|
|
||||||
|
# Reverse display of the message threads. Default order is the initial
|
||||||
|
# message is on the top with all the replies being displayed below. The
|
||||||
|
# reverse option will put the initial message at the bottom with the
|
||||||
|
# replies on top.
|
||||||
|
#
|
||||||
|
# Default: false
|
||||||
|
#reverse-thread-order=false
|
||||||
|
|
||||||
|
# Positions the cursor on the last message in the message list (at the
|
||||||
|
# bottom of the view) when opening a new folder.
|
||||||
|
#
|
||||||
|
# Default: false
|
||||||
|
#select-last-message=false
|
||||||
|
|
||||||
|
# Sort the thread siblings according to the sort criteria for the messages. If
|
||||||
|
# sort-thread-siblings is false, the thread siblings will be sorted based on
|
||||||
|
# the message UID in ascending order. This option is only applicable for
|
||||||
|
# client-side threading with a backend that enables sorting. Note that there's
|
||||||
|
# a performance impact when sorting is activated.
|
||||||
|
#
|
||||||
|
# Default: false
|
||||||
|
#sort-thread-siblings=false
|
||||||
|
|
||||||
|
# Set the scroll offset in number of lines from the top and bottom of the
|
||||||
|
# message list.
|
||||||
|
#
|
||||||
|
# Default: 0
|
||||||
|
#msglist-scroll-offset = 0
|
||||||
|
|
||||||
|
#
|
||||||
|
# Enable a threaded view of messages. If this is not supported by the backend
|
||||||
|
# (IMAP server or notmuch), threads will be built by the client.
|
||||||
|
#
|
||||||
|
# Default: false
|
||||||
|
#threading-enabled=false
|
||||||
|
|
||||||
|
# Force client-side thread building
|
||||||
|
#
|
||||||
|
# Default: false
|
||||||
|
#force-client-threads=false
|
||||||
|
|
||||||
|
# If no References nor In-Reply-To headers can be matched to build client side
|
||||||
|
# threads, fallback to similar subjects.
|
||||||
|
#
|
||||||
|
# Default: false
|
||||||
|
#threading-by-subject=false
|
||||||
|
|
||||||
|
# Show thread context enables messages which do not match the current query (or
|
||||||
|
# belong to the current mailbox) to be shown for context. These messages can be
|
||||||
|
# styled separately using "msglist_thread_context" in a styleset. This feature
|
||||||
|
# is not supported by all backends
|
||||||
|
#
|
||||||
|
# Default: false
|
||||||
|
#show-thread-context=false
|
||||||
|
|
||||||
|
# Debounce client-side thread building
|
||||||
|
#
|
||||||
|
# Default: 50ms
|
||||||
|
#client-threads-delay=50ms
|
||||||
|
|
||||||
|
#
|
||||||
|
# Thread prefix customization:
|
||||||
|
|
||||||
|
#
|
||||||
|
# Customize the thread prefix appearance by selecting the arrow head.
|
||||||
|
#
|
||||||
|
# Default: ">"
|
||||||
|
#thread-prefix-tip = ">"
|
||||||
|
|
||||||
|
#
|
||||||
|
# Customize the thread prefix appearance by selecting the arrow indentation.
|
||||||
|
#
|
||||||
|
# Default: " "
|
||||||
|
#thread-prefix-indent = " "
|
||||||
|
|
||||||
|
#
|
||||||
|
# Customize the thread prefix appearance by selecting the vertical extension of
|
||||||
|
# the arrow.
|
||||||
|
#
|
||||||
|
# Default: "│"
|
||||||
|
#thread-prefix-stem = "│"
|
||||||
|
|
||||||
|
#
|
||||||
|
# Customize the thread prefix appearance by selecting the horizontal extension
|
||||||
|
# of the arrow.
|
||||||
|
#
|
||||||
|
# Default: ""
|
||||||
|
#thread-prefix-limb = ""
|
||||||
|
|
||||||
|
#
|
||||||
|
# Customize the thread prefix appearance by selecting the folded thread
|
||||||
|
# indicator.
|
||||||
|
#
|
||||||
|
# Default: "+"
|
||||||
|
#thread-prefix-folded = "+"
|
||||||
|
|
||||||
|
#
|
||||||
|
# Customize the thread prefix appearance by selecting the unfolded thread
|
||||||
|
# indicator.
|
||||||
|
#
|
||||||
|
# Default: ""
|
||||||
|
#thread-prefix-unfolded = ""
|
||||||
|
|
||||||
|
#
|
||||||
|
# Customize the thread prefix appearance by selecting the first child connector.
|
||||||
|
#
|
||||||
|
# Default: ""
|
||||||
|
#thread-prefix-first-child = ""
|
||||||
|
|
||||||
|
#
|
||||||
|
# Customize the thread prefix appearance by selecting the connector used if
|
||||||
|
# the message has siblings.
|
||||||
|
#
|
||||||
|
# Default: "├─"
|
||||||
|
#thread-prefix-has-siblings = "├─"
|
||||||
|
|
||||||
|
#
|
||||||
|
# Customize the thread prefix appearance by selecting the connector used if the
|
||||||
|
# message has no parents and no children.
|
||||||
|
#
|
||||||
|
# Default: ""
|
||||||
|
#thread-prefix-lone = ""
|
||||||
|
|
||||||
|
#
|
||||||
|
# Customize the thread prefix appearance by selecting the connector used if the
|
||||||
|
# message has no parents and has children.
|
||||||
|
#
|
||||||
|
# Default: ""
|
||||||
|
#thread-prefix-orphan = ""
|
||||||
|
|
||||||
|
#
|
||||||
|
# Customize the thread prefix appearance by selecting the connector for the last
|
||||||
|
# sibling.
|
||||||
|
#
|
||||||
|
# Default: "└─"
|
||||||
|
#thread-prefix-last-sibling = "└─"
|
||||||
|
|
||||||
|
#
|
||||||
|
# Customize the reversed thread prefix appearance by selecting the connector for
|
||||||
|
# the last sibling.
|
||||||
|
#
|
||||||
|
# Default: "┌─"
|
||||||
|
#thread-prefix-last-sibling-reverse = "┌─"
|
||||||
|
|
||||||
|
#
|
||||||
|
# Customize the thread prefix appearance by selecting the connector for dummy
|
||||||
|
# thread.
|
||||||
|
#
|
||||||
|
# Default: "┬─"
|
||||||
|
#thread-prefix-dummy = "┬─"
|
||||||
|
|
||||||
|
#
|
||||||
|
# Customize the reversed thread prefix appearance by selecting the connector for
|
||||||
|
# dummy thread.
|
||||||
|
#
|
||||||
|
# Default: "┴─"
|
||||||
|
#thread-prefix-dummy-reverse = "┴─"
|
||||||
|
|
||||||
|
#
|
||||||
|
# Customize the reversed thread prefix appearance by selecting the first child
|
||||||
|
# connector.
|
||||||
|
#
|
||||||
|
# Default: ""
|
||||||
|
#thread-prefix-first-child-reverse = ""
|
||||||
|
|
||||||
|
#
|
||||||
|
# Customize the reversed thread prefix appearance by selecting the connector
|
||||||
|
# used if the message has no parents and has children.
|
||||||
|
#
|
||||||
|
# Default: ""
|
||||||
|
#thread-prefix-orphan-reverse = ""
|
||||||
|
|
||||||
|
[statusline]
|
||||||
|
#
|
||||||
|
# Describes the format for the status line. This is a comma separated list of
|
||||||
|
# column names with an optional align and width suffix. See [ui].index-columns
|
||||||
|
# for more details. To completely mute the status line except for push
|
||||||
|
# notifications, explicitly set status-columns to an empty string.
|
||||||
|
#
|
||||||
|
# Default: left<*,center:=,right>*
|
||||||
|
#status-columns=left<*,center:=,right>*
|
||||||
|
|
||||||
|
#
|
||||||
|
# Each name in status-columns must have a corresponding column-$name setting.
|
||||||
|
# All column-$name settings accept golang text/template syntax. See
|
||||||
|
# aerc-templates(7) for available template attributes and functions.
|
||||||
|
#
|
||||||
|
# Default settings
|
||||||
|
#column-left=[{{.Account}}] {{.StatusInfo}}
|
||||||
|
#column-center={{.PendingKeys}}
|
||||||
|
#column-right={{.TrayInfo}} | {{cwd}}
|
||||||
|
|
||||||
|
#
|
||||||
|
# String separator inserted between columns.
|
||||||
|
# See [ui].column-separator for more details.
|
||||||
|
#
|
||||||
|
#column-separator=" "
|
||||||
|
|
||||||
|
# Specifies the separator between grouped statusline elements.
|
||||||
|
#
|
||||||
|
# Default: " | "
|
||||||
|
#separator=" | "
|
||||||
|
|
||||||
|
# Defines the mode for displaying the status elements.
|
||||||
|
# Options: text, icon
|
||||||
|
#
|
||||||
|
# Default: text
|
||||||
|
#display-mode=text
|
||||||
|
|
||||||
|
[viewer]
|
||||||
|
# pager=less -Rc
|
||||||
|
pager= bat --style=plain --paging=always
|
||||||
|
# html-unsafe-images=false
|
||||||
|
# html-filter=dante
|
||||||
|
image/*=chafa -f kitty -s ${width}x${height}
|
||||||
|
text/html=w3m -T text/html -dump -o display_link_number=1
|
||||||
|
application/pdf=zathura
|
||||||
|
video/*=mpv
|
||||||
|
header-layout=From,To,Subject,Date
|
||||||
|
|
||||||
|
|
||||||
|
#w3m -dump -T text/html -o display_link_number=1
|
||||||
|
#
|
||||||
|
# Specifies the pager to use when displaying emails. Note that some filters
|
||||||
|
# may add ANSI codes to add color to rendered emails, so you may want to use a
|
||||||
|
# pager which supports ANSI codes.
|
||||||
|
#
|
||||||
|
# Default: less -Rc
|
||||||
|
|
||||||
|
#
|
||||||
|
# If an email offers several versions (multipart), you can configure which
|
||||||
|
# mimetype to prefer. For example, this can be used to prefer plaintext over
|
||||||
|
# html emails.
|
||||||
|
#
|
||||||
|
# Default: text/plain,text/html
|
||||||
|
#alternatives=text/plain,text/html
|
||||||
|
|
||||||
|
#
|
||||||
|
# Default setting to determine whether to show full headers or only parsed
|
||||||
|
# ones in message viewer.
|
||||||
|
#
|
||||||
|
# Default: false
|
||||||
|
#show-headers=false
|
||||||
|
|
||||||
|
#
|
||||||
|
# Layout of headers when viewing a message. To display multiple headers in the
|
||||||
|
# same row, separate them with a pipe, e.g. "From|To". Rows will be hidden if
|
||||||
|
# none of their specified headers are present in the message.
|
||||||
|
#
|
||||||
|
# Default: From|To,Cc|Bcc,Date,Subject
|
||||||
|
#header-layout=From|To,Cc|Bcc,Date,Subject
|
||||||
|
|
||||||
|
# Whether to always show the mimetype of an email, even when it is just a single part
|
||||||
|
#
|
||||||
|
# Default: false
|
||||||
|
#always-show-mime=false
|
||||||
|
|
||||||
|
# Define the maximum height of the mimetype switcher before a scrollbar is used.
|
||||||
|
# The height of the mimetype switcher is restricted to half of the display
|
||||||
|
# height. If the provided value for the height is zero, the number of parts will
|
||||||
|
# be used as the height of the type switcher.
|
||||||
|
#
|
||||||
|
# Default: 0
|
||||||
|
#max-mime-height = 0
|
||||||
|
|
||||||
|
# Parses and extracts http links when viewing a message. Links can then be
|
||||||
|
# accessed with the open-link command.
|
||||||
|
#
|
||||||
|
# Default: true
|
||||||
|
#parse-http-links=true
|
||||||
|
|
||||||
|
[compose]
|
||||||
|
editor=nvim +'set ft=mail.markdown'
|
||||||
|
|
||||||
|
# editor=nvim +setf\ mail
|
||||||
|
# editor=nvim # or your preferred editor
|
||||||
|
# Address book command
|
||||||
|
address-book-cmd=abook --mutt-query '%s'
|
||||||
|
|
||||||
|
# Order of headers in the compose editor (edit-headers=true: one per line, no | merging)
|
||||||
|
header-layout=To,From,Cc,Bcc,Subject
|
||||||
|
|
||||||
|
# Auto-complete addresses
|
||||||
|
# address-book-cmd-match='%s'
|
||||||
|
# address-book-cmd=abook --mutt-query '%s'
|
||||||
|
# terminal, though it may also launch a graphical window if the environment
|
||||||
|
# supports it. Defaults to $EDITOR, or vi.
|
||||||
|
#editor=
|
||||||
|
|
||||||
|
#
|
||||||
|
# When set, aerc will create and read .eml files for composing that have
|
||||||
|
# non-standard \n linebreaks. This is only relevant if the used editor does not
|
||||||
|
# support CRLF linebreaks.
|
||||||
|
#
|
||||||
|
#lf-editor=false
|
||||||
|
|
||||||
|
#
|
||||||
|
# Default header fields to display when composing a message. To display
|
||||||
|
# multiple headers in the same row, separate them with a pipe, e.g. "To|From".
|
||||||
|
#
|
||||||
|
# Default: To|From,Subject
|
||||||
|
#header-layout=To|From,Subject
|
||||||
|
|
||||||
|
#
|
||||||
|
# Edit headers into the text editor instead than separate fields.
|
||||||
|
#
|
||||||
|
# When this is true, address-book-cmd is not supported and address completion
|
||||||
|
# is left to the editor itself. Also, displaying multiple headers on the same
|
||||||
|
# line is not possible.
|
||||||
|
#
|
||||||
|
# Default: false
|
||||||
|
edit-headers=true
|
||||||
|
|
||||||
|
#
|
||||||
|
# Sets focus to the email body when the composer window opens.
|
||||||
|
#
|
||||||
|
# Default: false
|
||||||
|
#focus-body=false
|
||||||
|
|
||||||
|
#
|
||||||
|
# Specifies the command to be used to tab-complete email addresses. Any
|
||||||
|
# occurrence of "%s" in the address-book-cmd will be replaced with what the
|
||||||
|
# user has typed so far.
|
||||||
|
#
|
||||||
|
# The command must output the completions to standard output, one completion
|
||||||
|
# per line. Each line must be tab-delimited, with an email address occurring as
|
||||||
|
# the first field. Only the email address field is required. The second field,
|
||||||
|
# if present, will be treated as the contact name. Additional fields are
|
||||||
|
# ignored.
|
||||||
|
#
|
||||||
|
# This parameter can also be set per account in accounts.conf.
|
||||||
|
#address-book-cmd=
|
||||||
|
|
||||||
|
# Specifies the command to be used to select attachments. Any occurrence of
|
||||||
|
# '%s' in the file-picker-cmd will be replaced with the argument <arg>
|
||||||
|
# to :attach -m <arg>. Any occurrence of '%f' will be replaced by the
|
||||||
|
# location of a temporary file, from which aerc will read the selected files.
|
||||||
|
#
|
||||||
|
# If '%f' is not present, the command must output the selected files to
|
||||||
|
# standard output, one file per line. If it is present, then aerc does not
|
||||||
|
# capture the standard output and instead reads the files from the temporary
|
||||||
|
# file which should have the same format.
|
||||||
|
# file-picker-cmd: aerc runs it without a TTY, so TUI pickers (yazi) cannot run here.
|
||||||
|
# Attachments use :menu with yazi instead (see binds.conf [compose] `a`).
|
||||||
|
#file-picker-cmd=/home/liph/.local/bin/yazi-picker %f
|
||||||
|
|
||||||
|
#
|
||||||
|
# Allow to address yourself when replying
|
||||||
|
#
|
||||||
|
# Default: true
|
||||||
|
#reply-to-self=true
|
||||||
|
|
||||||
|
# Warn before sending an email with an empty subject.
|
||||||
|
#
|
||||||
|
# Default: false
|
||||||
|
#empty-subject-warning=false
|
||||||
|
|
||||||
|
#
|
||||||
|
# Warn before sending an email that matches the specified regexp but does not
|
||||||
|
# have any attachments. Leave empty to disable this feature.
|
||||||
|
#
|
||||||
|
# Uses Go's regexp syntax, documented at https://golang.org/s/re2syntax. The
|
||||||
|
# "(?im)" flags are set by default (case-insensitive and multi-line).
|
||||||
|
#
|
||||||
|
# Example:
|
||||||
|
# no-attachment-warning=^[^>]*attach(ed|ment)
|
||||||
|
#
|
||||||
|
#no-attachment-warning=
|
||||||
|
|
||||||
|
#
|
||||||
|
# When set, aerc will generate "format=flowed" bodies with a content type of
|
||||||
|
# "text/plain; format=flowed" as described in RFC3676. This format is easier to
|
||||||
|
# handle for some mailing software, and generally just looks like ordinary
|
||||||
|
# text. To actually make use of this format's features, you'll need support in
|
||||||
|
# your editor.
|
||||||
|
#
|
||||||
|
format-flowed=false
|
||||||
|
|
||||||
|
[multipart-converters]
|
||||||
|
text/html=/home/liph/.local/bin/aerc-md-convert
|
||||||
|
|
||||||
|
[filters]
|
||||||
|
#
|
||||||
|
text/plain=sed 's/^$//'
|
||||||
|
|
||||||
|
# Filters allow you to pipe an email body through a shell command to render
|
||||||
|
# certain emails differently, e.g. highlighting them with ANSI escape codes.
|
||||||
|
#
|
||||||
|
# The commands are invoked with sh -c. The following folders are prepended to
|
||||||
|
# the system $PATH to allow referencing filters from their name only:
|
||||||
|
#
|
||||||
|
# ${XDG_CONFIG_HOME:-~/.config}/aerc/filters
|
||||||
|
# ~/.local/libexec/aerc/filters
|
||||||
|
# ${XDG_DATA_HOME:-~/.local/share}/aerc/filters
|
||||||
|
# $PREFIX/libexec/aerc/filters
|
||||||
|
# $PREFIX/share/aerc/filters
|
||||||
|
# /usr/libexec/aerc/filters
|
||||||
|
# /usr/share/aerc/filters
|
||||||
|
#
|
||||||
|
# If you want to run a program in your default $PATH which has the same name
|
||||||
|
# as a builtin filter (e.g. /usr/bin/colorize), use its absolute path.
|
||||||
|
#
|
||||||
|
# The following variables are defined in the filter command environment:
|
||||||
|
#
|
||||||
|
# AERC_MIME_TYPE the part MIME type/subtype
|
||||||
|
# AERC_FORMAT the part content type format= parameter
|
||||||
|
# AERC_FILENAME the attachment filename (if any)
|
||||||
|
# AERC_SUBJECT the message Subject header value
|
||||||
|
# AERC_FROM the message From header value
|
||||||
|
#
|
||||||
|
# The first filter which matches the email's mimetype will be used, so order
|
||||||
|
# them from most to least specific.
|
||||||
|
#
|
||||||
|
# You can also match on non-mimetypes, by prefixing with the header to match
|
||||||
|
# against (non-case-sensitive) and a comma, e.g. subject,text will match a
|
||||||
|
# subject which contains "text". Use header,~regex to match against a regex.
|
||||||
|
#
|
||||||
|
text/plain=colorize
|
||||||
|
text/calendar=calendar
|
||||||
|
message/delivery-status=colorize
|
||||||
|
message/rfc822=colorize
|
||||||
|
text/html=w3m -T text/html -dump -o display_link_number=1
|
||||||
|
#text/*=bat -fP --file-name="$AERC_FILENAME"
|
||||||
|
#application/x-sh=bat -fP -l sh
|
||||||
|
#image/*=catimg -w $(tput cols) -
|
||||||
|
#subject,~Git(hub|lab)=lolcat -f
|
||||||
|
#from,thatguywhodoesnothardwraphismessages=wrap -w 100 | colorize
|
||||||
|
|
||||||
|
# This special filter is only used to post-process email headers when
|
||||||
|
# [viewer].show-headers=true
|
||||||
|
# By default, headers are piped directly into the pager.
|
||||||
|
#
|
||||||
|
.headers=colorize
|
||||||
|
|
||||||
|
[openers]
|
||||||
|
#
|
||||||
|
# Openers allow you to specify the command to use for the :open and :open-link
|
||||||
|
# actions on a per-MIME-type basis. The :open-link URL scheme is used to
|
||||||
|
# determine the MIME type as follows: x-scheme-handler/<scheme>.
|
||||||
|
#
|
||||||
|
# {} is expanded as the temporary filename or URL to be opened with proper
|
||||||
|
# shell quoting. If it is not encountered in the command, the filename/URL will
|
||||||
|
# be appended to the end of the command. The command will then be executed with
|
||||||
|
# `sh -c`.
|
||||||
|
#
|
||||||
|
# Like [filters], openers support basic shell globbing. The first opener which
|
||||||
|
# matches the part's MIME type (or URL scheme handler MIME type) will be used,
|
||||||
|
# so order them from most to least specific.
|
||||||
|
#
|
||||||
|
# Examples:
|
||||||
|
# x-scheme-handler/irc=hexchat
|
||||||
|
# x-scheme-handler/http*=printf '%s' {} | wl-copy
|
||||||
|
# text/html=surf -dfgms
|
||||||
|
# text/plain=gvim {} +125
|
||||||
|
# message/rfc822=thunderbird
|
||||||
|
text/html=w3m
|
||||||
|
x-scheme-handler/http=librewolf
|
||||||
|
x-scheme-handler/https=librewolf
|
||||||
|
text/calendar=khal import --batch
|
||||||
|
|
||||||
|
[hooks]
|
||||||
|
#
|
||||||
|
# Hooks are triggered whenever the associated event occurs.
|
||||||
|
|
||||||
|
#
|
||||||
|
# Executed when a new email arrives in the selected folder
|
||||||
|
#mail-received=notify-send "[$AERC_ACCOUNT/$AERC_FOLDER] New mail from $AERC_FROM_NAME" "$AERC_SUBJECT"
|
||||||
|
|
||||||
|
#
|
||||||
|
# Executed when mail is deleted from a folder
|
||||||
|
#mail-deleted=mbsync "$AERC_ACCOUNT:$AERC_FOLDER" &
|
||||||
|
|
||||||
|
#
|
||||||
|
# Executed when aerc adds mail to a folder
|
||||||
|
#mail-added=mbsync "$AERC_ACCOUNT:$AERC_FOLDER" &
|
||||||
|
|
||||||
|
#
|
||||||
|
# Executed when aerc starts
|
||||||
|
#aerc-startup=aerc :terminal calcurse && aerc :next-tab
|
||||||
|
|
||||||
|
#
|
||||||
|
# Executed when aerc shuts down.
|
||||||
|
#aerc-shutdown=
|
||||||
|
|
||||||
|
#
|
||||||
|
# Executed when notmuch tags are modified.
|
||||||
|
#tag-modified=
|
||||||
|
|
||||||
|
#
|
||||||
|
# Executed when flags are changed on a message.
|
||||||
|
#flag-changed=mbsync "$AERC_ACCOUNT:$AERC_FOLDER" &
|
||||||
|
|
||||||
|
[templates]
|
||||||
|
# Templates are used to populate email bodies automatically.
|
||||||
|
|
||||||
|
###
|
||||||
|
new-message=new-msg
|
||||||
|
quoted-reply=reply-quoted
|
||||||
|
|
||||||
|
|
||||||
|
# The directories where the templates are stored. It takes a colon-separated
|
||||||
|
# list of directories. If this is unset or if a template cannot be found, the
|
||||||
|
# following paths will be used as a fallback in that order:
|
||||||
|
#
|
||||||
|
# ${XDG_CONFIG_HOME:-~/.config}/aerc/templates
|
||||||
|
# ${XDG_DATA_HOME:-~/.local/share}/aerc/templates
|
||||||
|
# /usr/local/share/aerc/templates
|
||||||
|
# /usr/share/aerc/templates
|
||||||
|
#
|
||||||
|
template-dirs=${XDG_CONFIG_HOME:-~/.config}/aerc/templates
|
||||||
|
|
||||||
|
# The default template to be used for new messages.
|
||||||
|
#
|
||||||
|
# default: new_message
|
||||||
|
#new-message=new_message
|
||||||
|
|
||||||
|
# The default template to be used for quoted replies.
|
||||||
|
#
|
||||||
|
# default: quoted_reply
|
||||||
|
#quoted-reply=quoted_reply
|
||||||
|
|
||||||
|
# The default template to be used for forward as body.
|
||||||
|
#
|
||||||
|
# default: forward_as_body
|
||||||
|
#forwards=forward_as_body
|
||||||
|
|
||||||
|
[terminal]
|
||||||
|
passthrough=false
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
<A-n> = :next-tab<Enter>
|
<A-n> = :next-tab<Enter>
|
||||||
<C-t> = :terminal<Enter>
|
<C-t> = :terminal<Enter>
|
||||||
? = :help keys<Enter>
|
? = :help keys<Enter>
|
||||||
|
M = :menu -t Compose -c 'mail-compose' :compose<Enter>
|
||||||
<C-c> = :prompt 'Quit?' quit<Enter>
|
<C-c> = :prompt 'Quit?' quit<Enter>
|
||||||
<C-q> = :prompt 'Quit?' quit<Enter>
|
<C-q> = :prompt 'Quit?' quit<Enter>
|
||||||
<C-z> = :suspend<Enter>
|
<C-z> = :suspend<Enter>
|
||||||
@@ -98,7 +99,8 @@ rq = :reply -aq<Enter>
|
|||||||
Rr = :reply<Enter>
|
Rr = :reply<Enter>
|
||||||
Rq = :reply -q<Enter>
|
Rq = :reply -q<Enter>
|
||||||
H = :toggle-headers<Enter>
|
H = :toggle-headers<Enter>
|
||||||
,l = :pipe urlscan<Enter>
|
,l = :menu -lc fzf :open-link<Enter>
|
||||||
|
,lu = :pipe urlscan<Enter>
|
||||||
|
|
||||||
# Attachment Handling
|
# Attachment Handling
|
||||||
n = :next-part<Enter>
|
n = :next-part<Enter>
|
||||||
@@ -124,18 +126,19 @@ $complete = <C-o>
|
|||||||
<C-k> = :attach<space>
|
<C-k> = :attach<space>
|
||||||
<C-a> = :attach<space>
|
<C-a> = :attach<space>
|
||||||
<F4> = :attach<space>
|
<F4> = :attach<space>
|
||||||
|
,a = :menu -t Attach -c 'yazi --chooser-file=%f ~/Downloads' :attach<Enter>
|
||||||
<A-p> = :prev-tab<Enter>
|
<A-p> = :prev-tab<Enter>
|
||||||
<A-n> = :next-tab<Enter>
|
<A-n> = :next-tab<Enter>
|
||||||
<tab> = :next-field<Enter>
|
<tab> = :next-field<Enter>
|
||||||
<backtab> = :prev-field<Enter>
|
<backtab> = :prev-field<Enter>
|
||||||
|
|
||||||
[compose::editor]
|
[compose::editor]
|
||||||
# While focused in the text editor (nvim)
|
# While focused in the text editor (nvim) — only these keys reach aerc, the rest go to nvim
|
||||||
$noinherit = true
|
$noinherit = true
|
||||||
$ex = <C-x>
|
$ex = <C-x>
|
||||||
<C-k> = :attach<space>
|
<C-k> = :attach<space>
|
||||||
<C-a> = :attach<space>
|
<C-a> = :attach<space>
|
||||||
<F4> = :attach<space>
|
<F4> = :menu -t Attach -c 'yazi --chooser-file=%f ~/Downloads' :attach<Enter>
|
||||||
|
|
||||||
[compose::review]
|
[compose::review]
|
||||||
# After exiting the editor
|
# After exiting the editor
|
||||||
@@ -145,7 +148,7 @@ v = :preview<Enter>
|
|||||||
p = :postpone<Enter>
|
p = :postpone<Enter>
|
||||||
q = :choose -o d discard abort -o p postpone postpone<Enter>
|
q = :choose -o d discard abort -o p postpone postpone<Enter>
|
||||||
e = :edit<Enter>
|
e = :edit<Enter>
|
||||||
a = :attach<space>
|
,a = :menu -t Attach -c 'yazi --chooser-file=%f ~/Downloads' :attach<Enter>
|
||||||
<C-k> = :attach<space>
|
<C-k> = :attach<space>
|
||||||
<C-a> = :attach<space>
|
<C-a> = :attach<space>
|
||||||
d = :detach<space>
|
d = :detach<space>
|
||||||
@@ -155,6 +158,7 @@ d = :detach<space>
|
|||||||
$ex = <C-x>
|
$ex = <C-x>
|
||||||
<C-p> =
|
<C-p> =
|
||||||
<C-n> =
|
<C-n> =
|
||||||
|
M =
|
||||||
<tab> =
|
<tab> =
|
||||||
<backtab> =
|
<backtab> =
|
||||||
<A-p> = :prev-tab<Enter>
|
<A-p> = :prev-tab<Enter>
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
# Binds are of the form <key sequence> = <command to run>
|
||||||
|
# To use '=' in a key sequence, substitute it with "Eq": "<Ctrl+Eq>"
|
||||||
|
|
||||||
|
# GLOBAL Navigation & Contacts
|
||||||
|
( = :term abook<Enter>
|
||||||
|
,ab = :term abook<Enter>
|
||||||
|
,ca = :term khal interactive<Enter>
|
||||||
|
,c1 = :term khal interactive -a personal<Enter>
|
||||||
|
,c2 = :term khal interactive -a healplaylove<Enter>
|
||||||
|
,c3 = :term khal interactive -a glarisegg<Enter>
|
||||||
|
,ta = :term taskwarrior-tui<Enter>
|
||||||
|
|
||||||
|
<A-p> = :prev-tab<Enter>
|
||||||
|
<A-n> = :next-tab<Enter>
|
||||||
|
<C-t> = :terminal<Enter>
|
||||||
|
? = :help keys<Enter>
|
||||||
|
<C-c> = :prompt 'Quit?' quit<Enter>
|
||||||
|
<C-q> = :prompt 'Quit?' quit<Enter>
|
||||||
|
<C-z> = :suspend<Enter>
|
||||||
|
|
||||||
|
[messages]
|
||||||
|
# Sync
|
||||||
|
u = :term /home/liph/.local/bin/mailsync<Enter>
|
||||||
|
|
||||||
|
# Navigation
|
||||||
|
j = :next<Enter>
|
||||||
|
k = :prev<Enter>
|
||||||
|
<Down> = :next<Enter>
|
||||||
|
<Up> = :prev<Enter>
|
||||||
|
g = :select 0<Enter>
|
||||||
|
G = :select -1<Enter>
|
||||||
|
J = :next-folder<Enter>
|
||||||
|
K = :prev-folder<Enter>
|
||||||
|
H = :collapse-folder<Enter>
|
||||||
|
L = :expand-folder<Enter>
|
||||||
|
<Left> = :collapse-folder<Enter>
|
||||||
|
<Right> = :expand-folder<Enter>
|
||||||
|
|
||||||
|
# Marking/Flagging
|
||||||
|
v = :mark -v<Enter>
|
||||||
|
V = :mark -t<Enter>
|
||||||
|
<Space> = :mark -t<Enter>:next<Enter>
|
||||||
|
<Esc> = :unmark -a<Enter>
|
||||||
|
|
||||||
|
# Accounts
|
||||||
|
,1 = :change-tab phil<Enter>
|
||||||
|
,2 = :change-tab spam<Enter>
|
||||||
|
,3 = :change-tab proton<Enter>
|
||||||
|
|
||||||
|
# Folders
|
||||||
|
,i = :cf Inbox<Enter>
|
||||||
|
,u = :cf Unread<Enter>
|
||||||
|
,n = :cf Newsletters<Enter>
|
||||||
|
,f = :cf Finance<Enter>
|
||||||
|
,s = :cf Shopping<Enter>
|
||||||
|
,v = :cf Travel<Enter>
|
||||||
|
,d = :cf Development<Enter>
|
||||||
|
,x = :cf Spam<Enter>
|
||||||
|
,tr = :cf Trash<Enter>
|
||||||
|
,aa = :cf All Mail<Enter>
|
||||||
|
|
||||||
|
# Actions
|
||||||
|
<Enter> = :view<Enter>
|
||||||
|
s = :split<Enter>
|
||||||
|
S = :vsplit<Enter>
|
||||||
|
d = :modify-labels +deleted -inbox<Enter>
|
||||||
|
a = :archive flat<Enter>
|
||||||
|
C = :compose<Enter>
|
||||||
|
m = :compose<Enter>
|
||||||
|
rr = :reply -a<Enter>
|
||||||
|
rq = :reply -aq<Enter>
|
||||||
|
Rr = :reply<Enter>
|
||||||
|
Rq = :reply -q<Enter>
|
||||||
|
/ = :search<space>
|
||||||
|
\ = :filter<space>
|
||||||
|
n = :next-result<Enter>
|
||||||
|
N = :prev-result<Enter>
|
||||||
|
|
||||||
|
# Threads
|
||||||
|
T = :toggle-threads<Enter>
|
||||||
|
zc = :fold<Enter>
|
||||||
|
zo = :unfold<Enter>
|
||||||
|
za = :fold -t<Enter>
|
||||||
|
|
||||||
|
[messages:folder=Drafts]
|
||||||
|
<Enter> = :recall<Enter>
|
||||||
|
|
||||||
|
[view]
|
||||||
|
# Navigation & UI
|
||||||
|
q = :close<Enter>
|
||||||
|
x = :close<Enter>
|
||||||
|
| = :pipe<space>
|
||||||
|
D = :delete<Enter>
|
||||||
|
A = :archive flat<Enter>
|
||||||
|
f = :forward<Enter>
|
||||||
|
rr = :reply -a<Enter>
|
||||||
|
rq = :reply -aq<Enter>
|
||||||
|
Rr = :reply<Enter>
|
||||||
|
Rq = :reply -q<Enter>
|
||||||
|
H = :toggle-headers<Enter>
|
||||||
|
,l = :pipe urlscan<Enter>
|
||||||
|
|
||||||
|
# Attachment Handling
|
||||||
|
n = :next-part<Enter>
|
||||||
|
p = :prev-part<Enter>
|
||||||
|
J = :next<Enter>
|
||||||
|
K = :prev<Enter>
|
||||||
|
o = :open<Enter>
|
||||||
|
O = :open<Enter>
|
||||||
|
s = :save ~/Downloads/<Enter>
|
||||||
|
S = :save<space>
|
||||||
|
,sa = :save -a ~/Downloads/<Enter>
|
||||||
|
|
||||||
|
[view::passthrough]
|
||||||
|
$noinherit = true
|
||||||
|
$ex = <C-x>
|
||||||
|
<Esc> = :toggle-key-passthrough<Enter>
|
||||||
|
|
||||||
|
[compose]
|
||||||
|
# General compose view
|
||||||
|
$noinherit = true
|
||||||
|
$ex = <C-x>
|
||||||
|
$complete = <C-o>
|
||||||
|
<C-k> = :attach<space>
|
||||||
|
<C-a> = :attach<space>
|
||||||
|
<F4> = :attach<space>
|
||||||
|
<A-p> = :prev-tab<Enter>
|
||||||
|
<A-n> = :next-tab<Enter>
|
||||||
|
<tab> = :next-field<Enter>
|
||||||
|
<backtab> = :prev-field<Enter>
|
||||||
|
|
||||||
|
[compose::editor]
|
||||||
|
# While focused in the text editor (nvim)
|
||||||
|
$noinherit = true
|
||||||
|
$ex = <C-x>
|
||||||
|
<C-k> = :attach<space>
|
||||||
|
<C-a> = :attach<space>
|
||||||
|
<F4> = :attach<space>
|
||||||
|
|
||||||
|
[compose::review]
|
||||||
|
# After exiting the editor
|
||||||
|
y = :multipart text/html<Enter>:send<Enter>
|
||||||
|
n = :abort<Enter>
|
||||||
|
v = :preview<Enter>
|
||||||
|
p = :postpone<Enter>
|
||||||
|
q = :choose -o d discard abort -o p postpone postpone<Enter>
|
||||||
|
e = :edit<Enter>
|
||||||
|
a = :attach<space>
|
||||||
|
<C-k> = :attach<space>
|
||||||
|
<C-a> = :attach<space>
|
||||||
|
d = :detach<space>
|
||||||
|
|
||||||
|
[terminal]
|
||||||
|
# Clear keys to allow passthrough to Khal/Taskwarrior
|
||||||
|
$ex = <C-x>
|
||||||
|
<C-p> =
|
||||||
|
<C-n> =
|
||||||
|
<tab> =
|
||||||
|
<backtab> =
|
||||||
|
<A-p> = :prev-tab<Enter>
|
||||||
|
<A-n> = :next-tab<Enter>
|
||||||
+286
@@ -0,0 +1,286 @@
|
|||||||
|
#? Config file for btop v.1.4.7
|
||||||
|
|
||||||
|
#* Name of a btop++/bpytop/bashtop formatted ".theme" file, "Default" and "TTY" for builtin themes.
|
||||||
|
#* Themes should be placed in "../share/btop/themes" relative to binary or "$HOME/.config/btop/themes"
|
||||||
|
color_theme = "dracula.theme"
|
||||||
|
|
||||||
|
#* If the theme set background should be shown, set to False if you want terminal background transparency.
|
||||||
|
theme_background = true
|
||||||
|
|
||||||
|
#* Sets if 24-bit truecolor should be used, will convert 24-bit colors to 256 color (6x6x6 color cube) if false.
|
||||||
|
truecolor = true
|
||||||
|
|
||||||
|
#* Set to true to force tty mode regardless if a real tty has been detected or not.
|
||||||
|
#* Will force 16-color mode and TTY theme, set all graph symbols to "tty" and swap out other non tty friendly symbols.
|
||||||
|
force_tty = false
|
||||||
|
|
||||||
|
#* Option to disable presets. Either the default preset, custom presets, or all presets.
|
||||||
|
#* "Off" All presets are enabled.
|
||||||
|
#* "Default" preset is disabled.#* "Custom" presets are disabled.#* "All" presets are disabled.
|
||||||
|
disable_presets = "Off"
|
||||||
|
|
||||||
|
#* Define presets for the layout of the boxes. Preset 0 is always all boxes shown with default settings. Max 9 presets.
|
||||||
|
#* Format: "box_name:P:G,box_name:P:G" P=(0 or 1) for alternate positions, G=graph symbol to use for box.
|
||||||
|
#* Use whitespace " " as separator between different presets.
|
||||||
|
#* Example: "cpu:0:default,mem:0:tty,proc:1:default cpu:0:braille,proc:0:tty"
|
||||||
|
presets = "cpu:1:default,proc:0:default cpu:0:default,mem:0:default,net:0:default cpu:0:block,net:0:tty"
|
||||||
|
|
||||||
|
#* Set to True to enable "h,j,k,l,g,G" keys for directional control in lists.
|
||||||
|
#* Conflicting keys for h:"help" and k:"kill" is accessible while holding shift.
|
||||||
|
vim_keys = false
|
||||||
|
|
||||||
|
#* Disable all mouse events.
|
||||||
|
disable_mouse = false
|
||||||
|
|
||||||
|
#* Rounded corners on boxes, is ignored if TTY mode is ON.
|
||||||
|
rounded_corners = true
|
||||||
|
|
||||||
|
#* Use terminal synchronized output sequences to reduce flickering on supported terminals.
|
||||||
|
terminal_sync = true
|
||||||
|
|
||||||
|
#* Default symbols to use for graph creation, "braille", "block" or "tty".
|
||||||
|
#* "braille" offers the highest resolution but might not be included in all fonts.
|
||||||
|
#* "block" has half the resolution of braille but uses more common characters.
|
||||||
|
#* "tty" uses only 3 different symbols but will work with most fonts and should work in a real TTY.
|
||||||
|
#* Note that "tty" only has half the horizontal resolution of the other two, so will show a shorter historical view.
|
||||||
|
graph_symbol = "braille"
|
||||||
|
|
||||||
|
# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty".
|
||||||
|
graph_symbol_cpu = "default"
|
||||||
|
|
||||||
|
# Graph symbol to use for graphs in gpu box, "default", "braille", "block" or "tty".
|
||||||
|
graph_symbol_gpu = "default"
|
||||||
|
|
||||||
|
# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty".
|
||||||
|
graph_symbol_mem = "default"
|
||||||
|
|
||||||
|
# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty".
|
||||||
|
graph_symbol_net = "default"
|
||||||
|
|
||||||
|
# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty".
|
||||||
|
graph_symbol_proc = "default"
|
||||||
|
|
||||||
|
#* Manually set which boxes to show. Available values are "cpu mem net proc" and "gpu0" through "gpu5", separate values with whitespace.
|
||||||
|
shown_boxes = "cpu mem net proc"
|
||||||
|
|
||||||
|
#* Update time in milliseconds, recommended 2000 ms or above for better sample times for graphs.
|
||||||
|
update_ms = 2000
|
||||||
|
|
||||||
|
#* Processes sorting, "pid" "program" "arguments" "threads" "user" "memory" "cpu lazy" "cpu direct",
|
||||||
|
#* "cpu lazy" sorts top process over time (easier to follow), "cpu direct" updates top process directly.
|
||||||
|
proc_sorting = "cpu lazy"
|
||||||
|
|
||||||
|
#* Reverse sorting order, True or False.
|
||||||
|
proc_reversed = false
|
||||||
|
|
||||||
|
#* Show processes as a tree.
|
||||||
|
proc_tree = false
|
||||||
|
|
||||||
|
#* Use the cpu graph colors in the process list.
|
||||||
|
proc_colors = true
|
||||||
|
|
||||||
|
#* Use a darkening gradient in the process list.
|
||||||
|
proc_gradient = true
|
||||||
|
|
||||||
|
#* If process cpu usage should be of the core it's running on or usage of the total available cpu power.
|
||||||
|
proc_per_core = false
|
||||||
|
|
||||||
|
#* Show process memory as bytes instead of percent.
|
||||||
|
proc_mem_bytes = true
|
||||||
|
|
||||||
|
#* Show cpu graph for each process.
|
||||||
|
proc_cpu_graphs = true
|
||||||
|
|
||||||
|
#* Use /proc/[pid]/smaps for memory information in the process info box (very slow but more accurate)
|
||||||
|
proc_info_smaps = false
|
||||||
|
|
||||||
|
#* Show proc box on left side of screen instead of right.
|
||||||
|
proc_left = false
|
||||||
|
|
||||||
|
#* (Linux) Filter processes tied to the Linux kernel(similar behavior to htop).
|
||||||
|
proc_filter_kernel = false
|
||||||
|
|
||||||
|
#* Should the process list follow the selected process when detailed view is open.
|
||||||
|
proc_follow_detailed = true
|
||||||
|
|
||||||
|
#* In tree-view, always accumulate child process resources in the parent process.
|
||||||
|
proc_aggregate = false
|
||||||
|
|
||||||
|
#* Should cpu and memory usage display be preserved for dead processes when paused.
|
||||||
|
keep_dead_proc_usage = false
|
||||||
|
|
||||||
|
#* Sets the CPU stat shown in upper half of the CPU graph, "total" is always available.
|
||||||
|
#* Select from a list of detected attributes from the options menu.
|
||||||
|
cpu_graph_upper = "Auto"
|
||||||
|
|
||||||
|
#* Sets the CPU stat shown in lower half of the CPU graph, "total" is always available.
|
||||||
|
#* Select from a list of detected attributes from the options menu.
|
||||||
|
cpu_graph_lower = "Auto"
|
||||||
|
|
||||||
|
#* If gpu info should be shown in the cpu box. Available values = "Auto", "On" and "Off".
|
||||||
|
show_gpu_info = "Auto"
|
||||||
|
|
||||||
|
#* Toggles if the lower CPU graph should be inverted.
|
||||||
|
cpu_invert_lower = true
|
||||||
|
|
||||||
|
#* Set to True to completely disable the lower CPU graph.
|
||||||
|
cpu_single_graph = false
|
||||||
|
|
||||||
|
#* Show cpu box at bottom of screen instead of top.
|
||||||
|
cpu_bottom = false
|
||||||
|
|
||||||
|
#* Shows the system uptime in the CPU box.
|
||||||
|
show_uptime = true
|
||||||
|
|
||||||
|
#* Shows the CPU package current power consumption in watts. Requires running `make setcap` or `make setuid` or running with sudo.
|
||||||
|
show_cpu_watts = true
|
||||||
|
|
||||||
|
#* Show cpu temperature.
|
||||||
|
check_temp = true
|
||||||
|
|
||||||
|
#* Which sensor to use for cpu temperature, use options menu to select from list of available sensors.
|
||||||
|
cpu_sensor = "Auto"
|
||||||
|
|
||||||
|
#* Show temperatures for cpu cores also if check_temp is True and sensors has been found.
|
||||||
|
show_coretemp = true
|
||||||
|
|
||||||
|
#* Set a custom mapping between core and coretemp, can be needed on certain cpus to get correct temperature for correct core.
|
||||||
|
#* Use lm-sensors or similar to see which cores are reporting temperatures on your machine.
|
||||||
|
#* Format "x:y" x=core with wrong temp, y=core with correct temp, use space as separator between multiple entries.
|
||||||
|
#* Example: "4:0 5:1 6:3"
|
||||||
|
cpu_core_map = ""
|
||||||
|
|
||||||
|
#* Which temperature scale to use, available values: "celsius", "fahrenheit", "kelvin" and "rankine".
|
||||||
|
temp_scale = "celsius"
|
||||||
|
|
||||||
|
#* Use base 10 for bits/bytes sizes, KB = 1000 instead of KiB = 1024.
|
||||||
|
base_10_sizes = false
|
||||||
|
|
||||||
|
#* Show CPU frequency.
|
||||||
|
show_cpu_freq = true
|
||||||
|
|
||||||
|
#* How to calculate CPU frequency, available values: "first", "range", "lowest", "highest" and "average".
|
||||||
|
freq_mode = "first"
|
||||||
|
|
||||||
|
#* Draw a clock at top of screen, formatting according to strftime, empty string to disable.
|
||||||
|
#* Special formatting: /host = hostname | /user = username | /uptime = system uptime
|
||||||
|
clock_format = "%X"
|
||||||
|
|
||||||
|
#* Update main ui in background when menus are showing, set this to false if the menus is flickering too much for comfort.
|
||||||
|
background_update = true
|
||||||
|
|
||||||
|
#* Custom cpu model name, empty string to disable.
|
||||||
|
custom_cpu_name = ""
|
||||||
|
|
||||||
|
#* Optional filter for shown disks, should be full path of a mountpoint, separate multiple values with whitespace " ".
|
||||||
|
#* Only disks matching the filter will be shown. Prepend exclude= to only show disks not matching the filter. Examples: disk_filter="/boot /home/user", disks_filter="exclude=/boot /home/user"
|
||||||
|
disks_filter = ""
|
||||||
|
|
||||||
|
#* Show graphs instead of meters for memory values.
|
||||||
|
mem_graphs = true
|
||||||
|
|
||||||
|
#* Show mem box below net box instead of above.
|
||||||
|
mem_below_net = false
|
||||||
|
|
||||||
|
#* Count ZFS ARC in cached and available memory.
|
||||||
|
zfs_arc_cached = true
|
||||||
|
|
||||||
|
#* If swap memory should be shown in memory box.
|
||||||
|
show_swap = true
|
||||||
|
|
||||||
|
#* Show swap as a disk, ignores show_swap value above, inserts itself after first disk.
|
||||||
|
swap_disk = true
|
||||||
|
|
||||||
|
#* If mem box should be split to also show disks info.
|
||||||
|
show_disks = true
|
||||||
|
|
||||||
|
#* Filter out non physical disks. Set this to False to include network disks, RAM disks and similar.
|
||||||
|
only_physical = true
|
||||||
|
|
||||||
|
#* Read disks list from /etc/fstab. This also disables only_physical.
|
||||||
|
use_fstab = true
|
||||||
|
|
||||||
|
#* Setting this to True will hide all datasets, and only show ZFS pools. (IO stats will be calculated per-pool)
|
||||||
|
zfs_hide_datasets = false
|
||||||
|
|
||||||
|
#* Set to true to show available disk space for privileged users.
|
||||||
|
disk_free_priv = false
|
||||||
|
|
||||||
|
#* Toggles if io activity % (disk busy time) should be shown in regular disk usage view.
|
||||||
|
show_io_stat = true
|
||||||
|
|
||||||
|
#* Toggles io mode for disks, showing big graphs for disk read/write speeds.
|
||||||
|
io_mode = false
|
||||||
|
|
||||||
|
#* Set to True to show combined read/write io graphs in io mode.
|
||||||
|
io_graph_combined = false
|
||||||
|
|
||||||
|
#* Set the top speed for the io graphs in MiB/s (100 by default), use format "mountpoint:speed" separate disks with whitespace " ".
|
||||||
|
#* Example: "/mnt/media:100 /:20 /boot:1".
|
||||||
|
io_graph_speeds = ""
|
||||||
|
|
||||||
|
#* Swap the positions of the upload and download speed graphs. When true, upload will be on top.
|
||||||
|
swap_upload_download = false
|
||||||
|
|
||||||
|
#* Set fixed values for network graphs in Mebibits. Is only used if net_auto is also set to False.
|
||||||
|
net_download = 100
|
||||||
|
|
||||||
|
net_upload = 100
|
||||||
|
|
||||||
|
#* Use network graphs auto rescaling mode, ignores any values set above and rescales down to 10 Kibibytes at the lowest.
|
||||||
|
net_auto = true
|
||||||
|
|
||||||
|
#* Sync the auto scaling for download and upload to whichever currently has the highest scale.
|
||||||
|
net_sync = true
|
||||||
|
|
||||||
|
#* Starts with the Network Interface specified here.
|
||||||
|
net_iface = ""
|
||||||
|
|
||||||
|
#* "True" shows bitrates in base 10 (Kbps, Mbps). "False" shows bitrates in binary sizes (Kibps, Mibps, etc.). "Auto" uses base_10_sizes.
|
||||||
|
base_10_bitrate = "Auto"
|
||||||
|
|
||||||
|
#* Show battery stats in top right if battery is present.
|
||||||
|
show_battery = true
|
||||||
|
|
||||||
|
#* Which battery to use if multiple are present. "Auto" for auto detection.
|
||||||
|
selected_battery = "Auto"
|
||||||
|
|
||||||
|
#* Show power stats of battery next to charge indicator.
|
||||||
|
show_battery_watts = true
|
||||||
|
|
||||||
|
#* Set loglevel for "~/.local/state/btop.log" levels are: "ERROR" "WARNING" "INFO" "DEBUG".
|
||||||
|
#* The level set includes all lower levels, i.e. "DEBUG" will show all logging info.
|
||||||
|
log_level = "WARNING"
|
||||||
|
|
||||||
|
#* Automatically save current settings to config file on exit.
|
||||||
|
save_config_on_exit = true
|
||||||
|
|
||||||
|
#* Measure PCIe throughput on NVIDIA cards, may impact performance on certain cards.
|
||||||
|
nvml_measure_pcie_speeds = true
|
||||||
|
|
||||||
|
#* Measure PCIe throughput on AMD cards, may impact performance on certain cards.
|
||||||
|
rsmi_measure_pcie_speeds = true
|
||||||
|
|
||||||
|
#* Horizontally mirror the GPU graph.
|
||||||
|
gpu_mirror_graph = true
|
||||||
|
|
||||||
|
#* Set which GPU vendors to show. Available values are "nvidia amd intel apple"
|
||||||
|
shown_gpus = "nvidia amd intel apple"
|
||||||
|
|
||||||
|
#* Custom gpu0 model name, empty string to disable.
|
||||||
|
custom_gpu_name0 = ""
|
||||||
|
|
||||||
|
#* Custom gpu1 model name, empty string to disable.
|
||||||
|
custom_gpu_name1 = ""
|
||||||
|
|
||||||
|
#* Custom gpu2 model name, empty string to disable.
|
||||||
|
custom_gpu_name2 = ""
|
||||||
|
|
||||||
|
#* Custom gpu3 model name, empty string to disable.
|
||||||
|
custom_gpu_name3 = ""
|
||||||
|
|
||||||
|
#* Custom gpu4 model name, empty string to disable.
|
||||||
|
custom_gpu_name4 = ""
|
||||||
|
|
||||||
|
#* Custom gpu5 model name, empty string to disable.
|
||||||
|
custom_gpu_name5 = ""
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
#Bashtop theme with nord palette (https://www.nordtheme.com)
|
||||||
|
#by Justin Zobel <justin.zobel@gmail.com>
|
||||||
|
|
||||||
|
# Colors should be in 6 or 2 character hexadecimal or single spaced rgb decimal: "#RRGGBB", "#BW" or "0-255 0-255 0-255"
|
||||||
|
# example for white: "#ffffff", "#ff" or "255 255 255".
|
||||||
|
|
||||||
|
# All graphs and meters can be gradients
|
||||||
|
# For single color graphs leave "mid" and "end" variable empty.
|
||||||
|
# Use "start" and "end" variables for two color gradient
|
||||||
|
# Use "start", "mid" and "end" for three color gradient
|
||||||
|
|
||||||
|
# Main background, empty for terminal default, need to be empty if you want transparent background
|
||||||
|
theme[main_bg]="#282A36"
|
||||||
|
|
||||||
|
# Main text color
|
||||||
|
theme[main_fg]="#BD93F9"
|
||||||
|
|
||||||
|
# Title color for boxes
|
||||||
|
theme[title]="#f8f8f2"
|
||||||
|
|
||||||
|
# Higlight color for keyboard shortcuts
|
||||||
|
theme[hi_fg]="#ff79c6"
|
||||||
|
|
||||||
|
# Background color of selected item in processes box
|
||||||
|
theme[selected_bg]="#44475A"
|
||||||
|
|
||||||
|
# Foreground color of selected item in processes box
|
||||||
|
theme[selected_fg]="#ECEFF4"
|
||||||
|
|
||||||
|
# Color of inactive/disabled text
|
||||||
|
theme[inactive_fg]="#6272a4"
|
||||||
|
|
||||||
|
# Misc colors for processes box including mini cpu graphs, details memory graph and details status text
|
||||||
|
theme[proc_misc]="#BD93F9"
|
||||||
|
|
||||||
|
# Cpu box outline color
|
||||||
|
theme[cpu_box]="#ff79c6"
|
||||||
|
|
||||||
|
# Memory/disks box outline color
|
||||||
|
theme[mem_box]="#ff79c6"
|
||||||
|
|
||||||
|
# Net up/down box outline color
|
||||||
|
theme[net_box]="#ff79c6"
|
||||||
|
|
||||||
|
# Processes box outline color
|
||||||
|
theme[proc_box]="#ff79c6"
|
||||||
|
|
||||||
|
# Box divider line and small boxes line color
|
||||||
|
theme[div_line]="#ff79c6"
|
||||||
|
|
||||||
|
# Temperature graph colors
|
||||||
|
theme[temp_start]="#bd93f9"
|
||||||
|
theme[temp_mid]="#bd93f9"
|
||||||
|
theme[temp_end]="#bd93f9"
|
||||||
|
|
||||||
|
# CPU graph colors
|
||||||
|
theme[cpu_start]="#bd93f9"
|
||||||
|
theme[cpu_mid]="#bd93f9"
|
||||||
|
theme[cpu_end]="#bd93f9"
|
||||||
|
|
||||||
|
# Mem/Disk free meter
|
||||||
|
theme[free_start]="#bd93f9"
|
||||||
|
theme[free_mid]="#bd93f9"
|
||||||
|
theme[free_end]="#bd93f9"
|
||||||
|
|
||||||
|
# Mem/Disk cached meter
|
||||||
|
theme[cached_start]="#bd93f9"
|
||||||
|
theme[cached_mid]="#bd93f9"
|
||||||
|
theme[cached_end]="#bd93f9"
|
||||||
|
|
||||||
|
# Mem/Disk available meter
|
||||||
|
theme[available_start]="#bd93f9"
|
||||||
|
theme[available_mid]="#bd93f9"
|
||||||
|
theme[available_end]="#bd93f9"
|
||||||
|
|
||||||
|
# Mem/Disk used meter
|
||||||
|
theme[used_start]="#bd93f9"
|
||||||
|
theme[used_mid]="#bd93f9"
|
||||||
|
theme[used_end]="#bd93f9"
|
||||||
|
|
||||||
|
# Download graph colors
|
||||||
|
theme[download_start]="#bd93f9"
|
||||||
|
theme[download_mid]="#bd93f9"
|
||||||
|
theme[download_end]="#bd93f9"
|
||||||
|
|
||||||
|
# Upload graph colors
|
||||||
|
theme[upload_start]="#bd93f9"
|
||||||
|
theme[upload_mid]="#bd93f9"
|
||||||
|
theme[upload_end]="#bd93f9"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
theme = Dracula
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# theme = catppuccin-mocha
|
||||||
|
# font-size = 12
|
||||||
|
# background-blur-radius = 20
|
||||||
|
# mouse-hide-while-typing = true
|
||||||
|
# window-decoration = true
|
||||||
|
# keybind = global:cmd+/=toggle_quick_terminal
|
||||||
|
# macos-option-as-alt = true
|
||||||
|
# background-opacity = 0.7
|
||||||
|
# background-blur-radius = 20
|
||||||
|
|
||||||
|
# By default Reload config with shft + cmd + ,
|
||||||
|
|
||||||
|
gtk-titlebar = false
|
||||||
|
# background = #000000
|
||||||
|
background-opacity = 0.8
|
||||||
|
background-blur-radius = 20
|
||||||
|
|
||||||
|
font-family = JetBrainsMonoNL Nerd Font
|
||||||
|
font-size = 12
|
||||||
|
|
||||||
|
# cursor
|
||||||
|
# cursor-color = cell-background
|
||||||
|
cursor-style = block
|
||||||
|
cursor-style-blink = true
|
||||||
|
# cursor-invert-fg-bg = true
|
||||||
|
|
||||||
|
# macos-titlebar-style = hidden
|
||||||
|
# macos-option-as-alt = true
|
||||||
|
# mouse-hide-while-typing = true
|
||||||
|
|
||||||
|
#confirm-close-surface = false
|
||||||
|
#window-decoration = true
|
||||||
|
window-padding-x = 10
|
||||||
|
window-padding-y = 10
|
||||||
|
window-padding-balance = true
|
||||||
|
|
||||||
|
auto-update = check
|
||||||
|
auto-update-channel = stable
|
||||||
|
|
||||||
|
# extras
|
||||||
|
# shell-integration-features = no-cursor
|
||||||
|
copy-on-select = clipboard
|
||||||
|
|
||||||
|
# Custom Keybinds & Config (super represents cmd key on mac)
|
||||||
|
# For Simple Keybinds
|
||||||
|
# keybind = super+r=reload_config
|
||||||
|
keybind = super+i=inspector:toggle
|
||||||
|
keybind = super+b>,=toggle_quick_terminal
|
||||||
|
keybind = ctrl+alt+n=text:~
|
||||||
|
# Similar Tmux but with Ghostty
|
||||||
|
# (ctrl b for tmux -> cmb b for ghostty)
|
||||||
|
keybind = super+b>r=reload_config
|
||||||
|
keybind = super+b>x=close_surface
|
||||||
|
# this only works if you have `macos-titlebar-style = tabs or native or transparent`
|
||||||
|
# else it behaves like new window
|
||||||
|
keybind = super+b>c=new_tab
|
||||||
|
keybind = super+b>n=new_window
|
||||||
|
|
||||||
|
# tab nav
|
||||||
|
#keybind = super+b>1=goto_tab:1
|
||||||
|
#keybind = super+b>2=goto_tab:2
|
||||||
|
#keybind = super+b>3=goto_tab:3
|
||||||
|
#keybind = super+b>4=goto_tab:4
|
||||||
|
#keybind = super+b>5=goto_tab:5
|
||||||
|
#keybind = super+b>6=goto_tab:6
|
||||||
|
#keybind = super+b>7=goto_tab:7
|
||||||
|
#keybind = super+b>8=goto_tab:8
|
||||||
|
#keybind = super+b>9=goto_tab:9
|
||||||
|
|
||||||
|
# splitting
|
||||||
|
#keybind = super+a>\=new_split:right
|
||||||
|
#keybind = super+a>-=new_split:down
|
||||||
|
#keybind = super+a>e=equalize_splits
|
||||||
|
|
||||||
|
# split nav (setting this to control will not work if tmux already uses it)
|
||||||
|
#keybind = super+a>h=goto_split:left
|
||||||
|
#keybind = super+a>j=goto_split:bottom
|
||||||
|
#keybind = super+a>k=goto_split:top
|
||||||
|
#keybind = super+a>l=goto_split:right
|
||||||
|
|
||||||
|
# typecraft
|
||||||
|
# theme = catppuccin-mocha
|
||||||
|
# theme = GruvboxDark
|
||||||
|
# font-family = CaskaydiaCove Nerd Font
|
||||||
|
# font-size = 12
|
||||||
|
# gtk-titlebar = false
|
||||||
|
# custom-shader = shaders/cursor_sweep.glsl
|
||||||
|
# custom-shader = shaders/cursor_tail.glsl
|
||||||
|
custom-shader = shaders/cursor_warp.glsl
|
||||||
|
# custom-shader = shaders/cursor_sweep.glsl
|
||||||
|
config-file = ?auto/theme.ghostty
|
||||||
@@ -1,22 +1,21 @@
|
|||||||
# theme = catppuccin-mocha
|
# theme = catppuccin-mocha
|
||||||
# font-size = 12
|
|
||||||
# background-blur-radius = 20
|
|
||||||
# mouse-hide-while-typing = true
|
# mouse-hide-while-typing = true
|
||||||
# window-decoration = true
|
# window-decoration = true
|
||||||
# keybind = global:cmd+/=toggle_quick_terminal
|
# keybind = global:cmd+/=toggle_quick_terminal
|
||||||
# macos-option-as-alt = true
|
macos-option-as-alt = true
|
||||||
# background-opacity = 0.7
|
# background-opacity = 0.7
|
||||||
# background-blur-radius = 20
|
# background-blur-radius = 20
|
||||||
|
|
||||||
# By default Reload config with shft + cmd + ,
|
# By default Reload config with shft + cmd + ,
|
||||||
|
|
||||||
|
gtk-titlebar = false
|
||||||
# config-file = themes/rosepine
|
# config-file = themes/rosepine
|
||||||
# background = #000000
|
# background = #000000
|
||||||
# background-opacity = 0.85
|
background-opacity = 0.8
|
||||||
# background-blur-radius = 20
|
background-blur-radius = 20
|
||||||
|
|
||||||
# font-family = JetBrainsMonoNL Nerd Font
|
font-family = JetBrainsMonoNL Nerd Font
|
||||||
# font-size = 12
|
font-size = 12
|
||||||
cursor-style = block
|
cursor-style = block
|
||||||
cursor-style-blink = false
|
cursor-style-blink = false
|
||||||
cursor-invert-fg-bg = true
|
cursor-invert-fg-bg = true
|
||||||
@@ -78,9 +77,7 @@ keybind = super+b>n=new_window
|
|||||||
# typecraft
|
# typecraft
|
||||||
# theme = catppuccin-mocha
|
# theme = catppuccin-mocha
|
||||||
# theme = GruvboxDark
|
# theme = GruvboxDark
|
||||||
font-family = CaskaydiaCove Nerd Font
|
# font-family = CaskaydiaCove Nerd Font
|
||||||
font-size = 12
|
# font-size = 12
|
||||||
background-opacity = 0.95
|
# gtk-titlebar = false
|
||||||
gtk-titlebar = false
|
|
||||||
# custom-shader = /home/chris/bloom.glsl
|
|
||||||
#custom-shader = /home/chris/crt.glsl
|
#custom-shader = /home/chris/crt.glsl
|
||||||
Submodule
+1
Submodule ghostty/.config/ghostty/shaders added at 0a274beac8.
Whitespace-only changes.
@@ -96,3 +96,4 @@ description = "command bar"
|
|||||||
|
|
||||||
[experimental]
|
[experimental]
|
||||||
pane_history = true
|
pane_history = true
|
||||||
|
kitty_graphics = true
|
||||||
Submodule herdr/.config/herdr/plugins/.tmp-install-750065-1785311987680/checkout deleted from 5f76bc9eab.
@@ -0,0 +1,148 @@
|
|||||||
|
# Herdr Navigator default config
|
||||||
|
# Location after first run:
|
||||||
|
# $(herdr plugin config-dir herdr-navigator)/config.toml
|
||||||
|
|
||||||
|
[picker]
|
||||||
|
# What happens after selecting an entry:
|
||||||
|
# - true: focus an already-open workspace for the selected path
|
||||||
|
# - false: always use create_missing behavior
|
||||||
|
reuse_existing = true
|
||||||
|
|
||||||
|
# Create a new Herdr workspace when the selected path is not open yet.
|
||||||
|
create_missing = true
|
||||||
|
|
||||||
|
# Search engine: "nucleo" | "skim" | "simple"
|
||||||
|
# - nucleo: best default; fast, fzf-like scoring, good Unicode support
|
||||||
|
# - skim: skim/fzf-style matcher via fuzzy-matcher crate
|
||||||
|
# - simple: tiny built-in ordered-character matcher for debugging
|
||||||
|
engine = "nucleo"
|
||||||
|
|
||||||
|
# Source priority controls initial ordering and tie/near-tie ranking.
|
||||||
|
# Accepted names: agent, server/remote, workspace/open, project, session, zoxide, root, quick, plugin.
|
||||||
|
source_order = ["workspace", "agent", "project", "session", "zoxide", "root", "server", "quick", "plugin"]
|
||||||
|
|
||||||
|
# How strongly source_order influences search ranking.
|
||||||
|
# Set 0 for pure matcher score.
|
||||||
|
source_priority_boost = 5
|
||||||
|
|
||||||
|
# Agent view sorting for `@` and Ctrl-A:
|
||||||
|
# - herdr: read Herdr's agent_panel_sort setting
|
||||||
|
# - priority: blocked/error, attention/request, done, working, then idle
|
||||||
|
# - spaces: keep Herdr/pane order
|
||||||
|
agent_sort = "herdr"
|
||||||
|
|
||||||
|
# Show the right-side preview pane on open. Ctrl-O toggles it at runtime.
|
||||||
|
preview = true
|
||||||
|
|
||||||
|
# Use Herdr-style source-aware rows: metadata aligns right, while zoxide/root
|
||||||
|
# entries expand to a second full-path line. Set false for the compact list.
|
||||||
|
detailed_rows = true
|
||||||
|
|
||||||
|
# Check GitHub releases in the background at most once per day. Network errors
|
||||||
|
# are ignored; a newer version appears as a badge in the Navigator header.
|
||||||
|
check_updates = true
|
||||||
|
|
||||||
|
# Vim-style normal mode: j/k move, source keys filter, and / starts searching.
|
||||||
|
vim_mode = false
|
||||||
|
|
||||||
|
# Start searching immediately after a Vim source-filter key.
|
||||||
|
vim_filter_search = false
|
||||||
|
|
||||||
|
# Optional: override source filter shortcuts. Values accept "ctrl-x", "ctrl+x", "^x", or "x".
|
||||||
|
# Defaults: server=ctrl-s, agent=ctrl-a, quick=ctrl-q, workspace=ctrl-w, project=ctrl-p,
|
||||||
|
# zoxide=ctrl-z, root=ctrl-r, session=ctrl-l.
|
||||||
|
# [picker.filter_keys]
|
||||||
|
# server = "ctrl-g"
|
||||||
|
|
||||||
|
[notifications]
|
||||||
|
# Show Navigator success/error notifications.
|
||||||
|
enabled = true
|
||||||
|
# Enable notification audio. Notifications remain visible when this is false.
|
||||||
|
audio = false
|
||||||
|
# Audio mode when enabled: "default" uses Herdr's done/request sounds;
|
||||||
|
# "custom" plays custom_sound with afplay (macOS) or pw-play/paplay/aplay (Linux).
|
||||||
|
sound = "default"
|
||||||
|
custom_sound = "" # Example: "~/sounds/navigator.wav"
|
||||||
|
|
||||||
|
[jump_back]
|
||||||
|
# Master switch for Jump Back.
|
||||||
|
# - true: remember successful local workspace changes and enable the jump-back action
|
||||||
|
# - false: do not record, pin, or jump; any saved history is kept for later re-enabling
|
||||||
|
# The global keybinding itself belongs in Herdr's main config; see README.md.
|
||||||
|
enabled = true
|
||||||
|
|
||||||
|
# Put the previous workspace first when the picker opens with no query or source filter.
|
||||||
|
# This only changes the initial view; typed searches and Ctrl source filters keep normal ranking.
|
||||||
|
pin_previous = true
|
||||||
|
|
||||||
|
[sources]
|
||||||
|
# Existing Herdr workspaces. Selecting focuses the workspace.
|
||||||
|
open_workspaces = true
|
||||||
|
|
||||||
|
# Herdr Plus project templates from:
|
||||||
|
# ~/.config/herdr/plugins/config/cloudmanic.herdr-plus/projects/*.toml
|
||||||
|
herdr_plus_projects = true
|
||||||
|
|
||||||
|
# zoxide history from `zoxide query -l`.
|
||||||
|
zoxide = true
|
||||||
|
|
||||||
|
# Direct filesystem scan of [[roots]] below.
|
||||||
|
roots = true
|
||||||
|
|
||||||
|
# Agent panes from Herdr pane metadata. Selecting focuses the agent.
|
||||||
|
agents = true
|
||||||
|
|
||||||
|
# Remote Herdr targets from remote entries in [sessions] below.
|
||||||
|
servers = true
|
||||||
|
# Local Herdr sessions from `herdr session list --json` and local [sessions] entries below.
|
||||||
|
sessions = true
|
||||||
|
|
||||||
|
# Herdr Plus Quick Actions. Selecting opens the Quick Actions picker.
|
||||||
|
herdr_plus_quick_actions = true
|
||||||
|
|
||||||
|
[theme]
|
||||||
|
# Reads ~/.config/herdr/config.toml theme name and [theme.custom] tokens when available.
|
||||||
|
# Falls back to the built-in One Light palette.
|
||||||
|
inherit_herdr = true
|
||||||
|
|
||||||
|
# Keep roots short; zoxide should handle most frequent directories.
|
||||||
|
[[roots]]
|
||||||
|
path = "~/workspace"
|
||||||
|
max_depth = 3
|
||||||
|
|
||||||
|
[[roots]]
|
||||||
|
path = "~/projects"
|
||||||
|
max_depth = 3
|
||||||
|
|
||||||
|
# Optional agent aliases. All match fields are optional and matched as text contains.
|
||||||
|
# Aliases become extra search terms for matching agent panes.
|
||||||
|
#
|
||||||
|
# [[agent_aliases]]
|
||||||
|
# alias = "main ai dot"
|
||||||
|
# agent = "claude"
|
||||||
|
# workspace = "Dotfiles"
|
||||||
|
# path = "dotfiles"
|
||||||
|
|
||||||
|
[sessions]
|
||||||
|
# Include local named sessions from `herdr session list --json`.
|
||||||
|
local = true
|
||||||
|
|
||||||
|
# Manual remote targets. Selecting runs: herdr --remote <remote> --handoff.
|
||||||
|
# [[sessions.entries]]
|
||||||
|
# name = "prod"
|
||||||
|
# remote = "prod-api"
|
||||||
|
# tags = ["prod", "api"]
|
||||||
|
|
||||||
|
# Optional command/JSON integrations. A plugin only needs:
|
||||||
|
# - collect: command that prints JSON array
|
||||||
|
# - open: command to run when the item is selected
|
||||||
|
#
|
||||||
|
# [[integrations]]
|
||||||
|
# id = "demo"
|
||||||
|
# label = "Bookmarks"
|
||||||
|
# enabled = true
|
||||||
|
# collect = "bookmarks list --json"
|
||||||
|
# open = "bookmarks open {{id}}"
|
||||||
|
# notify_success = true
|
||||||
|
# notify_error = true
|
||||||
|
#
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
1788530642
|
||||||
|
0.3.6
|
||||||
@@ -11,14 +11,14 @@ workspaces:
|
|||||||
focus: true
|
focus: true
|
||||||
- label: Music
|
- label: Music
|
||||||
panes:
|
panes:
|
||||||
- focus: true
|
- focus: false
|
||||||
- split: right
|
- split: right
|
||||||
ratio: 0.5
|
ratio: 0.5
|
||||||
command: mc
|
command: mc
|
||||||
- label: Aerc
|
- label: Aerc
|
||||||
panes:
|
panes:
|
||||||
- command: aerc
|
- command: aerc
|
||||||
focus: true
|
focus: false
|
||||||
|
|
||||||
- name: Dotfiles
|
- name: Dotfiles
|
||||||
root: /home/liph/dotfiles
|
root: /home/liph/dotfiles
|
||||||
@@ -28,3 +28,13 @@ workspaces:
|
|||||||
panes:
|
panes:
|
||||||
- command: yazi
|
- command: yazi
|
||||||
focus: true
|
focus: true
|
||||||
|
|
||||||
|
- name: Coding
|
||||||
|
root: /home/liph/code
|
||||||
|
focus: false
|
||||||
|
tabs:
|
||||||
|
- label: Pi
|
||||||
|
panes:
|
||||||
|
- focus: false
|
||||||
|
- split: right
|
||||||
|
ratio: 0.5
|
||||||
Submodule herdr/.config/herdr/plugins/github/herdr-bar-96eebe452a9c updated: 58c23e5c8d...b57aa342f8.
Submodule herdr/.config/herdr/plugins/github/herdr-lazygit-b11d62e75f78 updated: a13e12c99e...6b1e34f426.
Submodule herdr/.config/herdr/plugins/github/herdr-navigator-3a5a6fa5dfb5 added at 03b803a003.
Submodule herdr/.config/herdr/plugins/github/herdr-spreader-f248c87aa2e2 updated: 5f76bc9eab...206ae55aff.
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "0.8.0",
|
"version": "0.9.3",
|
||||||
"body": "### Added\n- Added `herdr --skill` to print the agent skill bundled with the running Herdr binary.\n- Added `ui.pane_scrollbars = false` to hide terminal pane scrollbars and reclaim their reserved column. (#2167)\n- Added `ui.tab_bar_position = \"bottom\"` to place the desktop tab row below terminal panes. (#2117)\n- Added live filtering to the keybind help with `/`, Backspace, and `Ctrl+U`. (#1825, #1832, thanks @corrius)\n- Added Windows support for `experimental.switch_ascii_input_source_in_prefix` with Korean IMEs. (#1802, #1823, thanks @joonhwan)\n- Added Grok CLI session reporting and native restore with `grok --resume <id>`. (#1800, #1807, thanks @carlesso)\n- Added Antigravity CLI session reporting and native restore with `agy --conversation <id>`. (#1011, #1571, #2087, thanks @ludoo)\n- Added automatic text history reads for idle alternate-screen agents, with the application viewport restored after collection.\n- Added `workspace.move_block`, the `workspace.reordered` event, and atomic worktree-group reordering. (#1694)\n- Added a Simplified Chinese README. (#1990, thanks @patrick-xin)\n\n### Changed\n- Experimental options are no longer exposed in the Settings TUI and remain available through the config file.\n- Agent status indicators now use the same static workspace marks across the sidebar, navigator, and mobile views, eliminating continuous spinner rendering while agents work.\n- Hidden pane output no longer triggers unnecessary TUI rendering.\n- Windows preview downloads now include Herdr and a modern app-local ConPTY runtime in one archive. (#1533, #1644, #1828)\n- Worktree parents and children now stay packed together in the sidebar, including while groups are reordered.\n- Public documentation now separates stable, preview, and immutable versioned release snapshots.\n- Repository and installation links now use `herdrdev/herdr` after the GitHub organization migration.\n- Relicensed Herdr from AGPL-3.0-or-later to Apache-2.0.\n\n### Fixed\n- Pane applications now receive semantic light/dark query responses and live Mode 2031 updates when the host appearance changes. (#714)\n- Remote attach now falls back to `sh` when the login shell cannot perform path discovery. (#1201)\n- PTY output continues to be read while pane input is temporarily blocked. (#1295)\n- Worktree CLI help and docs no longer advertise the redundant `--json` flag; worktree commands remain JSON-only and continue accepting the flag for compatibility. (#2171)\n- OpenCode 2 preview panes now appear as OpenCode agents and use the existing OpenCode status detection. (#2169)\n- Pane text copied through VS Code Remote Tunnels now reaches the viewing machine's clipboard instead of overwriting the remote host clipboard. (#2015)\n- Windows agent detection now follows Git Bash-launched agents across emulated `exec` process boundaries. (#2107)\n- Detached Windows servers and pane processes now survive logout from the OpenSSH session that started them. (#2008)\n- Windows `agent start` now launches agents without native arguments instead of timing out on an invalid empty PowerShell argument list. (#2072)\n- Headless servers now resume restored agent sessions without waiting for a TUI client to attach. (#2064)\n- Vibe and other Kitty-keyboard pane applications now receive shifted letters and punctuation when they request associated text. (#2020)\n- Kitty-keyboard pane applications now receive printable key releases without duplicate text input. (#1746)\n- Kitty graphics remain visible during host repaints. (#1628)\n- Pane applications now receive correct XTWINOPS terminal and cell-size query responses. (#835)\n- WSL clients query the host cell size when the terminal ioctl reports no pixels, keeping graphics sharp instead of using the 8x16 fallback. (#2146, #2160, thanks @WakaTaira)\n- Linux runtimes without terminal foreground process groups can opt into child-group agent detection with `HERDR_PROCESS_DETECTION=child-groups`. (#1982)\n- Installing the Herdr agent skill with the `skills` CLI no longer copies the entire repository. (#2022)\n- Nix builds now include the bundled agent skill required by `herdr --skill`. (#1889, #1890, thanks @olafkfreund)\n- Agent prompts now wait briefly after sending text before pressing Enter, preventing prompts from remaining in agent composers without starting a turn. (#1878)\n- Empty clipboard writes from pane applications no longer erase existing clipboard contents or show a copied confirmation. (#1893)\n- Plain mouse movement no longer triggers continuous full renders while preserving Herdr menu hover and pane application mouse tracking. (#1865)\n- Extended-button drags now preserve Herdr hover state while applications receive the drag.\n- `ui.copy_on_select = false` now retains drag and double-click word selections without copying; `Ctrl+C`, or `Cmd+C` when the host terminal forwards it, copies and clears the selection. (#1782)\n- Pane and agent read responses now report `truncated: true` wLine truncated
|
"body": "This is a hotfix release for v0.9.2. See the v0.9.2 notes for the full feature release: https://github.com/herdrdev/herdr/releases/tag/v0.9.2\n\n### Fixed\n- Terminal shortcuts that send Escape followed by a key work again in panes. On macOS, Option+Left/Right and Option+Backspace from Ghostty's defaults or iTerm2's Natural Text Editing preset move and delete by word again, instead of typing `b` and `f` or deleting one character. Escape-based Shift+Enter bindings insert a newline in Claude Code instead of submitting. Clicking a pane still doesn't send a stray Escape. (#4751)\n- Alt+[ followed quickly by another key no longer merges into a different key. (#4751)",
|
||||||
"show_on_startup": true
|
"show_on_startup": true
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,7 @@ TARGET_DIR="$(realpath ~/dotfiles)"
|
|||||||
# Close any existing versions of these workspaces so this is idempotent.
|
# Close any existing versions of these workspaces so this is idempotent.
|
||||||
herdr workspace close Home 2>/dev/null || true
|
herdr workspace close Home 2>/dev/null || true
|
||||||
herdr workspace close Dotfiles 2>/dev/null || true
|
herdr workspace close Dotfiles 2>/dev/null || true
|
||||||
|
herdr workspace close Coding 2>/dev/null || true
|
||||||
|
|
||||||
# Apply the layout file, which creates both workspaces.
|
# Apply the layout file, which creates both workspaces.
|
||||||
"$SPREADER" apply --file "$CONFIG_DIR/home.yml"
|
"$SPREADER" apply --file "$CONFIG_DIR/home.yml"
|
||||||
|
|||||||
@@ -6,32 +6,44 @@
|
|||||||
{
|
{
|
||||||
"panes": {
|
"panes": {
|
||||||
"1": {
|
"1": {
|
||||||
"ansi": "\u001b[0m\u001b[38;5;5m❯ \u001b[0m\u001b[38;5;2msed\u001b[0m -n \u001b[0m\u001b[38;5;3m'103,111p'\u001b[0m \u001b[0m\u001b[4m~/.config/yazi/init.lua\u001b[0m\r\nrequire(\"mime-ext.local\"):setup({\r\n with_exts = {\r\n tex = \"text/makefile\",\r\n },\r\n})\r\n\r\nrequire(\"full-border\"):setup({\r\n type = ui.Border.ROUNDED,\r\n})\r\n\u001b[0m\u001b[38;5;5m❯ \u001b[0m\r\n\u001b[0m\u001b[38;5;5m❯ \u001b[0m\u001b[38;5;2my\u001b[0m\r\nError: Lua runtime failed\r\n\r\nCaused by:\r\n Plugin `mime-ext.local` requires at least Yazi 26.8.15, but your current version is Yazi 26.5.6 (Arch Linux 2026-06-15).\r\n stack traceback:\r\n [C]: in local 'poll'\r\n [string \"?\"]:28: in global 'require'\r\n [string \"init.lua\"]:103: in main chunk\r\n \r\n\u001b[0m\u001b[38;5;5m❯ \u001b[0m\u001b[4m\u001b[38;5;2msudo\u001b[0m \u001b[0m\u001b[38;5;2mpacman\u001b[0m -S unarchiver\r\n[sudo] password for liph: \r\n\u001b[0m\u001b[1m\u001b[38;5;1merror: \u001b[0mfailed to init transaction (unable to lock database)\r\n\u001b[0m\u001b[1m\u001b[38;5;1merror: \u001b[0mcould not lock database: File exists\r\n if you're sure a package manager is not already\r\n running, you can remove /var/lib/pacman/db.lck\r\n\u001b[0m\u001b[38;5;1m❯ \u001b[0m\u001b[38;5;2my\u001b[0m\r\nError: Lua runtime failed\r\n\r\nCaused by:\r\n Plugin `mime-ext.local` requires at least Yazi 26.8.15, but your current version is Yazi 26.5.6 (Arch Linux 2026-06-15).\r\n stack traceback:\r\n [C]: in local 'poll'\r\n [string \"?\"]:28: in global 'require'\r\n [string \"init.lua\"]:103: in main chunk\r\n \r\n\u001b[0m\u001b[38;5;5m❯ \u001b[0m\u001b[38;5;2mcd\u001b[0m yazi\r\n\u001b[0m\u001b[38;5;5m❯ \u001b[0m\u001b[38;5;2mn\u001b[0m \u001b[0m\u001b[4mkeymap.toml\u001b[0m\r\n\r\n\u001b[0m\u001b[38;5;5march liph \u001b[0m\u001b[38;5;4m~/dotfiles/yazi/.config/yazi\u001b[0m\u001b[38;5;3m main* \u001b[0m\r\n\u001b[0m\u001b[38;5;5m❯\u001b[0m ",
|
"ansi": "\u001b[0m\u001b[38;5;5march liph \u001b[0m\u001b[38;5;4m~\u001b[0m\r\n\u001b[0m\u001b[38;5;5m❯\u001b[0m ",
|
||||||
"lines": 42
|
"lines": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"2": {
|
{
|
||||||
"ansi": "\r\n\u001b[0m\u001b[38;5;5march liph \u001b[0m\u001b[38;5;4m~/Pictures\u001b[0m\r\n\u001b[0m\u001b[38;5;5m❯\u001b[0m ",
|
"tabs": [
|
||||||
|
{
|
||||||
|
"panes": {
|
||||||
|
"11": {
|
||||||
|
"ansi": "\u001b[0m\u001b[38;5;5march liph \u001b[0m\u001b[38;5;4m/mnt/tank/cloud_phil/consume/paint/invoices\u001b[0m\r\n\u001b[0m\u001b[38;5;5m❯\u001b[0m ",
|
||||||
|
"lines": 2
|
||||||
|
},
|
||||||
|
"3": {
|
||||||
|
"ansi": "\r\n\u001b[0m\u001b[38;5;5march liph \u001b[0m\u001b[38;5;4m~/cloud/consume/paint/invoices\u001b[0m\r\n\u001b[0m\u001b[38;5;5m❯\u001b[0m ",
|
||||||
"lines": 3
|
"lines": 3
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"panes": {
|
"panes": {
|
||||||
"3": {
|
|
||||||
"ansi": "\u001b[0m\u001b[38;2;144;140;170m\u001b[48;2;42;39;63m \u001b[0m\u001b[38;2;81;160;207m\u001b[48;2;42;39;63m\u001b[0m\u001b[38;2;144;140;170m\u001b[48;2;42;39;63m \u001b[0m\u001b[1m\u001b[38;2;144;140;170m\u001b[48;2;42;39;63mmain.lua\u001b[0m\u001b[38;2;144;140;170m\u001b[48;2;42;39;63m \u001b[0m\u001b[38;2;235;111;146m╭──────────── Error ────────────╮\u001b[0m\u001b[38;2;144;140;170m\u001b[48;2;42;39;63m \u001b[0m\r\n 88 \u001b[0m\u001b[4m\u001b[38;2;62;143;176mlocal\u001b[0m\u001b[4m \u001b[0m\u001b[4m\u001b[38;2;62;143;176mfunction\u001b[0m\u001b[4m \u001b[0m\u001b[3m\u001b[4m\u001b[38;2;234;154;151mget_filesystem_extra\u001b[0m\u001b[4m\u001b[38;2;235;111;146m(\u001b[0m\u001b[3m\u001b[4m\u001b[38;2;196;167;231mfile\u001b[0m\u001b[4m\u001b[38;2;235;111;146m)\u001b[0m\u001b[38;2;235;111;146m│\u001b[0m \u001b[0m\u001b[1m\u001b[38;2;235;111;146mLua callback: ...y/rainbow-delim\u001b[0m\u001b[38;2;110;106;134m…\u001b[0m\u001b[38;2;235;111;146m│\u001b[0m \r\n 89 \u001b[0m\u001b[38;2;156;207;216m▏\u001b[0m\u001b[38;2;57;53;82m \u001b[0m\u001b[38;2;62;143;176mlocal\u001b[0m \u001b[0m\u001b[3m\u001b[38;2;224;222;244mresult\u001b[0m \u001b[0m\u001b[38;2;144;140;170m=\u001b[0m \u001b[0m\u001b[38;2;235;111;146m{\u001b[0m \u001b[0m\u001b[38;2;235;111;146m│\u001b[0m \u001b[0m\u001b[1m\u001b[38;2;235;111;146mstack traceback:\u001b[0m\u001b[38;2;110;106;134m \u001b[0m \u001b[0m\u001b[38;2;235;111;146m│\u001b[0m \r\n 90 \u001b[0m\u001b[38;2;156;207;216m▏\u001b[0m\u001b[38;2;57;53;82m ▏ \u001b[0m\u001b[3m\u001b[38;2;156;207;216mfilesystem\u001b[0m \u001b[0m\u001b[38;2;144;140;170m=\u001b[0m \u001b[0m\u001b[38;2;246;193;119m\"\"\u001b[0m\u001b[38;2;144;140;170m,\u001b[0m \u001b[0m\u001b[38;2;235;111;146m│\u001b[0m \u001b[0m\u001b[1m\u001b[38;2;110;106;134m \u001b[0m\u001b[1m\u001b[38;2;235;111;146m...y/rainbow-delimiters.nvim/l\u001b[0m\u001b[38;2;110;106;134m…\u001b[0m\u001b[38;2;235;111;146m│\u001b[0m \r\n 91 \u001b[0m\u001b[38;2;156;207;216m▏\u001b[0m\u001b[38;2;57;53;82m ▏ \u001b[0m\u001b[3m\u001b[38;2;156;207;216mdevice\u001b[0m \u001b[0m\u001b[38;2;144;140;170m=\u001b[0m \u001b[0m\u001b[38;2;246;193;119m\"\"\u001b[0m\u001b[38;2;144;140;170m,\u001b[0m \u001b[0m\u001b[38;2;235;111;146m│\u001b[0m \u001b[0m\u001b[1m\u001b[38;2;110;106;134m \u001b[0m\u001b[1m\u001b[38;2;235;111;146m...zy/rainbow-delimiters.nvim/\u001b[0m\u001b[38;2;110;106;134m…\u001b[0m\u001b[38;2;235;111;146m│\u001b[0m \r\n 92 \u001b[0m\u001b[38;2;156;207;216m▏\u001b[0m\u001b[38;2;57;53;82m ▏ \u001b[0m\u001b[3m\u001b[38;2;156;207;216mtype\u001b[0m \u001b[0m\u001b[38;2;144;140;170m=\u001b[0m \u001b[0m\u001b[38;2;246;193;119m\"\"\u001b[0m\u001b[38;2;144;140;170m,\u001b[0m \u001b[0m\u001b[38;2;235;111;146m│\u001b[0m \u001b[0m\u001b[1m\u001b[38;2;110;106;134m \u001b[0m\u001b[1m\u001b[38;2;235;111;146mC: in function 'nvim_set_optio\u001b[0m\u001b[38;2;110;106;134m…\u001b[0m\u001b[38;2;235;111;146m│\u001b[0m\r\n 93 \u001b[0m\u001b[38;2;156;207;216m▏\u001b[0m\u001b[38;2;57;53;82m ▏ \u001b[0m\u001b[3m\u001b[38;2;156;207;216mused_space\u001b[0m \u001b[0m\u001b[38;2;144;140;170m=\u001b[0m \u001b[0m\u001b[38;2;246;193;119m\"\"\u001b[0m\u001b[38;2;144;140;170m,\u001b[0m \u001b[0m\u001b[38;2;235;111;146m│\u001b[0m \u001b[0m\u001b[1m\u001b[38;2;110;106;134m \u001b[0m\u001b[1m\u001b[38;2;235;111;146m...t/.local/share/nvim/lazy/nu\u001b[0m\u001b[38;2;110;106;134m…\u001b[0m\u001b[38;2;235;111;146m│\u001b[0m\r\n 94 \u001b[0m\u001b[38;2;156;207;216m▏\u001b[0m\u001b[38;2;57;53;82m ▏ \u001b[0m\u001b[3m\u001b[38;2;156;207;216mavail_space\u001b[0m \u001b[0m\u001b[38;2;144;140;170m=\u001b[0m \u001b[0m\u001b[38;2;246;193;119m\"\"\u001b[0m\u001b[38;2;144;140;170m, \u001b[0m\u001b[38;2;235;111;146m│\u001b[0m \u001b[0m\u001b[1m\u001b[38;2;110;106;134m \u001b[0m\u001b[1m\u001b[38;2;235;111;146m...t/.local/share/nvim/lazy/nu\u001b[0m\u001b[38;2;110;106;134m…\u001b[0m\u001b[38;2;235;111;146m│\u001b[0m\r\n 95 \u001b[0m\u001b[38;2;156;207;216m▏\u001b[0m\u001b[38;2;57;53;82m ▏ \u001b[0m\u001b[3m\u001b[38;2;156;207;216mtotal_space\u001b[0m \u001b[0m\u001b[38;2;144;140;170m=\u001b[0m \u001b[0m\u001b[38;2;246;193;119m\"\"\u001b[0m\u001b[38;2;144;140;170m, \u001b[0m\u001b[38;2;235;111;146m│\u001b[0m \u001b[0m\u001b[1m\u001b[38;2;110;106;134m \u001b[0m\u001b[1m\u001b[38;2;235;111;146m...t/.local/share/nvim/lazy/nu\u001b[0m\u001b[38;2;110;106;134m…\u001b[0m\u001b[38;2;235;111;146m│\u001b[0m\r\n 96 \u001b[0m\u001b[38;2;156;207;216m▏\u001b[0m\u001b[38;2;57;53;82m ▏ \u001b[0m\u001b[3m\u001b[38;2;156;207;216mused_space_percent\u001b[0m \u001b[0m\u001b[38;2;144;140;170m=\u001b[0m \u001b[0m\u001b[38;Line truncated
|
|
||||||
"lines": 33
|
|
||||||
},
|
|
||||||
"5": {
|
"5": {
|
||||||
"ansi": "\u001b[0m\u001b[38;5;6m~/.abook\u001b[0m\r\n\u001b[0m\u001b[38;5;7m╭───────────────┬──────────────────────────┬────────────────────────────────────────╮\u001b[0m\r\n\u001b[0m\u001b[38;5;7m│\u001b[0m\u001b[38;5;4m\u001b[0m\u001b[7m\u001b[38;5;4m .abook \u001b[0m\u001b[38;5;4m\u001b[0m\u001b[38;5;7m│\u001b[0m\u001b[0m\u001b[7m addressbook \u001b[0m\u001b[0m\u001b[38;5;7m│\u001b[0m\u001b[38;5;4mMetadata \u001b[0m\u001b[38;5;7m│\u001b[0m\r\n\u001b[0m\u001b[38;5;7m│\u001b[0m\u001b[38;5;4m \u001b[0m\u001b[38;2;3;169;244m \u001b[0m\u001b[38;5;4m.agents \u001b[0m\u001b[38;5;7m│ \u001b[0m\u001b[38;2;255;255;255m \u001b[0maddressbook~ \u001b[0m\u001b[38;5;7m│\u001b[0m File: \u001b[0m\u001b[38;5;4maddressbook \u001b[0m\u001b[38;5;7m│\u001b[0m\r\n\u001b[0m\u001b[38;5;7m│\u001b[0m\u001b[38;5;4m \u001b[0m\u001b[38;2;3;169;244m \u001b[0m\u001b[38;5;4m.ansible \u001b[0m\u001b[38;5;7m│ │\u001b[0m Mimetype: \u001b[0m\u001b[38;5;4mapplication/octet-stream \u001b[0m\u001b[38;5;7m│\u001b[0m\r\n\u001b[0m\u001b[38;5;7m│\u001b[0m\u001b[38;5;4m \u001b[0m\u001b[38;2;3;169;244m \u001b[0m\u001b[38;5;4m.aspnet \u001b[0m\u001b[38;5;7m│ │\u001b[0m Location: \u001b[0m\u001b[38;5;4m/home/liph/.abook \u001b[0m\u001b[38;5;7m│\u001b[0m\r\n\u001b[0m\u001b[38;5;7m│\u001b[0m\u001b[38;5;4m \u001b[0m\u001b[38;2;3;169;244m \u001b[0m\u001b[38;5;4m.atuin \u001b[0m\u001b[38;5;7m│ │\u001b[0m Mode: \u001b[0m\u001b[38;5;4m-rw-r--r-- \u001b[0m\u001b[38;5;7m│\u001b[0m\r\n\u001b[0m\u001b[38;5;7m│\u001b[0m\u001b[38;5;4m \u001b[0m\u001b[38;2;3;169;244m \u001b[0m\u001b[38;5;4m.bun \u001b[0m\u001b[38;5;7m│ │\u001b[0m Attributes: \u001b[0m\u001b[38;5;4m---------------------- \u001b[0m\u001b[38;5;7m│\u001b[0m\r\n\u001b[0m\u001b[38;5;7m│\u001b[0m\u001b[38;5;4m \u001b[0m\u001b[38;2;3;169;244m \u001b[0m\u001b[38;5;4m.cache \u001b[0m\u001b[38;5;7m│ │\u001b[0m Links: \u001b[0m\u001b[38;5;4m1 \u001b[0m\u001b[38;5;7m│\u001b[0m\r\n\u001b[0m\u001b[38;5;7m│\u001b[0m\u001b[38;5;4m \u001b[0m\u001b[38;2;3;169;244m \u001b[0m\u001b[38;5;4m.calcurse \u001b[0m\u001b[38;5;7m│ │\u001b[0m Owner: \u001b[0m\u001b[38;5;4mliph/liph \u001b[0m\u001b[38;5;7m│\u001b[0m\r\n\u001b[0m\u001b[38;5;7m│\u001b[0m\u001b[38;5;4m \u001b[0m\u001b[38;2;3;169;244m \u001b[0m\u001b[38;5;4m.cargo \u001b[0m\u001b[38;5;7m│ │\u001b[0m Size: \u001b[0m\u001b[38;5;4m89.9K \u001b[0m\u001b[38;5;7m│\u001b[0m\r\n\u001b[0m\u001b[38;5;7m│\u001b[0m\u001b[38;5;4m \u001b[0m\u001b[38;2;3;169;244m \u001b[0m\u001b[38;5;4m.claude \u001b[0m\u001b[38;5;7m│ │\u001b[0m Created: \u001b[0m\u001b[38;5;4m2026-08-24 16:28 \u001b[0m\u001b[38;5;7m│\u001b[0m\r\n\u001b[0m\u001b[38;5;7m│\u001b[0m\u001b[38;5;4m \u001b[0m\u001b[38;2;255;152;0m \u001b[0m\u001b[38;5;4m.config \u001b[0m\u001b[38;5;7m│ │\u001b[0m Modified: \u001b[0m\u001b[38;5;4m2026-08-24 16:28 \u001b[0m\u001b[38;5;7m│\u001b[0m\r\n\u001b[0m\u001b[38;5;7m│\u001b[0m\u001b[38;5;4m \u001b[0m\u001b[38;2;3;169;244m \u001b[0m\u001b[38;5;4m.dotnet \u001b[0m\u001b[38;5;7m│ │\u001b[0m Accessed: \u001b[0m\u001b[38;5;4m2026-08-24 16:28 \u001b[0m\u001b[38;5;7m│\u001b[0m\r\n\u001b[0m\u001b[38;5;7m│\u001b[0m\u001b[38;5;4m \u001b[0m\u001b[38;2;3;169;244m \u001b[0m\u001b[38;5;4m.electron-g… \u001b[0m\u001b[38;5;7m│ │\u001b[0m Filesystem: \u001b[0m\u001b[38;5;4m/home \u001b[0m\u001b[38;5;7m│\u001b[0m\r\n\u001b[0m\u001b[38;5;7m│\u001b[0m\u001b[38;5;4m \u001b[0m\u001b[38;2;3;169;244m \u001b[0m\u001b[38;5;4m.Garmin \u001b[0m\u001b[38;5;7m│ │\u001b[0m Device: \u001b[0m\u001b[38;5;4m/dev/sda2 \u001b[0m\u001b[38;5;7m│\u001b[0m\r\n\u001b[0m\u001b[38;5;7m│\u001b[0m\u001b[38;5;4m \u001b[0m\u001b[38;2;3;169;244m \u001b[0m\u001b[38;5;4m.gnupg \u001b[0m\u001b[38;5;7m│ │\u001b[0m Type: \u001b[0m\u001b[38;5;4mbtrfs \u001b[0m\u001b[38;5;7m│\u001b[0m\r\n\u001b[0m\u001b[38;5;7m│\u001b[0m\u001b[38;5;4m \u001b[0m\u001b[38;2;3;169;244m \u001b[0m\u001b[38;5;4m.herdr \u001b[0m\u001b[38;5;7m│ │\u001b[0m Free space: \u001b[0m\u001b[38;5;4mLine truncated
|
"ansi": "\u001b[0m\u001b[38;5;5m❯ \u001b[0m\u001b[4m\u001b[38;5;2msudo\u001b[0m \u001b[0m\u001b[38;5;2mpacman\u001b[0m -S freecad\r\n[sudo] password for liph: \r\nSorry, try again.\r\n[sudo] password for liph: \r\nresolving dependencies...\r\nlooking for conflicting packages...\r\n\r\n\u001b[0m\u001b[1mPackages (30)\u001b[0m coin\u001b[0m\u001b[38;5;243m-1:4.0.10-1\u001b[0m gumbo-parser\u001b[0m\u001b[38;5;243m-0.13.2-1\u001b[0m libspnav\u001b[0m\u001b[38;5;243m-1.2-1\u001b[0m\r\n litehtml\u001b[0m\u001b[38;5;243m-0.10-1\u001b[0m med\u001b[0m\u001b[38;5;243m-5.0.0-4\u001b[0m opencascade\u001b[0m\u001b[38;5;243m-1:7.9.3-3\u001b[0m\r\n pybind11\u001b[0m\u001b[38;5;243m-3.1.0-1\u001b[0m pyside6\u001b[0m\u001b[38;5;243m-6.11.2-1\u001b[0m\r\n python-contourpy\u001b[0m\u001b[38;5;243m-1.4.0-1\u001b[0m python-cycler\u001b[0m\u001b[38;5;243m-0.12.1-4\u001b[0m\r\n python-defusedxml\u001b[0m\u001b[38;5;243m-0.7.1-8\u001b[0m python-filelock\u001b[0m\u001b[38;5;243m-3.31.2-1\u001b[0m\r\n python-fonttools\u001b[0m\u001b[38;5;243m-4.66.1-1\u001b[0m python-kiwisolver\u001b[0m\u001b[38;5;243m-1.5.1-1\u001b[0m\r\n python-matplotlib\u001b[0m\u001b[38;5;243m-3.11.2-1\u001b[0m python-pillow\u001b[0m\u001b[38;5;243m-12.3.0-1\u001b[0m\r\n python-pip\u001b[0m\u001b[38;5;243m-26.2.1-1\u001b[0m python-pivy\u001b[0m\u001b[38;5;243m-1:0.6.11-2\u001b[0m\r\n python-ply\u001b[0m\u001b[38;5;243m-3.11-17\u001b[0m python-pyparsing\u001b[0m\u001b[38;5;243m-3.3.3-1\u001b[0m\r\n python-yaml\u001b[0m\u001b[38;5;243m-6.0.3-2\u001b[0m qhull\u001b[0m\u001b[38;5;243m-2020.2-6\u001b[0m\r\n qt6-tools\u001b[0m\u001b[38;5;243m-6.11.2-1\u001b[0m shiboken6\u001b[0m\u001b[38;5;243m-6.11.2-1\u001b[0m soqt\u001b[0m\u001b[38;5;243m-1.6.4-2\u001b[0m\r\n tcl\u001b[0m\u001b[38;5;243m-8.6.16-1\u001b[0m tk\u001b[0m\u001b[38;5;243m-8.6.16-1\u001b[0m xerces-c\u001b[0m\u001b[38;5;243m-3.3.0-4\u001b[0m\r\n yaml-cpp\u001b[0m\u001b[38;5;243m-0.9.0-1\u001b[0m freecad\u001b[0m\u001b[38;5;243m-1.1.4-1\u001b[0m\r\n\r\n\u001b[0m\u001b[1mTotal Download Size: \u001b[0m 165.09 MiB\r\n\u001b[0m\u001b[1mTotal Installed Size:\u001b[0m 704.81 MiB\r\n\r\n\u001b[0m\u001b[1m\u001b[38;5;4m::\u001b[0m\u001b[1m Proceed with installation? [Y/n] \u001b[0my\r\n\u001b[0m\u001b[1m\u001b[38;5;4m::\u001b[0m\u001b[1m Retrieving packages...\u001b[0m\r\n python-matplotli... 7.0 MiB 4.90 MiB/s 00:01 [-----------] 100%\r\n pyside6-6.11.2-1... 9.1 MiB 6.28 MiB/s 00:01 [-----------] 100%\r\n python-fonttools... 3.1 MiB 3.35 MiB/s 00:01 [-----------] 100%\r\n qt6-tools-6.11.2... 6.6 MiB 2.20 MiB/s 00:03 [-----------] 100%\r\n python-pip-26.2.... 2.8 MiB 3.30 MiB/s 00:01 [-----------] 100%\r\n coin-1:4.0.10-1-... 5.2 MiB 2.58 MiB/s 00:02 [-----------] 100%\r\n python-pivy-1:0.6.11-2-x86_64 2013.9 KiB 2.37 MiB/s 00:01 [-------------------------------------------------] 100%\r\n med-5.0.0-4-x86_64 1569.0 KiB 1235 KiB/s 00:01 [-------------------------------------------------] 100%\r\n xerces-c-3.3.0-4-x86_64 1452.9 KiB 1106 KiB/s 00:01 [-------------------------------------------------] 100%\r\n qhull-2020.2-6-x86_64 1280.5 KiB 1803 KiB/s 00:01 [-------------------------------------------------] 100%\r\n python-pillow-12.3.0-1-x86_64 954.5 KiB 3.54 MiB/s 00:00 [-------------------------------------------------] 100%\r\n litehtml-0.10-1-x86_64 501.3 KiB 2.83 MiB/s 00:00 [-------------------------------------------------] 100%\r\n shiboken6-6.11.2-1-x86_64 306.1 KiB 2.55 MiB/s 00:00 [-------------------------------------------------] 100%\r\n python-contourpy-1.4.0-1-x86_64 272.5 KiB 2.58 MiB/s 00:00 [-------------------------------------------------] 100%\r\n python-pyparsing-3.3.3-1-any 264.4 KiB 2.44 MiB/s 00:00 [-------------------------------------------------] 100%\r\n pybind11-3.1.0-1-any 239.5 KiB 2.13 MiB/s 00:00 [-------------------------------------------------] 100%\r\n python-yaml-6.0.3-2-x86_64 198.3 KiB 2.51 MiB/s 00:00 [-------------------------------------------------] 100%\r\n python-filelock-3.31.2-1-any 197.4 KiB 2035 KiB/s 00:00 [-------------------------------------------------] 100%\r\n yaml-cpp-0.9.0-1-x86_64 153.0 KiB 2.05 MiB/s 00:00 [-------------------------------------------------] 100%\r\n gumbo-parser-0.13.2-1-x86_64 126.9 KiB 1813 KiB/s 00:00 [-------------------------------------------------] 100%\r\n python-ply-3.11-17-Line truncated
|
||||||
"lines": 35
|
"lines": 183
|
||||||
|
},
|
||||||
|
"4": {
|
||||||
|
"ansi": "\r\n\u001b[0m\u001b[38;5;5march liph \u001b[0m\u001b[38;5;4m~\u001b[0m\r\n\u001b[0m\u001b[38;5;5m❯\u001b[0m ",
|
||||||
|
"lines": 3
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"panes": {
|
"panes": {
|
||||||
"6": {
|
"6": {
|
||||||
"ansi": "\u001b[0m\u001b[48;2;35;33;54m phil spam proton \u001b[0m\u001b[1m\u001b[38;2;234;154;151m\u001b[48;2;35;33;54m Complete code challenge \u001b[0m\u001b[48;2;35;33;54m \u001b[0m\r\n\u001b[0m\u001b[1m\u001b[38;2;196;167;231mFrom:\u001b[0m\u001b[38;2;156;207;216m\u001b[48;2;35;33;54m Ollama <no-reply@mail.ollama.com> \u001b[0m\r\n\u001b[0m\u001b[1m\u001b[38;2;196;167;231mTo:\u001b[0m\u001b[38;2;156;207;216m\u001b[48;2;35;33;54m <phil@liphlink.xyz> \u001b[0m\r\n\u001b[0m\u001b[1m\u001b[38;2;196;167;231mSubject:\u001b[0m\u001b[38;2;156;207;216m\u001b[48;2;35;33;54m Complete code challenge \u001b[0m\r\n\u001b[0m\u001b[1m\u001b[38;2;196;167;231mDate:\u001b[0m\u001b[38;2;156;207;216m\u001b[48;2;35;33;54m 2026 Aug 24, 16:27 GMT+0200 \u001b[0m\r\n\u001b[0m\u001b[1m\u001b[38;2;35;33;54m\u001b[48;2;35;33;54m───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[0m\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n\u001b[0m\u001b[38;2;224;222;244mComplete code challenge\u001b[0m Line truncated
|
"ansi": "\u001b[0m\u001b[1m\u001b[38;2;234;154;151m\u001b[48;2;35;33;54m phil \u001b[0m\u001b[48;2;35;33;54m spam proton \u001b[0m\r\n\u001b[0m\u001b[38;2;156;207;216m\u001b[48;2;35;33;54m+Action 0\u001b[0m\u001b[1m\u001b[38;2;35;33;54m\u001b[48;2;35;33;54m│\u001b[0m\u001b[1m\u001b[38;2;224;222;244m\u001b[48;2;42;39;63m O Megan <notifications@courses.ruzuku.… Payment receipt for Unity Tuition Payments & Plans 00:10\u001b[0m\u001b[38;2;35;33;54m\u001b[48;2;110;106;134m \u001b[0m\r\n\u001b[0m\u001b[38;2;156;207;216m\u001b[48;2;35;33;54mArchive 1\u001b[0m\u001b[1m\u001b[38;2;35;33;54m\u001b[48;2;35;33;54m│\u001b[0m\u001b[1m\u001b[38;2;224;222;244m\u001b[48;2;42;39;63m O HealPlayLove Connected <no-reply@not… This week on HealPlayLove Connected: September 24 - Octo… Oct 01\u001b[0m\u001b[38;2;35;33;54m\u001b[48;2;110;106;134m \u001b[0m\r\n\u001b[0m\u001b[38;2;156;207;216m\u001b[48;2;35;33;54m+Community 0\u001b[0m\u001b[1m\u001b[38;2;35;33;54m\u001b[48;2;35;33;54m│\u001b[0m\u001b[1m\u001b[38;2;35;33;54m\u001b[48;2;234;154;151m Ollama <hello@ollama.com> Ollama now supports Jev-like decision models Sep 30\u001b[0m \r\n\u001b[0m\u001b[38;2;156;207;216m\u001b[48;2;35;33;54mDrafts 0\u001b[0m\u001b[1m\u001b[38;2;35;33;54m\u001b[48;2;35;33;54m│\u001b[0m\u001b[1m\u001b[38;2;224;222;244m\u001b[48;2;42;39;63m O Heal Play Love <hello@healplaylove.c… Who brings out the best in you? Sep 29\u001b[0m \r\n\u001b[0m\u001b[38;2;156;207;216m\u001b[48;2;35;33;54mEverything 655 / 1.0K\u001b[0m\u001b[1m\u001b[38;2;35;33;54m\u001b[48;2;35;33;54m│\u001b[0m\u001b[1m\u001b[38;2;224;222;244m\u001b[48;2;42;39;63m O HealPlayLove Connected <no-reply@not… New event: Community Dinner incl Cacao Ceremony in Basel Sep 29\u001b[0m \r\n\u001b[0m\u001b[38;2;156;207;216m\u001b[48;2;35;33;54mFinance 0\u001b[0m\u001b[1m\u001b[38;2;35;33;54m\u001b[48;2;35;33;54m│\u001b[0m\u001b[38;2;110;106;134m\u001b[48;2;42;39;63m HealPlayLove Connected <no-reply@not… ✨ Save the date: Community Dinner in Basel Sep 29\u001b[0m \r\n\u001b[0m\u001b[1m\u001b[38;2;35;33;54m\u001b[48;2;234;154;151mInbox 172 / 346\u001b[0m\u001b[1m\u001b[38;2;35;33;54m\u001b[48;2;35;33;54m│\u001b[0m\u001b[1m\u001b[38;2;224;222;244m\u001b[48;2;42;39;63m O Universal Sounds Schweiz <info@unive… Clara Sophia, Laura Misch, YAIMA & die schönsten Bilder … Sep 29\u001b[0m \r\n\u001b[0m\u001b[38;2;156;207;216m\u001b[48;2;35;33;54mJunk 0\u001b[0m\u001b[1m\u001b[38;2;35;33;54m\u001b[48;2;35;33;54m│\u001b[0m\u001b[1m\u001b[38;2;224;222;244m\u001b[48;2;42;39;63m O HealPlayLove Connected <no-reply@not… Daily breathwork practice Sep 29\u001b[0m \r\n\u001b[0m\u001b[38;2;156;207;216m\u001b[48;2;35;33;54m+Mail 0\u001b[0m\u001b[1m\u001b[38;2;35;33;54m\u001b[48;2;35;33;54m│\u001b[0m\u001b[1m\u001b[38;2;224;222;244m\u001b[48;2;42;39;63m O Megan Ashton <meganashton@gmail.com> Your Friendly Tuition Reminder 🐖 Sep 28\u001b[0m \r\n\u001b[0m\u001b[38;2;156;207;216m\u001b[48;2;35;33;54mNewsletters 0\u001b[0m\u001b[1m\u001b[38;2;35;33;54m\u001b[48;2;35;33;54m│\u001b[0m\u001b[38;2;110;106;134m\u001b[48;2;42;39;63m HealPlayLove Connected <no-reply@not… Be there for Endings & Beginnings: A Breathwork Journey … Sep 28\u001b[0m \r\n\u001b[0m\u001b[38;2;156;207;216m\u001b[48;2;35;33;54mPersonal 0\u001b[0m\u001b[1m\u001b[38;2;35;33;54m\u001b[48;2;35;33;54m│\u001b[0m\u001b[38;2;110;106;134m\u001b[48;2;42;39;63m HealPlayLove Connected <no-reply@not… New event: Endings & Beginnings: A Breathwork Journey fo… Sep 28\u001b[0m \r\n\u001b[0m\u001b[38;2;156;207;216m\u001b[48;2;35;33;54m+Priority 0\u001b[0m\u001b[1m\u001b[38;2;35;33;54m\u001b[48;2;35;33;54m│\u001b[0m\u001b[1m\u001b[38;2;224;222;244m\u001b[48;2;42;39;63m O Breathing Space <notifications@train… Join Now: September: Sunday Sessions is starting Sep 27\u001b[0m \r\n\u001b[0m\u001b[38;2;156;207;216m\u001b[48;2;35;33;54mPriority1 0\u001b[0m\u001b[1m\u001b[38;2;35;33;54m\u001b[48;2;35;33;54m│\u001b[0m\u001b[1m\u001b[38;2;224;222;244m\u001b[48;2;42;39;63m O Breathing Space <notifications@train… Your live breathwork with Benedict is starting in an hour Sep 27\u001b[0m \r\n\u001b[0m\u001b[38;2;156;207;216m\u001b[48;2;35;33;54mPriority2 0\u001b[0m\u001b[1m\u001b[38;2;35;33;54m\u001b[48;2;35;33;54m│\u001b[0m\u001b[1m\u001b[38;2;224;222;244m\u001b[48;2;42;39;63m O HLine truncated
|
||||||
"lines": 39
|
"lines": 41
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -42,8 +54,28 @@
|
|||||||
{
|
{
|
||||||
"panes": {
|
"panes": {
|
||||||
"7": {
|
"7": {
|
||||||
"ansi": "yazi\r\n\u001b[0m\u001b[38;5;5m❯ \u001b[0m\u001b[38;5;2myazi\u001b[0m\r\n\r\n\u001b[0m\u001b[38;5;5march liph \u001b[0m\u001b[38;5;4m~/dotfiles\u001b[0m\u001b[38;5;3m main* \u001b[0m\r\n\u001b[0m\u001b[38;5;5m❯\u001b[0m \u001b[0m\u001b[1m\u001b[7m%\u001b[0m \u001b[0m\u001b[38;5;5march liph \u001b[0m\u001b[38;5;4m~/dotfiles\u001b[0m\u001b[38;5;3m main* \u001b[0m\r\n\u001b[0m\u001b[38;5;5m❯\u001b[0m \u001b[0m\u001b[1m\u001b[7m%\u001b[0m \u001b[0m\u001b[38;5;5march liph \u001b[0m\u001b[38;5;4m~/dotfiles\u001b[0m\u001b[38;5;3m main* \u001b[0m\r\n\u001b[0m\u001b[38;5;5m❯\u001b[0m ",
|
"ansi": "\u001b[0m\u001b[38;5;6m~/dotfiles\u001b[0m\r\n \u001b[0m\u001b[38;5;4m \u001b[0m\u001b[38;2;3;169;244m \u001b[0m\u001b[38;5;4m.atuin \u001b[0m\u001b[38;5;7m│\u001b[0m\u001b[38;5;4m\u001b[0m\u001b[7m\u001b[38;5;4m .git \u001b[0m\u001b[38;5;4m\u001b[0m\u001b[38;5;7m│ \u001b[0mNo items\r\n \u001b[0m\u001b[38;5;4m \u001b[0m\u001b[38;2;3;169;244m \u001b[0m\u001b[38;5;4m.cache \u001b[0m\u001b[38;5;7m│\u001b[0m\u001b[38;5;4m \u001b[0m\u001b[38;2;3;169;244m \u001b[0m\u001b[38;5;4maerc \u001b[0m\u001b[38;5;7m│\u001b[0m\r\n \u001b[0m\u001b[38;5;4m \u001b[0m\u001b[38;2;3;169;244m \u001b[0m\u001b[38;5;4m.cargo \u001b[0m\u001b[38;5;7m│\u001b[0m\u001b[38;5;4m \u001b[0m\u001b[38;2;3;169;244m \u001b[0m\u001b[38;5;4mansible \u001b[0m\u001b[38;5;7m│\u001b[0m\r\n \u001b[0m\u001b[38;5;4m \u001b[0m\u001b[38;2;3;169;244m \u001b[0m\u001b[38;5;4m.claude \u001b[0m\u001b[38;5;7m│\u001b[0m\u001b[38;5;4m \u001b[0m\u001b[38;2;3;169;244m \u001b[0m\u001b[38;5;4matuin \u001b[0m\u001b[38;5;7m│\u001b[0m\r\n \u001b[0m\u001b[38;5;4m \u001b[0m\u001b[38;2;255;152;0m \u001b[0m\u001b[38;5;4m.config \u001b[0m\u001b[38;5;7m│\u001b[0m\u001b[38;5;4m \u001b[0m\u001b[38;2;3;169;244m \u001b[0m\u001b[38;5;4mbackgrounds \u001b[0m\u001b[38;5;7m│\u001b[0m\r\n \u001b[0m\u001b[38;5;4m \u001b[0m\u001b[38;2;3;169;244m \u001b[0m\u001b[38;5;4m.gnupg \u001b[0m\u001b[38;5;7m│\u001b[0m\u001b[38;5;4m \u001b[0m\u001b[38;2;3;169;244m \u001b[0m\u001b[38;5;4mgarmin \u001b[0m\u001b[38;5;7m│\u001b[0m\r\n \u001b[0m\u001b[38;5;4m \u001b[0m\u001b[38;2;3;169;244m \u001b[0m\u001b[38;5;4m.local \u001b[0m\u001b[38;5;7m│\u001b[0m\u001b[38;5;4m \u001b[0m\u001b[38;2;3;169;244m \u001b[0m\u001b[38;5;4mghostty \u001b[0m\u001b[38;5;7m│\u001b[0m\r\n \u001b[0m\u001b[38;5;4m \u001b[0m\u001b[38;2;3;169;244m \u001b[0m\u001b[38;5;4m.locale \u001b[0m\u001b[38;5;7m│\u001b[0m\u001b[38;5;4m \u001b[0m\u001b[38;2;3;169;244m \u001b[0m\u001b[38;5;4mherdr \u001b[0m\u001b[38;5;3m \u001b[0m\u001b[38;5;4m \u001b[0m\u001b[38;5;7m│\u001b[0m\r\n \u001b[0m\u001b[38;5;4m \u001b[0m\u001b[38;2;3;169;244m \u001b[0m\u001b[38;5;4m.mozilla \u001b[0m\u001b[38;5;7m│\u001b[0m\u001b[38;5;4m \u001b[0m\u001b[38;2;3;169;244m \u001b[0m\u001b[38;5;4mhypr \u001b[0m\u001b[38;5;7m│\u001b[0m\r\n \u001b[0m\u001b[38;5;4m \u001b[0m\u001b[38;2;3;169;244m \u001b[0m\u001b[38;5;4m.npm \u001b[0m\u001b[38;5;7m│\u001b[0m\u001b[38;5;4m \u001b[0m\u001b[38;2;3;169;244m \u001b[0m\u001b[38;5;4mkanata \u001b[0m\u001b[38;5;7m│\u001b[0m\r\n \u001b[0m\u001b[38;5;4m \u001b[0m\u001b[38;2;3;169;244m \u001b[0m\u001b[38;5;4m.password-s… \u001b[0m\u001b[38;5;7m│\u001b[0m\u001b[38;5;4m \u001b[0m\u001b[38;2;3;169;244m \u001b[0m\u001b[38;5;4mkitty \u001b[0m\u001b[38;5;7m│\u001b[0m\r\n \u001b[0m\u001b[38;5;4m \u001b[0m\u001b[38;2;158;158;158m \u001b[0m\u001b[38;5;4m.pi\u001b[0m\u001b[3m\u001b[38;5;4m -> dotf\u001b[0m\u001b[38;5;4m… \u001b[0m\u001b[38;5;7m│\u001b[0m\u001b[38;5;4m \u001b[0m\u001b[38;2;3;169;244m \u001b[0m\u001b[38;5;4mniri \u001b[0m\u001b[38;5;7m│\u001b[0m\r\n \u001b[0m\u001b[38;5;4m \u001b[0m\u001b[38;2;3;169;244m \u001b[0m\u001b[38;5;4m.pyenv \u001b[0m\u001b[38;5;7m│\u001b[0m\u001b[38;5;4m \u001b[0m\u001b[38;2;3;169;244m \u001b[0m\u001b[38;5;4mnushell \u001b[0m\u001b[38;5;7m│\u001b[0m\r\n \u001b[0m\u001b[38;5;4m \u001b[0m\u001b[38;2;3;169;244m \u001b[0m\u001b[38;5;4m.rustup \u001b[0m\u001b[38;5;7m│\u001b[0m\u001b[38;5;4m \u001b[0m\u001b[38;2;3;169;244m \u001b[0m\u001b[38;5;4mnvim \u001b[0m\u001b[38;5;7m│\u001b[0m\r\n \u001b[0m\u001b[38;5;4m \u001b[0m\u001b[38;2;158;158;158m \u001b[0m\u001b[38;5;4m.ssh\u001b[0m\u001b[3m\u001b[38;5;4m -> dot\u001b[0m\u001b[38;5;4m… \u001b[0m\u001b[38;5;7m│\u001b[0m\u001b[38;5;4m \u001b[0m\u001b[38;2;3;169;244m \u001b[0m\u001b[38;5;4mohmyposh \u001b[0m\u001b[38;5;7m│\u001b[0m\r\n \u001b[0m\u001b[38;5;4m \u001b[0m\u001b[38;2;3;169;244m \u001b[0m\u001b[38;5;4m.w3m \u001b[0m\u001b[38;5;7m│\u001b[0m\u001b[38;5;4m \u001b[0m\u001b[38;2;3;169;244m \u001b[0m\u001b[38;5;4mopencode \u001b[0m\u001b[38;5;7m│\u001b[0m\r\n \u001b[0m\u001b[38;5;4m \u001b[0m\u001b[38;2;158;158;158m \u001b[0m\u001b[38;5;4maerc_back\u001b[0m\u001b[3m\u001b[38;5;4m -\u001b[0m\u001b[38;5;4m… \u001b[0m\u001b[38;5;7m│\u001b[0m\u001b[38;5;4m \u001b[0m\u001b[38;2;3;169;244m \u001b[0m\u001b[38;5;4mpi \u001b[0m\u001b[38;5;7m│\u001b[0m\r\n \u001b[0m\u001b[38;5;4m \u001b[0m\u001b[38;2Line truncated
|
||||||
"lines": 7
|
"lines": 39
|
||||||
|
},
|
||||||
|
"10": {
|
||||||
|
"ansi": "╭─[1]─Status──────────────────────────╮╮\r\n│\u001b[0m\u001b[38;5;2m✓\u001b[0m dotfiles → main │▐\r\n╰─────────────────────────────────────╯▐\r\n\u001b[0m\u001b[1m\u001b[38;5;2m╭─[2]─Files \u001b[0m- Worktrees - Submodules\u001b[0m\u001b[1m\u001b[38;5;2m──╮\u001b[0m▐\r\n\u001b[0m\u001b[1m\u001b[38;5;2m│\u001b[0m\u001b[1m\u001b[48;5;4m▼ herdr/.config/herdr/sessions/home \u001b[0m\u001b[1m\u001b[38;5;2m│\u001b[0m▐\r\n\u001b[0m\u001b[1m\u001b[38;5;2m│\u001b[0m \u001b[0m\u001b[38;5;1mM\u001b[0m session-history.json \u001b[0m\u001b[1m\u001b[38;5;2m│\u001b[0m│\r\n\u001b[0m\u001b[1m\u001b[38;5;2m│\u001b[0m \u001b[0m\u001b[38;5;1mM\u001b[0m session.json \u001b[0m\u001b[1m\u001b[38;5;2m│\u001b[0m│\r\n\u001b[0m\u001b[1m\u001b[38;5;2m│\u001b[0m \u001b[0m\u001b[1m\u001b[38;5;2m│\u001b[0m│\r\n\u001b[0m\u001b[1m\u001b[38;5;2m│\u001b[0m \u001b[0m\u001b[1m\u001b[38;5;2m│\u001b[0m│\r\n\u001b[0m\u001b[1m\u001b[38;5;2m│\u001b[0m \u001b[0m\u001b[1m\u001b[38;5;2m│\u001b[0m│\r\n\u001b[0m\u001b[1m\u001b[38;5;2m│\u001b[0m \u001b[0m\u001b[1m\u001b[38;5;2m│\u001b[0m│\r\n\u001b[0m\u001b[1m\u001b[38;5;2m│\u001b[0m \u001b[0m\u001b[1m\u001b[38;5;2m│\u001b[0m│\r\n\u001b[0m\u001b[1m\u001b[38;5;2m│\u001b[0m \u001b[0m\u001b[1m\u001b[38;5;2m│\u001b[0m│\r\n\u001b[0m\u001b[1m\u001b[38;5;2m│\u001b[0m \u001b[0m\u001b[1m\u001b[38;5;2m│\u001b[0m│\r\n\u001b[0m\u001b[1m\u001b[38;5;2m│\u001b[0m \u001b[0m\u001b[1m\u001b[38;5;2m│\u001b[0m│\r\n\u001b[0m\u001b[1m\u001b[38;5;2m│\u001b[0m \u001b[0m\u001b[1m\u001b[38;5;2m│\u001b[0m│\r\n\u001b[0m\u001b[1m\u001b[38;5;2m│\u001b[0m \u001b[0m\u001b[1m\u001b[38;5;2m│\u001b[0m│\r\n\u001b[0m\u001b[1m\u001b[38;5;2m│\u001b[0m \u001b[0m\u001b[1m\u001b[38;5;2m│\u001b[0m│\r\n\u001b[0m\u001b[1m\u001b[38;5;2m╰──────────────────────────────1 of 3─╯\u001b[0m│\r\n╭─[3]─\u001b[0m\u001b[38;5;2mLocal branches \u001b[0m- Remotes - Tags─╮│\r\n│\u001b[0m\u001b[38;5;2m *\u001b[0m main \u001b[0m\u001b[38;5;2m✓\u001b[0m ││\r\n│ ││\r\n│ ││\r\n│ ││\r\n│ ││\r\n│ ││\r\n╰──────────────────────────────1 of 1─╯│\r\n╭─[4]─\u001b[0m\u001b[38;5;2mCommits \u001b[0m- Reflog────────────────╮│\r\n│\u001b[0m\u001b[38;5;2m71b27f5a\u001b[0m \u001b[0m\u001b[38;2;36;244;94mli\u001b[0m \u001b[0m\u001b[38;2;36;244;94m○\u001b[0m updated kitty/ghostty e▐│\r\n│\u001b[0m\u001b[38;5;2m6079f663\u001b[0m \u001b[0m\u001b[38;2;36;244;94mli\u001b[0m \u001b[0m\u001b[38;2;36;244;94m○\u001b[0m updated everything ││\r\n│\u001b[0m\u001b[38;5;2md1b2fbdf\u001b[0m \u001b[0m\u001b[38;2;36;244;94mli\u001b[0m \u001b[0m\u001b[38;2;36;244;94m○\u001b[0m updated nvim treesitter││\r\n│\u001b[0m\u001b[38;5;2m893fe34a\u001b[0m \u001b[0m\u001b[38;2;36;244;94mli\u001b[0m \u001b[0m\u001b[38;2;36;244;94m○\u001b[0m updated dotfiles ││\r\n│\u001b[0m\u001b[38;5;2m89b1e380\u001b[0m \u001b[0m\u001b[38;2;36;244;94mli\u001b[0m \u001b[0m\u001b[38;2;36;244;94m○\u001b[0m udated colorscheme ││\r\n│\u001b[0m\u001b[38;5;2me078b218\u001b[0m \u001b[0m\u001b[38;2;36;244;94mli\u001b[0m \u001b[0m\u001b[38;2;36;244;94m○\u001b[0m changed to tokyo night ││\r\n╰─────────────────────────────1 of 80─╯╯\r\n╭─[5]─Stash───────────────────────────╮╮\r\n│\u001b[0m\u001b[38;5;6m3w\u001b[0m On main: tmp install ││\r\n╰──────────────────────────────1 of 1─╯╯\r\n\u001b[0m\u001b[38;5;4mStage: <space\u001b[0m \u001b[0m\u001b[4m\u001b[38;5;5mDonate\u001b[0m \u001b[0m\u001b[4m\u001b[38;5;3mAsk Question\u001b[0m 0.63.0",
|
||||||
|
"lines": 39
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tabs": [
|
||||||
|
{
|
||||||
|
"panes": {
|
||||||
|
"8": {
|
||||||
|
"ansi": "\u001b[0m\u001b[38;5;5march liph \u001b[0m\u001b[38;5;4m/mnt/tank/code\u001b[0m\r\n\u001b[0m\u001b[38;5;5m❯\u001b[0m ",
|
||||||
|
"lines": 2
|
||||||
|
},
|
||||||
|
"9": {
|
||||||
|
"ansi": "\u001b[0m\u001b[38;5;5march liph \u001b[0m\u001b[38;5;4m/mnt/tank/code\u001b[0m\r\n\u001b[0m\u001b[38;5;5m❯\u001b[0m ",
|
||||||
|
"lines": 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,48 @@
|
|||||||
{
|
{
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
|
{
|
||||||
|
"id": "w1",
|
||||||
|
"custom_name": null,
|
||||||
|
"identity_cwd": "/home/liph",
|
||||||
|
"public_pane_numbers": {
|
||||||
|
"1": 1
|
||||||
|
},
|
||||||
|
"next_public_pane_number": 2,
|
||||||
|
"public_tab_numbers": [
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"next_public_tab_number": 2,
|
||||||
|
"tabs": [
|
||||||
|
{
|
||||||
|
"custom_name": null,
|
||||||
|
"layout": {
|
||||||
|
"Pane": 1
|
||||||
|
},
|
||||||
|
"panes": {
|
||||||
|
"1": {
|
||||||
|
"cwd": "/home/liph"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"zoomed": false,
|
||||||
|
"focused": 1,
|
||||||
|
"root_pane": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"active_tab": 0
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "w2",
|
"id": "w2",
|
||||||
"custom_name": "Home",
|
"custom_name": "Home",
|
||||||
"identity_cwd": "/home/liph/dotfiles/yazi/.config/yazi",
|
"identity_cwd": "/mnt/tank/cloud_phil/consume/paint/invoices",
|
||||||
"public_pane_numbers": {
|
"public_pane_numbers": {
|
||||||
"3": 8,
|
"4": 3,
|
||||||
"1": 2,
|
"5": 4,
|
||||||
"5": 7,
|
"3": 2,
|
||||||
"2": 5,
|
"11": 6,
|
||||||
"6": 4
|
"6": 5
|
||||||
},
|
},
|
||||||
"next_public_pane_number": 9,
|
"next_public_pane_number": 7,
|
||||||
"public_tab_numbers": [
|
"public_tab_numbers": [
|
||||||
1,
|
1,
|
||||||
2,
|
2,
|
||||||
@@ -25,26 +55,26 @@
|
|||||||
"layout": {
|
"layout": {
|
||||||
"Split": {
|
"Split": {
|
||||||
"direction": "Horizontal",
|
"direction": "Horizontal",
|
||||||
"ratio": 0.46022728,
|
"ratio": 0.5,
|
||||||
"first": {
|
"first": {
|
||||||
"Pane": 1
|
"Pane": 3
|
||||||
},
|
},
|
||||||
"second": {
|
"second": {
|
||||||
"Pane": 2
|
"Pane": 11
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"panes": {
|
"panes": {
|
||||||
"1": {
|
"11": {
|
||||||
"cwd": "/home/liph/dotfiles/yazi/.config/yazi"
|
"cwd": "/mnt/tank/cloud_phil/consume/paint/invoices"
|
||||||
},
|
},
|
||||||
"2": {
|
"3": {
|
||||||
"cwd": "/home/liph/Pictures"
|
"cwd": "/mnt/tank/cloud_phil/consume/paint/invoices"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"zoomed": false,
|
"zoomed": false,
|
||||||
"focused": 1,
|
"focused": 3,
|
||||||
"root_pane": 1
|
"root_pane": 3
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"custom_name": "Music",
|
"custom_name": "Music",
|
||||||
@@ -53,7 +83,7 @@
|
|||||||
"direction": "Horizontal",
|
"direction": "Horizontal",
|
||||||
"ratio": 0.5,
|
"ratio": 0.5,
|
||||||
"first": {
|
"first": {
|
||||||
"Pane": 3
|
"Pane": 4
|
||||||
},
|
},
|
||||||
"second": {
|
"second": {
|
||||||
"Pane": 5
|
"Pane": 5
|
||||||
@@ -62,15 +92,15 @@
|
|||||||
},
|
},
|
||||||
"panes": {
|
"panes": {
|
||||||
"5": {
|
"5": {
|
||||||
"cwd": "/home/liph/.abook"
|
"cwd": "/home/liph"
|
||||||
},
|
},
|
||||||
"3": {
|
"4": {
|
||||||
"cwd": "/home/liph/dotfiles/yazi/.config/yazi/plugins/file-extra-metadata.yazi"
|
"cwd": "/home/liph"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"zoomed": false,
|
"zoomed": false,
|
||||||
"focused": 3,
|
"focused": 5,
|
||||||
"root_pane": 3
|
"root_pane": 4
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"custom_name": "Aerc",
|
"custom_name": "Aerc",
|
||||||
@@ -94,9 +124,10 @@
|
|||||||
"custom_name": "Dotfiles",
|
"custom_name": "Dotfiles",
|
||||||
"identity_cwd": "/home/liph/dotfiles",
|
"identity_cwd": "/home/liph/dotfiles",
|
||||||
"public_pane_numbers": {
|
"public_pane_numbers": {
|
||||||
"7": 1
|
"7": 1,
|
||||||
|
"10": 2
|
||||||
},
|
},
|
||||||
"next_public_pane_number": 2,
|
"next_public_pane_number": 3,
|
||||||
"public_tab_numbers": [
|
"public_tab_numbers": [
|
||||||
1
|
1
|
||||||
],
|
],
|
||||||
@@ -105,23 +136,84 @@
|
|||||||
{
|
{
|
||||||
"custom_name": "Dotfiles",
|
"custom_name": "Dotfiles",
|
||||||
"layout": {
|
"layout": {
|
||||||
|
"Split": {
|
||||||
|
"direction": "Horizontal",
|
||||||
|
"ratio": 0.71034485,
|
||||||
|
"first": {
|
||||||
"Pane": 7
|
"Pane": 7
|
||||||
},
|
},
|
||||||
|
"second": {
|
||||||
|
"Pane": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"panes": {
|
"panes": {
|
||||||
"7": {
|
"7": {
|
||||||
"cwd": "/home/liph/dotfiles"
|
"cwd": "/home/liph/dotfiles"
|
||||||
|
},
|
||||||
|
"10": {
|
||||||
|
"cwd": "/home/liph/dotfiles",
|
||||||
|
"label": "Git",
|
||||||
|
"launch_argv": [
|
||||||
|
"bash",
|
||||||
|
"-c",
|
||||||
|
"exec bash \"$HERDR_PLUGIN_ROOT/scripts/run-lazygit.sh\""
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"zoomed": false,
|
"zoomed": false,
|
||||||
"focused": 7,
|
"focused": 10,
|
||||||
"root_pane": 7
|
"root_pane": 7
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"active_tab": 0
|
"active_tab": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "w4",
|
||||||
|
"custom_name": "Coding",
|
||||||
|
"identity_cwd": "/mnt/tank/code",
|
||||||
|
"public_pane_numbers": {
|
||||||
|
"9": 2,
|
||||||
|
"8": 1
|
||||||
|
},
|
||||||
|
"next_public_pane_number": 3,
|
||||||
|
"public_tab_numbers": [
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"next_public_tab_number": 2,
|
||||||
|
"tabs": [
|
||||||
|
{
|
||||||
|
"custom_name": "Pi",
|
||||||
|
"layout": {
|
||||||
|
"Split": {
|
||||||
|
"direction": "Horizontal",
|
||||||
|
"ratio": 0.5,
|
||||||
|
"first": {
|
||||||
|
"Pane": 8
|
||||||
|
},
|
||||||
|
"second": {
|
||||||
|
"Pane": 9
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"panes": {
|
||||||
|
"9": {
|
||||||
|
"cwd": "/mnt/tank/code"
|
||||||
|
},
|
||||||
|
"8": {
|
||||||
|
"cwd": "/mnt/tank/code"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"zoomed": false,
|
||||||
|
"focused": 8,
|
||||||
|
"root_pane": 8
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"active_tab": 0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"active": 0,
|
"active": 1,
|
||||||
"selected": 0,
|
"selected": 1,
|
||||||
"sidebar_width": 26,
|
"sidebar_width": 26,
|
||||||
"sidebar_section_split": 0.5,
|
"sidebar_section_split": 0.5,
|
||||||
"collapsed_space_keys": []
|
"collapsed_space_keys": []
|
||||||
|
|||||||
Submodule herdr_1/.config/herdr/plugins/.tmp-install-750065-1785311987680/checkout deleted from 5f76bc9eab.
Submodule herdr_1/.config/herdr/plugins/github/herdr-bar-96eebe452a9c deleted from 58c23e5c8d.
Submodule herdr_1/.config/herdr/plugins/github/herdr-lazygit-b11d62e75f78 deleted from a13e12c99e.
Submodule herdr_1/.config/herdr/plugins/github/herdr-spreader-f248c87aa2e2 deleted from 5f76bc9eab.
Submodule herdr_1/.config/herdr/plugins/github/herdr-zoxide-f7b96c313637 deleted from 7be842fe84.
Submodule herdr_1/.config/herdr/plugins/github/osamahbeig.pane-mover-d60fe0209b83 deleted from a9fde7a4f9.
Submodule herdr_1/.config/herdr/plugins/github/sessionizer-66e03d1ff740 deleted from d3476681fe.
@@ -2,7 +2,7 @@ source = /home/liph/.config/hypr/mocha.conf
|
|||||||
|
|
||||||
$accent = $mauve
|
$accent = $mauve
|
||||||
$accentAlpha = $mauveAlpha
|
$accentAlpha = $mauveAlpha
|
||||||
$font = Inconsolata Nerd Font
|
$font = JetBrainsMono Nerd Font
|
||||||
|
|
||||||
# GENERAL
|
# GENERAL
|
||||||
general {
|
general {
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
;; defsrc is still necessary
|
||||||
|
(defcfg
|
||||||
|
process-unmapped-keys yes
|
||||||
|
)
|
||||||
|
|
||||||
|
(defsrc
|
||||||
|
caps a s d f j k l ;
|
||||||
|
)
|
||||||
|
(defvar
|
||||||
|
tap-time 250
|
||||||
|
hold-time 250
|
||||||
|
)
|
||||||
|
|
||||||
|
(defalias
|
||||||
|
escctrl (tap-hold 250 250 esc lctrl)
|
||||||
|
a (tap-hold $tap-time $hold-time a lmet)
|
||||||
|
s (tap-hold $tap-time $hold-time s lalt)
|
||||||
|
d (tap-hold $tap-time $hold-time d lsft)
|
||||||
|
f (tap-hold $tap-time $hold-time f lctl)
|
||||||
|
j (tap-hold $tap-time $hold-time j rctl)
|
||||||
|
k (tap-hold $tap-time $hold-time k rsft)
|
||||||
|
l (tap-hold $tap-time $hold-time l ralt)
|
||||||
|
; (tap-hold $tap-time $hold-time ; rmet)
|
||||||
|
)
|
||||||
|
|
||||||
|
(deflayer base
|
||||||
|
@escctrl @a @s @d @f @j @k @l @;
|
||||||
|
)
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
;; defsrc is still necessary
|
||||||
|
(defcfg
|
||||||
|
process-unmapped-keys yes
|
||||||
|
)
|
||||||
|
(defsrc
|
||||||
|
caps a s d f h j k l ; v c u i o
|
||||||
|
)
|
||||||
|
(defvar
|
||||||
|
tap-time 250
|
||||||
|
hold-time 250
|
||||||
|
mm-interval 10 ;; ms between pointer moves
|
||||||
|
mm-accel 1000 ;; ms to ramp from slow start to full speed
|
||||||
|
mm-min 2 ;; px per move at start (precision)
|
||||||
|
mm-max 20 ;; px per move at full speed
|
||||||
|
)
|
||||||
|
(defalias
|
||||||
|
escctrl (tap-hold 250 250 esc lctrl)
|
||||||
|
a (tap-hold $tap-time $hold-time a lmet)
|
||||||
|
s (tap-hold $tap-time $hold-time s lalt)
|
||||||
|
d (tap-hold $tap-time $hold-time d lsft)
|
||||||
|
f (tap-hold $tap-time $hold-time f lctl)
|
||||||
|
j (tap-hold $tap-time $hold-time j rctl)
|
||||||
|
k (tap-hold $tap-time $hold-time k rsft)
|
||||||
|
l (tap-hold $tap-time $hold-time l ralt)
|
||||||
|
; (tap-hold $tap-time $hold-time ; rmet)
|
||||||
|
|
||||||
|
;; hold v = pointer layer, tap v = v
|
||||||
|
ptr (layer-while-held pointer)
|
||||||
|
v (tap-hold $tap-time $hold-time v @ptr)
|
||||||
|
|
||||||
|
;; hold c = scroll layer, tap c = c
|
||||||
|
scl (layer-while-held scroll)
|
||||||
|
c (tap-hold $tap-time $hold-time c @scl)
|
||||||
|
|
||||||
|
;; pointer movement (accelerating: precise at first, fast when held)
|
||||||
|
plft (movemouse-accel-left $mm-interval $mm-accel $mm-min $mm-max)
|
||||||
|
pdn (movemouse-accel-down $mm-interval $mm-accel $mm-min $mm-max)
|
||||||
|
pup (movemouse-accel-up $mm-interval $mm-accel $mm-min $mm-max)
|
||||||
|
prgt (movemouse-accel-right $mm-interval $mm-accel $mm-min $mm-max)
|
||||||
|
|
||||||
|
;; scrolling (50 = ms between scrolls · 120 = distance, one wheel notch)
|
||||||
|
sclu (mwheel-up 50 120)
|
||||||
|
scld (mwheel-down 50 120)
|
||||||
|
scll (mwheel-left 50 120)
|
||||||
|
sclr (mwheel-right 50 120)
|
||||||
|
)
|
||||||
|
(deflayer base
|
||||||
|
@escctrl @a @s @d @f h @j @k @l @; @v @c u i o
|
||||||
|
)
|
||||||
|
(deflayer pointer
|
||||||
|
_ ;; caps — transparent → stays esc/ctrl
|
||||||
|
_ ;; a
|
||||||
|
_ ;; s
|
||||||
|
_ ;; d
|
||||||
|
_ ;; f
|
||||||
|
@plft ;; h = move left
|
||||||
|
@pdn ;; j = move down
|
||||||
|
@pup ;; k = move up
|
||||||
|
@prgt ;; l = move right
|
||||||
|
_ ;; ;
|
||||||
|
_ ;; v
|
||||||
|
_ ;; c
|
||||||
|
mlft ;; u = left click (held while pressed → drag works)
|
||||||
|
mmid ;; i = middle click
|
||||||
|
mrgt ;; o = right click
|
||||||
|
)
|
||||||
|
(deflayer scroll
|
||||||
|
_ ;; caps
|
||||||
|
_ ;; a
|
||||||
|
_ ;; s
|
||||||
|
_ ;; d
|
||||||
|
_ ;; f
|
||||||
|
@scll ;; h = scroll left
|
||||||
|
@scld ;; j = scroll down
|
||||||
|
@sclu ;; k = scroll up
|
||||||
|
@sclr ;; l = scroll right
|
||||||
|
_ ;; ;
|
||||||
|
_ ;; v
|
||||||
|
_ ;; c
|
||||||
|
mlft ;; u = left click
|
||||||
|
mmid ;; i = middle click
|
||||||
|
mrgt ;; o = right click
|
||||||
|
)
|
||||||
@@ -2,15 +2,17 @@
|
|||||||
(defcfg
|
(defcfg
|
||||||
process-unmapped-keys yes
|
process-unmapped-keys yes
|
||||||
)
|
)
|
||||||
|
|
||||||
(defsrc
|
(defsrc
|
||||||
caps a s d f j k l ;
|
caps a s d f h j k l ; v c b u i o m n
|
||||||
)
|
)
|
||||||
(defvar
|
(defvar
|
||||||
tap-time 250
|
tap-time 250
|
||||||
hold-time 250
|
hold-time 250
|
||||||
|
mm-interval 10 ;; ms between pointer moves
|
||||||
|
mm-accel 1000 ;; ms to ramp from slow start to full speed
|
||||||
|
mm-min 2 ;; px per move at start (precision)
|
||||||
|
mm-max 20 ;; px per move at full speed
|
||||||
)
|
)
|
||||||
|
|
||||||
(defalias
|
(defalias
|
||||||
escctrl (tap-hold 250 250 esc lctrl)
|
escctrl (tap-hold 250 250 esc lctrl)
|
||||||
a (tap-hold $tap-time $hold-time a lmet)
|
a (tap-hold $tap-time $hold-time a lmet)
|
||||||
@@ -21,8 +23,73 @@
|
|||||||
k (tap-hold $tap-time $hold-time k rsft)
|
k (tap-hold $tap-time $hold-time k rsft)
|
||||||
l (tap-hold $tap-time $hold-time l ralt)
|
l (tap-hold $tap-time $hold-time l ralt)
|
||||||
; (tap-hold $tap-time $hold-time ; rmet)
|
; (tap-hold $tap-time $hold-time ; rmet)
|
||||||
)
|
|
||||||
|
|
||||||
|
;; hold v = pointer layer, tap v = v
|
||||||
|
ptr (layer-while-held pointer)
|
||||||
|
v (tap-hold $tap-time $hold-time v @ptr)
|
||||||
|
|
||||||
|
;; hold c = scroll layer, tap c = c
|
||||||
|
scl (layer-while-held scroll)
|
||||||
|
c (tap-hold $tap-time $hold-time c @scl)
|
||||||
|
|
||||||
|
;; one-shot copy: taps and releases Ctrl+C (pointer layer m and ; fallback)
|
||||||
|
copy (multi lctl c)
|
||||||
|
|
||||||
|
;; cut: taps and releases Ctrl+X (pointer layer n)
|
||||||
|
cut (multi lctl x)
|
||||||
|
|
||||||
|
;; pointer movement (accelerating: precise at first, fast when held)
|
||||||
|
plft (movemouse-accel-left $mm-interval $mm-accel $mm-min $mm-max)
|
||||||
|
pdn (movemouse-accel-down $mm-interval $mm-accel $mm-min $mm-max)
|
||||||
|
pup (movemouse-accel-up $mm-interval $mm-accel $mm-min $mm-max)
|
||||||
|
prgt (movemouse-accel-right $mm-interval $mm-accel $mm-min $mm-max)
|
||||||
|
|
||||||
|
;; scrolling (50 = ms between scrolls · 120 = distance, one wheel notch)
|
||||||
|
sclu (mwheel-up 50 120)
|
||||||
|
scld (mwheel-down 50 120)
|
||||||
|
scll (mwheel-left 50 120)
|
||||||
|
sclr (mwheel-right 50 120)
|
||||||
|
)
|
||||||
(deflayer base
|
(deflayer base
|
||||||
@escctrl @a @s @d @f @j @k @l @;
|
@escctrl @a @s @d @f h @j @k @l @; @v @c b u i o m n
|
||||||
|
)
|
||||||
|
(deflayer pointer
|
||||||
|
_ ;; caps — transparent → stays esc/ctrl
|
||||||
|
_ ;; a
|
||||||
|
_ ;; s
|
||||||
|
mlft ;; d = left click hold (mark/drag)
|
||||||
|
_ ;; f = transparent → Ctrl
|
||||||
|
@plft ;; h = move left
|
||||||
|
@pdn ;; j = move down
|
||||||
|
@pup ;; k = move up
|
||||||
|
@prgt ;; l = move right
|
||||||
|
@copy ;; ; = one-shot copy (fallback)
|
||||||
|
_ ;; v
|
||||||
|
_ ;; c
|
||||||
|
_ ;; b
|
||||||
|
mlft ;; u = left click
|
||||||
|
mmid ;; i = middle click
|
||||||
|
mrgt ;; o = right click
|
||||||
|
@copy ;; m = copy (Ctrl+C)
|
||||||
|
@cut ;; n = cut (Ctrl+X)
|
||||||
|
)
|
||||||
|
(deflayer scroll
|
||||||
|
_ ;; caps
|
||||||
|
_ ;; a
|
||||||
|
_ ;; s
|
||||||
|
_ ;; d
|
||||||
|
_ ;; f
|
||||||
|
@scll ;; h = scroll left
|
||||||
|
@scld ;; j = scroll down
|
||||||
|
@sclu ;; k = scroll up
|
||||||
|
@sclr ;; l = scroll right
|
||||||
|
_ ;; ;
|
||||||
|
_ ;; v
|
||||||
|
_ ;; c
|
||||||
|
_ ;; b
|
||||||
|
mlft ;; u = left click
|
||||||
|
mmid ;; i = middle click
|
||||||
|
mrgt ;; o = right click
|
||||||
|
_ ;; m = plain m
|
||||||
|
_ ;; n = plain n
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
;; defsrc is still necessary
|
||||||
|
(defcfg
|
||||||
|
process-unmapped-keys yes
|
||||||
|
)
|
||||||
|
(defsrc
|
||||||
|
caps a s d f h j k l ; v c u i o
|
||||||
|
)
|
||||||
|
(defvar
|
||||||
|
tap-time 250
|
||||||
|
hold-time 250
|
||||||
|
mm-interval 10 ;; ms between pointer moves
|
||||||
|
mm-accel 1000 ;; ms to ramp from slow start to full speed
|
||||||
|
mm-min 2 ;; px per move at start (precision)
|
||||||
|
mm-max 20 ;; px per move at full speed
|
||||||
|
)
|
||||||
|
(defalias
|
||||||
|
escctrl (tap-hold 250 250 esc lctrl)
|
||||||
|
a (tap-hold $tap-time $hold-time a lmet)
|
||||||
|
s (tap-hold $tap-time $hold-time s lalt)
|
||||||
|
d (tap-hold $tap-time $hold-time d lsft)
|
||||||
|
f (tap-hold $tap-time $hold-time f lctl)
|
||||||
|
j (tap-hold $tap-time $hold-time j rctl)
|
||||||
|
k (tap-hold $tap-time $hold-time k rsft)
|
||||||
|
l (tap-hold $tap-time $hold-time l ralt)
|
||||||
|
; (tap-hold $tap-time $hold-time ; rmet)
|
||||||
|
|
||||||
|
;; hold v = pointer layer, tap v = v
|
||||||
|
ptr (layer-while-held pointer)
|
||||||
|
v (tap-hold $tap-time $hold-time v @ptr)
|
||||||
|
|
||||||
|
;; hold c = scroll layer, tap c = c
|
||||||
|
scl (layer-while-held scroll)
|
||||||
|
c (tap-hold $tap-time $hold-time c @scl)
|
||||||
|
|
||||||
|
;; pointer movement (accelerating: precise at first, fast when held)
|
||||||
|
plft (movemouse-accel-left $mm-interval $mm-accel $mm-min $mm-max)
|
||||||
|
pdn (movemouse-accel-down $mm-interval $mm-accel $mm-min $mm-max)
|
||||||
|
pup (movemouse-accel-up $mm-interval $mm-accel $mm-min $mm-max)
|
||||||
|
prgt (movemouse-accel-right $mm-interval $mm-accel $mm-min $mm-max)
|
||||||
|
|
||||||
|
;; scrolling (50 = ms between scrolls · 120 = distance, one wheel notch)
|
||||||
|
sclu (mwheel-up 50 120)
|
||||||
|
scld (mwheel-down 50 120)
|
||||||
|
scll (mwheel-left 50 120)
|
||||||
|
sclr (mwheel-right 50 120)
|
||||||
|
)
|
||||||
|
(deflayer base
|
||||||
|
@escctrl @a @s @d @f h @j @k @l @; @v @c u i o
|
||||||
|
)
|
||||||
|
(deflayer pointer
|
||||||
|
_ ;; caps — transparent → stays esc/ctrl
|
||||||
|
_ ;; a
|
||||||
|
_ ;; s
|
||||||
|
_ ;; d
|
||||||
|
_ ;; f
|
||||||
|
@plft ;; h = move left
|
||||||
|
@pdn ;; j = move down
|
||||||
|
@pup ;; k = move up
|
||||||
|
@prgt ;; l = move right
|
||||||
|
_ ;; ;
|
||||||
|
_ ;; v
|
||||||
|
_ ;; c
|
||||||
|
mlft ;; u = left click (held while pressed → drag works)
|
||||||
|
mmid ;; i = middle click
|
||||||
|
mrgt ;; o = right click
|
||||||
|
)
|
||||||
|
(deflayer scroll
|
||||||
|
_ ;; caps
|
||||||
|
_ ;; a
|
||||||
|
_ ;; s
|
||||||
|
_ ;; d
|
||||||
|
_ ;; f
|
||||||
|
@scll ;; h = scroll left
|
||||||
|
@scld ;; j = scroll down
|
||||||
|
@sclu ;; k = scroll up
|
||||||
|
@sclr ;; l = scroll right
|
||||||
|
_ ;; ;
|
||||||
|
_ ;; v
|
||||||
|
_ ;; c
|
||||||
|
mlft ;; u = left click
|
||||||
|
mmid ;; i = middle click
|
||||||
|
mrgt ;; o = right click
|
||||||
|
)
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
;; defsrc is still necessary
|
||||||
|
(defcfg
|
||||||
|
process-unmapped-keys yes
|
||||||
|
)
|
||||||
|
(defsrc
|
||||||
|
caps a s d f h j k l ; v c b u i o m
|
||||||
|
)
|
||||||
|
(defvar
|
||||||
|
tap-time 250
|
||||||
|
hold-time 250
|
||||||
|
mm-interval 10 ;; ms between pointer moves
|
||||||
|
mm-accel 1000 ;; ms to ramp from slow start to full speed
|
||||||
|
mm-min 2 ;; px per move at start (precision)
|
||||||
|
mm-max 20 ;; px per move at full speed
|
||||||
|
)
|
||||||
|
(defalias
|
||||||
|
escctrl (tap-hold 250 250 esc lctrl)
|
||||||
|
a (tap-hold $tap-time $hold-time a lmet)
|
||||||
|
s (tap-hold $tap-time $hold-time s lalt)
|
||||||
|
d (tap-hold $tap-time $hold-time d lsft)
|
||||||
|
f (tap-hold $tap-time $hold-time f lctl)
|
||||||
|
j (tap-hold $tap-time $hold-time j rctl)
|
||||||
|
k (tap-hold $tap-time $hold-time k rsft)
|
||||||
|
l (tap-hold $tap-time $hold-time l ralt)
|
||||||
|
; (tap-hold $tap-time $hold-time ; rmet)
|
||||||
|
|
||||||
|
;; hold v = pointer layer, tap v = v
|
||||||
|
ptr (layer-while-held pointer)
|
||||||
|
v (tap-hold $tap-time $hold-time v @ptr)
|
||||||
|
|
||||||
|
;; hold c = scroll layer, tap c = c
|
||||||
|
scl (layer-while-held scroll)
|
||||||
|
c (tap-hold $tap-time $hold-time c @scl)
|
||||||
|
|
||||||
|
;; hold b = pointer layer, tap b = b
|
||||||
|
;; tap-hold-press: a second keypress (e.g. u) immediately selects HOLD →
|
||||||
|
;; chord-style b+u activates the pointer layer instead of typing "bu"
|
||||||
|
b (tap-hold-press $tap-time $hold-time b @ptr)
|
||||||
|
|
||||||
|
;; one-shot copy: taps and releases Ctrl+Shift+C
|
||||||
|
copy (multi lsft lctl c)
|
||||||
|
|
||||||
|
;; pointer movement (accelerating: precise at first, fast when held)
|
||||||
|
plft (movemouse-accel-left $mm-interval $mm-accel $mm-min $mm-max)
|
||||||
|
pdn (movemouse-accel-down $mm-interval $mm-accel $mm-min $mm-max)
|
||||||
|
pup (movemouse-accel-up $mm-interval $mm-accel $mm-min $mm-max)
|
||||||
|
prgt (movemouse-accel-right $mm-interval $mm-accel $mm-min $mm-max)
|
||||||
|
|
||||||
|
;; scrolling (50 = ms between scrolls · 120 = distance, one wheel notch)
|
||||||
|
sclu (mwheel-up 50 120)
|
||||||
|
scld (mwheel-down 50 120)
|
||||||
|
scll (mwheel-left 50 120)
|
||||||
|
sclr (mwheel-right 50 120)
|
||||||
|
)
|
||||||
|
(deflayer base
|
||||||
|
@escctrl @a @s @d @f h @j @k @l @; @v @c @b u i o m
|
||||||
|
)
|
||||||
|
(deflayer pointer
|
||||||
|
_ ;; caps — transparent → stays esc/ctrl
|
||||||
|
_ ;; a
|
||||||
|
_ ;; s
|
||||||
|
_ ;; d
|
||||||
|
mlft ;; f = left click hold (while b held)
|
||||||
|
@plft ;; h = move left
|
||||||
|
@pdn ;; j = move down
|
||||||
|
@pup ;; k = move up
|
||||||
|
@prgt ;; l = move right
|
||||||
|
_ ;; ;
|
||||||
|
_ ;; v
|
||||||
|
_ ;; c
|
||||||
|
_ ;; b
|
||||||
|
_ ;; u = plain u
|
||||||
|
@copy ;; i = one-shot copy (Ctrl+Shift+C)
|
||||||
|
_ ;; o = plain o
|
||||||
|
_ ;; m = plain m
|
||||||
|
)
|
||||||
|
(deflayer scroll
|
||||||
|
_ ;; caps
|
||||||
|
_ ;; a
|
||||||
|
_ ;; s
|
||||||
|
_ ;; d
|
||||||
|
_ ;; f
|
||||||
|
@scll ;; h = scroll left
|
||||||
|
@scld ;; j = scroll down
|
||||||
|
@sclu ;; k = scroll up
|
||||||
|
@sclr ;; l = scroll right
|
||||||
|
_ ;; ;
|
||||||
|
_ ;; v
|
||||||
|
_ ;; c
|
||||||
|
_ ;; b
|
||||||
|
_ ;; u = plain u
|
||||||
|
mmid ;; i = middle click
|
||||||
|
mrgt ;; o = right click
|
||||||
|
mlft ;; m = left click hold
|
||||||
|
)
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
;; defsrc is still necessary
|
||||||
|
(defcfg
|
||||||
|
process-unmapped-keys yes
|
||||||
|
)
|
||||||
|
(defsrc
|
||||||
|
caps a s d f h j k l ; v c b u i o m
|
||||||
|
)
|
||||||
|
(defvar
|
||||||
|
tap-time 250
|
||||||
|
hold-time 250
|
||||||
|
mm-interval 10 ;; ms between pointer moves
|
||||||
|
mm-accel 1000 ;; ms to ramp from slow start to full speed
|
||||||
|
mm-min 2 ;; px per move at start (precision)
|
||||||
|
mm-max 20 ;; px per move at full speed
|
||||||
|
)
|
||||||
|
(defvirtualkeys
|
||||||
|
;; copy virtual key: tapped on mark (d) release → Ctrl+C
|
||||||
|
copyvk (multi lctl c)
|
||||||
|
)
|
||||||
|
(defalias
|
||||||
|
escctrl (tap-hold 250 250 esc lctrl)
|
||||||
|
a (tap-hold $tap-time $hold-time a lmet)
|
||||||
|
s (tap-hold $tap-time $hold-time s lalt)
|
||||||
|
d (tap-hold $tap-time $hold-time d lsft)
|
||||||
|
f (tap-hold $tap-time $hold-time f lctl)
|
||||||
|
j (tap-hold $tap-time $hold-time j rctl)
|
||||||
|
k (tap-hold $tap-time $hold-time k rsft)
|
||||||
|
l (tap-hold $tap-time $hold-time l ralt)
|
||||||
|
; (tap-hold $tap-time $hold-time ; rmet)
|
||||||
|
|
||||||
|
;; hold v = pointer layer, tap v = v
|
||||||
|
ptr (layer-while-held pointer)
|
||||||
|
v (tap-hold $tap-time $hold-time v @ptr)
|
||||||
|
|
||||||
|
;; hold c = scroll layer, tap c = c
|
||||||
|
scl (layer-while-held scroll)
|
||||||
|
c (tap-hold $tap-time $hold-time c @scl)
|
||||||
|
|
||||||
|
;; one-shot copy: taps and releases Ctrl+C (pointer-layer ; fallback)
|
||||||
|
copy (multi lctl c)
|
||||||
|
|
||||||
|
;; mark: left button held while d held; on release, auto-copy selection
|
||||||
|
mark (multi mlft (on-release-fakekey copyvk tap))
|
||||||
|
|
||||||
|
;; pointer movement (accelerating: precise at first, fast when held)
|
||||||
|
plft (movemouse-accel-left $mm-interval $mm-accel $mm-min $mm-max)
|
||||||
|
pdn (movemouse-accel-down $mm-interval $mm-accel $mm-min $mm-max)
|
||||||
|
pup (movemouse-accel-up $mm-interval $mm-accel $mm-min $mm-max)
|
||||||
|
prgt (movemouse-accel-right $mm-interval $mm-accel $mm-min $mm-max)
|
||||||
|
|
||||||
|
;; scrolling (50 = ms between scrolls · 120 = distance, one wheel notch)
|
||||||
|
sclu (mwheel-up 50 120)
|
||||||
|
scld (mwheel-down 50 120)
|
||||||
|
scll (mwheel-left 50 120)
|
||||||
|
sclr (mwheel-right 50 120)
|
||||||
|
)
|
||||||
|
(deflayer base
|
||||||
|
@escctrl @a @s @d @f h @j @k @l @; @v @c b u i o m
|
||||||
|
)
|
||||||
|
(deflayer pointer
|
||||||
|
_ ;; caps — transparent → stays esc/ctrl
|
||||||
|
_ ;; a
|
||||||
|
_ ;; s
|
||||||
|
@mark ;; d = mark: left-click hold, auto-copy on release
|
||||||
|
_ ;; f = transparent → Ctrl
|
||||||
|
@plft ;; h = move left
|
||||||
|
@pdn ;; j = move down
|
||||||
|
@pup ;; k = move up
|
||||||
|
@prgt ;; l = move right
|
||||||
|
@copy ;; ; = one-shot copy (safety fallback)
|
||||||
|
_ ;; v
|
||||||
|
_ ;; c
|
||||||
|
_ ;; b
|
||||||
|
mlft ;; u = left click
|
||||||
|
mmid ;; i = middle click
|
||||||
|
mrgt ;; o = right click
|
||||||
|
_ ;; m = plain m
|
||||||
|
)
|
||||||
|
(deflayer scroll
|
||||||
|
_ ;; caps
|
||||||
|
_ ;; a
|
||||||
|
_ ;; s
|
||||||
|
_ ;; d
|
||||||
|
_ ;; f
|
||||||
|
@scll ;; h = scroll left
|
||||||
|
@scld ;; j = scroll down
|
||||||
|
@sclu ;; k = scroll up
|
||||||
|
@sclr ;; l = scroll right
|
||||||
|
_ ;; ;
|
||||||
|
_ ;; v
|
||||||
|
_ ;; c
|
||||||
|
_ ;; b
|
||||||
|
mlft ;; u = left click
|
||||||
|
mmid ;; i = middle click
|
||||||
|
mrgt ;; o = right click
|
||||||
|
_ ;; m = plain m
|
||||||
|
)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/usr/lib/systemd/user/protonmail-bridge.service
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/home/liph/.config/systemd/user/swayidle.service
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/home/liph/dotfiles/systemd/.config/systemd/user/mailsync-login.timer
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/home/liph/dotfiles/systemd/.config/systemd/user/mailsync.service
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/home/liph/dotfiles/systemd/.config/systemd/user/mailsync.timer
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/home/liph/.config/systemd/user/mailsync-login.timer
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/home/liph/.config/systemd/user/mailsync.timer
|
||||||
@@ -1,56 +1,66 @@
|
|||||||
## name: Rosé Pine Moon
|
# https://draculatheme.com/kitty
|
||||||
## author: mvllow
|
#
|
||||||
## license: MIT
|
# Installation instructions:
|
||||||
## upstream: https://github.com/rose-pine/kitty/blob/main/dist/rose-pine-moon.conf
|
#
|
||||||
## blurb: All natural pine, faux fur and a bit of soho vibes for the classy minimalist
|
# cp dracula.conf ~/.config/kitty/
|
||||||
|
# echo "include dracula.conf" >> ~/.config/kitty/kitty.conf
|
||||||
|
#
|
||||||
|
# Then reload kitty for the config to take affect.
|
||||||
|
# Alternatively copy paste below directly into kitty.conf
|
||||||
|
|
||||||
foreground #e0def4
|
foreground #f8f8f2
|
||||||
background #232136
|
background #282a36
|
||||||
selection_foreground #e0def4
|
selection_foreground #ffffff
|
||||||
selection_background #44415a
|
selection_background #44475a
|
||||||
|
|
||||||
cursor #56526e
|
url_color #8be9fd
|
||||||
cursor_text_color #e0def4
|
|
||||||
|
|
||||||
url_color #c4a7e7
|
|
||||||
|
|
||||||
active_tab_foreground #e0def4
|
|
||||||
active_tab_background #393552
|
|
||||||
inactive_tab_foreground #6e6a86
|
|
||||||
inactive_tab_background #232136
|
|
||||||
|
|
||||||
active_border_color #3e8fb0
|
|
||||||
inactive_border_color #44415a
|
|
||||||
|
|
||||||
# black
|
# black
|
||||||
color0 #393552
|
color0 #21222c
|
||||||
color8 #6e6a86
|
color8 #6272a4
|
||||||
|
|
||||||
# red
|
# red
|
||||||
color1 #eb6f92
|
color1 #ff5555
|
||||||
color9 #eb6f92
|
color9 #ff6e6e
|
||||||
|
|
||||||
# green
|
# green
|
||||||
color2 #3e8fb0
|
color2 #50fa7b
|
||||||
color10 #3e8fb0
|
color10 #69ff94
|
||||||
|
|
||||||
# yellow
|
# yellow
|
||||||
color3 #f6c177
|
color3 #f1fa8c
|
||||||
color11 #f6c177
|
color11 #ffffa5
|
||||||
|
|
||||||
# blue
|
# blue
|
||||||
color4 #9ccfd8
|
color4 #bd93f9
|
||||||
color12 #9ccfd8
|
color12 #d6acff
|
||||||
|
|
||||||
# magenta
|
# magenta
|
||||||
color5 #c4a7e7
|
color5 #ff79c6
|
||||||
color13 #c4a7e7
|
color13 #ff92df
|
||||||
|
|
||||||
# cyan
|
# cyan
|
||||||
color6 #ea9a97
|
color6 #8be9fd
|
||||||
color14 #ea9a97
|
color14 #a4ffff
|
||||||
|
|
||||||
# white
|
# white
|
||||||
color7 #e0def4
|
color7 #f8f8f2
|
||||||
color15 #e0def4
|
color15 #ffffff
|
||||||
|
|
||||||
|
# Cursor colors
|
||||||
|
cursor #f8f8f2
|
||||||
|
cursor_text_color background
|
||||||
|
|
||||||
|
# Tab bar colors
|
||||||
|
active_tab_foreground #282a36
|
||||||
|
active_tab_background #f8f8f2
|
||||||
|
inactive_tab_foreground #282a36
|
||||||
|
inactive_tab_background #6272a4
|
||||||
|
|
||||||
|
# Marks
|
||||||
|
mark1_foreground #282a36
|
||||||
|
mark1_background #ff5555
|
||||||
|
|
||||||
|
# Splits/Windows
|
||||||
|
active_border_color #f8f8f2
|
||||||
|
inactive_border_color #6272a4
|
||||||
+2405
-2
File diff suppressed because it is too large.
Load diff
@@ -1,5 +1,5 @@
|
|||||||
# BEGIN_KITTY_THEME
|
# BEGIN_KITTY_THEME
|
||||||
# Gruvbox Material
|
# Rosé Pine Moon
|
||||||
include current-theme.conf
|
include current-theme.conf
|
||||||
# END_KITTY_THEME
|
# END_KITTY_THEME
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
startgroup
|
||||||
|
animation_start cursor-trail-move
|
||||||
|
animation_stop cursor-trail-stop
|
||||||
|
shaders cursor-trail-blaze
|
||||||
|
endgroup
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
#language slang 2026
|
||||||
|
// Copyright (C) 2026 Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
// Distributed under terms of the GPLv3 license.
|
||||||
|
//
|
||||||
|
// Blaze cursor trail — port of https://gist.github.com/chardskarth/95874c54e29da6b5a36ab7b50ae2d088
|
||||||
|
// by chardskarth. Draws a fading parallelogram trail from the previous cursor
|
||||||
|
// position to the current one, coloured with an amber fill and red-orange border.
|
||||||
|
// Ported to slang for kitty
|
||||||
|
|
||||||
|
import kitty_custom_shader_types;
|
||||||
|
|
||||||
|
static const float4 TRAIL_COLOR = float4(1.0, 0.725, 0.161, 1.0);
|
||||||
|
static const float4 TRAIL_COLOR_ACCENT = float4(1.0, 0.0, 0.0, 1.0);
|
||||||
|
// Trail lifetime in seconds.
|
||||||
|
static const float DURATION = 0.5;
|
||||||
|
|
||||||
|
// pow(1 - x, 10) via repeated squaring — branchless ease-out.
|
||||||
|
static float
|
||||||
|
ease_out10(float x) {
|
||||||
|
float y = 1.0 - x;
|
||||||
|
float y2 = y * y;
|
||||||
|
float y4 = y2 * y2;
|
||||||
|
return y4 * y4 * y2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Smooth blend curve for the progress value.
|
||||||
|
static float
|
||||||
|
blend_smooth(float t) {
|
||||||
|
float s = t * t;
|
||||||
|
return s / (2.0 * (s - t) + 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// SDF of an axis-aligned box. p = query point, center = box centre, hext = half-extents.
|
||||||
|
static float
|
||||||
|
sdBox(float2 p, float2 center, float2 hext) {
|
||||||
|
float2 d = abs(p - center) - hext;
|
||||||
|
return length(max(d, float2(0.0))) + min(max(d.x, d.y), 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single edge contribution for the quadrilateral SDF (signed winding accumulation).
|
||||||
|
static float
|
||||||
|
seg(float2 p, float2 a, float2 b, inout float s, float d) {
|
||||||
|
float2 e = b - a;
|
||||||
|
float2 w = p - a;
|
||||||
|
float2 proj = a + e * clamp(dot(w, e) / dot(e, e), 0.0, 1.0);
|
||||||
|
d = min(d, dot(p - proj, p - proj));
|
||||||
|
|
||||||
|
float c0 = step(0.0, p.y - a.y);
|
||||||
|
float c1 = 1.0 - step(0.0, p.y - b.y);
|
||||||
|
float c2 = 1.0 - step(0.0, e.x * w.y - e.y * w.x);
|
||||||
|
float allCond = c0 * c1 * c2;
|
||||||
|
float noneCond = (1.0 - c0) * (1.0 - c1) * (1.0 - c2);
|
||||||
|
s *= lerp(1.0, -1.0, step(0.5, allCond + noneCond));
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SDF of an arbitrary quadrilateral (uses winding number via four edge contributions).
|
||||||
|
static float
|
||||||
|
sdQuad(float2 p, float2 v0, float2 v1, float2 v2, float2 v3) {
|
||||||
|
float s = 1.0;
|
||||||
|
float d = dot(p - v0, p - v0);
|
||||||
|
d = seg(p, v0, v3, s, d);
|
||||||
|
d = seg(p, v1, v0, s, d);
|
||||||
|
d = seg(p, v2, v1, s, d);
|
||||||
|
d = seg(p, v3, v2, s, d);
|
||||||
|
return s * sqrt(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns 0 for diagonal movement (up-left / down-right), 1 otherwise.
|
||||||
|
// Selects which corner of the parallelogram aligns with the movement direction.
|
||||||
|
static float
|
||||||
|
movementVertexFactor(float2 a, float2 b) {
|
||||||
|
float c1 = step(b.x, a.x) * step(a.y, b.y);
|
||||||
|
float c2 = step(a.x, b.x) * step(b.y, a.y);
|
||||||
|
return 1.0 - max(c1, c2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public float4
|
||||||
|
fragment_main(float4 color, KittyTextures t, KittyCustomShaderData d) {
|
||||||
|
if (d.cursor_trail_color.a <= 0.0) return color;
|
||||||
|
|
||||||
|
float age = clamp((d.timestamp - d.cursor_trail_change_time) / DURATION, 0.0, 1.0);
|
||||||
|
if (age >= 1.0) return color;
|
||||||
|
|
||||||
|
float easedProgress = ease_out10(blend_smooth(age));
|
||||||
|
if (easedProgress <= 0.0) return color;
|
||||||
|
|
||||||
|
// Work in pixel space for correct aspect-ratio distance measurements.
|
||||||
|
float2 pixelDims = float2(d.viewport_size_pixels) * t.viewport.zw;
|
||||||
|
float2 px = t.pos * pixelDims;
|
||||||
|
|
||||||
|
// Current cursor rect in pixel space (edge: .x=left .y=right .z=top .w=bottom, y-up).
|
||||||
|
float curLeft = d.cursor_trail_edge.x * pixelDims.x;
|
||||||
|
float curRight = d.cursor_trail_edge.y * pixelDims.x;
|
||||||
|
float curTop = d.cursor_trail_edge.z * pixelDims.y;
|
||||||
|
float curBottom = d.cursor_trail_edge.w * pixelDims.y;
|
||||||
|
float curWidth = curRight - curLeft;
|
||||||
|
float curHeight = curTop - curBottom;
|
||||||
|
float2 curCenter = float2((curLeft + curRight) * 0.5, (curTop + curBottom) * 0.5);
|
||||||
|
|
||||||
|
float prevLeft = d.cursor_trail_prev_edge.x * pixelDims.x;
|
||||||
|
float prevTop = d.cursor_trail_prev_edge.z * pixelDims.y;
|
||||||
|
float prevBottom = d.cursor_trail_prev_edge.w * pixelDims.y;
|
||||||
|
float2 prevCenter = float2(
|
||||||
|
(d.cursor_trail_prev_edge.x + d.cursor_trail_prev_edge.y) * 0.5 * pixelDims.x,
|
||||||
|
(d.cursor_trail_prev_edge.z + d.cursor_trail_prev_edge.w) * 0.5 * pixelDims.y);
|
||||||
|
|
||||||
|
float lineLength = length(curCenter - prevCenter);
|
||||||
|
// Alpha fades to 0 at the previous-cursor end, reaching full effect at the current cursor.
|
||||||
|
float alphaModifier = min(length(px - curCenter) / (lineLength * easedProgress), 1.0);
|
||||||
|
|
||||||
|
// Build parallelogram vertices in pixel space.
|
||||||
|
float vf = movementVertexFactor(curCenter, prevCenter);
|
||||||
|
float ivf = 1.0 - vf;
|
||||||
|
float2 v0 = float2(lerp(curLeft, curRight, vf), curBottom);
|
||||||
|
float2 v1 = float2(lerp(curLeft, curRight, ivf), curTop);
|
||||||
|
float2 v2 = float2(prevLeft + curWidth * ivf, prevTop);
|
||||||
|
float2 v3 = float2(prevLeft + curWidth * vf, prevBottom);
|
||||||
|
|
||||||
|
float sdfCursor = sdBox(px, curCenter, float2(curWidth, curHeight) * 0.5);
|
||||||
|
float sdfTrail = sdQuad(px, v0, v1, v2, v3);
|
||||||
|
|
||||||
|
// ~2-pixel anti-aliasing range in pixel space.
|
||||||
|
float aaRange = 2.0;
|
||||||
|
float4 newColor = color;
|
||||||
|
// Inner border accent (red-orange inside the trail edges).
|
||||||
|
newColor = lerp(newColor, TRAIL_COLOR_ACCENT, 1.0 - smoothstep(sdfTrail, -aaRange, aaRange * 0.05));
|
||||||
|
// Main amber fill.
|
||||||
|
newColor = lerp(newColor, TRAIL_COLOR, 1.0 - smoothstep(0.0, aaRange, sdfTrail));
|
||||||
|
// Fade: effect is weakest toward the previous cursor end.
|
||||||
|
newColor = lerp(color, newColor, 1.0 - alphaModifier);
|
||||||
|
// Restore original pixels inside the cursor rectangle.
|
||||||
|
return lerp(newColor, color, step(sdfCursor, 0.0));
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Dracula purple with pink accent (default)
|
||||||
|
startgroup
|
||||||
|
var float4 TRAIL_COLOR = float4(0.51, 0.29, 0.95, 1.0)
|
||||||
|
var float4 TRAIL_COLOR_ACCENT = float4(1.00, 0.19, 0.56, 1.0)
|
||||||
|
animation_start cursor-trail-move
|
||||||
|
animation_stop cursor-trail-stop
|
||||||
|
shaders cursor-trail-blaze
|
||||||
|
endgroup
|
||||||
|
|
||||||
|
# Alternate combos (comment out the two lines above, uncomment these):
|
||||||
|
# ---- cyan + green ----
|
||||||
|
# var float4 TRAIL_COLOR = float4(0.26, 0.82, 0.98, 1.0) # #8be9fd
|
||||||
|
# var float4 TRAIL_COLOR_ACCENT = float4(0.08, 0.96, 0.20, 1.0) # #50fa7b
|
||||||
|
# ---- orange + pink ----
|
||||||
|
# var float4 TRAIL_COLOR = float4(1.00, 0.48, 0.15, 1.0) # #ffb86c
|
||||||
|
# var float4 TRAIL_COLOR_ACCENT = float4(1.00, 0.19, 0.56, 1.0) # #ff79c6
|
||||||
|
# ---- yellow + pink ----
|
||||||
|
# var float4 TRAIL_COLOR = float4(0.88, 0.96, 0.26, 1.0) # #f1fa8c
|
||||||
|
# var float4 TRAIL_COLOR_ACCENT = float4(1.00, 0.19, 0.56, 1.0) # #ff79c6
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Dracula purple with pink accent (default)
|
||||||
|
startgroup
|
||||||
|
var float4 TRAIL_COLOR = float4(0.51, 0.29, 0.95, 1.0)
|
||||||
|
var float4 TRAIL_COLOR_ACCENT = float4(1.00, 0.19, 0.56, 1.0)
|
||||||
|
animation_start cursor-trail-move
|
||||||
|
animation_stop cursor-trail-stop
|
||||||
|
shaders cursor-trail-blaze
|
||||||
|
endgroup
|
||||||
|
|
||||||
|
# Alternate combos (comment out the two lines above, uncomment these):
|
||||||
|
# ---- cyan + green ----
|
||||||
|
# var float4 TRAIL_COLOR = float4(0.26, 0.82, 0.98, 1.0) # #8be9fd
|
||||||
|
# var float4 TRAIL_COLOR_ACCENT = float4(0.08, 0.96, 0.20, 1.0) # #50fa7b
|
||||||
|
# ---- orange + pink ----
|
||||||
|
# var float4 TRAIL_COLOR = float4(1.00, 0.48, 0.15, 1.0) # #ffb86c
|
||||||
|
# var float4 TRAIL_COLOR_ACCENT = float4(1.00, 0.19, 0.56, 1.0) # #ff79c6
|
||||||
|
# ---- yellow + pink ----
|
||||||
|
# var float4 TRAIL_COLOR = float4(0.88, 0.96, 0.26, 1.0) # #f1fa8c
|
||||||
|
# var float4 TRAIL_COLOR_ACCENT = float4(1.00, 0.19, 0.56, 1.0) # #ff79c6
|
||||||
+112
-50
@@ -22,6 +22,10 @@ input {
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
cursor {
|
||||||
|
hide-after-inactive-ms 1000 // hide after 1s of no movement
|
||||||
|
hide-when-typing // hide as soon as you start typing
|
||||||
|
}
|
||||||
// x11 support for wayland
|
// x11 support for wayland
|
||||||
xwayland-satellite {
|
xwayland-satellite {
|
||||||
path "xwayland-satellite"
|
path "xwayland-satellite"
|
||||||
@@ -90,17 +94,19 @@ layout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
spawn-at-startup "bash" "/mnt/tank/scripts/fix-bitwarden.sh"
|
spawn-at-startup "bash" "/mnt/tank/scripts/fix-bitwarden.sh"
|
||||||
spawn-at-startup "waybar" "-c" "/home/liph/.config/niri/waybar-niri/config.jsonc" "-s" "/home/liph/.config/niri/waybar-niri/style.css"
|
// spawn-at-startup "waybar" "-c" "/home/liph/.config/niri/waybar-niri/config.jsonc" "-s" "/home/liph/.config/niri/waybar-niri/style.css"
|
||||||
spawn-at-startup "swaync"
|
// spawn-at-startup "swaync" // replaced by quickshell notification layer
|
||||||
|
spawn-at-startup "quickshell"
|
||||||
// spawn-at-startup "kanshi"
|
// spawn-at-startup "kanshi"
|
||||||
spawn-at-startup "awww-daemon"
|
spawn-at-startup "awww-daemon"
|
||||||
spawn-at-startup "xwayland-satellite"
|
spawn-at-startup "xwayland-satellite"
|
||||||
spawn-at-startup "gammastep" "-l" "47.38:8.54" "-t" "6000:4000"
|
spawn-at-startup "gammastep" "-l" "47.38:8.54" "-t" "6000:4000"
|
||||||
spawn-at-startup "wezterm"
|
spawn-at-startup "kitty"
|
||||||
|
// spawn-at-startup "kitty"
|
||||||
spawn-at-startup "obsidian"
|
spawn-at-startup "obsidian"
|
||||||
// spawn-at-startup "kanshi"
|
// spawn-at-startup "kanshi"
|
||||||
spawn-at-startup "librewolf"
|
spawn-at-startup "librewolf"
|
||||||
spawn-at-startup "sh" "-c" "sleep 5 && exec swayidle -w timeout 300 hyprlock timeout 600 'niri msg action power-off-monitors' resume 'niri msg action power-on-monitors' timeout 3600 'systemctl suspend'"
|
spawn-at-startup "swayidle" "-w" "timeout" "200" "hyprlock" "timeout" "600" "niri msg action power-off-monitors" "resume" "niri msg action power-on-monitors" "timeout" "3600" "systemctl suspend"
|
||||||
|
|
||||||
prefer-no-csd
|
prefer-no-csd
|
||||||
gestures {
|
gestures {
|
||||||
@@ -125,47 +131,82 @@ window-rule {
|
|||||||
match app-id="Zoom Workplace"
|
match app-id="Zoom Workplace"
|
||||||
open-floating true
|
open-floating true
|
||||||
}
|
}
|
||||||
// Wezterm window rules
|
|
||||||
|
////////////////////////////////////
|
||||||
|
////// Startup Window Rules ////////
|
||||||
|
////////////////////////////////////
|
||||||
|
|
||||||
|
// Wezterm
|
||||||
window-rule {
|
window-rule {
|
||||||
match at-startup=true app-id="wezterm"
|
match at-startup=true app-id="kitty"
|
||||||
open-on-workspace "1"
|
open-on-workspace "1"
|
||||||
open-maximized true
|
open-maximized true
|
||||||
open-floating false
|
open-floating false
|
||||||
// clip-to-geometry true
|
// clip-to-geometry true
|
||||||
}
|
}
|
||||||
|
// kitty
|
||||||
window-rule {
|
window-rule {
|
||||||
match at-startup=false app-id="wezterm"
|
match at-startup=true app-id="kitty"
|
||||||
|
open-on-workspace "1"
|
||||||
|
open-maximized true
|
||||||
open-floating false
|
open-floating false
|
||||||
// clip-to-geometry true
|
// clip-to-geometry true
|
||||||
}
|
}
|
||||||
// Obsidian window rules
|
// Librewolf
|
||||||
|
window-rule {
|
||||||
|
match at-startup=true app-id="librewolf"
|
||||||
|
open-on-workspace "2"
|
||||||
|
open-maximized true
|
||||||
|
}
|
||||||
|
// Obsidian
|
||||||
window-rule {
|
window-rule {
|
||||||
match at-startup=true app-id="obsidian"
|
match at-startup=true app-id="obsidian"
|
||||||
open-on-workspace "3"
|
open-on-workspace "3"
|
||||||
open-maximized true
|
open-maximized true
|
||||||
open-floating false
|
open-floating false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////
|
||||||
|
/////// Normal Window Rules ////////
|
||||||
|
////////////////////////////////////
|
||||||
|
|
||||||
|
// Wezterm
|
||||||
|
window-rule {
|
||||||
|
match app-id="kitty"
|
||||||
|
open-floating false
|
||||||
|
clip-to-geometry true
|
||||||
|
default-column-width {
|
||||||
|
proportion 0.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Ghostty
|
||||||
|
window-rule {
|
||||||
|
match app-id="kitty"
|
||||||
|
open-floating false
|
||||||
|
clip-to-geometry true
|
||||||
|
default-column-width {
|
||||||
|
proportion 0.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Steam
|
||||||
window-rule {
|
window-rule {
|
||||||
match app-id="steam_app_4197610"
|
match app-id="steam_app_4197610"
|
||||||
open-fullscreen true
|
open-fullscreen true
|
||||||
}
|
}
|
||||||
|
// Obsidian
|
||||||
window-rule {
|
window-rule {
|
||||||
match at-startup=false app-id="obsidian"
|
match at-startup=false app-id="obsidian"
|
||||||
open-maximized true
|
open-maximized true
|
||||||
open-floating false
|
open-floating false
|
||||||
}
|
}
|
||||||
// Librewolf window rules
|
// Librewolf
|
||||||
window-rule {
|
|
||||||
match at-startup=true app-id="librewolf"
|
|
||||||
open-on-workspace "2"
|
|
||||||
open-maximized true
|
|
||||||
}
|
|
||||||
window-rule {
|
window-rule {
|
||||||
match app-id="librewolf"
|
match app-id="librewolf"
|
||||||
open-maximized true
|
open-maximized true
|
||||||
clip-to-geometry true
|
clip-to-geometry true
|
||||||
open-floating false
|
open-floating false
|
||||||
}
|
}
|
||||||
|
// Slack
|
||||||
window-rule {
|
window-rule {
|
||||||
match app-id="slack"
|
match app-id="slack"
|
||||||
open-maximized true
|
open-maximized true
|
||||||
@@ -173,6 +214,7 @@ window-rule {
|
|||||||
open-on-workspace "4"
|
open-on-workspace "4"
|
||||||
open-floating false
|
open-floating false
|
||||||
}
|
}
|
||||||
|
// Tidal
|
||||||
window-rule {
|
window-rule {
|
||||||
match app-id="tidal-hifi"
|
match app-id="tidal-hifi"
|
||||||
open-maximized true
|
open-maximized true
|
||||||
@@ -180,6 +222,7 @@ window-rule {
|
|||||||
open-floating false
|
open-floating false
|
||||||
open-on-workspace "4"
|
open-on-workspace "4"
|
||||||
}
|
}
|
||||||
|
// Tor
|
||||||
window-rule {
|
window-rule {
|
||||||
match app-id="Tor Browser"
|
match app-id="Tor Browser"
|
||||||
open-maximized true
|
open-maximized true
|
||||||
@@ -187,15 +230,14 @@ window-rule {
|
|||||||
open-floating false
|
open-floating false
|
||||||
open-on-workspace "2"
|
open-on-workspace "2"
|
||||||
}
|
}
|
||||||
|
// Claude
|
||||||
window-rule {
|
window-rule {
|
||||||
match app-id="Claude"
|
match app-id="Claude"
|
||||||
open-on-workspace "4"
|
open-on-workspace "4"
|
||||||
default-column-width {
|
open-maximized true
|
||||||
proportion 0.6667
|
|
||||||
}
|
|
||||||
clip-to-geometry true
|
clip-to-geometry true
|
||||||
}
|
}
|
||||||
// 2. Specific rule for windows opening at startup
|
// Betterbird
|
||||||
window-rule {
|
window-rule {
|
||||||
match app-id="eu.betterbird.Betterbird"
|
match app-id="eu.betterbird.Betterbird"
|
||||||
open-maximized true
|
open-maximized true
|
||||||
@@ -203,26 +245,11 @@ window-rule {
|
|||||||
open-on-workspace "3"
|
open-on-workspace "3"
|
||||||
}
|
}
|
||||||
|
|
||||||
window-rule {
|
////////////////////////////////////
|
||||||
match app-id="Slack"
|
/////// Special Window Rules ///////
|
||||||
open-maximized true
|
////////////////////////////////////
|
||||||
clip-to-geometry true
|
|
||||||
open-on-workspace "4"
|
// Htop
|
||||||
}
|
|
||||||
window-rule {
|
|
||||||
match app-id="floating_btop"
|
|
||||||
default-column-width {
|
|
||||||
proportion 0.6667
|
|
||||||
}
|
|
||||||
open-floating true
|
|
||||||
}
|
|
||||||
window-rule {
|
|
||||||
match app-id="floating_musikcube"
|
|
||||||
default-column-width {
|
|
||||||
proportion 0.6667
|
|
||||||
}
|
|
||||||
open-floating true
|
|
||||||
}
|
|
||||||
window-rule {
|
window-rule {
|
||||||
match app-id="floating_htop"
|
match app-id="floating_htop"
|
||||||
default-column-width {
|
default-column-width {
|
||||||
@@ -230,6 +257,23 @@ window-rule {
|
|||||||
}
|
}
|
||||||
open-floating true
|
open-floating true
|
||||||
}
|
}
|
||||||
|
// Btop
|
||||||
|
window-rule {
|
||||||
|
match app-id="floating_btop"
|
||||||
|
default-column-width {
|
||||||
|
proportion 0.6667
|
||||||
|
}
|
||||||
|
open-floating true
|
||||||
|
}
|
||||||
|
// Musikcube
|
||||||
|
window-rule {
|
||||||
|
match app-id="floating_musikcube"
|
||||||
|
default-column-width {
|
||||||
|
proportion 0.6667
|
||||||
|
}
|
||||||
|
open-floating true
|
||||||
|
}
|
||||||
|
// Bluetui
|
||||||
window-rule {
|
window-rule {
|
||||||
match app-id="floating_bluetui"
|
match app-id="floating_bluetui"
|
||||||
default-column-width {
|
default-column-width {
|
||||||
@@ -237,6 +281,7 @@ window-rule {
|
|||||||
}
|
}
|
||||||
open-floating true
|
open-floating true
|
||||||
}
|
}
|
||||||
|
// Pulsemixer
|
||||||
window-rule {
|
window-rule {
|
||||||
match app-id="floating_pulsemixer"
|
match app-id="floating_pulsemixer"
|
||||||
default-column-width {
|
default-column-width {
|
||||||
@@ -244,6 +289,7 @@ window-rule {
|
|||||||
}
|
}
|
||||||
open-floating true
|
open-floating true
|
||||||
}
|
}
|
||||||
|
// Wavemon
|
||||||
window-rule {
|
window-rule {
|
||||||
match app-id="floating_wavemon"
|
match app-id="floating_wavemon"
|
||||||
default-column-width {
|
default-column-width {
|
||||||
@@ -251,6 +297,7 @@ window-rule {
|
|||||||
}
|
}
|
||||||
open-floating true
|
open-floating true
|
||||||
}
|
}
|
||||||
|
// network-tui
|
||||||
window-rule {
|
window-rule {
|
||||||
match app-id="floating_network-tui"
|
match app-id="floating_network-tui"
|
||||||
default-column-width {
|
default-column-width {
|
||||||
@@ -258,6 +305,7 @@ window-rule {
|
|||||||
}
|
}
|
||||||
open-floating true
|
open-floating true
|
||||||
}
|
}
|
||||||
|
// Yazi
|
||||||
window-rule {
|
window-rule {
|
||||||
match app-id="floating_yazi"
|
match app-id="floating_yazi"
|
||||||
default-column-width {
|
default-column-width {
|
||||||
@@ -265,12 +313,18 @@ window-rule {
|
|||||||
}
|
}
|
||||||
open-floating true
|
open-floating true
|
||||||
}
|
}
|
||||||
|
|
||||||
window-rule {
|
window-rule {
|
||||||
geometry-corner-radius 5
|
geometry-corner-radius 5
|
||||||
clip-to-geometry true
|
clip-to-geometry true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////
|
||||||
|
//////////// Keybinds///////////////
|
||||||
|
////////////////////////////////////
|
||||||
|
|
||||||
binds {
|
binds {
|
||||||
// Mod+Shift+Slash { show-hotkey-overlay; }
|
|
||||||
Mod+Mod5+J {
|
Mod+Mod5+J {
|
||||||
spawn "hyprlock"
|
spawn "hyprlock"
|
||||||
}
|
}
|
||||||
@@ -293,34 +347,42 @@ binds {
|
|||||||
spawn "librewolf"
|
spawn "librewolf"
|
||||||
}
|
}
|
||||||
Mod+Alt+Return hotkey-overlay-title="Open Kitty" {
|
Mod+Alt+Return hotkey-overlay-title="Open Kitty" {
|
||||||
spawn "wezterm"
|
spawn "kitty"
|
||||||
}
|
}
|
||||||
Mod+Alt+Space hotkey-overlay-title="Open Wofi" {
|
Mod+Alt+Space hotkey-overlay-title="Open Launcher" {
|
||||||
spawn "wofi" "--show" "drun"
|
spawn "qs" "ipc" "call" "launcher" "toggle"
|
||||||
}
|
}
|
||||||
Mod+Mod5+Q hotkey-overlay-title="Open Htop" {
|
Mod+Mod5+Q hotkey-overlay-title="Open Htop" {
|
||||||
spawn "wezterm" "start" "--class" "floating_htop" "-e" "htop"
|
// spawn "kitty" "start" "--class" "floating_htop" "-e" "htop"
|
||||||
|
spawn "kitty" "--class" "floating_htop" "-e" "htop";
|
||||||
}
|
}
|
||||||
Mod+Mod5+W hotkey-overlay-title="Open Btop" {
|
Mod+Mod5+W hotkey-overlay-title="Open Btop" {
|
||||||
spawn "wezterm" "start" "--class" "floating_btop" "-e" "btop"
|
// spawn "kitty" "start" "--class" "floating_btop" "-e" "btop"
|
||||||
|
spawn "kitty" "--class" "floating_btop" "-e" "btop";
|
||||||
}
|
}
|
||||||
Mod+Mod5+E hotkey-overlay-title="Open Pulsemixer" {
|
Mod+Mod5+E hotkey-overlay-title="Open Pulsemixer" {
|
||||||
spawn "wezterm" "start" "--class" "floating_pulsemixer" "-e" "pulsemixer"
|
// spawn "kitty" "start" "--class" "floating_pulsemixer" "-e" "pulsemixer"
|
||||||
|
spawn "kitty" "--class" "floating_btop" "-e" "pulsemixer";
|
||||||
}
|
}
|
||||||
Mod+Mod5+R allow-inhibiting=false hotkey-overlay-title="Open Bluetui" {
|
Mod+Mod5+R allow-inhibiting=false hotkey-overlay-title="Open Bluetui" {
|
||||||
spawn "wezterm" "start" "--class" "floating_bluetui" "-e" "bluetui"
|
// spawn "kitty" "start" "--class" "floating_bluetui" "-e" "bluetui"
|
||||||
|
spawn "kitty" "--class" "floating_btop" "-e" "bluetui";
|
||||||
}
|
}
|
||||||
Mod+Mod5+T hotkey-overlay-title="Open Wavemon" {
|
Mod+Mod5+T hotkey-overlay-title="Open Wavemon" {
|
||||||
spawn "wezterm" "start" "--class" "floating_wavemon" "-e" "wavemon"
|
// spawn "kitty" "start" "--class" "floating_wavemon" "-e" "wavemon"
|
||||||
|
spawn "kitty" "--class" "floating_btop" "-e" "wavemon";
|
||||||
}
|
}
|
||||||
Mod+Mod5+Z hotkey-overlay-title="Open network-tui" {
|
Mod+Mod5+Z hotkey-overlay-title="Open network-tui" {
|
||||||
spawn "wezterm" "start" "--class" "floating_network-tui" "-e" "network-tui"
|
// spawn "kitty" "start" "--class" "floating_network-tui" "-e" "network-tui"
|
||||||
|
spawn "kitty" "--class" "floating_btop" "-e" "network-tui";
|
||||||
}
|
}
|
||||||
Mod+Mod5+U hotkey-overlay-title="Open Musikcube" {
|
Mod+Mod5+U hotkey-overlay-title="Open Musikcube" {
|
||||||
spawn "wezterm" "start" "--class" "floating_musikcube" "-e" "musikcube"
|
// spawn "kitty" "start" "--class" "floating_musikcube" "-e" "musikcube"
|
||||||
|
spawn "kitty" "--class" "floating_btop" "-e" "musikcube";
|
||||||
}
|
}
|
||||||
Mod+Mod5+Y hotkey-overlay-title="Open Yazi" {
|
Mod+Mod5+Y hotkey-overlay-title="Open Yazi" {
|
||||||
spawn "wezterm" "start" "--class" "floating_yazi" "-e" "yazi"
|
// spawn "kitty" "start" "--class" "floating_yazi" "-e" "yazi"
|
||||||
|
spawn "kitty" "--class" "floating_btop" "-e" "yazi";
|
||||||
}
|
}
|
||||||
// Brightness Controll
|
// Brightness Controll
|
||||||
Mod+Ctrl+0 {
|
Mod+Ctrl+0 {
|
||||||
|
|||||||
@@ -0,0 +1,708 @@
|
|||||||
|
// This config is in the KDL format: https://kdl.dev
|
||||||
|
// "/-" comments out the following node.
|
||||||
|
// Check the wiki for a full description of the configuration:
|
||||||
|
// https://yalter.github.io/niri/Configuration:-Introduction
|
||||||
|
input {
|
||||||
|
keyboard {
|
||||||
|
|
||||||
|
xkb {
|
||||||
|
layout "ch"
|
||||||
|
variant "de_mac"
|
||||||
|
model "apple"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
touchpad {
|
||||||
|
tap
|
||||||
|
natural-scroll
|
||||||
|
}
|
||||||
|
mouse {
|
||||||
|
|
||||||
|
}
|
||||||
|
trackpoint {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cursor {
|
||||||
|
hide-after-inactive-ms 1000 // hide after 1s of no movement
|
||||||
|
hide-when-typing // hide as soon as you start typing
|
||||||
|
}
|
||||||
|
// x11 support for wayland
|
||||||
|
xwayland-satellite {
|
||||||
|
path "xwayland-satellite"
|
||||||
|
}
|
||||||
|
environment {
|
||||||
|
ELECTRON_OZONE_PLATFORM_HINT "auto"
|
||||||
|
ELECTRON_ENABLE_LOGGING "1"
|
||||||
|
// GDK_SCALE "1.75"
|
||||||
|
// GDK_DPI_SCALE "1"
|
||||||
|
}
|
||||||
|
output "eDP-1" {
|
||||||
|
mode "2880x1800@59.990"
|
||||||
|
scale 1.1
|
||||||
|
position x=0 y=0
|
||||||
|
focus-at-startup
|
||||||
|
}
|
||||||
|
output "eDP-2" {
|
||||||
|
mode "2880x1800@59.990"
|
||||||
|
scale 1.1
|
||||||
|
position x=0 y=0
|
||||||
|
focus-at-startup
|
||||||
|
}
|
||||||
|
output "eDP-3" {
|
||||||
|
mode "2880x1800@59.990"
|
||||||
|
scale 1.1
|
||||||
|
position x=0 y=0
|
||||||
|
focus-at-startup
|
||||||
|
}
|
||||||
|
output "DP-4" {
|
||||||
|
mode "2560x1440@59.951"
|
||||||
|
position x=1645 y=0
|
||||||
|
scale 1.1
|
||||||
|
}
|
||||||
|
layout {
|
||||||
|
gaps 2
|
||||||
|
center-focused-column "never"
|
||||||
|
preset-column-widths {
|
||||||
|
proportion 0.5
|
||||||
|
proportion 0.66667
|
||||||
|
proportion 0.3333
|
||||||
|
}
|
||||||
|
default-column-width {
|
||||||
|
proportion 0.5
|
||||||
|
}
|
||||||
|
focus-ring {
|
||||||
|
width 2
|
||||||
|
active-color "#7daea3"
|
||||||
|
inactive-color "#928374"
|
||||||
|
}
|
||||||
|
border {
|
||||||
|
off
|
||||||
|
width 4
|
||||||
|
active-color "#eba0ac"
|
||||||
|
inactive-color "#505050"
|
||||||
|
urgent-color "#9b0000"
|
||||||
|
}
|
||||||
|
shadow {
|
||||||
|
softness 30
|
||||||
|
spread 5
|
||||||
|
offset x=0 y=5
|
||||||
|
color "#0007"
|
||||||
|
}
|
||||||
|
struts {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spawn-at-startup "bash" "/mnt/tank/scripts/fix-bitwarden.sh"
|
||||||
|
// spawn-at-startup "waybar" "-c" "/home/liph/.config/niri/waybar-niri/config.jsonc" "-s" "/home/liph/.config/niri/waybar-niri/style.css"
|
||||||
|
// spawn-at-startup "swaync" // replaced by quickshell notification layer
|
||||||
|
spawn-at-startup "quickshell"
|
||||||
|
// spawn-at-startup "kanshi"
|
||||||
|
spawn-at-startup "awww-daemon"
|
||||||
|
spawn-at-startup "xwayland-satellite"
|
||||||
|
spawn-at-startup "gammastep" "-l" "47.38:8.54" "-t" "6000:4000"
|
||||||
|
spawn-at-startup "wezterm"
|
||||||
|
// spawn-at-startup "ghostty"
|
||||||
|
spawn-at-startup "obsidian"
|
||||||
|
// spawn-at-startup "kanshi"
|
||||||
|
spawn-at-startup "librewolf"
|
||||||
|
spawn-at-startup "swayidle" "-w" "timeout" "200" "hyprlock" "timeout" "600" "niri msg action power-off-monitors" "resume" "niri msg action power-on-monitors" "timeout" "3600" "systemctl suspend"
|
||||||
|
|
||||||
|
prefer-no-csd
|
||||||
|
gestures {
|
||||||
|
hot-corners {
|
||||||
|
off
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
workspace "1"
|
||||||
|
workspace "2"
|
||||||
|
workspace "3"
|
||||||
|
workspace "4"
|
||||||
|
|
||||||
|
// Example: block out two password managers from screen capture.
|
||||||
|
// (This example rule is commented out with a "/-" in front.)
|
||||||
|
/-window-rule {
|
||||||
|
match app-id=r#"^org\.keepassxc\.KeePassXC$"#
|
||||||
|
match app-id=r#"^org\.gnome\.World\.Secrets$"#
|
||||||
|
block-out-from "screen-capture"
|
||||||
|
}
|
||||||
|
window-rule {
|
||||||
|
match app-id="Zoom Workplace"
|
||||||
|
open-floating true
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////
|
||||||
|
////// Startup Window Rules ////////
|
||||||
|
////////////////////////////////////
|
||||||
|
|
||||||
|
// Wezterm
|
||||||
|
window-rule {
|
||||||
|
match at-startup=true app-id="wezterm"
|
||||||
|
open-on-workspace "1"
|
||||||
|
open-maximized true
|
||||||
|
open-floating false
|
||||||
|
// clip-to-geometry true
|
||||||
|
}
|
||||||
|
// ghostty
|
||||||
|
window-rule {
|
||||||
|
match at-startup=true app-id="ghostty"
|
||||||
|
open-on-workspace "1"
|
||||||
|
open-maximized true
|
||||||
|
open-floating false
|
||||||
|
// clip-to-geometry true
|
||||||
|
}
|
||||||
|
// Librewolf
|
||||||
|
window-rule {
|
||||||
|
match at-startup=true app-id="librewolf"
|
||||||
|
open-on-workspace "2"
|
||||||
|
open-maximized true
|
||||||
|
}
|
||||||
|
// Obsidian
|
||||||
|
window-rule {
|
||||||
|
match at-startup=true app-id="obsidian"
|
||||||
|
open-on-workspace "3"
|
||||||
|
open-maximized true
|
||||||
|
open-floating false
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////
|
||||||
|
/////// Normal Window Rules ////////
|
||||||
|
////////////////////////////////////
|
||||||
|
|
||||||
|
// Wezterm
|
||||||
|
window-rule {
|
||||||
|
match app-id="wezterm"
|
||||||
|
open-floating false
|
||||||
|
clip-to-geometry true
|
||||||
|
default-column-width {
|
||||||
|
proportion 0.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Ghostty
|
||||||
|
window-rule {
|
||||||
|
match app-id="ghostty"
|
||||||
|
open-floating false
|
||||||
|
clip-to-geometry true
|
||||||
|
default-column-width {
|
||||||
|
proportion 0.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Steam
|
||||||
|
window-rule {
|
||||||
|
match app-id="steam_app_4197610"
|
||||||
|
open-fullscreen true
|
||||||
|
}
|
||||||
|
// Obsidian
|
||||||
|
window-rule {
|
||||||
|
match at-startup=false app-id="obsidian"
|
||||||
|
open-maximized true
|
||||||
|
open-floating false
|
||||||
|
}
|
||||||
|
// Librewolf
|
||||||
|
window-rule {
|
||||||
|
match app-id="librewolf"
|
||||||
|
open-maximized true
|
||||||
|
clip-to-geometry true
|
||||||
|
open-floating false
|
||||||
|
}
|
||||||
|
// Slack
|
||||||
|
window-rule {
|
||||||
|
match app-id="slack"
|
||||||
|
open-maximized true
|
||||||
|
clip-to-geometry true
|
||||||
|
open-on-workspace "4"
|
||||||
|
open-floating false
|
||||||
|
}
|
||||||
|
// Tidal
|
||||||
|
window-rule {
|
||||||
|
match app-id="tidal-hifi"
|
||||||
|
open-maximized true
|
||||||
|
clip-to-geometry true
|
||||||
|
open-floating false
|
||||||
|
open-on-workspace "4"
|
||||||
|
}
|
||||||
|
// Tor
|
||||||
|
window-rule {
|
||||||
|
match app-id="Tor Browser"
|
||||||
|
open-maximized true
|
||||||
|
clip-to-geometry true
|
||||||
|
open-floating false
|
||||||
|
open-on-workspace "2"
|
||||||
|
}
|
||||||
|
// Claude
|
||||||
|
window-rule {
|
||||||
|
match app-id="Claude"
|
||||||
|
open-on-workspace "4"
|
||||||
|
open-maximized true
|
||||||
|
clip-to-geometry true
|
||||||
|
}
|
||||||
|
// Betterbird
|
||||||
|
window-rule {
|
||||||
|
match app-id="eu.betterbird.Betterbird"
|
||||||
|
open-maximized true
|
||||||
|
clip-to-geometry true
|
||||||
|
open-on-workspace "3"
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////
|
||||||
|
/////// Special Window Rules ///////
|
||||||
|
////////////////////////////////////
|
||||||
|
|
||||||
|
// Htop
|
||||||
|
window-rule {
|
||||||
|
match app-id="floating_htop"
|
||||||
|
default-column-width {
|
||||||
|
proportion 0.6667
|
||||||
|
}
|
||||||
|
open-floating true
|
||||||
|
}
|
||||||
|
// Btop
|
||||||
|
window-rule {
|
||||||
|
match app-id="floating_btop"
|
||||||
|
default-column-width {
|
||||||
|
proportion 0.6667
|
||||||
|
}
|
||||||
|
open-floating true
|
||||||
|
}
|
||||||
|
// Musikcube
|
||||||
|
window-rule {
|
||||||
|
match app-id="floating_musikcube"
|
||||||
|
default-column-width {
|
||||||
|
proportion 0.6667
|
||||||
|
}
|
||||||
|
open-floating true
|
||||||
|
}
|
||||||
|
// Bluetui
|
||||||
|
window-rule {
|
||||||
|
match app-id="floating_bluetui"
|
||||||
|
default-column-width {
|
||||||
|
proportion 0.6667
|
||||||
|
}
|
||||||
|
open-floating true
|
||||||
|
}
|
||||||
|
// Pulsemixer
|
||||||
|
window-rule {
|
||||||
|
match app-id="floating_pulsemixer"
|
||||||
|
default-column-width {
|
||||||
|
proportion 0.6667
|
||||||
|
}
|
||||||
|
open-floating true
|
||||||
|
}
|
||||||
|
// Wavemon
|
||||||
|
window-rule {
|
||||||
|
match app-id="floating_wavemon"
|
||||||
|
default-column-width {
|
||||||
|
proportion 0.6667
|
||||||
|
}
|
||||||
|
open-floating true
|
||||||
|
}
|
||||||
|
// network-tui
|
||||||
|
window-rule {
|
||||||
|
match app-id="floating_network-tui"
|
||||||
|
default-column-width {
|
||||||
|
proportion 0.6667
|
||||||
|
}
|
||||||
|
open-floating true
|
||||||
|
}
|
||||||
|
// Yazi
|
||||||
|
window-rule {
|
||||||
|
match app-id="floating_yazi"
|
||||||
|
default-column-width {
|
||||||
|
proportion 0.6667
|
||||||
|
}
|
||||||
|
open-floating true
|
||||||
|
}
|
||||||
|
|
||||||
|
window-rule {
|
||||||
|
geometry-corner-radius 5
|
||||||
|
clip-to-geometry true
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////
|
||||||
|
//////////// Keybinds///////////////
|
||||||
|
////////////////////////////////////
|
||||||
|
|
||||||
|
binds {
|
||||||
|
|
||||||
|
Mod+Mod5+J {
|
||||||
|
spawn "hyprlock"
|
||||||
|
}
|
||||||
|
Mod+Alt+P hotkey-overlay-title="Open Bitwarden" {
|
||||||
|
spawn "bitwarden-desktop"
|
||||||
|
}
|
||||||
|
Mod+Alt+T hotkey-overlay-title="Open Tipp10" {
|
||||||
|
spawn "tipp10"
|
||||||
|
}
|
||||||
|
Mod+Alt+S hotkey-overlay-title="Open Tidal" {
|
||||||
|
spawn "tidal-hifi" "--ozone-platform=wayland" "--enable-features=WaylandWindowDecorations"
|
||||||
|
}
|
||||||
|
Mod+Alt+O hotkey-overlay-title="Open Obsidian" {
|
||||||
|
spawn "obsidian"
|
||||||
|
}
|
||||||
|
Mod+Alt+C hotkey-overlay-title="Open Claude" {
|
||||||
|
spawn "claude-desktop"
|
||||||
|
}
|
||||||
|
Mod+Alt+Z hotkey-overlay-title="Open Librewolf" {
|
||||||
|
spawn "librewolf"
|
||||||
|
}
|
||||||
|
Mod+Alt+Return hotkey-overlay-title="Open Kitty" {
|
||||||
|
spawn "ghostty"
|
||||||
|
}
|
||||||
|
Mod+Alt+Space hotkey-overlay-title="Open Launcher" {
|
||||||
|
spawn "qs" "ipc" "call" "launcher" "toggle"
|
||||||
|
}
|
||||||
|
Mod+Mod5+Q hotkey-overlay-title="Open Htop" {
|
||||||
|
spawn "wezterm" "start" "--class" "floating_htop" "-e" "htop"
|
||||||
|
}
|
||||||
|
Mod+Mod5+W hotkey-overlay-title="Open Btop" {
|
||||||
|
spawn "wezterm" "start" "--class" "floating_btop" "-e" "btop"
|
||||||
|
}
|
||||||
|
Mod+Mod5+E hotkey-overlay-title="Open Pulsemixer" {
|
||||||
|
spawn "wezterm" "start" "--class" "floating_pulsemixer" "-e" "pulsemixer"
|
||||||
|
}
|
||||||
|
Mod+Mod5+R allow-inhibiting=false hotkey-overlay-title="Open Bluetui" {
|
||||||
|
spawn "wezterm" "start" "--class" "floating_bluetui" "-e" "bluetui"
|
||||||
|
}
|
||||||
|
Mod+Mod5+T hotkey-overlay-title="Open Wavemon" {
|
||||||
|
spawn "wezterm" "start" "--class" "floating_wavemon" "-e" "wavemon"
|
||||||
|
}
|
||||||
|
Mod+Mod5+Z hotkey-overlay-title="Open network-tui" {
|
||||||
|
spawn "wezterm" "start" "--class" "floating_network-tui" "-e" "network-tui"
|
||||||
|
}
|
||||||
|
Mod+Mod5+U hotkey-overlay-title="Open Musikcube" {
|
||||||
|
spawn "wezterm" "start" "--class" "floating_musikcube" "-e" "musikcube"
|
||||||
|
}
|
||||||
|
Mod+Mod5+Y hotkey-overlay-title="Open Yazi" {
|
||||||
|
spawn "wezterm" "start" "--class" "floating_yazi" "-e" "yazi"
|
||||||
|
}
|
||||||
|
// Brightness Controll
|
||||||
|
Mod+Ctrl+0 {
|
||||||
|
spawn "sh" "-c" "~/scripts/layer_notify.sh 0"
|
||||||
|
}
|
||||||
|
Mod+Ctrl+1 {
|
||||||
|
spawn "sh" "-c" "~/scripts/layer_notify.sh 1"
|
||||||
|
}
|
||||||
|
Mod+Ctrl+2 {
|
||||||
|
spawn "sh" "-c" "~/scripts/layer_notify.sh 2"
|
||||||
|
}
|
||||||
|
Mod+Ctrl+3 {
|
||||||
|
spawn "sh" "-c" "~/scripts/layer_notify.sh 3"
|
||||||
|
}
|
||||||
|
// Brightness controls
|
||||||
|
Mod+Ctrl+4 {
|
||||||
|
spawn "sh" "-c" "~/scripts/niri_br_up.sh"
|
||||||
|
}
|
||||||
|
Mod+Ctrl+5 {
|
||||||
|
spawn "sh" "-c" "~/scripts/niri_br_down.sh"
|
||||||
|
}
|
||||||
|
Mod+Ctrl+6 {
|
||||||
|
spawn "sh" "-c" "~/scripts/niri_br_blue.sh"
|
||||||
|
}
|
||||||
|
Mod+Ctrl+7 {
|
||||||
|
spawn "sh" "-c" "~/scripts/niri_br_reset.sh"
|
||||||
|
}
|
||||||
|
// Swaync Notifications
|
||||||
|
Mod+Ctrl+8 {
|
||||||
|
spawn "swaync-client" "-t"
|
||||||
|
}
|
||||||
|
Super+Alt+S allow-when-locked=true hotkey-overlay-title=null {
|
||||||
|
spawn-sh "pkill orca || exec orca"
|
||||||
|
}
|
||||||
|
XF86AudioRaiseVolume allow-when-locked=true {
|
||||||
|
spawn-sh "wpctl set-volume @DEFAULT_AUDIO_SINK@ 0.1+ -l 1.0"
|
||||||
|
}
|
||||||
|
XF86AudioLowerVolume allow-when-locked=true {
|
||||||
|
spawn-sh "wpctl set-volume @DEFAULT_AUDIO_SINK@ 0.1-"
|
||||||
|
}
|
||||||
|
XF86AudioMute allow-when-locked=true {
|
||||||
|
spawn-sh "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle"
|
||||||
|
}
|
||||||
|
XF86AudioMicMute allow-when-locked=true {
|
||||||
|
spawn-sh "wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle"
|
||||||
|
}
|
||||||
|
XF86AudioPlay allow-when-locked=true {
|
||||||
|
spawn-sh "playerctl play-pause"
|
||||||
|
}
|
||||||
|
XF86AudioStop allow-when-locked=true {
|
||||||
|
spawn-sh "playerctl stop"
|
||||||
|
}
|
||||||
|
XF86AudioPrev allow-when-locked=true {
|
||||||
|
spawn-sh "playerctl previous"
|
||||||
|
}
|
||||||
|
XF86AudioNext allow-when-locked=true {
|
||||||
|
spawn-sh "playerctl next"
|
||||||
|
}
|
||||||
|
XF86MonBrightnessUp allow-when-locked=true {
|
||||||
|
spawn "brightnessctl" "--class=backlight" "set" "+10%"
|
||||||
|
}
|
||||||
|
XF86MonBrightnessDown allow-when-locked=true {
|
||||||
|
spawn "brightnessctl" "--class=backlight" "set" "10%-"
|
||||||
|
}
|
||||||
|
Mod+O repeat=false {
|
||||||
|
toggle-overview
|
||||||
|
}
|
||||||
|
Mod+Shift+C repeat=false {
|
||||||
|
close-window
|
||||||
|
}
|
||||||
|
Mod+Left {
|
||||||
|
focus-column-left
|
||||||
|
}
|
||||||
|
Mod+Down {
|
||||||
|
focus-window-down
|
||||||
|
}
|
||||||
|
Mod+Up {
|
||||||
|
focus-window-up
|
||||||
|
}
|
||||||
|
Mod+Right {
|
||||||
|
focus-column-right
|
||||||
|
}
|
||||||
|
Mod+H {
|
||||||
|
focus-column-left
|
||||||
|
}
|
||||||
|
Mod+J {
|
||||||
|
focus-window-down
|
||||||
|
}
|
||||||
|
Mod+K {
|
||||||
|
focus-window-up
|
||||||
|
}
|
||||||
|
Mod+L {
|
||||||
|
focus-column-right
|
||||||
|
}
|
||||||
|
Mod+Ctrl+Left {
|
||||||
|
move-column-left
|
||||||
|
}
|
||||||
|
Mod+Ctrl+Down {
|
||||||
|
move-window-down
|
||||||
|
}
|
||||||
|
Mod+Ctrl+Up {
|
||||||
|
move-window-up
|
||||||
|
}
|
||||||
|
Mod+Ctrl+Right {
|
||||||
|
move-column-right
|
||||||
|
}
|
||||||
|
Mod+Ctrl+H {
|
||||||
|
move-column-left
|
||||||
|
}
|
||||||
|
Mod+Ctrl+J {
|
||||||
|
move-window-down
|
||||||
|
}
|
||||||
|
Mod+Ctrl+K {
|
||||||
|
move-window-up
|
||||||
|
}
|
||||||
|
Mod+Ctrl+L {
|
||||||
|
move-column-right
|
||||||
|
}
|
||||||
|
// Mod+Shift+Down { focus-monitor-down; }
|
||||||
|
// Mod+Shift+Up { focus-monitor-up; }
|
||||||
|
// Mod+Shift+Right { focus-monitor-right; }
|
||||||
|
Mod+Shift+H {
|
||||||
|
focus-monitor-left
|
||||||
|
}
|
||||||
|
// Mod+Shift+J { focus-monitor-down; }
|
||||||
|
// Mod+Shift+K { focus-monitor-up; }
|
||||||
|
Mod+Shift+L {
|
||||||
|
focus-monitor-right
|
||||||
|
}
|
||||||
|
Mod+Shift+Ctrl+Left {
|
||||||
|
move-column-to-monitor-left
|
||||||
|
}
|
||||||
|
// Mod+Shift+Ctrl+Down { move-column-to-monitor-down; }
|
||||||
|
// Mod+Shift+Ctrl+Up { move-column-to-monitor-up; }
|
||||||
|
Mod+Shift+Ctrl+Right {
|
||||||
|
move-column-to-monitor-right
|
||||||
|
}
|
||||||
|
Mod+Shift+Ctrl+H {
|
||||||
|
move-column-to-monitor-left
|
||||||
|
}
|
||||||
|
// Mod+Shift+Ctrl+J { move-column-to-monitor-down; }
|
||||||
|
// Mod+Shift+Ctrl+K { move-column-to-monitor-up; }
|
||||||
|
Mod+Shift+Ctrl+L {
|
||||||
|
move-column-to-monitor-right
|
||||||
|
}
|
||||||
|
Mod+U {
|
||||||
|
focus-workspace-down
|
||||||
|
}
|
||||||
|
Mod+I {
|
||||||
|
focus-workspace-up
|
||||||
|
}
|
||||||
|
Mod+Alt+J {
|
||||||
|
move-column-to-workspace-down
|
||||||
|
}
|
||||||
|
Mod+Alt+K {
|
||||||
|
move-column-to-workspace-up
|
||||||
|
}
|
||||||
|
Mod+Alt+U {
|
||||||
|
move-column-to-workspace-down
|
||||||
|
}
|
||||||
|
Mod+Alt+I {
|
||||||
|
move-column-to-workspace-up
|
||||||
|
}
|
||||||
|
Mod+Shift+U {
|
||||||
|
move-workspace-down
|
||||||
|
}
|
||||||
|
Mod+Shift+I {
|
||||||
|
move-workspace-up
|
||||||
|
}
|
||||||
|
Mod+WheelScrollDown cooldown-ms=150 {
|
||||||
|
focus-workspace-down
|
||||||
|
}
|
||||||
|
Mod+WheelScrollUp cooldown-ms=150 {
|
||||||
|
focus-workspace-up
|
||||||
|
}
|
||||||
|
Mod+Ctrl+WheelScrollDown cooldown-ms=150 {
|
||||||
|
move-column-to-workspace-down
|
||||||
|
}
|
||||||
|
Mod+Ctrl+WheelScrollUp cooldown-ms=150 {
|
||||||
|
move-column-to-workspace-up
|
||||||
|
}
|
||||||
|
Mod+WheelScrollRight {
|
||||||
|
focus-column-right
|
||||||
|
}
|
||||||
|
Mod+WheelScrollLeft {
|
||||||
|
focus-column-left
|
||||||
|
}
|
||||||
|
Mod+Ctrl+WheelScrollRight {
|
||||||
|
move-column-right
|
||||||
|
}
|
||||||
|
Mod+Ctrl+WheelScrollLeft {
|
||||||
|
move-column-left
|
||||||
|
}
|
||||||
|
Mod+Shift+WheelScrollDown {
|
||||||
|
focus-column-right
|
||||||
|
}
|
||||||
|
Mod+Shift+WheelScrollUp {
|
||||||
|
focus-column-left
|
||||||
|
}
|
||||||
|
Mod+Ctrl+Shift+WheelScrollDown {
|
||||||
|
move-column-right
|
||||||
|
}
|
||||||
|
Mod+Ctrl+Shift+WheelScrollUp {
|
||||||
|
move-column-left
|
||||||
|
}
|
||||||
|
Mod+1 {
|
||||||
|
focus-workspace 1
|
||||||
|
}
|
||||||
|
Mod+2 {
|
||||||
|
focus-workspace 2
|
||||||
|
}
|
||||||
|
Mod+3 {
|
||||||
|
focus-workspace 3
|
||||||
|
}
|
||||||
|
Mod+4 {
|
||||||
|
focus-workspace 4
|
||||||
|
}
|
||||||
|
Mod+5 {
|
||||||
|
focus-workspace 5
|
||||||
|
}
|
||||||
|
Mod+6 {
|
||||||
|
focus-workspace 6
|
||||||
|
}
|
||||||
|
Mod+7 {
|
||||||
|
focus-workspace 7
|
||||||
|
}
|
||||||
|
Mod+8 {
|
||||||
|
focus-workspace 8
|
||||||
|
}
|
||||||
|
Mod+9 {
|
||||||
|
focus-workspace 9
|
||||||
|
}
|
||||||
|
// Mod+Ctrl+1 { move-column-to-workspace 1; }
|
||||||
|
// Mod+Ctrl+2 { move-column-to-workspace 2; }
|
||||||
|
// Mod+Ctrl+3 { move-column-to-workspace 3; }
|
||||||
|
// Mod+Ctrl+4 { move-column-to-workspace 4; }
|
||||||
|
// Mod+Ctrl+5 { move-column-to-workspace 5; }
|
||||||
|
// Mod+Ctrl+6 { move-column-to-workspace 6; }
|
||||||
|
// Mod+Ctrl+7 { move-column-to-workspace 7; }
|
||||||
|
// Mod+Ctrl+8 { move-column-to-workspace 8; }
|
||||||
|
// Mod+Ctrl+9 { move-column-to-workspace 9; }
|
||||||
|
Mod+BracketLeft {
|
||||||
|
consume-or-expel-window-left
|
||||||
|
}
|
||||||
|
Mod+BracketRight {
|
||||||
|
consume-or-expel-window-right
|
||||||
|
}
|
||||||
|
Mod+Comma {
|
||||||
|
consume-window-into-column
|
||||||
|
}
|
||||||
|
Mod+Period {
|
||||||
|
expel-window-from-column
|
||||||
|
}
|
||||||
|
Mod+R {
|
||||||
|
switch-preset-column-width
|
||||||
|
}
|
||||||
|
// Cycling through the presets in reverse order is also possible.
|
||||||
|
// Mod+R { switch-preset-column-width-back; }
|
||||||
|
// Mod+Shift+R {
|
||||||
|
// switch-preset-window-height
|
||||||
|
// }
|
||||||
|
// Mod+Ctrl+R {
|
||||||
|
// reset-window-height
|
||||||
|
// }
|
||||||
|
Mod+F {
|
||||||
|
maximize-column
|
||||||
|
}
|
||||||
|
Mod+Shift+F {
|
||||||
|
fullscreen-window
|
||||||
|
}
|
||||||
|
Mod+Ctrl+F {
|
||||||
|
expand-column-to-available-width
|
||||||
|
}
|
||||||
|
// Mod+C { center-column; }
|
||||||
|
Mod+Ctrl+C {
|
||||||
|
center-visible-columns
|
||||||
|
}
|
||||||
|
Mod+Minus {
|
||||||
|
set-column-width "-10%"
|
||||||
|
}
|
||||||
|
Mod+Equal {
|
||||||
|
set-column-width "+10%"
|
||||||
|
}
|
||||||
|
// Finer height adjustments when in column with other windows.
|
||||||
|
Mod+Shift+Minus {
|
||||||
|
set-window-height "-10%"
|
||||||
|
}
|
||||||
|
Mod+Shift+Equal {
|
||||||
|
set-window-height "+10%"
|
||||||
|
}
|
||||||
|
// Move the focused window between the floating and the tiling layout.
|
||||||
|
Mod+V {
|
||||||
|
toggle-window-floating
|
||||||
|
}
|
||||||
|
Mod+Shift+V {
|
||||||
|
switch-focus-between-floating-and-tiling
|
||||||
|
}
|
||||||
|
Mod+W {
|
||||||
|
toggle-column-tabbed-display
|
||||||
|
}
|
||||||
|
// screenshot
|
||||||
|
Mod+Alt+4 {
|
||||||
|
spawn "sh" "-c" "grim -g \"$(slurp)\" ~/Pictures/screenshot-$(date +%Y%m%d-%H%M%S).png && notify-send 'Screenshot' 'Area screenshot saved'"
|
||||||
|
}
|
||||||
|
// Fullscreen and save
|
||||||
|
Mod+Alt+5 {
|
||||||
|
spawn "sh" "-c" "grim ~/Pictures/screenshot-$(date +%Y%m%d-%H%M%S).png && notify-send -t 2000 'Screenshot' 'Fullscreen screenshot saved'"
|
||||||
|
}
|
||||||
|
// Select area to clipboard
|
||||||
|
Mod+Shift+4 {
|
||||||
|
spawn "sh" "-c" "grim -g \"$(slurp)\" - | wl-copy && notify-send -t 2000 'Screenshot' 'Area copied to clipboard'"
|
||||||
|
}
|
||||||
|
// area screenshot → annotate in swappy
|
||||||
|
Mod+Shift+5 {
|
||||||
|
spawn "sh" "-c" "grim -g \"$(slurp)\" - | swappy -f -"
|
||||||
|
}
|
||||||
|
Mod+Escape allow-inhibiting=false {
|
||||||
|
toggle-keyboard-shortcuts-inhibit
|
||||||
|
}
|
||||||
|
Mod+Shift+E {
|
||||||
|
quit
|
||||||
|
}
|
||||||
|
Ctrl+Alt+Delete {
|
||||||
|
quit
|
||||||
|
}
|
||||||
|
Mod+Shift+P {
|
||||||
|
power-off-monitors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// include "theme.kdl"
|
||||||
|
|
||||||
@@ -140,3 +140,78 @@ end
|
|||||||
-- Keybinds
|
-- Keybinds
|
||||||
vim.keymap.set("i", "<C-f>", pick_email, { buffer = true, desc = "Pick email from abook" })
|
vim.keymap.set("i", "<C-f>", pick_email, { buffer = true, desc = "Pick email from abook" })
|
||||||
vim.keymap.set("n", "<C-f>", pick_email, { buffer = true, desc = "Pick email from abook" })
|
vim.keymap.set("n", "<C-f>", pick_email, { buffer = true, desc = "Pick email from abook" })
|
||||||
|
|
||||||
|
-- Jump to the Subject line when a fresh (non-reply) compose opens
|
||||||
|
local function is_reply_buffer(buf)
|
||||||
|
local lines = vim.api.nvim_buf_get_lines(buf, 0, 40, false)
|
||||||
|
for _, l in ipairs(lines) do
|
||||||
|
if l:match("^On .+ wrote:") or l:match("^%s*>") or l:match("^In%-Reply%-To:") then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
if not is_reply_buffer(0) then
|
||||||
|
local buf = 0
|
||||||
|
for i, l in ipairs(vim.api.nvim_buf_get_lines(buf, 0, 30, false)) do
|
||||||
|
if l:match("^%s*[Ss]ubject:") then
|
||||||
|
vim.api.nvim_win_set_cursor(0, { i, #l })
|
||||||
|
vim.schedule(function()
|
||||||
|
if vim.bo.filetype == "mail" or vim.bo[buf].filetype == "mail" then
|
||||||
|
vim.cmd("startinsert!")
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Attach files to the aerc draft via yazi (terminal split + aerc IPC)
|
||||||
|
-- Normal mode: `,a` · Insert mode: <Alt+a> · aerc-side fallback: <F4>
|
||||||
|
local function yazi_attach()
|
||||||
|
local chooser = vim.fn.tempname()
|
||||||
|
vim.cmd('belowright 15split')
|
||||||
|
local tbuf = vim.api.nvim_get_current_buf()
|
||||||
|
vim.fn.jobstart(
|
||||||
|
{ 'yazi', '--chooser-file=' .. chooser, vim.fn.expand('~/Downloads') },
|
||||||
|
{ term = true }
|
||||||
|
)
|
||||||
|
vim.cmd('startinsert')
|
||||||
|
vim.api.nvim_create_autocmd('TermClose', {
|
||||||
|
buffer = tbuf,
|
||||||
|
once = true,
|
||||||
|
callback = function()
|
||||||
|
vim.schedule(function()
|
||||||
|
local win = vim.fn.bufwinid(tbuf)
|
||||||
|
if win ~= -1 then pcall(vim.api.nvim_win_close, win, true) end
|
||||||
|
local f = io.open(chooser, 'r')
|
||||||
|
if not f then return end
|
||||||
|
local content = f:read('*a')
|
||||||
|
f:close()
|
||||||
|
os.remove(chooser)
|
||||||
|
local n = 0
|
||||||
|
for p in content:gmatch('[^\r\n]+') do
|
||||||
|
local path = p:gsub('%s+$', '')
|
||||||
|
if path ~= '' then
|
||||||
|
vim.fn.system({ 'aerc', ':attach', path })
|
||||||
|
n = n + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if n > 0 then
|
||||||
|
vim.notify(('Attached %d file(s) to the draft'):format(n), vim.log.levels.INFO)
|
||||||
|
else
|
||||||
|
vim.notify('yazi: nothing selected', vim.log.levels.WARN)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
vim.keymap.set('n', ',a', yazi_attach, { buffer = true, desc = 'Attach files via yazi' })
|
||||||
|
vim.keymap.set('i', '<M-a>', yazi_attach, { buffer = true, desc = 'Attach files via yazi' })
|
||||||
|
|
||||||
|
-- Spell-check while writing emails
|
||||||
|
vim.opt_local.spell = true
|
||||||
|
vim.opt_local.spelllang = 'en_us'
|
||||||
|
vim.opt_local.complete:append('kspell')
|
||||||
@@ -1,3 +1,23 @@
|
|||||||
|
-- lates ledger
|
||||||
|
-- LedgerSMB template preview: build current file's <base>.build.* directly
|
||||||
|
local function lsmb_base()
|
||||||
|
local b = vim.fn.expand("%:t:r") -- name without final extension
|
||||||
|
b = b:gsub("%.build$", ""):gsub("%.stripped$", "") -- work even from generated files
|
||||||
|
return b
|
||||||
|
end
|
||||||
|
|
||||||
|
-- <leader>lb -> regenerate + compile via latexmk (uses .latexmkrc hook)
|
||||||
|
vim.keymap.set("n", "<leader>lb", function()
|
||||||
|
vim.cmd("!cd %:p:h && latexmk " .. lsmb_base() .. ".build.tex")
|
||||||
|
end, { desc = "LedgerSMB: build preview" })
|
||||||
|
|
||||||
|
-- <leader>lv -> open the PDF in zathura, non-blocking
|
||||||
|
vim.keymap.set("n", "<leader>lv", function()
|
||||||
|
local dir = vim.fn.expand("%:p:h")
|
||||||
|
vim.fn.jobstart({ "zathura", dir .. "/" .. lsmb_base() .. ".build.pdf" }, { detach = true })
|
||||||
|
end, { desc = "LedgerSMB: view preview" })
|
||||||
|
|
||||||
|
vim.opt.clipboard = "unnamedplus"
|
||||||
-- In your init.lua or after lazy setup
|
-- In your init.lua or after lazy setup
|
||||||
vim.opt.termguicolors = true
|
vim.opt.termguicolors = true
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"LuaSnip": { "branch": "master", "commit": "fb525166ccc30296fb3457441eb979113de46b00" },
|
"LuaSnip": { "branch": "master", "commit": "642b0c595e11608b4c18219e93b88d7637af27bc" },
|
||||||
"R.nvim": { "branch": "main", "commit": "c56ebe0f8445e251673981c40ac2d74659ecd6ed" },
|
"R.nvim": { "branch": "main", "commit": "fccfebab6c47a62716615559ab2aec92688cbd0e" },
|
||||||
"barbecue": { "branch": "main", "commit": "cd7e7da622d68136e13721865b4d919efd6325ed" },
|
"barbecue": { "branch": "main", "commit": "cd7e7da622d68136e13721865b4d919efd6325ed" },
|
||||||
"catppuccin": { "branch": "main", "commit": "c7c692a0ad3080710893abbae100171819a3e4be" },
|
"catppuccin": { "branch": "main", "commit": "edefef779ab08ce1a4a404713e3012b0d202bd35" },
|
||||||
"cmp-buffer": { "branch": "main", "commit": "b74fab3656eea9de20a9b8116afa3cfc4ec09657" },
|
"cmp-buffer": { "branch": "main", "commit": "b74fab3656eea9de20a9b8116afa3cfc4ec09657" },
|
||||||
"cmp-cmdline": { "branch": "main", "commit": "d126061b624e0af6c3a556428712dd4d4194ec6d" },
|
"cmp-cmdline": { "branch": "main", "commit": "d126061b624e0af6c3a556428712dd4d4194ec6d" },
|
||||||
"cmp-nvim-lsp": { "branch": "main", "commit": "cbc7b02bb99fae35cb42f514762b89b5126651ef" },
|
"cmp-nvim-lsp": { "branch": "main", "commit": "cbc7b02bb99fae35cb42f514762b89b5126651ef" },
|
||||||
@@ -11,65 +11,64 @@
|
|||||||
"cmp-r": { "branch": "main", "commit": "70bfe8f4c062acc10266e24825439c009a0b1b89" },
|
"cmp-r": { "branch": "main", "commit": "70bfe8f4c062acc10266e24825439c009a0b1b89" },
|
||||||
"cmp-vimtex": { "branch": "master", "commit": "c09ca05bfa0641754516f13294d73d3799a02fc8" },
|
"cmp-vimtex": { "branch": "master", "commit": "c09ca05bfa0641754516f13294d73d3799a02fc8" },
|
||||||
"cmp_luasnip": { "branch": "master", "commit": "98d9cb5c2c38532bd9bdb481067b20fea8f32e90" },
|
"cmp_luasnip": { "branch": "master", "commit": "98d9cb5c2c38532bd9bdb481067b20fea8f32e90" },
|
||||||
"conform.nvim": { "branch": "master", "commit": "619363c30309d29ffa631e67c8183f2a72caa373" },
|
"conform.nvim": { "branch": "master", "commit": "016802de402556da54c36bd7359b441266b01cdd" },
|
||||||
"dashboard-nvim": { "branch": "master", "commit": "f787e3462c2ee2b6117b17c1aa4ddf66cb6f57fe" },
|
"dashboard-nvim": { "branch": "master", "commit": "f787e3462c2ee2b6117b17c1aa4ddf66cb6f57fe" },
|
||||||
"diffview.nvim": { "branch": "main", "commit": "4516612fe98ff56ae0415a259ff6361a89419b0a" },
|
"diffview.nvim": { "branch": "main", "commit": "4516612fe98ff56ae0415a259ff6361a89419b0a" },
|
||||||
"dracula.nvim": { "branch": "main", "commit": "ae752c13e95fb7c5f58da4b5123cb804ea7568ee" },
|
"dracula.nvim": { "branch": "main", "commit": "ae752c13e95fb7c5f58da4b5123cb804ea7568ee" },
|
||||||
"dressing.nvim": { "branch": "master", "commit": "2d7c2db2507fa3c4956142ee607431ddb2828639" },
|
"dressing.nvim": { "branch": "master", "commit": "2d7c2db2507fa3c4956142ee607431ddb2828639" },
|
||||||
"everforest": { "branch": "master", "commit": "85a86eb62409e3ec88713bff3d1b9d7374e112e4" },
|
"everforest": { "branch": "master", "commit": "85a86eb62409e3ec88713bff3d1b9d7374e112e4" },
|
||||||
"flash.nvim": { "branch": "main", "commit": "b6346946d10d07998efee029fb0f7a593806d0cd" },
|
"flash.nvim": { "branch": "main", "commit": "5f0f270fdc7c5b0c21d903ee85b9cb06f2ac636a" },
|
||||||
"friendly-snippets": { "branch": "main", "commit": "6cd7280adead7f586db6fccbd15d2cac7e2188b9" },
|
"friendly-snippets": { "branch": "main", "commit": "a5a73e586521fe5c12d751ad81a0db67649a94ed" },
|
||||||
"fzf-lua": { "branch": "main", "commit": "511651f198068ef5841ee1635ade58daa2486ef7" },
|
"fzf-lua": { "branch": "main", "commit": "05e44d38de0a79c11fba5f7bf8138791b1dbdd1e" },
|
||||||
"gitsigns.nvim": { "branch": "main", "commit": "31d6fb2d618bca1482b9f274751ead5f03461408" },
|
"gitsigns.nvim": { "branch": "main", "commit": "f2421c550618d257048afa650413d9e542ddbe67" },
|
||||||
"grapple.nvim": { "branch": "main", "commit": "b41ddfc1c39f87f3d1799b99c2f0f1daa524c5f7" },
|
"grapple.nvim": { "branch": "main", "commit": "b41ddfc1c39f87f3d1799b99c2f0f1daa524c5f7" },
|
||||||
"gruvbox-mat": { "branch": "master", "commit": "11d779b26a9ab2b3db8c22c6ac9fb6e8ed4fea79" },
|
"gruvbox-mat": { "branch": "master", "commit": "11d779b26a9ab2b3db8c22c6ac9fb6e8ed4fea79" },
|
||||||
"indent-blankline.nvim": { "branch": "master", "commit": "d28a3f70721c79e3c5f6693057ae929f3d9c0a03" },
|
"indent-blankline.nvim": { "branch": "master", "commit": "f1e186e44d3b7f9ae918008e2c28ce37c6023d2d" },
|
||||||
"kanagawa.nvim": { "branch": "master", "commit": "bb85e4bfc8d89b0e62c8fa53ccdd13d12e2f77b3" },
|
"kanagawa.nvim": { "branch": "master", "commit": "bb85e4bfc8d89b0e62c8fa53ccdd13d12e2f77b3" },
|
||||||
"lazy.nvim": { "branch": "main", "commit": "306a05526ada86a7b30af95c5cc81ffba93fef97" },
|
"lazy.nvim": { "branch": "main", "commit": "306a05526ada86a7b30af95c5cc81ffba93fef97" },
|
||||||
"lazydev.nvim": { "branch": "main", "commit": "ff2cbcba459b637ec3fd165a2be59b7bbaeedf0d" },
|
"lazydev.nvim": { "branch": "main", "commit": "ff2cbcba459b637ec3fd165a2be59b7bbaeedf0d" },
|
||||||
"live-preview.nvim": { "branch": "main", "commit": "a30e54e51e7480d7060c8c8185f2a963ad3518b4" },
|
"live-preview.nvim": { "branch": "main", "commit": "a6307fa340ed7c0d96f5c567afc8c991aad94ce0" },
|
||||||
"lualine.nvim": { "branch": "master", "commit": "221ce6b2d999187044529f49da6554a92f740a96" },
|
"lualine.nvim": { "branch": "master", "commit": "221ce6b2d999187044529f49da6554a92f740a96" },
|
||||||
"luvit-meta": { "branch": "main", "commit": "cc9b2d412d2fbd30b94a70cfc214c2a3be27a0a2" },
|
"luvit-meta": { "branch": "main", "commit": "7edcd52ac6f2acc2f83f0cc27ac989a142408fbf" },
|
||||||
"mason-lspconfig.nvim": { "branch": "main", "commit": "7adc933dabcc7c86ae6b07aff7ee68eac398491f" },
|
"mason-lspconfig.nvim": { "branch": "main", "commit": "b5576cd62899a1a478d0e67ef3d378ee55dba82d" },
|
||||||
"mason-tool-installer.nvim": { "branch": "main", "commit": "443f1ef8b5e6bf47045cb2217b6f748a223cf7dc" },
|
"mason-tool-installer.nvim": { "branch": "main", "commit": "443f1ef8b5e6bf47045cb2217b6f748a223cf7dc" },
|
||||||
"mason.nvim": { "branch": "main", "commit": "2a6940af80375532e5e9e7c1f2fc6319a1b7a69d" },
|
"mason.nvim": { "branch": "main", "commit": "2a6940af80375532e5e9e7c1f2fc6319a1b7a69d" },
|
||||||
"neo-tree.nvim": { "branch": "v3.x", "commit": "ebd66767191714e008ce73b769518a763ff31bdc" },
|
"neo-tree.nvim": { "branch": "v3.x", "commit": "1a14083046d96e88e361d1da00761c40d45012f3" },
|
||||||
"neogit": { "branch": "master", "commit": "2043096b7ae81e8350ec8cceb3e7ab08dca1dcfe" },
|
"neogit": { "branch": "master", "commit": "5adc81b26232954cd7a90f158aa7844c18fc3165" },
|
||||||
"nightfox.nvim": { "branch": "main", "commit": "4dacd3f0185a2227bdf3b6c0975a8f0bf87cac9a" },
|
"nightfox.nvim": { "branch": "main", "commit": "4dacd3f0185a2227bdf3b6c0975a8f0bf87cac9a" },
|
||||||
"noice.nvim": { "branch": "main", "commit": "7bfd942445fb63089b59f97ca487d605e715f155" },
|
"noice.nvim": { "branch": "main", "commit": "7bfd942445fb63089b59f97ca487d605e715f155" },
|
||||||
"nui.nvim": { "branch": "main", "commit": "de740991c12411b663994b2860f1a4fd0937c130" },
|
"nui.nvim": { "branch": "main", "commit": "10fc361835c856ba4233ef5ea135b919bf3dce97" },
|
||||||
"nvim-autopairs": { "branch": "master", "commit": "7b9923abad60b903ece7c52940e1321d39eccc79" },
|
"nvim-autopairs": { "branch": "master", "commit": "430522f95fe4fb7c511ec64f8c1a90cc6a66c05c" },
|
||||||
"nvim-cmp": { "branch": "main", "commit": "2ffe79f1f021def8dd1fcd81deb16f1bb0d989f3" },
|
"nvim-cmp": { "branch": "main", "commit": "2ffe79f1f021def8dd1fcd81deb16f1bb0d989f3" },
|
||||||
"nvim-colorizer.lua": { "branch": "master", "commit": "a065833f35a3a7cc3ef137ac88b5381da2ba302e" },
|
"nvim-colorizer.lua": { "branch": "master", "commit": "a065833f35a3a7cc3ef137ac88b5381da2ba302e" },
|
||||||
"nvim-lint": { "branch": "master", "commit": "a219b2c9e5b4765e5c845aba119dad55806fcaf1" },
|
"nvim-lint": { "branch": "master", "commit": "3d55c8f67c6ae5c15e1042571e107c7a3d5c5f4e" },
|
||||||
"nvim-lspconfig": { "branch": "master", "commit": "d592c1e6ad9a0a01b3d5ed3f0345d68407167181" },
|
"nvim-lspconfig": { "branch": "master", "commit": "84b6b6c3f13cb34a6c8a0354b0da4c67d8ccf867" },
|
||||||
"nvim-navic": { "branch": "master", "commit": "f5eba192f39b453675d115351808bd51276d9de5" },
|
"nvim-navic": { "branch": "master", "commit": "455808fd0fae06e6d70fadfd61432275db8f34ce" },
|
||||||
"nvim-notify": { "branch": "master", "commit": "8701bece920b38ea289b457f902e2ad184131a5d" },
|
"nvim-notify": { "branch": "master", "commit": "8701bece920b38ea289b457f902e2ad184131a5d" },
|
||||||
"nvim-spectre": { "branch": "master", "commit": "72f56f7585903cd7bf92c665351aa585e150af0f" },
|
"nvim-spectre": { "branch": "master", "commit": "72f56f7585903cd7bf92c665351aa585e150af0f" },
|
||||||
"nvim-surround": { "branch": "main", "commit": "2e93e154de9ff326def6480a4358bfc149d5da2c" },
|
"nvim-surround": { "branch": "main", "commit": "2e93e154de9ff326def6480a4358bfc149d5da2c" },
|
||||||
"nvim-tmux-navigation": { "branch": "main", "commit": "4898c98702954439233fdaf764c39636681e2861" },
|
"nvim-treesitter": { "branch": "main", "commit": "5cb0114e6242625db56dd6440e945ed1ece10bc7" },
|
||||||
"nvim-treesitter": { "branch": "main", "commit": "bd41519ff7901da11ba6e1c8f419f17f2d94305a" },
|
"nvim-treesitter-textobjects": { "branch": "main", "commit": "5c7b0263797dfd1bd6202f2b219f3b53a80b2187" },
|
||||||
"nvim-treesitter-textobjects": { "branch": "main", "commit": "898ee307df58f854d11cd7edd06472574d48014e" },
|
|
||||||
"nvim-ts-context-commentstring": { "branch": "main", "commit": "6141a40173c6efa98242dc951ed4b6f892c97027" },
|
"nvim-ts-context-commentstring": { "branch": "main", "commit": "6141a40173c6efa98242dc951ed4b6f892c97027" },
|
||||||
"nvim-ufo": { "branch": "main", "commit": "ab3eb124062422d276fae49e0dd63b3ad1062cfc" },
|
"nvim-ufo": { "branch": "main", "commit": "ab3eb124062422d276fae49e0dd63b3ad1062cfc" },
|
||||||
"nvim-web-devicons": { "branch": "master", "commit": "2ae6958df7ced50baac5035cec0c15799eedfbf7" },
|
"nvim-web-devicons": { "branch": "master", "commit": "5f032a85be210cd1c6ac98861eb3b187ff3bd5eb" },
|
||||||
"obsidian.nvim": { "branch": "main", "commit": "ae1f76a75c7ce36866e1d9342a8f6f5b9c2caf9b" },
|
"obsidian.nvim": { "branch": "main", "commit": "ae1f76a75c7ce36866e1d9342a8f6f5b9c2caf9b" },
|
||||||
"onedark-warm": { "branch": "master", "commit": "df4792accde9db0043121f32628bcf8e645d9aea" },
|
"onedark-warm": { "branch": "master", "commit": "df4792accde9db0043121f32628bcf8e645d9aea" },
|
||||||
"plenary.nvim": { "branch": "master", "commit": "74b06c6c75e4eeb3108ec01852001636d85a932b" },
|
"plenary.nvim": { "branch": "master", "commit": "74b06c6c75e4eeb3108ec01852001636d85a932b" },
|
||||||
"portal.nvim": { "branch": "main", "commit": "77d9d53fec945bfa407d5fd7120f1b4f117450ed" },
|
"portal.nvim": { "branch": "main", "commit": "77d9d53fec945bfa407d5fd7120f1b4f117450ed" },
|
||||||
"promise-async": { "branch": "main", "commit": "119e8961014c9bfaf1487bf3c2a393d254f337e2" },
|
"promise-async": { "branch": "main", "commit": "119e8961014c9bfaf1487bf3c2a393d254f337e2" },
|
||||||
"rainbow-delimiters.nvim": { "branch": "master", "commit": "a798325b7f36acc62741d1029930a7b96d4dd4bf" },
|
"rainbow-delimiters.nvim": { "branch": "master", "commit": "3a0fc08dd39e8bf034a4cfef3f2845bd5f565a2e" },
|
||||||
"render-markdown.nvim": { "branch": "main", "commit": "f422cb5c6855f150e2ddcfaf44e7157b98b34f6a" },
|
"render-markdown.nvim": { "branch": "main", "commit": "4663eb3ecd538bd5062628fb6d95bbe6bdca78f6" },
|
||||||
"rose-pine": { "branch": "main", "commit": "ff483051a47e27d84bdef47703538df1ed9f4a47" },
|
"rose-pine": { "branch": "main", "commit": "ff483051a47e27d84bdef47703538df1ed9f4a47" },
|
||||||
"snacks.nvim": { "branch": "main", "commit": "882c996cf28183f4d63640de0b4c02ec886d01f2" },
|
"snacks.nvim": { "branch": "main", "commit": "882c996cf28183f4d63640de0b4c02ec886d01f2" },
|
||||||
"sonokai": { "branch": "master", "commit": "b023c5280b16fe2366f5e779d8d2756b3e5ee9c3" },
|
"sonokai": { "branch": "master", "commit": "b023c5280b16fe2366f5e779d8d2756b3e5ee9c3" },
|
||||||
"telescope-ui-select.nvim": { "branch": "master", "commit": "6e51d7da30bd139a6950adf2a47fda6df9fa06d2" },
|
"telescope-ui-select.nvim": { "branch": "master", "commit": "6e51d7da30bd139a6950adf2a47fda6df9fa06d2" },
|
||||||
"telescope.nvim": { "branch": "master", "commit": "427b576c16792edad01a92b89721d923c19ad60f" },
|
"telescope.nvim": { "branch": "master", "commit": "40aedd8a68c78a656a10a8d62d80c54af59420fb" },
|
||||||
"todo-comments.nvim": { "branch": "main", "commit": "31e3c38ce9b29781e4422fc0322eb0a21f4e8668" },
|
"todo-comments.nvim": { "branch": "main", "commit": "31e3c38ce9b29781e4422fc0322eb0a21f4e8668" },
|
||||||
"toggleterm.nvim": { "branch": "main", "commit": "50ea089fc548917cc3cc16b46a8211833b9e3c7c" },
|
"toggleterm.nvim": { "branch": "main", "commit": "50ea089fc548917cc3cc16b46a8211833b9e3c7c" },
|
||||||
"tokyonight": { "branch": "main", "commit": "cdc07ac78467a233fd62c493de29a17e0cf2b2b6" },
|
"tokyonight": { "branch": "main", "commit": "cdc07ac78467a233fd62c493de29a17e0cf2b2b6" },
|
||||||
"trouble.nvim": { "branch": "main", "commit": "bd67efe408d4816e25e8491cc5ad4088e708a69a" },
|
"trouble.nvim": { "branch": "main", "commit": "bd67efe408d4816e25e8491cc5ad4088e708a69a" },
|
||||||
"vimtex": { "branch": "master", "commit": "1d27d95258d8273d0690c7d3d4ff6e1fa2bd24af" },
|
"vimtex": { "branch": "master", "commit": "e355987594cbf16283bee7b37e3aa3a65d5e1c5c" },
|
||||||
"which-key.nvim": { "branch": "main", "commit": "3aab2147e74890957785941f0c1ad87d0a44c15a" },
|
"which-key.nvim": { "branch": "main", "commit": "3aab2147e74890957785941f0c1ad87d0a44c15a" },
|
||||||
"yazi.nvim": { "branch": "main", "commit": "d87cd885e06d50e1bb5607e48d96c1094d3b10e3" }
|
"yazi.nvim": { "branch": "main", "commit": "bf3e26d87d96c00dcf4bb4f67230bf717b7b76f6" }
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
vim.cmd.colorscheme("rose-pine")
|
-- vim.cmd.colorscheme("rose-pine")
|
||||||
-- vim.cmd.colorscheme("sonokai")
|
-- vim.cmd.colorscheme("sonokai")
|
||||||
-- vim.cmd.colorscheme("one-dark")
|
-- vim.cmd.colorscheme("one-dark")
|
||||||
-- vim.cmd.colorscheme("nightfox")
|
-- vim.cmd.colorscheme("nightfox")
|
||||||
-- vim.cmd.colorscheme("kanagawa")
|
-- vim.cmd.colorscheme("kanagawa")
|
||||||
-- vim.cmd.colorscheme("gruvbox-material")
|
-- vim.cmd.colorscheme("gruvbox-material")
|
||||||
-- vim.cmd.colorscheme("everforest")
|
-- vim.cmd.colorscheme("everforest")
|
||||||
-- vim.cmd.colorscheme("dracula")
|
vim.cmd.colorscheme("dracula")
|
||||||
-- vim.cmd.colorscheme("carbonfox")
|
-- vim.cmd.colorscheme("carbonfox")
|
||||||
-- vim.cmd.colorscheme("tokyonight")
|
-- vim.cmd.colorscheme("tokyonight")
|
||||||
@@ -0,0 +1,235 @@
|
|||||||
|
-- lsp-config.lua: Configures Language Server Protocol for code analysis and diagnostics.
|
||||||
|
|
||||||
|
return {
|
||||||
|
{
|
||||||
|
"williamboman/mason.nvim",
|
||||||
|
lazy = false,
|
||||||
|
config = function()
|
||||||
|
require("mason").setup()
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"williamboman/mason-lspconfig.nvim",
|
||||||
|
lazy = false,
|
||||||
|
opts = {
|
||||||
|
auto_install = true,
|
||||||
|
ensure_installed = {
|
||||||
|
"lua_ls",
|
||||||
|
"pyright",
|
||||||
|
"bashls",
|
||||||
|
"ts_ls",
|
||||||
|
"solargraph",
|
||||||
|
"html",
|
||||||
|
"texlab",
|
||||||
|
"ltex",
|
||||||
|
"dockerls",
|
||||||
|
"yamlls",
|
||||||
|
"taplo",
|
||||||
|
"cssls",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"neovim/nvim-lspconfig",
|
||||||
|
lazy = false,
|
||||||
|
dependencies = {
|
||||||
|
"SmiteshP/nvim-navic",
|
||||||
|
},
|
||||||
|
config = function()
|
||||||
|
local capabilities = require("cmp_nvim_lsp").default_capabilities()
|
||||||
|
local navic = require("nvim-navic")
|
||||||
|
|
||||||
|
-- Common on_attach function
|
||||||
|
local on_attach = function(client, bufnr)
|
||||||
|
if client.server_capabilities.documentSymbolProvider then
|
||||||
|
navic.attach(client, bufnr)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Define server configurations
|
||||||
|
local servers = {
|
||||||
|
ts_ls = {
|
||||||
|
capabilities = capabilities,
|
||||||
|
on_attach = on_attach,
|
||||||
|
},
|
||||||
|
taplo = {
|
||||||
|
capabilities = capabilities,
|
||||||
|
on_attach = on_attach,
|
||||||
|
},
|
||||||
|
solargraph = {
|
||||||
|
capabilities = capabilities,
|
||||||
|
on_attach = on_attach,
|
||||||
|
},
|
||||||
|
textlsp = {
|
||||||
|
filetypes = { "ltx", "txt" },
|
||||||
|
capabilities = capabilities,
|
||||||
|
on_attach = on_attach,
|
||||||
|
},
|
||||||
|
texlab = {
|
||||||
|
capabilities = capabilities,
|
||||||
|
on_attach = on_attach,
|
||||||
|
settings = {
|
||||||
|
texlab = {
|
||||||
|
build = {
|
||||||
|
executable = "latexmk",
|
||||||
|
args = { "-pdf", "-interaction=nonstopmode", "-synctex=1", "%f" },
|
||||||
|
onSave = true,
|
||||||
|
},
|
||||||
|
forwardSearch = {
|
||||||
|
executable = "zathura",
|
||||||
|
args = { "--synctex-forward", "%l:1:%f", "%p" },
|
||||||
|
},
|
||||||
|
chktex = {
|
||||||
|
onOpenAndSave = true,
|
||||||
|
onEdit = false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ltex = {
|
||||||
|
capabilities = capabilities,
|
||||||
|
on_attach = on_attach,
|
||||||
|
filetypes = { "latex", "tex", "bib", "markdown", "mail", "text" },
|
||||||
|
settings = {
|
||||||
|
ltex = {
|
||||||
|
language = "en-US",
|
||||||
|
enabled = { "latex", "tex", "bib", "markdown", "mail", "text" },
|
||||||
|
checkFrequency = "save", -- Only check when you save, not on every keystroke
|
||||||
|
diagnosticSeverity = "warning", -- Less intrusive
|
||||||
|
completionEnabled = true,
|
||||||
|
additionalRules = {
|
||||||
|
enablePickyRules = true,
|
||||||
|
motherTongue = "en-US",
|
||||||
|
},
|
||||||
|
disabledRules = {
|
||||||
|
-- Add rules you want to disable, e.g.:
|
||||||
|
-- ["en-US"] = { "MORFOLOGIK_RULE_EN_US" },
|
||||||
|
},
|
||||||
|
dictionary = {
|
||||||
|
-- Add custom words here that shouldn't be flagged
|
||||||
|
["en-US"] = {
|
||||||
|
-- "aerc",
|
||||||
|
-- "notmuch",
|
||||||
|
-- "dovecot",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
java = {
|
||||||
|
path = "",
|
||||||
|
initialHeapSize = 64,
|
||||||
|
maximumHeapSize = 512,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
html = {
|
||||||
|
capabilities = capabilities,
|
||||||
|
on_attach = on_attach,
|
||||||
|
},
|
||||||
|
lua_ls = {
|
||||||
|
capabilities = capabilities,
|
||||||
|
on_attach = on_attach,
|
||||||
|
settings = {
|
||||||
|
Lua = {
|
||||||
|
diagnostics = {
|
||||||
|
globals = { "vim" },
|
||||||
|
},
|
||||||
|
workspace = {
|
||||||
|
checkThirdParty = false,
|
||||||
|
},
|
||||||
|
telemetry = {
|
||||||
|
enable = false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cssls = {
|
||||||
|
capabilities = capabilities,
|
||||||
|
on_attach = on_attach,
|
||||||
|
settings = {
|
||||||
|
css = {
|
||||||
|
validate = true,
|
||||||
|
lint = {
|
||||||
|
unknownAtRules = "ignore",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scss = {
|
||||||
|
validate = true,
|
||||||
|
lint = {
|
||||||
|
unknownAtRules = "ignore",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
less = {
|
||||||
|
validate = true,
|
||||||
|
lint = {
|
||||||
|
unknownAtRules = "ignore",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pyright = {
|
||||||
|
capabilities = capabilities,
|
||||||
|
on_attach = on_attach,
|
||||||
|
},
|
||||||
|
bashls = {
|
||||||
|
filetypes = { "sh", "bash", "zsh" },
|
||||||
|
capabilities = capabilities,
|
||||||
|
on_attach = on_attach,
|
||||||
|
settings = {
|
||||||
|
bashIde = {
|
||||||
|
globPattern = "*@(.sh|.inc|.bash|.command|.zsh)",
|
||||||
|
shellcheckPath = "shellcheck",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dockerls = {
|
||||||
|
capabilities = capabilities,
|
||||||
|
on_attach = on_attach,
|
||||||
|
},
|
||||||
|
yamlls = {
|
||||||
|
capabilities = capabilities,
|
||||||
|
on_attach = on_attach,
|
||||||
|
settings = {
|
||||||
|
yaml = {
|
||||||
|
schemas = {
|
||||||
|
["https://json.schemastore.org/docker-compose.json"] = "docker-compose*.{yml,yaml}",
|
||||||
|
["https://json.schemastore.org/github-workflow.json"] = ".github/workflows/*",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Setup each server
|
||||||
|
for server, config in pairs(servers) do
|
||||||
|
vim.lsp.config[server] = config
|
||||||
|
vim.lsp.enable(server)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Configure diagnostics globally
|
||||||
|
vim.diagnostic.config({
|
||||||
|
virtual_text = false, -- Disable inline diagnostic text (no more popups!)
|
||||||
|
signs = true, -- Keep gutter signs
|
||||||
|
underline = true, -- Underline errors
|
||||||
|
update_in_insert = false, -- Don't update diagnostics while typing
|
||||||
|
severity_sort = true,
|
||||||
|
float = {
|
||||||
|
border = "rounded",
|
||||||
|
source = "always",
|
||||||
|
header = "",
|
||||||
|
prefix = "",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
-- LSP keymaps
|
||||||
|
vim.keymap.set("n", "K", vim.lsp.buf.hover, {})
|
||||||
|
vim.keymap.set("n", "<leader>gd", vim.lsp.buf.definition, {})
|
||||||
|
vim.keymap.set("n", "<leader>gr", vim.lsp.buf.references, {})
|
||||||
|
vim.keymap.set("n", "<leader>ca", vim.lsp.buf.code_action, {})
|
||||||
|
|
||||||
|
-- Show diagnostics in floating window (optional - press this when you want to see the error)
|
||||||
|
vim.keymap.set("n", "<leader>e", vim.diagnostic.open_float, {})
|
||||||
|
vim.keymap.set("n", "[d", vim.diagnostic.goto_prev, {})
|
||||||
|
vim.keymap.set("n", "]d", vim.diagnostic.goto_next, {})
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
-- nvim-colorizer.lua: Highlights color codes (e.g., #FFFFFF) directly in code.
|
||||||
|
|
||||||
|
return {
|
||||||
|
{
|
||||||
|
"norcalli/nvim-colorizer.lua",
|
||||||
|
config = function()
|
||||||
|
require("colorizer").setup()
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -1,235 +1,55 @@
|
|||||||
-- lsp-config.lua: Configures Language Server Protocol for code analysis and diagnostics.
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
-- Core nvim-treesitter plugin (new main-branch API)
|
||||||
{
|
{
|
||||||
"williamboman/mason.nvim",
|
"nvim-treesitter/nvim-treesitter",
|
||||||
lazy = false,
|
branch = "main",
|
||||||
|
build = ":TSUpdate", -- Updates parsers on install
|
||||||
|
lazy = false, -- load at startup so highlighting works on the first buffer
|
||||||
config = function()
|
config = function()
|
||||||
require("mason").setup()
|
require("nvim-treesitter").setup()
|
||||||
end,
|
|
||||||
},
|
-- On the main branch, ensure_installed does not exist anymore;
|
||||||
{
|
-- install parsers explicitly instead (same list as your old config)
|
||||||
"williamboman/mason-lspconfig.nvim",
|
require("nvim-treesitter").install({
|
||||||
lazy = false,
|
"lua",
|
||||||
opts = {
|
"vim",
|
||||||
auto_install = true,
|
"vimdoc",
|
||||||
ensure_installed = {
|
"markdown",
|
||||||
"lua_ls",
|
"java",
|
||||||
"pyright",
|
"yaml",
|
||||||
"bashls",
|
"latex",
|
||||||
"ts_ls",
|
"r",
|
||||||
"solargraph",
|
"query",
|
||||||
|
"python",
|
||||||
|
"javascript",
|
||||||
|
"typescript",
|
||||||
|
"rust",
|
||||||
|
"go",
|
||||||
|
"bash",
|
||||||
|
"c",
|
||||||
|
"cpp",
|
||||||
"html",
|
"html",
|
||||||
"texlab",
|
|
||||||
"ltex",
|
|
||||||
"dockerls",
|
|
||||||
"yamlls",
|
|
||||||
"taplo",
|
|
||||||
"cssls",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"neovim/nvim-lspconfig",
|
|
||||||
lazy = false,
|
|
||||||
dependencies = {
|
|
||||||
"SmiteshP/nvim-navic",
|
|
||||||
},
|
|
||||||
config = function()
|
|
||||||
local capabilities = require("cmp_nvim_lsp").default_capabilities()
|
|
||||||
local navic = require("nvim-navic")
|
|
||||||
|
|
||||||
-- Common on_attach function
|
|
||||||
local on_attach = function(client, bufnr)
|
|
||||||
if client.server_capabilities.documentSymbolProvider then
|
|
||||||
navic.attach(client, bufnr)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Define server configurations
|
|
||||||
local servers = {
|
|
||||||
ts_ls = {
|
|
||||||
capabilities = capabilities,
|
|
||||||
on_attach = on_attach,
|
|
||||||
},
|
|
||||||
taplo = {
|
|
||||||
capabilities = capabilities,
|
|
||||||
on_attach = on_attach,
|
|
||||||
},
|
|
||||||
solargraph = {
|
|
||||||
capabilities = capabilities,
|
|
||||||
on_attach = on_attach,
|
|
||||||
},
|
|
||||||
textlsp = {
|
|
||||||
filetypes = { "ltx", "txt" },
|
|
||||||
capabilities = capabilities,
|
|
||||||
on_attach = on_attach,
|
|
||||||
},
|
|
||||||
texlab = {
|
|
||||||
capabilities = capabilities,
|
|
||||||
on_attach = on_attach,
|
|
||||||
settings = {
|
|
||||||
texlab = {
|
|
||||||
build = {
|
|
||||||
executable = "latexmk",
|
|
||||||
args = { "-pdf", "-interaction=nonstopmode", "-synctex=1", "%f" },
|
|
||||||
onSave = true,
|
|
||||||
},
|
|
||||||
forwardSearch = {
|
|
||||||
executable = "zathura",
|
|
||||||
args = { "--synctex-forward", "%l:1:%f", "%p" },
|
|
||||||
},
|
|
||||||
chktex = {
|
|
||||||
onOpenAndSave = true,
|
|
||||||
onEdit = false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ltex = {
|
|
||||||
capabilities = capabilities,
|
|
||||||
on_attach = on_attach,
|
|
||||||
filetypes = { "latex", "tex", "bib", "markdown", "mail", "text" },
|
|
||||||
settings = {
|
|
||||||
ltex = {
|
|
||||||
language = "en-US",
|
|
||||||
enabled = { "latex", "tex", "bib", "markdown", "mail", "text" },
|
|
||||||
checkFrequency = "save", -- Only check when you save, not on every keystroke
|
|
||||||
diagnosticSeverity = "warning", -- Less intrusive
|
|
||||||
completionEnabled = true,
|
|
||||||
additionalRules = {
|
|
||||||
enablePickyRules = true,
|
|
||||||
motherTongue = "en-US",
|
|
||||||
},
|
|
||||||
disabledRules = {
|
|
||||||
-- Add rules you want to disable, e.g.:
|
|
||||||
-- ["en-US"] = { "MORFOLOGIK_RULE_EN_US" },
|
|
||||||
},
|
|
||||||
dictionary = {
|
|
||||||
-- Add custom words here that shouldn't be flagged
|
|
||||||
["en-US"] = {
|
|
||||||
-- "aerc",
|
|
||||||
-- "notmuch",
|
|
||||||
-- "dovecot",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
java = {
|
|
||||||
path = "",
|
|
||||||
initialHeapSize = 64,
|
|
||||||
maximumHeapSize = 512,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
html = {
|
|
||||||
capabilities = capabilities,
|
|
||||||
on_attach = on_attach,
|
|
||||||
},
|
|
||||||
lua_ls = {
|
|
||||||
capabilities = capabilities,
|
|
||||||
on_attach = on_attach,
|
|
||||||
settings = {
|
|
||||||
Lua = {
|
|
||||||
diagnostics = {
|
|
||||||
globals = { "vim" },
|
|
||||||
},
|
|
||||||
workspace = {
|
|
||||||
checkThirdParty = false,
|
|
||||||
},
|
|
||||||
telemetry = {
|
|
||||||
enable = false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
cssls = {
|
|
||||||
capabilities = capabilities,
|
|
||||||
on_attach = on_attach,
|
|
||||||
settings = {
|
|
||||||
css = {
|
|
||||||
validate = true,
|
|
||||||
lint = {
|
|
||||||
unknownAtRules = "ignore",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
scss = {
|
|
||||||
validate = true,
|
|
||||||
lint = {
|
|
||||||
unknownAtRules = "ignore",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
less = {
|
|
||||||
validate = true,
|
|
||||||
lint = {
|
|
||||||
unknownAtRules = "ignore",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
pyright = {
|
|
||||||
capabilities = capabilities,
|
|
||||||
on_attach = on_attach,
|
|
||||||
},
|
|
||||||
bashls = {
|
|
||||||
filetypes = { "sh", "bash", "zsh" },
|
|
||||||
capabilities = capabilities,
|
|
||||||
on_attach = on_attach,
|
|
||||||
settings = {
|
|
||||||
bashIde = {
|
|
||||||
globPattern = "*@(.sh|.inc|.bash|.command|.zsh)",
|
|
||||||
shellcheckPath = "shellcheck",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dockerls = {
|
|
||||||
capabilities = capabilities,
|
|
||||||
on_attach = on_attach,
|
|
||||||
},
|
|
||||||
yamlls = {
|
|
||||||
capabilities = capabilities,
|
|
||||||
on_attach = on_attach,
|
|
||||||
settings = {
|
|
||||||
yaml = {
|
|
||||||
schemas = {
|
|
||||||
["https://json.schemastore.org/docker-compose.json"] = "docker-compose*.{yml,yaml}",
|
|
||||||
["https://json.schemastore.org/github-workflow.json"] = ".github/workflows/*",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
-- Setup each server
|
|
||||||
for server, config in pairs(servers) do
|
|
||||||
vim.lsp.config[server] = config
|
|
||||||
vim.lsp.enable(server)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Configure diagnostics globally
|
|
||||||
vim.diagnostic.config({
|
|
||||||
virtual_text = false, -- Disable inline diagnostic text (no more popups!)
|
|
||||||
signs = true, -- Keep gutter signs
|
|
||||||
underline = true, -- Underline errors
|
|
||||||
update_in_insert = false, -- Don't update diagnostics while typing
|
|
||||||
severity_sort = true,
|
|
||||||
float = {
|
|
||||||
border = "rounded",
|
|
||||||
source = "always",
|
|
||||||
header = "",
|
|
||||||
prefix = "",
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
-- LSP keymaps
|
-- highlight = { enable = true } also no longer exists;
|
||||||
vim.keymap.set("n", "K", vim.lsp.buf.hover, {})
|
-- start treesitter highlighting manually for every buffer
|
||||||
vim.keymap.set("n", "<leader>gd", vim.lsp.buf.definition, {})
|
vim.api.nvim_create_autocmd("FileType", {
|
||||||
vim.keymap.set("n", "<leader>gr", vim.lsp.buf.references, {})
|
callback = function()
|
||||||
vim.keymap.set("n", "<leader>ca", vim.lsp.buf.code_action, {})
|
pcall(vim.treesitter.start)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
|
||||||
-- Show diagnostics in floating window (optional - press this when you want to see the error)
|
-- Textobjects plugin that depends on the core plugin
|
||||||
vim.keymap.set("n", "<leader>e", vim.diagnostic.open_float, {})
|
{
|
||||||
vim.keymap.set("n", "[d", vim.diagnostic.goto_prev, {})
|
"nvim-treesitter/nvim-treesitter-textobjects",
|
||||||
vim.keymap.set("n", "]d", vim.diagnostic.goto_next, {})
|
branch = "main", -- must use the matching main branch, otherwise it breaks with the rewritten core
|
||||||
|
dependencies = { "nvim-treesitter/nvim-treesitter" },
|
||||||
|
config = function()
|
||||||
|
require("nvim-treesitter-textobjects").setup({
|
||||||
|
-- add select/move textobject keymaps here when you need them
|
||||||
|
})
|
||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
-- nvim-colorizer.lua: Highlights color codes (e.g., #FFFFFF) directly in code.
|
-- ~/.config/nvim/lua/plugins/nvim-colorizer.lua
|
||||||
|
|
||||||
return {
|
return {
|
||||||
{
|
"catgoose/nvim-colorizer.lua", -- maintained fork, same API
|
||||||
"norcalli/nvim-colorizer.lua",
|
|
||||||
config = function()
|
config = function()
|
||||||
require("colorizer").setup()
|
require("colorizer").setup({
|
||||||
|
-- your existing options from line 7 onward go here unchanged
|
||||||
|
})
|
||||||
end,
|
end,
|
||||||
},
|
|
||||||
}
|
}
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
-- nvim-tmux-navigator.lua: Switch between Neovim and tmux panes using consistent keybindings.
|
|
||||||
|
|
||||||
return {
|
|
||||||
"alexghergh/nvim-tmux-navigation",
|
|
||||||
config = function()
|
|
||||||
require("nvim-tmux-navigation").setup({})
|
|
||||||
vim.keymap.set("n", "<C-h>", "<Cmd>NvimTmuxNavigateLeft<CR>", {})
|
|
||||||
vim.keymap.set("n", "<C-j>", "<Cmd>NvimTmuxNavigateDown<CR>", {})
|
|
||||||
vim.keymap.set("n", "<C-k>", "<Cmd>NvimTmuxNavigateUp<CR>", {})
|
|
||||||
vim.keymap.set("n", "<C-l>", "<Cmd>NvimTmuxNavigateRight<CR>", {})
|
|
||||||
end,
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
-- obsidian.lua: Integrates Obsidian note-taking workflow into Neovim.
|
||||||
|
|
||||||
|
return {
|
||||||
|
{
|
||||||
|
"epwalsh/obsidian.nvim",
|
||||||
|
version = "*",
|
||||||
|
lazy = true,
|
||||||
|
ft = "markdown",
|
||||||
|
dependencies = {
|
||||||
|
"nvim-lua/plenary.nvim",
|
||||||
|
},
|
||||||
|
opts = {
|
||||||
|
workspaces = {
|
||||||
|
{
|
||||||
|
name = "liph",
|
||||||
|
path = "/home/liph/Documents/Obsidian/liph",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
completion = {
|
||||||
|
nvim_cmp = true,
|
||||||
|
min_chars = 2,
|
||||||
|
},
|
||||||
|
|
||||||
|
mappings = {
|
||||||
|
["<leader>ch"] = {
|
||||||
|
action = function()
|
||||||
|
return require("obsidian").util.toggle_checkbox()
|
||||||
|
end,
|
||||||
|
opts = { buffer = true, desc = "Toggle checkbox" },
|
||||||
|
},
|
||||||
|
["<cr>"] = {
|
||||||
|
action = function()
|
||||||
|
return require("obsidian").util.smart_action()
|
||||||
|
end,
|
||||||
|
opts = { buffer = true, expr = true },
|
||||||
|
},
|
||||||
|
["gf"] = {
|
||||||
|
action = function()
|
||||||
|
return require("obsidian").util.gf_passthrough()
|
||||||
|
end,
|
||||||
|
opts = { noremap = false, expr = true, buffer = true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
note_id_func = function(title)
|
||||||
|
return title
|
||||||
|
end,
|
||||||
|
|
||||||
|
ui = {
|
||||||
|
enable = false, -- DISABLE Obsidian UI, use markview instead
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -1,12 +1,16 @@
|
|||||||
return {
|
return {
|
||||||
-- Core nvim-treesitter plugin
|
-- Core nvim-treesitter plugin (new main-branch API)
|
||||||
{
|
{
|
||||||
"nvim-treesitter/nvim-treesitter",
|
"nvim-treesitter/nvim-treesitter",
|
||||||
|
branch = "main",
|
||||||
build = ":TSUpdate", -- Updates parsers on install
|
build = ":TSUpdate", -- Updates parsers on install
|
||||||
priority = 1000, -- ← HIGHEST priority
|
lazy = false, -- load at startup so highlighting works on the first buffer
|
||||||
config = true, -- Let lazy.nvim auto-call .setup() with opts
|
config = function()
|
||||||
opts = {
|
require("nvim-treesitter").setup()
|
||||||
ensure_installed = {
|
|
||||||
|
-- Replaces the old ensure_installed/auto_install options;
|
||||||
|
-- already-installed parsers are skipped, missing ones download in the background
|
||||||
|
require("nvim-treesitter").install({
|
||||||
"lua",
|
"lua",
|
||||||
"vim",
|
"vim",
|
||||||
"vimdoc",
|
"vimdoc",
|
||||||
@@ -25,26 +29,26 @@ return {
|
|||||||
"c",
|
"c",
|
||||||
"cpp",
|
"cpp",
|
||||||
"html",
|
"html",
|
||||||
},
|
})
|
||||||
sync_install = false,
|
|
||||||
auto_install = true,
|
-- Replaces the old highlight = { enable = true } option
|
||||||
highlight = {
|
vim.api.nvim_create_autocmd("FileType", {
|
||||||
enable = true,
|
callback = function()
|
||||||
additional_vim_regex_highlighting = false,
|
pcall(vim.treesitter.start)
|
||||||
},
|
end,
|
||||||
indent = {
|
})
|
||||||
enable = true,
|
end,
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
-- Textobjects plugin that depends on the core plugin
|
-- Textobjects plugin that depends on the core plugin
|
||||||
{
|
{
|
||||||
"nvim-treesitter/nvim-treesitter-textobjects",
|
"nvim-treesitter/nvim-treesitter-textobjects",
|
||||||
|
branch = "main", -- must match the new core API
|
||||||
dependencies = { "nvim-treesitter/nvim-treesitter" },
|
dependencies = { "nvim-treesitter/nvim-treesitter" },
|
||||||
config = function()
|
config = function()
|
||||||
-- Configuration is integrated into the main setup
|
require("nvim-treesitter-textobjects").setup({
|
||||||
-- No need to call require() here
|
-- add select/move textobject keymaps here when you need them
|
||||||
|
})
|
||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"ollama-cloud": {
|
"ollama-cloud": {
|
||||||
"type": "api_key",
|
"type": "api_key",
|
||||||
"key": "c06adb6cc58a4c3aaa459bd75f42085b.EAGExkidZO9hklD3KWhWL2Uu"
|
"key": "77a13370a1a148ff8667db60252cfabb.nxL8rySeqXECFKe6ZdNSSIIS"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loaded 100 of 142 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user