././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1612763830.9979815 visidata-2.2.1/0000770000175000017500000000000000000000000014006 5ustar00kefalakefala00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593229243.0 visidata-2.2.1/LICENSE.gpl30000660000175000017500000010451300000000000015664 0ustar00kefalakefala00000000000000 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 . ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612763790.0 visidata-2.2.1/MANIFEST.in0000660000175000017500000000017500000000000015550 0ustar00kefalakefala00000000000000include README.md include LICENSE.gpl3 include visidata/man/vd.1 include visidata/man/vd.txt include visidata/man/visidata.1 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1612763830.9979815 visidata-2.2.1/PKG-INFO0000660000175000017500000001051500000000000015106 0ustar00kefalakefala00000000000000Metadata-Version: 2.1 Name: visidata Version: 2.2.1 Summary: terminal interface for exploring and arranging tabular data Home-page: https://visidata.org Author: Saul Pwanson Author-email: visidata@saul.pw License: GPLv3 Download-URL: https://github.com/saulpw/visidata/tarball/2.2.1 Description: # VisiData v2.2.1 [![twitter @VisiData][1.1]][1] [![CircleCI](https://circleci.com/gh/saulpw/visidata/tree/stable.svg?style=svg)](https://circleci.com/gh/saulpw/visidata/tree/stable) [![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/saulpw/visidata) A terminal interface for exploring and arranging tabular data. ![Frequency table](http://visidata.org/freq-move-row.gif) 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.6+ - 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 $ vd $ | vd 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 [freenode.net](https://webchat.freenode.net/). 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 Keywords: console tabular data spreadsheet terminal viewer textpunkcurses csv hdf5 h5 xlsx excel tsv Platform: UNKNOWN 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.6 Description-Content-Type: text/markdown ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612763790.0 visidata-2.2.1/README.md0000660000175000017500000000525000000000000015270 0ustar00kefalakefala00000000000000 # VisiData v2.2.1 [![twitter @VisiData][1.1]][1] [![CircleCI](https://circleci.com/gh/saulpw/visidata/tree/stable.svg?style=svg)](https://circleci.com/gh/saulpw/visidata/tree/stable) [![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/saulpw/visidata) A terminal interface for exploring and arranging tabular data. ![Frequency table](http://visidata.org/freq-move-row.gif) 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.6+ - 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 $ vd $ | vd 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 [freenode.net](https://webchat.freenode.net/). 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 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1612763830.8819814 visidata-2.2.1/bin/0000770000175000017500000000000000000000000014556 5ustar00kefalakefala00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1606688014.0 visidata-2.2.1/bin/vd0000770000175000017500000000014400000000000015114 0ustar00kefalakefala00000000000000#!/usr/bin/env python3 import visidata.main if __name__ == '__main__': visidata.main.vd_cli() ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1612763830.9979815 visidata-2.2.1/setup.cfg0000660000175000017500000000004600000000000015630 0ustar00kefalakefala00000000000000[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612763790.0 visidata-2.2.1/setup.py0000770000175000017500000000372600000000000015533 0ustar00kefalakefala00000000000000#!/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__ = '2.2.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.6', author_email='visidata@saul.pw', url='https://visidata.org', download_url='https://github.com/saulpw/visidata/tarball/' + __version__, scripts=['bin/vd'], entry_points={'console_scripts': [ 'visidata=visidata.main:vd_cli' ], }, py_modules = ['visidata'], install_requires=['python-dateutil'], packages=['visidata', 'visidata.loaders', 'visidata.tests'], include_package_data=True, data_files = [('share/man/man1', ['visidata/man/vd.1', 'visidata/man/visidata.1'])], package_data={'visidata': ['man/vd.1', 'man/vd.txt']}, 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'), ) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1612763830.9379814 visidata-2.2.1/visidata/0000770000175000017500000000000000000000000015612 5ustar00kefalakefala00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612763790.0 visidata-2.2.1/visidata/__init__.py0000660000175000017500000000676200000000000017737 0ustar00kefalakefala00000000000000'VisiData: a curses interface for exploring and arranging tabular data' __version__ = '2.2.1' __version_info__ = 'VisiData v' + __version__ __author__ = 'Saul Pwanson ' __status__ = 'Production/Stable' __copyright__ = 'Copyright (c) 2016-2019 ' + __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(g): '''Update the VisiData globals dict with items from *g*, which is a mapping of names to functions. Importers can call ``addGlobals(globals())`` to have their globals accessible to execstrings.''' globals().update(g) def getGlobals(): 'Return the VisiData globals dict.' return globals() from builtins import * from copy import copy, deepcopy from .utils import * from .extensible import * from .vdobj import * vd = VisiData() vd.addGlobals = addGlobals vd.getGlobals = getGlobals from .basesheet import * from .settings import * from .errors import * from .editor import * from .cliptext import * from .color import * from .mainloop import * from .wrappers import * from .undo import * from ._types import * from .column import * theme = option # convert theme(...) to option(...) and move this down, eventually into deprecated.py from .sheets import * from .statusbar import * from .textsheet import * from .threads import * from ._input import * from .movement import * from .path import * from .urlcache import * from .selection import * from .loaders.tsv import * from .pyobj import * from .loaders.json import * from ._open import * from .save import * from .clipboard import * from .slide import * from .search import * from .expr import * from .choose import * from .metasheets import * from .join import * from .aggregators import * from .describe import * from .pivot import * from .freqtbl import * from .melt import * from .cmdlog import * from .freeze import * from .regex import * from .canvas import * from .graph import * from .motd import * from .transpose import * from .shell import * from .layout import * from .main import * from .help import * from .modify import * import visidata.sort import visidata.unfurl import visidata.fill import visidata.incr import visidata.customdate import visidata.misc from .macros import * from .menu import * from .loaders.csv import * from .loaders.archive import * from .loaders.xlsx import * from .loaders.xlsb import * from .loaders.hdf5 import * from .loaders.sqlite import * from .loaders.fixed_width import * from .loaders.postgres import * from .loaders.mysql import * from .loaders.shp import * from .loaders.geojson import * from .loaders.mbtiles import * from .loaders.http import * from .loaders.html import * from .loaders.markdown import * from .loaders.pcap import * from .loaders.png import * from .loaders.ttf import * from .loaders.sas import * from .loaders.spss import * from .loaders.xml import * from .loaders.yaml import * from .loaders._pandas import * from .loaders.graphviz import * from .loaders.npy import * from .loaders.usv import * from .loaders.frictionless import * from .loaders.imap import * from .loaders.pdf import * from .loaders.pandas_freqtbl import * from .loaders.xword import * from .loaders.vcf import * from .loaders.texttables import * from .loaders.rec import * from .loaders.eml import * from .loaders.vds import * from .plugins import * from .colorsheet import * from .deprecated import * from math import * vd.finalInit() vd.addGlobals(globals()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608541899.0 visidata-2.2.1/visidata/__main__.py0000660000175000017500000000004300000000000017702 0ustar00kefalakefala00000000000000from .main import vd_cli vd_cli() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612075960.0 visidata-2.2.1/visidata/_input.py0000660000175000017500000003454400000000000017475 0ustar00kefalakefala00000000000000from contextlib import suppress import collections import curses import visidata from visidata import EscapeException, ExpectedException, clipdraw, Sheet, VisiData from visidata import vd, option, options, theme, colors from visidata import launchExternalEditor, suspend, ColumnItem, AttrDict __all__ = ['confirm', 'CompleteKey'] theme('color_edit_cell', 'normal', 'cell color to use when editing cell') theme('disp_edit_fill', '_', 'edit field fill character') theme('disp_unprintable', '·', 'substitute character for unprintables') option('input_history', '', 'basename of file to store persistent input history') class AcceptInput(Exception): '*args[0]* is the input to be accepted' visidata.vd._nextCommands = [] @VisiData.api def queueCommand(vd, longname): #, input=None, sheet=None, col=None, row=None): vd._nextCommands.append(longname) 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) curses.mousemask(-1) def until_get_wch(scr): 'Ignores get_wch timeouts' ret = None while not ret: try: ret = scr.get_wch() except curses.error: pass return ret def splice(v, i, s): 'Insert `s` into string `v` at `i` (such that v[i] == s[0]).' return v if i < 0 else v[:i] + s + v[i:] def clean_printable(s): 'Escape unprintable characters.' return ''.join(c if c.isprintable() else 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:] class CompleteState: def __init__(self, completer_func): self.comps_idx = -1 self.completer_func = completer_func self.former_i = None self.just_completed = False def complete(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(self): if self.just_completed: self.just_completed = False else: self.former_i = None self.comps_idx = -1 class HistoryState: def __init__(self, history): self.history = history self.hist_idx = None self.prev_val = None def up(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(v) return v, i def down(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(v) return v, i # history: earliest entry first @VisiData.api def editline(vd, scr, y, x, w, i=0, attr=curses.A_NORMAL, value='', fillchar=' ', truncchar='-', unprintablechar='.', completer=lambda text,idx: None, history=[], display=True, updater=lambda val: None, bindings={}): 'A better curses line editing widget.' with EnableCursor(): ESC='^[' TAB='^I' history_state = HistoryState(history) complete_state = CompleteState(completer) insert_mode = True first_action = True v = str(value) # value under edit # i = 0 # index into v, initial value can be passed in as argument as of 1.2 if i != 0: first_action = False left_truncchar = right_truncchar = truncchar 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)) while True: updater(v) if display: dispval = clean_printable(v) else: dispval = '*' * len(v) dispi = i # the onscreen offset within the field where v[i] is displayed if len(dispval) < w: # entire value fits dispval += fillchar*(w-len(dispval)-1) elif i == len(dispval): # cursor after value (will append) dispi = w-1 dispval = left_truncchar + dispval[len(dispval)-w+2:] + fillchar elif i >= len(dispval)-w//2: # cursor within halfwidth of end dispi = 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: dispi = 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[i-w//2+1:i+w//2-k] + right_truncchar prew = clipdraw(scr, y, x, dispval[:dispi], attr, w) clipdraw(scr, y, x+prew, dispval[dispi:], attr, w-prew+1) scr.move(y, x+prew) ch = vd.getkeystroke(scr) if ch == '': continue elif ch == 'KEY_IC': insert_mode = not 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', ESC): 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 in ('^H', 'KEY_BACKSPACE', '^?'): i -= 1; v = delchar(v, i) elif ch == TAB: v, i = complete_state.complete(v, i, +1) elif ch == 'KEY_BTAB': v, i = complete_state.complete(v, i, -1) elif ch in ['^J', '^M']: break # ENTER to accept value elif ch == '^K': v = v[:i] # ^Kill to end-of-line elif ch == '^O': v = launchExternalEditor(v) elif ch == '^R': v = str(value) # ^Reload initial value elif ch == '^T': v = delchar(splice(v, i-2, v[i-1]), 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, vd.clipcells[0]) elif ch == '^Z': 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 history and ch == 'KEY_UP': v, i = history_state.up(v, i) elif history and ch == 'KEY_DOWN': v, i = history_state.down(v, i) elif ch in bindings: v, i = bindings[ch](v, i) elif len(ch) > 1: pass else: if first_action: v = '' if insert_mode: v = splice(v, i, ch) else: v = v[:i] + ch + v[i+1:] i += 1 if i < 0: i = 0 if i > len(v): i = len(v) first_action = False complete_state.reset() return v @VisiData.api def editText(vd, y, x, w, record=True, display=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.getLastArgs() if v is None: try: v = vd.editline(vd.sheets[0]._scr, y, x, w, display=display, **kwargs) except AcceptInput as e: v = e.args[0] # clear keyboard buffer to neutralize multi-line pastes (issue#585) curses.flushinp() if display: vd.status('"%s"' % v) if record and vd.cmdlog: vd.setLastArgs(v) return v @VisiData.api def inputsingle(vd, prompt, record=True): 'Display prompt and return single character of user input.' sheet = vd.sheets[0] rstatuslen = vd.drawRightStatus(sheet._scr, sheet) v = None if record and vd.cmdlog: v = vd.getLastArgs() 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) v = vd.getkeystroke(sheet._scr) if record and vd.cmdlog: vd.setLastArgs(v) return v @VisiData.api def input(self, prompt, type=None, defaultLast=False, history=[], **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). - *record*: pass False to not record input on cmdlog (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) ''' history = self.lastInputsSheet.history(type) sheet = self.activeSheet rstatuslen = self.drawRightStatus(sheet._scr, sheet) attr = 0 promptlen = clipdraw(sheet._scr, sheet.windowHeight-1, 0, prompt, attr, w=sheet.windowWidth-rstatuslen-1) ret = self.editText(sheet.windowHeight-1, promptlen, sheet.windowWidth-promptlen-rstatuslen-2, attr=colors.color_edit_cell, unprintablechar=options.disp_unprintable, truncchar=options.disp_truncator, history=history, **kwargs) if ret: self.lastInputsSheet.appendRow(AttrDict(type=type, input=ret)) elif defaultLast: history or vd.fail("no previous input") ret = history[-1] return ret @VisiData.global_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 options.batch: 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'), } bindings.update(kwargs.get('bindings', {})) kwargs['bindings'] = bindings editargs = dict(value=value, fillchar=options.disp_edit_fill, truncchar=options.disp_truncator) editargs.update(kwargs) # update with user-specified args r = vd.editText(y, x, w, **editargs) if rowidx >= 0: # if not header r = col.type(r) # convert input to column type, let exceptions be raised return r ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612075960.0 visidata-2.2.1/visidata/_open.py0000660000175000017500000000734100000000000017272 0ustar00kefalakefala00000000000000from visidata import * __all__ = ['open_txt'] option('filetype', '', 'specify file type', replay=True) @VisiData.api def inputFilename(vd, prompt, *args, **kwargs): return vd.input(prompt, type="filename", *args, completer=_completeFilename, **kwargs) @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 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.fp: # isinstance(p, UrlPath): openfunc = 'openurl_' + p.scheme try: return vd.getGlobals()[openfunc](p, filetype=filetype) except KeyError: vd.fail(f'no loader for url scheme: {p.scheme}') if not filetype: if p.is_dir(): filetype = 'dir' else: filetype = p.ext or options.filetype or 'txt' if not p.exists(): if not create: return None vd.warning('%s does not exist, creating new sheet' % p) return vd.newSheet(p.name, 1, source=p) filetype = filetype.lower() openfunc = getattr(vd, 'open_' + filetype, vd.getGlobals().get('open_' + filetype)) if not openfunc: vd.warning('unknown "%s" filetype' % filetype) filetype = 'txt' openfunc = vd.getGlobals().get('open_txt') vd.status('opening %s as %s' % (p.given, filetype)) return openfunc(p) @VisiData.global_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 not filetype: filetype = 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 == '-': vs = vd.openPath(Path('-', fp=vd._stdin), filetype=filetype) else: vs = vd.openPath(Path(p), filetype=filetype, create=create) # convert to Path and recurse else: vs = vs or vd.openPath(p, filetype=filetype, create=create) for optname, optval in kwargs.items(): vs.options[optname] = optval return vs #### enable external addons def open_txt(p): 'Create sheet from `.txt` file at Path `p`, checking whether it is TSV.' with p.open_text() as fp: if options.delimiter in next(fp): # peek at the first line return open_tsv(p) # TSV often have .txt extension return TextSheet(p.name, source=p) @VisiData.api def loadInternalSheet(vd, cls, p, **kwargs): 'Load internal sheet of given class. Internal sheets are always tsv.' vs = cls(p.name, source=p, **kwargs) options._set('encoding', 'utf8', vs) if p.exists(): vd.sheets.insert(0, vs) vs.reload.__wrapped__(vs) vd.sheets.pop(0) return vs BaseSheet.addCommand('o', 'open-file', 'vd.push(openSource(inputFilename("open: "), create=True))', 'open input in VisiData') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612075960.0 visidata-2.2.1/visidata/_types.py0000660000175000017500000001516400000000000017477 0ustar00kefalakefala00000000000000# VisiData uses Python native int, float, str, and adds simple date, currency, and anytype. import collections import functools import datetime import locale from visidata import option, options, TypedWrapper, vd, VisiData #__all__ = ['anytype', 'vdtype', ] option('disp_currency_fmt', '%.02f', 'default fmtstr to format for currency values', replay=True) option('disp_float_fmt', '{:.02f}', 'default fmtstr to format for float values', replay=True) option('disp_int_fmt', '{:.0f}', 'default fmtstr to format for int values', replay=True) option('disp_date_fmt','%Y-%m-%d', 'default fmtstr to strftime for date values', replay=True) try: import dateutil.parser except ImportError: pass # 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__ = '' def numericFormatter(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) vd.si_prefixes='p n u m . kK M G T P Q'.split() def floatsi(*args): if not args: return 0.0 if not isinstance(args[0], str): return args[0] s=args[0].strip() for i, p in enumerate(vd.si_prefixes): if s[-1] in p: return float(s[:-1]) * (1000 ** (i-4)) return float(s) def SIFormatter(fmtstr, val): level = 4 if val != 0: while abs(val) > 1000: val /= 1000 level += 1 while abs(val) < 0.001: val *= 1000 level -= 1 return numericFormatter(fmtstr, val) + (vd.si_prefixes[level][0] if level != 4 else '') class VisiDataType: 'Register *typetype* in the typemap.' def __init__(self, typetype=None, icon=None, fmtstr='', formatter=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=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 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, '∅') 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 (int,vlen,float,currency,date,floatsi,floatlocale) ## floatchars='+-0123456789.' def currency(*args): 'dirty float (strip non-numeric characters)' if args and isinstance(args[0], str): args = [''.join(ch for ch in args[0] if ch in floatchars)] return float(*args) def floatlocale(*args): 'Calculate float() using system locale set in LC_NUMERIC.' if not args: return 0.0 return locale.atof(*args) 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 class date(datetime.datetime): 'datetime wrapper, constructed from time_t or from str with dateutil.parse' def __new__(cls, *args, **kwargs): 'datetime is immutable so needs __new__ instead of __init__' if not args: return datetime.datetime.now() elif len(args) > 1: return super().__new__(cls, *args, **kwargs) s = args[0] if isinstance(s, int) or isinstance(s, float): r = datetime.datetime.fromtimestamp(s) elif isinstance(s, str): r = dateutil.parser.parse(s) elif isinstance(s, (datetime.datetime, datetime.date)): r = s else: raise Exception('invalid type for date %s' % type(s).__name__) t = r.timetuple() ms = getattr(r, 'microsecond', 0) tzinfo = getattr(r, 'tzinfo', None) return super().__new__(cls, *t[:6], microsecond=ms, tzinfo=tzinfo, **kwargs) def __str__(self): return self.strftime(options.disp_date_fmt) def __float__(self): return self.timestamp() def __radd__(self, n): return self.__add__(n) def __add__(self, n): 'add n days (int or float) to the date' if isinstance(n, (int, float)): n = datetime.timedelta(days=n) return date(super().__add__(n)) def __sub__(self, n): 'subtract n days (int or float) from the date. or subtract another date for a timedelta' if isinstance(n, (int, float)): n = datetime.timedelta(days=n) elif isinstance(n, (date, datetime.datetime)): return datedelta(super().__sub__(n).total_seconds()/(24*60*60)) return super().__sub__(n) class datedelta(datetime.timedelta): def __float__(self): return self.total_seconds() vdtype(vlen, '♯', '%.0f') vdtype(floatlocale, '%') vdtype(date, '@', '', formatter=lambda fmtstr,val: val.strftime(fmtstr or options.disp_date_fmt)) vdtype(currency, '$') vdtype(floatsi, '‱', formatter=SIFormatter) # simple constants, for expressions like 'timestamp+15*minutes' years=365.25 months=30.0 weeks=7.0 days=1.0 hours=days/24 minutes=days/(24*60) seconds=days/(24*60*60) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612075960.0 visidata-2.2.1/visidata/aggregators.py0000660000175000017500000001374000000000000020477 0ustar00kefalakefala00000000000000import math import functools import collections from statistics import mode, stdev from visidata import Progress, Column from visidata import * @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('aggregators', list) def aggstr_get(col): 'A space-separated names of aggregators on this column.' return ' '.join(aggr.name for aggr in col.aggregators) def aggstr_set(col, v): col.aggregators = list(vd.aggregators[k] for k in (v or '').split()) Column.aggstr = property(aggstr_get, aggstr_set) class Aggregator: def __init__(self, name, type, func, helpstr='foo'): 'Define aggregator `name` that calls func(col, rows)' self.type = type self.func = func self.helpstr = helpstr self.name = name def __call__(self, *args, **kwargs): return self.func(*args, **kwargs) _defaggr = Aggregator @VisiData.global_api def aggregator(vd, name, func, helpstr='', *args, type=None): 'Define simple aggregator *name* that calls ``func(values, *args)`` to aggregate *values*. Use *type* to force the default type of the aggregated column.' def _func(col, rows): # wrap builtins so they can have a .type vals = list(col.getValues(rows)) try: return func(vals, *args) except Exception as e: if len(vals) == 0: return None return e vd.aggregators[name] = _defaggr(name, type, _func, helpstr) ## specific aggregator implementations def mean(vals): vals = list(vals) if vals: return float(sum(vals))/len(vals) def median(values): L = sorted(values) return L[len(L)//2] # 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) def percentile(pct, helpstr=''): return _defaggr('p%s'%pct, None, lambda col,rows,pct=pct: _percentile(sorted(col.getValues(rows)), pct/100), helpstr) def quantiles(q, helpstr): return [percentile(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', median, 'median of values') vd.aggregator('mode', mode, 'mode of values') vd.aggregator('sum', sum, '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') vd.aggregator('stdev', 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/80th pctiles)') # returns keys of the row with the max value vd.aggregators['keymax'] = _defaggr('keymax', anytype, lambda col, rows: col.sheet.rowkey(max(col.getValueRows(rows))[1]), 'key of the maximum value') ColumnsSheet.columns += [ColumnAttr('aggregators','aggstr')] @Sheet.api def addAggregators(sheet, cols, aggrnames): 'Add each aggregator in list of *aggrnames* to each of *cols*.' 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 @asyncthread def show_aggregate(col, agg, rows): 'Show aggregated value in status, and add to memory.' aggval = agg(col, rows) typedval = wrapply(agg.type or col.type, aggval) dispval = col.format(typedval) vd.status(dispval) @VisiData.property def aggregator_choices(vd): return [ {'key': agg, 'desc': v[0].helpstr if isinstance(v, list) else v.helpstr} for agg, v in vd.aggregators.items() ] vd.addGlobals(globals()) Sheet.addCommand('+', 'aggregate-col', 'addAggregators([cursorCol], chooseMany(aggregator_choices))', 'add aggregator to current column') Sheet.addCommand('z+', 'show-aggregate', 'for agg in chooseMany(aggregator_choices): cursorCol.show_aggregate(aggregators[agg], selectedRows or rows)', 'display result of aggregator over values in selected rows for current column') ColumnsSheet.addCommand('g+', 'aggregate-cols', 'addAggregators(selectedRows or source[0].nonKeyVisibleCols, chooseMany(aggregator_choices))', 'add aggregators to selected source columns') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1609740788.0 visidata-2.2.1/visidata/basesheet.py0000660000175000017500000001711200000000000020132 0ustar00kefalakefala00000000000000import os import visidata from visidata import Extensible, VisiData, vd, EscapeException from unittest import mock 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): self.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 keys(self): return list(self.objs.keys()) # sum(set(dir(obj)) for obj in self.objs)) 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 BaseSheet(Extensible): '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 @visidata.classproperty def class_options(cls): return vd.OptionsObject(vd._options, obj=cls) @property def options(self): return vd.OptionsObject(vd._options, obj=self) def __init__(self, *names, **kwargs): self._name = None self.name = self.options.name_joiner.join(str(x) for x in names) self.source = None self.rows = UNLOADED # list of opaque objects self._scr = mock.MagicMock(__bool__=mock.Mock(return_value=False)) # disable curses in batch mode self.__dict__.update(kwargs) 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 # copies can be precious even if originals aren't 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 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 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 def execCommand(self, cmd, args='', vdglobals=None, keystrokes=None): """Execute `cmd` tuple with `vdglobals` as globals and this sheet's attributes as locals. Return True if user cancelled. `cmd` can be a longname, a keystroke, or a Command object.""" cmd = self.getCommand(cmd or keystrokes) if not cmd: if keystrokes: vd.debug('no command "%s"' % keystrokes) return True escaped = False err = '' if vdglobals is None: vdglobals = vd.getGlobals() self.sheet = self try: for hookfunc in vd.beforeExecHooks: hookfunc(self, cmd, '', keystrokes) code = compile(cmd.execstr, cmd.longname, 'exec') vd.debug(cmd.longname) exec(code, vdglobals, LazyChainMap(vd, self)) except EscapeException as e: # user aborted vd.warning(str(e)) escaped = True except Exception as e: vd.debug(cmd.execstr) err = vd.exceptionCaught(e) escaped = True try: if vd.cmdlog: # sheet may have changed vd.cmdlog.afterExecSheet(vd.sheets[0] if vd.sheets else None, escaped, err) except Exception as e: vd.exceptionCaught(e) self.checkCursorNoExceptions() vd.clearCaches() return escaped @property def name(self): 'Name of this sheet.' try: return self._name except AttributeError: return self.rowtype @name.setter def name(self, name): 'Set name without spaces.' if self._name: vd.addUndo(setattr, self, '_name', self._name) self._name = visidata.maybe_clean(str(name), self) def recalc(self): 'Clear any calculated value caches.' pass def draw(self, scr): 'Draw the sheet on the terminal window *scr*. Overrideable.' vd.error('no draw') def refresh(self): 'Clear the terminal screen and let the next draw cycle redraw everything.' self._scr.clear() self._scr.refresh() 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. Overrideable.' return None def checkCursor(self): 'Check cursor and fix if out-of-bounds. Overrideable.' pass def checkCursorNoExceptions(self): try: return self.checkCursor() except Exception as e: vd.exceptionCaught(e) def evalExpr(self, expr, **kwargs): 'Evaluate Python expression *expr* in the context of *kwargs* (may vary by sheet type).' return eval(expr, vd.getGlobals(), None) @VisiData.api def redraw(vd): 'Clear the terminal screen and let the next draw cycle recreate the windows and redraw everything.' vd.scrFull.clear() vd.win1.clear() vd.win2.clear() vd.setWindows(vd.scrFull) @VisiData.property def sheet(self): 'the top sheet on the stack' return self.sheets[0] if self.sheets else None @VisiData.api def isLongname(self, ks): '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 = self.optionsSheet vs.reload() vs.vd = vd return vs ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608541899.0 visidata-2.2.1/visidata/canvas.py0000660000175000017500000007222000000000000017443 0ustar00kefalakefala00000000000000 from collections import defaultdict, Counter from visidata import * # see www/design/graphics.md option('show_graph_labels', True, 'show axes and legend on graph') theme('plot_colors', 'green red yellow cyan magenta white 38 136 168', 'list of distinct colors to use for plotting distinct objects') theme('disp_pixel_random', False, 'randomly choose attr from set of pixels instead of most common') option('zoom_incr', 2.0, 'amount to multiply current zoomlevel when zooming') theme('color_graph_hidden', '238 blue', 'color of legend for hidden attribute') theme('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(self.windowHeight, self.windowWidth) @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=0, row=None): self.pixels[y][x][attr].append(row) def plotline(self, x1, y1, x2, y2, attr=0, 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=0, row=None): self.labels.append((x, y, text, attr, row)) def plotlegend(self, i, txt, attr=0, width=15): self.plotlabel(self.plotwidth-width*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): 'weighted-random choice of attr 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): 'most common attr at this pixel.' r = self.pixels[y][x] c = [(len(rows), attr, rows) for attr, rows in list(r.items()) if attr and attr not in self.hiddenAttrs] if not c: return 0 _, attr, rows = max(c) if isinstance(self.source, BaseSheet) and anySelected(self.source, rows): attr = update_attr(ColorAttr(attr, 0, 8, attr), colors.color_graph_selected, 10).attr return attr def hideAttr(self, attr, hide=True): if hide: self.hiddenAttrs.add(attr) else: self.hiddenAttrs.remove(attr) self.plotlegends() def rowsWithin(self, bbox): 'return list of deduped rows within bbox' ret = {} for y in range(bbox.ymin, bbox.ymax+1): for x in range(bbox.xmin, min(len(self.pixels[y]), bbox.xmax+1)): 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) if self.pixels: cursorBBox = self.plotterCursorBox getPixelAttr = self.getPixelAttrRandom if 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 if braille_num != 0: attr = Counter(c for c in block_attrs if c).most_common(1)[0][0] else: attr = 0 if cursorBBox.contains(char_x*2, char_y*4) or \ cursorBBox.contains(char_x*2+1, char_y*4+3): attr = update_attr(ColorAttr(attr, 0, 0, attr), colors.color_current_row).attr if attr: scr.addstr(char_y, char_x, chr(0x2800+braille_num), attr) 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 options.show_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 clipdraw(scr, char_y, char_x, txt, attr, len(txt)) # - 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 bottomMarginPixels = 1*4 # reserve bottom line for x axis def __init__(self, *names, **kwargs): 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), ...], attr, row) self.gridlabels = [] # list of (grid_x, grid_y, label, attr, row) self.legends = collections.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.legends.clear() self.legendwidth = 0 self.plotAttrs.clear() self.unusedAttrs = list(colors[colorname.translate(str.maketrans('_', ' '))] for colorname in options.plot_colors.split()) def plotColor(self, k): attr = self.plotAttrs.get(k, None) if attr is None: if len(self.unusedAttrs) > 1: attr = self.unusedAttrs.pop(0) legend = ' '.join(str(x) for x in k) else: attr = self.unusedAttrs[0] legend = '[other]' self.legendwidth = max(self.legendwidth, len(legend)) self.legends[legend] = attr self.plotAttrs[k] = attr self.plotlegends() return attr def resetCanvasDimensions(self, windowHeight, windowWidth): super().resetCanvasDimensions(windowHeight, windowWidth) self.plotviewBox = BoundingBox(self.leftMarginPixels, self.topMarginPixels, self.plotwidth-self.rightMarginPixels, self.plotheight-self.bottomMarginPixels-1) @property def statusLine(self): return 'canvas %s visible %s cursor %s' % (self.canvasBox, self.visibleBox, self.cursorBox) @property def canvasMouse(self): return self.canvasFromPlotterCoord(self.plotterMouse.x, self.plotterMouse.y) def canvasFromPlotterCoord(self, plotter_x, plotter_y): return Point(self.visibleBox.xmin + (plotter_x-self.plotviewBox.xmin)/self.xScaler, self.visibleBox.ymin + (plotter_y-self.plotviewBox.ymin)/self.yScaler) def canvasFromTerminalCoord(self, x, y): return self.canvasFromPlotterCoord(*self.plotterFromTerminalCoord(x, y)) def setCursorSize(self, p): 'sets width based on diagonal corner p' 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) @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 point(self, x, y, attr=0, row=None): self.polylines.append(([(x, y)], attr, row)) def line(self, x1, y1, x2, y2, attr=0, row=None): self.polylines.append(([(x1, y1), (x2, y2)], attr, row)) def polyline(self, vertexes, attr=0, row=None): 'adds lines for (x,y) vertexes of a polygon' self.polylines.append((vertexes, attr, row)) def polygon(self, vertexes, attr=0, row=None): 'adds lines for (x,y) vertexes of a polygon' self.polylines.append((vertexes + [vertexes[0]], attr, row)) def qcurve(self, vertexes, attr=0, 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] self.point(x1, y1, attr, row) self._recursive_bezier(x1, y1, x2, y2, x3, y3, attr, row) self.point(x3, y3, attr, row) def _recursive_bezier(self, x1, y1, x2, y2, x3, y3, attr, row, 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: self.point(x123, y123, attr, row) 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 self.point(x123, y123, attr, row) return else: # Collinear case dx = x123 - (x1 + x3) / 2 dy = y123 - (y1 + y3) / 2 if dx*dx + dy*dy <= m_distance_tolerance: self.point(x123, y123, attr, row) return # Continue subdivision self._recursive_bezier(x1, y1, x12, y12, x123, y123, attr, row, level + 1) self._recursive_bezier(x123, y123, x23, y23, x3, y3, attr, row, level + 1) def label(self, x, y, text, attr=0, 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.refresh() 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 def incrZoom(self, incr): self.xzoomlevel *= incr self.yzoomlevel *= incr self.resetBounds() def resetBounds(self): 'create canvasBox and cursorBox if necessary, and set visibleBox w/h according to zoomlevels. then redisplay labels.' 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 self.canvasBox = BoundingBox(float(xmin or 0), float(ymin or 0), float(xmax or 1), float(ymax or 1)) if not self.visibleBox: # initialize minx/miny, but w/h must be set first to center properly self.visibleBox = Box(0, 0, self.plotviewBox.w/self.xScaler, self.plotviewBox.h/self.yScaler) self.visibleBox.xmin = self.canvasBox.xcenter - self.visibleBox.w/2 self.visibleBox.ymin = self.canvasBox.ycenter - self.visibleBox.h/2 else: self.visibleBox.w = self.plotviewBox.w/self.xScaler self.visibleBox.h = self.plotviewBox.h/self.yScaler if not self.cursorBox: self.cursorBox = Box(self.visibleBox.xmin, self.visibleBox.ymin, self.canvasCharWidth, self.canvasCharHeight) self.plotlegends() def plotlegends(self): # display labels for i, (legend, attr) in enumerate(self.legends.items()): self.addCommand(str(i+1), 'toggle-%s'%(i+1), 'hideAttr(%s, %s not in hiddenAttrs)' % (attr, attr), 'toggle display of "%s"' % legend) if attr in self.hiddenAttrs: attr = colors.color_graph_hidden self.plotlegend(i, '%s:%s'%(i+1,legend), attr, width=self.legendwidth+4) 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 scaleX(self, x): 'returns plotter x coordinate' return round(self.plotviewBox.xmin+(x-self.visibleBox.xmin)*self.xScaler) def scaleY(self, y): 'returns plotter y coordinate' return round(self.plotviewBox.ymin+(y-self.visibleBox.ymin)*self.yScaler) 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 cancelThread(*(t for t in self.currentThreads if t.name == 'plotAll_async')) self.labels.clear() self.resetCanvasDimensions(h, w) self.render_async() @asyncthread def render_async(self): self.render_sync() def render_sync(self): '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, plotymin = self.plotviewBox.xmin, 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: x = plotxmin+(x1-xmin)*xfactor y = plotymin+(y1-ymin)*yfactor self.plotpixel(round(x), round(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 y1 = plotymin+float(y1-ymin)*yfactor x2 = plotxmin+float(x2-xmin)*xfactor 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.show_graph_labels = not options.show_graph_labels', 'toggle show_graph_labels option') Canvas.addCommand(None, 'go-left', 'sheet.cursorBox.xmin -= cursorBox.w', 'move cursor left by its width') Canvas.addCommand(None, 'go-right', 'sheet.cursorBox.xmin += cursorBox.w', 'move cursor right by its width' ) Canvas.addCommand(None, 'go-up', 'sheet.cursorBox.ymin -= cursorBox.h', 'move cursor up by its height') Canvas.addCommand(None, 'go-down', 'sheet.cursorBox.ymin += cursorBox.h', 'move cursor down by its height') Canvas.addCommand(None, 'go-leftmost', 'sheet.cursorBox.xmin = visibleBox.xmin', 'move cursor to left edge of visible canvas') Canvas.addCommand(None, 'go-rightmost', 'sheet.cursorBox.xmin = visibleBox.xmax-cursorBox.w', 'move cursor to right edge of visible canvas') Canvas.addCommand(None, 'go-top', 'sheet.cursorBox.ymin = visibleBox.ymin', 'move cursor to top edge of visible canvas') Canvas.addCommand(None, 'go-bottom', 'sheet.cursorBox.ymin = visibleBox.ymax', '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.zoom_incr); fixPoint(plotviewBox.center, tmp)', 'zoom out from cursor center') Canvas.addCommand('+', 'zoomin-cursor', 'tmp=cursorBox.center; incrZoom(1.0/options.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; refresh()', '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', 'sheet.cursorBox = Box(*canvasMouse.xy)', 'start cursor box with left mouse button press') Canvas.addCommand('BUTTON1_RELEASED', 'end-cursor', 'setCursorSize(canvasMouse)', 'end cursor box with left mouse button release') Canvas.addCommand('BUTTON3_PRESSED', 'start-move', 'sheet.anchorPoint = canvasMouse', 'mark grid point to move') Canvas.addCommand('BUTTON3_RELEASED', 'end-move', 'fixPoint(plotterMouse, anchorPoint)', 'mark canvas anchor point') Canvas.addCommand('BUTTON4_PRESSED', 'zoomin-mouse', 'tmp=canvasMouse; incrZoom(1.0/options.zoom_incr); fixPoint(plotterMouse, tmp)', 'zoom in with scroll wheel') Canvas.addCommand('REPORT_MOUSE_POSITION', 'zoomout-mouse', 'tmp=canvasMouse; incrZoom(options.zoom_incr); fixPoint(plotterMouse, tmp)', 'zoom out with scroll wheel') Canvas.bindkey('2097152', 'zoomout-mouse') 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') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612075960.0 visidata-2.2.1/visidata/choose.py0000660000175000017500000000563400000000000017455 0ustar00kefalakefala00000000000000from copy import copy from visidata import vd, option, options, VisiData, ListOfDictSheet, ENTER, CompleteKey, ReturnValue option('fancy_chooser', False, 'a nicer selection interface for aggregators and jointype') @VisiData.api def chooseOne(vd, choices): 'Return one user-selected key from *choices*.' return vd.choose(choices, 1) @VisiData.api def choose(vd, choices, n=None): 'Return a list of 1 to *n* "key" from elements of *choices* (see chooseMany).' ret = vd.chooseMany(choices) 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): '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}) for c in chosenstr.split(): poss = [p for p in choice_keys if str(p).startswith(c)] if not poss: vd.warning('invalid choice "%s"' % c) else: chosen.extend(poss) except ReturnValue as e: chosen = e.args[0] if vd.cmdlog: vd.status(str(chosen)) vd.setLastArgs(' '.join(chosen)) return chosen ChoiceSheet.addCommand(ENTER, 'choose-rows', 'makeChoice([cursorRow])') ChoiceSheet.addCommand('g'+ENTER, 'choose-rows-selected', 'makeChoice(onlySelectedRows)') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612761678.0 visidata-2.2.1/visidata/clipboard.py0000660000175000017500000002115400000000000020127 0ustar00kefalakefala00000000000000from copy import copy, deepcopy import shutil import subprocess import sys import tempfile import functools from visidata import VisiData, vd, asyncthread, option, options from visidata import Sheet, saveSheets, Path, Column option('clipboard_copy_cmd', '', 'command to copy stdin to system clipboard', sheettype=None) option('clipboard_paste_cmd', '', 'command to get contents of system clipboard', sheettype=None) VisiData.init('cliprows', list) # list of (source_sheet, source_row_idx, source_row) VisiData.init('clipcells', list) # list of strings def setslice(L, a, b, M): L[a:b] = M @Sheet.api def copyRows(sheet, rows): vd.cliprows = list((sheet, i, r) for i, r in enumerate(rows)) 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.clipcells = [col.getDisplayValue(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 syscopyRows(sheet, rows): if not rows: vd.fail('no %s selected' % sheet.rowtype) filetype = vd.input("copy %d %s to system clipboard as filetype: " % (len(rows), sheet.rowtype), value=options.save_filetype) saveToClipboard(sheet, rows, filetype) vd.status('copied %d %s to system clipboard' % (len(rows), sheet.rowtype)) @Sheet.api def syscopyCells(sheet, col, rows): if not rows: vd.fail('no %s selected' % sheet.rowtype) clipboard().copy("\n".join(col.getDisplayValue(r) for r in rows)) vd.status('copied %s from %d %s to system clipboard' % (col.name, len(rows), sheet.rowtype)) @Sheet.api def delete_row(sheet, rowidx): if not sheet.defer: oldrow = sheet.rows.pop(rowidx) vd.addUndo(sheet.rows.insert, rowidx, oldrow) else: oldrow = sheet.rows[rowidx] sheet.rowDeleted(oldrow) vd.cliprows = [(sheet, rowidx, oldrow)] @Sheet.api def paste_after(sheet, rowidx): vd.addUndo(sheet.rows.pop, rowidx+1) sheet.rows[rowidx+1:rowidx+1] = list(deepcopy(r) for s,i,r in vd.cliprows) @Sheet.api def paste_before(sheet, rowidx): sheet.rows[sheet.cursorRowIndex:sheet.cursorRowIndex] = list(deepcopy(r) for s,i,r in vd.cliprows) vd.addUndo(sheet.rows.pop, rowidx) # mapping of OS to list of possible (command name, command args) for copy and # paste commands __copy_commands = { # TODO TEST WINDOWS AND MAC 'win32': [('clip', '')], 'darwin': [('pbcopy', 'w')], # try these for all other platforms None: [('xclip', '-selection clipboard -filter'), ('xsel', '--clipboard --input')] } __paste_commands = { # TODO TEST WINDOWS AND MAC 'win32': [('clip', '')], 'darwin': [('pbpaste', '')], # try these for all other platforms None: [('xclip', '-selection clipboard -o'), ('xsel', '--clipboard')] } def detect_command(cmdlist): '''Detect available clipboard util and return cmdline to copy data to the system clipboard. cmddict is list of (platform, progname, argstr).''' for cmd, args in cmdlist.get(sys.platform, cmdlist[None]): path = shutil.which(cmd) if path: # see if command exists on system return ' '.join([path, args]) return '' detect_copy_command = lambda: detect_command(__copy_commands) detect_paste_command = lambda: detect_command(__paste_commands) @functools.lru_cache() def clipboard(): 'Detect cmd and set option at first use, to allow option to be changed by user later.' if not options.clipboard_copy_cmd: options.clipboard_copy_cmd = detect_copy_command() if not options.clipboard_paste_cmd: options.clipboard_paste_cmd = detect_paste_command() return _Clipboard() class _Clipboard: 'Cross-platform helper to copy a cell or multiple rows to the system clipboard.' def get_command(self, name): if name not in {'copy', 'paste'}: raise ValueError() name = 'clipboard_{}_cmd'.format(name) cmd = getattr(options, name) or vd.fail('options.{} not set'.format(name)) return cmd.split() def paste(self): return subprocess.check_output(self.get_command('paste')).decode('utf-8') def copy(self, value): 'Copy a cell to the system clipboard.' with tempfile.NamedTemporaryFile() as temp: with open(temp.name, 'w', encoding=options.encoding) as fp: fp.write(str(value)) p = subprocess.Popen( self.get_command('copy'), stdin=open(temp.name, 'r', encoding=options.encoding), stdout=subprocess.DEVNULL) p.communicate() def save(self, vs, filetype): 'Copy rows to the system clipboard.' # use NTF to generate filename and delete file on context exit with tempfile.NamedTemporaryFile(suffix='.'+filetype) as temp: vd.sync(saveSheets(Path(temp.name), vs)) p = subprocess.Popen( self.get_command('copy'), stdin=open(temp.name, 'r', encoding=options.encoding), stdout=subprocess.DEVNULL, close_fds=True) p.communicate() @VisiData.api def pasteFromClipboard(vd, cols, rows): text = vd.getLastArgs() or clipboard().paste().strip() or vd.error('system clipboard is empty') vd.addUndoSetValues(cols, rows) for line, r in zip(text.split('\n'), rows): for v, c in zip(line.split('\t'), cols): c.setValue(r, v) @asyncthread def saveToClipboard(sheet, rows, filetype=None): 'copy rows from sheet to system clipboard' filetype = filetype or options.save_filetype vs = copy(sheet) vs.rows = rows vd.status('copying rows to clipboard') clipboard().save(vs, filetype) Sheet.addCommand('y', 'copy-row', 'copyRows([cursorRow])', 'yank (copy) current row to clipboard') Sheet.addCommand('d', 'delete-row', 'delete_row(cursorRowIndex)', 'delete (cut) current row and move it to clipboard') Sheet.addCommand('p', 'paste-after', 'paste_after(cursorRowIndex)', 'paste clipboard rows after current row') Sheet.addCommand('P', 'paste-before', 'paste_before(cursorRowIndex)', 'paste clipboard rows before current row') Sheet.addCommand('gd', 'delete-selected', 'copyRows(onlySelectedRows); deleteSelected()', 'delete (cut) selected rows and move them to clipboard') Sheet.addCommand('gy', 'copy-selected', 'copyRows(onlySelectedRows)', 'yank (copy) selected rows to clipboard') Sheet.addCommand('zy', 'copy-cell', 'copyCells(cursorCol, [cursorRow])', 'yank (copy) current cell to clipboard') Sheet.addCommand('zp', 'paste-cell', 'cursorCol.setValuesTyped([cursorRow], vd.clipcells[0]) if vd.clipcells else warning("no cells to paste")', 'set contents of current cell to last clipboard value') Sheet.addCommand('zd', 'delete-cell', 'vd.clipcells = [cursorDisplay]; cursorCol.setValues([cursorRow], None)', 'delete (cut) current cell and move it to clipboard') Sheet.addCommand('gzd', 'delete-cells', 'vd.clipcells = list(vd.sheet.cursorCol.getDisplayValue(r) for r in onlySelectedRows); cursorCol.setValues(onlySelectedRows, None)', 'delete (cut) contents of current column for selected rows and move them to clipboard') Sheet.bindkey('BUTTON2_PRESSED', 'go-mouse') Sheet.addCommand('BUTTON2_RELEASED', 'syspaste-cells', 'pasteFromClipboard(visibleCols[cursorVisibleColIndex:], rows[cursorRowIndex:])', 'paste into VisiData from system clipboard') Sheet.bindkey('BUTTON2_CLICKED', 'go-mouse') Sheet.addCommand('gzy', 'copy-cells', 'copyCells(cursorCol, onlySelectedRows)', 'yank (copy) contents of current column for selected rows to clipboard') Sheet.addCommand('gzp', 'setcol-clipboard', 'for r, v in zip(onlySelectedRows, itertools.cycle(vd.clipcells)): cursorCol.setValuesTyped([r], v)', 'set cells of current column for selected rows to last clipboard value') Sheet.addCommand('Y', 'syscopy-row', 'syscopyRows([cursorRow])', 'yank (copy) current row to system clipboard (using options.clipboard_copy_cmd)') Sheet.addCommand('gY', 'syscopy-selected', 'syscopyRows(onlySelectedRows)', 'yank (copy) selected rows to system clipboard (using options.clipboard_copy_cmd)') Sheet.addCommand('zY', 'syscopy-cell', 'syscopyCells(cursorCol, [cursorRow])', 'yank (copy) current cell to system clipboard (using options.clipboard_copy_cmd)') Sheet.addCommand('gzY', 'syscopy-cells', 'syscopyCells(cursorCol, onlySelectedRows)', 'yank (copy) contents of current column from selected rows to system clipboard (using options.clipboard_copy_cmd') Sheet.bindkey('KEY_DC', 'delete-cell'), Sheet.bindkey('gKEY_DC', 'delete-cells'), ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1606688365.0 visidata-2.2.1/visidata/cliptext.py0000660000175000017500000000473300000000000020030 0ustar00kefalakefala00000000000000import unicodedata import sys import functools from visidata import options __all__ = ['clipstr', 'clipdraw', 'dispwidth'] disp_column_fill = ' ' ### Curses helpers def dispwidth(ss): 'Return display width of string, according to unicodedata width and options.disp_ambig_width.' disp_ambig_width = options.disp_ambig_width w = 0 for cc in ss: eaw = unicodedata.east_asian_width(cc) if eaw == 'A': # ambiguous w += disp_ambig_width elif eaw in 'WF': # wide/full w += 2 elif not unicodedata.combining(cc): w += 1 return w @functools.lru_cache(maxsize=100000) def clipstr(s, dispw): '''Return clipped string and width in terminal display characters. Note: width may differ from len(s) if East Asian chars are 'fullwidth'.''' w = 0 ret = '' trunch = options.disp_truncator for c in s: if c != ' ' and unicodedata.category(c) in ('Cc', 'Zs', 'Zl'): # control char, space, line sep c = options.disp_oddspace if c: c = c[0] # multi-char disp_oddspace just uses the first char ret += c w += dispwidth(c) if w > dispw-len(trunch)+1: ret = ret[:-2] + trunch # replace final char with ellipsis w += len(trunch) break return ret, w def clipdraw(scr, y, x, s, attr, w=None, rtl=False): 'Draw string `s` at (y,x)-(y,x+w) with curses attr, clipping with ellipsis char. if rtl, draw inside (x-w, x). Returns width drawn (max of w).' if not scr: return 0 _, windowWidth = scr.getmaxyx() dispw = 0 try: if w is None: w = len(s) w = min(w, (x-1) if rtl else (windowWidth-x-1)) if w <= 0: # no room anyway return 0 # convert to string just before drawing clipped, dispw = clipstr(str(s), w) if rtl: # clearing whole area (w) has negative display effects; clearing just dispw area is useless # scr.addstr(y, x-dispw-1, disp_column_fill*dispw, attr) scr.addstr(y, x-dispw-1, clipped, attr) else: scr.addstr(y, x, disp_column_fill*w, attr) # clear whole area before displaying scr.addstr(y, x, clipped, attr) except Exception as e: pass # raise type(e)('%s [clip_draw y=%s x=%s dispw=%s w=%s clippedlen=%s]' % (e, y, x, dispw, w, len(clipped)) # ).with_traceback(sys.exc_info()[2]) return dispw ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612075960.0 visidata-2.2.1/visidata/cmdlog.py0000660000175000017500000004032500000000000017436 0ustar00kefalakefala00000000000000import threading from visidata import * import visidata option('replay_wait', 0.0, 'time to wait between replayed commands, in seconds', sheettype=None) theme('disp_replay_play', '▶', 'status indicator for active replay') theme('disp_replay_pause', '‖', 'status indicator for paused replay') theme('color_status_replay', 'green', 'color of replay status indicator') option('replay_movement', False, 'insert movements during replay', sheettype=None) option('visidata_dir', '~/.visidata/', 'directory to load and store additional files', sheettype=None) # 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 go- search scroll prev next page start end zoom resize visibility mouse suspend redraw no-op help syscopy sysopen profile toggle'''.split() option('rowkey_prefix', 'キ', 'string prefix for rowkey in the cmdlog', sheettype=None) option('cmdlog_histfile', '', 'file to autorecord each cmdlog action to', sheettype=None) vd.activeCommand = UNLOADED def open_vd(p): return CommandLog(p.name, source=p, precious=True) def open_vdj(p): return CommandLogJsonl(p.name, source=p, precious=True) VisiData.save_vd = VisiData.save_tsv VisiData.save_vdj = VisiData.save_jsonl def checkVersion(desired_version): if desired_version != visidata.__version_info__: vd.fail("version %s required" % desired_version) def fnSuffix(prefix): i = 0 fn = prefix + '.vd' while Path(fn).exists(): i += 1 fn = f'{prefix}-{i}.vd' return fn def inputLongname(sheet): longnames = set(k for (k, obj), v in vd.commands.iter(sheet)) return vd.input("command name: ", completer=CompleteKey(sorted(longnames)), type='longname') 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 def keystr(k): return options.rowkey_prefix+','.join(map(str, k)) def isLoggableCommand(longname): for n in nonLogged: if longname.startswith(n): return False return True def isLoggableSheet(sheet): return sheet is not vd.cmdlog and not isinstance(sheet, (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 if vs.options.replay_movement: while vs.cursorRowIndex != rowidx: vs.cursorRowIndex += 1 if (rowidx - vs.cursorRowIndex) > 0 else -1 while not vd.delay(0.5): pass else: vs.cursorRowIndex = rowidx return True @Sheet.api def getRowIndexFromStr(vs, rowstr): index = indexMatch(vs.rows, lambda r,vs=vs,rowstr=rowstr: keystr(vs.rowkey(r)) == rowstr) if index is not None: return index try: return int(rowstr) except ValueError: return None @Sheet.api def moveToCol(vs, colstr): 'Move cursor to column given by *colstr*, which can be either the column number or column name.' try: vcolidx = int(colstr) except ValueError: vcolidx = indexMatch(vs.visibleCols, lambda c,name=colstr: name == c.name) if vcolidx is None: return False if vs.options.replay_movement: while vs.cursorVisibleColIndex != vcolidx: vs.cursorVisibleColIndex += 1 if (vcolidx - vs.cursorVisibleColIndex) > 0 else -1 while not vd.delay(0.5): pass else: vs.cursorVisibleColIndex = vcolidx return True # rowdef: namedlist (like TsvSheet) class _CommandLog: 'Log of commands for current session.' rowtype = 'logged commands' precious = False _rowtype = namedlist('CommandLogRow', 'sheet col row longname input keystrokes comment undofuncs'.split()) 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 not isLoggableCommand(cmd.longname): return 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 contains = lambda s, *substrs: any((a in s) for a in substrs) if contains(cmd.execstr, 'cursorTypedValue', 'cursorDisplay', 'cursorValue', 'cursorCell', 'cursorRow') and sheet.nRows > 0: k = sheet.rowkey(sheet.cursorRow) rowname = keystr(k) if k else sheet.cursorRowIndex if contains(cmd.execstr, 'cursorTypedValue', 'cursorDisplay', 'cursorValue', 'cursorCell', 'cursorCol', 'cursorVisibleCol'): colname = sheet.cursorCol.name or sheet.visibleCols.index(sheet.cursorCol) if contains(cmd.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(cmd.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)) if contains(cmd.execstr, 'pasteFromClipboard'): args = clipboard().paste().strip() comment = vd.currentReplayRow.comment if vd.currentReplayRow else cmd.helpstr vd.activeCommand = self.newRow(sheet=sheetname, col=str(colname), row=str(rowname), keystrokes=keystrokes, input=args, longname=cmd.longname, comment=comment, undofuncs=[]) def afterExecSheet(self, sheet, escaped, err): 'Records vd.activeCommand' if not vd.activeCommand: # nothing to record return if err: vd.activeCommand[-1] += ' [%s]' % err # remove user-aborted commands and simple movements if not escaped and isLoggableCommand(vd.activeCommand.longname): if isLoggableSheet(sheet): # don't record actions on global cmdlog or other internal sheets self.addRow(vd.activeCommand) # add to global cmdlog sheet.cmdlog_sheet.addRow(vd.activeCommand) # add to sheet-specific cmdlog if options.cmdlog_histfile: if not getattr(vd, 'sessionlog', None): vd.sessionlog = vd.loadInternalSheet(CommandLog, Path(date().strftime(options.cmdlog_histfile))) append_tsv_row(vd.sessionlog, vd.activeCommand) vd.activeCommand = None def openHook(self, vs, src): r = self.newRow(keystrokes='o', input=src, longname='open-file') vs.cmdlog_sheet.addRow(r) self.addRow(r) class CommandLog(_CommandLog, VisiDataMetaSheet): pass class CommandLogJsonl(_CommandLog, JsonLinesSheet): def newRow(self): return JsonLinesSheet.newRow(self) 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 vd.semaphore = threading.Semaphore(0) @VisiData.api def replay_pause(vd): if not vd.currentReplay: vd.fail('no replay to pause') else: if vd.paused: vd.replay_advance() vd.paused = not vd.paused vd.status('paused' if vd.paused else 'resumed') @VisiData.api def replay_advance(vd): vd.currentReplay or vd.fail("no replay to advance") vd.semaphore.release() @VisiData.api def replay_cancel(vd): vd.currentReplay or vd.fail("no replay to cancel") vd.currentReplayRow = None vd.currentReplay = None vd.semaphore.release() @VisiData.api def moveToReplayContext(vd, r, vs): 'set the sheet/row/col to the values in the replay row. return sheet' if r.row: vs.moveToRow(r.row) or vd.error('no "%s" row' % r.row) if r.col: vs.moveToCol(r.col) or vd.error('no "%s" column' % r.col) @VisiData.api def delay(vd, factor=1): 'returns True if delay satisfied' acquired = vd.semaphore.acquire(timeout=options.replay_wait*factor if not vd.paused else None) return acquired or not vd.paused @VisiData.property def activeSheet(vd): 'Return top sheet on sheets stack, or cmdlog sheets stack empty.' return vd.sheets[0] if vd.sheets else vd.cmdlog @VisiData.api def replayOne(vd, r): 'Replay the command in one given row.' vd.currentReplayRow = r longname = getattr(r, 'longname', None) 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: if vs: vd.push(vs) else: vs = vd.activeSheet vd.moveToReplayContext(r, vs) if r.comment: vd.status(r.comment) vd.keystrokes = r.keystrokes # <=v1.2 used keystrokes in longname column; getCommand fetches both escaped = vs.execCommand(longname if longname else r.keystrokes, keystrokes=r.keystrokes) vd.currentReplayRow = None if escaped: # escape during replay aborts replay vd.warning('replay aborted during %s' % (longname or r.keystrokes)) return escaped @VisiData.api def replay_sync(vd, cmdlog, live=False): 'Replay all commands in log.' 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) vd.activeSheet.ensureLoaded() vd.sync() while not vd.delay(): pass vd.status('replay complete') vd.currentReplay = None @VisiData.api @asyncthread def replay(vd, cmdlog): 'Inject commands into live execution with interface.' vd.replay_sync(cmdlog, live=True) @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 is not None) and (vd.activeCommand is not UNLOADED): if not vd.activeCommand.input: vd.activeCommand.input = args @VisiData.property def replayStatus(vd): x = options.disp_replay_pause if vd.paused else options.disp_replay_play return ' │ %s %s/%s' % (x, vd.currentReplay.cursorRowIndex, len(vd.currentReplay.rows)) @BaseSheet.property def cmdlog(sheet): rows = sheet.cmdlog_sheet.rows if isinstance(sheet.source, BaseSheet): rows = sheet.source.cmdlog.rows + rows return CommandLog(sheet.name+'_cmdlog', source=sheet, rows=rows) @BaseSheet.lazy_property def cmdlog_sheet(sheet): return CommandLog(sheet.name+'_cmdlog', source=sheet, rows=[]) @BaseSheet.property def shortcut(self): try: return str(vd.allSheets.index(self)+1) except ValueError: pass try: return self.cmdlog_sheet.rows[0].keystrokes except Exception: pass return '' @VisiData.lazy_property def cmdlog(vd): vs = CommandLog('cmdlog', rows=[]) vd.beforeExecHooks.append(vs.beforeExecHook) return vs @VisiData.property def modifyCommand(vd): if vd.activeCommand is not None and isLoggableCommand(vd.activeCommand.longname): return vd.activeCommand if not vd.cmdlog.rows: return None return vd.cmdlog.rows[-1] 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 current sheet\'s CommandLog with parent sheets commands\' removed') globalCommand('^D', 'save-cmdlog', 'saveSheets(inputPath("save cmdlog to: ", value=fnSuffix(name)), vd.cmdlog, confirm_overwrite=options.confirm_overwrite)', 'save CommandLog to filename.vd file') globalCommand('^U', 'replay-pause', 'vd.replay_pause()', 'pause/resume replay') globalCommand('^N', 'replay-advance', 'vd.replay_advance()', 'execute next row in replaying sheet') globalCommand('^K', 'replay-stop', 'vd.replay_cancel()', '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') globalCommand(' ', 'exec-longname', 'execCommand(inputLongname(sheet))', 'execute command by its longname') 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') CommandLog.addCommand('^C', 'replay-stop', 'sheet.cursorRowIndex = sheet.nRows', 'abort replay') 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') CommandLogJsonl.addCommand('^C', 'replay-stop', 'sheet.cursorRowIndex = sheet.nRows', 'abort replay') BaseSheet.addCommand('', 'repeat-last', 'execCommand(cmdlog_sheet.rows[-1].longname)', 'run most recent command with an empty, queried input') BaseSheet.addCommand('', 'repeat-input', 'r = copy(cmdlog_sheet.rows[-1]); r.sheet=r.row=r.col=""; vd.replayOne(r)', 'run previous command, along with any previous input to that command') CommandLog.class_options.json_sort_keys = False CommandLogJsonl.class_options.json_sort_keys = False ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1606688365.0 visidata-2.2.1/visidata/color.py0000660000175000017500000000710500000000000017306 0ustar00kefalakefala00000000000000import curses import functools from copy import copy from visidata import options, Extensible, drawcache, drawcache_property from collections import namedtuple __all__ = ['ColorAttr', 'colors', 'update_attr', 'ColorMaker'] ColorAttr = namedtuple('ColorAttr', ('color', 'attributes', 'precedence', 'attr')) def update_attr(oldattr, updattr, updprec=None): if isinstance(updattr, ColorAttr): if updprec is None: updprec = updattr.precedence updcolor = updattr.color updattr = updattr.attributes else: updcolor = updattr & curses.A_COLOR updattr = updattr & ~curses.A_COLOR if updprec is None: updprec = 0 # starting values, work backwards newcolor = oldattr.color newattr = oldattr.attributes | updattr newprec = oldattr.precedence if not newcolor or updprec > newprec: if updcolor: newcolor = updcolor newprec = updprec return ColorAttr(newcolor, newattr, newprec, newcolor | newattr) class ColorMaker: def __init__(self): self.attrs = {} self.color_attrs = {} @drawcache_property def colorcache(self): return {} def setup(self): if options.use_default_colors: curses.use_default_colors() default_bg = -1 else: default_bg = curses.COLOR_BLACK self.color_attrs['black'] = curses.color_pair(0) for c in range(0, options.force_256_colors and 256 or curses.COLORS): try: curses.init_pair(c+1, c, default_bg) self.color_attrs[str(c)] = curses.color_pair(c+1) except curses.error as e: pass # curses.init_pair gives a curses error on Windows for c in 'red green yellow blue magenta cyan white'.split(): colornum = getattr(curses, 'COLOR_' + c.upper()) self.color_attrs[c] = curses.color_pair(colornum+1) for a in 'normal blink bold dim reverse standout underline'.split(): self.attrs[a] = getattr(curses, 'A_' + a.upper()) def keys(self): return list(self.attrs.keys()) + list(self.color_attrs.keys()) def __getitem__(self, colornamestr): return self._colornames_to_cattr(colornamestr).attr def __getattr__(self, optname): 'colors.color_foo returns colors[options.color_foo]' return self.get_color(optname).attr @drawcache def resolve_colors(self, colorstack): 'Returns the ColorAttr for the colorstack, a list of color option names sorted highest-precedence color first.' cattr = ColorAttr(0,0,0,0) for coloropt in colorstack: c = self.get_color(coloropt) cattr = update_attr(cattr, c) return cattr def _colornames_to_cattr(self, colornamestr, precedence=0): color, attr = 0, 0 for colorname in colornamestr.split(' '): if colorname in self.color_attrs: if not color: color = self.color_attrs[colorname.lower()] elif colorname in self.attrs: attr = self.attrs[colorname.lower()] return ColorAttr(color, attr, precedence, color | attr) def get_color(self, optname, precedence=0): 'colors.color_foo returns colors[options.color_foo]' r = self.colorcache.get(optname, None) if r is None: coloropt = options._get(optname) colornamestr = coloropt.value if coloropt else optname r = self.colorcache[optname] = self._colornames_to_cattr(colornamestr, precedence) return r colors = ColorMaker() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1606688365.0 visidata-2.2.1/visidata/colorsheet.py0000660000175000017500000000155400000000000020341 0ustar00kefalakefala00000000000000import curses from visidata import globalCommand, colors, Sheet, Column, RowColorizer, wrapply class ColorSheet(Sheet): rowtype = 'colors' # rowdef: color number as assigned in the colors object columns = [ Column('color', type=int), Column('R', getter=lambda col,row: curses.color_content(curses.pair_number(colors[row])-1)[0]), Column('G', getter=lambda col,row: curses.color_content(curses.pair_number(colors[row])-1)[1]), Column('B', getter=lambda col,row: curses.color_content(curses.pair_number(colors[row])-1)[2]), ] colorizers = [ RowColorizer(7, None, lambda s,c,r,v: r) ] def reload(self): self.rows = sorted(colors.keys(), key=lambda n: wrapply(int, n)) globalCommand(None, 'colors', 'vd.push(ColorSheet("vdcolors"))', 'open Color Sheet with an overview of curses colors and codes') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612761678.0 visidata-2.2.1/visidata/column.py0000660000175000017500000004572700000000000017501 0ustar00kefalakefala00000000000000from copy import copy import collections import string import itertools import threading import re import time from visidata import option, options, anytype, stacktrace, vd from visidata import asyncthread, dispwidth from visidata import wrapply, TypedWrapper, TypedExceptionWrapper from visidata import Extensible, AttrDict, undoAttrFunc class InProgress(Exception): @property def stacktrace(self): return ['calculation in progress'] INPROGRESS = TypedExceptionWrapper(None, exception=InProgress()) # sentinel option('col_cache_size', 0, 'max number of cache entries in each cached column') option('clean_names', False, 'clean column/sheet names to be valid Python identifiers', replay=True) __all__ = [ 'clean_to_id', 'clean_name', 'maybe_clean', 'Column', 'setitem', 'getattrdeep', 'setattrdeep', 'getitemdef', 'ColumnAttr', 'AttrColumn', 'ColumnItem', 'ItemColumn', 'SettableColumn', 'SubColumnFunc', 'SubColumnItem', 'SubColumnAttr', 'ColumnExpr', 'ExprColumn', 'DisplayWrapper', ] class DisplayWrapper: def __init__(self, value=None, *, display=None, note=None, notecolor=None, error=None): self.value = value # actual value (any type) self.display = display # 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 maybe_clean(s, vs): if (vs or vd).options.clean_names: s = clean_name(s) return s def clean_name(s): s = re.sub(r'[^\w\d_]', '_', s) # replace non-alphanum chars with _ s = re.sub(r'_+', '_', s) # replace runs of _ with a single _ return s def clean_to_id(s): # [Nas Banov] https://stackoverflow.com/a/3305731 return re.sub(r'\W|^(?=\d)', '_', str(s)).strip('_') 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 = None # 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 = lambda col, row, value: vd.fail(col.name+' column cannot be changed') 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.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 __deepcopy__(self, memo): return self.__copy__() # no separate deepcopy def __getstate__(self): d = {k:getattr(self, k) for k in 'name width height expr keycol fmtstr voffset hoffset aggstr'.split()} d['type'] = self.type.__name__ return d 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): if name is None: name = '' if isinstance(name, str): name = name.strip() else: name = str(name) self._name = maybe_clean(name, self.sheet) @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 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 formatValue(self, typedval): 'Return displayable string of *typedval* according to ``Column.fmtstr``.' if typedval is None: return None if isinstance(typedval, (list, tuple)): return '[%s]' % len(typedval) if isinstance(typedval, dict): return '{%s}' % len(typedval) if isinstance(typedval, bytes): typedval = typedval.decode(options.encoding, options.encoding_errors) return vd.getType(self.type).formatter(self.fmtstr, typedval) format=formatValue 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 isues 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.sheet.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, display=dispval, note=options.note_getter_exc, notecolor='color_error') elif typedval.val is None: # early out for strict None return DisplayWrapper(None, display='', # 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, display=str(cellval), error=typedval.stacktrace, note=options.note_type_exc, notecolor='color_warning') else: return DisplayWrapper(typedval.val, display=str(typedval.val), error='unknown', note=options.note_type_exc, notecolor='color_warning') elif isinstance(typedval, threading.Thread): return DisplayWrapper(None, display=options.disp_pending, note=options.note_pending, notecolor='color_note_pending') dw = DisplayWrapper(cellval) try: dw.display = self.format(typedval) 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.display = str(cellval) except Exception as e: dw.display = str(e) dw.note = options.note_format_exc dw.notecolor = 'color_warning' return dw def getDisplayValue(self, row): 'Return string displayed in this column for given *row*.' return self.getCell(row).display def putValue(self, row, val): 'Change value for *row* in this column to *val* immediately. Does not check the type. Overrideable; by default calls ``.setter(row, val)``.' return self.setter(self, row, val) def setValue(self, row, val): 'Change value for *row* in this column to *val*. Call ``putValue`` immediately if parent ``sheet.defer`` is False, otherwise cache until later ``putChanges``. Caller must add undo function.' if self.sheet.defer: self.cellChanged(row, val) else: self.putValue(row, val) def setValueSafe(self, row, value): 'setValue and ignore exceptions.' try: return self.setValue(row, value) except Exception as e: vd.exceptionCaught(e) @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)): self.setValueSafe(r, v) self.recalc() return vd.status('set %d cells to %d values' % (len(rows), len(values))) 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)): self.setValueSafe(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.' w = 0 nlen = dispwidth(self.name) if len(rows) > 0: w = max(max(dispwidth(self.getDisplayValue(r)) for r in rows), nlen)+2 return max(w, nlen) # ---- Column makers def setitem(r, i, v): # function needed for use in lambda r[i] = v return True def getattrdeep(obj, attr, *default): 'Return dotted attr (like "a.b.c") from obj, or default if any of the components are missing.' attrs = attr.split('.') if default: getattr_default = lambda o,a,d=default[0]: getattr(o, a, d) else: getattr_default = lambda o,a: getattr(o, a) for a in attrs[:-1]: obj = getattr_default(obj, a) return getattr_default(obj, attrs[-1]) def setattrdeep(obj, attr, val): 'Set dotted attr (like "a.b.c") on obj to val.' attrs = attr.split('.') for a in attrs[:-1]: obj = getattr(obj, a) setattr(obj, attrs[-1], val) def AttrColumn(name='', attr=None, **kwargs): 'Column using getattr/setattr with *attr*.' return Column(name, expr=attr if attr is not None else name, getter=lambda col,row: getattrdeep(row, col.expr), setter=lambda col,row,val: setattrdeep(row, col.expr, val), **kwargs) def getitemdef(o, k, default=None): try: return default if o is None else o[k] except Exception: return default def ItemColumn(name=None, key=None, **kwargs): 'Column using getitem/setitem with *key*.' return Column(name, expr=key if key is not None else name, getter=lambda col,row: getitemdef(row, col.expr), setter=lambda col,row,val: setitem(row, col.expr, val), **kwargs) 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 ExprColumn(Column): 'Column using *expr* to derive the value from each row.' def __init__(self, name, expr=None, **kwargs): super().__init__(name, **kwargs) self.expr = expr or name self.ncalcs = 0 self.totaltime = 0 self.maxtime = 0 def calcValue(self, row): t0 = time.perf_counter() r = self.sheet.evalExpr(self.compiledExpr, row, col=self) t1 = time.perf_counter() self.ncalcs += 1 self.maxtime = max(self.maxtime, t1-t0) self.totaltime += (t1-t0) return r def putValue(self, row, val): a = self.getDisplayValue(row) b = self.format(self.type(val)) if a != b: vd.warning('%s calced %s not %s' % (self.name, a, b)) @property def expr(self): return self._expr @expr.setter def expr(self, expr): self.compiledExpr = compile(expr, '', 'eval') if expr else None self._expr = expr class SettableColumn(Column): 'Column using rowid to store and retrieve values internally.' def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._store = {} 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) # synonyms ColumnItem = ItemColumn ColumnAttr = AttrColumn ColumnExpr = ExprColumn ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608541899.0 visidata-2.2.1/visidata/customdate.py0000660000175000017500000000173600000000000020344 0ustar00kefalakefala00000000000000import time from visidata import vd, date, Sheet, ColumnsSheet @Sheet.api def customdate(sheet, fmtstr): 'Return date class with strptime parse format fixed to *fmtstr*.' class _customdate(date): def __new__(cls, *args, **kwargs): if len(args) == 1 and isinstance(args[0], str): return super().__new__(cls, *time.strptime(args[0], fmtstr)[:6]) return super().__new__(cls, *args, **kwargs) vd.addType(_customdate, '@', '', formatter=lambda fmt,val: val.strftime(fmt or sheet.options.disp_date_fmt)) _customdate.__name__ = 'customdate(%s)' % fmtstr return _customdate Sheet.addCommand('z@', 'type-customdate', 'cursorCol.type=cursorCol.type=customdate(input("date format: ", type="fmtstr"))', 'set type of current column to custom date format') ColumnsSheet.addCommand('gz@', 'type-customdate-selected', 'onlySelectedRows.type=customdate(input("date format: ", type="fmtstr"))', 'set type of selected columns to date') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612075960.0 visidata-2.2.1/visidata/deprecated.py0000660000175000017500000000703100000000000020266 0ustar00kefalakefala00000000000000from visidata import VisiData, vd import visidata alias = visidata.BaseSheet.bindkey def deprecated(ver, instead=''): def decorator(func): def wrapper(*args, **kwargs): import traceback for line in reversed(traceback.extract_stack(limit=6)[:-1]): vd.warning(f' file {line.filename} at line {line.lineno} in {line.name}') vd.warning(f'Deprecated call traceback (most recent last):') msg = f'{func.__name__} deprecated since v{ver}' if instead: msg += f'; use {instead}' vd.warning(msg) 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): 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.name, 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') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612075960.0 visidata-2.2.1/visidata/describe.py0000660000175000017500000001032600000000000017747 0ustar00kefalakefala00000000000000from statistics import mode, median, mean, stdev from visidata import * option('describe_aggrs', 'mean stdev', 'numeric aggregators to calculate on Describe sheet') max_threads = 2 @Column.api def isError(col, row): 'Return True if the computed or typed value for *row* in this column is an error.' try: v = col.getValue(row) if v is not None: col.type(v) return False except Exception as e: return True class DescribeColumn(Column): def __init__(self, name, **kwargs): super().__init__(name, getter=lambda col,srccol: col.sheet.describeData[srccol].get(col.expr, ''), expr=name, **kwargs) # rowdef: Column from source sheet class DescribeSheet(ColumnsSheet): # rowtype = 'columns' precious = True columns = [ ColumnAttr('sheet', 'sheet', width=0), ColumnAttr('column', 'name'), ColumnAttr('type', 'typestr', width=0), DescribeColumn('errors', type=vlen), DescribeColumn('nulls', type=vlen), DescribeColumn('distinct',type=vlen), DescribeColumn('mode', type=str), DescribeColumn('min', type=str), DescribeColumn('max', type=str), DescribeColumn('sum'), DescribeColumn('median', type=str), ] colorizers = [ RowColorizer(7, 'color_key_col', lambda s,c,r,v: r and r in r.sheet.keyCols), ] nKeys = 2 @asyncthread def reload(self): super().reload() self.rows = [c for c in self.rows if not c.hidden] self.describeData = { col: {} for col in self.rows } self.columns = [] for c in type(self).columns: self.addColumn(c) self.setKeys(self.columns[:self.nKeys]) for aggrname in options.describe_aggrs.split(): self.addColumn(DescribeColumn(aggrname, type=float)) for srccol in Progress(self.rows, 'categorizing'): if not srccol.hidden: self.reloadColumn(srccol) def reloadColumn(self, srccol): d = self.describeData[srccol] isNull = srccol.sheet.isNullFunc() vals = list() d['errors'] = list() d['nulls'] = list() d['distinct'] = set() for sr in Progress(srccol.sheet.rows, 'calculating'): try: v = srccol.getValue(sr) if isNull(v): d['nulls'].append(sr) else: v = srccol.type(v) vals.append(v) d['distinct'].add(v) except Exception as e: d['errors'].append(sr) d['mode'] = self.calcStatistic(d, mode, vals) if vd.isNumeric(srccol): for func in [min, max, sum, median]: # use type d[func.__name__] = self.calcStatistic(d, func, vals) for aggrname in options.describe_aggrs.split(): func = globals()[aggrname] d[func.__name__] = self.calcStatistic(d, func, vals) def calcStatistic(self, d, func, *args, **kwargs): r = wrapply(func, *args, **kwargs) d[func.__name__] = r return r def openCell(self, col, row): 'open copy of source sheet with rows described in current cell' val = col.getValue(row) if isinstance(val, list): vs=copy(row.sheet) vs.rows=val vs.name+="_%s_%s"%(row.name,col.name) return vs vd.warning(val) Sheet.addCommand('I', 'describe-sheet', 'vd.push(DescribeSheet(sheet.name+"_describe", source=[sheet]))', 'open Describe Sheet with descriptive statistics for all visible columns') globalCommand('gI', 'describe-all', 'vd.push(DescribeSheet("describe_all", source=vd.sheets))', 'open Describe Sheet with description statistics for all visible columns from all sheets') DescribeSheet.addCommand('zs', 'select-cell', 'cursorRow.sheet.select(cursorValue)', 'select rows on source sheet which are being described in current cell') DescribeSheet.addCommand('zu', 'unselect-cell', 'cursorRow.sheet.unselect(cursorValue)', 'unselect rows on source sheet which are being described in current cell') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608178530.0 visidata-2.2.1/visidata/editor.py0000660000175000017500000000317400000000000017460 0ustar00kefalakefala00000000000000import os import signal import subprocess import tempfile import curses import visidata class SuspendCurses: 'Context manager to leave windowed mode on enter and restore it on exit.' def __enter__(self): curses.endwin() def __exit__(self, exc_type, exc_val, tb): newscr = curses.initscr() newscr.refresh() curses.doupdate() @visidata.VisiData.global_api def launchEditor(vd, *args): 'Launch $EDITOR with *args* as arguments.' editor = os.environ.get('EDITOR') or vd.fail('$EDITOR not set') args = [editor] + list(args) with SuspendCurses(): return subprocess.call(args) @visidata.VisiData.global_api def launchExternalEditor(vd, v, linenum=0): 'Launch $EDITOR to edit string *v* starting on line *linenum*.' import tempfile with tempfile.NamedTemporaryFile() as temp: with open(temp.name, 'w') as fp: fp.write(v) return launchExternalEditorPath(visidata.Path(temp.name), linenum) def launchExternalEditorPath(path, linenum=0): 'Launch $EDITOR to edit *path* starting on line *linenum*.' if linenum: launchEditor(path, '+%s' % linenum) else: 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 '' def suspend(): import signal with SuspendCurses(): os.kill(os.getpid(), signal.SIGSTOP) visidata.globalCommand('^Z', 'suspend', 'suspend()', 'suspend VisiData process') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1606688365.0 visidata-2.2.1/visidata/errors.py0000660000175000017500000000145600000000000017507 0ustar00kefalakefala00000000000000import traceback from visidata import vd, VisiData, options __all__ = ['stacktrace', 'ExpectedException'] class ExpectedException(Exception): 'an expected exception' pass def stacktrace(e=None): if not e: return traceback.format_exc().strip().splitlines() return traceback.format_exception_only(type(e), e) @VisiData.global_api def exceptionCaught(vd, exc=None, **kwargs): 'Maintain list of most recent errors and return most recent one.' if isinstance(exc, ExpectedException): # already reported, don't log return vd.lastErrors.append(stacktrace()) if kwargs.get('status', True): vd.status(vd.lastErrors[-1][-1], priority=2) # last line of latest error if options.debug: raise # see textsheet.py for ErrorSheet and associated commands ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608706358.0 visidata-2.2.1/visidata/expr.py0000660000175000017500000000531700000000000017151 0ustar00kefalakefala00000000000000from visidata import Progress, Sheet, Column, asyncthread, vd, ExprColumn class CompleteExpr: def __init__(self, sheet=None): self.sheet = sheet def __call__(self, val, state): i = len(val)-1 while val[i:].isidentifier() and i >= 0: i -= 1 if i < 0: base = '' partial = val elif val[i] == '.': # no completion of attributes return None else: base = val[:i+1] partial = val[i+1:] varnames = [] varnames.extend(sorted((base+col.name) for col in self.sheet.columns if col.name.startswith(partial))) varnames.extend(sorted((base+x) for x in globals() if x.startswith(partial))) # Remove duplicate tabbing suggestions varnames_dict = {var:None for var in varnames} varnames = list(varnames_dict.keys()) return varnames[state%len(varnames)] @Column.api @asyncthread def setValuesFromExpr(self, rows, expr): 'Set values in this column for *rows* to the result of the Python expression *expr* applied to each row.' compiledExpr = compile(expr, '', 'eval') vd.addUndoSetValues([self], rows) for row in Progress(rows, 'setting'): # Note: expressions that are only calculated once, do not need to pass column identity # they can reference their "previous selves" once without causing a recursive problem self.setValueSafe(row, self.sheet.evalExpr(compiledExpr, row)) self.recalc() vd.status('set %d values = %s' % (len(rows), expr)) @Sheet.api def inputExpr(self, prompt, *args, **kwargs): return vd.input(prompt, "expr", *args, completer=CompleteExpr(self), **kwargs) Sheet.addCommand('=', 'addcol-expr', 'addColumnAtCursor(ExprColumn(inputExpr("new column expr="), curcol=cursorCol))', 'create new column from Python expression, with column names as variables') Sheet.addCommand('g=', 'setcol-expr', 'cursorCol.setValuesFromExpr(someSelectedRows, inputExpr("set selected="))', 'set current column for selected rows to result of Python expression') Sheet.addCommand('z=', 'setcell-expr', 'cursorCol.setValues([cursorRow], evalExpr(inputExpr("set expr="), cursorRow,))', 'evaluate Python expression on current row and set current cell with result of Python expression') Sheet.addCommand('gz=', 'setcol-iter', 'cursorCol.setValues(someSelectedRows, *list(itertools.islice(eval(input("set column= ", "expr", completer=CompleteExpr())), len(someSelectedRows))))', 'set current column for selected rows to the items in result of Python sequence expression') Sheet.addCommand(None, 'show-expr', 'status(evalExpr(inputExpr("show expr="), cursorRow))', 'evaluate Python expression on current row and show result on status line') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608178530.0 visidata-2.2.1/visidata/extensible.py0000660000175000017500000000555200000000000020336 0ustar00kefalakefala00000000000000from functools import wraps, lru_cache __all__ = ['Extensible', 'cache'] class Extensible: _cache_clearers = [] # list of func() to call in clearCaches() @classmethod def init(cls, membername, initfunc=lambda: None, copy=False): 'Append equivalent of ``self. = initfunc()`` to ``.__init__``.' oldinit = cls.__init__ def newinit(self, *args, **kwargs): oldinit(self, *args, **kwargs) if not hasattr(self, membername): # can be overridden by a subclass setattr(self, membername, initfunc()) cls.__init__ = newinit oldcopy = cls.__copy__ def newcopy(self, *args, **kwargs): ret = oldcopy(self, *args, **kwargs) setattr(ret, membername, getattr(self, membername) if copy else initfunc()) return ret cls.__copy__ = newcopy @classmethod def api(cls, func): oldfunc = getattr(cls, func.__name__, None) if oldfunc: func = wraps(oldfunc)(func) setattr(cls, func.__name__, func) return func @classmethod def class_api(cls, func): name = func.__get__(None, dict).__func__.__name__ oldfunc = getattr(cls, name, None) if oldfunc: func = wraps(oldfunc)(func) setattr(cls, name, func) return func @classmethod def property(cls, func): @property @wraps(func) def dofunc(self): return func(self) setattr(cls, func.__name__, dofunc) return dofunc @classmethod def lazy_property(cls, func): 'Return ``func()`` on first access and cache result; return cached result thereafter.' @property @wraps(func) def get_if_not(self): name = '_' + func.__name__ if not hasattr(self, name): setattr(self, name, func(self)) return getattr(self, name) setattr(cls, func.__name__, get_if_not) return get_if_not @classmethod def cached_property(cls, func): 'Return ``func()`` on first access, and cache result; return cached result until ``clearCaches()``.' @property @wraps(func) @lru_cache(maxsize=None) def get_if_not(self): return func(self) setattr(cls, func.__name__, get_if_not) Extensible._cache_clearers.append(get_if_not.fget.cache_clear) return get_if_not @classmethod def clear_all_caches(cls): for func in Extensible._cache_clearers: func() def cache(func): 'Return func(...) on first access, and cache result; return cached result until clearCaches().' @wraps(func) @lru_cache(maxsize=None) def call_if_not(self, *args, **kwargs): return func(self, *args, **kwargs) Extensible._cache_clearers.append(call_if_not.cache_clear) return call_if_not ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612763790.0 visidata-2.2.1/visidata/fill.py0000660000175000017500000000177600000000000017126 0ustar00kefalakefala00000000000000from visidata import * @VisiData.api @asyncthread def fillNullValues(vd, col, rows): 'Fill null cells in col with the previous non-null value' lastval = None oldvals = [] # for undo isNull = col.sheet.isNullFunc() n = 0 rowsToFill = [id(r) for r in rows] for r in Progress(col.sheet.rows, 'filling'): # loop over all rows try: val = col.getValue(r) except Exception as e: val = e if isNull(val): if lastval and (id(r) in rowsToFill): oldvals.append((col,r,val)) col.setValue(r, lastval) n += 1 else: lastval = val def _undo(): for c, r, v in oldvals: c.setValue(r, v) vd.addUndo(_undo) col.recalc() vd.status("filled %d values" % n) Sheet.addCommand('f', 'setcol-fill', 'fillNullValues(cursorCol, selectedRows)', 'fills null cells in selected rows of current column with contents of non-null cells up the current column') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608604413.0 visidata-2.2.1/visidata/freeze.py0000660000175000017500000000416600000000000017454 0ustar00kefalakefala00000000000000from visidata import * @Column.api def resetCache(col): col._cachedValues = collections.OrderedDict() vd.status("reset cache for " + col.name) @Sheet.api def StaticColumn(sheet, col): frozencol = SettableColumn(col.name+'_frozen', width=col.width, type=col.type, fmtstr=col._fmtstr) frozencol.recalc(sheet) @asyncthread def calcRows_async(frozencol, rows, col): # no need to undo, addColumn undo is enough for r in Progress(rows, 'calculating'): try: frozencol.putValue(r, col.getTypedValue(r)) except Exception as e: frozencol.putValue(r, e) calcRows_async(frozencol, sheet.rows, col) return frozencol class StaticSheet(Sheet): 'A copy of the source sheet with all cells frozen.' def __init__(self, source): super().__init__(source.name + "'", source=source) self.columns = [] for i, col in enumerate(self.source.visibleCols): colcopy = ColumnItem(col.name, i, width=col.width, type=col.type, fmtstr=col._fmtstr) self.addColumn(colcopy) if col in self.source.keyCols: self.setKeys([colcopy]) @asyncthread def reload(self): self.rows = [] for r in Progress(self.source.rows, 'calculating'): row = [] self.addRow(row) for col in self.source.visibleCols: val = col.getTypedValue(r) if isinstance(val, TypedExceptionWrapper): row.append(None) else: row.append(val) Sheet.addCommand("'", 'freeze-col', 'sheet.addColumnAtCursor(StaticColumn(cursorCol))', 'add a frozen copy of current column with all cells evaluated') Sheet.addCommand("g'", 'freeze-sheet', 'vd.push(StaticSheet(sheet)); status("pushed frozen copy of "+name)', 'open a frozen copy of current sheet with all visible columns evaluated') Sheet.addCommand("z'", 'cache-col', 'cursorCol.resetCache()', 'add/reset cache for current column') Sheet.addCommand("gz'", 'cache-cols', 'for c in visibleCols: c.resetCache()', 'add/reset cache for all visible columns') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608541899.0 visidata-2.2.1/visidata/freqtbl.py0000660000175000017500000001021000000000000017616 0ustar00kefalakefala00000000000000import math import collections from visidata import * theme('disp_histogram', '*', 'histogram element character') option('disp_histolen', 50, 'width of histogram column') option('histogram_bins', 0, 'number of bins for histogram of numeric columns') option('numeric_binning', False, 'bin numeric columns into ranges', replay=True) def valueNames(discrete_vals, numeric_vals): ret = [ '+'.join(str(x) for x in discrete_vals) ] if numeric_vals != (0, 0): ret.append('%s-%s' % numeric_vals) return '+'.join(ret) class FreqTableSheet(PivotSheet): 'Generate frequency-table sheet on currently selected column.' rowtype = 'bins' # rowdef FreqRow(keys, sourcerows) def __init__(self, sheet, *groupByCols): fqcolname = '%s_%s_freq' % (sheet.name, '-'.join(col.name for col in groupByCols)) super().__init__(fqcolname, groupByCols, [], source=sheet) self.largest = 1 def selectRow(self, row): self.source.select(row.sourcerows) # select all entries in the bin on the source sheet return super().selectRow(row) # then select the bin itself on this sheet def unselectRow(self, row): self.source.unselect(row.sourcerows) return super().unselectRow(row) def updateLargest(self, grouprow): self.largest = max(self.largest, len(grouprow.sourcerows)) @asyncthread def reload(self): 'Generate frequency table then reverse-sort by length.' super().initCols() # add default bonus columns for c in [ ColumnAttr('count', 'sourcerows', type=vlen), Column('percent', type=float, getter=lambda col,row: len(row.sourcerows)*100/col.sheet.source.nRows), Column('histogram', type=str, getter=lambda col,row: options.disp_histogram*(options.disp_histolen*len(row.sourcerows)//col.sheet.largest), width=options.disp_histolen+2), ]: self.addColumn(c) # two more threads vd.sync(self.addAggregateCols(), self.groupRows(self.updateLargest)) if self.nCols > len(self.groupByCols)+3: # hide percent/histogram if aggregations added self.column('percent').hide() self.column('histogram').hide() if not [c for c in self.groupByCols if vd.isNumeric(c)]: self.orderBy(self.column('count'), reverse=True) def openRow(self, row): 'open copy of source sheet with rows that are grouped in current row' if row.sourcerows: vs = copy(self.source) vs.name += "_"+valueNames(row.discrete_keys, row.numeric_key) vs.rows=copy(row.sourcerows) return vs vd.warning("no source rows") def openCell(self, col, row): return Sheet.openCell(self, col, row) class FreqTableSheetSummary(FreqTableSheet): 'Append a PivotGroupRow to FreqTable with only selectedRows.' @asyncthread def reload(self): FreqTableSheet.reload.__wrapped__(self) self.addRow(PivotGroupRow(['Selected'], (0,0), self.source.selectedRows, {})) Sheet.addCommand('F', 'freq-col', 'vd.push(FreqTableSheet(sheet, cursorCol))', 'open Frequency Table grouped on current column, with aggregations of other columns') Sheet.addCommand('gF', 'freq-keys', 'vd.push(FreqTableSheet(sheet, *keyCols))', 'open Frequency Table grouped by all key columns on source sheet, with aggregations of other columns') Sheet.addCommand('zF', 'freq-summary', 'vd.push(FreqTableSheetSummary(sheet, Column("Total", sheet=sheet, getter=lambda col, row: "Total")))', 'open one-line summary for all rows and selected rows') ColumnsSheet.addCommand(ENTER, 'freq-row', 'vd.push(FreqTableSheet(source[0], cursorRow))', 'open a Frequency Table sheet grouped on column referenced in current row') FreqTableSheet.addCommand('gu', 'unselect-rows', 'unselect(selectedRows)', 'unselect all source rows grouped in current row') FreqTableSheet.addCommand('g'+ENTER, 'dive-rows', 'vs = copy(source); vs.name += "_several"; vs.rows=list(itertools.chain.from_iterable(row.sourcerows for row in selectedRows)); vd.push(vs)', 'open copy of source sheet with rows that are grouped in selected rows') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608541899.0 visidata-2.2.1/visidata/graph.py0000660000175000017500000001665200000000000017300 0ustar00kefalakefala00000000000000from visidata import * option('color_graph_axis', 'bold', 'color for graph axis labels') def numericCols(cols): return [c for c in cols if vd.isNumeric(c)] class InvertedCanvas(Canvas): def zoomTo(self, bbox): super().zoomTo(bbox) self.fixPoint(Point(self.plotviewBox.xmin, self.plotviewBox.ymax), bbox.xymin) def plotpixel(self, x, y, attr, row=None): y = self.plotviewBox.ymax-y self.pixels[y][x][attr].append(row) def scaleY(self, canvasY): 'returns plotter y coordinate, with y-axis inverted' plotterY = super().scaleY(canvasY) return (self.plotviewBox.ymax-plotterY+4) def canvasH(self, plotterY): return (self.plotviewBox.ymax-plotterY)/self.yScaler @property def canvasMouse(self): p = super().canvasMouse p.y = self.visibleBox.ymin + (self.plotviewBox.ymax-self.plotterMouse.y)/self.yScaler return p # provides axis labels, legend class GraphSheet(InvertedCanvas): def __init__(self, *names, **kwargs): super().__init__(*names, **kwargs) self.xcols or vd.fail('at least one key col necessary for x-axis') self.ycols or vd.fail('%s is non-numeric' % '/'.join(yc.name for yc in kwargs.get('ycols'))) @asyncthread def reload(self): nerrors = 0 nplotted = 0 self.reset() vd.status('loading data points') catcols = [c for c in self.xcols if not vd.isNumeric(c)] numcols = numericCols(self.xcols) for ycol in self.ycols: for rownum, row in enumerate(Progress(self.sourceRows, 'plotting')): # rows being plotted from source try: k = tuple(c.getValue(row) for c in catcols) if catcols else (ycol.name,) # convert deliberately to float (to e.g. linearize date) graph_x = float(numcols[0].type(numcols[0].getValue(row))) if numcols else rownum graph_y = ycol.type(ycol.getValue(row)) attr = self.plotColor(k) self.point(graph_x, graph_y, attr, row) nplotted += 1 except Exception: nerrors += 1 if options.debug: raise vd.status('loaded %d points (%d errors)' % (nplotted, nerrors)) self.xzoomlevel=self.yzoomlevel=1.0 self.resetBounds() self.refresh() def resetBounds(self): super().resetBounds() self.createLabels() def moveToRow(self, rowstr): ymin, ymax = map(float, map(self.parseY, rowstr.split())) self.cursorBox.ymin = ymin self.cursorBox.h = ymax-ymin return True def moveToCol(self, colstr): xmin, xmax = map(float, map(self.parseX, colstr.split())) self.cursorBox.xmin = xmin self.cursorBox.w = xmax-xmin return True def formatX(self, amt): return ','.join(xcol.format(xcol.type(amt)) for xcol in self.xcols if vd.isNumeric(xcol)) def formatY(self, amt): srccol = self.ycols[0] return srccol.format(srccol.type(amt)) def parseX(self, txt): return self.xcols[0].type(txt) def parseY(self, txt): return self.ycols[0].type(txt) def add_y_axis_label(self, frac): txt = self.formatY(self.visibleBox.ymin + frac*self.visibleBox.h) # plot y-axis labels on the far left of the canvas, but within the plotview height-wise attr = colors.color_graph_axis self.plotlabel(0, self.plotviewBox.ymin + (1.0-frac)*self.plotviewBox.h, txt, attr) def add_x_axis_label(self, frac): txt = self.formatX(self.visibleBox.xmin + frac*self.visibleBox.w) # plot x-axis labels below the plotviewBox.ymax, but within the plotview width-wise attr = colors.color_graph_axis xmin = self.plotviewBox.xmin + frac*self.plotviewBox.w if frac == 1.0: # shift rightmost label to be readable xmin -= max(len(txt)*2 - self.rightMarginPixels+1, 0) self.plotlabel(xmin, self.plotviewBox.ymax+4, txt, attr) def createLabels(self): self.gridlabels = [] # y-axis self.add_y_axis_label(1.00) self.add_y_axis_label(0.75) self.add_y_axis_label(0.50) self.add_y_axis_label(0.25) self.add_y_axis_label(0.00) # x-axis self.add_x_axis_label(1.00) self.add_x_axis_label(0.75) self.add_x_axis_label(0.50) self.add_x_axis_label(0.25) self.add_x_axis_label(0.00) # TODO: if 0 line is within visible bounds, explicitly draw the axis # TODO: grid lines corresponding to axis labels xname = ','.join(xcol.name for xcol in self.xcols if vd.isNumeric(xcol)) or 'row#' xname, _ = clipstr(xname, self.leftMarginPixels//2-2) self.plotlabel(0, self.plotviewBox.ymax+4, xname+'»', colors.color_graph_axis) Sheet.addCommand('.', 'plot-column', 'vd.push(GraphSheet(sheet.name, "graph", source=sheet, sourceRows=rows, xcols=keyCols, ycols=numericCols([cursorCol])))', 'plot current numeric column vs key columns; numeric key column is used for x-axis, while categorical key columns determine color') Sheet.addCommand('g.', 'plot-numerics', 'vd.push(GraphSheet(sheet.name, "graph", source=sheet, sourceRows=rows, xcols=keyCols, ycols=numericCols(nonKeyVisibleCols)))', 'plot a graph of all visible numeric columns vs key columns') # swap directions of up/down InvertedCanvas.addCommand(None, 'go-up', 'sheet.cursorBox.ymin += cursorBox.h', 'move cursor up by its height') InvertedCanvas.addCommand(None, 'go-down', 'sheet.cursorBox.ymin -= cursorBox.h', 'move cursor down by its height') InvertedCanvas.addCommand(None, 'go-top', 'sheet.cursorBox.ymin = visibleBox.ymax', 'move cursor to top edge of visible canvas') InvertedCanvas.addCommand(None, 'go-bottom', 'sheet.cursorBox.ymin = visibleBox.ymin', 'move cursor to bottom edge of visible canvas') InvertedCanvas.addCommand(None, 'go-pagedown', 't=(visibleBox.ymax-visibleBox.ymin); sheet.cursorBox.ymin -= t; sheet.visibleBox.ymin -= t; sheet.refresh()', 'move cursor down to next visible page') InvertedCanvas.addCommand(None, 'go-pageup', 't=(visibleBox.ymax-visibleBox.ymin); sheet.cursorBox.ymin += t; sheet.visibleBox.ymin += t; sheet.refresh()', 'move cursor up to previous visible page') InvertedCanvas.addCommand(None, 'go-down-small', 'sheet.cursorBox.ymin -= canvasCharHeight', 'move cursor down one character') InvertedCanvas.addCommand(None, 'go-up-small', 'sheet.cursorBox.ymin += canvasCharHeight', 'move cursor up one character') InvertedCanvas.addCommand(None, 'resize-cursor-shorter', 'sheet.cursorBox.h -= canvasCharHeight', 'decrease cursor height by one character') InvertedCanvas.addCommand(None, 'resize-cursor-taller', 'sheet.cursorBox.h += canvasCharHeight', 'increase cursor height by one character') @GraphSheet.api def set_y(sheet, s): ymin, ymax = map(float, map(sheet.parseY, s.split())) sheet.zoomTo(BoundingBox(sheet.visibleBox.xmin, ymin, sheet.visibleBox.xmax, ymax)) sheet.refresh() @GraphSheet.api def set_x(sheet, s): xmin, xmax = map(float, map(sheet.parseX, s.split())) sheet.zoomTo(BoundingBox(xmin, sheet.visibleBox.ymin, xmax, sheet.visibleBox.ymax)) sheet.refresh() Canvas.addCommand('y', 'resize-y-input', 'sheet.set_y(input("set ymin ymax="))', 'set ymin/ymax on graph axes') Canvas.addCommand('x', 'resize-x-input', 'sheet.set_x(input("set xmin xmax="))', 'set xmin/xmax on graph axes') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1609740788.0 visidata-2.2.1/visidata/help.py0000660000175000017500000000573200000000000017124 0ustar00kefalakefala00000000000000from visidata import * class HelpSheet(MetaSheet): 'Show all commands available to the source sheet.' rowtype = 'commands' precious = False _ordering = [('sheet', False), ('longname', False)] columns = [ ColumnAttr('sheet'), ColumnAttr('longname'), Column('keystrokes', getter=lambda col,row: col.sheet.revbinds.get(row.longname)), Column('description', getter=lambda col,row: col.sheet.cmddict[(row.sheet, row.longname)].helpstr), ColumnAttr('execstr', width=0), Column('logged', width=0, getter=lambda col,row: isLoggableCommand(row.longname)), ] nKeys = 2 def iterload(self): from pkg_resources import resource_filename cmdlist = VisiDataMetaSheet('cmdlist', source=None) self.cmddict = {} itcmds = vd.commands.iterall() for (k, o), v in itcmds: yield v v.sheet = o self.cmddict[(v.sheet, v.longname)] = v for cmdrow in cmdlist.rows: k = (cmdrow.sheet, cmdrow.longname) if k in self.cmddict: self.cmddict[k].helpstr = cmdrow.helpstr self.revbinds = {} # [longname] -> keystrokes itbindings = vd.bindkeys.iterall() for (keystrokes, _), longname in itbindings: if (keystrokes not in self.revbinds) and not vd.isLongname(keystrokes): self.revbinds[longname] = keystrokes @VisiData.api @asyncthread def help_search(vd, sheet, regex): vs = HelpSheet(source=None) vs.rows = [] # do not trigger push reload vd.push(vs) # push first, then reload vd.sync(vs.reload()) # find rows matching regex on original HelpSheet rowidxs = list(vd.searchRegex(vs, regex=regex, columns="visibleCols")) # add only matching rows allrows = vs.rows vs.rows = [] for rowidx in rowidxs: vs.addRow(allrows[rowidx]) @VisiData.global_api def openManPage(vd): from pkg_resources import resource_filename import os with SuspendCurses(): if os.system(' '.join(['man', resource_filename(__name__, 'man/vd.1')])) != 0: vd.push(TextSheet('man_vd', source=Path(resource_filename(__name__, 'man/vd.txt')))) # in VisiData, ^H refers to the man page globalCommand('^H', 'sysopen-help', 'openManPage()', 'view vd man page') BaseSheet.addCommand('z^H', 'help-commands', 'vd.push(HelpSheet(name + "_commands", source=sheet, revbinds={}))', 'view sheet of command longnames and keybindings for current sheet') BaseSheet.addCommand('gz^H', 'help-commands-all', 'vd.push(HelpSheet("all_commands", source=None, revbinds={}))', 'view sheet of command longnames and keybindings for all sheet types') globalCommand(None, 'help-search', 'help_search(sheet, input("help: "))', 'search through command longnames with search terms') BaseSheet.bindkey('KEY_F(1)', 'sysopen-help') BaseSheet.bindkey('KEY_BACKSPACE', 'sysopen-help') BaseSheet.bindkey('zKEY_F(1)', 'help-commands') BaseSheet.bindkey('zKEY_BACKSPACE', 'help-commands') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608178530.0 visidata-2.2.1/visidata/incr.py0000660000175000017500000000247200000000000017125 0ustar00kefalakefala00000000000000from visidata import VisiData, Sheet, vd, options, option option('incr_base', 1.0, 'start value for column increments', replay=True) @VisiData.api def numrange(vd, n, step=1): 'Generate n values, starting from options.incr_base and increasing by step for each number.' base = type(step)(options.incr_base) yield from (base+x*step for x in range(n)) @VisiData.api def num(vd, *args): 'Return parsed string as number, preferring int to float.' try: return int(*args) except Exception: return float(*args) Sheet.addCommand('i', 'addcol-incr', 'c=SettableColumn(type=int); addColumnAtCursor(c); c.setValues(rows, *numrange(nRows))', 'add column with incremental values') Sheet.addCommand('gi', 'setcol-incr', 'cursorCol.setValues(selectedRows, *numrange(sheet.nSelectedRows))', 'set current column for selected rows to incremental values') Sheet.addCommand('zi', 'addcol-incr-step', 'n=num(input("interval step: ")); c=SettableColumn(type=type(n)); addColumnAtCursor(c); c.setValues(rows, *numrange(nRows, step=n))', 'add column with incremental values times given step') Sheet.addCommand('gzi', 'setcol-incr-step', 'n=num(input("interval step: ")); cursorCol.setValues(selectedRows, *numrange(nSelectedRows, n))', 'set current column for selected rows to incremental values times given step') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608541899.0 visidata-2.2.1/visidata/join.py0000660000175000017500000002421000000000000017123 0ustar00kefalakefala00000000000000import collections import itertools import functools from copy import copy from visidata import * def createJoinedSheet(sheets, jointype=''): sheets[1:] or vd.fail("join requires more than 1 sheet") if jointype == 'append': keyedcols = collections.defaultdict(list, {col.name:[col] for col in sheets[0].visibleCols}) for s in sheets[1:]: for col in s.visibleCols: key = col.name if col.name in keyedcols else col.sheet.visibleCols.index(col) keyedcols[key].append(col) return ConcatSheet('&'.join(vs.name for vs in sheets), sourceCols=list(keyedcols.values())) elif jointype == 'extend': vs = copy(sheets[0]) vs.name = '+'.join(vs.name for vs in sheets) vs.reload = functools.partial(ExtendedSheet_reload, vs, sheets) return vs else: return JoinSheet('+'.join(vs.name for vs in sheets), sources=sheets, jointype=jointype) jointypes = [{'key': k, 'desc': v} for k, v in { 'inner': 'only rows which match keys on all sheets', 'outer': 'all rows from first selected sheet', 'full': 'all rows from all sheets (union)', 'diff': 'only rows NOT in all sheets', 'append': 'columns all sheets; extend with rows from all sheets', 'extend': 'only rows from first sheet; extend with columns from all sheets', 'merge': 'merge differences from other sheets into first sheet', }.items()] def joinkey(sheet, row): return tuple(c.getDisplayValue(row) for c in sheet.keyCols) def groupRowsByKey(sheets, rowsBySheetKey, rowsByKey): with Progress(gerund='grouping', total=sum(len(vs.rows) for vs in sheets)*2) as prog: for vs in sheets: # tally rows by keys for each sheet rowsBySheetKey[vs] = collections.defaultdict(list) for r in vs.rows: prog.addProgress(1) key = joinkey(vs, r) rowsBySheetKey[vs][key].append(r) for vs in sheets: for r in vs.rows: prog.addProgress(1) key = joinkey(vs, r) if key not in rowsByKey: # gather for this key has not been done yet # multiplicative for non-unique keys rowsByKey[key] = [] for crow in itertools.product(*[rowsBySheetKey[vs2].get(key, [None]) for vs2 in sheets]): rowsByKey[key].append(list(crow)) class JoinKeyColumn(Column): def __init__(self, name='', keycols=None, **kwargs): super().__init__(name, type=keycols[0].type, width=keycols[0].width, **kwargs) self.keycols = keycols def calcValue(self, row): vals = set() for i, c in enumerate(self.keycols): if row[i] is not None: vals.add(c.getValue(row[i])) if len(vals) == 1: return vals.pop() else: raise Exception(f'inconsistent keys--reload join') def putValue(self, row, value): for i, c in enumerate(self.keycols): if row[i] is not None: c.setValues([row[i]], value) def recalc(self, sheet=None): Column.recalc(self, sheet) for c in self.keycols: c.recalc() class MergeColumn(Column): def calcValue(self, row): for i, c in enumerate(self.cols): if c: v = c.getTypedValue(row[i]) if v and not isinstance(v, TypedWrapper): return v def putValue(self, row, value): for r, c in zip(row, self.cols[::-1]): if c: c.setValue(r, value) #### slicing and dicing # rowdef: [sheet1_row, sheet2_row, ...] # if a sheet does not have this key, sheet#_row is None class JoinSheet(Sheet): 'Column-wise join/merge. `jointype` constructor arg should be one of jointypes.' colorizers = [ CellColorizer(0, 'color_diff', lambda s,c,r,v: c and r and isinstance(c, MergeColumn) and c.cols[0] and v.value != c.cols[0].getValue(r[0])) ] @asyncthread def reload(self): sheets = self.sources # first item in joined row is the key tuple from the first sheet. # first columns are the key columns from the first sheet, using its row (0) self.columns = [] keyDict = collections.defaultdict(list) for s in sheets: for keyCol in s.keyCols: keyDict[keyCol.name].append(keyCol) for i, cols in enumerate(keyDict.values()): self.addColumn(JoinKeyColumn(name=cols[0].name, keycols=cols)) # ColumnItem(c.name, i, sheet=sheets[0], type=c.type, width=c.width))) self.setKeys(self.columns) allcols = collections.defaultdict(lambda n=len(sheets): [None]*n) for sheetnum, vs in enumerate(sheets): for c in vs.nonKeyVisibleCols: allcols[c.name][sheetnum] = c if self.jointype == 'merge': for colname, cols in allcols.items(): self.addColumn(MergeColumn(colname, cols=cols)) else: ctr = collections.Counter(c.name for vs in sheets for c in vs.nonKeyVisibleCols) for sheetnum, vs in enumerate(sheets): # subsequent elements are the rows from each source, in order of the source sheets for c in vs.nonKeyVisibleCols: newname = c.name if ctr[c.name] == 1 else '%s_%s' % (vs.name, c.name) self.addColumn(SubColumnItem(sheetnum, c, name=newname)) rowsBySheetKey = {} rowsByKey = {} groupRowsByKey(sheets, rowsBySheetKey, rowsByKey) self.rows = [] with Progress(gerund='joining', total=len(rowsByKey)) as prog: for k, combinedRows in rowsByKey.items(): prog.addProgress(1) if self.jointype in ['full', 'merge']: # keep all rows from all sheets for combinedRow in combinedRows: self.addRow(combinedRow) elif self.jointype == 'inner': # only rows with matching key on all sheets for combinedRow in combinedRows: if all(combinedRow): self.addRow(combinedRow) elif self.jointype == 'outer': # all rows from first sheet for combinedRow in combinedRows: if combinedRow[0]: self.addRow(combinedRow) elif self.jointype == 'diff': # only rows without matching key on all sheets for combinedRow in combinedRows: if not all(combinedRow): self.addRow(combinedRow) ## for ExtendedSheet_reload below class ExtendedColumn(Column): def calcValue(self, row): key = joinkey(self.sheet.joinSources[0], row) srcsheet = self.sheet.joinSources[self.sheetnum] srcrow = self.sheet.rowsBySheetKey[srcsheet][key] if srcrow[0]: return self.sourceCol.calcValue(srcrow[0]) @asyncthread def ExtendedSheet_reload(self, sheets): self.joinSources = sheets # first item in joined row is the key tuple from the first sheet. # first columns are the key columns from the first sheet, using its row (0) self.columns = [] for i, c in enumerate(sheets[0].keyCols): self.addColumn(copy(c)) self.setKeys(self.columns) for i, c in enumerate(sheets[0].nonKeyVisibleCols): self.addColumn(copy(c)) for sheetnum, vs in enumerate(sheets[1:]): # subsequent elements are the rows from each source, in order of the source sheets # ctr = collections.Counter(c.name for c in vs.nonKeyVisibleCols) for c in vs.nonKeyVisibleCols: newname = '%s_%s' % (vs.name, c.name) newcol = ExtendedColumn(newname, sheetnum=sheetnum+1, sourceCol=c) self.addColumn(newcol) self.rowsBySheetKey = {} # [srcSheet][key] -> list(rowobjs from sheets[0]) rowsByKey = {} # [key] -> [rows0, rows1, ...] groupRowsByKey(sheets, self.rowsBySheetKey, rowsByKey) self.rows = [] with Progress(gerund='joining', total=len(rowsByKey)) as prog: for k, combinedRows in rowsByKey.items(): prog.addProgress(1) for combinedRow in combinedRows: if combinedRow[0]: self.addRow(combinedRow[0]) ## for ConcatSheet class ConcatColumn(Column): def __init__(self, name, cols, **kwargs): super().__init__(name, **kwargs) self.cols = cols def getColBySheet(self, s): for c in self.cols: if c.sheet is s: return c def calcValue(self, row): srcSheet, srcRow = row srcCol = self.getColBySheet(srcSheet) if srcCol: return srcCol.calcValue(srcRow) def setValue(self, row, v): srcSheet, srcRow = row srcCol = self.getColBySheet(srcSheet) if srcCol: srcCol.setValue(srcRow, v) else: vd.fail('column not on source sheet') # rowdef: (srcSheet, srcRow) class ConcatSheet(Sheet): 'combination of multiple sheets by row concatenation. sourceCols=list(cols). ' @asyncthread def reload(self): self.rows = [] sourceSheets = [] for cols in self.sourceCols: for c in cols: if c.sheet not in sourceSheets: sourceSheets.append(c.sheet) self.columns = [] self.addColumn(ColumnItem('origin_sheet', 0, width=0)) for cols in self.sourceCols: self.addColumn(ConcatColumn(cols[0].name, cols, type=cols[0].type)) for sheet in sourceSheets: for r in Progress(sheet.rows): self.addRow((sheet, r)) IndexSheet.addCommand('&', 'join-sheets', 'vd.push(createJoinedSheet(selectedRows or fail("no sheets selected to join"), jointype=chooseOne(jointypes)))', 'merge selected sheets with visible columns from all, keeping rows according to jointype') Sheet.addCommand('&', 'join-sheets-top2', 'vd.push(createJoinedSheet(vd.sheets[:2], jointype=chooseOne(jointypes)))', 'concatenate top two sheets in Sheets Stack') Sheet.addCommand('g&', 'join-sheets-all', 'vd.push(createJoinedSheet(vd.sheets, jointype=chooseOne(jointypes)))', 'concatenate all sheets in Sheets Stack') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608178530.0 visidata-2.2.1/visidata/layout.py0000660000175000017500000000366600000000000017515 0ustar00kefalakefala00000000000000from visidata import vd, Column, Sheet, options, Fanout @Column.api def setWidth(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 @Column.api def toggleWidth(self, width): 'Change column width to either given `width` or default value.' if self.width != width: self.width = width else: self.width = int(options.default_width) @Column.api def toggleVisibility(self): if self.height == 1: self.height = options.default_height else: self.height = 1 def unhide_cols(cols, rows): 'sets appropriate width if column was either hidden (0) or unseen (None)' for c in cols: c.setWidth(abs(c.width or 0) or c.getMaxWidth(rows)) Sheet.addCommand('_', 'resize-col-max', 'cursorCol.toggleWidth(cursorCol.getMaxWidth(visibleRows))', 'toggle width of current column between full and default width'), Sheet.addCommand('z_', 'resize-col-input', 'width = int(input("set width= ", value=cursorCol.width)); cursorCol.setWidth(width)', 'adjust width of current column to N') Sheet.addCommand('g_', 'resize-cols-max', 'for c in visibleCols: c.setWidth(c.getMaxWidth(visibleRows))', 'toggle widths of all visible clumns between full and default width'), Sheet.addCommand('gz_', 'resize-cols-input', 'width = int(input("set width= ", value=cursorCol.width)); Fanout(visibleCols).setWidth(width)', 'adjust widths of all visible columns to N') Sheet.addCommand('-', 'hide-col', 'cursorCol.hide()', 'hide current column') Sheet.addCommand('z-', 'resize-col-half', 'cursorCol.setWidth(cursorCol.width//2)', 'reduce width of current column by half'), Sheet.addCommand('gv', 'unhide-cols', 'unhide_cols(columns, visibleRows)', 'unhide all columns') Sheet.addCommand('v', 'visibility-sheet', 'for c in visibleCols: c.toggleVisibility()') Sheet.addCommand('zv', 'visibility-col', 'cursorCol.toggleVisibility()') ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1612763830.9859815 visidata-2.2.1/visidata/loaders/0000770000175000017500000000000000000000000017243 5ustar00kefalakefala00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1606688371.0 visidata-2.2.1/visidata/loaders/__init__.py0000660000175000017500000000000000000000000021343 0ustar00kefalakefala00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612075960.0 visidata-2.2.1/visidata/loaders/_pandas.py0000660000175000017500000003365500000000000021237 0ustar00kefalakefala00000000000000from functools import partial from visidata import * def open_pandas(p): return PandasSheet(p.name, source=p) def open_dta(p): return PandasSheet(p.name, source=p, filetype='stata') open_stata = open_pandas for ft in 'feather gbq orc parquet pickle sas stata'.split(): globals().setdefault('open_'+ft, lambda p,ft=ft: PandasSheet(p.name, source=p, filetype=ft)) class DataFrameAdapter: def __init__(self, df): import pandas as pd if not isinstance(df, pd.DataFrame): vd.fail('%s is not a dataframe' % type(df).__name__) self.df = df def __len__(self): if 'df' not in self.__dict__: return 0 return len(self.df) def __getitem__(self, k): if isinstance(k, slice): return DataFrameAdapter(self.df.iloc[k]) return self.df.iloc[k] def __getattr__(self, k): if 'df' not in self.__dict__: raise AttributeError(f"'{self.__class__.__name__}' has no attribute '{k}'") return getattr(self.df, k) # source=DataFrame class PandasSheet(Sheet): '''Sheet sourced from a pandas.DataFrame Warning: The index of the pandas.DataFrame input must be unique. Otherwise the selection functionality, which relies on looking up selected rows via the index, will break. This can be done by calling reset_index(). Note: Columns starting with "__vd_" are reserved for internal usage by the VisiData loader. ''' def dtype_to_type(self, dtype): import numpy as np # Find the underlying numpy dtype for any pandas extension dtypes dtype = getattr(dtype, 'numpy_dtype', dtype) try: if np.issubdtype(dtype, np.integer): return int if np.issubdtype(dtype, np.floating): return float if np.issubdtype(dtype, np.datetime64): return date except TypeError: # For categoricals and other pandas-defined dtypes pass return anytype def read_tsv(self, path, **kwargs): 'Partial function for reading TSV files using pd.read_csv' import pandas as pd return pd.read_csv(path, sep='\t', **kwargs) @property def df(self): if isinstance(getattr(self, 'rows', None), DataFrameAdapter): return self.rows.df @df.setter def df(self, val): if isinstance(getattr(self, 'rows', None), DataFrameAdapter): self.rows.df = val else: self.rows = DataFrameAdapter(val) def getValue(self, col, row): '''Look up column values in the underlying DataFrame.''' return col.sheet.df.loc[row.name, col.name] def setValue(self, col, row, val): ''' Update a column's value in the underlying DataFrame, loosening the column's type as needed. Take care to avoid assigning to a view or a copy as noted here: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#why-does-assignment-fail-when-using-chained-indexing ''' try: col.sheet.df.loc[row.name, col.name] = val except ValueError as err: vd.warning(f'Type of {val} does not match column {col.name}. Changing type.') col.type = anytype col.sheet.df.loc[row.name, col.name] = val def reload(self): import pandas as pd if isinstance(self.source, pd.DataFrame): df = self.source elif isinstance(self.source, Path): filetype = getattr(self, 'filetype', self.source.ext) if filetype == 'tsv': readfunc = self.read_tsv elif filetype == 'jsonl': readfunc = partial(pd.read_json, lines=True) else: readfunc = getattr(pd, 'read_'+filetype) or vd.error('no pandas.read_'+filetype) df = readfunc(str(self.source), **options.getall('pandas_'+filetype+'_')) else: try: df = pd.DataFrame(self.source) except ValueError as err: vd.fail('error building pandas DataFrame from source data: %s' % err) # reset the index here if type(df.index) is not pd.RangeIndex: df = df.reset_index() # VisiData assumes string column names but pandas does not. Forcing string # columns at load-time avoids various errors later. df.columns = df.columns.astype(str) self.columns = [] for col in (c for c in df.columns if not c.startswith("__vd_")): self.addColumn(Column( col, type=self.dtype_to_type(df[col]), getter=self.getValue, setter=self.setValue )) if self.columns[0].name == 'index': # if the df contains an index column self.column('index').hide() self.rows = DataFrameAdapter(df) self._selectedMask = pd.Series(False, index=df.index) if df.index.nunique() != df.shape[0]: vd.warning("Non-unique index, row selection API may not work or may be incorrect") @asyncthread def sort(self): '''Sort rows according to the current self._ordering.''' by_cols = [] ascending = [] for col, reverse in self._ordering[::-1]: by_cols.append(col.name) ascending.append(not reverse) self.rows.sort_values(by=by_cols, ascending=ascending, inplace=True) def _checkSelectedIndex(self): import pandas as pd if self._selectedMask.index is not self.df.index: # DataFrame was modified inplace, so the selection is no longer valid vd.status('pd.DataFrame.index updated, clearing {} selected rows' .format(self._selectedMask.sum())) self._selectedMask = pd.Series(False, index=self.df.index) def rowid(self, row): return getattr(row, 'name', None) or '' # Base selection API. Refer to GH #266: using id() will not identify # pandas rows since iterating on rows / selecting rows will return # different copies. Instead, re-implement the selection API by # keeping a boolean pd.Series indicating the selected rows. def isSelected(self, row): if row is None: return False self._checkSelectedIndex() return self._selectedMask.loc[row.name] def selectRow(self, row): 'Select given row' self._checkSelectedIndex() self._selectedMask.loc[row.name] = True def unselectRow(self, row): self._checkSelectedIndex() is_selected = self._selectedMask.loc[row.name] self._selectedMask.loc[row.name] = False return is_selected @property def nSelectedRows(self): self._checkSelectedIndex() return self._selectedMask.sum() @property def selectedRows(self): self._checkSelectedIndex() return DataFrameAdapter(self.df.loc[self._selectedMask]) # Vectorized implementation of multi-row selections @asyncthread def select(self, rows, status=True, progress=True): self.addUndoSelection() for row in (Progress(rows, 'selecting') if progress else rows): self.selectRow(row) @asyncthread def unselect(self, rows, status=True, progress=True): self.addUndoSelection() for row in (Progress(rows, 'unselecting') if progress else rows): self.unselectRow(row) def clearSelected(self): import pandas as pd self._selectedMask = pd.Series(False, index=self.df.index) def selectByIndex(self, start=None, end=None): self._checkSelectedIndex() self._selectedMask.iloc[start:end] = True def unselectByIndex(self, start=None, end=None): self._checkSelectedIndex() self._selectedMask.iloc[start:end] = False def toggleByIndex(self, start=None, end=None): self._checkSelectedIndex() self.addUndoSelection() self._selectedMask.iloc[start:end] = ~self._selectedMask.iloc[start:end] def _selectByILoc(self, mask, selected=True): self._checkSelectedIndex() self._selectedMask.iloc[mask] = selected @asyncthread def selectByRegex(self, regex, columns, unselect=False): ''' Find rows matching regex in the provided columns. By default, add matching rows to the selection. If unselect is True, remove from the active selection instead. ''' import pandas as pd case_sensitive = 'I' not in vd.options.regex_flags masks = pd.DataFrame([ self.df[col.name].astype(str).str.contains(pat=regex, case=case_sensitive, regex=True) for col in columns ]) if unselect: self._selectedMask = self._selectedMask & ~masks.any() else: self._selectedMask = self._selectedMask | masks.any() def addUndoSelection(self): vd.addUndo(undoAttrCopyFunc([self], '_selectedMask')) @property def nRows(self): if self.df is None: return 0 return len(self.df) def newRows(self, n): ''' Return n rows of empty data. Let pandas decide on the most appropriate missing value (NaN, NA, etc) based on the underlying DataFrame's dtypes. ''' import pandas as pd return pd.DataFrame({ col: [None] * n for col in self.df.columns }).astype(self.df.dtypes.to_dict(), errors='ignore') def _addRows(self, rows, idx): import pandas as pd if idx is None: self.df = self.df.append(pd.DataFrame(rows)) else: self.df = pd.concat((self.df.iloc[0:idx], pd.DataFrame(rows), self.df.iloc[idx:])) self.df.index = pd.RangeIndex(self.nRows) self._checkSelectedIndex() def _deleteRows(self, which): import pandas as pd self.df.drop(which, inplace=True) self.df.index = pd.RangeIndex(self.nRows) self._checkSelectedIndex() def addNewRows(self, n, idx=None): self._addRows(self.newRows(n), idx) idx = idx or self.nRows - 1 vd.addUndo(self._deleteRows, range(idx, idx + n)) def addRow(self, row, idx=None): self._addRows([row], idx) vd.addUndo(self._deleteRows, idx or self.nRows - 1) def delete_row(self, rowidx): import pandas as pd oldrow = self.df.iloc[rowidx:rowidx+1] # Use to_dict() here to work around an edge case when applying undos. # As an action is undone, its entry gets removed from the cmdlog sheet. # If we use `oldrow` directly, we get errors comparing DataFrame objects # when there are multiple deletion commands for the same row index. # There may be a better way to handle that case. vd.addUndo(self._addRows, oldrow.to_dict(), rowidx) self._deleteRows(rowidx) vd.cliprows = [(self, rowidx, oldrow)] def deleteBy(self, by): '''Delete rows for which func(row) is true. Returns number of deleted rows.''' import pandas as pd oldidx = self.cursorRowIndex nRows = self.nRows vd.addUndo(setattr, self, 'df', self.df.copy()) self.df = self.df[~by] self.df.index = pd.RangeIndex(self.nRows) ndeleted = nRows - self.nRows vd.status('deleted %s %s' % (ndeleted, self.rowtype)) return ndeleted def deleteSelected(self): '''Delete all selected rows.''' self.deleteBy(self._selectedMask) def view_pandas(df): run(PandasSheet('', source=df)) # Override with vectorized implementations PandasSheet.addCommand(None, 'stoggle-rows', 'toggleByIndex()', 'toggle selection of all rows') PandasSheet.addCommand(None, 'select-rows', 'selectByIndex()', 'select all rows') PandasSheet.addCommand(None, 'unselect-rows', 'unselectByIndex()', 'unselect all rows') PandasSheet.addCommand(None, 'stoggle-before', 'toggleByIndex(end=cursorRowIndex)', 'toggle selection of rows from top to cursor') PandasSheet.addCommand(None, 'select-before', 'selectByIndex(end=cursorRowIndex)', 'select all rows from top to cursor') PandasSheet.addCommand(None, 'unselect-before', 'unselectByIndex(end=cursorRowIndex)', 'unselect all rows from top to cursor') PandasSheet.addCommand(None, 'stoggle-after', 'toggleByIndex(start=cursorRowIndex)', 'toggle selection of rows from cursor to bottom') PandasSheet.addCommand(None, 'select-after', 'selectByIndex(start=cursorRowIndex)', 'select all rows from cursor to bottom') PandasSheet.addCommand(None, 'unselect-after', 'unselectByIndex(start=cursorRowIndex)', 'unselect all rows from cursor to bottom') PandasSheet.addCommand(None, 'random-rows', 'nrows=int(input("random number to select: ", value=nRows)); vs=copy(sheet); vs.name=name+"_sample"; vs.rows=DataFrameAdapter(sheet.df.sample(nrows or nRows)); vd.push(vs)', 'open duplicate sheet with a random population subset of N rows'), # Handle the regex selection family of commands through a single method, # since the core logic is shared PandasSheet.addCommand('|', 'select-col-regex', 'selectByRegex(regex=input("select regex: ", type="regex", defaultLast=True), columns=[cursorCol])', 'select rows matching regex in current column') PandasSheet.addCommand('\\', 'unselect-col-regex', 'selectByRegex(regex=input("select regex: ", type="regex", defaultLast=True), columns=[cursorCol], unselect=True)', 'unselect rows matching regex in current column') PandasSheet.addCommand('g|', 'select-cols-regex', 'selectByRegex(regex=input("select regex: ", type="regex", defaultLast=True), columns=visibleCols)', 'select rows matching regex in any visible column') PandasSheet.addCommand('g\\', 'unselect-cols-regex', 'selectByRegex(regex=input("select regex: ", type="regex", defaultLast=True), columns=visibleCols, unselect=True)', 'unselect rows matching regex in any visible column') # Override with a pandas/dataframe-aware implementation PandasSheet.addCommand('"', 'dup-selected', 'vs=PandasSheet(sheet.name, "selectedref", source=selectedRows.df); vd.push(vs)', 'open duplicate sheet with only selected rows'), ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608541899.0 visidata-2.2.1/visidata/loaders/archive.py0000660000175000017500000000572700000000000021252 0ustar00kefalakefala00000000000000import codecs import tarfile import zipfile from visidata import * def open_zip(p): return ZipSheet(p.name, source=p) def open_tar(p): return TarSheet(p.name, source=p) open_tgz = open_tar open_txz = open_tar open_tbz2 = open_tar class ZipSheet(Sheet): 'Wrapper for `zipfile` library.' rowtype = 'files' # rowdef ZipInfo columns = [ ColumnAttr('filename'), ColumnAttr('file_size', type=int), Column('date_time', type=date, getter=lambda col, row: datetime.datetime(*row.date_time)), ColumnAttr('compress_size', type=int) ] def openZipFile(self, fp, *args, **kwargs): '''Use VisiData input to handle password-protected zip files.''' try: return fp.open(*args, **kwargs) except RuntimeError as err: if 'password required' in err.args[0]: pwd = vd.input(f'{args[0].filename} is encrypted, enter password: ', display=False) return fp.open(*args, **kwargs, pwd=pwd.encode('utf-8')) vd.error(err) def openRow(self, fi): decodedfp = codecs.iterdecode(self.openZipFile(self.zfp, fi), encoding=options.encoding, errors=options.encoding_errors) return vd.openSource(Path(fi.filename, fp=decodedfp, filesize=fi.file_size), filetype=options.filetype) @asyncthread def extract(self, *rows, path=None): self.zfp.extractall(members=[r.filename for r in rows], path=path) @property def zfp(self): return zipfile.ZipFile(str(self.source), 'r') def iterload(self): with self.zfp as zf: for zi in Progress(zf.infolist()): yield zi class TarSheet(Sheet): 'Wrapper for `tarfile` library.' rowtype = 'files' # rowdef TarInfo columns = [ ColumnAttr('name'), ColumnAttr('size', type=int), ColumnAttr('mtime', type=date), ColumnAttr('type', type=int), ColumnAttr('mode', type=int), ColumnAttr('uname'), ColumnAttr('gname') ] def openRow(self, fi): tfp = tarfile.open(name=str(self.source)) decodedfp = codecs.iterdecode(tfp.extractfile(fi), encoding=options.encoding, errors=options.encoding_errors) return vd.openSource(Path(fi.name, fp=decodedfp, filesize=fi.size)) def iterload(self): with tarfile.open(name=str(self.source)) as tf: for ti in Progress(tf.getmembers()): yield ti ZipSheet.addCommand('x', 'extract-file', 'extract(cursorRow)') ZipSheet.addCommand('gx', 'extract-selected', 'extract(*onlySelectedRows)') ZipSheet.addCommand('zx', 'extract-file-to', 'extract(cursorRow, path=inputPath("extract to: "))') ZipSheet.addCommand('gzx', 'extract-selected-to', 'extract(*onlySelectedRows, path=inputPath("extract %d files to: " % nSelected))') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608178530.0 visidata-2.2.1/visidata/loaders/csv.py0000660000175000017500000000377100000000000020421 0ustar00kefalakefala00000000000000 from visidata import * import csv option('csv_dialect', 'excel', 'dialect passed to csv.reader', replay=True) option('csv_delimiter', ',', 'delimiter passed to csv.reader', replay=True) option('csv_quotechar', '"', 'quotechar passed to csv.reader', replay=True) option('csv_skipinitialspace', True, 'skipinitialspace passed to csv.reader', replay=True) option('csv_escapechar', None, 'escapechar passed to csv.reader', replay=True) option('csv_lineterminator', '\r\n', 'lineterminator passed to csv.writer', replay=True) option('safety_first', False, 'sanitize input/output to handle edge cases, with a performance cost', replay=True) csv.field_size_limit(2**31-1) # Windows has max 32-bit options_num_first_rows = 10 def open_csv(p): return CsvSheet(p.name, source=p) def removeNulls(fp): for line in fp: yield line.replace('\0', '') class CsvSheet(SequenceSheet): _rowtype = list # rowdef: list of values def iterload(self): 'Convert from CSV, first handling header row specially.' with self.source.open_text() as fp: if options.safety_first: rdr = csv.reader(removeNulls(fp), **options.getall('csv_')) else: rdr = csv.reader(fp, **options.getall('csv_')) while True: try: yield next(rdr) except csv.Error as e: e.stacktrace=stacktrace() yield [TypedExceptionWrapper(None, exception=e)] except StopIteration: return @VisiData.api def save_csv(vd, p, sheet): 'Save as single CSV file, handling column names as first line.' with p.open_text(mode='w') as fp: cw = csv.writer(fp, **options.getall('csv_')) colnames = [col.name for col in sheet.visibleCols] if ''.join(colnames): cw.writerow(colnames) with Progress(gerund='saving'): for dispvals in sheet.iterdispvals(format=True): cw.writerow(dispvals.values()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608178530.0 visidata-2.2.1/visidata/loaders/eml.py0000660000175000017500000000356200000000000020401 0ustar00kefalakefala00000000000000from visidata import * def open_eml(p): return EmailSheet(p.name, source=p) class EmailSheet(TableSheet): rowtype = 'parts' # rowdef: sub-Messages columns = [ Column('filename', getter=lambda c,r: r.get_filename()), Column('content_type', getter=lambda c,r: r.get_content_type()), Column('payload', type=vlen, getter=lambda c,r: r.get_payload(decode=False)), ] def iterload(self): import email parser = email.parser.Parser() with self.source.open_text() as fp: yield from parser.parse(fp).walk() @EmailSheet.api def extract_part(sheet, givenpath, part): with givenpath.open_bytes(mode='w') as fp: fp.write(part.get_payload(decode=True)) @EmailSheet.api def extract_parts(sheet, givenpath, *parts): 'Save all *parts* to Path *givenpath*.' if givenpath.exists() and sheet.options.confirm_overwrite: confirm("%s already exists. overwrite? " % givenpath.given) vd.status('saving %s parts to %s' % (len(parts), givenpath.given)) # forcibly specify save individual files into directory by ending path with / if givenpath.is_dir() or givenpath.given.endswith('/') or len(parts) > 1: # save as individual files in the givenpath directory try: os.makedirs(givenpath, exist_ok=True) except FileExistsError: pass for part in parts: vd.execAsync(sheet.extract_part, givenpath / part.get_filename(), part) elif len(parts) == 1: vd.execAsync(sheet.extract_part, givenpath, part) else: vd.fail('cannot save multiple parts to non-dir') EmailSheet.addCommand('x', 'extract-part', 'extract_part(inputPath("save part as: ", value=cursorRow.get_filename()), cursorRow)') EmailSheet.addCommand('gx', 'extract-part-selected', 'extract_parts(inputPath("save %d parts in: " % nSelectedRows), *selectedRows)') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608541899.0 visidata-2.2.1/visidata/loaders/fixed_width.py0000660000175000017500000000560200000000000022117 0ustar00kefalakefala00000000000000 from visidata import * option('fixed_rows', 1000, 'number of rows to check for fixed width columns') option('fixed_maxcols', 0, 'max number of fixed-width columns to create (0 is no max)') def open_fixed(p): return FixedWidthColumnsSheet(p.name, source=p, headerlines=[]) class FixedWidthColumn(Column): def __init__(self, name, i, j, **kwargs): super().__init__(name, **kwargs) self.i, self.j = i, j def calcValue(self, row): return row[0][self.i:self.j] def putValue(self, row, value): value = str(value)[:self.j-self.i] j = self.j or len(row) row[0] = row[0][:self.i] + '%-*s' % (j-self.i, value) + row[0][self.j:] def columnize(rows): 'Generate (i,j) indexes for fixed-width columns found in rows' ## find all character columns that are not spaces ever allNonspaces = set() for r in rows: for i, ch in enumerate(r): if not ch.isspace(): allNonspaces.add(i) colstart = 0 prev = 0 # collapse fields for i in allNonspaces: if i > prev+1: yield colstart, i colstart = i prev = i yield colstart, None # final column gets rest of line class FixedWidthColumnsSheet(SequenceSheet): rowtype = 'lines' # rowdef: [line] (wrapping in list makes it unique and modifiable) def addRow(self, row, index=None): Sheet.addRow(self, row, index=index) def iterload(self): itsource = iter(self.source) # compute fixed width columns from first fixed_rows lines maxcols = self.options.fixed_maxcols self.columns = [] fixedRows = list([x] for x in self.optlines(itsource, 'fixed_rows')) for i, j in columnize(list(r[0] for r in fixedRows)): if maxcols and self.nCols >= maxcols-1: self.addColumn(FixedWidthColumn('', i, None)) break else: self.addColumn(FixedWidthColumn('', i, j)) yield from fixedRows self.setColNames(self.headerlines) yield from ([line] for line in itsource) def setCols(self, headerlines): self.headerlines = headerlines @VisiData.api def save_fixed(vd, p, *vsheets): with p.open_text(mode='w') as fp: for sheet in vsheets: if len(vsheets) > 1: fp.write('%s\n\n' % vs.name) # headers for col in sheet.visibleCols: fp.write('{0:{width}}'.format(col.name, width=col.width)) fp.write('\n') # rows with Progress(gerund='saving'): for dispvals in sheet.iterdispvals(format=True): for col, val in dispvals.items(): fp.write('{0:{align}{width}}'.format(val, width=col.width, align='>' if vd.isNumeric(col) else '<')) fp.write('\n') vd.status('%s save finished' % p) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608541899.0 visidata-2.2.1/visidata/loaders/frictionless.py0000660000175000017500000000065300000000000022326 0ustar00kefalakefala00000000000000from visidata import * def open_frictionless(p): return FrictionlessIndexSheet(p.name, source=p) class FrictionlessIndexSheet(IndexSheet): def iterload(self): import datapackage self.dp = datapackage.Package(self.source.open_text()) for r in Progress(self.dp.resources): yield vd.openSource(self.source.with_name(r.descriptor['path']), filetype=r.descriptor.get('format', 'json')) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612075960.0 visidata-2.2.1/visidata/loaders/geojson.py0000660000175000017500000001057500000000000021272 0ustar00kefalakefala00000000000000from functools import reduce import json from visidata import * def open_geojson(p): return GeoJSONSheet(p.name, source=p) def getter_factory(prop): def getter(col, row): return row.get('properties', {}).get(prop) return getter class GeoJSONSheet(PythonSheet): rowtype = 'shapes' def iterload(self): self.colnames = {} with self.source.open_text() as fp: ret = json.load(fp) if ret['type'] == 'FeatureCollection': features = ret['features'] elif ret['type'] == 'Feature': features = [ret] elif ret['type'] == 'GeometryCollection': features = list(map(lambda g: { 'type': 'Feature', 'geometry': g }, ret['geometries'])) else: # Some form of geometry features = [{ 'type': 'Feature', 'geometry': ret }] for feature in Progress(features): for prop in feature.get('properties', {}).keys(): if prop not in self.colnames: c = Column(name=prop, getter=getter_factory(prop)) self.colnames[prop] = c self.addColumn(c) yield feature class GeoJSONMap(InvertedCanvas): aspectRatio = 1.0 filetype = 'geojson' @asyncthread def reload(self): self.reset() for row in Progress(self.sourceRows): k = self.source.rowkey(row) colour = self.plotColor(k) bbox = self.parse_geometry(row, colour) x1, y1, x2, y2 = bbox textx, texty = (x1+x2)/2, (y1+y2)/2 disptext = self.textCol.getDisplayValue(row) self.label(textx, texty, disptext, colour, row) self.refresh() def parse_geometry(self, row, colour, bbox=None): if bbox is None: bbox = [180, 90, -180, -90] typ = row['geometry']['type'] if typ == 'GeometryCollection': for g in row['geometries']: bbox = self.parse_geometry(row, colour, bbox) return bbox coords = row['geometry']['coordinates'] if typ in ('Point', 'LineString', 'Polygon'): coords = [coords] if typ in ('Point', 'MultiPoint'): for x, y in coords: self.point(x, y, colour, row) bbox = reduce_coords(coords, bbox) elif typ in ('LineString', 'MultiLineString'): for line in coords: self.polyline(line, colour, row) bbox = reduce_coords(line, bbox) elif typ in ('Polygon', 'MultiPolygon'): for polygon in coords: self.polygon(polygon[0], colour, row) bbox = reduce_coords(polygon[0], bbox) for hole in polygon[1:]: self.polygon(hole, 0, row) else: vd.status('notimpl shapeType %s' % typ) return bbox def reduce_coords(coords, initial): return reduce( lambda a,n: [min(a[0],n[0]), min(a[1],n[1]), max(a[2],n[0]), max(a[3],n[1])], coords, initial) @GeoJSONMap.api def save_geojson(vd, p, vs): features = [] visibleCols = list(map(lambda c: c.name, vs.source.visibleCols)) for row in Progress(vs.sourceRows, 'saving'): row = deepcopy(row) row['properties'] = {k:v for k,v in row.get('properties', {}).items() if k in visibleCols} features.append(row) featcoll = { 'type': 'FeatureCollection', 'features': features, } try: indent = int(vs.options.json_indent) except Exception: indent = vs.options.json_indent with p.open_text(mode='w') as fp: encoder = json.JSONEncoder(indent=indent, sort_keys=vs.options.json_sort_keys) for chunk in encoder.iterencode(featcoll): fp.write(chunk) GeoJSONSheet.addCommand('.', 'plot-row', 'vd.push(GeoJSONMap(name+"_map", sourceRows=[cursorRow], textCol=cursorCol, source=sheet))', 'plot geospatial vector in current row') GeoJSONSheet.addCommand('g.', 'plot-rows', 'vd.push(GeoJSONMap(name+"_map", sourceRows=rows, textCol=cursorCol, source=sheet))', 'plot all geospatial vectors in current sheet') GeoJSONMap.addCommand('^S', 'save-sheet', 'vd.saveSheets(inputPath("save to: ", value=getDefaultSaveName(sheet)), sheet, confirm_overwrite=options.confirm_overwrite)', 'save current sheet to filename in format determined by extension (default .geojson)') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608541899.0 visidata-2.2.1/visidata/loaders/graphviz.py0000660000175000017500000000434700000000000021460 0ustar00kefalakefala00000000000000from visidata import vd, options, option, TypedWrapper, asyncthread, Progress from visidata import wrapply, clean_to_id, VisiData, SIFormatter option('graphviz_edge_labels', True, 'whether to include edge labels on graphviz diagrams') def is_valid(v): if v is None: return False if isinstance(v, TypedWrapper): return False return True @VisiData.api def save_dot(vd, p, vs): unusedColors = 'orange green purple cyan red blue black'.split() assignedColors = {} srccol = vs.keyCols[0] dstcol = vs.keyCols[1] with p.open_text(mode='w') as fp: print('graph { concentrate=true;', file=fp) for row in Progress(vs.rows, 'saving'): src = srccol.getTypedValue(row) dst = dstcol.getTypedValue(row) if not is_valid(src) or not is_valid(dst): continue downsrc = clean_to_id(str(src)) or src downdst = clean_to_id(str(dst)) or dst edgenotes = [c.getTypedValue(row) for c in vs.nonKeyVisibleCols if not vd.isNumeric(c)] edgetype = '-'.join(str(x) for x in edgenotes if is_valid(x)) color = assignedColors.get(edgetype, None) if not color: color = unusedColors.pop() if unusedColors else 'black' assignedColors[edgetype] = color if options.graphviz_edge_labels: nodelabels = [wrapply(SIFormatter, '%0.1f', c.getTypedValue(row)) for c in vs.nonKeyVisibleCols if vd.isNumeric(c)] label = '/'.join(str(x) for x in nodelabels if is_valid(x)) else: label = '' print('\t%s[label="%s"];' % (downsrc, src), file=fp) print('\t%s[label="%s"];' % (downdst, dst), file=fp) print('\t%s -- %s[label="%s", color=%s];' % (downsrc, downdst, label, color), file=fp) print('label="%s"' % vs.name, file=fp) print('node[shape=plaintext];', file=fp) print('subgraph cluster_legend {', file=fp) print('label="Legend";', file=fp) for i, (k, color) in enumerate(assignedColors.items()): print('key%d[label="%s", fontcolor=%s];' % (i, k, color), file=fp) print('}', file=fp) # legend subgraph print('}', file=fp) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608541899.0 visidata-2.2.1/visidata/loaders/hdf5.py0000660000175000017500000000413500000000000020447 0ustar00kefalakefala00000000000000from visidata import * def open_h5(p): return Hdf5ObjSheet(p.name, source=p) open_hdf5 = open_h5 class Hdf5ObjSheet(Sheet): 'Support sheets in HDF5 format.' def iterload(self): import h5py source = self.source if isinstance(self.source, Path): source = h5py.File(str(self.source), 'r') self.columns = [] if isinstance(source, h5py.Group): self.rowtype = 'sheets' self.columns = [ Column(source.name, type=str, getter=lambda col,row: row.source.name.split('/')[-1], keycol=1), Column('type', type=str, getter=lambda col,row: type(row.source).__name__), Column('nItems', type=int, getter=lambda col,row: len(row.source)), ] self.recalc() for k, v in source.items(): yield Hdf5ObjSheet(self.name, k, source=v) elif isinstance(source, h5py.Dataset): if len(source.shape) == 1: for i, colname in enumerate(source.dtype.names or [0]): self.addColumn(ColumnItem(colname, colname), index=i) yield from source # copy elif len(source.shape) == 2: # matrix ncols = source.shape[1] for i in range(ncols): self.addColumn(ColumnItem('', i, width=8), index=i) self.recalc() yield from source # copy else: vd.status('too many dimensions in shape %s' % str(source.shape)) else: vd.status('unknown h5 object type %s' % type(source)) def openRow(self, row): import h5py if isinstance(row, BaseSheet): return row if isinstance(row, h5py.HLObject): return Hdf5ObjSheet(row) import numpy from .npy import NpySheet if isinstance(row, numpy.ndarray): return NpySheet(None, npy=row) Hdf5ObjSheet.addCommand('A', 'dive-metadata', 'vd.push(SheetDict(cursorRow.name + "_attrs", source=cursorRow.attrs))', 'open metadata sheet for object referenced in current row') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612075960.0 visidata-2.2.1/visidata/loaders/html.py0000660000175000017500000001137400000000000020570 0ustar00kefalakefala00000000000000import html from visidata import * option('html_title', '

{sheet.name}

', 'table header when saving to html') def open_html(p): return HtmlTablesSheet(p.name, source=p) open_htm = open_html class HtmlTablesSheet(IndexSheet): rowtype = 'sheets' # rowdef: HtmlTableSheet (sheet.html = lxml.html.HtmlElement) columns = IndexSheet.columns + [ Column('tag', width=0, getter=lambda col,row: row.html.tag), Column('id', getter=lambda col,row: row.html.attrib.get('id')), Column('classes', getter=lambda col,row: row.html.attrib.get('class')), ] def iterload(self): import lxml.html from lxml import etree utf8_parser = etree.HTMLParser(encoding='utf-8') with self.source.open_text() as fp: html = lxml.html.etree.parse(fp, parser=utf8_parser) self.setKeys([self.column('name')]) self.column('keys').hide() self.column('source').hide() for i, e in enumerate(html.iter('table')): if e.tag == 'table': yield HtmlTableSheet(e.attrib.get("id", "table_" + str(i)), source=e, html=e) def is_header(elem): scope = elem.attrib.get('scope', '') if elem.tag == 'th': if not scope or scope == 'col': return True return False class HtmlTableSheet(Sheet): rowtype = 'rows' # list of strings columns = [] def iterload(self): headers = [] maxlinks = {} # [colnum] -> nlinks:int for rownum, r in enumerate(self.source.iter('tr')): row = [] colnum = 0 # get starting column, which might be different if there were rowspan>1 already if rownum < len(headers): while colnum < len(headers[rownum]): if headers[rownum][colnum] is None: break colnum += 1 for cell in r.getchildren(): colspan = int(cell.attrib.get('colspan', 1)) rowspan = int(cell.attrib.get('rowspan', 1)) cellval = ' '.join(x.strip() for x in cell.itertext()) # text only without markup links = [x.get('href') for x in cell.iter('a')] maxlinks[colnum] = max(maxlinks.get(colnum, 0), len(links)) if is_header(cell): for k in range(rownum, rownum+rowspan): while k >= len(headers): # extend headers list with lists for all header rows headers.append([]) for j in range(colnum, colnum+colspan): while j >= len(headers[k]): headers[k].append(None) headers[k][j] = cellval cellval = '' # use empty non-None value for subsequent rows in the rowspan else: while colnum >= len(row): row.append(None) row[colnum] = (cellval, links) colnum += colspan if any(row): yield row self.columns = [] if headers: it = itertools.zip_longest(*headers, fillvalue='') else: it = [list(x) for x in self.rows[0]] self.rows = self.rows[1:] for colnum, names in enumerate(it): name = '_'.join(str(x) for x in names if x) self.addColumn(Column(name, getter=lambda c,r,i=colnum: r[i][0])) for linknum in range(maxlinks.get(colnum, 0)): self.addColumn(Column(name+'_link'+str(linknum), width=20, getter=lambda c,r,i=colnum,j=linknum: r[i][1][j])) @VisiData.api def save_html(vd, p, *vsheets): 'Save vsheets as HTML tables in a single file' with open(p, 'w', encoding='ascii', errors='xmlcharrefreplace') as fp: for sheet in vsheets: if options.html_title: fp.write(options.html_title.format(sheet=sheet, vd=vd)) fp.write('\n'.format(sheetname=html.escape(sheet.name))) # headers fp.write('') for col in sheet.visibleCols: contents = html.escape(col.name) fp.write(''.format(colname=contents)) fp.write('\n') # rows with Progress(gerund='saving'): for dispvals in sheet.iterdispvals(format=True): fp.write('') for val in dispvals.values(): fp.write('') fp.write('\n') fp.write('
{colname}
') fp.write(html.escape(val)) fp.write('
') vd.status('%s save finished' % p) VisiData.save_htm = VisiData.save_html ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612075960.0 visidata-2.2.1/visidata/loaders/http.py0000660000175000017500000000376400000000000020607 0ustar00kefalakefala00000000000000from visidata import Path, RepeatFile, options, vd content_filetypes = { 'tab-separated-values': 'tsv' } vd.option('http_max_next', 0, 'max next.url pages to follow in http response') #848 def openurl_http(path, filetype=None): import requests response = requests.get(path.given, stream=True) response.raise_for_status() if not filetype: # try auto-detect from extension ext = path.suffix[1:].lower() openfunc = vd.getGlobals().get(f'open_{ext}') if openfunc: filetype = ext else: # if extension unknown, fallback to mime-type contenttype = response.headers['content-type'] subtype = contenttype.split(';')[0].split('/')[-1] filetype = content_filetypes.get(subtype, subtype) # If no charset is provided by response headers, use the user-specified # encoding option (which defaults to UTF-8) and hope for the best. The # alternative is an error because iter_lines() will produce bytes. We're # streaming so can't use response.apparent_encoding. if not response.encoding: response.encoding = options.encoding # Automatically paginate if a 'next' URL is given def _iter_lines(path=path, response=response, max_next=options.http_max_next): path.responses = [] n = 0 while response: path.responses.append(response) yield from response.iter_lines(decode_unicode=True) src = response.links.get('next', {}).get('url', None) if not src: break n += 1 if n > max_next: vd.warning(f'stopping at max {max_next} pages') break vd.status(f'fetching next page from {src}') response = requests.get(src, stream=True) # add resettable iterator over contents as an already-open fp path.fp = RepeatFile(iter_lines=_iter_lines()) return vd.openSource(path, filetype=filetype) openurl_https = openurl_http ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608178530.0 visidata-2.2.1/visidata/loaders/imap.py0000660000175000017500000000406500000000000020551 0ustar00kefalakefala00000000000000from visidata import vd, TableSheet, asyncthread, ColumnItem, Column, ColumnAttr, Progress from urllib.parse import urlparse def openurl_imap(p, **kwargs): url = urlparse(p.given) password = url.password or vd.error('no password given in url') # vd.input("imap password for %s" % user, display=False)) return ImapSheet(url.hostname, source=url, password=password) class ImapSheet(TableSheet): columns = [ ColumnItem('message-id'), ColumnItem('folder'), ColumnItem('Date'), ColumnItem('From'), ColumnItem('To'), ColumnItem('Subject'), ColumnAttr('defects'), Column('payload', getter=lambda c,r: r.get_payload()), Column('content_type', getter=lambda c,r: r.get_content_type()), ] nKeys = 1 @asyncthread def reload(self): import imaplib import email.parser m = imaplib.IMAP4_SSL(host=self.source.hostname) user = self.source.username m.login(user, self.password) typ, folders = m.list() for r in Progress(folders, gerund="downloading"): fname = r.decode('utf-8').split()[-1][1:-1] try: m.select(fname) typ, data = m.search(None, 'ALL') for num in data[0].split(): typ, msgbytes = m.fetch(num, '(RFC822)') if typ != 'OK': vd.warning(typ, msgbytes) continue msg = email.message_from_bytes(msgbytes[0][1]) msg['folder'] = fname self.addRow(msg) m.close() except Exception: vd.exceptionCaught() m.logout() def addRow(self, row, **kwargs): if row.is_multipart(): for p in row.get_payload(): for hdr in 'message-id folder Date From To Subject'.split(): if hdr in row: p[hdr] = row[hdr] self.addRow(p, **kwargs) else: super().addRow(row, **kwargs) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608541899.0 visidata-2.2.1/visidata/loaders/json.py0000660000175000017500000001026100000000000020567 0ustar00kefalakefala00000000000000import json from collections import OrderedDict from visidata import * option('json_indent', None, 'indent to use when saving json') option('json_sort_keys', False, 'sort object keys when saving to json') option('default_colname', '', 'column name to use for non-dict rows') def open_jsonobj(p): return JsonSheet(p.name, source=p) def open_jsonl(p): return JsonSheet(p.name, source=p) open_ndjson = open_ldjson = open_json = open_jsonl class JsonSheet(PythonSheet): def iterload(self): self.colnames = {} # [colname] -> Column self.columns = [] with self.source.open_text() as fp: for L in fp: try: if L.startswith('#'): # skip commented lines continue ret = json.loads(L) if isinstance(ret, list): yield from Progress(ret) else: yield ret except ValueError as e: if self.rows: # if any rows have been added already e.stacktrace = stacktrace() yield TypedExceptionWrapper(json.loads, L, exception=e) # an error on one line else: with self.source.open_text() as fp: ret = json.load(fp) if isinstance(ret, list): yield from Progress(ret) else: yield ret break def addRow(self, row, index=None): # Wrap non-dict rows in a dummy object with a predictable key name. # This allows for more consistent handling of rows containing scalars # or lists. if not isinstance(row, dict): v = {options.default_colname: row} row = visidata.AlwaysDict(row, **v) super().addRow(row, index=index) for k in row: if k not in self.colnames: c = ColumnItem(k, type=deduceType(row[k])) self.colnames[k] = c self.addColumn(c) return row def newRow(self): return {} JsonLinesSheet=JsonSheet ## saving json and jsonl class Cell: def __init__(self, col, row): self.col = col self.row = row @property def value(cell): o = wrapply(cell.col.getTypedValue, cell.row) if isinstance(o, TypedExceptionWrapper): return options.safe_error or str(o.exception) elif isinstance(o, TypedWrapper): return o.val elif isinstance(o, date): return cell.col.getDisplayValue(cell.row) return o class _vjsonEncoder(json.JSONEncoder): def __init__(self, **kwargs): super().__init__(sort_keys=options.json_sort_keys, **kwargs) self.safe_error = options.safe_error def default(self, obj): return obj.value if isinstance(obj, Cell) else str(obj) def _rowdict(cols, row): ret = {} for c in cols: cell = Cell(c, row) if cell.value is not None: ret[c.name] = cell return ret @VisiData.api def save_json(vd, p, *vsheets): with p.open_text(mode='w') as fp: if len(vsheets) == 1: vs = vsheets[0] it = [_rowdict(vs.visibleCols, row) for row in vs.iterrows()] else: it = {vs.name: [_rowdict(vs.visibleCols, row) for row in vs.iterrows()] for vs in vsheets} try: indent = int(options.json_indent) except Exception: indent = options.json_indent jsonenc = _vjsonEncoder(indent=indent) with Progress(gerund='saving'): for chunk in jsonenc.iterencode(it): fp.write(chunk) @VisiData.api def save_jsonl(vd, p, *vsheets): with p.open_text(mode='w') as fp: for vs in vsheets: vcols = vs.visibleCols jsonenc = _vjsonEncoder() with Progress(gerund='saving'): for row in vs.iterrows(): rowdict = _rowdict(vcols, row) fp.write(jsonenc.encode(rowdict) + '\n') VisiData.save_ndjson = VisiData.save_jsonl VisiData.save_ldjson = VisiData.save_jsonl ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608541899.0 visidata-2.2.1/visidata/loaders/markdown.py0000660000175000017500000000314000000000000021436 0ustar00kefalakefala00000000000000from visidata import * def markdown_escape(s, style='orgmode'): if style == 'jira': return s ret = '' for ch in s: if ch in '\\`*_{}[]()>#+-.!': ret += '\\'+ch else: ret += ch return ret def markdown_colhdr(col): if vd.isNumeric(col): return ('-' * (col.width-1)) + ':' else: return '-' * (col.width or options.default_width) def write_md(p, *vsheets, md_style='orgmode'): 'pipe tables compatible with org-mode' if md_style == 'jira': delim = '||' else: delim = '|' with p.open_text(mode='w') as fp: for vs in vsheets: if len(vsheets) > 1: fp.write('# %s\n\n' % vs.name) fp.write(delim + delim.join('%-*s' % (col.width or options.default_width, markdown_escape(col.name, md_style)) for col in vs.visibleCols) + '|\n') if md_style == 'orgmode': fp.write('|' + '|'.join(markdown_colhdr(col) for col in vs.visibleCols) + '|\n') with Progress(gerund='saving'): for dispvals in vs.iterdispvals(format=True): s = '|' for col, val in dispvals.items(): s += '%-*s|' % (col.width or options.default_width, markdown_escape(val, md_style)) s += '\n' fp.write(s) fp.write('\n') vd.status('%s save finished' % p) @VisiData.api def save_md(vd, p, *sheets): write_md(p, *sheets, md_style='orgmode') @VisiData.api def save_jira(vd, p, *sheets): write_md(p, *sheets, md_style='jira') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608541899.0 visidata-2.2.1/visidata/loaders/mbtiles.py0000660000175000017500000001071200000000000021256 0ustar00kefalakefala00000000000000from visidata import * import json import gzip import sqlite3 def open_pbf(p): return PbfSheet(p.name, source=p) def open_mbtiles(p): return MbtilesSheet(p.name, source=p) def getListDepth(L): if not isinstance(L, list): return 0 if len(L) == 0: return 0 return getListDepth(L[0]) + 1 def getFeatures(tile_data): for layername, layer in tile_data.items(): for feat in layer['features']: yield layername, feat def tilename(row): return ",".join(str(x) for x in row) class MbtilesSheet(Sheet): columns = [ ColumnItem('zoom_level', 0), ColumnItem('tile_column', 1), ColumnItem('tile_row', 2), ] def getTile(self, zoom_level, tile_col, tile_row): import mapbox_vector_tile con = sqlite3.connect(str(self.source)) tile_data = con.execute(''' SELECT tile_data FROM tiles WHERE zoom_level = ? AND tile_column = ? AND tile_row = ?''', (zoom_level, tile_col, tile_row)).fetchone()[0] return mapbox_vector_tile.decode(gzip.decompress(tile_data)) def iterload(self): con = sqlite3.connect(str(self.source)) self.metadata = dict(con.execute('SELECT name, value FROM metadata').fetchall()) tiles = con.execute('SELECT zoom_level, tile_column, tile_row FROM tiles') yield from Progress(tiles.fetchall()) def getPlot(self, *rows): if len(rows) == 1: name = self.name+'_'+tilename(rows[0]) else: name = self.name+'_selected' sourceRows = sum((list(getFeatures(self.getTile(*r))) for r in rows), []) return PbfCanvas(name+"_map", source=PbfSheet(name, source=self), sourceRows=sourceRows) def openRow(self, row): 'load table referenced in current row into memory' return PbfSheet(tilename(row), source=self, sourceRow=row) class PbfSheet(Sheet): columns = [ ColumnItem('layer', 0), Column('geometry_type', getter=lambda col,row: row[1]['geometry']['type']), Column('geometry_coords', getter=lambda col,row: row[1]['geometry']['coordinates'], width=0), Column('geometry_coords_depth', getter=lambda col,row: getListDepth(row[1]['geometry']['coordinates']), width=0), ] nKeys = 1 # layer def iterload(self): props = set() # property names for r in getFeatures(self.source.getTile(*self.sourceRow)): yield r props.update(r[1]['properties'].keys()) for key in props: self.addColumn(Column(key, getter=lambda col,row,key=key: row[1]['properties'][key])) class PbfCanvas(InvertedCanvas): aspectRatio = 1.0 def iterpolylines(self, r): layername, feat = r geom = feat['geometry'] t = geom['type'] coords = geom['coordinates'] key = self.source.rowkey(r) if t == 'LineString': yield coords, self.plotColor(key), r elif t == 'Point': yield [coords], self.plotColor(key), r elif t == 'Polygon': for poly in coords: yield poly+[poly[0]], self.plotColor(key), r elif t == 'MultiLineString': for line in coords: yield line, self.plotColor(key), r elif t == 'MultiPolygon': for mpoly in coords: for poly in mpoly: yield poly+[poly[0]], self.plotColor(key), r else: vd.warning('unknown geometry type %s' % t) @asyncthread def reload(self): self.reset() for r in Progress(self.sourceRows): for vertexes, attr, row in self.iterpolylines(r): self.polyline(vertexes, attr, row) if len(vertexes) == 1: textx, texty = vertexes[0] disptext = self.textCol.getDisplayValue(row) if disptext: self.label(textx, texty, disptext, attr, row) self.refresh() PbfSheet.addCommand('.', 'plot-row', 'vd.push(PbfCanvas(name+"_map", source=sheet, sourceRows=[cursorRow], textCol=cursorCol))', 'plot blocks in current row') PbfSheet.addCommand('g.', 'plot-rows', 'vd.push(PbfCanvas(name+"_map", source=sheet, sourceRows=rows, textCol=cursorCol))', 'plot selected blocks') MbtilesSheet.addCommand('.', 'plot-row', 'vd.push(getPlot(cursorRow))', 'plot tiles in current row') MbtilesSheet.addCommand('g.', 'plot-selected', 'vd.push(getPlot(*selectedRows))', 'plot selected tiles'), ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612075960.0 visidata-2.2.1/visidata/loaders/mysql.py0000660000175000017500000000725600000000000020775 0ustar00kefalakefala00000000000000from visidata import * from contextlib import contextmanager __all__ = ['openurl_mysql', 'MyTable', 'MyTablesSheet'] def codeToType(type_code, colname): import MySQLdb as mysql types = mysql.constants.FIELD_TYPE if type_code in (types.TINY, types.SHORT, types.LONG, types.LONGLONG, types.INT24,): return int if type_code in (types.FLOAT, types.DOUBLE, types.DECIMAL, types.NEWDECIMAL,): return float if type_code == mysql.STRING: return str return anytype def openurl_mysql(url, filetype=None): url = urlparse(url.given) dbname = url.path[1:] return MyTablesSheet(dbname+"_tables", sql=SQL(url), schema=dbname) class SQL: def __init__(self, url): self.url = url @contextmanager def cur(self, qstr): import MySQLdb as mysql import MySQLdb.cursors as cursors dbname = self.url.path[1:] connection = mysql.connect( user=self.url.username, database=self.url.path[1:], host=self.url.hostname, port=self.url.port or 3306, password=self.url.password, use_unicode=True, charset='utf8', cursorclass=cursors.SSCursor) ## if SSCursor is not used mysql will first fetch ALL data, and only then visualize it try: cursor = connection.cursor() # one connection per request as SSCursor only allows to fetch data asynchronously from one query at a time cursor.execute(qstr) with cursor as c: yield c finally: cursor.close() connection.close() @asyncthread def query_async(self, qstr, callback=None): with self.cur(qstr) as cur: callback(cur) def cursorToColumns(cur, sheet): sheet.columns = [] for i, coldesc in enumerate(cur.description): name, type, *_ = coldesc sheet.addColumn(ColumnItem(name, i, type=codeToType(type, name))) # rowdef: (table_name, ncols) class MyTablesSheet(Sheet): rowtype = 'tables' def reload(self): qstr = f''' select t.table_name, column_count.ncols, t.table_rows as est_nrows from information_schema.tables t, ( select table_name, count(column_name) as ncols from information_schema.columns where table_schema = '{self.schema}' group by table_name ) as column_count where t.table_name = column_count.table_name AND t.table_schema = '{self.schema}'; ''' with self.sql.cur(qstr) as cur: self.rows = [] # try to get first row to make cur.description available r = cur.fetchone() if r: self.addRow(r) cursorToColumns(cur, self) self.setKeys(self.columns[0:1]) # table_name is the key for r in cur: self.addRow(r) def openRow(self, row): return MyTable(self.name+"."+row[0], source=row[0], sql=self.sql) # rowdef: tuple of values as returned by fetchone() class MyTable(Sheet): @asyncthread def reload(self): with self.sql.cur("SELECT * FROM " + self.source) as cur: self.rows = [] r = cur.fetchone() if r: self.addRow(r) cursorToColumns(cur, self) for r in cur: self.addRow(r) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612075960.0 visidata-2.2.1/visidata/loaders/npy.py0000660000175000017500000000510200000000000020422 0ustar00kefalakefala00000000000000from visidata import * 'Loaders for .npy and .npz. Save to .npy. Depends on the zip loader.' def open_npy(p): return NpySheet(p.name, source=p) def open_npz(p): return NpzSheet(p.name, source=p) class NpySheet(Sheet): def iterload(self): import numpy if not hasattr(self, 'npy'): self.npy = numpy.load(str(self.source), encoding='bytes') self.reloadCols() yield from Progress(self.npy, total=len(self.npy)) def reloadCols(self): self.columns = [] for i, (name, fmt, *shape) in enumerate(self.npy.dtype.descr): if shape: t = anytype elif 'M' in fmt: self.addColumn(Column(name, type=date, getter=lambda c,r,i=i: str(r[i]))) continue elif 'i' in fmt: t = int elif 'f' in fmt: t = float else: t = anytype self.addColumn(ColumnItem(name, i, type=t)) class NpzSheet(ZipSheet): # rowdef: tuple(tablename, table) columns = [ ColumnItem('name', 0), ColumnItem('length', 1, type=vlen), ] def iterload(self): import numpy self.npz = numpy.load(str(self.source), encoding='bytes') yield from Progress(self.npz.items()) def openRow(self, row): import numpy tablename, tbl = row if isinstance(tbl, numpy.ndarray): return NpySheet(tablename, npy=tbl) return PyobjSheet(tablename, source=tbl) @VisiData.api def save_npy(vd, p, sheet): import numpy as np dtype = [] for col in Progress(sheet.visibleCols): if col.type in (int, vlen): dt = 'i8' elif col.type in (float, currency, floatlocale): dt = 'f8' elif col.type is date: dt = 'datetime64[s]' else: # if col.type in (str, anytype): width = col.getMaxWidth(sheet.rows) dt = 'U'+str(width) dtype.append((col.name, dt)) data = [] for typedvals in sheet.iterdispvals(format=False): nprow = [] for col, val in typedvals.items(): if isinstance(val, TypedWrapper): if col.type is anytype: val = '' else: val = options.safe_error elif col.type is date: val = np.datetime64(val.isoformat()) nprow.append(val) data.append(tuple(nprow)) arr = np.array(data, dtype=dtype) with p.open_bytes(mode='w') as outf: np.save(outf, arr, allow_pickle=False) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612075960.0 visidata-2.2.1/visidata/loaders/pandas_freqtbl.py0000660000175000017500000002102100000000000022577 0ustar00kefalakefala00000000000000import math import collections from visidata import * class DataFrameRowSliceAdapter: """Tracks original dataframe and a boolean row mask This is a workaround to (1) save memory (2) keep id(row) consistent when iterating, as id() is used significantly by visidata's selectRow implementation. """ def __init__(self, df, mask): import pandas as pd import numpy as np if not isinstance(df, pd.DataFrame): vd.fail('%s is not a dataframe' % type(df).__name__) if not isinstance(mask, pd.Series): vd.fail('mask %s is not a Series' % type(mask).__name__) if df.shape[0] != mask.shape[0]: vd.fail('dataframe and mask have different shapes (%s vs %s)' % (df.shape[0], mask.shape[0])) self.df = df self.mask_bool = mask # boolean mask self.mask_iloc = np.where(mask.values)[0] # integer indexes corresponding to mask self.mask_count = mask.sum() def __len__(self): return self.mask_count def __getitem__(self, k): if isinstance(k, slice): import pandas as pd new_mask = pd.Series(False, index=self.df.index) new_mask.iloc[self.mask_iloc[k]] = True return DataFrameRowSliceAdapter(self.df, new_mask) return self.df.iloc[self.mask_iloc[k]] def __iter__(self): # With the internal selection API used by PandasSheet, # this should no longer be needed and can be replaced by # DataFrameAdapter(self.df[self.mask_iloc]) return DataFrameRowSliceIter(self.df, self.mask_iloc) def __getattr__(self, k): # This is trouble .. return getattr(self.df[self.mask_bool], k) class DataFrameRowSliceIter: def __init__(self, df, mask_iloc, index=0): self.df = df self.mask_iloc = mask_iloc self.index = index def __next__(self): # Accessing row of original dataframe, to ensure # that no copies are made and id() of selected rows # will match original dataframe's rows if self.index >= self.mask_iloc.shape[0]: raise StopIteration() row = self.df.iloc[self.mask_iloc[self.index]] self.index += 1 return row class PandasFreqTableSheet(PivotSheet): 'Generate frequency-table sheet on currently selected column.' rowtype = 'bins' # rowdef FreqRow(keys, sourcerows) def __init__(self, sheet, *groupByCols): fqcolname = '%s_%s_freq' % (sheet.name, '-'.join(col.name for col in groupByCols)) super().__init__(fqcolname, groupByCols, [], source=sheet) self.largest = 1 def selectRow(self, row): # Select all entries in the bin on the source sheet. # Use the internally defined _selectByLoc to avoid # looping which causes a significant performance hit. self.source._selectByILoc(row.sourcerows.mask_iloc, selected=True) # then select the bin itself on this sheet return super().selectRow(row) def unselectRow(self, row): self.source._selectByILoc(row.sourcerows.mask_iloc, selected=False) return super().unselectRow(row) def updateLargest(self, grouprow): self.largest = max(self.largest, len(grouprow.sourcerows)) @asyncthread def reload(self): 'Generate frequency table then reverse-sort by length.' import pandas as pd # Note: visidata's base FrequencyTable bins numeric data in ranges # (e.g. as a histogram). We currently don't provide support for this # for PandasSheet, although we could implement it with a pd.Grouper # that operates similarly to pd.cut. super().initCols() df = self.source.df.copy() # Implementation (special case): for one row, this degenerates # to .value_counts(); however this does not order in a stable manner. # if len(self.groupByCols) == 1: # this_column = df.loc[:, str(self.groupByCols[0].name)] # value_counts = this_column.value_counts() if len(self.groupByCols) >= 1: # Implementation (1): add a dummy column to aggregate over in a pd.pivot_table. # Is there a way to avoid having to mutate the dataframe? We can delete the # column afterwards but we do incur the overhead of block consolidation. _pivot_count_column = "__vd_pivot_count" if _pivot_count_column not in df.columns: df[_pivot_count_column] = 1 # Aggregate count over columns to group, and then apply a stable sort value_counts = df.pivot_table( index=[c.name for c in self.groupByCols], values=_pivot_count_column, aggfunc="count" )[_pivot_count_column].sort_values(ascending=False, kind="mergesort") # TODO: it seems that the ascending=False causes this to do a "reversed stable sort"? # TODO: possibly register something to delete this column as soon as # we exit visidata? # del df["__vd_pivot_count"] # Implementation (2) which does not require adding a dummy column: # Compute cross-tabulation to get counts, and sort/remove zero-entries. # Note that this is not space-efficient: the initial cross-tabulation will # have space on the order of product of number of unique elements for each # column, even though its possible the combinations present are sparse # and most combinations have zero count. # this_column = df.loc[:, str(self.groupByCols[0].name)] # value_counts = pd.crosstab(this_column, [df.df[c.name] for c in self.groupByCols[1:]]) # value_counts = value_counts.stack(list(range(len(self.groupByCols) - 1))) # value_counts = value_counts.loc[value_counts > 0].sort_values(ascending=False) else: vd.fail("Unable to do FrequencyTable, no columns to group on provided") # add default bonus columns for c in [ Column('count', type=int, getter=lambda col,row: len(row.sourcerows)), Column('percent', type=float, getter=lambda col,row: len(row.sourcerows)*100/df.shape[0]), Column('histogram', type=str, getter=lambda col,row: options.disp_histogram*(options.disp_histolen*len(row.sourcerows)//value_counts.max()), width=options.disp_histolen+2), ]: self.addColumn(c) for element in Progress(value_counts.index): if len(self.groupByCols) == 1: element = (element,) elif len(element) != len(self.groupByCols): vd.fail('different number of index cols and groupby cols (%s vs %s)' % (len(element), len(self.groupByCols))) mask = df[self.groupByCols[0].name] == element[0] for i in range(1, len(self.groupByCols)): mask = mask & (df[self.groupByCols[i].name] == element[i]) self.addRow(PivotGroupRow( element, (0, 0), DataFrameRowSliceAdapter(df, mask), {} )) def expand_source_rows(source, vd, cursorRow): """Support for expanding a row of frequency table to underlying rows""" if cursorRow.sourcerows is None: vd.error("no source rows") vs = PandasSheet(source.name, valueNames(cursorRow.discrete_keys, cursorRow.numeric_key), source=cursorRow.sourcerows) vd.push(vs) PandasSheet.addCommand('F', 'freq-col', 'vd.push(PandasFreqTableSheet(sheet, cursorCol))', 'open Frequency Table grouped on current column, with aggregations of other columns') PandasSheet.addCommand('gF', 'freq-keys', 'vd.push(PandasFreqTableSheet(sheet, *keyCols))', 'open Frequency Table grouped by all key columns on source sheet, with aggregations of other columns') PandasFreqTableSheet.addCommand('t', 'stoggle-row', 'toggle([cursorRow]); cursorDown(1)', 'toggle selection of rows grouped in current row in source sheet') PandasFreqTableSheet.addCommand('s', 'select-row', 'select([cursorRow]); cursorDown(1)', 'select rows grouped in current row in source sheet') PandasFreqTableSheet.addCommand('u', 'unselect-row', 'unselect([cursorRow]); cursorDown(1)', 'unselect rows grouped in current row in source sheet') PandasFreqTableSheet.addCommand(ENTER, 'open-row', 'expand_source_rows(source, vd, cursorRow)', 'open copy of source sheet with rows that are grouped in current row') PandasFreqTableSheet.class_options.numeric_binning = False ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612684761.0 visidata-2.2.1/visidata/loaders/pcap.py0000660000175000017500000003272000000000000020545 0ustar00kefalakefala00000000000000import collections import ipaddress from visidata import * option('pcap_internet', 'n', '(y/s/n) if save_dot includes all internet hosts separately (y), combined (s), or does not include the internet (n)') protocols = collections.defaultdict(dict) # ['ethernet'] = {[6] -> 'IP'} _flags = collections.defaultdict(dict) # ['tcp'] = {[4] -> 'FIN'} url_oui = 'https://visidata.org/plugins/pcap/wireshark-oui.tsv' url_iana = 'https://visidata.org/plugins/pcap/iana-ports.tsv' oui = {} # [macprefix (like '01:02:dd:0')] -> 'manufacturer' services = {} # [('tcp', 25)] -> 'smtp' def open_pcap(p): return PcapSheet(p.name, source=p) open_cap = open_pcap open_pcapng = open_pcap open_ntar = open_pcap def manuf(mac): return oui.get(mac[:13]) or oui.get(mac[:10]) or oui.get(mac[:8]) def macaddr(addrbytes): mac = ':'.join('%02x' % b for b in addrbytes) return mac def macmanuf(mac): manuf = oui.get(mac[:13]) if manuf: return manuf + mac[13:] manuf = oui.get(mac[:10]) if manuf: return manuf + mac[10:] manuf = oui.get(mac[:8]) if manuf: return manuf + mac[8:] return mac def norm_host(host): if not host: return None srcmac = str(host.macaddr) if srcmac == 'ff:ff:ff:ff:ff:ff': return None srcip = str(host.ipaddr) if srcip == '0.0.0.0' or srcip == '::': return None if srcip == '255.255.255.255': return None if host.ipaddr: if host.ipaddr.is_global: opt = options.pcap_internet if opt == 'n': return None elif opt == 's': return "internet" if host.ipaddr.is_multicast: # include in multicast (minus dns?) return 'multicast' names = [host.hostname, host.ipaddr, macmanuf(host.macaddr)] return '\\n'.join(str(x) for x in names if x) def FlagGetter(flagfield): def flags_func(fl): return ' '.join([flagname for f, flagname in _flags[flagfield].items() if fl & f]) return flags_func def init_pcap(): if protocols: # already init'ed return global dpkt, dnslib import dpkt import dnslib load_consts(protocols['ethernet'], dpkt.ethernet, 'ETH_TYPE_') load_consts(protocols['ip'], dpkt.ip, 'IP_PROTO_') load_consts(_flags['ip_tos'], dpkt.ip, 'IP_TOS_') load_consts(protocols['icmp'], dpkt.icmp, 'ICMP_') load_consts(_flags['tcp'], dpkt.tcp, 'TH_') load_oui(url_oui) load_iana(url_iana) def read_pcap(f): try: return dpkt.pcapng.Reader(f.open_bytes()) except ValueError: return dpkt.pcap.Reader(f.open_bytes()) @asyncthread def load_oui(url): vsoui = TsvSheet('vsoui', source=urlcache(url, days=30)) vsoui.reload.__wrapped__(vsoui) for r in vsoui.rows: if r.prefix.endswith('/36'): prefix = r.prefix[:13] elif r.prefix.endswith('/28'): prefix = r.prefix[:10] else: prefix = r.prefix[:8] try: oui[prefix.lower()] = r.shortname except Exception as e: vd.exceptionCaught(e) @asyncthread def load_iana(url): ports_tsv = TsvSheet('ports_tsv', source=urlcache(url, days=30)) ports_tsv.reload.__wrapped__(ports_tsv) for r in ports_tsv.rows: try: services[(r.transport, int(r.port))] = r.service except Exception as e: vd.exceptionCaught(e) class Host: dns = {} # [ipstr] -> dnsname hosts = {} # [macaddr] -> { [ipaddr] -> Host } @classmethod def get_host(cls, pkt, field='src'): mac = macaddr(getattr(pkt, field)) machosts = cls.hosts.get(mac, None) if not machosts: machosts = cls.hosts[mac] = {} ipraw = getattrdeep(pkt, 'ip.'+field, None) if ipraw is not None: ip = ipaddress.ip_address(ipraw) if ip not in machosts: machosts[ip] = Host(mac, ip) return machosts[ip] else: if machosts: return list(machosts.values())[0] return Host(mac, None) @classmethod def get_by_ip(cls, ip): 'Returns Host instance for the given ip address.' ret = cls.hosts_by_ip.get(ip) if ret is None: ret = cls.hosts_by_ip[ip] = [Host(ip)] return ret def __init__(self, mac, ip): self.ipaddr = ip self.macaddr = mac self.mac_manuf = None def __str__(self): return str(self.hostname or self.ipaddr or macmanuf(self.macaddr)) def __lt__(self, x): if isinstance(x, Host): return str(self.ipaddr) < str(x.ipaddr) return True @property def hostname(self): return Host.dns.get(str(self.ipaddr)) def load_consts(outdict, module, attrprefix): for k in dir(module): if k.startswith(attrprefix): v = getattr(module, k) outdict[v] = k[len(attrprefix):] def getTuple(pkt): if getattrdeep(pkt, 'ip.tcp', None): tup = ('tcp', Host.get_host(pkt, 'src'), pkt.ip.tcp.sport, Host.get_host(pkt, 'dst'), pkt.ip.tcp.dport) elif getattrdeep(pkt, 'ip.udp', None): tup = ('udp', Host.get_host(pkt, 'src'), pkt.ip.udp.sport, Host.get_host(pkt, 'dst'), pkt.ip.udp.dport) else: return None a,b,c,d,e = tup if b > d: return a,d,e,b,c # swap src/sport and dst/dport else: return tup def getService(tup): if not tup: return transport, _, sport, _, dport = tup if (transport, dport) in services: return services.get((transport, dport)) if (transport, sport) in services: return services.get((transport, sport)) def get_transport(pkt): ret = 'ether' if getattr(pkt, 'arp', None): return 'arp' if getattr(pkt, 'ip', None): ret = 'ip' if getattr(pkt.ip, 'tcp', None): ret = 'tcp' elif getattr(pkt.ip, 'udp', None): ret = 'udp' elif getattr(pkt.ip, 'icmp', None): ret = 'icmp' if getattr(pkt, 'ip6', None): ret = 'ipv6' if getattr(pkt.ip6, 'tcp', None): ret = 'tcp' elif getattr(pkt.ip6, 'udp', None): ret = 'udp' elif getattr(pkt.ip6, 'icmp6', None): ret = 'icmpv6' return ret def get_port(pkt, field='sport'): return getattrdeep(pkt, 'ip.tcp.'+field, None) or getattrdeep(pkt, 'ip.udp.'+field, None) class EtherSheet(Sheet): 'Layer 2 (ethernet) packets' rowtype = 'packets' columns = [ ColumnAttr('timestamp', type=date, fmtstr="%H:%M:%S.%f"), Column('ether_manuf', type=str, getter=lambda col,row: mac_manuf(macaddr(row.src))), Column('ether_src', type=str, getter=lambda col,row: macaddr(row.src), width=6), Column('ether_dst', type=str, getter=lambda col,row: macaddr(row.dst), width=6), ColumnAttr('ether_data', 'data', type=vlen, width=0), ] class IPSheet(Sheet): rowtype = 'packets' columns = [ ColumnAttr('timestamp', type=date, fmtstr="%H:%M:%S.%f"), ColumnAttr('ip', type=str, width=0), Column('ip_src', type=str, width=14, getter=lambda col,row: ipaddress.ip_address(row.ip.src)), Column('ip_dst', type=str, width=14, getter=lambda col,row: ipaddress.ip_address(row.ip.dst)), ColumnAttr('ip_hdrlen', 'ip.hl', type=int, width=0, helpstr="IPv4 Header Length"), ColumnAttr('ip_proto', 'ip.p', type=lambda v: protocols['ip'].get(v), width=8, helpstr="IPv4 Protocol"), ColumnAttr('ip_id', 'ip.id', type=int, width=10, helpstr="IPv4 Identification"), ColumnAttr('ip_rf', 'ip.rf', type=int, width=10, helpstr="IPv4 Reserved Flag (Evil Bit)"), ColumnAttr('ip_df', 'ip.df', type=int, width=10, helpstr="IPv4 Don't Fragment flag"), ColumnAttr('ip_mf', 'ip.mf', type=int, width=10, helpstr="IPv4 More Fragments flag"), ColumnAttr('ip_tos', 'ip.tos', width=10, type=FlagGetter('ip_tos'), helpstr="IPv4 Type of Service"), ColumnAttr('ip_ttl', 'ip.ttl', type=int, width=10, helpstr="IPv4 Time To Live"), ColumnAttr('ip_ver', 'ip.v', type=int, width=10, helpstr="IPv4 Version"), ] def iterload(self): for pkt in Progress(self.source.rows): if getattr(pkt, 'ip', None): yield pkt class TCPSheet(IPSheet): columns = IPSheet.columns + [ ColumnAttr('tcp_srcport', 'ip.tcp.sport', type=int, width=8, helpstr="TCP Source Port"), ColumnAttr('tcp_dstport', 'ip.tcp.dport', type=int, width=8, helpstr="TCP Dest Port"), ColumnAttr('tcp_opts', 'ip.tcp.opts', width=0), ColumnAttr('tcp_flags', 'ip.tcp.flags', type=FlagGetter('tcp'), helpstr="TCP Flags"), ] def iterload(self): for pkt in Progress(self.source.rows): if getattrdeep(pkt, 'ip.tcp', None): yield pkt class UDPSheet(IPSheet): columns = IPSheet.columns + [ ColumnAttr('udp_srcport', 'ip.udp.sport', type=int, width=8, helpstr="UDP Source Port"), ColumnAttr('udp_dstport', 'ip.udp.dport', type=int, width=8, helpstr="UDP Dest Port"), ColumnAttr('ip.udp.data', type=vlen, width=0), ColumnAttr('ip.udp.ulen', type=int, width=0), ] def iterload(self): for pkt in Progress(self.source.rows): if getattrdeep(pkt, 'ip.udp', None): yield pkt class PcapSheet(Sheet): rowtype = 'packets' columns = [ ColumnAttr('timestamp', type=date, fmtstr="%H:%M:%S.%f"), Column('transport', type=get_transport, width=5), Column('srcmanuf', type=str, getter=lambda col,row: manuf(macaddr(row.src))), Column('srchost', type=str, getter=lambda col,row: row.srchost), Column('srcport', type=int, getter=lambda col,row: get_port(row, 'sport')), Column('dstmanuf', type=str, getter=lambda col,row: manuf(macaddr(row.dst))), Column('dsthost', type=str, getter=lambda col,row: row.dsthost), Column('dstport', type=int, getter=lambda col,row: get_port(row, 'dport')), ColumnAttr('ether_proto', 'type', type=lambda v: protocols['ethernet'].get(v), width=0), ColumnAttr('tcp_flags', 'ip.tcp.flags', type=FlagGetter('tcp'), helpstr="TCP Flags"), Column('service', type=str, getter=lambda col,row: getService(getTuple(row))), ColumnAttr('data', type=vlen), ColumnAttr('ip_len', 'ip.len', type=int), ColumnAttr('tcp', 'ip.tcp', width=4, type=vlen), ColumnAttr('udp', 'ip.udp', width=4, type=vlen), ColumnAttr('icmp', 'ip.icmp', width=4, type=vlen), ColumnAttr('dns', type=str, width=4), ] def iterload(self): init_pcap() self.pcap = read_pcap(self.source) self.rows = [] with Progress(total=filesize(self.source)) as prog: for ts, buf in self.pcap: eth = dpkt.ethernet.Ethernet(buf) yield eth prog.addProgress(len(buf)) eth.timestamp = ts if not getattr(eth, 'ip', None): eth.ip = getattr(eth, 'ip6', None) eth.dns = try_apply(lambda eth: dnslib.DNSRecord.parse(eth.ip.udp.data), eth) if eth.dns: for rr in eth.dns.rr: Host.dns[str(rr.rdata)] = str(rr.rname) eth.srchost = Host.get_host(eth, 'src') eth.dsthost = Host.get_host(eth, 'dst') flowtype = collections.namedtuple('flow', 'packets transport src sport dst dport'.split()) class PcapFlowsSheet(Sheet): rowtype = 'netflows' # rowdef: flowtype _rowtype = flowtype columns = [ ColumnAttr('transport', type=str), Column('src', type=str, getter=lambda col,row: row.src), ColumnAttr('sport', type=int), Column('dst', type=str, getter=lambda col,row: row.dst), ColumnAttr('dport', type=int), Column('service', type=str, width=8, getter=lambda col,row: getService(getTuple(row.packets[0]))), ColumnAttr('packets', type=vlen), Column('connect_latency_ms', type=float, getter=lambda col,row: col.sheet.latency[getTuple(row.packets[0])]), ] def iterload(self): self.flows = {} self.latency = {} # [flowtuple] -> float ms of latency self.syntimes = {} # [flowtuple] -> timestamp of SYN flags = FlagGetter('tcp') for pkt in Progress(self.source.rows): tup = getTuple(pkt) if tup: flowpkts = self.flows.get(tup) if flowpkts is None: flowpkts = self.flows[tup] = [] yield flowtype(flowpkts, *tup) flowpkts.append(pkt) if not getattr(pkt.ip, 'tcp', None): continue tcpfl = flags(pkt.ip.tcp.flags) if 'SYN' in tcpfl: if 'ACK' in tcpfl: if tup in self.syntimes: self.latency[tup] = (pkt.timestamp - self.syntimes[tup])*1000 else: self.syntimes[tup] = pkt.timestamp def openRow(self, row): return PcapSheet("%s_packets"%flowname(row), rows=row.packets) def flowname(flow): return '%s_%s:%s-%s:%s' % (flow.transport, flow.src, flow.sport, flow.dst, flow.dport) def try_apply(func, *args, **kwargs): try: return func(*args, **kwargs) except Exception as e: pass PcapSheet.addCommand('W', 'flows', 'vd.push(PcapFlowsSheet(sheet.name+"_flows", source=sheet))') PcapSheet.addCommand('2', 'l2-packet', 'vd.push(IPSheet("L2packets", source=sheet))') PcapSheet.addCommand('3', 'l3-packet', 'vd.push(TCPSheet("L3packets", source=sheet))') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608541899.0 visidata-2.2.1/visidata/loaders/pdf.py0000660000175000017500000000302000000000000020362 0ustar00kefalakefala00000000000000import io from visidata import * vd.option('pdf_tables', False, 'parse PDF for tables instead of pages of text', replay=True) def open_pdf(p): if vd.options.pdf_tables: return TabulaSheet(p.name, source=p) return PdfMinerSheet(p.name, source=p) class PdfMinerSheet(TableSheet): rowtype='pages' # rowdef: [pdfminer.LTPage, pageid, text] columns=[ ColumnItem('pdfpage', 0, width=0), ColumnItem('pagenum', 1, type=int), ColumnItem('contents', 2), ] def iterload(self): import pdfminer.high_level from pdfminer.pdfinterp import PDFResourceManager, PDFPageInterpreter from pdfminer.converter import TextConverter, PDFPageAggregator from pdfminer.layout import LAParams from pdfminer.pdfpage import PDFPage with self.source.open_bytes() as fp: for page in PDFPage.get_pages(fp): with io.StringIO() as output_string: newrsrcmgr = PDFResourceManager() txtconv = TextConverter(newrsrcmgr, output_string, codec=options.encoding, laparams=LAParams()) interpreter = PDFPageInterpreter(newrsrcmgr, txtconv) interpreter.process_page(page) yield [page, page.pageid, output_string.getvalue()] class TabulaSheet(IndexSheet): def iterload(self): import tabula for i, t in enumerate(tabula.read_pdf(self.source, pages='all', multiple_tables=True)): yield PandasSheet(self.source.name, i, source=t) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608178530.0 visidata-2.2.1/visidata/loaders/png.py0000660000175000017500000000532600000000000020410 0ustar00kefalakefala00000000000000from visidata import * def open_png(p): return PNGSheet(p.name, source=p) @functools.lru_cache(256) def rgb_to_attr(r,g,b,a): if a == 0: return 0 if r > g and r > b: return colors['red'] if g > r and g > b: return colors['green'] if b > r and b > g: return colors['blue'] if a == 255: return colors['white'] return 0 class PNGSheet(Sheet): rowtype = 'pixels' # rowdef: tuple(x, y, r, g, b, a) columns = [ColumnItem(name, i, type=int) for i, name in enumerate('x y R G B A'.split())] + [ Column('attr', type=int, getter=lambda col,row: rgb_to_attr(*row[2:])) ] nKeys = 2 def newRow(self): return list((None, None, 0, 0, 0, 0)) def iterload(self): import png r = png.Reader(bytes=self.source.read_bytes()) self.width, self.height, pixels, md = r.asRGBA() for y, row in enumerate(pixels): for i in range(0, len(row)-1, 4): r,g,b,a = row[i:i+4] yield [i//4, y, r, g, b, a] class PNGDrawing(Canvas): aspectRatio = 1.0 rowtype = 'pixels' def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def togglePixel(self, rows): for row in rows: x,y,r,g,b,a = row self.pixels[y][x][rgb_to_attr(r,g,b,a)].remove(row) row[5] = a = 0 if row[5] else 255 self.plotpixel(x, y, rgb_to_attr(r,g,b,a), row) def setPixel(self, rows, attr): for row in rows: x,y,r,g,b,a = row self.pixels[y][x][rgb_to_attr(r,g,b,a)].remove(row) row[5] = a = attr self.plotpixel(x, y, rgb_to_attr(r,g,b,a), row) @asyncthread def reload(self): self.reset() for row in self.sourceRows: x, y, r, g, b, a = row self.point(x, y, rgb_to_attr(r,g,b,a), row) self.refresh() @VisiData.api def save_png(vd, p, vs): if isinstance(vs, Canvas): return save_png(p, vs.source) palette = collections.OrderedDict() palette[(0,0,0,0)] = 0 # invisible black is 0 pixels = list([0]*vs.width for y in range(vs.height)) for x,y,r,g,b,a in Progress(sorted(vs.rows), 'saving'): color = tuple((r,g,b,a)) colornum = palette.get(color, None) if colornum is None: colornum = palette[color] = len(palette) pixels[y][x] = colornum vd.status('saving %sx%sx%s' % (vs.width, vs.height, len(palette))) import png with open(p, 'wb') as fp: w = png.Writer(vs.width, vs.height, palette=list(palette.keys())) w.write(fp, pixels) vd.status('saved') PNGSheet.addCommand('.', 'plot-sheet', 'vd.push(PNGDrawing(name+"_plot", source=sheet, sourceRows=rows))', 'plot this png') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1609740788.0 visidata-2.2.1/visidata/loaders/postgres.py0000660000175000017500000000732300000000000021471 0ustar00kefalakefala00000000000000from visidata import * __all__ = ['openurl_postgres', 'openurl_rds', 'PgTable', 'PgTablesSheet'] option('postgres_schema', 'public', 'The desired schema for the Postgres database') def codeToType(type_code, colname): import psycopg2 try: tname = psycopg2._psycopg.string_types[type_code].name if 'INTEGER' in tname: return int if 'STRING' in tname: return str except KeyError: vd.status('unknown postgres type_code %s for %s' % (type_code, colname)) return anytype def openurl_rds(url, filetype=None): import boto3 import psycopg2 rds = boto3.client('rds') url = urlparse(url.given) _, region, dbname = url.path.split('/') token = rds.generate_db_auth_token(url.hostname, url.port, url.username, region) conn = psycopg2.connect( user=url.username, dbname=dbname, host=url.hostname, port=url.port, password=token) return PgTablesSheet(dbname+"_tables", sql=SQL(conn)) def openurl_postgres(url, filetype=None): import psycopg2 url = urlparse(url.given) dbname = url.path[1:] conn = psycopg2.connect( user=url.username, dbname=dbname, host=url.hostname, port=url.port, password=url.password) return PgTablesSheet(dbname+"_tables", sql=SQL(conn)) class SQL: def __init__(self, conn): self.conn = conn def cur(self, qstr): import string randomname = ''.join(random.choice(string.ascii_uppercase) for _ in range(6)) cur = self.conn.cursor(randomname) cur.execute(qstr) return cur @asyncthread def query_async(self, qstr, callback=None): with self.cur(qstr) as cur: callback(cur) cur.close() def cursorToColumns(cur, sheet): sheet.columns = [] for i, coldesc in enumerate(cur.description): sheet.addColumn(ColumnItem(coldesc.name, i, type=codeToType(coldesc.type_code, coldesc.name))) # rowdef: (table_name, ncols) class PgTablesSheet(Sheet): rowtype = 'tables' def reload(self): schema = options.postgres_schema qstr = f''' SELECT relname table_name, column_count.ncols, reltuples::bigint est_nrows FROM pg_class, pg_namespace, ( SELECT table_name, COUNT(column_name) AS ncols FROM information_schema.COLUMNS WHERE table_schema = '{schema}' GROUP BY table_name ) AS column_count WHERE pg_class.relnamespace = pg_namespace.oid AND pg_namespace.nspname = '{schema}' AND column_count.table_name = relname; ''' with self.sql.cur(qstr) as cur: self.nrowsPerTable = {} self.rows = [] # try to get first row to make cur.description available r = cur.fetchone() if r: self.addRow(r) cursorToColumns(cur, self) self.setKeys(self.columns[0:1]) # table_name is the key for r in cur: self.addRow(r) def openRow(self, row): return PgTable(self.name+"."+row[0], source=row[0], sql=self.sql) # rowdef: tuple of values as returned by fetchone() class PgTable(Sheet): @asyncthread def reload(self): if self.options.postgres_schema: source = f"{self.options.postgres_schema}.{self.source}" else: source = self.source with self.sql.cur(f"SELECT * FROM {source}") as cur: self.rows = [] r = cur.fetchone() if r: self.addRow(r) cursorToColumns(cur, self) for r in cur: self.addRow(r) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608541899.0 visidata-2.2.1/visidata/loaders/rec.py0000660000175000017500000001067500000000000020400 0ustar00kefalakefala00000000000000from visidata import * @VisiData.api def open_rec(vd, p): return RecIndexSheet(p.name, source=p) def decode_multiline(line, fp): 'Parse *line* and lookahead into *fp* as iterator for continuing lines. Return (multiline, next_line) where *multiline* can contain newlines and *next_line is the line after the combined *multiline*. Handle "\\" at end and "+" at beginning of lines. *next_line* will be None iff iterator is exhausted.' while True: try: next_line = next(fp) except StopIteration: return line, None if line.endswith('\\'): line = line[:-1] + next_line elif next_line.startswith('+'): # strip leading r'+ ?' next_line = next_line[2:] if next_line.startswith('+ ') else next_line[1:] line += '\n' + next_line else: return line, next_line def encode_multiline(s): return '\n+ '.join(s.splitlines()) def get_kv(line): return re.split(r':[ \t]?', line, maxsplit=1) class RecSheet(TableSheet): def addColumn(self, c, index=None): super().addColumn(c, index=index) self.colnames[c.name] = c RecSheet.init('colnames', dict) class RecIndexSheet(IndexSheet): def iterload(self): sheet = None row = None newRecord = True next_line = '' comments = [] fp = iter(self.source) while next_line is not None: line, next_line = decode_multiline(next_line, fp) line = line.lstrip() if not line: # end of record separator newRecord = True continue elif line[0] == '#': comments.append(line) continue if not sheet or (newRecord and line[0] == '%'): sheet = RecSheet('', columns=[], rows=[], source=self, comments=comments) comments = [] yield sheet newRecord = False if line[0] == '%': desc, rest = get_kv(line[1:]) if desc == 'rec': sheet.name = rest elif desc in 'mandatory allowed': for colname in rest.split(): if colname not in sheet.colnames: sheet.addColumn(ItemColumn(colname)) elif desc in ['key', 'unique']: for i, colname in enumerate(rest.split()): if colname not in sheet.colnames: sheet.addColumn(ItemColumn(colname, keycol=i+1)) elif desc in ['sort']: sheet.orderBy([sheet.column(colname) for colname in rest.split()]) elif desc in ['type', 'typedef']: pass elif desc in ['auto']: # autoincrement columns should be present already pass elif desc in ['size', 'constraint']: # ignore constraints pass elif desc in ['confidential']: # encrypted pass else: vd.warning('Unhandled descriptor: ' +line) else: if newRecord: row = None newRecord = False if not row: row = {} sheet.addRow(row) name, rest = get_kv(line) if name not in sheet.colnames: sheet.addColumn(ColumnItem(name)) if name in row: if not isinstance(row[name], list): row[name] = [row[name]] row[name].append(rest) else: row[name] = rest for sheet in Progress(self.rows): sheet.sort() @VisiData.api def save_rec(vd, p, *vsheets): with p.open_text(mode='w') as fp: for vs in vsheets: comments = getattr(vs, 'comments', []) if comments: fp.write('# ' + '\n# '.join(comments) + '\n') fp.write('%rec: ' + vs.name + '\n') fp.write('\n') for col in vs.visibleCols: if col.keycol: fp.write('%key: ' + col.name + '\n') for row in Progress(vs.rows): for col in vs.visibleCols: fp.write(col.name+': '+encode_multiline(col.getDisplayValue(row))+'\n') fp.write('\n') fp.write('\n') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1606688371.0 visidata-2.2.1/visidata/loaders/sas.py0000660000175000017500000000201100000000000020376 0ustar00kefalakefala00000000000000from visidata import * import logging SASTypes = { 'string': str, 'number': float, } def open_xpt(p): return XptSheet(p.name, source=p) def open_sas7bdat(p): return SasSheet(p.name, source=p) class XptSheet(Sheet): def iterload(self): import xport with open(self.source, 'rb') as fp: self.rdr = xport.Reader(fp) self.columns = [] for i, var in enumerate(self.rdr._variables): self.addColumn(ColumnItem(var.name, i, type=float if var.numeric else str)) yield from self.rdr class SasSheet(Sheet): def iterload(self): import sas7bdat self.dat = sas7bdat.SAS7BDAT(str(self.source), skip_header=True, log_level=logging.CRITICAL) self.columns = [] for col in self.dat.columns: self.addColumn(ColumnItem(col.name.decode('utf-8'), col.col_id, type=SASTypes.get(col.type, anytype))) with self.dat as fp: yield from Progress(fp, total=self.dat.properties.row_count) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612075960.0 visidata-2.2.1/visidata/loaders/shp.py0000660000175000017500000000647700000000000020426 0ustar00kefalakefala00000000000000from visidata import * # requires pyshp def open_shp(p): return ShapeSheet(p.name, source=p) open_dbf = open_shp shptypes = { 'C': str, 'N': float, 'L': float, 'F': float, 'D': date, 'M': str, } def shptype(ftype, declen): t = shptypes[ftype[:1]] if t is float and declen == 0: return int return t # rowdef: shaperec class ShapeSheet(Sheet): rowtype = 'shapes' columns = [ Column('shapeType', width=0, getter=lambda col,row: row.shape.shapeType) ] def iterload(self): import shapefile self.sf = shapefile.Reader(str(self.source)) self.reloadCols() for shaperec in Progress(self.sf.iterShapeRecords(), total=self.sf.numRecords): yield shaperec def reloadCols(self): self.columns = [] for c in ShapeSheet.columns: self.addColumn(copy(c)) for i, (fname, ftype, fieldlen, declen) in enumerate(self.sf.fields[1:]): # skip DeletionFlag self.addColumn(Column(fname, getter=lambda col,row,i=i: row.record[i], type=shptype(ftype, declen))) class ShapeMap(InvertedCanvas): aspectRatio = 1.0 filetype = 'geojson' @asyncthread def reload(self): self.reset() for row in Progress(self.sourceRows): # color according to key k = self.source.rowkey(row) if row.shape.shapeType in (5, 15, 25): self.polygon(row.shape.points, self.plotColor(k), row) elif row.shape.shapeType in (3, 13, 23): self.polyline(row.shape.points, self.plotColor(k), row) elif row.shape.shapeType in (1, 11, 21): x, y = row.shape.points[0] self.point(x, y, self.plotColor(k), row) else: vd.status('notimpl shapeType %s' % row.shape.shapeType) x1, y1, x2, y2 = row.shape.bbox textx, texty = (x1+x2)/2, (y1+y2)/2 disptext = self.textCol.getDisplayValue(row) self.label(textx, texty, disptext, self.plotColor(k), row) self.refresh() @ShapeMap.api def save_geojson(vd, p, vs): features = [] for coords, attr, row in Progress(vs.polylines, 'saving'): feat = { 'type': 'Feature', 'geometry': { 'type': 'LineString', 'coordinates': [[x, y] for x, y in coords], }, 'properties': { col.name: col.getTypedValue(row) for col in vs.source.visibleCols } } features.append(feat) featcoll = { 'type': 'FeatureCollection', 'features': features, } with p.open_text(mode='w') as fp: for chunk in json.JSONEncoder().iterencode(featcoll): fp.write(chunk) ShapeSheet.addCommand('.', 'plot-row', 'vd.push(ShapeMap(name+"_map", source=sheet, sourceRows=[cursorRow], textCol=cursorCol))', 'plot geospatial vector in current row') ShapeSheet.addCommand('g.', 'plot-rows', 'vd.push(ShapeMap(name+"_map", source=sheet, sourceRows=rows, textCol=cursorCol))', 'plot all geospatial vectors in current sheet') ShapeMap.addCommand('^S', 'save-sheet', 'vd.saveSheets(inputPath("save to: ", value=getDefaultSaveName(sheet)), sheet, confirm_overwrite=options.confirm_overwrite)', 'save current sheet to filename in format determined by extension (default .geojson)') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1606688371.0 visidata-2.2.1/visidata/loaders/spss.py0000660000175000017500000000121700000000000020607 0ustar00kefalakefala00000000000000from visidata import * def open_spss(p): return SpssSheet(p.name, source=p) open_sav = open_spss class SpssSheet(Sheet): @asyncthread def reload(self): import savReaderWriter self.rdr = savReaderWriter.SavReader(str(self.source)) with self.rdr as reader: self.columns = [] for i, vname in enumerate(reader.varNames): vtype = float if reader.varTypes[vname] == 0 else str self.addColumn(ColumnItem(vname.decode('utf-8'), i, type=vtype)) self.rows = [] for r in Progress(reader, total=reader.shape.nrows): self.addRow(r) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608541899.0 visidata-2.2.1/visidata/loaders/sqlite.py0000660000175000017500000001422200000000000021120 0ustar00kefalakefala00000000000000from visidata import * def open_sqlite(p): return SqliteIndexSheet(p.name, source=p) open_sqlite3 = open_sqlite open_db = open_sqlite # rowdef: list of values class SqliteSheet(Sheet): 'Provide functionality for importing SQLite databases.' savesToSource = True defer = True def resolve(self): 'Resolve all the way back to the original source Path.' return self.source.resolve() def conn(self): import sqlite3 return sqlite3.connect(str(self.resolve())) def execute(self, conn, sql, parms=None): parms = parms or [] vd.status(sql) return conn.execute(sql, parms) def iterload(self): sqltypes = { 'INTEGER': int, 'TEXT': anytype, 'BLOB': str, 'REAL': float } with self.conn() as conn: tblname = self.tableName if not isinstance(self, SqliteIndexSheet): self.columns = [] self.addColumn(ColumnItem('rowid', 0, type=int, width=0)) for i, r in enumerate(self.execute(conn, 'PRAGMA TABLE_INFO("%s")' % tblname)): c = ColumnItem(r[1], i+1, type=sqltypes.get(r[2].upper(), anytype)) self.addColumn(c) if r[-1]: self.setKeys([c]) r = self.execute(conn, 'SELECT COUNT(*) FROM "%s"' % tblname).fetchall() rowcount = r[0][0] for row in Progress(self.execute(conn, 'SELECT rowid, * FROM "%s"' % tblname), total=rowcount-1): yield list(row) @asyncthread def putChanges(self): adds, mods, dels = self.getDeferredChanges() options_safe_error = options.safe_error def value(row, col): v = col.getTypedValue(row) if isinstance(v, TypedWrapper): if isinstance(v, TypedExceptionWrapper): return options_safe_error else: return None elif not isinstance(v, (int, float, str)): v = col.getDisplayValue(r) return v def values(row, cols): vals = [] for c in cols: vals.append(value(row, c)) return vals with self.conn() as conn: wherecols = [self.columns[0]] # self.column("rowid") for r in adds.values(): cols = self.visibleCols sql = 'INSERT INTO "%s" ' % self.tableName sql += '(%s)' % ','.join(c.name for c in cols) sql += ' VALUES (%s)' % ','.join('?' for c in cols) res = self.execute(conn, sql, parms=values(r, cols)) if res.rowcount != res.arraysize: vd.warning('not all rows inserted') # f'{res.rowcount}/{res.arraysize} rows inserted' for row, rowmods in mods.values(): sql = 'UPDATE "%s" SET ' % self.tableName sql += ', '.join('%s=?' % c.name for c, _ in rowmods.items()) sql += ' WHERE %s' % ' AND '.join('"%s"=?' % c.name for c in wherecols) newvals=values(row, [c for c, _ in rowmods.items()]) # calcValue gets the 'previous' value (before update) wherevals=list(Column.calcValue(c, row) or '' for c in wherecols) res = self.execute(conn, sql, parms=newvals+wherevals) if res.rowcount != res.arraysize: vd.warning('not all rows updated') # f'{res.rowcount}/{res.arraysize} rows updated' for row in dels.values(): sql = 'DELETE FROM "%s" ' % self.tableName sql += ' WHERE %s' % ' AND '.join('"%s"=?' % c.name for c in wherecols) wherevals=list(Column.calcValue(c, row) for c in wherecols) res = self.execute(conn, sql, parms=wherevals) if res.rowcount != res.arraysize: vd.warning('not all rows deleted') # f'{res.rowcount}/{res.arraysize} rows deleted' conn.commit() self.preloadHook() self.reload() class SqliteIndexSheet(SqliteSheet, IndexSheet): tableName = 'sqlite_master' def iterload(self): for row in SqliteSheet.iterload(self): if row[1] != 'index': tblname = row[2] yield SqliteSheet(tblname, source=self, tableName=tblname, row=row) class SqliteQuerySheet(SqliteSheet): def iterload(self): with self.conn() as conn: self.columns = [] self.addColumn(ColumnItem('rowid', 0, type=int)) self.result = self.execute(conn, self.query, parms=getattr(self, 'parms', [])) for i, desc in enumerate(self.result.description): self.addColumn(ColumnItem(desc[0], i+1)) for row in self.result: yield row @VisiData.api def save_sqlite(vd, p, *vsheets): import sqlite3 conn = sqlite3.connect(str(p)) c = conn.cursor() sqltypes = { int: 'INTEGER', float: 'REAL', currency: 'REAL' } for vs in vsheets: vs.ensureLoaded() vd.sync() for vs in vsheets: tblname = clean_to_id(vs.name) sqlcols = [] for col in vs.visibleCols: sqlcols.append('"%s" %s' % (col.name, sqltypes.get(col.type, 'TEXT'))) sql = 'CREATE TABLE IF NOT EXISTS "%s" (%s)' % (tblname, ', '.join(sqlcols)) c.execute(sql) for r in Progress(vs.rows, 'saving'): sqlvals = [] for col in vs.visibleCols: v = col.getTypedValue(r) if isinstance(v, TypedWrapper): if isinstance(v, TypedExceptionWrapper): v = options.safe_error else: v = None elif not isinstance(v, (int, float, str)): v = col.getDisplayValue(r) sqlvals.append(v) sql = 'INSERT INTO "%s" VALUES (%s)' % (tblname, ','.join('?' for v in sqlvals)) c.execute(sql, sqlvals) conn.commit() vd.status("%s save finished" % p) SqliteSheet.class_options.header = 0 VisiData.save_db = VisiData.save_sqlite ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608541899.0 visidata-2.2.1/visidata/loaders/texttables.py0000660000175000017500000000117700000000000022003 0ustar00kefalakefala00000000000000import functools from visidata import vd, Progress try: import tabulate for fmt in tabulate.tabulate_formats: def save_table(path, *sheets, fmt=fmt): import tabulate with path.open_text(mode='w') as fp: for vs in sheets: fp.write(tabulate.tabulate( vs.itervals(*vs.visibleCols, format=True), headers=[ col.name for col in vs.visibleCols ], tablefmt=fmt)) if not getattr(vd, 'save_'+fmt, None): setattr(vd, 'save_'+fmt, save_table) except ModuleNotFoundError: pass ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608178530.0 visidata-2.2.1/visidata/loaders/tsv.py0000660000175000017500000000613500000000000020437 0ustar00kefalakefala00000000000000import os import contextlib import itertools import collections from visidata import asyncthread, options, Progress, ColumnItem, SequenceSheet, Sheet, FileExistsError, getType, option, VisiData from visidata import namedlist, filesize option('delimiter', '\t', 'field delimiter to use for tsv/usv filetype', replay=True) option('row_delimiter', '\n', 'row delimiter to use for tsv/usv filetype', replay=True) option('tsv_safe_newline', '\u001e', 'replacement for newline character when saving to tsv', replay=True) option('tsv_safe_tab', '\u001f', 'replacement for tab character when saving to tsv', replay=True) def open_tsv(p): return TsvSheet(p.name, source=p) def splitter(fp, delim='\n'): 'Generates one line/row/record at a time from fp, separated by delim' buf = '' while True: nextbuf = fp.read(512) if not nextbuf: break buf += nextbuf *rows, buf = buf.split(delim) yield from rows yield from buf.rstrip(delim).split(delim) # rowdef: list class TsvSheet(SequenceSheet): def iterload(self): delim = self.options.delimiter rowdelim = self.options.row_delimiter with self.source.open_text() as fp: with Progress(total=filesize(self.source)) as prog: for line in splitter(fp, rowdelim): if not line: continue prog.addProgress(len(line)) row = list(line.split(delim)) if len(row) < self.nVisibleCols: # extend rows that are missing entries row.extend([None]*(self.nVisibleCols-len(row))) yield row def load_tsv(fn): vs = open_tsv(Path(fn)) yield from vs.iterload() @VisiData.api def save_tsv(vd, p, vs): 'Write sheet to file `fn` as TSV.' unitsep = vs.options.delimiter rowsep = vs.options.row_delimiter trdict = vs.safe_trdict() with p.open_text(mode='w') as fp: colhdr = unitsep.join(col.name.translate(trdict) for col in vs.visibleCols) + options.row_delimiter fp.write(colhdr) for dispvals in vs.iterdispvals(format=True): fp.write(unitsep.join(dispvals.values())) fp.write(rowsep) vd.status('%s save finished' % p) def append_tsv_row(vs, row): 'Append `row` to vs.source, creating file with correct headers if necessary. For internal use only.' if not vs.source.exists(): with contextlib.suppress(FileExistsError): parentdir = vs.source.parent if parentdir: os.makedirs(parentdir) # Write tsv header for Sheet `vs` to Path `p` trdict = vs.safe_trdict() unitsep = options.delimiter with vs.source.open_text(mode='w') as fp: colhdr = unitsep.join(col.name.translate(trdict) for col in vs.visibleCols) + options.row_delimiter if colhdr.strip(): # is anything but whitespace fp.write(colhdr) with vs.source.open_text(mode='a') as fp: fp.write('\t'.join(col.getDisplayValue(row) for col in vs.visibleCols) + '\n') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608178530.0 visidata-2.2.1/visidata/loaders/ttf.py0000660000175000017500000000535300000000000020421 0ustar00kefalakefala00000000000000from visidata import * def open_ttf(p): return TTFTablesSheet(p.name, source=p) open_otf = open_ttf class TTFTablesSheet(Sheet): rowtype = 'font tables' columns = [ ColumnAttr('cmap'), ColumnAttr('format', type=int), ColumnAttr('language', type=int), ColumnAttr('length', type=int), ColumnAttr('platEncID', type=int), ColumnAttr('platformID', type=int), Column('isSymbol', getter=lambda col,row: row.isSymbol()), Column('isUnicode', getter=lambda col,row: row.isUnicode()), ] def openRow(self, row): return TTFGlyphsSheet(self.name+'_glyphs', source=self, sourceRows=[row], ttf=self.ttf) def iterload(self): import fontTools.ttLib self.ttf = fontTools.ttLib.TTFont(str(self.source), 0, allowVID=0, ignoreDecompileErrors=True, fontNumber=-1) for cmap in self.ttf["cmap"].tables: yield cmap class TTFGlyphsSheet(Sheet): rowtype = 'glyphs' # rowdef: (codepoint, glyphid, fontTools.ttLib.ttFont._TTGlyphGlyf) columns = [ ColumnItem('codepoint', 0, type=int, fmtstr='%0X'), ColumnItem('glyphid', 1), SubColumnItem(2, ColumnAttr('height', type=int)), SubColumnItem(2, ColumnAttr('width', type=int)), SubColumnItem(2, ColumnAttr('lsb')), SubColumnItem(2, ColumnAttr('tsb')), ] def openRow(self, row): return makePen(self.name+"_"+row[1], source=row[2], glyphSet=self.ttf.getGlyphSet()) def iterload(self): glyphs = self.ttf.getGlyphSet() for cmap in self.sourceRows: for codepoint, glyphid in Progress(cmap.cmap.items(), total=len(cmap.cmap)): yield (codepoint, glyphid, glyphs[glyphid]) def makePen(*args, **kwargs): try: from fontTools.pens.basePen import BasePen except ImportError as e: vd.error('fonttools not installed') class GlyphPen(InvertedCanvas, BasePen): aspectRatio = 1.0 def __init__(self, name, **kwargs): super().__init__(name, **kwargs) self.lastxy = None self.attr = self.plotColor(('glyph',)) def _moveTo(self, xy): self.lastxy = xy def _lineTo(self, xy): x1, y1 = self.lastxy x2, y2 = xy self.line(x1, y1, x2, y2, self.attr) self._moveTo(xy) def _curveToOne(self, xy1, xy2, xy3): vd.error('NotImplemented') def _qCurveToOne(self, xy1, xy2): self.qcurve([self.lastxy, xy1, xy2], self.attr) self._moveTo(xy2) def reload(self): self.reset() self.source.draw(self) self.refresh() return GlyphPen(*args, **kwargs) #TTFGlyphsSheet.bindkey('.', 'open-row') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1606688371.0 visidata-2.2.1/visidata/loaders/usv.py0000660000175000017500000000064000000000000020433 0ustar00kefalakefala00000000000000from copy import copy from visidata import Sheet, TsvSheet, options, vd, VisiData def open_usv(p): vs = TsvSheet(p.name, source=p) vs.options.delimiter = '\u241e' vs.options.row_delimiter = '\u241f' return vs @VisiData.api def save_usv(vd, p, vs): usvs = copy(vs) usvs.rows = vs.rows usvs.options.delimiter = '\u241e' usvs.options.row_delimiter = '\u241f' vd.save_tsv(p, usvs) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608178530.0 visidata-2.2.1/visidata/loaders/vcf.py0000660000175000017500000000203000000000000020367 0ustar00kefalakefala00000000000000from visidata import * # requires (deb): libbz2-dev libcurl4-openssl-dev liblzma-dev def open_vcf(p): return VcfSheet(p.name, source=p) def unbox(col, row): v = getitemdef(row, col.expr) if not v: return None if len(v) == 1: return v[0].value return v class VcfSheet(PythonSheet): rowtype = 'cards' @asyncthread def reload(self): import vobject self.rows = [] self.columns = [] addedCols = set() lines = [] for line in self.source.open_text(): lines.append(line) if line.startswith('END:'): row = vobject.readOne('\n'.join(lines)) for k, v in row.contents.items(): if v and str(v[0].value).startswith('(None)'): continue if not k in addedCols: addedCols.add(k) self.addColumn(Column(k, expr=k, getter=unbox)) self.addRow(row.contents) lines = [] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612075960.0 visidata-2.2.1/visidata/loaders/vds.py0000660000175000017500000000463700000000000020424 0ustar00kefalakefala00000000000000'Custom VisiData save format' import json from visidata import * NL='\n' @VisiData.api def open_vds(vd, p): return VdsIndexSheet(p.name, source=p) @VisiData.api def save_vds(vd, p, *sheets): 'Save in custom VisiData format, preserving columns and their attributes.' with p.open_text(mode='w') as fp: for vs in sheets: # class and attrs for vs d = { 'name': vs.name, } fp.write('#'+json.dumps(d)+NL) # class and attrs for each column in vs for col in vs.visibleCols: d = col.__getstate__() d['col'] = type(col).__name__ fp.write('#'+json.dumps(d)+NL) with Progress(gerund='saving'): for row in vs.iterdispvals(*vs.visibleCols, format=True): d = {col.name:val for col, val in row.items()} fp.write(json.dumps(d)+NL) class VdsIndexSheet(IndexSheet): def iterload(self): vs = None with self.source.open_text() as fp: line = fp.readline() while line: if line.startswith('#{'): d = json.loads(line[1:]) if 'col' not in d: vs = VdsSheet(d.pop('name'), columns=[], source=self.source, source_fpos=fp.tell()) yield vs line = fp.readline() class VdsSheet(Sheet): def newRow(self): return {} # rowdef: dict def iterload(self): self.colnames = {} self.columns = [] with self.source.open_text() as fp: fp.seek(self.source_fpos) # consume all metadata, create columns line = fp.readline() while line and line.startswith('#{'): d = json.loads(line[1:]) if 'col' not in d: raise Exception(d) classname = d.pop('col') if classname == 'Column': classname = 'ItemColumn' d['expr'] = d['name'] c = globals()[classname](d.pop('name')) self.colnames[c.name] = c self.addColumn(c) for k, v in d.items(): setattr(c, k, v) line = fp.readline() while line and not line.startswith('#{'): d = json.loads(line) yield d line = fp.readline() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612075960.0 visidata-2.2.1/visidata/loaders/xlsb.py0000660000175000017500000000053400000000000020570 0ustar00kefalakefala00000000000000from visidata import vd, IndexSheet 'Requires visidata/deps/pyxlsb fork' def open_xlsb(p): return XlsbIndex(p.name, source=p) class XlsbIndex(IndexSheet): def iterload(self): from pyxlsb import open_workbook wb = open_workbook(str(self.source)) for name in wb.sheets: yield wb.get_sheet(name, True) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612075960.0 visidata-2.2.1/visidata/loaders/xlsx.py0000660000175000017500000000661500000000000020624 0ustar00kefalakefala00000000000000from visidata import * def open_xls(p): return XlsIndexSheet(p.name, source=p) def open_xlsx(p): return XlsxIndexSheet(p.name, source=p) class XlsxIndexSheet(IndexSheet): 'Load XLSX file (in Excel Open XML format).' rowtype = 'sheets' # rowdef: xlsxSheet columns = [ Column('sheet', getter=lambda col,row: row.source.title), # xlsx sheet title ColumnAttr('name', width=0), # visidata Sheet name ColumnAttr('nRows', type=int), ColumnAttr('nCols', type=int), Column('active', getter=lambda col,row: row.source is col.sheet.workbook.active), ] nKeys = 1 def iterload(self): import openpyxl self.workbook = openpyxl.load_workbook(str(self.source), data_only=True, read_only=True) for sheetname in self.workbook.sheetnames: src = self.workbook[sheetname] yield XlsxSheet(self.name, sheetname, source=src) class XlsxSheet(SequenceSheet): def iterload(self): worksheet = self.source for row in Progress(worksheet.iter_rows(), total=worksheet.max_row or 0): yield list(wrapply(getattr, cell, 'value') for cell in row) class XlsIndexSheet(IndexSheet): 'Load XLS file (in Excel format).' rowtype = 'sheets' # rowdef: xlsSheet columns = [ Column('sheet', getter=lambda col,row: row.source.name), # xls sheet name ColumnAttr('name', width=0), # visidata sheet name ColumnAttr('nRows', type=int), ColumnAttr('nCols', type=int), ] nKeys = 1 def iterload(self): import xlrd self.workbook = xlrd.open_workbook(str(self.source)) for sheetname in self.workbook.sheet_names(): yield XlsSheet(self.name, sheetname, source=self.workbook.sheet_by_name(sheetname)) class XlsSheet(SequenceSheet): def iterload(self): worksheet = self.source for rownum in Progress(range(worksheet.nrows)): yield list(worksheet.cell(rownum, colnum).value for colnum in range(worksheet.ncols)) def xls_name(name): # sheet name can not be longer than 31 characters xname = clean_name(name)[:31] if xname != name: vd.warning(f'{name} saved as {xname}') return xname @VisiData.api def save_xlsx(vd, p, *sheets): import openpyxl wb = openpyxl.Workbook() wb.remove_sheet(wb['Sheet']) for vs in sheets: ws = wb.create_sheet(title=xls_name(vs.name)) headers = [col.name for col in vs.visibleCols] ws.append(headers) for dispvals in vs.iterdispvals(format=False): row = [] for col, v in dispvals.items(): if col.type == date: v = datetime.datetime.fromtimestamp(int(v.timestamp())) elif not vd.isNumeric(col): v = str(v) row.append(v) ws.append(row) wb.active = ws wb.save(filename=p) vd.status(f'{p} save finished') @VisiData.api def save_xls(vd, p, *sheets): import xlwt wb = xlwt.Workbook() for vs in sheets: ws1 = wb.add_sheet(xls_name(vs.name)) for col_i, col in enumerate(vs.visibleCols): ws1.write(0, col_i, col.name) for r_i, dispvals in enumerate(vs.iterdispvals(format=True)): r_i += 1 for c_i, v in enumerate(dispvals.values()): ws1.write(r_i, c_i, v) wb.save(p) vd.status(f'{p} save finished') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608178530.0 visidata-2.2.1/visidata/loaders/xml.py0000660000175000017500000000543500000000000020425 0ustar00kefalakefala00000000000000from visidata import * def open_xml(p): return XmlSheet(p.name, source=p) open_svg = open_xml def unns(k): 'de-namespace key k' if '}' in k: return k[k.find('}')+1:] return k def AttribColumn(name, k, **kwargs): return Column(name, getter=lambda c,r,k=k: r.attrib.get(k), setter=lambda c,r,v,k=k: setitem(r.attrib, k, v), **kwargs) # source is Path or xml.Element; root is xml.Element class XmlSheet(Sheet): rowtype = 'elements' # rowdef: lxml.xml.Element columns = [ ColumnAttr('sourceline', type=int, width=0), ColumnAttr('prefix', width=0), ColumnAttr('nstag', 'tag', width=0), Column('path', width=0, getter=lambda c,r: c.sheet.root.getpath(r)), Column('tag', getter=lambda c,r: unns(r.tag)), Column('children', type=vlen, getter=lambda c,r: r.getchildren()), ColumnAttr('text'), ColumnAttr('tail', width=0), ] colorizers = [ RowColorizer(8, None, lambda s,c,r,v: 'green' if r is s.source else None) ] def showColumnsBasedOnRow(self, row): for c in self.columns: nstag = getattr(c, 'nstag', '') if nstag: c.hide(nstag not in row.attrib) def iterload(self): if isinstance(self.source, Path): from lxml import etree, objectify self.root = etree.parse(self.source.open_text()) objectify.deannotate(self.root, cleanup_namespaces=True) else: # elif isinstance(self.source, XmlElement): self.root = self.source self.attribcols = {} self.columns = [] for c in XmlSheet.columns: self.addColumn(copy(c)) if getattr(self.root, 'iterancestors', None): for elem in Progress(list(self.root.iterancestors())[::-1]): yield elem for elem in self.root.iter(): yield elem def openRow(self, row): return XmlSheet("%s_%s" % (unns(row.tag), row.attrib.get("id")), source=row) def addRow(self, elem): super().addRow(elem) for k in elem.attrib: if k not in self.attribcols: c = AttribColumn(unns(k), k) self.addColumn(c) self.attribcols[k] = c c.nstag = k @VisiData.api def save_xml(vd, p, vs): isinstance(XmlSheet) or vd.fail('must save xml from XmlSheet') vs.root.write(str(p), encoding=options.encoding, standalone=False, pretty_print=True) VisiData.save_svg = VisiData.save_xml XmlSheet.addCommand('za', 'addcol-xmlattr', 'attr=input("add attribute: "); addColumnAtCursor(AttribColumn(attr, attr))', 'add column for xml attribute') XmlSheet.addCommand('v', 'visibility', 'showColumnsBasedOnRow(cursorRow)', 'show only columns in current row attributes') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1606688371.0 visidata-2.2.1/visidata/loaders/xword.py0000770000175000017500000000643100000000000020767 0ustar00kefalakefala00000000000000from collections import defaultdict import string from visidata import * theme('color_xword_active', 'green', 'color of active clue') def open_puz(p): return PuzSheet(p.name, source=p) def open_xd(p): if p.is_dir(): return CrosswordsSheet(p.name, source=p) return CrosswordSheet(p.name, source=p) class CrosswordsSheet(Sheet): rowtype = 'puzzles' columns = [ Column('Author', getter=lambda col, row: row.author), Column('Copyright', getter=lambda col, row: row.copyright), Column('Notes', getter=lambda col, row: row.notes), Column('Postscript', getter=lambda col, row: ''.join(x for x in row.postscript if ord(x) >= ord(' '))), Column('Preamble', getter=lambda col, row: row.preamble), Column('Title', getter=lambda col, row: row.title) ] @asyncthread def reload(self): self.rows = [] for p in self.source.iterdir(): self.addRow(Crossword(p.read(), str(p))) class GridSheet(Sheet): rowtype = 'gridrow' # rowdef: puzzle_row:str colorizers = [ CellColorizer(7, 'color_xword_active', lambda s,c,r,v: r and s.pos in s.cells[(s.rows.index(r),c)]) ] @asyncthread def reload(self): grid = self.source.xd.grid ncols = len(grid[0]) self.columns = [ColumnItem('', i, width=2) for i in range(ncols)] for row in grid: row = list(row) self.addRow(row) self.cells = defaultdict(list) # [rownum, col] -> [ Apos, Dpos ] or [] (if black) # find starting r,c from self.pos for cluedir, cluenum, answer, r, c in self.source.xd.iteranswers_full(): # across if cluedir == 'A': for i in range(0, len(answer)): self.cells[(r, self.columns[c+i])].append(('A', cluenum)) if cluedir == 'D': for i in range(0, len(answer)): self.cells[(r+i, self.columns[c])].append(('D', cluenum)) if cluenum == self.pos[1]: self.cursorRowIndex, self.cursorVisibleColIndex = r, c class CrosswordSheet(Sheet): rowtype = 'clues' # rowdef: (cluenum, clue, answer) columns = [ Column('clue_number', getter=lambda col, row: row[0][0]+str(row[0][1])), Column('clue', getter=lambda col, row: row[1]), Column('answer', getter=lambda col, row: row[2]) ] def reload(self): import xdfile self.xd = xdfile.xdfile(xd_contents=self.source.read_text(), filename=self.source) self.rows = self.xd.clues class PuzSheet(CrosswordSheet): @asyncthread def reload(self): import xdfile.puz2xd self.xd = xdfile.puz2xd.parse_puz(self.source.read_bytes(), str(self.source)) self.rows = self.xd.clues @VisiData.api def save_xd(vd, p, vs): with p.open_text(mode='w') as fp: fp.write(vs.xd.to_unicode()) CrosswordsSheet.addCommand(ENTER, 'open-clues', 'vd.push(CrosswordSheet("clues_"+cursorRow.title, source=cursorRow))', 'open CrosswordSheet: clue answer pair for crossword') CrosswordSheet.addCommand(ENTER, 'open-grid', 'vd.push(GridSheet("grid", source=sheet, pos=cursorRow[0]))', 'open GridSheet: grid for crossword') GridSheet.class_options.disp_column_sep = '' ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1606688371.0 visidata-2.2.1/visidata/loaders/yaml.py0000660000175000017500000000245300000000000020564 0ustar00kefalakefala00000000000000from visidata import * from itertools import chain def open_yml(p): return YamlSheet(p.name, source=p) open_yaml = open_yml class YamlSheet(JsonSheet): def iterload(self): import yaml with self.source.open_text() as fp: documents = yaml.safe_load_all(fp) self.columns = [] self.colnames = {} # Peek at the document stream to determine how to best DWIM. # # This code is a bit verbose because it avoids slurping the generator # all at once into memory. try: first = next(documents) except StopIteration: # Empty file‽ yield None return try: second = next(documents) except StopIteration: if isinstance(first, list): # A file with a single YAML list: yield one row per list item. yield from Progress(first) else: # A file with a single YAML non-list value, e.g a dict. yield first else: # A file containing multiple YAML documents: yield one row per document. yield from Progress(chain([first, second], documents), total=0) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1609740788.0 visidata-2.2.1/visidata/macros.py0000660000175000017500000000427400000000000017460 0ustar00kefalakefala00000000000000from visidata import * from functools import wraps vd.macroMode = None vd.macrobindings = {} @VisiData.lazy_property def macrosheet(vd): macrospath = Path(os.path.join(options.visidata_dir, 'macros.tsv')) macrosheet = vd.loadInternalSheet(TsvSheet, macrospath, columns=(ColumnItem('command', 0), ColumnItem('filename', 1))) or vd.error('error loading macros') real_macrosheet = IndexSheet('user_macros', rows=[], source=macrosheet) for ks, fn in macrosheet.rows: vs = vd.loadInternalSheet(CommandLog, Path(fn)) vd.status(f"setting {ks}") setMacro(ks, vs) real_macrosheet.addRow(vs) return real_macrosheet @VisiData.api def runMacro(vd, macro): vd.replay_sync(macro, live=True) def setMacro(ks, vs): vd.macrobindings[ks] = vs if vd.isLongname(ks): BaseSheet.addCommand('', ks, 'runMacro(vd.macrobindings[longname])') else: BaseSheet.addCommand(ks, vs.name, 'runMacro(vd.macrobindings[keystrokes])') @CommandLog.api def saveMacro(self, rows, ks): vs = copy(self) vs.rows = rows macropath = Path(fnSuffix(options.visidata_dir+"macro")) vd.save_vd(macropath, vs) setMacro(ks, vs) append_tsv_row(vd.macrosheet.source, (ks, macropath)) @CommandLog.api @wraps(CommandLog.afterExecSheet) def afterExecSheet(cmdlog, sheet, escaped, err): if vd.macroMode and (vd.activeCommand is not None) and (vd.activeCommand is not UNLOADED): cmd = copy(vd.activeCommand) cmd.row = cmd.col = cmd.sheet = '' vd.macroMode.addRow(cmd) # the following needs to happen at the end, bc # once cmdlog.afterExecSheet.__wrapped__ runs, vd.activeCommand resets to None cmdlog.afterExecSheet.__wrapped__(cmdlog, sheet, escaped, err) @CommandLog.api def startMacro(cmdlog): if vd.macroMode: ks = vd.input('save macro for keystroke: ') vd.cmdlog.saveMacro(vd.macroMode.rows, ks) vd.macroMode = None else: vd.status("recording macro") vd.macroMode = CommandLog('current_macro', rows=[]) vd.status(vd.macrosheet) Sheet.addCommand('m', 'macro-record', 'vd.cmdlog.startMacro()') Sheet.addCommand('gm', 'macro-sheet', 'vd.push(vd.macrosheet)') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612763790.0 visidata-2.2.1/visidata/main.py0000770000175000017500000002401400000000000017114 0ustar00kefalakefala00000000000000# # Usage: $0 [] [ ...] # $0 [] --play [--batch] [-w ] [-o ] [field=value ...] __version__ = '2.2.1' __version_info__ = 'saul.pw/VisiData v' + __version__ from copy import copy import os import io import sys import locale import warnings from visidata import vd, option, options, run, BaseSheet, AttrDict from visidata import Path, saveSheets, domotd import visidata option('config', '~/.visidatarc', 'config file to exec in Python', sheettype=None) option('play', '', 'file.vd to replay') option('batch', False, 'replay in batch mode (with no interface and all status sent to stdout)') option('output', None, 'save the final visible sheet to output at the end of replay') option('preplay', '', 'longnames to preplay before replay') option('imports', 'plugins', 'imports to preload before .visidatarc (command-line only)') # for --play def eval_vd(logpath, *args, **kwargs): 'Instantiate logpath with args/kwargs replaced and replay all commands.' log = logpath.read_text() if args or kwargs: log = log.format(*args, **kwargs) src = Path(logpath.given, fp=io.StringIO(log), filesize=len(log)) vs = vd.openSource(src, filetype=src.ext) vs.name += '_vd' vs.reload() vs.vd = vd return vs def duptty(): 'Duplicate stdin/stdout for input/output and reopen tty as stdin/stdout. Return (stdin, stdout).' try: fin = open('/dev/tty') fout = open('/dev/tty', mode='w') stdin = open(os.dup(0)) stdout = open(os.dup(1)) # for dumping to stdout from interface os.dup2(fin.fileno(), 0) os.dup2(fout.fileno(), 1) # close file descriptors for original stdin/stdout fin.close() fout.close() except Exception as e: print(e) stdin = sys.stdin stdout = sys.stdout return stdin, stdout option_aliases = {} def optalias(abbr, name, val=None): option_aliases[abbr] = (name, val) optalias('f', 'filetype') optalias('p', 'play') optalias('b', 'batch') optalias('P', 'preplay') optalias('y', 'confirm_overwrite', False) optalias('o', 'output') optalias('w', 'replay_wait') optalias('d', 'delimiter') optalias('c', 'config') optalias('r', 'dir_recurse') optalias('force_valid_colnames', 'clean_names') # deprecated def main_vd(): 'Open the given sources using the VisiData interface.' locale.setlocale(locale.LC_ALL, '') warnings.showwarning = vd.warning flPipedInput = not sys.stdin.isatty() flPipedOutput = not sys.stdout.isatty() vd._stdin, vd._stdout = duptty() # always dup stdin/stdout stdinSource = Path('-', fp=vd._stdin) # parse args, including +sheetname:subsheet:4:3 starting at row:col on sheetname:subsheet[:...] start_positions = [] # (list_of_sheetstr, str, str) # empty sheetstr means all sheets startsheets, startrow, startcol = [], None, None fmtargs = [] fmtkwargs = {} inputs = [] i=1 current_args = {} global_args = {} flGlobal = True optsdone = False while i < len(sys.argv): arg = sys.argv[i] if optsdone: # copied from final else: clause below inputs.append((arg, copy(current_args))) fmtargs.append(arg) elif arg in ['--']: optsdone = True elif arg in ['-v', '--version']: print(__version_info__) return 0 elif arg == '-': inputs.append((stdinSource, copy(current_args))) elif arg in ['-h', '--help']: import curses curses.wrapper(lambda scr: vd.openManPage()) return 0 elif arg in ['-g', '--global']: flGlobal = True elif arg in ['-n', '--nonglobal']: flGlobal = False elif arg[0] == '-': optname = arg.lstrip('-') optval = None try: optname, optval = optname.split('=', maxsplit=1) # convert to type except Exception: pass optname = optname.replace('-', '_') optname, optval = option_aliases.get(optname, (optname, optval)) if optval is None: opt = options._get(optname) if opt: if type(opt.value) is bool: optval = True else: if i >= len(sys.argv): vd.error(f'"-{optname}" missing argument') optval = sys.argv[i+1] i += 1 current_args[optname] = optval if flGlobal: global_args[optname] = optval elif arg.startswith('+'): # position cursor at start if ':' in arg: pos = arg[1:].split(':') if len(pos) == 1: startsheet = [Path(inputs[-1]).name] if inputs else None start_positions.append((startsheet, pos[0], None)) elif len(pos) == 2: startsheet = [Path(inputs[-1]).name] if inputs else None startrow, startcol = pos start_positions.append((None, startrow, startcol)) elif len(pos) >= 3: startsheets = pos[:-2] startrow, startcol = pos[-2:] start_positions.append((startsheets, startrow, startcol)) else: start_positions.append((None, arg[1:], None)) elif current_args.get('play', None) and '=' in arg: # parse 'key=value' pairs for formatting cmdlog template in replay mode k, v = arg.split('=', maxsplit=1) fmtkwargs[k] = v else: inputs.append((arg, copy(current_args))) fmtargs.append(arg) i += 1 args = AttrDict(current_args) vd.loadConfigAndPlugins(args) for k, v in global_args.items(): options.set(k, v, obj='global') # fetch motd and plugins *after* options parsing/setting vd.pluginsSheet.ensureLoaded() domotd() if args.batch: options.undo = False vd.status = lambda *args, **kwargs: print(*args, file=sys.stderr) # ignore kwargs (like priority) vd.editline = lambda *args, **kwargs: '' vd.execAsync = lambda func, *args, **kwargs: func(*args, **kwargs) # disable async for cmd in (args.preplay or '').split(): BaseSheet('').execCommand(cmd) if not args.play: if flPipedInput and not inputs: # '|vd' without explicit '-' inputs.append((stdinSource, copy(current_args))) sources = [] for p, opts in inputs: # filetype is a special option, bc it is needed to construct the specific sheet type if ('filetype' in current_args) and ('filetype' not in opts): opts['filetype'] = current_args['filetype'] vs = vd.openSource(p, create=True, **opts) or vd.fail(f'could not open {p}') for k, v in current_args.items(): # apply final set of args to sheets specifically on cli, if not set otherwise #573 if not vs.options.is_set(k, vs): vs.options[k] = v vd.cmdlog.openHook(vs, vs.source) sources.append(vs) vd.sheets.extend(sources) # purposefully do not load everything if not vd.sheets and not args.play and not args.batch: vd.push(vd.vdmenu) if not args.play: if args.batch: vd.push(sources[0]) sources[0].reload() for startsheets, startrow, startcol in start_positions: sheets = [] # sheets to apply startrow:startcol to if not startsheets: sheets = sources # apply row/col to all sheets else: startsheet = startsheets[0] or sources[-1] vs = vd.getSheet(startsheet) vd.sync(vs.ensureLoaded()) vd.clearCaches() for startsheet in startsheets[1:]: rowidx = vs.getRowIndexFromStr(options.rowkey_prefix + startsheet) if rowidx is None: vs = None vd.warning(f'no sheet "{startsheet}"') break vs = vs.rows[rowidx] vd.sync(vs.ensureLoaded()) vd.clearCaches() if vs: vd.push(vs) sheets = [vs] if startrow: for vs in sheets: if vs: vs.moveToRow(startrow) or vd.warning(f'{vs} has no row "{startrow}"') if startcol: for vs in sheets: if vs: vs.moveToCol(startcol) or vd.warning(f'{vs} has no column "{startcol}"') if not args.batch: run(vd.sheets[0]) else: if args.play == '-': vdfile = stdinSource vdfile.name = 'stdin.vd' else: vdfile = Path(args.play) vs = eval_vd(vdfile, *fmtargs, **fmtkwargs) vd.sync(vs.reload()) if args.batch: if vd.replay_sync(vs): # error return 1 else: vd.currentReplay = vs vd.replay(vs) run() if vd.sheets and (flPipedOutput or args.output): outpath = Path(args.output or '-') saveSheets(outpath, vd.sheets[0], confirm_overwrite=False) saver_threads = [t for t in vd.unfinishedThreads if t.name.startswith('save_')] if saver_threads: print('finishing %d savers' % len(saver_threads)) vd.sync(*saver_threads) vd._stdout.flush() return 0 def vd_cli(): vd.status(__version_info__) rc = -1 try: rc = main_vd() except BrokenPipeError: os.dup2(os.open(os.devnull, os.O_WRONLY), sys.stdout.fileno()) # handle broken pipe gracefully except visidata.ExpectedException as e: print('Error: ' + str(e)) except FileNotFoundError as e: print(e) if options.debug: raise sys.stderr.flush() sys.stdout.flush() os._exit(rc) # cleanup can be expensive with large datasets ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608541899.0 visidata-2.2.1/visidata/mainloop.py0000660000175000017500000001664000000000000020012 0ustar00kefalakefala00000000000000import contextlib import os import curses import threading import time from unittest import mock from visidata import vd, VisiData, colors, ESC, options, option curses_timeout = 100 # curses timeout in ms timeouts_before_idle = 10 option('disp_splitwin_pct', 0, 'height of second sheet on screen') option('mouse_interval', 1, 'max time between press/release for click (ms)', sheettype=None) class ReturnValue(BaseException): 'raise ReturnValue(ret) to exit from an inner runresult() with its result.' pass @VisiData.api def draw_sheet(self, scr, sheet): 'Erase *scr* and draw *sheet* on it, including status bars.' scr.erase() # clear screen before every re-draw sheet._scr = scr self.drawLeftStatus(scr, sheet) self.drawRightStatus(scr, sheet) # visible during this getkeystroke try: sheet.draw(scr) except Exception as e: self.exceptionCaught(e) scr.refresh() vd.windowConfig = None vd.winTop = mock.MagicMock(__bool__=mock.Mock(return_value=False)) @VisiData.api def setWindows(vd, scr): 'Assign winTop, winBottom, win1 and win2 according to options.disp_splitwin_pct.' pct = options.disp_splitwin_pct # percent of window for secondary sheet (negative means bottom) h, w = scr.getmaxyx() n = abs(pct)*h//100 # on 100 line screen, pct = 25 means second window on lines 75-100. pct -25 -> lines 0-25 desiredConfig = dict(pct=pct, n=n, h=h, w=w) if vd.scrFull is not scr or vd.windowConfig != desiredConfig: vd.winTop = curses.newwin(n, w, 0, 0) vd.winTop.keypad(1) vd.winBottom = curses.newwin(h-n, w, n, 0) vd.winBottom.keypad(1) if pct == 0 or pct >= 100: # no second window vd.win1 = vd.winBottom # drawing to 0-line window causes problems vd.win2 = mock.MagicMock(__bool__=mock.Mock(return_value=False)) elif pct > 0: # second window line n to bottom vd.win1 = vd.winTop vd.win2 = vd.winBottom elif pct < 0: # second window line 0 to n vd.win1 = vd.winBottom vd.win2 = vd.winTop vd.windowConfig = desiredConfig vd.scrFull = scr return True @VisiData.api def draw_all(vd): 'Draw all sheets in all windows.' if not vd.sheets: return vd.draw_sheet(vd.win1, vd.sheets[0]) if vd.win2 and len(vd.sheets) > 1: vd.draw_sheet(vd.win2, vd.sheets[1]) else: vd.win2.erase() vd.win2.refresh() @VisiData.api def runresult(vd): try: err = vd.mainloop(vd.scrFull) if err: raise Exception(err) except ReturnValue as e: return e.args[0] @VisiData.api def mainloop(self, scr): 'Manage execution of keystrokes and subsequent redrawing of screen.' scr.timeout(curses_timeout) with contextlib.suppress(curses.error): curses.curs_set(0) numTimeouts = 0 prefixWaiting = False vd.scrFull = scr self.keystrokes = '' while True: if not self.sheets and self.currentReplay is None: return sheet = self.activeSheet threading.current_thread().sheet = sheet vd.drawThread = threading.current_thread() sheet.ensureLoaded() vd.setWindows(scr) self.draw_all() if vd._nextCommands: sheet.execCommand(vd._nextCommands.pop(0), keystrokes=self.keystrokes) continue keystroke = self.getkeystroke(scr, sheet) if not keystroke and prefixWaiting and ESC in self.keystrokes: # timeout ESC self.keystrokes = '' if keystroke: # wait until next keystroke to clear statuses and previous keystrokes numTimeouts = 0 if not prefixWaiting: self.keystrokes = '' self.statuses.clear() if keystroke == 'KEY_MOUSE': self.keystrokes = '' clicktype = '' try: devid, x, y, z, bstate = curses.getmouse() sheet.mouseX, sheet.mouseY = x, y if bstate & curses.BUTTON_CTRL: clicktype += "CTRL-" bstate &= ~curses.BUTTON_CTRL if bstate & curses.BUTTON_ALT: clicktype += "ALT-" bstate &= ~curses.BUTTON_ALT if bstate & curses.BUTTON_SHIFT: clicktype += "SHIFT-" bstate &= ~curses.BUTTON_SHIFT keystroke = clicktype + curses.mouseEvents.get(bstate, str(bstate)) f = self.getMouse(scr, x, y, keystroke) if f: if isinstance(f, str): for cmd in f.split(): sheet.execCommand(cmd) else: f(y, x, keystroke) self.keystrokes = keystroke keystroke = '' except curses.error: pass except Exception as e: self.exceptionCaught(e) if keystroke in self.keystrokes[:-1]: vd.warning('duplicate prefix') self.keystrokes = '' else: self.keystrokes += keystroke self.drawRightStatus(sheet._scr, sheet) # visible for commands that wait for input if not keystroke: # timeout instead of keypress pass elif keystroke == '^Q': return self.lastErrors and '\n'.join(self.lastErrors[-1]) elif vd.bindkeys._get(self.keystrokes): sheet.execCommand(self.keystrokes, keystrokes=self.keystrokes) prefixWaiting = False elif keystroke in self.allPrefixes: prefixWaiting = True else: vd.status('no command for "%s"' % (self.keystrokes)) prefixWaiting = False self.checkForFinishedThreads() sheet.checkCursorNoExceptions() # no idle redraw unless background threads are running time.sleep(0) # yield to other threads which may not have started yet if vd.unfinishedThreads: scr.timeout(curses_timeout) else: numTimeouts += 1 if numTimeouts > timeouts_before_idle: scr.timeout(-1) else: scr.timeout(curses_timeout) def setupcolors(stdscr, f, *args): curses.raw() # get control keys instead of signals curses.meta(1) # allow "8-bit chars" curses.mousemask(-1 if options.mouse_interval else 0) curses.mouseinterval(options.mouse_interval) curses.mouseEvents = {} for k in dir(curses): if k.startswith('BUTTON') or k in ('REPORT_MOUSE_POSITION', '2097152'): curses.mouseEvents[getattr(curses, k)] = k return f(stdscr, *args) def wrapper(f, *args): return curses.wrapper(setupcolors, f, *args) def run(*sheetlist): 'Main entry point; launches vdtui with the given sheets already pushed (last one is visible)' # reduce ESC timeout to 25ms. http://en.chys.info/2009/09/esdelay-ncurses/ os.putenv('ESCDELAY', '25') curses.use_env(True) ret = wrapper(cursesMain, sheetlist) if ret: print(ret) def cursesMain(_scr, sheetlist): 'Populate VisiData object with sheets from a given list.' colors.setup() for vs in sheetlist: vd.push(vs) vd.status('Ctrl+H opens help') return vd.mainloop(_scr) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1612763830.9899817 visidata-2.2.1/visidata/man/0000770000175000017500000000000000000000000016365 5ustar00kefalakefala00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612763790.0 visidata-2.2.1/visidata/man/vd.10000660000175000017500000012612600000000000017071 0ustar00kefalakefala00000000000000.Dd Feb 07, 2021 .Dt vd \&1 "Quick Reference Guide" .Os Linux/MacOS . .\" Long option with arg: .Lo f filetype format .\" Long flag: .Lo f filetype .de Lo .It Cm -\\$1 Ns , Cm --\\$2 Ns = Ns Ar \\$3 .. .de Lf .It Cm -\\$1 Ns , Cm --\\$2 .. .Sh NAME . .Nm VisiData .Nd a terminal utility for exploring and arranging tabular data . .Sh SYNOPSIS . .Nm vd .Op Ar options .Op Ar input No ... . .Nm vd .Op Ar options .Cm --play Ar cmdlog .Op Cm -w Ar waitsecs .Op Cm --batch .Op Cm -o Ar output .Op Ar field Ns Cm = Ns Ar value . .Nm vd .Op Ar options .Op Ar input No ... .Cm + Ns Ar toplevel Ns : Ns Ar subsheet Ns : Ns Ar col Ns : Ns Ar row . .Sh DESCRIPTION .Nm VisiData No is an easy-to-use multipurpose tool to explore, clean, edit, and restructure data. Rows can be selected, filtered, and grouped; columns can be rearranged, transformed, and derived via regex or Python expressions; and workflows can be saved, documented, and replayed. . .Ss REPLAY MODE .Bl -tag -width XXXXXXXXXXXXXXXXXXXXXX -compact .Lo p play cmdlog .No replay a saved Ar cmdlog No within the interface . .Lo w replay-wait seconds .No wait Ar seconds No between commands . .Lf b batch replay in batch mode (with no interface) . .Lo o output file .No save final visible sheet to Ar file No as .tsv . .It Sy --replay-movement .No toggle Sy --play No to move cursor cell-by-cell .It Ar field Ns Cm = Ns Ar value .No replace \&"{ Ns Ar field Ns }\&" in Ar cmdlog No contents with Ar value .El . .Ss Commands During Replay .Bl -tag -width XXXXXXXXXXXXXXXXXXX -compact -offset XXX .It Sy ^U pause/resume replay .It Sy ^N execute next row in replaying sheet .It Sy ^K cancel current replay .El . .Ss GLOBAL COMMANDS .No All keystrokes are case sensitive. The Sy ^ No prefix is shorthand for Sy Ctrl Ns . .Pp .Ss Keystrokes to start off with .Bl -tag -width XXXXXXXXXXXXXXX -compact .It Ic " ^Q" abort program immediately .It Ic " ^C" cancel user input or abort all async threads on current sheet .It Ic "g^C" abort all secondary threads .It Ic " q" quit current sheet .It Ic " gq" quit all sheets (clean exit) .Pp .It Ic " ^H" view this man page .It Ic "z^H" view sheet of command longnames and keybindings .It Ic "Space" Ar longname .No execute command by its Ar longname .Pp .It Ic " U" .No undo the most recent modification ( requires enabled Sy options.undo Ns ) .It Ic " R" .No redo the most recent undo ( requires enabled Sy options.undo Ns ) .El .Ss "Cursor Movement" . .Bl -tag -width XXXXXXXXXXXXXXX -compact . .It Ic "Arrow PgUp Home" go as expected .It Ic " h j k l" go left/down/up/right .It Ic "gh gj gk gl" go all the way to the left/bottom/top/right of sheet .It Ic " G gg" go all the way to the bottom/top of sheet .It Ic "^B ^F" scroll one page back/forward .It Ic "zz" scroll current row to center of screen .Pp .It Ic "^^" No (Ctrl+^) jump to previous sheet (swaps with current sheet) .Pp .It Ic " / ?" Ar regex .No search for Ar regex No forward/backward in current column .It Ic "g/ g?" Ar regex .No search for Ar regex No forward/backward over all visible columns .It Ic "z/ z?" Ar expr .No search by Python Ar expr No forward/backward in current column (with column names as variables) .It Ic " n N" go to next/previous match from last regex search .Pp .It Ic " < >" go up/down current column to next value .It Ic "z< z>" go up/down current column to next null value .It Ic " { }" go up/down current column to next selected row . .El .Pp .Bl -tag -width XXXXXXXXXXXXXXX -compact .Pp .It Ic " c" Ar regex .No go to next column with name matching Ar regex .It Ic " r" Ar regex .No go to next row with key matching Ar regex .It Ic "zc zr" Ar number .No go to column/row Ar number No (0-based) .Pp .It Ic " H J K L" slide current row/column left/down/up/right .It Ic "gH gJ gK gL" slide current row/column all the way to the left/bottom/top/right of sheet .It Ic "zH zJ zK zK" Ar number .No slide current row/column Ar number No positions to the left/down/up/right .Pp .It Ic "zh zj zk zl" scroll one left/down/up/right .El . .Ss Column Manipulation . .Bl -tag -width XXXXXXXXXXXXXXX -compact . .It Ic " _" Ns " (underscore)" toggle width of current column between full and default width .It Ic " g_" toggle widths of all visible columns between full and default width .It Ic " z_" Ar number .No adjust width of current column to Ar number .It Ic "gz_" Ar number adjust widths of all visible columns to Ar number .Pp .It Ic " -" Ns " (hyphen)" hide current column .It Ic "z-" Ns reduce width of current column by half .It Ic "gv" Ns unhide all columns .Pp .It Ic "! z!" Ns toggle/unset current column as a key column .It Ic "~ # % $ @ z#" set type of current column to str/int/float/currency/date/len .It Ic " ^" edit name of current column .It Ic " g^" set names of all unnamed visible columns to contents of selected rows (or current row) .It Ic " z^" set name of current column to combined contents of current cell in selected rows (or current row) .It Ic "gz^" set name of all visible columns to combined contents of current column for selected rows (or current row) .Pp .It Ic " =" Ar expr .No create new column from Python Ar expr Ns , with column names, and attributes, as variables .It Ic " g=" Ar expr .No set current column for selected rows to result of Python Ar expr .It Ic "gz=" Ar expr .No set current column for selected rows to the items in result of Python sequence Ar expr .It Ic " z=" Ar expr .No evaluate Python expression on current row and set current cell with result of Python Ar expr .Pp .It " i" (iota) .No add column with incremental values .It " gi" .No set current column for selected rows to incremental values .It " zi" Ar step .No add column with values at increment Ar step .It "gzi" Ar step .No set current column for selected rows at increment Ar step .El .Pp .Bl -tag -width XXXXXXXXXXXXXXX -compact .It Ic " '" Ns " (tick)" add a frozen copy of current column with all cells evaluated .It Ic "g'" open a frozen copy of current sheet with all visible columns evaluated .It Ic "z' gz'" add/reset cache for current/all visible column(s) .Pp .It Ic " \&:" Ar regex .No add new columns from Ar regex No split; number of columns determined by example row at cursor .It Ic " \&;" Ar regex .No add new columns from capture groups of Ar regex No (also requires example row) .It Ic "z" Ns Ic "\&;" Ar expr .No create new column from bash Ar expr Ns , with Sy $ Ns columnNames as variables .It Ic " *" Ar regex Ns Sy / Ns Ar subst .No add column derived from current column, replacing Ar regex No with Ar subst No (may include Sy \e1 No backrefs) .It Ic "g* gz*" Ar regex Ns Sy / Ns Ar subst .No modify selected rows in current/all visible column(s), replacing Ar regex No with Ar subst No (may include Sy \e1 No backrefs) .Pp .It Ic " ( g(" .No expand current/all visible column(s) of lists (e.g. Sy [3] Ns ) or dicts (e.g. Sy {3} Ns ) fully .It Ic "z( gz(" Ar depth .No expand current/all visible column(s) of lists (e.g. Sy [3] Ns ) or dicts (e.g. Sy {3} Ns ) to given Ar depth ( Ar 0 Ns = fully) .It Ic " )" unexpand current column; restore original column and remove other columns at this level .It Ic "zM" .No row-wise expand current column of lists (e.g. Sy [3] Ns ) or dicts (e.g. Sy {3} Ns ) within that column .El .Ss Row Selection . .Bl -tag -width XXXXXXXXXXXXXXX -compact . .It Ic " s t u" select/toggle/unselect current row .It Ic " gs gt gu" select/toggle/unselect all rows .It Ic " zs zt zu" select/toggle/unselect all rows from top to cursor .It Ic "gzs gzt gzu" select/toggle/unselect all rows from cursor to bottom .It Ic " | \e\ " Ns Ar regex .No select/unselect rows matching Ar regex No in current column .It Ic "g| g\e\ " Ns Ar regex .No select/unselect rows matching Ar regex No in any visible column .It Ic "z| z\e\ " Ns Ar expr .No select/unselect rows matching Python Ar expr No in any visible column .It Ic " \&," Ns " (comma)" select rows matching display value of current cell in current column .It Ic "g\&," select rows matching display value of current row in all visible columns .It Ic "z\&, gz\&," select rows matching typed value of current cell/row in current column/all visible columns . .El . . .Ss Row Sorting/Filtering . .Bl -tag -width XXXXXXXXXXXXXXX -compact . .It Ic " [ ]" sort ascending/descending by current column; replace any existing sort criteria .It Ic " g[ g]" sort ascending/descending by all key columns; replace any existing sort criteria .It Ic " z[ z]" sort ascending/descending by current column; add to existing sort criteria .It Ic "gz[ gz]" sort ascending/descending by all key columns; add to existing sort criteria .It Ic " \&"" open duplicate sheet with only selected rows .It Ic "g\&"" open duplicate sheet with all rows .It Ic "gz\&"" open duplicate sheet with deepcopy of selected rows .El .Ss Editing Rows and Cells . .Bl -tag -width XXXXXXXXXXXXXXX -compact .It Ic " a za" append a blank row/column; appended columns cannot be copied to clipboard .It Ic " ga gza" Ar number .No append Ar number No blank rows/columns .It Ic " d gd" delete (cut) current/selected row(s) and move to clipboard .It Ic " y gy" yank (copy) current/all selected row(s) to clipboard .It Ic " zy gzy" yank (copy) contents of current column for current/selected row(s) to clipboard .It Ic " zd gzd" delete (cut) contents of current column for current/selected row(s) and move to clipboard .It Ic " p P" paste clipboard rows after/before current row .It Ic " zp gzp" set cells of current column for current/selected row(s) to last clipboard value .It Ic " Y gY" .No yank (copy) current/all selected row(s) to system clipboard (using Sy options.clipboard_copy_cmd Ns ) .It Ic " zY gzY" .No yank (copy) contents of current column for current/selected row(s) to system clipboard (using Sy options.clipboard_copy_cmd Ns ) .It Ic " f" fill null cells in current column with contents of non-null cells up the current column . . .It Ic " e" Ar text edit contents of current cell .It Ic " ge" Ar text .No set contents of current column for selected rows to Ar text . .El . .Ss " Commands While Editing Input" .Bl -tag -width XXXXXXXXXXXXXXX -compact -offset XXX .It Ic "Enter ^C" accept/abort input .It Ic ^O open external $EDITOR to edit contents .It Ic ^R reload initial value .It Ic "^A ^E" go to beginning/end of line .It Ic "^B ^F" go back/forward one character .It Ic "^\[u2190] ^\[u2192]" No (arrow) go back/forward one word .It Ic "^H ^D" delete previous/current character .It Ic ^T transpose previous and current characters .It Ic "^U ^K" clear from cursor to beginning/end of line .It Ic "^Y" paste from cell clipboard .It Ic "Backspace Del" delete previous/current character .It Ic Insert toggle insert mode .It Ic "Up Down" set contents to previous/next in history .It Ic "Tab Shift+Tab" autocomplete input (when available) .It Ic "Shift+Arrow" .No move cursor in direction of Sy Arrow No and re-enter edit mode . .El . .Ss Data Toolkit .Bl -tag -width XXXXXXXXXXXXXXX -compact .It Ic " o" Ar input open .Ar input No in Sy VisiData .It Ic "^S g^S" Ar filename .No save current/all sheet(s) to Ar filename No in format determined by extension (default .tsv) .It "" .No Note: if the format does not support multisave, or the Ar filename No ends in a Sy / Ns , a directory will be created. .It Ic "z^S" Ar filename .No save current column only to Ar filename No in format determined by extension (default .tsv) .It Ic "^D" Ar filename.vd .No save Sy CommandLog No to Ar filename.vd No file .It Ic "A" .No open new blank sheet with one column .It Ic "T" .No open new sheet that has rows and columns of current sheet transposed .Pp .It Ic " +" Ar aggregator .No add Ar aggregator No to current column (see Sy "Frequency Table" Ns ) .It Ic "z+" Ar aggregator .No display result of Ar aggregator No over values in selected rows for current column .It Ic " &" .No concatenate top two sheets in Sy Sheets Stack .It Ic "g&" .No concatenate all sheets in Sy Sheets Stack .Pp .El .Ss Data Visualization .Bl -tag -width XXXXXXXXXXXXX -compact .It Ic " ." No (dot) .No plot current numeric column vs key columns. The numeric key column is used for the x-axis; categorical key column values determine color. .It Ic "g." .No plot a graph of all visible numeric columns vs key columns. .Pp .El .No If rows on the current sheet represent plottable coordinates (as in .shp or vector .mbtiles sources), .Ic " ." No plots the current row, and Ic "g." No plots all selected rows (or all rows if none selected). .Ss " Canvas-specific Commands" .Bl -tag -width XXXXXXXXXXXXXXXXXX -compact -offset XXX .It Ic " + -" increase/decrease zoom level, centered on cursor .It Ic " _" No (underscore) zoom to fit full extent .It Ic "z_" No (underscore) set aspect ratio .It Ic " x" Ar xmin xmax .No set Ar xmin Ns / Ns Ar xmax No on graph .It Ic " y" Ar ymin ymax .No set Ar ymin Ns / Ns Ar ymax No on graph .It Ic " s t u" select/toggle/unselect rows on source sheet contained within canvas cursor .It Ic "gs gt gu" select/toggle/unselect rows on source sheet visible on screen .It Ic " d" delete rows on source sheet contained within canvas cursor .It Ic "gd" delete rows on source sheet visible on screen .It Ic " Enter" open sheet of source rows contained within canvas cursor .It Ic "gEnter" open sheet of source rows visible on screen .It Ic " 1" No - Ic "9" toggle display of layers .It Ic "^L" redraw all pixels on canvas .It Ic " v" .No toggle Ic show_graph_labels No option .It Ic "mouse scrollwheel" zoom in/out of canvas .It Ic "left click-drag" set canvas cursor .It Ic "right click-drag" scroll canvas .El .Ss Split Screen .Bl -tag -width XXXXXXXXXXXXX -compact .It Ic " Z" .No split screen in half, so that second sheet on the stack is visible in a second pane .It Ic "zZ" .No split screen, and queries for height of second pane .El .Ss " Split Window specific Commands" .Bl -tag -width XXXXXXXXXXXXXXXXXX -compact -offset XXX .It Ic "gZ" .No close an already split screen, current pane full screens .It Ic "Tab" .No jump to other pane .It Ic "^^" No (Ctrl+^) .No swap which sheet is in current pane .Pp .El .Ss Other Commands . .Bl -tag -width XXXXXXXXXXXXXXX -compact .It Ic "Q" .No quit current sheet and remove it from the Sy CommandLog .It Ic "v" toggle sheet-specific visibility (multi-line rows on Sheet, legends/axes on Graph) .Pp .Pp .It Ic " ^E g^E" view traceback for most recent error(s) .It Ic "z^E" view traceback for error in current cell .Pp .It Ic " ^L" refresh screen .It Ic " ^R" reload current sheet .It Ic "z^R" clear cache for current column .It Ic " ^Z" suspend VisiData process .It Ic " ^G" show cursor position and bounds of current sheet on status line .It Ic " ^V" show version and copyright information on status line .It Ic " ^P" .No open Sy Status History .It "m" Ar keystroke .No first, begin recording macro; second, prompt for Ar keystroke No, and complete recording. Macro can then be executed everytime provided keystroke is used. Will override existing keybinding. Macros will run on current row, column, sheet. .It "gm" .No open an index of all existing macros. Can be directly viewed with Sy Enter Ns , and then modified with Sy ^S Ns . . .El .Pp .Bl -tag -width XXXXXXXXXXXXXXX -compact .It Ic " ^Y z^Y g^Y" open current row/cell/sheet as Python object .It Ic " ^X" Ar expr .No evaluate Python Ar expr No and opens result as Python object .It Ic "z^X" Ar expr .No evaluate Python Ar expr Ns , in context of current row, and open result as Python object .It Ic "g^X" Ar stmt .No execute Python Ar stmt No in the global scope .El . .Ss Internal Sheets List .Bl -tag -width Xx -compact .It Sy " \&." .Sy VisiDataMenu No (Shift+V) " browse list of core sheets" .It Sy " \&." .Sy Directory Sheet No " browse properties of files in a directory" .It Sy " \&." .Sy Plugins Sheet No " browse, install, and (de)activate plugins" .It " " .It Sy Metasheets .It Sy " \&." .Sy Columns Sheet No (Shift+C) " edit column properties" .It Sy " \&." .Sy Sheets Sheet No (Shift+S) " jump between sheets or join them together" .It Sy " \&." .Sy Options Sheet No (Shift+O) " edit configuration options" .It Sy " \&." .Sy Commandlog No (Shift+D) " modify and save commands for replay" .It Sy " \&." .Sy Error Sheet No (Ctrl+E) " view last error" .It Sy " \&." .Sy Status History No (Ctrl+P) " view history of status messages" .It Sy " \&." .Sy Threads Sheet No (Ctrl+T) " view, cancel, and profile asynchronous threads" .Pp .It Sy Derived Sheets .It Sy " \&." .Sy Frequency Table No (Shift+F) " group rows by column value, with aggregations of other columns" .It Sy " \&." .Sy Describe Sheet No (Shift+I) " view summary statistics for each column" .It Sy " \&." .Sy Pivot Table No (Shift+W) " group rows by key and summarize current column" .It Sy " \&." .Sy Melted Sheet No (Shift+M) " unpivot non-key columns into variable/value columns" .It Sy " \&." .Sy Transposed Sheet No (Shift+T) " open new sheet with rows and columns transposed" .El . .Ss INTERNAL SHEETS .Ss VisiDataMenu (Shift+V) .Bl -inset -compact .It (sheet-specific commands) .El .Bl -tag -width XXXXXXXXXXXXXXX -compact -offset XXX .It Ic Enter .No load sheet in current row .El .Ss Directory Sheet .Bl -inset -compact .It (global commands) .El .Bl -tag -width XXXXXXXXXXXXXXX -compact -offset XXX .It Ic Space Ar open-dir-current .No open the Sy Directory Sheet No for the current directory .El .Bl -inset -compact .It (sheet-specific commands) .El .Bl -tag -width XXXXXXXXXXXXXXX -compact -offset XXX .It Ic "Enter gEnter" open current/selected file(s) as new sheet(s) .It Ic " ^O g^O" open current/selected file(s) in external $EDITOR .It Ic " ^R z^R gz^R" reload information for all/current/selected file(s) .El . .Ss Plugins Sheet .Bl -inset -compact .It Browse through a list of available plugins. VisiData needs to be restarted before plugin activation takes effect. Installation may require internet access. .El .Bl -inset -compact .It (global commands) .El .Bl -tag -width XXXXXXXXXXXXXXX -compact -offset XXX .It Ic Space Ar open-plugins .No open the Sy Plugins Sheet .El .Bl -inset -compact .It (sheet-specific commands) .El .Bl -tag -width XXXXXXXXXXXXXXX -compact -offset XXX .It Ic "a" install and activate current plugin .It Ic "d" deactivate current plugin .El .Ss METASHEETS .Ss Columns Sheet (Shift+C) .Bl -inset -compact .It Properties of columns on the source sheet can be changed with standard editing commands ( Ns Sy e ge g= Del Ns ) on the Sy Columns Sheet Ns . Multiple aggregators can be set by listing them (separated by spaces) in the aggregators column. The 'g' commands affect the selected rows, which are the literal columns on the source sheet. .El .Bl -inset -compact .It (global commands) .El .Bl -tag -width XXXXXXXXXXXXXXX -compact -offset XXX .It Ic gC .No open Sy Columns Sheet No with all visible columns from all sheets .El .Bl -inset -compact .It (sheet-specific commands) .El .Bl -tag -width XXXXXXXXXXXXXXX -compact -offset XXX .It Ic " &" add column from concatenating selected source columns .It Ic "g! gz!" toggle/unset selected columns as key columns on source sheet .It Ic "g+" Ar aggregator add Ar aggregator No to selected source columns .It Ic "g-" No (hyphen) hide selected columns on source sheet .It Ic "g~ g# g% g$ g@ gz# z%" set type of selected columns on source sheet to str/int/float/currency/date/len/floatsi .It Ic " Enter" .No open a Sy Frequency Table No sheet grouped by column referenced in current row .El . .Ss Sheets Sheet (Shift+S) .Bl -inset -compact .It open Sy Sheets Stack Ns , which contains only the active sheets on the current stack .It (global commands) .El .Bl -tag -width XXXXXXXXXXXXXXX -compact -offset XXX .It Ic gS .No open Sy Sheets Sheet Ns , which contains all sheets from current session, active and inactive .It Ic "Alt" Ar number .No jump to sheet Ar number Ns .El .Bl -inset -compact .It (sheet-specific commands) .El .Bl -tag -width XXXXXXXXXXXXXXX -compact -offset XXX .It Ic " Enter" jump to sheet referenced in current row .It Ic "gEnter" push selected sheets to top of sheet stack .It Ic " a" add row to reference a new blank sheet .It Ic "gC gI" .No open Sy Columns Sheet Ns / Ns Sy Describe Sheet No with all visible columns from selected sheets .It Ic "g^R" .No reload all selected sheets .It Ic "z^C gz^C" abort async threads for current/selected sheets(s) .It Ic "g^S" save selected or all sheets .It Ic " &" Ar jointype .No merge selected sheets with visible columns from all, keeping rows according to Ar jointype Ns : .El .Bl -tag -width x -compact -offset XXXXXXXXXXXXXXXXXXXX .It Sy "\&." .Sy inner No " keep only rows which match keys on all sheets" .It Sy "\&." .Sy outer No " keep all rows from first selected sheet" .It Sy "\&." .Sy full No " keep all rows from all sheets (union)" .It Sy "\&." .Sy diff No " keep only rows NOT in all sheets" .It Sy "\&." .Sy append No "keep all rows from all sheets (concatenation)" .It Sy "\&." .Sy extend No "copy first selected sheet, keeping all rows and sheet type, and extend with columns from other sheets" .It Sy "\&." .Sy merge No " mostly keep all rows from first selected sheet, except prioritise cells with non-null/non-error values" .El . .Ss Options Sheet (Shift+O) .Bl -inset -compact .It (global commands) .El .Bl -tag -width XXXXXXXXXXXXXXX -compact -offset XXX .It Ic Shift+O .No edit global options (apply to Sy all sheets Ns ) .It Ic zO .No edit sheet options (apply to Sy current sheet No only) .It Ic gO .No open Sy options.config No as Sy TextSheet .El .Bl -inset -compact .It (sheet-specific commands) .El .Bl -tag -width XXXXXXXXXXXXXXX -compact -offset XXX .It Ic "Enter e" edit option at current row .It Ic "d" remove option override for this context .El . .Ss CommandLog (Shift+D) .Bl -inset -compact .It (global commands) .El .Bl -tag -width XXXXXXXXXXXXXXX -compact -offset XXX .It Ic D .No open current sheet's Sy CommandLog No with all other loose ends removed; includes commands from parent sheets .It Ic gD .No open global Sy CommandLog No for all commands executed in the current session .It Ic zD .No open current sheet's Sy CommandLog No with the parent sheets commands' removed .El .Bl -inset -compact .It (sheet-specific commands) .El .Bl -tag -width XXXXXXXXXXXXXXX -compact -offset XXX .It Ic " x" replay command in current row .It Ic " gx" replay contents of entire CommandLog .It Ic " ^C" abort replay .El . .Ss DERIVED SHEETS .Ss Frequency Table (Shift+F) .Bl -inset -compact .It A Sy Frequency Table No groups rows by one or more columns, and includes summary columns for those with aggregators. .It (global commands) .El .Bl -tag -width XXXXXXXXXXXXXXX -compact -offset XXX .It Ic gF open Frequency Table, grouped by all key columns on source sheet .It Ic zF open one-line summary for all rows and selected rows .El .Bl -inset -compact .It (sheet-specific commands) .El .Bl -tag -width XXXXXXXXXXXXXXX -compact -offset XXX .It Ic " s t u" select/toggle/unselect these entries in source sheet .It Ic " Enter gEnter" open copy of source sheet with rows that are grouped in current cell / selected rows .El . .Ss Describe Sheet (Shift+I) .Bl -inset -compact .It A Sy Describe Sheet No contains descriptive statistics for all visible columns. .It (global commands) .El .Bl -tag -width XXXXXXXXXXXXXXX -compact -offset XXX .It Ic gI .No open Sy Describe Sheet No for all visible columns on all sheets .El .Bl -inset -compact .It (sheet-specific commands) .El .Bl -tag -width XXXXXXXXXXXXXXX -compact -offset XXX .It Ic "zs zu" select/unselect rows on source sheet that are being described in current cell .It Ic " !" toggle/unset current column as a key column on source sheet .It Ic " Enter" .No open a Sy Frequency Table No sheet grouped on column referenced in current row .It Ic "zEnter" open copy of source sheet with rows described in current cell .El . .Ss Pivot Table (Shift+W) .Bl -inset -compact .It Set key column(s) and aggregators on column(s) before pressing Sy Shift+W No on the column to pivot. .It (sheet-specific commands) .El .Bl -tag -width XXXXXXXXXXXXXXX -compact -offset XXX .It Ic " Enter" open sheet of source rows aggregated in current pivot row .It Ic "zEnter" open sheet of source rows aggregated in current pivot cell .El .Ss Melted Sheet (Shift+M) .Bl -inset -compact .It Open Melted Sheet (unpivot), with key columns retained and all non-key columns reduced to Variable-Value rows. .It (global commands) .El .Bl -tag -width XXXXXXXXXXXXXXX -compact -offset XXX .It Ic "gM" Ar regex .No open Melted Sheet (unpivot), with key columns retained and Ar regex No capture groups determining how the non-key columns will be reduced to Variable-Value rows. .El .Ss Python Object Sheet (^X ^Y g^Y z^Y) .Bl -inset -compact .It (sheet-specific commands) .El .Bl -tag -width XXXXXXXXXXXXXXX -compact -offset XXX .It Ic " Enter" dive further into Python object .It Ic " v" toggle show/hide for methods and hidden properties .It Ic "gv zv" show/hide methods and hidden properties .El . .Sh COMMANDLINE OPTIONS .No Add Sy -n Ns / Ns Sy --nonglobal No to make subsequent CLI options "sheet-specific" (applying only to paths specified directly on the CLI). By default, CLI options apply to all sheets. .Pp .No Options can also be set via the Ar Options Sheet No or a Ar .visidatarc No (see Sx FILES Ns ). .Pp .Bl -tag -width XXXXXXXXXXXXXXXXXXXXXXXXXXX -compact .It Cm -P Ns = Ns Ar longname .No preplay Ar longname No before replay or regular launch; limited to Sy Base Sheet No bound commands .It Cm + Ns Ar toplevel Ns : Ns Ar subsheet Ns : Ns Ar col Ns : Ns Ar row .No launch vd with Ar subsheet No of Ar toplevel No at top-of-stack, and cursor at Ar col No and Ar row Ns ; all arguments are optional .Pp .Lo f filetype filetype .No "tsv " set loader to use for .Ar filetype instead of file extension . .Lo y confirm-overwrite F .No "True " overwrite existing files without confirmation . . .El .Bl -tag -width XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -compact .It Sy --mouse-interval Ns = Ns Ar "int " No "1" max time between press/release for click (ms) .It Sy --null-value Ns = Ns Ar "NoneType " No "None" a value to be counted as null .It Sy --undo Ns = Ns Ar "bool " No "True" enable undo/redo .It Sy --col-cache-size Ns = Ns Ar "int " No "0" max number of cache entries in each cached column .It Sy --clean-names No " False" clean column/sheet names to be valid Python identifiers .It Sy --default-width Ns = Ns Ar "int " No "20" default column width .It Sy --default-height Ns = Ns Ar "int " No "10" default column height .It Sy --textwrap-cells Ns = Ns Ar "bool " No "True" wordwrap text for multiline rows .It Sy --quitguard No " False" confirm before quitting last sheet .It Sy --debug No " False" exit on error and display stacktrace .It Sy --skip Ns = Ns Ar "int " No "0" skip N rows before header .It Sy --header Ns = Ns Ar "int " No "1" parse first N rows as column names .It Sy --load-lazy No " False" load subsheets always (False) or lazily (True) .It Sy --force-256-colors No " False" use 256 colors even if curses reports fewer .It Sy --use-default-colors No " False" curses use default terminal colors .It Sy --note-pending Ns = Ns Ar "str " No "\[u231B]" note to display for pending cells .It Sy --note-format-exc Ns = Ns Ar "str " No "?" cell note for an exception during formatting .It Sy --note-getter-exc Ns = Ns Ar "str " No "!" cell note for an exception during computation .It Sy --note-type-exc Ns = Ns Ar "str " No "!" cell note for an exception during type conversion .It Sy --scroll-incr Ns = Ns Ar "int " No "3" amount to scroll with scrollwheel .It Sy --name-joiner Ns = Ns Ar "str " No "_" string to join sheet or column names .It Sy --value-joiner Ns = Ns Ar "str " No " " string to join display values .It Sy --wrap No " False" wrap text to fit window width on TextSheet .It Sy --save-filetype Ns = Ns Ar "str " No "tsv" specify default file type to save as .It Sy --profile Ns = Ns Ar "str " No "" filename to save binary profiling data .It Sy --min-memory-mb Ns = Ns Ar "int " No "0" minimum memory to continue loading and async processing .It Sy --input-history Ns = Ns Ar "str " No "" basename of file to store persistent input history .It Sy --encoding Ns = Ns Ar "str " No "utf-8" encoding passed to codecs.open .It Sy --encoding-errors Ns = Ns Ar "str " No "surrogateescape" encoding_errors passed to codecs.open .It Sy --bulk-select-clear No " False" clear selected rows before new bulk selections .It Sy --some-selected-rows No " False" if no rows selected, if True, someSelectedRows returns all rows; if False, fails .It Sy --delimiter Ns = Ns Ar "str " No " " field delimiter to use for tsv/usv filetype .It Sy --row-delimiter Ns = Ns Ar "str " No " " row delimiter to use for tsv/usv filetype .It Sy --tsv-safe-newline Ns = Ns Ar "str " No "" replacement for newline character when saving to tsv .It Sy --tsv-safe-tab Ns = Ns Ar "str " No "" replacement for tab character when saving to tsv .It Sy --visibility Ns = Ns Ar "int " No "0" visibility level (0=low, 1=high) .It Sy --expand-col-scanrows Ns = Ns Ar "int " No "1000" number of rows to check when expanding columns (0 = all) .It Sy --json-indent Ns = Ns Ar "NoneType " No "None" indent to use when saving json .It Sy --json-sort-keys No " False" sort object keys when saving to json .It Sy --default-colname Ns = Ns Ar "str " No "" column name to use for non-dict rows .It Sy --filetype Ns = Ns Ar "str " No "" specify file type .It Sy --confirm-overwrite Ns = Ns Ar "bool " No "True" whether to prompt for overwrite confirmation on save .It Sy --safe-error Ns = Ns Ar "str " No "#ERR" error string to use while saving .It Sy --clipboard-copy-cmd Ns = Ns Ar "str " No "" command to copy stdin to system clipboard .It Sy --clipboard-paste-cmd Ns = Ns Ar "str " No "" command to get contents of system clipboard .It Sy --fancy-chooser No " False" a nicer selection interface for aggregators and jointype .It Sy --describe-aggrs Ns = Ns Ar "str " No "mean stdev" numeric aggregators to calculate on Describe sheet .It Sy --histogram-bins Ns = Ns Ar "int " No "0" number of bins for histogram of numeric columns .It Sy --numeric-binning No " False" bin numeric columns into ranges .It Sy --replay-wait Ns = Ns Ar "float " No "0.0" time to wait between replayed commands, in seconds .It Sy --replay-movement No " False" insert movements during replay .It Sy --visidata-dir Ns = Ns Ar "str " No "~/.visidata/" directory to load and store additional files .It Sy --rowkey-prefix Ns = Ns Ar "str " No "\[u30AD]" string prefix for rowkey in the cmdlog .It Sy --cmdlog-histfile Ns = Ns Ar "str " No "" file to autorecord each cmdlog action to .It Sy --regex-flags Ns = Ns Ar "str " No "I" flags to pass to re.compile() [AILMSUX] .It Sy --regex-maxsplit Ns = Ns Ar "int " No "0" maxsplit to pass to regex.split .It Sy --default-sample-size Ns = Ns Ar "int " No "100" number of rows to sample for regex.split .It Sy --show-graph-labels Ns = Ns Ar "bool " No "True" show axes and legend on graph .It Sy --plot-colors Ns = Ns Ar "str " No "" list of distinct colors to use for plotting distinct objects .It Sy --zoom-incr Ns = Ns Ar "float " No "2.0" amount to multiply current zoomlevel when zooming .It Sy --motd-url Ns = Ns Ar "str " No "" source of randomized startup messages .It Sy --dir-recurse No " False" walk source path recursively on DirSheet .It Sy --dir-hidden No " False" load hidden files on DirSheet .It Sy --config Ns = Ns Ar "str " No "~/.visidatarc" config file to exec in Python .It Sy --play Ns = Ns Ar "str " No "" file.vd to replay .It Sy --batch No " False" replay in batch mode (with no interface and all status sent to stdout) .It Sy --output Ns = Ns Ar "NoneType " No "None" save the final visible sheet to output at the end of replay .It Sy --preplay Ns = Ns Ar "str " No "" longnames to preplay before replay .It Sy --imports Ns = Ns Ar "str " No "plugins" imports to preload before .visidatarc (command-line only) .It Sy --incr-base Ns = Ns Ar "float " No "1.0" start value for column increments .It Sy --csv-dialect Ns = Ns Ar "str " No "excel" dialect passed to csv.reader .It Sy --csv-delimiter Ns = Ns Ar "str " No "," delimiter passed to csv.reader .It Sy --csv-quotechar Ns = Ns Ar "str " No """ quotechar passed to csv.reader .It Sy --csv-skipinitialspace Ns = Ns Ar "bool " No "True" skipinitialspace passed to csv.reader .It Sy --csv-escapechar Ns = Ns Ar "NoneType " No "None" escapechar passed to csv.reader .It Sy --csv-lineterminator Ns = Ns Ar "str " No " " lineterminator passed to csv.writer .It Sy --safety-first No " False" sanitize input/output to handle edge cases, with a performance cost .It Sy --fixed-rows Ns = Ns Ar "int " No "1000" number of rows to check for fixed width columns .It Sy --fixed-maxcols Ns = Ns Ar "int " No "0" max number of fixed-width columns to create (0 is no max) .It Sy --postgres-schema Ns = Ns Ar "str " No "public" The desired schema for the Postgres database .It Sy --http-max-next Ns = Ns Ar "int " No "0" max next.url pages to follow in http response .It Sy --html-title Ns = Ns Ar "str " No "

{sheet.name}

" table header when saving to html .It Sy --pcap-internet Ns = Ns Ar "str " No "n" (y/s/n) if save_dot includes all internet hosts separately (y), combined (s), or does not include the internet (n) .It Sy --graphviz-edge-labels Ns = Ns Ar "bool " No "True" whether to include edge labels on graphviz diagrams .It Sy --pdf-tables No " False" parse PDF for tables instead of pages of text .It Sy --plugins-url Ns = Ns Ar "str " No "https://visidata.org/plugins/plugins.jsonl" source of plugins sheet .El . .Ss DISPLAY OPTIONS .No Display options can only be set via the Sx Options Sheet No or a Pa .visidatarc No (see Sx FILES Ns ). .Pp . .Bl -tag -width XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -compact .It Sy "disp_splitwin_pct " No "0" height of second sheet on screen .It Sy "disp_currency_fmt " No "%.02f" default fmtstr to format for currency values .It Sy "disp_float_fmt " No "{:.02f}" default fmtstr to format for float values .It Sy "disp_int_fmt " No "{:.0f}" default fmtstr to format for int values .It Sy "disp_date_fmt " No "%Y-%m-%d" default fmtstr to strftime for date values .It Sy "disp_note_none " No "\[u2300]" visible contents of a cell whose value is None .It Sy "disp_truncator " No "\[u2026]" indicator that the contents are only partially visible .It Sy "disp_oddspace " No "\[u00B7]" displayable character for odd whitespace .It Sy "disp_more_left " No "<" header note indicating more columns to the left .It Sy "disp_more_right " No ">" header note indicating more columns to the right .It Sy "disp_error_val " No "" displayed contents for computation exception .It Sy "disp_ambig_width " No "1" width to use for unicode chars marked ambiguous .It Sy "disp_pending " No "" string to display in pending cells .It Sy "color_note_pending " No "bold magenta" color of note in pending cells .It Sy "color_note_type " No "226 yellow" color of cell note for non-str types in anytype columns .It Sy "color_note_row " No "220 yellow" color of row note on left edge .It Sy "disp_column_sep " No "|" separator between columns .It Sy "disp_keycol_sep " No "\[u2551]" separator between key columns and rest of columns .It Sy "disp_rowtop_sep " No "|" .It Sy "disp_rowmid_sep " No "\[u205D]" .It Sy "disp_rowbot_sep " No "\[u205D]" .It Sy "disp_rowend_sep " No "\[u2551]" .It Sy "disp_keytop_sep " No "\[u2551]" .It Sy "disp_keymid_sep " No "\[u2551]" .It Sy "disp_keybot_sep " No "\[u2551]" .It Sy "disp_endtop_sep " No "\[u2551]" .It Sy "disp_endmid_sep " No "\[u2551]" .It Sy "disp_endbot_sep " No "\[u2551]" .It Sy "disp_selected_note " No "\[u2022]" .It Sy "disp_sort_asc " No "\[u2191]\[u219F]\[u21DE]\[u21E1]\[u21E7]\[u21D1]" characters for ascending sort .It Sy "disp_sort_desc " No "\[u2193]\[u21A1]\[u21DF]\[u21E3]\[u21E9]\[u21D3]" characters for descending sort .It Sy "color_default " No "normal" the default color .It Sy "color_default_hdr " No "bold" color of the column headers .It Sy "color_bottom_hdr " No "underline" color of the bottom header row .It Sy "color_current_row " No "reverse" color of the cursor row .It Sy "color_current_col " No "bold" color of the cursor column .It Sy "color_current_hdr " No "bold reverse" color of the header for the cursor column .It Sy "color_column_sep " No "246 blue" color of column separators .It Sy "color_key_col " No "81 cyan" color of key columns .It Sy "color_hidden_col " No "8" color of hidden columns on metasheets .It Sy "color_selected_row " No "215 yellow" color of selected rows .It Sy "disp_rstatus_fmt " No " {sheet.longname} {sheet.nRows:9d} {sheet.rowtype} " right-side status format string .It Sy "disp_status_fmt " No "{sheet.shortcut}\[u203A] {sheet.name}| " status line prefix .It Sy "disp_lstatus_max " No "0" maximum length of left status line .It Sy "disp_status_sep " No " | " separator between statuses .It Sy "color_keystrokes " No "white" color of input keystrokes on status line .It Sy "color_status " No "bold" status line color .It Sy "color_error " No "red" error message color .It Sy "color_warning " No "yellow" warning message color .It Sy "color_top_status " No "underline" top window status bar color .It Sy "color_active_status" No "bold" active window status bar color .It Sy "color_inactive_status" No "8" inactive window status bar color .It Sy "color_working " No "green" color of system running smoothly .It Sy "color_edit_cell " No "normal" cell color to use when editing cell .It Sy "disp_edit_fill " No "_" edit field fill character .It Sy "disp_unprintable " No "\[u00B7]" substitute character for unprintables .It Sy "disp_histogram " No "*" histogram element character .It Sy "disp_histolen " No "50" width of histogram column .It Sy "disp_replay_play " No "\[u25B6]" status indicator for active replay .It Sy "disp_replay_pause " No "\[u2016]" status indicator for paused replay .It Sy "color_status_replay" No "green" color of replay status indicator .It Sy "disp_pixel_random " No "False" randomly choose attr from set of pixels instead of most common .It Sy "color_graph_hidden " No "238 blue" color of legend for hidden attribute .It Sy "color_graph_selected" No "bold" color of selected graph points .It Sy "color_graph_axis " No "bold" color for graph axis labels .It Sy "color_add_pending " No "green" color for rows pending add .It Sy "color_change_pending" No "reverse yellow" color for cells pending modification .It Sy "color_delete_pending" No "red" color for rows pending delete .It Sy "color_xword_active " No "green" color of active clue .El . .Sh EXAMPLES .Dl Nm vd Cm foo.tsv .No open the file foo.tsv in the current directory .Pp .Dl Nm vd Cm -f sqlite bar.db .No open the file bar.db as a sqlite database .Pp .Dl Nm vd Cm foo.tsv -n -f sqlite bar.db .No open foo.tsv as tsv and bar.db as a sqlite database .Pp .Dl Nm vd Cm -f sqlite foo.tsv bar.db .No open both foo.tsv and bar.db as a sqlite database .Pp .Dl Nm vd Cm -b countries.fixed -o countries.tsv .No convert countries.fixed (in fixed width format) to countries.tsv (in tsv format) .Pp .Dl Nm vd Cm postgres:// Ns Ar username Ns Sy "\&:" Ns Ar password Ns Sy @ Ns Ar hostname Ns Sy "\&:" Ns Ar port Ns Sy / Ns Ar database .No open a connection to the given postgres database .Pp .Dl Nm vd Cm --play tests/pivot.vd --replay-wait 1 --output tests/pivot.tsv .No replay tests/pivot.vd, waiting 1 second between commands, and output the final sheet to test/pivot.tsv .Pp .Dl Ic ls -l | Nm vd Cm -f fixed --skip 1 --header 0 .No parse the output of ls -l into usable data .Pp .Dl Ic ls | vd | lpr .No interactively select a list of filenames to send to the printer .Pp .Dl Ic vd newfile.tsv .No open a blank sheet named Ar newfile No if file does not exist .Pp .Dl Ic vd sample.xlsx +:sheet1:2:3 .No launch with Sy sheet1 No at top-of-stack, and cursor at column Sy 2 No and row Sy 3 .Pp .Dl Ic vd -P open-plugins .No preplay longname Sy open-plugins No before starting the session .Sh FILES At the start of every session, .Sy VisiData No looks for Pa $HOME/.visidatarc Ns , and calls Python exec() on its contents if it exists. For example: .Bd -literal options.min_memory_mb=100 # stop processing without 100MB free bindkey('0', 'go-leftmost') # alias '0' to go to first column, like vim def median(values): L = sorted(values) return L[len(L)//2] aggregator('median', median) .Ed .Pp Functions defined in .visidatarc are available in python expressions (e.g. in derived columns). . .Sh SUPPORTED SOURCES Core VisiData includes these sources: .Pp .Bl -inset -compact -offset xxx .It Sy tsv No (tab-separated value) .Bl -inset -compact -offset xxx .It Plain and simple. Nm VisiData No writes tsv format by default. See the Sy --tsv-delimiter No option. .El .El .Pp .Bl -inset -compact -offset xxx .It Sy csv No (comma-separated value) .Bl -inset -compact -offset xxx .It .csv files are a scourge upon the earth, and still regrettably common. .It See the Sy --csv-dialect Ns , Sy --csv-delimiter Ns , Sy --csv-quotechar Ns , and Sy --csv-skipinitialspace No options. .It Accepted dialects are Ic excel-tab Ns , Ic unix Ns , and Ic excel Ns . .El .El .Pp .Bl -inset -compact -offset xxx .It Sy fixed No (fixed width text) .Bl -inset -compact -offset xxx .It Columns are autodetected from the first 1000 rows (adjustable with Sy --fixed-rows Ns ). .El .El .Pp .Bl -inset -compact -offset xxx .It Sy json No (single object) and Sy jsonl Ns / Ns Sy ndjson Ns / Ns Sy ldjson No (one object per line). .Bl -inset -compact -offset xxx .It Cells containing lists (e.g. Sy [3] Ns ) or dicts ( Ns Sy {3} Ns ) can be expanded into new columns with Sy "\&(" No and unexpanded with Sy "\&)" Ns . .El .El .Pp .Bl -inset -compact -offset xxx .It Sy sqlite .Bl -inset -compact -offset xxx .It May include multiple tables. The initial sheet is the table directory; .Sy Enter No loads the entire table into memory. Sy z^S No saves modifications to source. .El .El .Pp URL schemes are also supported: .Bl -inset -compact -offset xxx .It Sy http No (requires Sy requests Ns ); can be used as transport for with another filetype .El . .Pp For a list of all remaining formats supported by VisiData, see https://visidata.org/formats. .Pp In addition, .Sy .zip Ns , Sy .gz Ns , Sy .bz2 Ns , and Sy .xz No files are decompressed on the fly. .Pp . .Sh SUPPORTED OUTPUT FORMATS These are the supported savers: .Pp .Bl -inset -compact -offset xxx .It Sy tsv No (tab-separated value) .It Sy csv No (comma-separated value) .It Sy json No (one object with all rows) .It Sy jsonl Ns / Ns Sy ndjson Ns / Ns Sy ldjson No (one object per line/row) .Bl -inset -compact -offset xxx .It All expanded subcolumns must be closed (with Sy "\&)" Ns ) to retain the same structure. .El .It Sy sqlite No (save to source with Sy z^S Ns ) .It Sy md No (markdown table) .El .Pp . .Sh AUTHOR .Nm VisiData was made by .An Saul Pwanson Aq Mt vd@saul.pw Ns . ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612763790.0 visidata-2.2.1/visidata/man/vd.txt0000660000175000017500000014114000000000000017541 0ustar00kefalakefala00000000000000vd(1) Quick Reference Guide vd(1) NAME VisiData — a terminal utility for exploring and arranging tabular data SYNOPSIS vd [options] [input ...] vd [options] --play cmdlog [-w waitsecs] [--batch] [-o output] [field=value] vd [options] [input ...] +toplevel:subsheet:col:row DESCRIPTION VisiData is an easy-to-use multipurpose tool to explore, clean, edit, and restructure data. Rows can be selected, filtered, and grouped; columns can be rearranged, transformed, and derived via regex or Python expres‐ sions; and workflows can be saved, documented, and replayed. REPLAY MODE -p, --play=cmdlog replay a saved cmdlog within the interface -w, --replay-wait=seconds wait seconds between commands -b, --batch replay in batch mode (with no interface) -o, --output=file save final visible sheet to file as .tsv --replay-movement toggle --play to move cursor cell-by-cell field=value replace "{field}" in cmdlog contents with value Commands During Replay ^U pause/resume replay ^N execute next row in replaying sheet ^K cancel current replay GLOBAL COMMANDS All keystrokes are case sensitive. The ^ prefix is shorthand for Ctrl. Keystrokes to start off with ^Q abort program immediately ^C cancel user input or abort all async threads on current sheet g^C abort all secondary threads q quit current sheet gq quit all sheets (clean exit) ^H view this man page z^H view sheet of command longnames and keybindings Space longname execute command by its longname U undo the most recent modification (requires enabled options.undo) R redo the most recent undo (requires enabled options.undo) Cursor Movement Arrow PgUp Home go as expected h j k l go left/down/up/right gh gj gk gl go all the way to the left/bottom/top/right of sheet G gg go all the way to the bottom/top of sheet ^B ^F scroll one page back/forward zz scroll current row to center of screen ^^ (Ctrl+^) jump to previous sheet (swaps with current sheet) / ? regex search for regex forward/backward in current column g/ g? regex search for regex forward/backward over all visible columns z/ z? expr search by Python expr forward/backward in current column (with column names as variables) n N go to next/previous match from last regex search < > go up/down current column to next value z< z> go up/down current column to next null value { } go up/down current column to next selected row c regex go to next column with name matching regex r regex go to next row with key matching regex zc zr number go to column/row number (0-based) H J K L slide current row/column left/down/up/right gH gJ gK gL slide current row/column all the way to the left/bot‐ tom/top/right of sheet zH zJ zK zK number slide current row/column number positions to the left/down/up/right zh zj zk zl scroll one left/down/up/right Column Manipulation _ (underscore) toggle width of current column between full and default width g_ toggle widths of all visible columns between full and default width z_ number adjust width of current column to number gz_ number adjust widths of all visible columns to Ar number - (hyphen) hide current column z- reduce width of current column by half gv unhide all columns ! z! toggle/unset current column as a key column ~ # % $ @ z# set type of current column to str/int/float/cur‐ rency/date/len ^ edit name of current column g^ set names of all unnamed visible columns to contents of selected rows (or current row) z^ set name of current column to combined contents of cur‐ rent cell in selected rows (or current row) gz^ set name of all visible columns to combined contents of current column for selected rows (or current row) = expr create new column from Python expr, with column names, and attributes, as variables g= expr set current column for selected rows to result of Python expr gz= expr set current column for selected rows to the items in result of Python sequence expr z= expr evaluate Python expression on current row and set current cell with result of Python expr i (iota) add column with incremental values gi set current column for selected rows to incremental values zi step add column with values at increment step gzi step set current column for selected rows at increment step ' (tick) add a frozen copy of current column with all cells eval‐ uated g' open a frozen copy of current sheet with all visible columns evaluated z' gz' add/reset cache for current/all visible column(s) : regex add new columns from regex split; number of columns determined by example row at cursor ; regex add new columns from capture groups of regex (also requires example row) z; expr create new column from bash expr, with $columnNames as variables * regex/subst add column derived from current column, replacing regex with subst (may include \1 backrefs) g* gz* regex/subst modify selected rows in current/all visible column(s), replacing regex with subst (may include \1 backrefs) ( g( expand current/all visible column(s) of lists (e.g. [3]) or dicts (e.g. {3}) fully z( gz( depth expand current/all visible column(s) of lists (e.g. [3]) or dicts (e.g. {3}) to given depth (0= fully) ) unexpand current column; restore original column and re‐ move other columns at this level zM row-wise expand current column of lists (e.g. [3]) or dicts (e.g. {3}) within that column Row Selection s t u select/toggle/unselect current row gs gt gu select/toggle/unselect all rows zs zt zu select/toggle/unselect all rows from top to cursor gzs gzt gzu select/toggle/unselect all rows from cursor to bottom | \ regex select/unselect rows matching regex in current column g| g\ regex select/unselect rows matching regex in any visible column z| z\ expr select/unselect rows matching Python expr in any visible column , (comma) select rows matching display value of current cell in current column g, select rows matching display value of current row in all visible columns z, gz, select rows matching typed value of current cell/row in current column/all visible columns Row Sorting/Filtering [ ] sort ascending/descending by current column; replace any existing sort criteria g[ g] sort ascending/descending by all key columns; replace any existing sort criteria z[ z] sort ascending/descending by current column; add to ex‐ isting sort criteria gz[ gz] sort ascending/descending by all key columns; add to ex‐ isting sort criteria " open duplicate sheet with only selected rows g" open duplicate sheet with all rows gz" open duplicate sheet with deepcopy of selected rows Editing Rows and Cells a za append a blank row/column; appended columns cannot be copied to clipboard ga gza number append number blank rows/columns d gd delete (cut) current/selected row(s) and move to clip‐ board y gy yank (copy) current/all selected row(s) to clipboard zy gzy yank (copy) contents of current column for current/se‐ lected row(s) to clipboard zd gzd delete (cut) contents of current column for current/se‐ lected row(s) and move to clipboard p P paste clipboard rows after/before current row zp gzp set cells of current column for current/selected row(s) to last clipboard value Y gY yank (copy) current/all selected row(s) to system clipboard (using options.clipboard_copy_cmd) zY gzY yank (copy) contents of current column for current/selected row(s) to system clipboard (using options.clipboard_copy_cmd) f fill null cells in current column with contents of non- null cells up the current column e text edit contents of current cell ge text set contents of current column for selected rows to text Commands While Editing Input Enter ^C accept/abort input ^O open external $EDITOR to edit contents ^R reload initial value ^A ^E go to beginning/end of line ^B ^F go back/forward one character ^← ^→ (arrow) go back/forward one word ^H ^D delete previous/current character ^T transpose previous and current characters ^U ^K clear from cursor to beginning/end of line ^Y paste from cell clipboard Backspace Del delete previous/current character Insert toggle insert mode Up Down set contents to previous/next in history Tab Shift+Tab autocomplete input (when available) Shift+Arrow move cursor in direction of Arrow and re-enter edit mode Data Toolkit o input open input in VisiData ^S g^S filename save current/all sheet(s) to filename in format determined by extension (default .tsv) Note: if the format does not support multisave, or the filename ends in a /, a directory will be created. z^S filename save current column only to filename in format determined by extension (default .tsv) ^D filename.vd save CommandLog to filename.vd file A open new blank sheet with one column T open new sheet that has rows and columns of current sheet transposed + aggregator add aggregator to current column (see Frequency Table) z+ aggregator display result of aggregator over values in selected rows for current column & concatenate top two sheets in Sheets Stack g& concatenate all sheets in Sheets Stack Data Visualization . (dot) plot current numeric column vs key columns. The numeric key column is used for the x-axis; categorical key column values determine color. g. plot a graph of all visible numeric columns vs key columns. If rows on the current sheet represent plottable coordinates (as in .shp or vector .mbtiles sources), . plots the current row, and g. plots all selected rows (or all rows if none selected). Canvas-specific Commands + - increase/decrease zoom level, centered on cursor _ (underscore) zoom to fit full extent z_ (underscore) set aspect ratio x xmin xmax set xmin/xmax on graph y ymin ymax set ymin/ymax on graph s t u select/toggle/unselect rows on source sheet con‐ tained within canvas cursor gs gt gu select/toggle/unselect rows on source sheet visi‐ ble on screen d delete rows on source sheet contained within can‐ vas cursor gd delete rows on source sheet visible on screen Enter open sheet of source rows contained within canvas cursor gEnter open sheet of source rows visible on screen 1 - 9 toggle display of layers ^L redraw all pixels on canvas v toggle show_graph_labels option mouse scrollwheel zoom in/out of canvas left click-drag set canvas cursor right click-drag scroll canvas Split Screen Z split screen in half, so that second sheet on the stack is visible in a second pane zZ split screen, and queries for height of second pane Split Window specific Commands gZ close an already split screen, current pane full screens Tab jump to other pane ^^ (Ctrl+^) swap which sheet is in current pane Other Commands Q quit current sheet and remove it from the CommandLog v toggle sheet-specific visibility (multi-line rows on Sheet, legends/axes on Graph) ^E g^E view traceback for most recent error(s) z^E view traceback for error in current cell ^L refresh screen ^R reload current sheet z^R clear cache for current column ^Z suspend VisiData process ^G show cursor position and bounds of current sheet on sta‐ tus line ^V show version and copyright information on status line ^P open Status History m keystroke first, begin recording macro; second, prompt for keystroke No, and complete recording. Macro can then be executed everytime provided keystroke is used. Will override existing keybinding. Macros will run on current row, column, sheet. gm open an index of all existing macros. Can be directly viewed with Enter, and then modified with ^S. ^Y z^Y g^Y open current row/cell/sheet as Python object ^X expr evaluate Python expr and opens result as Python object z^X expr evaluate Python expr, in context of current row, and open result as Python object g^X stmt execute Python stmt in the global scope Internal Sheets List . VisiDataMenu (Shift+V) browse list of core sheets . Directory Sheet browse properties of files in a directory . Plugins Sheet browse, install, and (de)activate plugins Metasheets . Columns Sheet (Shift+C) edit column properties . Sheets Sheet (Shift+S) jump between sheets or join them together . Options Sheet (Shift+O) edit configuration options . Commandlog (Shift+D) modify and save commands for replay . Error Sheet (Ctrl+E) view last error . Status History (Ctrl+P) view history of status messages . Threads Sheet (Ctrl+T) view, cancel, and profile asynchronous threads Derived Sheets . Frequency Table (Shift+F) group rows by column value, with aggregations of other columns . Describe Sheet (Shift+I) view summary statistics for each column . Pivot Table (Shift+W) group rows by key and summarize current column . Melted Sheet (Shift+M) unpivot non-key columns into variable/value columns . Transposed Sheet (Shift+T) open new sheet with rows and columns transposed INTERNAL SHEETS VisiDataMenu (Shift+V) (sheet-specific commands) Enter load sheet in current row Directory Sheet (global commands) Space open-dir-current open the Directory Sheet for the current directory (sheet-specific commands) Enter gEnter open current/selected file(s) as new sheet(s) ^O g^O open current/selected file(s) in external $EDITOR ^R z^R gz^R reload information for all/current/selected file(s) Plugins Sheet Browse through a list of available plugins. VisiData needs to be restarted before plugin activation takes effect. Installation may require internet access. (global commands) Space open-plugins open the Plugins Sheet (sheet-specific commands) a install and activate current plugin d deactivate current plugin METASHEETS Columns Sheet (Shift+C) Properties of columns on the source sheet can be changed with standard editing commands (e ge g= Del) on the Columns Sheet. Multiple aggregators can be set by listing them (separated by spaces) in the aggregators column. The 'g' commands affect the selected rows, which are the literal columns on the source sheet. (global commands) gC open Columns Sheet with all visible columns from all sheets (sheet-specific commands) & add column from concatenating selected source columns g! gz! toggle/unset selected columns as key columns on source sheet g+ aggregator add Ar aggregator No to selected source columns g- (hyphen) hide selected columns on source sheet g~ g# g% g$ g@ gz# z% set type of selected columns on source sheet to str/int/float/currency/date/len/floatsi Enter open a Frequency Table sheet grouped by column referenced in current row Sheets Sheet (Shift+S) open Sheets Stack, which contains only the active sheets on the current stack (global commands) gS open Sheets Sheet, which contains all sheets from current session, active and inactive Alt number jump to sheet number (sheet-specific commands) Enter jump to sheet referenced in current row gEnter push selected sheets to top of sheet stack a add row to reference a new blank sheet gC gI open Columns Sheet/Describe Sheet with all visible columns from selected sheets g^R reload all selected sheets z^C gz^C abort async threads for current/selected sheets(s) g^S save selected or all sheets & jointype merge selected sheets with visible columns from all, keeping rows according to jointype: . inner keep only rows which match keys on all sheets . outer keep all rows from first selected sheet . full keep all rows from all sheets (union) . diff keep only rows NOT in all sheets . append keep all rows from all sheets (concatenation) . extend copy first selected sheet, keeping all rows and sheet type, and extend with columns from other sheets . merge mostly keep all rows from first selected sheet, except prioritise cells with non-null/non- error values Options Sheet (Shift+O) (global commands) Shift+O edit global options (apply to all sheets) zO edit sheet options (apply to current sheet only) gO open options.config as TextSheet (sheet-specific commands) Enter e edit option at current row d remove option override for this context CommandLog (Shift+D) (global commands) D open current sheet's CommandLog with all other loose ends removed; includes commands from parent sheets gD open global CommandLog for all commands executed in the current session zD open current sheet's CommandLog with the parent sheets commands' removed (sheet-specific commands) x replay command in current row gx replay contents of entire CommandLog ^C abort replay DERIVED SHEETS Frequency Table (Shift+F) A Frequency Table groups rows by one or more columns, and includes summary columns for those with aggregators. (global commands) gF open Frequency Table, grouped by all key columns on source sheet zF open one-line summary for all rows and selected rows (sheet-specific commands) s t u select/toggle/unselect these entries in source sheet Enter gEnter open copy of source sheet with rows that are grouped in current cell / selected rows Describe Sheet (Shift+I) A Describe Sheet contains descriptive statistics for all visible columns. (global commands) gI open Describe Sheet for all visible columns on all sheets (sheet-specific commands) zs zu select/unselect rows on source sheet that are being described in current cell ! toggle/unset current column as a key column on source sheet Enter open a Frequency Table sheet grouped on column referenced in current row zEnter open copy of source sheet with rows described in cur‐ rent cell Pivot Table (Shift+W) Set key column(s) and aggregators on column(s) before pressing Shift+W on the column to pivot. (sheet-specific commands) Enter open sheet of source rows aggregated in current pivot row zEnter open sheet of source rows aggregated in current pivot cell Melted Sheet (Shift+M) Open Melted Sheet (unpivot), with key columns retained and all non-key columns reduced to Variable-Value rows. (global commands) gM regex open Melted Sheet (unpivot), with key columns retained and regex capture groups determining how the non-key columns will be reduced to Variable-Value rows. Python Object Sheet (^X ^Y g^Y z^Y) (sheet-specific commands) Enter dive further into Python object v toggle show/hide for methods and hidden properties gv zv show/hide methods and hidden properties COMMANDLINE OPTIONS Add -n/--nonglobal to make subsequent CLI options sheet-specific (applying only to paths specified directly on the CLI). By default, CLI options apply to all sheets. Options can also be set via the Options Sheet or a .visidatarc (see FILES). -P=longname preplay longname before replay or regular launch; limited to Base Sheet bound commands +toplevel:subsheet:col:row launch vd with subsheet of toplevel at top-of-stack, and cursor at col and row; all arguments are optional -f, --filetype=filetype tsv set loader to use for filetype instead of file extension -y, --confirm-overwrite=F True overwrite existing files without confirmation --mouse-interval=int 1 max time between press/release for click (ms) --null-value=NoneType None a value to be counted as null --undo=bool True enable undo/redo --col-cache-size=int 0 max number of cache en‐ tries in each cached col‐ umn --clean-names False clean column/sheet names to be valid Python iden‐ tifiers --default-width=int 20 default column width --default-height=int 10 default column height --textwrap-cells=bool True wordwrap text for multi‐ line rows --quitguard False confirm before quitting last sheet --debug False exit on error and display stacktrace --skip=int 0 skip N rows before header --header=int 1 parse first N rows as column names --load-lazy False load subsheets always (False) or lazily (True) --force-256-colors False use 256 colors even if curses reports fewer --use-default-colors False curses use default termi‐ nal colors --note-pending=str ⌛ note to display for pend‐ ing cells --note-format-exc=str ? cell note for an excep‐ tion during formatting --note-getter-exc=str ! cell note for an excep‐ tion during computation --note-type-exc=str ! cell note for an excep‐ tion during type conver‐ sion --scroll-incr=int 3 amount to scroll with scrollwheel --name-joiner=str _ string to join sheet or column names --value-joiner=str string to join display values --wrap False wrap text to fit window width on TextSheet --save-filetype=str tsv specify default file type to save as --profile=str filename to save binary profiling data --min-memory-mb=int 0 minimum memory to con‐ tinue loading and async processing --input-history=str basename of file to store persistent input history --encoding=str utf-8 encoding passed to codecs.open --encoding-errors=str surrogateescape encoding_errors passed to codecs.open --bulk-select-clear False clear selected rows be‐ fore new bulk selections --some-selected-rows False if no rows selected, if True, someSelectedRows returns all rows; if False, fails --delimiter=str field delimiter to use for tsv/usv filetype --row-delimiter=str " row delimiter to use for tsv/usv filetype --tsv-safe-newline=str replacement for newline character when saving to tsv --tsv-safe-tab=str replacement for tab char‐ acter when saving to tsv --visibility=int 0 visibility level (0=low, 1=high) --expand-col-scanrows=int 1000 number of rows to check when expanding columns (0 = all) --json-indent=NoneType None indent to use when saving json --json-sort-keys False sort object keys when saving to json --default-colname=str column name to use for non-dict rows --filetype=str specify file type --confirm-overwrite=bool True whether to prompt for overwrite confirmation on save --safe-error=str #ERR error string to use while saving --clipboard-copy-cmd=str command to copy stdin to system clipboard --clipboard-paste-cmd=str command to get contents of system clipboard --fancy-chooser False a nicer selection inter‐ face for aggregators and jointype --describe-aggrs=str mean stdev numeric aggregators to calculate on Describe sheet --histogram-bins=int 0 number of bins for his‐ togram of numeric columns --numeric-binning False bin numeric columns into ranges --replay-wait=float 0.0 time to wait between re‐ played commands, in sec‐ onds --replay-movement False insert movements during replay --visidata-dir=str ~/.visidata/ directory to load and store additional files --rowkey-prefix=str キ string prefix for rowkey in the cmdlog --cmdlog-histfile=str file to autorecord each cmdlog action to --regex-flags=str I flags to pass to re.com‐ pile() [AILMSUX] --regex-maxsplit=int 0 maxsplit to pass to regex.split --default-sample-size=int 100 number of rows to sample for regex.split --show-graph-labels=bool True show axes and legend on graph --plot-colors=str list of distinct colors to use for plotting dis‐ tinct objects --zoom-incr=float 2.0 amount to multiply cur‐ rent zoomlevel when zoom‐ ing --motd-url=str source of randomized startup messages --dir-recurse False walk source path recur‐ sively on DirSheet --dir-hidden False load hidden files on DirSheet --config=str ~/.visidatarc config file to exec in Python --play=str file.vd to replay --batch False replay in batch mode (with no interface and all status sent to std‐ out) --output=NoneType None save the final visible sheet to output at the end of replay --preplay=str longnames to preplay be‐ fore replay --imports=str plugins imports to preload before .visidatarc (command-line only) --incr-base=float 1.0 start value for column increments --csv-dialect=str excel dialect passed to csv.reader --csv-delimiter=str , delimiter passed to csv.reader --csv-quotechar=str " quotechar passed to csv.reader --csv-skipinitialspace=bool True skipinitialspace passed to csv.reader --csv-escapechar=NoneType None escapechar passed to csv.reader --csv-lineterminator=str " lineterminator passed to csv.writer --safety-first False sanitize input/output to handle edge cases, with a performance cost --fixed-rows=int 1000 number of rows to check for fixed width columns --fixed-maxcols=int 0 max number of fixed-width columns to create (0 is no max) --postgres-schema=str public The desired schema for the Postgres database --http-max-next=int 0 max next.url pages to follow in http response --html-title=str

{sheet.name}

table header when saving to html --pcap-internet=str n (y/s/n) if save_dot in‐ cludes all internet hosts separately (y), combined (s), or does not include the internet (n) --graphviz-edge-labels=bool True whether to include edge labels on graphviz dia‐ grams --pdf-tables False parse PDF for tables in‐ stead of pages of text --plugins-url=str https://visidata.org/plugins/plugins.jsonl source of plugins sheet DISPLAY OPTIONS Display options can only be set via the Options Sheet or a .visidatarc (see FILES). disp_splitwin_pct 0 height of second sheet on screen disp_currency_fmt %.02f default fmtstr to format for cur‐ rency values disp_float_fmt {:.02f} default fmtstr to format for float values disp_int_fmt {:.0f} default fmtstr to format for int values disp_date_fmt %Y-%m-%d default fmtstr to strftime for date values disp_note_none ⌀ visible contents of a cell whose value is None disp_truncator … indicator that the contents are only partially visible disp_oddspace · displayable character for odd whitespace disp_more_left < header note indicating more col‐ umns to the left disp_more_right > header note indicating more col‐ umns to the right disp_error_val displayed contents for computa‐ tion exception disp_ambig_width 1 width to use for unicode chars marked ambiguous disp_pending string to display in pending cells color_note_pending bold magenta color of note in pending cells color_note_type 226 yellow color of cell note for non-str types in anytype columns color_note_row 220 yellow color of row note on left edge disp_column_sep | separator between columns disp_keycol_sep ║ separator between key columns and rest of columns disp_rowtop_sep | disp_rowmid_sep ⁝ disp_rowbot_sep ⁝ disp_rowend_sep ║ disp_keytop_sep ║ disp_keymid_sep ║ disp_keybot_sep ║ disp_endtop_sep ║ disp_endmid_sep ║ disp_endbot_sep ║ disp_selected_note • disp_sort_asc ↑↟⇞⇡⇧⇑ characters for ascending sort disp_sort_desc ↓↡⇟⇣⇩⇓ characters for descending sort color_default normal the default color color_default_hdr bold color of the column headers color_bottom_hdr underline color of the bottom header row color_current_row reverse color of the cursor row color_current_col bold color of the cursor column color_current_hdr bold reverse color of the header for the cur‐ sor column color_column_sep 246 blue color of column separators color_key_col 81 cyan color of key columns color_hidden_col 8 color of hidden columns on metasheets color_selected_row 215 yellow color of selected rows disp_rstatus_fmt {sheet.longname} {sheet.nRows:9d} {sheet.rowtype} right-side status format string disp_status_fmt {sheet.shortcut}› {sheet.name}| status line prefix disp_lstatus_max 0 maximum length of left status line disp_status_sep | separator between statuses color_keystrokes white color of input keystrokes on sta‐ tus line color_status bold status line color color_error red error message color color_warning yellow warning message color color_top_status underline top window status bar color color_active_status bold active window status bar color color_inactive_status 8 inactive window status bar color color_working green color of system running smoothly color_edit_cell normal cell color to use when editing cell disp_edit_fill _ edit field fill character disp_unprintable · substitute character for unprint‐ ables disp_histogram * histogram element character disp_histolen 50 width of histogram column disp_replay_play ▶ status indicator for active re‐ play disp_replay_pause ‖ status indicator for paused re‐ play color_status_replay green color of replay status indicator disp_pixel_random False randomly choose attr from set of pixels instead of most common color_graph_hidden 238 blue color of legend for hidden attri‐ bute color_graph_selected bold color of selected graph points color_graph_axis bold color for graph axis labels color_add_pending green color for rows pending add color_change_pending reverse yellow color for cells pending modifica‐ tion color_delete_pending red color for rows pending delete color_xword_active green color of active clue EXAMPLES vd foo.tsv open the file foo.tsv in the current directory vd -f sqlite bar.db open the file bar.db as a sqlite database vd foo.tsv -n -f sqlite bar.db open foo.tsv as tsv and bar.db as a sqlite database vd -f sqlite foo.tsv bar.db open both foo.tsv and bar.db as a sqlite database vd -b countries.fixed -o countries.tsv convert countries.fixed (in fixed width format) to countries.tsv (in tsv format) vd postgres://username:password@hostname:port/database open a connection to the given postgres database vd --play tests/pivot.vd --replay-wait 1 --output tests/pivot.tsv replay tests/pivot.vd, waiting 1 second between commands, and output the final sheet to test/pivot.tsv ls -l | vd -f fixed --skip 1 --header 0 parse the output of ls -l into usable data ls | vd | lpr interactively select a list of filenames to send to the printer vd newfile.tsv open a blank sheet named newfile if file does not exist vd sample.xlsx +:sheet1:2:3 launch with sheet1 at top-of-stack, and cursor at column 2 and row 3 vd -P open-plugins preplay longname open-plugins before starting the session FILES At the start of every session, VisiData looks for $HOME/.visidatarc, and calls Python exec() on its contents if it exists. For example: options.min_memory_mb=100 # stop processing without 100MB free bindkey('0', 'go-leftmost') # alias '0' to go to first column, like vim def median(values): L = sorted(values) return L[len(L)//2] aggregator('median', median) Functions defined in .visidatarc are available in python expressions (e.g. in derived columns). SUPPORTED SOURCES Core VisiData includes these sources: tsv (tab-separated value) Plain and simple. VisiData writes tsv format by default. See the --tsv-delimiter option. csv (comma-separated value) .csv files are a scourge upon the earth, and still regrettably common. See the --csv-dialect, --csv-delimiter, --csv-quotechar, and --csv-skipinitialspace options. Accepted dialects are excel-tab, unix, and excel. fixed (fixed width text) Columns are autodetected from the first 1000 rows (adjustable with --fixed-rows). json (single object) and jsonl/ndjson/ldjson (one object per line). Cells containing lists (e.g. [3]) or dicts ({3}) can be expanded into new columns with ( and unexpanded with ). sqlite May include multiple tables. The initial sheet is the table directory; Enter loads the entire table into memory. z^S saves modifications to source. URL schemes are also supported: http (requires requests); can be used as transport for with another filetype For a list of all remaining formats supported by VisiData, see https://visidata.org/formats. In addition, .zip, .gz, .bz2, and .xz files are decompressed on the fly. SUPPORTED OUTPUT FORMATS These are the supported savers: tsv (tab-separated value) csv (comma-separated value) json (one object with all rows) jsonl/ndjson/ldjson (one object per line/row) All expanded subcolumns must be closed (with )) to retain the same structure. sqlite (save to source with z^S) md (markdown table) AUTHOR VisiData was made by Saul Pwanson . Linux/MacOS Feb 07, 2021 Linux/MacOS ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612763790.0 visidata-2.2.1/visidata/man/visidata.10000660000175000017500000012612600000000000020264 0ustar00kefalakefala00000000000000.Dd Feb 07, 2021 .Dt vd \&1 "Quick Reference Guide" .Os Linux/MacOS . .\" Long option with arg: .Lo f filetype format .\" Long flag: .Lo f filetype .de Lo .It Cm -\\$1 Ns , Cm --\\$2 Ns = Ns Ar \\$3 .. .de Lf .It Cm -\\$1 Ns , Cm --\\$2 .. .Sh NAME . .Nm VisiData .Nd a terminal utility for exploring and arranging tabular data . .Sh SYNOPSIS . .Nm vd .Op Ar options .Op Ar input No ... . .Nm vd .Op Ar options .Cm --play Ar cmdlog .Op Cm -w Ar waitsecs .Op Cm --batch .Op Cm -o Ar output .Op Ar field Ns Cm = Ns Ar value . .Nm vd .Op Ar options .Op Ar input No ... .Cm + Ns Ar toplevel Ns : Ns Ar subsheet Ns : Ns Ar col Ns : Ns Ar row . .Sh DESCRIPTION .Nm VisiData No is an easy-to-use multipurpose tool to explore, clean, edit, and restructure data. Rows can be selected, filtered, and grouped; columns can be rearranged, transformed, and derived via regex or Python expressions; and workflows can be saved, documented, and replayed. . .Ss REPLAY MODE .Bl -tag -width XXXXXXXXXXXXXXXXXXXXXX -compact .Lo p play cmdlog .No replay a saved Ar cmdlog No within the interface . .Lo w replay-wait seconds .No wait Ar seconds No between commands . .Lf b batch replay in batch mode (with no interface) . .Lo o output file .No save final visible sheet to Ar file No as .tsv . .It Sy --replay-movement .No toggle Sy --play No to move cursor cell-by-cell .It Ar field Ns Cm = Ns Ar value .No replace \&"{ Ns Ar field Ns }\&" in Ar cmdlog No contents with Ar value .El . .Ss Commands During Replay .Bl -tag -width XXXXXXXXXXXXXXXXXXX -compact -offset XXX .It Sy ^U pause/resume replay .It Sy ^N execute next row in replaying sheet .It Sy ^K cancel current replay .El . .Ss GLOBAL COMMANDS .No All keystrokes are case sensitive. The Sy ^ No prefix is shorthand for Sy Ctrl Ns . .Pp .Ss Keystrokes to start off with .Bl -tag -width XXXXXXXXXXXXXXX -compact .It Ic " ^Q" abort program immediately .It Ic " ^C" cancel user input or abort all async threads on current sheet .It Ic "g^C" abort all secondary threads .It Ic " q" quit current sheet .It Ic " gq" quit all sheets (clean exit) .Pp .It Ic " ^H" view this man page .It Ic "z^H" view sheet of command longnames and keybindings .It Ic "Space" Ar longname .No execute command by its Ar longname .Pp .It Ic " U" .No undo the most recent modification ( requires enabled Sy options.undo Ns ) .It Ic " R" .No redo the most recent undo ( requires enabled Sy options.undo Ns ) .El .Ss "Cursor Movement" . .Bl -tag -width XXXXXXXXXXXXXXX -compact . .It Ic "Arrow PgUp Home" go as expected .It Ic " h j k l" go left/down/up/right .It Ic "gh gj gk gl" go all the way to the left/bottom/top/right of sheet .It Ic " G gg" go all the way to the bottom/top of sheet .It Ic "^B ^F" scroll one page back/forward .It Ic "zz" scroll current row to center of screen .Pp .It Ic "^^" No (Ctrl+^) jump to previous sheet (swaps with current sheet) .Pp .It Ic " / ?" Ar regex .No search for Ar regex No forward/backward in current column .It Ic "g/ g?" Ar regex .No search for Ar regex No forward/backward over all visible columns .It Ic "z/ z?" Ar expr .No search by Python Ar expr No forward/backward in current column (with column names as variables) .It Ic " n N" go to next/previous match from last regex search .Pp .It Ic " < >" go up/down current column to next value .It Ic "z< z>" go up/down current column to next null value .It Ic " { }" go up/down current column to next selected row . .El .Pp .Bl -tag -width XXXXXXXXXXXXXXX -compact .Pp .It Ic " c" Ar regex .No go to next column with name matching Ar regex .It Ic " r" Ar regex .No go to next row with key matching Ar regex .It Ic "zc zr" Ar number .No go to column/row Ar number No (0-based) .Pp .It Ic " H J K L" slide current row/column left/down/up/right .It Ic "gH gJ gK gL" slide current row/column all the way to the left/bottom/top/right of sheet .It Ic "zH zJ zK zK" Ar number .No slide current row/column Ar number No positions to the left/down/up/right .Pp .It Ic "zh zj zk zl" scroll one left/down/up/right .El . .Ss Column Manipulation . .Bl -tag -width XXXXXXXXXXXXXXX -compact . .It Ic " _" Ns " (underscore)" toggle width of current column between full and default width .It Ic " g_" toggle widths of all visible columns between full and default width .It Ic " z_" Ar number .No adjust width of current column to Ar number .It Ic "gz_" Ar number adjust widths of all visible columns to Ar number .Pp .It Ic " -" Ns " (hyphen)" hide current column .It Ic "z-" Ns reduce width of current column by half .It Ic "gv" Ns unhide all columns .Pp .It Ic "! z!" Ns toggle/unset current column as a key column .It Ic "~ # % $ @ z#" set type of current column to str/int/float/currency/date/len .It Ic " ^" edit name of current column .It Ic " g^" set names of all unnamed visible columns to contents of selected rows (or current row) .It Ic " z^" set name of current column to combined contents of current cell in selected rows (or current row) .It Ic "gz^" set name of all visible columns to combined contents of current column for selected rows (or current row) .Pp .It Ic " =" Ar expr .No create new column from Python Ar expr Ns , with column names, and attributes, as variables .It Ic " g=" Ar expr .No set current column for selected rows to result of Python Ar expr .It Ic "gz=" Ar expr .No set current column for selected rows to the items in result of Python sequence Ar expr .It Ic " z=" Ar expr .No evaluate Python expression on current row and set current cell with result of Python Ar expr .Pp .It " i" (iota) .No add column with incremental values .It " gi" .No set current column for selected rows to incremental values .It " zi" Ar step .No add column with values at increment Ar step .It "gzi" Ar step .No set current column for selected rows at increment Ar step .El .Pp .Bl -tag -width XXXXXXXXXXXXXXX -compact .It Ic " '" Ns " (tick)" add a frozen copy of current column with all cells evaluated .It Ic "g'" open a frozen copy of current sheet with all visible columns evaluated .It Ic "z' gz'" add/reset cache for current/all visible column(s) .Pp .It Ic " \&:" Ar regex .No add new columns from Ar regex No split; number of columns determined by example row at cursor .It Ic " \&;" Ar regex .No add new columns from capture groups of Ar regex No (also requires example row) .It Ic "z" Ns Ic "\&;" Ar expr .No create new column from bash Ar expr Ns , with Sy $ Ns columnNames as variables .It Ic " *" Ar regex Ns Sy / Ns Ar subst .No add column derived from current column, replacing Ar regex No with Ar subst No (may include Sy \e1 No backrefs) .It Ic "g* gz*" Ar regex Ns Sy / Ns Ar subst .No modify selected rows in current/all visible column(s), replacing Ar regex No with Ar subst No (may include Sy \e1 No backrefs) .Pp .It Ic " ( g(" .No expand current/all visible column(s) of lists (e.g. Sy [3] Ns ) or dicts (e.g. Sy {3} Ns ) fully .It Ic "z( gz(" Ar depth .No expand current/all visible column(s) of lists (e.g. Sy [3] Ns ) or dicts (e.g. Sy {3} Ns ) to given Ar depth ( Ar 0 Ns = fully) .It Ic " )" unexpand current column; restore original column and remove other columns at this level .It Ic "zM" .No row-wise expand current column of lists (e.g. Sy [3] Ns ) or dicts (e.g. Sy {3} Ns ) within that column .El .Ss Row Selection . .Bl -tag -width XXXXXXXXXXXXXXX -compact . .It Ic " s t u" select/toggle/unselect current row .It Ic " gs gt gu" select/toggle/unselect all rows .It Ic " zs zt zu" select/toggle/unselect all rows from top to cursor .It Ic "gzs gzt gzu" select/toggle/unselect all rows from cursor to bottom .It Ic " | \e\ " Ns Ar regex .No select/unselect rows matching Ar regex No in current column .It Ic "g| g\e\ " Ns Ar regex .No select/unselect rows matching Ar regex No in any visible column .It Ic "z| z\e\ " Ns Ar expr .No select/unselect rows matching Python Ar expr No in any visible column .It Ic " \&," Ns " (comma)" select rows matching display value of current cell in current column .It Ic "g\&," select rows matching display value of current row in all visible columns .It Ic "z\&, gz\&," select rows matching typed value of current cell/row in current column/all visible columns . .El . . .Ss Row Sorting/Filtering . .Bl -tag -width XXXXXXXXXXXXXXX -compact . .It Ic " [ ]" sort ascending/descending by current column; replace any existing sort criteria .It Ic " g[ g]" sort ascending/descending by all key columns; replace any existing sort criteria .It Ic " z[ z]" sort ascending/descending by current column; add to existing sort criteria .It Ic "gz[ gz]" sort ascending/descending by all key columns; add to existing sort criteria .It Ic " \&"" open duplicate sheet with only selected rows .It Ic "g\&"" open duplicate sheet with all rows .It Ic "gz\&"" open duplicate sheet with deepcopy of selected rows .El .Ss Editing Rows and Cells . .Bl -tag -width XXXXXXXXXXXXXXX -compact .It Ic " a za" append a blank row/column; appended columns cannot be copied to clipboard .It Ic " ga gza" Ar number .No append Ar number No blank rows/columns .It Ic " d gd" delete (cut) current/selected row(s) and move to clipboard .It Ic " y gy" yank (copy) current/all selected row(s) to clipboard .It Ic " zy gzy" yank (copy) contents of current column for current/selected row(s) to clipboard .It Ic " zd gzd" delete (cut) contents of current column for current/selected row(s) and move to clipboard .It Ic " p P" paste clipboard rows after/before current row .It Ic " zp gzp" set cells of current column for current/selected row(s) to last clipboard value .It Ic " Y gY" .No yank (copy) current/all selected row(s) to system clipboard (using Sy options.clipboard_copy_cmd Ns ) .It Ic " zY gzY" .No yank (copy) contents of current column for current/selected row(s) to system clipboard (using Sy options.clipboard_copy_cmd Ns ) .It Ic " f" fill null cells in current column with contents of non-null cells up the current column . . .It Ic " e" Ar text edit contents of current cell .It Ic " ge" Ar text .No set contents of current column for selected rows to Ar text . .El . .Ss " Commands While Editing Input" .Bl -tag -width XXXXXXXXXXXXXXX -compact -offset XXX .It Ic "Enter ^C" accept/abort input .It Ic ^O open external $EDITOR to edit contents .It Ic ^R reload initial value .It Ic "^A ^E" go to beginning/end of line .It Ic "^B ^F" go back/forward one character .It Ic "^\[u2190] ^\[u2192]" No (arrow) go back/forward one word .It Ic "^H ^D" delete previous/current character .It Ic ^T transpose previous and current characters .It Ic "^U ^K" clear from cursor to beginning/end of line .It Ic "^Y" paste from cell clipboard .It Ic "Backspace Del" delete previous/current character .It Ic Insert toggle insert mode .It Ic "Up Down" set contents to previous/next in history .It Ic "Tab Shift+Tab" autocomplete input (when available) .It Ic "Shift+Arrow" .No move cursor in direction of Sy Arrow No and re-enter edit mode . .El . .Ss Data Toolkit .Bl -tag -width XXXXXXXXXXXXXXX -compact .It Ic " o" Ar input open .Ar input No in Sy VisiData .It Ic "^S g^S" Ar filename .No save current/all sheet(s) to Ar filename No in format determined by extension (default .tsv) .It "" .No Note: if the format does not support multisave, or the Ar filename No ends in a Sy / Ns , a directory will be created. .It Ic "z^S" Ar filename .No save current column only to Ar filename No in format determined by extension (default .tsv) .It Ic "^D" Ar filename.vd .No save Sy CommandLog No to Ar filename.vd No file .It Ic "A" .No open new blank sheet with one column .It Ic "T" .No open new sheet that has rows and columns of current sheet transposed .Pp .It Ic " +" Ar aggregator .No add Ar aggregator No to current column (see Sy "Frequency Table" Ns ) .It Ic "z+" Ar aggregator .No display result of Ar aggregator No over values in selected rows for current column .It Ic " &" .No concatenate top two sheets in Sy Sheets Stack .It Ic "g&" .No concatenate all sheets in Sy Sheets Stack .Pp .El .Ss Data Visualization .Bl -tag -width XXXXXXXXXXXXX -compact .It Ic " ." No (dot) .No plot current numeric column vs key columns. The numeric key column is used for the x-axis; categorical key column values determine color. .It Ic "g." .No plot a graph of all visible numeric columns vs key columns. .Pp .El .No If rows on the current sheet represent plottable coordinates (as in .shp or vector .mbtiles sources), .Ic " ." No plots the current row, and Ic "g." No plots all selected rows (or all rows if none selected). .Ss " Canvas-specific Commands" .Bl -tag -width XXXXXXXXXXXXXXXXXX -compact -offset XXX .It Ic " + -" increase/decrease zoom level, centered on cursor .It Ic " _" No (underscore) zoom to fit full extent .It Ic "z_" No (underscore) set aspect ratio .It Ic " x" Ar xmin xmax .No set Ar xmin Ns / Ns Ar xmax No on graph .It Ic " y" Ar ymin ymax .No set Ar ymin Ns / Ns Ar ymax No on graph .It Ic " s t u" select/toggle/unselect rows on source sheet contained within canvas cursor .It Ic "gs gt gu" select/toggle/unselect rows on source sheet visible on screen .It Ic " d" delete rows on source sheet contained within canvas cursor .It Ic "gd" delete rows on source sheet visible on screen .It Ic " Enter" open sheet of source rows contained within canvas cursor .It Ic "gEnter" open sheet of source rows visible on screen .It Ic " 1" No - Ic "9" toggle display of layers .It Ic "^L" redraw all pixels on canvas .It Ic " v" .No toggle Ic show_graph_labels No option .It Ic "mouse scrollwheel" zoom in/out of canvas .It Ic "left click-drag" set canvas cursor .It Ic "right click-drag" scroll canvas .El .Ss Split Screen .Bl -tag -width XXXXXXXXXXXXX -compact .It Ic " Z" .No split screen in half, so that second sheet on the stack is visible in a second pane .It Ic "zZ" .No split screen, and queries for height of second pane .El .Ss " Split Window specific Commands" .Bl -tag -width XXXXXXXXXXXXXXXXXX -compact -offset XXX .It Ic "gZ" .No close an already split screen, current pane full screens .It Ic "Tab" .No jump to other pane .It Ic "^^" No (Ctrl+^) .No swap which sheet is in current pane .Pp .El .Ss Other Commands . .Bl -tag -width XXXXXXXXXXXXXXX -compact .It Ic "Q" .No quit current sheet and remove it from the Sy CommandLog .It Ic "v" toggle sheet-specific visibility (multi-line rows on Sheet, legends/axes on Graph) .Pp .Pp .It Ic " ^E g^E" view traceback for most recent error(s) .It Ic "z^E" view traceback for error in current cell .Pp .It Ic " ^L" refresh screen .It Ic " ^R" reload current sheet .It Ic "z^R" clear cache for current column .It Ic " ^Z" suspend VisiData process .It Ic " ^G" show cursor position and bounds of current sheet on status line .It Ic " ^V" show version and copyright information on status line .It Ic " ^P" .No open Sy Status History .It "m" Ar keystroke .No first, begin recording macro; second, prompt for Ar keystroke No, and complete recording. Macro can then be executed everytime provided keystroke is used. Will override existing keybinding. Macros will run on current row, column, sheet. .It "gm" .No open an index of all existing macros. Can be directly viewed with Sy Enter Ns , and then modified with Sy ^S Ns . . .El .Pp .Bl -tag -width XXXXXXXXXXXXXXX -compact .It Ic " ^Y z^Y g^Y" open current row/cell/sheet as Python object .It Ic " ^X" Ar expr .No evaluate Python Ar expr No and opens result as Python object .It Ic "z^X" Ar expr .No evaluate Python Ar expr Ns , in context of current row, and open result as Python object .It Ic "g^X" Ar stmt .No execute Python Ar stmt No in the global scope .El . .Ss Internal Sheets List .Bl -tag -width Xx -compact .It Sy " \&." .Sy VisiDataMenu No (Shift+V) " browse list of core sheets" .It Sy " \&." .Sy Directory Sheet No " browse properties of files in a directory" .It Sy " \&." .Sy Plugins Sheet No " browse, install, and (de)activate plugins" .It " " .It Sy Metasheets .It Sy " \&." .Sy Columns Sheet No (Shift+C) " edit column properties" .It Sy " \&." .Sy Sheets Sheet No (Shift+S) " jump between sheets or join them together" .It Sy " \&." .Sy Options Sheet No (Shift+O) " edit configuration options" .It Sy " \&." .Sy Commandlog No (Shift+D) " modify and save commands for replay" .It Sy " \&." .Sy Error Sheet No (Ctrl+E) " view last error" .It Sy " \&." .Sy Status History No (Ctrl+P) " view history of status messages" .It Sy " \&." .Sy Threads Sheet No (Ctrl+T) " view, cancel, and profile asynchronous threads" .Pp .It Sy Derived Sheets .It Sy " \&." .Sy Frequency Table No (Shift+F) " group rows by column value, with aggregations of other columns" .It Sy " \&." .Sy Describe Sheet No (Shift+I) " view summary statistics for each column" .It Sy " \&." .Sy Pivot Table No (Shift+W) " group rows by key and summarize current column" .It Sy " \&." .Sy Melted Sheet No (Shift+M) " unpivot non-key columns into variable/value columns" .It Sy " \&." .Sy Transposed Sheet No (Shift+T) " open new sheet with rows and columns transposed" .El . .Ss INTERNAL SHEETS .Ss VisiDataMenu (Shift+V) .Bl -inset -compact .It (sheet-specific commands) .El .Bl -tag -width XXXXXXXXXXXXXXX -compact -offset XXX .It Ic Enter .No load sheet in current row .El .Ss Directory Sheet .Bl -inset -compact .It (global commands) .El .Bl -tag -width XXXXXXXXXXXXXXX -compact -offset XXX .It Ic Space Ar open-dir-current .No open the Sy Directory Sheet No for the current directory .El .Bl -inset -compact .It (sheet-specific commands) .El .Bl -tag -width XXXXXXXXXXXXXXX -compact -offset XXX .It Ic "Enter gEnter" open current/selected file(s) as new sheet(s) .It Ic " ^O g^O" open current/selected file(s) in external $EDITOR .It Ic " ^R z^R gz^R" reload information for all/current/selected file(s) .El . .Ss Plugins Sheet .Bl -inset -compact .It Browse through a list of available plugins. VisiData needs to be restarted before plugin activation takes effect. Installation may require internet access. .El .Bl -inset -compact .It (global commands) .El .Bl -tag -width XXXXXXXXXXXXXXX -compact -offset XXX .It Ic Space Ar open-plugins .No open the Sy Plugins Sheet .El .Bl -inset -compact .It (sheet-specific commands) .El .Bl -tag -width XXXXXXXXXXXXXXX -compact -offset XXX .It Ic "a" install and activate current plugin .It Ic "d" deactivate current plugin .El .Ss METASHEETS .Ss Columns Sheet (Shift+C) .Bl -inset -compact .It Properties of columns on the source sheet can be changed with standard editing commands ( Ns Sy e ge g= Del Ns ) on the Sy Columns Sheet Ns . Multiple aggregators can be set by listing them (separated by spaces) in the aggregators column. The 'g' commands affect the selected rows, which are the literal columns on the source sheet. .El .Bl -inset -compact .It (global commands) .El .Bl -tag -width XXXXXXXXXXXXXXX -compact -offset XXX .It Ic gC .No open Sy Columns Sheet No with all visible columns from all sheets .El .Bl -inset -compact .It (sheet-specific commands) .El .Bl -tag -width XXXXXXXXXXXXXXX -compact -offset XXX .It Ic " &" add column from concatenating selected source columns .It Ic "g! gz!" toggle/unset selected columns as key columns on source sheet .It Ic "g+" Ar aggregator add Ar aggregator No to selected source columns .It Ic "g-" No (hyphen) hide selected columns on source sheet .It Ic "g~ g# g% g$ g@ gz# z%" set type of selected columns on source sheet to str/int/float/currency/date/len/floatsi .It Ic " Enter" .No open a Sy Frequency Table No sheet grouped by column referenced in current row .El . .Ss Sheets Sheet (Shift+S) .Bl -inset -compact .It open Sy Sheets Stack Ns , which contains only the active sheets on the current stack .It (global commands) .El .Bl -tag -width XXXXXXXXXXXXXXX -compact -offset XXX .It Ic gS .No open Sy Sheets Sheet Ns , which contains all sheets from current session, active and inactive .It Ic "Alt" Ar number .No jump to sheet Ar number Ns .El .Bl -inset -compact .It (sheet-specific commands) .El .Bl -tag -width XXXXXXXXXXXXXXX -compact -offset XXX .It Ic " Enter" jump to sheet referenced in current row .It Ic "gEnter" push selected sheets to top of sheet stack .It Ic " a" add row to reference a new blank sheet .It Ic "gC gI" .No open Sy Columns Sheet Ns / Ns Sy Describe Sheet No with all visible columns from selected sheets .It Ic "g^R" .No reload all selected sheets .It Ic "z^C gz^C" abort async threads for current/selected sheets(s) .It Ic "g^S" save selected or all sheets .It Ic " &" Ar jointype .No merge selected sheets with visible columns from all, keeping rows according to Ar jointype Ns : .El .Bl -tag -width x -compact -offset XXXXXXXXXXXXXXXXXXXX .It Sy "\&." .Sy inner No " keep only rows which match keys on all sheets" .It Sy "\&." .Sy outer No " keep all rows from first selected sheet" .It Sy "\&." .Sy full No " keep all rows from all sheets (union)" .It Sy "\&." .Sy diff No " keep only rows NOT in all sheets" .It Sy "\&." .Sy append No "keep all rows from all sheets (concatenation)" .It Sy "\&." .Sy extend No "copy first selected sheet, keeping all rows and sheet type, and extend with columns from other sheets" .It Sy "\&." .Sy merge No " mostly keep all rows from first selected sheet, except prioritise cells with non-null/non-error values" .El . .Ss Options Sheet (Shift+O) .Bl -inset -compact .It (global commands) .El .Bl -tag -width XXXXXXXXXXXXXXX -compact -offset XXX .It Ic Shift+O .No edit global options (apply to Sy all sheets Ns ) .It Ic zO .No edit sheet options (apply to Sy current sheet No only) .It Ic gO .No open Sy options.config No as Sy TextSheet .El .Bl -inset -compact .It (sheet-specific commands) .El .Bl -tag -width XXXXXXXXXXXXXXX -compact -offset XXX .It Ic "Enter e" edit option at current row .It Ic "d" remove option override for this context .El . .Ss CommandLog (Shift+D) .Bl -inset -compact .It (global commands) .El .Bl -tag -width XXXXXXXXXXXXXXX -compact -offset XXX .It Ic D .No open current sheet's Sy CommandLog No with all other loose ends removed; includes commands from parent sheets .It Ic gD .No open global Sy CommandLog No for all commands executed in the current session .It Ic zD .No open current sheet's Sy CommandLog No with the parent sheets commands' removed .El .Bl -inset -compact .It (sheet-specific commands) .El .Bl -tag -width XXXXXXXXXXXXXXX -compact -offset XXX .It Ic " x" replay command in current row .It Ic " gx" replay contents of entire CommandLog .It Ic " ^C" abort replay .El . .Ss DERIVED SHEETS .Ss Frequency Table (Shift+F) .Bl -inset -compact .It A Sy Frequency Table No groups rows by one or more columns, and includes summary columns for those with aggregators. .It (global commands) .El .Bl -tag -width XXXXXXXXXXXXXXX -compact -offset XXX .It Ic gF open Frequency Table, grouped by all key columns on source sheet .It Ic zF open one-line summary for all rows and selected rows .El .Bl -inset -compact .It (sheet-specific commands) .El .Bl -tag -width XXXXXXXXXXXXXXX -compact -offset XXX .It Ic " s t u" select/toggle/unselect these entries in source sheet .It Ic " Enter gEnter" open copy of source sheet with rows that are grouped in current cell / selected rows .El . .Ss Describe Sheet (Shift+I) .Bl -inset -compact .It A Sy Describe Sheet No contains descriptive statistics for all visible columns. .It (global commands) .El .Bl -tag -width XXXXXXXXXXXXXXX -compact -offset XXX .It Ic gI .No open Sy Describe Sheet No for all visible columns on all sheets .El .Bl -inset -compact .It (sheet-specific commands) .El .Bl -tag -width XXXXXXXXXXXXXXX -compact -offset XXX .It Ic "zs zu" select/unselect rows on source sheet that are being described in current cell .It Ic " !" toggle/unset current column as a key column on source sheet .It Ic " Enter" .No open a Sy Frequency Table No sheet grouped on column referenced in current row .It Ic "zEnter" open copy of source sheet with rows described in current cell .El . .Ss Pivot Table (Shift+W) .Bl -inset -compact .It Set key column(s) and aggregators on column(s) before pressing Sy Shift+W No on the column to pivot. .It (sheet-specific commands) .El .Bl -tag -width XXXXXXXXXXXXXXX -compact -offset XXX .It Ic " Enter" open sheet of source rows aggregated in current pivot row .It Ic "zEnter" open sheet of source rows aggregated in current pivot cell .El .Ss Melted Sheet (Shift+M) .Bl -inset -compact .It Open Melted Sheet (unpivot), with key columns retained and all non-key columns reduced to Variable-Value rows. .It (global commands) .El .Bl -tag -width XXXXXXXXXXXXXXX -compact -offset XXX .It Ic "gM" Ar regex .No open Melted Sheet (unpivot), with key columns retained and Ar regex No capture groups determining how the non-key columns will be reduced to Variable-Value rows. .El .Ss Python Object Sheet (^X ^Y g^Y z^Y) .Bl -inset -compact .It (sheet-specific commands) .El .Bl -tag -width XXXXXXXXXXXXXXX -compact -offset XXX .It Ic " Enter" dive further into Python object .It Ic " v" toggle show/hide for methods and hidden properties .It Ic "gv zv" show/hide methods and hidden properties .El . .Sh COMMANDLINE OPTIONS .No Add Sy -n Ns / Ns Sy --nonglobal No to make subsequent CLI options "sheet-specific" (applying only to paths specified directly on the CLI). By default, CLI options apply to all sheets. .Pp .No Options can also be set via the Ar Options Sheet No or a Ar .visidatarc No (see Sx FILES Ns ). .Pp .Bl -tag -width XXXXXXXXXXXXXXXXXXXXXXXXXXX -compact .It Cm -P Ns = Ns Ar longname .No preplay Ar longname No before replay or regular launch; limited to Sy Base Sheet No bound commands .It Cm + Ns Ar toplevel Ns : Ns Ar subsheet Ns : Ns Ar col Ns : Ns Ar row .No launch vd with Ar subsheet No of Ar toplevel No at top-of-stack, and cursor at Ar col No and Ar row Ns ; all arguments are optional .Pp .Lo f filetype filetype .No "tsv " set loader to use for .Ar filetype instead of file extension . .Lo y confirm-overwrite F .No "True " overwrite existing files without confirmation . . .El .Bl -tag -width XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -compact .It Sy --mouse-interval Ns = Ns Ar "int " No "1" max time between press/release for click (ms) .It Sy --null-value Ns = Ns Ar "NoneType " No "None" a value to be counted as null .It Sy --undo Ns = Ns Ar "bool " No "True" enable undo/redo .It Sy --col-cache-size Ns = Ns Ar "int " No "0" max number of cache entries in each cached column .It Sy --clean-names No " False" clean column/sheet names to be valid Python identifiers .It Sy --default-width Ns = Ns Ar "int " No "20" default column width .It Sy --default-height Ns = Ns Ar "int " No "10" default column height .It Sy --textwrap-cells Ns = Ns Ar "bool " No "True" wordwrap text for multiline rows .It Sy --quitguard No " False" confirm before quitting last sheet .It Sy --debug No " False" exit on error and display stacktrace .It Sy --skip Ns = Ns Ar "int " No "0" skip N rows before header .It Sy --header Ns = Ns Ar "int " No "1" parse first N rows as column names .It Sy --load-lazy No " False" load subsheets always (False) or lazily (True) .It Sy --force-256-colors No " False" use 256 colors even if curses reports fewer .It Sy --use-default-colors No " False" curses use default terminal colors .It Sy --note-pending Ns = Ns Ar "str " No "\[u231B]" note to display for pending cells .It Sy --note-format-exc Ns = Ns Ar "str " No "?" cell note for an exception during formatting .It Sy --note-getter-exc Ns = Ns Ar "str " No "!" cell note for an exception during computation .It Sy --note-type-exc Ns = Ns Ar "str " No "!" cell note for an exception during type conversion .It Sy --scroll-incr Ns = Ns Ar "int " No "3" amount to scroll with scrollwheel .It Sy --name-joiner Ns = Ns Ar "str " No "_" string to join sheet or column names .It Sy --value-joiner Ns = Ns Ar "str " No " " string to join display values .It Sy --wrap No " False" wrap text to fit window width on TextSheet .It Sy --save-filetype Ns = Ns Ar "str " No "tsv" specify default file type to save as .It Sy --profile Ns = Ns Ar "str " No "" filename to save binary profiling data .It Sy --min-memory-mb Ns = Ns Ar "int " No "0" minimum memory to continue loading and async processing .It Sy --input-history Ns = Ns Ar "str " No "" basename of file to store persistent input history .It Sy --encoding Ns = Ns Ar "str " No "utf-8" encoding passed to codecs.open .It Sy --encoding-errors Ns = Ns Ar "str " No "surrogateescape" encoding_errors passed to codecs.open .It Sy --bulk-select-clear No " False" clear selected rows before new bulk selections .It Sy --some-selected-rows No " False" if no rows selected, if True, someSelectedRows returns all rows; if False, fails .It Sy --delimiter Ns = Ns Ar "str " No " " field delimiter to use for tsv/usv filetype .It Sy --row-delimiter Ns = Ns Ar "str " No " " row delimiter to use for tsv/usv filetype .It Sy --tsv-safe-newline Ns = Ns Ar "str " No "" replacement for newline character when saving to tsv .It Sy --tsv-safe-tab Ns = Ns Ar "str " No "" replacement for tab character when saving to tsv .It Sy --visibility Ns = Ns Ar "int " No "0" visibility level (0=low, 1=high) .It Sy --expand-col-scanrows Ns = Ns Ar "int " No "1000" number of rows to check when expanding columns (0 = all) .It Sy --json-indent Ns = Ns Ar "NoneType " No "None" indent to use when saving json .It Sy --json-sort-keys No " False" sort object keys when saving to json .It Sy --default-colname Ns = Ns Ar "str " No "" column name to use for non-dict rows .It Sy --filetype Ns = Ns Ar "str " No "" specify file type .It Sy --confirm-overwrite Ns = Ns Ar "bool " No "True" whether to prompt for overwrite confirmation on save .It Sy --safe-error Ns = Ns Ar "str " No "#ERR" error string to use while saving .It Sy --clipboard-copy-cmd Ns = Ns Ar "str " No "" command to copy stdin to system clipboard .It Sy --clipboard-paste-cmd Ns = Ns Ar "str " No "" command to get contents of system clipboard .It Sy --fancy-chooser No " False" a nicer selection interface for aggregators and jointype .It Sy --describe-aggrs Ns = Ns Ar "str " No "mean stdev" numeric aggregators to calculate on Describe sheet .It Sy --histogram-bins Ns = Ns Ar "int " No "0" number of bins for histogram of numeric columns .It Sy --numeric-binning No " False" bin numeric columns into ranges .It Sy --replay-wait Ns = Ns Ar "float " No "0.0" time to wait between replayed commands, in seconds .It Sy --replay-movement No " False" insert movements during replay .It Sy --visidata-dir Ns = Ns Ar "str " No "~/.visidata/" directory to load and store additional files .It Sy --rowkey-prefix Ns = Ns Ar "str " No "\[u30AD]" string prefix for rowkey in the cmdlog .It Sy --cmdlog-histfile Ns = Ns Ar "str " No "" file to autorecord each cmdlog action to .It Sy --regex-flags Ns = Ns Ar "str " No "I" flags to pass to re.compile() [AILMSUX] .It Sy --regex-maxsplit Ns = Ns Ar "int " No "0" maxsplit to pass to regex.split .It Sy --default-sample-size Ns = Ns Ar "int " No "100" number of rows to sample for regex.split .It Sy --show-graph-labels Ns = Ns Ar "bool " No "True" show axes and legend on graph .It Sy --plot-colors Ns = Ns Ar "str " No "" list of distinct colors to use for plotting distinct objects .It Sy --zoom-incr Ns = Ns Ar "float " No "2.0" amount to multiply current zoomlevel when zooming .It Sy --motd-url Ns = Ns Ar "str " No "" source of randomized startup messages .It Sy --dir-recurse No " False" walk source path recursively on DirSheet .It Sy --dir-hidden No " False" load hidden files on DirSheet .It Sy --config Ns = Ns Ar "str " No "~/.visidatarc" config file to exec in Python .It Sy --play Ns = Ns Ar "str " No "" file.vd to replay .It Sy --batch No " False" replay in batch mode (with no interface and all status sent to stdout) .It Sy --output Ns = Ns Ar "NoneType " No "None" save the final visible sheet to output at the end of replay .It Sy --preplay Ns = Ns Ar "str " No "" longnames to preplay before replay .It Sy --imports Ns = Ns Ar "str " No "plugins" imports to preload before .visidatarc (command-line only) .It Sy --incr-base Ns = Ns Ar "float " No "1.0" start value for column increments .It Sy --csv-dialect Ns = Ns Ar "str " No "excel" dialect passed to csv.reader .It Sy --csv-delimiter Ns = Ns Ar "str " No "," delimiter passed to csv.reader .It Sy --csv-quotechar Ns = Ns Ar "str " No """ quotechar passed to csv.reader .It Sy --csv-skipinitialspace Ns = Ns Ar "bool " No "True" skipinitialspace passed to csv.reader .It Sy --csv-escapechar Ns = Ns Ar "NoneType " No "None" escapechar passed to csv.reader .It Sy --csv-lineterminator Ns = Ns Ar "str " No " " lineterminator passed to csv.writer .It Sy --safety-first No " False" sanitize input/output to handle edge cases, with a performance cost .It Sy --fixed-rows Ns = Ns Ar "int " No "1000" number of rows to check for fixed width columns .It Sy --fixed-maxcols Ns = Ns Ar "int " No "0" max number of fixed-width columns to create (0 is no max) .It Sy --postgres-schema Ns = Ns Ar "str " No "public" The desired schema for the Postgres database .It Sy --http-max-next Ns = Ns Ar "int " No "0" max next.url pages to follow in http response .It Sy --html-title Ns = Ns Ar "str " No "

{sheet.name}

" table header when saving to html .It Sy --pcap-internet Ns = Ns Ar "str " No "n" (y/s/n) if save_dot includes all internet hosts separately (y), combined (s), or does not include the internet (n) .It Sy --graphviz-edge-labels Ns = Ns Ar "bool " No "True" whether to include edge labels on graphviz diagrams .It Sy --pdf-tables No " False" parse PDF for tables instead of pages of text .It Sy --plugins-url Ns = Ns Ar "str " No "https://visidata.org/plugins/plugins.jsonl" source of plugins sheet .El . .Ss DISPLAY OPTIONS .No Display options can only be set via the Sx Options Sheet No or a Pa .visidatarc No (see Sx FILES Ns ). .Pp . .Bl -tag -width XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -compact .It Sy "disp_splitwin_pct " No "0" height of second sheet on screen .It Sy "disp_currency_fmt " No "%.02f" default fmtstr to format for currency values .It Sy "disp_float_fmt " No "{:.02f}" default fmtstr to format for float values .It Sy "disp_int_fmt " No "{:.0f}" default fmtstr to format for int values .It Sy "disp_date_fmt " No "%Y-%m-%d" default fmtstr to strftime for date values .It Sy "disp_note_none " No "\[u2300]" visible contents of a cell whose value is None .It Sy "disp_truncator " No "\[u2026]" indicator that the contents are only partially visible .It Sy "disp_oddspace " No "\[u00B7]" displayable character for odd whitespace .It Sy "disp_more_left " No "<" header note indicating more columns to the left .It Sy "disp_more_right " No ">" header note indicating more columns to the right .It Sy "disp_error_val " No "" displayed contents for computation exception .It Sy "disp_ambig_width " No "1" width to use for unicode chars marked ambiguous .It Sy "disp_pending " No "" string to display in pending cells .It Sy "color_note_pending " No "bold magenta" color of note in pending cells .It Sy "color_note_type " No "226 yellow" color of cell note for non-str types in anytype columns .It Sy "color_note_row " No "220 yellow" color of row note on left edge .It Sy "disp_column_sep " No "|" separator between columns .It Sy "disp_keycol_sep " No "\[u2551]" separator between key columns and rest of columns .It Sy "disp_rowtop_sep " No "|" .It Sy "disp_rowmid_sep " No "\[u205D]" .It Sy "disp_rowbot_sep " No "\[u205D]" .It Sy "disp_rowend_sep " No "\[u2551]" .It Sy "disp_keytop_sep " No "\[u2551]" .It Sy "disp_keymid_sep " No "\[u2551]" .It Sy "disp_keybot_sep " No "\[u2551]" .It Sy "disp_endtop_sep " No "\[u2551]" .It Sy "disp_endmid_sep " No "\[u2551]" .It Sy "disp_endbot_sep " No "\[u2551]" .It Sy "disp_selected_note " No "\[u2022]" .It Sy "disp_sort_asc " No "\[u2191]\[u219F]\[u21DE]\[u21E1]\[u21E7]\[u21D1]" characters for ascending sort .It Sy "disp_sort_desc " No "\[u2193]\[u21A1]\[u21DF]\[u21E3]\[u21E9]\[u21D3]" characters for descending sort .It Sy "color_default " No "normal" the default color .It Sy "color_default_hdr " No "bold" color of the column headers .It Sy "color_bottom_hdr " No "underline" color of the bottom header row .It Sy "color_current_row " No "reverse" color of the cursor row .It Sy "color_current_col " No "bold" color of the cursor column .It Sy "color_current_hdr " No "bold reverse" color of the header for the cursor column .It Sy "color_column_sep " No "246 blue" color of column separators .It Sy "color_key_col " No "81 cyan" color of key columns .It Sy "color_hidden_col " No "8" color of hidden columns on metasheets .It Sy "color_selected_row " No "215 yellow" color of selected rows .It Sy "disp_rstatus_fmt " No " {sheet.longname} {sheet.nRows:9d} {sheet.rowtype} " right-side status format string .It Sy "disp_status_fmt " No "{sheet.shortcut}\[u203A] {sheet.name}| " status line prefix .It Sy "disp_lstatus_max " No "0" maximum length of left status line .It Sy "disp_status_sep " No " | " separator between statuses .It Sy "color_keystrokes " No "white" color of input keystrokes on status line .It Sy "color_status " No "bold" status line color .It Sy "color_error " No "red" error message color .It Sy "color_warning " No "yellow" warning message color .It Sy "color_top_status " No "underline" top window status bar color .It Sy "color_active_status" No "bold" active window status bar color .It Sy "color_inactive_status" No "8" inactive window status bar color .It Sy "color_working " No "green" color of system running smoothly .It Sy "color_edit_cell " No "normal" cell color to use when editing cell .It Sy "disp_edit_fill " No "_" edit field fill character .It Sy "disp_unprintable " No "\[u00B7]" substitute character for unprintables .It Sy "disp_histogram " No "*" histogram element character .It Sy "disp_histolen " No "50" width of histogram column .It Sy "disp_replay_play " No "\[u25B6]" status indicator for active replay .It Sy "disp_replay_pause " No "\[u2016]" status indicator for paused replay .It Sy "color_status_replay" No "green" color of replay status indicator .It Sy "disp_pixel_random " No "False" randomly choose attr from set of pixels instead of most common .It Sy "color_graph_hidden " No "238 blue" color of legend for hidden attribute .It Sy "color_graph_selected" No "bold" color of selected graph points .It Sy "color_graph_axis " No "bold" color for graph axis labels .It Sy "color_add_pending " No "green" color for rows pending add .It Sy "color_change_pending" No "reverse yellow" color for cells pending modification .It Sy "color_delete_pending" No "red" color for rows pending delete .It Sy "color_xword_active " No "green" color of active clue .El . .Sh EXAMPLES .Dl Nm vd Cm foo.tsv .No open the file foo.tsv in the current directory .Pp .Dl Nm vd Cm -f sqlite bar.db .No open the file bar.db as a sqlite database .Pp .Dl Nm vd Cm foo.tsv -n -f sqlite bar.db .No open foo.tsv as tsv and bar.db as a sqlite database .Pp .Dl Nm vd Cm -f sqlite foo.tsv bar.db .No open both foo.tsv and bar.db as a sqlite database .Pp .Dl Nm vd Cm -b countries.fixed -o countries.tsv .No convert countries.fixed (in fixed width format) to countries.tsv (in tsv format) .Pp .Dl Nm vd Cm postgres:// Ns Ar username Ns Sy "\&:" Ns Ar password Ns Sy @ Ns Ar hostname Ns Sy "\&:" Ns Ar port Ns Sy / Ns Ar database .No open a connection to the given postgres database .Pp .Dl Nm vd Cm --play tests/pivot.vd --replay-wait 1 --output tests/pivot.tsv .No replay tests/pivot.vd, waiting 1 second between commands, and output the final sheet to test/pivot.tsv .Pp .Dl Ic ls -l | Nm vd Cm -f fixed --skip 1 --header 0 .No parse the output of ls -l into usable data .Pp .Dl Ic ls | vd | lpr .No interactively select a list of filenames to send to the printer .Pp .Dl Ic vd newfile.tsv .No open a blank sheet named Ar newfile No if file does not exist .Pp .Dl Ic vd sample.xlsx +:sheet1:2:3 .No launch with Sy sheet1 No at top-of-stack, and cursor at column Sy 2 No and row Sy 3 .Pp .Dl Ic vd -P open-plugins .No preplay longname Sy open-plugins No before starting the session .Sh FILES At the start of every session, .Sy VisiData No looks for Pa $HOME/.visidatarc Ns , and calls Python exec() on its contents if it exists. For example: .Bd -literal options.min_memory_mb=100 # stop processing without 100MB free bindkey('0', 'go-leftmost') # alias '0' to go to first column, like vim def median(values): L = sorted(values) return L[len(L)//2] aggregator('median', median) .Ed .Pp Functions defined in .visidatarc are available in python expressions (e.g. in derived columns). . .Sh SUPPORTED SOURCES Core VisiData includes these sources: .Pp .Bl -inset -compact -offset xxx .It Sy tsv No (tab-separated value) .Bl -inset -compact -offset xxx .It Plain and simple. Nm VisiData No writes tsv format by default. See the Sy --tsv-delimiter No option. .El .El .Pp .Bl -inset -compact -offset xxx .It Sy csv No (comma-separated value) .Bl -inset -compact -offset xxx .It .csv files are a scourge upon the earth, and still regrettably common. .It See the Sy --csv-dialect Ns , Sy --csv-delimiter Ns , Sy --csv-quotechar Ns , and Sy --csv-skipinitialspace No options. .It Accepted dialects are Ic excel-tab Ns , Ic unix Ns , and Ic excel Ns . .El .El .Pp .Bl -inset -compact -offset xxx .It Sy fixed No (fixed width text) .Bl -inset -compact -offset xxx .It Columns are autodetected from the first 1000 rows (adjustable with Sy --fixed-rows Ns ). .El .El .Pp .Bl -inset -compact -offset xxx .It Sy json No (single object) and Sy jsonl Ns / Ns Sy ndjson Ns / Ns Sy ldjson No (one object per line). .Bl -inset -compact -offset xxx .It Cells containing lists (e.g. Sy [3] Ns ) or dicts ( Ns Sy {3} Ns ) can be expanded into new columns with Sy "\&(" No and unexpanded with Sy "\&)" Ns . .El .El .Pp .Bl -inset -compact -offset xxx .It Sy sqlite .Bl -inset -compact -offset xxx .It May include multiple tables. The initial sheet is the table directory; .Sy Enter No loads the entire table into memory. Sy z^S No saves modifications to source. .El .El .Pp URL schemes are also supported: .Bl -inset -compact -offset xxx .It Sy http No (requires Sy requests Ns ); can be used as transport for with another filetype .El . .Pp For a list of all remaining formats supported by VisiData, see https://visidata.org/formats. .Pp In addition, .Sy .zip Ns , Sy .gz Ns , Sy .bz2 Ns , and Sy .xz No files are decompressed on the fly. .Pp . .Sh SUPPORTED OUTPUT FORMATS These are the supported savers: .Pp .Bl -inset -compact -offset xxx .It Sy tsv No (tab-separated value) .It Sy csv No (comma-separated value) .It Sy json No (one object with all rows) .It Sy jsonl Ns / Ns Sy ndjson Ns / Ns Sy ldjson No (one object per line/row) .Bl -inset -compact -offset xxx .It All expanded subcolumns must be closed (with Sy "\&)" Ns ) to retain the same structure. .El .It Sy sqlite No (save to source with Sy z^S Ns ) .It Sy md No (markdown table) .El .Pp . .Sh AUTHOR .Nm VisiData was made by .An Saul Pwanson Aq Mt vd@saul.pw Ns . ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608178530.0 visidata-2.2.1/visidata/melt.py0000660000175000017500000000674000000000000017135 0ustar00kefalakefala00000000000000import collections import re from visidata import * melt_var_colname = 'Variable' # column name to use for the melted variable name melt_value_colname = 'Value' # column name to use for the melted value # rowdef: {0:sourceRow, 1:Category1, ..., N:CategoryN, ColumnName:Column, ...} class MeltedSheet(Sheet): "Perform 'melt', the inverse of 'pivot', on input sheet." rowtype = 'melted values' @asyncthread def reload(self): self.columns = [] isNull = self.isNullFunc() sheet = self.source for c in sheet.keyCols: self.addColumn(SubColumnItem(0, c)) self.setKeys(self.columns) colsToMelt = [copy(c) for c in sheet.nonKeyVisibleCols] # break down Category1_Category2_ColumnName as per regex valcols = collections.OrderedDict() # ('Category1', 'Category2') -> list of tuple('ColumnName', Column) for c in colsToMelt: c.aggregators = [vd.aggregators['max']] m = re.match(self.regex, c.name) if m: if len(m.groups()) == 1: varvals = m.groups() valcolname = melt_value_colname else: *varvals, valcolname = m.groups() cats = tuple(varvals) if cats not in valcols: valcols[cats] = [] valcols[cats].append((valcolname, c)) ncats = len(varvals) else: vd.status('"%s" column does not match regex, skipping' % c.name) ncats = 0 othercols = set() for colnames, cols in valcols.items(): for cname, _ in cols: othercols.add(cname) if ncats == 1: self.addColumn(ColumnItem(melt_var_colname, 1)) else: for i in range(ncats): self.addColumn(ColumnItem('%s%d' % (melt_var_colname, i+1), i+1)) for cname in othercols: self.addColumn(Column(cname, getter=lambda col,row,cname=cname: row[cname].getValue(row[0]), setter=lambda col,row,val,cname=cname: row[cname].setValues([row[0]], val), aggregators=[vd.aggregators['max']])) self.rows = [] for r in Progress(self.source.rows, 'melting'): for colnames, cols in valcols.items(): meltedrow = {} for varval, c in cols: try: if not isNull(c.getValue(r)): meltedrow[varval] = c except Exception as e: pass if meltedrow: # remove rows with no content (all nulls) meltedrow[0] = r for i, colname in enumerate(colnames): meltedrow[i+1] = colname self.addRow(meltedrow) @Sheet.command('M', 'melt', 'open Melted Sheet (unpivot), with key columns retained and all non-key columns reduced to Variable-Value rows') def melt(sheet): vs = MeltedSheet(sheet.name + '_melted', source=sheet, regex='(.*)') vd.push(vs) @Sheet.command('gM', 'melt-regex', 'open Melted Sheet (unpivot), with key columns retained and regex capture groups determining how the non-key columns will be reduced to Variable-Value rows') def melt_regex(sheet): regex = vd.input("regex to split colname: ", value="(.*)_(.*)", type="regex-capture") vs = MeltedSheet(sheet.name + '_melted', source=sheet, regex=regex) vd.push(vs) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608541899.0 visidata-2.2.1/visidata/menu.py0000660000175000017500000000152400000000000017133 0ustar00kefalakefala00000000000000from visidata import * def open_mnu(p): return MenuSheet(p.name, source=p) vd.save_mnu=vd.save_tsv class MenuSheet(VisiDataMetaSheet): rowtype='labels' # { .x .y .text .color .command .input } class MenuCanvas(BaseSheet): rowtype='labels' def click(self, r): vd.replayOne(vd.cmdlog.newRow(sheet=self.name, col='', row='', longname=r.command, input=r.input)) def reload(self): self.rows = self.source.rows def draw(self, scr): vd.clearCaches() for r in Progress(self.source.rows): x, y = map(int, (r.x, r.y)) clipdraw(scr, y, x, r.text, colors[r.color]) vd.onMouse(scr, y, x, 1, len(r.text), BUTTON1_RELEASED=lambda y,x,key,r=r,sheet=self: sheet.click(r)) MenuSheet.addCommand('z.', 'disp-menu', 'vd.push(MenuCanvas(name, "disp", source=sheet))', '') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612075960.0 visidata-2.2.1/visidata/metasheets.py0000660000175000017500000002624100000000000020334 0ustar00kefalakefala00000000000000import collections from visidata import globalCommand, BaseSheet, Column, options, vd, anytype, ENTER, asyncthread, option, Sheet, IndexSheet from visidata import CellColorizer, RowColorizer, JsonLinesSheet, AttrDict from visidata import ColumnAttr, ColumnItem from visidata import TsvSheet, Path, Option from visidata import undoAttrFunc, VisiData, vlen option('visibility', 0, 'visibility level (0=low, 1=high)') vd_system_sep = '\t' @BaseSheet.lazy_property def optionsSheet(sheet): return OptionsSheet(sheet.name+"_options", source=sheet) @VisiData.lazy_property def globalOptionsSheet(vd): return OptionsSheet('global_options', source='global') class ColumnsSheet(Sheet): rowtype = 'columns' _rowtype = Column _coltype = ColumnAttr precious = False class ValueColumn(Column): 'passthrough to the value on the source cursorRow' def calcValue(self, srcCol): return srcCol.getDisplayValue(srcCol.sheet.cursorRow) def setValue(self, srcCol, val): srcCol.setValue(srcCol.sheet.cursorRow, val) columns = [ ColumnAttr('sheet', type=str), ColumnAttr('name'), ColumnAttr('keycol', type=int, width=0), ColumnAttr('width', type=int), ColumnAttr('height', type=int), ColumnAttr('hoffset', type=int, width=0), ColumnAttr('voffset', type=int, width=0), ColumnAttr('type', 'typestr'), ColumnAttr('fmtstr'), ValueColumn('value'), ColumnAttr('expr'), ColumnAttr('ncalcs', type=int, width=0, cache=False), ColumnAttr('maxtime', type=float, width=0, cache=False), ColumnAttr('totaltime', type=float, width=0, cache=False), ] nKeys = 2 colorizers = [ RowColorizer(7, 'color_key_col', lambda s,c,r,v: r and r.keycol), RowColorizer(8, 'color_hidden_col', lambda s,c,r,v: r and r.hidden), ] def reload(self): if len(self.source) == 1: self.rows = self.source[0].columns self.cursorRowIndex = self.source[0].cursorColIndex self.columns[0].hide() # hide 'sheet' column if only one sheet else: self.rows = [col for vs in self.source for col in vs.visibleCols if vs is not self] def newRow(self): c = type(self.source[0])._coltype() c.recalc(self.source[0]) return c class MetaSheet(Sheet): pass class VisiDataMetaSheet(TsvSheet): pass # commandline must not override these for internal sheets VisiDataMetaSheet.class_options.delimiter = vd_system_sep VisiDataMetaSheet.class_options.header = 1 VisiDataMetaSheet.class_options.skip = 0 VisiDataMetaSheet.class_options.row_delimiter = '\n' VisiDataMetaSheet.class_options.encoding = 'utf-8' class OptionsSheet(Sheet): _rowtype = Option # rowdef: Option rowtype = 'options' precious = False columns = ( ColumnAttr('option', 'name'), Column('value', getter=lambda col,row: col.sheet.diffOption(row.name), setter=lambda col,row,val: options.set(row.name, val, col.sheet.source), ), Column('default', getter=lambda col,row: options.getdefault(row.name)), Column('description', width=40, getter=lambda col,row: options._get(row.name, 'default').helpstr), ColumnAttr('replayable'), ) colorizers = [ CellColorizer(3, None, lambda s,c,r,v: v.value if r and c in s.columns[1:3] and r.name.startswith('color_') else None), ] nKeys = 1 def diffOption(self, optname): return options.getonly(optname, self.source, '') def editOption(self, row): currentValue = options.getobj(row.name, self.source) vd.addUndo(options.set, row.name, currentValue, self.source) if isinstance(row.value, bool): options.set(row.name, not currentValue, self.source) else: options.set(row.name, self.editCell(1, value=currentValue), self.source) def reload(self): self.rows = [] for k in options.keys(): opt = options._get(k) self.addRow(opt) self.columns[1].name = 'global_value' if self.source == 'global' else 'sheet_value' vd._lastInputs = collections.defaultdict(dict) # [input_type] -> {'input': anything} class LastInputsSheet(JsonLinesSheet): columns = [ ColumnItem('type'), ColumnItem('input'), ] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.colnames = {col.name:col for col in self.columns} def addRow(self, row): 'Update lastInputs before adding row.' row = AttrDict(row) vd._lastInputs[row.type][row.input] = 1 return super().addRow(row) def appendRow(self, row): 'Append *row* (AttrDict with *type* and *input*) directly to source.' hist = self.history(row.type) if hist and hist[-1] == row.input: return self.addRow(row) if self.source: with self.source.open_text(mode='a') as fp: import json fp.write(json.dumps(row) + '\n') def history(self, t): 'Return list of inputs in category *t*, with last element being the most recently added.' return list(vd._lastInputs[t].keys()) @VisiData.cached_property def lastInputsSheet(vd): name = options.input_history if not name: return LastInputsSheet('last_inputs', source=None, rows=[]) p = Path(options.visidata_dir)/f'{options.input_history}.jsonl' vs = LastInputsSheet(name, source=p) try: vs.reload.__wrapped__(vs) except FileNotFoundError: pass return vs class VisiDataSheet(IndexSheet): rowtype = 'metasheets' precious = False columns = [ ColumnAttr('items', 'nRows', type=int), ColumnAttr('name', width=0), ColumnAttr('description', width=50), ColumnAttr('command', 'longname', width=0), ColumnAttr('shortcut', 'shortcut_en', width=11), ] nKeys = 0 def reload(self): self.rows = [] for vdattr, sheetname, longname, shortcut, desc in [ ('currentDirSheet', '.', 'open-dir-current', '', 'DirSheet for the current directory'), ('sheetsSheet', 'sheets', 'sheets-stack', 'Shift+S', 'current sheet stack'), ('allSheetsSheet', 'sheets_all', 'sheets-all', 'g Shift+S', 'all sheets this session'), ('allColumnsSheet', 'all_columns', 'columns-all', 'g Shift+C', 'all columns from all sheets'), ('cmdlog', 'cmdlog', 'cmdlog-all', 'g Shift+D', 'log of all commands this session'), ('globalOptionsSheet', 'options_global', 'open-global', 'Shift+O', 'default option values applying to every sheet'), ('recentErrorsSheet', 'errors', 'open-errors', 'Ctrl+E', 'stacktrace of most recent error'), ('statusHistorySheet', 'statuses', 'open-statuses', 'Ctrl+P', 'status messages from current session'), ('threadsSheet', 'threads', 'open-threads', 'Ctrl+T', 'threads and profiling'), ('vdmenu', 'visidata_menu', 'open-vd', 'Shift+V', 'VisiData menu (this sheet)'), ('pluginsSheet', 'plugins', 'open-plugins', '', 'VisiData community plugins'), ]: vs = getattr(vd, vdattr) vs.description = desc vs.shortcut_en = shortcut vs.longname = longname if vs is not self: vs.ensureLoaded() self.addRow(vs) @VisiData.lazy_property def vdmenu(vd): return VisiDataSheet('visidata_menu', source=vd) @VisiData.property def allColumnsSheet(vd): return ColumnsSheet("all_columns", source=list(vd.sheets)) @ColumnsSheet.command('&', 'join-cols', 'add column from concatenating selected source columns') def join_cols(sheet): cols = sheet.onlySelectedRows destSheet = cols[0].sheet if len(set(c.sheet for c in cols)) > 1: vd.fail('joined columns must come from the same source sheet') c = Column(options.name_joiner.join(c.name for c in cols), getter=lambda col,row,cols=cols,ch=options.value_joiner: ch.join(c.getDisplayValue(row) for c in cols)) vd.status(f"added {c.name} to {destSheet}") destSheet.addColumn(c, index=sheet.cursorRowIndex) # copy vd.sheets so that ColumnsSheet itself isn't included (for recalc in addRow) globalCommand('gC', 'columns-all', 'vd.push(vd.allColumnsSheet)', 'open Columns Sheet: edit column properties for all visible columns from all sheets') globalCommand('O', 'options-global', 'vd.push(vd.globalOptionsSheet)', 'open Options Sheet: edit global options (apply to all sheets)') BaseSheet.addCommand('V', 'open-vd', 'vd.push(vd.vdmenu)', 'open VisiData menu: browse list of core sheets') BaseSheet.addCommand('zO', 'options-sheet', 'vd.push(sheet.optionsSheet)', 'open Options Sheet: edit sheet options (apply to current sheet only)') BaseSheet.addCommand(None, 'open-inputs', 'vd.push(lastInputsSheet)', '') Sheet.addCommand('C', 'columns-sheet', 'vd.push(ColumnsSheet(name+"_columns", source=[sheet]))', 'open Columns Sheet: edit column properties for current sheet') # used ColumnsSheet, affecting the 'row' (source column) ColumnsSheet.addCommand('g!', 'key-selected', 'for c in onlySelectedRows: c.sheet.setKeys([c])', 'toggle selected rows as key columns on source sheet') ColumnsSheet.addCommand('gz!', 'key-off-selected', 'for c in onlySelectedRows: c.sheet.unsetKeys([c])', 'unset selected rows as key columns on source sheet') ColumnsSheet.addCommand('g-', 'hide-selected', 'onlySelectedRows.hide()', 'hide selected columns on source sheet') ColumnsSheet.addCommand(None, 'resize-source-rows-max', 'for c in selectedRows or [cursorRow]: c.setWidth(c.getMaxWidth(c.sheet.visibleRows))', 'adjust widths of selected source columns') ColumnsSheet.addCommand('g%', 'type-float-selected', 'onlySelectedRows.type=float', 'set type of selected columns to float') ColumnsSheet.addCommand('g#', 'type-int-selected', 'onlySelectedRows.type=int', 'set type of selected columns to int') ColumnsSheet.addCommand('gz#', 'type-len-selected', 'onlySelectedRows.type=vlen', 'set type of selected columns to len') ColumnsSheet.addCommand('g@', 'type-date-selected', 'onlySelectedRows.type=date', 'set type of selected columns to date') ColumnsSheet.addCommand('g$', 'type-currency-selected', 'onlySelectedRows.type=currency', 'set type of selected columns to currency') ColumnsSheet.addCommand('g~', 'type-string-selected', 'onlySelectedRows.type=str', 'set type of selected columns to str') ColumnsSheet.addCommand('gz~', 'type-any-selected', 'onlySelectedRows.type=anytype', 'set type of selected columns to anytype') ColumnsSheet.addCommand('gz%', 'type-floatsi-selected', 'onlySelectedRows.type=floatsi', 'set type of selected columns to floatsi') ColumnsSheet.addCommand('', 'type-floatlocale-selected', 'onlySelectedRows.type=floatlocale', 'set type of selected columns to float using system locale') OptionsSheet.addCommand('d', 'unset-option', 'options.unset(cursorRow.name, str(source))', 'remove option override for this context') OptionsSheet.addCommand(None, 'edit-option', 'editOption(cursorRow)', 'edit option at current row') OptionsSheet.bindkey('e', 'edit-option') OptionsSheet.bindkey(ENTER, 'edit-option') MetaSheet.class_options.header = 0 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608178530.0 visidata-2.2.1/visidata/misc.py0000660000175000017500000000046300000000000017123 0ustar00kefalakefala00000000000000import random from visidata import Sheet Sheet.addCommand(None, 'random-rows', 'nrows=int(input("random number to select: ", value=nRows)); vs=copy(sheet); vs.name=name+"_sample"; vs.rows=random.sample(rows, nrows or nRows); vd.push(vs)', 'open duplicate sheet with a random population subset of N rows') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612664826.0 visidata-2.2.1/visidata/modify.py0000660000175000017500000002151100000000000017454 0ustar00kefalakefala00000000000000from visidata import * option('color_add_pending', 'green', 'color for rows pending add') option('color_change_pending', 'reverse yellow', 'color for cells pending modification') option('color_delete_pending', 'red', 'color for rows pending delete') # deferred cached @Sheet.lazy_property def _deferredAdds(sheet): return dict() # [s.rowid(row)] -> row @Sheet.lazy_property def _deferredMods(sheet): return dict() # [s.rowid(row)] -> (row, { [col] -> val }) @Sheet.lazy_property def _deferredDels(sheet): return dict() # [s.rowid(row)] -> row Sheet.colorizers += [ RowColorizer(9, 'color_add_pending', lambda s,c,r,v: s.rowid(r) in s._deferredAdds), CellColorizer(8, 'color_change_pending', lambda s,c,r,v: s.isChanged(c, r)), RowColorizer(9, 'color_delete_pending', lambda s,c,r,v: s.isDeleted(r)), ] @Sheet.api def preloadHook(sheet): sheet._deferredAdds.clear() sheet._deferredMods.clear() sheet._deferredDels.clear() # how to call the previous preloadHook? Sheet.preloadHook(sheet) @Sheet.api def rowAdded(self, row): 'Mark row as a deferred add-row' self._deferredAdds[self.rowid(row)] = row def _undoRowAdded(sheet, row): del sheet._deferredAdds[sheet.rowid(row)] vd.addUndo(_undoRowAdded, self, row) @Column.api def cellChanged(col, row, val): 'Mark cell at row for col as a deferred edit-cell' oldval = col.getValue(row) if oldval != val: rowid = col.sheet.rowid(row) if rowid not in col.sheet._deferredMods: rowmods = {} col.sheet._deferredMods[rowid] = (row, rowmods) else: _, rowmods = col.sheet._deferredMods[rowid] rowmods[col] = val def _undoCellChanged(col, row, oldval): if oldval == col.getSourceValue(row): # if we have reached the original value, remove from defermods entirely del col.sheet._deferredMods[col.sheet.rowid(row)] else: # otherwise, update deferredMods with previous value _, rowmods = col.sheet._deferredMods[rowid] rowmods[col] = oldval vd.addUndo(_undoCellChanged, col, row, oldval) @Sheet.api def rowDeleted(self, row): 'Mark row as a deferred delete-row' self._deferredDels[self.rowid(row)] = row def _undoRowDeleted(sheet, row): del sheet._deferredDels[sheet.rowid(row)] vd.addUndo(_undoRowDeleted, self, row) @Sheet.api @asyncthread def addNewRows(sheet, n, idx): 'Add *n* new rows after row at *idx*.' addedRows = {} for i in Progress(range(n), 'adding'): row = sheet.newRow() addedRows[sheet.rowid(row)] = row sheet.addRow(row, idx+1) if sheet.defer: sheet.rowAdded(row) @asyncthread def _removeRows(): sheet.deleteBy(lambda r,sheet=sheet,addedRows=addedRows: sheet.rowid(r) in addedRows, commit=True) vd.addUndo(_removeRows) @Sheet.api def deleteBy(sheet, func, commit=False): 'Delete rows on sheet for which ``func(row)`` returns true. Return number of rows deleted. If sheet.defer is set and *commit* is True, remove rows immediately without deferring.' oldrows = copy(sheet.rows) oldidx = sheet.cursorRowIndex ndeleted = 0 row = None # row to re-place cursor after # if commit is True, commit to delete, even if defer is True if sheet.defer and not commit: ndeleted = 0 for r in sheet.gatherBy(func, 'deleting'): sheet.rowDeleted(r) ndeleted += 1 return ndeleted while oldidx < len(oldrows): if not func(oldrows[oldidx]): row = sheet.rows[oldidx] break oldidx += 1 sheet.rows.clear() # must delete from the existing rows object for r in Progress(oldrows, 'deleting'): if not func(r): sheet.rows.append(r) if r is row: sheet.cursorRowIndex = len(sheet.rows)-1 else: sheet.deleteSourceRow(r) ndeleted += 1 if not commit: vd.addUndo(setattr, sheet, 'rows', oldrows) if ndeleted: vd.status('deleted %s %s' % (ndeleted, sheet.rowtype)) return ndeleted @Sheet.api def isDeleted(self, row): 'Return True if *row* has been deferred for deletion.' return self.rowid(row) in self._deferredDels @Sheet.api def isChanged(self, col, row): 'Return True if cell at *row* for *col* has been deferred for modification.' try: row, rowmods = self._deferredMods[self.rowid(row)] newval = rowmods[col] curval = col.getSourceValue(row) return col.type(newval) != col.type(curval) except KeyError: return False except Exception: return False @Column.api def getSourceValue(col, row): 'For deferred sheets, return value for *row* in this *col* as it would be in the source, without any deferred modifications applied.' return Column.calcValue(col, row) @Sheet.api def commitAdds(self): 'Return the number of rows that have been marked for deferred add-row. Clear the marking.' nadded = len(self._deferredAdds.values()) if nadded: vd.status('added %s %s' % (nadded, self.rowtype)) self._deferredAdds.clear() return nadded @Sheet.api def commitMods(self): 'Return the number of modifications (that are not deferred deletes or adds) that been marked for defer mod. Change value to mod for row in col. Clear the marking.' nmods = 0 for row, rowmods in self._deferredMods.values(): for col, val in rowmods.items(): try: col.putValue(row, val) nmods += 1 except Exception as e: vd.exceptionCaught(e) self._deferredMods.clear() return nmods @Sheet.api def commitDeletes(self): 'Return the number of rows that have been marked for deletion. Delete the rows. Clear the marking.' ndeleted = self.deleteBy(self.isDeleted, commit=True) if ndeleted: vd.status('deleted %s %s' % (ndeleted, self.rowtype)) return ndeleted @Sheet.api def deleteSourceRow(sheet, row): pass @asyncthread @Sheet.api def putChanges(sheet): 'Commit changes to ``sheet.source``. May overwrite source completely without confirmation. Overrideable.' sheet.commitAdds() sheet.commitMods() sheet.commitDeletes() saveSheets(Path(sheet.source), sheet, confirm_overwrite=False) # clear after save, to ensure cstr (in commit()) is aware of deletes sheet._deferredDels.clear() @Sheet.api def getDeferredChanges(sheet): '''Return changes made to deferred sheets that have not been committed, as a tuple (added_rows, modified_rows, deleted_rows). *modified_rows* does not include any *added_rows* or *deleted_rows*. - *added_rows*: { rowid:row, ... } - *modified_rows*: { rowid: (row, { col:val, ... }), ... } - *deleted_rows*: { rowid: row } *rowid* is from ``Sheet.rowid(row)``. *col* is an actual Column object. ''' # only report mods if they aren't adds or deletes mods = {} # [rowid] -> (row, dict(col:val)) for row, rowmods in sheet._deferredMods.values(): rowid = sheet.rowid(row) if rowid not in sheet._deferredAdds and rowid not in sheet._deferredDels: mods[rowid] = (row, {col:val for col, val in rowmods.items() if sheet.isChanged(col, row)}) return sheet._deferredAdds, mods, sheet._deferredDels @Sheet.api def changestr(self, adds, mods, deletes): 'Return a str for status that outlines how many deferred changes are going to be committed.' cstr = '' if adds: cstr += 'add %d %s' % (len(adds), self.rowtype) if mods: if cstr: cstr += ' and ' cstr += 'change %d values' % sum(len(rowmods) for row, rowmods in mods.values()) if deletes: if cstr: cstr += ' and ' cstr += 'delete %d %s' % (len(deletes), self.rowtype) return cstr @Sheet.api def commit(sheet, *rows): 'Commit all deferred changes on this sheet to original ``sheet.source``.' if not sheet.defer: vd.fail('commit-sheet is not enabled for this sheet type') adds, mods, deletes = sheet.getDeferredChanges() cstr = sheet.changestr(adds, mods, deletes) path = sheet.source if sheet.options.confirm_overwrite: vd.confirm('really %s? ' % cstr) sheet.putChanges() Sheet.addCommand('a', 'add-row', 'addNewRows(1, cursorRowIndex); cursorDown(1)', 'append a blank row') Sheet.addCommand('ga', 'add-rows', 'addNewRows(int(input("add rows: ", value=1)), cursorRowIndex); cursorDown(1)', 'append N blank rows') Sheet.addCommand('za', 'addcol-new', 'addColumnAtCursor(SettableColumn(input("column name: "))); cursorRight(1)', 'append an empty column') Sheet.addCommand('gza', 'addcol-bulk', 'addColumnAtCursor(*(SettableColumn() for c in range(int(input("add columns: "))))); cursorRight(1)', 'append N empty columns') Sheet.addCommand('z^S', 'commit-sheet', 'commit()') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608178530.0 visidata-2.2.1/visidata/motd.py0000660000175000017500000000151400000000000017131 0ustar00kefalakefala00000000000000'''motd: display a low-priority random Message Of The Day on startup. Call `domotd()` to spawn an asyncthread to read and/or fetch a motd file from a url. The file may be text or unheaded TSV, with one message per row in the first column. Any Exception ends the thread silently. options.motd_url may be set to another URL, or empty to disable entirely. ''' import random from visidata import option, options, asyncsingle, urlcache, vd from visidata import __version__ option('motd_url', 'https://visidata.org/motd-'+__version__, 'source of randomized startup messages', sheettype=None) @asyncsingle def domotd(): try: if options.motd_url: p = urlcache(options.motd_url, days=1) line = random.choice(list(p)) vd.status(line.split('\t')[0], priority=-1) except Exception: pass ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612075960.0 visidata-2.2.1/visidata/movement.py0000660000175000017500000002677200000000000020035 0ustar00kefalakefala00000000000000import itertools import re from visidata import vd, VisiData, BaseSheet, Sheet, Column, Progress, globalCommand, ALT __all__ = ['rotateRange'] def rotateRange(n, idx, reverse=False): 'Wraps an iter starting from idx. Yields indices from idx to n and then 0 to idx.' if reverse: rng = range(idx-1, -1, -1) rng2 = range(n-1, idx-1, -1) else: rng = range(idx+1, n) rng2 = range(0, idx+1) wrapped = False with Progress(total=n) as prog: for r in itertools.chain(rng, rng2): prog.addProgress(1) if not wrapped and r in rng2: vd.status('search wrapped') wrapped = True yield r @Sheet.api def pageLeft(self): '''Redraw page one screen to the left. Note: keep the column cursor in the same general relative position: - if it is on the furthest right column, then it should stay on the furthest right column if possible - likewise on the left or in the middle So really both the `leftIndex` and the `cursorIndex` should move in tandem until things are correct.''' targetIdx = self.leftVisibleColIndex # for rightmost column firstNonKeyVisibleColIndex = self.visibleCols.index(self.nonKeyVisibleCols[0]) while self.rightVisibleColIndex != targetIdx and self.leftVisibleColIndex > firstNonKeyVisibleColIndex: self.cursorVisibleColIndex -= 1 self.leftVisibleColIndex -= 1 self.calcColLayout() # recompute rightVisibleColIndex # in case that rightmost column is last column, try to squeeze maximum real estate from screen if self.rightVisibleColIndex == self.nVisibleCols-1: # try to move further left while right column is still full width while self.leftVisibleColIndex > 0: rightcol = self.visibleCols[self.rightVisibleColIndex] if (rightcol.width or 0) > self._visibleColLayout[self.rightVisibleColIndex][1]: # went too far self.cursorVisibleColIndex += 1 self.leftVisibleColIndex += 1 break else: self.cursorVisibleColIndex -= 1 self.leftVisibleColIndex -= 1 self.calcColLayout() # recompute rightVisibleColIndex @Sheet.api def moveToNextRow(vs, func, reverse=False): 'Move cursor to next (prev if reverse) row for which func returns True. Returns False if no row meets the criteria.' rng = range(vs.cursorRowIndex-1, -1, -1) if reverse else range(vs.cursorRowIndex+1, vs.nRows) for i in rng: try: if func(vs.rows[i]): vs.cursorRowIndex = i return True except Exception: pass return False @Sheet.api def nextColRegex(sheet, colregex): 'Go to first visible column after the cursor matching `colregex`.' pivot = sheet.cursorVisibleColIndex for i in itertools.chain(range(pivot+1, len(sheet.visibleCols)), range(0, pivot+1)): c = sheet.visibleCols[i] if re.search(colregex, c.name, sheet.regex_flags()): return i vd.fail('no column name matches /%s/' % colregex) @Column.property def visibleWidth(self): 'Width of column as is displayed in terminal' vcolidx = self.sheet.visibleCols.index(self) return self.sheet._visibleColLayout[vcolidx][1] Sheet.addCommand(None, 'go-left', 'cursorRight(-1)', 'go left'), Sheet.addCommand(None, 'go-down', 'cursorDown(+1)', 'go down'), Sheet.addCommand(None, 'go-up', 'cursorDown(-1)', 'go up'), Sheet.addCommand(None, 'go-right', 'cursorRight(+1)', 'go right'), Sheet.addCommand(None, 'go-pagedown', 'cursorDown(nScreenRows-1); sheet.topRowIndex = bottomRowIndex', 'scroll one page forward'), Sheet.addCommand(None, 'go-pageup', 'cursorDown(-nScreenRows+1); sheet.bottomRowIndex = topRowIndex', 'scroll one page backward'), Sheet.addCommand(None, 'go-leftmost', 'sheet.cursorVisibleColIndex = sheet.leftVisibleColIndex = 0', 'go all the way to the left of sheet'), Sheet.addCommand(None, 'go-top', 'sheet.cursorRowIndex = sheet.topRowIndex = 0', 'go all the way to the top of sheet'), Sheet.addCommand(None, 'go-bottom', 'sheet.cursorRowIndex = len(rows); sheet.topRowIndex = cursorRowIndex-nScreenRows', 'go all the way to the bottom of sheet'), Sheet.addCommand(None, 'go-rightmost', 'sheet.leftVisibleColIndex = len(visibleCols)-1; pageLeft(); sheet.cursorVisibleColIndex = len(visibleCols)-1', 'go all the way to the right of sheet'), @Sheet.command('BUTTON1_PRESSED', 'go-mouse', 'set cursor to row and column where mouse was clicked') def go_mouse(sheet): ridx = sheet.visibleRowAtY(sheet.mouseY) if ridx is not None: sheet.cursorRowIndex = ridx cidx = sheet.visibleColAtX(sheet.mouseX) if cidx is not None: sheet.cursorVisibleColIndex = cidx Sheet.addCommand(None, 'scroll-mouse', 'sheet.topRowIndex=cursorRowIndex-mouseY+1', 'scroll to mouse cursor location'), Sheet.addCommand('BUTTON4_PRESSED', 'scroll-up', 'cursorDown(options.scroll_incr); sheet.topRowIndex += options.scroll_incr', 'scroll one row up'), Sheet.addCommand('REPORT_MOUSE_POSITION', 'scroll-down', 'cursorDown(-options.scroll_incr); sheet.topRowIndex -= options.scroll_incr', 'scroll one row down'), Sheet.bindkey('2097152', 'scroll-down') Sheet.addCommand('c', 'go-col-regex', 'sheet.cursorVisibleColIndex=nextColRegex(input("column name regex: ", type="regex-col", defaultLast=True))', 'go to next column with name matching regex') Sheet.addCommand('zc', 'go-col-number', 'sheet.cursorVisibleColIndex = int(input("move to column number: "))', 'go to given column number (0-based)') Sheet.addCommand('zr', 'go-row-number', 'sheet.cursorRowIndex = int(input("move to row number: "))', 'go to the given row number (0-based)') Sheet.addCommand('<', 'go-prev-value', 'moveToNextRow(lambda row,sheet=sheet,col=cursorCol,val=cursorTypedValue: col.getTypedValue(row) != val, reverse=True) or status("no different value up this column")', 'go up current column to next value'), Sheet.addCommand('>', 'go-next-value', 'moveToNextRow(lambda row,sheet=sheet,col=cursorCol,val=cursorTypedValue: col.getTypedValue(row) != val) or status("no different value down this column")', 'go down current column to next value'), Sheet.addCommand('{', 'go-prev-selected', 'moveToNextRow(lambda row,sheet=sheet: sheet.isSelected(row), reverse=True) or status("no previous selected row")', 'go up current column to previous selected row'), Sheet.addCommand('}', 'go-next-selected', 'moveToNextRow(lambda row,sheet=sheet: sheet.isSelected(row)) or status("no next selected row")', 'go down current column to next selected row'), Sheet.addCommand('z<', 'go-prev-null', 'moveToNextRow(lambda row,col=cursorCol,isnull=isNullFunc(): isnull(col.getValue(row)), reverse=True) or status("no null down this column")', 'go up current column to next null value'), Sheet.addCommand('z>', 'go-next-null', 'moveToNextRow(lambda row,col=cursorCol,isnull=isNullFunc(): isnull(col.getValue(row))) or status("no null down this column")', 'go down current column to next null value'), for i in range(1, 11): globalCommand(ALT+str(i)[-1], 'jump-sheet-'+str(i), f'vd.push(*(list(s for s in allSheets if s.shortcut==str({i})) or fail("no sheet")))', f'jump to sheet {i}') BaseSheet.bindkey('KEY_LEFT', 'go-left') BaseSheet.bindkey('KEY_DOWN', 'go-down') BaseSheet.bindkey('KEY_UP', 'go-up') BaseSheet.bindkey('KEY_RIGHT', 'go-right') BaseSheet.bindkey('KEY_HOME', 'go-leftmost') BaseSheet.bindkey('KEY_END', 'go-rightmost') BaseSheet.bindkey('KEY_NPAGE', 'go-pagedown') BaseSheet.bindkey('KEY_PPAGE', 'go-pageup') BaseSheet.bindkey('kHOM5', 'go-top') # Ctrl+Home BaseSheet.bindkey('KEY_EOL', 'go-bottom') # Ctrl+End BaseSheet.bindkey('gKEY_LEFT', 'go-leftmost'), BaseSheet.bindkey('gKEY_RIGHT', 'go-rightmost'), BaseSheet.bindkey('gKEY_UP', 'go-top'), BaseSheet.bindkey('gKEY_DOWN', 'go-bottom'), Sheet.bindkey('BUTTON1_CLICKED', 'go-mouse') Sheet.bindkey('BUTTON3_PRESSED', 'go-mouse') # vim-style scrolling with the 'z' prefix Sheet.addCommand('zz', 'scroll-middle', 'sheet.topRowIndex = cursorRowIndex-int(nScreenRows/2)', 'scroll current row to center of screen') Sheet.addCommand(None, 'go-right-page', 'sheet.cursorVisibleColIndex = sheet.leftVisibleColIndex = rightVisibleColIndex', 'scroll cursor one page right') Sheet.addCommand(None, 'go-left-page', 'pageLeft()', 'scroll cursor one page left') Sheet.addCommand(None, 'scroll-left', 'sheet.cursorVisibleColIndex -= options.scroll_incr', 'scroll one column left') Sheet.addCommand(None, 'scroll-right', 'sheet.cursorVisibleColIndex += options.scroll_incr', 'scroll one column right') Sheet.addCommand(None, 'scroll-leftmost', 'sheet.leftVisibleColIndex = cursorVisibleColIndex', 'scroll sheet to leftmost column') Sheet.addCommand(None, 'scroll-rightmost', 'tmp = cursorVisibleColIndex; pageLeft(); sheet.cursorVisibleColIndex = tmp', 'scroll sheet to rightmost column') Sheet.addCommand('zl', 'scroll-cells-right', 'cursorCol.hoffset += cursorCol.visibleWidth-2', 'scroll display of current column to the right') Sheet.addCommand('zh', 'scroll-cells-left', 'cursorCol.hoffset -= cursorCol.visibleWidth-2', 'scroll display of current column to the left') Sheet.addCommand('gzl', 'scroll-cells-rightmost', 'cursorCol.hoffset = -cursorCol.visibleWidth+2', 'scroll display of current column to the end') Sheet.addCommand('gzh', 'scroll-cells-leftmost', 'cursorCol.hoffset = 0', 'scroll display of current column to the beginning') Sheet.addCommand('zj', 'scroll-cells-down', 'cursorCol.voffset += 1 if cursorCol.height > 1 else fail("multiline column needed for scrolling")', 'scroll display of current column down one line') Sheet.addCommand('zk', 'scroll-cells-up', 'cursorCol.voffset -= 1 if cursorCol.height > 1 else fail("multiline column needed for scrolling")', 'scroll display of current column up one line') Sheet.addCommand('gzj', 'scroll-cells-bottom', 'cursorCol.voffset = -1', 'scroll display of current column to the bottom') Sheet.addCommand('gzk', 'scroll-cells-top', 'cursorCol.voffset = 0', 'scroll display of current column to the top') Sheet.addCommand(None, 'go-end', 'sheet.cursorRowIndex = len(rows)-1; sheet.cursorVisibleColIndex = len(visibleCols)-1', 'go to last row and last column') Sheet.addCommand(None, 'go-home', 'sheet.topRowIndex = sheet.cursorRowIndex = 0; sheet.leftVisibleColIndex = sheet.cursorVisibleColIndex = 0', 'go to first row and first column') BaseSheet.bindkey('CTRL-BUTTON4_PRESSED', 'scroll-left') BaseSheet.bindkey('CTRL-REPORT_MOUSE_POSITION', 'scroll-right') BaseSheet.bindkey('CTRL-2097152', 'scroll-right') BaseSheet.bindkey('zKEY_UP', 'scroll-up') BaseSheet.bindkey('zKEY_DOWN', 'scroll-down') BaseSheet.bindkey('zKEY_LEFT', 'scroll-left') BaseSheet.bindkey('zKEY_RIGHT', 'scroll-right') # vim-like keybindings BaseSheet.bindkey('h', 'go-left'), BaseSheet.bindkey('j', 'go-down'), BaseSheet.bindkey('k', 'go-up'), BaseSheet.bindkey('l', 'go-right'), BaseSheet.bindkey('^F', 'go-pagedown'), BaseSheet.bindkey('^B', 'go-pageup'), BaseSheet.bindkey('gg', 'go-top'), BaseSheet.bindkey('G', 'go-bottom'), BaseSheet.bindkey('gj', 'go-bottom'), BaseSheet.bindkey('gk', 'go-top'), BaseSheet.bindkey('gh', 'go-leftmost'), BaseSheet.bindkey('gl', 'go-rightmost') BaseSheet.addCommand('^^', 'jump-prev', 'vd.sheets[1:] or fail("no previous sheet"); vd.push(vd.sheets[1])', 'jump to previous sheet (swap with current sheet)') BaseSheet.addCommand(None, 'mouse-enable', 'mm, _ = curses.mousemask(-1); status("mouse "+("ON" if mm else "OFF"))', 'enable mouse events') BaseSheet.addCommand(None, 'mouse-disable', 'mm, _ = curses.mousemask(0); status("mouse "+("ON" if mm else "OFF"))', 'disable mouse events') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608541899.0 visidata-2.2.1/visidata/path.py0000660000175000017500000002047300000000000017127 0ustar00kefalakefala00000000000000import os import os.path import sys import pathlib from urllib.parse import urlparse, urlunparse from functools import wraps from visidata import * option('encoding', 'utf-8', 'encoding passed to codecs.open', replay=True) option('encoding_errors', 'surrogateescape', 'encoding_errors passed to codecs.open', replay=True) @functools.lru_cache() def vstat(path, force=False): try: return os.stat(path) except Exception as e: return None def filesize(path): if hasattr(path, 'filesize') and path.filesize is not None: return path.filesize if path.fp or path.is_url(): return 0 st = path.stat() # vstat(path) return st and st.st_size def modtime(path): st = path.stat() return st and st.st_mtime class Path(os.PathLike): 'File and path-handling class, modeled on `pathlib.Path`.' def __init__(self, given, fp=None, lines=None, filesize=None): # Resolve pathname shell variables and ~userdir self.given = os.path.expandvars(os.path.expanduser(given)) self.fp = fp self.lines = lines or [] # shared among all RepeatFile instances self.filesize = filesize self.rfile = None @functools.lru_cache() def stat(self, force=False): return self._path.stat() @property def given(self): 'The path as given to the constructor.' return self._given @given.setter def given(self, given): self._given = given if isinstance(given, os.PathLike): self._path = given else: self._path = pathlib.Path(given) self.ext = self.suffix[1:] if self.suffix: self.name = self._path.name[:-len(self.suffix)] else: self.name = self._path.name # check if file is compressed if self.suffix in ['.gz', '.bz2', '.xz']: self.compression = self.ext uncompressedpath = Path(self.given[:-len(self.suffix)]) self.name = uncompressedpath.name self.ext = uncompressedpath.ext else: self.compression = None def __getattr__(self, k): if hasattr(self.__dict__, k): r = getattr(self.__dict__, k) else: if self.__dict__.get('_path', None) is not None: r = getattr(self._path, k) else: raise AttributeError(k) if isinstance(r, pathlib.Path): return Path(r) return r def __fspath__(self): return self._path.__fspath__() def __lt__(self, a): return self._path.__lt__(a) def __truediv__(self, a): return Path(self._path.__truediv__(a)) def open_text(self, mode='rt'): 'Open path in text mode, using options.encoding and options.encoding_errors. Return open file-pointer or file-pointer-like.' # rfile makes a single-access fp reusable if self.rfile: return self.rfile if self.fp: self.rfile = RepeatFile(fp=self.fp) return self.rfile if 't' not in mode: mode += 't' if self.given == '-': if 'r' in mode: return vd._stdin elif 'w' in mode or 'a' in mode: # convert 'a' to 'w' for stdout: https://bugs.python.org/issue27805 return open(os.dup(vd._stdout.fileno()), 'wt') else: vd.error('invalid mode "%s" for Path.open_text()' % mode) return sys.stderr return self.open(mode=mode, encoding=options.encoding, errors=options.encoding_errors) @wraps(pathlib.Path.read_text) def read_text(self, *args, **kwargs): 'Open the file in text mode and return its entire decoded contents.' if 'encoding' not in kwargs: kwargs['encoding'] = options.encoding if 'errors' not in kwargs: kwargs['errors'] = kwargs.get('encoding_errors', options.encoding_errors) if self.lines: return RepeatFile(iter_lines=self.lines).read() elif self.fp: return self.fp.read() else: return self._path.read_text(*args, **kwargs) @wraps(pathlib.Path.open) def open(self, *args, **kwargs): fn = self if self.compression == 'gz': import gzip return gzip.open(fn, *args, **kwargs) elif self.compression == 'bz2': import bz2 return bz2.open(fn, *args, **kwargs) elif self.compression == 'xz': import lzma return lzma.open(fn, *args, **kwargs) else: return self._path.open(*args, **kwargs) def __iter__(self): with Progress(total=filesize(self)) as prog: with self.open_text() as fd: for i, line in enumerate(fd): prog.addProgress(len(line)) yield line.rstrip('\n') def open_bytes(self, mode='rb'): 'Open the file pointed by this path and return a file object in binary mode.' if 'b' not in mode: mode += 'b' return self.open(mode=mode) def read_bytes(self): 'Return the entire binary contents of the pointed-to file as a bytes object.' with self.open(mode='rb') as fp: return fp.read() def is_url(self): 'Return True if the given path appears to be a URL.' return '://' in self.given def __str__(self): if self.is_url(): return self.given return str(self._path) @wraps(pathlib.Path.stat) @functools.lru_cache() def stat(self, force=False): 'Return Path.stat() if relevant.' try: if not self.is_url(): return self._path.stat() except Exception as e: return None @wraps(pathlib.Path.exists) def exists(self): 'Return True if the path can be opened.' if self.fp or self.is_url(): return True return self._path.exists() @property def scheme(self): 'The URL scheme component, if path is a URL.' if self.is_url(): return urlparse(self.given).scheme def with_name(self, name): 'Return a sibling Path with *name* as a filename in the same directory.' if self.is_url(): urlparts = list(urlparse(self.given)) urlparts[2] = '/'.join(Path(urlparts[2])._parts[1:-1] + [name]) return Path(urlunparse(urlparts)) else: return Path(self._from_parsed_parts(self._drv, self._root, self._parts[:-1] + [name])) class RepeatFile: def __init__(self, *, fp=None, iter_lines=None): 'Provide either fp or iter_lines, and lines will be filled from it.' self.fp = fp self.iter_lines = iter_lines self.lines = [] self.iter = RepeatFileIter(self) def __enter__(self): self.iter = RepeatFileIter(self) return self def __exit__(self, a,b,c): pass def read(self, n=None): r = '' if n is None: n = 10**12 # some too huge number while len(r) < n: try: s = next(self.iter) r += s + '\n' n += len(r) except StopIteration: break # end of file return r def seek(self, n): if n != 0: vd.error('RepeatFile can only seek to beginning') self.iter = RepeatFileIter(self) def __iter__(self): return RepeatFileIter(self) def __next__(self): return next(self.iter) def exists(self): return True class RepeatFileIter: def __init__(self, rf): self.rf = rf self.nextIndex = 0 def __iter__(self): return RepeatFileIter(self.rf) def __next__(self): if self.nextIndex < len(self.rf.lines): r = self.rf.lines[self.nextIndex] elif self.rf.iter_lines: try: r = next(self.rf.iter_lines) self.rf.lines.append(r) except StopIteration: self.rf.iter_lines = None raise elif self.rf.fp: try: r = next(self.rf.fp) self.rf.lines.append(r) except StopIteration: self.rf.fp = None raise else: raise StopIteration() self.nextIndex += 1 return r ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612075960.0 visidata-2.2.1/visidata/pivot.py0000660000175000017500000002506200000000000017333 0ustar00kefalakefala00000000000000import collections from visidata import * # discrete_keys = tuple of formatted discrete keys that group the row # numeric_key is a range # sourcerows is list(all source.rows in group) # pivotrows is { pivot_values: list(source.rows in group with pivot_values) } PivotGroupRow = collections.namedtuple('PivotGroupRow', 'discrete_keys numeric_key sourcerows pivotrows'.split()) def Pivot(source, groupByCols, pivotCols): return PivotSheet('', groupByCols, pivotCols, source=source) def makeErrorKey(col): if col.type is date: return date.min # date('2000-01-01') else: return col.type() def formatRange(col, numeric_key): a, b = numeric_key nankey = makeErrorKey(col) if a is nankey and b is nankey: return '#ERR' elif a == b: return col.format(a) return ' - '.join(col.format(x) for x in numeric_key) class RangeColumn(Column): def format(self, typedval): if typedval is None: return None return formatRange(self.origcol, typedval) class PivotSheet(Sheet): 'Summarize key columns in pivot table and display as new sheet.' rowtype = 'grouped rows' # rowdef: PivotGroupRow def __init__(self, name, groupByCols, pivotCols, **kwargs): super().__init__(name, **kwargs) self.pivotCols = pivotCols # whose values become columns self.groupByCols = groupByCols # whose values become rows def isNumericRange(self, col): return vd.isNumeric(col) and self.source.options.numeric_binning def initCols(self): self.columns = [] # add key columns (grouped by) for colnum, c in enumerate(self.groupByCols): if c in self.pivotCols: continue if self.isNumericRange(c): newcol = RangeColumn(c.name, origcol=c, width=c.width and c.width*2, getter=lambda c,r: r.numeric_key) else: newcol = Column(c.name, width=c.width, fmtstr=c.fmtstr, type=c.type if c.type in vd.typemap else anytype, origcol=c, getter=lambda col,row,i=colnum: row.discrete_keys[i], setter=lambda col,row,val,i=colnum: setitem(row.discrete_keys, i, val) and col.origcol.setValues(row.sourcerows, val)) self.addColumn(newcol) self.setKeys(self.columns) def openRow(self, row): 'open sheet of source rows aggregated in current pivot row' vs = copy(self.source) vs.name += "_%s"%"+".join(row.discrete_keys) vs.rows = sum(row.pivotrows.values(), []) return vs def openCell(self, col, row): 'open sheet of source rows aggregated in current pivot cell' vs = copy(self.source) vs.name += "_%s"%col.aggvalue vs.rows = row.pivotrows.get(col.aggvalue, []) return vs def reload(self): self.initCols() # two different threads for better interactive display self.addAggregateCols() self.groupRows() @asyncthread def addAggregateCols(self): # add aggregated columns aggcols = { # [Column] -> list(aggregators) sourcecol: sourcecol.aggregators for sourcecol in self.source.visibleCols if sourcecol.aggregators } or { # if pivot given but no aggregators specified sourcecol: [vd.aggregators["count"]] for sourcecol in self.pivotCols } if not aggcols: # self.addColumn(ColumnAttr('count', 'sourcerows', type=vlen)) return # aggregators without pivot if not self.pivotCols: for aggcol, aggregatorlist in aggcols.items(): for aggregator in aggregatorlist: aggname = '%s_%s' % (aggcol.name, aggregator.name) c = Column(aggname, type=aggregator.type or aggcol.type, getter=lambda col,row,aggcol=aggcol,agg=aggregator: agg(aggcol, row.sourcerows)) self.addColumn(c) # add pivoted columns for pivotcol in self.pivotCols: allValues = set() for value in Progress(pivotcol.getValues(self.source.rows), 'pivoting', total=len(self.source.rows)): if value in allValues: continue allValues.add(value) if len(self.pivotCols) > 1: valname = '%s_%s' % (pivotcol.name, value) else: valname = str(value) for aggcol, aggregatorlist in aggcols.items(): for aggregator in aggregatorlist: if len(aggcols) > 1: # if more than one aggregated column, include that column name in the new column name aggname = '%s_%s' % (aggcol.name, aggregator.name) else: aggname = aggregator.name if len(aggregatorlist) > 1 or len(aggcols) > 1: colname = '%s_%s' % (aggname, valname) if not self.name: self.name = self.source.name+'_pivot_'+''.join(c.name for c in self.pivotCols) else: colname = valname if not self.name: self.name = self.source.name+'_pivot_'+''.join(c.name for c in self.pivotCols) + '_' + aggname c = Column(colname, type=aggregator.type or aggcol.type, aggvalue=value, getter=lambda col,row,aggcol=aggcol,agg=aggregator: agg(aggcol, row.pivotrows.get(col.aggvalue, []))) self.addColumn(c) # if aggregator.name != 'count': # already have count above # c = Column('Total_' + aggcol.name, # type=aggregator.type or aggcol.type, # getter=lambda col,row,aggcol=aggcol,agg=aggregator: agg(aggcol, row.sourcerows)) # self.addColumn(c) @asyncthread def groupRows(self, rowfunc=None): self.rows = [] discreteCols = [c for c in self.groupByCols if not self.isNumericRange(c)] numericCols = [c for c in self.groupByCols if self.isNumericRange(c)] if len(numericCols) > 1: vd.fail('only one numeric column can be binned') numericBins = [] degenerateBinning = False if numericCols: nbins = options.histogram_bins or int(len(self.source.rows) ** (1./2)) vals = tuple(numericCols[0].getValues(self.source.rows)) minval = min(vals) maxval = max(vals) width = (maxval - minval)/nbins if width == 0: # only one value (and maybe errors) numericBins = [(minval, maxval)] elif (numericCols[0].type in (int, vlen) and nbins > (maxval - minval)) or (width == 1): # (more bins than int vals) or (if bins are of width 1), just use the vals as bins degenerateBinning = True numericBins = [(val, val) for val in sorted(set(vals))] nbins = len(numericBins) else: numericBins = [(minval+width*i, minval+width*(i+1)) for i in range(nbins)] # group rows by their keys (groupByCols), and separate by their pivot values (pivotCols) groups = {} # [formattedDiscreteKeys] -> (numericGroupRows:dict(formattedNumericKeyRange -> PivotGroupRow), groupRow:PivotGroupRow) # groupRow is main/error row for sourcerow in Progress(self.source.iterrows(), 'grouping', total=self.source.nRows): discreteKeys = list(forward(origcol.getTypedValue(sourcerow)) for origcol in discreteCols) # wrapply will pass-through a key-able TypedWrapper formattedDiscreteKeys = tuple(wrapply(c.format, v) for v, c in zip(discreteKeys, discreteCols)) numericGroupRows, groupRow = groups.get(formattedDiscreteKeys, (None, None)) if numericGroupRows is None: # add new group rows numericGroupRows = {formatRange(numericCols[0], numRange): PivotGroupRow(discreteKeys, numRange, [], {}) for numRange in numericBins} groups[formattedDiscreteKeys] = (numericGroupRows, None) for r in numericGroupRows.values(): self.addRow(r) # find the grouprow this sourcerow belongs in, by numericbin if numericCols: try: val = numericCols[0].getValue(sourcerow) if val is not None: val = numericCols[0].type(val) if not width: binidx = 0 elif degenerateBinning: # in degenerate binning, each val has its own bin binidx = numericBins.index((val, val)) else: binidx = int((val-minval)//width) groupRow = numericGroupRows[formatRange(numericCols[0], numericBins[min(binidx, nbins-1)])] except Exception as e: # leave in main/error bin pass # add the main bin if no numeric bin (error, or no numeric cols) if groupRow is None: nankey = makeErrorKey(numericCols[0]) if numericCols else 0 groupRow = PivotGroupRow(discreteKeys, (nankey, nankey), [], {}) groups[formattedDiscreteKeys] = (numericGroupRows, groupRow) self.addRow(groupRow) # add the sourcerow to its all bin groupRow.sourcerows.append(sourcerow) # separate by pivot value for col in self.pivotCols: varval = col.getTypedValue(sourcerow) matchingRows = groupRow.pivotrows.get(varval) if matchingRows is None: matchingRows = groupRow.pivotrows[varval] = [] matchingRows.append(sourcerow) if rowfunc: rowfunc(groupRow) # automatically add cache to all columns now that everything is binned for c in self.nonKeyVisibleCols: c.setCache(True) Sheet.addCommand('W', 'pivot', 'vd.push(Pivot(sheet, keyCols, [cursorCol]))', 'open Pivot Table: group rows by key column and summarize current column') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608541899.0 visidata-2.2.1/visidata/plugins.py0000660000175000017500000001406300000000000017652 0ustar00kefalakefala00000000000000import os import re import zipfile import importlib from visidata import * option('plugins_url', 'https://visidata.org/plugins/plugins.jsonl', 'source of plugins sheet') @VisiData.lazy_property def pluginsSheet(p): return PluginsSheet('plugins_global') def _plugin_path(plugin): return Path(os.path.join(options.visidata_dir, "plugins", plugin.name+".py")) def _plugin_init(): return Path(os.path.join(options.visidata_dir, "plugins", "__init__.py")) def _plugin_import(plugin): return "import " + _plugin_import_name(plugin) def _plugin_import_name(plugin): return "plugins."+plugin.name def _plugin_in_import_list(plugin): with Path(_plugin_init()).open_text(mode='r') as fprc: r = re.compile(r'^{}\W'.format(_plugin_import(plugin))) for line in fprc.readlines(): if r.match(line): return True def _installedStatus(col, plugin): return '*' if importlib.util.find_spec(_plugin_import_name(plugin)) else '' def _loadedVersion(plugin): name = _plugin_import_name(plugin) if name not in sys.modules: return '' mod = sys.modules[name] return getattr(mod, '__version__', 'unknown version installed') def _checkHash(data, sha): import hashlib return hashlib.sha256(data.strip().encode('utf-8')).hexdigest() == sha def _pluginColorizer(s,c,r,v): if not r: return None ver = _loadedVersion(r) if not ver: return None if ver != r.latest_ver: return 'color_warning' return 'color_working' class PluginsSheet(JsonLinesSheet): rowtype = "plugins" # rowdef: AttrDict of json dict colorizers = [ CellColorizer(3, None, _pluginColorizer) ] def iterload(self): for r in JsonLinesSheet.iterload(self): yield AttrDict(r) @asyncsingle def reload(self): self.source = urlcache(options.plugins_url or vd.fail(), days=0) # for VisiDataMetaSheet.reload() super().reload.__wrapped__(self) self.addColumn(Column('available', width=0, getter=_installedStatus), index=1) self.addColumn(Column('installed', width=8, getter=lambda c,r: _loadedVersion(r)), index=2) self.column('description').width = 40 self.setKeys([self.column("name")]) for r in Progress(self.rows): for funcname in (r.provides or '').split(): func = lambda *args, **kwargs: vd.fail('this requires the %s plugin' % r.name) vd.addGlobals({funcname: func}) setattr(vd, funcname, func) def installPlugin(self, plugin): # pip3 install requirements initpath = _plugin_init() os.makedirs(initpath.parent, exist_ok=True) if not initpath.exists(): initpath.touch() outpath = _plugin_path(plugin) overwrite = True if outpath.exists(): try: confirm("plugin path already exists, overwrite? ") except ExpectedException: overwrite = False if _plugin_in_import_list(plugin): vd.fail("plugin already loaded") else: self._loadPlugin(plugin) if overwrite: self._install(plugin) @asyncthread def _install(self, plugin): outpath = _plugin_path(plugin) with urlcache(plugin.url, 0).open_text() as pyfp: contents = pyfp.read() if not _checkHash(contents, plugin.sha256): vd.error('%s plugin SHA256 does not match!' % plugin.name) with outpath.open_text(mode='w') as outfp: outfp.write(contents) if plugin.pydeps: p = subprocess.Popen([sys.executable, '-m', 'pip', 'install', '--target', str(Path(options.visidata_dir)/"plugins-deps"), ]+plugin.pydeps.split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, err = p.communicate() vd.status(out) if err: vd.warning(err) vd.status('%s plugin installed' % plugin.name) if _plugin_in_import_list(plugin): vd.warning("plugin already loaded") else: self._loadPlugin(plugin) def _loadPlugin(self, plugin): with Path(_plugin_init()).open_text(mode='a') as fprc: print(_plugin_import(plugin), file=fprc) importlib.import_module(_plugin_import_name(plugin)) vd.status('%s plugin loaded' % plugin.name) def removePluginIfExists(self, plugin): self.removePlugin(plugin) def removePlugin(self, plugin): if not _plugin_in_import_list(plugin): vd.fail("plugin not in import list") initpath = Path(_plugin_init()) oldinitpath = Path(initpath.with_suffix(initpath.suffix + '.bak')) try: shutil.copyfile(initpath, oldinitpath) # Copy lines from the backup init file into its replacement, skipping lines that import the removed plugin. # # By matching from the start of a line through a word boundary, we avoid removing commented lines or inadvertently removing # plugins with similar names. with oldinitpath.open_text() as old, initpath.open_text(mode='w') as new: r = re.compile(r'^{}\W'.format(_plugin_import(plugin))) new.writelines(line for line in old.readlines() if not r.match(line)) os.unlink(_plugin_path(plugin)) sys.modules.pop(_plugin_import_name(plugin)) importlib.invalidate_caches() vd.warning('{0} plugin uninstalled'.format(plugin['name'])) except FileNotFoundError: vd.warning("no plugins/__init__.py found") globalCommand(None, 'open-plugins', 'vd.push(vd.pluginsSheet)', 'open Plugins Sheet: manage your session environment for a curated set of plugins') PluginsSheet.addCommand('a', 'add-plugin', 'installPlugin(cursorRow)', 'install and activate current plugin') PluginsSheet.addCommand('d', 'delete-plugin', 'removePluginIfExists(cursorRow)', 'deactivate current plugin') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1609740788.0 visidata-2.2.1/visidata/pyobj.py0000660000175000017500000003207600000000000017320 0ustar00kefalakefala00000000000000from functools import singledispatch from visidata import * __all__ = ['PythonSheet', 'expand_cols_deep', 'deduceType', 'closeColumn', 'ListOfDictSheet', 'SheetDict', 'PyobjSheet', 'view'] option('visibility', 0, 'visibility level') option('expand_col_scanrows', 1000, 'number of rows to check when expanding columns (0 = all)') class PythonSheet(Sheet): def openRow(self, row): return PyobjSheet("%s[%s]" % (self.name, self.keystr(row)), source=row) def _getWraparoundSlice(seq, n, center): '''Return a slice of length n from a sequence, centered around a given index''' start = int(center - n / 2) % len(seq) end = (start + n) % len(seq) if start < end: return seq[start:end] return seq[start:] + seq[:end] @asyncthread def expand_cols_deep(sheet, cols, rows=None, depth=0): # depth == 0 means drill all the way 'expand all visible columns of containers to the given depth (0=fully)' ret = [] if not rows: scanrows = options.expand_col_scanrows if scanrows == 0 or scanrows >= sheet.nRows: rows = sheet.rows else: rows = _getWraparoundSlice(sheet.rows, scanrows, sheet.cursorRowIndex) for col in cols: newcols = _addExpandedColumns(col, rows, sheet.columns.index(col)) if depth != 1: # countdown not yet complete, or negative (indefinite) ret.extend(expand_cols_deep.__wrapped__(sheet, newcols, rows, depth-1)) return ret @singledispatch def _createExpandedColumns(sampleValue, col, rows): '''By default, a column is not expandable. Supported container types for sampleValue trigger alternate, type-specific expansions.''' return [] @_createExpandedColumns.register(dict) def _(sampleValue, col, vals): '''Build a set of columns to add, using the first occurrence of each key to determine column type''' newcols = {} for val in Progress(vals, 'expanding'): colsToAdd = set(val).difference(newcols) colsToAdd and newcols.update({ k: deduceType(v) for k, v in val.items() if k in colsToAdd }) return [ ExpandedColumn('%s.%s' % (col.name, k), type=v, origCol=col, key=k) for k, v in newcols.items() ] @_createExpandedColumns.register(list) @_createExpandedColumns.register(tuple) def _(sampleValue, col, vals): '''Use the longest sequence to determine the number of columns we need to create, and their presumed types''' longestSeq = max(vals, key=len) colTypes = [deduceType(v) for v in longestSeq] return [ ExpandedColumn('%s[%s]' % (col.name, k), type=colType, origCol=col, key=k) for k, colType in enumerate(colTypes) ] def _addExpandedColumns(col, rows, idx): isNull = col.sheet.isNullFunc() nonNulls = [ col.getTypedValue(row) for row in rows if not isNull(col.getValue(row)) ] if not nonNulls: return [] # The type of the first non-null value for col determines if and how the # column can be expanded. expandedCols = _createExpandedColumns(nonNulls[0], col, nonNulls) for i, c in enumerate(expandedCols): col.sheet.addColumn(c, index=idx+i+1) if expandedCols: col.hide() return expandedCols def deduceType(v): if isinstance(v, (float, int)): return type(v) else: return anytype class ExpandedColumn(Column): def calcValue(self, row): return getitemdef(self.origCol.getValue(row), self.key) def setValue(self, row, value): self.origCol.getValue(row)[self.key] = value def closeColumn(sheet, col): if hasattr(col, 'origCol'): origCol = col.origCol else: vd.fail('column has not been expanded') vd.addUndo(setattr, sheet, 'columns', sheet.columns) origCol.width = options.default_width cols = [c for c in sheet.columns if getattr(c, "origCol", None) is not origCol] sheet.columns = cols #### generic list/dict/object browsing def view(obj): run(PyobjSheet(getattr(obj, '__name__', ''), source=obj)) def getPublicAttrs(obj): 'Return all public attributes (not methods or `_`-prefixed) on object.' return [k for k in dir(obj) if not k.startswith('_') and not callable(getattr(obj, k))] def PyobjColumns(obj): 'Return columns for each public attribute on an object.' return [ColumnAttr(k, type=deduceType(getattr(obj, k))) for k in getPublicAttrs(obj)] def AttrColumns(attrnames): 'Return column names for all elements of list `attrnames`.' return [ColumnAttr(name) for name in attrnames] def DictKeyColumns(d): 'Return a list of Column objects from dictionary keys.' return [ColumnItem(k, k, type=deduceType(d[k])) for k in d.keys()] def SheetList(*names, **kwargs): 'Creates a Sheet from a list of homogenous dicts or namedtuples.' src = kwargs.get('source', None) if not src: vd.status('no content in %s' % names) return if isinstance(src[0], dict): return ListOfDictSheet(*names, **kwargs) elif isinstance(src[0], tuple): if getattr(src[0], '_fields', None): # looks like a namedtuple return ListOfNamedTupleSheet(*names, **kwargs) # simple list return ListOfPyobjSheet(*names, **kwargs) class ListOfPyobjSheet(PythonSheet): rowtype = 'python objects' def reload(self): self.rows = self.source self.columns = [] self.addColumn(Column(self.name, getter=lambda col,row: row, setter=lambda col,row,val: setitem(col.sheet.source, col.sheet.source.index(row), val))) for c in PyobjColumns(self.rows[0]): self.addColumn(c) if len(self.columns) > 1: self.columns[0].width = 0 # rowdef: dict class ListOfDictSheet(PythonSheet): rowtype = 'dicts' def reload(self): self.columns = [] addedCols = set() for row in self.source: newCols = {k: v for k, v in row.items() if k not in addedCols} if not newCols: continue for c in DictKeyColumns(newCols): self.addColumn(c) addedCols.add(c.name) self.rows = self.source # rowdef: namedtuple class ListOfNamedTupleSheet(PythonSheet): rowtype = 'namedtuples' def reload(self): self.columns = [] for i, k in enumerate(self.source[0]._fields): self.addColumn(ColumnItem(k, i)) self.rows = self.source # rowdef: PyObj class SheetNamedTuple(PythonSheet): 'a single namedtuple, with key and value columns' rowtype = 'values' columns = [ColumnItem('name', 0), ColumnItem('value', 1)] def __init__(self, *names, **kwargs): super().__init__(*names, **kwargs) def reload(self): self.rows = list(zip(self.source._fields, self.source)) def openRow(self, row): return PyobjSheet(self.name, row[0], source=row[1]) # source is dict class SheetDict(PythonSheet): rowtype = 'items' # rowdef: keys columns = [ Column('key'), Column('value', getter=lambda c,r: c.sheet.source[r], setter=lambda c,r,v: setitem(c.sheet.source, r, v)), ] nKeys = 1 def reload(self): self.rows = list(self.source.keys()) def openRow(self, row): return PyobjSheet(self.name, row, source=self.source[row]) class ColumnSourceAttr(Column): 'Use row as attribute name on sheet source' def calcValue(self, attrname): return getattr(self.sheet.source, attrname) def setValue(self, attrname, value): return setattr(self.sheet.source, attrname, value) def docstring(obj, attr): v = getattr(obj, attr) if callable(v): return v.__doc__ return '' % type(v).__name__ # rowdef: attrname class PyobjSheet(PythonSheet): 'Generic Sheet for any Python object. Return specialized subclasses for lists of objects, namedtuples, and dicts.' rowtype = 'attributes' columns = [ Column('attribute'), ColumnSourceAttr('value'), Column('docstring', getter=lambda c,r: docstring(c.sheet.source, r)) ] nKeys = 1 def __new__(cls, *names, **kwargs): 'Return Sheet object of appropriate type for given sources in `args`.' pyobj=kwargs.get('source', object()) if isinstance(pyobj, list) or isinstance(pyobj, tuple): if getattr(pyobj, '_fields', None): # list of namedtuple return SheetNamedTuple(*names, **kwargs) else: return SheetList(*names, **kwargs) elif isinstance(pyobj, dict): return SheetDict(*names, **kwargs) elif isinstance(pyobj, str): return TextSheet(*names, source=pyobj.splitlines()) elif isinstance(pyobj, bytes): return TextSheet(*names, source=pyobj.decode(options.encoding).splitlines()) elif isinstance(pyobj, object): obj = super().__new__(cls) #, *names, **kwargs) return obj else: vd.error("cannot load '%s' as pyobj" % type(pyobj).__name__) def reload(self): self.rows = [] vislevel = options.visibility for r in dir(self.source): try: if vislevel <= 2 and r.startswith('__'): continue if vislevel <= 1 and r.startswith('_'): continue if vislevel <= 0 and callable(getattr(self.source, r)): continue self.addRow(r) except Exception: pass def openRow(self, row): 'dive further into Python object' v = getattr(self.source, row) return PyobjSheet(self.name + "." + str(row), source=v() if callable(v) else v) @TableSheet.api def openRow(sheet, row): 'Return Sheet diving into *row*.' k = sheet.keystr(row) or [sheet.cursorRowIndex] name = f'{sheet.name}[{k}]' return PyobjSheet(name, source=tuple(c.getTypedValue(row) for c in sheet.visibleCols)) @TableSheet.api def openCell(sheet, col, row): 'Return Sheet diving into cell at *row* in *col*.' k = sheet.keystr(row) or [str(sheet.cursorRowIndex)] name = f'{sheet.name}[{k}].{col.name}' return PyobjSheet(name, source=col.getTypedValue(row)) globalCommand('^X', 'pyobj-expr', 'expr = input("eval: ", "expr", completer=CompleteExpr()); vd.push(PyobjSheet(expr, source=evalExpr(expr, None)))', 'evaluate Python expression and open result as Python object') globalCommand('g^X', 'exec-python', 'expr = input("exec: ", "expr", completer=CompleteExpr()); exec(expr, getGlobals())', 'execute Python statement in the global scope') globalCommand('z^X', 'pyobj-expr-row', 'expr = input("eval over current row: ", "expr", completer=CompleteExpr()); vd.push(PyobjSheet(expr, source=evalExpr(expr, cursorRow)))', 'evaluate Python expression, in context of current row, and open result as Python object') Sheet.addCommand('^Y', 'pyobj-row', 'status(type(cursorRow)); vd.push(PyobjSheet("%s[%s]" % (sheet.name, cursorRowIndex), source=cursorRow))', 'open current row as Python object') Sheet.addCommand('z^Y', 'pyobj-cell', 'status(type(cursorValue)); vd.push(PyobjSheet("%s[%s].%s" % (sheet.name, cursorRowIndex, cursorCol.name), source=cursorValue))', 'open current cell as Python object') globalCommand('g^Y', 'pyobj-sheet', 'status(type(sheet)); vd.push(PyobjSheet(sheet.name+"_sheet", source=sheet))', 'open current sheet as Python object') Sheet.addCommand('(', 'expand-col', 'expand_cols_deep(sheet, [cursorCol], depth=0)', 'expand current column of containers fully') Sheet.addCommand('g(', 'expand-cols', 'expand_cols_deep(sheet, visibleCols, depth=0)', 'expand all visible columns of containers fully') Sheet.addCommand('z(', 'expand-col-depth', 'expand_cols_deep(sheet, [cursorCol], depth=int(input("expand depth=", value=1)))', 'expand current column of containers to given depth (0=fully)') Sheet.addCommand('gz(', 'expand-cols-depth', 'expand_cols_deep(sheet, visibleCols, depth=int(input("expand depth=", value=1)))', 'expand all visible columns of containers to given depth (0=fully)') Sheet.addCommand(')', 'contract-col', 'closeColumn(sheet, cursorCol)', 'unexpand current column; restore original column and remove other columns at this level') Sheet.addCommand(ENTER, 'open-row', 'vd.push(openRow(cursorRow))', 'open sheet with copies of rows referenced in current row') Sheet.addCommand('z'+ENTER, 'open-cell', 'vd.push(openCell(cursorCol, cursorRow))', 'open sheet with copies of rows referenced in current cell') Sheet.addCommand('g'+ENTER, 'dive-selected', 'for r in selectedRows: vd.push(openRow(r))', 'open sheet with copies of rows referenced in selected rows') Sheet.addCommand('gz'+ENTER, 'dive-selected-cells', 'for r in selectedRows: vd.push(openCell(cursorCol, r))', 'open sheet with copies of rows referenced in selected rows') PyobjSheet.addCommand('v', 'visibility', 'sheet.options.visibility = 0 if sheet.options.visibility else 2; reload()', 'toggle show/hide for methods and hidden properties') PyobjSheet.addCommand('gv', 'show-hidden', 'sheet.options.visibility = 2; reload()', 'show methods and hidden properties') PyobjSheet.addCommand('zv', 'hide-hidden', 'sheet.options.visibility -= 1; reload()', 'hide methods and hidden properties') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608541899.0 visidata-2.2.1/visidata/regex.py0000660000175000017500000001132100000000000017275 0ustar00kefalakefala00000000000000import re import random from visidata import asyncthread, option, options, vd from visidata import BaseSheet, Sheet, Column, Progress @Sheet.api def setSubst(sheet, cols, rows): if not rows: vd.warning('no %s selected' % sheet.rowtype) return modified = 'column' if len(cols) == 1 else 'columns' rex = vd.input("transform %s by regex: " % modified, type="regex-subst") setValuesFromRegex(cols, rows, rex) option('regex_flags', 'I', 'flags to pass to re.compile() [AILMSUX]', replay=True) option('regex_maxsplit', 0, 'maxsplit to pass to regex.split', replay=True) option('default_sample_size', 100, 'number of rows to sample for regex.split', replay=True) def makeRegexSplitter(regex, origcol): return lambda row, regex=regex, origcol=origcol, maxsplit=options.regex_maxsplit: regex.split(origcol.getDisplayValue(row), maxsplit=maxsplit) def makeRegexMatcher(regex, origcol): def _regexMatcher(row): m = regex.search(origcol.getDisplayValue(row)) if m: return m.groupdict() if m.groupdict() else m.groups() return _regexMatcher @asyncthread def addRegexColumns(regexMaker, vs, origcol, regexstr): regexstr or vd.fail('regex required') regex = re.compile(regexstr, vs.regex_flags()) func = regexMaker(regex, origcol) n = options.default_sample_size if n and n < len(vs.rows): exampleRows = random.sample(vs.rows, max(0, n-1)) # -1 to account for included cursorRow else: exampleRows = vs.rows cols = {} ncols = 0 # number of new columns added already for r in Progress(exampleRows + [vs.cursorRow]): try: m = func(r) if not m: continue except Exception as e: vd.exceptionCaught(e) if isinstance(m, dict): for name in m: if name in cols: continue cols[name] = Column(origcol.name+'_'+str(name), getter=lambda col,row,name=name,func=func: func(row)[name], origCol=origcol) elif isinstance(m, (tuple, list)): for _ in range(len(m)-len(cols)): cols[len(cols)] = Column(origcol.name+'_re'+str(len(cols)), getter=lambda col,row,i=len(cols),func=func: func(row)[i], origCol=origcol) else: raise TypeError("addRegexColumns() expects a dict, list, or tuple from regexMaker, but got a "+type(m).__name__) vs.addColumnAtCursor(*cols.values()) def regexTransform(origcol, instr): i = indexWithEscape(instr, '/') if i is None: before = instr after = '' else: before = instr[:i] after = instr[i+1:] return lambda col,row,origcol=origcol,before=before,after=after,flags=origcol.sheet.regex_flags(): re.sub(before, after, origcol.getDisplayValue(row), flags=flags) def indexWithEscape(s, char, escape_char='\\'): i=0 while i < len(s): if s[i] == escape_char: i += 1 elif s[i] == char: return i i += 1 return None @asyncthread def setValuesFromRegex(cols, rows, rex): transforms = [regexTransform(col, rex) for col in cols] vd.addUndoSetValues(cols, rows) for r in Progress(rows, 'replacing'): for col, transform in zip(cols, transforms): col.setValueSafe(r, transform(col, r)) for col in cols: col.recalc() @BaseSheet.api def regex_flags(sheet): 'Return flags to pass to regex functions from options' return sum(getattr(re, f.upper()) for f in options.regex_flags) Sheet.addCommand(':', 'split-col', 'addRegexColumns(makeRegexSplitter, sheet, cursorCol, input("split regex: ", type="regex-split"))', 'add new columns from regex split; number of columns determined by example row at cursor') Sheet.addCommand(';', 'capture-col', 'addRegexColumns(makeRegexMatcher, sheet, cursorCol, input("match regex: ", type="regex-capture"))', 'add new column from capture groups of regex; requires example row') Sheet.addCommand('*', 'addcol-subst', 'addColumnAtCursor(Column(cursorCol.name + "_re", getter=regexTransform(cursorCol, input("transform column by regex: ", type="regex-subst"))))', 'add column derived from current column, replacing regex with subst (may include \1 backrefs)') Sheet.addCommand('g*', 'setcol-subst', 'setSubst([cursorCol], someSelectedRows)', 'regex/subst - modify selected rows in current column, replacing regex with subst, (may include backreferences \\1 etc)') Sheet.addCommand('gz*', 'setcol-subst-all', 'setSubst(visibleCols, someSelectedRows)', 'modify selected rows in all visible columns, replacing regex with subst (may include \\1 backrefs)') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612075960.0 visidata-2.2.1/visidata/save.py0000660000175000017500000001304400000000000017125 0ustar00kefalakefala00000000000000from visidata import * option('confirm_overwrite', True, 'whether to prompt for overwrite confirmation on save') option('safe_error', '#ERR', 'error string to use while saving', replay=True) @Sheet.api def safe_trdict(vs): 'returns string.translate dictionary for replacing tabs and newlines' if options.safety_first: delim = vs.options.delimiter return { 0: '', # strip NUL completely ord(delim): vs.options.tsv_safe_tab, # \t 10: vs.options.tsv_safe_newline, # \n 13: vs.options.tsv_safe_newline, # \r } return {} @Sheet.api def iterdispvals(sheet, *cols, format=False): 'For each row in sheet, yield OrderedDict of values for given cols. Values are typed if format=False, or a formatted display string if format=True.' if not cols: cols = sheet.visibleCols transformers = collections.OrderedDict() # list of transformers for each column in order for col in cols: transformers[col] = [ col.type ] if format: transformers[col].append(col.format) trdict = sheet.safe_trdict() if trdict: transformers[col].append(lambda v,trdict=trdict: v.translate(trdict)) options_safe_error = options.safe_error for r in Progress(sheet.rows): dispvals = collections.OrderedDict() # [col] -> value for col, transforms in transformers.items(): try: dispval = col.getValue(r) except Exception as e: vd.exceptionCaught(e) dispval = options_safe_error or str(e) try: for t in transforms: if dispval is None: dispval = '' break elif isinstance(dispval, TypedExceptionWrapper): dispval = options_safe_error or str(dispval) break dispval = t(dispval) except Exception as e: dispval = str(dispval) dispvals[col] = dispval yield dispvals @Sheet.api def itervals(sheet, *cols, format=False): for row in sheet.iterdispvals(*cols, format=format): yield [row[c] for c in cols] @Sheet.api def getDefaultSaveName(sheet): src = getattr(sheet, 'source', None) if hasattr(src, 'scheme') and src.scheme: return src.name + src.suffix if isinstance(src, Path): return str(src) else: return sheet.name+'.'+getattr(sheet, 'filetype', options.save_filetype) @VisiData.api def save_cols(vd, cols): sheet = cols[0].sheet vs = copy(sheet) vs.columns = list(cols) vs.rows = sheet.rows if len(cols) == 1: savedcoltxt = cols[0].name + ' column' else: savedcoltxt = '%s columns' % len(cols) path = inputPath('save %s to: ' % savedcoltxt, value=vs.getDefaultSaveName()) vd.saveSheets(path, vs, confirm_overwrite=options.confirm_overwrite) @VisiData.global_api def saveSheets(vd, givenpath, *vsheets, confirm_overwrite=False): 'Save all *vsheets* to *givenpath*.' filetype = givenpath.ext or options.save_filetype savefunc = getattr(vsheets[0], 'save_' + filetype, None) or getattr(vd, 'save_' + filetype, None) or vd.fail('no function to save as type %s' % filetype) if givenpath.exists() and confirm_overwrite: confirm("%s already exists. overwrite? " % givenpath.given) vd.status('saving %s sheets to %s as %s' % (len(vsheets), givenpath.given, filetype)) if not givenpath.given.endswith('/'): # forcibly specify save individual files into directory by ending path with / return vd.execAsync(savefunc, givenpath, *vsheets) # more than one sheet; either no specific multisave for save filetype, or path ends with / # save as individual files in the givenpath directory try: os.makedirs(givenpath, exist_ok=True) except FileExistsError: pass if not givenpath.is_dir(): vd.fail(f'cannot save multiple {filetype} sheets to non-dir') # get save function to call for vs in vsheets: p = Path((givenpath / vs.name).with_suffix('.'+filetype)) vd.execAsync(savefunc, p, vs) return @VisiData.api def save_txt(vd, p, *vsheets): with p.open_text(mode='w') as fp: for vs in vsheets: unitsep = vs.options.delimiter rowsep = vs.options.row_delimiter for dispvals in vs.iterdispvals(*vs.visibleCols, format=True): fp.write(unitsep.join(dispvals.values())) fp.write(rowsep) vd.status('%s save finished' % p) Sheet.addCommand('^S', 'save-sheet', 'vd.saveSheets(inputPath("save to: ", value=getDefaultSaveName()), sheet, confirm_overwrite=options.confirm_overwrite)', 'save current sheet to filename in format determined by extension (default .tsv)') BaseSheet.addCommand('g^S', 'save-all', 'vd.saveSheets(inputPath("save all sheets to: "), *vd.sheets, confirm_overwrite=options.confirm_overwrite)', 'save all sheets to given file or directory)') IndexSheet.addCommand('g^S', 'save-selected', 'vd.saveSheets(inputPath("save %d sheets to: " % nSelectedRows, value=str(source)), *selectedRows, confirm_overwrite=options.confirm_overwrite)', 'save all selected sheets to given file or directory') Sheet.addCommand('', 'save-col', 'save_cols([cursorCol])', 'save current column only to filename in format determined by extension (default .tsv)') Sheet.addCommand('', 'save-col-keys', 'save_cols(keyCols + [cursorCol])', 'save key columns and current column to filename in format determined by extension (default .tsv)') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608541899.0 visidata-2.2.1/visidata/search.py0000660000175000017500000001026400000000000017435 0ustar00kefalakefala00000000000000import re from visidata import vd, VisiData, BaseSheet, Sheet, Column, Progress, asyncthread, rotateRange VisiData.init('searchContext', dict) # [(regex, columns, backward)] -> kwargs from previous search @VisiData.api @asyncthread def moveRegex(vd, sheet, *args, **kwargs): list(vd.searchRegex(sheet, *args, moveCursor=True, **kwargs)) # kwargs: regex=None, columns=None, backward=False @VisiData.api def searchRegex(vd, sheet, moveCursor=False, reverse=False, **kwargs): 'Set row index if moveCursor, otherwise return list of row indexes.' def findMatchingColumn(sheet, row, columns, func): 'Find column for which func matches the displayed value in this row' for c in columns: if func(c.getDisplayValue(row)): return c vd.searchContext.update(kwargs) regex = kwargs.get("regex") if regex: vd.searchContext["regex"] = re.compile(regex, sheet.regex_flags()) or vd.error('invalid regex: %s' % regex) regex = vd.searchContext.get("regex") or vd.fail("no regex") columns = vd.searchContext.get("columns") if columns == "cursorCol": columns = [sheet.cursorCol] elif columns == "visibleCols": columns = tuple(sheet.visibleCols) elif isinstance(columns, Column): columns = [columns] if not columns: vd.error('bad columns') searchBackward = vd.searchContext.get("backward") if reverse: searchBackward = not searchBackward matchingRowIndexes = 0 for rowidx in rotateRange(len(sheet.rows), sheet.cursorRowIndex, reverse=searchBackward): c = findMatchingColumn(sheet, sheet.rows[rowidx], columns, regex.search) if c: if moveCursor: sheet.cursorRowIndex = rowidx sheet.cursorVisibleColIndex = sheet.visibleCols.index(c) return else: matchingRowIndexes += 1 yield rowidx vd.status('%s matches for /%s/' % (matchingRowIndexes, regex.pattern)) @Sheet.api @asyncthread def search_expr(sheet, expr, reverse=False): for i in rotateRange(len(sheet.rows), sheet.cursorRowIndex, reverse=reverse): try: if sheet.evalExpr(expr, sheet.rows[i]): sheet.cursorRowIndex=i return except Exception as e: vd.exceptionCaught(e) vd.fail(f'no {sheet.rowtype} where {expr}') Sheet.addCommand('r', 'search-keys', 'tmp=cursorVisibleColIndex; vd.moveRegex(sheet, regex=input("row key regex: ", type="regex-row", defaultLast=True), columns=keyCols or [visibleCols[0]]); sheet.cursorVisibleColIndex=tmp', 'go to next row with key matching regex') Sheet.addCommand('/', 'search-col', 'vd.moveRegex(sheet, regex=input("/", type="regex", defaultLast=True), columns="cursorCol", backward=False)', 'search for regex forwards in current column'), Sheet.addCommand('?', 'searchr-col', 'vd.moveRegex(sheet, regex=input("?", type="regex", defaultLast=True), columns="cursorCol", backward=True)', 'search for regex backwards in current column'), Sheet.addCommand('n', 'search-next', 'vd.moveRegex(sheet, reverse=False)', 'go to next match from last regex search'), Sheet.addCommand('N', 'searchr-next', 'vd.moveRegex(sheet, reverse=True)', 'go to previous match from last regex search'), Sheet.addCommand('g/', 'search-cols', 'vd.moveRegex(sheet, regex=input("g/", type="regex", defaultLast=True), backward=False, columns="visibleCols")', 'search for regex forwards over all visible columns'), Sheet.addCommand('g?', 'searchr-cols', 'vd.moveRegex(sheet, regex=input("g?", type="regex", defaultLast=True), backward=True, columns="visibleCols")', 'search for regex backwards over all visible columns'), Sheet.addCommand('z/', 'search-expr', 'search_expr(inputExpr("search by expr: ") or fail("no expr"))', 'search by Python expression forwards in current column (with column names as variables)') Sheet.addCommand('z?', 'searchr-expr', 'search_expr(inputExpr("searchr by expr: ") or fail("no expr"), reverse=True)', 'search by Python expression backwards in current column (with column names as variables)') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608541899.0 visidata-2.2.1/visidata/selection.py0000660000175000017500000002113000000000000020147 0ustar00kefalakefala00000000000000from visidata import vd, Sheet, Progress, option, asyncthread, options, rotateRange, Fanout, undoAttrCopyFunc, copy option('bulk_select_clear', False, 'clear selected rows before new bulk selections', replay=True) option('some_selected_rows', False, 'if no rows selected, if True, someSelectedRows returns all rows; if False, fails') Sheet.init('_selectedRows', dict) # rowid(row) -> row @Sheet.api def isSelected(self, row): 'Return True if *row* is selected.' return self.rowid(row) in self._selectedRows @Sheet.api @asyncthread def toggle(self, rows): 'Toggle selection of given *rows*. Async.' self.addUndoSelection() for r in Progress(rows, 'toggling', total=len(self.rows)): if not self.unselectRow(r): self.selectRow(r) @Sheet.api def selectRow(self, row): 'Add *row* to set of selected rows. Overrideable.' self._selectedRows[self.rowid(row)] = row @Sheet.api def unselectRow(self, row): 'Remove *row* from set of selected rows. Return True if row was previously selected. Overrideable.' if self.rowid(row) in self._selectedRows: del self._selectedRows[self.rowid(row)] return True else: return False @Sheet.api def clearSelected(self): 'Clear set of selected rows, without calling ``unselectRow`` for each one.' self.addUndoSelection() self._selectedRows.clear() @Sheet.api @asyncthread def select(self, rows, status=True, progress=True): "Add *rows* to set of selected rows. Async. Don't show progress if *progress* is False; don't show status if *status* is False." self.addUndoSelection() before = self.nSelectedRows if options.bulk_select_clear: self.clearSelected() for r in (Progress(rows, 'selecting') if progress else rows): self.selectRow(r) if status: if options.bulk_select_clear: msg = 'selected %s %s%s' % (self.nSelectedRows, self.rowtype, ' instead' if before > 0 else '') else: msg = 'selected %s%s %s' % (self.nSelectedRows-before, ' more' if before > 0 else '', self.rowtype) vd.status(msg) @Sheet.api @asyncthread def unselect(self, rows, status=True, progress=True): "Remove *rows* from set of selected rows. Async. Don't show progress if *progress* is False; don't show status if *status* is False." self.addUndoSelection() before = self.nSelectedRows for r in (Progress(rows, 'unselecting') if progress else rows): self.unselectRow(r) if status: vd.status('unselected %s/%s %s' % (before-self.nSelectedRows, before, self.rowtype)) @Sheet.api def selectByIdx(self, rowIdxs): 'Add rows indicated by row indexes in *rowIdxs* to set of selected rows. Async.' self.select((self.rows[i] for i in rowIdxs), progress=False) @Sheet.api def unselectByIdx(self, rowIdxs): 'Remove rows indicated by row indexes in *rowIdxs* from set of selected rows. Async.' self.unselect((self.rows[i] for i in rowIdxs), progress=False) @Sheet.api def gatherBy(self, func, gerund='gathering'): 'Generate rows for which ``func(row)`` returns True, starting from the cursor.' for i in Progress(rotateRange(self.nRows, self.cursorRowIndex-1), total=self.nRows, gerund=gerund): try: r = self.rows[i] if func(r): yield r except Exception: pass @Sheet.property def selectedRows(self): 'List of selected rows in sheet order.' if self.nSelectedRows <= 1: return Fanout(self._selectedRows.values()) return Fanout((r for r in self.rows if self.rowid(r) in self._selectedRows)) @Sheet.property def onlySelectedRows(self): 'List of selected rows in sheet order. Fail if no rows are selected.' if self.nSelectedRows == 0: vd.fail('no rows selected') return self.selectedRows @Sheet.property def someSelectedRows(self): '''Return a list of rows: (a) in batch mode, always return selectedRows (b) in interactive mode, if options.some_selected_rows is True, return selectedRows or all rows if none selected (c) in interactive mode, if options.some_selected_rows is False, return selectedRows or fail if none selected''' if options.batch: return self.selectedRows if options.some_selected_rows: return self.selectedRows or self.rows return self.onlySelectedRows @Sheet.property def nSelectedRows(self): 'Number of selected rows.' return len(self._selectedRows) @Sheet.api @asyncthread def deleteSelected(self): 'Delete all selected rows. Async.' ndeleted = self.deleteBy(self.isSelected) nselected = self.nSelectedRows self.clearSelected() if ndeleted != nselected: vd.warning(f'deleted {ndeleted}, expected {nselected}') @Sheet.api def addUndoSelection(sheet): vd.addUndo(undoAttrCopyFunc([sheet], '_selectedRows')) Sheet.addCommand('t', 'stoggle-row', 'toggle([cursorRow]); cursorDown(1)', 'toggle selection of current row') Sheet.addCommand('s', 'select-row', 'select([cursorRow]); cursorDown(1)', 'select current row') Sheet.addCommand('u', 'unselect-row', 'unselect([cursorRow]); cursorDown(1)', 'unselect current row') Sheet.addCommand('gt', 'stoggle-rows', 'toggle(rows)', 'toggle selection of all rows') Sheet.addCommand('gs', 'select-rows', 'select(rows)', 'select all rows') Sheet.addCommand('gu', 'unselect-rows', 'clearSelected()', 'unselect all rows') Sheet.addCommand('zt', 'stoggle-before', 'toggle(rows[:cursorRowIndex])', 'toggle selection of rows from top to cursor') Sheet.addCommand('zs', 'select-before', 'select(rows[:cursorRowIndex])', 'select all rows from top to cursor') Sheet.addCommand('zu', 'unselect-before', 'unselect(rows[:cursorRowIndex])', 'unselect all rows from top to cursor') Sheet.addCommand('gzt', 'stoggle-after', 'toggle(rows[cursorRowIndex:])', 'toggle selection of all rows from cursor to bottom') Sheet.addCommand('gzs', 'select-after', 'select(rows[cursorRowIndex:])', 'select all rows from cursor to bottom') Sheet.addCommand('gzu', 'unselect-after', 'unselect(rows[cursorRowIndex:])', 'unselect all rows from cursor to bottom') Sheet.addCommand('|', 'select-col-regex', 'selectByIdx(vd.searchRegex(sheet, regex=input("select regex: ", type="regex", defaultLast=True), columns="cursorCol"))', 'select rows matching regex in current column') Sheet.addCommand('\\', 'unselect-col-regex', 'unselectByIdx(vd.searchRegex(sheet, regex=input("unselect regex: ", type="regex", defaultLast=True), columns="cursorCol"))', 'unselect rows matching regex in current column') Sheet.addCommand('g|', 'select-cols-regex', 'selectByIdx(vd.searchRegex(sheet, regex=input("select regex: ", type="regex", defaultLast=True), columns="visibleCols"))', 'select rows matching regex in any visible column') Sheet.addCommand('g\\', 'unselect-cols-regex', 'unselectByIdx(vd.searchRegex(sheet, regex=input("unselect regex: ", type="regex", defaultLast=True), columns="visibleCols"))', 'unselect rows matching regex in any visible column') Sheet.addCommand(',', 'select-equal-cell', 'select(gatherBy(lambda r,c=cursorCol,v=cursorDisplay: c.getDisplayValue(r) == v), progress=False)', 'select rows matching current cell in current column') Sheet.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') Sheet.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') Sheet.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') Sheet.addCommand('z|', 'select-expr', 'expr=inputExpr("select by expr: "); select(gatherBy(lambda r, sheet=sheet, expr=expr: sheet.evalExpr(expr, r)), progress=False)', 'select rows matching Python expression in any visible column') Sheet.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') Sheet.addCommand(None, 'select-error-col', 'select(gatherBy(lambda r,c=cursorCol: c.isError(r)), progress=False)', 'select rows with errors in current column') Sheet.addCommand(None, 'select-error', 'select(gatherBy(lambda r,vcols=visibleCols: isinstance(r, TypedExceptionWrapper) or any([c.isError(r) for c in vcols])), progress=False)', 'select rows with errors in any column') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612075960.0 visidata-2.2.1/visidata/settings.py0000660000175000017500000003253000000000000020030 0ustar00kefalakefala00000000000000import collections import sys import inspect import argparse import importlib import os import visidata from visidata import VisiData, BaseSheet, vd # [settingname] -> { objname(Sheet-instance/Sheet-type/'global'/'default'): Option/Command/longname } class SettingsMgr(collections.OrderedDict): def __init__(self): super().__init__() self.allobjs = {} def objname(self, obj): if isinstance(obj, str): v = obj elif obj is None: v = 'global' elif isinstance(obj, BaseSheet): v = obj.name elif inspect.isclass(obj) and issubclass(obj, BaseSheet): v = obj.__name__ else: return None self.allobjs[v] = obj return v def getobj(self, objname): 'Inverse of objname(obj); returns obj if available' return self.allobjs.get(objname) def unset(self, k, obj='default'): 'Remove setting for given key in the given context.' objstr = self.objname(obj) if objstr in self[k]: del self[k][objstr] def set(self, k, v, obj): 'obj is a Sheet instance, or a Sheet [sub]class. obj="global" means override default unless there is a sheet-specific override; obj="default" means last resort.' if k not in self: self[k] = dict() self[k][self.objname(obj)] = v return v def setdefault(self, k, v): return self.set(k, v, 'default') def _mappings(self, obj): '''Return list of contexts in order to resolve settings. ordering is, from lowest to highest precedence: 1. "default": default specified in option() definition 2. "global": in order of program execution: a. .visidatarc b. command-line options, applied on top of the overrides in .visidatarc c. at runtime via 'O'ptions meta-sheet 3. objname(type(obj)): current sheet class and parents, recursively 4. objname(obj): the specific sheet instance a. can override at runtime, replace value for sheet instance ''' mappings = [] if obj: mappings += [self.objname(obj)] mappings += [self.objname(cls) for cls in inspect.getmro(type(obj))] mappings += ['global', 'default'] return mappings def _get(self, key, obj=None): d = self.get(key, None) if d: for m in self._mappings(obj or vd.sheet): v = d.get(m) if v: return v def iter(self, obj=None): 'Iterate through all keys considering context of obj. If obj is None, uses the context of the top sheet.' if obj is None and vd: obj = vd.sheet for o in self._mappings(obj): for k in self.keys(): for o2 in self[k]: if o == o2: yield (k, o), self[k][o2] def iterall(self): for k in self.keys(): for o in self[k]: yield (k, o), self[k][o] class Command: def __init__(self, longname, execstr, helpstr=''): self.longname = longname self.execstr = execstr self.helpstr = helpstr class Option: def __init__(self, name, value, helpstr=''): self.name = name self.value = value self.helpstr = helpstr self.replayable = False self.sheettype = BaseSheet def __str__(self): return str(self.value) def __eq__(self, other): return self.name == other.name @VisiData.api class OptionsObject: 'minimalist options framework' def __init__(self, mgr, obj=None): object.__setattr__(self, '_opts', mgr) object.__setattr__(self, '_cache', {}) object.__setattr__(self, '_obj', obj) def keys(self, obj=None): for k, d in self._opts.items(): if obj is None or self._opts.objname(obj) in d: yield k def _get(self, k, obj=None): 'Return Option object for k in context of obj. Cache result until any set().' opt = self._cache.get((k, obj or vd.sheet), None) if opt is None: opt = self._opts._get(k, obj) self._cache[(k, obj or vd.sheet)] = opt return opt def _set(self, k, v, obj=None, helpstr=''): self._cache.clear() # invalidate entire cache on any change return self._opts.set(k, Option(k, v, helpstr), obj) def is_set(self, k, obj=None): d = self._opts.get(k, None) if d: return d.get(self._opts.objname(obj), None) def get(self, optname, default=None): 'Return the value of the given *optname* option in the options context. *default* is only returned if the option is not defined. An Exception is never raised.' d = self._get(optname, None) if d: return d.value return default def getobj(self, optname, obj=None): 'Return value of option optname as set on obj, or on option context if obj is None.' return self._get(optname, obj).value def getdefault(self, optname): return self._get(optname, 'default').value def getonly(self, optname, obj, default): 'Return value of option optname as set on obj, or default if not set specifically on obj' d = self._opts.get(optname, None) if d: opt = d.get(self._opts.objname(obj), None) if opt: return opt.value return default def set(self, optname, value, obj='global'): "Override *value* for *optname* in the options context, or in the *obj* context if given." opt = self._get(optname) if opt: curval = opt.value t = type(curval) if value is None and curval is not None: return self.unset(optname, obj=obj) elif isinstance(value, str) and t is bool: # special case for bool options value = value and (value[0] not in "0fFnN") # ''/0/false/no are false, everything else is true elif type(value) is t: # if right type, no conversion pass elif curval is None: # if None, do not apply type conversion pass else: value = t(value) if curval != value and self._get(optname, 'default').replayable: if obj != 'default' and type(obj) is not type: # default and class options set on init aren't recorded if vd.cmdlog: objname = self._opts.objname(obj) vd.cmdlog.addRow(vd.cmdlog.newRow(sheet=objname, row=optname, keystrokes='', input=str(value), longname='set-option')) else: curval = None vd.warning('setting unknown option %s' % optname) return self._set(optname, value, obj) def unset(self, optname, obj=None): 'Remove setting value for given context.' v = self._opts.unset(optname, obj) opt = self._get(optname) if vd.cmdlog and opt and opt.replayable: objname = self._opts.objname(obj) vd.cmdlog.addRow(vd.cmdlog.newRow(sheet=objname, row=optname, keystrokes='', input='', longname='unset-option')) self._cache.clear() # invalidate entire cache on any change return v def setdefault(self, optname, value, helpstr): return self._set(optname, value, 'default', helpstr=helpstr) def getall(self, prefix=''): 'Return dictionary of all options beginning with `prefix` (with `prefix` removed from the name).' return { optname[len(prefix):] : options[optname] for optname in options.keys() if optname.startswith(prefix) } def __getattr__(self, optname): # options.foo 'Return value of option `optname` for stored options context.' return self.__getitem__(optname) def __setattr__(self, optname, value): # options.foo = value 'Set *value* of option *optname* for stored options context.' self.__setitem__(optname, value) def __getitem__(self, optname): # options[optname] opt = self._get(optname, obj=self._obj) if not opt: raise ValueError('no option "%s"' % optname) return opt.value def __setitem__(self, optname, value): # options[optname] = value self.set(optname, value, obj=self._obj) vd.commands = SettingsMgr() vd.bindkeys = SettingsMgr() vd._options = SettingsMgr() vd.options = vd.OptionsObject(vd._options) # global option settings options = vd.options # legacy @VisiData.global_api def option(vd, name, default, helpstr, replay=False, sheettype=BaseSheet): '''Declare a new option. - `name`: name of option - `default`: default value when no other override exists - `helpstr`: short description of option (as shown in the **Options Sheet**) - `replay`: ``True`` if changes to the option should be stored in the **Command Log** - `sheettype`: ``None`` if the option is not sheet-specific, to make it global on CLI ''' opt = options.setdefault(name, default, helpstr) opt.replayable = replay opt.sheettype=sheettype return opt @BaseSheet.class_api @classmethod def addCommand(cls, keystrokes, longname, execstr, helpstr='', **kwargs): '''Add a new command to *cls* sheet type. - *keystrokes*: default keybinding, including **prefixes**. - *longname*: name of the command. - *execstr*: Python statement to pass to `exec()`'ed when the command is executed. - *helpstr*: help string shown in the **Commands Sheet**. ''' vd.commands.set(longname, Command(longname, execstr, helpstr=helpstr, **kwargs), cls) if keystrokes: vd.bindkeys.set(keystrokes, longname, cls) def _command(cls, binding, longname, helpstr, **kwargs): def decorator(func): funcname = longname.replace('-', '_') setattr(vd, funcname, func) cls.addCommand(binding, longname, f'vd.{funcname}(sheet)', helpstr, **kwargs) return decorator BaseSheet.command = classmethod(_command) globalCommand = BaseSheet.addCommand @VisiData.api def bindkey(vd, keystrokes, longname): 'Bind *keystrokes* to *longname* on BaseSheet and unbind more-specific bindings of keystrokes.' vd.bindkeys[keystrokes] = {'BaseSheet': longname} @BaseSheet.class_api @classmethod def bindkey(cls, keystrokes, longname): 'Bind *keystrokes* to *longname* on the *cls* sheet type.' oldlongname = vd.bindkeys._get(keystrokes, cls) if oldlongname: vd.warning('%s was already bound to %s' % (keystrokes, oldlongname)) vd.bindkeys.set(keystrokes, longname, cls) @BaseSheet.class_api @classmethod def unbindkey(cls, keystrokes): '''Unbind `keystrokes` on a ``. May be necessary to avoid a warning when overriding a binding on the same exact class.''' vd.bindkeys.unset(keystrokes, cls) @BaseSheet.api def getCommand(sheet, cmd): 'Return the Command for the given *cmd*, which may be keystrokes, longname, or a Command itself, within the context of `sheet`.' if isinstance(cmd, Command): return cmd longname = cmd while vd.bindkeys._get(longname, obj=sheet): longname = vd.bindkeys._get(longname, obj=sheet) if not longname: vd.fail('no binding for %s' % cmd) return vd.commands._get(longname, obj=sheet) or vd.fail('no command "%s"' % longname) def loadConfigFile(fnrc, _globals=None): if not fnrc: return p = visidata.Path(fnrc) if _globals is None: _globals = globals() if p.exists(): try: with open(p) as fd: code = compile(fd.read(), str(p), 'exec') exec(code, _globals) except Exception as e: vd.exceptionCaught(e) vd.addGlobals(_globals) def addOptions(parser): for optname in options.keys('default'): if optname.startswith('color_') or optname.startswith('disp_'): continue action = 'store_true' if options[optname] is False else 'store' try: parser.add_argument('--' + optname.replace('_', '-'), action=action, dest=optname, default=None, help=options._opts._get(optname).helpstr) except argparse.ArgumentError: pass @VisiData.api def loadConfigAndPlugins(vd, args): # set visidata_dir and config manually before loading config file, so visidata_dir can be set from cli or from $VD_DIR options.visidata_dir = args.visidata_dir if args.visidata_dir is not None else os.getenv('VD_DIR', '') or options.visidata_dir options.config = args.config if args.config is not None else os.getenv('VD_CONFIG', '') or options.config sys.path.append(str(visidata.Path(options.visidata_dir))) sys.path.append(str(visidata.Path(options.visidata_dir)/"plugins-deps")) # import plugins from .visidata/plugins before .visidatarc, so plugin options can be overridden for modname in (args.imports or options.imports or '').split(): try: vd.addGlobals(importlib.import_module(modname).__dict__) except ModuleNotFoundError: continue # user customisations in config file in standard location loadConfigFile(options.config, vd.getGlobals()) BaseSheet.bindkey('^M', '^J') # for windows ENTER BaseSheet.addCommand('gO', 'open-config', 'vd.push(open_txt(Path(options.config)))', 'open options.config as text sheet') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612075960.0 visidata-2.2.1/visidata/sheets.py0000660000175000017500000013354600000000000017474 0ustar00kefalakefala00000000000000import collections import itertools from copy import copy import textwrap from visidata import VisiData, Extensible, globalCommand, ColumnAttr, ColumnItem, vd, ENTER, EscapeException, drawcache, drawcache_property, LazyChainMap, asyncthread, ExpectedException from visidata import (options, theme, Column, option, namedlist, SettableColumn, TypedExceptionWrapper, BaseSheet, UNLOADED, vd, clipdraw, ColorAttr, update_attr, colors, undoAttrFunc) import visidata __all__ = ['RowColorizer', 'CellColorizer', 'ColumnColorizer', 'Sheet', 'TableSheet', 'IndexSheet', 'SheetsSheet', 'LazyComputeRow', 'SequenceSheet'] option('default_width', 20, 'default column width', replay=True) # TODO: make not replay and remove from markdown saver option('default_height', 10, 'default column height') option('textwrap_cells', True, 'wordwrap text for multiline rows') option('quitguard', False, 'confirm before quitting last sheet') option('debug', False, 'exit on error and display stacktrace') option('skip', 0, 'skip N rows before header', replay=True) option('header', 1, 'parse first N rows as column names', replay=True) option('load_lazy', False, 'load subsheets always (False) or lazily (True)') theme('force_256_colors', False, 'use 256 colors even if curses reports fewer') theme('use_default_colors', False, 'curses use default terminal colors') theme('disp_note_none', '⌀', 'visible contents of a cell whose value is None') theme('disp_truncator', '…', 'indicator that the contents are only partially visible') theme('disp_oddspace', '\u00b7', 'displayable character for odd whitespace') theme('disp_more_left', '<', 'header note indicating more columns to the left') theme('disp_more_right', '>', 'header note indicating more columns to the right') theme('disp_error_val', '', 'displayed contents for computation exception') theme('disp_ambig_width', 1, 'width to use for unicode chars marked ambiguous') theme('disp_pending', '', 'string to display in pending cells') theme('note_pending', '⌛', 'note to display for pending cells') theme('note_format_exc', '?', 'cell note for an exception during formatting') theme('note_getter_exc', '!', 'cell note for an exception during computation') theme('note_type_exc', '!', 'cell note for an exception during type conversion') theme('color_note_pending', 'bold magenta', 'color of note in pending cells') theme('color_note_type', '226 yellow', 'color of cell note for non-str types in anytype columns') theme('color_note_row', '220 yellow', 'color of row note on left edge') theme('scroll_incr', 3, 'amount to scroll with scrollwheel') theme('disp_column_sep', '|', 'separator between columns') theme('disp_keycol_sep', '║', 'separator between key columns and rest of columns') theme('disp_rowtop_sep', '|', '') # ╷│┬╽⌜⌐▇ theme('disp_rowmid_sep', '⁝', '') # ┃┊│█ theme('disp_rowbot_sep', '⁝', '') # ┊┴╿⌞█⍿╵⎢┴⌊ ⋮⁝ theme('disp_rowend_sep', '║', '') # ┊┴╿⌞█⍿╵⎢┴⌊ theme('disp_keytop_sep', '║', '') # ╽╿┃╖╟ theme('disp_keymid_sep', '║', '') # ╽╿┃ theme('disp_keybot_sep', '║', '') # ╽╿┃╜‖ theme('disp_endtop_sep', '║', '') # ╽╿┃╖╢ theme('disp_endmid_sep', '║', '') # ╽╿┃ theme('disp_endbot_sep', '║', '') # ╽╿┃╜‖ theme('disp_selected_note', '•', '') # theme('disp_sort_asc', '↑↟⇞⇡⇧⇑', 'characters for ascending sort') # ↑▲↟↥↾↿⇞⇡⇧⇈⤉⤒⥔⥘⥜⥠⍏˄ˆ theme('disp_sort_desc', '↓↡⇟⇣⇩⇓', 'characters for descending sort') # ↓▼↡↧⇂⇃⇟⇣⇩⇊⤈⤓⥕⥙⥝⥡⍖˅ˇ theme('color_default', 'normal', 'the default color') theme('color_default_hdr', 'bold', 'color of the column headers') theme('color_bottom_hdr', 'underline', 'color of the bottom header row') theme('color_current_row', 'reverse', 'color of the cursor row') theme('color_current_col', 'bold', 'color of the cursor column') theme('color_current_hdr', 'bold reverse', 'color of the header for the cursor column') theme('color_column_sep', '246 blue', 'color of column separators') theme('color_key_col', '81 cyan', 'color of key columns') theme('color_hidden_col', '8', 'color of hidden columns on metasheets') theme('color_selected_row', '215 yellow', 'color of selected rows') option('name_joiner', '_', 'string to join sheet or column names') option('value_joiner', ' ', 'string to join display values') def splitcell(s, width=0): if width <= 0 or not options.textwrap_cells: return [s] ret = [] for L in s.splitlines(): ret.extend(textwrap.wrap(L, width=width, break_long_words=False, replace_whitespace=False)) return ret disp_column_fill = ' ' # pad chars after column value # higher precedence color overrides lower; all non-color attributes combine # coloropt is the color option name (like 'color_error') # func(sheet,col,row,value) should return a true value if coloropt should be applied # if coloropt is None, func() should return a coloropt (or None) instead RowColorizer = collections.namedtuple('RowColorizer', 'precedence coloropt func') CellColorizer = collections.namedtuple('CellColorizer', 'precedence coloropt func') ColumnColorizer = collections.namedtuple('ColumnColorizer', 'precedence coloropt func') class RecursiveExprException(Exception): pass class LazyComputeRow: 'Calculate column values as needed.' def __init__(self, sheet, row, col=None): self.row = row self.col = col self.sheet = sheet self._usedcols = set() self._keys = [c.name for c in self.sheet.columns] self._lcm.clear() # reset locals on lcm @property def _lcm(self): lcmobj = self.col or self.sheet if not hasattr(lcmobj, '_lcm'): lcmobj._lcm = LazyChainMap(self.sheet, vd, self.col) return lcmobj._lcm def keys(self): return self._keys + self._lcm.keys() + ['row', 'sheet', 'col'] def __str__(self): return str(self.as_dict()) def as_dict(self): return {c.name:self[c.name] for c in self.sheet.visibleCols} def __getattr__(self, k): return self.__getitem__(k) def __getitem__(self, colid): try: i = self._keys.index(colid) c = self.sheet.columns[i] if c is self.col: j = self._keys[i+1:].index(colid) c = self.sheet.columns[i+j+1] except ValueError: try: c = self._lcm[colid] except (KeyError, AttributeError): if colid == 'sheet': return self.sheet elif colid == 'row': c = self.row elif colid == 'col': c = self.col else: raise KeyError(colid) if not isinstance(c, Column): # columns calc in the context of the row of the cell being calc'ed return c if c in self._usedcols: raise RecursiveExprException() self._usedcols.add(c) ret = c.getTypedValue(self.row) self._usedcols.remove(c) return ret class BasicRow(collections.defaultdict): def __init__(self, *args): collections.defaultdict.__init__(self, lambda: None, *args) def __bool__(self): return True class TableSheet(BaseSheet): 'Base class for sheets with row objects and column views.' _rowtype = lambda: BasicRow() _coltype = SettableColumn rowtype = 'rows' columns = [] # list of Column colorizers = [ # list of Colorizer CellColorizer(2, 'color_default_hdr', lambda s,c,r,v: r is None), ColumnColorizer(2, 'color_current_col', lambda s,c,r,v: c is s.cursorCol), ColumnColorizer(1, 'color_key_col', lambda s,c,r,v: c and c.keycol), CellColorizer(0, 'color_default', lambda s,c,r,v: True), RowColorizer(2, 'color_selected_row', lambda s,c,r,v: s.isSelected(r)), RowColorizer(1, 'color_error', lambda s,c,r,v: isinstance(r, (Exception, TypedExceptionWrapper))), ] nKeys = 0 # columns[:nKeys] are key columns def __init__(self, *names, **kwargs): super().__init__(*names, **kwargs) self.rows = UNLOADED # list of opaque row objects (UNLOADED before first reload) self.cursorRowIndex = 0 # absolute index of cursor into self.rows self.cursorVisibleColIndex = 0 # index of cursor into self.visibleCols self._topRowIndex = 0 # cursorRowIndex of topmost row self.leftVisibleColIndex = 0 # cursorVisibleColIndex of leftmost column self.rightVisibleColIndex = 0 # as computed during draw() self._rowLayout = {} # [rowidx] -> (y, w) self._visibleColLayout = {} # [vcolidx] -> (x, w) # list of all columns in display order self.columns = kwargs.get('columns') or [copy(c) for c in self.columns] or [Column('_')] self._colorizers = [] self.recalc() # set .sheet on columns and start caches self.setKeys(self.columns[:self.nKeys]) # initial list of key columns self.__dict__.update(kwargs) # also done earlier in BaseSheet.__init__ @property def topRowIndex(self): return self._topRowIndex @topRowIndex.setter def topRowIndex(self, v): self._topRowIndex = v self._rowLayout.clear() def addColorizer(self, c): 'Add Colorizer *c* to the list of colorizers for this sheet.' self._colorizers.append(c) def removeColorizer(self, c): 'Remove Colorizer *c* from the list of colorizers for this sheet.' self._colorizers.remove(c) @drawcache_property def allColorizers(self): # all colorizers must be in the same bucket # otherwise, precedence does not get applied properly _colorizers = set() def allParents(cls): yield from cls.__bases__ for b in cls.__bases__: yield from allParents(b) for b in [self] + list(allParents(self.__class__)): for c in getattr(b, 'colorizers', []): _colorizers.add(c) _colorizers |= set(self._colorizers) return sorted(_colorizers, key=lambda x: x.precedence, reverse=True) def _colorize(self, col, row, value=None) -> ColorAttr: 'Returns ColorAttr for the given colorizers/col/row/value' colorstack = [] for colorizer in self.allColorizers: try: r = colorizer.func(self, col, row, value) if r: colorstack.append(colorizer.coloropt if colorizer.coloropt else r) except Exception as e: vd.exceptionCaught(e) return colors.resolve_colors(tuple(colorstack)) def addRow(self, row, index=None): 'Insert *row* at *index*, or append at end of rows if *index* is None.' if index is None: self.rows.append(row) else: self.rows.insert(index, row) return row def newRow(self): 'Return new blank row compatible with this sheet. Overrideable.' return type(self)._rowtype() @drawcache_property def colsByName(self): 'Return dict of colname:col' # dict comprehension in reverse order so first column with the name is used return {col.name:col for col in self.columns[::-1]} def column(self, colname): 'Return first column whose name matches *colname*.' return self.colsByName.get(colname) or vd.fail('no column matching "%s"' % colname) def recalc(self): 'Clear caches and set the ``sheet`` attribute on all columns.' for c in self.columns: c.recalc(self) @asyncthread def reload(self): 'Load rows and/or columns from ``self.source``. Async. Override in subclass.' self.rows = [] with vd.Progress(gerund='loading', total=0): for r in self.iterload(): self.addRow(r) # if an ordering has been specified, sort the sheet if self._ordering: vd.sync(self.sort()) def iterload(self): 'Generate rows from ``self.source``. Override in subclass.' if False: yield vd.fail('no iterload for this loader yet') def iterrows(self): if self.rows is UNLOADED: try: self.rows = [] for row in self.iterload(): self.addRow(row) yield row return except ExpectedException: vd.sync(self.reload()) for row in vd.Progress(self.rows): yield row def __iter__(self): for row in self.iterrows(): yield LazyComputeRow(self, row) def __copy__(self): 'Copy sheet design but remain unloaded. Deepcopy columns so their attributes (width, type, name) may be adjusted independently of the original.' ret = super().__copy__() ret.rows = UNLOADED ret.columns = [copy(c) for c in self.keyCols] ret.setKeys(ret.columns) ret.columns.extend(copy(c) for c in self.columns if c not in self.keyCols) ret.recalc() # set .sheet on columns ret.topRowIndex = ret.cursorRowIndex = 0 return ret @property def bottomRowIndex(self): return max(self._rowLayout.keys()) if self._rowLayout else self.topRowIndex+self.nScreenRows-1 @bottomRowIndex.setter def bottomRowIndex(self, newidx): 'Set topRowIndex, by getting height of *newidx* row and going backwards until more than nScreenRows is allocated.' nrows = 0 i = 0 while nrows < self.nScreenRows and newidx-i >= 0: h = self.calc_height(self.rows[newidx-i]) nrows += h i += 1 self._topRowIndex = newidx-i+2 if nrows == self.nScreenRows-1 else newidx-self.nScreenRows+1 def __deepcopy__(self, memo): 'same as __copy__' ret = self.__copy__() memo[id(self)] = ret return ret def __repr__(self): return self.name def evalExpr(self, expr, row=None, col=None): if row: # contexts are cached by sheet/rowid for duration of drawcycle contexts = vd._evalcontexts.setdefault((self, self.rowid(row), col), LazyComputeRow(self, row, col=col)) else: contexts = None return eval(expr, vd.getGlobals(), contexts) def rowid(self, row): 'Return a unique and stable hash of the *row* object. Must be fast. Overrideable.' return id(row) @property def nScreenRows(self): 'Number of visible rows at the current window height.' return self.windowHeight-self.nHeaderRows-self.nFooterRows @drawcache_property def nHeaderRows(self): vcols = self.visibleCols return max(len(col.name.split('\n')) for col in vcols) if vcols else 0 @property def nFooterRows(self): 'Number of lines reserved at the bottom, including status line.' return 1 @property def cursorCol(self): 'Current Column object.' vcols = self.visibleCols return vcols[min(self.cursorVisibleColIndex, len(vcols)-1)] if vcols else None @property def cursorRow(self): 'The row object at the row cursor.' return self.rows[self.cursorRowIndex] if self.nRows > 0 else None @property def visibleRows(self): # onscreen rows 'List of rows onscreen.' return self.rows[self.topRowIndex:self.topRowIndex+self.nScreenRows] @drawcache_property def visibleCols(self): # non-hidden cols 'List of non-hidden columns in display order.' return self.keyCols + [c for c in self.columns if not c.hidden and not c.keycol] def visibleColAtX(self, x): for vcolidx, (colx, w) in self._visibleColLayout.items(): if colx <= x <= colx+w: return vcolidx def visibleRowAtY(self, y): for rowidx, (rowy, h) in self._rowLayout.items(): if rowy <= y <= rowy+h-1: return rowidx @drawcache_property def keyCols(self): 'List of visible key columns.' return sorted([c for c in self.columns if c.keycol and not c.hidden], key=lambda c:c.keycol) @property def cursorColIndex(self): 'Index of current column into `Sheet.columns`. Linear search; prefer `cursorCol` or `cursorVisibleColIndex`.' return self.columns.index(self.cursorCol) @property def nonKeyVisibleCols(self): 'List of visible non-key columns.' return [c for c in self.columns if not c.hidden and c not in self.keyCols] @property def keyColNames(self): 'String of key column names, for SheetsSheet convenience.' return ' '.join(c.name for c in self.keyCols) @property def cursorCell(self): 'Displayed value (DisplayWrapper) at current row and column.' return self.cursorCol.getCell(self.cursorRow) @property def cursorDisplay(self): 'Displayed value (DisplayWrapper.display) at current row and column.' return self.cursorCol.getDisplayValue(self.cursorRow) @property def cursorTypedValue(self): 'Typed value at current row and column.' return self.cursorCol.getTypedValue(self.cursorRow) @property def cursorValue(self): 'Raw value at current row and column.' return self.cursorCol.getValue(self.cursorRow) @property def statusLine(self): 'Position of cursor and bounds of current sheet.' rowinfo = 'row %d (%d selected)' % (self.cursorRowIndex, self.nSelectedRows) colinfo = 'col %d (%d visible)' % (self.cursorVisibleColIndex, len(self.visibleCols)) return '%s %s' % (rowinfo, colinfo) @property def nRows(self): 'Number of rows on this sheet.' return len(self.rows) @property def nCols(self): 'Number of columns on this sheet.' return len(self.columns) @property def nVisibleCols(self): 'Number of visible columns on this sheet.' return len(self.visibleCols) def cursorDown(self, n=1): 'Move cursor down `n` rows (or up if `n` is negative).' self.cursorRowIndex += n def cursorRight(self, n=1): 'Move cursor right `n` visible columns (or left if `n` is negative).' self.cursorVisibleColIndex += n self.calcColLayout() def addColumn(self, *cols, index=None): 'Insert all *cols* into columns at *index*, or append to end of columns if *index* is None. Return first column.' for i, col in enumerate(cols): vd.addUndo(self.columns.remove, col) if index is None: index = len(self.columns) col.recalc(self) self.columns.insert(index+i, col) Sheet.visibleCols.fget.cache_clear() return cols[0] def addColumnAtCursor(self, *cols): 'Insert all *cols* into columns after cursor. Return first column.' return self.addColumn(*cols, index=0 if self.cursorCol.keycol else self.columns.index(self.cursorCol)+1) def setColNames(self, rows): for c in self.visibleCols: c.name = '\n'.join(str(c.getDisplayValue(r)) for r in rows) def setKeys(self, cols): 'Make all *cols* into key columns.' vd.addUndo(undoAttrFunc(cols, 'keycol')) lastkeycol = 0 if self.keyCols: lastkeycol = max(c.keycol for c in self.keyCols) for col in cols: if not col.keycol: col.keycol = lastkeycol+1 lastkeycol += 1 # clears the keyCols cache visidata.Extensible.clear_all_caches() def unsetKeys(self, cols): 'Make all *cols* non-key columns.' vd.addUndo(undoAttrFunc(cols, 'keycol')) for col in cols: col.keycol = 0 def toggleKeys(self, cols): for col in cols: if col.keycol: self.unsetKeys([col]) else: self.setKeys([col]) def rowkey(self, row): 'Return tuple of the key for *row*.' return tuple(c.getTypedValue(row) for c in self.keyCols) def keystr(self, row): 'Return string of the key for *row*.' return ','.join(map(str, self.rowkey(row))) def checkCursor(self): 'Keep cursor in bounds of data and screen.' # keep cursor within actual available rowset if self.nRows == 0 or self.cursorRowIndex <= 0: self.cursorRowIndex = 0 elif self.cursorRowIndex >= self.nRows: self.cursorRowIndex = self.nRows-1 if self.cursorVisibleColIndex <= 0: self.cursorVisibleColIndex = 0 elif self.cursorVisibleColIndex >= self.nVisibleCols: self.cursorVisibleColIndex = self.nVisibleCols-1 if self.topRowIndex < 0: self.topRowIndex = 0 elif self.topRowIndex > self.nRows-1: self.topRowIndex = self.nRows-1 # check bounds, scroll if necessary if self.topRowIndex > self.cursorRowIndex: self.topRowIndex = self.cursorRowIndex elif self.bottomRowIndex < self.cursorRowIndex: self.bottomRowIndex = self.cursorRowIndex elif self.bottomRowIndex == self.cursorRowIndex and self._rowLayout and self._rowLayout[self.bottomRowIndex][1] > 1: self.bottomRowIndex = self.cursorRowIndex if self.cursorCol and self.cursorCol.keycol: return if self.leftVisibleColIndex >= self.cursorVisibleColIndex: self.leftVisibleColIndex = self.cursorVisibleColIndex else: while True: if self.leftVisibleColIndex == self.cursorVisibleColIndex: # not much more we can do break self.calcColLayout() if not self._visibleColLayout: break mincolidx, maxcolidx = min(self._visibleColLayout.keys()), max(self._visibleColLayout.keys()) if self.cursorVisibleColIndex < mincolidx: self.leftVisibleColIndex -= max((self.cursorVisibleColIndex - mincolid)//2, 1) continue elif self.cursorVisibleColIndex > maxcolidx: self.leftVisibleColIndex += max((maxcolidx - self.cursorVisibleColIndex)//2, 1) continue cur_x, cur_w = self._visibleColLayout[self.cursorVisibleColIndex] if cur_x+cur_w < self.windowWidth: # current columns fit entirely on screen break self.leftVisibleColIndex += 1 # once within the bounds, walk over one column at a time def calcColLayout(self): 'Set right-most visible column, based on calculation.' minColWidth = len(options.disp_more_left)+len(options.disp_more_right)+2 sepColWidth = len(options.disp_column_sep) winWidth = self.windowWidth self._visibleColLayout = {} x = 0 vcolidx = 0 for vcolidx in range(0, self.nVisibleCols): col = self.visibleCols[vcolidx] if col.width is None and len(self.visibleRows) > 0: vrows = self.visibleRows if self.nRows > 1000 else self.rows # handle delayed column width-finding col.width = max(col.getMaxWidth(vrows), minColWidth) if vcolidx != self.nVisibleCols-1: # let last column fill up the max width col.width = min(col.width, options.default_width) width = col.width if col.width is not None else options.default_width if col in self.keyCols: width = max(width, 1) # keycols must all be visible if col in self.keyCols or vcolidx >= self.leftVisibleColIndex: # visible columns self._visibleColLayout[vcolidx] = [x, min(width, winWidth-x)] x += width+sepColWidth if x > winWidth-1: break self.rightVisibleColIndex = vcolidx def drawColHeader(self, scr, y, h, vcolidx): 'Compose and draw column header for given vcolidx.' col = self.visibleCols[vcolidx] # hdrattr highlights whole column header # sepattr is for header separators and indicators sepcattr = colors.get_color('color_column_sep') hdrcattr = self._colorize(col, None) if vcolidx == self.cursorVisibleColIndex: hdrcattr = update_attr(hdrcattr, colors.color_current_hdr, 2) C = options.disp_column_sep if (self.keyCols and col is self.keyCols[-1]) or vcolidx == self.rightVisibleColIndex: C = options.disp_keycol_sep x, colwidth = self._visibleColLayout[vcolidx] # AnameTC T = vd.getType(col.type).icon if T is None: # still allow icon to be explicitly non-displayed '' T = '?' hdrs = col.name.split('\n') for i in range(h): name = ' ' # save room at front for LeftMore or sorted arrow for j, (sortcol, sortdir) in enumerate(self._ordering): if col is sortcol: try: name = self.options.disp_sort_desc[j] if sortdir else self.options.disp_sort_asc[j] except IndexError: pass if h-i-1 < len(hdrs): name += hdrs[::-1][h-i-1] if len(name) > colwidth-1: name = name[:colwidth-len(options.disp_truncator)] + options.disp_truncator if i == h-1: hdrcattr = update_attr(hdrcattr, colors.color_bottom_hdr, 5) clipdraw(scr, y+i, x, name, hdrcattr.attr, colwidth) vd.onMouse(scr, y+i, x, 1, colwidth, BUTTON3_RELEASED='rename-col') if C and x+colwidth+len(C) < self.windowWidth and y+i < self.windowWidth: scr.addstr(y+i, x+colwidth, C, sepcattr.attr) clipdraw(scr, y+h-1, x+colwidth-len(T), T, hdrcattr.attr) try: if vcolidx == self.leftVisibleColIndex and col not in self.keyCols and self.nonKeyVisibleCols.index(col) > 0: A = options.disp_more_left scr.addstr(y, x, A, sepcattr.attr) except ValueError: # from .index pass def isVisibleIdxKey(self, vcolidx): 'Return boolean: is given column index a key column?' return self.visibleCols[vcolidx] in self.keyCols def draw(self, scr): 'Draw entire screen onto the `scr` curses object.' vd.clearCaches() if not self.columns: if options.debug: self.addColumn(Column()) else: return drawparams = { 'isNull': self.isNullFunc(), 'topsep': options.disp_rowtop_sep, 'midsep': options.disp_rowmid_sep, 'botsep': options.disp_rowbot_sep, 'endsep': options.disp_rowend_sep, 'keytopsep': options.disp_keytop_sep, 'keymidsep': options.disp_keymid_sep, 'keybotsep': options.disp_keybot_sep, 'endtopsep': options.disp_endtop_sep, 'endmidsep': options.disp_endmid_sep, 'endbotsep': options.disp_endbot_sep, 'colsep': options.disp_column_sep, 'keysep': options.disp_keycol_sep, 'selectednote': options.disp_selected_note, 'disp_truncator': options.disp_truncator, } self._rowLayout = {} # [rowidx] -> (y, height) self.calcColLayout() numHeaderRows = self.nHeaderRows vcolidx = 0 headerRow = 0 for vcolidx, colinfo in sorted(self._visibleColLayout.items()): self.drawColHeader(scr, headerRow, numHeaderRows, vcolidx) y = headerRow + numHeaderRows rows = self.rows[self.topRowIndex:min(self.topRowIndex+self.nScreenRows, self.nRows)] self.checkCursorNoExceptions() for rowidx, row in enumerate(rows): if y >= self.windowHeight-1: break rowcattr = self._colorize(None, row) y += self.drawRow(scr, row, self.topRowIndex+rowidx, y, rowcattr, maxheight=self.windowHeight-y-1, **drawparams) if vcolidx+1 < self.nVisibleCols: scr.addstr(headerRow, self.windowWidth-2, options.disp_more_right, colors.color_column_sep) scr.refresh() def calc_height(self, row, displines=None, isNull=None): if displines is None: displines = {} # [vcolidx] -> list of lines in that cell for vcolidx, (x, colwidth) in sorted(self._visibleColLayout.items()): if x < self.windowWidth: # only draw inside window vcols = self.visibleCols if vcolidx >= len(vcols): continue col = vcols[vcolidx] cellval = col.getCell(row) if colwidth > 1 and vd.isNumeric(col): cellval.display = cellval.display.rjust(colwidth-2) try: if isNull and isNull(cellval.value): cellval.note = options.disp_note_none cellval.notecolor = 'color_note_type' except (TypeError, ValueError): pass if col.voffset or col.height > 1: lines = splitcell(cellval.display, width=colwidth-2) else: lines = [cellval.display] displines[vcolidx] = (col, cellval, lines) heights = [0] for col, cellval, lines in displines.values(): h = len(lines) # of this cell heights.append(min(col.height, h)) return max(heights) def drawRow(self, scr, row, rowidx, ybase, rowcattr: ColorAttr, maxheight, isNull='', topsep='', midsep='', botsep='', endsep='', keytopsep='', keymidsep='', keybotsep='', endtopsep='', endmidsep='', endbotsep='', colsep='', keysep='', selectednote='', disp_truncator='' ): # sepattr is the attr between cell/columns sepcattr = update_attr(rowcattr, colors.color_column_sep, 1) # apply current row here instead of in a colorizer, because it needs to know dispRowIndex if rowidx == self.cursorRowIndex: color_current_row = colors.get_color('color_current_row', 5) basecellcattr = sepcattr = update_attr(rowcattr, color_current_row) else: basecellcattr = rowcattr displines = {} # [vcolidx] -> list of lines in that cell height = min(self.calc_height(row, displines), maxheight) or 1 # display even empty rows self._rowLayout[rowidx] = (ybase, height) for vcolidx, (col, cellval, lines) in displines.items(): if vcolidx not in self._visibleColLayout: continue x, colwidth = self._visibleColLayout[vcolidx] hoffset = col.hoffset voffset = col.voffset cattr = self._colorize(col, row, cellval) cattr = update_attr(cattr, basecellcattr) note = getattr(cellval, 'note', None) if note: notecattr = update_attr(cattr, colors.get_color(cellval.notecolor), 10) clipdraw(scr, ybase, x+colwidth-len(note), note, notecattr.attr) if voffset >= 0: if len(lines)-voffset > height: # last line should always include as much as possible firstn = sum(len(i)+1 for i in lines[:voffset+height-1]) lines = lines[:voffset+height] lines[-1] = cellval.display[firstn:][:col.width] lines = lines[voffset:] if len(lines) > height: lines = lines[:height] elif len(lines) < height: lines.extend(['']*(height-len(lines))) for i, line in enumerate(lines): y = ybase+i if vcolidx == self.rightVisibleColIndex: # right edge of sheet if len(lines) == 1: sepchars = endsep else: if i == 0: sepchars = endtopsep elif i == len(lines)-1: sepchars = endbotsep else: sepchars = endmidsep elif (self.keyCols and col is self.keyCols[-1]): # last keycol if len(lines) == 1: sepchars = keysep else: if i == 0: sepchars = keytopsep elif i == len(lines)-1: sepchars = keybotsep else: sepchars = keymidsep else: if len(lines) == 1: sepchars = colsep else: if i == 0: sepchars = topsep elif i == len(lines)-1: sepchars = botsep else: sepchars = midsep pre = disp_truncator if hoffset != 0 else disp_column_fill clipdraw(scr, y, x, (pre if colwidth > 2 else '')+line[hoffset:], cattr.attr, w=colwidth-(1 if note else 0)) vd.onMouse(scr, y, x, 1, colwidth, BUTTON3_RELEASED='edit-cell') if x+colwidth+len(sepchars) <= self.windowWidth: scr.addstr(y, x+colwidth, sepchars, sepcattr.attr) for notefunc in vd.rowNoters: ch = notefunc(self, row) if ch: clipdraw(scr, ybase, 0, ch, colors.color_note_row) break return height vd.rowNoters = [ lambda sheet, row: sheet.isSelected(row) and options.disp_selected_note, ] Sheet = TableSheet # deprecated in 2.0 but still widely used internally class SequenceSheet(Sheet): 'Sheets with ``ColumnItem`` columns, and rows that are Python sequences (list, namedtuple, etc).' def setCols(self, headerrows): self.columns = [] for i, colnamelines in enumerate(itertools.zip_longest(*headerrows, fillvalue='')): colnamelines = ['' if c is None else c for c in colnamelines] self.addColumn(ColumnItem(''.join(colnamelines), i)) self._rowtype = namedlist('tsvobj', [(c.name or '_') for c in self.columns]) def newRow(self): return self._rowtype() def addRow(self, row, index=None): for i in range(len(self.columns), len(row)): # no-op if already done self.addColumn(ColumnItem('', i)) self._rowtype = namedlist('tsvobj', [(c.name or '_') for c in self.columns]) if type(row) is not self._rowtype: row = self._rowtype(row) super().addRow(row, index=index) def optlines(self, it, optname): 'Generate next options. elements from iterator with exceptions wrapped.' for i in range(options.getobj(optname, self)): try: yield next(it) except StopIteration: break @asyncthread def reload(self): 'Skip first options.skip rows; set columns from next options.header rows.' itsource = self.iterload() # skip the first options.skip rows list(self.optlines(itsource, 'skip')) # use the next options.header rows as columns self.setCols(list(self.optlines(itsource, 'header'))) self.rows = [] # add the rest of the rows for r in vd.Progress(itsource, gerund='loading', total=0): self.addRow(r) # if an ordering has been specified, sort the sheet if self._ordering: vd.sync(self.sort()) class IndexSheet(Sheet): 'Base class for tabular sheets with rows that are Sheets.' rowtype = 'sheets' # rowdef: Sheet precious = False columns = [ ColumnAttr('name'), ColumnAttr('rows', 'nRows', type=int), ColumnAttr('cols', 'nCols', type=int), ColumnAttr('keys', 'keyColNames'), ColumnAttr('source'), ] nKeys = 1 def newRow(self): return Sheet('', columns=[ColumnItem('', 0)], rows=[]) def openRow(self, row): return row # rowdef is Sheet def getSheet(self, k): for vs in self.rows: if vs.name == k: return vs def addRow(self, sheet): super().addRow(sheet) if not self.options.load_lazy: sheet.ensureLoaded() class SheetsSheet(IndexSheet): columns = [ ColumnAttr('name'), ColumnAttr('type', '__class__.__name__'), ColumnAttr('shortcut'), ColumnAttr('nRows', type=int), ColumnAttr('nCols', type=int), ColumnAttr('nVisibleCols', type=int), ColumnAttr('cursorDisplay'), ColumnAttr('keyColNames'), ColumnAttr('source'), ColumnAttr('progressPct'), # ColumnAttr('threads', 'currentThreads', type=vlen), ] nKeys = 1 def reload(self): self.rows = self.source def sort(self): self.rows[1:] = sorted(self.rows[1:], key=self.sortkey) @VisiData.property @drawcache def _evalcontexts(vd): return {} ## VisiData sheet manipulation @VisiData.global_api def replace(vd, vs): 'Replace top sheet with the given sheet `vs`.' vd.sheets.pop(0) return vd.push(vs) @VisiData.global_api def remove(vd, vs): 'Remove *vs* from sheets stack, without asking for confirmation.' if vs in vd.sheets: vd.sheets.remove(vs) if vs in vd.allSheets: vd.allSheets.remove(vs) vd.allSheets.append(vs) else: vd.fail('sheet not on stack') @VisiData.global_api def push(vd, vs): 'Push Sheet *vs* onto ``vd.sheets`` stack. Remove from other position if already on sheets stack.' if not isinstance(vs, BaseSheet): return # return instead of raise, some commands need this vs.vd = vd if vs in vd.sheets: vd.sheets.remove(vs) vd.sheets.insert(0, vs) if vs.precious and vs not in vd.allSheets: vd.allSheets.append(vs) vs.ensureLoaded() @VisiData.lazy_property def allSheetsSheet(vd): return SheetsSheet("sheets_all", source=vd.allSheets) @VisiData.lazy_property def sheetsSheet(vd): return SheetsSheet("sheets", source=vd.sheets) @VisiData.api def quit(vd, *sheets): 'Remove *sheets* from sheets stack, asking for confirmation if options.quitguard set (either global or sheet-specific).' if len(vd.sheets) == len(sheets) and options.getonly('quitguard', 'global', False): vd.confirm("quit last sheet? ") for vs in sheets: if options.getonly('quitguard', vs, False): vd.draw_all() vd.confirm(f'quit guarded sheet "{vs.name}?" ') vd.remove(vs) @BaseSheet.api def preloadHook(sheet): 'Override to setup for reload().' pass @VisiData.api def newSheet(vd, name, ncols, **kwargs): return Sheet(name, columns=[SettableColumn() for i in range(ncols)], **kwargs) @Sheet.api def updateColNames(sheet, rows, cols, overwrite=False): vd.addUndoColNames(cols) for c in cols: if not c._name or overwrite: c.name = "\n".join(c.getDisplayValue(r) for r in rows) IndexSheet.class_options.header = 0 IndexSheet.class_options.skip = 0 globalCommand('S', 'sheets-stack', 'vd.push(vd.sheetsSheet)', 'open Sheets Stack: join or jump between the active sheets on the current stack') globalCommand('gS', 'sheets-all', 'vd.push(vd.allSheetsSheet)', 'open Sheets Sheet: join or jump between all sheets from current session') BaseSheet.addCommand('^R', 'reload-sheet', 'preloadHook(); vd.addUndoReload(rows, columns); reload(); recalc(); status("reloaded")', 'reload current sheet'), Sheet.addCommand('^G', 'show-cursor', 'status(statusLine)', 'show cursor position and bounds of current sheet on status line'), Sheet.addCommand('!', 'key-col', 'toggleKeys([cursorCol])', 'toggle current column as a key column') Sheet.addCommand('z!', 'key-col-off', 'unsetKeys([cursorCol])', 'unset current column as a key column') Sheet.addCommand('e', 'edit-cell', 'cursorCol.setValues([cursorRow], editCell(cursorVisibleColIndex)) if not (cursorRow is None) else fail("no rows to edit")', 'edit contents of current cell') Sheet.addCommand('ge', 'setcol-input', 'cursorCol.setValuesTyped(selectedRows, input("set selected to: ", value=cursorDisplay))', 'set contents of current column for selected rows to same input') Sheet.addCommand('"', 'dup-selected', 'vs=copy(sheet); vs.name += "_selectedref"; vs.reload=lambda vs=vs,rows=selectedRows: setattr(vs, "rows", list(rows)); vd.push(vs)', 'open duplicate sheet with only selected rows'), Sheet.addCommand('g"', 'dup-rows', 'vs=copy(sheet); vs.name+="_copy"; vs.rows=list(rows); status("copied "+vs.name); vs.select(selectedRows); vd.push(vs)', 'open duplicate sheet with all rows'), Sheet.addCommand('z"', 'dup-selected-deep', 'vs = deepcopy(sheet); vs.name += "_selecteddeepcopy"; vs.rows = async_deepcopy(vs, selectedRows); vd.push(vs); status("pushed sheet with async deepcopy of selected rows")', 'open duplicate sheet with deepcopy of selected rows'), Sheet.addCommand('gz"', 'dup-rows-deep', 'vs = deepcopy(sheet); vs.name += "_deepcopy"; vs.rows = async_deepcopy(vs, rows); vd.push(vs); status("pushed sheet with async deepcopy of all rows")', 'open duplicate sheet with deepcopy of all rows'), Sheet.addCommand('z~', 'type-any', 'cursorCol.type = anytype', 'set type of current column to anytype') Sheet.addCommand('~', 'type-string', 'cursorCol.type = str', 'set type of current column to str') Sheet.addCommand('@', 'type-date', 'cursorCol.type = date', 'set type of current column to date') Sheet.addCommand('#', 'type-int', 'cursorCol.type = int', 'set type of current column to int') Sheet.addCommand('z#', 'type-len', 'cursorCol.type = vlen', 'set type of current column to len') Sheet.addCommand('$', 'type-currency', 'cursorCol.type = currency', 'set type of current column to currency') Sheet.addCommand('%', 'type-float', 'cursorCol.type = float', 'set type of current column to float') Sheet.addCommand('z%', 'type-floatsi', 'cursorCol.type = floatsi', 'set type of current column to SI float') Sheet.addCommand('', 'type-floatlocale', 'cursorCol.type = floatlocale', 'set type of current column to float using system locale set in LC_NUMERIC') # when diving into a sheet, remove the index unless it is precious IndexSheet.addCommand('g^R', 'reload-selected', 'for vs in selectedRows or rows: vs.reload()', 'reload all selected sheets') SheetsSheet.addCommand('gC', 'columns-selected', 'vd.push(ColumnsSheet("all_columns", source=selectedRows))', 'open Columns Sheet with all visible columns from selected sheets') SheetsSheet.addCommand('gI', 'describe-selected', 'vd.push(DescribeSheet("describe_all", source=selectedRows))', 'open Describe Sheet with all visble columns from selected sheets') SheetsSheet.addCommand('z^C', 'cancel-row', 'cancelThread(*cursorRow.currentThreads)', 'abort async thread for current sheet') SheetsSheet.addCommand('gz^C', 'cancel-rows', 'for vs in selectedRows: cancelThread(*vs.currentThreads)', 'abort async threads for selected sheets') SheetsSheet.addCommand(ENTER, 'open-row', 'dest=cursorRow; vd.sheets.remove(sheet) if not sheet.precious else None; vd.push(openRow(dest))', 'open sheet referenced in current row') BaseSheet.addCommand('q', 'quit-sheet', 'vd.quit(sheet)', 'quit current sheet') globalCommand('gq', 'quit-all', 'vd.quit(*vd.sheets)', 'quit all sheets (clean exit)') BaseSheet.addCommand('Z', 'splitwin-half', 'options.disp_splitwin_pct = 50', 'split screen in half, so that second sheet on stack is visible in a second pane') BaseSheet.addCommand('gZ', 'splitwin-close', 'options.disp_splitwin_pct = 0', 'close an already split screen, current pane full screens') BaseSheet.addCommand('^I', 'splitwin-swap', 'vd.push(vd.sheets[1]) if len(sheets) >=2 else fail("at least 2 sheets required for splitwin-swap"); options.disp_splitwin_pct = -options.disp_splitwin_pct', 'jump to other pane') BaseSheet.addCommand('zZ', 'splitwin-input', 'options.disp_splitwin_pct = input("% height for split window: ", value=options.disp_splitwin_pct)', 'split screen and queries for height of second pane, second sheet on stack is visible in second pane') BaseSheet.addCommand('^L', 'redraw', 'vd.redraw(); sheet.refresh()', 'refresh screen') BaseSheet.addCommand(None, 'guard-sheet', 'options.set("quitguard", True, sheet); status("guarded")', 'guard current sheet from accidental quitting') BaseSheet.addCommand(None, 'open-source', 'vd.push(source)', 'jump to the source of this sheet') BaseSheet.bindkey('KEY_RESIZE', 'redraw') BaseSheet.addCommand('A', 'open-new', 'vd.push(vd.newSheet("unnamed", 1))', 'open new blank sheet') Sheet.addCommand('^', 'rename-col', 'vd.addUndoColNames([cursorCol]); cursorCol.name = editCell(cursorVisibleColIndex, -1)', 'edit name of current column') Sheet.addCommand('z^', 'rename-col-selected', 'updateColNames(selectedRows or [cursorRow], [sheet.cursorCol], overwrite=True)', 'set name of current column to combined contents of current cell in selected rows (or current row)') Sheet.addCommand('g^', 'rename-cols-row', 'updateColNames(selectedRows or [cursorRow], sheet.visibleCols)', 'set names of all unnamed visible columns to contents of selected rows (or current row)') Sheet.addCommand('gz^', 'rename-cols-selected', 'updateColNames(selectedRows or [cursorRow], sheet.visibleCols, overwrite=True)', 'set names of all visible columns to combined contents of selected rows (or current row)') BaseSheet.addCommand(None, 'rename-sheet', 'sheet.name = input("rename sheet to: ", value=sheet.name)', 'rename current sheet to input') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608541899.0 visidata-2.2.1/visidata/shell.py0000660000175000017500000001742300000000000017303 0ustar00kefalakefala00000000000000import os import io import sys import stat import locale import subprocess import contextlib try: import pwd import grp except ImportError: pass # pwd,grp modules not available on Windows from visidata import Column, Sheet, LazyComputeRow, asynccache, options, option, globalCommand from visidata import Path, ENTER, date, asyncthread, confirm, fail, FileExistsError, VisiData from visidata import CellColorizer, RowColorizer, modtime, filesize, vstat option('dir_recurse', False, 'walk source path recursively on DirSheet') option('dir_hidden', False, 'load hidden files on DirSheet') @VisiData.lazy_property def currentDirSheet(p): 'Support opening the current DirSheet from the vdmenu' return DirSheet('.', source=Path('.')) @asyncthread def exec_shell(*args): p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, err = p.communicate() if err or out: lines = err.decode('utf8').splitlines() + out.decode('utf8').splitlines() vd.push(TextSheet(' '.join(args), source=lines)) def open_dir(p): return DirSheet(p.name, source=p) def open_fdir(p): return FileListSheet(p.name, source=p) def addShellColumns(cmd, sheet): shellcol = ColumnShell(cmd, source=sheet, width=0) sheet.addColumnAtCursor( shellcol, Column(cmd+'_stdout', srccol=shellcol, getter=lambda col,row: col.srccol.getValue(row)[0]), Column(cmd+'_stderr', srccol=shellcol, getter=lambda col,row: col.srccol.getValue(row)[1])) class ColumnShell(Column): def __init__(self, name, cmd=None, **kwargs): super().__init__(name, **kwargs) self.expr = cmd or name @asynccache(lambda col,row: (col, col.sheet.rowid(row))) def calcValue(self, row): try: import shlex args = [] context = LazyComputeRow(self.source, row) for arg in shlex.split(self.expr): if arg.startswith('$'): args.append(str(context[arg[1:]])) else: args.append(arg) p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) return p.communicate() except Exception as e: vd.exceptionCaught(e) class DirSheet(Sheet): 'Sheet displaying directory, using ENTER to open a particular file. Edited fields are applied to the filesystem.' rowtype = 'files' # rowdef: Path defer = True columns = [ Column('directory', getter=lambda col,row: str(row.parent) if str(row.parent) == '.' else str(row.parent) + '/', setter=lambda col,row,val: col.sheet.moveFile(row, val)), Column('filename', getter=lambda col,row: row.name + row.suffix, setter=lambda col,row,val: col.sheet.renameFile(row, val)), Column('abspath', width=0, type=str, getter=lambda col,row: row, setter=lambda col,row,val: os.rename(row, val)), Column('ext', getter=lambda col,row: row.is_dir() and '/' or row.ext), Column('size', type=int, getter=lambda col,row: filesize(row), setter=lambda col,row,val: os.truncate(row, int(val))), Column('modtime', type=date, getter=lambda col,row: modtime(row), setter=lambda col,row,val: os.utime(row, times=((row.stat().st_atime, float(val))))), Column('owner', width=0, getter=lambda col,row: pwd.getpwuid(row.stat().st_uid).pw_name, setter=lambda col,row,val: os.chown(row, pwd.getpwnam(val).pw_uid, -1)), Column('group', width=0, getter=lambda col,row: grp.getgrgid(row.stat().st_gid).gr_name, setter=lambda col,row,val: os.chown(row, -1, grp.getgrnam(val).pw_gid)), Column('mode', width=0, getter=lambda col,row: '{:o}'.format(row.stat().st_mode), setter=lambda col,row,val: os.chmod(row, int(val, 8))), Column('filetype', width=0, cache='async', getter=lambda col,row: subprocess.Popen(['file', '--brief', row], stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()[0].strip()), ] nKeys = 2 _ordering = [('modtime', True)] # sort by reverse modtime initially @staticmethod def colorOwner(sheet, col, row, val): ret = '' if col.name == 'group': mode = row.stat().st_mode if mode & stat.S_IXGRP: ret = 'bold ' if mode & stat.S_IWGRP: return ret + 'green' if mode & stat.S_IRGRP: return ret + 'yellow' elif col.name == 'owner': mode = row.stat().st_mode if mode & stat.S_IXUSR: ret = 'bold ' if mode & stat.S_IWUSR: return ret + 'green' if mode & stat.S_IRUSR: return ret + 'yellow' def moveFile(self, row, newparent): parent = Path(newparent) newpath = Path(parent/(row.name + row.suffix)) if parent.exists(): if not parent.is_dir(): vd.error('destination %s not a directory' % parent) else: with contextlib.suppress(FileExistsError): os.makedirs(parent) row.rename(newpath) row.given = newpath # modify visidata.Path self.restat() def renameFile(self, row, val): newpath = row.with_name(val) row.rename(newpath) row.given = newpath self.restat() def removeFile(self, path): if path.is_dir(): os.rmdir(path) else: path.unlink() def deleteSourceRow(self, r): self.removeFile(r) def iterload(self): def _walkfiles(p): basepath = str(p) for folder, subdirs, files in os.walk(basepath): subfolder = folder[len(basepath)+1:] if subfolder in ['.', '..']: continue fpath = Path(folder) yield fpath for fn in files: yield fpath/fn def _listfiles(p): basepath = str(p) for fn in os.listdir(basepath): yield p/fn basepath = str(self.source) folders = set() f = _walkfiles if self.options.dir_recurse else _listfiles hidden_files = options.dir_hidden for p in f(self.source): if hidden_files and p.name.startswith('.'): continue yield p def preloadHook(self): super().preloadHook() Path.stat.cache_clear() def restat(self): vstat.cache_clear() @asyncthread def putChanges(self): self.commitAdds() self.commitMods() self.commitDeletes() self._deferredDels.clear() self.reload() class FileListSheet(DirSheet): _ordering = [] def iterload(self): for fn in self.source.open_text(): yield Path(fn.rstrip()) @VisiData.api def inputShell(vd): cmd = vd.input("sh$ ", type="sh") if '$' not in cmd: vd.warning('no $column in command') return cmd globalCommand('', 'open-dir-current', 'vd.push(vd.currentDirSheet)', 'open Directory Sheet: browse properties of files in current directory') Sheet.addCommand('z;', 'addcol-sh', 'cmd=inputShell(); addShellColumns(cmd, sheet)', 'create new column from bash expression, with $columnNames as variables') DirSheet.addCommand(ENTER, 'open-row', 'vd.push(openSource(cursorRow or fail("no row"), filetype="dir" if cursorRow.is_dir() else LazyComputeRow(sheet, cursorRow).ext))', 'open current file as a new sheet') DirSheet.addCommand('g'+ENTER, 'open-rows', 'for r in selectedRows: vd.push(openSource(r))', 'open selected files as new sheets') DirSheet.addCommand('^O', 'sysopen-row', 'launchEditor(cursorRow)', 'open current file in external $EDITOR') DirSheet.addCommand('g^O', 'sysopen-rows', 'launchEditor(*selectedRows)', 'open selected files in external $EDITOR') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612075960.0 visidata-2.2.1/visidata/slide.py0000660000175000017500000001234600000000000017273 0ustar00kefalakefala00000000000000'''slide rows/columns around''' from visidata import Sheet, moveListItem, globalCommand, vd @Sheet.api def slide_col(sheet, colidx, newcolidx): vd.addUndo(moveVisibleCol, sheet, newcolidx, colidx) return moveVisibleCol(sheet, colidx, newcolidx) @Sheet.api def slide_keycol(sheet, fromKeyColIdx, toKeyColIdx): vd.addUndo(moveKeyCol, sheet, toKeyColIdx, fromKeyColIdx) return moveKeyCol(sheet, fromKeyColIdx, toKeyColIdx) @Sheet.api def slide_row(sheet, rowidx, newcolidx): vd.addUndo(moveListItem, sheet.rows, newcolidx, rowidx) return moveListItem(sheet.rows, rowidx, newcolidx) @Sheet.api def onClick(sheet, vcolidx, rowidx): pass @Sheet.api def onRelease(sheet, vcolidx, rowidx, destx, desty): newvcolidx = sheet.visibleColAtX(destx) newrowidx = sheet.visibleRowAtY(desty) if newvcolidx is not None and newvcolidx != vcolidx: sheet.cursorVisibleColIndex = sheet.slide_col(vcolidx, newvcolidx) # else: only move row if within same column (if column not moved above) elif newrowidx is not None and newrowidx != rowidx: sheet.cursorRowIndex = sheet.slide_row(rowidx, newrowidx) else: sheet.onClick(vcolidx, rowidx) def moveKeyCol(sheet, fromKeyColIdx, toKeyColIdx): 'Move key column to another key column position in sheet.' if not (1 <= toKeyColIdx <= len(sheet.keyCols)): vd.warning('already at edge') return fromKeyColIdx-1 for col in sheet.keyCols: if col.keycol == fromKeyColIdx: col.keycol = toKeyColIdx elif toKeyColIdx < fromKeyColIdx: # moving to the left if toKeyColIdx <= col.keycol < fromKeyColIdx: col.keycol += 1 else: # moving to the right if fromKeyColIdx < col.keycol <= toKeyColIdx: col.keycol -= 1 # key columns are 1-indexed; columns in general are 0-indexed return toKeyColIdx-1 def moveVisibleCol(sheet, fromVisColIdx, toVisColIdx): 'Move visible column to another visible index in sheet.' # a regular column cannot move to the left of keycols if 0 <= toVisColIdx < sheet.nVisibleCols: fromVisColIdx = min(max(fromVisColIdx, 0), sheet.nVisibleCols-1) fromColIdx = sheet.columns.index(sheet.visibleCols[fromVisColIdx]) if toVisColIdx < len(sheet.keyCols): vd.warning('already at edge') return fromVisColIdx else: toColIdx = sheet.columns.index(sheet.visibleCols[toVisColIdx]) moveListItem(sheet.columns, fromColIdx, toColIdx) return toVisColIdx else: vd.warning('already at edge') return fromVisColIdx Sheet.addCommand('H', 'slide-left', 'sheet.cursorVisibleColIndex = slide_col(cursorVisibleColIndex, cursorVisibleColIndex-1) if not cursorCol.keycol else slide_keycol(cursorCol.keycol, cursorCol.keycol-1)', 'slide current column left') Sheet.addCommand('L', 'slide-right', 'sheet.cursorVisibleColIndex = slide_col(cursorVisibleColIndex, cursorVisibleColIndex+1) if not cursorCol.keycol else slide_keycol(cursorCol.keycol, cursorCol.keycol+1)', 'slide current column right') Sheet.addCommand('J', 'slide-down', 'sheet.cursorRowIndex = slide_row(cursorRowIndex, cursorRowIndex+1)', 'slide current row down') Sheet.addCommand('K', 'slide-up', 'sheet.cursorRowIndex = slide_row(cursorRowIndex, cursorRowIndex-1)', 'slide current row up') Sheet.addCommand('gH', 'slide-leftmost', 'slide_col(cursorVisibleColIndex, len(keyCols) + 0) if not cursorCol.keycol else slide_keycol(cursorCol.keycol, 1)', 'slide current column all the way to the left of sheet') Sheet.addCommand('gL', 'slide-rightmost', 'slide_col(cursorVisibleColIndex, nVisibleCols-1) if not cursorCol.keycol else slide_keycol(cursorCol.keycol, len(keyCols))', 'slide current column all the way to the right of sheet') Sheet.addCommand('gJ', 'slide-bottom', 'slide_row(cursorRowIndex, nRows)', 'slide current row all the way to the bottom of sheet') Sheet.addCommand('gK', 'slide-top', 'slide_row(cursorRowIndex, 0)', 'slide current row to top of sheet') Sheet.addCommand('zH', 'slide-left-n', 'slide_col(cursorVisibleColIndex, cursorVisibleColIndex-int(input("slide col left n=", value=1)))', 'slide current column N positions to the left') Sheet.addCommand('zL', 'slide-right-n', 'slide_col(cursorVisibleColIndex, cursorVisibleColIndex+int(input("slide col left n=", value=1)))', 'slide current column N positions to the right') Sheet.addCommand('zJ', 'slide-down-n', 'slide_row(cursorRowIndex, cursorRowIndex+int(input("slide row down n=", value=1)))', 'slide current row N positions down') Sheet.addCommand('zK', 'slide-up-n', 'slide_row(cursorRowIndex, cursorRowIndex-int(input("slide row up n=", value=1)))', 'slide current row N positions up') Sheet.addCommand('BUTTON1_RELEASED','release-mouse','onRelease(cursorVisibleColIndex, cursorRowIndex, mouseX, mouseY)', 'slide current row/column to mouse cursor release position') Sheet.bindkey('KEY_SLEFT', 'slide-left') Sheet.bindkey('KEY_SR', 'slide-left') Sheet.bindkey('kDN', 'slide-down') Sheet.bindkey('kUP', 'slide-up') Sheet.bindkey('KEY_SRIGHT', 'slide-right') Sheet.bindkey('KEY_SF', 'slide-right') Sheet.bindkey('gKEY_SLEFT', 'slide-leftmost') Sheet.bindkey('gkDN', 'slide-bottom') Sheet.bindkey('gkUP', 'slide-top') Sheet.bindkey('gKEY_SRIGHT', 'slide-rightmost') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1609740788.0 visidata-2.2.1/visidata/sort.py0000660000175000017500000000614100000000000017156 0ustar00kefalakefala00000000000000from copy import copy from visidata import vd, asyncthread, Progress, Sheet, options, UNLOADED Sheet.init('_ordering', list, copy=True) # (col:Column, reverse:bool) @Sheet.api def orderBy(sheet, *cols, reverse=False): 'Add *cols* to internal ordering and re-sort the rows accordingly. Pass *reverse* as True to order these *cols* descending. Pass empty *cols* (or cols[0] of None) to clear internal ordering.' if options.undo: vd.addUndo(setattr, sheet, '_ordering', copy(sheet._ordering)) if sheet._ordering: vd.addUndo(sheet.sort) else: vd.addUndo(setattr, sheet, 'rows', copy(sheet.rows)) do_sort = False if not cols or cols[0] is None: sheet._ordering.clear() cols = cols[1:] do_sort = True for c in cols: sheet._ordering.append((c, reverse)) do_sort = True if do_sort: sheet.sort() class Reversor: def __init__(self, obj): self.obj = obj def __eq__(self, other): return other.obj == self.obj def __lt__(self, other): return other.obj < self.obj @Sheet.api def sortkey(self, r, prog=None): ret = [] for col, reverse in self._ordering: if isinstance(col, str): col = self.column(col) val = col.getTypedValue(r) ret.append(Reversor(val) if reverse else val) if prog: prog.addProgress(1) return ret @Sheet.api @asyncthread def sort(self): 'Sort rows according to the current internal ordering.' if self.rows is UNLOADED: return try: with Progress(gerund='sorting', total=self.nRows) as prog: # must not reassign self.rows: use .sort() instead of sorted() self.rows.sort(key=lambda r,self=self,prog=prog: self.sortkey(r, prog=prog)) except TypeError as e: vd.warning('sort incomplete due to TypeError; change column type') vd.exceptionCaught(e, status=False) # replace existing sort criteria Sheet.addCommand('[', 'sort-asc', 'orderBy(None, cursorCol)', 'sort ascending by current column; replace any existing sort criteria') Sheet.addCommand(']', 'sort-desc', 'orderBy(None, cursorCol, reverse=True)', 'sort descending by current column; replace any existing sort criteria ') Sheet.addCommand('g[', 'sort-keys-asc', 'orderBy(None, *keyCols)', 'sort ascending by all key columns; replace any existing sort criteria') Sheet.addCommand('g]', 'sort-keys-desc', 'orderBy(None, *keyCols, reverse=True)', 'sort descending by all key columns; replace any existing sort criteria') # add to existing sort criteria Sheet.addCommand('z[', 'sort-asc-add', 'orderBy(cursorCol)', 'sort ascending by current column; add to existing sort criteria') Sheet.addCommand('z]', 'sort-desc-add', 'orderBy(cursorCol, reverse=True)', 'sort descending by current column; add to existing sort criteria') Sheet.addCommand('gz[', 'sort-keys-asc-add', 'orderBy(*keyCols)', 'sort ascending by all key columns; add to existing sort criteria') Sheet.addCommand('gz]', 'sort-keys-desc-add', 'orderBy(*keyCols, reverse=True)', 'sort descending by all key columns; add to existing sort criteria') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608541899.0 visidata-2.2.1/visidata/statusbar.py0000660000175000017500000001675400000000000020212 0ustar00kefalakefala00000000000000import collections import curses from visidata import vd, VisiData, BaseSheet, Sheet, ColumnItem, Column, RowColorizer, options, colors, wrmap, clipdraw, ExpectedException, update_attr, theme, MissingAttrFormatter __all__ = ['StatusSheet', 'status', 'error', 'fail', 'warning', 'debug'] theme('disp_rstatus_fmt', ' {sheet.longname} {sheet.nRows:9d} {sheet.rowtype} ', 'right-side status format string') theme('disp_status_fmt', '{sheet.shortcut}› {sheet.name}| ', 'status line prefix') theme('disp_lstatus_max', 0, 'maximum length of left status line') theme('disp_status_sep', ' | ', 'separator between statuses') theme('color_keystrokes', 'white', 'color of input keystrokes on status line') theme('color_status', 'bold', 'status line color') theme('color_error', 'red', 'error message color') theme('color_warning', 'yellow', 'warning message color') theme('color_top_status', 'underline', 'top window status bar color') theme('color_active_status', 'bold', ' active window status bar color') theme('color_inactive_status', '8', 'inactive window status bar color') BaseSheet.init('longname', lambda: '') vd.beforeExecHooks.append(lambda sheet, cmd, args, ks: setattr(sheet, 'longname', cmd.longname)) @VisiData.lazy_property def statuses(vd): return collections.OrderedDict() # (priority, statusmsg) -> num_repeats; shown until next action @VisiData.lazy_property def statusHistory(vd): return list() # list of [priority, statusmsg, repeats] for all status messages ever @VisiData.global_api def status(self, *args, priority=0): 'Display *args* on status until next action.' if not args: return True k = (priority, tuple(map(str, args))) self.statuses[k] = self.statuses.get(k, 0) + 1 if self.statusHistory: prevpri, prevargs, prevn = self.statusHistory[-1] if prevpri == priority and prevargs == args: self.statusHistory[-1][2] += 1 return True self.statusHistory.append([priority, args, 1]) return True @VisiData.global_api def error(vd, *args): 'Abort with ExpectedException, and display *args* on status as an error.' vd.status(*args, priority=3) raise ExpectedException(args[0] if args else '') @VisiData.global_api def fail(vd, *args): 'Abort with ExpectedException, and display *args* on status as a warning.' vd.status(*args, priority=2) raise ExpectedException(args[0] if args else '') @VisiData.global_api def warning(vd, *args): 'Display *args* on status as a warning.' vd.status(*args, priority=1) @VisiData.global_api def debug(vd, *args, **kwargs): 'Display *args* on status if options.debug is set.' if options.debug: return vd.status(*args, **kwargs) def middleTruncate(s, w): if len(s) <= w: return s return s[:w] + options.disp_truncator + s[-w:] def composeStatus(msgparts, n): msg = '; '.join(wrmap(str, msgparts)) if n > 1: msg = '[%sx] %s' % (n, msg) return msg @BaseSheet.api def leftStatus(sheet): 'Return left side of status bar for this sheet. Overridable.' return options.disp_status_fmt.format(sheet=sheet, vd=vd) @VisiData.api def drawLeftStatus(vd, scr, vs): 'Draw left side of status bar.' cattr = colors.get_color('color_status') active = (vs is vd.sheets[0]) if vd.sheets else False # active sheet if active: cattr = update_attr(cattr, colors.color_active_status, 0) else: cattr = update_attr(cattr, colors.color_inactive_status, 0) if scr is vd.winTop: cattr = update_attr(cattr, colors.color_top_status, 1) attr = cattr.attr error_attr = update_attr(cattr, colors.color_error, 1).attr warn_attr = update_attr(cattr, colors.color_warning, 2).attr sep = options.disp_status_sep x = 0 y = vs.windowHeight-1 # status for each window try: lstatus = vs.leftStatus() maxwidth = options.disp_lstatus_max if maxwidth > 0: lstatus = middleTruncate(lstatus, maxwidth//2) x = clipdraw(scr, y, 0, lstatus, attr, w=vs.windowWidth-1) vd.onMouse(scr, y, 0, 1, x, BUTTON1_PRESSED='sheets', BUTTON3_PRESSED='rename-sheet', BUTTON3_CLICKED='rename-sheet') except Exception as e: vd.exceptionCaught(e) if not active: return one = False for (pri, msgparts), n in sorted(vd.statuses.items(), key=lambda k: -k[0][0]): try: if x > vs.windowWidth: break if one: # any messages already: x += clipdraw(scr, y, x, sep, attr, w=vs.windowWidth-x) one = True msg = composeStatus(msgparts, n) if pri == 3: msgattr = error_attr elif pri == 2: msgattr = warn_attr elif pri == 1: msgattr = warn_attr else: msgattr = attr x += clipdraw(scr, y, x, msg, msgattr, w=vs.windowWidth-x) except Exception as e: vd.exceptionCaught(e) @VisiData.api def rightStatus(vd, sheet): 'Return right side of status bar. Overrideable.' return MissingAttrFormatter().format(sheet.options.disp_rstatus_fmt, sheet=sheet, vd=vd) @VisiData.api def drawRightStatus(vd, scr, vs): 'Draw right side of status bar. Return length displayed.' rightx = vs.windowWidth ret = 0 statcolors = [ (vd.rightStatus(vs), 'color_status'), ] active = vs is vd.activeSheet if active: statcolors.append((vd.keystrokes or '', 'color_keystrokes')) if vs.currentThreads: statcolors.insert(0, vd.checkMemoryUsage()) if vs.progresses: gerund = vs.progresses[0].gerund else: gerund = 'processing' statcolors.insert(1, (' %s %s…' % (vs.progressPct, gerund), 'color_working')) if active and vd.currentReplay: statcolors.insert(0, (vd.replayStatus, 'color_status_replay')) for rstatcolor in statcolors: if rstatcolor: try: rstatus, coloropt = rstatcolor rstatus = ' '+rstatus cattr = colors.get_color(coloropt) if scr is vd.winTop: cattr = update_attr(cattr, colors.color_top_status, 0) if active: cattr = update_attr(cattr, colors.color_active_status, 0) else: cattr = update_attr(cattr, colors.color_inactive_status, 0) statuslen = clipdraw(scr, vs.windowHeight-1, rightx, rstatus, cattr.attr, w=vs.windowWidth-1, rtl=True) rightx -= statuslen ret += statuslen except Exception as e: vd.exceptionCaught(e) if scr: curses.doupdate() return ret class StatusSheet(Sheet): precious = False rowtype = 'statuses' # rowdef: (priority, args, nrepeats) columns = [ ColumnItem('priority', 0, type=int, width=0), ColumnItem('nrepeats', 2, type=int, width=0), ColumnItem('args', 1, width=0), Column('message', getter=lambda col,row: composeStatus(row[1], row[2])), ] colorizers = [ RowColorizer(1, 'color_error', lambda s,c,r,v: r and r[0] == 3), RowColorizer(1, 'color_warning', lambda s,c,r,v: r and r[0] in [1,2]), ] def reload(self): self.rows = self.source @VisiData.property def statusHistorySheet(vd): return StatusSheet("status_history", source=vd.statusHistory[::-1]) # in reverse order BaseSheet.addCommand('^P', 'open-statuses', 'vd.push(vd.statusHistorySheet)', 'open Status History') ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1612763830.9979815 visidata-2.2.1/visidata/tests/0000770000175000017500000000000000000000000016754 5ustar00kefalakefala00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1550939336.0 visidata-2.2.1/visidata/tests/__init__.py0000660000175000017500000000000000000000000021054 0ustar00kefalakefala00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1602554535.0 visidata-2.2.1/visidata/tests/conftest.py0000660000175000017500000000075100000000000021157 0ustar00kefalakefala00000000000000import pytest from unittest.mock import Mock @pytest.fixture(scope="class") def curses_setup(): """Perform some curses prepwork""" import curses import visidata curses.curs_set = lambda v: None visidata.options.confirm_overwrite = False @pytest.fixture(scope="function") def mock_screen(): """Set up and return a mock curses screen object.""" scr = Mock() scr.addstr = Mock() scr.move = Mock() scr.getmaxyx = lambda: (25, 80) return scr ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1602554535.0 visidata-2.2.1/visidata/tests/test_commands.py0000660000175000017500000000655000000000000022175 0ustar00kefalakefala00000000000000import pkg_resources import pytest from unittest.mock import Mock import itertools import visidata # test separately as needed inputLines = { 'save-sheet': 'tests/jetsam.csv', # save to some tmp file 'open-file': 'tests/jetsam.csv', # reopen what was just saved ('o' must come after ^S in the commands list) 'save-col': 'tests/flotsam.csv', 'pyobj-expr': '2+2', # open the python object for '4' 'search-col': 'foo', 'searchr-col': 'bar', 'select-col-regex': '.', 'select-cols-regex': '.', 'unselect-col-regex': '.', 'unselect-cols-regex': '.', # 'e': '', # no change should not error 'go-col-regex': 'Unit', # column name in sample 'go-col-number': '2', 'go-row-number': '5', # go to row 5 'addcol-bulk': '1', 'addcol-expr': 'Unit', # just copy the column 'split-col': '-', 'show-expr': 'OrderDate', 'setcol-expr': 'OrderDate', 'setcell-expr': 'OrderDate', 'setcol-range': 'range(100)', 'capture-col': '(.)(.*)', 'addcol-subst': r'Unit/(\w)/\1', # the first character 'search-cols': 'foo', 'searchr-cols': 'bar', 'select-cols-regex': '.', 'unselect-cols-regex': '.', 'random-rows': '3', } @pytest.mark.usefixtures('curses_setup') class TestCommands: def test_baseCommands(self, mock_screen): 'exec each global command at least once' cmdlist = visidata.vd.commands vs = visidata.Sheet('test_commands') vs.reload() vd = visidata.vd nerrs = 0 ntotal = 0 for longname in cmdlist.keys(): if 'Sheet' not in cmdlist[longname]: continue ntotal += 1 print(longname) self.runOneTest(mock_screen, longname) vd.sync() if vd.lastErrors: # longname, execstr, and vd.lastErrors print("{0} FAILED: {1}\n\n\n {2}".format(longname, cmdlist[longname]['Sheet'].execstr, '\n'.join('\n'.join(x) for x in vd.lastErrors))) nerrs += 1 break vs.checkCursor() print('%s/%s commands had errors' % (nerrs, ntotal)) def runOneTest(self, mock_screen, longname): visidata.vd.clearCaches() # we want vd to return a new VisiData object for each command vd = visidata.vd vd.scr = mock_screen if longname in inputLines: line = [ch for ch in inputLines[longname]] + ['^J'] vd.getkeystroke = Mock(side_effect=line) else: vd.getkeystroke = Mock(side_effect=['^J']) sample_file = pkg_resources.resource_filename('visidata', '../sample_data/sample.tsv') vs = visidata.TsvSheet('test_commands', source=visidata.Path(sample_file)) vs.reload.__wrapped__(vs) vs.vd = vd vd.sheets = [vs] vs.mouseX, vs.mouseY = (4, 4) vs.draw(mock_screen) vs.execCommand(longname, vdglobals=vars(visidata)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1602554535.0 visidata-2.2.1/visidata/tests/test_edittext.py0000660000175000017500000000352600000000000022226 0ustar00kefalakefala00000000000000import pytest from unittest.mock import Mock, patch import visidata class TestEditText: @pytest.fixture(autouse=True, scope='function') def setUp(self): self.chars = [] visidata.vd.getkeystroke = Mock(side_effect=self.chars) @pytest.mark.parametrize('keys, result, kwargs', [ ('^J', '', {}), ('a b KEY_HOME c d ^A e f ^J', 'efcdab', {}), ('a b KEY_LEFT 1 KEY_LEFT KEY_LEFT KEY_LEFT 2 ^J', '2a1b', {}), # Left, past home ('a b ^C', None, dict(exception=visidata.EscapeException)), ('a b ^[', None, dict(exception=visidata.EscapeException)), ('a KEY_DC ^J', 'a', {}), ('a b KEY_LEFT KEY_DC ^J', 'a', {}), ('a b KEY_LEFT c KEY_END d ^J', 'acbd', {}), ('a b KEY_HOME KEY_RIGHT c ^J', 'acb', {}), ('a b KEY_BACKSPACE c ^J', 'ac', {}), # Backspace no longer deletes the first character at the start ('a b KEY_HOME KEY_BACKSPACE c ^J', 'cab', {}), # Backspace works in different combos, including on the mac. ('a b c KEY_BACKSPACE ^H KEY_LEFT KEY_DC ^J', '', {}), ('a b c ^B ^B ^K ^J', 'a', {}), ('a ^R ^J', '', {}), ('a ^R ^J', 'foo', dict(value='foo')), # Two characters swaps characters ('a b ^T ^J', 'ba', {}), # Home with multiple characters acts like delete ('a b KEY_HOME ^T ^J', 'b', {}), ('a b KEY_LEFT ^U ^J', 'b', {}), ('a b ^U c ^J', 'c', {}), ]) def test_keys(self, mock_screen, keys, result, kwargs): self.chars.extend(keys.split()) exception = kwargs.pop('exception', None) if exception: with pytest.raises(exception): visidata.vd.editline(mock_screen, 0, 0, 0, **kwargs) else: r = visidata.vd.editline(mock_screen, 0, 0, 0, **kwargs) assert r == result ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1602554535.0 visidata-2.2.1/visidata/tests/test_path.py0000660000175000017500000000162100000000000021322 0ustar00kefalakefala00000000000000import pytest import visidata class TestVisidataPath: def test_withName(self): 'tests for visidata.Path().with_name' file_path = visidata.Path('sample_data/sample.tsv') url_path = visidata.Path('https://visidata.org/hello/sample.tsv') assert 'sample_data/b.tsv' == str(file_path.with_name('b.tsv')), '{} should be sample_data/b.tsv'.format(file_path.with_name('b.tsv')) assert 'sample_data/a/b.tsv' == str(file_path.with_name('a/b.tsv')), '{} should be sample_data/a/b.tsv'.format(file_path.with_name('a/b.tsv')) assert "https://visidata.org/hello/b.tsv" == str(url_path.with_name('b.tsv')), '{} should be https://visidata.org/hello/b.tsv'.format(url_path.with_name('b.tsv')) assert "https://visidata.org/hello/a/b.tsv" == str(url_path.with_name('a/b.tsv')), '{} should be https://visidata.org/hello/a/b.tsv'.format(url_path.with_name('a/b.tsv')) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608178530.0 visidata-2.2.1/visidata/textsheet.py0000660000175000017500000000655700000000000020217 0ustar00kefalakefala00000000000000import textwrap from visidata import vd, option, options, Sheet, ColumnItem, asyncthread from visidata import Column, ColumnItem, vlen from visidata import globalCommand, error, stacktrace, VisiData __all__ = ['TextSheet', 'ErrorSheet'] option('wrap', False, 'wrap text to fit window width on TextSheet') option('save_filetype', 'tsv', 'specify default file type to save as', replay=True) ## text viewer # rowdef: (linenum, str) class TextSheet(Sheet): 'Displays any iterable source, with linewrap if ``options.wrap`` is set.' rowtype = 'lines' filetype = 'txt' columns = [ ColumnItem('linenum', 0, type=int, width=0), ColumnItem('text', 1), ] def iterload(self): winWidth = min(self.columns[1].width or 78, self.windowWidth-2) wrap = options.wrap for startingLine, text in enumerate(self.source): if wrap and text: for i, L in enumerate(textwrap.wrap(str(text), width=winWidth)): yield [startingLine+i+1, L] else: yield [startingLine+1, text] def sysopen(sheet, linenum=0): @asyncthread def writelines(sheet, fn): with open(fn, 'w') as fp: for row in sheet.rows: fp.write(row[1]) fp.write('\n') @asyncthread def readlines(sheet, fn): sheet.rows = [] with open(fn, 'r') as fp: try: for i, line in enumerate(fp): sheet.addRow((i, line)) except Exception as e: vd.exceptionCaught(e) return '' import tempfile with tempfile.NamedTemporaryFile() as temp: writelines(sheet, temp.name) vd.launchEditor(temp.name, '+%s' % linenum) readlines(sheet, temp.name) # .source is list of source text lines to 'load' # .sourceSheet is Sheet error came from class ErrorSheet(TextSheet): precious = False class ErrorsSheet(Sheet): columns = [ Column('nlines', type=vlen), ColumnItem('lastline', -1) ] def reload(self): self.rows = self.source def openRow(self, row): return ErrorSheet(source=self.cursorRow) @VisiData.property def allErrorsSheet(self): return ErrorsSheet("errors_all", source=vd.lastErrors) @VisiData.property def recentErrorsSheet(self): error = vd.lastErrors[-1] if vd.lastErrors else '' return ErrorSheet("errors_recent", source=error) globalCommand('^E', 'error-recent', 'vd.lastErrors and vd.push(recentErrorsSheet) or status("no error")', 'view traceback for most recent error') globalCommand('g^E', 'errors-all', 'vd.push(vd.allErrorsSheet)', 'view traceback for most recent errors') Sheet.addCommand(None, 'view-cell', 'vd.push(ErrorSheet("%s[%s].%s" % (name, cursorRowIndex, cursorCol.name), sourceSheet=sheet, source=cursorDisplay.splitlines()))', 'view contents of current cell in a new sheet'), Sheet.addCommand('z^E', 'error-cell', 'vd.push(ErrorSheet(sheet.name+"_cell_error", sourceSheet=sheet, source=getattr(cursorCell, "error", None) or fail("no error this cell")))', 'view traceback for error in current cell') TextSheet.addCommand('^O', 'sysopen-sheet', 'sheet.sysopen(sheet.cursorRowIndex)', 'open copy of text sheet in $EDITOR and reload on exit') TextSheet.class_options.save_filetype = 'txt' ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1609371779.0 visidata-2.2.1/visidata/threads.py0000660000175000017500000003303100000000000017617 0ustar00kefalakefala00000000000000import ctypes import threading import os.path import functools import cProfile import threading import collections from visidata import VisiData, vd, option, options, globalCommand, Sheet, EscapeException from visidata import ColumnAttr, Column from visidata import * __all__ = ['Progress', 'asynccache', 'async_deepcopy', 'elapsed_s', 'cancelThread', 'ThreadsSheet', 'ProfileSheet', 'codestr', 'asyncsingle'] option('profile', '', 'filename to save binary profiling data') option('min_memory_mb', 0, 'minimum memory to continue loading and async processing') theme('color_working', 'green', 'color of system running smoothly') BaseSheet.init('currentThreads', list) def asynccache(key=lambda *args, **kwargs: str(args)+str(kwargs)): def _decorator(func): 'Function decorator, so first call to `func()` spawns a separate thread. Calls return the Thread until the wrapped function returns; subsequent calls return the cached return value.' d = {} # per decoration cache def _func(k, *args, **kwargs): d[k] = func(*args, **kwargs) @functools.wraps(func) def _execAsync(*args, **kwargs): k = key(*args, **kwargs) if k not in d: d[k] = vd.execAsync(_func, k, *args, **kwargs) return d.get(k) return _execAsync return _decorator class _Progress: def __init__(self, iterable=None, gerund="", total=None, sheet=None): self.iterable = iterable if total is None: if iterable is not None: self.total = len(iterable) else: self.total = 0 else: self.total = total self.sheet = sheet if sheet else getattr(threading.current_thread(), 'sheet', None) self.gerund = gerund self.made = 0 def __enter__(self): if self.sheet: self.sheet.progresses.append(self) return self def addProgress(self, n): 'Increase the progress count by *n*.' self.made += n return True def __exit__(self, exc_type, exc_val, tb): if self.sheet: self.sheet.progresses.remove(self) def __iter__(self): with self as prog: for item in self.iterable: yield item self.made += 1 @VisiData.global_api def Progress(vd, iterable=None, gerund="", total=None, sheet=None): '''Maintain progress count as either an iterable wrapper, or a context manager. - *iterable*: wrapped iterable if used as an iterator. - *gerund*: status text shown while this Progress is active. - *total*: total count expected. - *sheet*: specific sheet to associate this progress with. Default is sheet from current thread. ''' return _Progress(iterable=iterable, gerund=gerund, total=total, sheet=sheet) @asyncthread def _async_deepcopy(vs, newlist, oldlist): for r in Progress(oldlist, 'copying'): newlist.append(deepcopy(r)) def async_deepcopy(vs, rowlist): ret = [] _async_deepcopy(vs, ret, rowlist) return ret @VisiData.global_api def cancelThread(vd, *threads, exception=EscapeException): 'Raise *exception* in one or more *threads*.' for t in threads: ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(t.ident), ctypes.py_object(exception)) # each row is an augmented threading.Thread object class ThreadsSheet(Sheet): rowtype = 'threads' precious = False columns = [ ColumnAttr('name'), Column('process_time', type=float, getter=lambda col,row: elapsed_s(row)), ColumnAttr('profile'), ColumnAttr('status'), ColumnAttr('exception'), ] def reload(self): self.rows = vd.threads def openRow(self, row): 'push profile sheet for this action' if row.profile: return ProfileSheet(row.name+"_profile", source=row.profile) vd.warning("no profile") def elapsed_s(t): return (t.endTime or time.process_time())-t.startTime @VisiData.api def checkMemoryUsage(vd): min_mem = options.min_memory_mb threads = vd.unfinishedThreads if not threads: return None ret = '' attr = 'color_working' if min_mem: try: freestats = subprocess.run('free --total --mega'.split(), check=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE).stdout.strip().splitlines() except FileNotFoundError as e: if options.debug: vd.exceptionCaught(e) options.min_memory_mb = 0 vd.warning('disabling min_memory_mb: "free" not installed') return '', attr tot_m, used_m, free_m = map(int, freestats[-1].split()[1:]) ret = '[%dMB] ' % free_m + ret if free_m < min_mem: attr = 'color_warning' vd.warning('%dMB free < %dMB minimum, stopping threads' % (free_m, min_mem)) cancelThread(*vd.unfinishedThreads) curses.flash() return ret, attr # for progress bar BaseSheet.init('progresses', list) # list of Progress objects @BaseSheet.property def progressMade(self): return sum(prog.made for prog in self.progresses) @BaseSheet.property def progressTotal(self): return sum(prog.total for prog in self.progresses) @BaseSheet.property def progressPct(sheet): 'Percent complete string as indicated by async actions.' if sheet.progresses and sheet.progressTotal > 0: return '%2d%%' % int(sheet.progressMade*100//sheet.progressTotal) return '' ## threads def _annotate_thread(t, endTime=None): t.startTime = time.process_time() t.endTime = endTime # endTime is None means unfinished. endTime=0 for main thread t.status = '' t.profile = None t.exception = None return t # all long-running threads, including main and finished VisiData.init('threads', lambda: [_annotate_thread(threading.current_thread(), 0)]) @VisiData.lazy_property def threadsSheet(self): return ThreadsSheet('threads') @VisiData.api def execAsync(self, func, *args, sheet=None, **kwargs): 'Execute ``func(*args, **kwargs)`` in a separate thread.' thread = threading.Thread(target=_toplevelTryFunc, daemon=True, args=(func,)+args, kwargs=kwargs) self.threads.append(_annotate_thread(thread)) if sheet is None and self.sheets: sheet = self.sheets[0] if sheet is not None: sheet.currentThreads.append(thread) thread.sheet = sheet thread.start() return thread def _toplevelTryFunc(func, *args, status=status, **kwargs): with ThreadProfiler(threading.current_thread()) as prof: t = threading.current_thread() t.name = func.__name__ try: t.status = func(*args, **kwargs) except EscapeException as e: # user aborted t.status = 'aborted by user' if status: status('%s aborted' % t.name, priority=2) except Exception as e: t.exception = e t.status = 'exception' vd.exceptionCaught(e) if t.sheet: t.sheet.currentThreads.remove(t) def asyncsingle(func): '''Function decorator like `@asyncthread` but as a singleton. When called, `func(...)` spawns a new thread, and cancels any previous thread still running *func*. ``vd.sync()`` does not wait for unfinished asyncsingle threads. ''' @functools.wraps(func) def _execAsync(*args, **kwargs): def _func(*args, **kwargs): func(*args, **kwargs) _execAsync.searchThread = None # end of thread # cancel previous thread if running if _execAsync.searchThread: cancelThread(_execAsync.searchThread) _func.__name__ = func.__name__ # otherwise, the the thread's name is '_func' _execAsync.searchThread = vd.execAsync(_func, *args, **kwargs) _execAsync.searchThread.noblock = True _execAsync.searchThread = None return _execAsync @VisiData.property def unfinishedThreads(self): 'A list of unfinished threads (those without a recorded `endTime`).' return [t for t in self.threads if getattr(t, 'endTime', None) is None] @VisiData.api def checkForFinishedThreads(self): 'Mark terminated threads with endTime.' for t in self.unfinishedThreads: if not t.is_alive(): t.endTime = time.process_time() if getattr(t, 'status', None) is None: t.status = 'ended' @VisiData.api def sync(self, *joiningThreads): 'Wait for one or more *joiningThreads* to finish. If no *joiningThreads* specified, wait for all but current thread and interface thread to finish.' joiningThreads = set(joiningThreads) while True: deads = set() # dead threads threads = joiningThreads or set(self.unfinishedThreads) threads -= set([threading.current_thread(), getattr(vd, 'drawThread', None)]) threads -= deads for t in threads: try: if not t.is_alive() or getattr(t, 'noblock', False) is True: deads.add(t) else: t.join() except RuntimeError as e: # maybe thread hasn't started yet or has already joined vd.exceptionCaught(e) pass if len(threads - deads) == 0: break min_thread_time_s = 0.10 # only keep threads that take longer than this number of seconds def open_pyprof(p): return ProfileSheet(p.name, p.open_bytes()) @VisiData.api def toggleProfiling(vd, t): if not t.profile: t.profile = cProfile.Profile() t.profile.enable() if not options.profile: options.set('profile', 'vdprofile') else: t.profile.disable() t.profile = None options.set('profile', '') vd.status('profiling ' + ('ON' if t.profile else 'OFF')) class ThreadProfiler: numProfiles = 0 def __init__(self, thread): self.thread = thread if options.profile: self.thread.profile = cProfile.Profile() else: self.thread.profile = None ThreadProfiler.numProfiles += 1 self.profileNumber = ThreadProfiler.numProfiles def __enter__(self): if self.thread.profile: self.thread.profile.enable() return self def __exit__(self, exc_type, exc_val, tb): if self.thread.profile: self.thread.profile.disable() self.thread.profile.dump_stats(options.profile + str(self.profileNumber)) if exc_val: self.thread.exception = exc_val else: # remove very-short-lived async actions if elapsed_s(self.thread) < min_thread_time_s: vd.threads.remove(self.thread) class ProfileSheet(Sheet): columns = [ Column('funcname', getter=lambda col,row: codestr(row.code)), Column('filename', getter=lambda col,row: os.path.split(row.code.co_filename)[-1] if not isinstance(row.code, str) else ''), Column('linenum', type=int, getter=lambda col,row: row.code.co_firstlineno if not isinstance(row.code, str) else None), Column('inlinetime_us', type=int, getter=lambda col,row: row.inlinetime*1000000), Column('totaltime_us', type=int, getter=lambda col,row: row.totaltime*1000000), ColumnAttr('callcount', type=int), Column('avg_inline_us', type=int, getter=lambda col,row: row.inlinetime*1000000/row.callcount), Column('avg_total_us', type=int, getter=lambda col,row: row.totaltime*1000000/row.callcount), ColumnAttr('reccallcount', type=int), ColumnAttr('calls'), Column('callers', getter=lambda col,row: col.sheet.callers[row.code]), ] nKeys=3 def reload(self): self.rows = self.source.getstats() self.orderBy(None, self.column('inlinetime_us'), reverse=True) self.callers = collections.defaultdict(list) # [row.code] -> list(code) for r in self.rows: calls = getattr(r, 'calls', None) if calls: for callee in calls: self.callers[callee.code].append(r) def openRow(self, row): 'open ProfileSheet for calls referenced in current row' if row.calls: return ProfileSheet(codestr(row.code)+"_calls", source=row.calls) vd.warning("no calls") def openCell(self, col, row): 'open ProfileSheet for caller referenced in current cell' val = col.getValue(row) if val: return ProfileSheet(codestr(row.code)+"_"+col.name, source=val) vd.warning("no callers") def codestr(code): if isinstance(code, str): return code return code.co_name ThreadsSheet.addCommand('^C', 'cancel-thread', 'cancelThread(cursorRow)', 'abort thread at current row') ThreadsSheet.addCommand(None, 'add-row', 'fail("cannot add new rows on Threads Sheet")', 'invalid command') ProfileSheet.addCommand('z^S', 'save-profile', 'source.dump_stats(input("save profile to: ", value=name+".prof"))', 'save profile') ProfileSheet.addCommand('^O', 'sysopen-row', 'launchEditor(cursorRow.code.co_filename, "+%s" % cursorRow.code.co_firstlineno)', 'open current file at referenced row in external $EDITOR') globalCommand('^_', 'toggle-profile', 'toggleProfiling(threading.current_thread())', 'turn profiling on for main process') BaseSheet.addCommand('^C', 'cancel-sheet', 'cancelThread(*sheet.currentThreads or fail("no active threads on this sheet"))', 'abort all threads on current sheet') globalCommand('g^C', 'cancel-all', 'liveThreads=list(t for vs in vd.sheets for t in vs.currentThreads); cancelThread(*liveThreads); status("canceled %s threads" % len(liveThreads))', 'abort all secondary threads') globalCommand('^T', 'threads-all', 'vd.push(vd.threadsSheet)', 'open Threads Sheet') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1606688365.0 visidata-2.2.1/visidata/transpose.py0000660000175000017500000000157000000000000020206 0ustar00kefalakefala00000000000000from visidata import * # rowdef: Column class TransposeSheet(Sheet): @asyncthread def reload(self): # key rows become column names col = Column('_'.join(c.name for c in self.source.keyCols), getter=lambda c,origcol: origcol.name) # associate column with sheet col.recalc(self) self.columns = [col] self.setKeys(self.columns) # rows become columns for row in Progress(self.source.rows, 'transposing'): self.addColumn(Column('_'.join(map(str, self.source.rowkey(row))), getter=lambda c,origcol,row=row: origcol.getValue(row))) # columns become rows self.rows = list(self.source.nonKeyVisibleCols) Sheet.addCommand('T', 'transpose', 'vd.push(TransposeSheet(name+"_T", source=sheet))', 'open new sheet with rows and columns transposed') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608178530.0 visidata-2.2.1/visidata/undo.py0000660000175000017500000000706000000000000017135 0ustar00kefalakefala00000000000000import itertools from copy import copy from visidata import vd, options, VisiData, BaseSheet, option, UNLOADED BaseSheet.init('undone', list) # list of CommandLogRow for redo after undo option('undo', True, 'enable undo/redo') @VisiData.api def addUndo(vd, undofunc, *args, **kwargs): 'On undo of latest command, call ``undofunc(*args, **kwargs)``.' if options.undo: # occurs when VisiData is just starting up if getattr(vd, 'activeCommand', UNLOADED) is UNLOADED: return r = vd.modifyCommand # some special commands, like open-file, do not have an undofuncs set if not r or not isinstance(r.undofuncs, list): return r.undofuncs.append((undofunc, args, kwargs)) @VisiData.api def undo(vd, sheet): if not options.undo: vd.fail("options.undo not enabled") # don't allow undo of first command on a sheet, which is always the command that created the sheet. for cmdlogrow in sheet.cmdlog_sheet.rows[:0:-1]: if cmdlogrow.undofuncs: for undofunc, args, kwargs, in cmdlogrow.undofuncs: undofunc(*args, **kwargs) sheet.undone.append(cmdlogrow) sheet.cmdlog_sheet.rows.remove(cmdlogrow) vd.clearCaches() # undofunc can invalidate the drawcache vd.moveToReplayContext(cmdlogrow, sheet) vd.status("%s undone" % cmdlogrow.longname) return vd.fail("nothing to undo on current sheet") @VisiData.api def redo(vd, sheet): sheet.undone or vd.fail("nothing to redo") cmdlogrow = sheet.undone.pop() vd.replayOne(cmdlogrow) vd.status("%s redone" % cmdlogrow.longname) # undoers def undoAttrFunc(objs, attrname): 'Return closure that sets attrname on each obj to its former value.' oldvals = [(o, getattr(o, attrname)) for o in objs] def _undofunc(): for o, v in oldvals: setattr(o, attrname, v) return _undofunc class Fanout(list): 'Fan out attribute changes to every element in a list.' def __getattr__(self, k): return Fanout([getattr(o, k) for o in self]) def __setattr__(self, k, v): vd.addUndo(undoAttrFunc(self, k)) for o in self: setattr(o, k, v) def __call__(self, *args, **kwargs): return Fanout([o(*args, **kwargs) for o in self]) def undoAttrCopyFunc(objs, attrname): 'Return closure that sets attrname on each obj to its former value.' oldvals = [(o, copy(getattr(o, attrname))) for o in objs] def _undofunc(): for o, v in oldvals: setattr(o, attrname, v) return _undofunc @VisiData.api def addUndoSetValues(vd, cols, rows): 'Add undo function to reset values for *rows* in *cols*.' oldvals = [(c, r, c.getValue(r)) for c,r in itertools.product(cols, vd.Progress(rows, gerund='doing'))] def _undo(): for c, r, v in oldvals: c.setValue(r, v) vd.addUndo(_undo) @VisiData.api def addUndoColNames(vd, cols): oldnames = [(c, c.name) for c in cols] def _undo(): for c, name in oldnames: c.name = name vd.addUndo(_undo) @VisiData.api def addUndoReload(vd, rows, cols): oldrows = rows oldcolumns = cols def _undo(): sheet = oldcolumns[0].sheet sheet.rows = oldrows sheet.columns = oldcolumns vd.addUndo(_undo) BaseSheet.addCommand('U', 'undo-last', 'vd.undo(sheet)', 'undo the most recent modification (requires enabled options.undo)') BaseSheet.addCommand('R', 'redo-last', 'vd.redo(sheet)', 'redo the most recent undo (requires enabled options.undo)') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1608178530.0 visidata-2.2.1/visidata/unfurl.py0000660000175000017500000000407500000000000017506 0ustar00kefalakefala00000000000000'''This adds the `unfurl-col` command, to unfurl a column containing iterable values, such as lists, dicts, and strings. Unfurling pushes a new sheet, with each key/value pair in the unfurled column values getting its own row, with the rest of the source sheet's columns copied for each of those rows. Note: When unfurling a column, non-iterable objects (such as integers) are treated as single-item lists, so that they too can be unfurled. Credit to Jeremy Singer-Vine for the idea and original implementation. ''' from collections.abc import Iterable, Mapping from visidata import vd, Progress, Sheet, Column, ColumnItem, SettableColumn, SubColumnFunc, asyncthread, clean_to_id class UnfurledSheet(Sheet): @asyncthread def reload(self): # Copy over base sheet, using SubColumnFunc self.columns = [] for col in self.source.columns: if col is self.source_col: # Replace iterable column with two columns: keys and values self.cursorVisibleColIndex = len(self.columns)-1 self.addColumn(ColumnItem(col.name + "_key", 1)) self.addColumn(ColumnItem(col.name + "_value", 2)) else: self.addColumn(SubColumnFunc(col.name, col, 0, keycol=col.keycol)) self.rows = [] for row in Progress(self.source.rows): val = self.source_col.getValue(row) if not isinstance(val, Iterable) or isinstance(val, str): val = [ val ] if isinstance(val, Mapping): gen = val.items() else: gen = enumerate(val) for key, sub_value in gen: new_row = [ row, key, sub_value ] self.addRow(new_row) @Sheet.api def unfurl_col(sheet, col): clean_id = clean_to_id(col.name) vs = UnfurledSheet(f"{sheet.name}_{clean_id}_unfurled", source=sheet, source_col=col) return vs Sheet.addCommand("zM", "unfurl-col", "vd.push(unfurl_col(cursorCol))", "row-wise expand current column of lists (e.g. [2]) or dicts (e.g. {3}) within that column") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1606688365.0 visidata-2.2.1/visidata/urlcache.py0000660000175000017500000000164400000000000017760 0ustar00kefalakefala00000000000000import os import os.path import time from urllib.request import Request, urlopen import urllib.parse from visidata import Path, options, modtime def urlcache(url, days=1, text=True, headers={}): 'Return Path object to local cache of url contents.' p = Path(os.path.join(options.visidata_dir, 'cache', urllib.parse.quote(url, safe=''))) if p.exists(): secs = time.time() - modtime(p) if secs < days*24*60*60: return p if not p.parent.exists(): os.makedirs(p.parent, exist_ok=True) 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_text(mode='w') as fpout: fpout.write(ret) else: with p.open_bytes(mode='w') as fpout: fpout.write(ret) return p ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612761678.0 visidata-2.2.1/visidata/utils.py0000660000175000017500000000634400000000000017334 0ustar00kefalakefala00000000000000import operator import string 'Various helper classes and functions.' __all__ = ['AlwaysDict', 'AttrDict', 'moveListItem', 'namedlist', 'classproperty', 'MissingAttrFormatter'] class AlwaysDict(dict): 'return same val for all keys' def __init__(self, val, **kwargs): super().__init__(**kwargs) self._val = val def __getitem__(self, k): return self._val class AttrDict(dict): 'Augment a dict with more convenient .attr syntax. not-present keys return None.' def __getattr__(self, k): try: return self[k] except KeyError: if k.startswith("__"): raise AttributeError return None def __setattr__(self, k, v): self[k] = v def __dir__(self): return self.keys() class classproperty(property): def __get__(self, cls, obj): return classmethod(self.fget).__get__(None, obj or cls)() def moveListItem(L, fromidx, toidx): "Move element within list `L` and return element's new index." toidx = min(max(toidx, 0), len(L)-1) fromidx = min(max(fromidx, 0), len(L)-1) r = L.pop(fromidx) L.insert(toidx, r) return toidx 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) def itemsetter(i): def g(obj, v): obj[i] = v return g def namedlist(objname, fieldnames): 'like namedtuple but editable' class NamedListTemplate(list): __name__ = objname _fields = fieldnames def __init__(self, L=None, **kwargs): if L is None: L = [None]*len(self._fields) elif len(L) < len(self._fields): L.extend([None]*(len(self._fields) - len(L))) super().__init__(L) for k, v in kwargs.items(): setattr(self, k, v) def __getattr__(self, k): 'to enable .fieldname' try: return self[self._fields.index(k)] except ValueError: raise AttributeError def __setattr__(self, k, v): 'to enable .fieldname =' try: self[self._fields.index(k)] = v except ValueError: super().__setattr__(k, v) return NamedListTemplate class MissingAttrFormatter(string.Formatter): "formats {} fields with `''`, that would normally result in a raised KeyError or AttributeError; intended for user customisable format strings." def get_field(self, field_name, *args, **kwargs): try: return super().get_field(field_name, *args, **kwargs) except (KeyError, AttributeError): return (None, field_name) def format_field(self, value, format_spec): # value is missing if value is None: return '' elif not value: return str(value) return super().format_field(value, format_spec) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1606688365.0 visidata-2.2.1/visidata/vdobj.py0000660000175000017500000000652200000000000017276 0ustar00kefalakefala00000000000000from functools import wraps from unittest import mock import curses import visidata __all__ = ['ENTER', 'ALT', 'ESC', 'asyncthread', 'VisiData', 'drawcache', 'drawcache_property'] ENTER='^J' ALT=ESC='^[' # define @asyncthread for potentially long-running functions # when function is called, instead launches a thread def asyncthread(func): 'Function decorator, to make calls to `func()` spawn a separate thread if available.' @wraps(func) def _execAsync(*args, **kwargs): return visidata.vd.execAsync(func, *args, **kwargs) return _execAsync # @drawcache is vd alias for @cache, since vd clears it every frame drawcache = visidata.cache def drawcache_property(func): return property(drawcache(func)) class VisiData(visidata.Extensible): allPrefixes = ['g', 'z', ESC] # embig'g'en, 'z'mallify, ESC=Alt/Meta @classmethod def global_api(cls, func): 'Make global func() and identical vd.func()' def _vdfunc(*args, **kwargs): return func(visidata.vd, *args, **kwargs) setattr(cls, func.__name__, func) return wraps(func)(_vdfunc) def __init__(self): self.sheets = [] # list of BaseSheet; all sheets on the sheet stack self.allSheets = [] # list of all non-precious sheets ever pushed self.lastErrors = [] self.keystrokes = '' self.scrFull = mock.MagicMock(__bool__=mock.Mock(return_value=False)) # disable curses in batch mode self.cmdlog = None @drawcache_property def mousereg(self): return [] def __copy__(self): 'Dummy method for Extensible.init()' pass def finalInit(self): 'Initialize members specified in other modules with init()' pass @classmethod def init(cls, membername, initfunc, **kwargs): 'Overload Extensible.init() to call finalInit instead of __init__' oldinit = cls.finalInit def newinit(self, *args, **kwargs): oldinit(self, *args, **kwargs) setattr(self, membername, initfunc()) cls.finalInit = newinit super().init(membername, lambda: None, **kwargs) def clearCaches(self): 'Invalidate internal caches between command inputs.' visidata.Extensible.clear_all_caches() def getkeystroke(self, scr, vs=None): 'Get keystroke and display it on status bar.' k = None try: scr.refresh() k = scr.get_wch() vs = vs or self.sheets[0] self.drawRightStatus(vs._scr, vs) # continue to display progress % except curses.error: return '' # curses timeout if isinstance(k, str): if ord(k) >= 32 and ord(k) != 127: # 127 == DEL or ^? return k k = ord(k) return curses.keyname(k).decode('utf-8') def onMouse(self, scr, y, x, h, w, **kwargs): self.mousereg.append((scr, y, x, h, w, kwargs)) def getMouse(self, _scr, _x, _y, button): for scr, y, x, h, w, kwargs in self.mousereg[::-1]: if scr == _scr and x <= _x < x+w and y <= _y < y+h and button in kwargs: return kwargs[button] @property def screenHeight(self): return self.scrFull.getmaxyx()[0] if self.scrFull else 25 @property def screenWidth(self): return self.scrFull.getmaxyx()[1] if self.scrFull else 80 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612075960.0 visidata-2.2.1/visidata/wrappers.py0000660000175000017500000000633700000000000020041 0ustar00kefalakefala00000000000000'value wrappers for nulls and errors' from copy import copy import functools from visidata import options, stacktrace, option, BaseSheet __all__ = ['forward', 'wrmap', 'wrapply', 'TypedWrapper', 'TypedExceptionWrapper'] option('null_value', None, 'a value to be counted as null', replay=True) @BaseSheet.api def isNullFunc(sheet): 'Return func(value) which returns whether or not *value* is null.' nullv = sheet.options.null_value if nullv is None: return lambda v: v is None or isinstance(v, TypedWrapper) return lambda v, nullv=nullv: v is None or v == nullv or isinstance(v, TypedWrapper) @functools.total_ordering class TypedWrapper: def __init__(self, func, *args): self.type = func self.args = args self.val = args[0] if args else '' def __bool__(self): return False def __len__(self): return 0 def __str__(self): return '%s(%s)' % (self.type.__name__, ','.join(str(x) for x in self.args)) def __lt__(self, x): 'maintain sortability; wrapped objects are always least' return True def __add__(self, x): return x def __radd__(self, x): return x def __hash__(self): return hash((self.type, str(self.val))) def __eq__(self, x): if isinstance(x, TypedWrapper): return self.type == x.type and self.val == x.val def __iter__(self): return self def __next__(self): raise StopIteration class TypedExceptionWrapper(TypedWrapper): def __init__(self, func, *args, exception=None): TypedWrapper.__init__(self, func, *args) self.exception = exception self.stacktrace = stacktrace() self.forwarded = False def __str__(self): return str(self.exception) def __hash__(self): return hash((type(self.exception), ''.join(self.stacktrace[:-1]))) def __eq__(self, x): if isinstance(x, TypedExceptionWrapper): return type(self.exception) is type(x.exception) and self.stacktrace[:-1] == x.stacktrace[:-1] def forward(wr): if isinstance(wr, TypedExceptionWrapper): wr.forwarded = True return wr def wrmap(func, iterable, *args): 'Same as map(func, iterable, *args), but ignoring exceptions.' for it in iterable: try: yield func(it, *args) except Exception as e: pass def wrapply(func, *args, **kwargs): 'Like apply(), but which wraps Exceptions and passes through Wrappers (if first arg)' if args: val = args[0] if val is None: # None values propagate to TypedWrappers return TypedWrapper(func, None) elif isinstance(val, TypedExceptionWrapper): # previous Exceptions propagate, marked 'forwarded' tew = copy(val) tew.forwarded = True return tew elif isinstance(val, TypedWrapper): # TypedWrappers (likely None, from above) propagate return val elif isinstance(val, Exception): # Exception values become TypedWrappers return TypedWrapper(func, *args) try: return func(*args, **kwargs) except Exception as e: e.stacktrace = stacktrace() return TypedExceptionWrapper(func, *args, exception=e) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1612763830.9459815 visidata-2.2.1/visidata.egg-info/0000770000175000017500000000000000000000000017304 5ustar00kefalakefala00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612763830.0 visidata-2.2.1/visidata.egg-info/PKG-INFO0000660000175000017500000001051500000000000020404 0ustar00kefalakefala00000000000000Metadata-Version: 2.1 Name: visidata Version: 2.2.1 Summary: terminal interface for exploring and arranging tabular data Home-page: https://visidata.org Author: Saul Pwanson Author-email: visidata@saul.pw License: GPLv3 Download-URL: https://github.com/saulpw/visidata/tarball/2.2.1 Description: # VisiData v2.2.1 [![twitter @VisiData][1.1]][1] [![CircleCI](https://circleci.com/gh/saulpw/visidata/tree/stable.svg?style=svg)](https://circleci.com/gh/saulpw/visidata/tree/stable) [![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/saulpw/visidata) A terminal interface for exploring and arranging tabular data. ![Frequency table](http://visidata.org/freq-move-row.gif) 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.6+ - 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 $ vd $ | vd 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 [freenode.net](https://webchat.freenode.net/). 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 Keywords: console tabular data spreadsheet terminal viewer textpunkcurses csv hdf5 h5 xlsx excel tsv Platform: UNKNOWN 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.6 Description-Content-Type: text/markdown ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612763830.0 visidata-2.2.1/visidata.egg-info/SOURCES.txt0000660000175000017500000000521300000000000021172 0ustar00kefalakefala00000000000000LICENSE.gpl3 MANIFEST.in README.md setup.py bin/vd visidata/__init__.py visidata/__main__.py visidata/_input.py visidata/_open.py visidata/_types.py visidata/aggregators.py visidata/basesheet.py visidata/canvas.py visidata/choose.py visidata/clipboard.py visidata/cliptext.py visidata/cmdlog.py visidata/color.py visidata/colorsheet.py visidata/column.py visidata/customdate.py visidata/deprecated.py visidata/describe.py visidata/editor.py visidata/errors.py visidata/expr.py visidata/extensible.py visidata/fill.py visidata/freeze.py visidata/freqtbl.py visidata/graph.py visidata/help.py visidata/incr.py visidata/join.py visidata/layout.py visidata/macros.py visidata/main.py visidata/mainloop.py visidata/melt.py visidata/menu.py visidata/metasheets.py visidata/misc.py visidata/modify.py visidata/motd.py visidata/movement.py visidata/path.py visidata/pivot.py visidata/plugins.py visidata/pyobj.py visidata/regex.py visidata/save.py visidata/search.py visidata/selection.py visidata/settings.py visidata/sheets.py visidata/shell.py visidata/slide.py visidata/sort.py visidata/statusbar.py visidata/textsheet.py visidata/threads.py visidata/transpose.py visidata/undo.py visidata/unfurl.py visidata/urlcache.py visidata/utils.py visidata/vdobj.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/loaders/__init__.py visidata/loaders/_pandas.py visidata/loaders/archive.py visidata/loaders/csv.py visidata/loaders/eml.py visidata/loaders/fixed_width.py visidata/loaders/frictionless.py visidata/loaders/geojson.py visidata/loaders/graphviz.py visidata/loaders/hdf5.py visidata/loaders/html.py visidata/loaders/http.py visidata/loaders/imap.py visidata/loaders/json.py visidata/loaders/markdown.py visidata/loaders/mbtiles.py visidata/loaders/mysql.py visidata/loaders/npy.py visidata/loaders/pandas_freqtbl.py visidata/loaders/pcap.py visidata/loaders/pdf.py visidata/loaders/png.py visidata/loaders/postgres.py visidata/loaders/rec.py visidata/loaders/sas.py visidata/loaders/shp.py visidata/loaders/spss.py visidata/loaders/sqlite.py visidata/loaders/texttables.py visidata/loaders/tsv.py visidata/loaders/ttf.py visidata/loaders/usv.py visidata/loaders/vcf.py visidata/loaders/vds.py visidata/loaders/xlsb.py visidata/loaders/xlsx.py visidata/loaders/xml.py visidata/loaders/xword.py visidata/loaders/yaml.py visidata/man/vd.1 visidata/man/vd.txt visidata/man/visidata.1 visidata/tests/__init__.py visidata/tests/conftest.py visidata/tests/test_commands.py visidata/tests/test_edittext.py visidata/tests/test_path.py././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612763830.0 visidata-2.2.1/visidata.egg-info/dependency_links.txt0000660000175000017500000000000100000000000023353 0ustar00kefalakefala00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612763830.0 visidata-2.2.1/visidata.egg-info/entry_points.txt0000660000175000017500000000006300000000000022602 0ustar00kefalakefala00000000000000[console_scripts] visidata = visidata.main:vd_cli ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612763830.0 visidata-2.2.1/visidata.egg-info/requires.txt0000660000175000017500000000002000000000000021675 0ustar00kefalakefala00000000000000python-dateutil ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612763830.0 visidata-2.2.1/visidata.egg-info/top_level.txt0000660000175000017500000000001100000000000022027 0ustar00kefalakefala00000000000000visidata