pax_global_header 0000666 0000000 0000000 00000000064 14770045464 0014525 g ustar 00root root 0000000 0000000 52 comment=28afd867e24b02db235ff569d0b3f78b640fccb9
visidata-3.1.1/ 0000775 0000000 0000000 00000000000 14770045464 0013333 5 ustar 00root root 0000000 0000000 visidata-3.1.1/LICENSE.gpl3 0000664 0000000 0000000 00000104513 14770045464 0015210 0 ustar 00root root 0000000 0000000 GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
Copyright (C)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
Copyright (C)
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
.
visidata-3.1.1/MANIFEST.in 0000664 0000000 0000000 00000001030 14770045464 0015063 0 ustar 00root root 0000000 0000000 include README.md
include LICENSE.gpl3
include visidata/man/vd.1
include visidata/man/vd.txt
include visidata/man/visidata.1
include visidata/ddw/input.ddw
include visidata/ddw/regex.ddw
include visidata/tests/sample.tsv
include visidata/tests/benchmark.csv
include visidata/desktop/visidata.desktop
include visidata/experimental/noahs_tapestry/*.json
include visidata/experimental/noahs_tapestry/*.md
include visidata/experimental/noahs_tapestry/*.ddw
include visidata/experimental/noahs_tapestry/*.sqlite
include visidata/guides/*.md
visidata-3.1.1/PKG-INFO 0000664 0000000 0000000 00000010312 14770045464 0014425 0 ustar 00root root 0000000 0000000 Metadata-Version: 2.1
Name: visidata
Version: 3.1.1
Summary: terminal interface for exploring and arranging tabular data
Home-page: https://visidata.org
Download-URL: https://github.com/saulpw/visidata/tarball/3.1.1
Author: Saul Pwanson
Author-email: visidata@saul.pw
License: GPLv3
Keywords: console tabular data spreadsheet terminal viewer textpunkcurses csv hdf5 h5 xlsx excel tsv
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Console
Classifier: Environment :: Console :: Curses
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: Intended Audience :: System Administrators
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Database :: Front-Ends
Classifier: Topic :: Scientific/Engineering
Classifier: Topic :: Office/Business :: Financial :: Spreadsheet
Classifier: Topic :: Scientific/Engineering :: Visualization
Classifier: Topic :: Utilities
Requires-Python: >=3.8
Description-Content-Type: text/markdown
Provides-Extra: test
License-File: LICENSE.gpl3
# VisiData v3.1
[](https://github.com/saulpw/visidata/actions/workflows/main.yml)
[](https://gitpod.io/#https://github.com/saulpw/visidata)
[](https://visidata.org/chat)
[![mastodon @visidata@fosstodon.org][2.1]][2]
[![twitter @VisiData][1.1]][1]
A terminal interface for exploring and arranging tabular data.

VisiData supports tsv, csv, sqlite, json, xlsx (Excel), hdf5, and [many other formats](https://visidata.org/formats).
## Platform requirements
- Linux, OS/X, or Windows (with WSL)
- Python 3.8+
- additional Python modules are required for certain formats and sources
## Install
To install the latest release from PyPi:
pip3 install visidata
To install the cutting edge `develop` branch (no warranty expressed or implied):
pip3 install git+https://github.com/saulpw/visidata.git@develop
See [visidata.org/install](https://visidata.org/install) for detailed instructions for all available platforms and package managers.
### Usage
On Linux and OS/X
$ vd
$ | vd
On Windows
$ visidata
$ | visidata
Press `Ctrl+Q` to quit at any time.
Hundreds of other commands and options are also available; see the documentation.
### Documentation
* [VisiData documentation](https://visidata.org/docs)
* [Plugin Author's Guide and API Reference](https://visidata.org/docs/api)
* [Quick reference](https://visidata.org/man) (available within `vd` with `Ctrl+H`), which has a list of commands and options.
* [Intro to VisiData Tutorial](https://jsvine.github.io/intro-to-visidata/) by [Jeremy Singer-Vine](https://www.jsvine.com/)
### Help and Support
If you have a question, issue, or suggestion regarding VisiData, please [create an issue on Github](https://github.com/saulpw/visidata/issues) or chat with us at #visidata on [irc.libera.chat](https://libera.chat/).
If you use VisiData regularly, please [support me on Patreon](https://www.patreon.com/saulpw)!
## License
Code in the `stable` branch of this repository, including the main `vd` application, loaders, and plugins, is available for use and redistribution under GPLv3.
## Credits
VisiData is conceived and developed by Saul Pwanson ``.
Anja Kefala `` maintains the documentation and packages for all platforms.
Many thanks to numerous other [contributors](https://visidata.org/credits/), and to those wonderful users who provide feedback, for helping to make VisiData the awesome tool that it is.
[1.1]: http://i.imgur.com/tXSoThF.png
[1]: http://www.twitter.com/VisiData
[2.1]: https://raw.githubusercontent.com/mastodon/mastodon/main/app/javascript/images/logo.svg
[2]: https://fosstodon.org/@visidata
visidata-3.1.1/README.md 0000664 0000000 0000000 00000006034 14770045464 0014615 0 ustar 00root root 0000000 0000000 # VisiData v3.1
[](https://github.com/saulpw/visidata/actions/workflows/main.yml)
[](https://gitpod.io/#https://github.com/saulpw/visidata)
[](https://visidata.org/chat)
[![mastodon @visidata@fosstodon.org][2.1]][2]
[![twitter @VisiData][1.1]][1]
A terminal interface for exploring and arranging tabular data.

VisiData supports tsv, csv, sqlite, json, xlsx (Excel), hdf5, and [many other formats](https://visidata.org/formats).
## Platform requirements
- Linux, OS/X, or Windows (with WSL)
- Python 3.8+
- additional Python modules are required for certain formats and sources
## Install
To install the latest release from PyPi:
pip3 install visidata
To install the cutting edge `develop` branch (no warranty expressed or implied):
pip3 install git+https://github.com/saulpw/visidata.git@develop
See [visidata.org/install](https://visidata.org/install) for detailed instructions for all available platforms and package managers.
### Usage
On Linux and OS/X
$ vd
$ | vd
On Windows
$ visidata
$ | visidata
Press `Ctrl+Q` to quit at any time.
Hundreds of other commands and options are also available; see the documentation.
### Documentation
* [VisiData documentation](https://visidata.org/docs)
* [Plugin Author's Guide and API Reference](https://visidata.org/docs/api)
* [Quick reference](https://visidata.org/man) (available within `vd` with `Ctrl+H`), which has a list of commands and options.
* [Intro to VisiData Tutorial](https://jsvine.github.io/intro-to-visidata/) by [Jeremy Singer-Vine](https://www.jsvine.com/)
### Help and Support
If you have a question, issue, or suggestion regarding VisiData, please [create an issue on Github](https://github.com/saulpw/visidata/issues) or chat with us at #visidata on [irc.libera.chat](https://libera.chat/).
If you use VisiData regularly, please [support me on Patreon](https://www.patreon.com/saulpw)!
## License
Code in the `stable` branch of this repository, including the main `vd` application, loaders, and plugins, is available for use and redistribution under GPLv3.
## Credits
VisiData is conceived and developed by Saul Pwanson ``.
Anja Kefala `` maintains the documentation and packages for all platforms.
Many thanks to numerous other [contributors](https://visidata.org/credits/), and to those wonderful users who provide feedback, for helping to make VisiData the awesome tool that it is.
[1.1]: http://i.imgur.com/tXSoThF.png
[1]: http://www.twitter.com/VisiData
[2.1]: https://raw.githubusercontent.com/mastodon/mastodon/main/app/javascript/images/logo.svg
[2]: https://fosstodon.org/@visidata
visidata-3.1.1/bin/ 0000775 0000000 0000000 00000000000 14770045464 0014103 5 ustar 00root root 0000000 0000000 visidata-3.1.1/bin/vd 0000775 0000000 0000000 00000000144 14770045464 0014441 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
import visidata.main
if __name__ == '__main__':
visidata.main.vd_cli()
visidata-3.1.1/bin/vd2to3.vdx 0000775 0000000 0000000 00000000267 14770045464 0015757 0 ustar 00root root 0000000 0000000 #!/usr/bin/env -S vd -p
# VisiData v3.0dev
open-file ~/.visidata/macros.tsv
col command
rename-col binding
col filename
rename-col source
save-sheet ~/.visidata/macros.jsonl
quit-all
visidata-3.1.1/setup.cfg 0000664 0000000 0000000 00000000046 14770045464 0015154 0 ustar 00root root 0000000 0000000 [egg_info]
tag_build =
tag_date = 0
visidata-3.1.1/setup.py 0000775 0000000 0000000 00000006546 14770045464 0015063 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
from setuptools import setup
# tox can't actually run python3 setup.py: https://github.com/tox-dev/tox/issues/96
# from visidata import __version__
__version__ = "3.1.1"
setup(
name="visidata",
version=__version__,
description="terminal interface for exploring and arranging tabular data",
long_description=open("README.md").read(),
long_description_content_type="text/markdown",
author="Saul Pwanson",
python_requires=">=3.8",
author_email="visidata@saul.pw",
url="https://visidata.org",
download_url="https://github.com/saulpw/visidata/tarball/" + __version__,
scripts=["bin/vd", "bin/vd2to3.vdx"],
entry_points={
"console_scripts": ["visidata=visidata.main:vd_cli"],
},
py_modules=["visidata"],
install_requires=[
"python-dateutil",
'windows-curses != 2.3.1; platform_system == "Windows"', # 1841
"importlib-metadata >= 3.6",
'importlib_resources; python_version<"3.9"',
],
packages=[
"visidata",
"visidata.loaders",
"visidata.vendor",
"visidata.tests",
"visidata.guides",
"visidata.ddw",
"visidata.man",
"visidata.themes",
"visidata.features",
"visidata.experimental",
"visidata.experimental.noahs_tapestry",
"visidata.apps",
"visidata.apps.vgit",
"visidata.apps.vdsql",
"visidata.desktop",
],
data_files=[
("share/man/man1", ["visidata/man/vd.1", "visidata/man/visidata.1"]),
("share/applications", ["visidata/desktop/visidata.desktop"]),
],
extras_require={
"test": [
"brotli",
"dnslib",
"dpkt",
"fecfile",
"Faker",
"h5py",
"lxml",
"msgpack",
"odfpy",
"openpyxl",
"pandas>=1.5.3",
"pyarrow",
"pyconll",
"pypng",
"pytest",
"PyYAML>=5.1",
"tabulate",
"tomli",
"wcwidth",
"xport>=3.0",
]
},
package_data={
"visidata.man": ["vd.1", "vd.txt"],
"visidata.ddw": ["input.ddw", "regex.ddw"],
"visidata": ["guides/*.md"],
"visidata.tests": ["sample.tsv", "benchmark.csv"],
"visidata.desktop": ["visidata.desktop"],
"visidata.experimenta.noahs_tapestry": [
"*.ddw",
"*.md",
"*.json",
"noahs.sqlite",
],
},
license="GPLv3",
classifiers=[
"Development Status :: 5 - Production/Stable",
"Environment :: Console",
"Environment :: Console :: Curses",
"Intended Audience :: Developers",
"Intended Audience :: Science/Research",
"Intended Audience :: System Administrators",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Topic :: Database :: Front-Ends",
"Topic :: Scientific/Engineering",
"Topic :: Office/Business :: Financial :: Spreadsheet",
"Topic :: Scientific/Engineering :: Visualization",
"Topic :: Utilities",
],
keywords=(
"console tabular data spreadsheet terminal viewer textpunk"
"curses csv hdf5 h5 xlsx excel tsv"
),
)
visidata-3.1.1/visidata.egg-info/ 0000775 0000000 0000000 00000000000 14770045464 0016631 5 ustar 00root root 0000000 0000000 visidata-3.1.1/visidata.egg-info/PKG-INFO 0000664 0000000 0000000 00000010312 14770045464 0017723 0 ustar 00root root 0000000 0000000 Metadata-Version: 2.1
Name: visidata
Version: 3.1.1
Summary: terminal interface for exploring and arranging tabular data
Home-page: https://visidata.org
Download-URL: https://github.com/saulpw/visidata/tarball/3.1.1
Author: Saul Pwanson
Author-email: visidata@saul.pw
License: GPLv3
Keywords: console tabular data spreadsheet terminal viewer textpunkcurses csv hdf5 h5 xlsx excel tsv
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Console
Classifier: Environment :: Console :: Curses
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: Intended Audience :: System Administrators
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Database :: Front-Ends
Classifier: Topic :: Scientific/Engineering
Classifier: Topic :: Office/Business :: Financial :: Spreadsheet
Classifier: Topic :: Scientific/Engineering :: Visualization
Classifier: Topic :: Utilities
Requires-Python: >=3.8
Description-Content-Type: text/markdown
Provides-Extra: test
License-File: LICENSE.gpl3
# VisiData v3.1
[](https://github.com/saulpw/visidata/actions/workflows/main.yml)
[](https://gitpod.io/#https://github.com/saulpw/visidata)
[](https://visidata.org/chat)
[![mastodon @visidata@fosstodon.org][2.1]][2]
[![twitter @VisiData][1.1]][1]
A terminal interface for exploring and arranging tabular data.

VisiData supports tsv, csv, sqlite, json, xlsx (Excel), hdf5, and [many other formats](https://visidata.org/formats).
## Platform requirements
- Linux, OS/X, or Windows (with WSL)
- Python 3.8+
- additional Python modules are required for certain formats and sources
## Install
To install the latest release from PyPi:
pip3 install visidata
To install the cutting edge `develop` branch (no warranty expressed or implied):
pip3 install git+https://github.com/saulpw/visidata.git@develop
See [visidata.org/install](https://visidata.org/install) for detailed instructions for all available platforms and package managers.
### Usage
On Linux and OS/X
$ vd
$ | vd
On Windows
$ visidata
$ | visidata
Press `Ctrl+Q` to quit at any time.
Hundreds of other commands and options are also available; see the documentation.
### Documentation
* [VisiData documentation](https://visidata.org/docs)
* [Plugin Author's Guide and API Reference](https://visidata.org/docs/api)
* [Quick reference](https://visidata.org/man) (available within `vd` with `Ctrl+H`), which has a list of commands and options.
* [Intro to VisiData Tutorial](https://jsvine.github.io/intro-to-visidata/) by [Jeremy Singer-Vine](https://www.jsvine.com/)
### Help and Support
If you have a question, issue, or suggestion regarding VisiData, please [create an issue on Github](https://github.com/saulpw/visidata/issues) or chat with us at #visidata on [irc.libera.chat](https://libera.chat/).
If you use VisiData regularly, please [support me on Patreon](https://www.patreon.com/saulpw)!
## License
Code in the `stable` branch of this repository, including the main `vd` application, loaders, and plugins, is available for use and redistribution under GPLv3.
## Credits
VisiData is conceived and developed by Saul Pwanson ``.
Anja Kefala `` maintains the documentation and packages for all platforms.
Many thanks to numerous other [contributors](https://visidata.org/credits/), and to those wonderful users who provide feedback, for helping to make VisiData the awesome tool that it is.
[1.1]: http://i.imgur.com/tXSoThF.png
[1]: http://www.twitter.com/VisiData
[2.1]: https://raw.githubusercontent.com/mastodon/mastodon/main/app/javascript/images/logo.svg
[2]: https://fosstodon.org/@visidata
visidata-3.1.1/visidata.egg-info/SOURCES.txt 0000664 0000000 0000000 00000020115 14770045464 0020514 0 ustar 00root root 0000000 0000000 LICENSE.gpl3
MANIFEST.in
README.md
setup.py
bin/vd
bin/vd2to3.vdx
visidata/__init__.py
visidata/__main__.py
visidata/_input.py
visidata/_open.py
visidata/_types.py
visidata/_urlcache.py
visidata/aggregators.py
visidata/basesheet.py
visidata/bezier.py
visidata/canvas.py
visidata/canvas_text.py
visidata/choose.py
visidata/clean_names.py
visidata/clipboard.py
visidata/cliptext.py
visidata/cmdlog.py
visidata/color.py
visidata/column.py
visidata/ddwplay.py
visidata/deprecated.py
visidata/editor.py
visidata/errors.py
visidata/expr.py
visidata/extensible.py
visidata/form.py
visidata/freqtbl.py
visidata/fuzzymatch.py
visidata/graph.py
visidata/guide.py
visidata/help.py
visidata/hint.py
visidata/indexsheet.py
visidata/input_history.py
visidata/interface.py
visidata/keys.py
visidata/macos.py
visidata/macros.py
visidata/main.py
visidata/mainloop.py
visidata/memory.py
visidata/menu.py
visidata/metasheets.py
visidata/modify.py
visidata/motd.py
visidata/mouse.py
visidata/movement.py
visidata/optionssheet.py
visidata/path.py
visidata/pivot.py
visidata/plugins.py
visidata/pyobj.py
visidata/rename_col.py
visidata/save.py
visidata/search.py
visidata/selection.py
visidata/settings.py
visidata/sheets.py
visidata/shell.py
visidata/sidebar.py
visidata/sort.py
visidata/statusbar.py
visidata/stored_list.py
visidata/text_source.py
visidata/textsheet.py
visidata/theme.py
visidata/threads.py
visidata/tuiwin.py
visidata/type_currency.py
visidata/type_date.py
visidata/type_floatsi.py
visidata/undo.py
visidata/utils.py
visidata/vdobj.py
visidata/windows.py
visidata/wrappers.py
visidata.egg-info/PKG-INFO
visidata.egg-info/SOURCES.txt
visidata.egg-info/dependency_links.txt
visidata.egg-info/entry_points.txt
visidata.egg-info/requires.txt
visidata.egg-info/top_level.txt
visidata/apps/__init__.py
visidata/apps/vdsql/__about__.py
visidata/apps/vdsql/__init__.py
visidata/apps/vdsql/__main__.py
visidata/apps/vdsql/_ibis.py
visidata/apps/vdsql/bigquery.py
visidata/apps/vdsql/clickhouse.py
visidata/apps/vdsql/setup.py
visidata/apps/vdsql/snowflake.py
visidata/apps/vgit/__init__.py
visidata/apps/vgit/__main__.py
visidata/apps/vgit/abort.py
visidata/apps/vgit/blame.py
visidata/apps/vgit/branch.py
visidata/apps/vgit/config.py
visidata/apps/vgit/diff.py
visidata/apps/vgit/gitsheet.py
visidata/apps/vgit/grep.py
visidata/apps/vgit/log.py
visidata/apps/vgit/main.py
visidata/apps/vgit/remote.py
visidata/apps/vgit/repos.py
visidata/apps/vgit/setup.py
visidata/apps/vgit/stash.py
visidata/apps/vgit/status.py
visidata/apps/vgit/statusbar.py
visidata/ddw/input.ddw
visidata/ddw/regex.ddw
visidata/desktop/visidata.desktop
visidata/experimental/__init__.py
visidata/experimental/diff_sheet.py
visidata/experimental/digit_autoedit.py
visidata/experimental/gdrive.py
visidata/experimental/google.py
visidata/experimental/gsheets.py
visidata/experimental/live_search.py
visidata/experimental/liveupdate.py
visidata/experimental/mark.py
visidata/experimental/rownum.py
visidata/experimental/slide_cells.py
visidata/experimental/sort_selected.py
visidata/experimental/noahs_tapestry/__init__.py
visidata/experimental/noahs_tapestry/clues.json
visidata/experimental/noahs_tapestry/flame.ddw
visidata/experimental/noahs_tapestry/menorah.ddw
visidata/experimental/noahs_tapestry/noahs.sqlite
visidata/experimental/noahs_tapestry/puzzle0.md
visidata/experimental/noahs_tapestry/puzzle1.md
visidata/experimental/noahs_tapestry/puzzle2.md
visidata/experimental/noahs_tapestry/puzzle3.md
visidata/experimental/noahs_tapestry/puzzle4.md
visidata/experimental/noahs_tapestry/puzzle5.md
visidata/experimental/noahs_tapestry/puzzle6.md
visidata/experimental/noahs_tapestry/puzzle7.md
visidata/experimental/noahs_tapestry/puzzle8.md
visidata/experimental/noahs_tapestry/solutions.json
visidata/experimental/noahs_tapestry/tapestry.ddw
visidata/experimental/noahs_tapestry/tapestry.py
visidata/features/__init__.py
visidata/features/addcol_audiometadata.py
visidata/features/addcol_histogram.py
visidata/features/canvas_save_svg.py
visidata/features/change_precision.py
visidata/features/cmdpalette.py
visidata/features/colorbrewer.py
visidata/features/colorsheet.py
visidata/features/command_server.py
visidata/features/currency_to_usd.py
visidata/features/customdate.py
visidata/features/dedupe.py
visidata/features/describe.py
visidata/features/expand_cols.py
visidata/features/fill.py
visidata/features/freeze.py
visidata/features/go_col.py
visidata/features/graph_seaborn.py
visidata/features/helloworld.py
visidata/features/hint_types.py
visidata/features/incr.py
visidata/features/join.py
visidata/features/known_cols.py
visidata/features/layout.py
visidata/features/melt.py
visidata/features/normcol.py
visidata/features/open_config.py
visidata/features/open_syspaste.py
visidata/features/ping.py
visidata/features/procmgr.py
visidata/features/random_sample.py
visidata/features/regex.py
visidata/features/reload_every.py
visidata/features/rename_col_cascade.py
visidata/features/repeat.py
visidata/features/scroll_context.py
visidata/features/select_equal_selected.py
visidata/features/setcol_fake.py
visidata/features/slide.py
visidata/features/sparkline.py
visidata/features/status_source.py
visidata/features/sysedit.py
visidata/features/sysopen_mailcap.py
visidata/features/term_extras.py
visidata/features/transpose.py
visidata/features/type_ipaddr.py
visidata/features/type_url.py
visidata/features/unfurl.py
visidata/features/window.py
visidata/guides/ClipboardGuide.md
visidata/guides/ColumnsGuide.md
visidata/guides/CommandsSheet.md
visidata/guides/DirSheet.md
visidata/guides/ErrorsSheet.md
visidata/guides/FrequencyTable.md
visidata/guides/GrepSheet.md
visidata/guides/JsonSheet.md
visidata/guides/MacrosSheet.md
visidata/guides/MeltGuide.md
visidata/guides/MemorySheet.md
visidata/guides/MenuGuide.md
visidata/guides/ModifyGuide.md
visidata/guides/PivotGuide.md
visidata/guides/RegexGuide.md
visidata/guides/SelectionGuide.md
visidata/guides/SlideGuide.md
visidata/guides/SplitpaneGuide.md
visidata/guides/TypesSheet.md
visidata/guides/XsvGuide.md
visidata/loaders/__init__.py
visidata/loaders/_pandas.py
visidata/loaders/api_airtable.py
visidata/loaders/api_matrix.py
visidata/loaders/api_reddit.py
visidata/loaders/api_zulip.py
visidata/loaders/archive.py
visidata/loaders/arrow.py
visidata/loaders/conll.py
visidata/loaders/csv.py
visidata/loaders/eml.py
visidata/loaders/f5log.py
visidata/loaders/fec.py
visidata/loaders/fixed_width.py
visidata/loaders/frictionless.py
visidata/loaders/geojson.py
visidata/loaders/google.py
visidata/loaders/graphviz.py
visidata/loaders/grep.py
visidata/loaders/hdf5.py
visidata/loaders/html.py
visidata/loaders/http.py
visidata/loaders/imap.py
visidata/loaders/jrnl.py
visidata/loaders/json.py
visidata/loaders/jsonla.py
visidata/loaders/lsv.py
visidata/loaders/mailbox.py
visidata/loaders/markdown.py
visidata/loaders/mbtiles.py
visidata/loaders/msgpack.py
visidata/loaders/mysql.py
visidata/loaders/npy.py
visidata/loaders/odf.py
visidata/loaders/orgmode.py
visidata/loaders/pandas_freqtbl.py
visidata/loaders/parquet.py
visidata/loaders/pcap.py
visidata/loaders/pdf.py
visidata/loaders/png.py
visidata/loaders/postgres.py
visidata/loaders/rec.py
visidata/loaders/s3.py
visidata/loaders/sas.py
visidata/loaders/scrape.py
visidata/loaders/shp.py
visidata/loaders/spss.py
visidata/loaders/sqlite.py
visidata/loaders/texttables.py
visidata/loaders/toml.py
visidata/loaders/tsv.py
visidata/loaders/ttf.py
visidata/loaders/unzip_http.py
visidata/loaders/usv.py
visidata/loaders/vcf.py
visidata/loaders/vds.py
visidata/loaders/vdx.py
visidata/loaders/xlsb.py
visidata/loaders/xlsx.py
visidata/loaders/xml.py
visidata/loaders/xword.py
visidata/loaders/yaml.py
visidata/man/parse_options.py
visidata/man/vd.1
visidata/man/vd.txt
visidata/man/visidata.1
visidata/tests/__init__.py
visidata/tests/benchmark.csv
visidata/tests/conftest.py
visidata/tests/sample.tsv
visidata/tests/test_cliptext.py
visidata/tests/test_commands.py
visidata/tests/test_completer.py
visidata/tests/test_date.py
visidata/tests/test_edittext.py
visidata/tests/test_features.py
visidata/tests/test_menu.py
visidata/tests/test_path.py
visidata/themes/__init__.py
visidata/themes/ascii8.py
visidata/themes/asciimono.py
visidata/themes/light.py
visidata/vendor/appdirs.py visidata-3.1.1/visidata.egg-info/dependency_links.txt 0000664 0000000 0000000 00000000001 14770045464 0022677 0 ustar 00root root 0000000 0000000
visidata-3.1.1/visidata.egg-info/entry_points.txt 0000664 0000000 0000000 00000000062 14770045464 0022125 0 ustar 00root root 0000000 0000000 [console_scripts]
visidata = visidata.main:vd_cli
visidata-3.1.1/visidata.egg-info/requires.txt 0000664 0000000 0000000 00000000461 14770045464 0021232 0 ustar 00root root 0000000 0000000 python-dateutil
importlib-metadata>=3.6
[:platform_system == "Windows"]
windows-curses!=2.3.1
[:python_version < "3.9"]
importlib_resources
[test]
brotli
dnslib
dpkt
fecfile
Faker
h5py
lxml
msgpack
odfpy
openpyxl
pandas>=1.5.3
pyarrow
pyconll
pypng
pytest
PyYAML>=5.1
tabulate
tomli
wcwidth
xport>=3.0
visidata-3.1.1/visidata.egg-info/top_level.txt 0000664 0000000 0000000 00000000011 14770045464 0021353 0 ustar 00root root 0000000 0000000 visidata
visidata-3.1.1/visidata/ 0000775 0000000 0000000 00000000000 14770045464 0015137 5 ustar 00root root 0000000 0000000 visidata-3.1.1/visidata/__init__.py 0000664 0000000 0000000 00000010735 14770045464 0017256 0 ustar 00root root 0000000 0000000 'VisiData: a curses interface for exploring and arranging tabular data'
__version__ = '3.1.1'
__version_info__ = 'VisiData v' + __version__
__author__ = 'Saul Pwanson '
__status__ = 'Production/Stable'
__copyright__ = 'Copyright (c) 2016-2021 ' + __author__
class EscapeException(BaseException):
'Inherits from BaseException to avoid "except Exception" clauses. Do not use a blanket "except:" or the task will be uncancelable.'
pass
def addGlobals(*args, **kwargs):
'''Update the VisiData globals dict with items from *args* and *kwargs*; to add symbols available to command execstrings and eval strings like command expr.'
Dunder methods are ignored, to prevent accidentally overwriting housekeeping methods.'''
drop_dunder = lambda d: {k: v for k, v in d.items() if not k.startswith("__")}
for g in args:
globals().update(drop_dunder(g))
globals().update(drop_dunder(kwargs))
def getGlobals():
'Return the VisiData globals dict.'
return globals()
from .utils import *
from .extensible import *
from .vdobj import *
vd = VisiData()
vd.version = __version__
vd.addGlobals = addGlobals
vd.getGlobals = getGlobals
import visidata.keys
from .basesheet import *
import visidata.settings
# importModule tracks where commands/options/etc are coming from (via vd.importingModule)
core_imports = '''
import visidata.errors
import visidata.editor
import visidata.color
import visidata.cliptext
import visidata.mainloop
import visidata.menu
import visidata.wrappers
import visidata.undo
import visidata._types
import visidata.column
import visidata.interface
import visidata.sheets
import visidata.rename_col
import visidata.indexsheet
import visidata.statusbar
import visidata.textsheet
import visidata.threads
import visidata.path
import visidata.guide
import visidata.stored_list
import visidata._input
import visidata.tuiwin
import visidata.mouse
import visidata.movement
import visidata.type_date
import visidata._urlcache
import visidata.selection
import visidata.text_source
import visidata.loaders
import visidata.loaders.tsv
import visidata.pyobj
import visidata.loaders.json
import visidata._open
import visidata.save
import visidata.search
import visidata.expr
import visidata.metasheets
import visidata.input_history
import visidata.optionssheet
import visidata.type_currency
import visidata.type_floatsi
import visidata.clean_names
import visidata.cmdlog
import visidata.clipboard
import visidata.choose
import visidata.aggregators
import visidata.pivot
import visidata.freqtbl
import visidata.canvas
import visidata.canvas_text
import visidata.graph
import visidata.motd
import visidata.shell
import visidata.main
import visidata.help
import visidata.modify
import visidata.sort
import visidata.memory
import visidata.macros
import visidata.macos
import visidata.windows
import visidata.form
import visidata.sidebar
import visidata.ddwplay
import visidata.plugins
import visidata.theme
import visidata.apps
import visidata.fuzzymatch
import visidata.hint
'''
for line in core_imports.splitlines():
if not line: continue
module = line[len('import '):]
vd.importModule(module)
vd.importSubmodules('visidata.loaders')
def importFeatures():
vd.importSubmodules('visidata.features')
vd.importSubmodules('visidata.themes')
import visidata.deprecated
vd.importModule('copy', 'copy deepcopy'.split())
vd.importModule('builtins', 'abs all any ascii bin bool bytes callable chr complex dict dir divmod enumerate eval filter float format getattr hex int len list map max min next oct ord pow range repr reversed round set sorted str sum tuple type zip'.split())
vd.importModule('math', 'acos acosh asin asinh atan atan2 atanh ceil copysign cos cosh degrees dist erf erfc exp expm1 fabs factorial floor fmod frexp fsum gamma gcd hypot isclose isfinite isinf isnan isqrt lcm ldexp lgamma log log1p log10 log2 modf radians remainder sin sinh sqrt tan tanh trunc prod perm comb nextafter ulp pi e tau inf nan'.split())
vd.importModule('random', 'randrange randint choice choices sample uniform gauss lognormvariate'.split())
vd.importModule('string', 'ascii_letters ascii_lowercase ascii_uppercase digits hexdigits punctuation printable whitespace'.split())
vd.importModule('json')
vd.importModule('itertools')
vd.importModule('curses')
import visidata.experimental # import nothing by default but make package accessible
vd.finalInit() # call all VisiData.init() from modules
importFeatures()
vd.addGlobals(vd=vd) # globals())
visidata-3.1.1/visidata/__main__.py 0000664 0000000 0000000 00000000043 14770045464 0017226 0 ustar 00root root 0000000 0000000 from .main import vd_cli
vd_cli()
visidata-3.1.1/visidata/_input.py 0000664 0000000 0000000 00000055277 14770045464 0017027 0 ustar 00root root 0000000 0000000 from contextlib import suppress
import curses
import visidata
from visidata import EscapeException, ExpectedException, clipdraw, Sheet, VisiData, BaseSheet
from visidata import vd, colors, dispwidth, ColorAttr
from visidata import AttrDict
vd.theme_option('color_edit_unfocused', '238 on 110', 'display color for unfocused input in form')
vd.theme_option('color_edit_cell', '233 on 110', 'cell color to use when editing cell')
vd.theme_option('disp_edit_fill', '_', 'edit field fill character')
vd.theme_option('disp_unprintable', '·', 'substitute character for unprintables')
vd.theme_option('mouse_interval', 1, 'max time between press/release for click (ms)', sheettype=None)
vd.disp_help = 0 # current page of help shown
vd._help_sidebars = [] # list of (help:str|HelpPane, title:str)
class AcceptInput(Exception):
'*args[0]* is the input to be accepted'
vd._injectedInput = None # for vd.injectInput
@VisiData.api
def injectInput(vd, x):
'Use *x* as input to next command.'
assert vd._injectedInput is None, vd._injectedInput
vd._injectedInput = x
@VisiData.api
def getCommandInput(vd):
if vd._injectedInput is not None:
r = vd._injectedInput
vd._injectedInput = None
return r
return vd.getLastArgs()
@BaseSheet.after
def execCommand(sheet, longname, *args, **kwargs):
if vd._injectedInput is not None:
vd.debug(f'{longname} did not consume input "{vd._injectedInput}"')
vd._injectedInput = None
def acceptThenFunc(*longnames):
def _acceptthen(v, i):
for longname in longnames:
vd.queueCommand(longname)
raise AcceptInput(v)
return _acceptthen
# editline helpers
class EnableCursor:
def __enter__(self):
with suppress(curses.error):
curses.mousemask(0)
curses.curs_set(1)
def __exit__(self, exc_type, exc_val, tb):
with suppress(curses.error):
curses.curs_set(0)
if vd.options.mouse_interval:
curses.mousemask(curses.MOUSE_ALL if hasattr(curses, "MOUSE_ALL") else 0xffffffff)
else:
curses.mousemask(0)
def until_get_wch(scr):
'Ignores get_wch timeouts'
ret = None
while not ret:
try:
ret = vd.get_wch(scr)
except curses.error:
pass
if isinstance(ret, int):
return chr(ret)
return ret
def splice(v:str, i:int, s:str):
'Insert `s` into string `v` at `i` (such that v[i] == s[0]).'
return v if i < 0 else v[:i] + s + v[i:]
@VisiData.api
def drawInputHelp(vd, scr):
if not scr or not vd.cursesEnabled:
return
sheet = vd.activeSheet
if not sheet:
return
vd.drawSidebar(scr, sheet)
def clean_printable(s):
'Escape unprintable characters.'
return ''.join(c if c.isprintable() else vd.options.disp_unprintable for c in str(s))
def delchar(s, i, remove=1):
'Delete `remove` characters from str `s` beginning at position `i`.'
return s if i < 0 else s[:i] + s[i+remove:]
def find_nonword(s, a, b, incr):
if not s: return 0
a = min(max(a, 0), len(s)-1)
b = min(max(b, 0), len(s)-1)
if incr < 0:
while not s[b].isalnum() and b >= a: # first skip non-word chars
b += incr
while s[b].isalnum() and b >= a:
b += incr
return min(max(b, -1), len(s))
else:
while not s[a].isalnum() and a < b: # first skip non-word chars
a += incr
while s[a].isalnum() and a < b:
a += incr
return min(max(a, 0), len(s))
class InputWidget:
def __init__(self,
value:str='',
i=0,
display=True,
history=[],
completer=lambda text,idx: None,
options=None,
fillchar=''):
'''
- value: starting value
- i: starting index into value
- display: False to not display input (for sensitive input, e.g. a password)
- history: list of strings; earliest entry first.
- completer: func(value:str, idx:int) takes the current value and tab completion index, and returns a string if there is a completion available, or None if not.
- options: sheet.options; defaults to vd.options.
'''
options = options or vd.options
self.orig_value = value
self.first_action = (i == 0) # whether this would be the 'first action'; if so, clear text on input
# display theme
self.fillchar = fillchar or options.disp_edit_fill
self.truncchar = options.disp_truncator
self.display = display # if False, obscure before displaying
# main state
self.value = self.orig_value # value under edit
self.current_i = i
self.insert_mode = True
# history state
self.history = history
self.hist_idx = None
self.prev_val = None
# completion state
self.comps_idx = -1
self.completer_func = completer
self.former_i = None
self.just_completed = False
def editline(self, scr, y, x, w, attr=ColorAttr(), updater=lambda val: None, bindings={}, clear=True) -> str:
'If *clear* is True, clear whole editing area before displaying.'
with EnableCursor():
while True:
vd.drawSheet(scr, vd.activeSheet)
if updater:
updater(self.value)
vd.drawInputHelp(scr)
self.draw(scr, y, x, w, attr, clear=clear)
ch = vd.getkeystroke(scr)
if ch in bindings:
self.value, self.current_i = bindings[ch](self.value, self.current_i)
else:
if self.handle_key(ch, scr):
return self.value
def draw(self, scr, y, x, w, attr=ColorAttr(), clear=True):
i = self.current_i # the onscreen offset within the field where v[i] is displayed
left_truncchar = right_truncchar = self.truncchar
if self.display:
dispval = clean_printable(self.value)
else:
dispval = '*' * len(self.value)
if len(dispval) < w: # entire value fits
dispval += self.fillchar*(w-len(dispval)-1)
elif i == len(dispval): # cursor after value (will append)
i = w-1
dispval = left_truncchar + dispval[len(dispval)-w+2:] + self.fillchar
elif i >= len(dispval)-w//2: # cursor within halfwidth of end
i = w-(len(dispval)-i)
dispval = left_truncchar + dispval[len(dispval)-w+1:]
elif i <= w//2: # cursor within halfwidth of beginning
dispval = dispval[:w-1] + right_truncchar
else:
i = w//2 # visual cursor stays right in the middle
k = 1 if w%2==0 else 0 # odd widths have one character more
dispval = left_truncchar + dispval[self.current_i-w//2+1:self.current_i+w//2-k] + right_truncchar
prew = clipdraw(scr, y, x, dispval[:i], attr, w, clear=clear, literal=True)
clipdraw(scr, y, x+prew, dispval[i:], attr, w-prew+1, clear=clear, literal=True)
if scr:
scr.move(y, x+prew)
def handle_key(self, ch:str, scr) -> bool:
'Return True to accept current input. Raise EscapeException on Ctrl+C, Ctrl+Q, or ESC.'
i = self.current_i
v = self.value
if ch == '': return False
elif ch == 'KEY_IC': self.insert_mode = not self.insert_mode
elif ch == '^A' or ch == 'KEY_HOME': i = 0
elif ch == '^B' or ch == 'KEY_LEFT': i -= 1
elif ch in ('^C', '^Q', '^['): raise EscapeException(ch)
elif ch == '^D' or ch == 'KEY_DC': v = delchar(v, i)
elif ch == '^E' or ch == 'KEY_END': i = len(v)
elif ch == '^F' or ch == 'KEY_RIGHT': i += 1
elif ch == '^G':
vd.cycleSidebar()
return False # not considered a first keypress
elif ch in ('^H', 'KEY_BACKSPACE', '^?'): i -= 1; v = delchar(v, i)
elif ch == '^I': v, i = self.completion(v, i, +1)
elif ch == 'KEY_BTAB': v, i = self.completion(v, i, -1)
elif ch in ['^J', '^M']: return True # ENTER to accept value
elif ch == '^K': v = v[:i] # ^Kill to end-of-line
elif ch == '^N':
c = ''
while not c:
c = vd.getkeystroke(scr)
c = vd.prettykeys(c)
i += len(c)
v += c
elif ch == '^O': self.value = vd.launchExternalEditor(v); return True # auto-accept after $EDITOR
elif ch == '^R': v = self.orig_value # ^Reload initial value
elif ch == '^T': v = delchar(splice(v, i-2, v[i-1:i]), i) # swap chars
elif ch == '^U': v = v[i:]; i = 0 # clear to beginning
elif ch == '^V': v = splice(v, i, until_get_wch(scr)); i += 1 # literal character
elif ch == '^W': j = find_nonword(v, 0, i-1, -1); v = v[:j+1] + v[i:]; i = j+1 # erase word
elif ch == '^Y': v = splice(v, i, str(vd.memory.clipval))
elif ch == '^Z': vd.suspend()
# CTRL+arrow
elif ch == 'kLFT5': i = find_nonword(v, 0, i-1, -1)+1; # word left
elif ch == 'kRIT5': i = find_nonword(v, i+1, len(v)-1, +1)+1; # word right
elif ch == 'kUP5': pass
elif ch == 'kDN5': pass
elif self.history and ch == 'KEY_UP': v, i = self.prev_history(v, i)
elif self.history and ch == 'KEY_DOWN': v, i = self.next_history(v, i)
elif len(ch) > 1: pass
else:
if self.first_action:
v = ''
if self.insert_mode:
v = splice(v, i, ch)
else:
v = v[:i] + ch + v[i+1:]
i += 1
if i < 0: i = 0
# v may have a non-str type with no len()
v = str(v)
if i > len(v): i = len(v)
self.current_i = i
self.value = v
self.first_action = False
self.reset_completion()
return False
def completion(self, v, i, state_incr):
self.just_completed = True
self.comps_idx += state_incr
if self.former_i is None:
self.former_i = i
try:
r = self.completer_func(v[:self.former_i], self.comps_idx)
except Exception as e:
# raise # beep/flash; how to report exception?
return v, i
if not r:
# beep/flash to indicate no matches?
return v, i
v = r + v[i:]
return v, len(v)
def reset_completion(self):
if self.just_completed:
self.just_completed = False
else:
self.former_i = None
self.comps_idx = -1
def prev_history(self, v, i):
if self.hist_idx is None:
self.hist_idx = len(self.history)
self.prev_val = v
if self.hist_idx > 0:
self.hist_idx -= 1
v = self.history[self.hist_idx]
i = len(str(v))
return v, i
def next_history(self, v, i):
if self.hist_idx is None:
return v, i
elif self.hist_idx < len(self.history)-1:
self.hist_idx += 1
v = self.history[self.hist_idx]
else:
v = self.prev_val
self.hist_idx = None
i = len(str(v))
return v, i
@VisiData.api
def editText(vd, y, x, w, attr=ColorAttr(), value='',
help='',
updater=None, bindings={},
display=True, record=True, clear=True, **kwargs):
'Invoke modal single-line editor at (*y*, *x*) for *w* terminal chars. Use *display* is False for sensitive input like passphrases. If *record* is True, get input from the cmdlog in batch mode, and save input to the cmdlog if *display* is also True. Return new value as string.'
v = None
if record and vd.cmdlog:
v = vd.getCommandInput()
if v is None:
if vd.options.batch:
return ''
if vd.activeSheet._scr is None:
raise Exception('active sheet does not have a screen')
if value is None:
value = ''
try:
widget = InputWidget(value=str(value), display=display, **kwargs)
with vd.AddedHelp(vd.getHelpPane('input', module='visidata'), 'Input Keystrokes Help'), \
vd.AddedHelp(help, 'Input Field Help'):
v = widget.editline(vd.activeSheet._scr, y, x, w, attr=attr, updater=updater, bindings=bindings, clear=clear)
except AcceptInput as e:
v = e.args[0]
if vd.cursesEnabled:
# clear keyboard buffer to neutralize multi-line pastes (issue#585)
curses.flushinp()
if display:
if record and vd.cmdlog:
vd.setLastArgs(v)
if value:
if isinstance(value, (int, float)) and v[-1] == '%': #2082
pct = float(v[:-1])
v = pct*value/100
# convert back to type of original value
v = type(value)(v)
return v
@VisiData.api
def inputsingle(vd, prompt, record=True):
'Display prompt and return single character of user input.'
sheet = vd.activeSheet
v = None
if record and vd.cmdlog:
v = vd.getCommandInput()
if v is not None:
return v
y = sheet.windowHeight-1
w = sheet.windowWidth
rstatuslen = vd.drawRightStatus(sheet._scr, sheet)
promptlen = clipdraw(sheet._scr, y, 0, prompt, 0, w=w-rstatuslen-1)
sheet._scr.move(y, w-promptlen-rstatuslen-2)
while not v:
v = vd.getkeystroke(sheet._scr)
if record and vd.cmdlog:
vd.setLastArgs(v)
return v
@VisiData.api
def inputMultiple(vd, updater=lambda val: None, record=True, **kwargs):
'A simple form, where each input is an entry in `kwargs`, with the key being the key in the returned dict, and the value being a dictionary of kwargs to the singular input().'
sheet = vd.activeSheet
scr = sheet._scr
previnput = vd.getCommandInput()
if previnput is not None:
ret = None
if isinstance(previnput, str):
if previnput.startswith('{'):
ret = json.loads(previnput)
else:
ret = {k:v.get('value', '') for k,v in kwargs.items()}
primekey = list(ret.keys())[0]
ret[primekey] = previnput
if isinstance(previnput, dict):
ret = previnput
if ret:
if record and vd.cmdlog:
vd.setLastArgs(ret)
return ret
assert False, type(previnput)
y = sheet.windowHeight-1
maxw = sheet.windowWidth//2
attr = colors.color_edit_unfocused
keys = list(kwargs.keys())
cur_input_key = keys[0]
if scr:
scr.erase()
for i, (k, v) in enumerate(kwargs.items()):
v['dy'] = i
v['w'] = maxw-dispwidth(v.get('prompt'))
class ChangeInput(Exception):
pass
def change_input(offset):
def _throw(v, i):
if scr:
scr.erase()
raise ChangeInput(v, offset)
return _throw
def _drawPrompt(val):
for k, v in kwargs.items():
maxw = min(sheet.windowWidth-1, max(dispwidth(v.get('prompt')), dispwidth(str(v.get('value', '')))))
promptlen = clipdraw(scr, y-v.get('dy'), 0, v.get('prompt'), attr, w=maxw) #1947
promptlen = clipdraw(scr, y-v.get('dy'), promptlen, v.get('value', ''), attr, w=maxw)
return updater(val)
while True:
try:
input_kwargs = kwargs[cur_input_key]
input_kwargs['value'] = vd.input(**input_kwargs,
attr=colors.color_edit_cell,
updater=_drawPrompt,
record=False,
bindings={
'KEY_BTAB': change_input(-1),
'^I': change_input(+1),
'KEY_SR': change_input(-1),
'KEY_SF': change_input(+1),
'kUP': change_input(-1),
'kDN': change_input(+1),
})
break
except ChangeInput as e:
input_kwargs['value'] = e.args[0]
offset = e.args[1]
i = keys.index(cur_input_key)
cur_input_key = keys[(i+offset)%len(keys)]
retargs = {}
lastargs = {}
for k, input_kwargs in kwargs.items():
v = input_kwargs.get('value', '')
retargs[k] = v
if input_kwargs.get('record', record):
if input_kwargs.get('display', True):
lastargs[k] = v
vd.addInputHistory(v, input_kwargs.get('type', ''))
if record:
if vd.cmdlog and lastargs:
vd.setLastArgs(lastargs)
return retargs
@VisiData.api
def input(vd, prompt, type=None, defaultLast=False, history=[], dy=0, attr=None, updater=lambda v: None, **kwargs):
'''Display *prompt* and return line of user input.
- *type*: string indicating the type of input to use for history.
- *history*: list of strings to use for input history.
- *defaultLast*: on empty input, if True, return last history item.
- *display*: pass False to not display input (for sensitive input, e.g. a password), and to also prevent recording input as if *record* is False
- *record*: pass False to not record input on cmdlog or input history (for sensitive or inconsequential input).
- *completer*: ``completer(val, idx)`` is called on TAB to get next completed value.
- *updater*: ``updater(val)`` is called every keypress or timeout.
- *bindings*: dict of keystroke to func(v, i) that returns updated (v, i)
- *dy*: number of lines from bottom of pane
- *attr*: curses attribute for prompt
- *help*: string to include in help
'''
if attr is None:
attr = ColorAttr()
sheet = vd.activeSheet
if not vd.cursesEnabled:
if kwargs.get('record', True) and vd.cmdlog:
return vd.getCommandInput()
if kwargs.get('display', True):
import builtins
return builtins.input(prompt)
else:
import getpass
return getpass.getpass(prompt)
if not history:
history = list(vd.inputHistory.setdefault(type, {}).keys())
y = sheet.windowHeight-dy-1
promptlen = dispwidth(prompt)
def _drawPrompt(val=''):
rstatuslen = vd.drawRightStatus(sheet._scr, sheet)
clipdraw(sheet._scr, y, 0, prompt, attr, w=sheet.windowWidth-rstatuslen-1)
updater(val)
return sheet.windowWidth-promptlen-rstatuslen-2
w = kwargs.pop('w', _drawPrompt())
ret = vd.editText(y, promptlen, w=w,
attr=colors.color_edit_cell,
options=vd.options,
history=history,
updater=_drawPrompt,
**kwargs)
if ret:
if kwargs.get('record', True) and kwargs.get('display', True):
vd.addInputHistory(ret, type=type)
elif defaultLast:
history or vd.fail("no previous input")
ret = history[-1]
return ret
@VisiData.api
def confirm(vd, prompt, exc=EscapeException):
'Display *prompt* on status line and demand input that starts with "Y" or "y" to proceed. Raise *exc* otherwise. Return True.'
if vd.options.batch and not vd.options.interactive:
return vd.fail('cannot confirm in batch mode: ' + prompt)
yn = vd.input(prompt, value='no', record=False)[:1]
if not yn or yn not in 'Yy':
msg = 'disconfirmed: ' + prompt
if exc:
raise exc(msg)
vd.warning(msg)
return False
return True
class CompleteKey:
def __init__(self, items):
self.items = items
def __call__(self, val, state):
opts = [x for x in self.items if x.startswith(val)]
return opts[state%len(opts)] if opts else val
@Sheet.api
def editCell(self, vcolidx=None, rowidx=None, value=None, **kwargs):
'''Call vd.editText for the cell at (*rowidx*, *vcolidx*). Return the new value, properly typed.
- *rowidx*: numeric index into ``self.rows``. If negative, indicates the column name in the header.
- *value*: if given, the starting input; otherwise the starting input is the cell value or column name as appropriate.
- *kwargs*: passthrough args to ``vd.editText``.
'''
if vcolidx is None:
vcolidx = self.cursorVisibleColIndex
x, w = self._visibleColLayout.get(vcolidx, (0, 0))
col = self.visibleCols[vcolidx]
if rowidx is None:
rowidx = self.cursorRowIndex
if rowidx < 0: # header
y = 0
value = value or col.name
else:
y, h = self._rowLayout.get(rowidx, (0, 0))
value = value or col.getDisplayValue(self.rows[self.cursorRowIndex])
bindings={
'kUP': acceptThenFunc('go-up', 'rename-col' if rowidx < 0 else 'edit-cell'),
'KEY_SR': acceptThenFunc('go-up', 'rename-col' if rowidx < 0 else 'edit-cell'),
'kDN': acceptThenFunc('go-down', 'rename-col' if rowidx < 0 else 'edit-cell'),
'KEY_SF': acceptThenFunc('go-down', 'rename-col' if rowidx < 0 else 'edit-cell'),
'KEY_SRIGHT': acceptThenFunc('go-right', 'rename-col' if rowidx < 0 else 'edit-cell'),
'KEY_SLEFT': acceptThenFunc('go-left', 'rename-col' if rowidx < 0 else 'edit-cell'),
'^I': acceptThenFunc('go-right', 'rename-col' if rowidx < 0 else 'edit-cell'),
'KEY_BTAB': acceptThenFunc('go-left', 'rename-col' if rowidx < 0 else 'edit-cell'),
}
if vcolidx >= self.nVisibleCols-1:
bindings['^I'] = acceptThenFunc('go-down', 'go-leftmost', 'edit-cell')
if vcolidx <= 0:
bindings['KEY_BTAB'] = acceptThenFunc('go-up', 'go-rightmost', 'edit-cell')
# update local bindings with kwargs.bindings instead of the inverse, to preserve kwargs.bindings for caller
bindings.update(kwargs.get('bindings', {}))
kwargs['bindings'] = bindings
editargs = dict(value=value, options=self.options)
editargs.update(kwargs) # update with user-specified args
r = vd.editText(y, x, w, attr=colors.color_edit_cell, **editargs)
if rowidx >= 0: # if not header
r = col.type(r) # convert input to column type, let exceptions be raised
return r
vd.addGlobals(CompleteKey=CompleteKey, AcceptInput=AcceptInput, InputWidget=InputWidget)
visidata-3.1.1/visidata/_open.py 0000664 0000000 0000000 00000015003 14770045464 0016610 0 ustar 00root root 0000000 0000000 import os
import os.path
import sys
from visidata import VisiData, vd, Path, BaseSheet, TableSheet, TextSheet, SettableColumn
vd.option('filetype', '', 'specify file type', replay=True)
@VisiData.api
def inputFilename(vd, prompt, *args, **kwargs):
completer= _completeFilename
if not vd.couldOverwrite(): #1805 don't suggest an existing file
completer = None
v = kwargs.get('value', '')
if v and Path(v).exists():
kwargs['value'] = ''
return vd.input(prompt, type="filename", *args, completer=completer, **kwargs).strip()
@VisiData.api
def inputPath(vd, *args, **kwargs):
return Path(vd.inputFilename(*args, **kwargs))
def _completeFilename(val, state):
i = val.rfind('/')
if i < 0: # no /
base = ''
partial = val
elif i == 0: # root /
base = '/'
partial = val[1:]
else:
base = val[:i]
partial = val[i+1:]
files = []
for f in os.listdir(Path(base or '.')):
if f.startswith(partial):
files.append(os.path.join(base, f))
files.sort()
return files[state%len(files)]
@VisiData.api
def guessFiletype(vd, p, *args, funcprefix='guess_'):
'''Call all vd.guess_(p) functions and return best candidate sheet based on file contents.'''
guessfuncs = [getattr(vd, x) for x in dir(vd) if x.startswith(funcprefix)]
filetypes = []
for f in guessfuncs:
try:
filetype = f(p, *args)
if filetype:
filetype['_guesser'] = f.__name__
filetypes.append(filetype)
except FileNotFoundError:
pass
except Exception as e:
vd.debug(f'{f.__name__}: {e}')
if filetypes:
return sorted(filetypes, key=lambda r: -r.get('_likelihood', 1))[0]
return {}
@VisiData.api
def guess_extension(vd, path):
# try auto-detect from extension
ext = path.suffix[1:].lower()
openfunc = getattr(vd, f'open_{ext}', vd.getGlobals().get(f'open_{ext}'))
if openfunc:
return dict(filetype=ext, _likelihood=3)
@VisiData.api
def openPath(vd, p, filetype=None, create=False):
'''Call ``open_(p)`` or ``openurl_(p, filetype)``. Return constructed but unloaded sheet of appropriate type.
If True, *create* will return a new, blank **Sheet** if file does not exist.'''
if p.scheme and not p.has_fp():
schemes = p.scheme.split('+')
openfuncname = 'openurl_' + schemes[-1]
openfunc = getattr(vd, openfuncname, None) or vd.getGlobals().get(openfuncname, None)
if not openfunc:
vd.fail(f'no loader for url scheme: {p.scheme}')
return openfunc(p, filetype=filetype)
if not p.exists() and not create:
return None
if not filetype:
filetype = p.ext or vd.options.filetype
filetype = filetype.lower()
if not p.exists():
newfunc = getattr(vd, 'new_' + filetype, vd.getGlobals().get('new_' + filetype))
if not newfunc:
vd.warning('%s does not exist, creating new sheet' % p)
return vd.newSheet(p.base_stem, 1, source=p)
vd.status('creating blank %s' % (p.given))
return newfunc(p)
if p.is_fifo():
# read the file as text, into a RepeatFile that can be opened multiple times
p = Path(p.given, fp=p.open(mode='rb'))
openfuncname = 'open_' + filetype
openfunc = getattr(vd, openfuncname, vd.getGlobals().get(openfuncname))
if not openfunc:
opts = vd.guessFiletype(p)
if opts and 'filetype' in opts:
filetype = opts['filetype']
openfuncname = 'open_' + filetype
openfunc = getattr(vd, openfuncname, vd.getGlobals().get(openfuncname))
if not openfunc:
vd.error(f'guessed {filetype} but no {openfuncname}')
vs = openfunc(p)
for k, v in opts.items():
if k != 'filetype' and not k.startswith('_'):
setattr(vs.options, k, v)
vd.warning('guessed "%s" filetype based on contents' % opts['filetype'])
return vs
vd.warning('unknown "%s" filetype' % filetype)
filetype = 'txt'
openfunc = vd.open_txt
vd.status('opening %s as %s' % (p.given, filetype))
return openfunc(p)
@VisiData.api
def openSource(vd, p, filetype=None, create=False, **kwargs):
'''Return unloaded sheet object for *p* opened as the given *filetype* and with *kwargs* as option overrides. *p* can be a Path or a string (filename, url, or "-" for stdin).
when true, *create* will return a blank sheet, if file does not exist.'''
if isinstance(p, BaseSheet):
return p
filetype = filetype or vd.options.getonly('filetype', str(p), '') #1710
filetype = filetype or vd.options.getonly('filetype', 'global', '')
vs = None
if isinstance(p, str):
if '://' in p:
vs = vd.openPath(Path(p), filetype=filetype) # convert to Path and recurse
elif p == '-':
if sys.stdin.isatty():
vd.fail('cannot open stdin when it is a tty')
vs = vd.openPath(vd.stdinSource, filetype=filetype)
else:
vs = vd.openPath(Path(p), filetype=filetype, create=create) # convert to Path and recurse
else:
vs = vd.openPath(p, filetype=filetype, create=create)
for optname, optval in kwargs.items():
vs.options[optname] = optval
return vs
#### enable external addons
@VisiData.api
def open_txt(vd, p):
'Create sheet from `.txt` file at Path `p`, checking whether it is TSV.'
if p.exists(): #1611
with p.open(encoding=vd.options.encoding) as fp:
delimiter = vd.options.delimiter
try:
if delimiter and delimiter in next(fp): # peek at the first line
return vd.open_tsv(p) # TSV often have .txt extension
except StopIteration:
return TableSheet(p.base_stem, columns=[SettableColumn(width=vd.options.default_width)], source=p)
return TextSheet(p.base_stem, source=p)
BaseSheet.addCommand('o', 'open-file', 'vd.push(openSource(inputFilename("open: "), create=True))', 'Open file or URL')
TableSheet.addCommand('zo', 'open-cell-file', 'vd.push(openSource(cursorDisplay) or fail(f"file {cursorDisplay} does not exist"))', 'Open file or URL from path in current cell')
BaseSheet.addCommand('gU', 'undo-last-quit', 'push(allSheets[-1])', 'reopen most recently closed sheet')
vd.addMenuItems('''
File > Open > input file/url > open-file
File > Reopen last closed > undo-last-quit
''')
visidata-3.1.1/visidata/_types.py 0000664 0000000 0000000 00000011150 14770045464 0017012 0 ustar 00root root 0000000 0000000 # VisiData uses Python native int, float, str, and adds simple anytype.
import locale
from visidata import options, TypedWrapper, vd, VisiData
vd.help_float_fmt = '''
- fmt starting with `'%'` (like `%0.2f`) will use [:onclick https://docs.python.org/3.6/library/locale.html#locale.format_string]locale.format_string[/]
- other fmt (like `{:.02f}` is passed to Python [:onclick https://docs.python.org/3/library/string.html#custom-string-formatting)]string.format][/]
'''
vd.help_int_fmt = '''
- fmt starting with `'%'` (like `%04d`) will use [:onclick https://docs.python.org/3.6/library/locale.html#locale.format_string]locale.format_string[/]
- other fmt (like `{:4d}` is passed to Python [:onclick https://docs.python.org/3/library/string.html#custom-string-formatting)]string.format[/]
'''
vd.option('disp_float_fmt', '{:.02f}', 'default fmtstr to format float values', replay=True, help=vd.help_float_fmt)
vd.option('disp_int_fmt', '{:d}', 'default fmtstr to format int values', replay=True, help=vd.help_int_fmt)
vd.numericTypes = [int,float]
# VisiDataType .typetype are e.g. int, float, str, and used internally in these ways:
#
# o = typetype(val) # for interpreting raw value
# o = typetype(str) # for conversion from string (when setting)
# o = typetype() # for default value to be used when conversion fails
#
# The resulting object o must be orderable and convertible to a string for display and certain outputs (like csv).
#
# .icon is a single character that appears in the notes field of cells and column headers.
# .formatter(fmtstr, typedvalue) returns a string of the formatted typedvalue according to fmtstr.
# .fmtstr is the default fmtstr passed to .formatter.
def anytype(r=None):
'minimalist "any" passthrough type'
return r
anytype.__name__ = ''
@VisiData.global_api
def numericFormatter(vd, fmtstr, typedval):
try:
fmtstr = fmtstr or options['disp_'+type(typedval).__name__+'_fmt']
if fmtstr[0] == '%':
return locale.format_string(fmtstr, typedval, grouping=False)
else:
return fmtstr.format(typedval)
except ValueError:
return str(typedval)
@VisiData.api
def numericType(vd, icon='', fmtstr='', formatter=vd.numericFormatter):
'''Decorator for numeric types.'''
def _decorator(f):
vd.addType(f, icon=icon, fmtstr=fmtstr, formatter=formatter, name=f.__name__)
vd.numericTypes.append(f)
return f
return _decorator
class VisiDataType:
'Register *typetype* in the typemap.'
def __init__(self, typetype=None, icon=None, fmtstr='', formatter=vd.numericFormatter, key='', name=None):
self.typetype = typetype or anytype # int or float or other constructor
self.name = name or getattr(typetype, '__name__', str(typetype))
self.icon = icon # show in rightmost char of column
self.fmtstr = fmtstr
self.formatter = formatter
self.key = key
@VisiData.api
def addType(vd, typetype=None, icon=None, fmtstr='', formatter=vd.numericFormatter, key='', name=None):
'''Add type to type map.
- *typetype*: actual type class *TYPE* above
- *icon*: unicode character in column header
- *fmtstr*: format string to use if fmtstr not given
- *formatter*: formatting function to call as ``formatter(fmtstr, typedvalue)``
'''
t = VisiDataType(typetype=typetype, icon=icon, fmtstr=fmtstr, formatter=formatter, key=key, name=name)
if typetype:
vd.typemap[typetype] = t
if name:
vd.addGlobals({name: typetype})
return t
vdtype = vd.addType
# typemap [vtype] -> VisiDataType
vd.typemap = {}
@VisiData.api
def getType(vd, typetype):
return vd.typemap.get(typetype) or VisiDataType()
vdtype(None, '∅', name='none')
vdtype(anytype, '', formatter=lambda _,v: str(v))
vdtype(str, '~', formatter=lambda _,v: v)
vdtype(int, '#')
vdtype(float, '%')
vdtype(dict, '')
vdtype(list, '')
@VisiData.api
def isNumeric(vd, col):
return col.type in vd.numericTypes
def deduceType(v):
if isinstance(v, (float, int)):
return type(v)
else:
return anytype
##
@vd.numericType('%')
def floatlocale(*args):
'Calculate float() using system locale set in LC_NUMERIC.'
if not args:
return 0.0
return locale.atof(*args)
@vd.numericType('♯', fmtstr='%d')
class vlen(int):
def __new__(cls, v=0):
if isinstance(v, (vlen, int, float)):
return super(vlen, cls).__new__(cls, v)
else:
return super(vlen, cls).__new__(cls, len(v))
def __len__(self):
return self
vd.addGlobals(anytype=anytype,
vdtype=vdtype,
deduceType=deduceType)
visidata-3.1.1/visidata/_urlcache.py 0000664 0000000 0000000 00000002561 14770045464 0017442 0 ustar 00root root 0000000 0000000 import os
import os.path
import time
from visidata import vd, VisiData, Path, modtime
from visidata.settings import _get_cache_dir
@VisiData.global_api
def urlcache(vd, url, days=1, text=True, headers={}):
'Return Path object to local cache of url contents.'
from urllib.request import Request, urlopen
import urllib.parse
cache_dir = _get_cache_dir()
os.makedirs(cache_dir, exist_ok=True)
p = Path(cache_dir / urllib.parse.quote(url, safe=''))
if p.exists():
secs = time.time() - modtime(p)
if secs < days*24*60*60:
return p
req = Request(url)
for k, v in headers.items():
req.add_header(k, v)
with urlopen(req) as fp:
ret = fp.read()
if text:
ret = ret.decode('utf-8').strip()
with p.open(mode='w', encoding='utf-8') as fpout:
fpout.write(ret)
else:
with p.open_bytes(mode='w') as fpout:
fpout.write(ret)
return p
@VisiData.api
def enable_requests_cache(vd):
try:
import requests
import requests_cache
requests_cache.install_cache(str(Path(os.path.join(vd.options.visidata_dir, 'httpcache'))), backend='sqlite', expire_after=24*60*60)
except ModuleNotFoundError:
vd.warning('install requests_cache for less intrusive scraping')
vd.addGlobals({'urlcache': urlcache})
visidata-3.1.1/visidata/aggregators.py 0000664 0000000 0000000 00000025070 14770045464 0020022 0 ustar 00root root 0000000 0000000 import sys
import math
import functools
import collections
import statistics
from visidata import Progress, Sheet, Column, ColumnsSheet, VisiData
from visidata import vd, anytype, vlen, asyncthread, wrapply, AttrDict, date, INPROGRESS
vd.help_aggregators = '''# Choose Aggregators
Start typing an aggregator name or description.
Multiple aggregators can be added by separating spaces.
- `Enter` to select top aggregator.
- `Tab` to highlight top aggregator.
## When Aggregator Highlighted
- `Tab`/`Shift+Tab` to cycle highlighted aggregator.
- `Enter` to select aggregators.
- `Space` to add highlighted aggregator.
- `0-9` to add numbered aggregator.
'''
vd.option('null_value', None, 'a value to be counted as null', replay=True)
@Column.api
def getValueRows(self, rows):
'Generate (value, row) for each row in *rows* at this column, excluding null and error values.'
f = self.sheet.isNullFunc()
for r in Progress(rows, 'calculating'):
try:
v = self.getTypedValue(r)
if not f(v):
yield v, r
except Exception:
pass
@Column.api
def getValues(self, rows):
'Generate value for each row in *rows* at this column, excluding null and error values.'
for v, r in self.getValueRows(rows):
yield v
vd.aggregators = collections.OrderedDict() # [aggname] -> annotated func, or list of same
Column.init('aggstr', str, copy=True)
Column.init('_aggregatedTotals', dict) # [aggname] -> agg total over all rows
def aggregators_get(col):
'A space-separated names of aggregators on this column.'
aggs = []
for k in (col.aggstr or '').split():
agg = vd.aggregators[k]
aggs += agg if isinstance(agg, list) else [agg]
return aggs
def aggregators_set(col, aggs):
if isinstance(aggs, str):
newaggs = []
for agg in aggs.split():
if agg not in vd.aggregators:
vd.fail(f'unknown aggregator {agg}')
newaggs.append(agg)
elif aggs is None:
newaggs = ''
else:
newaggs = [agg.name for agg in aggs]
col.aggstr = ' '.join(newaggs)
Column.aggregators = property(aggregators_get, aggregators_set)
class Aggregator:
def __init__(self, name, type, funcValues=None, helpstr='foo'):
'Define aggregator `name` that calls funcValues(values)'
self.type = type
self.funcValues = funcValues # funcValues(values)
self.helpstr = helpstr
self.name = name
def aggregate(self, col, rows): # wrap builtins so they can have a .type
vals = list(col.getValues(rows))
try:
return self.funcValues(vals)
except Exception as e:
if len(vals) == 0:
return None
raise e
@VisiData.api
def aggregator(vd, name, funcValues, helpstr='', *, type=None):
'''Define simple aggregator *name* that calls ``funcValues(values)`` to aggregate *values*.
Use *type* to force type of aggregated column (default to use type of source column).'''
vd.aggregators[name] = Aggregator(name, type, funcValues=funcValues, helpstr=helpstr)
## specific aggregator implementations
def mean(vals):
vals = list(vals)
if vals:
return float(sum(vals))/len(vals)
def vsum(vals):
return sum(vals, start=type(vals[0] if len(vals) else 0)()) #1996
# http://code.activestate.com/recipes/511478-finding-the-percentile-of-the-values/
def _percentile(N, percent, key=lambda x:x):
"""
Find the percentile of a list of values.
@parameter N - is a list of values. Note N MUST BE already sorted.
@parameter percent - a float value from 0.0 to 1.0.
@parameter key - optional key function to compute value from each element of N.
@return - the percentile of the values
"""
if not N:
return None
k = (len(N)-1) * percent
f = math.floor(k)
c = math.ceil(k)
if f == c:
return key(N[int(k)])
d0 = key(N[int(f)]) * (c-k)
d1 = key(N[int(c)]) * (k-f)
return d0+d1
@functools.lru_cache(100)
class PercentileAggregator(Aggregator):
def __init__(self, pct, helpstr=''):
super().__init__('p%s'%pct, None, helpstr=helpstr)
self.pct = pct
def aggregate(self, col, rows):
return _percentile(sorted(col.getValues(rows)), self.pct/100, key=float)
def quantiles(q, helpstr):
return [PercentileAggregator(round(100*i/q), helpstr) for i in range(1, q)]
vd.aggregator('min', min, 'minimum value')
vd.aggregator('max', max, 'maximum value')
vd.aggregator('avg', mean, 'arithmetic mean of values', type=float)
vd.aggregator('mean', mean, 'arithmetic mean of values', type=float)
vd.aggregator('median', statistics.median, 'median of values')
vd.aggregator('mode', statistics.mode, 'mode of values')
vd.aggregator('sum', vsum, 'sum of values')
vd.aggregator('distinct', set, 'distinct values', type=vlen)
vd.aggregator('count', lambda values: sum(1 for v in values), 'number of values', type=int)
vd.aggregator('list', list, 'list of values', type=anytype)
vd.aggregator('stdev', statistics.stdev, 'standard deviation of values', type=float)
vd.aggregators['q3'] = quantiles(3, 'tertiles (33/66th pctile)')
vd.aggregators['q4'] = quantiles(4, 'quartiles (25/50/75th pctile)')
vd.aggregators['q5'] = quantiles(5, 'quintiles (20/40/60/80th pctiles)')
vd.aggregators['q10'] = quantiles(10, 'deciles (10/20/30/40/50/60/70/80/90th pctiles)')
# since bb29b6e, a record of every aggregator
# is needed in vd.aggregators
for pct in (10, 20, 25, 30, 33, 40, 50, 60, 67, 70, 75, 80, 90, 95, 99):
vd.aggregators[f'p{pct}'] = PercentileAggregator(pct, f'{pct}th percentile')
class KeyFindingAggregator(Aggregator):
'''Return the key of the row that results from applying *aggr_func* to *rows*.
Return None if *rows* is an empty list.
*aggr_func* takes a list of (value, row) tuples, one for each row in the column,
excluding rows where the column holds null and error values.
*aggr_func* must also take the parameters *default* and *key*, as max() does:
https://docs.python.org/3/library/functions.html#max'''
def __init__(self, aggr_func, *args, **kwargs):
self.aggr_func = aggr_func
super().__init__(*args, **kwargs)
def aggregate(self, col, rows):
if not col.sheet.keyCols:
vd.error('key aggregator function requires one or more key columns')
return None
# convert dicts to lists because functions like max() can't compare dicts
sortkey = lambda t: (t[0], sorted(t[1].items())) if isinstance(t[1], dict) else t
row = self.aggr_func(col.getValueRows(rows), default=(None, None), key=sortkey)[1]
return col.sheet.rowkey(row) if row else None
vd.aggregators['keymin'] = KeyFindingAggregator(min, 'keymin', anytype, helpstr='key of the minimum value')
vd.aggregators['keymax'] = KeyFindingAggregator(max, 'keymax', anytype, helpstr='key of the maximum value')
ColumnsSheet.columns += [
Column('aggregators',
getter=lambda c,r:r.aggstr,
setter=lambda c,r,v:setattr(r, 'aggregators', v),
help='change the metrics calculated in every Frequency or Pivot derived from the source sheet')
]
@Sheet.api
def addAggregators(sheet, cols, aggrnames):
'Add each aggregator in list of *aggrnames* to each of *cols*. Ignores names that are not valid.'
for aggrname in aggrnames:
aggrs = vd.aggregators.get(aggrname)
aggrs = aggrs if isinstance(aggrs, list) else [aggrs]
for aggr in aggrs:
for c in cols:
if not hasattr(c, 'aggregators'):
c.aggregators = []
if aggr and aggr not in c.aggregators:
c.aggregators += [aggr]
@Column.api
def aggname(col, agg):
'Consistent formatting of the name of given aggregator for this column. e.g. "col1_sum"'
return '%s_%s' % (col.name, agg.name)
@Column.api
def aggregateTotal(col, agg):
if agg not in col._aggregatedTotals:
col._aggregatedTotals[agg] = INPROGRESS
col._aggregateTotalAsync(agg)
return col._aggregatedTotals[agg]
@Column.api
@asyncthread
def _aggregateTotalAsync(col, agg):
col._aggregatedTotals[agg] = agg.aggregate(col, col.sheet.rows)
@Column.api
@asyncthread
def memo_aggregate(col, agg_choices, rows):
'Show aggregated value in status, and add to memory.'
for agg_choice in agg_choices:
agg = vd.aggregators.get(agg_choice)
if not agg: continue
aggs = agg if isinstance(agg, list) else [agg]
for agg in aggs:
aggval = agg.aggregate(col, rows)
typedval = wrapply(agg.type or col.type, aggval)
dispval = col.format(typedval)
k = col.name+'_'+agg.name
vd.status(f'{k}={dispval}')
vd.memory[k] = typedval
@VisiData.property
def aggregator_choices(vd):
return [
AttrDict(key=agg, desc=v[0].helpstr if isinstance(v, list) else v.helpstr)
for agg, v in vd.aggregators.items()
if not agg.startswith('p') # skip all the percentiles, user should use q# instead
]
@VisiData.api
def chooseAggregators(vd):
'''Return a list of aggregator name strings chosen or entered by the user. User-entered names may be invalid.'''
prompt = 'choose aggregators: '
def _fmt_aggr_summary(match, row, trigger_key):
formatted_aggrname = match.formatted.get('key', row.key) if match else row.key
r = ' '*(len(prompt)-3)
r += f'[:keystrokes]{trigger_key}[/] '
r += formatted_aggrname
if row.desc:
r += ' - '
r += match.formatted.get('desc', row.desc) if match else row.desc
return r
r = vd.activeSheet.inputPalette(prompt,
vd.aggregator_choices,
value_key='key',
formatter=_fmt_aggr_summary,
type='aggregators',
help=vd.help_aggregators,
multiple=True)
aggrs = r.split()
valid_choices = vd.aggregators.keys()
for aggr in aggrs:
vd.usedInputs[aggr] += 1
if aggr not in valid_choices:
vd.warning(f'aggregator does not exist: {aggr}')
return aggrs
Sheet.addCommand('+', 'aggregate-col', 'addAggregators([cursorCol], chooseAggregators())', 'add aggregator to current column')
Sheet.addCommand('z+', 'memo-aggregate', 'cursorCol.memo_aggregate(chooseAggregators(), selectedRows or rows)', 'memo result of aggregator over values in selected rows for current column')
ColumnsSheet.addCommand('g+', 'aggregate-cols', 'addAggregators(selectedRows or source[0].nonKeyVisibleCols, chooseAggregators())', 'add aggregators to selected source columns')
vd.addMenuItems('''
Column > Add aggregator > aggregate-col
''')
visidata-3.1.1/visidata/apps/ 0000775 0000000 0000000 00000000000 14770045464 0016102 5 ustar 00root root 0000000 0000000 visidata-3.1.1/visidata/apps/__init__.py 0000664 0000000 0000000 00000000000 14770045464 0020201 0 ustar 00root root 0000000 0000000 visidata-3.1.1/visidata/apps/vdsql/ 0000775 0000000 0000000 00000000000 14770045464 0017233 5 ustar 00root root 0000000 0000000 visidata-3.1.1/visidata/apps/vdsql/__about__.py 0000664 0000000 0000000 00000000503 14770045464 0021511 0 ustar 00root root 0000000 0000000 __all__ = '__title__ __author__ __version__ __description__ __license__ __copyright__'.split()
__title__ = 'vdsql'
__author__ = 'Saul Pwanson '
__version__ = '0.3dev'
__description__ = 'VisiData for database queries'
__license__ = 'Apache License, Version 2.0'
__copyright__ = 'Copyright 2022 ' + __author__
visidata-3.1.1/visidata/apps/vdsql/__init__.py 0000664 0000000 0000000 00000000171 14770045464 0021343 0 ustar 00root root 0000000 0000000 from .__about__ import *
from ._ibis import *
from .bigquery import *
from .snowflake import *
from .clickhouse import *
visidata-3.1.1/visidata/apps/vdsql/__main__.py 0000775 0000000 0000000 00000001406 14770045464 0021331 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
def main():
import ibis
import visidata
from visidata import main, vd
from . import __version__
visidata.__version_info__ = f'vdsql {__version__}'
for ext in "db ddb duckdb sqlite sqlite3".split():
setattr(vd, f"open_{ext}", vd.open_vdsql)
for entry_point in ibis.util.backend_entry_points():
if entry_point.name in ['bigquery', 'clickhouse', 'snowflake']:
# these have their own custom openurl_ funcs already installed
continue
attrname = f"openurl_{entry_point.name}"
# when running vdsql directly, override visidata builtin loader with vdsql loader #1929
setattr(vd, attrname, vd.open_vdsql)
main.vd_cli()
if __name__ == "__main__":
main()
visidata-3.1.1/visidata/apps/vdsql/_ibis.py 0000664 0000000 0000000 00000065573 14770045464 0020712 0 ustar 00root root 0000000 0000000 from copy import copy
import functools
import operator
import re
from contextlib import contextmanager
from visidata import VisiData, Sheet, IndexSheet, vd, date, anytype, vlen, clipdraw, colors, stacktrace, PyobjSheet, BaseSheet, ExpectedException
from visidata import ItemColumn, AttrColumn, Column, TextSheet, asyncthread, wrapply, ColumnsSheet, UNLOADED, ExprColumn, undoAttrCopyFunc, ENTER
vd.option('disp_ibis_sidebar', 'pending_sql', 'which sidebar property to display')
vd.option('sql_always_count', False, 'whether to include count of total number of results')
vd.option('ibis_limit', 500, 'max number of rows to get in query')
def vdtype_to_ibis_type(t):
from ibis.expr import datatypes as dt
return {
int: dt.int,
float: dt.float,
date: dt.date,
str: dt.string,
}.get(t)
def dtype_to_vdtype(dtype):
from ibis.expr import datatypes as dt
try:
if isinstance(dtype, dt.Decimal):
if dtype.scale == 0:
return int
else:
return float
if isinstance(dtype, dt.Integer):
return int
if isinstance(dtype, dt.Floating):
return float
if isinstance(dtype, (dt.Date, dt.Timestamp)):
return date
except TypeError:
# For categoricals and other pandas-defined dtypes
pass
return anytype
@VisiData.api
def configure_ibis(vd):
import ibis
vd.aggregator('collect', ibis.expr.types.AnyValue.collect, 'collect a list of values')
ibis.options.verbose_log = vd.status
if vd.options.debug:
ibis.options.verbose = True
if 'ibis_type' not in set(c.expr for c in ColumnsSheet.columns):
ColumnsSheet.columns += [
AttrColumn('ibis_type', type=str)
]
@VisiData.api
def open_vdsql(vd, p, filetype=None):
import ibis
vd.configure_ibis()
# on-demand aliasing, so we don't need deps for all backends
ext_aliases = dict(db='sqlite', ddb='duckdb', sqlite3='sqlite')
if p.ext in ext_aliases:
setattr(ibis, p.ext, ext_aliases.get(p.ext))
return IbisTableIndexSheet(p.base_stem, source=p, filetype=None, database_name=None,
ibis_conpool=IbisConnectionPool(p), sheet_type=IbisTableSheet)
vd.open_ibis = vd.open_vdsql
vd.openurl_sqlite = vd.open_vdsql
class IbisConnectionPool:
def __init__(self, source, pool=None, total=0):
self.source = source
self.pool = pool if pool is not None else []
self.total = total
def __copy__(self):
return IbisConnectionPool(self.source, pool=self.pool, total=self.total)
@contextmanager
def get_conn(self):
if not self.pool:
import ibis
r = ibis.connect(str(self.source))
else:
r = self.pool.pop(0)
try:
yield r
finally:
self.pool.append(r)
class IbisTableIndexSheet(IndexSheet):
# sheet_type = IbisTableSheet # set below
@property
def con(self):
return self.ibis_conpool.get_conn()
def rawSql(self, qstr):
with self.con as con:
return IbisTableSheet('rawsql',
ibis_conpool=self.ibis_conpool,
ibis_source=qstr,
source=qstr,
query=con.sql(qstr))
def iterload(self):
with self.con as con:
if self.database_name:
con.set_database(self.database_name)
# use the actual count instead of the returned limit
nrows_col = self.column('rows')
nrows_col.expr = 'countRows'
nrows_col.width += 3
for tblname in con.list_tables():
yield self.sheet_type(tblname,
ibis_source=self.source,
ibis_filetype=self.filetype,
ibis_conpool=self.ibis_conpool,
database_name=self.database_name,
table_name=tblname,
source=self.source,
query=None)
class IbisColumn(ItemColumn):
@property
def ibis_type(self):
return self.sheet.query[self.ibis_name].type()
@asyncthread
def memo_aggregate(self, agg, rows):
'Show aggregated value in status, and add ibis expr to memory for use later.'
aggexpr = self.ibis_aggr(agg.name) # ignore rows, do over whole query
with self.sheet.con as con:
aggval = con.execute(aggexpr)
typedval = wrapply(agg.type or self.type, aggval)
dispval = self.format(typedval)
k = self.name+'_'+agg.name
vd.status(f'{k}={dispval}')
vd.memory[k] = aggval
# store aggexpr somewhere to use in later subquery
def expand(self, rows):
return self.expand_struct(rows)
def expand_struct(self, rows):
oldexpr = self.sheet.ibis_current_expr
struct_field = self.get_ibis_col(oldexpr)
# if struct_field is not StructType:
# vd.fail('vdsql can only expand Struct columns')
struct_fields = [struct_field[name] for name in struct_field.names]
expandedCols = super().expand(rows) # this must go after ibis_current_expr, because it alters ibis_current_expr
fields = []
for ibiscol, expcol in zip(struct_fields, expandedCols):
fields.append(ibiscol.name(expcol.name))
# self.sheet.query = oldexpr.drop([struct_field.get_name()]).mutate(fields)
self.sheet.query = oldexpr.mutate(fields)
return expandedCols
class LazyIbisColMap:
def __init__(self, sheet, q):
self._sheet = sheet
self._query = q
self._map = {col.name: col for col in sheet.columns}
def __getitem__(self, k):
col = self._map[k]
return col.get_ibis_col(self._query)
class IbisTableSheet(Sheet):
@property
def con(self):
return self.ibis_conpool.get_conn()
def choose_sidebar(self):
sidebars = ['base_sql', 'pending_sql', 'ibis_current_expr', 'curcol_sql', 'pending_expr']
opts = []
for s in sidebars:
try:
opts.append({'key': s, 'value':getattr(self, s)})
except Exception as e:
if self.options.debug:
vd.exceptionCaught()
vd.options.disp_ibis_sidebar = vd.chooseOne(opts)
@property
def curcol_sql(self):
expr = self.cursorCol.get_ibis_col(self.ibis_current_expr)
if expr is not None:
return self.ibis_expr_to_sql(expr, fragment=True)
def ibis_expr_to_sql(self, expr, fragment=False):
import sqlparse
with self.con as con:
context = con.compiler.make_context()
trclass = con.compiler.translator_class(expr.op(), context=context)
if fragment:
compiled = trclass.get_result()
else:
compiled = con.compile(expr)
if not isinstance(compiled, str):
compiled = str(compiled.compile(compile_kwargs={'literal_binds': True}))
return sqlparse.format(compiled, reindent=True, keyword_case='upper', wrap_after=40)
@property
def sidebar(self) -> str:
sbtype = self.options.disp_ibis_sidebar
if sbtype:
txt = str(getattr(self, sbtype, ''))
if txt:
return f'# {sbtype}\n'+txt
@property
def ibis_locals(self):
return LazyIbisColMap(self, self.query)
def select_row(self, row):
k = self.rowkey(row) or vd.fail('need key column to select individual rows')
super().selectRow(row)
self.ibis_selection.append(self.matchRowKeyExpr(row))
def stoggle_row(self, row):
vd.fail('cannot toggle selection of individual row in vdsql')
def unselect_row(self, row):
super().unselectRow(row)
self.ibis_selection = [ self.ibis_filter & ~self.matchRowKeyExpr(row) ]
def matchRowKeyExpr(self, row):
import ibis
k = self.rowkey(row) or vd.fail('need key column to select individual rows')
return functools.reduce(operator.and_, [
c.get_ibis_col(self.query, typed=True) == k[i]
for i, c in enumerate(self.keyCols)
])
@property
def ibis_current_expr(self):
return self.get_current_expr(typed=False)
def get_current_expr(self, typed=False):
q = self.query
extra_cols = {}
for c in self.visibleCols:
ibis_col = c.get_ibis_col(q, typed=typed)
if ibis_col is not None:
extra_cols[c.name] = ibis_col
else:
vd.warning(f'no column {c.name}')
if extra_cols:
q = q.mutate(**extra_cols)
return q
@property
def ibis_filter(self):
import ibis
selectors = [self.ibisCompileExpr(f, self.get_current_expr(typed=True)) for f in self.ibis_selection]
if not selectors:
return ibis.literal(True)
return functools.reduce(operator.or_, selectors)
@property
def pending_expr(self):
import ibis
q = self.get_current_expr(typed=True)
if self.ibis_selection:
q = q.filter(self.ibis_filter)
if self._ordering:
colorder = []
for col, rev in self._ordering:
ibiscol = col.get_ibis_col(q) #1856
if rev:
ibiscol = ibis.desc(ibiscol)
colorder.append(ibiscol)
q = q.order_by(colorder)
return q
def ibisCompileExpr(self, expr, q):
if isinstance(expr, str):
return eval(expr, vd.getGlobals(), LazyIbisColMap(self, q))
else:
return expr
def evalIbisExpr(self, expr):
return eval(expr, vd.getGlobals(), self.ibis_locals)
@property
def base_sql(self):
return self.sqlize(self.ibis_current_expr)
@property
def pending_sql(self):
return self.sqlize(self.pending_expr)
def sqlize(self, expr):
if vd.options.debug:
expr = self.withRowcount(expr)
return self.ibis_expr_to_sql(expr)
@property
def substrait(self):
from ibis_substrait.compiler.core import SubstraitCompiler
compiler = SubstraitCompiler()
return compiler.compile(self.ibis_current_expr)
def withRowcount(self, q):
if self.options.sql_always_count:
# return q.mutate(__n__=q.count())
return q.cross_join(q.aggregate(__n__=lambda t: t.count()))
return q
def beforeLoad(self):
self.options.disp_rstatus_fmt = self.options.disp_rstatus_fmt.replace('nRows', 'countRows')
self.options.disp_rstatus_fmt = self.options.disp_rstatus_fmt.replace('nSelectedRows', 'countSelectedRows')
def baseQuery(self, con):
'Return base table for {database_name}.{table_name}'
import ibis
tbl = con.table(self.table_name)
return ibis.table(tbl.schema(), name=self.fqtblname(con))
def fqtblname(self, con) -> str:
'Return fully-qualified table name including database/schema, or whatever connection needs to identify this table.'
if hasattr(con, '_fully_qualified_name'):
return con._fully_qualified_name(self.table_name, self.database_name)
return self.table_name
def iterload(self):
with self.con as con:
if self.query is None:
self.query = self.baseQuery(con)
self.reloadColumns(self.query) # columns based on query without metadata
self.query_result = con.execute(self.withRowcount(self.query),
limit=self.options.ibis_limit or None)
yield from self.query_result.itertuples()
def reloadColumns(self, expr, start=1):
oldkeycols = {c.name:c for c in self.keyCols}
self._nrows_col = -1
for i, (colname, dtype) in enumerate(expr.schema().items(), start=start):
keycol=oldkeycols.get(colname, Column()).keycol
if i-start < self.nKeys:
keycol = i+1
if colname == '__n__':
self._nrows_col = i
continue
self.addColumn(IbisColumn(colname, i,
type=dtype_to_vdtype(dtype),
keycol=keycol,
ibis_name=colname))
@property
def countSelectedRows(self):
return f'{self.nSelectedRows}+'
@property
def countRows(self):
if self.rows is UNLOADED:
return None
if not self.rows or self._nrows_col < 0:
return self.nRows
return self.rows[0][self._nrows_col] # __n__
def groupBy(self, groupByCols):
from ibis import _
import ibis
from ibis.expr import datatypes as dt
aggr_cols = [groupByCols[0].ibis_col.count().name('count')]
for c in self.visibleCols:
aggr_cols.extend(c.ibis_aggrs)
q = self.ibis_current_expr
groupq = q.aggregate(aggr_cols, by=[c.ibis_col for c in groupByCols])
try:
win = ibis.window(order_by=ibis.NA)
except ibis.common.exceptions.IbisTypeError: # ibis bug: there is not yet a good workaround that covers all backends
win = ibis.window(order_by=None)
groupq = groupq.mutate(percent=_['count']*100 / _['count'].sum().over(win))
histolen = 40
histogram_char = self.options.disp_histogram
if histogram_char and len(aggr_cols) == 1:
groupq = groupq.mutate(maxcount=_['count'].max())
hval = ibis.literal(histogram_char, type=dt.string)
def _histogram(t):
return hval.repeat((histolen*t['count']/t.maxcount).cast(dt.int))
groupq = groupq.mutate(histogram=_histogram)
groupq = groupq.order_by(ibis.desc('count'))
return IbisFreqTable(self.name, *(col.name for col in groupByCols), 'freq',
ibis_conpool=self.ibis_conpool,
ibis_source=self.ibis_source,
source=self,
groupByCols=groupByCols,
query=groupq,
nKeys=len(groupByCols))
def unfurl_col(self, col):
vs = copy(self)
vs.names = [self.name, col.name, 'unfurled']
vs.query = self.ibis_current_expr.mutate(**{col.name:col.ibis_col.unnest()})
vs.cursorVisibleColIndex = self.cursorVisibleColIndex
return vs
def openJoin(self, others, jointype=''):
sheets = [self] + others
sheets[1:] or vd.fail("join requires more than 1 sheet")
if jointype == 'append':
q = self.ibis_current_expr
for other in others:
q = q.union(other.ibis_current_expr)
return IbisTableSheet('&'.join(vs.name for vs in sheets), query=q, ibis_source=self.ibis_source, ibis_conpool=self.ibis_conpool)
for s in sheets:
s.keyCols or vd.fail(f'{s.name} has no key cols to join')
if jointype in ['extend', 'outer']:
jointype = 'left'
elif jointype in ['full']:
jointype = 'outer'
# elif jointype in ['inner']:
# jointype = 'inner'
q = self.ibis_current_expr
for other in others:
preds = [(a.ibis_col == b.ibis_col) for a, b in zip(self.keyCols, other.keyCols)]
q = q.join(other.ibis_current_expr, predicates=preds, how=jointype, suffixes=('', '_'+other.name))
return IbisTableSheet('+'.join(vs.name for vs in sheets), sources=sheets, query=q, ibis_source=self.ibis_source, ibis_conpool=self.ibis_conpool)
@Column.property
def ibis_col(col):
return col.get_ibis_col(col.sheet.ibis_current_expr)
@Column.api
def get_ibis_col(col, query:'ibis.Expr', typed=False) -> 'ibis.Expr':
'Return ibis.Expr for `col` within context of `query`, cast by VisiData column type if `typed`.'
import ibis.common.exceptions
r = None
if isinstance(col, ExprColumn):
r = col.sheet.evalIbisExpr(col.expr)
elif isinstance(col, vd.ExpandedColumn):
r = query[col.name]
elif not hasattr(col, 'ibis_name'):
return
else:
try:
r = query[col.ibis_name]
except (ibis.common.exceptions.IbisTypeError, AttributeError):
r = query[col.name]
if r is None:
return r
if typed:
import ibis.expr.datatypes as dt
if col.type is str: r = r.cast(dt.string)
if col.type is int: r = r.cast(dt.int)
if col.type is float: r = r.cast(dt.float)
if col.type is date:
if not isinstance(r.type(), (dt.Timestamp, dt.Date)):
r = r.cast(dt.date)
r = r.name(col.name)
return r
@Column.property
def ibis_aggrs(col):
return [col.ibis_aggr(aggname) for aggname in (col.aggstr or '').split()]
@Column.api
def ibis_aggr(col, aggname):
aggname = {
'avg': 'mean',
'median': 'approx_median',
'mode': 'notimpl',
'distinct': 'nunique',
'list': 'collect',
'stdev': 'std',
# 'p99': 'quantile(0.99)',
# 'q10': 'quantile([.1,.2,.3,.4,.5,.6,.7,.8,.9])',
}.get(aggname, aggname)
agg = getattr(col.ibis_col, aggname)
return agg().name(f'{aggname}_{col.name}')
IbisTableSheet.init('ibis_selection', list, copy=False)
IbisTableSheet.init('_sqlscr', lambda: None, copy=False)
IbisTableSheet.init('query_result', lambda: None, copy=False)
IbisTableSheet.init('ibis_conpool', lambda: None, copy=True)
@IbisTableSheet.api
def stoggle_rows(sheet):
sheet.toggle(sheet.rows)
sheet.ibis_selection = [~sheet.ibis_filter]
@IbisTableSheet.api
def clearSelected(sheet):
super(IbisTableSheet, sheet).clearSelected()
sheet.ibis_selection.clear()
@IbisTableSheet.api
def addUndoSelection(sheet):
super(IbisTableSheet, sheet).addUndoSelection()
vd.addUndo(undoAttrCopyFunc([sheet], 'ibis_selection'))
@IbisTableSheet.api
def select_equal_cell(sheet, col, typedval):
if sheet.isNullFunc()(typedval):
expr = col.ibis_col.isnull()
else:
q = sheet.get_current_expr(typed=True)
ibis_col = col.get_ibis_col(q, typed=True)
expr = (ibis_col == typedval)
sheet.ibis_selection.append(expr)
sheet.select(sheet.gatherBy(lambda r,c=col,v=typedval: c.getTypedValue(r) == v), progress=False)
@IbisTableSheet.api
def select_col_regex(sheet, col, regex):
sheet.selectByIdx(vd.searchRegex(sheet, regex=regex, columns="cursorCol"))
sheet.ibis_selection.append(col.get_ibis_col(col.sheet.query).re_search(regex))
@IbisTableSheet.api
def select_expr(sheet, expr):
sheet.select(sheet.gatherBy(lambda r, sheet=sheet, expr=expr: sheet.evalExpr(expr, r)), progress=False)
sheet.ibis_selection.append(expr)
@IbisTableSheet.api
def addcol_split(sheet, col, delim):
from ibis.expr import datatypes as dt
c = Column(col.name+'_split',
getter=lambda col,row: col.origCol.getDisplayValue(row).split(col.expr),
expr=delim,
origCol=col,
ibis_name=col.name+'_split')
sheet.query = sheet.query.mutate(**{c.name:col.get_ibis_col(sheet.query).cast(dt.string).split(delim)})
return c
@IbisTableSheet.api
def addcol_subst(sheet, col, before='', after=''):
c = Column(col.name + "_re",
getter=lambda col,row,before=before,after=after: re.sub(before, after, col.origCol.getDisplayValue(row)),
origCol=col,
ibis_name=col.name + "_re")
sheet.query = sheet.query.mutate(**{c.name:col.get_ibis_col(sheet.query).re_replace(before, after)})
return c
@IbisTableSheet.api
def addcol_cast(sheet, col):
# sheet.query and sheet.ibis_current_expr don't match
new_type = vdtype_to_ibis_type(col.type)
if new_type is None:
vd.warning(f"no type for vd type {col.type}")
return
expr = sheet.query[col.name].cast(new_type)
sheet.query = sheet.query.mutate(**{col.name: expr})
newcol = copy(col)
col.hide()
sheet.addColumnAtCursor(newcol)
# disable not implemented commands
@BaseSheet.api
def notimpl(sheet):
vd.fail(f"{vd.activeCommand.longname} not implemented for {type(sheet).__name__}; copy to new non-ibis sheet with g'")
dml_cmds = '''addcol-bulk addcol-new add-row add-rows
copy-cell copy-cells copy-row copy-selected commit-sheet cut-cell cut-cells cut-row cut-selected delete-cell delete-cells delete-row delete-selected
edit-cell paste-after paste-before paste-cell setcell-expr
setcol-clipboard setcol-expr setcol-fake setcol-fill setcol-format-enum setcol-formatter setcol-incr setcol-incr-step setcol-input setcol-iter setcol-subst setcol-subst-all
'''.split()
neverimpl_cmds = '''
select-after select-around-n select-before select-equal-row select-error stoggle-after stoggle-before stoggle-row unselect-after unselect-before select-cols-regex unselect-cols-regex transpose
'''.split()
notimpl_cmds = '''
addcol-capture addcol-incr addcol-incr-step addcol-window
contract-col expand-col-depth expand-cols expand-cols-depth melt melt-regex pivot random-rows
select-error-col select-exact-cell select-exact-row select-rows
describe-sheet freq-summary
cache-col cache-cols
dive-selected-cells
dup-rows dup-rows-deep dup-selected-deep
'''.split()
for longname in list(notimpl_cmds) + list(neverimpl_cmds) + list(dml_cmds):
if longname:
IbisTableSheet.addCommand('', longname, 'notimpl()')
@IbisTableSheet.api
def dup_selected(sheet):
vs=copy(sheet)
vs.query=sheet.pending_expr
vs.incrementName()
vd.push(vs)
@BaseSheet.api
def incrementName(sheet):
if isinstance(sheet.names[-1], int):
sheet.names[-1] += 1
else:
sheet.names = list(sheet.names) + [1]
sheet.name = '_'.join(map(str, sheet.names))
@IbisTableSheet.api
def dup_limit(sheet, limit:int):
vs=copy(sheet)
vs.name += f"_top{limit}" if limit else "_all"
vs.query=sheet.pending_expr
vs.options.ibis_limit=limit
return vs
@IbisTableSheet.api
def rawSql(sheet, qstr):
with sheet.con as con:
return IbisTableSheet('rawsql',
ibis_conpool=sheet.ibis_conpool,
ibis_source=qstr,
source=qstr,
query=con.sql(qstr))
class IbisFreqTable(IbisTableSheet):
def freqExpr(self, row):
# matching key of grouped columns
return functools.reduce(operator.and_, [
c.get_ibis_col(self.source.query, typed=True) == self.rowkey(row)[i]
for i, c in enumerate(self.groupByCols)
])
def selectRow(self, row):
super().selectRow(row)
self.source.select(self.gatherBy(lambda r, sheet=self, expr=self.freqExpr(row): sheet.evalExpr(expr, r)), progress=False)
self.source.ibis_selection.append(self.freqExpr(row))
def openRow(self, row):
vs = copy(self.source)
vs.names = list(vs.names) + ['_'.join(str(x) for x in self.rowkey(row))]
vs.query = self.source.query.filter(self.freqExpr(row))
return vs
def openRows(self, rows):
'Return sheet with union of all selected items.'
vs = copy(self.source)
vs.names = list(vs.names) + ['several']
vs.query = self.source.query.filter([
functools.reduce(operator.or_, [self.freqExpr(row) for row in rows])
])
return vs
IbisTableSheet.addCommand('F', 'freq-col', 'vd.push(groupBy([cursorCol]))')
IbisTableSheet.addCommand('gF', 'freq-keys', 'vd.push(groupBy(keyCols))')
IbisTableSheet.addCommand('"', 'dup-selected', 'vd.push(dup_selected())', 'open duplicate sheet with selected rows (default limit)')
IbisTableSheet.addCommand('z"', 'dup-limit', 'vd.push(dup_limit(input("max rows: ", value=options.ibis_limit)))', 'open duplicate sheet with only selected rows (input limit)')
IbisTableSheet.addCommand('gz"', 'dup-nolimit', 'vd.push(dup_limit(0))', 'open duplicate sheet with only selected rows (no limit--be careful!)')
IbisTableSheet.addCommand("'", 'addcol-cast', 'addcol_cast(cursorCol)')
IbisTableSheet.addCommand('zb', 'sidebar-choose', 'choose_sidebar()', 'choose vdsql sidebar to show')
IbisTableSheet.addCommand('', 'exec-sql', 'vd.push(rawSql(input("SQL query: ")))', 'open sheet with results of raw SQL query')
IbisTableSheet.addCommand('', 'addcol-subst', 'addColumnAtCursor(addcol_subst(cursorCol, **inputRegexSubstOld("transform column by regex: ")))') # deprecated
IbisTableSheet.addCommand('', 'addcol-regex-subst', 'addColumnAtCursor(addcol_subst(cursorCol, **inputRegexSubst("transform column by regex: ")))')
IbisTableSheet.addCommand('', 'addcol-split', 'addColumnAtCursor(addcol_split(cursorCol, input("split by delimiter: ", type="delim-split")))')
IbisTableSheet.addCommand('gt', 'stoggle-rows', 'stoggle_rows()', 'select rows matching current cell in current column')
IbisTableSheet.addCommand(',', 'select-equal-cell', 'select_equal_cell(cursorCol, cursorTypedValue)', 'select rows matching current cell in current column')
IbisTableSheet.addCommand('t', 'stoggle-row', 'stoggle_row(cursorRow); cursorDown(1)', 'toggle selection of current row')
IbisTableSheet.addCommand('s', 'select-row', 'select_row(cursorRow); cursorDown(1)', 'select current row')
IbisTableSheet.addCommand('u', 'unselect-row', 'unselect_row(cursorRow); cursorDown(1)', 'unselect current row')
#IbisTableSheet.addCommand('g,', 'select-equal-row', 'select(gatherBy(lambda r,currow=cursorRow,vcols=visibleCols: all([c.getDisplayValue(r) == c.getDisplayValue(currow) for c in vcols])), progress=False)', 'select rows matching current row in all visible columns')
#IbisTableSheet.addCommand('z,', 'select-exact-cell', 'select(gatherBy(lambda r,c=cursorCol,v=cursorTypedValue: c.getTypedValue(r) == v), progress=False)', 'select rows matching current cell in current column')
#IbisTableSheet.addCommand('gz,', 'select-exact-row', 'select(gatherBy(lambda r,currow=cursorRow,vcols=visibleCols: all([c.getTypedValue(r) == c.getTypedValue(currow) for c in vcols])), progress=False)', 'select rows matching current row in all visible columns')
IbisTableSheet.addCommand('', 'select-col-regex', 'select_col_regex(cursorCol, inputRegex("select regex: ", type="regex", defaultLast=True))', 'select rows matching regex in current column')
IbisTableSheet.addCommand('z|', 'select-expr', 'expr=inputExpr("select by expr: "); select_expr(expr)', 'select rows matching Python expression in any visible column')
IbisTableSheet.addCommand('z\\', 'unselect-expr', 'expr=inputExpr("unselect by expr: "); unselect(gatherBy(lambda r, sheet=sheet, expr=expr: sheet.evalExpr(expr, r)), progress=False)', 'unselect rows matching Python expression in any visible column')
IbisFreqTable.addCommand('g'+ENTER, 'open-selected', 'vd.push(openRows(selectedRows))')
IbisTableIndexSheet.addCommand('', 'exec-sql', 'vd.push(rawSql(input("SQL query: ")))', 'open sheet with results of raw SQL query')
IbisTableIndexSheet.class_options.load_lazy = True
IbisTableIndexSheet.sheet_type = IbisTableSheet
IbisTableSheet.class_options.clean_names = True
IbisTableSheet.class_options.regex_flags = ''
vd.addMenuItem('View', 'Sidebar', 'choose', 'sidebar-choose')
visidata-3.1.1/visidata/apps/vdsql/bigquery.py 0000664 0000000 0000000 00000003563 14770045464 0021443 0 ustar 00root root 0000000 0000000 '''
Specify the billing_project_id as the netloc, and the actual dataset_id as the path:
vdsql bigquery:///''
'''
from visidata import vd, VisiData, Sheet, AttrColumn
from . import IbisTableSheet, IbisTableIndexSheet, IbisConnectionPool
import ibis
import ibis.expr.operations as ops
@VisiData.api
def openurl_bigquery(vd, p, filetype=None):
vd.configure_ibis()
vd.configure_bigquery()
return BigqueryDatabaseIndexSheet(p.base_stem, source=p, ibis_con=None)
vd.openurl_bq = vd.openurl_bigquery
@VisiData.api
def configure_bigquery(vd):
@ibis.bigquery.add_operation(ops.TimestampDiff)
def bq_timestamp_diff(t, expr):
op = expr.op()
left = t.translate(op.left)
right = t.translate(op.right)
return f"TIMESTAMP_DIFF({left}, {right}, SECOND)"
class BigqueryDatabaseIndexSheet(Sheet):
rowtype = 'databases' # rowdef: DatasetListItem
columns = [
# AttrColumn('project', width=0),
AttrColumn('dataset_id'),
AttrColumn('friendly_name'),
AttrColumn('full_dataset_id', width=0),
AttrColumn('labels'),
]
nKeys = 1
@property
def con(self):
if not self.ibis_con:
import ibis
self.ibis_con = ibis.connect(self.source)
return self.ibis_con
def iterload(self):
yield from self.con.client.list_datasets(project=self.source.name)
def openRow(self, row):
return IbisTableIndexSheet(row.dataset_id,
database_name=self.source.name+'.'+row.dataset_id,
ibis_con=self.con,
ibis_conpool=IbisConnectionPool(f"{self.source}/{row.dataset_id}"),
source=row,
filetype=None,
sheet_type=IbisTableSheet)
visidata-3.1.1/visidata/apps/vdsql/clickhouse.py 0000664 0000000 0000000 00000003402 14770045464 0021735 0 ustar 00root root 0000000 0000000 import time
from visidata import vd, VisiData, Progress
from ._ibis import IbisTableSheet, IbisTableIndexSheet, IbisConnectionPool
@VisiData.api
def openurl_clickhouse(vd, p, filetype=None):
vd.configure_ibis()
return IbisTableIndexSheet(p.base_stem, source=p, filetype=None, database_name=None,
ibis_conpool=IbisConnectionPool(p), sheet_type=ClickhouseSheet)
vd.openurl_clickhouses = vd.openurl_clickhouse
class ClickhouseSheet(IbisTableSheet):
@property
def countRows(self):
if self.total_rows is not None:
return self.total_rows
return super().countRows
def iterload(self):
with self.con as con:
qid = None
try:
if self.query is None:
self.query = self.baseQuery(con)
self.reloadColumns(self.query, start=0) # columns based on query without metadata
sqlstr = con.compile(self.query.limit(self.options.ibis_limit or None))
with Progress(gerund='clickhousing', sheet=self) as prog:
settings = {'max_block_size': 10000}
with con.con.query_rows_stream(sqlstr, settings) as stream:
prog.total = int(stream.source.summary['total_rows_to_read'])
prog.made = 0
for row in stream:
prog.made += 1
yield row
self.total_rows = prog.total
except Exception as e:
raise
except BaseException:
if qid:
con._client.cancel(qid)
ClickhouseSheet.init('total_rows', lambda: None)
#ClickhouseSheet.class_options.sql_always_count = True
visidata-3.1.1/visidata/apps/vdsql/setup.py 0000664 0000000 0000000 00000002112 14770045464 0020741 0 ustar 00root root 0000000 0000000 # SPDX-License-Identifier: MIT
from setuptools import setup, find_packages
from pathlib import Path
exec(Path('__about__.py').read_text())
def readme():
return Path("README.md").read_text()
def requirements():
return Path("requirements.txt").read_text().splitlines()
def requirements_extra():
return Path("requirements-extra.txt").read_text().splitlines()
setup(
name="vdsql",
version=__version__,
description=__description__,
long_description=readme(),
long_description_content_type="text/markdown",
classifiers=[
"Development Status :: 4 - Beta",
"Programming Language :: Python :: 3",
],
keywords="visidata sql rdbms ibis substrait",
author="Saul Pwanson",
url="https://github.com/visidata/vdsql",
python_requires=">=3.9",
packages=find_packages(exclude=["tests"]),
entry_points={'visidata.plugins': 'vdsql=visidata.apps.vdsql'},
scripts=['vdsql'],
install_requires=requirements(),
extra_requires=requirements_extra(),
)
visidata-3.1.1/visidata/apps/vdsql/snowflake.py 0000664 0000000 0000000 00000004173 14770045464 0021603 0 ustar 00root root 0000000 0000000 import time
from visidata import vd, VisiData
from ._ibis import IbisTableSheet, IbisConnectionPool, IbisTableIndexSheet
@VisiData.api
def openurl_snowflake(vd, p, filetype=None):
return IbisTableIndexSheet(p.base_stem, source=p, filetype=None, database_name=None,
ibis_conpool=IbisConnectionPool(p),
sheet_type=SnowflakeSheet)
class SnowflakeSheet(IbisTableSheet):
@property
def countRows(self):
r = super().countRows
if r is None and self.cursor is None:
return None # no cursor yet
return r
def executeSql(self, sql):
assert self.cursor is None
with self.con as con:
con = con.con
if self.warehouse:
con.execute(f'USE WAREHOUSE {self.warehouse}')
with con.begin() as c:
snowflake_conn = c.connection.dbapi_connection
cursor = self.cursor = snowflake_conn.cursor()
cursor.execute_async(sql)
while snowflake_conn.is_still_running(snowflake_conn.get_query_status(cursor.sfqid)):
time.sleep(.1)
cursor.get_results_from_sfqid(cursor.sfqid)
yield from cursor.fetchall()
self.cursor = None
def iterload(self):
try:
with self.con as con:
if self.query is None:
self.query = self.baseQuery(con)
yield from self.executeSql(self.ibis_to_sql(self.withRowcount(self.baseQuery(con))))
except BaseException:
if self.cursor:
self.cancelQuery(self.cursor.sfqid)
raise
self.reloadColumns(self.query) # columns based on query without metadata
def cancelQuery(self, qid):
vd.status(f'canceling "{qid}"')
with self.con as con:
with con.begin() as con:
cursor = con.connection.dbapi_connection.cursor()
cursor.execute(f"SELECT SYSTEM$CANCEL_QUERY('{qid}')")
vd.status(cursor.fetchall())
SnowflakeSheet.init('cursor', lambda: None)
SnowflakeSheet.init('warehouse', str)
visidata-3.1.1/visidata/apps/vgit/ 0000775 0000000 0000000 00000000000 14770045464 0017053 5 ustar 00root root 0000000 0000000 visidata-3.1.1/visidata/apps/vgit/__init__.py 0000664 0000000 0000000 00000000237 14770045464 0021166 0 ustar 00root root 0000000 0000000 from . import abort, statusbar
from . import (
grep,
config,
branch,
remote,
blame,
status,
log,
diff,
stash,
repos,
)
visidata-3.1.1/visidata/apps/vgit/__main__.py 0000664 0000000 0000000 00000000047 14770045464 0021146 0 ustar 00root root 0000000 0000000 from .main import vgit_cli
vgit_cli()
visidata-3.1.1/visidata/apps/vgit/abort.py 0000664 0000000 0000000 00000001263 14770045464 0020536 0 ustar 00root root 0000000 0000000 from visidata import vd, Menu
from .gitsheet import GitSheet
@GitSheet.api
def abortWhatever(sheet):
inp = sheet.gitInProgress()
if inp.startswith('cherry-pick'):
sheet.modifyGit('cherry-pick', '--abort')
elif inp.startswith('merg'):
sheet.modifyGit('merge', '--abort')
elif inp.startswith('bisect'):
sheet.modifyGit('bisect', 'reset')
elif inp.startswith('rebas') or inp.startswith('apply'):
sheet.modifyGit('rebase', '--abort') # or --quit?
else:
vd.status('nothing to abort')
GitSheet.addCommand('^A', 'git-abort', 'abortWhatever()', 'abort the current in-progress action')
vd.addMenuItems('Git > Abort > git-abort')
visidata-3.1.1/visidata/apps/vgit/blame.py 0000664 0000000 0000000 00000004670 14770045464 0020514 0 ustar 00root root 0000000 0000000 from visidata import vd, Column, VisiData, ItemColumn, Path, AttrDict, date
from .gitsheet import GitSheet
@VisiData.api
def git_blame(vd, gitpath, args, **kwargs):
if args and not args[-1].startswith('-'):
fn = args[-1]
return GitBlame('blame', fn, source=Path(fn), **kwargs)
class FormatColumn(Column):
def calcValue(self, row):
return self.expr.format(**row)
# rowdef: (hdr, orig_linenum, linenum, line)
# hdr = { 'sha': .., 'orig_linenum': .., 'final_linenum': .. }
# source = GitSheet; .gitfile=GitFile
class GitBlame(GitSheet):
rowtype = 'lines'
guide = '''
# git blame
'''
columns = [
ItemColumn('sha', width=0),
ItemColumn('orig_linenum', width=0, type=int),
ItemColumn('final_linenum', width=0, type=int),
ItemColumn('author', width=15),
ItemColumn('author_time', width=13, type=date),
FormatColumn('committer', width=0, expr='{committer} {committer_mail}'),
ItemColumn('committer_time', width=0, type=date),
ItemColumn('linenum', width=6, type=int),
ItemColumn('line', width=72),
]
def iterload(self):
lines = list(self.git_lines('blame', '--porcelain', str(self.source)))
i = 0
headers = {} # [sha1] -> hdr
while i < len(lines):
# header
parts = lines[i].split()
sha, orig, final = parts[:3]
if len(parts) > 3:
nlines_this_group = parts[3]
if sha not in headers:
hdr = AttrDict(sha=sha, orig_linenum=orig, final_linenum=final)
headers[sha] = hdr
else:
hdr = headers[sha]
while lines[i][0] != '\t':
try:
k, v = lines[i].split(maxsplit=1)
k = k.replace('-', '_')
if '_time' in k:
v = int(v)
hdr[k] = v
except Exception:
vd.status(lines[i])
i += 1
yield AttrDict(
linenum=final,
line=lines[i][1:],
**hdr
)
i += 1
#GitBlame.addCommand(ENTER, 'diff-line', 'openDiff(str(gitfile), cursorRow[0]["sha"]+"^", cursorRow[0]["sha"])', 'open diff of the commit when this line changed')
#GitStatus.addCommand(None, 'git-blame', 'vd.push(GitBlame(cursorRow, source=sheet))', 'push blame for this file')
visidata-3.1.1/visidata/apps/vgit/branch.py 0000664 0000000 0000000 00000013261 14770045464 0020665 0 ustar 00root root 0000000 0000000 import re
from visidata import vd, Column, VisiData, ItemColumn, AttrColumn, Path, AttrDict, RowColorizer, date, Progress
from .gitsheet import GitSheet
vd.theme_option('color_git_current_branch', 'underline', 'color of current branch on branches sheet')
vd.theme_option('color_git_remote_branch', 'cyan', 'color of remote branches on branches sheet')
@VisiData.api
def git_branch(vd, p, args):
nonListArgs = '--track --no-track --set-upstream-to -u --unset-upstream -m -M -c -C -d -D --edit-description'.split()
if any(x in args for x in nonListArgs):
return
return GitBranch('git-branch-list', source=p, git_args=args)
def _remove_prefix(text, prefix):
if text.startswith(prefix):
return text[len(prefix):]
return text
class GitBranchNameColumn(Column):
def calcValue(self, row):
return _remove_prefix(row.localbranch, 'remotes/')
def putValue(self, row, val):
self.sheet.loggit('branch', '-v', '--move', row.localbranch, val)
class GitBranch(GitSheet):
guide = '''
# git branch
List of all branches, including relevant metadata.
- `d` to mark a branch for deletion
- `e` on the _branch_ column to rename the branch
- `z Ctrl+S` to commit changes
'''
defer = True
rowtype = 'branches' # rowdef: AttrDict from regex (in reload below)
columns = [
GitBranchNameColumn('branch', width=20),
# Column('remote', getter=lambda c,r: r['localbranch'].startswith('remotes/') and '*' or '', width=3),
ItemColumn('head_commitid', 'refid', width=0),
ItemColumn('tracking', 'remotebranch'),
ItemColumn('upstream'),
ItemColumn('merge_base', 'merge_name', width=20),
ItemColumn('extra', width=0),
ItemColumn('head_commitmsg', 'msg', width=50),
ItemColumn('last_commit', type=date),
ItemColumn('last_author'),
]
colorizers = [
RowColorizer(10, 'color_git_current_branch', lambda s,c,r,v: r and r['current']),
RowColorizer(10, 'color_git_remote_branch', lambda s,c,r,v: r and r['localbranch'].startswith('remotes/')),
]
nKeys = 1
def iterload(self):
branches_lines = self.git_lines(
'branch',
'--list',
'--format',
' '.join((
'%(if)%(symref)%(then)yes%(else)no%(end)',
'%(HEAD) %(refname:short) %(objectname:short)',
'%(if)%(upstream)%(then)[%(upstream:short)',
'%(if)%(upstream:track)%(then): %(upstream:track,nobracket)%(end)]',
'%(end)',
'%(contents:subject)'
)),
'-vv',
'--no-color',
*self.git_args)
for line in branches_lines:
m = re.match(r'''(?P(yes|no)?)\s+
(?P\*?)\s+
(?P\S+)\s+
(?P\w+)\s+
(?:\[
(?P[^\s\]:]+):?
\s*(?P.*?)
\])?
\s*(?P.*)''', line, re.VERBOSE)
if not m:
continue
branch_details = AttrDict(m.groupdict())
if branch_details.is_symref == 'yes':
continue
yield branch_details
branch_stats = self.gitRootSheet.gitBranchStatuses
for row in Progress(self.rows):
merge_base = self.git_all("show-branch", "--merge-base", row.localbranch, self.gitRootSheet.branch, _ok_code=[0,1]).strip()
row.update(dict(
merge_name = self.git_all("name-rev", "--name-only", merge_base).strip() if merge_base else '',
upstream = branch_stats.get(row.localbranch),
last_commit = self.git_all("show", "--no-patch", '--pretty=%ai', row.localbranch).strip(),
last_author = self.git_all("show", "--no-patch", '--pretty=%an', row.localbranch).strip()
))
def commitAddRow(self, row):
self.loggit('branch', row.localbranch)
def commitDeleteRow(self, row):
self.loggit('branch', '--delete', _remove_prefix(row.localbranch, 'remotes/'))
@GitSheet.lazy_property
def gitBranchStatuses(sheet):
ret = {} # localbranchname -> "+5/-2"
for branch_status in sheet.git_lines('for-each-ref', '--format=%(refname:short) %(upstream:short) %(upstream:track)', 'refs/heads'):
m = re.search(r'''(\S+)\s*
(\S+)?\s*
(\[
(ahead.(\d+)),?\s*
(behind.(\d+))?
\])?''', branch_status, re.VERBOSE)
if not m:
vd.status('unmatched branch status: ' + branch_status)
continue
localb, remoteb, _, _, nahead, _, nbehind = m.groups()
if nahead:
r = '+%s' % nahead
else:
r = ''
if nbehind:
if r:
r += '/'
r += '-%s' % nbehind
ret[localb] = r
return ret
GitSheet.addCommand('', 'git-open-branches', 'vd.push(git_branch(source, []))', 'push branches sheet')
GitSheet.addCommand('', 'git-branch-create', 'git("branch", input("create branch: ", type="branch"))', 'create a new branch off the current checkout')
GitBranch.addCommand('', 'git-branch-checkout', 'git("checkout", cursorRow.localbranch)', 'checkout this branch')
vd.addMenuItems('''
Git > Branch > add > git-branch-create
Git > Branch > delete > git-branch-delete
Git > Branch > rename > git-branch-rename
Git > Branch > checkout > git-branch-checkout
Git > Open > branches > git-open-branches
''')
visidata-3.1.1/visidata/apps/vgit/config.py 0000664 0000000 0000000 00000005142 14770045464 0020674 0 ustar 00root root 0000000 0000000 from itertools import islice
from visidata import vd, Column, VisiData, ItemColumn, AttrColumn, Path, AttrDict
from .gitsheet import GitSheet
CONFIG_CONTEXTS = ('local', 'global', 'system')
@VisiData.api
def git_config(vd, p, args):
if not args or '-l' in args:
return GitConfig('git-config', source=p)
vd.git_options = vd.git_config
def batched(iterable, n=1):
"Batch data into tuples of length n. The last batch may be shorter."
# batched('ABCDEFG', 3) --> ABC DEF G
assert n >= 1, 'n must be at least one'
while (batch := tuple(islice(iter(iterable), n))):
yield batch
class GitConfigColumn(Column):
def calcValue(self, row):
return row.get(self.expr)
def putValue(self, row, val):
if val is None:
self.sheet.loggit('config', '--unset', '--'+self.expr, row['option'])
else:
self.sheet.loggit('config', '--'+self.expr, row['option'], val)
row[self.expr] = val
class GitConfig(GitSheet):
guide = '''
# git config
Add, edit, or delete git config options.
- Make changes using standard commands like `a`dd and `e`dit
- `z Ctrl+S` to commit changes to git config file.
'''
rowtype = 'git options' # rowdef: [scope, origin, opt, val]
defer = True
columns = [
ItemColumn('option', width=20),
]
nKeys = 1
def iterload(self):
cmd = self.git_iter('config', '--list', '--show-scope', '--show-origin', '-z')
self.gitopts = {}
scopes = {c.name:c for c in self.columns}
for row in batched(cmd, 3):
if len(row) < 3:
break
scope, origin, optval = row
opt, val = optval.split('\n', 1)
if opt in self.gitopts:
r = self.gitopts[opt]
else:
r = AttrDict(option=opt)
self.gitopts[opt] = r
yield r
r[scope] = val
if scope not in scopes:
c = GitConfigColumn(scope, expr=scope)
self.addColumn(c)
scopes[scope] = c
def commitDeleteRow(self, row):
for k in CONFIG_CONTEXTS:
if row.get(k):
self.loggit('config', '--unset', '--'+k, row['option'])
def commitAddRow(self, row):
for k in CONFIG_CONTEXTS:
if row.get(k):
self.loggit('config', '--add', '--'+k, row['option'], row.get(k))
GitSheet.addCommand('gO', 'git-config', 'vd.push(GitConfig("git_config", source=Path(".")))', 'push sheet of git options')
vd.addMenuItems('''
Git > Config > git-config
''')
visidata-3.1.1/visidata/apps/vgit/diff.py 0000664 0000000 0000000 00000014410 14770045464 0020335 0 ustar 00root root 0000000 0000000 from visidata import vd, VisiData, ItemColumn, RowColorizer, AttrDict, Column
from .gitsheet import GitSheet
vd.option('git_diff_algo', 'minimal', 'algorithm to use for git diff')
vd.theme_option('color_git_hunk_add', 'green', 'color for added hunk lines')
vd.theme_option('color_git_hunk_del', 'red', 'color for deleted hunk lines')
vd.theme_option('color_git_hunk_diff', 'yellow', 'color for hunk diffs')
@VisiData.api
def git_diff(vd, p, args):
return GitDiffSheet('git-diff', source=p, gitargs=args)
def _parseStartCount(s):
sc = s.split(',')
if len(sc) == 2:
return sc
if len(sc) == 1:
return sc[0], 1
class GitDiffSheet(GitSheet):
columns = [
ItemColumn('a_fn', width=0),
ItemColumn('fn', 'b_fn', width=30, hoffset=-28),
ItemColumn('a_lineno', type=int, width=0),
ItemColumn('lineno', 'b_lineno', type=int, width=8),
Column('count', width=10, getter=lambda c,r: c.sheet.hunkCount(r)),
ItemColumn('context'),
ItemColumn('lines', type=''.join),
]
guide = '''# {sheet.cursorRow.a_fn}
{sheet.cursorLines}'''
def hunkCount(self, row):
return f'-{row.a_count}/+{row.b_count}'
@property
def cursorLines(self):
r = ''
for line in self.cursorRow.lines[2:]:
if line.startswith('-'):
line = '[:git_hunk_del]' + line + '[/]'
elif line.startswith('+'):
line = '[:git_hunk_add]' + line + '[/]'
r += line + '\n'
r = r[4:]
return r
def iterload(self):
current_hunk = None
for line in self.git_lines('diff --patch --inter-hunk-context=2 --find-renames --no-color --no-prefix', *self.gitargs):
if line.startswith('diff'):
diff_started = True
continue
if not diff_started:
continue
if line.startswith('---'):
hunk_lines = [line] # new file
leftfn = line[4:]
elif line.startswith('+++'):
hunk_lines.append(line)
rightfn = line[4:]
elif line.startswith('@@'):
hunk_lines.append(line)
_, linenums, context = line.split('@@')
leftlinenums, rightlinenums = linenums.split()
leftstart, leftcount = _parseStartCount(leftlinenums[1:])
rightstart, rightcount = _parseStartCount(rightlinenums[1:])
current_hunk = AttrDict(
a_fn=leftfn,
b_fn=rightfn,
context=context,
a_lineno=int(leftstart),
a_count=0,
b_lineno=int(rightstart),
b_count=0,
lines=hunk_lines
)
yield current_hunk
hunk_lines = hunk_lines[:2] # keep file context only
elif line[0] in ' +-':
current_hunk.lines.append(line)
if line[0] == '+':
current_hunk.a_count += 1
elif line[0] == '-':
current_hunk.b_count += 1
def openRow(self, row):
return HunkViewer(f'{row.a_fn}:{row.a_lineno}', source=self.source, hunks=[row])
class HunkViewer(GitSheet):
colorizers = [
RowColorizer(4, 'color_git_hunk_add', lambda s,c,r,v: r and r.old != r.new and r.old is None),
RowColorizer(4, 'color_git_hunk_del', lambda s,c,r,v: r and r.old != r.new and r.new is None),
RowColorizer(5, 'color_git_hunk_diff', lambda s,c,r,v: r and r.old != r.new and r.new is not None and r.old is not None),
]
columns = [
ItemColumn('1', 'old', width=40),
ItemColumn('2', 'new', width=40),
]
def draw(self, scr):
self.column('1').width=self.windowWidth//2-1
self.column('2').width=self.windowWidth//2-1
super().draw(scr)
def iterload(self):
nextDelIdx = None
for hunk in self.hunks:
for line in hunk.lines[3:]: # diff without the patch headers
typech = line[0]
line = line[1:]
if typech == '-': # deleted
yield AttrDict(hunk=hunk, type=typech, old=line)
if nextDelIdx is None:
nextDelIdx = len(self.rows)-1
elif typech == '+': # added
if nextDelIdx is not None:
if nextDelIdx < len(self.rows):
self.rows[nextDelIdx].new = line
nextDelIdx += 1
continue
yield AttrDict(hunk=hunk, type=typech, new=line)
nextDelIdx = None
elif typech == ' ': # unchanged
yield AttrDict(hunk=hunk, type=typech, old=line, new=line)
nextDelIdx = None
else:
continue # header
HunkViewer.addCommand('2', 'git-apply-hunk', 'source.git_apply(cursorRow.hunk, "--cached"); reload()', 'apply this hunk to the index and move to the next hunk')
HunkViewer.addCommand('1', 'git-remove-hunk', 'source.git_apply(cursorRow.hunk, "--reverse"); reload()', 'remove this hunk from staging')
HunkViewer.addCommand('Enter', 'git-skip-hunk', 'hunks.pop(0); reload()', 'move to the next hunk without applying this hunk')
HunkViewer.addCommand('d', 'delete-line', 'source[7].pop(cursorRow[3]); reload()', 'delete a line from the patch')
#HunksSheet.addCommand('g^J', 'git-diff-selected', 'vd.push(HunkViewer(selectedRows or rows, source=sheet))', 'view the diffs for the selected hunks (or all hunks)')
@GitDiffSheet.api
def git_apply(sheet, row, *args):
sheet.git('apply -p0 -', *args, _in='\n'.join(row.lines)+'\n')
c = sheet.hunkCount(row)
vd.status(f'applied hunk ({c})')
sheet.reload()
#DiffSheet.addCommand('[', '', 'cursorRowIndex = findDiffRow(cursorCol.refnum, cursorRowIndex, -1)', 'go to previous diff')
#DiffSheet.addCommand(']', '', 'cursorRowIndex = findDiffRow(cursorCol.refnum, cursorRowIndex, +1)', 'go to next diff')
GitDiffSheet.addCommand('a', 'git-add-hunk', 'git_apply(cursorRow, "--cached")', 'apply this hunk to the index')
vd.addMenuItems('''
Git > Stage > current hunk > git-add-hunk
Git > Stage > current hunk > git-add-hunk
''')
vd.addGlobals(
GitDiffSheet=GitDiffSheet,
HunkViewer=HunkViewer
)
visidata-3.1.1/visidata/apps/vgit/gitsheet.py 0000664 0000000 0000000 00000012327 14770045464 0021246 0 ustar 00root root 0000000 0000000 import io
from visidata import AttrDict, vd, Path, asyncthread, Sheet
class GitSheet(Sheet):
@property
def gitargstr(self):
return ' '.join(self.gitargs)
def git(self, subcmd, *args, **kwargs):
'For non-modifying commands; not logged except in debug mode'
sh = vd.importExternal('sh')
args = list(subcmd.split()) + list(args)
vd.debug('git ' + ' '.join(str(x) for x in args))
return sh.git(*args,
_cwd=self.gitRootPath,
**kwargs)
def loggit(self, subcmd, *args, **kwargs):
'Run git command with *args*, and post a status message.'
import sh
args = list(subcmd.split()) + list(args)
vd.warning('git ' + ' '.join(str(x) for x in args))
return sh.git(*args,
_cwd=self.gitRootPath,
**kwargs)
def git_all(self, *args, **kwargs):
'Return entire output of git command.'
sh = vd.importExternal('sh')
try:
vd.debug('git ' + ' '.join(str(x) for x in args))
out = self.git('--no-pager',
*args,
_decode_errors='replace',
_bg_exc=False,
**kwargs)
except sh.ErrorReturnCode as e:
vd.warning('git '+' '.join(str(x) for x in args), 'error=%s' % e.exit_code)
out = e.stdout
return out
def git_lines(self, subcmd, *args, **kwargs):
'Generator of stdout lines from given git command'
sh = vd.importExternal('sh')
err = io.StringIO()
args = list(subcmd.split()) + list(args)
try:
vd.debug('git ' + ' '.join(str(x) for x in args))
for line in self.git('--no-pager',
*args,
_decode_errors='replace',
_iter=True,
_bg_exc=False,
_err=err,
**kwargs):
yield line[:-1] # remove EOL
except sh.ErrorReturnCode as e:
vd.warning('git '+' '.join(str(x) for x in args), 'error=%s' % e.exit_code)
errlines = err.getvalue().splitlines()
if errlines:
vd.warning('git stderr: ' + '\n'.join(errlines))
def git_iter(self, subcmd, *args, sep='\0', **kwargs):
'Generator of chunks of stdout from given git command *subcmd*, delineated by sep character.'
sh = vd.importExternal('sh')
import sh
err = io.StringIO()
args = list(subcmd.split()) + list(args)
bufsize = 512
chunks = []
try:
vd.debug('git ' + ' '.join(str(x) for x in args))
for data in self.git('--no-pager',
*args,
_decode_errors='replace',
_out_bufsize=bufsize,
_iter=True,
_bg_exc=False,
_err=err,
**kwargs):
while True:
i = data.find(sep)
if i < 0:
break
chunks.append(data[:i])
data = data[i+1:]
yield ''.join(chunks)
chunks.clear()
chunks.append(data)
except sh.ErrorReturnCode as e:
vd.warning('git '+' '.join(str(x) for x in args), 'error=%s' % e.exit_code)
if chunks:
yield ''.join(chunks)
errlines = err.getvalue().splitlines()
if errlines:
vd.warning('git stderr: ' + '\n'.join(errlines))
@asyncthread
def modifyGit(self, *args, **kwargs):
'Run git command that modifies the repo'
vd.warning('git ' + ' '.join(str(x) for x in args))
ret = self.git_all(*args, **kwargs)
vd.status(ret)
if isinstance(self.source, GitSheet):
self.source.reload()
self.reload()
@property
def gitRootSheet(self):
if isinstance(self.source, GitSheet):
return self.source.gitRootSheet
return self
def iterload(self):
for line in self.git_lines(*self.gitargs):
yield AttrDict(line=line)
@GitSheet.lazy_property
def gitRootPath(self):
'Return Path of git root (nearest ancestor directory with a .git/)'
def _getRepoPath(p):
'Return path at p or above which has .git subdir'
if p.joinpath('.git').exists():
return p
if str(p) in ['/','']:
return None
return _getRepoPath(p.resolve().parent)
p = _getRepoPath(self.gitRootSheet.source)
if p:
return p
@GitSheet.lazy_property
def branch(self):
return self.git('rev-parse', '--abbrev-ref', 'HEAD').strip()
GitSheet.options.disp_note_none = ''
GitSheet.options.disp_status_fmt = '{sheet.progressStatus}‹{sheet.branchStatus}› {sheet.name}| '
GitSheet.addCommand('gi', 'git-exec', 'cmdstr=input("gi", type="git"); vd.push(GitSheet(cmdstr, gitargs=cmdstr.split()))', 'execute git command')
GitSheet.addCommand('Alt+g', 'menu-git', 'pressMenu("Git")', '')
vd.addMenuItems('''
Git > Execute command > git-exec
''')
visidata-3.1.1/visidata/apps/vgit/grep.py 0000664 0000000 0000000 00000003034 14770045464 0020362 0 ustar 00root root 0000000 0000000 from visidata import vd, VisiData, Path, ColumnItem, ESC
from .gitsheet import GitSheet
@VisiData.api
def git_grep(vd, p, args):
return GitGrep(args[0], regex=args[0], source=p)
class GitGrep(GitSheet):
rowtype = 'results' # rowdef: list(file, line, line_contents)
guide = '''
# vgit grep
Each row on this sheet is a line matching the regex pattern `{sheet.regex}` in the tracked files of the current directory.
- `Ctrl+O` to open _{sheet.cursorRow[0]}:{sheet.cursorRow[1]}_ in the system editor; saved changes will be reflected automatically.
'''
columns = [
ColumnItem('file', 0, help='filename of the match'),
ColumnItem('line', 1, help='line number within file'),
ColumnItem('text', 2, width=120, help='matching line of text'),
]
nKeys = 2
def iterload(self):
tmp = (self.topRowIndex, self.cursorRowIndex)
for line in self.git_lines('grep', '--no-color', '-z', '--line-number', '--ignore-case', self.regex):
# line = line.replace(ESC+'[1;31m', '[:green]')
# line = line.replace(ESC+'[m', '[/]')
yield list(line.split('\0'))
self.topRowIndex, self.cursorRowIndex = tmp
GitSheet.addCommand('g/', 'git-grep', 'rex=inputRegex("git grep: "); vd.push(GitGrep(rex, regex=rex, source=sheet))', 'find in all files in this repo')
GitGrep.addCommand('Ctrl+O', 'sysopen-row', 'launchExternalEditorPath(Path(cursorRow[0]), linenum=cursorRow[1]); reload()', 'open this file in $EDITOR')
GitGrep.bindkey('Enter', 'sysopen-row')
visidata-3.1.1/visidata/apps/vgit/log.py 0000664 0000000 0000000 00000005714 14770045464 0020215 0 ustar 00root root 0000000 0000000 import functools
from visidata import vd, VisiData, Column, ItemColumn, date, RowColorizer, asyncthread, Progress, AttrDict
from .gitsheet import GitSheet
@VisiData.api
def git_log(vd, p, *args):
return GitLogSheet('git-log', source=p, gitargs=args)
# rowdef: (commit_hash, refnames, author, author_date, body, notes)
class GitLogSheet(GitSheet):
guide = '''
# git log {sheet.gitargstr}
{sheet.cursorRow.message}
'''
GIT_LOG_FORMAT = ['%H', '%D', '%an <%ae>', '%ai', '%B', '%N']
rowtype = 'commits' # rowdef: AttrDict
defer = True
savesToSource = True
columns = [
ItemColumn('commitid', width=8),
ItemColumn('refnames', width=12),
ItemColumn('message', type=str.strip, setter=lambda c,r,v: c.sheet.git('commit --amend --no-edit --quiet --message', v), width=50),
ItemColumn('author', setter=lambda c,r,v: c.sheet.git('commit --amend --no-edit --quiet --author', v)),
ItemColumn('author_date', type=date, setter=lambda c,r,v: c.sheet.git('commit --amend --no-edit --quiet --date', v)),
ItemColumn('notes', setter=lambda c,r,v: c.sheet.git('notes add --force --message', v, r.commitid)),
]
colorizers = [
RowColorizer(5, 'color_vgit_unpushed', lambda s,c,r,v: r and not s.inRemoteBranch(r.commitid)),
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@functools.lru_cache()
def inRemoteBranch(self, commitid):
return self.git_all('branch -r --contains', commitid, _ok_code=[0, 1])
def iterload(self):
lines = self.git_iter('log --no-color -z', '--pretty=format:' + '%x1f'.join(self.GIT_LOG_FORMAT), *self.gitargs)
for record in Progress(tuple(lines)):
r = record.split('\x1f')
yield AttrDict(
commitid=r[0],
refnames=r[1],
author=r[2],
author_date=r[3],
message=r[4],
notes=r[5],
)
def openRow(self, row):
'open this commit'
return getCommitSheet(row[0][:7], self, row[0])
@asyncthread
def commit(self, path, adds, mods, dels):
assert not adds
assert not dels
for row, rowmods in mods.values():
for col, val in rowmods.values():
vd.callNoExceptions(col.putValue, row, val)
self.reload()
self.resetDeferredCommit()
GitLogSheet.addCommand(None, 'delete-row', 'error("delete is not supported")')
GitLogSheet.addCommand(None, 'add-row', 'error("commits cannot be added")')
#GitLogSheet.addCommand('x', 'git-pick', 'git("cherry-pick", cursorRow.commitid)', 'cherry-pick this commit onto current branch')
#GitLogSheet.addCommand('r', 'git-reset-here', 'git("update-ref", "refs/heads/"+source, cursorRow[0])', 'reset this branch to this commit')
GitSheet.addCommand('', 'git-log', 'vd.push(git_log(gitRootPath, branch))', 'push log of current branch')
vd.addMenuItems('''
Git > Open > log > git-log
''')
visidata-3.1.1/visidata/apps/vgit/main.py 0000664 0000000 0000000 00000002623 14770045464 0020354 0 ustar 00root root 0000000 0000000 '''
# vgit: VisiData wrapper for git
The syntax for vgit is the same as the syntax for git.
By default, will pass the command to git verbatim, as quickly as possible.
If vgit can provide an interactive interface for a particular subcommand,
it will open the sheet returned by vd.git_(path, args).
'''
import os
import sys
def vgit_cli():
import visidata
from visidata import vd, Path
args = sys.argv[1:]
flDebug = '--debug' in args
if flDebug:
args.remove('--debug')
if not args:
args = ['help']
func = getattr(vd, 'git_'+args[0], None)
if func:
vd.loadConfigAndPlugins()
vd.status(visidata.__version_info__)
vd.domotd()
if flDebug:
vd.options.debug = True
rc = 0
try:
p = Path('.')
vs = func(p, args[1:])
if vs:
vd.run(vs)
except BrokenPipeError:
os.dup2(os.open(os.devnull, os.O_WRONLY), sys.stdout.fileno()) # handle broken pipe gracefully
except visidata.ExpectedException as e:
print(str(e))
except Exception as e:
rc = 1
vd.exceptionCaught(e)
if flDebug:
raise
sys.stderr.flush()
sys.stdout.flush()
os._exit(rc) # cleanup can be expensive
import subprocess
return subprocess.run(['git', *args]).returncode
visidata-3.1.1/visidata/apps/vgit/remote.py 0000664 0000000 0000000 00000003412 14770045464 0020720 0 ustar 00root root 0000000 0000000 from visidata import vd, VisiData, ItemColumn, AttrDict, RowColorizer, Path
from .gitsheet import GitSheet
@VisiData.api
def git_remote(vd, p, args):
if not args or 'show' in args:
return GitRemotes('remotes', source=p)
class GitRemotes(GitSheet):
guide = '''
# git remote
Manage the set of repositories ("remotes") whose branches you track.
- `a` to add a remote
- `d` to mark a remote for deletion
- `e` to edit the _remote_ or _url_
- `z Ctrl+S` to commit the changes.
'''
rowtypes = 'remotes' # rowdef: dict(remote=, url=, type=)
columns=[
ItemColumn('remote', setter=lambda c,r,v: c.sheet.set_remote(c,r,v)),
ItemColumn('type'),
ItemColumn('url', width=40, setter=lambda c,r,v: c.sheet.set_url(c,r,v)),
]
nKeys = 1
defer = True
def set_remote(self, col, row, val):
self.loggit('remote', 'rename', self.column('remote').getSourceValue(row), val)
def set_url(self, col, row, val):
self.loggit('remote', 'set-url', row.remote, val)
def iterload(self):
for line in self.git_lines('remote', '-v', 'show'):
name, url, paren_type = line.split()
yield AttrDict(remote=name, url=url, type=paren_type[1:-1])
def commitDeleteRow(self, row):
self.loggit('remote', 'remove', row.remote)
def commitAddRow(self, row):
row.remote = self.column('remote').getValue(row)
row.url = self.column('url').getValue(row)
self.loggit('remote', 'add', row.remote, row.url)
def newRow(self):
return AttrDict()
GitSheet.addCommand('', 'git-open-remotes', 'vd.push(git_remote(Path("."), ""))', 'open git remotes sheet')
vd.addMenuItems('''
Git > Open > remotes > git-open-remotes
''')
visidata-3.1.1/visidata/apps/vgit/repos.py 0000664 0000000 0000000 00000004120 14770045464 0020552 0 ustar 00root root 0000000 0000000 from visidata import vd, VisiData, Sheet, Column, AttrColumn, date, vlen, asyncthread, Path, namedlist, PyobjSheet, modtime, AttrDict
from .gitsheet import GitSheet
@VisiData.api
def guess_git(vd, p):
if (p/'.git').is_dir():
return dict(filetype='git', _likelihood=10)
@VisiData.api
def open_git(vd, p):
return vd.git_status(p, [])
@VisiData.api
def git_repos(vd, p, args):
return GitRepos(p.base_stem, source=p)
class GitLinesColumn(Column):
def __init__(self, name, cmd, *args, **kwargs):
super().__init__(name, cache='async', **kwargs)
cmdparts = cmd.split()
if cmdparts[0] == 'git':
cmdparts = cmdparts[1:]
self.gitargs = cmdparts + list(args)
def calcValue(self, r):
lines = list(GitSheet(source=r).git_lines(*self.gitargs))
if lines:
return lines
class GitAllColumn(GitLinesColumn):
def calcValue(self, r):
return GitSheet(source=r).git_all(*self.gitargs).strip()
class GitRepos(GitSheet):
guide = '''
# git repos
A list of git repositories under `{sheet.source}`
- `Enter` to open the status sheet for the current repo
'''
rowtype = 'git repos' # rowdef: Path
columns = [
Column('repo', type=str, width=30),
GitAllColumn('branch', 'git rev-parse --abbrev-ref HEAD', width=8),
GitLinesColumn('diffs', 'git diff --no-color', type=vlen, width=8),
GitLinesColumn('staged_diffs', 'git diff --cached', type=vlen, width=8),
GitLinesColumn('branches', 'git branch --no-color', type=vlen, width=10),
GitLinesColumn('stashes', 'git stash list', type=vlen, width=8),
Column('modtime', type=date, getter=lambda c,r: modtime(r)),
]
nKeys = 1
def iterload(self):
import glob
for fn in glob.glob('**/.git', root_dir=self.source, recursive=True):
yield Path(fn).parent
def openRow(self, row):
return vd.git_status(row, [])
def openCell(self, col, row):
val = col.getValue(row)
return PyobjSheet(getattr(val, '__name__', ''), source=val)
visidata-3.1.1/visidata/apps/vgit/setup.py 0000664 0000000 0000000 00000002566 14770045464 0020576 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
from setuptools import setup, find_packages
# Note: use `python3 visidata/apps/setup.py install` from the root directory
__version__ = '0.2-dev'
setup(name='vgit',
version=__version__,
description='a sleek terminal user interface for git',
# long_description=open('README.md').read(),
install_requires=['sh<2'], # visidata
packages=find_packages(exclude=["tests"]),
scripts=['vgit'],
entry_points={'visidata.plugins': 'vgit=visidata.apps.vgit'},
author='Saul Pwanson',
author_email='vgit@saul.pw',
url='https://github.com/saulpw/visidata/vgit',
license='GPLv3',
python_requires='>=3.7',
classifiers=[
'Development Status :: 2 - Pre-Alpha',
'Environment :: Console',
'Environment :: Console :: Curses',
'Intended Audience :: Developers',
'Intended Audience :: System Administrators',
'Intended Audience :: Information Technology',
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
'Operating System :: OS Independent',
'Programming Language :: Python :: 3',
'Topic :: Utilities',
'Topic :: Software Development :: Version Control',
'Topic :: Terminals'
],
keywords=('console textpunk git version-control curses visidata tui terminal'),
)
visidata-3.1.1/visidata/apps/vgit/stash.py 0000664 0000000 0000000 00000004767 14770045464 0020565 0 ustar 00root root 0000000 0000000 from visidata import vd, VisiData, ItemColumn, AttrDict
from .gitsheet import GitSheet
from .diff import GitDiffSheet
@VisiData.api
def git_stash(vd, p, args):
if 'list' in args:
return GitStashes('git-stash-list', source=p, gitargs=args)
class GitStashes(GitSheet):
guide = '''
# git stash
This is the list of changes that have been stashed previously.
`a` to apply this stashed change (without removing it)
`d` to drop this stashed change
`b` to create a branch from this stashed change'),
'''
rowtype = 'stashed change' # rowdef: AttrDict(stashid=, branched_from=, sha1=, msg=)
columns = [
ItemColumn('stashid'),
ItemColumn('branched_from'),
ItemColumn('sha1'),
ItemColumn('msg'),
ItemColumn('line', width=0),
]
def iterload(self):
for line in self.git_lines('stash', *self.gitargs):
stashid, ctx, rest = line.split(': ', 2)
if ctx.startswith('WIP on '):
branched_from = ctx[len('WIP on '):]
sha1, msg = rest.split(' ', 1)
elif ctx.startswith('On '):
branched_from = ctx[len('On '):]
sha1 = ''
msg = rest
yield AttrDict(
line=line,
stashid=stashid,
branched_from=branched_from,
sha1=sha1,
msg=msg.strip(),
)
def openRow(self, row):
'open this stashed change'
return GitDiffSheet(row.stashid, "diffs", gitargs=['stash show --no-color --patch', row.stashid], source=self.source)
GitSheet.addCommand('', 'git-open-stashes', 'vd.push(git_stash(source, ["list"]))', 'push stashes sheet')
GitStashes.addCommand('a', 'git-stash-apply', 'loggit("stash", "apply", cursorRow[0])', 'apply this stashed change without removing')
GitStashes.addCommand('', 'git-stash-pop', 'loggit("stash", "pop", cursorRow[0])', 'apply this stashed change and drop it')
GitStashes.addCommand('d', 'git-stash-drop', 'loggit("stash", "drop", cursorRow[0])', 'drop this stashed change')
GitStashes.addCommand('b', 'git-stash-branch', 'loggit("stash", "branch", input("create branch from stash named: "), cursorRow[0])', 'create branch from stash')
vd.addMenuItems('''
Git > Open > stashes > git-open-stashes
Git > Stash > apply > git-stash-apply
Git > Stash > drop > git-stash-drop
Git > Stash > apply then drop > git-stash-pop
Git > Stash > create branch > git-stash-branch
''')
visidata-3.1.1/visidata/apps/vgit/status.py 0000664 0000000 0000000 00000021316 14770045464 0020753 0 ustar 00root root 0000000 0000000 from visidata import vd, Column, VisiData, ItemColumn, Path, AttrDict, BaseSheet, IndexSheet
from visidata import RowColorizer, CellColorizer
from visidata import filesize, modtime, date
from .gitsheet import GitSheet
#from .diff import DifferSheet
vd.option('vgit_show_ignored', False, 'show ignored files on git status')
vd.theme_option('color_git_staged_mod', 'green', 'color of files staged with modifications')
vd.theme_option('color_git_staged_add', 'green', 'color of files staged for addition')
vd.theme_option('color_git_staged_del', 'red', 'color of files staged for deletion')
vd.theme_option('color_git_unstaged_del', 'on 88', 'color of files deleted but unstaged')
vd.theme_option('color_git_untracked', '243 blue', 'color of ignored/untracked files')
@VisiData.api
def git_status(vd, p, args, **kwargs):
vs = GitStatus('/'.join(p.parts[-2:]), source=p)
if not vs.gitRootPath:
return vd.git_repos(p, [])
return vs
class GitFile:
def __init__(self, path, gitsrc):
self.path = path
self.filename = path.relative_to(gitsrc)
self.is_dir = self.path.is_dir()
def __str__(self):
return str(self.filename) + (self.is_dir and '/' or '')
class GitStatus(GitSheet):
rowtype = 'files' # rowdef: GitFile
guide = '''
# git status
An overview of the local git checkout.
- `Enter` to open diff of file (`git diff`)
- `a` to stage changes in file (`git add`)
- `r` to unstage changes in file (`git reset`)
- `c` to revert all unstaged changes in file (`git checkout`)
- `d` to stage the entire file for deletion (`git rm`)
- `z Ctrl+S` to commit staged changes (`git commit`)
'''
columns = [
Column('path', width=40, getter=lambda c,r: str(r)),
Column('status', getter=lambda c,r: c.sheet.statusText(c.sheet.git_status(r)), width=8),
Column('status_raw', getter=lambda c,r: c.sheet.git_status(r), width=0),
Column('staged', getter=lambda c,r: c.sheet.git_status(r).dels),
Column('unstaged', getter=lambda c,r: c.sheet.git_status(r).adds),
Column('type', getter=lambda c,r: r.is_dir() and '/' or r.suffix, width=0),
Column('size', type=int, getter=lambda c,r: filesize(r)),
Column('modtime', type=date, getter=lambda c,r: modtime(r)),
]
nKeys = 1
colorizers = [
CellColorizer(3, 'color_git_staged_mod', lambda s,c,r,v: r and c and c.name == 'staged' and s.git_status(r).status[0] == 'M'), # staged mod
CellColorizer(1, 'color_git_staged_del', lambda s,c,r,v: r and c and c.name == 'staged' and s.git_status(r).status == 'D '), # staged delete
RowColorizer(1, 'color_git_staged_add', lambda s,c,r,v: r and s.git_status(r).status in ['A ', 'M ']), # staged add/mod
RowColorizer(1, 'color_git_unstaged_del', lambda s,c,r,v: r and s.git_status(r).status[1] == 'D'), # unstaged delete
RowColorizer(3, 'color_git_untracked', lambda s,c,r,v: r and s.git_status(r).status == '!!'), # ignored
RowColorizer(1, 'color_git_untracked', lambda s,c,r,v: r and s.git_status(r).status == '??'), # untracked
]
def statusText(self, st):
vmod = {'A': 'add', 'D': 'rm', 'M': 'mod', 'T': 'chmod', '?': '', '!': 'ignored', 'U': 'unmerged'}
x, y = st.status
if st == '??': # untracked
return 'new'
elif st == '!!': # ignored
return 'ignored'
elif x != ' ' and y == ' ': # staged
return vmod.get(x, x)
elif y != ' ': # unstaged
return vmod.get(y, y)
else:
return ''
@property
def workdir(self):
return str(self.source)
def git_status(self, r):
'''return tuple of (status, adds, dels).
status like !! ??
adds and dels are lists of additions and deletions.
'''
if not r:
return None
fn = str(r)
ret = self._cachedStatus.get(fn, None)
if not ret:
ret = AttrDict(status='??')
self._cachedStatus[fn] = ret
return ret
def ignored(self, fn):
if self.options.vgit_show_ignored:
return False
if fn in self._cachedStatus:
return self._cachedStatus[fn].status == '!!'
return False
@property
def remotediff(self):
return self.gitBranchStatuses.get(self.branch, 'no branch')
def iterload(self):
files = [GitFile(p, self.source) for p in self.source.iterdir() if p.stem not in ('.git')] # files in working dir
filenames = dict((gf.filename, gf) for gf in files)
self._cachedStatus.clear()
for fn in self.git_iter('ls-files', '-z'):
self._cachedStatus[fn] = AttrDict(status=' ')
for line in self.git_iter('status', '-z', '-unormal', '--ignored'):
if not line: continue
if line[2:3] == ' ':
st, fn = line[:2], line[3:]
else:
fn = line
st = '??' # untracked file
self._cachedStatus[fn] = AttrDict(status=st)
if not self.ignored(fn):
yield Path(fn)
for line in self.git_iter('diff-files', '--numstat', '-z'):
if not line: continue
adds, dels, fn = line.split('\t')
if fn not in self._cachedStatus:
self._cachedStatus[fn] = AttrDict(status='##')
cs = self._cachedStatus[fn]
cs.adds = '+%s/-%s' % (adds, dels)
for line in self.git_iter('diff-index', '--cached', '--numstat', '-z', 'HEAD'):
if not line: continue
adds, dels, fn = line.split('\t')
if fn not in self._cachedStatus:
self._cachedStatus[fn] = AttrDict(status='$$')
cs = self._cachedStatus[fn]
cs.dels = '+%s/-%s' % (adds, dels)
self.orderBy(None, self.columns[-1], reverse=True)
self.recalc() # erase column caches
def openRow(self, row):
'Open unstaged diffs for this file, or dive into directory'
if row.is_dir:
return GitStatus(row.path)
else:
return DifferSheet(row, "HEAD", "index", "working", source=sheet)
def openRows(self, rows):
'Open unstaged hunks for selected rows'
return getHunksSheet(sheet, *rows)
@GitStatus.lazy_property
def _cachedStatus(self):
return {} # [filename] -> AttrDict(status='xx', adds=, dels=)
GitStatus.addCommand('a', 'git-add', 'loggit("add", cursorRow.filename)', 'add this new file or modified file to staging')
#GitStatus.addCommand('m', 'git-mv', 'loggit("mv", cursorRow.filename, input("rename file to: ", value=cursorRow.filename))', 'rename this file')
GitStatus.addCommand('d', 'git-rm', 'loggit("rm", cursorRow.filename)', 'stage this file for deletion')
GitStatus.addCommand('r', 'git-reset', 'loggit("reset", "HEAD", cursorRow.filename)', 'reset/unstage this file')
GitStatus.addCommand('c', 'git-checkout', 'loggit("checkout", cursorRow.filename)', 'checkout this file')
GitStatus.addCommand('ga', 'git-add-selected', 'loggit("add", *[r for r in selectedRows])', 'add all selected files to staging')
GitStatus.addCommand('gd', 'git-rm-selected', 'loggit("rm", *[r for r in selectedRows])', 'delete all selected files')
GitStatus.addCommand(None, 'git-commit', 'loggit("commit", "-m", input("commit message: "))', 'commit changes')
GitStatus.addCommand(None, 'git-ignore-file', 'open(rootPath/".gitignore", "a").write(cursorRow.filename+"\\n"); reload()', 'add file to toplevel .gitignore')
GitStatus.addCommand(None, 'git-ignore-wildcard', 'open(rootPath/.gitignore, "a").write(input("add wildcard to .gitignore: "))', 'add input line to toplevel .gitignore')
#GitStatus.addCommand('z^J', 'diff-file-staged', 'vd.push(getStagedHunksSheet(sheet, cursorRow))', 'push staged diffs for this file')
#GitStatus.addCommand('gz^J', 'diff-selected-staged', 'vd.push(getStagedHunksSheet(sheet, *(selectedRows or rows)))', 'push staged diffs for selected files or all files')
#GitStatus.addCommand('^O', 'sysopen-row', 'launchExternalEditorPath(Path(cursorRow.path))', 'open this file in $EDITOR')
vd.addMenuItems('''
Git > View staged changes > current file > diff-file-staged
Git > View staged changes > selected files > staged changes > diff-selected-staged
Git > Stage > current file > git-add
Git > Stage > selected files > git-add-selected
Git > Unstage > current file > git-reset
Git > Unstage > selected files > git-reset-selected
Git > Rename file > git-mv
Git > Delete > file > git-rm
Git > Delete > selected files > git-rm-selected
Git > Ignore > file > ignore-file
Git > Ignore > wildcard > ignore-wildcard
Git > Commit staged changes > git-commit
Git > Revert unstaged changes > current file > git-checkout
''')
visidata-3.1.1/visidata/apps/vgit/statusbar.py 0000664 0000000 0000000 00000001623 14770045464 0021437 0 ustar 00root root 0000000 0000000 from .gitsheet import vd, GitSheet
GitSheet.options.disp_status_fmt = '{sheet.progressStatus}‹{sheet.branchStatus}› {sheet.name}| '
@GitSheet.property
def progressStatus(sheet):
inp = sheet.gitInProgress()
return ('[%s] ' % inp) if inp else ''
@GitSheet.property
def branchStatus(sheet):
if hasattr(sheet.gitRootSheet, 'branch'):
return '%s%s' % (sheet.rootSheet.branch, sheet.rootSheet.remotediff)
return ''
@GitSheet.api
def gitInProgress(sheet):
p = sheet.gitPath
if not p:
return 'no repo'
if (p/'rebase-merge').exists() or (p/'rebase-apply/rebasing').exists():
return 'rebasing'
elif p/'rebase-apply'.exists():
return 'applying'
elif p/'CHERRY_PICK_HEAD'.exists():
return 'cherry-picking'
elif p/'MERGE_HEAD'.exists():
return 'merging'
elif p/'BISECT_LOG'.exists():
return 'bisecting'
return ''
visidata-3.1.1/visidata/basesheet.py 0000664 0000000 0000000 00000024353 14770045464 0017463 0 ustar 00root root 0000000 0000000 import os
import visidata
from visidata import Extensible, VisiData, vd, EscapeException, MissingAttrFormatter, AttrDict
UNLOADED = tuple() # sentinel for a sheet not yet loaded for the first time
vd.beforeExecHooks = [] # func(sheet, cmd, args, keystrokes) called before the exec()
class LazyChainMap:
'provides a lazy mapping to obj attributes. useful when some attributes are expensive properties.'
def __init__(self, *objs, locals=None):
self.locals = {} if locals is None else locals
self.objs = {} # [k] -> obj
for obj in objs:
for k in dir(obj):
if k not in self.objs:
self.objs[k] = obj
def __iter__(self):
return iter(self.objs)
def __contains__(self, k):
return k in self.objs
def keys(self):
return list(self.objs.keys()) # sum(set(dir(obj)) for obj in self.objs))
def get(self, key, default=None):
if key in self.locals:
return self.locals[key]
return self.objs.get(key, default)
def clear(self):
self.locals.clear()
def __getitem__(self, k):
obj = self.objs.get(k, None)
if obj:
return getattr(obj, k)
return self.locals[k]
def __setitem__(self, k, v):
obj = self.objs.get(k, None)
if obj:
return setattr(obj, k, v)
self.locals[k] = v
class DrawablePane(Extensible):
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
'Base class for all interaction owners that can be drawn in a window.'
def draw(self, scr):
'Draw on the terminal window *scr*. Should be overridden.'
vd.error('no draw')
@property
def windowHeight(self):
'Height of the current sheet window, in terminal lines.'
return self._scr.getmaxyx()[0] if self._scr else 25
@property
def windowWidth(self):
'Width of the current sheet window, in single-width characters.'
return self._scr.getmaxyx()[1] if self._scr else 80
@property
def currow(self):
return None
def execCommand2(self, cmd, vdglobals=None):
"Execute `cmd` with `vdglobals` as globals and this sheet's attributes as locals. Return True if user cancelled."
try:
self.sheet = self
code = compile(cmd.execstr, cmd.longname, 'exec')
exec(code, vdglobals, LazyChainMap(vd, self))
return False
except EscapeException as e: # user aborted
vd.warning(str(e))
return True
class _dualproperty:
'Return *obj_method* or *cls_method* depending on whether property is on instance or class.'
def __init__(self, obj_method, cls_method):
self._obj_method = obj_method
self._cls_method = cls_method
def __get__(self, obj, objtype=None):
if obj is None:
return self._cls_method(objtype)
else:
return self._obj_method(obj)
class BaseSheet(DrawablePane):
'Base class for all sheet types.'
_rowtype = object # callable (no parms) that returns new empty item
_coltype = None # callable (no parms) that returns new settable view into that item
rowtype = 'objects' # one word, plural, describing the items
precious = True # False for a few discardable metasheets
defer = False # False for not deferring changes until save
guide = '' # default to show in sidebar
def _obj_options(self):
return vd.OptionsObject(vd._options, obj=self)
def _class_options(cls):
return vd.OptionsObject(vd._options, obj=cls)
class_options = options = _dualproperty(_obj_options, _class_options)
def __init__(self, *names, rows=UNLOADED, **kwargs):
self._name = None # initial cache value necessary for self.options
self._names = []
self.loading = False
self.names = list(names)
self.source = None
self.rows = rows # list of opaque objects
self._scr = None
self.hasBeenModified = False
super().__init__(**kwargs)
self._sidebar = ''
def setModified(self):
if not self.hasBeenModified:
vd.addUndo(setattr, self, 'hasBeenModified', self.hasBeenModified)
self.hasBeenModified = True
def __lt__(self, other):
if self.name != other.name:
return self.name < other.name
else:
return id(self) < id(other)
def __copy__(self):
'Return shallow copy of sheet.'
cls = self.__class__
ret = cls.__new__(cls)
ret.__dict__.update(self.__dict__)
ret.precious = True # copy can be precious even if original is not
ret.hasBeenModified = False # copy is not modified even if original is
return ret
def __bool__(self):
'an instantiated Sheet always tests true'
return True
def __len__(self):
'Number of elements on this sheet.'
return self.nRows
def __str__(self):
return self.name
@property
def rows(self):
return self._rows
@rows.setter
def rows(self, rows):
self._rows = rows
@property
def nRows(self):
'Number of rows on this sheet. Override in subclass.'
return 0
def __contains__(self, vs):
if self.source is vs:
return True
if isinstance(self.source, BaseSheet):
return vs in self.source
return False
@property
def displaySource(self):
if isinstance(self.source, BaseSheet):
return f'the *{self.source}* sheet'
if isinstance(self.source, (list, tuple)):
if len(self.source) == 1:
return f'the **{self.source[0]}** sheet'
return f'{len(self.source)} sheets'
return f'**{self.source}**'
def execCommand(self, longname, vdglobals=None, keystrokes=None):
if ' ' in longname:
cmd, arg = longname.split(' ', maxsplit=1)
vd.injectInput(arg)
cmd = self.getCommand(longname or keystrokes)
if not cmd:
vd.fail('no command for %s' % (longname or keystrokes))
return False
escaped = False
err = ''
if vdglobals is None:
vdglobals = vd.getGlobals()
vd.cmdlog # make sure cmdlog has been created for first command
try:
for hookfunc in vd.beforeExecHooks:
hookfunc(self, cmd, '', keystrokes)
escaped = super().execCommand2(cmd, vdglobals=vdglobals)
except Exception as e:
vd.debug(cmd.execstr)
err = vd.exceptionCaught(e)
escaped = True
if vd.cmdlog:
# sheet may have changed
vd.callNoExceptions(vd.cmdlog.afterExecSheet, vd.activeSheet, escaped, err)
vd.callNoExceptions(self.checkCursor)
vd.clearCaches()
for t in self.currentThreads:
if not hasattr(t, 'lastCommand'):
t.lastCommand = True
return escaped
@property
def lastCommandThreads(self):
return [t for t in self.currentThreads if getattr(t, 'lastCommand', None)]
@property
def names(self):
return self._names
@names.setter
def names(self, names):
if self._names:
vd.addUndo(setattr, self, 'names', self._names)
self._names = names
self._name = self.options.name_joiner.join(self.maybeClean(str(x)) for x in self._names)
@property
def name(self):
'Name of this sheet.'
return self._name
@name.setter
def name(self, name):
'Set name without spaces.'
if self._names:
vd.addUndo(setattr, self, 'names', self._names)
self._name = self.maybeClean(str(name))
self._names = [self._name]
def maybeClean(self, s):
'stub'
return s
def recalc(self):
'Clear any calculated value caches.'
pass
def refresh(self):
'Recalculate any internal state needed for `draw()`. Overridable.'
pass
def ensureLoaded(self):
'Call ``reload()`` if not already loaded.'
if self.rows is UNLOADED:
self.rows = [] # prevent auto-reload from running twice
return self.reload() # likely launches new thread
def reload(self):
'Load sheet from *self.source*. Override in subclass.'
vd.error('no reload')
@property
def cursorRow(self):
'The row object at the row cursor. Overridable.'
return None
def checkCursor(self):
'Check cursor and fix if out-of-bounds. Overridable.'
pass
def evalExpr(self, expr, **kwargs):
'Evaluate Python expression *expr* in the context of *kwargs* (may vary by sheet type).'
return eval(expr, vd.getGlobals(), dict(sheet=self))
def formatString(self, fmt, **kwargs):
'Return formatted string with *sheet* and *vd* accessible to expressions. Missing expressions return empty strings instead of error.'
return MissingAttrFormatter().format(fmt, sheet=self, vd=vd, **kwargs)
@VisiData.api
def redraw(vd):
'Clear the terminal screen and let the next draw cycle recreate the windows and redraw everything.'
for vs in vd.sheets:
vs._scr = None
if vd.win1: vd.win1.clear()
if vd.win2: vd.win2.clear()
if vd.scrFull:
vd.scrFull.clear()
vd.setWindows(vd.scrFull)
@VisiData.property
def sheet(self):
return self.activeSheet
@VisiData.api
def isLongname(self, ks:str):
'Return True if *ks* is a longname.'
return ('-' in ks) and (ks[-1] != '-') or (len(ks) > 3 and ks.islower())
@VisiData.api
def getSheet(vd, sheetname):
'Return Sheet from the sheet stack. *sheetname* can be a sheet name or a sheet number indexing directly into ``vd.sheets``.'
if isinstance(sheetname, BaseSheet):
return sheetname
matchingSheets = [x for x in vd.sheets if x.name == sheetname]
if matchingSheets:
if len(matchingSheets) > 1:
vd.warning('more than one sheet named "%s"' % sheetname)
return matchingSheets[0]
try:
sheetidx = int(sheetname)
return vd.sheets[sheetidx]
except ValueError:
pass
if sheetname == 'options':
vs = vd.globalOptionsSheet
vs.reload()
vs.vd = vd
return vs
visidata-3.1.1/visidata/bezier.py 0000664 0000000 0000000 00000004014 14770045464 0016770 0 ustar 00root root 0000000 0000000 import math
def bezier(x1, y1, x2, y2, x3, y3):
'Generate (x,y) coordinates on quadratic curve from (x1,y1) to (x3,y3) with control point at (x2,y2).'
yield (x1, y1)
yield from _recursive_bezier(x1, y1, x2, y2, x3, y3)
yield (x3, y3)
def _recursive_bezier(x1, y1, x2, y2, x3, y3, level=0):
'from http://www.antigrain.com/research/adaptive_bezier/'
m_approximation_scale = 10.0
m_distance_tolerance = (0.5 / m_approximation_scale) ** 2
m_angle_tolerance = 1 * 2*math.pi/360 # 15 degrees in rads
curve_angle_tolerance_epsilon = 0.01
curve_recursion_limit = 32
curve_collinearity_epsilon = 1e-30
if level > curve_recursion_limit:
return
# Calculate all the mid-points of the line segments
x12 = (x1 + x2) / 2
y12 = (y1 + y2) / 2
x23 = (x2 + x3) / 2
y23 = (y2 + y3) / 2
x123 = (x12 + x23) / 2
y123 = (y12 + y23) / 2
dx = x3-x1
dy = y3-y1
d = abs(((x2 - x3) * dy - (y2 - y3) * dx))
if d > curve_collinearity_epsilon:
# Regular care
if d*d <= m_distance_tolerance * (dx*dx + dy*dy):
# If the curvature doesn't exceed the distance_tolerance value, we tend to finish subdivisions.
if m_angle_tolerance < curve_angle_tolerance_epsilon:
yield (x123, y123)
return
# Angle & Cusp Condition
da = abs(math.atan2(y3 - y2, x3 - x2) - math.atan2(y2 - y1, x2 - x1))
if da >= math.pi:
da = 2*math.pi - da
if da < m_angle_tolerance:
# Finally we can stop the recursion
yield (x123, y123)
return
else:
# Collinear case
dx = x123 - (x1 + x3) / 2
dy = y123 - (y1 + y3) / 2
if dx*dx + dy*dy <= m_distance_tolerance:
yield (x123, y123)
return
# Continue subdivision
yield from _recursive_bezier(x1, y1, x12, y12, x123, y123, level + 1)
yield from _recursive_bezier(x123, y123, x23, y23, x3, y3, level + 1)
visidata-3.1.1/visidata/canvas.py 0000664 0000000 0000000 00000112732 14770045464 0016772 0 ustar 00root root 0000000 0000000 import math
import random
from collections import defaultdict, Counter, OrderedDict
from visidata import vd, asyncthread, ENTER, colors, update_attr, clipdraw, dispwidth
from visidata import BaseSheet, Column, Progress, ColorAttr
from visidata.bezier import bezier
# see www/design/graphics.md
vd.theme_option('disp_graph_labels', True, 'show axes and legend on graph')
vd.theme_option('plot_colors', 'green red yellow cyan magenta white 38 136 168', 'list of distinct colors to use for plotting distinct objects')
vd.theme_option('disp_canvas_charset', ''.join(chr(0x2800+i) for i in range(256)), 'charset to render 2x4 blocks on canvas')
vd.theme_option('disp_pixel_random', False, 'randomly choose attr from set of pixels instead of most common')
vd.theme_option('disp_zoom_incr', 2.0, 'amount to multiply current zoomlevel when zooming')
vd.theme_option('color_graph_hidden', '238 blue', 'color of legend for hidden attribute')
vd.theme_option('color_graph_selected', 'bold', 'color of selected graph points')
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
if isinstance(self.x, int):
return '(%d,%d)' % (self.x, self.y)
else:
return '(%.02f,%.02f)' % (self.x, self.y)
@property
def xy(self):
return (self.x, self.y)
class Box:
def __init__(self, x, y, w=0, h=0):
self.xmin = x
self.ymin = y
self.w = w
self.h = h
def __repr__(self):
return '[%s+%s,%s+%s]' % (self.xmin, self.w, self.ymin, self.h)
@property
def xymin(self):
return Point(self.xmin, self.ymin)
@property
def xmax(self):
return self.xmin + self.w
@property
def ymax(self):
return self.ymin + self.h
@property
def center(self):
return Point(self.xcenter, self.ycenter)
@property
def xcenter(self):
return self.xmin + self.w/2
@property
def ycenter(self):
return self.ymin + self.h/2
def contains(self, x, y):
return x >= self.xmin and \
x < self.xmax and \
y >= self.ymin and \
y < self.ymax
def BoundingBox(x1, y1, x2, y2):
return Box(min(x1, x2), min(y1, y2), abs(x2-x1), abs(y2-y1))
def clipline(x1, y1, x2, y2, xmin, ymin, xmax, ymax):
'Liang-Barsky algorithm, returns [xn1,yn1,xn2,yn2] of clipped line within given area, or None'
dx = x2-x1
dy = y2-y1
pq = [
(-dx, x1-xmin), # left
( dx, xmax-x1), # right
(-dy, y1-ymin), # bottom
( dy, ymax-y1), # top
]
u1, u2 = 0, 1
for p, q in pq:
if p < 0: # from outside to inside
u1 = max(u1, q/p)
elif p > 0: # from inside to outside
u2 = min(u2, q/p)
else: # p == 0: # parallel to bbox
if q < 0: # completely outside bbox
return None
if u1 > u2: # completely outside bbox
return None
xn1 = x1 + dx*u1
yn1 = y1 + dy*u1
xn2 = x1 + dx*u2
yn2 = y1 + dy*u2
return xn1, yn1, xn2, yn2
def iterline(x1, y1, x2, y2):
'Yields (x, y) coords of line from (x1, y1) to (x2, y2)'
xdiff = abs(x2-x1)
ydiff = abs(y2-y1)
xdir = 1 if x1 <= x2 else -1
ydir = 1 if y1 <= y2 else -1
r = math.ceil(max(xdiff, ydiff))
if r == 0: # point, not line
yield x1, y1
else:
x, y = math.floor(x1), math.floor(y1)
i = 0
while i < r:
x += xdir * xdiff / r
y += ydir * ydiff / r
yield x, y
i += 1
def anySelected(vs, rows):
for r in rows:
if vs.isSelected(r):
return True
# - width/height are exactly equal to the number of pixels displayable, and can change at any time.
# - needs to refresh from source on resize
class Plotter(BaseSheet):
'pixel-addressable display of entire terminal with (x,y) integer pixel coordinates'
columns=[Column('_')] # to eliminate errors outside of draw()
rowtype='pixels'
def __init__(self, *names, **kwargs):
super().__init__(*names, **kwargs)
self.labels = [] # (x, y, text, attr, row)
self.hiddenAttrs = set()
self.needsRefresh = False
self.resetCanvasDimensions(1, 1) #2171
@property
def nRows(self):
return (self.plotwidth* self.plotheight)
def resetCanvasDimensions(self, windowHeight, windowWidth):
'sets total available canvas dimensions to (windowHeight, windowWidth) (in char cells)'
self.plotwidth = windowWidth*2
self.plotheight = (windowHeight-1)*4 # exclude status line
# pixels[y][x] = { attr: list(rows), ... }
self.pixels = [[defaultdict(list) for x in range(self.plotwidth)] for y in range(self.plotheight)]
def plotpixel(self, x, y, attr:"str|ColorAttr=''", row=None):
self.pixels[y][x][attr].append(row)
def plotline(self, x1, y1, x2, y2, attr:"str|ColorAttr=''", row=None):
for x, y in iterline(x1, y1, x2, y2):
self.plotpixel(math.ceil(x), math.ceil(y), attr, row)
def plotlabel(self, x, y, text, attr:"str|ColorAttr=''", row=None):
self.labels.append((x, y, text, attr, row))
def plotlegend(self, i, txt, attr:"str|ColorAttr=''", width=15):
# move it 1 character to the left b/c the rightmost column can't be drawn to
self.plotlabel(self.plotwidth-(width+1)*2, i*4, txt, attr)
@property
def plotterCursorBox(self):
'Returns pixel bounds of cursor as a Box. Override to provide a cursor.'
return Box(0,0,0,0)
@property
def plotterMouse(self):
return Point(*self.plotterFromTerminalCoord(self.mouseX, self.mouseY))
def plotterFromTerminalCoord(self, x, y):
return x*2, y*4
def getPixelAttrRandom(self, x, y) -> str:
'weighted-random choice of colornum at this pixel.'
c = list(attr for attr, rows in self.pixels[y][x].items()
for r in rows if attr and attr not in self.hiddenAttrs)
return random.choice(c) if c else 0
def getPixelAttrMost(self, x, y) -> str:
'most common colornum at this pixel.'
r = self.pixels[y][x]
if not r:
return 0
c = [(len(rows), attr, rows) for attr, rows in r.items() if attr and attr not in self.hiddenAttrs]
if not c:
return 0
_, attr, rows = max(c)
return attr
def hideAttr(self, attr:str, hide=True):
if hide:
self.hiddenAttrs.add(attr)
else:
self.hiddenAttrs.remove(attr)
self.plotlegends()
def rowsWithin(self, plotter_bbox, invert_y=False):
'return list of deduped rows within plotter_bbox'
ret = {}
x_start = max(0, plotter_bbox.xmin)
if len(self.pixels) == 0: return []
x_end = min(len(self.pixels[0]), plotter_bbox.xmax)
y_start = max(0, plotter_bbox.ymin)
y_end = min(len(self.pixels), plotter_bbox.ymax)
if invert_y:
y_range = range(y_end-1, y_start-1, -1)
else:
y_range = range(y_start, y_end)
for x in range(x_start, x_end):
for y in y_range:
for attr, rows in self.pixels[y][x].items():
if attr not in self.hiddenAttrs:
for r in rows:
ret[self.source.rowid(r)] = r
return list(ret.values())
def draw(self, scr):
windowHeight, windowWidth = scr.getmaxyx()
if self.needsRefresh:
self.render(windowHeight, windowWidth)
self.draw_pixels(scr)
self.draw_labels(scr)
def draw_empty(self, scr):
# use draw_empty() when calling draw_pixels() with clear_empty_squares=False
cursorBBox = self.plotterCursorBox
for char_y in range(0, self.plotheight//4):
for char_x in range(0, self.plotwidth//2):
cattr = ColorAttr()
ch = ' '
# draw cursor
if cursorBBox.contains(char_x*2, char_y*4) or \
cursorBBox.contains(char_x*2+1, char_y*4+3):
cattr = update_attr(cattr, colors.color_current_row)
scr.addstr(char_y, char_x, ch, cattr.attr)
def draw_pixels(self, scr, clear_empty_squares=True):
disp_canvas_charset = self.options.disp_canvas_charset or ' o'
disp_canvas_charset += (256 - len(disp_canvas_charset)) * disp_canvas_charset[-1]
if self.pixels:
cursorBBox = self.plotterCursorBox
getPixelAttr = self.getPixelAttrRandom if self.options.disp_pixel_random else self.getPixelAttrMost
for char_y in range(0, self.plotheight//4):
for char_x in range(0, self.plotwidth//2):
block_attrs = [
getPixelAttr(char_x*2 , char_y*4 ),
getPixelAttr(char_x*2 , char_y*4+1),
getPixelAttr(char_x*2 , char_y*4+2),
getPixelAttr(char_x*2+1, char_y*4 ),
getPixelAttr(char_x*2+1, char_y*4+1),
getPixelAttr(char_x*2+1, char_y*4+2),
getPixelAttr(char_x*2 , char_y*4+3),
getPixelAttr(char_x*2+1, char_y*4+3),
]
pow2 = 1
braille_num = 0
for c in block_attrs:
if c:
braille_num += pow2
pow2 *= 2
ch = disp_canvas_charset[braille_num]
if braille_num != 0:
color = Counter(c for c in block_attrs if c).most_common(1)[0][0]
cattr = colors.get_color(color)
else:
cattr = ColorAttr()
# don't erase empty squares, useful for subclasses that draw elements like reflines
# before pixels are drawn
if not clear_empty_squares:
continue
# draw cursor
if cursorBBox.contains(char_x*2, char_y*4) or \
cursorBBox.contains(char_x*2+1, char_y*4+3):
cattr = update_attr(cattr, colors.color_current_row)
if cattr.attr:
scr.addstr(char_y, char_x, ch, cattr.attr)
def draw_labels(self, scr):
def _mark_overlap_text(labels, textobj):
def _overlaps(a, b):
a_x1, _, a_txt, _, _ = a
b_x1, _, b_txt, _, _ = b
a_x2 = a_x1 + len(a_txt)
b_x2 = b_x1 + len(b_txt)
if a_x1 < b_x1 < a_x2 or a_x1 < b_x2 < a_x2 or \
b_x1 < a_x1 < b_x2 or b_x1 < a_x2 < b_x2:
return True
else:
return False
label_fldraw = [textobj, True]
labels.append(label_fldraw)
for o in labels:
if _overlaps(o[0], textobj):
o[1] = False
label_fldraw[1] = False
if self.options.disp_graph_labels:
labels_by_line = defaultdict(list) # y -> text labels
for pix_x, pix_y, txt, attr, row in self.labels:
if attr in self.hiddenAttrs:
continue
if row is not None:
pix_x -= len(txt)/2*2
char_y = int(pix_y/4)
char_x = int(pix_x/2)
o = (char_x, char_y, txt, attr, row)
_mark_overlap_text(labels_by_line[char_y], o)
for line in labels_by_line.values():
for o, fldraw in line:
if fldraw:
char_x, char_y, txt, attr, row = o
cattr = colors.get_color(attr)
clipdraw(scr, char_y, char_x, txt, cattr, dispwidth(txt))
cursorBBox = self.plotterCursorBox
for c in txt:
w = dispwidth(c)
# draw cursor if the cursor contains the midpoint of the character cell
if cursorBBox.contains(char_x*2+1, char_y*4+2):
char_attr = update_attr(cattr, colors.color_current_row)
clipdraw(scr, char_y, char_x, c, char_attr, w)
char_x += w
# - has a cursor, of arbitrary position and width/height (not restricted to current zoom)
class Canvas(Plotter):
'zoomable/scrollable virtual canvas with (x,y) coordinates in arbitrary units'
rowtype = 'plots'
leftMarginPixels = 10*2
rightMarginPixels = 4*2
topMarginPixels = 0*4
bottomMarginPixels = 1*4 # reserve bottom line for x axis
def __init__(self, *names, **kwargs):
self.left_margin = self.leftMarginPixels
super().__init__(*names, **kwargs)
self.canvasBox = None # bounding box of entire canvas, in canvas units
self.visibleBox = None # bounding box of visible canvas, in canvas units
self.cursorBox = None # bounding box of cursor, in canvas units
self.aspectRatio = 0.0
self.xzoomlevel = 1.0
self.yzoomlevel = 1.0
self.needsRefresh = False
self.polylines = [] # list of ([(canvas_x, canvas_y), ...], fgcolornum, row)
self.gridlabels = [] # list of (grid_x, grid_y, label, fgcolornum, row)
self.legends = OrderedDict() # txt: attr (visible legends only)
self.plotAttrs = {} # key: attr (all keys, for speed)
self.reset()
@property
def nRows(self):
return len(self.polylines)
def reset(self):
'clear everything in preparation for a fresh reload()'
self.polylines.clear()
self.left_margin = self.leftMarginPixels
self.legends.clear()
self.legendwidth = 0
self.plotAttrs.clear()
self.unusedAttrs = list(self.options.plot_colors.split())
def plotColor(self, k) -> str:
attr = self.plotAttrs.get(k, None)
if attr is None:
if self.unusedAttrs:
attr = self.unusedAttrs.pop(0)
legend = ' '.join(str(x) for x in k)
else:
lastlegend, attr = list(self.legends.items())[-1]
del self.legends[lastlegend]
legend = '[other]'
self.legendwidth = max(self.legendwidth, dispwidth(legend))
self.legends[legend] = attr
self.plotAttrs[k] = attr
return attr
def resetCanvasDimensions(self, windowHeight, windowWidth):
old_plotsize = None
realign_cursor = False
if hasattr(self, 'plotwidth') and hasattr(self, 'plotheight'):
old_plotsize = [self.plotheight, self.plotwidth]
if hasattr(self, 'cursorBox') and self.cursorBox and self.visibleBox:
# if the cursor is at the origin, realign it with the origin after the resize
if self.cursorBox.xmin == self.visibleBox.xmin and self.cursorBox.ymin == self.calcBottomCursorY():
realign_cursor = True
super().resetCanvasDimensions(windowHeight, windowWidth)
# if window is not big enough to contain a particular margin, pretend that margin is 0
pvbox_x = pvbox_y = 0
if self.plotwidth > self.left_margin:
pvbox_x = self.left_margin
if self.plotheight > self.topMarginPixels:
pvbox_y = self.topMarginPixels
if hasattr(self, 'legendwidth'):
# +4 = 1 empty space after the graph + 2 characters for the legend prefixes of "1:", "2:", etc +
# 1 character for the empty rightmost column
new_margin = max(self.rightMarginPixels, (self.legendwidth+4)*2)
pvbox_xmax = self.plotwidth-new_margin-1
# ensure the graph data takes up at least 3/4 of the width of the screen no matter how wide the legend gets
pvbox_xmax = max(pvbox_xmax, math.ceil(self.plotwidth * 3/4)//2*2 + 1)
else:
pvbox_xmax = self.plotwidth-self.rightMarginPixels-1
self.left_margin = min(self.left_margin, math.ceil(self.plotwidth * 1/3)//2*2)
# enforce a minimum plotview box size of 1x1
pvbox_xmax = max(pvbox_xmax, 1)
pvbox_ymax = max(self.plotheight-self.bottomMarginPixels-1, 1)
self.plotviewBox = BoundingBox(pvbox_x, pvbox_y, pvbox_xmax, pvbox_ymax)
if [self.plotheight, self.plotwidth] != old_plotsize:
if hasattr(self, 'cursorBox') and self.cursorBox:
self.setCursorSizeInPlotterPixels(2, 4)
if realign_cursor:
self.cursorBox.ymin = self.calcBottomCursorY()
@property
def statusLine(self):
return 'canvas %s visible %s cursor %s' % (self.canvasBox, self.visibleBox, self.cursorBox)
@property
def canvasMouse(self):
x = self.plotterMouse.x
y = self.plotterMouse.y
if not self.canvasBox: return None
p = Point(self.unscaleX(x), self.unscaleY(y))
return p
def setCursorSize(self, p):
'sets width based on diagonal corner p'
if not p: return
self.cursorBox = BoundingBox(self.cursorBox.xmin, self.cursorBox.ymin, p.x, p.y)
self.cursorBox.w = max(self.cursorBox.w, self.canvasCharWidth)
self.cursorBox.h = max(self.cursorBox.h, self.canvasCharHeight)
def setCursorSizeInPlotterPixels(self, w, h):
self.setCursorSize(Point(self.cursorBox.xmin + w/2 * self.canvasCharWidth,
self.cursorBox.ymin + h/4 * self.canvasCharHeight))
def formatX(self, v):
return str(v)
def formatY(self, v):
return str(v)
def commandCursor(sheet, execstr):
'Return (col, row) of cursor suitable for cmdlog replay of execstr.'
contains = lambda s, *substrs: any((a in s) for a in substrs)
colname, rowname = '', ''
if contains(execstr, 'plotterCursorBox'):
bb = sheet.cursorBox
colname = '%s %s' % (sheet.formatX(bb.xmin), sheet.formatX(bb.xmax))
rowname = '%s %s' % (sheet.formatY(bb.ymin), sheet.formatY(bb.ymax))
elif contains(execstr, 'plotterVisibleBox'):
bb = sheet.visibleBox
colname = '%s %s' % (sheet.formatX(bb.xmin), sheet.formatX(bb.xmax))
rowname = '%s %s' % (sheet.formatY(bb.ymin), sheet.formatY(bb.ymax))
return colname, rowname
@property
def canvasCharWidth(self):
'Width in canvas units of a single char in the terminal'
return self.visibleBox.w*2/self.plotviewBox.w
@property
def canvasCharHeight(self):
'Height in canvas units of a single char in the terminal'
return self.visibleBox.h*4/self.plotviewBox.h
@property
def plotterVisibleBox(self):
return BoundingBox(self.scaleX(self.visibleBox.xmin),
self.scaleY(self.visibleBox.ymin),
self.scaleX(self.visibleBox.xmax),
self.scaleY(self.visibleBox.ymax))
@property
def plotterCursorBox(self):
if self.cursorBox is None:
return Box(0,0,0,0)
return BoundingBox(self.scaleX(self.cursorBox.xmin),
self.scaleY(self.cursorBox.ymin),
self.scaleX(self.cursorBox.xmax),
self.scaleY(self.cursorBox.ymax))
def startCursor(self):
cm = self.canvasMouse
if cm:
self.cursorBox = Box(*cm.xy)
return True
else:
return None
def point(self, x, y, attr:"str|ColorAttr=''", row=None):
self.polylines.append(([(x, y)], attr, row))
def line(self, x1, y1, x2, y2, attr:"str|ColorAttr=''", row=None):
self.polylines.append(([(x1, y1), (x2, y2)], attr, row))
def polyline(self, vertexes, attr:"str|ColorAttr=''", row=None):
'adds lines for (x,y) vertexes of a polygon'
self.polylines.append((vertexes, attr, row))
def polygon(self, vertexes, attr:"str|ColorAttr=''", row=None):
'adds lines for (x,y) vertexes of a polygon'
self.polylines.append((vertexes + [vertexes[0]], attr, row))
def qcurve(self, vertexes, attr:"str|ColorAttr=''", row=None):
'Draw quadratic curve from vertexes[0] to vertexes[2] with control point at vertexes[1]'
if len(vertexes) != 3:
vd.fail('need exactly 3 points for qcurve (got %d)' % len(vertexes))
x1, y1 = vertexes[0]
x2, y2 = vertexes[1]
x3, y3 = vertexes[2]
for x, y in bezier(x1, y1, x2, y2, x3, y3):
self.point(x, y, attr, row)
def label(self, x, y, text, attr:"str|ColorAttr=''", row=None):
self.gridlabels.append((x, y, text, attr, row))
def fixPoint(self, plotterPoint, canvasPoint):
'adjust visibleBox.xymin so that canvasPoint is plotted at plotterPoint'
self.visibleBox.xmin = canvasPoint.x - self.canvasW(plotterPoint.x-self.plotviewBox.xmin)
self.visibleBox.ymin = canvasPoint.y - self.canvasH(plotterPoint.y-self.plotviewBox.ymin)
self.resetBounds()
def zoomTo(self, bbox):
'set visible area to bbox, maintaining aspectRatio if applicable'
self.fixPoint(self.plotviewBox.xymin, bbox.xymin)
self.xzoomlevel=bbox.w/self.canvasBox.w
self.yzoomlevel=bbox.h/self.canvasBox.h
self.resetBounds()
def incrZoom(self, incr):
self.xzoomlevel *= incr
self.yzoomlevel *= incr
self.resetBounds()
def resetBounds(self, refresh=True):
'create canvasBox and cursorBox if necessary, and set visibleBox w/h according to zoomlevels. then redisplay legends.'
if not self.canvasBox:
xmin, ymin, xmax, ymax = None, None, None, None
for vertexes, attr, row in self.polylines:
for x, y in vertexes:
if xmin is None or x < xmin: xmin = x
if ymin is None or y < ymin: ymin = y
if xmax is None or x > xmax: xmax = x
if ymax is None or y > ymax: ymax = y
xmin = xmin or 0
xmax = xmax or 0
ymin = ymin or 0
ymax = ymax or 0
if xmin == xmax:
xmax += 1
if ymin == ymax:
ymax += 1
self.canvasBox = BoundingBox(float(xmin), float(ymin), float(xmax), float(ymax))
w = self.calcVisibleBoxWidth()
h = self.calcVisibleBoxHeight()
if not self.visibleBox:
# initialize minx/miny, but w/h must be set first to center properly
self.visibleBox = Box(0, 0, w, h)
self.visibleBox.xmin = self.canvasBox.xmin + (self.canvasBox.w / 2) * (1 - self.xzoomlevel)
self.visibleBox.ymin = self.canvasBox.ymin + (self.canvasBox.h / 2) * (1 - self.yzoomlevel)
else:
self.visibleBox.w = w
self.visibleBox.h = h
if not self.cursorBox:
cb_xmin = self.visibleBox.xmin
cb_ymin = self.calcBottomCursorY()
self.cursorBox = Box(cb_xmin, cb_ymin, self.canvasCharWidth, self.canvasCharHeight)
self.plotlegends()
if refresh:
self.refresh()
def calcTopCursorY(self):
'ymin for the cursor that will align its top with the top edge of the graph'
# + (1/4*self.canvasCharHeight) shifts the cursor up by 1 plotter pixel.
# That shift makes the cursor contain the top data point.
# Otherwise, the top data point would have y == plotterCursorBox.ymax,
# which would not be inside plotterCursorBox. Shifting the cursor makes
# plotterCursorBox.ymax > y for that top point.
return self.visibleBox.ymax - self.cursorBox.h + (1/4*self.canvasCharHeight)
def calcBottomCursorY(self):
'ymin for the cursor that will align its bottom with the bottom edge of the graph'
return self.visibleBox.ymin
def plotlegends(self):
# display labels
for i, (legend, attr) in enumerate(self.legends.items()):
self.addCommand(str(i+1), f'toggle-{i+1}', f'hideAttr("{attr}", "{attr}" not in hiddenAttrs)', f'toggle display of "{legend}"')
if attr in self.hiddenAttrs:
attr = 'graph_hidden'
# add 2 characters to width to account for '1:' '2:' etc
self.plotlegend(i, '%s:%s'%(i+1,legend), attr, width=self.legendwidth+2)
def checkCursor(self):
'override Sheet.checkCursor'
if self.visibleBox and self.cursorBox:
if self.cursorBox.h < self.canvasCharHeight:
self.cursorBox.h = self.canvasCharHeight*3/4
if self.cursorBox.w < self.canvasCharWidth:
self.cursorBox.w = self.canvasCharWidth*3/4
return False
@property
def xScaler(self):
xratio = self.plotviewBox.w/(self.canvasBox.w*self.xzoomlevel)
if self.aspectRatio:
yratio = self.plotviewBox.h/(self.canvasBox.h*self.yzoomlevel)
return self.aspectRatio*min(xratio, yratio)
else:
return xratio
@property
def yScaler(self):
yratio = self.plotviewBox.h/(self.canvasBox.h*self.yzoomlevel)
if self.aspectRatio:
xratio = self.plotviewBox.w/(self.canvasBox.w*self.xzoomlevel)
return min(xratio, yratio)
else:
return yratio
def calcVisibleBoxWidth(self):
w = self.canvasBox.w * self.xzoomlevel
if self.aspectRatio:
h = self.canvasBox.h * self.yzoomlevel
xratio = self.plotviewBox.w / w
yratio = self.plotviewBox.h / h
if xratio <= yratio:
return w / self.aspectRatio
else:
return self.plotviewBox.w / (self.aspectRatio * yratio)
else:
return w
def calcVisibleBoxHeight(self):
h = self.canvasBox.h * self.yzoomlevel
if self.aspectRatio:
w = self.canvasBox.w * self.yzoomlevel
xratio = self.plotviewBox.w / w
yratio = self.plotviewBox.h / h
if xratio < yratio:
return self.plotviewBox.h / xratio
else:
return h
else:
return h
def scaleX(self, canvasX) -> int:
'returns a plotter x coordinate'
return self.plotviewBox.xmin+round((canvasX-self.visibleBox.xmin)*self.xScaler)
def scaleY(self, canvasY) -> int:
'returns a plotter y coordinate'
return self.plotviewBox.ymin+round((canvasY-self.visibleBox.ymin)*self.yScaler)
def unscaleX(self, plotterX):
'performs the inverse of scaleX, returns a canvas x coordinate'
return (plotterX-self.plotviewBox.xmin)/self.xScaler + self.visibleBox.xmin
def unscaleY(self, plotterY):
'performs the inverse of scaleY, returns a canvas y coordinate'
return (plotterY-self.plotviewBox.ymin)/self.yScaler + self.visibleBox.ymin
def canvasW(self, plotter_width):
'plotter X units to canvas units'
return plotter_width/self.xScaler
def canvasH(self, plotter_height):
'plotter Y units to canvas units'
return plotter_height/self.yScaler
def refresh(self):
'triggers render() on next draw()'
self.needsRefresh = True
def render(self, h, w):
'resets plotter, cancels previous render threads, spawns a new render'
self.needsRefresh = False
vd.cancelThread(*(t for t in self.currentThreads if t.name == 'plotAll_async'))
self.labels.clear()
self.resetCanvasDimensions(h, w)
self.resetBounds(refresh=False)
self.render_async()
@asyncthread
def render_async(self):
self.plot_elements()
def plot_elements(self, invert_y=False):
'plots points and lines and text onto the plotter'
self.resetBounds()
bb = self.visibleBox
xmin, ymin, xmax, ymax = bb.xmin, bb.ymin, bb.xmax, bb.ymax
xfactor, yfactor = self.xScaler, self.yScaler
plotxmin = self.plotviewBox.xmin
if invert_y:
plotymax = self.plotviewBox.ymax
else:
plotymin = self.plotviewBox.ymin
for vertexes, attr, row in Progress(self.polylines, 'rendering'):
if len(vertexes) == 1: # single point
x1, y1 = vertexes[0]
x1, y1 = float(x1), float(y1)
if xmin <= x1 <= xmax and ymin <= y1 <= ymax:
# equivalent to self.scaleX(x1) and self.scaleY(y1), inlined for speed
x = plotxmin+round((x1-xmin)*xfactor)
if invert_y:
y = plotymax-round((y1-ymin)*yfactor)
else:
y = plotymin+round((y1-ymin)*yfactor)
self.plotpixel(x, y, attr, row)
continue
prev_x, prev_y = vertexes[0]
for x, y in vertexes[1:]:
r = clipline(prev_x, prev_y, x, y, xmin, ymin, xmax, ymax)
if r:
x1, y1, x2, y2 = r
x1 = plotxmin+float(x1-xmin)*xfactor
x2 = plotxmin+float(x2-xmin)*xfactor
if invert_y:
y1 = plotymax-float(y1-ymin)*yfactor
y2 = plotymax-float(y2-ymin)*yfactor
else:
y1 = plotymin+float(y1-ymin)*yfactor
y2 = plotymin+float(y2-ymin)*yfactor
self.plotline(x1, y1, x2, y2, attr, row)
prev_x, prev_y = x, y
for x, y, text, attr, row in Progress(self.gridlabels, 'labeling'):
self.plotlabel(self.scaleX(x), self.scaleY(y), text, attr, row)
@asyncthread
def deleteSourceRows(self, rows):
rows = list(rows)
self.source.copyRows(rows)
self.source.deleteBy(lambda r,rows=rows: r in rows)
self.reload()
Plotter.addCommand('v', 'visibility', 'options.disp_graph_labels = not options.disp_graph_labels', 'toggle disp_graph_labels option')
Canvas.addCommand(None, 'go-left', 'if cursorBox: sheet.cursorBox.xmin -= cursorBox.w', 'move cursor left by its width')
Canvas.addCommand(None, 'go-right', 'if cursorBox: sheet.cursorBox.xmin += cursorBox.w', 'move cursor right by its width' )
Canvas.addCommand(None, 'go-up', 'if cursorBox: sheet.cursorBox.ymin -= cursorBox.h', 'move cursor up by its height')
Canvas.addCommand(None, 'go-down', 'if cursorBox: sheet.cursorBox.ymin += cursorBox.h', 'move cursor down by its height')
Canvas.addCommand(None, 'go-leftmost', 'if cursorBox: sheet.cursorBox.xmin = visibleBox.xmin', 'move cursor to left edge of visible canvas')
Canvas.addCommand(None, 'go-rightmost', 'if cursorBox: sheet.cursorBox.xmin = visibleBox.xmax-cursorBox.w+(1/2*canvasCharWidth)', 'move cursor to right edge of visible canvas')
Canvas.addCommand(None, 'go-top', 'if cursorBox: sheet.cursorBox.ymin = sheet.calcTopCursorY()', 'move cursor to top edge of visible canvas')
Canvas.addCommand(None, 'go-bottom', 'if cursorBox: sheet.cursorBox.ymin = sheet.calcBottomCursorY()', 'move cursor to bottom edge of visible canvas')
Canvas.addCommand(None, 'go-pagedown', 't=(visibleBox.ymax-visibleBox.ymin); sheet.cursorBox.ymin += t; sheet.visibleBox.ymin += t; refresh()', 'move cursor down to next visible page')
Canvas.addCommand(None, 'go-pageup', 't=(visibleBox.ymax-visibleBox.ymin); sheet.cursorBox.ymin -= t; sheet.visibleBox.ymin -= t; refresh()', 'move cursor up to previous visible page')
Canvas.addCommand('zh', 'go-left-small', 'sheet.cursorBox.xmin -= canvasCharWidth', 'move cursor left one character')
Canvas.addCommand('zl', 'go-right-small', 'sheet.cursorBox.xmin += canvasCharWidth', 'move cursor right one character')
Canvas.addCommand('zj', 'go-down-small', 'sheet.cursorBox.ymin += canvasCharHeight', 'move cursor down one character')
Canvas.addCommand('zk', 'go-up-small', 'sheet.cursorBox.ymin -= canvasCharHeight', 'move cursor up one character')
Canvas.addCommand('gH', 'resize-cursor-halfwide', 'sheet.cursorBox.w /= 2', 'halve cursor width')
Canvas.addCommand('gL', 'resize-cursor-doublewide', 'sheet.cursorBox.w *= 2', 'double cursor width')
Canvas.addCommand('gJ','resize-cursor-halfheight', 'sheet.cursorBox.h /= 2', 'halve cursor height')
Canvas.addCommand('gK', 'resize-cursor-doubleheight', 'sheet.cursorBox.h *= 2', 'double cursor height')
Canvas.addCommand('H', 'resize-cursor-thinner', 'sheet.cursorBox.w -= canvasCharWidth', 'decrease cursor width by one character')
Canvas.addCommand('L', 'resize-cursor-wider', 'sheet.cursorBox.w += canvasCharWidth', 'increase cursor width by one character')
Canvas.addCommand('J', 'resize-cursor-taller', 'sheet.cursorBox.h += canvasCharHeight', 'increase cursor height by one character')
Canvas.addCommand('K', 'resize-cursor-shorter', 'sheet.cursorBox.h -= canvasCharHeight', 'decrease cursor height by one character')
Canvas.addCommand('zz', 'zoom-cursor', 'zoomTo(cursorBox)', 'set visible bounds to cursor')
Canvas.addCommand('-', 'zoomout-cursor', 'tmp=cursorBox.center; incrZoom(options.disp_zoom_incr); fixPoint(plotviewBox.center, tmp)', 'zoom out from cursor center')
Canvas.addCommand('+', 'zoomin-cursor', 'tmp=cursorBox.center; incrZoom(1.0/options.disp_zoom_incr); fixPoint(plotviewBox.center, tmp)', 'zoom into cursor center')
Canvas.addCommand('_', 'zoom-all', 'sheet.canvasBox = None; sheet.visibleBox = None; sheet.xzoomlevel=sheet.yzoomlevel=1.0; resetBounds()', 'zoom to fit full extent')
Canvas.addCommand('z_', 'set-aspect', 'sheet.aspectRatio = float(input("aspect ratio=", value=aspectRatio)); refresh()', 'set aspect ratio')
# set cursor box with left click
Canvas.addCommand('BUTTON1_PRESSED', 'start-cursor', 'startCursor()', 'start cursor box with left mouse button press')
Canvas.addCommand('BUTTON1_RELEASED', 'end-cursor', 'cm=canvasMouse; setCursorSize(cm) if cm else None', 'end cursor box with left mouse button release')
Canvas.addCommand('BUTTON1_CLICKED', 'remake-cursor', 'startCursor(); cm=canvasMouse; setCursorSize(cm) if cm else None', 'end cursor box with left mouse button release')
Canvas.bindkey('BUTTON1_DOUBLE_CLICKED', 'remake-cursor')
Canvas.bindkey('BUTTON1_TRIPLE_CLICKED', 'remake-cursor')
Canvas.addCommand('BUTTON3_PRESSED', 'start-move', 'cm=canvasMouse; sheet.anchorPoint = cm if cm else None', 'mark grid point to move')
Canvas.addCommand('BUTTON3_RELEASED', 'end-move', 'fixPoint(plotterMouse, anchorPoint) if anchorPoint else None', 'mark canvas anchor point')
# A click does not actually move the canvas, but gives useful UI feedback. It helps users understand that they can do press-drag-release.
Canvas.addCommand('BUTTON3_CLICKED', 'move-canvas', '', 'move canvas (in place)')
Canvas.bindkey('BUTTON3_DOUBLE_CLICKED', 'move-canvas')
Canvas.bindkey('BUTTON3_TRIPLE_CLICKED', 'move-canvas')
Canvas.addCommand('ScrollUp', 'zoomin-mouse', 'cm=canvasMouse; incrZoom(1.0/options.disp_zoom_incr) if cm else fail("cannot zoom in on unplotted canvas"); fixPoint(plotterMouse, cm)', 'zoom in with scroll wheel')
Canvas.addCommand('ScrollDown', 'zoomout-mouse', 'cm=canvasMouse; incrZoom(options.disp_zoom_incr) if cm else fail("cannot zoom out on unplotted canvas"); fixPoint(plotterMouse, cm)', 'zoom out with scroll wheel')
Canvas.addCommand('s', 'select-cursor', 'source.select(list(rowsWithin(plotterCursorBox)))', 'select rows on source sheet contained within canvas cursor')
Canvas.addCommand('t', 'stoggle-cursor', 'source.toggle(list(rowsWithin(plotterCursorBox)))', 'toggle selection of rows on source sheet contained within canvas cursor')
Canvas.addCommand('u', 'unselect-cursor', 'source.unselect(list(rowsWithin(plotterCursorBox)))', 'unselect rows on source sheet contained within canvas cursor')
Canvas.addCommand(ENTER, 'dive-cursor', 'vs=copy(source); vs.rows=list(rowsWithin(plotterCursorBox)); vd.push(vs)', 'open sheet of source rows contained within canvas cursor')
Canvas.addCommand('d', 'delete-cursor', 'deleteSourceRows(rowsWithin(plotterCursorBox))', 'delete rows on source sheet contained within canvas cursor')
Canvas.addCommand('gs', 'select-visible', 'source.select(list(rowsWithin(plotterVisibleBox)))', 'select rows on source sheet visible on screen')
Canvas.addCommand('gt', 'stoggle-visible', 'source.toggle(list(rowsWithin(plotterVisibleBox)))', 'toggle selection of rows on source sheet visible on screen')
Canvas.addCommand('gu', 'unselect-visible', 'source.unselect(list(rowsWithin(plotterVisibleBox)))', 'unselect rows on source sheet visible on screen')
Canvas.addCommand('g'+ENTER, 'dive-visible', 'vs=copy(source); vs.rows=list(rowsWithin(plotterVisibleBox)); vd.push(vs)', 'open sheet of source rows visible on screen')
Canvas.addCommand('gd', 'delete-visible', 'deleteSourceRows(rowsWithin(plotterVisibleBox))', 'delete rows on source sheet visible on screen')
vd.addGlobals({
'Canvas': Canvas,
'Plotter': Plotter,
'BoundingBox': BoundingBox,
'Box': Box,
'Point': Point,
})
vd.addMenuItems('''
Plot > Resize cursor > height > double > resize-cursor-doubleheight
Plot > Resize cursor > height > half > resize-cursor-halfheight
Plot > Resize cursor > height > shorter > resize-cursor-shorter
Plot > Resize cursor > height > taller > resize-cursor-taller
Plot > Resize cursor > width > double > resize-cursor-doublewide
Plot > Resize cursor > width > half > resize-cursor-halfwide
Plot > Resize cursor > width > thinner > resize-cursor-thinner
Plot > Resize cursor > width > wider > resize-cursor-wider
Plot > Resize graph > X axis > resize-x-input
Plot > Resize graph > Y axis > resize-y-input
Plot > Resize graph > aspect ratio > set-aspect
Plot > Zoom > out > zoomout-cursor
Plot > Zoom > in > zoomin-cursor
Plot > Zoom > cursor > zoom-all
Plot > Dive into cursor > dive-cursor
Plot > Delete > under cursor > delete-cursor
''')
visidata-3.1.1/visidata/canvas_text.py 0000664 0000000 0000000 00000017312 14770045464 0020034 0 ustar 00root root 0000000 0000000 from visidata import vd, BaseSheet, ENTER, colors, dispwidth
import curses
def boundingBox(rows):
'Return (xmin, ymin, xmax, ymax) of rows.'
xmin, ymin, xmax, ymax = 9999, 9999, 0, 0
for r in rows:
if r.x is not None:
xmin = min(xmin, r.x)
xmax = max(xmax, r.x + (r.w or dispwidth(r.text or '')))
if r.y is not None:
ymin = min(ymin, r.y)
ymax = max(ymax, r.y + (r.h or 0))
return xmin, ymin, xmax, ymax
class CharBox:
def __init__(self, scr=None, x1=0, y1=0, w=None, h=None):
scrh, scrw = scr.getmaxyx() if scr else (25, 80)
self.scr = scr
self.x1 = x1
self.y1 = y1
self.w = scrw if w is None else w
self.h = scrh if h is None else h
self.normalize()
def __str__(self):
return f'({self.x1}+{self.w},{self.y1}+{self.h})'
def normalize(self):
'Make sure w and h are non-negative, swapping coordinates as needed.'
if self.w < 0:
self.x1 += self.w
self.w = -self.w
if self.h < 0:
self.y1 += self.h
self.h = -self.h
@property
def x2(self):
return self.x1+self.w+1
@x2.setter
def x2(self, v):
self.w = v-self.x1-1
self.normalize()
@property
def y2(self):
return self.y1+self.h+1
@y2.setter
def y2(self, v):
self.h = v-self.y1-1
self.normalize()
def contains(self, b):
'Return True if this box contains any part of the given x,y,w,h.'
xA = max(self.x1, b.x1) # left
xB = min(self.x2, b.x2) # right
yA = max(self.y1, b.y1) # top
yB = min(self.y2, b.y2) # bottom
return xA < xB-1 and yA < yB-1 # xA+.5 < xB-.5 and yA+.5 < yB-.5
class TextCanvas(BaseSheet):
@property
def rows(self):
return self.source.rows
@rows.setter
def rows(self, v):
pass
def reload(self):
pass
def draw(self, scr):
for i in range(self.cursorBox.h):
for j in range(self.cursorBox.w):
scr.addstr(self.cursorBox.y1+i, self.cursorBox.x1+j, ' ', colors.color_current_row)
def commandCursor(self, execstr):
if 'cursor' in execstr:
return '%s %s' % (self.cursorBox.x1, self.cursorBox.x2), '%s %s' % (self.cursorBox.y1, self.cursorBox.y2)
return '', ''
def checkCursor(self):
self.cursorBox.x1 = min(self.windowWidth-2, max(0, self.cursorBox.x1))
self.cursorBox.y1 = min(self.windowHeight-2, max(0, self.cursorBox.y1))
def iterbox(self, box, n=None):
'Return *n* top elements from each cell within the given *box*.'
ret = list()
for r in self.source.rows:
if r.pos.x is None or r.pos.y is None: continue
if box.contains(CharBox(None, r.pos.x, r.pos.y, 1, 1)):
ret.append(r)
return ret[:-n] if n else ret
def itercursor(self, n=None):
return self.iterbox(self.cursorBox, n=n)
@property
def cursorRows(self):
return list(self.iterbox(self.cursorBox))
def slide(self, rows, dx, dy):
maxX, maxY = self.windowWidth, self.windowHeight
x1, y1, x2, y2 = boundingBox(rows)
dx = -x1 if x1+dx < 0 else (maxX-x2-1 if x2+dx > maxX-1 else dx)
dy = -y1 if y1+dy < 0 else (maxY-y2-1 if y2+dy > maxY-1 else dy)
xcol = self.source.column('x')
ycol = self.source.column('y')
for r in rows:
oldx = xcol.getValue(r)
oldy = ycol.getValue(r)
if oldx is not None:
xcol.setValue(r, oldx+dx)
if oldy is not None:
ycol.setValue(r, oldy+dy)
TextCanvas.addCommand('', 'go-down', 'cursorBox.y1 += 1')
TextCanvas.addCommand('', 'go-up', 'cursorBox.y1 -= 1')
TextCanvas.addCommand('', 'go-left', 'cursorBox.x1 -= 1')
TextCanvas.addCommand('', 'go-right', 'cursorBox.x1 += 1')
TextCanvas.addCommand('kRIT5', 'resize-cursor-wider', 'cursorBox.w += 1', 'increase cursor width by one character')
TextCanvas.addCommand('kLFT5', 'resize-cursor-thinner', 'cursorBox.w -= 1', 'decrease cursor width by one character')
TextCanvas.addCommand('kUP5', 'resize-cursor-shorter', 'cursorBox.h -= 1', 'decrease cursor height by one character')
TextCanvas.addCommand('kDN5', 'resize-cursor-taller', 'cursorBox.h += 1', 'increase cursor height by one character')
TextCanvas.addCommand('gzKEY_LEFT', 'resize-cursor-min-width', 'cursorBox.w = 1')
TextCanvas.addCommand('gzKEY_UP', 'resize-cursor-min-height', 'cursorBox.h = 1')
TextCanvas.addCommand('z_', 'resize-cursor-min', 'cursorBox.h = cursorBox.w = 1')
TextCanvas.addCommand('g_', 'resize-cursor-max', 'cursorBox.x1=cursorBox.y1=0; cursorBox.h=maxY+1; cursorBox.w=maxX+1')
TextCanvas.bindkey('zKEY_RIGHT', 'resize-cursor-wider')
TextCanvas.bindkey('zKEY_LEFT', 'resize-cursor-thinner')
TextCanvas.bindkey('zKEY_UP', 'resize-cursor-shorter')
TextCanvas.bindkey('zKEY_DOWN', 'resize-cursor-taller')
TextCanvas.addCommand('BUTTON1_PRESSED', 'move-cursor', 'sheet.cursorBox = CharBox(None, mouseX, mouseY, 1, 1)', 'start cursor box with left mouse button press')
TextCanvas.addCommand('BUTTON1_RELEASED', 'end-cursor', 'cursorBox.x2=mouseX+2; cursorBox.y2=mouseY+2; cursorBox.normalize()', 'end cursor box with left mouse button release')
TextCanvas.addCommand('s', 'select-cursor', 'source.select(cursorRows)')
TextCanvas.addCommand('t', 'toggle-cursor', 'source.toggle(cursorRows)')
TextCanvas.addCommand('u', 'unselect-cursor', 'source.unselect(cursorRows)')
TextCanvas.addCommand('gs', 'select-all', 'source.select(source.rows)')
TextCanvas.addCommand('gt', 'toggle-all', 'source.toggle(source.rows)')
TextCanvas.addCommand('gu', 'unselect-all', 'source.clearSelected()')
TextCanvas.addCommand('zs', 'select-top-cursor', 'source.select(list(itercursor(n=1)))')
TextCanvas.addCommand('zt', 'toggle-top-cursor', 'source.toggle(list(itercursor(n=1)))')
TextCanvas.addCommand('zu', 'unselect-top-cursor', 'source.unselect(list(itercursor(n=1)))')
TextCanvas.addCommand('d', 'delete-cursor', 'source.deleteBy(lambda r,rows=cursorRows: r in rows)', 'delete first item under cursor')
TextCanvas.addCommand('gd', 'delete-selected', 'source.deleteSelected()', 'delete selected rows on source sheet')
TextCanvas.addCommand(ENTER, 'dive-cursor', 'vs=copy(source); vs.rows=cursorRows; vs.source=sheet; vd.push(vs)', 'dive into source rows under cursor')
TextCanvas.addCommand('g'+ENTER, 'dive-selected', 'vd.push(type(source)(source=sheet, rows=source.selectedRows))', 'dive into selected source rows')
TextCanvas.addCommand('H', 'slide-left-obj', 'slide(source.selectedRows, -1, 0)', 'slide selected objects left one character')
TextCanvas.addCommand('J', 'slide-down-obj', 'slide(source.selectedRows, 0, +1)', 'slide selected objects down one character')
TextCanvas.addCommand('K', 'slide-up-obj', 'slide(source.selectedRows, 0, -1)', 'slide selected objects up one character')
TextCanvas.addCommand('L', 'slide-right-obj', 'slide(source.selectedRows, +1, 0)', 'slide selected objects right one character')
TextCanvas.addCommand('gH', 'slide-leftmost-obj', 'slide(source.selectedRows, -maxX, 0)', 'slide selected objects all the way left')
TextCanvas.addCommand('gJ', 'slide-bottom-obj', 'slide(source.selectedRows, 0, +maxY)', 'slide all selected objects all the way bottom')
TextCanvas.addCommand('gK', 'slide-top-obj', 'slide(source.selectedRows, 0, -maxY)', 'slide all selected objects all the way top')
TextCanvas.addCommand('gL', 'slide-rightmost-obj', 'slide(source.selectedRows, +maxX, 0)', 'slide all selected objects all the way right')
TextCanvas.init('cursorBox', lambda: CharBox(None, 0,0,1,1))
vd.addGlobals({
'CharBox': CharBox,
'boundingBox': boundingBox,
'TextCanvas': TextCanvas,
})
visidata-3.1.1/visidata/choose.py 0000664 0000000 0000000 00000005606 14770045464 0017000 0 ustar 00root root 0000000 0000000 from copy import copy
from visidata import vd, options, VisiData, ListOfDictSheet, ENTER, CompleteKey, ReturnValue
vd.option('fancy_chooser', False, 'a nicer selection interface for aggregators and jointype')
@VisiData.api
def chooseOne(vd, choices, type=''):
'Return one user-selected key from *choices*.'
return vd.choose(choices, 1, type=type)
@VisiData.api
def choose(vd, choices, n=None, type=''):
'Return a list of 1 to *n* "key" from elements of *choices* (see chooseMany).'
ret = vd.chooseMany(choices, type=type) or vd.fail('no choice made')
if n and len(ret) > n:
vd.fail('can only choose %s' % n)
return ret[0] if n==1 else ret
class ChoiceSheet(ListOfDictSheet):
rowtype = 'choices' # rowdef = dict
precious = False
def makeChoice(self, rows):
# selected rows by their keys, separated by spaces
raise ReturnValue([r['key'] for r in rows])
@VisiData.api
def chooseFancy(vd, choices):
vs = ChoiceSheet('choices', source=copy(choices))
options.set('disp_splitwin_pct', -75, vs)
vs.reload()
vs.setKeys([vs.column('key')])
vd.push(vs)
chosen = vd.runresult()
vd.remove(vs)
return chosen
@VisiData.api
def chooseMany(vd, choices, type=''):
'''Return a list of 1 or more keys from *choices*, which is a list of
dicts. Each element dict must have a unique "key", which must be typed
directly by the user in non-fancy mode (therefore no spaces). All other
items in the dicts are also shown in fancy chooser mode. Use previous
choices from the replay input if available. Add chosen keys
(space-separated) to the cmdlog as input for the current command.'''
if vd.cmdlog:
v = vd.getLastArgs()
if v is not None:
# check that each key in v is in choices?
vd.setLastArgs(v)
return v.split()
if options.fancy_chooser:
chosen = vd.chooseFancy(choices)
else:
chosen = []
choice_keys = [c['key'] for c in choices]
prompt='choose any of %d options (Ctrl+X for menu)' % len(choice_keys)
try:
def throw_fancy(v, i):
ret = vd.chooseFancy(choices)
if ret:
raise ReturnValue(ret)
return v, i
chosenstr = vd.input(prompt+': ', completer=CompleteKey(choice_keys), bindings={'^X': throw_fancy}, type=type)
for c in chosenstr.split():
if c in choice_keys:
chosen.append(c)
else:
vd.warning('invalid choice "%s"' % c)
except ReturnValue as e:
chosen = e.args[0]
if vd.cmdlog:
vd.setLastArgs(' '.join(chosen))
return chosen
ChoiceSheet.addCommand(ENTER, 'choose-rows', 'makeChoice([cursorRow])')
ChoiceSheet.addCommand('g'+ENTER, 'choose-rows-selected', 'makeChoice(onlySelectedRows)')
visidata-3.1.1/visidata/clean_names.py 0000664 0000000 0000000 00000001440 14770045464 0017755 0 ustar 00root root 0000000 0000000 import re
from visidata import vd, VisiData, Sheet
vd.option('clean_names', False, 'clean column/sheet names to be valid Python identifiers', replay=True)
@VisiData.global_api
def cleanName(vd, s):
#[Nas Banov] https://stackoverflow.com/a/3305731
# return re.sub(r'\W|^(?=\d)', '_', str(s)).strip('_')
s = re.sub(r'[^\w\d_]', '_', s) # replace non-alphanum chars with _
s = re.sub(r'_+', '_', s) # replace runs of _ with a single _
s = s.strip('_')
return s
@Sheet.api
def maybeClean(sheet, s):
if sheet.options.clean_names:
s = vd.cleanName(s)
return s
Sheet.addCommand('', 'clean-names', '''
options.clean_names = True;
for c in visibleCols:
c.name = cleanName(c.name)
''', 'set options.clean_names on sheet and clean visible column names')
visidata-3.1.1/visidata/clipboard.py 0000664 0000000 0000000 00000023331 14770045464 0017452 0 ustar 00root root 0000000 0000000 from copy import copy, deepcopy
import shutil
import subprocess
import io
import sys
import tempfile
import functools
import os
import itertools
from visidata import VisiData, vd, asyncthread, SettableColumn
from visidata import Sheet, Path, Column
if sys.platform == 'win32':
syscopy_cmd_default = 'clip.exe'
syspaste_cmd_default = 'powershell -command Get-Clipboard'
elif sys.platform == 'darwin':
syscopy_cmd_default = 'pbcopy w'
syspaste_cmd_default = 'pbpaste'
else:
if 'WAYLAND_DISPLAY' in os.environ:
syscopy_cmd_default = 'wl-copy'
syspaste_cmd_default = 'wl-paste'
else:
syscopy_cmd_default = 'xclip -selection clipboard -filter' # xsel --clipboard --input
syspaste_cmd_default = 'xclip -selection clipboard -o' # xsel --clipboard
vd.option('clipboard_copy_cmd', syscopy_cmd_default, 'command to copy stdin to system clipboard', sheettype=None)
vd.option('clipboard_paste_cmd', syspaste_cmd_default, 'command to send contents of system clipboard to stdout', sheettype=None)
@Sheet.api
def copyRows(sheet, rows):
vd.memory.cliprows = rows
vd.memory.clipcols = list(sheet.visibleCols)
if not rows:
vd.warning('no %s selected; clipboard emptied' % sheet.rowtype)
else:
vd.status('copied %d %s to clipboard' % (len(rows), sheet.rowtype))
@Sheet.api
def copyCells(sheet, col, rows):
vd.memory.clipcells = [col.getTypedValue(r) for r in rows]
if not rows:
vd.warning('no %s selected; clipboard emptied' % sheet.rowtype)
return
vd.status('copied %d %s.%s to clipboard' % (len(rows), sheet.rowtype, col.name))
@Sheet.api
def syscopyValue(sheet, val):
# pipe val to stdin of clipboard command
p = subprocess.run(
sheet.options.clipboard_copy_cmd.split(),
input=val,
encoding='utf-8',
stdout=subprocess.DEVNULL)
vd.status('copied value to system clipboard')
@Sheet.api
def setColClipboard(sheet):
if not vd.memory.clipcells:
vd.warning("nothing to paste from clipcells")
return
sheet.cursorCol.setValuesTyped(sheet.onlySelectedRows, *vd.memory.clipcells)
@Sheet.api
def syscopyCells(sheet, cols, rows, filetype=None):
filetype = filetype or vd.input("copy %d %s as filetype: " % (len(rows), sheet.rowtype), value=sheet.options.save_filetype or 'tsv')
sheet.syscopyCells_async(cols, rows, filetype)
@Sheet.api
@asyncthread
def syscopyCells_async(sheet, cols, rows, filetype):
vs = copy(sheet)
vs.rows = rows or vd.fail('no %s selected' % sheet.rowtype)
vs.columns = cols
vd.status(f'copying {vs.nRows} {vs.rowtype} to system clipboard as {filetype}')
with io.StringIO() as buf:
with tempfile.NamedTemporaryFile() as temp:
temp.close() #2118
vd.sync(vd.saveSheets(Path(f'{temp.name}.{filetype}', fptext=buf), vs, confirm_overwrite=False))
subprocess.run(
sheet.options.clipboard_copy_cmd.split(),
input=buf.getvalue(),
encoding='utf-8',
stdout=subprocess.DEVNULL)
@VisiData.api
def sysclipValue(vd):
cmd = vd.options.clipboard_paste_cmd
return subprocess.check_output(vd.options.clipboard_paste_cmd.split()).decode('utf-8')
@VisiData.api
@asyncthread
def pasteFromClipboard(vd, cols, rows):
text = vd.getLastArgs() or vd.sysclipValue().strip() or vd.fail('nothing to paste from system clipboard')
vd.addUndoSetValues(cols, rows)
lines = text.split('\n')
if not lines:
vd.warning('nothing to paste from system clipboard')
return
vs = cols[0].sheet
newrows = [vs.newRow() for i in range(len(lines)-len(rows))]
if newrows:
rows.extend(newrows)
vs.addRows(newrows)
for line, r in zip(lines, rows):
for v, c in zip(line.split('\t'), cols):
c.setValue(r, v)
@Sheet.api
def delete_row(sheet, rowidx):
if not sheet.rows:
vd.fail("no row to delete")
if not sheet.defer:
oldrow = sheet.rows.pop(rowidx)
vd.addUndo(sheet.rows.insert, rowidx, oldrow)
# clear the deleted row from selected rows
if sheet.isSelected(oldrow):
sheet.addUndoSelection()
sheet.unselectRow(oldrow)
else:
oldrow = sheet.rows[rowidx]
sheet.rowDeleted(oldrow)
sheet.setModified()
return oldrow
@Sheet.api
@asyncthread
def paste_after(sheet, rowidx):
'Paste rows from *vd.cliprows* at *rowidx*.'
if not vd.memory.cliprows: #1793
vd.warning('nothing to paste from cliprows')
return
for col in vd.memory.clipcols[sheet.nVisibleCols:]:
newcol = SettableColumn()
newcol.__setstate__(col.__getstate__())
sheet.addColumn(newcol)
addedRows = []
for extrow in vd.memory.cliprows:
if isinstance(extrow, Column):
newrow = copy(extrow)
else:
newrow = sheet.newRow()
for col, extcol in zip(sheet.visibleCols, vd.memory.clipcols):
col.setValue(newrow, extcol.getTypedValue(extrow))
addedRows.append(newrow)
sheet.addRows(addedRows, index=rowidx)
Sheet.addCommand('y', 'copy-row', 'copyRows([cursorRow])', 'yank (copy) current row to clipboard')
Sheet.addCommand('p', 'paste-after', 'paste_after(cursorRowIndex)', 'paste clipboard rows after current row')
Sheet.addCommand('P', 'paste-before', 'paste_after(cursorRowIndex-1)', 'paste clipboard rows before current row')
Sheet.addCommand('gy', 'copy-selected', 'copyRows(onlySelectedRows)', 'yank (copy) selected rows to clipboard')
Sheet.addCommand('zy', 'copy-cell', 'copyCells(cursorCol, [cursorRow]); vd.memoValue("clipval", cursorTypedValue, cursorDisplay)', 'yank (copy) current cell to clipboard')
Sheet.addCommand('zp', 'paste-cell', 'cursorCol.setValuesTyped([cursorRow], vd.memory.clipval) if vd.memory.clipval else vd.warning("nothing to paste from clipval")', 'set contents of current cell to last clipboard value')
Sheet.addCommand('d', 'delete-row', 'delete_row(cursorRowIndex); defer and cursorDown(1)', 'delete current row')
Sheet.addCommand('gd', 'delete-selected', 'deleteSelected()', 'delete selected rows')
Sheet.addCommand('zd', 'delete-cell', 'cursorCol.setValues([cursorRow], options.null_value)', 'delete current cell (set to None)')
Sheet.addCommand('gzd', 'delete-cells', 'cursorCol.setValues(onlySelectedRows, options.null_value)', 'delete contents of current column for selected rows (set to None)')
Sheet.bindkey('BUTTON2_PRESSED', 'go-mouse')
Sheet.addCommand('BUTTON2_RELEASED', 'syspaste-cells', 'pasteFromClipboard(visibleCols[cursorVisibleColIndex:], rows[cursorRowIndex:])', 'paste from system clipboard to region starting at cursor')
Sheet.bindkey('BUTTON2_CLICKED', 'go-mouse')
Sheet.bindkey('zP', 'syspaste-cells')
Sheet.addCommand('gzP', 'syspaste-cells-selected', 'pasteFromClipboard(visibleCols[cursorVisibleColIndex:], someSelectedRows)', 'paste from system clipboard to selected cells')
Sheet.addCommand('gzy', 'copy-cells', 'copyCells(cursorCol, onlySelectedRows)', 'yank (copy) contents of current column for selected rows to clipboard')
Sheet.addCommand('gzp', 'setcol-clipboard', 'setColClipboard()', 'set cells of current column for selected rows to last clipboard value')
Sheet.addCommand('Y', 'syscopy-row', 'syscopyCells(visibleCols, [cursorRow])', 'yank (copy) current row to system clipboard (using options.clipboard_copy_cmd)')
Sheet.addCommand('gY', 'syscopy-selected', 'syscopyCells(visibleCols, onlySelectedRows)', 'yank (copy) selected rows to system clipboard (using options.clipboard_copy_cmd)')
Sheet.addCommand('zY', 'syscopy-cell', 'syscopyValue(cursorDisplay)', 'yank (copy) current cell to system clipboard (using options.clipboard_copy_cmd)')
Sheet.addCommand('gzY', 'syscopy-cells', 'syscopyCells([cursorCol], onlySelectedRows, filetype="txt")', 'yank (copy) contents of current column from selected rows to system clipboard (using options.clipboard_copy_cmd')
Sheet.addCommand('x', 'cut-row', 'copyRows([sheet.delete_row(cursorRowIndex)]); defer and cursorDown(1)', 'delete (cut) current row and move it to clipboard')
Sheet.addCommand('gx', 'cut-selected', 'copyRows(onlySelectedRows); deleteSelected()', 'delete (cut) selected rows and move them to clipboard')
Sheet.addCommand('zx', 'cut-cell', 'copyCells(cursorCol, [cursorRow]); cursorCol.setValues([cursorRow], None)', 'delete (cut) current cell and move it to clipboard')
Sheet.addCommand('gzx', 'cut-cells', 'copyCells(cursorCol, onlySelectedRows); cursorCol.setValues(onlySelectedRows, None)', 'delete (cut) contents of current column for selected rows and move them to clipboard')
Sheet.bindkey('KEY_DC', 'delete-cell'),
Sheet.bindkey('gKEY_DC', 'delete-cells'),
vd.addMenuItems('''
Edit > Delete > current row > delete-row
Edit > Delete > current cell > delete-cell
Edit > Delete > selected rows > delete-selected
Edit > Delete > selected cells > delete-cells
Edit > Copy > current cell > copy-cell
Edit > Copy > current row > copy-row
Edit > Copy > selected cells > copy-cells
Edit > Copy > selected rows > copy-selected
Edit > Copy > to system clipboard > current cell > syscopy-cell
Edit > Copy > to system clipboard > current row > syscopy-row
Edit > Copy > to system clipboard > selected cells > syscopy-cells
Edit > Copy > to system clipboard > selected rows > syscopy-selected
Edit > Cut > current row > cut-row
Edit > Cut > selected cells > cut-selected
Edit > Cut > current cell > cut-cell
Edit > Paste > row after > paste-after
Edit > Paste > row before > paste-before
Edit > Paste > into selected cells > setcol-clipboard
Edit > Paste > into current cell > paste-cell
Edit > Paste > from system clipboard > cells at cursor > syspaste-cells
Edit > Paste > from system clipboard > selected cells > syspaste-cells-selected
''')
visidata-3.1.1/visidata/cliptext.py 0000664 0000000 0000000 00000026254 14770045464 0017356 0 ustar 00root root 0000000 0000000 import unicodedata
import sys
import re
import functools
import textwrap
from visidata import options, drawcache, vd, update_attr, colors, ColorAttr
disp_column_fill = ' '
internal_markup_re = r'(\[[:/][^\]]*?\])' # [:whatever until the closing bracket] or [/whatever] or [:]
### Curses helpers
# ZERO_WIDTH_CF is from wcwidth:
# NOTE: created by hand, there isn't anything identifiable other than
# general Cf category code to identify these, and some characters in Cf
# category code are of non-zero width.
# Also includes some Cc, Mn, Zl, and Zp characters
ZERO_WIDTH_CF = set(map(chr, [
0, # Null (Cc)
0x034F, # Combining grapheme joiner (Mn)
0x200B, # Zero width space
0x200C, # Zero width non-joiner
0x200D, # Zero width joiner
0x200E, # Left-to-right mark
0x200F, # Right-to-left mark
0x2028, # Line separator (Zl)
0x2029, # Paragraph separator (Zp)
0x202A, # Left-to-right embedding
0x202B, # Right-to-left embedding
0x202C, # Pop directional formatting
0x202D, # Left-to-right override
0x202E, # Right-to-left override
0x2060, # Word joiner
0x2061, # Function application
0x2062, # Invisible times
0x2063, # Invisible separator
]))
def wcwidth(cc, ambig=1):
if cc in ZERO_WIDTH_CF:
return 1
eaw = unicodedata.east_asian_width(cc)
if eaw in 'AN': # ambiguous or neutral
if unicodedata.category(cc) == 'Mn':
return 1
else:
return ambig
elif eaw in 'WF': # wide/full
return 2
elif not unicodedata.combining(cc):
return 1
return 0
def is_vdcode(s:str) -> bool:
return (s.startswith('[:') and s.endswith(']')) or \
(s.startswith('[/') and s.endswith(']'))
def iterchunks(s, literal=False):
attrstack = [dict(link='', cattr=ColorAttr())]
legitopens = 0
chunks = re.split(internal_markup_re, s)
for chunk in chunks:
if not chunk:
continue
if not literal and is_vdcode(chunk):
cattr = attrstack[-1]['cattr']
link = attrstack[-1]['link']
if chunk.startswith('[:onclick '):
attrstack.append(dict(link=chunk[2:-1], cattr=cattr.update(colors.clickable)))
continue
elif chunk == '[:]': # clear stack, keep origattr
if len(attrstack) > 1:
del attrstack[1:]
continue
elif chunk.startswith('[/'): # pop last attr off stack
if len(attrstack) > 1:
attrstack.pop()
continue # don't display trailing [/foo] ever
else: # push updated color on stack
newcolor = colors.get_color(chunk[2:-1])
if newcolor:
cattr = update_attr(cattr, newcolor, len(attrstack))
attrstack.append(dict(link=link, cattr=cattr))
continue
yield attrstack[-1], chunk
@functools.lru_cache(maxsize=100000)
def dispwidth(ss, maxwidth=None, literal=False):
'Return display width of string, according to unicodedata width and options.disp_ambig_width.'
disp_ambig_width = options.disp_ambig_width
w = 0
for _, s in iterchunks(ss, literal=literal):
for cc in s:
if cc:
w += wcwidth(cc, disp_ambig_width)
if maxwidth and w > maxwidth:
return maxwidth
return w
@functools.lru_cache(maxsize=100000)
def _dispch(c, oddspacech=None, combch=None, modch=None):
ccat = unicodedata.category(c)
if ccat in ['Mn', 'Sk', 'Lm']:
if unicodedata.name(c).startswith('MODIFIER'):
return modch, 1
elif c != ' ' and ccat in ('Cc', 'Zs', 'Zl', 'Cs'): # control char, space, line sep, surrogate
return oddspacech, 1
elif c in ZERO_WIDTH_CF:
return combch, 1
return c, dispwidth(c, literal=True)
def iterchars(x):
if isinstance(x, dict):
yield from '{%d}' % len(x)
for k, v in x.items():
yield ' '
yield from iterchars(k)
yield '='
yield from iterchars(v)
elif isinstance(x, (list, tuple)):
yield from '[%d] ' % len(x)
for i, v in enumerate(x):
if i != 0:
yield from '; '
yield from iterchars(v)
else:
yield from str(x)
@functools.lru_cache(maxsize=100000)
def _clipstr(s, dispw, trunch='', oddspacech='', combch='', modch=''):
'''Return clipped string and width in terminal display characters.
Note: width may differ from len(s) if East Asian chars are 'fullwidth'.'''
if not s:
return '', 0
if dispw == 1:
return s[0], 1
w = 0
ret = ''
trunchlen = dispwidth(trunch)
for c in s:
newc, chlen = _dispch(c, oddspacech=oddspacech, combch=combch, modch=modch)
if not newc:
newc = c
chlen = dispwidth(c)
if dispw and w+chlen > dispw:
if trunchlen and dispw > trunchlen:
lastchlen = _dispch(ret[-1])[1]
if w+trunchlen > dispw:
ret = ret[:-1]
w -= lastchlen
ret += trunch # replace final char with ellipsis
w += trunchlen
break
w += chlen
ret += newc
return ret, w
@drawcache
def clipstr(s, dispw, truncator=None, oddspace=None):
if options.visibility:
return _clipstr(s, dispw,
trunch=options.disp_truncator if truncator is None else truncator,
oddspacech=options.disp_oddspace if oddspace is None else oddspace,
modch='\u25e6',
combch='\u25cc')
else:
return _clipstr(s, dispw,
trunch=options.disp_truncator if truncator is None else truncator,
oddspacech=options.disp_oddspace if oddspace is None else oddspace,
modch='',
combch='')
def clipdraw(scr, y, x, s, attr, w=None, clear=True, literal=False, **kwargs):
'''Draw `s` at (y,x)-(y,x+w) with curses `attr`, clipping with ellipsis char.
If `clear`, clear whole editing area before displaying.
If `literal`, do not interpret internal color code markup.
Return width drawn (max of w).
'''
if not literal:
chunks = iterchunks(s, literal=literal)
else:
chunks = [(dict(link='', cattr=ColorAttr()), s)]
x = max(0, x)
y = max(0, y)
assert x >= 0, x
assert y >= 0, y
return clipdraw_chunks(scr, y, x, chunks, attr, w=w, clear=clear, **kwargs)
def clipdraw_chunks(scr, y, x, chunks, cattr:ColorAttr=ColorAttr(), w=None, clear=True, literal=False, **kwargs):
'''Draw `chunks` (sequence of (color:str, text:str) as from iterchunks) at (y,x)-(y,x+w) with curses `attr`, clipping with ellipsis char.
If `clear`, clear whole editing area before displaying.
Return width drawn (max of w).
'''
if scr:
windowHeight, windowWidth = scr.getmaxyx()
else:
windowHeight, windowWidth = 25, 80
totaldispw = 0
assert isinstance(cattr, ColorAttr), cattr
origattr = cattr
origw = w
clipped = ''
link = ''
if w and clear:
actualw = min(w, windowWidth-x-1)
if scr:
scr.addstr(y, x, disp_column_fill*actualw, cattr.attr) # clear whole area before displaying
try:
for colorstate, chunk in chunks:
if colorstate:
if isinstance(colorstate, str):
cattr = cattr.update(colors.get_color(colorstate), 100)
else:
cattr = origattr.update(colorstate['cattr'], 100)
link = colorstate['link']
if not chunk:
continue
if origw is None:
chunkw = dispwidth(chunk, maxwidth=windowWidth-totaldispw)
else:
chunkw = origw-totaldispw
chunkw = min(chunkw, windowWidth-x-1)
if chunkw <= 0: # no room anyway
return totaldispw
if not scr:
return totaldispw
# convert to string just before drawing
clipped, dispw = clipstr(chunk, chunkw, **kwargs)
if y >= 0 and y < windowHeight:
scr.addstr(y, x, clipped, cattr.attr)
else:
if vd.options.debug:
raise Exception(f'addstr(y={y} x={x}) out of bounds')
if link:
vd.onMouse(scr, x, y, dispw, 1, BUTTON1_RELEASED=link)
x += dispw
totaldispw += dispw
if chunkw < dispw:
break
except Exception as e:
if vd.options.debug:
raise
# raise type(e)('%s [clip_draw y=%s x=%s dispw=%s w=%s clippedlen=%s]' % (e, y, x, totaldispw, w, len(clipped))
# ).with_traceback(sys.exc_info()[2])
return totaldispw
def _markdown_to_internal(text):
'Return markdown-formatted `text` converted to internal formatting (like `[:color]text[/]`).'
text = re.sub(r'`(.*?)`', r'[:code]\1[/]', text)
text = re.sub(r'(^#.*?)$', r'[:heading]\1[/]', text)
text = re.sub(r'\*\*(.*?)\*\*', r'[:bold]\1[/]', text)
text = re.sub(r'\*(.*?)\*', r'[:italic]\1[/]', text)
text = re.sub(r'\b_(.*?)_\b', r'[:underline]\1[/]', text)
return text
def wraptext(text, width=80, indent=''):
'''
Word-wrap `text` and yield (formatted_line, textonly_line) for each line of at most `width` characters.
Formatting like `[:color]text[/]` is ignored for purposes of computing width, and not included in `textonly_line`.
'''
import re
if width <= 0:
return
for line in text.splitlines():
if not line:
yield '', ''
continue
line = _markdown_to_internal(line)
chunks = re.split(internal_markup_re, line)
textchunks = [x for x in chunks if not is_vdcode(x)]
for linenum, textline in enumerate(textwrap.wrap(''.join(textchunks), width=width, drop_whitespace=False)):
txt = textline
r = ''
while chunks:
c = chunks[0]
if len(c) > len(txt):
r += txt
chunks[0] = c[len(txt):]
break
if len(chunks) == 1:
r += chunks.pop(0)
else:
chunks.pop(0)
r += txt[:len(c)] + chunks.pop(0)
txt = txt[len(c):]
r = r.strip()
if linenum > 0:
r = indent + r
yield r, textline
for c in chunks:
yield c, ''
def clipbox(scr, lines, attr, title=''):
scr.erase()
scr.bkgd(attr)
scr.box()
h, w = scr.getmaxyx()
for i, line in enumerate(lines):
clipdraw(scr, i+1, 2, line, attr)
clipdraw(scr, 0, w-len(title)-6, f"| {title} |", attr)
vd.addGlobals(clipstr=clipstr,
clipdraw=clipdraw,
clipdraw_chunks=clipdraw_chunks,
clipbox=clipbox,
dispwidth=dispwidth,
iterchars=iterchars,
iterchunks=iterchunks,
wraptext=wraptext)
visidata-3.1.1/visidata/cmdlog.py 0000664 0000000 0000000 00000041205 14770045464 0016760 0 ustar 00root root 0000000 0000000 import threading
from visidata import vd, UNLOADED, namedlist, vlen, asyncthread, globalCommand, date
from visidata import VisiData, BaseSheet, Sheet, ColumnAttr, VisiDataMetaSheet, JsonLinesSheet, TypedWrapper, AttrDict, Progress, ErrorSheet, CompleteKey, Path
import visidata
vd.option('replay_wait', 0.0, 'time to wait between replayed commands, in seconds', sheettype=None)
vd.theme_option('disp_replay_play', '▶', 'status indicator for active replay')
vd.theme_option('disp_replay_record', '⏺', 'status indicator for macro record')
vd.theme_option('color_status_replay', 'green', 'color of replay status indicator')
# prefixes which should not be logged
nonLogged = '''forget exec-longname undo redo quit
show error errors statuses options threads jump
replay cancel save-cmdlog macro cmdlog-sheet menu repeat reload-every
search scroll prev next page start end zoom visibility sidebar
mouse suspend redraw no-op help syscopy sysopen profile toggle'''.split()
vd.option('rowkey_prefix', 'キ', 'string prefix for rowkey in the cmdlog', sheettype=None)
vd._nextCommands = [] # list[str|CommandLogRow] for vd.queueCommand
CommandLogRow = namedlist('CommandLogRow', 'sheet col row longname input keystrokes comment undofuncs'.split())
@VisiData.api
def queueCommand(vd, longname, input=None, sheet=None, col=None, row=None):
'Add command to queue of next commands to execute.'
vd._nextCommands.append(CommandLogRow(longname=longname, input=input, sheet=sheet, col=col, row=row))
@VisiData.api
def open_vd(vd, p):
return CommandLog(p.base_stem, source=p, precious=True)
@VisiData.api
def open_vdj(vd, p):
return CommandLogJsonl(p.base_stem, source=p, precious=True)
VisiData.save_vd = VisiData.save_tsv
@VisiData.api
def save_vdj(vd, p, *vsheets):
with p.open(mode='w', encoding=vsheets[0].options.save_encoding) as fp:
fp.write("#!vd -p\n")
for vs in vsheets:
vs.write_jsonl(fp)
@VisiData.api
def checkVersion(vd, desired_version):
if desired_version != visidata.__version_info__:
vd.fail("version %s required" % desired_version)
@VisiData.api
def fnSuffix(vd, prefix:str):
i = 0
fn = prefix + '.vdj'
while Path(fn).exists():
i += 1
fn = f'{prefix}-{i}.vdj'
return fn
def indexMatch(L, func):
'returns the smallest i for which func(L[i]) is true'
for i, x in enumerate(L):
if func(x):
return i
@VisiData.api
def isLoggableCommand(vd, cmd):
'Return whether command should be logged to the cmdlog, depending if it has a prefix in nonLogged, or was defined with replay=False.'
if not cmd.replayable:
return False
for n in nonLogged:
if cmd.longname.startswith(n):
return False
return True
def isLoggableSheet(sheet):
return sheet is not vd.cmdlog and not isinstance(sheet, (vd.OptionsSheet, ErrorSheet))
@Sheet.api
def moveToRow(vs, rowstr):
'Move cursor to row given by *rowstr*, which can be either the row number or keystr.'
rowidx = vs.getRowIndexFromStr(rowstr)
if rowidx is None:
return False
vs.cursorRowIndex = rowidx
return True
@Sheet.api
def getRowIndexFromStr(vs, row):
prefix = vd.options.rowkey_prefix
index = None
if isinstance(row, int):
index = row
elif isinstance(row, str) and row.startswith(prefix):
rowk = row[len(prefix):]
index = indexMatch(vs.rows, lambda r,vs=vs,rowk=rowk: rowk == ','.join(map(str, vs.rowkey(r))))
else:
try:
index = int(row)
except ValueError:
vd.warning('invalid type for row index')
return index
@Sheet.api
def moveToCol(vs, col):
'Move cursor to column given by *col*, which can be either the column number or column name.'
if isinstance(col, str):
vcolidx = indexMatch(vs.visibleCols, lambda c,name=col: name == c.name)
elif isinstance(col, int):
vcolidx = col
if vcolidx is None or vcolidx >= vs.nVisibleCols:
return False
vs.cursorVisibleColIndex = vcolidx
return True
@BaseSheet.api
def commandCursor(sheet, execstr):
'Return (col, row) of cursor suitable for cmdlog replay of execstr.'
colname, rowname = '', ''
contains = lambda s, *substrs: any((a in s) for a in substrs)
if contains(execstr, 'cursorTypedValue', 'cursorDisplay', 'cursorValue', 'cursorCell', 'cursorRow') and sheet.nRows > 0:
rowname = sheet.cursorRowIndex
if contains(execstr, 'cursorTypedValue', 'cursorDisplay', 'cursorValue', 'cursorCell', 'cursorCol', 'cursorVisibleCol', 'ColumnAtCursor'):
if sheet.cursorCol:
colname = sheet.cursorCol.name or sheet.visibleCols.index(sheet.cursorCol)
else:
colname = None
return colname, rowname
# rowdef: namedlist (like TsvSheet)
class CommandLogBase:
'Log of commands for current session.'
rowtype = 'logged commands'
precious = False
_rowtype = CommandLogRow
columns = [
ColumnAttr('sheet'),
ColumnAttr('col'),
ColumnAttr('row'),
ColumnAttr('longname'),
ColumnAttr('input'),
ColumnAttr('keystrokes'),
ColumnAttr('comment'),
ColumnAttr('undo', 'undofuncs', type=vlen, width=0)
]
filetype = 'vd'
def newRow(self, **fields):
return self._rowtype(**fields)
def beforeExecHook(self, sheet, cmd, args, keystrokes):
if vd.activeCommand:
self.afterExecSheet(sheet, False, '')
colname, rowname, sheetname = '', '', None
if sheet and not (cmd.longname.startswith('open-') and not cmd.longname in ('open-row', 'open-cell')):
sheetname = sheet.name
colname, rowname = sheet.commandCursor(cmd.execstr)
contains = lambda s, *substrs: any((a in s) for a in substrs)
if contains(cmd.execstr, 'pasteFromClipboard'):
args = vd.sysclipValue().strip()
comment = vd.currentReplayRow.comment if vd.currentReplayRow else cmd.helpstr
vd.activeCommand = self.newRow(sheet=sheetname,
col=colname,
row=rowname,
keystrokes=keystrokes,
input=args,
longname=cmd.longname,
comment=comment,
replayable=cmd.replayable,
undofuncs=[])
def afterExecSheet(self, sheet, escaped, err):
'Records vd.activeCommand'
if not vd.activeCommand: # nothing to record
return
if escaped:
vd.activeCommand = None
return
# remove user-aborted commands and simple movements (unless first command on the sheet, which created the sheet)
if not sheet.cmdlog_sheet.rows or vd.isLoggableCommand(vd.activeCommand):
if isLoggableSheet(sheet): # don't record actions from cmdlog or other internal sheets on global cmdlog
self.addRow(vd.activeCommand) # add to global cmdlog
sheet.cmdlog_sheet.addRow(vd.activeCommand) # add to sheet-specific cmdlog
vd.activeCommand = None
def openHook(self, vs, src):
while isinstance(src, BaseSheet):
src = src.source
r = self.newRow(keystrokes='o', input=str(src), longname='open-file', replayable=True)
vs.cmdlog_sheet.addRow(r)
self.addRow(r)
class CommandLog(CommandLogBase, VisiDataMetaSheet):
pass
class CommandLogJsonl(CommandLogBase, JsonLinesSheet):
filetype = 'vdj'
def newRow(self, **fields):
return AttrDict(JsonLinesSheet.newRow(self, **fields))
def iterload(self):
for r in JsonLinesSheet.iterload(self):
if isinstance(r, TypedWrapper):
yield r
else:
yield AttrDict(r)
### replay
vd.paused = False
vd.currentReplay = None # CommandLog replaying currently
vd.currentReplayRow = None # must be global, to allow replay
@VisiData.api
def replay_cancel(vd):
vd.currentReplayRow = None
vd.currentReplay = None
vd._nextCommands.clear()
@VisiData.api
def moveToReplayContext(vd, r, vs):
'set the sheet/row/col to the values in the replay row'
vs.ensureLoaded()
vd.sync()
vd.clearCaches()
if r.row not in [None, '']:
vs.moveToRow(r.row) or vd.error(f'no "{r.row}" row on {vs}')
if r.col not in [None, '']:
vs.moveToCol(r.col) or vd.error(f'no "{r.col}" column on {vs}')
@VisiData.api
def replayOne(vd, r):
'Replay the command in one given row.'
vd.currentReplayRow = r
longname = getattr(r, 'longname', None)
if longname is None and getattr(r, 'keystrokes', None) is None:
vd.fail('failed to find command to replay')
if r.sheet and longname not in ['set-option', 'unset-option']:
vs = vd.getSheet(r.sheet) or vd.error('no sheet named %s' % r.sheet)
else:
vs = None
if longname in ['set-option', 'unset-option']:
try:
context = vs if r.sheet and vs else vd
option_scope = r.sheet or r.col or 'global'
if option_scope == 'override': option_scope = 'global' # override is deprecated, is now global
if longname == 'set-option':
context.options.set(r.row, r.input, option_scope)
else:
context.options.unset(r.row, option_scope)
escaped = False
except Exception as e:
vd.exceptionCaught(e)
escaped = True
else:
vs = vs or vd.activeSheet
if vs:
if vs in vd.sheets: # if already on sheet stack, push to top
vd.push(vs)
else:
vs = vd.cmdlog
try:
vd.moveToReplayContext(r, vs)
if r.comment:
vd.status(r.comment)
# <=v1.2 used keystrokes in longname column; getCommand fetches both
escaped = vs.execCommand(longname if longname else r.keystrokes, keystrokes=r.keystrokes)
except Exception as e:
vd.exceptionCaught(e)
escaped = True
vd.currentReplayRow = None
if escaped: # escape during replay aborts replay
vd.warning('replay aborted during %s' % (longname or r.keystrokes))
return escaped
@VisiData.api
class DisableAsync:
def __enter__(self):
vd.execAsync = vd.execSync
def __exit__(self, exc_type, exc_val, tb):
vd.execAsync = lambda *args, vd=vd, **kwargs: visidata.VisiData.execAsync(vd, *args, **kwargs)
@VisiData.api
def replay_sync(vd, cmdlog):
'Replay all commands in *cmdlog*.'
with vd.DisableAsync():
vd.sync() #2352 let cmdlog finish loading
cmdlog.cursorRowIndex = 0
vd.currentReplay = cmdlog
with Progress(total=len(cmdlog.rows)) as prog:
while cmdlog.cursorRowIndex < len(cmdlog.rows):
if vd.currentReplay is None:
vd.status('replay canceled')
return
vd.statuses.clear()
try:
if vd.replayOne(cmdlog.cursorRow):
vd.replay_cancel()
return True
except Exception as e:
vd.replay_cancel()
vd.exceptionCaught(e)
vd.status('replay canceled')
return True
cmdlog.cursorRowIndex += 1
prog.addProgress(1)
if vd.activeSheet:
vd.activeSheet.ensureLoaded()
vd.status('replay complete')
vd.currentReplay = None
@VisiData.api
def replay(vd, cmdlog):
'Inject commands into live execution with interface.'
vd.push(cmdlog)
vd._nextCommands.extend(cmdlog.rows)
@VisiData.api
def getLastArgs(vd):
'Get user input for the currently playing command.'
if vd.currentReplayRow:
return vd.currentReplayRow.input
return None
@VisiData.api
def setLastArgs(vd, args):
'Set user input on last command, if not already set.'
# only set if not already set (second input usually confirmation)
if vd.activeCommand:
if not vd.activeCommand.input:
vd.activeCommand.input = args
@VisiData.property
def replayStatus(vd):
if vd.macroMode:
return f'|[:error] {len(vd.macroMode)} {vd.options.disp_replay_record} [:]'
if vd._nextCommands:
return f'|[:status_replay] {len(vd._nextCommands)} {vd.options.disp_replay_play} [:]'
return ''
@BaseSheet.property
def cmdlog(sheet):
rows = sheet.cmdlog_sheet.rows
if isinstance(sheet.source, BaseSheet):
rows = sheet.source.cmdlog.rows + rows
return CommandLogJsonl(sheet.name+'_cmdlog', source=sheet, rows=rows)
@BaseSheet.lazy_property
def cmdlog_sheet(sheet):
c = CommandLogJsonl(sheet.name+'_cmdlog', source=sheet, rows=[])
# copy over all existing globally set options
# you only need to do this for the first BaseSheet in a tree
if not isinstance(sheet.source, BaseSheet):
for r in vd.cmdlog.rows:
if r.sheet == 'global' and (r.longname == 'set-option') or (r.longname == 'unset-option'):
c.addRow(r)
return c
@BaseSheet.property
def shortcut(self):
if self._shortcut:
return self._shortcut
try:
return str(vd.allSheets.index(self)+1)
except ValueError:
pass
try:
return self.cmdlog_sheet.rows[0].keystrokes or '' #2293
except Exception:
pass
return ''
@VisiData.property
def cmdlog(vd):
if not vd._cmdlog:
vd._cmdlog = CommandLogJsonl('cmdlog', rows=[]) # no reload
vd._cmdlog.resetCols()
vd.beforeExecHooks.append(vd._cmdlog.beforeExecHook)
return vd._cmdlog
@VisiData.property
def modifyCommand(vd):
if vd.activeCommand and vd.isLoggableCommand(vd.activeCommand):
return vd.activeCommand
if not vd.cmdlog.rows:
return None
return vd.cmdlog.rows[-1]
@CommandLogJsonl.api
@asyncthread
def repeat_for_n(cmdlog, r, n=1):
r.sheet = r.row = r.col = ""
for i in range(n):
vd.replayOne(r)
@CommandLogJsonl.api
@asyncthread
def repeat_for_selected(cmdlog, r):
r.sheet = r.row = r.col = ""
for idx, r in enumerate(vd.sheet.rows):
if vd.sheet.isSelected(r):
vd.sheet.cursorRowIndex = idx
vd.replayOne(r)
BaseSheet.init('_shortcut')
globalCommand('gD', 'cmdlog-all', 'vd.push(vd.cmdlog)', 'open global CommandLog for all commands executed in current session')
globalCommand('D', 'cmdlog-sheet', 'vd.push(sheet.cmdlog)', "open current sheet's CommandLog with all other loose ends removed; includes commands from parent sheets")
globalCommand('zD', 'cmdlog-sheet-only', 'vd.push(sheet.cmdlog_sheet)', 'open CommandLog for current sheet with commands from parent sheets removed')
BaseSheet.addCommand('^D', 'save-cmdlog', 'saveSheets(inputPath("save cmdlog to: ", value=fnSuffix(name)), vd.cmdlog)', 'save CommandLog to filename.vdj file')
BaseSheet.bindkey('^N', 'no-op')
BaseSheet.addCommand('^K', 'replay-stop', 'vd.replay_cancel(); vd.warning("replay canceled")', 'cancel current replay')
globalCommand(None, 'show-status', 'status(input("status: "))', 'show given message on status line')
globalCommand('^V', 'show-version', 'status(__version_info__);', 'Show version and copyright information on status line')
globalCommand('z^V', 'check-version', 'checkVersion(input("require version: ", value=__version_info__))', 'check VisiData version against given version')
CommandLog.addCommand('x', 'replay-row', 'vd.replayOne(cursorRow); status("replayed one row")', 'replay command in current row')
CommandLog.addCommand('gx', 'replay-all', 'vd.replay(sheet)', 'replay contents of entire CommandLog')
CommandLogJsonl.addCommand('x', 'replay-row', 'vd.replayOne(cursorRow); status("replayed one row")', 'replay command in current row')
CommandLogJsonl.addCommand('gx', 'replay-all', 'vd.replay(sheet)', 'replay contents of entire CommandLog')
CommandLog.options.json_sort_keys = False
CommandLog.options.encoding = 'utf-8'
CommandLogJsonl.options.json_sort_keys = False
CommandLogJsonl.options.regex_skip = r'^(//|#).*'
vd.addGlobals(CommandLogBase=CommandLogBase, CommandLogRow=CommandLogRow)
vd.addMenuItems('''
View > Command log > this sheet > cmdlog-sheet
View > Command log > this sheet only > cmdlog-sheet-only
View > Command log > all commands > cmdlog-all
System > Execute longname > exec-longname
Help > Version > show-version
''')
visidata-3.1.1/visidata/color.py 0000664 0000000 0000000 00000020311 14770045464 0016624 0 ustar 00root root 0000000 0000000 import curses
import functools
from copy import copy
from collections import namedtuple
from dataclasses import dataclass
from visidata import vd, options, Extensible, drawcache, drawcache_property, VisiData
import visidata
__all__ = ['ColorAttr', 'colors', 'update_attr', 'ColorMaker', 'rgb_to_attr']
vd.help_color = '''Color syntax: ` on `
- attributes: [:bold]bold[/] [:underline]underline[/] [:italic]italic[/] [:reverse]reverse[/]
- colors: 0-255 or [:black on 238]black[/] [:red on 238]red[/] [:green on 238]green[/] [:yellow on 238]yellow[/] [:blue on 238]blue[/] [:magenta on 238]magenta[/] [:cyan on 238]cyan[/] [:white on 238]white[/]
- the second color is used as a fallback if the first color is not available
See [:onclick https://visidata.org/docs/colors]https://visidata.org/docs/colors[/] for more detailed info.
'''
@dataclass
class ColorAttr:
fg:int = -1 # default is no foreground specified
bg:int = -1 # default is no background specified
attributes:int = 0 # default is no attributes
precedence:int = 0 # default is lowest priority
colorname:str = ''
def __init__(self, fg:int=-1, bg:int=-1, attributes:int=0, precedence:int=0, colorname:str=''):
assert fg < 256, fg
assert bg < 256, bg
self.fg = fg
self.bg = bg
self.attributes = attributes
self.precedence = precedence
self.colorname = colorname
def update(self, b:'ColorAttr', prec:int=10) -> 'ColorAttr':
return update_attr(self, b, prec)
@property
def attr(self) -> int:
a = colors._get_colorpair(self.fg, self.bg, self.colorname) | self.attributes
assert a >= 0, a
return a
def as_str(self) -> str:
attrnames = [attrname for attrname, attr in colors._attrs.items() if self.attributes & attr]
attrnames.append(f'{self.fg} on {self.bg}')
return ' '.join(attrnames)
def update_attr(oldattr:ColorAttr, updattr:ColorAttr, updprec:int=None) -> ColorAttr:
assert isinstance(updattr, ColorAttr), updattr
if updprec is None:
updprec = updattr.precedence
updfg = updattr.fg
updbg = updattr.bg
updattr = updattr.attributes
# starting values, work backwards
newfg = oldattr.fg
newbg = oldattr.bg
newattr = oldattr.attributes | updattr
newprec = oldattr.precedence
if newfg < 0 or (updfg >= 0 and updprec > newprec):
newfg = updfg
if newbg < 0 or (updbg >= 0 and updprec > newprec):
newbg = updbg
newprec = max(updprec, newprec)
return ColorAttr(newfg, newbg, newattr, newprec)
class ColorMaker:
def __init__(self):
self.color_pairs = {} # (fg,bg) -> (pairnum, colornamestr) (pairnum can be or'ed with other attrs)
self.color_cache = {} # colorname -> colorpair
@drawcache_property
def colorcache(self):
return {}
def setup(self):
try:
curses.use_default_colors()
except Exception as e:
pass
@drawcache_property
def colors(self):
'not computed until curses color has been initialized'
return {x[6:]:getattr(curses, x) for x in dir(curses) if x.startswith('COLOR_') and x != 'COLOR_PAIRS'}
def __getitem__(self, colorname:str) -> ColorAttr:
'colors["green"] or colors["foo"] returns parsed ColorAttr.'
return self.get_color(colorname)
def __getattr__(self, optname) -> ColorAttr:
'colors.color_foo or colors.foo returns parsed ColorAttr.'
return self.get_color(optname)
@drawcache
def resolve_colors(self, colorstack):
'Returns the ColorAttr for the colorstack, a list of (prec, color_option_name) sorted highest-precedence color first.'
cattr = ColorAttr()
for prec, coloropt in colorstack:
c = self.get_color(coloropt)
cattr = update_attr(cattr, c, prec)
return cattr
def get_color(self, optname:str, precedence:int=0) -> ColorAttr:
'''Return ColorAttr for options.color_foo if *optname* of either "foo" or "color_foo",
Otherwise parse *optname* for colorstring like "bold 34 red on 135 blue".'''
r = self.colorcache.get(optname, None)
if r is None:
coloropt = vd.options._get(optname) or vd.options._get(f'color_{optname}')
colornamestr = coloropt.value if coloropt else optname
r = self.colorcache[optname] = self._colornames_to_cattr(colornamestr, precedence)
return r
def _split_colorstr(self, colorstr):
'Return (fgstr, bgstr, attrlist) parsed from colorstr.'
fgbgattrs = ['', '', []] # fgstr, bgstr, attrlist
if not colorstr:
return fgbgattrs
colorstr = str(colorstr)
i = 0 # fg by default
for x in colorstr.split():
if x == 'fg':
i = 0
continue
elif x in ['on', 'bg']:
i = 1
continue
if hasattr(curses, 'A_' + x.upper()):
fgbgattrs[2].append(x)
else:
if not fgbgattrs[i]: # keep first known color
if self._get_colornum(x) is not None: # only set known colors
fgbgattrs[i] = x
else:
fgbgattrs[i] = None
return fgbgattrs
def _get_colornum(self, colorname:'str|int', default:int=-1) -> int:
'Return terminal color number for colorname.'
if isinstance(colorname, int):
return colorname
if not colorname:
return default
r = self.color_cache.get(colorname, None)
if r is not None:
return r
try:
r = int(colorname)
except Exception:
r = self.colors.get(colorname.upper())
if r is None:
return None
try: # test to see if color is available
curses.init_pair(255, r, 0)
self.color_cache[colorname] = r
return r
except curses.error as e:
return None # not available
except ValueError: # Python 3.10+ issue #1227
return None
def _attrnames_to_num(self, attrnames:'list[str]') -> int:
attrs = 0
for attr in attrnames:
attrs |= getattr(curses, 'A_'+attr.upper())
return attrs
@drawcache_property
def _attrs(self):
return {k[2:].lower():getattr(curses, k) for k in dir(curses) if k.startswith('A_') and k != 'A_ATTRIBUTES'}
@drawcache
def _colornames_to_cattr(self, colorname:str, precedence=0) -> ColorAttr:
fg, bg, attrlist = self._split_colorstr(colorname)
fg = self._get_colornum(fg)
bg = self._get_colornum(bg)
return ColorAttr(fg, bg,
self._attrnames_to_num(attrlist),
precedence, colorname)
def _get_colorpair(self, fg:'int|None', bg:'int|None', colorname:str) -> int:
pairnum, _ = self.color_pairs.get((fg, bg), (None, ''))
if pairnum is None:
if len(self.color_pairs) > 254:
self.color_pairs.clear() # start over
self.color_cache.clear()
pairnum = len(self.color_pairs)+1
if fg is None: fg = -1
if bg is None: bg = -1
try:
curses.init_pair(pairnum, fg, bg)
except curses.error as e:
return 0 # do not cache
self.color_pairs[(fg, bg)] = (pairnum, colorname)
return curses.color_pair(pairnum)
colors = ColorMaker()
def rgb_to_xterm256(r:int,g:int,b:int,a:int=255) -> int:
if a == 0:
return -1
if max(r,g,b) - min(r,g,b) < 8:
if r <= 4: return 16
elif r <= 8: return 232
elif r >= 247: return 231
elif r >= 238: return 255
else:
return int(232 + (r-8)//10)
else:
r = max(0, r-(95-40)) // 40
g = max(0, g-(95-40)) // 40
b = max(0, b-(95-40)) // 40
return int(16 + r*36 + g*6 + b)
@functools.lru_cache(256)
def rgb_to_attr(r:int,g:int,b:int,a:int=255) -> str:
return str(rgb_to_xterm256(r,g,b,a))
import sys
vd.addGlobals({k:getattr(sys.modules[__name__], k) for k in __all__})
visidata-3.1.1/visidata/column.py 0000664 0000000 0000000 00000051530 14770045464 0017012 0 ustar 00root root 0000000 0000000 from copy import copy
import collections
import string
import itertools
import threading
import re
import time
import json
from visidata import options, anytype, stacktrace, vd, drawcache
from visidata import asyncthread, dispwidth, clipstr, iterchars
from visidata import wrapply, TypedWrapper, TypedExceptionWrapper
from visidata import Extensible, AttrDict, undoAttrFunc, ExplodingMock, MissingAttrFormatter
from visidata import getitem, setitem, getitemdef, getitemdeep, setitemdeep, getattrdeep, setattrdeep, iterchunks
class InProgress(Exception):
@property
def stacktrace(self):
return ['calculation in progress']
INPROGRESS = TypedExceptionWrapper(None, exception=InProgress()) # sentinel
vd.option('col_cache_size', 0, 'max number of cache entries in each cached column')
vd.option('disp_formatter', 'generic', 'formatter to create the text in each cell (also used by text savers)', replay=True)
vd.option('disp_displayer', 'generic', 'displayer to render the text in each cell', replay=False)
class DisplayWrapper:
def __init__(self, value=None, *, typedval=None, text='', note=None, notecolor=None, error=None):
self.value = value # actual value (any type)
self.typedval = typedval # consistently typed value (or None)
self.text = text # displayed string
self.note = note # single unicode character displayed in cell far right
self.notecolor = notecolor # configurable color name (like 'color_warning')
self.error = error # list of strings for stacktrace
def __bool__(self):
return bool(self.value)
def __eq__(self, other):
return self.value == other
def _default_colnames():
'A B C .. Z AA AB .. ZZ AAA .. to infinity'
i=0
while True:
i += 1
for x in itertools.product(string.ascii_uppercase, repeat=i):
yield ''.join(x)
default_colnames = _default_colnames()
class Column(Extensible):
'''Base class for all column types.
- *name*: name of this column.
- *type*: ``anytype str int float date`` or other type-like conversion function.
- *cache*: cache behavior
- ``False`` (default): getValue never caches; calcValue is always called.
- ``True``: getValue maintains a cache of ``options.col_cache_size``.
- ``"async"``: ``getValue`` launches thread for every uncached result, returns invalid value until cache entry available.
- *width*: == 0 if hidden, None if auto-compute next time.
- *height*: max height, None/0 to auto-compute for each row.
- *fmtstr*: format string as applied by column type.
- *getter*: default calcValue calls ``getter(col, row)``.
- *setter*: default putValue calls ``setter(col, row, val)``.
- *kwargs*: other attributes to be set on this column.
'''
def __init__(self, name=None, *, type=anytype, cache=False, **kwargs):
self.sheet = ExplodingMock('use addColumn() on all columns') # owning Sheet, set in .recalc() via Sheet.addColumn
if name is None:
name = next(default_colnames)
self.name = str(name) # display visible name
self.fmtstr = '' # by default, use str()
self._type = type # anytype/str/int/float/date/func
self.getter = lambda col, row: row
self.setter = None
self._width = None # == 0 if hidden, None if auto-compute next time
self.hoffset = 0 # starting horizontal (char) offset of displayed column value
self.voffset = 0 # starting vertical (line) offset of displayed column value
self.height = 1 # max height, None/0 to auto-compute for each row
self.keycol = 0 # keycol index (or 0 if not key column)
self.expr = None # Column-type-dependent parameter
self.formatter = ''
self.displayer = ''
self.defer = False
self.disp_expert = 0 # auto-hide if options.disp_expert less than col.disp_expert
self.setCache(cache)
for k, v in kwargs.items():
setattr(self, k, v) # instead of __dict__.update(kwargs) to invoke property.setters
def __copy__(self):
cls = self.__class__
ret = cls.__new__(cls)
ret.__dict__.update(self.__dict__)
ret.keycol = 0 # column copies lose their key status
if self._cachedValues is not None:
ret._cachedValues = collections.OrderedDict() # an unrelated cache for copied columns
return ret
def __str__(self):
return f'{type(self).__name__}:{self.name}'
def __repr__(self):
return f'<{type(self).__name__}: {self.name}>'
def __deepcopy__(self, memo):
return self.__copy__() # no separate deepcopy
def __getstate__(self):
return {k:getattr(self, k) for k in 'name typestr width height expr keycol formatter displayer fmtstr voffset hoffset aggstr'.split() if hasattr(self, k)}
def __setstate__(self, d):
for attr, v in d.items():
setattr(self, attr, v)
def recalc(self, sheet=None):
'Reset column cache, attach column to *sheet*, and reify column name.'
if self._cachedValues:
self._cachedValues.clear()
if sheet:
self.sheet = sheet
self.name = self._name
@property
def name(self):
'Name of this column.'
return self._name or ''
@name.setter
def name(self, name):
self.setName(name)
def setName(self, name):
if name is None:
name = ''
if isinstance(name, str):
name = name.strip()
else:
name = str(name)
if self.sheet:
name = self.sheet.maybeClean(name)
self._name = name
@property
def typestr(self):
'Type of this column as string.'
return self._type.__name__
@typestr.setter
def typestr(self, v):
self.type = vd.getGlobals()[v or 'anytype']
@property
def type(self):
'Type of this column.'
return self._type
@type.setter
def type(self, t):
if self._type != t:
vd.addUndo(setattr, self, '_type', self.type)
if not t:
self._type = anytype
elif isinstance(t, str):
self.typestr = t
else:
self._type = t
@property
def width(self):
'Width of this column in characters. 0 or negative means hidden. None means not-yet-autocomputed.'
return self._width
@width.setter
def width(self, w):
if self.width != w:
if self.width == 0 or w == 0: # hide/unhide
vd.addUndo(setattr, self, '_width', self.width)
self._width = w
@property
def formatted_help(self):
return MissingAttrFormatter().format(self.help, sheet=self.sheet, col=self, vd=vd)
@property
def help_formatters(self):
formatters = [k[10:] for k in dir(self) if k.startswith('formatter_')]
return ' '.join(formatters)
@property
def help_displayers(self):
displayers = [k[10:] for k in dir(self) if k.startswith('displayer_')]
return ' '.join(displayers)
@property
def _formatdict(col):
if '=' in col.fmtstr:
return dict(val.split('=', maxsplit=1) for val in col.fmtstr.split())
return {}
@property
def fmtstr(self):
'Format string to use to display this column.'
return self._fmtstr or vd.getType(self.type).fmtstr
@fmtstr.setter
def fmtstr(self, v):
self._fmtstr = v
def _format_len(self, typedval, **kwargs):
if isinstance(typedval, dict):
return f'{{{len(typedval)}}}'
elif isinstance(typedval, (list, tuple)):
return f'[{len(typedval)}]'
return self.formatValue(typedval, **kwargs)
def formatter_len(self, fmtstr):
return self._format_len
def formatter_generic(self, fmtstr):
return self.formatValue
def formatter_json(self, fmtstr):
return lambda v,*args,**kwargs: json.dumps(v)
def formatter_python(self, fmtstr):
return lambda v,*args,**kwargs: str(v)
@drawcache
def make_formatter(self):
'Return function for format(v) from the current formatter and fmtstr'
_formatMaker = getattr(self, 'formatter_'+(self.formatter or self.sheet.options.disp_formatter))
return _formatMaker(self._formatdict)
def format(self, *args, **kwargs):
return self.make_formatter()(*args, **kwargs)
def formatValue(self, typedval, width=None):
'Return displayable string of *typedval* according to ``Column.fmtstr``.'
if typedval is None:
return None
if self.type is anytype:
if isinstance(typedval, (dict, list, tuple)):
dispval, dispw = clipstr(iterchars(typedval), width)
return dispval
if isinstance(typedval, bytes):
typedval = typedval.decode(options.encoding, options.encoding_errors)
gt = vd.getType(self._type)
return gt.formatter(self._fmtstr or gt.fmtstr, typedval)
def displayer_generic(self, dw:DisplayWrapper, width=None):
'''Fit *dw.text* into *width* charcells.
Generate list of (attr:str, text:str) suitable for clipdraw_chunks.
The 'generic' displayer does not do any formatting.
'''
if width is not None and width > 1 and vd.isNumeric(self):
yield ('', dw.text.rjust(width-2))
else:
yield ('', dw.text)
def displayer_full(self, dw:DisplayWrapper, width=None):
'''Fit *dw.text* into *width* charcells.
Generate list of (attr:str, text:str) suitable for clipdraw_chunks.
The 'full' displayer allows formatting like [:color].
'''
if width is not None and width > 1 and vd.isNumeric(self):
yield from iterchunks(dw.text.rjust(width-2))
else:
yield from iterchunks(dw.text)
def display(self, *args, **kwargs):
f = getattr(self, 'displayer_'+(self.displayer or self.sheet.options.disp_displayer), self.displayer_generic)
return f(*args, **kwargs)
def hide(self, hide=True):
if hide:
self.setWidth(0)
else:
self.setWidth(abs(self.width or self.getMaxWidth(self.sheet.visibleRows)))
@property
def hidden(self):
'Return True if width of this column is 0 or negative.'
if self.width is None:
return False
return self.width <= 0
def calcValue(self, row):
'Calculate and return value for *row* in this column.'
return (self.getter)(self, row)
def getTypedValue(self, row):
'Return the properly-typed value for the given row at this column, or a TypedWrapper object in case of null or error.'
return wrapply(self.type, wrapply(self.getValue, row))
def setCache(self, cache):
'''Set cache behavior for this column to *cache*:
- ``False`` (default): getValue never caches; calcValue is always called.
- ``True``: getValue maintains a cache of ``options.col_cache_size``.
- ``"async"``: ``getValue`` launches thread for every uncached result, maintains cache of infinite size. Returns invalid value until cache entry available.'''
self.cache = cache
self._cachedValues = collections.OrderedDict() if self.cache else None
@asyncthread
def _calcIntoCacheAsync(self, row):
# causes issues when moved into _calcIntoCache gen case
self._cachedValues[self.sheet.rowid(row)] = INPROGRESS
self._calcIntoCache(row)
def _calcIntoCache(self, row):
ret = wrapply(self.calcValue, row)
if not isinstance(ret, TypedExceptionWrapper) or ret.val is not INPROGRESS:
self._cachedValues[self.sheet.rowid(row)] = ret
return ret
def getValue(self, row):
'Return value for *row* in this column, calculating if not cached.'
if self.defer:
try:
row, rowmods = self.sheet._deferredMods[self.sheet.rowid(row)]
return rowmods[self]
except KeyError:
pass
if self._cachedValues is None:
return self.calcValue(row)
k = self.sheet.rowid(row)
if k in self._cachedValues:
return self._cachedValues[k]
if self.cache == 'async':
ret = self._calcIntoCacheAsync(row)
else:
ret = self._calcIntoCache(row)
cachesize = options.col_cache_size
if cachesize > 0 and len(self._cachedValues) > cachesize:
self._cachedValues.popitem(last=False)
return ret
def getCell(self, row):
'Return DisplayWrapper for displayable cell value.'
cellval = wrapply(self.getValue, row)
typedval = wrapply(self.type, cellval)
if isinstance(typedval, TypedWrapper):
if isinstance(cellval, TypedExceptionWrapper): # calc failed
exc = cellval.exception
if cellval.forwarded:
dispval = str(cellval) # traceback.format_exception_only(type(exc), exc)[-1].strip()
else:
dispval = options.disp_error_val
return DisplayWrapper(cellval.val, error=exc.stacktrace,
text=dispval,
note=f'[:onclick error-cell]{options.disp_note_getexc}[:]',
notecolor='color_error')
elif typedval.val is None: # early out for strict None
return DisplayWrapper(None, text='', # force empty display for None
note=options.disp_note_none,
notecolor='color_note_type')
elif isinstance(typedval, TypedExceptionWrapper): # calc succeeded, type failed
return DisplayWrapper(typedval.val, text=str(cellval),
error=typedval.stacktrace,
note=f'[:onclick error-cell]{options.disp_note_typeexc}[:]',
notecolor='color_warning')
else:
return DisplayWrapper(typedval.val, text=str(typedval.val),
error=['unknown'],
note=f'[:onclick error-cell]{options.disp_note_typeexc}[:]',
notecolor='color_warning')
elif isinstance(typedval, threading.Thread):
return DisplayWrapper(None,
text=options.disp_pending,
note=options.disp_note_pending,
notecolor='color_note_pending')
dw = DisplayWrapper(cellval)
dw.typedval = typedval
try:
dw.text = self.format(typedval, width=(self.width or 0)*2) or ''
# annotate cells with raw value type in anytype columns, except for strings
if self.type is anytype and type(cellval) is not str:
typedesc = vd.typemap.get(type(cellval), None)
if typedesc:
dw.note = typedesc.icon
dw.notecolor = 'color_note_type'
except Exception as e: # formatting failure
e.stacktrace = stacktrace()
dw.error = e.stacktrace
try:
dw.text = str(cellval)
except Exception as e:
dw.text = str(e)
dw.note = options.disp_note_fmtexc
dw.notecolor = 'color_warning'
# dw.display = self.display(dw) # set during draw() once colwidth is known
return dw
def getDisplayValue(self, row):
'Return string displayed in this column for given *row*.'
return self.getCell(row).text
def putValue(self, row, val):
'Change value for *row* in this column to *val* immediately. Does not check the type. Overridable; by default calls ``.setter(row, val)``.'
if self.setter:
return self.setter(self, row, val)
def setValue(self, row, val, setModified=True):
'Change value for *row* in this column to *val*. Call ``putValue`` immediately if not a deferred column (added to deferred parent at load-time); otherwise cache until later ``putChanges``. Caller must add undo function.'
if self.defer:
self.cellChanged(row, val)
else:
self.putValue(row, val)
if setModified: #1800
self.sheet.setModified()
@asyncthread
def setValues(self, rows, *values):
'Set values in this column for *rows* to *values*, recycling values as needed to fill *rows*.'
vd.addUndoSetValues([self], rows)
for r, v in zip(rows, itertools.cycle(values)):
vd.callNoExceptions(self.setValue, r, v)
self.recalc()
return vd.status('set %d cells to %d values' % (len(rows), len(values)))
@asyncthread
def setValuesTyped(self, rows, *values):
'Set values on this column for *rows* to *values*, coerced to column type, recycling values as needed to fill *rows*. Abort on type exception.'
vd.addUndoSetValues([self], rows)
for r, v in zip(rows, itertools.cycle(self.type(val) for val in values)):
vd.callNoExceptions(self.setValue, r, v)
self.recalc()
return vd.status('set %d cells to %d values' % (len(rows), len(values)))
def getMaxWidth(self, rows):
'Return the maximum length of any cell in column or its header (up to window width).'
w = 0
nlen = dispwidth(self.name)
if len(rows) > 0:
w_max = 0
for r in rows:
row_w = dispwidth(self.getDisplayValue(r), maxwidth=self.sheet.windowWidth)
if w_max < row_w:
w_max = row_w
if w_max >= self.sheet.windowWidth:
break #1747 early out to speed up wide columns
w = w_max
w = max(w, nlen)+2
w = min(w, self.sheet.windowWidth)
return w
# ---- basic Columns
class AttrColumn(Column):
'Column using getattr/setattr with *attr*.'
def __init__(self, name=None, expr=None, **kwargs):
super().__init__(name,
expr=expr if expr is not None else name,
getter=lambda col,row: getattrdeep(row, col.expr, None),
**kwargs)
def putValue(self, row, val):
super().putValue(row, val)
setattrdeep(row, self.expr, val)
class ItemColumn(Column):
'Column using getitem/setitem with *expr*.'
def __init__(self, name=None, expr=None, **kwargs):
super().__init__(name,
expr=expr if expr is not None else name,
getter=lambda col,row: getitemdeep(row, col.expr, None),
**kwargs)
def putValue(self, row, val):
super().putValue(row, val)
setitemdeep(row, self.expr, val)
class SubColumnFunc(Column):
'Column compositor; preprocess row with *subfunc*(row, *expr*) before passing to *origcol*.getValue and *origcol*.setValue.'
def __init__(self, name='', origcol=None, expr=None, subfunc=getitemdef, **kwargs):
super().__init__(name, type=origcol.type, width=origcol.width, expr=expr, **kwargs)
self.origcol = origcol
self.subfunc = subfunc
def calcValue(self, row):
subrow = self.subfunc(row, self.expr)
if subrow is not None:
# call getValue to use deferred values from source sheet
return self.origcol.getValue(subrow)
def putValue(self, row, value):
subrow = self.subfunc(row, self.expr)
if subrow is None:
vd.fail('no source row')
self.origcol.setValue(subrow, value)
def recalc(self, sheet=None):
Column.recalc(self, sheet)
self.origcol.recalc() # reset cache but don't change sheet
def SubColumnAttr(attrname, c, **kwargs):
if 'name' not in kwargs:
kwargs['name'] = c.name
return SubColumnFunc(origcol=c, subfunc=getattrdeep, expr=attrname, **kwargs)
def SubColumnItem(idx, c, **kwargs):
if 'name' not in kwargs:
kwargs['name'] = c.name
return SubColumnFunc(origcol=c, subfunc=getitemdef, expr=idx, **kwargs)
class SettableColumn(Column):
'Column using rowid to store and retrieve values internally.'
def putValue(self, row, value):
self._store[self.sheet.rowid(row)] = value
def calcValue(self, row):
return self._store.get(self.sheet.rowid(row), None)
SettableColumn.init('_store', dict, copy=True)
vd.addGlobals(
INPROGRESS=INPROGRESS,
Column=Column,
setitem=setitem,
getattrdeep=getattrdeep,
setattrdeep=setattrdeep,
getitemdef=getitemdef,
AttrColumn=AttrColumn,
ItemColumn=ItemColumn,
SettableColumn=SettableColumn,
SubColumnFunc=SubColumnFunc,
SubColumnItem=SubColumnItem,
SubColumnAttr=SubColumnAttr,
# synonyms
ColumnItem=ItemColumn,
ColumnAttr=AttrColumn,
DisplayWrapper=DisplayWrapper
)
visidata-3.1.1/visidata/ddw/ 0000775 0000000 0000000 00000000000 14770045464 0015715 5 ustar 00root root 0000000 0000000 visidata-3.1.1/visidata/ddw/input.ddw 0000664 0000000 0000000 00000015366 14770045464 0017567 0 ustar 00root root 0000000 0000000 {"id": null, "type": null, "x": 6, "y": 7, "text": "\u2190", "color": "keystrokes", "tags": [], "group": "", "frame": null, "rows": null, "duration_ms": null, "ref": null, "onclick": null}
{"x": 11, "y": 7, "text": "go to prev/next char", "color": "", "tags": [], "group": ""}
{"x": 3, "y": 4, "text": "Ctrl", "color": "keystrokes", "tags": [], "group": ""}
{"x": 11, "y": 4, "text": "cancel input (abort)", "color": "", "tags": [], "group": ""}
{"x": 45, "y": 4, "text": "delete one char at cursor", "color": "", "tags": [], "group": ""}
{"x": 45, "y": 5, "text": "delete one char before cursor", "color": "", "tags": [], "group": ""}
{"x": 0, "y": 13, "text": "Shift", "color": "keystrokes", "tags": [], "group": ""}
{"x": 4, "y": 3, "text": "Enter", "color": "keystrokes", "tags": [], "group": ""}
{"x": 11, "y": 3, "text": "accept input", "color": "", "tags": [], "group": ""}
{"x": 42, "y": 6, "text": "K", "color": "keystrokes", "tags": [], "group": ""}
{"x": 45, "y": 6, "text": "delete all before/after cursor", "color": "", "tags": [], "group": ""}
{"x": 42, "y": 12, "text": "O", "color": "keystrokes", "tags": [], "group": ""}
{"x": 45, "y": 12, "text": "open input in external editor", "color": "", "tags": [], "group": ""}
{"x": 42, "y": 13, "text": "R", "color": "keystrokes", "tags": [], "group": ""}
{"x": 45, "y": 13, "text": "restore starting value", "color": "", "tags": [], "group": ""}
{"x": 42, "y": 8, "text": "T", "color": "keystrokes", "tags": [], "group": ""}
{"x": 45, "y": 8, "text": "swap last two chars", "color": "", "tags": [], "group": ""}
{"x": 40, "y": 6, "text": "U", "color": "keystrokes", "tags": [], "group": ""}
{"x": 42, "y": 9, "text": "V", "color": "keystrokes", "tags": [], "group": ""}
{"x": 45, "y": 9, "text": "insert literal char", "color": "", "tags": [], "group": ""}
{"x": 42, "y": 7, "text": "W", "color": "keystrokes", "tags": [], "group": ""}
{"x": 45, "y": 7, "text": "delete one word before cursor", "color": "", "tags": [], "group": ""}
{"x": 42, "y": 11, "text": "Y", "color": "keystrokes", "tags": [], "group": ""}
{"x": 45, "y": 11, "text": "insert clipboard text at cursor", "color": "", "tags": [], "group": ""}
{"x": 6, "y": 8, "text": "\u2190", "color": "keystrokes", "tags": [], "group": ""}
{"x": 11, "y": 8, "text": "go to prev/next word", "color": "", "tags": [], "group": ""}
{"x": 0, "y": 0, "text": "Begin typing to overwrite the starting value.", "color": "", "tags": [], "group": ""}
{"x": 0, "y": 1, "text": "(To edit the starting value, press a movement key first.)", "color": "", "tags": [], "group": ""}
{"x": 6, "y": 11, "text": "\u2191", "color": "keystrokes", "tags": [], "group": ""}
{"x": 1, "y": 9, "text": "Home", "color": "keystrokes", "tags": [], "group": ""}
{"x": 11, "y": 9, "text": "go to start/end", "color": "", "tags": [], "group": ""}
{"x": 6, "y": 9, "text": "End", "color": "keystrokes", "tags": [], "group": ""}
{"x": 8, "y": 7, "text": "\u2192", "color": "keystrokes", "tags": [], "group": ""}
{"x": 37, "y": 4, "text": "Delete", "color": "keystrokes", "tags": [], "group": ""}
{"x": 34, "y": 5, "text": "Backspace", "color": "keystrokes", "tags": [], "group": ""}
{"x": 37, "y": 3, "text": "Insert", "color": "keystrokes", "tags": [], "group": ""}
{"x": 45, "y": 3, "text": "toggle insert mode", "color": "", "tags": [], "group": ""}
{"x": 6, "y": 12, "text": "Tab", "color": "keystrokes", "tags": [], "group": ""}
{"x": 1, "y": 8, "text": "Ctrl", "color": "keystrokes", "tags": [], "group": ""}
{"x": 35, "y": 6, "text": "Ctrl", "color": "keystrokes", "tags": [], "group": ""}
{"x": 37, "y": 8, "text": "Ctrl", "color": "keystrokes", "tags": [], "group": ""}
{"x": 37, "y": 7, "text": "Ctrl", "color": "keystrokes", "tags": [], "group": ""}
{"x": 37, "y": 9, "text": "Ctrl", "color": "keystrokes", "tags": [], "group": ""}
{"x": 37, "y": 11, "text": "Ctrl", "color": "keystrokes", "tags": [], "group": ""}
{"x": 37, "y": 12, "text": "Ctrl", "color": "keystrokes", "tags": [], "group": ""}
{"x": 37, "y": 13, "text": "Ctrl", "color": "keystrokes", "tags": [], "group": ""}
{"x": 5, "y": 8, "text": "+", "color": "keystrokes", "tags": [], "group": ""}
{"x": 39, "y": 6, "text": "+", "color": "keystrokes", "tags": [], "group": ""}
{"x": 41, "y": 7, "text": "+", "color": "keystrokes", "tags": [], "group": ""}
{"x": 41, "y": 8, "text": "+", "color": "keystrokes", "tags": [], "group": ""}
{"x": 41, "y": 9, "text": "+", "color": "keystrokes", "tags": [], "group": ""}
{"x": 41, "y": 11, "text": "+", "color": "keystrokes", "tags": [], "group": ""}
{"x": 41, "y": 12, "text": "+", "color": "keystrokes", "tags": [], "group": ""}
{"x": 41, "y": 13, "text": "+", "color": "keystrokes", "tags": [], "group": ""}
{"x": 11, "y": 11, "text": "prev/next in history", "color": "", "tags": [], "group": ""}
{"x": 11, "y": 12, "text": "next autocomplete", "color": "", "tags": [], "group": ""}
{"x": 11, "y": 13, "text": "prev autocomplete", "color": "", "tags": [], "group": ""}
{"x": 7, "y": 4, "text": "+", "color": "keystrokes", "tags": [], "group": ""}
{"x": 8, "y": 4, "text": "C", "color": "keystrokes", "tags": [], "group": ""}
{"x": 5, "y": 13, "text": "+", "color": "keystrokes", "tags": [], "group": ""}
{"x": 6, "y": 13, "text": "Tab", "color": "keystrokes", "tags": [], "group": ""}
{"x": 32, "y": 3, "text": "\u2502", "color": "", "tags": [], "group": ""}
{"x": 32, "y": 4, "text": "\u2502", "color": "", "tags": [], "group": ""}
{"x": 32, "y": 5, "text": "\u2502", "color": "", "tags": [], "group": ""}
{"x": 32, "y": 6, "text": "\u2502", "color": "", "tags": [], "group": ""}
{"x": 32, "y": 7, "text": "\u2502", "color": "", "tags": [], "group": ""}
{"x": 32, "y": 8, "text": "\u2502", "color": "", "tags": [], "group": ""}
{"x": 32, "y": 9, "text": "\u2502", "color": "", "tags": [], "group": ""}
{"x": 32, "y": 10, "text": "\u2502", "color": "", "tags": [], "group": ""}
{"x": 32, "y": 11, "text": "\u2502", "color": "", "tags": [], "group": ""}
{"x": 32, "y": 12, "text": "\u2502", "color": "", "tags": [], "group": ""}
{"x": 32, "y": 13, "text": "\u2502", "color": "", "tags": [], "group": ""}
{"x": 7, "y": 7, "text": "/", "color": "", "tags": [], "group": ""}
{"x": 5, "y": 9, "text": "/", "color": "", "tags": [], "group": ""}
{"x": 8, "y": 8, "text": "\u2192", "color": "keystrokes", "tags": [], "group": ""}
{"x": 8, "y": 11, "text": "\u2193", "color": "keystrokes", "tags": [], "group": ""}
{"x": 41, "y": 6, "text": "/", "color": "", "tags": [], "group": ""}
{"x": 7, "y": 11, "text": "/", "color": "", "tags": [], "group": ""}
{"x": 7, "y": 8, "text": "/", "color": "", "tags": [], "group": ""}
{"x": 75, "y": 0, "text": "?", "color": "bold 117 cyan", "tags": [], "group": "", "onclick": "https://visidata.org/input"}
{"x": 3, "y": 5, "text": "Ctrl+G", "color": "keystrokes", "tags": [], "group": ""}
{"x": 11, "y": 5, "text": "cycle help sidebar", "color": "", "tags": [], "group": ""}
visidata-3.1.1/visidata/ddw/regex.ddw 0000664 0000000 0000000 00000011511 14770045464 0017526 0 ustar 00root root 0000000 0000000 {"id": null, "type": null, "x": 5, "y": 1, "text": ".", "color": "keystrokes", "tags": [], "group": "", "frame": null, "rows": null, "duration_ms": null, "ref": null, "href": null}
{"x": 5, "y": 2, "text": "^", "color": "keystrokes", "tags": [], "group": ""}
{"x": 5, "y": 3, "text": "$", "color": "keystrokes", "tags": [], "group": ""}
{"x": 38, "y": 1, "text": "*", "color": "keystrokes", "tags": [], "group": ""}
{"x": 38, "y": 2, "text": "+", "color": "keystrokes", "tags": [], "group": ""}
{"x": 38, "y": 3, "text": "?", "color": "keystrokes", "tags": [], "group": ""}
{"x": 34, "y": 5, "text": "{m,n}", "color": "keystrokes", "tags": [], "group": ""}
{"x": 8, "y": 1, "text": "any char except newline", "color": "", "tags": [], "group": ""}
{"x": 8, "y": 2, "text": "start of string", "color": "", "tags": [], "group": ""}
{"x": 8, "y": 3, "text": "end of string", "color": "", "tags": [], "group": ""}
{"x": 42, "y": 1, "text": "0 or more", "color": "", "tags": [], "group": ""}
{"x": 42, "y": 2, "text": "1 or more", "color": "", "tags": [], "group": ""}
{"x": 42, "y": 3, "text": "0 or 1", "color": "", "tags": [], "group": ""}
{"x": 42, "y": 5, "text": "between m and n", "color": "", "tags": [], "group": ""}
{"x": 1, "y": 11, "text": "[abc]", "color": "keystrokes", "tags": [], "group": ""}
{"x": 0, "y": 12, "text": "[^abc]", "color": "keystrokes", "tags": [], "group": ""}
{"x": 4, "y": 4, "text": "\\d", "color": "keystrokes", "tags": [], "group": ""}
{"x": 4, "y": 6, "text": "\\s", "color": "keystrokes", "tags": [], "group": ""}
{"x": 4, "y": 8, "text": "\\w", "color": "keystrokes", "tags": [], "group": ""}
{"x": 8, "y": 4, "text": "digit char", "color": "", "tags": [], "group": ""}
{"x": 8, "y": 6, "text": "whitespace char", "color": "", "tags": [], "group": ""}
{"x": 4, "y": 10, "text": "\\b", "color": "keystrokes", "tags": [], "group": ""}
{"x": 8, "y": 10, "text": "word boundary", "color": "", "tags": [], "group": ""}
{"x": 8, "y": 8, "text": "word char", "color": "", "tags": [], "group": ""}
{"x": 18, "y": 8, "text": "[a-zA-Z0-9_]", "color": "keystrokes", "tags": [], "group": ""}
{"x": 36, "y": 7, "text": "(\u2026)", "color": "keystrokes", "tags": [], "group": ""}
{"x": 34, "y": 8, "text": "(?:\u2026)", "color": "keystrokes", "tags": [], "group": ""}
{"x": 8, "y": 11, "text": "any of", "color": "", "tags": [], "group": ""}
{"x": 15, "y": 11, "text": "a", "color": "keystrokes", "tags": [], "group": ""}
{"x": 17, "y": 11, "text": "or", "color": "", "tags": [], "group": ""}
{"x": 20, "y": 11, "text": "b", "color": "keystrokes", "tags": [], "group": ""}
{"x": 22, "y": 11, "text": "or", "color": "", "tags": [], "group": ""}
{"x": 25, "y": 11, "text": "c", "color": "keystrokes", "tags": [], "group": ""}
{"x": 19, "y": 4, "text": "[0-9]", "color": "keystrokes", "tags": [], "group": ""}
{"x": 42, "y": 7, "text": "capturing group", "color": "", "tags": [], "group": ""}
{"x": 42, "y": 8, "text": "non-capturing group", "color": "", "tags": [], "group": ""}
{"x": 9, "y": 12, "text": "", "color": "", "tags": [], "group": ""}
{"x": 8, "y": 12, "text": "not", "color": "", "tags": [], "group": ""}
{"x": 12, "y": 12, "text": "a", "color": "keystrokes", "tags": [], "group": ""}
{"x": 14, "y": 12, "text": "or", "color": "", "tags": [], "group": ""}
{"x": 17, "y": 12, "text": "b", "color": "keystrokes", "tags": [], "group": ""}
{"x": 19, "y": 12, "text": "or", "color": "", "tags": [], "group": ""}
{"x": 22, "y": 12, "text": "c", "color": "keystrokes", "tags": [], "group": ""}
{"x": 38, "y": 10, "text": "|", "color": "keystrokes", "tags": [], "group": ""}
{"x": 42, "y": 10, "text": "logical OR", "color": "", "tags": [], "group": ""}
{"x": 4, "y": 5, "text": "\\D", "color": "keystrokes", "tags": [], "group": ""}
{"x": 4, "y": 7, "text": "\\S", "color": "keystrokes", "tags": [], "group": ""}
{"x": 4, "y": 9, "text": "\\W", "color": "keystrokes", "tags": [], "group": ""}
{"x": 8, "y": 5, "text": "non-digit char", "color": "", "tags": [], "group": ""}
{"x": 8, "y": 7, "text": "non-whitespace char", "color": "", "tags": [], "group": ""}
{"x": 8, "y": 9, "text": "non-word char", "color": "", "tags": [], "group": ""}
{"x": 36, "y": 4, "text": "{m}", "color": "keystrokes", "tags": [], "group": ""}
{"x": 42, "y": 4, "text": "exactly m", "color": "", "tags": [], "group": ""}
{"x": 4, "y": 0, "text": "Character classes", "color": "bold underline", "tags": [], "group": ""}
{"x": 35, "y": 0, "text": "Repetition", "color": "bold underline", "tags": [], "group": ""}
{"x": 2, "y": 15, "text": "For full documentation on Python regular expressions, see", "color": "", "tags": [], "group": ""}
{"x": 6, "y": 16, "text": "https://docs.python.org/3/library/re.html", "color": "underline", "tags": [], "group": "", "href": "https://docs.python.org/3/library/re.html"}
{"x": 2, "y": 14, "text": "Note: regex operations apply to the displayed value in a cell", "color": "", "tags": [], "group": ""}
visidata-3.1.1/visidata/ddwplay.py 0000664 0000000 0000000 00000012153 14770045464 0017157 0 ustar 00root root 0000000 0000000 from collections import defaultdict
import json
import time
from visidata import colors, vd, clipdraw, ColorAttr
__all__ = ['Animation', 'AnimationMgr']
# duplicate of visidata.AttrDict, except default return value is '' instead of None
class AttrDict(dict):
def __getattr__(self, k):
try:
v = self[k]
if isinstance(v, dict):
v = AttrDict(v)
return v
except KeyError as e:
if k.startswith("__"):
raise AttributeError from e
return ''
def __setattr__(self, k, v):
self[k] = v
def __dir__(self):
return self.keys()
class Animation:
def __init__(self, fp):
self.frames = defaultdict(AttrDict) # frame.id -> frame row
self.groups = defaultdict(AttrDict) # group.id -> group row
self.height = 0
self.width = 0
self.load_from(fp)
def iterdeep(self, rows, x=0, y=0, parents=None, **kwargs):
'Walk rows deeply and generate (row, x, y, [ancestors]) for each row, filtering on kwargs.'
for r in rows:
newparents = (parents or []) + [r]
if r.type == 'frame': continue
if r.ref:
assert r.type == 'ref'
g = self.groups[r.ref]
if self.matches(r, kwargs):
yield from self.iterdeep(map(AttrDict, g.rows or []), x+r.x, y+r.y, newparents)
else:
if self.matches(r, kwargs):
yield r, x+r.x, y+r.y, newparents
yield from self.iterdeep(map(AttrDict, r.rows or []), x+r.x, x+r.y, newparents)
def matches(self, row, values):
for k, v in values.items():
actualv = getattr(row, k, None)
if isinstance(actualv, (list, tuple)) and isinstance(v, (list, tuple)):
if not any(x in actualv for x in v):
return False
elif v != actualv:
return False
return True
def load_from(self, fp):
for line in fp.readlines():
r = AttrDict(json.loads(line))
if r.type == 'frame':
f = self.frames[r.id]
f.update(r)
f.rows = []
elif r.type == 'group':
self.groups[r.id].update(r)
f = self.frames[r.frame or '']
if not f.rows:
f.rows = [r]
else:
f.rows.append(r)
self.total_ms = 0
if self.frames:
self.total_ms = sum(f.duration_ms or 0 for f in self.frames.values())
for f in self.frames.values():
for r, x, y, _ in self.iterdeep(f.rows):
self.width = max(self.width, x+len(r.text))
self.height = max(self.height, y)
def draw(self, scr, *, t=0, x=0, y=0, loop=False, attr=ColorAttr(), **kwargs):
windowHeight, windowWidth = scr.getmaxyx()
for r, dx, dy, _ in self.iterdeep(self.frames[''].rows, **kwargs):
text = f'[:onclick {r.href}]{r.text}[/]' if r.href else r.text
if y+dy < windowHeight:
clipdraw(scr, y+dy, x+dx, text, attr.update(colors[r.color], 2))
if not self.total_ms:
return None
ms = int(t*1000) % self.total_ms
for f in self.frames.values():
ms -= int(f.duration_ms or 0)
if ms < 0:
for r, dx, dy, _ in self.iterdeep(f.rows, **kwargs):
text = f'[:onclick {r.href}]{r.text}[/]' if r.href else r.text
if y+dy < windowHeight:
clipdraw(scr, y+dy, x+dx, text, colors[r.color])
return -ms/1000
if loop:
return -ms/1000
class AnimationMgr:
def __init__(self):
self.library = {} # animation name -> Animation
self.active = [] # list of (start_time, Animation, kwargs)
def trigger(self, name, **kwargs):
if name in self.library:
self.active.append((time.time(), self.library[name], kwargs))
else:
vd.debug('unknown drawing "%s"' % name)
def load(self, name, fp):
self.library[name] = Animation(fp)
@property
def maxHeight(self):
return max(anim.height for _, anim, _ in self.active) if self.active else 0
@property
def maxWidth(self):
return max(anim.width for _, anim, _ in self.active) if self.active else 0
def draw(self, scr, t=None, **kwargs):
'Draw all active animations on *scr* at time *t*. Return next t to be called at.'
if t is None:
t = time.time()
times = []
done = []
for row in self.active:
startt, anim, akwargs = row
kwargs.update(akwargs)
nextt = anim.draw(scr, t=t+startt, **kwargs)
if nextt is None:
if not akwargs.get('loop', True):
done.append(row)
else:
times.append(t+nextt)
for row in done:
self.active.remove(row)
return min(times) if times else None
vd.addGlobals({'Animation': Animation, 'AnimationMgr': AnimationMgr})
visidata-3.1.1/visidata/deprecated.py 0000664 0000000 0000000 00000022013 14770045464 0017607 0 ustar 00root root 0000000 0000000 import functools
from visidata import VisiData, vd
import visidata
alias = visidata.BaseSheet.bindkey
def deprecated_warn(func, ver, instead):
import traceback
msg = f'{func.__name__} deprecated since v{ver}'
if instead:
msg += f'; use {instead}'
vd.warning(msg)
if vd.options.debug:
for line in reversed(traceback.extract_stack(limit=7)[:-2]):
vd.warning(f' {line.name} at {line.filename}:{line.lineno}')
vd.warning(f'Deprecated call traceback (most recent last):')
def deprecated(ver, instead=''):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
deprecated_warn(func, ver, instead)
return func(*args, **kwargs)
return wrapper
return decorator
@deprecated('1.6', 'vd instead of vd()')
@VisiData.api
def __call__(vd):
'Deprecated; use plain "vd"'
return vd
@deprecated('1.6')
def copyToClipboard(value):
vd.error("copyToClipboard longer implemented")
return visidata.clipboard_copy(value)
@deprecated('1.6')
def replayableOption(optname, default, helpstr):
vd.option(optname, default, helpstr, replay=True)
@deprecated('1.6')
def SubrowColumn(*args, **kwargs):
return visidata.SubColumnFunc(*args, **kwargs)
@deprecated('1.6')
def DeferredSetColumn(*args, **kwargs):
return visidata.Column(*args, defer=True, **kwargs)
@deprecated('2.0')
def bindkey_override(keystrokes, longname):
vd.bindkeys.set(keystrokes, longname)
bindkey = visidata.BaseSheet.bindkey
unbindkey = visidata.BaseSheet.unbindkey
@deprecated('2.0')
@visidata.Sheet.api
def exec_keystrokes(self, keystrokes, vdglobals=None):
return self.execCommand(self.getCommand(keystrokes), vdglobals, keystrokes=keystrokes)
visidata.Sheet.exec_command = deprecated('2.0')(visidata.Sheet.execCommand)
@deprecated('2.0', 'def open_ instead')
@VisiData.api
def filetype(vd, ext, constructor):
'Add constructor to handle the given file type/extension.'
globals().setdefault('open_'+ext, lambda p,ext=ext: constructor(p.base_stem, source=p, filetype=ext))
@deprecated('2.0', 'Sheet(namepart1, namepart2, ...)')
@VisiData.global_api
def joinSheetnames(vd, *sheetnames):
'Concatenate sheet names in a standard way'
return visidata.options.name_joiner.join(str(x) for x in sheetnames)
@deprecated('2.0', 'PyobjSheet')
@VisiData.global_api
def load_pyobj(*names, **kwargs):
return visidata.PyobjSheet(*names, **kwargs)
@deprecated('2.0', 'PyobjSheet')
@VisiData.global_api
def push_pyobj(name, pyobj):
vs = visidata.PyobjSheet(name, source=pyobj)
if vs:
return vd.push(vs)
else:
vd.error("cannot push '%s' as pyobj" % type(pyobj).__name__)
@deprecated('2.1', 'vd.isNumeric instead')
def isNumeric(col):
return vd.isNumeric(col)
visidata.addGlobals({'load_pyobj': load_pyobj, 'isNumeric': isNumeric})
# The longnames on the left are deprecated for 2.0
alias('edit-cells', 'setcol-input')
alias('fill-nulls', 'setcol-fill')
alias('paste-cells', 'setcol-clipboard')
alias('frequency-rows', 'frequency-summary')
alias('dup-cell', 'dive-cell')
alias('dup-row', 'dive-row')
alias('next-search', 'search-next')
alias('prev-search', 'search-prev')
alias('search-prev', 'searchr-next')
alias('prev-sheet', 'jump-prev')
alias('prev-value', 'go-prev-value')
alias('next-value', 'go-next-value')
alias('prev-selected', 'go-prev-selected')
alias('next-selected', 'go-next-selected')
alias('prev-null', 'go-prev-null')
alias('next-null', 'go-next-null')
alias('page-right', 'go-right-page')
alias('page-left', 'go-left-page')
alias('dive-cell', 'open-cell')
alias('dive-row', 'open-row')
alias('add-sheet', 'open-new')
alias('save-sheets-selected', 'save-selected')
alias('join-sheets', 'join-selected')
alias('dive-rows', 'dive-selected')
# v2.3
alias('show-aggregate', 'memo-aggregate')
#theme('use_default_colors', True, 'curses use default terminal colors')
#option('expand_col_scanrows', 1000, 'number of rows to check when expanding columns (0 = all)')
# 2.6
def clean_name(s):
return visidata.vd.cleanName(s)
def maybe_clean(s, vs):
if (vs or visidata.vd).options.clean_names:
s = visidata.vd.cleanName(s)
return s
def load_tsv(fn):
vs = open_tsv(Path(fn))
yield from vs.iterload()
# NOTE: you cannot use deprecated() with nonfuncs
cancelThread = deprecated('2.6', 'vd.cancelThread')(vd.cancelThread)
status = deprecated('2.6', 'vd.status')(vd.status)
warning = deprecated('2.6', 'vd.warning')(vd.warning)
error = deprecated('2.6', 'vd.error')(vd.error)
debug = deprecated('2.6', 'vd.debug')(vd.debug)
fail = deprecated('2.6', 'vd.fail')(vd.fail)
option = theme = vd.option # deprecated('2.6', 'vd.option')(vd.option)
jointypes = vd.jointypes # deprecated('2.6', 'vd.jointypes')(vd.jointypes)
confirm = deprecated('2.6', 'vd.confirm')(vd.confirm)
launchExternalEditor = deprecated('2.6', 'vd.launchExternalEditor')(vd.launchExternalEditor)
launchEditor = deprecated('2.6', 'vd.launchEditor')(vd.launchEditor)
exceptionCaught = deprecated('2.6', 'vd.exceptionCaught')(vd.exceptionCaught)
openSource = deprecated('2.6', 'vd.openSource')(vd.openSource)
globalCommand = visidata.BaseSheet.addCommand
visidata.Sheet.StaticColumn = deprecated('2.11', 'Sheet.freeze_col')(visidata.Sheet.freeze_col)
#visidata.Path.open_text = deprecated('3.0', 'visidata.Path.open')(visidata.Path.open) # undeprecated in 3.1
vd.sysclip_value = deprecated('3.0', 'vd.sysclipValue')(vd.sysclipValue)
def itemsetter(i):
def g(obj, v):
obj[i] = v
return g
vd.optalias('force_valid_colnames', 'clean_names')
vd.optalias('dir_recurse', 'dir_depth', 100000)
vd.optalias('confirm_overwrite', 'overwrite', 'confirm')
vd.optalias('show_graph_labels', 'disp_graph_labels')
vd.optalias('zoom_incr', 'disp_zoom_incr')
alias('visibility-sheet', 'toggle-multiline')
alias('visibility-col', 'toggle-multiline')
def clean_to_id(s):
return visidata.vd.cleanName(s)
@deprecated('3.0', 'use try/finally')
class OnExit:
'"with OnExit(func, ...):" calls func(...) when the context is exited'
def __init__(self, func, *args, **kwargs):
self.func = func
self.args = args
self.kwargs = kwargs
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, exc_traceback):
try:
self.func(*self.args, **self.kwargs)
except Exception as e:
vd.exceptionCaught(e)
alias('open-inputs', 'open-input-history')
#vd.option('plugins_url', 'https://visidata.org/plugins/plugins.jsonl', 'source of plugins sheet')
@visidata.VisiData.api
def inputRegexSubstOld(vd, prompt):
'Input regex transform via oneliner (separated with `/`). Return parsed transformer as dict(before=, after=).'
rex = vd.inputRegex(prompt, type='regex-subst')
before, after = vd.parse_sed_transform(rex)
return dict(before=before, after=after)
visidata.Sheet.addCommand('', 'addcol-subst', 'addColumnAtCursor(Column(cursorCol.name + "_re", getter=regexTransform(cursorCol, **inputRegexSubstOld("transform column by regex: "))))', 'add column derived from current column, replacing regex with subst (may include \1 backrefs)', deprecated=True)
visidata.Sheet.addCommand('', 'setcol-subst', 'setValuesFromRegex([cursorCol], someSelectedRows, **inputRegexSubstOld("transform column by regex: "))', 'regex/subst - modify selected rows in current column, replacing regex with subst, (may include backreferences \\1 etc)', deprecated=True)
visidata.Sheet.addCommand('', 'setcol-subst-all', 'setValuesFromRegex(visibleCols, someSelectedRows, **inputRegexSubstOld(f"transform {nVisibleCols} columns by regex: "))', 'modify selected rows in all visible columns, replacing regex with subst (may include \\1 backrefs)', deprecated=True)
visidata.Sheet.addCommand('', 'split-col', 'addRegexColumns(makeRegexSplitter, cursorCol, inputRegex("split regex: ", type="regex-split"))', 'Add new columns from regex split', deprecated=True)
visidata.Sheet.addCommand('', 'capture-col', 'addRegexColumns(makeRegexMatcher, cursorCol, inputRegex("capture regex: ", type="regex-capture"))', 'add new column from capture groups of regex; requires example row', deprecated=True)
#vd.option('cmdlog_histfile', '', 'file to autorecord each cmdlog action to', sheettype=None)
#BaseSheet.bindkey('KEY_BACKSPACE', 'menu-help')
@deprecated('3.0', 'vd.callNoExceptions(col.setValue, row, value)')
@visidata.Column.api
def setValueSafe(self, row, value):
'setValue and ignore exceptions.'
return vd.callNoExceptions(self.setValue, row, value)
@deprecated('3.0', 'vd.callNoExceptions(sheet.checkCursor)')
@visidata.BaseSheet.api
def checkCursorNoExceptions(sheet):
return vd.callNoExceptions(sheet.checkCursor)
@deprecated('3.1', 'vd.memoValue(name, value, displayvalue)')
@VisiData.api
def memo(vd, name, col, row):
return vd.memoValue(name, col.getTypedValue(row), col.getDisplayValue(row))
alias('view-cell', 'pyobj-cell')
vd.optalias('textwrap_cells', 'disp_wrap_max_lines', 3) # wordwrap text for multiline rows
@deprecated('3.1', 'sheet.rowname(row)')
@visidata.TableSheet.api
def keystr(sheet, row):
return sheet.rowname(row)
visidata-3.1.1/visidata/desktop/ 0000775 0000000 0000000 00000000000 14770045464 0016610 5 ustar 00root root 0000000 0000000 visidata-3.1.1/visidata/desktop/visidata.desktop 0000664 0000000 0000000 00000000175 14770045464 0022012 0 ustar 00root root 0000000 0000000 [Desktop Entry]
Type=Application
Terminal=true
Name=VisiData
Icon=utilities-terminal
Exec=vd %F
Categories=Utility;TextTools
visidata-3.1.1/visidata/editor.py 0000664 0000000 0000000 00000005655 14770045464 0017012 0 ustar 00root root 0000000 0000000 import os
import sys
import signal
import subprocess
import tempfile
import curses
import visidata
visidata.vd.tstp_signal = None
class SuspendCurses:
'Context manager to leave windowed mode on enter and restore it on exit.'
def __enter__(self):
if visidata.vd.scrFull:
curses.endwin()
if visidata.vd.tstp_signal:
signal.signal(signal.SIGTSTP, visidata.vd.tstp_signal)
def __exit__(self, exc_type, exc_val, tb):
if visidata.vd.scrFull:
curses.reset_prog_mode()
visidata.vd.scrFull.refresh()
curses.doupdate()
@visidata.VisiData.api
def launchEditor(vd, *args):
'Launch $EDITOR with *args* as arguments.'
editor = os.environ.get('EDITOR') or vd.fail('$EDITOR not set')
args = editor.split() + list(args)
with SuspendCurses():
return subprocess.call(args)
@visidata.VisiData.api
def launchBrowser(vd, *args):
'Launch $BROWSER with *args* as arguments.'
browser = os.environ.get('BROWSER') or vd.fail('no $BROWSER for %s' % args[0])
vd.status('opening ' + args[0])
args = [browser] + list(args)
subprocess.call(args)
@visidata.VisiData.api
def launchExternalEditor(vd, v, linenum=0):
'Launch $EDITOR to edit string *v* starting on line *linenum*.'
import tempfile
with tempfile.NamedTemporaryFile() as temp:
temp.close() #2118 must close before re-opening on windows
with open(temp.name, 'w') as fp:
fp.write(v)
return vd.launchExternalEditorPath(visidata.Path(temp.name), linenum)
@visidata.VisiData.api
def launchExternalEditorPath(vd, path, linenum=0):
'Launch $EDITOR to edit *path* starting on line *linenum*.'
if linenum:
visidata.vd.launchEditor(path, '+%s' % linenum)
else:
visidata.vd.launchEditor(path)
with open(path, 'r') as fp:
try:
return fp.read().rstrip('\n') # trim inevitable trailing newlines
except Exception as e:
visidata.vd.exceptionCaught(e)
return ''
@visidata.VisiData.api
def suspend(vd):
import signal
with SuspendCurses():
os.kill(os.getpid(), signal.SIGSTOP)
def _breakpoint(*args, **kwargs):
import pdb
class VisiDataPdb(pdb.Pdb):
def precmd(self, line):
r = super().precmd(line)
if not r:
SuspendCurses.__exit__(None, None, None, None)
return r
def postcmd(self, stop, line):
if stop:
SuspendCurses.__enter__(None)
return super().postcmd(stop, line)
SuspendCurses.__enter__(None)
VisiDataPdb(nosigint=True).set_trace()
sys.breakpointhook = _breakpoint
visidata.BaseSheet.addCommand('^Z', 'suspend', 'suspend()', 'suspend VisiData process')
visidata.BaseSheet.addCommand('', 'breakpoint', 'breakpoint()', 'drop into pdb REPL')
visidata.vd.addGlobals(SuspendCurses=SuspendCurses)
visidata-3.1.1/visidata/errors.py 0000664 0000000 0000000 00000002113 14770045464 0017022 0 ustar 00root root 0000000 0000000 import traceback
from visidata import vd, VisiData
vd.option('debug', False, 'exit on error and display stacktrace')
class ExpectedException(Exception):
'Controlled Exception from fail() or confirm(). Status or other interface update is done by raiser.'
pass
def stacktrace(e=None):
if not e:
return traceback.format_exc().strip().splitlines()
return traceback.format_exception_only(type(e), e)
@VisiData.api
def exceptionCaught(vd, exc=None, status=True, **kwargs):
'Add *exc* to list of last errors and add to status history. Show on left status bar if *status* is True. Reraise exception if options.debug is True.'
if isinstance(exc, ExpectedException): # already reported, don't log
return
vd.lastErrors.append(stacktrace())
if status:
vd.status(f'{type(exc).__name__}: {exc}', priority=2)
else:
vd.addToStatusHistory(vd.lastErrors[-1][-1])
if vd.options.debug:
raise
vd.addGlobals(stacktrace=stacktrace, ExpectedException=ExpectedException)
# see textsheet.py for ErrorSheet and associated commands
visidata-3.1.1/visidata/experimental/ 0000775 0000000 0000000 00000000000 14770045464 0017634 5 ustar 00root root 0000000 0000000 visidata-3.1.1/visidata/experimental/__init__.py 0000664 0000000 0000000 00000000000 14770045464 0021733 0 ustar 00root root 0000000 0000000 visidata-3.1.1/visidata/experimental/diff_sheet.py 0000664 0000000 0000000 00000002113 14770045464 0022303 0 ustar 00root root 0000000 0000000 # command setdiff-sheet adds a diff colorizer for all sheets against current sheet
from visidata import Sheet, CellColorizer, vd
vd.theme_option('color_diff', 'red', 'color of values different from --diff source')
vd.theme_option('color_diff_add', 'yellow', 'color of rows/columns added to --diff source')
def makeDiffColorizer(othersheet):
def colorizeDiffs(sheet, col, row, cellval):
if row is None or col is None:
return None
vcolidx = sheet.visibleCols.index(col)
rowidx = sheet.rows.index(row)
if vcolidx < len(othersheet.visibleCols) and rowidx < len(othersheet.rows):
otherval = othersheet.visibleCols[vcolidx].getDisplayValue(othersheet.rows[rowidx])
if cellval.display != otherval:
return 'color_diff'
else:
return 'color_diff_add'
return colorizeDiffs
@Sheet.api
def setDiffSheet(vs):
Sheet.colorizers.append(CellColorizer(8, None, makeDiffColorizer(vs)))
Sheet.addCommand(None, 'setdiff-sheet', 'setDiffSheet()', 'set this sheet as diff sheet for all new sheets')
visidata-3.1.1/visidata/experimental/digit_autoedit.py 0000664 0000000 0000000 00000000460 14770045464 0023204 0 ustar 00root root 0000000 0000000 'Enter edit mode automatically when typing numeric digits.'
from visidata import Sheet
for i in range(0, 10):
Sheet.addCommand(str(i), 'autoedit-%s' % i, 'cursorCol.setValues([cursorRow], editCell(cursorVisibleColIndex, value="%s", i=1))' % i, 'replace cell value with input starting with %s' % i)
visidata-3.1.1/visidata/experimental/gdrive.py 0000664 0000000 0000000 00000005525 14770045464 0021475 0 ustar 00root root 0000000 0000000 import visidata
from visidata import vd, VisiData, Sheet, IndexSheet, SequenceSheet, ColumnItem, Path, AttrDict, ColumnAttr, asyncthread, Progress, ColumnExpr, date
from .gsheets import GSheetsIndex
@VisiData.api
def open_gdrive(vd, p):
return GDriveSheet(p.base_stem)
FILES_FIELDS_VISIBLE='''name size modifiedTime mimeType description'''.split()
FILES_FIELDS='''
id name size modifiedTime mimeType description owners
starred properties spaces version webContentLink webViewLink sharingUser lastModifyingUser shared
ownedByMe originalFilename md5Checksum size quotaBytesUsed headRevisionId imageMediaMetadata videoMediaMetadata parents
exportLinks contentRestrictions contentHints trashed
'''.split()
@VisiData.cached_property
def _drivebuild(vd):
return vd.google_discovery.build("drive", "v3", credentials=vd.google_auth('drive.readonly'))
@VisiData.cached_property
def _gdrive(self):
return vd.google_discovery.build("drive", "v3", credentials=vd.google_auth('drive.readonly')).files()
@VisiData.cached_property
def _gdrive_rw(self):
return vd.google_discovery.build("drive", "v3", credentials=vd.google_auth('drive')).files()
class GDriveSheet(Sheet):
rowtype='files' # rowdef: AttrDict of result from Google Drive files.list API
defer=True
columns = [
ColumnItem('name'),
ColumnItem('size', type=int),
ColumnItem('modifiedTime', type=date),
ColumnItem('mimeType'),
ColumnItem('name'),
ColumnExpr('owner', expr='owners[0]["displayName"]')
] + [
ColumnItem(x, width=0) for x in FILES_FIELDS if x not in FILES_FIELDS_VISIBLE
]
def iterload(self):
self.results = []
page_token = None
while True:
ret = vd._gdrive.list(
pageSize=1000,
pageToken=page_token,
fields="nextPageToken, files(%s)" % ','.join(FILES_FIELDS)
).execute()
self.results.append(ret)
for r in ret.get('files', []):
yield AttrDict(r)
page_token = ret.get('nextPageToken', None)
if not page_token:
break
def openRow(self, r):
if r.mimeType == 'application/vnd.google-apps.spreadsheet':
return GSheetsIndex(r.name, source=r.id)
if r.mimeType.startswith('image'):
return vd.launchBrowser(r.webViewLink)
return vd.openSource(r.webContentLink)
@asyncthread
def deleteFile(self, **kwargs):
with Progress(total=1) as prog:
vd._gdrive_rw.delete(**kwargs).execute()
prog.addProgress(1)
@asyncthread
def putChanges(self):
adds, mods, dels = self.getDeferredChanges()
for row in Progress(dels.values()):
self.deleteFile(fileId=row.id)
vd.sync()
self.preloadHook()
self.reload()
visidata-3.1.1/visidata/experimental/google.py 0000664 0000000 0000000 00000002705 14770045464 0021466 0 ustar 00root root 0000000 0000000 '''
# Using VisiData with Google Sheets and Google Drive
## Setup and Authentication
Add to .visidatarc:
import visidata.experimental.google
When VisiData attempts to use the Google API, it uses the "web authentication flow", which causes a web page to open asking for permissions to read and/or write your Google Sheets.
After granting permissions, VisiData caches the auth token in the .visidata directory. Remove `.visidata/google-*.pickle` to unauthenticate.
## Load a Google Sheet into VisiData
Use VisiData to open the URL or spreadsheet id, with filetype `g` (or `gsheets`):
vd https://docs.google.com/spreadsheets/d/1WV0JI_SsGfmoocXWJILK2nhfcxU1H7roqL1HE7zBdsY/ -f g
VisiData assumes the first row is the header row with column names.
## Save one or more sheets in VisiData as a Google Sheet
Save to `.g` using either `Ctrl+S` (current sheet only) or `g Ctrl+S` (all sheets on the sheet stack).
will be the visible name of the Spreadsheet in Google Drive; each sheet tab within the Spreadsheet will be named according to the sheet name within VisiData.
## List files in Google Drive
Use the `gdrive` filetype (the path doesn't matter):
vd . -f gdrive
- Files can be marked for deletion with `d` and execute those deletions with `z Ctrl+S` (same as on the DirSheet for the local filesystem).
- Images can be viewed with `Enter` (in browser).
'''
import visidata.experimental.gdrive
import visidata.experimental.gsheets
visidata-3.1.1/visidata/experimental/gsheets.py 0000664 0000000 0000000 00000005466 14770045464 0021663 0 ustar 00root root 0000000 0000000 import re
from visidata import vd, VisiData, Sheet, IndexSheet, SequenceSheet, ColumnItem, Path, AttrDict, ColumnAttr, asyncthread
SPREADSHEET_FIELDS='properties sheets namedRanges spreadsheetUrl developerMetadata dataSources dataSourceSchedules'.split()
SHEET_FIELDS='merges conditionalFormats filterViews protectedRanges basicFilter charts bandedRanges developerMetadata rowGroups columnGroups slicers'.split()
@VisiData.api
def open_gsheets(vd, p):
m = re.search(r'([A-z0-9_]{44})', p.given)
if m:
return GSheetsIndex(p.base_stem, source=m.groups()[0])
vd.open_g = vd.open_gsheets
@VisiData.lazy_property
def google_discovery(self):
googleapiclient = vd.importExternal('googleapiclient', 'google-api-python-client')
from googleapiclient import discovery
return discovery
@VisiData.cached_property
def _gsheets(vd):
return vd.google_discovery.build("sheets", "v4", credentials=vd.google_auth('spreadsheets.readonly')).spreadsheets()
@VisiData.cached_property
def _gsheets_rw(vd):
return vd.google_discovery.build("sheets", "v4", credentials=vd.google_auth('spreadsheets')).spreadsheets()
class GSheetsIndex(Sheet):
columns = [
ColumnAttr('title', 'properties.title'),
ColumnAttr('type', 'properties.sheetType', width=0),
ColumnAttr('nRows', 'properties.gridProperties.rowCount', type=int),
ColumnAttr('nCols', 'properties.gridProperties.columnCount', type=int),
]
def iterload(self):
googlesheet = vd._gsheets.get(spreadsheetId=self.source, fields=','.join(SPREADSHEET_FIELDS)).execute()
vd.status(googlesheet['properties']['title'])
for gsheet in googlesheet['sheets']:
yield AttrDict(gsheet)
def openRow(self, r):
return GSheet(r.properties.title, source=self.source)
class GSheet(SequenceSheet):
'.source is gsheet id; .name is sheet name'
def iterload(self):
result = vd._gsheets.values().get(spreadsheetId=self.source, range=self.name).execute()
yield from result.get('values', [])
@VisiData.api
def save_gsheets(vd, p, *sheets):
gsheet = vd._gsheets_rw.create(body={
'properties': { 'title': p.base_stem },
'sheets': list({'properties': { 'title': vs.name }} for vs in sheets),
}, fields='spreadsheetId').execute()
gsheetId = gsheet.get('spreadsheetId')
vd.status(f'https://docs.google.com/spreadsheets/d/{gsheetId}/')
for vs in sheets:
rows = [list(c.name for c in vs.visibleCols)]
rows += list(list(val for col, val in row.items())
for row in vs.iterdispvals(*vs.visibleCols, format=True))
vd._gsheets_rw.values().append(
spreadsheetId=gsheetId,
valueInputOption='RAW',
range=vs.name,
body=dict(values=rows)
).execute()
vd.save_g = vd.save_gsheets
visidata-3.1.1/visidata/experimental/live_search.py 0000664 0000000 0000000 00000002307 14770045464 0022474 0 ustar 00root root 0000000 0000000 from copy import copy
from visidata import Sheet, vd, asyncsingle
@Sheet.api
def dup_search(sheet, cols='cursorCol'):
vs = copy(sheet)
vs.name += "_search"
vs.rows = sheet.rows
vs.source = sheet
vs.search = ''
@asyncsingle
def live_search_async(val, status=False):
if not val:
vs.rows = vs.source.rows
else:
vs.rows = []
for i in vd.searchRegex(vs.source, regex=val, columns=cols, printStatus=status):
vs.addRow(vs.source.rows[i])
def live_search(val):
vs.draw(vs._scr)
vd.drawRightStatus(vs._scr, vs)
val = val.rstrip('\n')
if val == vs.search:
return
vs.search = val
live_search_async(val, sheet=vs, status=False)
vd.input("search regex: ", updater=live_search)
vd.push(vs)
vs.name = vs.source.name+'_'+vs.search
Sheet.addCommand('^[s', 'dup-search', 'dup_search("cursorCol")', 'search for regex forwards in current column, creating duplicate sheet with matching rows live')
Sheet.addCommand('g^[s', 'dup-search-cols', 'dup_search("visibleCols")', 'search for regex forwards in all columns, creating duplicate sheet with matching rows live')
visidata-3.1.1/visidata/experimental/liveupdate.py 0000664 0000000 0000000 00000003050 14770045464 0022346 0 ustar 00root root 0000000 0000000 from visidata import Column, vd, ColumnExpr, CompleteExpr, EscapeException, Sheet
@Column.api
def updateExpr(col, val):
col.name = val
try:
col.expr = val
except SyntaxError:
col.expr = None
col.sheet.draw(col.sheet._scr)
@Column.api # expr.setter
def expr(self, expr):
try:
self.compiledExpr = compile(expr, '', 'eval') if expr else None
self._expr = expr
except SyntaxError as e:
self._expr = None
@Sheet.api
def addcol_expr(sheet):
try:
c = sheet.addColumnAtCursor(ColumnExpr("", width=sheet.options.default_width))
oldidx = sheet.cursorVisibleColIndex
sheet.cursorVisibleColIndex = sheet.visibleCols.index(c)
expr = sheet.editCell(sheet.cursorVisibleColIndex, -1,
completer=CompleteExpr(sheet),
updater=lambda val,col=c: col.updateExpr(val))
c.expr = expr or vd.fail("no expr")
c.name = expr
c.width = None
except (Exception, EscapeException):
sheet.columns.remove(c)
sheet.cursorVisibleColIndex = oldidx
raise
Sheet.addCommand(None, 'addcol-expr', 'sheet.addcol_expr()', "create new column from Python expression, updating the column's calculated values live")
Sheet.addCommand(None, 'addcol-new', 'c=addColumnAtIndex(SettableColumn(width=options.default_width)); draw(sheet._scr); cursorVisibleColIndex=visibleCols.index(c); c.name=editCell(cursorVisibleColIndex, -1); c.width=None', 'append new column, updating the column name live')
visidata-3.1.1/visidata/experimental/mark.py 0000664 0000000 0000000 00000011370 14770045464 0021142 0 ustar 00root root 0000000 0000000 '''
Marking selected rows with a keystroke, selecting marked rows,
and viewing lists of marks and their rows.
'''
from copy import copy
from visidata import vd, asyncthread, vlen, VisiData, TableSheet, ColumnItem, RowColorizer
@VisiData.lazy_property
def marks(vd):
return MarksSheet('marks')
class MarkSheet(TableSheet):
pass
class MarksSheet(TableSheet):
'''
The Marks Sheet shows all marks in use (on all sheets) and how many rows have each mark.
'''
rowtype = "marks" # rowdef: [mark, color, [rows]]
columns = [
ColumnItem('mark', 0),
ColumnItem('color', 1),
ColumnItem('rows', 2, type=vlen),
]
colorizers = [
RowColorizer(2, None, lambda s,c,r,v: r and r[1])
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.marknotes = list('0123456789')
self.marks = [] #
self.markedRows = {} # rowid(row): [row, set(marks)]
self.rows = []
def getColor(self, sheet, row):
mark = self.getMark(sheet, row)
if not mark:
return ''
return self.getMarkRow(sheet, mark)[1]
def getMark(self, sheet, row):
mrow = self.markedRows.get(sheet.rowid(row), None)
if not mrow:
return ''
if mrow[1]:
return next(iter(mrow[1])) # first item in set
def getMarks(self, row):
'Return set of all marks for given row'
return self.markedRows[self.rowid(row)][1]
def isMarked(self, row, mark):
'Return True if given row has given mark'
return mark in self.getMarks(row)
def getMarkRow(self, sheet, mark):
for r in self.rows:
if r[0] == mark:
return r
r = [mark, 'color_note_type', MarkSheet('mark_', rows=[], columns=copy(sheet.columns))]
self.addRow(r)
return r
def setMark(self, sheet, row, mark):
rowid = self.rowid(row)
if rowid not in self.markedRows:
self.markedRows[rowid] = [row, set(mark)]
else:
self.markedRows[rowid][1].add(mark)
vd.marks.getMarkRow(sheet, mark)[2].addRow(row)
def unsetMark(self, sheet, row, mark):
rowid = self.rowid(row)
if rowid in self.markedRows:
self.markedRows[rowid][1].remove(mark)
vd.marks.getMarkRow(sheet, mark)[2].deleteBy(lambda r,x=row: r is x)
def inputmark(self):
return vd.inputsingle('mark: ') or self.marknotes.pop(0)
def openRow(self, row):
return row[2]
@VisiData.api
@asyncthread
def mark(vd, sheet, rows, m):
for r in rows:
vd.marks.setMark(sheet, r, m)
@VisiData.api
@asyncthread
def unmark(vd, sheet, rows, m):
for r in rows:
vd.marks.unsetMark(sheet, r, m)
vd.rowNoters.insert(0, lambda sheet, row: vd.marks.getMark(sheet, row))
TableSheet.colorizers.append(RowColorizer(2, None, lambda s,c,r,v: not c and r and vd.marks.getColor(s, r)))
TableSheet.addCommand('', 'mark-row', 'vd.mark(sheet, [cursorRow], vd.marks.inputmark())', '')
TableSheet.addCommand('', 'unmark-row', 'vd.unmark(sheet, [cursorRow], vd.marks.inputmark())', '')
TableSheet.addCommand('', 'mark-selected', 'vd.mark(sheet, selectedRows, vd.marks.inputmark())', '')
TableSheet.addCommand('', 'unmark-selected', 'vd.unmark(sheet, selectedRows, vd.marks.inputmark())', '')
TableSheet.addCommand('', 'select-marks', 'select(gatherBy(lambda r,mark=vd.marks.inputmark(): vd.marks.isMarked(r, mark)), progress=False)', '')
TableSheet.addCommand('', 'stoggle-marks', 'toggle(gatherBy(lambda r,mark=vd.marks.inputmark(): vd.marks.isMarked(r, mark)), progress=False)', '')
TableSheet.addCommand('', 'unselect-marks', 'unselect(gatherBy(lambda r,mark=vd.marks.inputmark(): vd.marks.isMarked(r, mark)), progress=False)', '')
TableSheet.addCommand('', 'open-marks', 'vd.push(vd.marks)', '')
TableSheet.addCommand('', 'go-prev-mark', 'moveToNextRow(lambda row,mark=vd.marks.inputmark(): vd.marks.isMarked(row, mark), reverse=True, msg="no previous marked row")', 'go up current column to previous row with given mark')
TableSheet.addCommand('', 'go-next-mark', 'moveToNextRow(lambda row,mark=vd.marks.inputmark(): vd.marks.isMarked(row, mark), msg="no next marked row")', 'go down current column to next row with given mark')
vd.addMenuItems('''
View > Marks > open-marks
Row > Mark > open Marks Sheet > open-marks
Row > Mark > current row > mark-row
Row > Mark > selected rows > mark-selected
Row > Unmark > current row > unmark-row
Row > Unmark > selected rows > unmark-selected
Row > Select > marked rows > select-marks
Row > Unselect > marked rows > unselect-marks
Row > Toggle select > marked rows > stoggle-marks
Row > Goto > next marked row > go-next-mark
Row > Goto > previous marked row > go-prev-mark
''')
visidata-3.1.1/visidata/experimental/noahs_tapestry/ 0000775 0000000 0000000 00000000000 14770045464 0022677 5 ustar 00root root 0000000 0000000 visidata-3.1.1/visidata/experimental/noahs_tapestry/__init__.py 0000664 0000000 0000000 00000000027 14770045464 0025007 0 ustar 00root root 0000000 0000000 from . import tapestry
visidata-3.1.1/visidata/experimental/noahs_tapestry/clues.json 0000664 0000000 0000000 00000000407 14770045464 0024706 0 ustar 00root root 0000000 0000000 {"cleanerinits": "DS", "p3_sign": "Libra", "p3_signsymbol": "\ufe0e", "p3_signadj": "balanced", "p3_chinesezodiac": "Goat", "clue5a": "in the Bronx", "clue6a": "Deborah", "clue6b": "who lives here i│·n Bronx", "clue7a": "in Astoria", "clue8a": "in Manhattan"}
visidata-3.1.1/visidata/experimental/noahs_tapestry/flame.ddw 0000664 0000000 0000000 00000073525 14770045464 0024477 0 ustar 00root root 0000000 0000000 {"id": "0", "type": "frame", "text": "", "color": "", "tags": [], "group": "", "duration_ms": 100}
{"id": "1", "type": "frame", "text": "", "color": "", "tags": [], "group": "", "duration_ms": 100}
{"id": "2", "type": "frame", "text": "", "color": "", "tags": [], "group": "", "duration_ms": 100}
{"id": "3", "type": "frame", "text": "", "color": "", "tags": [], "group": "", "duration_ms": 100}
{"id": "4", "type": "frame", "text": "", "color": "", "tags": [], "group": "", "duration_ms": 100}
{"id": "5", "type": "frame", "text": "", "color": "", "tags": [], "group": "", "duration_ms": 100}
{"id": "6", "type": "frame", "text": "", "color": "", "tags": [], "group": "", "duration_ms": 100}
{"id": "7", "type": "frame", "text": "", "color": "", "tags": [], "group": "", "duration_ms": 100}
{"id": "8", "type": "frame", "text": "", "color": "", "tags": [], "group": "", "duration_ms": 100}
{"id": "9", "type": "frame", "text": "", "color": "", "tags": [], "group": "", "duration_ms": 100}
{"id": "10", "type": "frame", "text": "", "color": "", "tags": [], "group": "", "duration_ms": 100}
{"id": "11", "type": "frame", "text": "", "color": "", "tags": [], "group": "", "duration_ms": 100}
{"id": "12", "type": "frame", "text": "", "color": "", "tags": [], "group": "", "duration_ms": 100}
{"id": "13", "type": "frame", "text": "", "color": "", "tags": [], "group": "", "duration_ms": 100}
{"id": "14", "type": "frame", "text": "", "color": "", "tags": [], "group": "", "duration_ms": 100}
{"id": "15", "type": "frame", "text": "", "color": "", "tags": [], "group": "", "duration_ms": 100}
{"id": "16", "type": "frame", "text": "", "color": "", "tags": [], "group": "", "duration_ms": 100}
{"id": "17", "type": "frame", "text": "", "color": "", "tags": [], "group": "", "duration_ms": 100}
{"id": "18", "type": "frame", "text": "", "color": "", "tags": [], "group": "", "duration_ms": 100}
{"id": "19", "type": "frame", "text": "", "color": "", "tags": [], "group": "", "duration_ms": 100}
{"id": "20", "type": "frame", "text": "", "color": "", "tags": [], "group": "", "duration_ms": 100}
{"id": "21", "type": "frame", "text": "", "color": "", "tags": [], "group": "", "duration_ms": 100}
{"id": "22", "type": "frame", "text": "", "color": "", "tags": [], "group": "", "duration_ms": 100}
{"id": "23", "type": "frame", "text": "", "color": "", "tags": [], "group": "", "duration_ms": 100}
{"id": "24", "type": "frame", "text": "", "color": "", "tags": [], "group": "", "duration_ms": 100}
{"id": "25", "type": "frame", "text": "", "color": "", "tags": [], "group": "", "duration_ms": 100}
{"id": "26", "type": "frame", "text": "", "color": "", "tags": [], "group": "", "duration_ms": 100}
{"id": "27", "type": "frame", "text": "", "color": "", "tags": [], "group": "", "duration_ms": 100}
{"id": "28", "type": "frame", "text": "", "color": "", "tags": [], "group": "", "duration_ms": 100}
{"id": "29", "type": "frame", "text": "", "color": "", "tags": [], "group": "", "duration_ms": 100}
{"id": "30", "type": "frame", "text": "", "color": "", "tags": [], "group": "", "duration_ms": 100}
{"id": "31", "type": "frame", "text": "", "color": "", "tags": [], "group": "", "duration_ms": 100}
{"id": "32", "type": "frame", "text": "", "color": "", "tags": [], "group": "", "duration_ms": 100}
{"id": "33", "type": "frame", "text": "", "color": "", "tags": [], "group": "", "duration_ms": 100}
{"id": "34", "type": "frame", "text": "", "color": "", "tags": [], "group": "", "duration_ms": 100}
{"id": "35", "type": "frame", "text": "", "color": "", "tags": [], "group": "", "duration_ms": 100}
{"id": "36", "type": "frame", "text": "", "color": "", "tags": [], "group": "", "duration_ms": 100}
{"id": "37", "type": "frame", "text": "", "color": "", "tags": [], "group": "", "duration_ms": 100}
{"id": "38", "type": "frame", "text": "", "color": "", "tags": [], "group": "", "duration_ms": 100}
{"id": "39", "type": "frame", "text": "", "color": "", "tags": [], "group": "", "duration_ms": 100}
{"id": "40", "type": "frame", "text": "", "color": "", "tags": [], "group": "", "duration_ms": 100}
{"id": "41", "type": "frame", "text": "", "color": "", "tags": [], "group": "", "duration_ms": 100}
{"x": 0, "y": 2, "text": "\u239d", "color": "202", "tags": [], "group": "", "frame": "0"}
{"x": 2, "y": 2, "text": "\u23a0", "color": "202", "tags": [], "group": "", "frame": "0"}
{"x": 2, "y": 1, "text": "\u239e", "color": "214 bold", "tags": [], "group": "", "frame": "0"}
{"x": 1, "y": 2, "text": "\u203f", "color": "228 bold", "tags": [], "group": "", "frame": "0"}
{"x": 1, "y": 1, "text": "\u239e", "color": "230 bold", "tags": [], "group": "", "frame": "0"}
{"x": 0, "y": 1, "text": "\u23a0", "color": "214 bold", "tags": [], "group": "", "frame": "0"}
{"x": 1, "y": 0, "text": "\u239b", "color": "228 bold", "tags": [], "group": "", "frame": "0"}
{"x": 0, "y": 2, "text": "\u239d", "color": "202", "tags": [], "group": "", "frame": "1"}
{"x": 2, "y": 2, "text": "\u23a0", "color": "202", "tags": [], "group": "", "frame": "1"}
{"x": 2, "y": 1, "text": "\u239e", "color": "214 bold", "tags": [], "group": "", "frame": "1"}
{"x": 1, "y": 2, "text": "\u203f", "color": "228 bold", "tags": [], "group": "", "frame": "1"}
{"x": 1, "y": 0, "text": "\u239b", "color": "228 bold", "tags": [], "group": "", "frame": "1"}
{"x": 0, "y": 1, "text": "\u23a0", "color": "214 bold", "tags": [], "group": "", "frame": "1"}
{"x": 1, "y": 1, "text": "\u239d", "color": "230 bold", "tags": [], "group": "", "frame": "1"}
{"x": 0, "y": 2, "text": "\u239d", "color": "202", "tags": [], "group": "", "frame": "2"}
{"x": 2, "y": 2, "text": "\u23a0", "color": "202", "tags": [], "group": "", "frame": "2"}
{"x": 2, "y": 1, "text": "\u239e", "color": "214 bold", "tags": [], "group": "", "frame": "2"}
{"x": 1, "y": 2, "text": "\u203f", "color": "228 bold", "tags": [], "group": "", "frame": "2"}
{"x": 1, "y": 0, "text": "\u239b", "color": "228 bold", "tags": [], "group": "", "frame": "2"}
{"x": 1, "y": 1, "text": "\u239d", "color": "230 bold", "tags": [], "group": "", "frame": "2"}
{"x": 0, "y": 1, "text": "\u239b", "color": "214 bold", "tags": [], "group": "", "frame": "2"}
{"x": 0, "y": 2, "text": "\u239d", "color": "202", "tags": [], "group": "", "frame": "3"}
{"x": 2, "y": 2, "text": "\u23a0", "color": "202", "tags": [], "group": "", "frame": "3"}
{"x": 1, "y": 2, "text": "\u203f", "color": "228 bold", "tags": [], "group": "", "frame": "3"}
{"x": 1, "y": 0, "text": "\u239b", "color": "228 bold", "tags": [], "group": "", "frame": "3"}
{"x": 1, "y": 1, "text": "\u239d", "color": "230 bold", "tags": [], "group": "", "frame": "3"}
{"x": 0, "y": 1, "text": "\u239b", "color": "214 bold", "tags": [], "group": "", "frame": "3"}
{"x": 2, "y": 1, "text": "\u02ce", "color": "214 bold", "tags": [], "group": "", "frame": "3"}
{"x": 0, "y": 2, "text": "\u239d", "color": "202", "tags": [], "group": "", "frame": "4"}
{"x": 0, "y": 1, "text": "\u239b", "color": "214 bold", "tags": [], "group": "", "frame": "4"}
{"x": 1, "y": 2, "text": "\u203f", "color": "228 bold", "tags": [], "group": "", "frame": "4"}
{"x": 2, "y": 2, "text": ")", "color": "202", "tags": [], "group": "", "frame": "4"}
{"x": 1, "y": 1, "text": "\u239d", "color": "230 bold", "tags": [], "group": "", "frame": "4"}
{"x": 1, "y": 0, "text": "\u239b", "color": "228 bold", "tags": [], "group": "", "frame": "4"}
{"x": 0, "y": 2, "text": "\u239d", "color": "202", "tags": [], "group": "", "frame": "5"}
{"x": 0, "y": 1, "text": "\u239b", "color": "214 bold", "tags": [], "group": "", "frame": "5"}
{"x": 1, "y": 2, "text": "\u203f", "color": "228 bold", "tags": [], "group": "", "frame": "5"}
{"x": 2, "y": 2, "text": ")", "color": "202", "tags": [], "group": "", "frame": "5"}
{"x": 1, "y": 0, "text": "\u239b", "color": "228 bold", "tags": [], "group": "", "frame": "5"}
{"x": 1, "y": 1, "text": "(", "color": "230 bold", "tags": [], "group": "", "frame": "5"}
{"x": 0, "y": 2, "text": "\u239d", "color": "202", "tags": [], "group": "", "frame": "6"}
{"x": 0, "y": 1, "text": "\u239b", "color": "214 bold", "tags": [], "group": "", "frame": "6"}
{"x": 1, "y": 2, "text": "\u203f", "color": "228 bold", "tags": [], "group": "", "frame": "6"}
{"x": 2, "y": 2, "text": ")", "color": "202", "tags": [], "group": "", "frame": "6"}
{"x": 1, "y": 1, "text": "(", "color": "230 bold", "tags": [], "group": "", "frame": "6"}
{"x": 1, "y": 0, "text": "\u23a0", "color": "228 bold", "tags": [], "group": "", "frame": "6"}
{"x": 0, "y": 2, "text": "\u239d", "color": "202", "tags": [], "group": "", "frame": "7"}
{"x": 0, "y": 1, "text": "\u239b", "color": "214 bold", "tags": [], "group": "", "frame": "7"}
{"x": 1, "y": 2, "text": "\u203f", "color": "228 bold", "tags": [], "group": "", "frame": "7"}
{"x": 2, "y": 2, "text": ")", "color": "202", "tags": [], "group": "", "frame": "7"}
{"x": 1, "y": 1, "text": "(", "color": "230 bold", "tags": [], "group": "", "frame": "7"}
{"x": 2, "y": 0, "text": "\u23a0", "color": "228 bold", "tags": [], "group": "", "frame": "7"}
{"x": 1, "y": 0, "text": "\u02ec", "color": "202", "tags": [], "group": "", "frame": "7"}
{"x": 1, "y": 0, "text": ",", "color": "202", "tags": [], "group": "", "frame": "7"}
{"x": 0, "y": 2, "text": "\u239d", "color": "202", "tags": [], "group": "", "frame": "8"}
{"x": 1, "y": 2, "text": "\u203f", "color": "228 bold", "tags": [], "group": "", "frame": "8"}
{"x": 2, "y": 2, "text": ")", "color": "202", "tags": [], "group": "", "frame": "8"}
{"x": 1, "y": 1, "text": "(", "color": "230 bold", "tags": [], "group": "", "frame": "8"}
{"x": 2, "y": 0, "text": "\u23a0", "color": "228 bold", "tags": [], "group": "", "frame": "8"}
{"x": 1, "y": 0, "text": "\u02ec", "color": "202", "tags": [], "group": "", "frame": "8"}
{"x": 1, "y": 0, "text": ",", "color": "202", "tags": [], "group": "", "frame": "8"}
{"x": 0, "y": 1, "text": "\u23a0", "color": "214 bold", "tags": [], "group": "", "frame": "8"}
{"x": 0, "y": 2, "text": "\u239d", "color": "202", "tags": [], "group": "", "frame": "9"}
{"x": 1, "y": 2, "text": "\u203f", "color": "228 bold", "tags": [], "group": "", "frame": "9"}
{"x": 2, "y": 2, "text": ")", "color": "202", "tags": [], "group": "", "frame": "9"}
{"x": 1, "y": 1, "text": "(", "color": "230 bold", "tags": [], "group": "", "frame": "9"}
{"x": 1, "y": 0, "text": "\u23a0", "color": "228 bold", "tags": [], "group": "", "frame": "9"}
{"x": 0, "y": 1, "text": "\u23a0", "color": "214 bold", "tags": [], "group": "", "frame": "9"}
{"x": 1, "y": 2, "text": "\u203f", "color": "228 bold", "tags": [], "group": "", "frame": "10"}
{"x": 1, "y": 0, "text": "\u23a0", "color": "228 bold", "tags": [], "group": "", "frame": "10"}
{"x": 0, "y": 2, "text": "\u239d", "color": "202", "tags": [], "group": "", "frame": "10"}
{"x": 0, "y": 1, "text": "\u23a0", "color": "214 bold", "tags": [], "group": "", "frame": "10"}
{"x": 2, "y": 2, "text": ")", "color": "202", "tags": [], "group": "", "frame": "10"}
{"x": 1, "y": 1, "text": "\u239d", "color": "230 bold", "tags": [], "group": "", "frame": "10"}
{"x": 0, "y": 2, "text": "\u239d", "color": "202", "tags": [], "group": "", "frame": "11"}
{"x": 0, "y": 1, "text": "\u239b", "color": "214 bold", "tags": [], "group": "", "frame": "11"}
{"x": 1, "y": 0, "text": "\u23a0", "color": "228 bold", "tags": [], "group": "", "frame": "11"}
{"x": 1, "y": 2, "text": "\u203f", "color": "228 bold", "tags": [], "group": "", "frame": "11"}
{"x": 2, "y": 2, "text": ")", "color": "202", "tags": [], "group": "", "frame": "11"}
{"x": 1, "y": 1, "text": "\u239d", "color": "230 bold", "tags": [], "group": "", "frame": "11"}
{"x": 0, "y": 2, "text": "\u239d", "color": "202", "tags": [], "group": "", "frame": "12"}
{"x": 0, "y": 1, "text": "\u239b", "color": "214 bold", "tags": [], "group": "", "frame": "12"}
{"x": 1, "y": 2, "text": "\u203f", "color": "228 bold", "tags": [], "group": "", "frame": "12"}
{"x": 2, "y": 2, "text": ")", "color": "202", "tags": [], "group": "", "frame": "12"}
{"x": 1, "y": 1, "text": "\u239d", "color": "230 bold", "tags": [], "group": "", "frame": "12"}
{"x": 0, "y": 0, "text": "\u239e", "color": "228 bold", "tags": [], "group": "", "frame": "12"}
{"x": 0, "y": 2, "text": "\u239d", "color": "202", "tags": [], "group": "", "frame": "13"}
{"x": 0, "y": 1, "text": "\u239b", "color": "214 bold", "tags": [], "group": "", "frame": "13"}
{"x": 1, "y": 2, "text": "\u203f", "color": "228 bold", "tags": [], "group": "", "frame": "13"}
{"x": 2, "y": 2, "text": ")", "color": "202", "tags": [], "group": "", "frame": "13"}
{"x": 1, "y": 1, "text": "\u239d", "color": "230 bold", "tags": [], "group": "", "frame": "13"}
{"x": 1, "y": 0, "text": "\u239e", "color": "228 bold", "tags": [], "group": "", "frame": "13"}
{"x": 0, "y": 2, "text": "\u239d", "color": "202", "tags": [], "group": "", "frame": "14"}
{"x": 0, "y": 1, "text": "\u239b", "color": "214 bold", "tags": [], "group": "", "frame": "14"}
{"x": 1, "y": 2, "text": "\u203f", "color": "228 bold", "tags": [], "group": "", "frame": "14"}
{"x": 2, "y": 2, "text": ")", "color": "202", "tags": [], "group": "", "frame": "14"}
{"x": 1, "y": 1, "text": "\u239d", "color": "230 bold", "tags": [], "group": "", "frame": "14"}
{"x": 1, "y": 0, "text": "\u23a0", "color": "228 bold", "tags": [], "group": "", "frame": "14"}
{"x": 1, "y": 2, "text": "\u203f", "color": "228 bold", "tags": [], "group": "", "frame": "15"}
{"x": 0, "y": 2, "text": "\u239d", "color": "202", "tags": [], "group": "", "frame": "15"}
{"x": 2, "y": 2, "text": ")", "color": "202", "tags": [], "group": "", "frame": "15"}
{"x": 2, "y": 1, "text": "\u239d", "color": "214 bold", "tags": [], "group": "", "frame": "15"}
{"x": 1, "y": 0, "text": "\u23a0", "color": "228 bold", "tags": [], "group": "", "frame": "15"}
{"x": 0, "y": 1, "text": "\u239b", "color": "214 bold", "tags": [], "group": "", "frame": "15"}
{"x": 1, "y": 1, "text": "\u239d", "color": "230 bold", "tags": [], "group": "", "frame": "15"}
{"x": 1, "y": 2, "text": "\u203f", "color": "228 bold", "tags": [], "group": "", "frame": "16"}
{"x": 0, "y": 2, "text": "\u239d", "color": "202", "tags": [], "group": "", "frame": "16"}
{"x": 0, "y": 1, "text": "\u239d", "color": "214 bold", "tags": [], "group": "", "frame": "16"}
{"x": 0, "y": 1, "text": "\u23a0", "color": "214 bold", "tags": [], "group": "", "frame": "16"}
{"x": 1, "y": 0, "text": "\u23a0", "color": "228 bold", "tags": [], "group": "", "frame": "16"}
{"x": 2, "y": 1, "text": "\u239d", "color": "214 bold", "tags": [], "group": "", "frame": "16"}
{"x": 2, "y": 2, "text": "\u23a0", "color": "202", "tags": [], "group": "", "frame": "16"}
{"x": 1, "y": 1, "text": "\u239d", "color": "230 bold", "tags": [], "group": "", "frame": "16"}
{"x": 1, "y": 2, "text": "\u203f", "color": "228 bold", "tags": [], "group": "", "frame": "17"}
{"x": 0, "y": 2, "text": "\u239d", "color": "202", "tags": [], "group": "", "frame": "17"}
{"x": 0, "y": 1, "text": "\u239d", "color": "214 bold", "tags": [], "group": "", "frame": "17"}
{"x": 0, "y": 1, "text": "\u23a0", "color": "214 bold", "tags": [], "group": "", "frame": "17"}
{"x": 1, "y": 1, "text": "\u239b", "color": "230 bold", "tags": [], "group": "", "frame": "17"}
{"x": 1, "y": 0, "text": "\u23a0", "color": "228 bold", "tags": [], "group": "", "frame": "17"}
{"x": 2, "y": 1, "text": "\u239d", "color": "214 bold", "tags": [], "group": "", "frame": "17"}
{"x": 2, "y": 2, "text": "\u23a0", "color": "202", "tags": [], "group": "", "frame": "17"}
{"x": 1, "y": 2, "text": "\u203f", "color": "228 bold", "tags": [], "group": "", "frame": "18"}
{"x": 0, "y": 2, "text": "\u239d", "color": "202", "tags": [], "group": "", "frame": "18"}
{"x": 0, "y": 1, "text": "\u239d", "color": "214 bold", "tags": [], "group": "", "frame": "18"}
{"x": 0, "y": 1, "text": "\u23a0", "color": "214 bold", "tags": [], "group": "", "frame": "18"}
{"x": 1, "y": 1, "text": "\u239b", "color": "230 bold", "tags": [], "group": "", "frame": "18"}
{"x": 1, "y": 0, "text": "\u23a0", "color": "228 bold", "tags": [], "group": "", "frame": "18"}
{"x": 2, "y": 2, "text": "\u23a0", "color": "202", "tags": [], "group": "", "frame": "18"}
{"x": 2, "y": 1, "text": "\u239e", "color": "214 bold", "tags": [], "group": "", "frame": "18"}
{"x": 1, "y": 2, "text": "\u203f", "color": "228 bold", "tags": [], "group": "", "frame": "19"}
{"x": 0, "y": 2, "text": "\u239d", "color": "202", "tags": [], "group": "", "frame": "19"}
{"x": 1, "y": 1, "text": "\u239b", "color": "230 bold", "tags": [], "group": "", "frame": "19"}
{"x": 1, "y": 0, "text": "\u23a0", "color": "228 bold", "tags": [], "group": "", "frame": "19"}
{"x": 2, "y": 2, "text": "\u23a0", "color": "202", "tags": [], "group": "", "frame": "19"}
{"x": 2, "y": 1, "text": "\u239e", "color": "214 bold", "tags": [], "group": "", "frame": "19"}
{"x": 0, "y": 1, "text": "\u239b", "color": "214 bold", "tags": [], "group": "", "frame": "19"}
{"x": 1, "y": 2, "text": "\u203f", "color": "228 bold", "tags": [], "group": "", "frame": "20"}
{"x": 0, "y": 2, "text": "\u239d", "color": "202", "tags": [], "group": "", "frame": "20"}
{"x": 1, "y": 1, "text": "\u239b", "color": "230 bold", "tags": [], "group": "", "frame": "20"}
{"x": 2, "y": 2, "text": "\u23a0", "color": "202", "tags": [], "group": "", "frame": "20"}
{"x": 2, "y": 1, "text": "\u239e", "color": "214 bold", "tags": [], "group": "", "frame": "20"}
{"x": 0, "y": 1, "text": "\u239b", "color": "214 bold", "tags": [], "group": "", "frame": "20"}
{"x": 1, "y": 0, "text": "\u239e", "color": "228 bold", "tags": [], "group": "", "frame": "20"}
{"x": 1, "y": 0, "text": "\u239e", "color": "228 bold", "tags": [], "group": "", "frame": "21"}
{"x": 2, "y": 1, "text": "\u239d", "color": "214 bold", "tags": [], "group": "", "frame": "21"}
{"x": 2, "y": 2, "text": "\u23a0", "color": "202", "tags": [], "group": "", "frame": "21"}
{"x": 1, "y": 1, "text": "\u239b", "color": "230 bold", "tags": [], "group": "", "frame": "21"}
{"x": 0, "y": 2, "text": "\u239d", "color": "202", "tags": [], "group": "", "frame": "21"}
{"x": 1, "y": 2, "text": "\u203f", "color": "228 bold", "tags": [], "group": "", "frame": "21"}
{"x": 0, "y": 1, "text": "\u239b", "color": "214 bold", "tags": [], "group": "", "frame": "21"}
{"x": 1, "y": 0, "text": "\u239e", "color": "228 bold", "tags": [], "group": "", "frame": "22"}
{"x": 2, "y": 2, "text": "\u23a0", "color": "202", "tags": [], "group": "", "frame": "22"}
{"x": 0, "y": 2, "text": "\u239d", "color": "202", "tags": [], "group": "", "frame": "22"}
{"x": 1, "y": 2, "text": "\u203f", "color": "228 bold", "tags": [], "group": "", "frame": "22"}
{"x": 0, "y": 1, "text": "\u239b", "color": "214 bold", "tags": [], "group": "", "frame": "22"}
{"x": 1, "y": 1, "text": "\u23a0", "color": "230 bold", "tags": [], "group": "", "frame": "22"}
{"x": 2, "y": 1, "text": "\u239d", "color": "214 bold", "tags": [], "group": "", "frame": "22"}
{"x": 1, "y": 0, "text": "\u239e", "color": "228 bold", "tags": [], "group": "", "frame": "23"}
{"x": 2, "y": 2, "text": "\u23a0", "color": "202", "tags": [], "group": "", "frame": "23"}
{"x": 0, "y": 2, "text": "\u239d", "color": "202", "tags": [], "group": "", "frame": "23"}
{"x": 1, "y": 2, "text": "\u203f", "color": "228 bold", "tags": [], "group": "", "frame": "23"}
{"x": 0, "y": 1, "text": "\u239b", "color": "214 bold", "tags": [], "group": "", "frame": "23"}
{"x": 1, "y": 1, "text": "\u23a0", "color": "230 bold", "tags": [], "group": "", "frame": "23"}
{"x": 2, "y": 1, "text": "\u239e", "color": "214 bold", "tags": [], "group": "", "frame": "23"}
{"x": 1, "y": 0, "text": "\u239e", "color": "228 bold", "tags": [], "group": "", "frame": "24"}
{"x": 2, "y": 2, "text": "\u23a0", "color": "202", "tags": [], "group": "", "frame": "24"}
{"x": 0, "y": 2, "text": "\u239d", "color": "202", "tags": [], "group": "", "frame": "24"}
{"x": 1, "y": 2, "text": "\u203f", "color": "228 bold", "tags": [], "group": "", "frame": "24"}
{"x": 1, "y": 1, "text": "\u23a0", "color": "230 bold", "tags": [], "group": "", "frame": "24"}
{"x": 2, "y": 1, "text": "\u239e", "color": "214 bold", "tags": [], "group": "", "frame": "24"}
{"x": 0, "y": 1, "text": "\u02ec", "color": "202", "tags": [], "group": "", "frame": "24"}
{"x": 0, "y": 1, "text": ",", "color": "202", "tags": [], "group": "", "frame": "24"}
{"x": 1, "y": 0, "text": "\u239e", "color": "228 bold", "tags": [], "group": "", "frame": "25"}
{"x": 2, "y": 2, "text": "\u23a0", "color": "202", "tags": [], "group": "", "frame": "25"}
{"x": 1, "y": 2, "text": "\u203f", "color": "228 bold", "tags": [], "group": "", "frame": "25"}
{"x": 1, "y": 1, "text": "\u23a0", "color": "230 bold", "tags": [], "group": "", "frame": "25"}
{"x": 2, "y": 1, "text": "\u239e", "color": "214 bold", "tags": [], "group": "", "frame": "25"}
{"x": 0, "y": 2, "text": "(", "color": "202", "tags": [], "group": "", "frame": "25"}
{"x": 1, "y": 0, "text": "\u239e", "color": "228 bold", "tags": [], "group": "", "frame": "26"}
{"x": 2, "y": 2, "text": "\u23a0", "color": "202", "tags": [], "group": "", "frame": "26"}
{"x": 1, "y": 2, "text": "\u203f", "color": "228 bold", "tags": [], "group": "", "frame": "26"}
{"x": 2, "y": 1, "text": "\u239e", "color": "214 bold", "tags": [], "group": "", "frame": "26"}
{"x": 0, "y": 2, "text": "(", "color": "202", "tags": [], "group": "", "frame": "26"}
{"x": 1, "y": 1, "text": ")", "color": "230 bold", "tags": [], "group": "", "frame": "26"}
{"x": 2, "y": 2, "text": "\u23a0", "color": "202", "tags": [], "group": "", "frame": "27"}
{"x": 1, "y": 2, "text": "\u203f", "color": "228 bold", "tags": [], "group": "", "frame": "27"}
{"x": 2, "y": 1, "text": "\u239e", "color": "214 bold", "tags": [], "group": "", "frame": "27"}
{"x": 0, "y": 2, "text": "(", "color": "202", "tags": [], "group": "", "frame": "27"}
{"x": 1, "y": 1, "text": ")", "color": "230 bold", "tags": [], "group": "", "frame": "27"}
{"x": 1, "y": 0, "text": "\u239d", "color": "228 bold", "tags": [], "group": "", "frame": "27"}
{"x": 2, "y": 2, "text": "\u23a0", "color": "202", "tags": [], "group": "", "frame": "28"}
{"x": 1, "y": 2, "text": "\u203f", "color": "228 bold", "tags": [], "group": "", "frame": "28"}
{"x": 2, "y": 1, "text": "\u239e", "color": "214 bold", "tags": [], "group": "", "frame": "28"}
{"x": 0, "y": 2, "text": "(", "color": "202", "tags": [], "group": "", "frame": "28"}
{"x": 1, "y": 1, "text": ")", "color": "230 bold", "tags": [], "group": "", "frame": "28"}
{"x": 0, "y": 0, "text": "\u239d", "color": "228 bold", "tags": [], "group": "", "frame": "28"}
{"x": 1, "y": 0, "text": "\u02ce", "color": "202", "tags": [], "group": "", "frame": "28"}
{"x": 2, "y": 2, "text": "\u23a0", "color": "202", "tags": [], "group": "", "frame": "29"}
{"x": 1, "y": 2, "text": "\u203f", "color": "228 bold", "tags": [], "group": "", "frame": "29"}
{"x": 0, "y": 2, "text": "(", "color": "202", "tags": [], "group": "", "frame": "29"}
{"x": 1, "y": 1, "text": ")", "color": "230 bold", "tags": [], "group": "", "frame": "29"}
{"x": 0, "y": 0, "text": "\u239d", "color": "228 bold", "tags": [], "group": "", "frame": "29"}
{"x": 1, "y": 0, "text": "\u02ce", "color": "202", "tags": [], "group": "", "frame": "29"}
{"x": 2, "y": 1, "text": "\u239d", "color": "214 bold", "tags": [], "group": "", "frame": "29"}
{"x": 2, "y": 2, "text": "\u23a0", "color": "202", "tags": [], "group": "", "frame": "30"}
{"x": 1, "y": 2, "text": "\u203f", "color": "228 bold", "tags": [], "group": "", "frame": "30"}
{"x": 0, "y": 2, "text": "(", "color": "202", "tags": [], "group": "", "frame": "30"}
{"x": 1, "y": 1, "text": ")", "color": "230 bold", "tags": [], "group": "", "frame": "30"}
{"x": 1, "y": 0, "text": "\u239d", "color": "228 bold", "tags": [], "group": "", "frame": "30"}
{"x": 2, "y": 1, "text": "\u239d", "color": "214 bold", "tags": [], "group": "", "frame": "30"}
{"x": 2, "y": 2, "text": "\u23a0", "color": "202", "tags": [], "group": "", "frame": "31"}
{"x": 1, "y": 2, "text": "\u203f", "color": "228 bold", "tags": [], "group": "", "frame": "31"}
{"x": 0, "y": 2, "text": "(", "color": "202", "tags": [], "group": "", "frame": "31"}
{"x": 1, "y": 0, "text": "\u239d", "color": "228 bold", "tags": [], "group": "", "frame": "31"}
{"x": 2, "y": 1, "text": "\u239d", "color": "214 bold", "tags": [], "group": "", "frame": "31"}
{"x": 1, "y": 1, "text": "\u23a0", "color": "230 bold", "tags": [], "group": "", "frame": "31"}
{"x": 2, "y": 2, "text": "\u23a0", "color": "202", "tags": [], "group": "", "frame": "32"}
{"x": 1, "y": 2, "text": "\u203f", "color": "228 bold", "tags": [], "group": "", "frame": "32"}
{"x": 0, "y": 2, "text": "(", "color": "202", "tags": [], "group": "", "frame": "32"}
{"x": 1, "y": 0, "text": "\u239d", "color": "228 bold", "tags": [], "group": "", "frame": "32"}
{"x": 1, "y": 1, "text": "\u23a0", "color": "230 bold", "tags": [], "group": "", "frame": "32"}
{"x": 2, "y": 1, "text": "\u239e", "color": "214 bold", "tags": [], "group": "", "frame": "32"}
{"x": 2, "y": 2, "text": "\u23a0", "color": "202", "tags": [], "group": "", "frame": "33"}
{"x": 1, "y": 2, "text": "\u203f", "color": "228 bold", "tags": [], "group": "", "frame": "33"}
{"x": 0, "y": 2, "text": "(", "color": "202", "tags": [], "group": "", "frame": "33"}
{"x": 1, "y": 1, "text": "\u23a0", "color": "230 bold", "tags": [], "group": "", "frame": "33"}
{"x": 2, "y": 1, "text": "\u239e", "color": "214 bold", "tags": [], "group": "", "frame": "33"}
{"x": 2, "y": 0, "text": "\u239b", "color": "228 bold", "tags": [], "group": "", "frame": "33"}
{"x": 2, "y": 2, "text": "\u23a0", "color": "202", "tags": [], "group": "", "frame": "34"}
{"x": 1, "y": 2, "text": "\u203f", "color": "228 bold", "tags": [], "group": "", "frame": "34"}
{"x": 0, "y": 2, "text": "(", "color": "202", "tags": [], "group": "", "frame": "34"}
{"x": 1, "y": 1, "text": "\u23a0", "color": "230 bold", "tags": [], "group": "", "frame": "34"}
{"x": 2, "y": 1, "text": "\u239e", "color": "214 bold", "tags": [], "group": "", "frame": "34"}
{"x": 1, "y": 0, "text": "\u239b", "color": "228 bold", "tags": [], "group": "", "frame": "34"}
{"x": 2, "y": 2, "text": "\u23a0", "color": "202", "tags": [], "group": "", "frame": "35"}
{"x": 1, "y": 2, "text": "\u203f", "color": "228 bold", "tags": [], "group": "", "frame": "35"}
{"x": 0, "y": 2, "text": "(", "color": "202", "tags": [], "group": "", "frame": "35"}
{"x": 1, "y": 1, "text": "\u23a0", "color": "230 bold", "tags": [], "group": "", "frame": "35"}
{"x": 2, "y": 1, "text": "\u239e", "color": "214 bold", "tags": [], "group": "", "frame": "35"}
{"x": 1, "y": 0, "text": "\u239d", "color": "228 bold", "tags": [], "group": "", "frame": "35"}
{"x": 2, "y": 2, "text": "\u23a0", "color": "202", "tags": [], "group": "", "frame": "36"}
{"x": 1, "y": 2, "text": "\u203f", "color": "228 bold", "tags": [], "group": "", "frame": "36"}
{"x": 0, "y": 2, "text": "(", "color": "202", "tags": [], "group": "", "frame": "36"}
{"x": 1, "y": 1, "text": "\u23a0", "color": "230 bold", "tags": [], "group": "", "frame": "36"}
{"x": 2, "y": 1, "text": "\u239e", "color": "214 bold", "tags": [], "group": "", "frame": "36"}
{"x": 1, "y": 0, "text": "\u239d", "color": "228 bold", "tags": [], "group": "", "frame": "36"}
{"x": 0, "y": 1, "text": "\u23a0", "color": "214 bold", "tags": [], "group": "", "frame": "36"}
{"x": 2, "y": 2, "text": "\u23a0", "color": "202", "tags": [], "group": "", "frame": "37"}
{"x": 1, "y": 2, "text": "\u203f", "color": "228 bold", "tags": [], "group": "", "frame": "37"}
{"x": 1, "y": 1, "text": "\u23a0", "color": "230 bold", "tags": [], "group": "", "frame": "37"}
{"x": 1, "y": 0, "text": "\u239d", "color": "228 bold", "tags": [], "group": "", "frame": "37"}
{"x": 0, "y": 1, "text": "\u23a0", "color": "214 bold", "tags": [], "group": "", "frame": "37"}
{"x": 2, "y": 1, "text": "\u239d", "color": "214 bold", "tags": [], "group": "", "frame": "37"}
{"x": 0, "y": 2, "text": "\u239d", "color": "202", "tags": [], "group": "", "frame": "37"}
{"x": 2, "y": 2, "text": "\u23a0", "color": "202", "tags": [], "group": "", "frame": "38"}
{"x": 1, "y": 2, "text": "\u203f", "color": "228 bold", "tags": [], "group": "", "frame": "38"}
{"x": 1, "y": 0, "text": "\u239d", "color": "228 bold", "tags": [], "group": "", "frame": "38"}
{"x": 0, "y": 1, "text": "\u23a0", "color": "214 bold", "tags": [], "group": "", "frame": "38"}
{"x": 2, "y": 1, "text": "\u239d", "color": "214 bold", "tags": [], "group": "", "frame": "38"}
{"x": 1, "y": 1, "text": "\u239e", "color": "230 bold", "tags": [], "group": "", "frame": "38"}
{"x": 0, "y": 2, "text": "\u239d", "color": "202", "tags": [], "group": "", "frame": "38"}
{"x": 2, "y": 2, "text": "\u23a0", "color": "202", "tags": [], "group": "", "frame": "39"}
{"x": 1, "y": 2, "text": "\u203f", "color": "228 bold", "tags": [], "group": "", "frame": "39"}
{"x": 1, "y": 0, "text": "\u239d", "color": "228 bold", "tags": [], "group": "", "frame": "39"}
{"x": 2, "y": 1, "text": "\u239d", "color": "214 bold", "tags": [], "group": "", "frame": "39"}
{"x": 1, "y": 1, "text": "\u239e", "color": "230 bold", "tags": [], "group": "", "frame": "39"}
{"x": 0, "y": 2, "text": "\u239d", "color": "202", "tags": [], "group": "", "frame": "39"}
{"x": 0, "y": 1, "text": "\u239b", "color": "214 bold", "tags": [], "group": "", "frame": "39"}
{"x": 2, "y": 2, "text": "\u23a0", "color": "202", "tags": [], "group": "", "frame": "40"}
{"x": 1, "y": 2, "text": "\u203f", "color": "228 bold", "tags": [], "group": "", "frame": "40"}
{"x": 1, "y": 0, "text": "\u239d", "color": "228 bold", "tags": [], "group": "", "frame": "40"}
{"x": 1, "y": 1, "text": "\u239e", "color": "230 bold", "tags": [], "group": "", "frame": "40"}
{"x": 0, "y": 2, "text": "\u239d", "color": "202", "tags": [], "group": "", "frame": "40"}
{"x": 0, "y": 1, "text": "\u239b", "color": "214 bold", "tags": [], "group": "", "frame": "40"}
{"x": 2, "y": 1, "text": "\u239e", "color": "214 bold", "tags": [], "group": "", "frame": "40"}
{"x": 2, "y": 2, "text": "\u23a0", "color": "202", "tags": [], "group": "", "frame": "41"}
{"x": 1, "y": 2, "text": "\u203f", "color": "228 bold", "tags": [], "group": "", "frame": "41"}
{"x": 1, "y": 1, "text": "\u239e", "color": "230 bold", "tags": [], "group": "", "frame": "41"}
{"x": 0, "y": 2, "text": "\u239d", "color": "202", "tags": [], "group": "", "frame": "41"}
{"x": 0, "y": 1, "text": "\u239b", "color": "214 bold", "tags": [], "group": "", "frame": "41"}
{"x": 2, "y": 1, "text": "\u239e", "color": "214 bold", "tags": [], "group": "", "frame": "41"}
{"x": 1, "y": 0, "text": "\u239b", "color": "228 bold", "tags": [], "group": "", "frame": "41"}
visidata-3.1.1/visidata/experimental/noahs_tapestry/menorah.ddw 0000664 0000000 0000000 00000252035 14770045464 0025037 0 ustar 00root root 0000000 0000000 {"id": "off", "type": "frame", "x": null, "y": null, "text": "", "color": "", "tags": [], "group": "", "frame": null, "rows": null, "duration_ms": 100, "ref": null, "href": null}
{"id": "on", "type": "frame", "text": "", "color": "", "tags": [], "group": "", "duration_ms": 100}
{"x": 4, "y": 5, "text": "\u0194", "color": "88", "tags": ["wick", "day8"], "group": "", "frame": "on", "href": "open-puzzle-8"}
{"x": 13, "y": 5, "text": "\u0194", "color": "88", "tags": ["wick", "day7"], "group": "", "frame": "on", "href": "open-puzzle-7"}
{"x": 22, "y": 5, "text": "\u0194", "color": "88", "tags": ["wick", "day6"], "group": "", "frame": "on", "href": "open-puzzle-6"}
{"x": 31, "y": 5, "text": "\u0194", "color": "88", "tags": ["wick", "day5"], "group": "", "frame": "on", "href": "open-puzzle-5"}
{"x": 49, "y": 5, "text": "\u0194", "color": "88", "tags": ["wick", "day4"], "group": "", "frame": "on", "href": "open-puzzle-4"}
{"x": 58, "y": 5, "text": "\u0194", "color": "88", "tags": ["wick", "day3"], "group": "", "frame": "on", "href": "open-puzzle-3"}
{"x": 67, "y": 5, "text": "\u0194", "color": "88", "tags": ["wick", "day2"], "group": "", "frame": "on", "href": "open-puzzle-2"}
{"x": 76, "y": 5, "text": "\u0194", "color": "88", "tags": ["wick", "day1"], "group": "", "frame": "on", "href": "open-puzzle-1"}
{"x": 40, "y": 1, "text": "\u0194", "color": "88", "tags": ["wick", "day0"], "group": "", "frame": "on", "href": "open-puzzle-0"}
{"x": 40, "y": 3, "text": " ", "color": "", "tags": ["day0"], "group": "", "frame": "on"}
{"x": 40, "y": 4, "text": " ", "color": "", "tags": ["day0"], "group": "", "frame": "on"}
{"x": 39, "y": 4, "text": " ", "color": "", "tags": ["day0"], "group": "", "frame": "on"}
{"x": 40, "y": 5, "text": " ", "color": "", "tags": ["day0"], "group": "", "frame": "on"}
{"x": 41, "y": 5, "text": " ", "color": "", "tags": ["day0"], "group": "", "frame": "on"}
{"x": 41, "y": 6, "text": " ", "color": "", "tags": ["day0"], "group": "", "frame": "on"}
{"x": 39, "y": 6, "text": " ", "color": "", "tags": ["day0"], "group": "", "frame": "on"}
{"x": 4, "y": 7, "text": " ", "color": "", "tags": ["day8"], "group": "", "frame": "on"}
{"x": 4, "y": 8, "text": " ", "color": "", "tags": ["day8"], "group": "", "frame": "on"}
{"x": 3, "y": 8, "text": " ", "color": "", "tags": ["day8"], "group": "", "frame": "on"}
{"x": 4, "y": 9, "text": " ", "color": "", "tags": ["day8"], "group": "", "frame": "on"}
{"x": 5, "y": 9, "text": " ", "color": "", "tags": ["day8"], "group": "", "frame": "on"}
{"x": 5, "y": 10, "text": " ", "color": "", "tags": ["day8"], "group": "", "frame": "on"}
{"x": 3, "y": 10, "text": " ", "color": "", "tags": ["day8"], "group": "", "frame": "on"}
{"x": 39, "y": 7, "text": "\u0268", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 37, "y": 7, "text": "{", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 43, "y": 7, "text": "}", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 38, "y": 7, "text": "\u00ec", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 42, "y": 7, "text": "\u00ed", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 40, "y": 7, "text": "\u0289", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 41, "y": 7, "text": "\u0268", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 48, "y": 11, "text": "\u0268", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 46, "y": 11, "text": "{", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 52, "y": 11, "text": "}", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 47, "y": 11, "text": "\u00ec", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 51, "y": 11, "text": "\u00ed", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 49, "y": 11, "text": "\u0289", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 50, "y": 11, "text": "\u0268", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 57, "y": 11, "text": "\u0268", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 55, "y": 11, "text": "{", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 61, "y": 11, "text": "}", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 56, "y": 11, "text": "\u00ec", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 60, "y": 11, "text": "\u00ed", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 58, "y": 11, "text": "\u0289", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 59, "y": 11, "text": "\u0268", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 66, "y": 11, "text": "\u0268", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 64, "y": 11, "text": "{", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 70, "y": 11, "text": "}", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 65, "y": 11, "text": "\u00ec", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 69, "y": 11, "text": "\u00ed", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 67, "y": 11, "text": "\u0289", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 68, "y": 11, "text": "\u0268", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 75, "y": 11, "text": "\u0268", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 73, "y": 11, "text": "{", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 79, "y": 11, "text": "}", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 74, "y": 11, "text": "\u00ec", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 78, "y": 11, "text": "\u00ed", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 76, "y": 11, "text": "\u0289", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 77, "y": 11, "text": "\u0268", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 3, "y": 11, "text": "\u0268", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 1, "y": 11, "text": "{", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 7, "y": 11, "text": "}", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 3, "y": 12, "text": "\u02ca", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 2, "y": 11, "text": "\u00ec", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 6, "y": 11, "text": "\u00ed", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 5, "y": 12, "text": "\u02cb", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 4, "y": 11, "text": "\u0289", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 5, "y": 11, "text": "\u0268", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 21, "y": 11, "text": "\u0268", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 19, "y": 11, "text": "{", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 25, "y": 11, "text": "}", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 20, "y": 11, "text": "\u00ec", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 24, "y": 11, "text": "\u00ed", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 22, "y": 11, "text": "\u0289", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 23, "y": 11, "text": "\u0268", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 12, "y": 11, "text": "\u0268", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 10, "y": 11, "text": "{", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 16, "y": 11, "text": "}", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 11, "y": 11, "text": "\u00ec", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 15, "y": 11, "text": "\u00ed", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 13, "y": 11, "text": "\u0289", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 14, "y": 11, "text": "\u0268", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 30, "y": 11, "text": "\u0268", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 34, "y": 11, "text": "}", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 29, "y": 11, "text": "\u00ec", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 33, "y": 11, "text": "\u00ed", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 31, "y": 11, "text": "\u0289", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 32, "y": 11, "text": "\u0268", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 28, "y": 11, "text": "{", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 6, "y": 13, "text": "\u23a0", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 2, "y": 13, "text": "\u239d", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 2, "y": 12, "text": ")", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 6, "y": 12, "text": "(", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 75, "y": 12, "text": "\u02ca", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 77, "y": 12, "text": "\u02cb", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 78, "y": 13, "text": "\u23a0", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 74, "y": 13, "text": "\u239d", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 74, "y": 12, "text": ")", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 78, "y": 12, "text": "(", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 12, "y": 12, "text": "\u02ca", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 14, "y": 12, "text": "\u02cb", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 15, "y": 13, "text": "\u23a0", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 11, "y": 13, "text": "\u239d", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 11, "y": 12, "text": ")", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 15, "y": 12, "text": "(", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 3, "y": 14, "text": ")", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 5, "y": 14, "text": "(", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 1, "y": 18, "text": "\u1fed", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 2, "y": 15, "text": "\u1fee", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 2, "y": 18, "text": "\u02d1", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 6, "y": 18, "text": "\u02d1", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 7, "y": 18, "text": "\u1fee", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 12, "y": 14, "text": ")", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 11, "y": 15, "text": "\u1fee", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 14, "y": 14, "text": "(", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 15, "y": 15, "text": ")", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 14, "y": 17, "text": "\\", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 14, "y": 16, "text": "\u239b", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 6, "y": 15, "text": ")", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 5, "y": 16, "text": "\u239b", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 5, "y": 18, "text": ".", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 3, "y": 18, "text": ".", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 4, "y": 18, "text": "\u02cd", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 6, "y": 17, "text": "\u203f", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 7, "y": 17, "text": ".", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 1, "y": 15, "text": "/", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 10, "y": 15, "text": "/", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 5, "y": 17, "text": "\\", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 3, "y": 15, "text": "\u02db", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 5, "y": 15, "text": "\u1fed", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 0, "y": 16, "text": "\u239f", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 2, "y": 16, "text": "|", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 4, "y": 15, "text": "\u2040", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 4, "y": 12, "text": "\u2040", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 3, "y": 13, "text": "\u02ce", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 5, "y": 13, "text": "\u02cf", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 12, "y": 13, "text": "\u02ce", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 14, "y": 13, "text": "\u02cf", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 13, "y": 12, "text": "\u2040", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 12, "y": 15, "text": "\u02db", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 14, "y": 15, "text": "\u1fed", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 13, "y": 15, "text": "\u2040", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 11, "y": 16, "text": "|", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 15, "y": 17, "text": "\u203f", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 16, "y": 17, "text": ".", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 10, "y": 18, "text": "\u1fed", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 11, "y": 18, "text": "\u02d1", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 15, "y": 18, "text": "\u02d1", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 16, "y": 18, "text": "\u1fee", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 14, "y": 18, "text": ".", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 12, "y": 18, "text": ".", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 13, "y": 18, "text": "\u02cd", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 9, "y": 17, "text": "\u2144", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 21, "y": 12, "text": "\u02ca", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 23, "y": 12, "text": "\u02cb", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 24, "y": 13, "text": "\u23a0", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 20, "y": 13, "text": "\u239d", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 20, "y": 12, "text": ")", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 24, "y": 12, "text": "(", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 21, "y": 14, "text": ")", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 20, "y": 15, "text": "\u1fee", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 23, "y": 14, "text": "(", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 24, "y": 15, "text": ")", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 23, "y": 17, "text": "\\", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 23, "y": 16, "text": "\u239b", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 18, "y": 16, "text": "\u239f", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 19, "y": 15, "text": "/", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 21, "y": 13, "text": "\u02ce", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 23, "y": 13, "text": "\u02cf", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 22, "y": 12, "text": "\u2040", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 21, "y": 15, "text": "\u02db", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 23, "y": 15, "text": "\u1fed", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 22, "y": 15, "text": "\u2040", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 20, "y": 16, "text": "|", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 24, "y": 17, "text": "\u203f", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 25, "y": 17, "text": ".", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 19, "y": 18, "text": "\u1fed", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 20, "y": 18, "text": "\u02d1", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 24, "y": 18, "text": "\u02d1", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 25, "y": 18, "text": "\u1fee", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 23, "y": 18, "text": ".", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 21, "y": 18, "text": ".", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 22, "y": 18, "text": "\u02cd", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 18, "y": 17, "text": "\u2144", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 30, "y": 12, "text": "\u02ca", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 32, "y": 12, "text": "\u02cb", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 33, "y": 13, "text": "\u23a0", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 29, "y": 13, "text": "\u239d", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 29, "y": 12, "text": ")", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 33, "y": 12, "text": "(", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 30, "y": 14, "text": ")", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 29, "y": 15, "text": "\u1fee", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 32, "y": 14, "text": "(", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 33, "y": 15, "text": ")", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 32, "y": 17, "text": "\\", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 32, "y": 16, "text": "\u239b", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 27, "y": 16, "text": "\u239f", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 28, "y": 15, "text": "/", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 30, "y": 13, "text": "\u02ce", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 32, "y": 13, "text": "\u02cf", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 31, "y": 12, "text": "\u2040", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 30, "y": 15, "text": "\u02db", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 32, "y": 15, "text": "\u1fed", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 31, "y": 15, "text": "\u2040", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 29, "y": 16, "text": "|", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 33, "y": 17, "text": "\u203f", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 34, "y": 17, "text": ".", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 28, "y": 18, "text": "\u1fed", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 29, "y": 18, "text": "\u02d1", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 33, "y": 18, "text": "\u02d1", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 34, "y": 18, "text": "\u1fee", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 32, "y": 18, "text": ".", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 30, "y": 18, "text": ".", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 31, "y": 18, "text": "\u02cd", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 27, "y": 17, "text": "\u2144", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 39, "y": 8, "text": "\u02ca", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 41, "y": 8, "text": "\u02cb", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 42, "y": 9, "text": "\u23a0", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 38, "y": 9, "text": "\u239d", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 38, "y": 8, "text": ")", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 42, "y": 8, "text": "(", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 39, "y": 10, "text": ")", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 41, "y": 10, "text": "(", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 39, "y": 9, "text": "\u02ce", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 41, "y": 9, "text": "\u02cf", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 40, "y": 8, "text": "\u2040", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 38, "y": 11, "text": "\u1fee", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 42, "y": 11, "text": "\u1fed", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 40, "y": 11, "text": "\u2040", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 37, "y": 11, "text": "(", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 43, "y": 11, "text": ")", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 42, "y": 12, "text": "\u239b", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 38, "y": 12, "text": "\u239e", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 42, "y": 13, "text": "\u239c", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 38, "y": 13, "text": "\u239f", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 39, "y": 11, "text": "\u02ca", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 41, "y": 11, "text": "\u02cb", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 39, "y": 13, "text": "!", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 39, "y": 12, "text": ".", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 41, "y": 12, "text": ".", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 41, "y": 13, "text": "!", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 77, "y": 14, "text": "(", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 75, "y": 14, "text": ")", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 79, "y": 15, "text": "\\", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 78, "y": 15, "text": "\u1fed", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 75, "y": 15, "text": "\u1fee", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 76, "y": 15, "text": "\u2040", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 74, "y": 15, "text": "(", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 75, "y": 16, "text": "\u239e", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 74, "y": 17, "text": "\u203f", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 73, "y": 17, "text": ".", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 80, "y": 16, "text": "\u239c", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 78, "y": 16, "text": "|", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 77, "y": 15, "text": "\u00b8", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 79, "y": 18, "text": "\u1fee", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 74, "y": 18, "text": "\u02d1", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 77, "y": 18, "text": ".", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 75, "y": 18, "text": ".", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 76, "y": 18, "text": "\u02cd", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 78, "y": 18, "text": "\u02d1", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 73, "y": 18, "text": "\u1fed", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 66, "y": 12, "text": "\u02ca", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 68, "y": 12, "text": "\u02cb", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 69, "y": 13, "text": "\u23a0", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 65, "y": 13, "text": "\u239d", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 65, "y": 12, "text": ")", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 69, "y": 12, "text": "(", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 68, "y": 14, "text": "(", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 66, "y": 14, "text": ")", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 70, "y": 15, "text": "\\", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 69, "y": 15, "text": "\u1fed", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 66, "y": 15, "text": "\u1fee", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 67, "y": 15, "text": "\u2040", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 65, "y": 15, "text": "(", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 66, "y": 16, "text": "\u239e", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 65, "y": 17, "text": "\u203f", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 64, "y": 17, "text": ".", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 71, "y": 16, "text": "\u239c", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 69, "y": 16, "text": "|", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 68, "y": 15, "text": "\u00b8", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 70, "y": 18, "text": "\u1fee", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 65, "y": 18, "text": "\u02d1", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 68, "y": 18, "text": ".", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 66, "y": 18, "text": ".", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 67, "y": 18, "text": "\u02cd", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 69, "y": 18, "text": "\u02d1", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 71, "y": 17, "text": "\u2144", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 57, "y": 12, "text": "\u02ca", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 59, "y": 12, "text": "\u02cb", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 60, "y": 13, "text": "\u23a0", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 56, "y": 13, "text": "\u239d", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 56, "y": 12, "text": ")", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 60, "y": 12, "text": "(", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 59, "y": 14, "text": "(", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 57, "y": 14, "text": ")", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 61, "y": 15, "text": "\\", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 60, "y": 15, "text": "\u1fed", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 57, "y": 15, "text": "\u1fee", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 58, "y": 15, "text": "\u2040", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 56, "y": 15, "text": "(", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 57, "y": 16, "text": "\u239e", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 56, "y": 17, "text": "\u203f", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 55, "y": 17, "text": ".", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 62, "y": 16, "text": "\u239c", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 60, "y": 16, "text": "|", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 59, "y": 15, "text": "\u00b8", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 61, "y": 18, "text": "\u1fee", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 56, "y": 18, "text": "\u02d1", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 59, "y": 18, "text": ".", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 57, "y": 18, "text": ".", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 58, "y": 18, "text": "\u02cd", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 60, "y": 18, "text": "\u02d1", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 55, "y": 18, "text": "\u1fed", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 64, "y": 18, "text": "\u1fed", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 62, "y": 17, "text": "\u2144", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 48, "y": 12, "text": "\u02ca", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 50, "y": 12, "text": "\u02cb", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 51, "y": 13, "text": "\u23a0", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 47, "y": 13, "text": "\u239d", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 47, "y": 12, "text": ")", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 51, "y": 12, "text": "(", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 50, "y": 14, "text": "(", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 48, "y": 14, "text": ")", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 52, "y": 15, "text": "\\", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 51, "y": 15, "text": "\u1fed", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 48, "y": 15, "text": "\u1fee", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 49, "y": 15, "text": "\u2040", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 47, "y": 15, "text": "(", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 48, "y": 16, "text": "\u239e", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 47, "y": 17, "text": "\u203f", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 46, "y": 17, "text": ".", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 53, "y": 16, "text": "\u239c", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 51, "y": 16, "text": "|", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 50, "y": 15, "text": "\u00b8", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 52, "y": 18, "text": "\u1fee", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 47, "y": 18, "text": "\u02d1", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 50, "y": 18, "text": ".", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 48, "y": 18, "text": ".", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 49, "y": 18, "text": "\u02cd", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 51, "y": 18, "text": "\u02d1", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 46, "y": 18, "text": "\u1fed", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 53, "y": 17, "text": "\u2144", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 0, "y": 17, "text": "\u239d", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 80, "y": 17, "text": "\u23a0", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 75, "y": 17, "text": "\u23a0", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 66, "y": 17, "text": "\u23a0", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 57, "y": 17, "text": "\u23a0", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 48, "y": 17, "text": "\u23a0", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 78, "y": 17, "text": "\u23a0", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 69, "y": 17, "text": "\u23a0", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 60, "y": 17, "text": "\u23a0", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 51, "y": 17, "text": "\u23a0", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 2, "y": 17, "text": "\u239d", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 11, "y": 17, "text": "\u239d", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 20, "y": 17, "text": "\u239d", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 29, "y": 17, "text": "\u239d", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 36, "y": 17, "text": "\u2144", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 36, "y": 16, "text": "\u239f", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 37, "y": 15, "text": "/", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 38, "y": 15, "text": "\u1fee", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 43, "y": 15, "text": "\\", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 42, "y": 15, "text": "\u1fed", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 44, "y": 16, "text": "\u239c", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 44, "y": 17, "text": "\u2144", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 40, "y": 15, "text": "\u2040", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 39, "y": 15, "text": "\u02db", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 41, "y": 15, "text": "\u00b8", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 38, "y": 16, "text": "|", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 42, "y": 16, "text": "|", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 38, "y": 17, "text": "\u239d", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 42, "y": 17, "text": "\u23a0", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 37, "y": 18, "text": "\\", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 43, "y": 18, "text": "/", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 39, "y": 18, "text": "\u1fed", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 41, "y": 18, "text": "\u1fee", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 67, "y": 12, "text": "\u2040", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 76, "y": 12, "text": "\u2040", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 58, "y": 12, "text": "\u2040", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 49, "y": 12, "text": "\u2040", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 48, "y": 13, "text": "\u02ce", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 50, "y": 13, "text": "\u02cf", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 57, "y": 13, "text": "\u02ce", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 59, "y": 13, "text": "\u02cf", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 66, "y": 13, "text": "\u02ce", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 68, "y": 13, "text": "\u02cf", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 75, "y": 13, "text": "\u02ce", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 77, "y": 13, "text": "\u02cf", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 8, "y": 18, "text": "\u02c9", "color": "250 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 9, "y": 18, "text": "\u02c9", "color": "250 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 8, "y": 17, "text": "\u02cd", "color": "250 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 9, "y": 16, "text": "\u239f", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 17, "y": 18, "text": "\u02c9", "color": "250 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 18, "y": 18, "text": "\u02c9", "color": "250 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 17, "y": 17, "text": "\u02cd", "color": "250 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 26, "y": 18, "text": "\u02c9", "color": "250 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 27, "y": 18, "text": "\u02c9", "color": "250 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 26, "y": 17, "text": "\u02cd", "color": "250 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 35, "y": 18, "text": "\u02c9", "color": "250 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 36, "y": 18, "text": "\u02c9", "color": "250 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 35, "y": 17, "text": "\u02cd", "color": "250 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 45, "y": 17, "text": "\u02cd", "color": "250 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 54, "y": 17, "text": "\u02cd", "color": "250 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 63, "y": 17, "text": "\u02cd", "color": "250 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 72, "y": 17, "text": "\u02cd", "color": "250 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 44, "y": 18, "text": "\u02c9", "color": "250 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 45, "y": 18, "text": "\u02c9", "color": "250 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 53, "y": 18, "text": "\u02c9", "color": "250 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 54, "y": 18, "text": "\u02c9", "color": "250 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 62, "y": 18, "text": "\u02c9", "color": "250 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 63, "y": 18, "text": "\u02c9", "color": "250 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 71, "y": 18, "text": "\u02c9", "color": "250 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 72, "y": 18, "text": "\u02c9", "color": "250 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 39, "y": 19, "text": "\u00b8", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 41, "y": 19, "text": "\u02db", "color": "244 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 38, "y": 20, "text": "\u239f", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 42, "y": 20, "text": "\u239c", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 38, "y": 14, "text": "\u23a0", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 42, "y": 14, "text": "\u239d", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 42, "y": 19, "text": "\u239b", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 38, "y": 19, "text": "\u239e", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 4, "y": 6, "text": "\u02e1", "color": "21 bold", "tags": ["day8"], "group": "", "frame": ""}
{"x": 5, "y": 8, "text": "\u02c8", "color": "21 bold", "tags": ["day8"], "group": "", "frame": ""}
{"x": 3, "y": 9, "text": ".", "color": "21 bold", "tags": ["day8"], "group": "", "frame": ""}
{"x": 4, "y": 10, "text": "\u02c8", "color": "21 bold", "tags": ["day8"], "group": "", "frame": ""}
{"x": 6, "y": 6, "text": "\u03b9", "color": "21 bold", "tags": ["day8"], "group": "", "frame": ""}
{"x": 4, "y": 5, "text": "8", "color": "230 bold", "tags": ["wick", "day8"], "group": "", "frame": "off", "href": "open-puzzle-8"}
{"x": 5, "y": 6, "text": "\u1fed", "color": "21 bold", "tags": ["day8"], "group": "", "frame": ""}
{"x": 3, "y": 6, "text": "\u1fee", "color": "21 bold", "tags": ["day8"], "group": "", "frame": ""}
{"x": 2, "y": 6, "text": "\u2041", "color": "21 bold", "tags": ["day8"], "group": "", "frame": ""}
{"x": 3, "y": 7, "text": "\u00a1", "color": "21 bold", "tags": ["day8"], "group": "", "frame": ""}
{"x": 5, "y": 7, "text": "\u1fcd", "color": "21 bold", "tags": ["day8"], "group": "", "frame": ""}
{"x": 4, "y": 4, "text": "\u02cc", "color": "230 bold", "tags": ["day8"], "group": "", "frame": "off"}
{"x": 6, "y": 7, "text": "\u239f", "color": "21 bold", "tags": ["day8"], "group": "", "frame": ""}
{"x": 6, "y": 8, "text": "\u239f", "color": "21 bold", "tags": ["day8"], "group": "", "frame": ""}
{"x": 6, "y": 9, "text": "\u239f", "color": "21 bold", "tags": ["day8"], "group": "", "frame": ""}
{"x": 6, "y": 10, "text": "\u239f", "color": "21 bold", "tags": ["day8"], "group": "", "frame": ""}
{"x": 2, "y": 7, "text": "\u239c", "color": "21 bold", "tags": ["day8"], "group": "", "frame": ""}
{"x": 2, "y": 8, "text": "\u239c", "color": "21 bold", "tags": ["day8"], "group": "", "frame": ""}
{"x": 2, "y": 9, "text": "\u239c", "color": "21 bold", "tags": ["day8"], "group": "", "frame": ""}
{"x": 2, "y": 10, "text": "\u239c", "color": "21 bold", "tags": ["day8"], "group": "", "frame": ""}
{"x": 13, "y": 6, "text": "\u02e1", "color": "21 bold", "tags": ["day7"], "group": "", "frame": ""}
{"x": 14, "y": 8, "text": "\u02c8", "color": "21 bold", "tags": ["day7"], "group": "", "frame": ""}
{"x": 12, "y": 9, "text": ".", "color": "21 bold", "tags": ["day7"], "group": "", "frame": ""}
{"x": 13, "y": 10, "text": "\u02c8", "color": "21 bold", "tags": ["day7"], "group": "", "frame": ""}
{"x": 15, "y": 6, "text": "\u03b9", "color": "21 bold", "tags": ["day7"], "group": "", "frame": ""}
{"x": 13, "y": 5, "text": "7", "color": "230 bold", "tags": ["wick", "day7"], "group": "", "frame": "off", "href": "open-puzzle-7"}
{"x": 14, "y": 6, "text": "\u1fed", "color": "21 bold", "tags": ["day7"], "group": "", "frame": ""}
{"x": 12, "y": 6, "text": "\u1fee", "color": "21 bold", "tags": ["day7"], "group": "", "frame": ""}
{"x": 11, "y": 6, "text": "\u2041", "color": "21 bold", "tags": ["day7"], "group": "", "frame": ""}
{"x": 12, "y": 7, "text": "\u00a1", "color": "21 bold", "tags": ["day7"], "group": "", "frame": ""}
{"x": 14, "y": 7, "text": "\u1fcd", "color": "21 bold", "tags": ["day7"], "group": "", "frame": ""}
{"x": 13, "y": 4, "text": "\u02cc", "color": "230 bold", "tags": ["day7"], "group": "", "frame": "off"}
{"x": 15, "y": 7, "text": "\u239f", "color": "21 bold", "tags": ["day7"], "group": "", "frame": ""}
{"x": 15, "y": 8, "text": "\u239f", "color": "21 bold", "tags": ["day7"], "group": "", "frame": ""}
{"x": 15, "y": 9, "text": "\u239f", "color": "21 bold", "tags": ["day7"], "group": "", "frame": ""}
{"x": 15, "y": 10, "text": "\u239f", "color": "21 bold", "tags": ["day7"], "group": "", "frame": ""}
{"x": 11, "y": 7, "text": "\u239c", "color": "21 bold", "tags": ["day7"], "group": "", "frame": ""}
{"x": 11, "y": 8, "text": "\u239c", "color": "21 bold", "tags": ["day7"], "group": "", "frame": ""}
{"x": 11, "y": 9, "text": "\u239c", "color": "21 bold", "tags": ["day7"], "group": "", "frame": ""}
{"x": 11, "y": 10, "text": "\u239c", "color": "21 bold", "tags": ["day7"], "group": "", "frame": ""}
{"x": 22, "y": 6, "text": "\u02e1", "color": "21 bold", "tags": ["day6"], "group": "", "frame": ""}
{"x": 23, "y": 8, "text": "\u02c8", "color": "21 bold", "tags": ["day6"], "group": "", "frame": ""}
{"x": 21, "y": 9, "text": ".", "color": "21 bold", "tags": ["day6"], "group": "", "frame": ""}
{"x": 22, "y": 10, "text": "\u02c8", "color": "21 bold", "tags": ["day6"], "group": "", "frame": ""}
{"x": 24, "y": 6, "text": "\u03b9", "color": "21 bold", "tags": ["day6"], "group": "", "frame": ""}
{"x": 22, "y": 5, "text": "6", "color": "230 bold", "tags": ["wick", "day6"], "group": "", "frame": "off", "href": "open-puzzle-6"}
{"x": 23, "y": 6, "text": "\u1fed", "color": "21 bold", "tags": ["day6"], "group": "", "frame": ""}
{"x": 21, "y": 6, "text": "\u1fee", "color": "21 bold", "tags": ["day6"], "group": "", "frame": ""}
{"x": 20, "y": 6, "text": "\u2041", "color": "21 bold", "tags": ["day6"], "group": "", "frame": ""}
{"x": 21, "y": 7, "text": "\u00a1", "color": "21 bold", "tags": ["day6"], "group": "", "frame": ""}
{"x": 23, "y": 7, "text": "\u1fcd", "color": "21 bold", "tags": ["day6"], "group": "", "frame": ""}
{"x": 22, "y": 4, "text": "\u02cc", "color": "230 bold", "tags": ["day6"], "group": "", "frame": "off"}
{"x": 24, "y": 7, "text": "\u239f", "color": "21 bold", "tags": ["day6"], "group": "", "frame": ""}
{"x": 24, "y": 8, "text": "\u239f", "color": "21 bold", "tags": ["day6"], "group": "", "frame": ""}
{"x": 24, "y": 9, "text": "\u239f", "color": "21 bold", "tags": ["day6"], "group": "", "frame": ""}
{"x": 24, "y": 10, "text": "\u239f", "color": "21 bold", "tags": ["day6"], "group": "", "frame": ""}
{"x": 20, "y": 7, "text": "\u239c", "color": "21 bold", "tags": ["day6"], "group": "", "frame": ""}
{"x": 20, "y": 8, "text": "\u239c", "color": "21 bold", "tags": ["day6"], "group": "", "frame": ""}
{"x": 20, "y": 9, "text": "\u239c", "color": "21 bold", "tags": ["day6"], "group": "", "frame": ""}
{"x": 20, "y": 10, "text": "\u239c", "color": "21 bold", "tags": ["day6"], "group": "", "frame": ""}
{"x": 31, "y": 6, "text": "\u02e1", "color": "21 bold", "tags": ["day5"], "group": "", "frame": ""}
{"x": 32, "y": 8, "text": "\u02c8", "color": "21 bold", "tags": ["day5"], "group": "", "frame": ""}
{"x": 30, "y": 9, "text": ".", "color": "21 bold", "tags": ["day5"], "group": "", "frame": ""}
{"x": 31, "y": 10, "text": "\u02c8", "color": "21 bold", "tags": ["day5"], "group": "", "frame": ""}
{"x": 33, "y": 6, "text": "\u03b9", "color": "21 bold", "tags": ["day5"], "group": "", "frame": ""}
{"x": 31, "y": 5, "text": "5", "color": "230 bold", "tags": ["wick", "day5"], "group": "", "frame": "off", "href": "open-puzzle-5"}
{"x": 32, "y": 6, "text": "\u1fed", "color": "21 bold", "tags": ["day5"], "group": "", "frame": ""}
{"x": 30, "y": 6, "text": "\u1fee", "color": "21 bold", "tags": ["day5"], "group": "", "frame": ""}
{"x": 29, "y": 6, "text": "\u2041", "color": "21 bold", "tags": ["day5"], "group": "", "frame": ""}
{"x": 30, "y": 7, "text": "\u00a1", "color": "21 bold", "tags": ["day5"], "group": "", "frame": ""}
{"x": 32, "y": 7, "text": "\u1fcd", "color": "21 bold", "tags": ["day5"], "group": "", "frame": ""}
{"x": 31, "y": 4, "text": "\u02cc", "color": "230 bold", "tags": ["day5"], "group": "", "frame": "off"}
{"x": 33, "y": 7, "text": "\u239f", "color": "21 bold", "tags": ["day5"], "group": "", "frame": ""}
{"x": 33, "y": 8, "text": "\u239f", "color": "21 bold", "tags": ["day5"], "group": "", "frame": ""}
{"x": 33, "y": 9, "text": "\u239f", "color": "21 bold", "tags": ["day5"], "group": "", "frame": ""}
{"x": 33, "y": 10, "text": "\u239f", "color": "21 bold", "tags": ["day5"], "group": "", "frame": ""}
{"x": 29, "y": 7, "text": "\u239c", "color": "21 bold", "tags": ["day5"], "group": "", "frame": ""}
{"x": 29, "y": 8, "text": "\u239c", "color": "21 bold", "tags": ["day5"], "group": "", "frame": ""}
{"x": 29, "y": 9, "text": "\u239c", "color": "21 bold", "tags": ["day5"], "group": "", "frame": ""}
{"x": 29, "y": 10, "text": "\u239c", "color": "21 bold", "tags": ["day5"], "group": "", "frame": ""}
{"x": 49, "y": 6, "text": "\u02e1", "color": "21 bold", "tags": ["day4"], "group": "", "frame": ""}
{"x": 50, "y": 8, "text": "\u02c8", "color": "21 bold", "tags": ["day4"], "group": "", "frame": ""}
{"x": 48, "y": 9, "text": ".", "color": "21 bold", "tags": ["day4"], "group": "", "frame": ""}
{"x": 49, "y": 10, "text": "\u02c8", "color": "21 bold", "tags": ["day4"], "group": "", "frame": ""}
{"x": 51, "y": 6, "text": "\u03b9", "color": "21 bold", "tags": ["day4"], "group": "", "frame": ""}
{"x": 49, "y": 5, "text": "4", "color": "230 bold", "tags": ["wick", "day4"], "group": "", "frame": "off", "href": "open-puzzle-4"}
{"x": 50, "y": 6, "text": "\u1fed", "color": "21 bold", "tags": ["day4"], "group": "", "frame": ""}
{"x": 48, "y": 6, "text": "\u1fee", "color": "21 bold", "tags": ["day4"], "group": "", "frame": ""}
{"x": 47, "y": 6, "text": "\u2041", "color": "21 bold", "tags": ["day4"], "group": "", "frame": ""}
{"x": 48, "y": 7, "text": "\u00a1", "color": "21 bold", "tags": ["day4"], "group": "", "frame": ""}
{"x": 50, "y": 7, "text": "\u1fcd", "color": "21 bold", "tags": ["day4"], "group": "", "frame": ""}
{"x": 49, "y": 4, "text": "\u02cc", "color": "230 bold", "tags": ["day4"], "group": "", "frame": "off"}
{"x": 51, "y": 7, "text": "\u239f", "color": "21 bold", "tags": ["day4"], "group": "", "frame": ""}
{"x": 51, "y": 8, "text": "\u239f", "color": "21 bold", "tags": ["day4"], "group": "", "frame": ""}
{"x": 51, "y": 9, "text": "\u239f", "color": "21 bold", "tags": ["day4"], "group": "", "frame": ""}
{"x": 51, "y": 10, "text": "\u239f", "color": "21 bold", "tags": ["day4"], "group": "", "frame": ""}
{"x": 47, "y": 7, "text": "\u239c", "color": "21 bold", "tags": ["day4"], "group": "", "frame": ""}
{"x": 47, "y": 8, "text": "\u239c", "color": "21 bold", "tags": ["day4"], "group": "", "frame": ""}
{"x": 47, "y": 9, "text": "\u239c", "color": "21 bold", "tags": ["day4"], "group": "", "frame": ""}
{"x": 47, "y": 10, "text": "\u239c", "color": "21 bold", "tags": ["day4"], "group": "", "frame": ""}
{"x": 58, "y": 6, "text": "\u02e1", "color": "21 bold", "tags": ["day3"], "group": "", "frame": ""}
{"x": 59, "y": 8, "text": "\u02c8", "color": "21 bold", "tags": ["day3"], "group": "", "frame": ""}
{"x": 57, "y": 9, "text": ".", "color": "21 bold", "tags": ["day3"], "group": "", "frame": ""}
{"x": 58, "y": 10, "text": "\u02c8", "color": "21 bold", "tags": ["day3"], "group": "", "frame": ""}
{"x": 60, "y": 6, "text": "\u03b9", "color": "21 bold", "tags": ["day3"], "group": "", "frame": ""}
{"x": 58, "y": 5, "text": "3", "color": "230 bold", "tags": ["wick", "day3"], "group": "", "frame": "off", "href": "open-puzzle-3"}
{"x": 59, "y": 6, "text": "\u1fed", "color": "21 bold", "tags": ["day3"], "group": "", "frame": ""}
{"x": 57, "y": 6, "text": "\u1fee", "color": "21 bold", "tags": ["day3"], "group": "", "frame": ""}
{"x": 56, "y": 6, "text": "\u2041", "color": "21 bold", "tags": ["day3"], "group": "", "frame": ""}
{"x": 57, "y": 7, "text": "\u00a1", "color": "21 bold", "tags": ["day3"], "group": "", "frame": ""}
{"x": 59, "y": 7, "text": "\u1fcd", "color": "21 bold", "tags": ["day3"], "group": "", "frame": ""}
{"x": 58, "y": 4, "text": "\u02cc", "color": "230 bold", "tags": ["day3"], "group": "", "frame": "off"}
{"x": 60, "y": 7, "text": "\u239f", "color": "21 bold", "tags": ["day3"], "group": "", "frame": ""}
{"x": 60, "y": 8, "text": "\u239f", "color": "21 bold", "tags": ["day3"], "group": "", "frame": ""}
{"x": 60, "y": 9, "text": "\u239f", "color": "21 bold", "tags": ["day3"], "group": "", "frame": ""}
{"x": 60, "y": 10, "text": "\u239f", "color": "21 bold", "tags": ["day3"], "group": "", "frame": ""}
{"x": 56, "y": 7, "text": "\u239c", "color": "21 bold", "tags": ["day3"], "group": "", "frame": ""}
{"x": 56, "y": 8, "text": "\u239c", "color": "21 bold", "tags": ["day3"], "group": "", "frame": ""}
{"x": 56, "y": 9, "text": "\u239c", "color": "21 bold", "tags": ["day3"], "group": "", "frame": ""}
{"x": 56, "y": 10, "text": "\u239c", "color": "21 bold", "tags": ["day3"], "group": "", "frame": ""}
{"x": 67, "y": 6, "text": "\u02e1", "color": "21 bold", "tags": ["day2"], "group": "", "frame": ""}
{"x": 68, "y": 8, "text": "\u02c8", "color": "21 bold", "tags": ["day2"], "group": "", "frame": ""}
{"x": 66, "y": 9, "text": ".", "color": "21 bold", "tags": ["day2"], "group": "", "frame": ""}
{"x": 67, "y": 10, "text": "\u02c8", "color": "21 bold", "tags": ["day2"], "group": "", "frame": ""}
{"x": 69, "y": 6, "text": "\u03b9", "color": "21 bold", "tags": ["day2"], "group": "", "frame": ""}
{"x": 68, "y": 6, "text": "\u1fed", "color": "21 bold", "tags": ["day2"], "group": "", "frame": ""}
{"x": 66, "y": 6, "text": "\u1fee", "color": "21 bold", "tags": ["day2"], "group": "", "frame": ""}
{"x": 65, "y": 6, "text": "\u2041", "color": "21 bold", "tags": ["day2"], "group": "", "frame": ""}
{"x": 66, "y": 7, "text": "\u00a1", "color": "21 bold", "tags": ["day2"], "group": "", "frame": ""}
{"x": 68, "y": 7, "text": "\u1fcd", "color": "21 bold", "tags": ["day2"], "group": "", "frame": ""}
{"x": 67, "y": 4, "text": "\u02cc", "color": "230 bold", "tags": ["day2"], "group": "", "frame": "off"}
{"x": 69, "y": 7, "text": "\u239f", "color": "21 bold", "tags": ["day2"], "group": "", "frame": ""}
{"x": 69, "y": 8, "text": "\u239f", "color": "21 bold", "tags": ["day2"], "group": "", "frame": ""}
{"x": 69, "y": 9, "text": "\u239f", "color": "21 bold", "tags": ["day2"], "group": "", "frame": ""}
{"x": 69, "y": 10, "text": "\u239f", "color": "21 bold", "tags": ["day2"], "group": "", "frame": ""}
{"x": 65, "y": 7, "text": "\u239c", "color": "21 bold", "tags": ["day2"], "group": "", "frame": ""}
{"x": 65, "y": 8, "text": "\u239c", "color": "21 bold", "tags": ["day2"], "group": "", "frame": ""}
{"x": 65, "y": 9, "text": "\u239c", "color": "21 bold", "tags": ["day2"], "group": "", "frame": ""}
{"x": 65, "y": 10, "text": "\u239c", "color": "21 bold", "tags": ["day2"], "group": "", "frame": ""}
{"x": 76, "y": 6, "text": "\u02e1", "color": "21 bold", "tags": ["day1"], "group": "", "frame": ""}
{"x": 77, "y": 8, "text": "\u02c8", "color": "21 bold", "tags": ["day1"], "group": "", "frame": ""}
{"x": 75, "y": 9, "text": ".", "color": "21 bold", "tags": ["day1"], "group": "", "frame": ""}
{"x": 76, "y": 10, "text": "\u02c8", "color": "21 bold", "tags": ["day1"], "group": "", "frame": ""}
{"x": 78, "y": 6, "text": "\u03b9", "color": "21 bold", "tags": ["day1"], "group": "", "frame": ""}
{"x": 76, "y": 5, "text": "1", "color": "230 bold", "tags": ["wick", "day1"], "group": "", "frame": "off", "href": "open-puzzle-1"}
{"x": 77, "y": 6, "text": "\u1fed", "color": "21 bold", "tags": ["day1"], "group": "", "frame": ""}
{"x": 75, "y": 6, "text": "\u1fee", "color": "21 bold", "tags": ["day1"], "group": "", "frame": ""}
{"x": 74, "y": 6, "text": "\u2041", "color": "21 bold", "tags": ["day1"], "group": "", "frame": ""}
{"x": 75, "y": 7, "text": "\u00a1", "color": "21 bold", "tags": ["day1"], "group": "", "frame": ""}
{"x": 77, "y": 7, "text": "\u1fcd", "color": "21 bold", "tags": ["day1"], "group": "", "frame": ""}
{"x": 76, "y": 4, "text": "\u02cc", "color": "230 bold", "tags": ["day1"], "group": "", "frame": "off"}
{"x": 78, "y": 7, "text": "\u239f", "color": "21 bold", "tags": ["day1"], "group": "", "frame": ""}
{"x": 78, "y": 8, "text": "\u239f", "color": "21 bold", "tags": ["day1"], "group": "", "frame": ""}
{"x": 78, "y": 9, "text": "\u239f", "color": "21 bold", "tags": ["day1"], "group": "", "frame": ""}
{"x": 78, "y": 10, "text": "\u239f", "color": "21 bold", "tags": ["day1"], "group": "", "frame": ""}
{"x": 74, "y": 7, "text": "\u239c", "color": "21 bold", "tags": ["day1"], "group": "", "frame": ""}
{"x": 74, "y": 8, "text": "\u239c", "color": "21 bold", "tags": ["day1"], "group": "", "frame": ""}
{"x": 74, "y": 9, "text": "\u239c", "color": "21 bold", "tags": ["day1"], "group": "", "frame": ""}
{"x": 74, "y": 10, "text": "\u239c", "color": "21 bold", "tags": ["day1"], "group": "", "frame": ""}
{"x": 40, "y": 2, "text": "\u02e1", "color": "21 bold", "tags": [], "group": "", "frame": ""}
{"x": 41, "y": 4, "text": "\u02c8", "color": "21 bold", "tags": [], "group": "", "frame": ""}
{"x": 39, "y": 5, "text": ".", "color": "21 bold", "tags": [], "group": "", "frame": ""}
{"x": 40, "y": 6, "text": "\u02c8", "color": "21 bold", "tags": [], "group": "", "frame": ""}
{"x": 42, "y": 2, "text": "\u03b9", "color": "21 bold", "tags": [], "group": "", "frame": ""}
{"x": 40, "y": 1, "text": "0", "color": "230 bold", "tags": ["wick", "day0"], "group": "", "frame": "off", "href": "open-puzzle-0"}
{"x": 41, "y": 2, "text": "\u1fed", "color": "21 bold", "tags": [], "group": "", "frame": ""}
{"x": 39, "y": 2, "text": "\u1fee", "color": "21 bold", "tags": [], "group": "", "frame": ""}
{"x": 38, "y": 2, "text": "\u2041", "color": "21 bold", "tags": [], "group": "", "frame": ""}
{"x": 39, "y": 3, "text": "\u00a1", "color": "21 bold", "tags": [], "group": "", "frame": ""}
{"x": 41, "y": 3, "text": "\u1fcd", "color": "21 bold", "tags": [], "group": "", "frame": ""}
{"x": 40, "y": 0, "text": "\u02cc", "color": "230 bold", "tags": [], "group": "", "frame": "off"}
{"x": 42, "y": 3, "text": "\u239f", "color": "21 bold", "tags": [], "group": "", "frame": ""}
{"x": 42, "y": 4, "text": "\u239f", "color": "21 bold", "tags": [], "group": "", "frame": ""}
{"x": 42, "y": 5, "text": "\u239f", "color": "21 bold", "tags": [], "group": "", "frame": ""}
{"x": 42, "y": 6, "text": "\u239f", "color": "21 bold", "tags": [], "group": "", "frame": ""}
{"x": 38, "y": 3, "text": "\u239c", "color": "21 bold", "tags": [], "group": "", "frame": ""}
{"x": 38, "y": 4, "text": "\u239c", "color": "21 bold", "tags": [], "group": "", "frame": ""}
{"x": 38, "y": 5, "text": "\u239c", "color": "21 bold", "tags": [], "group": "", "frame": ""}
{"x": 38, "y": 6, "text": "\u239c", "color": "21 bold", "tags": [], "group": "", "frame": ""}
{"x": 40, "y": 10, "text": "\u0240", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 4, "y": 14, "text": "\u0240", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 13, "y": 14, "text": "\u0240", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 22, "y": 14, "text": "\u0240", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 31, "y": 14, "text": "\u0240", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 49, "y": 14, "text": "\u0240", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 58, "y": 14, "text": "\u0240", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 67, "y": 14, "text": "\u0240", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 76, "y": 14, "text": "\u0240", "color": "255 bold", "tags": ["menorah"], "group": "", "frame": "off"}
{"x": 4, "y": 13, "text": "", "color": "15 bold", "tags": [], "group": "", "frame": "off"}
{"x": 67, "y": 5, "text": "2", "color": "230 bold", "tags": ["wick", "day2"], "group": "", "frame": "off", "href": "open-puzzle-2"}
{"x": 50, "y": 9, "text": " ", "color": "21 bold", "tags": ["day4"], "group": "", "frame": ""}
{"x": 49, "y": 9, "text": " ", "color": "21 bold", "tags": ["day4"], "group": "", "frame": ""}
{"x": 48, "y": 8, "text": " ", "color": "21 bold", "tags": ["day4"], "group": "", "frame": ""}
{"x": 49, "y": 8, "text": " ", "color": "21 bold", "tags": ["day4"], "group": "", "frame": ""}
{"x": 49, "y": 7, "text": " ", "color": "21 bold", "tags": ["day4"], "group": "", "frame": ""}
{"x": 48, "y": 10, "text": " ", "color": "21 bold", "tags": ["day4"], "group": "", "frame": ""}
{"x": 50, "y": 10, "text": " ", "color": "21 bold", "tags": ["day4"], "group": "", "frame": ""}
{"x": 3, "y": 8, "text": "\u00b8", "color": "27 bold", "tags": ["day8"], "group": "", "frame": "on"}
{"x": 3, "y": 9, "text": ".", "color": "21 bold", "tags": ["day8"], "group": "", "frame": "on"}
{"x": 4, "y": 10, "text": "\u02c8", "color": "21 bold", "tags": ["day8"], "group": "", "frame": "on"}
{"x": 2, "y": 6, "text": "\u03b6", "color": "33 bold", "tags": ["day8"], "group": "", "frame": "on"}
{"x": 5, "y": 7, "text": "\u20b0", "color": "39 bold", "tags": ["day8"], "group": "", "frame": "on"}
{"x": 5, "y": 6, "text": "\u210c", "color": "39 bold", "tags": ["day8"], "group": "", "frame": "on"}
{"x": 5, "y": 9, "text": "\u02c8", "color": "21 bold", "tags": ["day8"], "group": "", "frame": "on"}
{"x": 5, "y": 10, "text": "\u1fed", "color": "27 bold", "tags": ["day8"], "group": "", "frame": "on"}
{"x": 6, "y": 8, "text": "\u1f37", "color": "27 bold", "tags": ["day8"], "group": "", "frame": "on"}
{"x": 3, "y": 6, "text": "\u214b", "color": "39 bold", "tags": ["day8"], "group": "", "frame": "on"}
{"x": 2, "y": 7, "text": "\u1f86", "color": "27 bold", "tags": ["day8"], "group": "", "frame": "on"}
{"x": 3, "y": 7, "text": "\u1fdf", "color": "39 bold", "tags": ["day8"], "group": "", "frame": "on"}
{"x": 6, "y": 6, "text": "\u00b8", "color": "33 bold", "tags": ["day8"], "group": "", "frame": "on"}
{"x": 5, "y": 8, "text": "\u1fdf", "color": "33 bold", "tags": ["day8"], "group": "", "frame": "on"}
{"x": 6, "y": 7, "text": "\u1f97", "color": "33 bold", "tags": ["day8"], "group": "", "frame": "on"}
{"x": 4, "y": 6, "text": "\u0267", "color": "39 bold", "tags": ["day8"], "group": "", "frame": "on"}
{"x": 4, "y": 7, "text": "\u00a1", "color": "33 bold", "tags": ["day8"], "group": "", "frame": "on"}
{"x": 4, "y": 8, "text": "\u01be", "color": "33 bold", "tags": ["day8"], "group": "", "frame": "on"}
{"x": 30, "y": 8, "text": "\u00b8", "color": "27 bold", "tags": ["day5"], "group": "", "frame": "on"}
{"x": 30, "y": 9, "text": ".", "color": "21 bold", "tags": ["day5"], "group": "", "frame": "on"}
{"x": 31, "y": 10, "text": "\u02c8", "color": "21 bold", "tags": ["day5"], "group": "", "frame": "on"}
{"x": 29, "y": 6, "text": "\u03b6", "color": "33 bold", "tags": ["day5"], "group": "", "frame": "on"}
{"x": 32, "y": 7, "text": "\u20b0", "color": "39 bold", "tags": ["day5"], "group": "", "frame": "on"}
{"x": 32, "y": 6, "text": "\u210c", "color": "39 bold", "tags": ["day5"], "group": "", "frame": "on"}
{"x": 32, "y": 9, "text": "\u02c8", "color": "21 bold", "tags": ["day5"], "group": "", "frame": "on"}
{"x": 32, "y": 10, "text": "\u1fed", "color": "27 bold", "tags": ["day5"], "group": "", "frame": "on"}
{"x": 33, "y": 8, "text": "\u1f37", "color": "27 bold", "tags": ["day5"], "group": "", "frame": "on"}
{"x": 30, "y": 6, "text": "\u214b", "color": "39 bold", "tags": ["day5"], "group": "", "frame": "on"}
{"x": 29, "y": 7, "text": "\u1f86", "color": "27 bold", "tags": ["day5"], "group": "", "frame": "on"}
{"x": 30, "y": 7, "text": "\u1fdf", "color": "39 bold", "tags": ["day5"], "group": "", "frame": "on"}
{"x": 33, "y": 6, "text": "\u00b8", "color": "33 bold", "tags": ["day5"], "group": "", "frame": "on"}
{"x": 32, "y": 8, "text": "\u1fdf", "color": "33 bold", "tags": ["day5"], "group": "", "frame": "on"}
{"x": 33, "y": 7, "text": "\u1f97", "color": "33 bold", "tags": ["day5"], "group": "", "frame": "on"}
{"x": 31, "y": 6, "text": "\u0267", "color": "39 bold", "tags": ["day5"], "group": "", "frame": "on"}
{"x": 31, "y": 7, "text": "\u00a1", "color": "33 bold", "tags": ["day5"], "group": "", "frame": "on"}
{"x": 31, "y": 8, "text": "\u01be", "color": "33 bold", "tags": ["day5"], "group": "", "frame": "on"}
{"x": 57, "y": 8, "text": "\u00b8", "color": "27 bold", "tags": ["day3"], "group": "", "frame": "on"}
{"x": 57, "y": 9, "text": ".", "color": "21 bold", "tags": ["day3"], "group": "", "frame": "on"}
{"x": 58, "y": 10, "text": "\u02c8", "color": "21 bold", "tags": ["day3"], "group": "", "frame": "on"}
{"x": 56, "y": 6, "text": "\u03b6", "color": "33 bold", "tags": ["day3"], "group": "", "frame": "on"}
{"x": 59, "y": 7, "text": "\u20b0", "color": "39 bold", "tags": ["day3"], "group": "", "frame": "on"}
{"x": 59, "y": 6, "text": "\u210c", "color": "39 bold", "tags": ["day3"], "group": "", "frame": "on"}
{"x": 59, "y": 9, "text": "\u02c8", "color": "21 bold", "tags": ["day3"], "group": "", "frame": "on"}
{"x": 59, "y": 10, "text": "\u1fed", "color": "27 bold", "tags": ["day3"], "group": "", "frame": "on"}
{"x": 60, "y": 8, "text": "\u1f37", "color": "27 bold", "tags": ["day3"], "group": "", "frame": "on"}
{"x": 57, "y": 6, "text": "\u214b", "color": "39 bold", "tags": ["day3"], "group": "", "frame": "on"}
{"x": 56, "y": 7, "text": "\u1f86", "color": "27 bold", "tags": ["day3"], "group": "", "frame": "on"}
{"x": 57, "y": 7, "text": "\u1fdf", "color": "39 bold", "tags": ["day3"], "group": "", "frame": "on"}
{"x": 60, "y": 6, "text": "\u00b8", "color": "33 bold", "tags": ["day3"], "group": "", "frame": "on"}
{"x": 59, "y": 8, "text": "\u1fdf", "color": "33 bold", "tags": ["day3"], "group": "", "frame": "on"}
{"x": 60, "y": 7, "text": "\u1f97", "color": "33 bold", "tags": ["day3"], "group": "", "frame": "on"}
{"x": 58, "y": 6, "text": "\u0267", "color": "39 bold", "tags": ["day3"], "group": "", "frame": "on"}
{"x": 58, "y": 7, "text": "\u00a1", "color": "33 bold", "tags": ["day3"], "group": "", "frame": "on"}
{"x": 58, "y": 8, "text": "\u01be", "color": "33 bold", "tags": ["day3"], "group": "", "frame": "on"}
{"x": 15, "y": 8, "text": "\u026e", "color": "33 bold", "tags": ["day7"], "group": "", "frame": "on"}
{"x": 13, "y": 10, "text": "\u0296", "color": "21 bold", "tags": ["day7"], "group": "", "frame": "on"}
{"x": 12, "y": 9, "text": ".", "color": "21 bold", "tags": ["day7"], "group": "", "frame": "on"}
{"x": 15, "y": 7, "text": "\u1f97", "color": "39 bold", "tags": ["day7"], "group": "", "frame": "on"}
{"x": 11, "y": 7, "text": "\u1f86", "color": "33 bold", "tags": ["day7"], "group": "", "frame": "on"}
{"x": 12, "y": 7, "text": "\u1fce", "color": "39 bold", "tags": ["day7"], "group": "", "frame": "on"}
{"x": 13, "y": 9, "text": "\u00a1", "color": "27 bold", "tags": ["day7"], "group": "", "frame": "on"}
{"x": 13, "y": 7, "text": "\u03d4", "color": "39 bold", "tags": ["day7"], "group": "", "frame": "on"}
{"x": 14, "y": 8, "text": "\u02c8", "color": "27 bold", "tags": ["day7"], "group": "", "frame": "on"}
{"x": 12, "y": 6, "text": "\u1faf", "color": "39 bold", "tags": ["day7"], "group": "", "frame": "on"}
{"x": 11, "y": 6, "text": "\u0481", "color": "33 bold", "tags": ["day7"], "group": "", "frame": "on"}
{"x": 13, "y": 8, "text": "\u1f37", "color": "33 bold", "tags": ["day7"], "group": "", "frame": "on"}
{"x": 14, "y": 6, "text": "\u04a9", "color": "39 bold", "tags": ["day7"], "group": "", "frame": "on"}
{"x": 14, "y": 7, "text": "\u1fa7", "color": "33 bold", "tags": ["day7"], "group": "", "frame": "on"}
{"x": 12, "y": 8, "text": ";", "color": "27 bold", "tags": ["day7"], "group": "", "frame": "on"}
{"x": 15, "y": 6, "text": "\u2137", "color": "39 bold", "tags": ["day7"], "group": "", "frame": "on"}
{"x": 13, "y": 6, "text": "\u00a1", "color": "33 bold", "tags": ["day7"], "group": "", "frame": "on"}
{"x": 24, "y": 8, "text": "\u0296", "color": "27 bold", "tags": ["day6"], "group": "", "frame": "on"}
{"x": 20, "y": 6, "text": "\u02a7", "color": "33 bold", "tags": ["day6"], "group": "", "frame": "on"}
{"x": 24, "y": 7, "text": "\u1fd2", "color": "33 bold", "tags": ["day6"], "group": "", "frame": "on"}
{"x": 21, "y": 6, "text": "\u20a7", "color": "39 bold", "tags": ["day6"], "group": "", "frame": "on"}
{"x": 21, "y": 7, "text": "\u1f27", "color": "39 bold", "tags": ["day6"], "group": "", "frame": "on"}
{"x": 20, "y": 7, "text": "\u1e08", "color": "33 bold", "tags": ["day6"], "group": "", "frame": "on"}
{"x": 22, "y": 6, "text": "\u2118", "color": "39 bold", "tags": ["day6"], "group": "", "frame": "on"}
{"x": 23, "y": 8, "text": "\u1fdd", "color": "21 bold", "tags": ["day6"], "group": "", "frame": "on"}
{"x": 21, "y": 8, "text": "\u04b8", "color": "33 bold", "tags": ["day6"], "group": "", "frame": "on"}
{"x": 23, "y": 10, "text": "\u1fcd", "color": "27 bold", "tags": ["day6"], "group": "", "frame": "on"}
{"x": 22, "y": 10, "text": "\u0387", "color": "21 bold", "tags": ["day6"], "group": "", "frame": "on"}
{"x": 21, "y": 9, "text": "\u00bf", "color": "27 bold", "tags": ["day6"], "group": "", "frame": "on"}
{"x": 22, "y": 8, "text": "\u1fcf", "color": "27 bold", "tags": ["day6"], "group": "", "frame": "on"}
{"x": 23, "y": 6, "text": "\u04a9", "color": "39 bold", "tags": ["day6"], "group": "", "frame": "on"}
{"x": 22, "y": 7, "text": "\u2103", "color": "33 bold", "tags": ["day6"], "group": "", "frame": "on"}
{"x": 23, "y": 7, "text": "'", "color": "39 bold", "tags": ["day6"], "group": "", "frame": "on"}
{"x": 24, "y": 6, "text": "\u00b8", "color": "33 bold", "tags": ["day6"], "group": "", "frame": "on"}
{"x": 22, "y": 9, "text": "'", "color": "27 bold", "tags": ["day6"], "group": "", "frame": "on"}
{"x": 42, "y": 4, "text": "\u026e", "color": "33 bold", "tags": ["day0"], "group": "", "frame": "on"}
{"x": 40, "y": 6, "text": "\u0296", "color": "21 bold", "tags": ["day0"], "group": "", "frame": "on"}
{"x": 39, "y": 5, "text": ".", "color": "21 bold", "tags": ["day0"], "group": "", "frame": "on"}
{"x": 42, "y": 3, "text": "\u1f97", "color": "39 bold", "tags": ["day0"], "group": "", "frame": "on"}
{"x": 38, "y": 3, "text": "\u1f86", "color": "33 bold", "tags": ["day0"], "group": "", "frame": "on"}
{"x": 39, "y": 3, "text": "\u1fce", "color": "39 bold", "tags": ["day0"], "group": "", "frame": "on"}
{"x": 40, "y": 5, "text": "\u00a1", "color": "27 bold", "tags": ["day0"], "group": "", "frame": "on"}
{"x": 40, "y": 3, "text": "\u03d4", "color": "39 bold", "tags": ["day0"], "group": "", "frame": "on"}
{"x": 41, "y": 4, "text": "\u02c8", "color": "27 bold", "tags": ["day0"], "group": "", "frame": "on"}
{"x": 39, "y": 2, "text": "\u1faf", "color": "39 bold", "tags": ["day0"], "group": "", "frame": "on"}
{"x": 38, "y": 2, "text": "\u0481", "color": "33 bold", "tags": ["day0"], "group": "", "frame": "on"}
{"x": 40, "y": 4, "text": "\u1f37", "color": "33 bold", "tags": ["day0"], "group": "", "frame": "on"}
{"x": 41, "y": 2, "text": "\u04a9", "color": "39 bold", "tags": ["day0"], "group": "", "frame": "on"}
{"x": 41, "y": 3, "text": "\u1fa7", "color": "33 bold", "tags": ["day0"], "group": "", "frame": "on"}
{"x": 39, "y": 4, "text": ";", "color": "27 bold", "tags": ["day0"], "group": "", "frame": "on"}
{"x": 42, "y": 2, "text": "\u2137", "color": "39 bold", "tags": ["day0"], "group": "", "frame": "on"}
{"x": 40, "y": 2, "text": "\u00a1", "color": "33 bold", "tags": ["day0"], "group": "", "frame": "on"}
{"x": 51, "y": 8, "text": "\u0296", "color": "27 bold", "tags": ["day4"], "group": "", "frame": "on"}
{"x": 47, "y": 6, "text": "\u02a7", "color": "33 bold", "tags": ["day4"], "group": "", "frame": "on"}
{"x": 51, "y": 7, "text": "\u1fd2", "color": "33 bold", "tags": ["day4"], "group": "", "frame": "on"}
{"x": 48, "y": 6, "text": "\u20a7", "color": "39 bold", "tags": ["day4"], "group": "", "frame": "on"}
{"x": 48, "y": 7, "text": "\u1f27", "color": "39 bold", "tags": ["day4"], "group": "", "frame": "on"}
{"x": 47, "y": 7, "text": "\u1e08", "color": "33 bold", "tags": ["day4"], "group": "", "frame": "on"}
{"x": 49, "y": 6, "text": "\u2118", "color": "39 bold", "tags": ["day4"], "group": "", "frame": "on"}
{"x": 50, "y": 8, "text": "\u1fdd", "color": "21 bold", "tags": ["day4"], "group": "", "frame": "on"}
{"x": 48, "y": 8, "text": "\u04b8", "color": "33 bold", "tags": ["day4"], "group": "", "frame": "on"}
{"x": 50, "y": 10, "text": "\u1fcd", "color": "27 bold", "tags": ["day4"], "group": "", "frame": "on"}
{"x": 49, "y": 10, "text": "\u0387", "color": "21 bold", "tags": ["day4"], "group": "", "frame": "on"}
{"x": 48, "y": 9, "text": "\u00bf", "color": "27 bold", "tags": ["day4"], "group": "", "frame": "on"}
{"x": 49, "y": 8, "text": "\u1fcf", "color": "27 bold", "tags": ["day4"], "group": "", "frame": "on"}
{"x": 50, "y": 6, "text": "\u04a9", "color": "39 bold", "tags": ["day4"], "group": "", "frame": "on"}
{"x": 49, "y": 7, "text": "\u2103", "color": "33 bold", "tags": ["day4"], "group": "", "frame": "on"}
{"x": 50, "y": 7, "text": "'", "color": "39 bold", "tags": ["day4"], "group": "", "frame": "on"}
{"x": 51, "y": 6, "text": "\u00b8", "color": "33 bold", "tags": ["day4"], "group": "", "frame": "on"}
{"x": 49, "y": 9, "text": "'", "color": "27 bold", "tags": ["day4"], "group": "", "frame": "on"}
{"x": 78, "y": 8, "text": "\u0296", "color": "27 bold", "tags": ["day1"], "group": "", "frame": "on"}
{"x": 74, "y": 6, "text": "\u02a7", "color": "33 bold", "tags": ["day1"], "group": "", "frame": "on"}
{"x": 78, "y": 7, "text": "\u1fd2", "color": "33 bold", "tags": ["day1"], "group": "", "frame": "on"}
{"x": 75, "y": 6, "text": "\u20a7", "color": "39 bold", "tags": ["day1"], "group": "", "frame": "on"}
{"x": 75, "y": 7, "text": "\u1f27", "color": "39 bold", "tags": ["day1"], "group": "", "frame": "on"}
{"x": 74, "y": 7, "text": "\u1e08", "color": "33 bold", "tags": ["day1"], "group": "", "frame": "on"}
{"x": 76, "y": 6, "text": "\u2118", "color": "39 bold", "tags": ["day1"], "group": "", "frame": "on"}
{"x": 77, "y": 8, "text": "\u1fdd", "color": "21 bold", "tags": ["day1"], "group": "", "frame": "on"}
{"x": 75, "y": 8, "text": "\u04b8", "color": "33 bold", "tags": ["day1"], "group": "", "frame": "on"}
{"x": 77, "y": 10, "text": "\u1fcd", "color": "27 bold", "tags": ["day1"], "group": "", "frame": "on"}
{"x": 76, "y": 10, "text": "\u0387", "color": "21 bold", "tags": ["day1"], "group": "", "frame": "on"}
{"x": 75, "y": 9, "text": "\u00bf", "color": "27 bold", "tags": ["day1"], "group": "", "frame": "on"}
{"x": 76, "y": 8, "text": "\u1fcf", "color": "27 bold", "tags": ["day1"], "group": "", "frame": "on"}
{"x": 77, "y": 6, "text": "\u04a9", "color": "39 bold", "tags": ["day1"], "group": "", "frame": "on"}
{"x": 76, "y": 7, "text": "\u2103", "color": "33 bold", "tags": ["day1"], "group": "", "frame": "on"}
{"x": 77, "y": 7, "text": "'", "color": "39 bold", "tags": ["day1"], "group": "", "frame": "on"}
{"x": 78, "y": 6, "text": "\u00b8", "color": "33 bold", "tags": ["day1"], "group": "", "frame": "on"}
{"x": 76, "y": 9, "text": "'", "color": "27 bold", "tags": ["day1"], "group": "", "frame": "on"}
{"x": 69, "y": 8, "text": "\u026e", "color": "33 bold", "tags": ["day2"], "group": "", "frame": "on"}
{"x": 67, "y": 10, "text": "\u0296", "color": "21 bold", "tags": ["day2"], "group": "", "frame": "on"}
{"x": 66, "y": 9, "text": ".", "color": "21 bold", "tags": ["day2"], "group": "", "frame": "on"}
{"x": 69, "y": 7, "text": "\u1f97", "color": "39 bold", "tags": ["day2"], "group": "", "frame": "on"}
{"x": 65, "y": 7, "text": "\u1f86", "color": "33 bold", "tags": ["day2"], "group": "", "frame": "on"}
{"x": 66, "y": 7, "text": "\u1fce", "color": "39 bold", "tags": ["day2"], "group": "", "frame": "on"}
{"x": 67, "y": 9, "text": "\u00a1", "color": "27 bold", "tags": ["day2"], "group": "", "frame": "on"}
{"x": 67, "y": 7, "text": "\u03d4", "color": "39 bold", "tags": ["day2"], "group": "", "frame": "on"}
{"x": 68, "y": 8, "text": "\u02c8", "color": "27 bold", "tags": ["day2"], "group": "", "frame": "on"}
{"x": 66, "y": 6, "text": "\u1faf", "color": "39 bold", "tags": ["day2"], "group": "", "frame": "on"}
{"x": 65, "y": 6, "text": "\u0481", "color": "33 bold", "tags": ["day2"], "group": "", "frame": "on"}
{"x": 67, "y": 8, "text": "\u1f37", "color": "33 bold", "tags": ["day2"], "group": "", "frame": "on"}
{"x": 68, "y": 6, "text": "\u04a9", "color": "39 bold", "tags": ["day2"], "group": "", "frame": "on"}
{"x": 68, "y": 7, "text": "\u1fa7", "color": "33 bold", "tags": ["day2"], "group": "", "frame": "on"}
{"x": 66, "y": 8, "text": ";", "color": "27 bold", "tags": ["day2"], "group": "", "frame": "on"}
{"x": 69, "y": 6, "text": "\u2137", "color": "39 bold", "tags": ["day2"], "group": "", "frame": "on"}
{"x": 67, "y": 6, "text": "\u00a1", "color": "33 bold", "tags": ["day2"], "group": "", "frame": "on"}
{"x": 2, "y": 9, "text": "\u239c", "color": "21 bold", "tags": ["day8"], "group": "", "frame": "on"}
{"x": 2, "y": 10, "text": "\u239c", "color": "21 bold", "tags": ["day8"], "group": "", "frame": "on"}
{"x": 6, "y": 10, "text": "\u239f", "color": "21 bold", "tags": ["day8"], "group": "", "frame": "on"}
{"x": 15, "y": 10, "text": "\u239f", "color": "21 bold", "tags": ["day7"], "group": "", "frame": "on"}
{"x": 24, "y": 10, "text": "\u239f", "color": "21 bold", "tags": ["day6"], "group": "", "frame": "on"}
{"x": 24, "y": 9, "text": "\u239f", "color": "27 bold", "tags": ["day6"], "group": "", "frame": "on"}
{"x": 15, "y": 9, "text": "\u239f", "color": "27 bold", "tags": ["day7"], "group": "", "frame": "on"}
{"x": 6, "y": 9, "text": "\u239f", "color": "21 bold", "tags": ["day8"], "group": "", "frame": "on"}
{"x": 20, "y": 10, "text": "\u239c", "color": "21 bold", "tags": ["day6"], "group": "", "frame": "on"}
{"x": 20, "y": 9, "text": "\u239c", "color": "21 bold", "tags": ["day6"], "group": "", "frame": "on"}
{"x": 20, "y": 8, "text": "\u239c", "color": "27 bold", "tags": ["day6"], "group": "", "frame": "on"}
{"x": 11, "y": 8, "text": "\u239c", "color": "33 bold", "tags": ["day7"], "group": "", "frame": "on"}
{"x": 11, "y": 9, "text": "\u239c", "color": "27 bold", "tags": ["day7"], "group": "", "frame": "on"}
{"x": 11, "y": 10, "text": "\u239c", "color": "21 bold", "tags": ["day7"], "group": "", "frame": "on"}
{"x": 2, "y": 8, "text": "\u239c", "color": "27 bold", "tags": ["day8"], "group": "", "frame": "on"}
{"x": 29, "y": 9, "text": "\u239c", "color": "21 bold", "tags": ["day5"], "group": "", "frame": "on"}
{"x": 29, "y": 10, "text": "\u239c", "color": "21 bold", "tags": ["day5"], "group": "", "frame": "on"}
{"x": 33, "y": 10, "text": "\u239f", "color": "21 bold", "tags": ["day5"], "group": "", "frame": "on"}
{"x": 33, "y": 9, "text": "\u239f", "color": "21 bold", "tags": ["day5"], "group": "", "frame": "on"}
{"x": 29, "y": 8, "text": "\u239c", "color": "27 bold", "tags": ["day5"], "group": "", "frame": "on"}
{"x": 56, "y": 9, "text": "\u239c", "color": "21 bold", "tags": ["day3"], "group": "", "frame": "on"}
{"x": 56, "y": 10, "text": "\u239c", "color": "21 bold", "tags": ["day3"], "group": "", "frame": "on"}
{"x": 60, "y": 10, "text": "\u239f", "color": "21 bold", "tags": ["day3"], "group": "", "frame": "on"}
{"x": 60, "y": 9, "text": "\u239f", "color": "21 bold", "tags": ["day3"], "group": "", "frame": "on"}
{"x": 56, "y": 8, "text": "\u239c", "color": "27 bold", "tags": ["day3"], "group": "", "frame": "on"}
{"x": 42, "y": 6, "text": "\u239f", "color": "21 bold", "tags": ["day0"], "group": "", "frame": "on"}
{"x": 42, "y": 5, "text": "\u239f", "color": "27 bold", "tags": ["day0"], "group": "", "frame": "on"}
{"x": 38, "y": 4, "text": "\u239c", "color": "33 bold", "tags": ["day0"], "group": "", "frame": "on"}
{"x": 38, "y": 5, "text": "\u239c", "color": "27 bold", "tags": ["day0"], "group": "", "frame": "on"}
{"x": 38, "y": 6, "text": "\u239c", "color": "21 bold", "tags": ["day0"], "group": "", "frame": "on"}
{"x": 69, "y": 10, "text": "\u239f", "color": "21 bold", "tags": ["day2"], "group": "", "frame": "on"}
{"x": 69, "y": 9, "text": "\u239f", "color": "27 bold", "tags": ["day2"], "group": "", "frame": "on"}
{"x": 65, "y": 8, "text": "\u239c", "color": "33 bold", "tags": ["day2"], "group": "", "frame": "on"}
{"x": 65, "y": 9, "text": "\u239c", "color": "27 bold", "tags": ["day2"], "group": "", "frame": "on"}
{"x": 65, "y": 10, "text": "\u239c", "color": "21 bold", "tags": ["day2"], "group": "", "frame": "on"}
{"x": 51, "y": 10, "text": "\u239f", "color": "21 bold", "tags": ["day4"], "group": "", "frame": "on"}
{"x": 51, "y": 9, "text": "\u239f", "color": "27 bold", "tags": ["day4"], "group": "", "frame": "on"}
{"x": 47, "y": 10, "text": "\u239c", "color": "21 bold", "tags": ["day4"], "group": "", "frame": "on"}
{"x": 47, "y": 9, "text": "\u239c", "color": "21 bold", "tags": ["day4"], "group": "", "frame": "on"}
{"x": 47, "y": 8, "text": "\u239c", "color": "27 bold", "tags": ["day4"], "group": "", "frame": "on"}
{"x": 78, "y": 10, "text": "\u239f", "color": "21 bold", "tags": ["day1"], "group": "", "frame": "on"}
{"x": 78, "y": 9, "text": "\u239f", "color": "27 bold", "tags": ["day1"], "group": "", "frame": "on"}
{"x": 74, "y": 10, "text": "\u239c", "color": "21 bold", "tags": ["day1"], "group": "", "frame": "on"}
{"x": 74, "y": 9, "text": "\u239c", "color": "21 bold", "tags": ["day1"], "group": "", "frame": "on"}
{"x": 74, "y": 8, "text": "\u239c", "color": "27 bold", "tags": ["day1"], "group": "", "frame": "on"}
{"x": 55, "y": 20, "text": "\u20ab", "color": "243", "tags": ["day0"], "group": "", "frame": "off"}
{"x": 61, "y": 20, "text": "\u211f", "color": "243", "tags": ["day0"], "group": "", "frame": "off"}
{"x": 60, "y": 20, "text": "\u2107", "color": "243", "tags": ["day0"], "group": "", "frame": "off"}
{"x": 56, "y": 20, "text": "\u0461", "color": "243", "tags": ["day0"], "group": "", "frame": "off"}
{"x": 59, "y": 20, "text": "\u04ce", "color": "243", "tags": ["day0"], "group": "", "frame": "off"}
{"x": 57, "y": 20, "text": "\u2139", "color": "243", "tags": ["day0"], "group": "", "frame": "off"}
{"x": 58, "y": 20, "text": "\u04cd", "color": "243", "tags": ["day0"], "group": "", "frame": "off"}
{"x": 63, "y": 20, "text": "\u2661", "color": "235", "tags": ["day0"], "group": "", "frame": "off"}
{"x": 65, "y": 20, "text": "\u20ab", "color": "243", "tags": ["day0"], "group": "", "frame": "off"}
{"x": 66, "y": 20, "text": "\u2107", "color": "243", "tags": ["day0"], "group": "", "frame": "off"}
{"x": 67, "y": 20, "text": "\u2123", "color": "243", "tags": ["day0"], "group": "", "frame": "off"}
{"x": 69, "y": 20, "text": "\u20ae", "color": "243", "tags": ["day0"], "group": "", "frame": "off"}
{"x": 68, "y": 20, "text": "\u0298", "color": "243", "tags": ["day0"], "group": "", "frame": "off"}
{"x": 70, "y": 20, "text": "\u0167", "color": "243", "tags": ["day0"], "group": "", "frame": "off"}
{"x": 72, "y": 20, "text": "\u023f", "color": "242", "tags": ["day0"], "group": "", "frame": "off"}
{"x": 71, "y": 20, "text": "\u024f", "color": "243", "tags": ["day0"], "group": "", "frame": "off"}
visidata-3.1.1/visidata/experimental/noahs_tapestry/noahs.sqlite 0000664 0000000 0000000 00176710000 14770045464 0025246 0 ustar 00root root 0000000 0000000 SQLite format 3 @ .WJ
6
%%?tableorders_itemsorders_itemsCREATE TABLE orders_items (orderid integer,sku text,qty integer,unit_price decimal(10,2))ytableordersordersCREATE TABLE orders (orderid text,customerid text,ordered timestamp,shipped timestamp,total decimal(10,2),items array)@
OtablecustomerscustomersCREATE TABLE customers (customerid integer,name text,address text,citystatezip text,birthdate text,phone text,timezone text,lat decimal(10,5),long decimal(10,5))r 7tableproductsproductsCREATE TABLE products (sku text,desc text,wholesale_cost decimal(10,2),dims_cm array) 9 p ( `
Q A
x 1 j " Z > K , ysmga[UOIC=71+%
}wqke_YSMGA;5/)#
{
u
o
i
c
]
W
Q
K
E
?
9
3
-
'
!
ysmga[UOIC=71+%
}wqke_YSMGA;5/)#
{
u
o
i
c
]
W
Q
K
E
?
9
3
-
'
!
y s m g a [ U O I C = 7 1 + %
e C ! ] ; w U 3 o M + g E !# # %^ &<